@superblocksteam/vite-plugin-file-sync 2.0.155 → 2.0.156-next.1

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 (177) hide show
  1. package/dist/ai-service/agent/prompts/build-base-system-prompt.d.ts +9 -2
  2. package/dist/ai-service/agent/prompts/build-base-system-prompt.d.ts.map +1 -1
  3. package/dist/ai-service/agent/prompts/build-base-system-prompt.js +34 -6
  4. package/dist/ai-service/agent/prompts/build-base-system-prompt.js.map +1 -1
  5. package/dist/ai-service/agent/prompts/build-security-scan-prompt.d.ts +2 -1
  6. package/dist/ai-service/agent/prompts/build-security-scan-prompt.d.ts.map +1 -1
  7. package/dist/ai-service/agent/prompts/build-security-scan-prompt.js +52 -9
  8. package/dist/ai-service/agent/prompts/build-security-scan-prompt.js.map +1 -1
  9. package/dist/ai-service/agent/tool-permission-delivery-metrics.d.ts +3 -0
  10. package/dist/ai-service/agent/tool-permission-delivery-metrics.d.ts.map +1 -0
  11. package/dist/ai-service/agent/tool-permission-delivery-metrics.js +23 -0
  12. package/dist/ai-service/agent/tool-permission-delivery-metrics.js.map +1 -0
  13. package/dist/ai-service/agent/tool-permission-delivery.d.ts +25 -16
  14. package/dist/ai-service/agent/tool-permission-delivery.d.ts.map +1 -1
  15. package/dist/ai-service/agent/tool-permission-delivery.js +384 -38
  16. package/dist/ai-service/agent/tool-permission-delivery.js.map +1 -1
  17. package/dist/ai-service/agent/tools/apis/get-sdk-api-docs.d.ts +9 -35
  18. package/dist/ai-service/agent/tools/apis/get-sdk-api-docs.d.ts.map +1 -1
  19. package/dist/ai-service/agent/tools/apis/get-sdk-api-docs.js +71 -98
  20. package/dist/ai-service/agent/tools/apis/get-sdk-api-docs.js.map +1 -1
  21. package/dist/ai-service/agent/tools/apis/test-api.d.ts.map +1 -1
  22. package/dist/ai-service/agent/tools/apis/test-api.js +3 -2
  23. package/dist/ai-service/agent/tools/apis/test-api.js.map +1 -1
  24. package/dist/ai-service/agent/tools/app-environment/update-app-environment.d.ts +1 -1
  25. package/dist/ai-service/agent/tools/app-environment/update-app-environment.d.ts.map +1 -1
  26. package/dist/ai-service/agent/tools/app-environment/update-app-environment.js +13 -1
  27. package/dist/ai-service/agent/tools/app-environment/update-app-environment.js.map +1 -1
  28. package/dist/ai-service/agent/tools/build-capture-screenshot.d.ts.map +1 -1
  29. package/dist/ai-service/agent/tools/build-capture-screenshot.js +8 -3
  30. package/dist/ai-service/agent/tools/build-capture-screenshot.js.map +1 -1
  31. package/dist/ai-service/agent/tools/build-navigate-preview.d.ts.map +1 -1
  32. package/dist/ai-service/agent/tools/build-navigate-preview.js +4 -2
  33. package/dist/ai-service/agent/tools/build-navigate-preview.js.map +1 -1
  34. package/dist/ai-service/agent/tools/build-reload-file.d.ts.map +1 -1
  35. package/dist/ai-service/agent/tools/build-reload-file.js +4 -2
  36. package/dist/ai-service/agent/tools/build-reload-file.js.map +1 -1
  37. package/dist/ai-service/agent/tools/get-logs.d.ts +2 -2
  38. package/dist/ai-service/agent/tools/integrations/delete-integration.d.ts.map +1 -1
  39. package/dist/ai-service/agent/tools/integrations/delete-integration.js +6 -3
  40. package/dist/ai-service/agent/tools/integrations/delete-integration.js.map +1 -1
  41. package/dist/ai-service/agent/tools/integrations/execute-request.d.ts +13 -13
  42. package/dist/ai-service/agent/tools/integrations/integration-setup.d.ts +18 -6
  43. package/dist/ai-service/agent/tools/integrations/integration-setup.d.ts.map +1 -1
  44. package/dist/ai-service/agent/tools/integrations/integration-setup.js +41 -15
  45. package/dist/ai-service/agent/tools/integrations/integration-setup.js.map +1 -1
  46. package/dist/ai-service/agent/tools/integrations/internal.d.ts.map +1 -1
  47. package/dist/ai-service/agent/tools/integrations/internal.js +4 -2
  48. package/dist/ai-service/agent/tools/integrations/internal.js.map +1 -1
  49. package/dist/ai-service/agent/tools/report-security-findings.d.ts +23 -23
  50. package/dist/ai-service/agent/tools.d.ts +4 -2
  51. package/dist/ai-service/agent/tools.d.ts.map +1 -1
  52. package/dist/ai-service/agent/tools.js +21 -14
  53. package/dist/ai-service/agent/tools.js.map +1 -1
  54. package/dist/ai-service/agent/tools2/tools/git.d.ts +2 -2
  55. package/dist/ai-service/index.d.ts +26 -25
  56. package/dist/ai-service/index.d.ts.map +1 -1
  57. package/dist/ai-service/index.js +340 -121
  58. package/dist/ai-service/index.js.map +1 -1
  59. package/dist/ai-service/integrations/store.d.ts +2 -1
  60. package/dist/ai-service/integrations/store.d.ts.map +1 -1
  61. package/dist/ai-service/integrations/store.js +3 -2
  62. package/dist/ai-service/integrations/store.js.map +1 -1
  63. package/dist/ai-service/judge/tools/playwright-action.d.ts +1 -1
  64. package/dist/ai-service/judge/tools/submit-feedback.d.ts +1 -1
  65. package/dist/ai-service/llm/client.d.ts +9 -3
  66. package/dist/ai-service/llm/client.d.ts.map +1 -1
  67. package/dist/ai-service/llm/client.js +10 -3
  68. package/dist/ai-service/llm/client.js.map +1 -1
  69. package/dist/ai-service/llm/context-v2/context-metrics.d.ts.map +1 -1
  70. package/dist/ai-service/llm/context-v2/context-metrics.js +3 -0
  71. package/dist/ai-service/llm/context-v2/context-metrics.js.map +1 -1
  72. package/dist/ai-service/llm/stream/observers/llmobs.d.ts +6 -6
  73. package/dist/ai-service/llm/stream/observers/llmobs.d.ts.map +1 -1
  74. package/dist/ai-service/llm/stream/observers/llmobs.js +10 -9
  75. package/dist/ai-service/llm/stream/observers/llmobs.js.map +1 -1
  76. package/dist/ai-service/llm/stream/observers/step-token-usage.d.ts +15 -0
  77. package/dist/ai-service/llm/stream/observers/step-token-usage.d.ts.map +1 -0
  78. package/dist/ai-service/llm/stream/observers/step-token-usage.js +88 -0
  79. package/dist/ai-service/llm/stream/observers/step-token-usage.js.map +1 -0
  80. package/dist/ai-service/llm/stream/orchestrator.d.ts +4 -0
  81. package/dist/ai-service/llm/stream/orchestrator.d.ts.map +1 -1
  82. package/dist/ai-service/llm/stream/orchestrator.js +19 -41
  83. package/dist/ai-service/llm/stream/orchestrator.js.map +1 -1
  84. package/dist/ai-service/llm/stream/retry-engine.d.ts +1 -1
  85. package/dist/ai-service/llm/stream/retry-engine.d.ts.map +1 -1
  86. package/dist/ai-service/llm/stream/retry-engine.js +26 -1
  87. package/dist/ai-service/llm/stream/retry-engine.js.map +1 -1
  88. package/dist/ai-service/llm/stream/terminal-reason.d.ts +6 -0
  89. package/dist/ai-service/llm/stream/terminal-reason.d.ts.map +1 -0
  90. package/dist/ai-service/llm/stream/terminal-reason.js +73 -0
  91. package/dist/ai-service/llm/stream/terminal-reason.js.map +1 -0
  92. package/dist/ai-service/policy-agent-metrics.d.ts +10 -0
  93. package/dist/ai-service/policy-agent-metrics.d.ts.map +1 -1
  94. package/dist/ai-service/policy-agent-metrics.js +33 -0
  95. package/dist/ai-service/policy-agent-metrics.js.map +1 -1
  96. package/dist/ai-service/state-machine/clark-fsm.d.ts +29 -1
  97. package/dist/ai-service/state-machine/clark-fsm.d.ts.map +1 -1
  98. package/dist/ai-service/state-machine/clark-fsm.js +3 -1
  99. package/dist/ai-service/state-machine/clark-fsm.js.map +1 -1
  100. package/dist/ai-service/state-machine/handlers/agent-planning.d.ts.map +1 -1
  101. package/dist/ai-service/state-machine/handlers/agent-planning.js +7 -6
  102. package/dist/ai-service/state-machine/handlers/agent-planning.js.map +1 -1
  103. package/dist/ai-service/state-machine/handlers/llm-generating.d.ts.map +1 -1
  104. package/dist/ai-service/state-machine/handlers/llm-generating.js +18 -5
  105. package/dist/ai-service/state-machine/handlers/llm-generating.js.map +1 -1
  106. package/dist/ai-service/state-machine/helpers/fetch-with-reconnect-retry.d.ts.map +1 -1
  107. package/dist/ai-service/state-machine/helpers/fetch-with-reconnect-retry.js +8 -6
  108. package/dist/ai-service/state-machine/helpers/fetch-with-reconnect-retry.js.map +1 -1
  109. package/dist/ai-service/state-machine/helpers/policy-gate-usage-metrics.d.ts +0 -14
  110. package/dist/ai-service/state-machine/helpers/policy-gate-usage-metrics.d.ts.map +1 -1
  111. package/dist/ai-service/state-machine/helpers/policy-gate-usage-metrics.js +0 -36
  112. package/dist/ai-service/state-machine/helpers/policy-gate-usage-metrics.js.map +1 -1
  113. package/dist/ai-service/state-machine/helpers/pre-session-metrics.d.ts +2 -0
  114. package/dist/ai-service/state-machine/helpers/pre-session-metrics.d.ts.map +1 -1
  115. package/dist/ai-service/state-machine/helpers/pre-session-metrics.js +3 -0
  116. package/dist/ai-service/state-machine/helpers/pre-session-metrics.js.map +1 -1
  117. package/dist/ai-service/state-machine/helpers/prompt-degradation-metrics.d.ts +28 -0
  118. package/dist/ai-service/state-machine/helpers/prompt-degradation-metrics.d.ts.map +1 -0
  119. package/dist/ai-service/state-machine/helpers/prompt-degradation-metrics.js +254 -0
  120. package/dist/ai-service/state-machine/helpers/prompt-degradation-metrics.js.map +1 -0
  121. package/dist/ai-service/state-machine/helpers/prompt-request-metrics.d.ts +1 -0
  122. package/dist/ai-service/state-machine/helpers/prompt-request-metrics.d.ts.map +1 -1
  123. package/dist/ai-service/state-machine/helpers/prompt-request-metrics.js +60 -27
  124. package/dist/ai-service/state-machine/helpers/prompt-request-metrics.js.map +1 -1
  125. package/dist/ai-service/state-machine/helpers/stable-peer.d.ts +12 -1
  126. package/dist/ai-service/state-machine/helpers/stable-peer.d.ts.map +1 -1
  127. package/dist/ai-service/state-machine/helpers/stable-peer.js +42 -9
  128. package/dist/ai-service/state-machine/helpers/stable-peer.js.map +1 -1
  129. package/dist/ai-service/state-machine/mocks.d.ts.map +1 -1
  130. package/dist/ai-service/state-machine/mocks.js +6 -0
  131. package/dist/ai-service/state-machine/mocks.js.map +1 -1
  132. package/dist/ai-service/util/call-peer.d.ts +11 -0
  133. package/dist/ai-service/util/call-peer.d.ts.map +1 -0
  134. package/dist/ai-service/util/call-peer.js +47 -0
  135. package/dist/ai-service/util/call-peer.js.map +1 -0
  136. package/dist/ai-service/util/peer-liveness.d.ts +23 -0
  137. package/dist/ai-service/util/peer-liveness.d.ts.map +1 -0
  138. package/dist/ai-service/util/peer-liveness.js +42 -0
  139. package/dist/ai-service/util/peer-liveness.js.map +1 -0
  140. package/dist/ai-service/util/rpc-timeout.d.ts +12 -5
  141. package/dist/ai-service/util/rpc-timeout.d.ts.map +1 -1
  142. package/dist/ai-service/util/rpc-timeout.js +12 -5
  143. package/dist/ai-service/util/rpc-timeout.js.map +1 -1
  144. package/dist/file-sync-vite-plugin.d.ts.map +1 -1
  145. package/dist/file-sync-vite-plugin.js +8 -21
  146. package/dist/file-sync-vite-plugin.js.map +1 -1
  147. package/dist/lock-service/activity-tracker.d.ts +0 -5
  148. package/dist/lock-service/activity-tracker.d.ts.map +1 -1
  149. package/dist/lock-service/activity-tracker.js +0 -13
  150. package/dist/lock-service/activity-tracker.js.map +1 -1
  151. package/dist/lock-service/index.d.ts +308 -27
  152. package/dist/lock-service/index.d.ts.map +1 -1
  153. package/dist/lock-service/index.js +680 -156
  154. package/dist/lock-service/index.js.map +1 -1
  155. package/dist/policy-gate-callback-mapper.d.ts +3 -2
  156. package/dist/policy-gate-callback-mapper.d.ts.map +1 -1
  157. package/dist/policy-gate-callback-mapper.js.map +1 -1
  158. package/dist/policy-gate-runner.d.ts +4 -2
  159. package/dist/policy-gate-runner.d.ts.map +1 -1
  160. package/dist/policy-gate-runner.js +31 -8
  161. package/dist/policy-gate-runner.js.map +1 -1
  162. package/dist/scan-scope-merge.d.ts +16 -0
  163. package/dist/scan-scope-merge.d.ts.map +1 -0
  164. package/dist/scan-scope-merge.js +90 -0
  165. package/dist/scan-scope-merge.js.map +1 -0
  166. package/dist/socket-manager.d.ts +47 -6
  167. package/dist/socket-manager.d.ts.map +1 -1
  168. package/dist/socket-manager.js +283 -66
  169. package/dist/socket-manager.js.map +1 -1
  170. package/dist/sync-service/index.d.ts.map +1 -1
  171. package/dist/sync-service/index.js +3 -3
  172. package/dist/sync-service/index.js.map +1 -1
  173. package/dist/test-setup.d.ts +2 -0
  174. package/dist/test-setup.d.ts.map +1 -0
  175. package/dist/test-setup.js +21 -0
  176. package/dist/test-setup.js.map +1 -0
  177. package/package.json +8 -8
@@ -2,9 +2,9 @@ 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 { hasToolCall, streamText as aiStreamText, } from "ai";
5
+ import { hasToolCall, stepCountIs, } from "ai";
6
6
  import { z } from "zod";
7
- import { AiGenerationState, AiMode, MAX_OUTPUT_TOKENS, resolveAttachmentFileName, } from "@superblocksteam/library-shared/types";
7
+ import { AiGenerationState, AiMode, resolveAttachmentFileName, } from "@superblocksteam/library-shared/types";
8
8
  import { isToolPermissionClarification } from "@superblocksteam/library-shared/types/tool-permission-response";
9
9
  import { addTracingToMethods, AiSystemTriggerType, classifyAttachmentDelivery, FactAccessType, FactCreatedSource, FactEntityScope, TracedEventEmitter, getAiQuotaPaywallReasonFromMessage, } from "@superblocksteam/shared";
10
10
  import { npmRegistryEmitter } from "@superblocksteam/telemetry";
@@ -14,6 +14,8 @@ import { POLICY_GATE_RUN_IN_PROGRESS_ERROR, policyGateRunRegistry, } from "../po
14
14
  import { getErrorMeta, getLogger, setDefaultLogger } from "../util/logger.js";
15
15
  import { composeSecurityAgentPrompt, } from "./agent/prompts/build-security-scan-prompt.js";
16
16
  import { getToolCallArguments, getToolErrorCode, getToolOutput, } from "./agent/tool-message-utils.js";
17
+ import { recordToolPermissionDeliveryOutcome } from "./agent/tool-permission-delivery-metrics.js";
18
+ import { createExpiredPermissionPromptDeliveryError, flushPendingToolPermissionPrompt, inFlightToolPermissionReplay, } from "./agent/tool-permission-delivery.js";
17
19
  import { buildSecurityScanTools, isBackgroundTaskFrameworkEnabled, } from "./agent/tools.js";
18
20
  import { hasPendingEscalations, flushPendingEscalations, } from "./agent/tools/apis/api-validation-orchestrator.js";
19
21
  import { safeSampleJson } from "./agent/tools/apis/sample-json.js";
@@ -22,7 +24,7 @@ import { applyDevDatabaseMigrationsTaskMetadata, createDevDatabaseTaskTypes, wir
22
24
  import { debugCache } from "./agent/tools/debug-cache.js";
23
25
  import { securityScanResultSchema, } from "./agent/tools/report-security-findings.js";
24
26
  import { SessionEntityPermissionStore, } from "./agent/tools2/entity-permissions.js";
25
- import { ToolRegistry } from "./agent/tools2/registry.js";
27
+ import { ToolRegistry, } from "./agent/tools2/registry.js";
26
28
  import { FileSystemInterface } from "./app-interface/file-system-interface.js";
27
29
  import { createDefaultVirtualFileSystem, createPathValidator, } from "./app-interface/filesystem/index.js";
28
30
  import { NpmPackageLookup } from "./app-interface/npm-package-lookup.js";
@@ -50,6 +52,7 @@ import { LiveEditEventForwarder, readForwarderConfigFromEnv, } from "./live-edit
50
52
  import { createChaosFetch } from "./llm/chaos-fetch.js";
51
53
  import { createClarkStreamTelemetrySink } from "./llm/clark-stream-telemetry-sink.js";
52
54
  import { LLMClient } from "./llm/client.js";
55
+ import { resolveContextOptionsV2 } from "./llm/context-v2/config.js";
53
56
  import { selectContextManagementStrategy, } from "./llm/context-v2/context-management.js";
54
57
  import { recordCheckpointRestore } from "./llm/context-v2/context-metrics.js";
55
58
  import { ContextManagerV2 } from "./llm/context-v2/manager.js";
@@ -57,8 +60,7 @@ import { JsonlEventStore } from "./llm/context-v2/storage/jsonl-event-store.js";
57
60
  import { adoptLegacyPerUserContextDirs } from "./llm/context-v2/storage/legacy-context-adoption.js";
58
61
  import { LocalContextStorageV2 } from "./llm/context-v2/storage/local.js";
59
62
  import { createLLMProvider, setSpanError } from "./llm/provider.js";
60
- import { traceLLM } from "./llmobs/helpers.js";
61
- import { getJwtTraceTags } from "./llmobs/helpers.js";
63
+ import { getJwtTraceTags, traceLLM } from "./llmobs/helpers.js";
62
64
  import llmobs from "./llmobs/index.js";
63
65
  import { PlaywrightMcpServerManager } from "./mcp/playwright-server.js";
64
66
  import { evaluateMigrationAutoDispatchReadiness } from "./migration-auto-dispatch-readiness.js";
@@ -87,10 +89,10 @@ import { doRuntimeReviewing } from "./state-machine/handlers/runtime-reviewing.j
87
89
  import { classificationHelper } from "./state-machine/helpers/classification.js";
88
90
  import { getContextId } from "./state-machine/helpers/context-id.js";
89
91
  import { bindHeadlessTurnLifecycle } from "./state-machine/helpers/headless-turn-lifecycle.js";
90
- import { sendUserGenerationStateChannel } from "./state-machine/helpers/peer.js";
91
- import { sendUserMessageChannel, clearPendingToolPermissionRequest, } from "./state-machine/helpers/peer.js";
92
- import { collectPolicyGateLlmUsage, finalizePolicyGateUsageMetrics, policyGateFailureFromStream, recordPolicyGateAttempt, } from "./state-machine/helpers/policy-gate-usage-metrics.js";
92
+ import { clearPendingToolPermissionRequest, sendUserGenerationStateChannel, sendUserMessageChannel, } from "./state-machine/helpers/peer.js";
93
+ import { finalizePolicyGateUsageMetrics, policyGateFailureFromStream, recordPolicyGateAttempt, } from "./state-machine/helpers/policy-gate-usage-metrics.js";
93
94
  import { finalizePromptBillingMetrics } from "./state-machine/helpers/prompt-billing-metrics.js";
95
+ import { setPromptDegradationMetricIdentity } from "./state-machine/helpers/prompt-degradation-metrics.js";
94
96
  import { paywallStopAdvice } from "./state-machine/helpers/prompt-failure.js";
95
97
  import { CLARK_USAGE_ORIGIN, promptFailureFromStreamAbort, promptFailurePaywall, promptFailurePlatform, promptFailureUserCanceled, PROMPT_ATTEMPT_ENTRYPOINT, recordBaseCommitResolutionFailed, recordPromptAttempt, recordPromptEntryRejection, recordPromptOutcome, recordAwaitingUserHandoffDismissed, resolvePromptMetricLabels, resolvePromptOutcomeDedupeKey, resolvePromptRequestDedupeKey, setPromptRequestMetricIdentity, } from "./state-machine/helpers/prompt-request-metrics.js";
96
98
  import { catchBackgroundPeerDisconnect, StablePeer, } from "./state-machine/helpers/stable-peer.js";
@@ -100,6 +102,7 @@ import { TemplateRenderer } from "./template-renderer.js";
100
102
  import { describeMigrationInferenceJwt, isUsableMigrationInferenceJwt, } from "./util/inference-jwt.js";
101
103
  import { buildModelConfigAttributes, processLLMConfig, } from "./util/llm-config-utils.js";
102
104
  import { parseJwt } from "./util/parse-jwt.js";
105
+ import { PERMISSION_PROMPT_DELIVERY_TIMEOUT_MS } from "./util/rpc-timeout.js";
103
106
  import { getToolCallSignature } from "./util/tool-signature.js";
104
107
  export { AiServiceFeatureFlags } from "./features.js";
105
108
  export { getManagedNativeDbGateDenialReason, hasEditLifecycleCapability, isManagedNativeDbAvailableForClark, } from "./managed-native-db-availability.js";
@@ -142,6 +145,9 @@ const MAX_POLICY_GATE_STREAM_ERROR_FIELD_LENGTH = 500;
142
145
  const MAX_POLICY_GATE_STREAM_ERROR_OBJECT_DEPTH = 3;
143
146
  const MAX_POLICY_GATE_STREAM_ERROR_OBJECT_KEYS = 20;
144
147
  const POLICY_GATE_MAX_TOTAL_TIME_MS = 15 * 60 * 1000;
148
+ // Ceiling above the historical 30-step cap; 100 is an existing policy-agent
149
+ // histogram bucket. Tighten from production p99 if scans complete far below this.
150
+ const POLICY_GATE_MAX_STEPS = 100;
145
151
  // AbortError near the budget is treated as timeout (SDK may abort slightly early).
146
152
  const POLICY_GATE_TIMEOUT_CLASSIFICATION_SLACK_MS = 5_000;
147
153
  const POLICY_GATE_STREAM_ERROR_REDACTED_VALUE = "[redacted]";
@@ -354,6 +360,9 @@ const POLICY_GATE_USAGE_METADATA = {
354
360
  origin: CLARK_USAGE_ORIGIN.policy_gate,
355
361
  surface: "policy_gate",
356
362
  };
363
+ const ERROR_JSON_OUTPUT_TYPE = "error-json";
364
+ const TOOL_MESSAGE_ROLE = "tool";
365
+ const TOOL_RESULT_CONTENT_TYPE = "tool-result";
357
366
  export class AiService extends TracedEventEmitter {
358
367
  config;
359
368
  templateRenderer;
@@ -496,10 +505,6 @@ export class AiService extends TracedEventEmitter {
496
505
  this._runtimeSyncService = syncService;
497
506
  this._runtimeGitOperationLock = gitOperationLock;
498
507
  }
499
- /**
500
- * Sets the sync context provider used by the remote context restore fallback.
501
- * Called by the socket manager once the sync service is initialized.
502
- */
503
508
  setSyncContextProvider(provider) {
504
509
  this._syncContextProvider = provider;
505
510
  }
@@ -564,7 +569,6 @@ export class AiService extends TracedEventEmitter {
564
569
  this.tryAutoDispatchMigrationTurnIfReady();
565
570
  return true;
566
571
  }
567
- // ===== Recording API =====
568
572
  /**
569
573
  * Check if recording is currently enabled.
570
574
  */
@@ -621,6 +625,10 @@ export class AiService extends TracedEventEmitter {
621
625
  applicationId: config.applicationId,
622
626
  organizationId: config.organizationId,
623
627
  });
628
+ setPromptDegradationMetricIdentity({
629
+ applicationId: config.applicationId,
630
+ organizationId: config.organizationId,
631
+ });
624
632
  this.startLiveEditEventForwarding();
625
633
  // Store the wrapped logger so getLogger() can return it
626
634
  // The wrapped logger forwards logs to the browser console when Clark logs are enabled
@@ -904,6 +912,12 @@ export class AiService extends TracedEventEmitter {
904
912
  organizationId: config.organizationId,
905
913
  applicationId: config.applicationId,
906
914
  getJwt: () => this.requireProviderInferenceAuthToken(),
915
+ stepTokenUsageLabels: () => ({
916
+ coreModel: this.clark?.context.llmConfig?.coreModel,
917
+ mode: this.clark?.context.currentMode === AiMode.PLAN ? "plan" : "build",
918
+ surface: this.clark?.context.usageMetadata?.surface ??
919
+ APP_CLARK_GENERATION_METADATA.surface,
920
+ }),
907
921
  onUsageRecorded: (usage) => {
908
922
  // Accumulate workflow-level metrics
909
923
  this.clark.updateContext((context) => {
@@ -937,9 +951,7 @@ export class AiService extends TracedEventEmitter {
937
951
  createTagProvider: this.createClarkTagProviderFactory(),
938
952
  sessionId: this.sessionId,
939
953
  });
940
- // Initialize judge service
941
954
  this.mcpServerManager = new PlaywrightMcpServerManager(this.clark.logger);
942
- // Initialize recording manager for capturing AI interactions
943
955
  this.recordingManager = new RecordingManager({
944
956
  recordingsDir: path.join(config.appRootDirPath, ".superblocks", "recordings"),
945
957
  appRootDirPath: config.appRootDirPath,
@@ -1307,6 +1319,7 @@ export class AiService extends TracedEventEmitter {
1307
1319
  llmClient: this.llmClient,
1308
1320
  toolRegistry: this.toolRegistry,
1309
1321
  entityPermissionStore: this.entityPermissionStore,
1322
+ onToolPermissionParked: (messageId) => this.scheduleToolPermissionReplayWindow(messageId),
1310
1323
  mcpServerManager: this.mcpServerManager,
1311
1324
  viteServer: this.viteServer,
1312
1325
  recordingManager: this.recordingManager,
@@ -2259,7 +2272,6 @@ export class AiService extends TracedEventEmitter {
2259
2272
  useMockGeneration: request.mockGeneration ?? false,
2260
2273
  planContext: mergePlanContexts(context.planContext, request.planContext),
2261
2274
  browserContext: request.browserContext,
2262
- // Initialize workflow metrics tracking
2263
2275
  workflowMetrics: {
2264
2276
  toolCallCount: 0,
2265
2277
  stepCount: 0,
@@ -2609,7 +2621,6 @@ export class AiService extends TracedEventEmitter {
2609
2621
  const timeoutController = new AbortController();
2610
2622
  const timeoutId = setTimeout(() => timeoutController.abort(), timeout);
2611
2623
  try {
2612
- // Build the appropriate content part based on attachment type
2613
2624
  const contentPart = this.attachmentToContentPart(attachment);
2614
2625
  // Route labeling through the shared LLMClient (APPS-5061) as a stateless
2615
2626
  // one-shot, mirroring the APPS-1213 (#20263) migrations. No safety config:
@@ -2663,7 +2674,6 @@ export class AiService extends TracedEventEmitter {
2663
2674
  };
2664
2675
  }
2665
2676
  else if (attachment.type === "pdf" || attachment.type === "archive") {
2666
- // Parse data URL to extract base64 data
2667
2677
  const match = attachment.data.match(/^data:([^;]+);base64,(.+)$/);
2668
2678
  const data = match?.[2] ?? attachment.data;
2669
2679
  const defaultMediaType = attachment.type === "pdf"
@@ -2763,6 +2773,10 @@ export class AiService extends TracedEventEmitter {
2763
2773
  await this.clark.recordSystemMessage(params, "Environment event", "User closed the integration setup form. The integration setup might not have been completed successfully.");
2764
2774
  }
2765
2775
  async handleUserCanceled() {
2776
+ // Drop parked/in-flight permission prompts first so interval replay and
2777
+ // grace expiry cannot keep pushing the dialog or inject a continuation
2778
+ // after the user has already stopped the turn.
2779
+ clearPendingToolPermissionRequest(this.clark, "user-canceled");
2766
2780
  const awaitingUser = this.isAwaitingUser();
2767
2781
  if (!this.isBusy() && !awaitingUser) {
2768
2782
  return;
@@ -3021,11 +3035,9 @@ export class AiService extends TracedEventEmitter {
3021
3035
  logger.warn(`[ai-service] Skip request for testRunId=${testRunId} but current is ${this.clark.context.currentTestRunId}`);
3022
3036
  return;
3023
3037
  }
3024
- // Update context with skip request
3025
3038
  this.clark.updateContext({
3026
3039
  skipRequestedForTestCase: testCaseId,
3027
3040
  });
3028
- // Update the test case status to skipped
3029
3041
  const testCaseStatuses = this.clark.context.testCaseStatuses ??
3030
3042
  new Map();
3031
3043
  testCaseStatuses.set(testCaseId, "skipped");
@@ -3046,24 +3058,127 @@ export class AiService extends TracedEventEmitter {
3046
3058
  }
3047
3059
  logger.info(`[ai-service] Test case ${testCaseId} marked as skipped, flag set for subagent`);
3048
3060
  }
3061
+ scheduleToolPermissionReplayWindow(messageId) {
3062
+ const pending = this.clark.context.pendingToolPermissionRequest;
3063
+ if (pending?.messageId !== messageId ||
3064
+ pending.delivery.status !== "parked") {
3065
+ return;
3066
+ }
3067
+ this.scheduleToolPermissionExpiry(messageId);
3068
+ this.scheduleToolPermissionReplay(messageId);
3069
+ }
3070
+ scheduleToolPermissionExpiry(messageId) {
3071
+ const pending = this.clark.context.pendingToolPermissionRequest;
3072
+ if (pending?.messageId !== messageId ||
3073
+ pending.delivery.status !== "parked") {
3074
+ return;
3075
+ }
3076
+ const expiresInMs = Math.max(0, pending.delivery.expiresAtMs - Date.now());
3077
+ setTimeout(() => {
3078
+ void (async () => {
3079
+ await inFlightToolPermissionReplay(this.clark);
3080
+ const current = this.clark.context.pendingToolPermissionRequest;
3081
+ if (current?.messageId !== messageId ||
3082
+ current.delivery.status !== "parked") {
3083
+ return;
3084
+ }
3085
+ if (Date.now() < current.delivery.expiresAtMs) {
3086
+ this.scheduleToolPermissionExpiry(messageId);
3087
+ return;
3088
+ }
3089
+ if (!(await this.waitUntilToolPermissionContinuationAllowed(messageId))) {
3090
+ return;
3091
+ }
3092
+ const expired = this.clark.context.pendingToolPermissionRequest;
3093
+ if (expired?.messageId !== messageId ||
3094
+ expired.delivery.status !== "parked") {
3095
+ return;
3096
+ }
3097
+ const error = createExpiredPermissionPromptDeliveryError(expired.toolName, expired.delivery);
3098
+ recordToolPermissionDeliveryOutcome("expired");
3099
+ await this.handleToolPermissionOutcome({
3100
+ kind: "delivery_error",
3101
+ messageId,
3102
+ error,
3103
+ });
3104
+ })().catch((cause) => {
3105
+ this.getLogger().error("[ai-service] Failed to continue after permission delivery expired", getErrorMeta(cause));
3106
+ });
3107
+ }, expiresInMs);
3108
+ }
3049
3109
  /**
3050
- * Handles the user's response to a tool permission request.
3051
- *
3052
- * If approved, immediately executes the tool with the original input,
3053
- * injects the tool call and result into the conversation context,
3054
- * and restarts the LLM with the updated context.
3055
- *
3056
- * If denied, injects a tool error into the context and restarts the LLM.
3110
+ * `userSentPrompt` is only valid from Idle or AwaitingUser. A parked
3111
+ * permission can expire while a sibling tool is still generating; wait
3112
+ * until the continuation can actually run, or abort if the request is gone.
3057
3113
  */
3114
+ async waitUntilToolPermissionContinuationAllowed(messageId) {
3115
+ const TOOL_PERMISSION_CONTINUATION_POLL_MS = 250;
3116
+ for (;;) {
3117
+ const current = this.clark.context.pendingToolPermissionRequest;
3118
+ if (current?.messageId !== messageId ||
3119
+ current.delivery.status !== "parked") {
3120
+ return false;
3121
+ }
3122
+ switch (this.clark.state) {
3123
+ case ClarkStateNames.Idle:
3124
+ case ClarkStateNames.AwaitingUser:
3125
+ return true;
3126
+ case ClarkStateNames.Dead:
3127
+ return false;
3128
+ case ClarkStateNames.AgentPlanning:
3129
+ case ClarkStateNames.LLMGenerating:
3130
+ case ClarkStateNames.PostProcessing:
3131
+ case ClarkStateNames.RuntimeReviewing: {
3132
+ await new Promise((resolve) => {
3133
+ setTimeout(resolve, TOOL_PERMISSION_CONTINUATION_POLL_MS);
3134
+ });
3135
+ break;
3136
+ }
3137
+ default: {
3138
+ const exhaustive = this.clark.state;
3139
+ return exhaustive;
3140
+ }
3141
+ }
3142
+ }
3143
+ }
3144
+ scheduleToolPermissionReplay(messageId) {
3145
+ setTimeout(() => {
3146
+ const pending = this.clark.context.pendingToolPermissionRequest;
3147
+ if (pending?.messageId !== messageId ||
3148
+ pending.delivery.status !== "parked" ||
3149
+ Date.now() >= pending.delivery.expiresAtMs) {
3150
+ return;
3151
+ }
3152
+ const replay = this.stablePeer.isConnected
3153
+ ? flushPendingToolPermissionPrompt(this.clark, { source: "retry" })
3154
+ : Promise.resolve();
3155
+ void replay.finally(() => {
3156
+ const current = this.clark.context.pendingToolPermissionRequest;
3157
+ if (current?.messageId === messageId &&
3158
+ current.delivery.status === "parked") {
3159
+ this.scheduleToolPermissionReplay(messageId);
3160
+ }
3161
+ });
3162
+ }, PERMISSION_PROMPT_DELIVERY_TIMEOUT_MS);
3163
+ }
3058
3164
  async handleAiRespondToToolPermission(request) {
3165
+ return this.handleToolPermissionOutcome({
3166
+ kind: "response",
3167
+ ...request,
3168
+ });
3169
+ }
3170
+ async handleToolPermissionOutcome(request) {
3059
3171
  const logger = this.getLogger();
3060
- logger.info(`[ai-service] handleAiRespondToToolPermission: messageId=${request.messageId}, approved=${request.approved}`);
3172
+ const approved = request.kind === "response" && request.approved;
3173
+ logger.info(`[ai-service] handleAiRespondToToolPermission: messageId=${request.messageId}, approved=${approved}`, { outcomeKind: request.kind });
3061
3174
  // a reconnect can cause the client to re-send this response. If
3062
3175
  // we already applied a response with this requestId, treat the repeat as a
3063
3176
  // no-op so the tool is not executed (or denied) twice for the same user
3064
3177
  // decision. Only a peek here — the id is recorded once the response is
3065
3178
  // accepted below.
3066
- if (request.requestId && this.requestDeduplicator.has(request.requestId)) {
3179
+ if (request.kind === "response" &&
3180
+ request.requestId &&
3181
+ this.requestDeduplicator.has(request.requestId)) {
3067
3182
  logger.info("[ai-service] Ignoring duplicate tool-permission re-send", {
3068
3183
  requestId: request.requestId,
3069
3184
  });
@@ -3083,14 +3198,17 @@ export class AiService extends TracedEventEmitter {
3083
3198
  // and honor the result so a re-send dropped here can't be applied twice. A
3084
3199
  // duplicate that loses the atomic `markIfNew` bails before the tool runs.
3085
3200
  // (APPS-4663)
3086
- if (request.requestId &&
3201
+ if (request.kind === "response" &&
3202
+ request.requestId &&
3087
3203
  !this.requestDeduplicator.markIfNew(request.requestId)) {
3088
3204
  logger.warn("[ai-service] Ignoring duplicate tool-permission re-send (lost dedup race)", { requestId: request.requestId });
3089
3205
  return;
3090
3206
  }
3091
3207
  logger.info(`[ai-service] Clearing pending request`);
3092
3208
  // Clear the pending request and notify UI
3093
- clearPendingToolPermissionRequest(this.clark, "tool-permission-response");
3209
+ clearPendingToolPermissionRequest(this.clark, request.kind === "delivery_error"
3210
+ ? "tool-permission-delivery-expired"
3211
+ : "tool-permission-response");
3094
3212
  const toolCallId = pendingRequest.messageId;
3095
3213
  const toolName = pendingRequest.toolName;
3096
3214
  const rawInput = pendingRequest.rawInput;
@@ -3098,7 +3216,6 @@ export class AiService extends TracedEventEmitter {
3098
3216
  const llmConfig = this.clark.context.llmConfig;
3099
3217
  logger.info(`[ai-service] Getting context toolCallId=${toolCallId})`);
3100
3218
  const sendUserMessage = sendUserMessageChannel(this.clark, this.chatSessionStore);
3101
- // Build the assistant message with the NEW tool call ID
3102
3219
  const assistantMessage = {
3103
3220
  role: "assistant",
3104
3221
  content: [
@@ -3110,9 +3227,8 @@ export class AiService extends TracedEventEmitter {
3110
3227
  },
3111
3228
  ],
3112
3229
  };
3113
- // Build the tool result message based on approval/execution
3114
3230
  let toolResultMessage;
3115
- if (request.approved) {
3231
+ if (approved) {
3116
3232
  // Approve in entity permission store
3117
3233
  this.entityPermissionStore.approveToolCall(toolName, rawInput);
3118
3234
  const signature = getToolCallSignature(toolName, rawInput);
@@ -3128,9 +3244,11 @@ export class AiService extends TracedEventEmitter {
3128
3244
  signature,
3129
3245
  });
3130
3246
  // Store always-allow permissions
3131
- request.alwaysAllowPermissions?.forEach((permission) => {
3132
- void this.entityPermissionStore.approve(permission.entityType, permission.entityId, permission.permissionType);
3133
- });
3247
+ if (request.kind === "response") {
3248
+ request.alwaysAllowPermissions?.forEach((permission) => {
3249
+ void this.entityPermissionStore.approve(permission.entityType, permission.entityId, permission.permissionType);
3250
+ });
3251
+ }
3134
3252
  // Execute the tool with proper access control context from Clark
3135
3253
  const accessControlContext = this.clark.getAccessControl(this.config.features);
3136
3254
  logger.info(`[ai-service] Executing approved tool: ${toolName}`);
@@ -3222,6 +3340,23 @@ export class AiService extends TracedEventEmitter {
3222
3340
  });
3223
3341
  }
3224
3342
  }
3343
+ else if (request.kind === "delivery_error") {
3344
+ toolResultMessage = {
3345
+ role: TOOL_MESSAGE_ROLE,
3346
+ content: [
3347
+ {
3348
+ type: TOOL_RESULT_CONTENT_TYPE,
3349
+ toolCallId,
3350
+ toolName,
3351
+ output: {
3352
+ type: ERROR_JSON_OUTPUT_TYPE,
3353
+ value: { error: request.error.message },
3354
+ },
3355
+ isError: true,
3356
+ },
3357
+ ],
3358
+ };
3359
+ }
3225
3360
  else {
3226
3361
  const denyReason = request.denyMessage || "User denied permission for this tool call";
3227
3362
  if (toolName === "testApi") {
@@ -3296,10 +3431,11 @@ export class AiService extends TracedEventEmitter {
3296
3431
  if (!this.clark.context.llmConfig) {
3297
3432
  throw new Error("Cannot continue after tool permission: llmConfig is missing from context");
3298
3433
  }
3299
- const isClarification = isToolPermissionClarification(request.approved, request.denyMessage);
3434
+ const isClarification = isToolPermissionClarification(approved, request.kind === "response" ? request.denyMessage : undefined);
3300
3435
  logger.info("[ai-service] tool_permission_response continuation", {
3301
- approved: request.approved,
3436
+ approved,
3302
3437
  clarification: isClarification,
3438
+ outcomeKind: request.kind,
3303
3439
  });
3304
3440
  trace
3305
3441
  .getActiveSpan()
@@ -3320,9 +3456,9 @@ export class AiService extends TracedEventEmitter {
3320
3456
  outcomeDedupeKey: promptOutcomeDedupeKey,
3321
3457
  });
3322
3458
  const continueRequest = {
3323
- prompt: request.approved
3459
+ prompt: approved || request.kind === "delivery_error"
3324
3460
  ? ``
3325
- : isClarification
3461
+ : isClarification && request.denyMessage
3326
3462
  ? request.denyMessage.trim()
3327
3463
  : this.buildDenialContinuationPrompt(toolName, rawInput),
3328
3464
  promptContext: {
@@ -3337,8 +3473,13 @@ export class AiService extends TracedEventEmitter {
3337
3473
  responseMetadata: {
3338
3474
  type: "tool_permission_response",
3339
3475
  responseToMessageId: request.messageId,
3340
- approved: request.approved,
3341
- alwaysAllowPermissions: request.alwaysAllowPermissions,
3476
+ approved,
3477
+ outcome: request.kind === "delivery_error"
3478
+ ? "delivery_expired"
3479
+ : "user_response",
3480
+ alwaysAllowPermissions: request.kind === "response"
3481
+ ? request.alwaysAllowPermissions
3482
+ : undefined,
3342
3483
  ...(isClarification ? { clarification: true } : {}),
3343
3484
  },
3344
3485
  };
@@ -3416,9 +3557,9 @@ export class AiService extends TracedEventEmitter {
3416
3557
  * Returned so the caller can be told which turn it now owns; a turn started
3417
3558
  * without one is reachable by this socket alone.
3418
3559
  */
3419
- beginHeadlessTurn(peer, peerId, turnId) {
3560
+ beginHeadlessTurn(peer, peerId, turnId, liveness) {
3420
3561
  this.getLogger().info(`[ai-service] headless turn started by gateway peer ${peerId}`);
3421
- this.stablePeer.beginTurn({ peer, peerId, turnId });
3562
+ this.stablePeer.beginTurn({ liveness, peer, peerId, turnId });
3422
3563
  return turnId;
3423
3564
  }
3424
3565
  /**
@@ -3430,8 +3571,9 @@ export class AiService extends TracedEventEmitter {
3430
3571
  * the caller already owns the turn, or is talking about a turn that is not
3431
3572
  * this one — are silent. Only an actual handover is worth a line.
3432
3573
  */
3433
- claimHeadlessTurn(peer, peerId, turnId) {
3434
- if (this.stablePeer.claimTurn({ peer, peerId, turnId }) !== "claimed") {
3574
+ claimHeadlessTurn(peer, peerId, turnId, liveness) {
3575
+ if (this.stablePeer.claimTurn({ liveness, peer, peerId, turnId }) !==
3576
+ "claimed") {
3435
3577
  return;
3436
3578
  }
3437
3579
  this.getLogger().info(`[ai-service] headless turn moved to reattached gateway peer ${peerId}`);
@@ -3452,14 +3594,19 @@ export class AiService extends TracedEventEmitter {
3452
3594
  * it, so a socket that a reattach already replaced cannot strip routing from
3453
3595
  * its successor.
3454
3596
  */
3455
- endHeadlessTurn(peerId) {
3456
- if (this.stablePeer.turnPeerId !== peerId) {
3597
+ endHeadlessTurn(peerId, turnIdOrLiveness) {
3598
+ const turnId = typeof turnIdOrLiveness === "string" ? turnIdOrLiveness : undefined;
3599
+ const liveness = typeof turnIdOrLiveness === "string" ? undefined : turnIdOrLiveness;
3600
+ if (this.stablePeer.turnPeerId !== peerId ||
3601
+ (turnId !== undefined && this.stablePeer.turnId !== turnId) ||
3602
+ (liveness &&
3603
+ this.stablePeer.turnLivenessGeneration !== liveness.generation)) {
3457
3604
  return;
3458
3605
  }
3459
3606
  this.getLogger().info(`[ai-service] headless turn peer ${peerId} detached; falling back to the editor binding`);
3460
3607
  this.stablePeer.releaseTurnPeer(peerId);
3461
3608
  }
3462
- handleUserConnected(peer, peerId) {
3609
+ handleUserConnected(peer, peerId, liveness) {
3463
3610
  const existingPeerId = this.stablePeer.peerId;
3464
3611
  this.getLogger().info(`[ai-service] handleUserConnected: peerId=${peerId}`);
3465
3612
  if (existingPeerId) {
@@ -3476,7 +3623,7 @@ export class AiService extends TracedEventEmitter {
3476
3623
  // `clark.context.peer` is a permanent StablePeer proxy; we swap its
3477
3624
  // inner (and peerId) here. Any queued calls accumulated while
3478
3625
  // disconnected flush in order as part of setInner.
3479
- this.stablePeer.setInner({ peer, peerId });
3626
+ this.stablePeer.setInner({ liveness, peer, peerId });
3480
3627
  this.getLogger().info(`[ai-service] Peer set: ${peerId}`);
3481
3628
  // A replacement (or freshly reconnecting) peer must re-prove its
3482
3629
  // identity via `notifyAuthenticatedRpc` before live task pushes
@@ -3504,25 +3651,18 @@ export class AiService extends TracedEventEmitter {
3504
3651
  messageId: request.messageId,
3505
3652
  toolName: request.toolName,
3506
3653
  });
3507
- // Note: The promise from the original RPC call will have timed out,
3508
- // but resending allows the user to see and respond to the request.
3509
- // The tool execution will fail, but the agent can retry it naturally.
3510
- void peer.call
3511
- .aiRequestToolPermission({
3512
- messageId: request.messageId,
3513
- toolName: request.toolName,
3514
- actionName: request.actionName ?? undefined,
3515
- formattedInput: request.formattedInput ?? undefined,
3516
- rawInput: request.rawInput,
3517
- permissions: request.permissions ?? [],
3518
- })
3519
- .catch((error) => {
3520
- this.getLogger().error(`[ai-service] Failed to resend tool permission request: ${error}`);
3521
- this.getLogger().warn(`[debug] reconnect: resend pending tool permission failed`, {
3522
- messageId: request.messageId,
3523
- toolName: request.toolName,
3524
- error: error instanceof Error ? error.message : String(error),
3525
- });
3654
+ // replaceInFlight also covers an in-progress first delivery
3655
+ // (awaiting_ack). The replacement takes park/ack ownership so a
3656
+ // miss while the tab is still loading still starts the replay
3657
+ // grace window instead of wedging the session.
3658
+ const needsReplayWindow = request.delivery.status === "acked";
3659
+ void flushPendingToolPermissionPrompt(this.clark, {
3660
+ replaceInFlight: true,
3661
+ source: "reconnect",
3662
+ }).then((outcome) => {
3663
+ if (needsReplayWindow && outcome === "parked") {
3664
+ this.scheduleToolPermissionReplayWindow(request.messageId);
3665
+ }
3526
3666
  });
3527
3667
  }
3528
3668
  else {
@@ -3621,7 +3761,7 @@ export class AiService extends TracedEventEmitter {
3621
3761
  * will never answer — the case a headless build leaves behind when its
3622
3762
  * gateway socket closes over an open editor tab.
3623
3763
  */
3624
- handleUserDisconnected(peerId, replacement) {
3764
+ handleUserDisconnected(peerId, replacement, liveness) {
3625
3765
  const currentPeerId = this.stablePeer.peerId;
3626
3766
  this.getLogger().info(`[ai-service] handleUserDisconnected: peerId=${peerId}`);
3627
3767
  if (!currentPeerId) {
@@ -3631,6 +3771,19 @@ export class AiService extends TracedEventEmitter {
3631
3771
  this.getLogger().info(`[ai-service] Stale disconnect ignored: peer ${peerId} disconnecting but current peer is ${currentPeerId}`);
3632
3772
  return; // Don't clear the current peer on stale disconnect
3633
3773
  }
3774
+ if (liveness &&
3775
+ this.stablePeer.editorLivenessGeneration !== liveness.generation) {
3776
+ this.getLogger().info(`[ai-service] Stale disconnect ignored for replaced connection generation`);
3777
+ return;
3778
+ }
3779
+ if (replacement?.peerId === peerId &&
3780
+ replacement.liveness &&
3781
+ liveness &&
3782
+ replacement.liveness.generation !== liveness.generation) {
3783
+ this.getLogger().info(`[ai-service] Rebinding peer ${peerId} to surviving editor connection`);
3784
+ this.handleUserConnected(replacement.peer, replacement.peerId, replacement.liveness);
3785
+ return;
3786
+ }
3634
3787
  this.getLogger().info(`[ai-service] peer disconnecting: ${peerId}`);
3635
3788
  // Drop any deferred state-push registration for this peer so a
3636
3789
  // disconnect that races a never-arrived auth proof doesn't leave
@@ -3650,11 +3803,11 @@ export class AiService extends TracedEventEmitter {
3650
3803
  if (contextId) {
3651
3804
  this.contextManagerV2.clearContext(contextId);
3652
3805
  }
3653
- if (replacement && replacement.peerId !== peerId) {
3806
+ if (replacement && (replacement.peerId !== peerId || liveness)) {
3654
3807
  this.getLogger().info(`[ai-service] Rebinding peer ${peerId} to surviving editor ${replacement.peerId}`);
3655
3808
  // Same door a fresh tab comes through, so the survivor gets the state
3656
3809
  // resync and re-auth a newly bound peer is owed.
3657
- this.handleUserConnected(replacement.peer, replacement.peerId);
3810
+ this.handleUserConnected(replacement.peer, replacement.peerId, replacement.liveness);
3658
3811
  return;
3659
3812
  }
3660
3813
  this.clearPeerConnection();
@@ -3938,12 +4091,12 @@ export class AiService extends TracedEventEmitter {
3938
4091
  let textDeltaChars = 0;
3939
4092
  let modelName;
3940
4093
  let promptHash;
3941
- // Hoisted so the outer catch can attribute a failed run to the same model
3942
- // and provider as the attempt it belongs to, and can still price whatever
3943
- // tokens the stream consumed before it threw.
4094
+ // Hoisted so the outer catch can attribute a failed run to the same model as
4095
+ // the attempt it belongs to. Usage arrives from the client's usage observer,
4096
+ // which also flushes on abort, on error, and for compaction calls.
3944
4097
  let coreModel;
3945
- let providerName = "unknown";
3946
- let streamResult;
4098
+ const llmUsage = [];
4099
+ let hadFallback = false;
3947
4100
  let attemptRecorded = false;
3948
4101
  const scanRoot = typeof this.appShell.appRootDirPath === "string"
3949
4102
  ? this.appShell.appRootDirPath
@@ -3958,13 +4111,15 @@ export class AiService extends TracedEventEmitter {
3958
4111
  metricsRecorded = true;
3959
4112
  recordPolicyAgentExecution({
3960
4113
  durationMs: Date.now() - startedAt,
4114
+ llmUsage,
3961
4115
  outcome,
3962
4116
  stepCount,
3963
4117
  toolCallCount: totalToolCalls(),
3964
4118
  });
3965
4119
  };
3966
4120
  const buildRuntimeMetadata = (finalFailureReason) => ({
3967
- diagnosticsVersion: 2,
4121
+ diagnosticsVersion: 3,
4122
+ diffFiles: request.scanScopeJson?.changedFiles ?? [],
3968
4123
  durationMs: Date.now() - startedAt,
3969
4124
  filePathsListed: Array.from(filePathsListed).slice(0, 100),
3970
4125
  filePathsRead: Array.from(filePathsRead).slice(0, 100),
@@ -4001,8 +4156,8 @@ export class AiService extends TracedEventEmitter {
4001
4156
  trigger_type: "policy_gate",
4002
4157
  }));
4003
4158
  const { model, providerConfig } = llmProvider.modelForTask("generateBroadEdit");
4159
+ const { model: summarizationModel, providerConfig: summarizationProviderConfig, } = llmProvider.modelForTask("summarizeMessages");
4004
4160
  modelName = typeof model === "string" ? model : model.modelId;
4005
- providerName = providerConfig?.provider ?? "unknown";
4006
4161
  coreModel = llmConfig?.coreModel;
4007
4162
  // Counted as soon as the model resolves, because everything below is
4008
4163
  // inside this try and so is guaranteed a terminal outcome from the catch.
@@ -4015,37 +4170,105 @@ export class AiService extends TracedEventEmitter {
4015
4170
  name: i.name,
4016
4171
  pluginType: i.plugin?.id ?? "unknown",
4017
4172
  }));
4173
+ let scanScopeJson = request.scanScopeJson;
4174
+ if (scanScopeJson?.unifiedDiff) {
4175
+ try {
4176
+ const scanResult = await scanContentForSecrets(scanScopeJson.unifiedDiff);
4177
+ if (scanResult.success && scanResult.hasSecrets) {
4178
+ scanScopeJson = {
4179
+ ...scanScopeJson,
4180
+ unifiedDiff: new SecretRedactor(scanResult.findings).redact(scanScopeJson.unifiedDiff),
4181
+ };
4182
+ }
4183
+ else if (!scanResult.success) {
4184
+ logger.warn("[secret-scan] Secret scan failed on policy gate diff; using the original diff");
4185
+ }
4186
+ }
4187
+ catch {
4188
+ logger.warn("[secret-scan] Secret scan threw on policy gate diff; using the original diff");
4189
+ }
4190
+ }
4018
4191
  const appContext = { integrations };
4019
- const systemPrompt = composeSecurityAgentPrompt(appContext, request.adminPrompt);
4192
+ const systemPrompt = composeSecurityAgentPrompt(appContext, request.adminPrompt, scanScopeJson);
4020
4193
  promptHash = createHash("sha256").update(systemPrompt).digest("hex");
4194
+ const scanContext = {};
4195
+ const { headers, interleavedThinking, maxOutputTokens, providerOptions, thinkingBudgetTokens, thinkingEnabled, } = processLLMConfig(llmConfig);
4196
+ // Scan-owned client: gate JWT, own usage sink, built first so scan tools
4197
+ // (webFetch generateObject) use this client instead of Clark's.
4198
+ const policyGateLlmClient = new LLMClient({
4199
+ appShell: this.appShell,
4200
+ applicationId: request.applicationId,
4201
+ contextManagerV2: this.contextManagerV2,
4202
+ getJwt: () => this.requireProvidedInferenceJwt(request.jwt, "policy gate"),
4203
+ onUsageRecorded: (usage) => {
4204
+ if (usage.hadProviderFallback === true) {
4205
+ hadFallback = true;
4206
+ }
4207
+ llmUsage.push(...usage.promptCostUsageEntries);
4208
+ },
4209
+ organizationId: this.config.organizationId,
4210
+ stepTokenUsageLabels: () => ({
4211
+ coreModel,
4212
+ mode: "build",
4213
+ surface: POLICY_GATE_USAGE_METADATA.surface,
4214
+ }),
4215
+ retryOptions: streamRetryOptionsFromConfig(this.config.retryOptions, this.config.features.streamTimeoutBudgets),
4216
+ unifiedProviderEnabled: this.config.features.unifiedProviderEnabled ?? false,
4217
+ });
4021
4218
  const scanToolsDeps = {
4022
4219
  appShell: this.appShell,
4023
4220
  features: this.config.features ?? {},
4024
4221
  integrationStore: this.integrationStore,
4025
- llmClient: this.llmClient,
4222
+ llmClient: policyGateLlmClient,
4026
4223
  llmProvider: llmProvider,
4224
+ storeSummary: (toolCallId, summary) => {
4225
+ scanContext.current?.storeSummary(toolCallId, summary);
4226
+ },
4027
4227
  };
4028
4228
  const tools = await buildSecurityScanTools(this.clark, scanToolsDeps);
4029
- const result = aiStreamText({
4030
- messages: [
4031
- { content: systemPrompt, role: "system" },
4032
- {
4033
- content: "Begin your security review of this application now.",
4034
- role: "user",
4229
+ const result = await policyGateLlmClient.streamText({
4230
+ abortSignal: AbortSignal.timeout(POLICY_GATE_MAX_TOTAL_TIME_MS),
4231
+ context: {
4232
+ contextManagement: {
4233
+ summarizationModel,
4234
+ summarizationProviderConfig,
4235
+ type: "client",
4035
4236
  },
4036
- ],
4037
- // Same output cap Clark uses (see processLLMConfig). Without it the
4038
- // provider default applies, which is model-dependent.
4039
- maxOutputTokens: MAX_OUTPUT_TOKENS,
4237
+ // Product defaults: caller llmConfig cannot shrink the scan window to 0.
4238
+ contextOptionsV2: resolveContextOptionsV2(),
4239
+ },
4240
+ enableFallback: llmConfig?.unifiedProviderEnabled ?? false,
4241
+ fallbackStrategy: llmConfig?.fallbackStrategy,
4242
+ headers,
4243
+ logging: false,
4244
+ // processLLMConfig returns the same MAX_OUTPUT_TOKENS cap Clark uses.
4245
+ maxOutputTokens,
4040
4246
  model,
4041
- // The unified router resolves the model from the request, falling back
4042
- // to Clark's core-model flag when absent.
4043
- ...(coreModel ? { providerOptions: { unified: { coreModel } } } : {}),
4247
+ onContextReady: (ctx) => {
4248
+ scanContext.current = ctx;
4249
+ },
4250
+ providerConfig,
4251
+ providerOptions: {
4252
+ ...providerOptions,
4253
+ ...(coreModel ? { unified: { coreModel } } : {}),
4254
+ },
4255
+ recordObsContent: false,
4256
+ system: systemPrompt,
4257
+ thinking: {
4258
+ budgetTokens: thinkingBudgetTokens,
4259
+ enabled: thinkingEnabled,
4260
+ interleaved: interleavedThinking,
4261
+ },
4044
4262
  tools,
4045
- stopWhen: hasToolCall("reportReviewRun"),
4046
- timeout: { totalMs: POLICY_GATE_MAX_TOTAL_TIME_MS },
4263
+ stopWhen: [
4264
+ hasToolCall("reportReviewRun"),
4265
+ stepCountIs(POLICY_GATE_MAX_STEPS),
4266
+ ],
4267
+ user: {
4268
+ content: "Begin your security review of this application now.",
4269
+ role: "user",
4270
+ },
4047
4271
  });
4048
- streamResult = result;
4049
4272
  // Best-effort: surface the agent's latest tool/reasoning step in the editor
4050
4273
  // so a running policy row shows live activity instead of an opaque spinner.
4051
4274
  // Only meaningful when an editor session is connected; never blocks the run.
@@ -4076,16 +4299,6 @@ export class AiService extends TracedEventEmitter {
4076
4299
  : "unknown";
4077
4300
  streamPartTypeCounts[partType] =
4078
4301
  (streamPartTypeCounts[partType] ?? 0) + 1;
4079
- if (partType === "error") {
4080
- const streamError = serializePolicyGateStreamError(part.error, part);
4081
- if (streamErrors.length < MAX_POLICY_GATE_STREAM_ERRORS) {
4082
- streamErrors.push(streamError);
4083
- }
4084
- logger.warn("[ai-service] Policy gate stream error", {
4085
- ...policyRunLogContext,
4086
- streamError,
4087
- });
4088
- }
4089
4302
  const textDelta = part.textDelta ??
4090
4303
  part.text ??
4091
4304
  part.delta;
@@ -4198,7 +4411,9 @@ export class AiService extends TracedEventEmitter {
4198
4411
  }
4199
4412
  catch (error) {
4200
4413
  if (error instanceof Error &&
4201
- (error.name === "AbortError" || error.name === "TimeoutError")) {
4414
+ (error.name === "AbortError" ||
4415
+ error.name === "AbortedError" ||
4416
+ error.name === "TimeoutError")) {
4202
4417
  const elapsedMs = Date.now() - startedAt;
4203
4418
  const isBudgetTimeout = error.name === "TimeoutError" ||
4204
4419
  elapsedMs + POLICY_GATE_TIMEOUT_CLASSIFICATION_SLACK_MS >=
@@ -4218,6 +4433,18 @@ export class AiService extends TracedEventEmitter {
4218
4433
  });
4219
4434
  failPolicyGateExecution("Policy gate agent aborted", "aborted");
4220
4435
  }
4436
+ if (error instanceof Error &&
4437
+ (error.name === "LLMProviderError" ||
4438
+ error.name === "RetriesExhaustedError")) {
4439
+ const streamError = serializePolicyGateStreamError(error);
4440
+ if (streamErrors.length < MAX_POLICY_GATE_STREAM_ERRORS) {
4441
+ streamErrors.push(streamError);
4442
+ }
4443
+ logger.warn("[ai-service] Policy gate stream error", {
4444
+ ...policyRunLogContext,
4445
+ streamError,
4446
+ });
4447
+ }
4221
4448
  throw error;
4222
4449
  }
4223
4450
  if (!scanResult) {
@@ -4239,11 +4466,8 @@ export class AiService extends TracedEventEmitter {
4239
4466
  aiAgentRunId: request.aiAgentRunId,
4240
4467
  chatSessionStore: this.chatSessionStore,
4241
4468
  coreModel,
4242
- llmUsage: await collectPolicyGateLlmUsage({
4243
- modelName: modelName ?? "unknown",
4244
- providerName,
4245
- result,
4246
- }),
4469
+ hadFallback,
4470
+ llmUsage,
4247
4471
  logger,
4248
4472
  outcome: "success",
4249
4473
  });
@@ -4266,13 +4490,8 @@ export class AiService extends TracedEventEmitter {
4266
4490
  chatSessionStore: this.chatSessionStore,
4267
4491
  coreModel,
4268
4492
  failure: policyGateFailureFromStream(streamErrors),
4269
- llmUsage: streamResult
4270
- ? await collectPolicyGateLlmUsage({
4271
- modelName: modelName ?? "unknown",
4272
- providerName,
4273
- result: streamResult,
4274
- })
4275
- : [],
4493
+ hadFallback,
4494
+ llmUsage,
4276
4495
  logger,
4277
4496
  outcome: "failure",
4278
4497
  });