@superblocksteam/vite-plugin-file-sync 2.0.151 → 2.0.152-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/dist/ai-service/agent/middleware.d.ts.map +1 -1
  2. package/dist/ai-service/agent/middleware.js +10 -1
  3. package/dist/ai-service/agent/middleware.js.map +1 -1
  4. package/dist/ai-service/agent/prompts/build-base-system-prompt.d.ts.map +1 -1
  5. package/dist/ai-service/agent/prompts/build-base-system-prompt.js +25 -6
  6. package/dist/ai-service/agent/prompts/build-base-system-prompt.js.map +1 -1
  7. package/dist/ai-service/agent/prompts/build-security-scan-prompt.d.ts.map +1 -1
  8. package/dist/ai-service/agent/prompts/build-security-scan-prompt.js +7 -2
  9. package/dist/ai-service/agent/prompts/build-security-scan-prompt.js.map +1 -1
  10. package/dist/ai-service/agent/tools/apis/get-sdk-api-docs.d.ts +47 -1
  11. package/dist/ai-service/agent/tools/apis/get-sdk-api-docs.d.ts.map +1 -1
  12. package/dist/ai-service/agent/tools/apis/get-sdk-api-docs.js +129 -15
  13. package/dist/ai-service/agent/tools/apis/get-sdk-api-docs.js.map +1 -1
  14. package/dist/ai-service/agent/tools/apis/test-api.d.ts.map +1 -1
  15. package/dist/ai-service/agent/tools/apis/test-api.js +1 -24
  16. package/dist/ai-service/agent/tools/apis/test-api.js.map +1 -1
  17. package/dist/ai-service/agent/tools/databases/dev-database-tasks.d.ts +53 -9
  18. package/dist/ai-service/agent/tools/databases/dev-database-tasks.d.ts.map +1 -1
  19. package/dist/ai-service/agent/tools/databases/dev-database-tasks.js +304 -67
  20. package/dist/ai-service/agent/tools/databases/dev-database-tasks.js.map +1 -1
  21. package/dist/ai-service/agent/tools/databases/dev-database-tools.d.ts +47 -1
  22. package/dist/ai-service/agent/tools/databases/dev-database-tools.d.ts.map +1 -1
  23. package/dist/ai-service/agent/tools/databases/dev-database-tools.js +38 -1
  24. package/dist/ai-service/agent/tools/databases/dev-database-tools.js.map +1 -1
  25. package/dist/ai-service/agent/tools/integrations/execute-request.d.ts +391 -0
  26. package/dist/ai-service/agent/tools/integrations/execute-request.d.ts.map +1 -1
  27. package/dist/ai-service/agent/tools/integrations/execute-request.js +257 -44
  28. package/dist/ai-service/agent/tools/integrations/execute-request.js.map +1 -1
  29. package/dist/ai-service/agent/tools/integrations/index.d.ts +1 -1
  30. package/dist/ai-service/agent/tools/integrations/index.d.ts.map +1 -1
  31. package/dist/ai-service/agent/tools/integrations/index.js +1 -1
  32. package/dist/ai-service/agent/tools/integrations/index.js.map +1 -1
  33. package/dist/ai-service/agent/tools/integrations/managed-app-database.d.ts +16 -0
  34. package/dist/ai-service/agent/tools/integrations/managed-app-database.d.ts.map +1 -0
  35. package/dist/ai-service/agent/tools/integrations/managed-app-database.js +31 -0
  36. package/dist/ai-service/agent/tools/integrations/managed-app-database.js.map +1 -0
  37. package/dist/ai-service/agent/tools/integrations/sql-classification.d.ts +44 -0
  38. package/dist/ai-service/agent/tools/integrations/sql-classification.d.ts.map +1 -0
  39. package/dist/ai-service/agent/tools/integrations/sql-classification.js +352 -0
  40. package/dist/ai-service/agent/tools/integrations/sql-classification.js.map +1 -0
  41. package/dist/ai-service/agent/tools/report-security-findings.d.ts +5 -5
  42. package/dist/ai-service/agent/tools.d.ts.map +1 -1
  43. package/dist/ai-service/agent/tools.js +5 -1
  44. package/dist/ai-service/agent/tools.js.map +1 -1
  45. package/dist/ai-service/agent/tools2/registry.d.ts.map +1 -1
  46. package/dist/ai-service/agent/tools2/registry.js +2 -0
  47. package/dist/ai-service/agent/tools2/registry.js.map +1 -1
  48. package/dist/ai-service/agent/tools2/tools/check-task.d.ts.map +1 -1
  49. package/dist/ai-service/agent/tools2/tools/check-task.js +4 -1
  50. package/dist/ai-service/agent/tools2/tools/check-task.js.map +1 -1
  51. package/dist/ai-service/agent/tools2/tools/exit-plan-mode.d.ts.map +1 -1
  52. package/dist/ai-service/agent/tools2/tools/exit-plan-mode.js +12 -1
  53. package/dist/ai-service/agent/tools2/tools/exit-plan-mode.js.map +1 -1
  54. package/dist/ai-service/agent/tools2/tools/wait-for-task.d.ts.map +1 -1
  55. package/dist/ai-service/agent/tools2/tools/wait-for-task.js +5 -1
  56. package/dist/ai-service/agent/tools2/tools/wait-for-task.js.map +1 -1
  57. package/dist/ai-service/chat/chat-session-store.d.ts +10 -1
  58. package/dist/ai-service/chat/chat-session-store.d.ts.map +1 -1
  59. package/dist/ai-service/chat/chat-session-store.js +65 -15
  60. package/dist/ai-service/chat/chat-session-store.js.map +1 -1
  61. package/dist/ai-service/context-archive-paths.d.ts +7 -0
  62. package/dist/ai-service/context-archive-paths.d.ts.map +1 -1
  63. package/dist/ai-service/context-archive-paths.js +7 -0
  64. package/dist/ai-service/context-archive-paths.js.map +1 -1
  65. package/dist/ai-service/context-download.d.ts.map +1 -1
  66. package/dist/ai-service/context-download.js +8 -10
  67. package/dist/ai-service/context-download.js.map +1 -1
  68. package/dist/ai-service/context-flush.d.ts +36 -0
  69. package/dist/ai-service/context-flush.d.ts.map +1 -0
  70. package/dist/ai-service/context-flush.js +90 -0
  71. package/dist/ai-service/context-flush.js.map +1 -0
  72. package/dist/ai-service/context-restore.d.ts +64 -0
  73. package/dist/ai-service/context-restore.d.ts.map +1 -0
  74. package/dist/ai-service/context-restore.js +136 -0
  75. package/dist/ai-service/context-restore.js.map +1 -0
  76. package/dist/ai-service/context-upload.d.ts +13 -4
  77. package/dist/ai-service/context-upload.d.ts.map +1 -1
  78. package/dist/ai-service/context-upload.js +28 -18
  79. package/dist/ai-service/context-upload.js.map +1 -1
  80. package/dist/ai-service/dev-database-client.d.ts +51 -0
  81. package/dist/ai-service/dev-database-client.d.ts.map +1 -1
  82. package/dist/ai-service/dev-database-client.js +3 -0
  83. package/dist/ai-service/dev-database-client.js.map +1 -1
  84. package/dist/ai-service/index.d.ts +19 -0
  85. package/dist/ai-service/index.d.ts.map +1 -1
  86. package/dist/ai-service/index.js +449 -182
  87. package/dist/ai-service/index.js.map +1 -1
  88. package/dist/ai-service/llm/context-v2/adapter.d.ts.map +1 -1
  89. package/dist/ai-service/llm/context-v2/adapter.js +1 -0
  90. package/dist/ai-service/llm/context-v2/adapter.js.map +1 -1
  91. package/dist/ai-service/llm/context-v2/compaction/client-side.d.ts +30 -0
  92. package/dist/ai-service/llm/context-v2/compaction/client-side.d.ts.map +1 -1
  93. package/dist/ai-service/llm/context-v2/compaction/client-side.js +77 -4
  94. package/dist/ai-service/llm/context-v2/compaction/client-side.js.map +1 -1
  95. package/dist/ai-service/llm/context-v2/compaction/compaction-strategy.d.ts +5 -0
  96. package/dist/ai-service/llm/context-v2/compaction/compaction-strategy.d.ts.map +1 -1
  97. package/dist/ai-service/llm/context-v2/compaction/server-side.d.ts +1 -0
  98. package/dist/ai-service/llm/context-v2/compaction/server-side.d.ts.map +1 -1
  99. package/dist/ai-service/llm/context-v2/compaction/server-side.js +3 -0
  100. package/dist/ai-service/llm/context-v2/compaction/server-side.js.map +1 -1
  101. package/dist/ai-service/llm/context-v2/compactor.d.ts.map +1 -1
  102. package/dist/ai-service/llm/context-v2/compactor.js +57 -6
  103. package/dist/ai-service/llm/context-v2/compactor.js.map +1 -1
  104. package/dist/ai-service/llm/context-v2/config.d.ts +13 -1
  105. package/dist/ai-service/llm/context-v2/config.d.ts.map +1 -1
  106. package/dist/ai-service/llm/context-v2/config.js +39 -2
  107. package/dist/ai-service/llm/context-v2/config.js.map +1 -1
  108. package/dist/ai-service/llm/context-v2/context-metrics.d.ts +23 -0
  109. package/dist/ai-service/llm/context-v2/context-metrics.d.ts.map +1 -1
  110. package/dist/ai-service/llm/context-v2/context-metrics.js +52 -1
  111. package/dist/ai-service/llm/context-v2/context-metrics.js.map +1 -1
  112. package/dist/ai-service/llm/context-v2/context.d.ts +27 -3
  113. package/dist/ai-service/llm/context-v2/context.d.ts.map +1 -1
  114. package/dist/ai-service/llm/context-v2/context.js +107 -10
  115. package/dist/ai-service/llm/context-v2/context.js.map +1 -1
  116. package/dist/ai-service/llm/context-v2/manager.d.ts +66 -0
  117. package/dist/ai-service/llm/context-v2/manager.d.ts.map +1 -1
  118. package/dist/ai-service/llm/context-v2/manager.js +194 -2
  119. package/dist/ai-service/llm/context-v2/manager.js.map +1 -1
  120. package/dist/ai-service/llm/context-v2/storage/event-store.d.ts +31 -5
  121. package/dist/ai-service/llm/context-v2/storage/event-store.d.ts.map +1 -1
  122. package/dist/ai-service/llm/context-v2/storage/jsonl-event-store.d.ts +30 -9
  123. package/dist/ai-service/llm/context-v2/storage/jsonl-event-store.d.ts.map +1 -1
  124. package/dist/ai-service/llm/context-v2/storage/jsonl-event-store.js +96 -13
  125. package/dist/ai-service/llm/context-v2/storage/jsonl-event-store.js.map +1 -1
  126. package/dist/ai-service/llm/context-v2/storage/legacy-context-adoption.d.ts +46 -0
  127. package/dist/ai-service/llm/context-v2/storage/legacy-context-adoption.d.ts.map +1 -0
  128. package/dist/ai-service/llm/context-v2/storage/legacy-context-adoption.js +240 -0
  129. package/dist/ai-service/llm/context-v2/storage/legacy-context-adoption.js.map +1 -0
  130. package/dist/ai-service/llm/context-v2/storage/local.d.ts +1 -1
  131. package/dist/ai-service/llm/context-v2/storage/local.d.ts.map +1 -1
  132. package/dist/ai-service/llm/context-v2/storage/local.js +3 -2
  133. package/dist/ai-service/llm/context-v2/storage/local.js.map +1 -1
  134. package/dist/ai-service/llm/context-v2/types.d.ts +5 -1
  135. package/dist/ai-service/llm/context-v2/types.d.ts.map +1 -1
  136. package/dist/ai-service/llm/context-v2/types.js.map +1 -1
  137. package/dist/ai-service/llm/stream/config.d.ts +4 -3
  138. package/dist/ai-service/llm/stream/config.d.ts.map +1 -1
  139. package/dist/ai-service/llm/stream/config.js.map +1 -1
  140. package/dist/ai-service/llm/stream/idle-monitor.d.ts +37 -4
  141. package/dist/ai-service/llm/stream/idle-monitor.d.ts.map +1 -1
  142. package/dist/ai-service/llm/stream/idle-monitor.js +38 -3
  143. package/dist/ai-service/llm/stream/idle-monitor.js.map +1 -1
  144. package/dist/ai-service/policy-agent-metrics.d.ts +18 -0
  145. package/dist/ai-service/policy-agent-metrics.d.ts.map +1 -0
  146. package/dist/ai-service/policy-agent-metrics.js +99 -0
  147. package/dist/ai-service/policy-agent-metrics.js.map +1 -0
  148. package/dist/ai-service/state-machine/clark-fsm.d.ts +9 -0
  149. package/dist/ai-service/state-machine/clark-fsm.d.ts.map +1 -1
  150. package/dist/ai-service/state-machine/helpers/prompt-request-metrics.d.ts +9 -0
  151. package/dist/ai-service/state-machine/helpers/prompt-request-metrics.d.ts.map +1 -1
  152. package/dist/ai-service/state-machine/helpers/prompt-request-metrics.js +27 -0
  153. package/dist/ai-service/state-machine/helpers/prompt-request-metrics.js.map +1 -1
  154. package/dist/ai-service/tasks/factory.d.ts +7 -0
  155. package/dist/ai-service/tasks/factory.d.ts.map +1 -1
  156. package/dist/ai-service/tasks/factory.js +5 -0
  157. package/dist/ai-service/tasks/factory.js.map +1 -1
  158. package/dist/ai-service/tasks/task-store.d.ts +11 -1
  159. package/dist/ai-service/tasks/task-store.d.ts.map +1 -1
  160. package/dist/ai-service/tasks/task-store.js +104 -29
  161. package/dist/ai-service/tasks/task-store.js.map +1 -1
  162. package/dist/ai-service/tasks/types.d.ts +9 -1
  163. package/dist/ai-service/tasks/types.d.ts.map +1 -1
  164. package/dist/extract-api-integrations.d.ts +76 -0
  165. package/dist/extract-api-integrations.d.ts.map +1 -1
  166. package/dist/extract-api-integrations.js +428 -109
  167. package/dist/extract-api-integrations.js.map +1 -1
  168. package/dist/file-sync-vite-plugin.d.ts +1 -1
  169. package/dist/file-sync-vite-plugin.d.ts.map +1 -1
  170. package/dist/file-sync-vite-plugin.js +42 -79
  171. package/dist/file-sync-vite-plugin.js.map +1 -1
  172. package/dist/file-system-helpers.d.ts +1 -1
  173. package/dist/file-system-helpers.d.ts.map +1 -1
  174. package/dist/file-system-helpers.js +1 -0
  175. package/dist/file-system-helpers.js.map +1 -1
  176. package/dist/git-auth-state.d.ts +4 -0
  177. package/dist/git-auth-state.d.ts.map +1 -0
  178. package/dist/git-auth-state.js +25 -0
  179. package/dist/git-auth-state.js.map +1 -0
  180. package/dist/git-service/checkpoint-commit-metrics.d.ts +2 -0
  181. package/dist/git-service/checkpoint-commit-metrics.d.ts.map +1 -0
  182. package/dist/git-service/checkpoint-commit-metrics.js +15 -0
  183. package/dist/git-service/checkpoint-commit-metrics.js.map +1 -0
  184. package/dist/git-service/checkpoint-committer.d.ts +22 -0
  185. package/dist/git-service/checkpoint-committer.d.ts.map +1 -0
  186. package/dist/git-service/checkpoint-committer.js +91 -0
  187. package/dist/git-service/checkpoint-committer.js.map +1 -0
  188. package/dist/git-service/commit-live-branch.d.ts +17 -0
  189. package/dist/git-service/commit-live-branch.d.ts.map +1 -0
  190. package/dist/git-service/commit-live-branch.js +28 -0
  191. package/dist/git-service/commit-live-branch.js.map +1 -0
  192. package/dist/git-service/provider-workflow-files.d.ts +12 -0
  193. package/dist/git-service/provider-workflow-files.d.ts.map +1 -0
  194. package/dist/git-service/provider-workflow-files.js +36 -0
  195. package/dist/git-service/provider-workflow-files.js.map +1 -0
  196. package/dist/plugin-options.d.ts +8 -0
  197. package/dist/plugin-options.d.ts.map +1 -1
  198. package/dist/plugin-options.js.map +1 -1
  199. package/dist/policy-gate-runner.d.ts.map +1 -1
  200. package/dist/policy-gate-runner.js +7 -9
  201. package/dist/policy-gate-runner.js.map +1 -1
  202. package/dist/server-rpc/client.d.ts.map +1 -1
  203. package/dist/server-rpc/client.js +2 -0
  204. package/dist/server-rpc/client.js.map +1 -1
  205. package/dist/server-rpc/index.d.ts +43 -1
  206. package/dist/server-rpc/index.d.ts.map +1 -1
  207. package/dist/server-rpc/index.js +143 -5
  208. package/dist/server-rpc/index.js.map +1 -1
  209. package/dist/server-rpc/request-abort-context.d.ts +3 -0
  210. package/dist/server-rpc/request-abort-context.d.ts.map +1 -0
  211. package/dist/server-rpc/request-abort-context.js +23 -0
  212. package/dist/server-rpc/request-abort-context.js.map +1 -0
  213. package/dist/server-rpc/rpc-invalidate-metrics.d.ts +20 -0
  214. package/dist/server-rpc/rpc-invalidate-metrics.d.ts.map +1 -0
  215. package/dist/server-rpc/rpc-invalidate-metrics.js +86 -0
  216. package/dist/server-rpc/rpc-invalidate-metrics.js.map +1 -0
  217. package/dist/socket-manager.d.ts.map +1 -1
  218. package/dist/socket-manager.js +21 -60
  219. package/dist/socket-manager.js.map +1 -1
  220. package/dist/sync-service/index.d.ts +26 -2
  221. package/dist/sync-service/index.d.ts.map +1 -1
  222. package/dist/sync-service/index.js +74 -37
  223. package/dist/sync-service/index.js.map +1 -1
  224. package/package.json +9 -8
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { context as otelContext, propagation, SpanKind, SpanStatusCode, trace, } from "@opentelemetry/api";
5
- import { stepCountIs, streamText as aiStreamText, } from "ai";
5
+ import { hasToolCall, streamText as aiStreamText, } from "ai";
6
6
  import { z } from "zod";
7
7
  import { AiGenerationState, AiMode, resolveAttachmentFileName, } from "@superblocksteam/library-shared/types";
8
8
  import { isToolPermissionClarification } from "@superblocksteam/library-shared/types/tool-permission-response";
@@ -34,6 +34,7 @@ import { ChatSessionStore } from "./chat/chat-session-store.js";
34
34
  import { normalizeTextAttachmentForModel } from "./chat/utils.js";
35
35
  import { API_MIGRATION_ORIGINS, isApiMigrationOrigin, readPersistedChecklist, updateChecklistItem, } from "./checklist/persisted-checklist-store.js";
36
36
  import { downloadContextFromBucketeer, downloadLatestContextFromBucketeer, restoreContextFromCheckpoint, } from "./context-download.js";
37
+ import { restoreConversationToCheckpoint, } from "./context-restore.js";
37
38
  import { uploadContextToBucketeer } from "./context-upload.js";
38
39
  import { AppContextStore } from "./context/app-context.js";
39
40
  import { createDevDatabaseHttpTransport, DevDatabaseClient, } from "./dev-database-client.js";
@@ -47,8 +48,10 @@ import { createChaosFetch } from "./llm/chaos-fetch.js";
47
48
  import { createClarkStreamTelemetrySink } from "./llm/clark-stream-telemetry-sink.js";
48
49
  import { LLMClient } from "./llm/client.js";
49
50
  import { selectContextManagementStrategy, } from "./llm/context-v2/context-management.js";
51
+ import { recordCheckpointRestore } from "./llm/context-v2/context-metrics.js";
50
52
  import { ContextManagerV2 } from "./llm/context-v2/manager.js";
51
53
  import { JsonlEventStore } from "./llm/context-v2/storage/jsonl-event-store.js";
54
+ import { adoptLegacyPerUserContextDirs } from "./llm/context-v2/storage/legacy-context-adoption.js";
52
55
  import { LocalContextStorageV2 } from "./llm/context-v2/storage/local.js";
53
56
  import { createLLMProvider, setSpanError } from "./llm/provider.js";
54
57
  import { traceLLM } from "./llmobs/helpers.js";
@@ -60,6 +63,7 @@ import { recordMigrationAutoDispatchOutcome } from "./migration-autodispatch-met
60
63
  import { buildServerMigrationLlmConfig, isMigrationAutoDispatchEnabled, } from "./migration-llm-config.js";
61
64
  import { isMigrationSessionActive } from "./migration-session-active.js";
62
65
  import { hasSettledNormalMigrationTurn, recordMigrationTurnDispatched, recordMigrationTurnSettled, } from "./migration-turns-log.js";
66
+ import { recordPolicyAgentExecution, } from "./policy-agent-metrics.js";
63
67
  import { policyGateStreamPartToAgentStep } from "./policy-gate-step.js";
64
68
  import { ClarkProfiler } from "./profiler/clark-profiler.js";
65
69
  import { buildSummarizeApiUsagePrompt } from "./prompts/summarize-api-usage.js";
@@ -83,7 +87,7 @@ import { sendUserGenerationStateChannel } from "./state-machine/helpers/peer.js"
83
87
  import { sendUserMessageChannel, clearPendingToolPermissionRequest, } from "./state-machine/helpers/peer.js";
84
88
  import { finalizePromptBillingMetrics } from "./state-machine/helpers/prompt-billing-metrics.js";
85
89
  import { paywallStopAdvice } from "./state-machine/helpers/prompt-failure.js";
86
- import { promptFailureFromStreamAbort, promptFailurePaywall, promptFailurePlatform, promptFailureUserCanceled, recordPromptAttempt, recordPromptEntryRejection, recordPromptOutcome, recordAwaitingUserHandoffDismissed, resolvePromptMetricLabels, resolvePromptOutcomeDedupeKey, resolvePromptRequestDedupeKey, setPromptRequestMetricIdentity, } from "./state-machine/helpers/prompt-request-metrics.js";
90
+ import { promptFailureFromStreamAbort, promptFailurePaywall, promptFailurePlatform, promptFailureUserCanceled, recordBaseCommitResolutionFailed, recordPromptAttempt, recordPromptEntryRejection, recordPromptOutcome, recordAwaitingUserHandoffDismissed, resolvePromptMetricLabels, resolvePromptOutcomeDedupeKey, resolvePromptRequestDedupeKey, setPromptRequestMetricIdentity, } from "./state-machine/helpers/prompt-request-metrics.js";
87
91
  import { catchBackgroundPeerDisconnect, StablePeer, } from "./state-machine/helpers/stable-peer.js";
88
92
  import { transitionFrom } from "./state-machine/helpers/transition.js";
89
93
  import { isTaskAccessibleTo, registerBackgroundTaskTypes, resyncActiveTaskStatusesToPeer, TaskStore, wireTaskStatusPushToPeer, } from "./tasks/index.js";
@@ -132,6 +136,9 @@ const MAX_POLICY_GATE_STREAM_ERROR_ARRAY_LENGTH = 10;
132
136
  const MAX_POLICY_GATE_STREAM_ERROR_FIELD_LENGTH = 500;
133
137
  const MAX_POLICY_GATE_STREAM_ERROR_OBJECT_DEPTH = 3;
134
138
  const MAX_POLICY_GATE_STREAM_ERROR_OBJECT_KEYS = 20;
139
+ const POLICY_GATE_MAX_TOTAL_TIME_MS = 15 * 60 * 1000;
140
+ // AbortError near the budget is treated as timeout (SDK may abort slightly early).
141
+ const POLICY_GATE_TIMEOUT_CLASSIFICATION_SLACK_MS = 5_000;
135
142
  const POLICY_GATE_STREAM_ERROR_REDACTED_VALUE = "[redacted]";
136
143
  const POLICY_GATE_STREAM_ERROR_SECRET_KEY_PATTERN = /(access[-_]?key|api[-_]?key|authorization|connection[-_]?string|credential|dsn|password|private[-_]?key|secret|token)/i;
137
144
  const POLICY_GATE_STREAM_ERROR_SECRET_VALUE_PATTERN_SOURCES = [
@@ -284,6 +291,17 @@ const NPM_REGISTRY_JWT_WAIT_MS = 5_000;
284
291
  * via the second-401 branch in `NpmRegistryClient.fetchWithRefresh`.
285
292
  */
286
293
  const NPM_REGISTRY_REFRESH_WAIT_MS = 5_000;
294
+ /**
295
+ * Deadline for the chat-store fallback in `resolveBaseCommitId` (2 seconds).
296
+ *
297
+ * baseCommitId is a billing annotation, not something the prompt needs to
298
+ * start -- a slow or hung `chatSessionStore.getLatestCommitId()` call must
299
+ * not delay the turn. A store that never resolves within this window is
300
+ * treated the same as a failure: the counter records
301
+ * `source: "chat_store_timeout"`, a correlated warn is logged, and the
302
+ * prompt proceeds with no baseCommitId rather than waiting indefinitely.
303
+ */
304
+ const RESOLVE_BASE_COMMIT_ID_CHAT_STORE_TIMEOUT_MS = 2_000;
287
305
  export function truncateString(str, maxBytes) {
288
306
  // Rough approximation: assume 2 bytes per character
289
307
  const maxChars = Math.floor(maxBytes / 2);
@@ -800,6 +818,9 @@ export class AiService extends TracedEventEmitter {
800
818
  rpcClient: config.rpcClient,
801
819
  logger: config.logger,
802
820
  });
821
+ // Lazy closure: this.clark is assigned later in the constructor, so the
822
+ // provider reads the active turn's promptId at append time, not now.
823
+ this.chatSessionStore.setPromptIdProvider(() => this.clark?.context?.promptId);
803
824
  if (this.config.tokenManager) {
804
825
  this.config.tokenManager.on("tokenUpdated", this.handleTokenUpdated.bind(this));
805
826
  }
@@ -809,6 +830,7 @@ export class AiService extends TracedEventEmitter {
809
830
  storage: new LocalContextStorageV2(config.appRootDirPath),
810
831
  eventStore: new JsonlEventStore(config.appRootDirPath),
811
832
  rootDir: config.appRootDirPath,
833
+ adoptLegacyContexts: () => adoptLegacyPerUserContextDirs(config.appRootDirPath),
812
834
  getModeState: () => {
813
835
  const mode = this.clark?.context?.currentMode;
814
836
  if (mode === undefined)
@@ -1267,10 +1289,11 @@ export class AiService extends TracedEventEmitter {
1267
1289
  to === ClarkStateNames.AwaitingUser) {
1268
1290
  // The turn is over: drop per-turn billing attribution so stray
1269
1291
  // LLM calls before the next prompt bill as default generation.
1270
- if (clark.context.usageMetadata) {
1292
+ if (clark.context.usageMetadata || clark.context.baseCommitId) {
1271
1293
  clark.updateContext((context) => ({
1272
1294
  ...context,
1273
1295
  usageMetadata: undefined,
1296
+ baseCommitId: undefined,
1274
1297
  }));
1275
1298
  }
1276
1299
  this.triggerGenerationComplete();
@@ -1412,8 +1435,80 @@ export class AiService extends TracedEventEmitter {
1412
1435
  summarizeModel: () => createLLMProvider(this.getLLMProviderSettings(llmConfig), () => this.getProviderInferenceAuthToken(), llmConfig, () => this.getLlmAiAttributes(this.clark), this.clark.context.traceHeaders).modelForTask("summarizeMessages"),
1413
1436
  });
1414
1437
  }
1438
+ /**
1439
+ * The checkpoint the app is currently based on: the last non-draft commit
1440
+ * seeded by sync (preferred — same key the post-generation upload uses) or
1441
+ * the most recent chat-history commit. Captured once at prompt start
1442
+ * because the checkpoint an eventual accept will create does not exist yet
1443
+ * while the LLM calls run. Never throws: a billing annotation must not be
1444
+ * able to break generation.
1445
+ */
1446
+ async resolveBaseCommitId(promptId, chatStoreTimeoutMs = RESOLVE_BASE_COMMIT_ID_CHAT_STORE_TIMEOUT_MS) {
1447
+ let preferredSourceFailed = false;
1448
+ try {
1449
+ // Normalize falsy (e.g. "") to undefined so an empty string can't
1450
+ // short-circuit the `??` fallback below and then be silently dropped
1451
+ // by the truthy merge guard downstream in getLlmAiAttributes.
1452
+ const syncCommitId = this._syncContextProvider?.getLastCommitId() || undefined;
1453
+ if (syncCommitId) {
1454
+ return syncCommitId;
1455
+ }
1456
+ }
1457
+ catch (error) {
1458
+ preferredSourceFailed = true;
1459
+ this.getLogger().warn("[ai-service] Failed to resolve sync base commit id; falling back to chat history", {
1460
+ applicationId: this.config.applicationId,
1461
+ sessionId: this.sessionId,
1462
+ promptId,
1463
+ ...getErrorMeta(error),
1464
+ });
1465
+ }
1466
+ let source = "chat_store";
1467
+ try {
1468
+ const chatStoreCommitId = await this.raceChatStoreLatestCommitId(chatStoreTimeoutMs, () => {
1469
+ source = "chat_store_timeout";
1470
+ });
1471
+ return chatStoreCommitId || undefined;
1472
+ }
1473
+ catch (error) {
1474
+ this.recordAndWarnBaseCommitResolutionFailure(source, preferredSourceFailed, promptId, error);
1475
+ return undefined;
1476
+ }
1477
+ }
1478
+ /**
1479
+ * Races the chat-store fallback against `timeoutMs` so a hung store
1480
+ * degrades exactly like a failure instead of delaying prompt start. The
1481
+ * timeout rejects (never resolves) so the caller's catch block handles
1482
+ * both outcomes uniformly; `onTimeout` lets the caller distinguish which
1483
+ * branch fired for metrics/logging without a second code path.
1484
+ * `.unref()` keeps the fallback timer from holding the process open.
1485
+ */
1486
+ raceChatStoreLatestCommitId(timeoutMs, onTimeout) {
1487
+ let timeoutHandle;
1488
+ const timeout = new Promise((_, reject) => {
1489
+ timeoutHandle = setTimeout(() => {
1490
+ onTimeout();
1491
+ reject(new Error(`chatSessionStore.getLatestCommitId timed out after ${timeoutMs}ms`));
1492
+ }, timeoutMs);
1493
+ timeoutHandle.unref?.();
1494
+ });
1495
+ return Promise.race([
1496
+ this.chatSessionStore.getLatestCommitId(),
1497
+ timeout,
1498
+ ]).finally(() => clearTimeout(timeoutHandle));
1499
+ }
1500
+ recordAndWarnBaseCommitResolutionFailure(source, preferredSourceFailed, promptId, error) {
1501
+ recordBaseCommitResolutionFailed({ source, preferredSourceFailed });
1502
+ this.getLogger().warn("[ai-service] Failed to resolve base commit id for usage metadata", {
1503
+ applicationId: this.config.applicationId,
1504
+ sessionId: this.sessionId,
1505
+ promptId,
1506
+ ...getErrorMeta(error),
1507
+ });
1508
+ }
1415
1509
  getLlmAiAttributes(clark) {
1416
1510
  const usageMetadata = clark.context.usageMetadata ?? APP_CLARK_GENERATION_METADATA;
1511
+ const baseCommitId = clark.context.baseCommitId;
1417
1512
  return {
1418
1513
  prompt_id: clark.context.promptId,
1419
1514
  chatmessage_id: clark.context.chatmessageId,
@@ -1422,7 +1517,9 @@ export class AiService extends TracedEventEmitter {
1422
1517
  // Note: retry_count for LLM retries is added by orchestrator via providerOptions
1423
1518
  mode: clark.context.currentMode === AiMode.PLAN ? "plan" : "build",
1424
1519
  plan_size: clark.context.planSize,
1425
- metadata: usageMetadata,
1520
+ metadata: baseCommitId
1521
+ ? { ...usageMetadata, baseCommitId }
1522
+ : usageMetadata,
1426
1523
  };
1427
1524
  }
1428
1525
  /**
@@ -1626,9 +1723,11 @@ export class AiService extends TracedEventEmitter {
1626
1723
  retryMode,
1627
1724
  triggeredBy,
1628
1725
  };
1726
+ const baseCommitId = await this.resolveBaseCommitId(promptId);
1629
1727
  this.clark.updateContext((context) => ({
1630
1728
  ...context,
1631
1729
  promptId,
1730
+ baseCommitId,
1632
1731
  usageMetadata: migrationUsageMetadata,
1633
1732
  llmConfig: migrationLlmConfig,
1634
1733
  }));
@@ -2072,11 +2171,13 @@ export class AiService extends TracedEventEmitter {
2072
2171
  dedupeKeySource: request.requestId ? "requestId" : "ephemeral",
2073
2172
  applicationId: this.config.applicationId,
2074
2173
  });
2174
+ const baseCommitId = await this.resolveBaseCommitId(promptId);
2075
2175
  this.clark.updateContext((context) => {
2076
2176
  return {
2077
2177
  ...context,
2078
2178
  promptId,
2079
2179
  promptOutcomeDedupeKey,
2180
+ baseCommitId,
2080
2181
  isInterruptAbort: undefined,
2081
2182
  userInitiatedPromptAbort: undefined,
2082
2183
  // Third-party import turns are billed as app_migration; everything
@@ -2646,6 +2747,7 @@ export class AiService extends TracedEventEmitter {
2646
2747
  promptId: undefined,
2647
2748
  promptOutcomeDedupeKey: undefined,
2648
2749
  usageMetadata: undefined,
2750
+ baseCommitId: undefined,
2649
2751
  awaitingUserHandoffPromptId: undefined,
2650
2752
  awaitingUserHandoffRecorded: undefined,
2651
2753
  }));
@@ -3645,25 +3747,11 @@ export class AiService extends TracedEventEmitter {
3645
3747
  async handlePolicyGateExecution(request) {
3646
3748
  const logger = this.getLogger();
3647
3749
  logger.info(`[ai-service] Policy gate execution requested for app=${request.applicationId}`);
3648
- const requestLlmConfig = request.llmConfig;
3649
- const contextLlmConfig = this.clark.context.llmConfig;
3650
- const llmConfig = requestLlmConfig ?? contextLlmConfig;
3651
- const llmProvider = createLLMProvider(this.getLLMProviderSettings(llmConfig), () => this.requireProvidedInferenceJwt(request.jwt, "policy gate"), llmConfig, () => ({
3652
- ai_agent_run_id: request.aiAgentRunId,
3653
- application_id: request.applicationId,
3654
- metadata: POLICY_GATE_USAGE_METADATA,
3655
- session_id: this.sessionId,
3656
- trigger_type: "policy_gate",
3657
- }));
3658
- const { model } = llmProvider.modelForTask("generateBroadEdit");
3659
- const availableIntegrations = await this.integrationStore.availableIntegrations();
3660
- const integrations = availableIntegrations.map((i) => ({
3661
- name: i.name,
3662
- pluginType: i.plugin?.id ?? "unknown",
3663
- }));
3664
- const appContext = { integrations };
3665
- const systemPrompt = composeSecurityAgentPrompt(appContext, request.adminPrompt);
3666
3750
  const startedAt = Date.now();
3751
+ const policyRunLogContext = {
3752
+ applicationId: request.applicationId,
3753
+ reviewPolicyId: request.reviewPolicyId,
3754
+ };
3667
3755
  const toolCallCounts = {};
3668
3756
  const filePathsRead = new Set();
3669
3757
  const filePathsListed = new Set();
@@ -3671,15 +3759,31 @@ export class AiService extends TracedEventEmitter {
3671
3759
  const grepPatterns = [];
3672
3760
  const grepMatchCounts = [];
3673
3761
  const grepPatternByToolCallId = new Map();
3762
+ const lsPathByToolCallId = new Map();
3674
3763
  const streamErrors = [];
3675
3764
  const streamPartTypeCounts = {};
3765
+ let stepCount = 0;
3676
3766
  let textDeltaChars = 0;
3677
- const promptHash = createHash("sha256").update(systemPrompt).digest("hex");
3678
- const modelName = typeof model === "string" ? model : model.modelId;
3767
+ let modelName;
3768
+ let promptHash;
3679
3769
  const scanRoot = typeof this.appShell.appRootDirPath === "string"
3680
3770
  ? this.appShell.appRootDirPath
3681
3771
  : undefined;
3682
3772
  let scanResult;
3773
+ let metricsRecorded = false;
3774
+ const totalToolCalls = () => Object.values(toolCallCounts).reduce((sum, count) => sum + count, 0);
3775
+ const recordExecutionMetrics = (outcome) => {
3776
+ if (metricsRecorded) {
3777
+ return;
3778
+ }
3779
+ metricsRecorded = true;
3780
+ recordPolicyAgentExecution({
3781
+ durationMs: Date.now() - startedAt,
3782
+ outcome,
3783
+ stepCount,
3784
+ toolCallCount: totalToolCalls(),
3785
+ });
3786
+ };
3683
3787
  const buildRuntimeMetadata = (finalFailureReason) => ({
3684
3788
  diagnosticsVersion: 2,
3685
3789
  durationMs: Date.now() - startedAt,
@@ -3694,156 +3798,260 @@ export class AiService extends TracedEventEmitter {
3694
3798
  reportToolCalled: Boolean(scanResult),
3695
3799
  scanComplete: scanResult?.scanComplete === true,
3696
3800
  scanRoot,
3801
+ stepCount,
3697
3802
  streamErrors,
3698
3803
  streamPartTypeCounts,
3699
3804
  textDeltaChars,
3700
3805
  toolCallCounts,
3701
3806
  });
3702
- const failPolicyGateExecution = (message) => {
3807
+ const failPolicyGateExecution = (message, outcome) => {
3808
+ recordExecutionMetrics(outcome);
3703
3809
  const error = new Error(message);
3704
3810
  error.runtimeMetadataJson = buildRuntimeMetadata(message);
3705
3811
  throw error;
3706
3812
  };
3707
- const scanToolsDeps = {
3708
- appShell: this.appShell,
3709
- features: this.config.features ?? {},
3710
- integrationStore: this.integrationStore,
3711
- llmClient: this.llmClient,
3712
- llmProvider: llmProvider,
3713
- };
3714
- const tools = await buildSecurityScanTools(this.clark, scanToolsDeps);
3715
- const result = aiStreamText({
3716
- messages: [
3717
- { content: systemPrompt, role: "system" },
3718
- {
3719
- content: "Begin your security review of this application now.",
3720
- role: "user",
3721
- },
3722
- ],
3723
- model,
3724
- tools,
3725
- stopWhen: stepCountIs(30),
3726
- });
3727
- // Best-effort: surface the agent's latest tool/reasoning step in the editor
3728
- // so a running policy row shows live activity instead of an opaque spinner.
3729
- // Only meaningful when an editor session is connected; never blocks the run.
3730
- const peer = this.clark.context.peer;
3731
- const emitAgentStep = (part) => {
3732
- if (!this.hasConnectedPeer() || !peer) {
3733
- return;
3734
- }
3735
- const step = policyGateStreamPartToAgentStep(part);
3736
- if (!step) {
3737
- return;
3738
- }
3739
- void peer.call
3740
- .aiPushPolicyAgentStep({
3741
- applicationId: request.applicationId,
3742
- policyId: request.reviewPolicyId,
3743
- step,
3744
- })
3745
- .catch((error) => {
3746
- logger.warn("[ai-service] Failed to push policy agent step", getErrorMeta(error));
3813
+ try {
3814
+ const requestLlmConfig = request.llmConfig;
3815
+ const contextLlmConfig = this.clark.context.llmConfig;
3816
+ const llmConfig = requestLlmConfig ?? contextLlmConfig;
3817
+ const llmProvider = createLLMProvider(this.getLLMProviderSettings(llmConfig), () => this.requireProvidedInferenceJwt(request.jwt, "policy gate"), llmConfig, () => ({
3818
+ ai_agent_run_id: request.aiAgentRunId,
3819
+ application_id: request.applicationId,
3820
+ metadata: POLICY_GATE_USAGE_METADATA,
3821
+ session_id: this.sessionId,
3822
+ trigger_type: "policy_gate",
3823
+ }));
3824
+ const { model } = llmProvider.modelForTask("generateBroadEdit");
3825
+ modelName = typeof model === "string" ? model : model.modelId;
3826
+ const availableIntegrations = await this.integrationStore.availableIntegrations();
3827
+ const integrations = availableIntegrations.map((i) => ({
3828
+ name: i.name,
3829
+ pluginType: i.plugin?.id ?? "unknown",
3830
+ }));
3831
+ const appContext = { integrations };
3832
+ const systemPrompt = composeSecurityAgentPrompt(appContext, request.adminPrompt);
3833
+ promptHash = createHash("sha256").update(systemPrompt).digest("hex");
3834
+ const scanToolsDeps = {
3835
+ appShell: this.appShell,
3836
+ features: this.config.features ?? {},
3837
+ integrationStore: this.integrationStore,
3838
+ llmClient: this.llmClient,
3839
+ llmProvider: llmProvider,
3840
+ };
3841
+ const tools = await buildSecurityScanTools(this.clark, scanToolsDeps);
3842
+ const result = aiStreamText({
3843
+ messages: [
3844
+ { content: systemPrompt, role: "system" },
3845
+ {
3846
+ content: "Begin your security review of this application now.",
3847
+ role: "user",
3848
+ },
3849
+ ],
3850
+ model,
3851
+ tools,
3852
+ stopWhen: hasToolCall("reportReviewRun"),
3853
+ timeout: { totalMs: POLICY_GATE_MAX_TOTAL_TIME_MS },
3747
3854
  });
3748
- };
3749
- for await (const part of result.fullStream) {
3750
- emitAgentStep(part);
3751
- const partType = typeof part.type === "string"
3752
- ? part.type
3753
- : "unknown";
3754
- streamPartTypeCounts[partType] =
3755
- (streamPartTypeCounts[partType] ?? 0) + 1;
3756
- if (partType === "error") {
3757
- const streamError = serializePolicyGateStreamError(part.error, part);
3758
- if (streamErrors.length < MAX_POLICY_GATE_STREAM_ERRORS) {
3759
- streamErrors.push(streamError);
3855
+ // Best-effort: surface the agent's latest tool/reasoning step in the editor
3856
+ // so a running policy row shows live activity instead of an opaque spinner.
3857
+ // Only meaningful when an editor session is connected; never blocks the run.
3858
+ const peer = this.clark.context.peer;
3859
+ const emitAgentStep = (part) => {
3860
+ if (!this.hasConnectedPeer() || !peer) {
3861
+ return;
3760
3862
  }
3761
- logger.warn("[ai-service] Policy gate stream error", { streamError });
3762
- }
3763
- const textDelta = part.textDelta ??
3764
- part.text ??
3765
- part.delta;
3766
- if (typeof textDelta === "string") {
3767
- textDeltaChars += textDelta.length;
3768
- }
3769
- const finishReason = part.finishReason;
3770
- if (typeof finishReason === "string") {
3771
- finishReasons.push(finishReason);
3772
- }
3773
- if (part.type === "tool-call") {
3774
- toolCallCounts[part.toolName] =
3775
- (toolCallCounts[part.toolName] ?? 0) + 1;
3776
- const input = part.input ??
3777
- part.args;
3778
- if (input && typeof input === "object") {
3779
- const record = input;
3780
- if (part.toolName === "build_readFile" &&
3781
- typeof record.filePath === "string") {
3782
- filePathsRead.add(record.filePath);
3863
+ const step = policyGateStreamPartToAgentStep(part);
3864
+ if (!step) {
3865
+ return;
3866
+ }
3867
+ void peer.call
3868
+ .aiPushPolicyAgentStep({
3869
+ applicationId: request.applicationId,
3870
+ policyId: request.reviewPolicyId,
3871
+ step,
3872
+ })
3873
+ .catch((error) => {
3874
+ logger.warn("[ai-service] Failed to push policy agent step", getErrorMeta(error));
3875
+ });
3876
+ };
3877
+ try {
3878
+ for await (const part of result.fullStream) {
3879
+ emitAgentStep(part);
3880
+ const partType = typeof part.type === "string"
3881
+ ? part.type
3882
+ : "unknown";
3883
+ streamPartTypeCounts[partType] =
3884
+ (streamPartTypeCounts[partType] ?? 0) + 1;
3885
+ if (partType === "error") {
3886
+ const streamError = serializePolicyGateStreamError(part.error, part);
3887
+ if (streamErrors.length < MAX_POLICY_GATE_STREAM_ERRORS) {
3888
+ streamErrors.push(streamError);
3889
+ }
3890
+ logger.warn("[ai-service] Policy gate stream error", {
3891
+ ...policyRunLogContext,
3892
+ streamError,
3893
+ });
3783
3894
  }
3784
- if (part.toolName === "grep" && typeof record.pattern === "string") {
3785
- grepPatterns.push(record.pattern);
3786
- const toolCallId = typeof part.toolCallId === "string"
3787
- ? part.toolCallId
3788
- : `grep-${grepPatterns.length}`;
3789
- grepPatternByToolCallId.set(toolCallId, record.pattern);
3895
+ const textDelta = part.textDelta ??
3896
+ part.text ??
3897
+ part.delta;
3898
+ if (typeof textDelta === "string") {
3899
+ textDeltaChars += textDelta.length;
3790
3900
  }
3791
- }
3792
- }
3793
- if (part.type === "tool-result") {
3794
- const output = part.output ??
3795
- part.result;
3796
- if (output && typeof output === "object") {
3797
- const record = output;
3798
- if (part.toolName === "build_listFiles" &&
3799
- Array.isArray(record.files)) {
3800
- for (const file of record.files) {
3801
- if (typeof file === "string") {
3802
- filePathsListed.add(file);
3901
+ if (partType === "finish-step") {
3902
+ stepCount += 1;
3903
+ }
3904
+ const finishReason = part
3905
+ .finishReason;
3906
+ if (typeof finishReason === "string") {
3907
+ finishReasons.push(finishReason);
3908
+ }
3909
+ if (part.type === "tool-call") {
3910
+ toolCallCounts[part.toolName] =
3911
+ (toolCallCounts[part.toolName] ?? 0) + 1;
3912
+ const input = part.input ??
3913
+ part.args;
3914
+ if (input && typeof input === "object") {
3915
+ const record = input;
3916
+ if (part.toolName === "build_readFile" &&
3917
+ typeof record.filePath === "string") {
3918
+ filePathsRead.add(record.filePath);
3919
+ }
3920
+ if (part.toolName === "grep" &&
3921
+ typeof record.pattern === "string") {
3922
+ grepPatterns.push(record.pattern);
3923
+ const toolCallId = typeof part.toolCallId ===
3924
+ "string"
3925
+ ? part.toolCallId
3926
+ : `grep-${grepPatterns.length}`;
3927
+ grepPatternByToolCallId.set(toolCallId, record.pattern);
3928
+ }
3929
+ if (part.toolName === "ls") {
3930
+ const toolCallId = typeof part.toolCallId ===
3931
+ "string"
3932
+ ? part.toolCallId
3933
+ : `ls-${lsPathByToolCallId.size + 1}`;
3934
+ lsPathByToolCallId.set(toolCallId, typeof record.path === "string" ? record.path : ".");
3803
3935
  }
3804
3936
  }
3805
3937
  }
3806
- if (part.toolName === "grep" && typeof record.matches === "number") {
3807
- const toolCallId = typeof part.toolCallId === "string"
3808
- ? part.toolCallId
3809
- : `grep-${grepMatchCounts.length + 1}`;
3810
- grepMatchCounts.push({
3811
- matches: record.matches,
3812
- pattern: grepPatternByToolCallId.get(toolCallId) ??
3813
- grepPatterns[grepPatterns.length - 1] ??
3814
- "",
3815
- });
3938
+ if (part.type === "tool-result") {
3939
+ const output = part.output ??
3940
+ part.result;
3941
+ if (output && typeof output === "object") {
3942
+ const record = output;
3943
+ if ((part.toolName === "build_listFiles" ||
3944
+ part.toolName === "glob") &&
3945
+ Array.isArray(record.files)) {
3946
+ for (const file of record.files) {
3947
+ if (typeof file === "string") {
3948
+ filePathsListed.add(file);
3949
+ }
3950
+ }
3951
+ }
3952
+ if (part.toolName === "ls" && typeof record.output === "string") {
3953
+ const toolCallId = typeof part.toolCallId ===
3954
+ "string"
3955
+ ? part.toolCallId
3956
+ : undefined;
3957
+ const dirPath = (toolCallId
3958
+ ? lsPathByToolCallId.get(toolCallId)
3959
+ : undefined) ?? ".";
3960
+ for (const line of record.output.split("\n")) {
3961
+ const trimmed = line.trim();
3962
+ if (!trimmed || trimmed.startsWith("total")) {
3963
+ continue;
3964
+ }
3965
+ const name = trimmed.split(/\s+/).at(-1);
3966
+ if (!name) {
3967
+ continue;
3968
+ }
3969
+ filePathsListed.add(dirPath === "."
3970
+ ? name
3971
+ : `${dirPath.replace(/\/$/, "")}/${name}`);
3972
+ }
3973
+ }
3974
+ if (part.toolName === "grep" &&
3975
+ typeof record.matches === "number") {
3976
+ const toolCallId = typeof part.toolCallId ===
3977
+ "string"
3978
+ ? part.toolCallId
3979
+ : `grep-${grepMatchCounts.length + 1}`;
3980
+ grepMatchCounts.push({
3981
+ matches: record.matches,
3982
+ pattern: grepPatternByToolCallId.get(toolCallId) ??
3983
+ grepPatterns[grepPatterns.length - 1] ??
3984
+ "",
3985
+ });
3986
+ }
3987
+ }
3988
+ }
3989
+ if (part.type === "tool-call" &&
3990
+ part.toolName === "reportReviewRun") {
3991
+ const input = part.input ??
3992
+ part.args;
3993
+ const parsedInput = securityScanResultSchema.safeParse(input);
3994
+ if (!parsedInput.success) {
3995
+ logger.error("[ai-service] Invalid policy gate reportReviewRun input", {
3996
+ ...policyRunLogContext,
3997
+ ...getErrorMeta(parsedInput.error),
3998
+ });
3999
+ failPolicyGateExecution("Invalid policy gate reportReviewRun input", "invalid_report");
4000
+ }
4001
+ scanResult = parsedInput.data;
3816
4002
  }
3817
4003
  }
3818
4004
  }
3819
- if (part.type === "tool-call" && part.toolName === "reportReviewRun") {
3820
- const input = part.input ??
3821
- part.args;
3822
- const parsedInput = securityScanResultSchema.safeParse(input);
3823
- if (!parsedInput.success) {
3824
- logger.error("[ai-service] Invalid policy gate reportReviewRun input", getErrorMeta(parsedInput.error));
3825
- failPolicyGateExecution("Invalid policy gate reportReviewRun input");
4005
+ catch (error) {
4006
+ if (error instanceof Error &&
4007
+ (error.name === "AbortError" || error.name === "TimeoutError")) {
4008
+ const elapsedMs = Date.now() - startedAt;
4009
+ const isBudgetTimeout = error.name === "TimeoutError" ||
4010
+ elapsedMs + POLICY_GATE_TIMEOUT_CLASSIFICATION_SLACK_MS >=
4011
+ POLICY_GATE_MAX_TOTAL_TIME_MS;
4012
+ if (isBudgetTimeout) {
4013
+ logger.warn("[ai-service] Policy gate agent timed out", {
4014
+ ...policyRunLogContext,
4015
+ ...getErrorMeta(error),
4016
+ elapsedMs,
4017
+ });
4018
+ failPolicyGateExecution("Policy gate agent timed out", "timeout");
4019
+ }
4020
+ logger.warn("[ai-service] Policy gate agent aborted", {
4021
+ ...policyRunLogContext,
4022
+ ...getErrorMeta(error),
4023
+ elapsedMs,
4024
+ });
4025
+ failPolicyGateExecution("Policy gate agent aborted", "aborted");
3826
4026
  }
3827
- scanResult = parsedInput.data;
4027
+ throw error;
3828
4028
  }
4029
+ if (!scanResult) {
4030
+ logger.warn("[ai-service] Policy gate agent did not call reportReviewRun", policyRunLogContext);
4031
+ failPolicyGateExecution("Policy gate agent did not complete a report", "missing_report");
4032
+ }
4033
+ const completedScanResult = scanResult;
4034
+ if (!completedScanResult.scanComplete) {
4035
+ logger.warn("[ai-service] Policy gate agent reported an incomplete scan", {
4036
+ ...policyRunLogContext,
4037
+ findingCount: completedScanResult.findings.length,
4038
+ scannedFiles: completedScanResult.scannedFiles,
4039
+ });
4040
+ failPolicyGateExecution("Policy gate agent did not complete a report", "incomplete_report");
4041
+ }
4042
+ logger.info(`[ai-service] Policy gate execution complete: ${completedScanResult.findings.length} findings`);
4043
+ recordExecutionMetrics("success");
4044
+ return {
4045
+ ...completedScanResult,
4046
+ runtimeMetadataJson: buildRuntimeMetadata(),
4047
+ };
3829
4048
  }
3830
- if (!scanResult) {
3831
- logger.warn("[ai-service] Policy gate agent did not call reportReviewRun");
3832
- failPolicyGateExecution("Policy gate agent did not complete a report");
3833
- }
3834
- const completedScanResult = scanResult;
3835
- if (!completedScanResult.scanComplete) {
3836
- logger.warn("[ai-service] Policy gate agent reported an incomplete scan", {
3837
- findingCount: completedScanResult.findings.length,
3838
- scannedFiles: completedScanResult.scannedFiles,
3839
- });
3840
- failPolicyGateExecution("Policy gate agent did not complete a report");
4049
+ catch (error) {
4050
+ if (!metricsRecorded) {
4051
+ recordExecutionMetrics("error");
4052
+ }
4053
+ throw error;
3841
4054
  }
3842
- logger.info(`[ai-service] Policy gate execution complete: ${completedScanResult.findings.length} findings`);
3843
- return {
3844
- ...completedScanResult,
3845
- runtimeMetadataJson: buildRuntimeMetadata(),
3846
- };
3847
4055
  }
3848
4056
  handleUserChange(change) {
3849
4057
  this.getLogger().info(`[ai-service] User changed: ${change.type}`, change);
@@ -3867,7 +4075,7 @@ export class AiService extends TracedEventEmitter {
3867
4075
  // instead of the one the user just cleared (APPS-4407).
3868
4076
  if (contextId) {
3869
4077
  await this.contextManagerV2.persistEmptyContext(contextId);
3870
- await this.uploadEmptyContextToBucketeer(contextId.userId, commitIdForUpload);
4078
+ await this.uploadEmptyContextToBucketeer(commitIdForUpload);
3871
4079
  }
3872
4080
  }
3873
4081
  catch (error) {
@@ -3898,7 +4106,7 @@ export class AiService extends TracedEventEmitter {
3898
4106
  * commitId is captured before newChat() clears the session.
3899
4107
  * Failures are logged but do not fail clear chat.
3900
4108
  */
3901
- async uploadEmptyContextToBucketeer(userId, commitId) {
4109
+ async uploadEmptyContextToBucketeer(commitId) {
3902
4110
  const jwt = this.getInferenceJwt();
3903
4111
  const branchName = this._syncContextProvider?.getBranchName();
3904
4112
  if (!jwt || !commitId || !branchName) {
@@ -3910,7 +4118,6 @@ export class AiService extends TracedEventEmitter {
3910
4118
  commitId,
3911
4119
  branchName,
3912
4120
  appRootDirPath: this.config.appRootDirPath,
3913
- userId,
3914
4121
  useServerUpload: this.isAiFileUploadEnabled,
3915
4122
  superblocksBaseUrl: this.config.superblocksBaseUrl,
3916
4123
  logger: this.getLogger(),
@@ -4018,44 +4225,104 @@ export class AiService extends TracedEventEmitter {
4018
4225
  this.getLogger().warn(`[handleRestoreToCommit] No JWT available, cannot restore or truncate context`);
4019
4226
  return;
4020
4227
  }
4021
- // 1. Try to restore context from checkpoint
4022
- let restored = false;
4228
+ // 1. Roll the conversation back to the checkpoint. The local event log is
4229
+ // the source of truth (see restoreConversationToCheckpoint); the remote
4230
+ // checkpoint archive is only a fallback for a recycled pod, because a
4231
+ // post-checkpoint flush may have overwritten it. The orchestrator returns
4232
+ // an outcome and does NOT touch the metric — this method is the single
4233
+ // place restore outcomes are recorded, so every exit is counted exactly
4234
+ // once.
4235
+ let outcome;
4023
4236
  try {
4024
4237
  const contextId = getContextId(this.clark, this.config);
4025
- const result = await restoreContextFromCheckpoint({
4026
- applicationId,
4027
- commitId,
4028
- branchName,
4238
+ outcome = await restoreConversationToCheckpoint({
4239
+ contextManagerV2: this.contextManagerV2,
4029
4240
  contextId,
4030
- appRootDirPath: this.config.appRootDirPath,
4031
- superblocksBaseUrl: this.config.superblocksBaseUrl,
4241
+ sinceTimestamp,
4032
4242
  logger: this.getLogger(),
4033
- jwt,
4034
- contextManagerV2: this.contextManagerV2,
4243
+ coldRestore: async () => {
4244
+ const result = await restoreContextFromCheckpoint({
4245
+ applicationId,
4246
+ commitId,
4247
+ branchName,
4248
+ contextId,
4249
+ appRootDirPath: this.config.appRootDirPath,
4250
+ superblocksBaseUrl: this.config.superblocksBaseUrl,
4251
+ logger: this.getLogger(),
4252
+ jwt,
4253
+ contextManagerV2: this.contextManagerV2,
4254
+ });
4255
+ if (!result.success) {
4256
+ const reason = result.savedFiles.length > 0
4257
+ ? "main.json not found in checkpoint"
4258
+ : "no checkpoint found";
4259
+ this.getLogger().info(`[handleRestoreToCommit] Context restore failed: ${reason} for commitId=${commitId}, preserving chat messages`);
4260
+ }
4261
+ return result.success;
4262
+ },
4263
+ republish: async () => {
4264
+ await uploadContextToBucketeer({
4265
+ applicationId,
4266
+ commitId,
4267
+ branchName,
4268
+ appRootDirPath: this.config.appRootDirPath,
4269
+ useServerUpload: this.isAiFileUploadEnabled,
4270
+ superblocksBaseUrl: this.config.superblocksBaseUrl,
4271
+ logger: this.getLogger(),
4272
+ jwt,
4273
+ // The heal may have deleted the legacy snapshot, leaving an
4274
+ // events-only archive that IS the rolled-back state; upload it so the
4275
+ // remote /context/latest is healed (the download rebuilds the record
4276
+ // from the truncated event log) instead of staying poisoned.
4277
+ allowEventsOnlyArchive: true,
4278
+ });
4279
+ },
4035
4280
  });
4036
- if (result.success) {
4037
- restored = true;
4038
- }
4039
- else {
4040
- const reason = result.savedFiles.length > 0
4041
- ? "main.json not found in checkpoint"
4042
- : "no checkpoint found";
4043
- this.getLogger().info(`[handleRestoreToCommit] Context restore failed: ${reason} for commitId=${commitId}, preserving chat messages`);
4044
- }
4045
4281
  }
4046
4282
  catch (error) {
4047
- this.getLogger().warn(`[handleRestoreToCommit] Error restoring context from checkpoint, preserving chat messages`, getErrorMeta(error));
4048
- }
4049
- if (!restored) {
4283
+ // The orchestrator itself threw (e.g. a cold restore's scoped-token or
4284
+ // network error propagating out of coldRestore). Count it so the restore
4285
+ // metric reflects every attempt, not just the ones that return normally.
4286
+ this.getLogger().error(`[handleRestoreToCommit] Error restoring context from checkpoint, preserving chat messages`, getErrorMeta(error));
4287
+ recordCheckpointRestore("aborted");
4050
4288
  return;
4051
4289
  }
4052
- // 2. Checkpoint restored — truncate server chat to match
4053
- try {
4054
- await this.chatSessionStore.truncateAfterTimestamp(sinceTimestamp);
4055
- this.getLogger().info(`[handleRestoreToCommit] Successfully restored context and truncated chat for commitId=${commitId}`);
4290
+ if (outcome !== "rolled_back") {
4291
+ // Context was NOT rolled back (aborted / unhealed / no_checkpoint). Record
4292
+ // the outcome and leave the chat intact so it keeps matching the context.
4293
+ recordCheckpointRestore(outcome);
4294
+ this.getLogger().info(`[handleRestoreToCommit] No context restored (${outcome}) for commitId=${commitId}; leaving chat intact`, { applicationId, branchName });
4295
+ return;
4056
4296
  }
4057
- catch (error) {
4058
- this.getLogger().warn(`[handleRestoreToCommit] Context restored but failed to truncate chat for commitId=${commitId}`, getErrorMeta(error));
4297
+ // 2. Context is rolled back — truncate the server chat to match. This is a
4298
+ // cross-service RPC and is NOT transactional with the context rollback
4299
+ // above, so a failure here leaves the context rolled back while the chat
4300
+ // still shows the reverted turns (a chat-ahead-of-context divergence).
4301
+ // Retry a few times to ride out a transient blip; if it still fails,
4302
+ // surface it at error level with a metric so the divergence is alertable,
4303
+ // not buried.
4304
+ // truncateAfterTimestamp is idempotent (same cutoff → same result), so the
4305
+ // retries are safe.
4306
+ const CHAT_TRUNCATE_ATTEMPTS = 3;
4307
+ for (let attempt = 1; attempt <= CHAT_TRUNCATE_ATTEMPTS; attempt++) {
4308
+ try {
4309
+ await this.chatSessionStore.truncateAfterTimestamp(sinceTimestamp);
4310
+ this.getLogger().info(`[handleRestoreToCommit] Successfully restored context and truncated chat for commitId=${commitId}`);
4311
+ recordCheckpointRestore("restored");
4312
+ return;
4313
+ }
4314
+ catch (error) {
4315
+ if (attempt < CHAT_TRUNCATE_ATTEMPTS) {
4316
+ this.getLogger().warn(`[handleRestoreToCommit] Chat truncation attempt ${attempt}/${CHAT_TRUNCATE_ATTEMPTS} failed for commitId=${commitId}, retrying`, getErrorMeta(error));
4317
+ await new Promise((resolve) => setTimeout(resolve, 200 * attempt));
4318
+ continue;
4319
+ }
4320
+ // Exhausted retries: context is rolled back but the chat is not. Log at
4321
+ // error (a real, if less-harmful, chat/context divergence) and count it.
4322
+ this.getLogger().error(`[handleRestoreToCommit] Context rolled back but chat truncation failed after ${CHAT_TRUNCATE_ATTEMPTS} attempts for commitId=${commitId} ` +
4323
+ `(applicationId=${applicationId}, branchName=${branchName}); chat may show reverted turns until the next turn resyncs it`, getErrorMeta(error));
4324
+ recordCheckpointRestore("chat_truncate_failed");
4325
+ }
4059
4326
  }
4060
4327
  }
4061
4328
  async handleApiUpdate(update) {