@gajae-code/coding-agent 0.15.5 → 0.16.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 (272) hide show
  1. package/CHANGELOG.md +85 -1
  2. package/dist/types/async/job-manager.d.ts +83 -0
  3. package/dist/types/cli/args.d.ts +2 -0
  4. package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
  5. package/dist/types/cli/model-presets-cli.d.ts +9 -0
  6. package/dist/types/cli/root-flags.d.ts +14 -0
  7. package/dist/types/cli/update-cli.d.ts +15 -0
  8. package/dist/types/cli.d.ts +7 -0
  9. package/dist/types/commands/auth-gateway.d.ts +3 -0
  10. package/dist/types/commands/gc.d.ts +13 -0
  11. package/dist/types/commands/launch.d.ts +10 -0
  12. package/dist/types/commands/model-presets.d.ts +21 -0
  13. package/dist/types/config/autorouting-tier-map.d.ts +120 -0
  14. package/dist/types/config/file-lock.d.ts +54 -2
  15. package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
  16. package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
  17. package/dist/types/config/keybindings.d.ts +3 -2
  18. package/dist/types/config/model-preset-registry.d.ts +368 -0
  19. package/dist/types/config/model-profile-activation.d.ts +39 -0
  20. package/dist/types/config/model-profiles.d.ts +5 -3
  21. package/dist/types/config/model-registry.d.ts +17 -5
  22. package/dist/types/config/settings-schema.d.ts +19 -1
  23. package/dist/types/config/settings.d.ts +2 -0
  24. package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
  25. package/dist/types/coordinator-mcp/projection-scan.d.ts +54 -0
  26. package/dist/types/crash/record-loader.d.ts +15 -0
  27. package/dist/types/extensibility/extensions/types.d.ts +2 -5
  28. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
  29. package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
  30. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  31. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  32. package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
  33. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +72 -1
  34. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  35. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  36. package/dist/types/index.d.ts +1 -0
  37. package/dist/types/main.d.ts +3 -1
  38. package/dist/types/master-mode/context.d.ts +21 -0
  39. package/dist/types/master-mode/first-request.d.ts +34 -0
  40. package/dist/types/master-mode/snapshot.d.ts +32 -0
  41. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  42. package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
  43. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  44. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  45. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  46. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  47. package/dist/types/modes/jobs-observer.d.ts +36 -0
  48. package/dist/types/modes/print-mode.d.ts +17 -0
  49. package/dist/types/modes/ui-language.d.ts +7 -1
  50. package/dist/types/sdk/broker/broker.d.ts +80 -1
  51. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  52. package/dist/types/sdk/broker/index.d.ts +3 -0
  53. package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
  54. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  55. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  56. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  57. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  58. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  59. package/dist/types/sdk/broker/state-version.d.ts +4 -1
  60. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  61. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  62. package/dist/types/sdk/bus/index.d.ts +6 -0
  63. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
  64. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
  65. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  66. package/dist/types/sdk/cli/index.d.ts +1 -0
  67. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  68. package/dist/types/sdk/cli/rows.d.ts +4 -3
  69. package/dist/types/sdk/cli/session-cli.d.ts +18 -2
  70. package/dist/types/sdk/client/client.d.ts +3 -2
  71. package/dist/types/sdk/client/discovery.d.ts +1 -0
  72. package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
  73. package/dist/types/sdk/host/control/index.d.ts +1 -1
  74. package/dist/types/sdk/host/control/operations.d.ts +2 -0
  75. package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
  76. package/dist/types/sdk/host/host.d.ts +10 -0
  77. package/dist/types/sdk/host/model-pin.d.ts +7 -2
  78. package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
  79. package/dist/types/sdk/host/session-runtime.d.ts +67 -1
  80. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  81. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  82. package/dist/types/sdk/lifecycle-session.d.ts +2 -0
  83. package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
  84. package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
  85. package/dist/types/sdk/router/session-router.d.ts +11 -0
  86. package/dist/types/sdk/session.d.ts +16 -0
  87. package/dist/types/sdk/startup-capability.d.ts +2 -1
  88. package/dist/types/session/account-inventory.d.ts +1 -0
  89. package/dist/types/session/agent-session.d.ts +24 -7
  90. package/dist/types/session/credential-store-errors.d.ts +1 -0
  91. package/dist/types/session/fold-coordinator.d.ts +201 -0
  92. package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
  93. package/dist/types/session/sdk-run-capability.d.ts +2 -0
  94. package/dist/types/session/session-manager.d.ts +65 -0
  95. package/dist/types/session/yield-queue.d.ts +5 -4
  96. package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
  97. package/dist/types/telemetry/control.d.ts +11 -0
  98. package/dist/types/telemetry/events.d.ts +20 -0
  99. package/dist/types/telemetry/index.d.ts +3 -0
  100. package/dist/types/telemetry/transport.d.ts +22 -0
  101. package/dist/types/tools/bash-interactive.d.ts +23 -0
  102. package/dist/types/tools/bash.d.ts +4 -0
  103. package/dist/types/tools/gh.d.ts +8 -0
  104. package/dist/types/tools/index.d.ts +10 -5
  105. package/dist/types/web/search/providers/base.d.ts +9 -4
  106. package/dist/types/web/search/providers/xai.d.ts +2 -1
  107. package/dist/types/web/search/types.d.ts +14 -0
  108. package/package.json +10 -7
  109. package/scripts/generate-hotkeys-docs.ts +4 -0
  110. package/scripts/generate-sdk-operation-inventory.ts +9 -3
  111. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  112. package/src/async/job-manager.ts +671 -39
  113. package/src/cli/accounts-cli.ts +9 -5
  114. package/src/cli/args.ts +17 -0
  115. package/src/cli/auth-broker-cli.ts +40 -12
  116. package/src/cli/auth-gateway-cli.ts +273 -97
  117. package/src/cli/config-cli.ts +2 -2
  118. package/src/cli/fast-help.ts +4 -0
  119. package/src/cli/model-presets-cli.ts +100 -0
  120. package/src/cli/root-flags.ts +5 -0
  121. package/src/cli/update-cli.ts +117 -16
  122. package/src/cli.ts +6 -2
  123. package/src/commands/auth-gateway.ts +12 -8
  124. package/src/commands/gc.ts +19 -0
  125. package/src/commands/launch.ts +86 -18
  126. package/src/commands/model-presets.ts +24 -0
  127. package/src/commands/sdk.ts +113 -21
  128. package/src/config/autorouting-tier-map.ts +44 -0
  129. package/src/config/file-lock-gc.ts +9 -4
  130. package/src/config/file-lock.ts +890 -99
  131. package/src/config/internal/model-preset-registry-test-state.ts +28 -0
  132. package/src/config/internal/model-preset-registry-test-support.ts +11 -0
  133. package/src/config/keybindings.ts +6 -2
  134. package/src/config/model-preset-registry.ts +2795 -0
  135. package/src/config/model-profile-activation.ts +115 -28
  136. package/src/config/model-profile-contract.ts +4 -1
  137. package/src/config/model-profiles.ts +56 -14
  138. package/src/config/model-registry.ts +1044 -122
  139. package/src/config/model-resolver.ts +5 -0
  140. package/src/config/settings-schema.ts +19 -1
  141. package/src/config/settings.ts +11 -1
  142. package/src/coordinator-mcp/model-preset.ts +32 -17
  143. package/src/coordinator-mcp/projection-scan.ts +592 -0
  144. package/src/coordinator-mcp/question-state.ts +9 -11
  145. package/src/coordinator-mcp/server.ts +111 -42
  146. package/src/coordinator-mcp/session-reaper.ts +9 -3
  147. package/src/crash/index-store.ts +2 -0
  148. package/src/crash/record-loader.ts +10 -0
  149. package/src/extensibility/extensions/loader.ts +0 -1
  150. package/src/extensibility/extensions/types.ts +2 -3
  151. package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
  152. package/src/gjc-runtime/empty-delete-gc.ts +301 -0
  153. package/src/gjc-runtime/gc-render.ts +29 -0
  154. package/src/gjc-runtime/gc-runtime.ts +174 -11
  155. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  156. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  157. package/src/gjc-runtime/session-state-lock.ts +1321 -147
  158. package/src/gjc-runtime/session-state-sidecar.ts +1004 -25
  159. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  160. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
  161. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  162. package/src/hooks/skill-state.ts +6 -0
  163. package/src/index.ts +1 -0
  164. package/src/internal-urls/docs-index.generated.ts +14 -13
  165. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  166. package/src/lsp/config.ts +4 -3
  167. package/src/lsp/path-trust.ts +12 -6
  168. package/src/main.ts +66 -4
  169. package/src/master-mode/context.ts +50 -0
  170. package/src/master-mode/first-request.ts +65 -0
  171. package/src/master-mode/snapshot.ts +205 -0
  172. package/src/modes/acp/acp-agent.ts +616 -161
  173. package/src/modes/components/footer.ts +4 -2
  174. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  175. package/src/modes/components/jobs-overlay-model.ts +99 -16
  176. package/src/modes/components/model-selector.ts +158 -18
  177. package/src/modes/components/oauth-selector.ts +6 -1
  178. package/src/modes/components/settings-selector.ts +1 -0
  179. package/src/modes/components/status-line/model-name.ts +53 -0
  180. package/src/modes/components/status-line/priority-row.ts +107 -0
  181. package/src/modes/components/status-line/segments.ts +29 -10
  182. package/src/modes/components/tool-status-header.ts +151 -15
  183. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  184. package/src/modes/controllers/input-controller.ts +51 -6
  185. package/src/modes/controllers/selector-controller.ts +191 -96
  186. package/src/modes/interactive-mode.ts +14 -1
  187. package/src/modes/jobs-observer.ts +149 -16
  188. package/src/modes/print-mode.ts +64 -0
  189. package/src/modes/runtime-init.ts +1 -1
  190. package/src/modes/ui-language.ts +49 -2
  191. package/src/modes/utils/injected-user-submission.ts +15 -1
  192. package/src/prompts/system/master-mode.md +28 -0
  193. package/src/prompts/tools/bash.md +2 -2
  194. package/src/prompts/tools/fold-receipt.md +3 -0
  195. package/src/prompts/tools/job.md +1 -1
  196. package/src/sdk/acp/adapter.ts +17 -2
  197. package/src/sdk/broker/broker.ts +1671 -34
  198. package/src/sdk/broker/endpoint-authority.ts +86 -0
  199. package/src/sdk/broker/ensure.ts +5 -0
  200. package/src/sdk/broker/index.ts +3 -0
  201. package/src/sdk/broker/lifecycle.ts +217 -46
  202. package/src/sdk/broker/master-capability.ts +232 -0
  203. package/src/sdk/broker/session-index.ts +121 -17
  204. package/src/sdk/broker/session-scope.ts +272 -0
  205. package/src/sdk/broker/spawn-authority.ts +1031 -0
  206. package/src/sdk/broker/spawn-substrate.ts +548 -0
  207. package/src/sdk/broker/state-version.ts +26 -14
  208. package/src/sdk/broker/transport.ts +1 -0
  209. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  210. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  211. package/src/sdk/bus/chat-daemon-runtime.ts +46 -5
  212. package/src/sdk/bus/index.ts +51 -4
  213. package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
  214. package/src/sdk/bus/prompt-reconciliation.ts +28 -4
  215. package/src/sdk/bus/slack-conversation.ts +5 -1
  216. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  217. package/src/sdk/cli/index.ts +1 -0
  218. package/src/sdk/cli/master-cli.ts +259 -0
  219. package/src/sdk/cli/rows.ts +11 -8
  220. package/src/sdk/cli/session-cli.ts +276 -6
  221. package/src/sdk/client/client.ts +14 -8
  222. package/src/sdk/client/discovery.ts +3 -1
  223. package/src/sdk/host/control/dispatch.ts +101 -37
  224. package/src/sdk/host/control/index.ts +2 -0
  225. package/src/sdk/host/control/operations.ts +2 -0
  226. package/src/sdk/host/control/runtime-gate.ts +203 -11
  227. package/src/sdk/host/host.ts +27 -2
  228. package/src/sdk/host/model-pin.ts +185 -25
  229. package/src/sdk/host/sdk-run-capability.ts +1 -0
  230. package/src/sdk/host/session-runtime.test.ts +581 -43
  231. package/src/sdk/host/session-runtime.ts +655 -101
  232. package/src/sdk/lifecycle/broker-client.ts +28 -0
  233. package/src/sdk/lifecycle/service.ts +279 -20
  234. package/src/sdk/lifecycle-session.ts +10 -2
  235. package/src/sdk/prompt-deadline-manager.ts +16 -1
  236. package/src/sdk/protocol/adapter-validation.ts +27 -0
  237. package/src/sdk/protocol/operation-inventory.generated.json +53 -2
  238. package/src/sdk/protocol/operation-registry.ts +17 -4
  239. package/src/sdk/router/session-router.ts +294 -154
  240. package/src/sdk/session.ts +440 -167
  241. package/src/sdk/startup-capability.ts +1 -0
  242. package/src/session/account-inventory.ts +27 -11
  243. package/src/session/agent-session.ts +1292 -180
  244. package/src/session/blob-store.ts +15 -5
  245. package/src/session/credential-store-errors.ts +2 -0
  246. package/src/session/fold-coordinator.ts +433 -0
  247. package/src/session/internal/managed-session-storage.ts +153 -78
  248. package/src/session/sdk-run-capability-internal.ts +15 -0
  249. package/src/session/sdk-run-capability.ts +2 -0
  250. package/src/session/session-manager.ts +342 -28
  251. package/src/session/yield-queue.ts +24 -10
  252. package/src/setup/frictionless-onboarding.ts +2 -0
  253. package/src/setup/provider-presets.json +1 -1
  254. package/src/slash-commands/helpers/credential-switch.ts +6 -1
  255. package/src/task/executor.ts +19 -3
  256. package/src/telemetry/control.ts +26 -0
  257. package/src/telemetry/events.ts +1268 -0
  258. package/src/telemetry/index.ts +3 -0
  259. package/src/telemetry/transport.ts +87 -0
  260. package/src/tools/bash-interactive.ts +182 -85
  261. package/src/tools/bash.ts +1028 -109
  262. package/src/tools/gh.ts +104 -48
  263. package/src/tools/github-cache.ts +2 -2
  264. package/src/tools/index.ts +11 -5
  265. package/src/tools/tool-catalog.generated.ts +1 -1
  266. package/src/web/search/provider.ts +120 -11
  267. package/src/web/search/providers/anthropic.ts +89 -17
  268. package/src/web/search/providers/base.ts +9 -4
  269. package/src/web/search/providers/gemini.ts +87 -15
  270. package/src/web/search/providers/openai-compatible.ts +23 -8
  271. package/src/web/search/providers/xai.ts +98 -12
  272. package/src/web/search/types.ts +16 -0
@@ -117,6 +117,7 @@ import {
117
117
  modelsAreEqual,
118
118
  streamSimple,
119
119
  } from "@gajae-code/ai/core";
120
+ import { normalizeAnthropicBaseUrl } from "@gajae-code/ai/providers/anthropic";
120
121
  import {
121
122
  type AuthDisposition,
122
123
  beginAttempt,
@@ -181,8 +182,10 @@ export interface ForkContextSeedOptions {
181
182
  }
182
183
 
183
184
  import type { AuthCredentialSelector } from "@gajae-code/ai/core";
185
+ import { isFoundryEnabled } from "@gajae-code/ai/utils/foundry";
184
186
  import type { MacOSPowerAssertion } from "@gajae-code/natives";
185
187
  import {
188
+ $pickCredentialEnv,
186
189
  extractRetryHint,
187
190
  hasFsCode,
188
191
  isEacces,
@@ -312,9 +315,16 @@ import {
312
315
  } from "../gjc-runtime/session-layout";
313
316
  import {
314
317
  type CoordinatorToolObservation,
318
+ clearCoordinatorRuntimeStateRescope,
319
+ hasCoordinatorRuntimeStateRescopeJournal,
320
+ markCoordinatorRuntimeStateRescopePublishing,
315
321
  ownerTerminalContextFromEnvironment,
316
322
  persistCoordinatorRuntimeStateFromEvent,
323
+ persistCoordinatorWorkerIntegrationOutcome,
324
+ prepareCoordinatorRuntimeStateRescope,
325
+ recoverCoordinatorRuntimeStateRescope,
317
326
  registerCoordinatorRuntimeStateFinalizer,
327
+ relocateCoordinatorRuntimeStateForRescope,
318
328
  UNPROVEN_TOOL_LABEL,
319
329
  } from "../gjc-runtime/session-state-sidecar";
320
330
  import {
@@ -443,6 +453,7 @@ import {
443
453
  } from "./contribution-prep";
444
454
  import { canonicalCoordinatorToolLabel } from "./coordinator-tool-label";
445
455
  import { pruneStaleFileMentions } from "./file-mention-pruning";
456
+ import { describeFoldReceipt, FOLD_WAKE_MERGE_WINDOW_MS, type FoldAdapter, FoldCoordinator } from "./fold-coordinator";
446
457
  import type { MemoryGuardRestoreResult } from "./memory-guard-checkpoint-participant";
447
458
  import {
448
459
  type BashExecutionMessage,
@@ -457,6 +468,7 @@ import {
457
468
  SKILL_PROMPT_MESSAGE_TYPE,
458
469
  } from "./messages";
459
470
  import { isLegacyProviderSafetyStopMessage } from "./provider-safety-stop";
471
+ import { readSdkRunCapability } from "./sdk-run-capability";
460
472
  import { formatSessionDumpText } from "./session-dump-format";
461
473
  import type {
462
474
  BranchSummaryEntry,
@@ -471,7 +483,6 @@ import type {
471
483
  SessionManagerCloseOutcome,
472
484
  SessionMemoryStats,
473
485
  } from "./session-manager";
474
-
475
486
  import {
476
487
  createReadonlySessionManager,
477
488
  getLatestCompactionEntry,
@@ -488,6 +499,8 @@ import {
488
499
  bindToolLineage,
489
500
  classifyOwnedEnvelope,
490
501
  isOwnedCompletionEnvelope,
502
+ isOwnedCompletionEnvelopeAllowed,
503
+ lookupOwnedRegistration,
491
504
  lookupTerminalScope,
492
505
  mintTurnLineageIdHash,
493
506
  type OwnedCompletionEnvelope,
@@ -496,11 +509,35 @@ import {
496
509
  settleToolLineageRegistrationWindow,
497
510
  unregisterOwnedRegistration,
498
511
  } from "./terminal-abort";
499
-
500
512
  import { ToolChoiceQueue } from "./tool-choice-queue";
501
513
  import { pruneSupersededMaintenanceReminders, pruneSupersededVolatileProjectContext } from "./volatile-context-pruning";
502
514
  import { YieldQueue } from "./yield-queue";
503
515
 
516
+ const ASYNC_INLINE_RESULT_MAX_CHARS = 12_000;
517
+ const ASYNC_PREVIEW_MAX_CHARS = 4_000;
518
+
519
+ async function formatParkedAsyncResult(
520
+ sessionManager: SessionManager,
521
+ result: string,
522
+ allowArtifact = true,
523
+ ): Promise<string> {
524
+ if (result.length <= ASYNC_INLINE_RESULT_MAX_CHARS) return result;
525
+ const preview = `${result.slice(0, ASYNC_PREVIEW_MAX_CHARS)}\n\n[Output truncated. Showing first ${ASYNC_PREVIEW_MAX_CHARS.toLocaleString()} characters.]`;
526
+ try {
527
+ if (!allowArtifact) return preview;
528
+ const { path: artifactPath, id: artifactId } = await sessionManager.allocateArtifactPath("async");
529
+ if (artifactPath && artifactId) {
530
+ await Bun.write(artifactPath, result);
531
+ return `${preview}\nFull output: artifact://${artifactId}`;
532
+ }
533
+ } catch (error) {
534
+ logger.warn("Failed to persist parked async follow-up artifact", {
535
+ error: error instanceof Error ? error.message : String(error),
536
+ });
537
+ }
538
+ return preview;
539
+ }
540
+
504
541
  /**
505
542
  * #4560: structured workflow recovery projection from canonical durable
506
543
  * Ralplan/Ultragoal state, consumed by the compaction summary context and
@@ -1048,10 +1085,23 @@ export interface PromptOptions {
1048
1085
  onSkillPrepared?: (meta: { name: string; path: string; lineCount?: number; cleanedArgs?: string }) => void;
1049
1086
  /** Optional invocation-scoped cancellation fence used before an accepted skill starts execution. */
1050
1087
  preflightSignal?: AbortSignal;
1051
- /** Internal SDK lifecycle owner token. */
1052
- sdkRunToken?: string;
1088
+ /** Internal host capability; ordinary package callers cannot construct the branded value. */
1089
+ sdkRunCapability?: unknown;
1053
1090
  }
1054
1091
 
1092
+ type InternalPromptOptions = PromptOptions & { sdkRunToken?: string };
1093
+ type InternalCustomMessageOptions = Pick<
1094
+ PromptOptions,
1095
+ | "streamingBehavior"
1096
+ | "toolChoice"
1097
+ | "followUpQueuePolicy"
1098
+ | "onPreflightAccepted"
1099
+ | "onPreflightAcceptCommit"
1100
+ | "preflightSignal"
1101
+ > & {
1102
+ sdkRunToken?: string;
1103
+ };
1104
+
1055
1105
  function promptPreflightCancelledError(): Error {
1056
1106
  const error = Object.assign(new Error("Prompt preflight was cancelled before execution."), { code: "busy" });
1057
1107
  error.name = "PromptPreflightCancelledError";
@@ -1626,14 +1676,44 @@ function buildSessionMetadata(
1626
1676
  provider: string,
1627
1677
  authStorage: AuthStorage | undefined,
1628
1678
  credentialSessionId = sessionId,
1679
+ model?: Model,
1680
+ owner?: object,
1629
1681
  ): Record<string, unknown> {
1630
1682
  const userId: Record<string, string> = { session_id: sessionId };
1631
1683
  // Only look up account_uuid when the request is going to Anthropic. Injecting
1632
1684
  // a Anthropic model OAuth account_uuid into requests bound for other providers (including
1633
1685
  // Anthropic-format-compatible proxies like cloudflare-ai-gateway or gitlab-duo)
1634
1686
  // would leak the user's Anthropic identity to unrelated third-party APIs.
1635
- if (provider === "anthropic") {
1636
- const accountUuid = authStorage?.getOAuthAccountId("anthropic", credentialSessionId);
1687
+ const effectiveBaseUrl =
1688
+ model?.provider === "anthropic"
1689
+ ? (normalizeAnthropicBaseUrl(
1690
+ (isFoundryEnabled() ? $pickCredentialEnv("FOUNDRY_BASE_URL") : undefined) ?? model.baseUrl,
1691
+ ) ?? "https://api.anthropic.com")
1692
+ : undefined;
1693
+ let officialAnthropicEndpoint = false;
1694
+ if (provider === "anthropic" && model?.api === "anthropic-messages") {
1695
+ try {
1696
+ const candidate = effectiveBaseUrl ?? "https://api.anthropic.com";
1697
+ const url = new URL(candidate);
1698
+ const authority = candidate.match(/^[a-z][a-z\d+.-]*:\/\/([^/?#]*)/iu)?.[1];
1699
+ officialAnthropicEndpoint =
1700
+ url.protocol === "https:" &&
1701
+ url.hostname === "api.anthropic.com" &&
1702
+ authority?.toLowerCase() === "api.anthropic.com" &&
1703
+ !url.username &&
1704
+ !url.password &&
1705
+ !url.search &&
1706
+ !url.hash &&
1707
+ (url.pathname === "/" || url.pathname === "/v1" || url.pathname === "/v1/");
1708
+ } catch {
1709
+ officialAnthropicEndpoint = false;
1710
+ }
1711
+ }
1712
+ // Without the registry owner, do not query shared auth storage: an unscoped
1713
+ // lookup can attribute a registry-scoped API-key request to another OAuth
1714
+ // account. Compatibility facades therefore retain session_id-only metadata.
1715
+ if (officialAnthropicEndpoint && owner) {
1716
+ const accountUuid = authStorage?.getOAuthAccountId("anthropic", credentialSessionId, { owner });
1637
1717
  if (typeof accountUuid === "string" && accountUuid.length > 0) {
1638
1718
  userId.account_uuid = accountUuid;
1639
1719
  // Derive device_id from account_uuid so the payload matches the real CC
@@ -1914,9 +1994,24 @@ export type BeforeAgentStartContributor = (event: {
1914
1994
  }) => Promise<BeforeAgentStartInternalMessage | undefined>;
1915
1995
 
1916
1996
  const AGENT_END_WORKER_INTEGRATION_TIMEOUT_MS = 5_000;
1997
+ const POST_PUBLICATION_ERROR_MAX_BYTES = 512;
1998
+
1999
+ export type WorkerIntegrationOutcome =
2000
+ | { status: "completed" }
2001
+ | { status: "failed"; error: string }
2002
+ | { status: "timed_out" };
2003
+
2004
+ function sanitizePostPublicationError(error: string | undefined): string | undefined {
2005
+ if (error === undefined) return undefined;
2006
+ const normalized = error
2007
+ .replace(/[\u0000-\u001f\u007f]/gu, " ")
2008
+ .replace(/\s+/gu, " ")
2009
+ .trim();
2010
+ return truncateHeadBytes(normalized || "unknown worker integration error", POST_PUBLICATION_ERROR_MAX_BYTES).text;
2011
+ }
1917
2012
 
1918
2013
  export class WorkerIntegrationRequestScheduler {
1919
- #inFlight: Promise<void> | undefined = undefined;
2014
+ #inFlight: Promise<WorkerIntegrationOutcome> | undefined = undefined;
1920
2015
  #pending = false;
1921
2016
 
1922
2017
  constructor(
@@ -1933,9 +2028,15 @@ export class WorkerIntegrationRequestScheduler {
1933
2028
  }
1934
2029
 
1935
2030
  async flush(): Promise<void> {
2031
+ await this.flushWithOutcome();
2032
+ }
2033
+
2034
+ async flushWithOutcome(): Promise<WorkerIntegrationOutcome> {
2035
+ let outcome: WorkerIntegrationOutcome = { status: "completed" };
1936
2036
  while (this.#inFlight) {
1937
- await this.#inFlight;
2037
+ outcome = await this.#inFlight;
1938
2038
  }
2039
+ return outcome;
1939
2040
  }
1940
2041
 
1941
2042
  #start(): void {
@@ -1945,16 +2046,22 @@ export class WorkerIntegrationRequestScheduler {
1945
2046
  try {
1946
2047
  request = this.request(controller.signal);
1947
2048
  } catch {
1948
- request = Promise.resolve();
2049
+ request = Promise.reject(new Error("Worker integration request failed before dispatch."));
1949
2050
  }
1950
2051
  let timeout: ReturnType<typeof setTimeout> | undefined;
1951
- const deadline = new Promise<void>(resolve => {
2052
+ const deadline = new Promise<WorkerIntegrationOutcome>(resolve => {
1952
2053
  timeout = setTimeout(() => {
1953
2054
  controller.abort(new Error("Worker integration request timed out"));
1954
- resolve();
2055
+ resolve({ status: "timed_out" });
1955
2056
  }, this.timeoutMs);
1956
2057
  });
1957
- this.#inFlight = Promise.race([request.catch(() => {}), deadline]).finally(() => {
2058
+ this.#inFlight = Promise.race([
2059
+ request.then(
2060
+ () => ({ status: "completed" as const }),
2061
+ error => ({ status: "failed" as const, error: String(error) }),
2062
+ ),
2063
+ deadline,
2064
+ ]).finally(() => {
1958
2065
  if (timeout) clearTimeout(timeout);
1959
2066
  controller.abort();
1960
2067
  this.#inFlight = undefined;
@@ -2285,6 +2392,7 @@ export class AgentSession {
2285
2392
  #preProfileModel: Model | undefined;
2286
2393
  #sessionAdmissionQueue: SessionAdmissionEntry[] = [];
2287
2394
  #activeSessionAdmission: SessionAdmissionEntry | undefined;
2395
+ #startupPromptWaiters = new Set<Promise<void>>();
2288
2396
  #sessionAdmissionClosing = false;
2289
2397
  #sessionAdmissionClosed = false;
2290
2398
  #sessionAdmissionContext = new AsyncLocalStorage<SessionAdmissionEntry>();
@@ -2475,6 +2583,22 @@ export class AgentSession {
2475
2583
  this.#activeAttemptScope = undefined;
2476
2584
  this.#activeLogicalRunId = undefined;
2477
2585
  }
2586
+ #acceptSdkAttemptRun(handle: AttemptRunHandle, sdkRunToken?: string): void {
2587
+ const predecessorScope = this.#activeAttemptScope;
2588
+ const predecessorSdkRunToken =
2589
+ predecessorScope === undefined ? undefined : this.#sdkRunTokensByAttemptScope.get(predecessorScope);
2590
+ const carryRecoverySkip =
2591
+ sdkRunToken !== undefined &&
2592
+ predecessorSdkRunToken === sdkRunToken &&
2593
+ predecessorScope !== undefined &&
2594
+ this.#skipPostPromptRecoveryWaitByAttemptScope.delete(predecessorScope);
2595
+ this.#acceptRunHandle(handle);
2596
+ if (sdkRunToken !== undefined) {
2597
+ this.#activeSdkRunToken = sdkRunToken;
2598
+ this.#sdkRunTokensByAttemptScope.set(handle.scope, sdkRunToken);
2599
+ }
2600
+ if (carryRecoverySkip) this.#skipPostPromptRecoveryWaitByAttemptScope.add(handle.scope);
2601
+ }
2478
2602
  /** Messages queued to be included with the next user prompt as context ("asides"). */
2479
2603
  #pendingNextTurnMessages: Array<{ message: CustomMessage; origin: "turn" | "external" }> = [];
2480
2604
  #scheduledHiddenNextTurnGeneration: number | undefined = undefined;
@@ -2608,7 +2732,88 @@ export class AgentSession {
2608
2732
  onPersisted?: () => void;
2609
2733
  appendedToAgent: boolean;
2610
2734
  }> = [];
2611
- #foregroundBashBackgroundRequestHandler: (() => void) | undefined;
2735
+ /**
2736
+ * Set by a fold and consumed once by the pause checkpoint, so a fold ends its
2737
+ * own turn without pausing any later turn.
2738
+ */
2739
+ #foldStopRequested = false;
2740
+ /**
2741
+ * The single linearization point for folding a foreground wait. Arrow deps
2742
+ * capture `this` but are only invoked after construction.
2743
+ */
2744
+ readonly #foldCoordinator = new FoldCoordinator({
2745
+ hasActiveTurn: () => this.activePromptHandle !== undefined,
2746
+ armSteeringFence: () => {
2747
+ this.agent.setSteeringAdmissionFence(() => true);
2748
+ return () => this.agent.setSteeringAdmissionFence(undefined);
2749
+ },
2750
+ requestStop: () => {
2751
+ this.#foldStopRequested = true;
2752
+ },
2753
+ captureRemainingIntent: () => this.#captureRemainingIntentForFold(),
2754
+ // A parked completion replayed after the fold must schedule its own wake.
2755
+ // This mirrors exactly what the sdk delivery seam does for a live receipt
2756
+ // delivery, so the parked path cannot rely on an unrelated idle rearm.
2757
+ deliverParked: (job, disposition) => {
2758
+ const endpointId = this.#ownedAsyncJobManager
2759
+ ? AsyncJobManager.endpointIdOf(this.#ownedAsyncJobManager)
2760
+ : undefined;
2761
+ const registration = lookupOwnedRegistration(job.id, job.generation, endpointId);
2762
+ if (this.#ownedAsyncJobManager?.isDeliverySuppressed(job.id, job.generation)) {
2763
+ this.#ownedAsyncJobManager.clearParkedDelivery(job.generation);
2764
+ return;
2765
+ }
2766
+ if (this.#foldCoordinator.claimCompletionNotice(job)) {
2767
+ this.emitNotice(
2768
+ "info",
2769
+ `Folded job ${disposition.receipt.jobId} (${disposition.receipt.label}) finished.`,
2770
+ "fold",
2771
+ );
2772
+ }
2773
+ const ownedCompletion = registration
2774
+ ? {
2775
+ lineageIdHash: registration.lineageIdHash,
2776
+ promptAttemptEpoch: registration.promptAttemptEpoch,
2777
+ registration,
2778
+ }
2779
+ : undefined;
2780
+ const allowArtifact = ownedCompletion === undefined || isOwnedCompletionEnvelopeAllowed(ownedCompletion);
2781
+ void formatParkedAsyncResult(this.sessionManager, disposition.text, allowArtifact)
2782
+ .then(formattedResult => {
2783
+ const manager = this.#ownedAsyncJobManager;
2784
+ if (manager?.isDeliverySuppressed(job.id, job.generation)) {
2785
+ manager.clearParkedDelivery(job.generation);
2786
+ return;
2787
+ }
2788
+ if (!this.#foldCoordinator.claimCompletionDelivery(job)) {
2789
+ manager?.clearParkedDelivery(job.generation);
2790
+ return;
2791
+ }
2792
+ try {
2793
+ manager?.retainDeliveryClaim(job);
2794
+ if (manager?.isDeliverySuppressed(job.id, job.generation)) {
2795
+ manager.releaseDeliveryClaim(job.generation);
2796
+ manager.clearParkedDelivery(job.generation);
2797
+ return;
2798
+ }
2799
+ this.yieldQueue.enqueue("async-result", {
2800
+ jobId: disposition.receipt.jobId,
2801
+ generation: disposition.receipt.jobGeneration,
2802
+ result: `${formattedResult}\n\n${describeFoldReceipt(disposition.receipt)}`,
2803
+ job,
2804
+ durationMs: undefined,
2805
+ ownedCompletion,
2806
+ });
2807
+ } finally {
2808
+ manager?.clearParkedDelivery(job.generation);
2809
+ }
2810
+ })
2811
+ .catch(error => {
2812
+ this.#ownedAsyncJobManager?.clearParkedDelivery(job.generation);
2813
+ logger.warn("Parked folded delivery formatting failed", { error: String(error) });
2814
+ });
2815
+ },
2816
+ });
2612
2817
 
2613
2818
  // Python execution state
2614
2819
  #evalAbortControllers = new Set<AbortController>();
@@ -2616,6 +2821,27 @@ export class AgentSession {
2616
2821
  /** Idempotent unregister handle for this session's resource-GC registration. */
2617
2822
  #unregisterResourceGc?: () => void;
2618
2823
  #unregisterRuntimeStateFinalizer?: () => void;
2824
+ #unregisterBeforeMoveListener?: () => void;
2825
+ #unregisterMoveAbortListener?: () => void;
2826
+ #unregisterMovePublicationListener?: () => void;
2827
+ #unregisterAfterMoveListener?: () => void;
2828
+ /**
2829
+ * (Re)register the postmortem finalizer bound to the CURRENT cwd/session file. Called at
2830
+ * construction and after every committed rescope, so an unexpected exit writes terminal
2831
+ * state to the session's live location rather than the abandoned launch root.
2832
+ */
2833
+ #registerRuntimeStateFinalizer(): void {
2834
+ this.#unregisterRuntimeStateFinalizer?.();
2835
+ const currentContext = () => ({
2836
+ sessionId: this.sessionId,
2837
+ cwd: this.sessionManager.getCwd(),
2838
+ sessionFile: this.sessionManager.getSessionFile(),
2839
+ });
2840
+ this.#unregisterRuntimeStateFinalizer = registerCoordinatorRuntimeStateFinalizer(
2841
+ currentContext(),
2842
+ currentContext,
2843
+ );
2844
+ }
2619
2845
  #unregisterSessionMemorySettings?: () => void;
2620
2846
  /**
2621
2847
  * AsyncJobManager owned by this session (top-level only). Subagents leave
@@ -2625,6 +2851,7 @@ export class AgentSession {
2625
2851
  readonly #disposeAsyncJobManager: boolean;
2626
2852
  #ownedMcpManager: MCPManager | undefined;
2627
2853
  #startupTurnBarrier: Promise<void> | undefined;
2854
+ #startupTurnBarrierPending = false;
2628
2855
  #pendingPythonMessages: Array<{
2629
2856
  message: PythonExecutionMessage;
2630
2857
  onPersisted?: () => void;
@@ -2650,6 +2877,9 @@ export class AgentSession {
2650
2877
  readonly #asyncJobProviderSessionId: string | undefined;
2651
2878
  #isDisposed = false;
2652
2879
  #disposePromise: Promise<void> | undefined;
2880
+ readonly #disposeAbortController = new AbortController();
2881
+ #disposeAdmissionClosed: Promise<void> | undefined;
2882
+ #disposePostPromptDrain: Promise<void> | undefined;
2653
2883
  readonly #toolSessionCleanups = new Set<() => Promise<void> | void>();
2654
2884
  readonly #toolSessionTransitionCleanups = new Set<() => Promise<void> | void>();
2655
2885
  readonly #deferredOwnerShutdownFinalizations = new Set<Promise<void>>();
@@ -2658,6 +2888,7 @@ export class AgentSession {
2658
2888
  #extensionRunner: ExtensionRunner | undefined = undefined;
2659
2889
  /** SDK follow-up ownership by queued message and the attempt that dequeues it. */
2660
2890
  #sdkRunTokensByQueuedMessage = new WeakMap<AgentMessage, string>();
2891
+ #skipPostPromptRecoveryWaitByAttemptScope = new WeakSet<AttemptScope>();
2661
2892
  #sdkRunTokensByAttemptScope = new WeakMap<AttemptScope, string>();
2662
2893
  #activeSdkRunToken: string | undefined;
2663
2894
  #activeAttemptScope: AttemptScope | undefined;
@@ -2820,6 +3051,7 @@ export class AgentSession {
2820
3051
  #cancelAndSubmitAbortOutcomeProviderForTests: (() => Promise<AbortOutcome>) | undefined = undefined;
2821
3052
  #postPromptTasks = new Set<Promise<void>>();
2822
3053
  #postPromptTaskSelectionFenceGenerations = new Map<Promise<void>, number>();
3054
+ #postPromptTaskRecoveryExcluded = new Set<Promise<void>>();
2823
3055
  #postPromptTasksPromise: Promise<void> | undefined = undefined;
2824
3056
  #postPromptTasksResolve: (() => void) | undefined = undefined;
2825
3057
  #postPromptTasksAbortController = new AbortController();
@@ -3006,6 +3238,66 @@ export class AgentSession {
3006
3238
  * orchestrator does not hold it), so there is no self-deadlock.
3007
3239
  */
3008
3240
  #sessionTransitionKind: string | undefined;
3241
+ #coordinatorPersistGeneration = 0;
3242
+ #coordinatorRescopeBarrier: Promise<void> | undefined;
3243
+ #releaseCoordinatorRescopeBarrier: (() => void) | undefined;
3244
+ #coordinatorRescopeMoveId: string | undefined;
3245
+ #coordinatorEventHandlers = new Set<Promise<void>>();
3246
+ #coordinatorUnbarrieredPersists = new Set<Promise<void>>();
3247
+ #coordinatorReleasedBarrierPersists = new Set<Promise<void>>();
3248
+
3249
+ #beginCoordinatorRescopeBarrier(): {
3250
+ eventHandlers: Promise<void>[];
3251
+ releasedBarrierPersists: Promise<void>[];
3252
+ } {
3253
+ if (this.#coordinatorRescopeBarrier) throw new Error("Coordinator rescope barrier is already active.");
3254
+ const releasedBarrierPersists = [...this.#coordinatorReleasedBarrierPersists];
3255
+ const barrier = Promise.withResolvers<void>();
3256
+ this.#coordinatorRescopeBarrier = barrier.promise;
3257
+ this.#releaseCoordinatorRescopeBarrier = barrier.resolve;
3258
+ this.#bindPendingAgentEndToRescopeBarrier();
3259
+ return { eventHandlers: [...this.#coordinatorEventHandlers], releasedBarrierPersists };
3260
+ }
3261
+
3262
+ #trackReleasedBarrierPersist(persist: Promise<void>): void {
3263
+ this.#coordinatorReleasedBarrierPersists.add(persist);
3264
+ void persist.then(
3265
+ () => this.#coordinatorReleasedBarrierPersists.delete(persist),
3266
+ () => this.#coordinatorReleasedBarrierPersists.delete(persist),
3267
+ );
3268
+ }
3269
+
3270
+ #bindPendingAgentEndToRescopeBarrier(): void {
3271
+ if (!this.#coordinatorRescopeBarrier || !this.#pendingAgentEndEmit) return;
3272
+ const admission = this.#agentEventAdmission.get(this.#pendingAgentEndEmit);
3273
+ if (admission) admission.persistBarrier = this.#coordinatorRescopeBarrier;
3274
+ }
3275
+
3276
+ #appendCoordinatorPersist(run: () => Promise<void>): Promise<void> {
3277
+ const queued = this.#coordinatorPersistQueue.then(run, run);
3278
+ this.#coordinatorPersistQueue = queued.catch(() => {});
3279
+ return queued;
3280
+ }
3281
+
3282
+ #trackUnbarrieredCoordinatorPersist(persist: Promise<void>): void {
3283
+ this.#coordinatorUnbarrieredPersists.add(persist);
3284
+ void persist.then(
3285
+ () => this.#coordinatorUnbarrieredPersists.delete(persist),
3286
+ () => this.#coordinatorUnbarrieredPersists.delete(persist),
3287
+ );
3288
+ }
3289
+
3290
+ async #drainUnbarrieredCoordinatorPersists(): Promise<void> {
3291
+ while (this.#coordinatorUnbarrieredPersists.size > 0) {
3292
+ await Promise.allSettled([...this.#coordinatorUnbarrieredPersists]);
3293
+ }
3294
+ }
3295
+
3296
+ #endCoordinatorRescopeBarrier(): void {
3297
+ this.#releaseCoordinatorRescopeBarrier?.();
3298
+ this.#releaseCoordinatorRescopeBarrier = undefined;
3299
+ this.#coordinatorRescopeBarrier = undefined;
3300
+ }
3009
3301
 
3010
3302
  #beginSessionTransition(kind: string): void {
3011
3303
  if (this.#sessionTransitionKind !== undefined) {
@@ -3015,6 +3307,7 @@ export class AgentSession {
3015
3307
  );
3016
3308
  }
3017
3309
  this.#sessionTransitionKind = kind;
3310
+ this.#coordinatorPersistGeneration += 1;
3018
3311
  }
3019
3312
 
3020
3313
  #endSessionTransition(): void {
@@ -3041,16 +3334,61 @@ export class AgentSession {
3041
3334
  return { entry, capability: entry.continuationCapability };
3042
3335
  }
3043
3336
 
3044
- async #awaitStartupTurnBarrier(): Promise<void> {
3045
- const barrier = this.#startupTurnBarrier;
3337
+ #setStartupTurnBarrier(barrier: Promise<void> | undefined): void {
3338
+ this.#startupTurnBarrier = barrier;
3339
+ this.#startupTurnBarrierPending = barrier !== undefined;
3046
3340
  if (!barrier) return;
3047
- await barrier;
3048
- if (this.#startupTurnBarrier === barrier) this.#startupTurnBarrier = undefined;
3341
+ void barrier.then(
3342
+ () => {
3343
+ if (this.#startupTurnBarrier === barrier) this.#startupTurnBarrierPending = false;
3344
+ },
3345
+ () => {
3346
+ if (this.#startupTurnBarrier === barrier) this.#startupTurnBarrierPending = false;
3347
+ },
3348
+ );
3349
+ }
3350
+ async #awaitStartupTurnBarrier(signal?: AbortSignal): Promise<void> {
3351
+ while (true) {
3352
+ const barrier = this.#startupTurnBarrier;
3353
+ if (!barrier) return;
3354
+ await awaitPromptInvocationPreflight(barrier, signal);
3355
+ // Cross one task boundary before clearing readiness. Promise reaction
3356
+ // chains fully drain before timers, so every extension published by the
3357
+ // captured barrier's settlement becomes visible without an arbitrary
3358
+ // microtask-depth cap.
3359
+ const publicationTurn = Promise.withResolvers<void>();
3360
+ const timer = setTimeout(publicationTurn.resolve, 0);
3361
+ try {
3362
+ await awaitPromptInvocationPreflight(publicationTurn.promise, signal);
3363
+ } finally {
3364
+ clearTimeout(timer);
3365
+ }
3366
+ if (this.#startupTurnBarrier !== barrier) continue;
3367
+ this.#setStartupTurnBarrier(undefined);
3368
+ return;
3369
+ }
3370
+ }
3371
+ async #awaitStartupPromptWaiters(signal?: AbortSignal): Promise<void> {
3372
+ while (this.#startupPromptWaiters.size > 0) {
3373
+ await awaitPromptInvocationPreflight(Promise.all([...this.#startupPromptWaiters]), signal);
3374
+ await Promise.resolve();
3375
+ }
3376
+ }
3377
+ #reserveStartupPromptWaiter(): () => void {
3378
+ const waiter = Promise.withResolvers<void>();
3379
+ this.#startupPromptWaiters.add(waiter.promise);
3380
+ let released = false;
3381
+ return () => {
3382
+ if (released) return;
3383
+ released = true;
3384
+ this.#startupPromptWaiters.delete(waiter.promise);
3385
+ waiter.resolve();
3386
+ };
3049
3387
  }
3050
3388
  extendStartupTurnBarrier(barrier: Promise<void>): void {
3051
3389
  const current = this.#startupTurnBarrier;
3052
- this.#startupTurnBarrier = current ? Promise.all([current, barrier]).then(() => {}) : barrier;
3053
- void this.#startupTurnBarrier.catch(() => {});
3390
+ this.#setStartupTurnBarrier(current ? Promise.all([current, barrier]).then(() => {}) : barrier);
3391
+ void this.#startupTurnBarrier?.catch(() => {});
3054
3392
  }
3055
3393
 
3056
3394
  async #withSessionAdmission<T>(
@@ -3062,6 +3400,8 @@ export class AgentSession {
3062
3400
  allowDuringClosing?: boolean;
3063
3401
  bypassSelectionFenceGeneration?: number;
3064
3402
  allowPromptContinuationReentry?: boolean;
3403
+ idleDelivery?: boolean;
3404
+ startupPromptWaiterRelease?: () => void;
3065
3405
  },
3066
3406
  ): Promise<T> {
3067
3407
  const owner = this.#sessionAdmissionContext.getStore();
@@ -3076,7 +3416,21 @@ export class AgentSession {
3076
3416
  }
3077
3417
  throw this.#sessionAdmissionBusyError();
3078
3418
  }
3079
- if (kind === "prompt") await awaitPromptInvocationPreflight(this.#awaitStartupTurnBarrier(), signal);
3419
+ const releaseStartupPromptWaiter =
3420
+ kind === "prompt" && options?.idleDelivery !== true
3421
+ ? (options?.startupPromptWaiterRelease ?? this.#reserveStartupPromptWaiter())
3422
+ : undefined;
3423
+ if (kind === "prompt") {
3424
+ const startupSignal = signal
3425
+ ? AbortSignal.any([signal, this.#disposeAbortController.signal])
3426
+ : this.#disposeAbortController.signal;
3427
+ try {
3428
+ await awaitPromptInvocationPreflight(this.#awaitStartupTurnBarrier(startupSignal), startupSignal);
3429
+ } finally {
3430
+ releaseStartupPromptWaiter?.();
3431
+ }
3432
+ if (options?.idleDelivery === true) await this.#awaitStartupPromptWaiters(startupSignal);
3433
+ }
3080
3434
  const bypassesSelectionFence =
3081
3435
  options?.bypassSelectionFenceGeneration !== undefined &&
3082
3436
  options.bypassSelectionFenceGeneration < this.#selectionFenceGeneration;
@@ -3614,7 +3968,7 @@ export class AgentSession {
3614
3968
  // Deferrals behind still-pending selection fences are excluded here: they
3615
3969
  // cannot settle before their fence resolves, and the fence owner may be
3616
3970
  // waiting on this very settlement (#4519).
3617
- if (!predecessorPromptStillInFlight) {
3971
+ if (promptWait !== "publication" && !predecessorPromptStillInFlight) {
3618
3972
  await this.#waitForSessionSettlement(this.#turnSettlementDeferralFloor());
3619
3973
  }
3620
3974
  if (flushError) throw flushError;
@@ -3656,28 +4010,50 @@ export class AgentSession {
3656
4010
  this.#pendingAgentEndEmit = undefined;
3657
4011
  const lease = this.#deferredAgentEndLeases.get(pending);
3658
4012
  if (lease) this.#deferredAgentEndLeases.delete(pending);
4013
+ this.#startAgentEndPublication(pending, lease);
4014
+ }
4015
+ #startAgentEndPublication(pending: AgentSessionEvent, lease?: RunResourceProducerLease): void {
3659
4016
  this.#agentEndPublicationInFlight++;
3660
- this.#agentEndPublicationPromise = this.#publishDeferredAgentEnd(pending, lease);
4017
+ const pendingScope = (pending as AgentSessionEvent & { scope?: AttemptScopeRef }).scope as
4018
+ | AttemptScope
4019
+ | undefined;
4020
+ const sdkTerminal = pendingScope !== undefined && this.#sdkRunTokensByAttemptScope.has(pendingScope);
4021
+ this.#agentEndPublicationPromise = this.#publishDeferredAgentEnd(pending, lease, sdkTerminal);
3661
4022
  void this.#agentEndPublicationPromise;
3662
4023
  }
3663
4024
 
3664
4025
  async #publishDeferredAgentEnd(
3665
4026
  pending: AgentSessionEvent,
3666
4027
  lease: RunResourceProducerLease | undefined,
4028
+ workerIntegrationSettled: boolean,
3667
4029
  ): Promise<void> {
4030
+ const publicationContext = {
4031
+ sessionId: this.sessionId,
4032
+ cwd: this.sessionManager.getCwd(),
4033
+ sessionFile: this.sessionManager.getSessionFile(),
4034
+ };
4035
+ const publicationScope = (pending as AgentSessionEvent & { scope?: AttemptScopeRef }).scope as
4036
+ | AttemptScope
4037
+ | undefined;
4038
+ const publicationCorrelationId = publicationScope
4039
+ ? this.#sdkRunTokensByAttemptScope.get(publicationScope)
4040
+ : undefined;
3668
4041
  let extensionDelivery: Promise<void> | undefined;
3669
4042
  const releaseLease = () => {
3670
4043
  if (!lease) return;
4044
+ this.#deferredAgentEndLeases.delete(pending);
3671
4045
  if (this.#postPromptLeases.get(lease.resourceRunId) === lease) {
3672
4046
  this.#postPromptLeases.delete(lease.resourceRunId);
3673
4047
  }
3674
4048
  lease.closeDiscovery();
3675
4049
  };
3676
4050
  const publish = async () => {
3677
- // Worker integration is first-party lifecycle persistence, not an extension
3678
- // hook. Settle it before publishing the terminal boundary while user extension
3679
- // delivery and coordinator sidecar persistence remain asynchronous.
3680
- await this.#flushWorkerIntegrationForAgentEnd();
4051
+ let workerIntegration: Promise<WorkerIntegrationOutcome> | undefined;
4052
+ let workerIntegrationOutcome: WorkerIntegrationOutcome | undefined;
4053
+ if (!workerIntegrationSettled) {
4054
+ workerIntegration = this.#flushWorkerIntegrationForAgentEnd();
4055
+ workerIntegrationOutcome = await workerIntegration;
4056
+ }
3681
4057
  // Reserve persistence before notifying synchronous subscribers: a subscriber
3682
4058
  // may start a successor prompt from agent_end, whose running state must
3683
4059
  // serialize after this terminal boundary rather than be overwritten by it.
@@ -3686,16 +4062,72 @@ export class AgentSession {
3686
4062
  // behind this terminal transition, but do not make the interactive terminal
3687
4063
  // wait for the filesystem-backed lock/write to settle. On slow or lock-hostile
3688
4064
  // mounts such as WSL drvfs, waiting here leaves the foreground activity loader
3689
- // and busy input state visible after the model has already finished.
3690
- const terminalPersistence = this.#queueCoordinatorRuntimeStatePersist(pending);
4065
+ // and busy input state visible after the model has already finished. The queue
4066
+ // preserves transaction order; terminal publication is the user-visible
4067
+ // authority and must not be suppressed by a secondary persistence failure.
4068
+ const terminalPersistence = this.#queueCoordinatorRuntimeStatePersist(pending, true);
3691
4069
  this.#emit(pending);
3692
- await terminalPersistence;
4070
+ void terminalPersistence.then(
4071
+ () => {
4072
+ if (workerIntegrationOutcome) {
4073
+ this.#recordPostPublicationOutcome(
4074
+ publicationContext,
4075
+ publicationCorrelationId,
4076
+ "worker_integration",
4077
+ workerIntegrationOutcome,
4078
+ );
4079
+ }
4080
+ this.#recordPostPublicationOutcome(
4081
+ publicationContext,
4082
+ publicationCorrelationId,
4083
+ "terminal_persistence",
4084
+ { status: "completed" },
4085
+ );
4086
+ },
4087
+ error => {
4088
+ if (workerIntegrationOutcome) {
4089
+ this.#recordPostPublicationOutcome(
4090
+ publicationContext,
4091
+ publicationCorrelationId,
4092
+ "worker_integration",
4093
+ workerIntegrationOutcome,
4094
+ );
4095
+ }
4096
+ this.#recordPostPublicationOutcome(
4097
+ publicationContext,
4098
+ publicationCorrelationId,
4099
+ "terminal_persistence",
4100
+ {
4101
+ status: "failed",
4102
+ error: String(error),
4103
+ },
4104
+ );
4105
+ logger.warn("Failed to persist terminal coordinator runtime state", { error: String(error) });
4106
+ },
4107
+ );
3693
4108
  extensionDelivery = this.#queueExtensionEvent(
3694
4109
  pending,
3695
4110
  undefined,
3696
4111
  true,
3697
4112
  (pending as AgentSessionEvent & { scope?: AttemptScopeRef }).scope,
3698
4113
  );
4114
+ if (workerIntegrationSettled) {
4115
+ workerIntegration = this.#flushWorkerIntegrationForAgentEnd();
4116
+ void workerIntegration.then(outcome =>
4117
+ this.#recordPostPublicationOutcome(
4118
+ publicationContext,
4119
+ publicationCorrelationId,
4120
+ "worker_integration",
4121
+ outcome,
4122
+ ),
4123
+ );
4124
+ await extensionDelivery;
4125
+ void terminalPersistence.catch(error =>
4126
+ logger.warn("Terminal persistence continued after SDK publication", { error }),
4127
+ );
4128
+ } else {
4129
+ // Non-SDK/public publication has already awaited worker integration above.
4130
+ }
3699
4131
  };
3700
4132
  try {
3701
4133
  if (lease) await this.#runResourceLeaseContext.run(lease, publish);
@@ -3760,10 +4192,96 @@ export class AgentSession {
3760
4192
  cwd: () => this.sessionManager.getCwd(),
3761
4193
  });
3762
4194
  }
3763
- this.#unregisterRuntimeStateFinalizer = registerCoordinatorRuntimeStateFinalizer({
3764
- sessionId: this.sessionId,
3765
- cwd: this.sessionManager.getCwd(),
3766
- sessionFile: this.sessionManager.getSessionFile(),
4195
+ this.#registerRuntimeStateFinalizer();
4196
+ this.#unregisterBeforeMoveListener = this.sessionManager.registerBeforeMoveListener(async move => {
4197
+ const admittedBeforeBarrier = this.#beginCoordinatorRescopeBarrier();
4198
+ await Promise.allSettled([
4199
+ ...admittedBeforeBarrier.eventHandlers,
4200
+ ...admittedBeforeBarrier.releasedBarrierPersists,
4201
+ ]);
4202
+ this.#bindPendingAgentEndToRescopeBarrier();
4203
+ await this.#drainUnbarrieredCoordinatorPersists();
4204
+ this.#coordinatorPersistGeneration += 1;
4205
+ const moveId = crypto.randomUUID();
4206
+ this.#coordinatorRescopeMoveId = moveId;
4207
+ await prepareCoordinatorRuntimeStateRescope({
4208
+ moveId,
4209
+ sessionId: this.sessionId,
4210
+ previousCwd: move.previousCwd,
4211
+ newCwd: move.newCwd,
4212
+ previousCwdIdentity: move.previousCwdIdentity,
4213
+ newCwdIdentity: move.newCwdIdentity,
4214
+ previousSessionFile: move.previousSessionFile ?? null,
4215
+ newSessionFile: move.newSessionFile ?? null,
4216
+ });
4217
+ });
4218
+ this.#unregisterMoveAbortListener = this.sessionManager.registerMoveAbortListener(async move => {
4219
+ const moveId = this.#coordinatorRescopeMoveId;
4220
+ try {
4221
+ if (moveId && !move.preserveRecoveryJournal) {
4222
+ await clearCoordinatorRuntimeStateRescope(
4223
+ {
4224
+ sessionId: this.sessionId,
4225
+ cwd: move.newCwd,
4226
+ sessionFile: move.newSessionFile ?? null,
4227
+ },
4228
+ moveId,
4229
+ move.previousCwd,
4230
+ );
4231
+ }
4232
+ } finally {
4233
+ this.#coordinatorRescopeMoveId = undefined;
4234
+ this.#endCoordinatorRescopeBarrier();
4235
+ }
4236
+ });
4237
+ this.#unregisterMovePublicationListener = this.sessionManager.registerMovePublicationListener(async move => {
4238
+ const moveId = this.#coordinatorRescopeMoveId;
4239
+ if (!moveId) throw new Error("Coordinator rescope publication has no prepared move identity.");
4240
+ await markCoordinatorRuntimeStateRescopePublishing(
4241
+ {
4242
+ sessionId: this.sessionId,
4243
+ cwd: move.newCwd,
4244
+ sessionFile: move.newSessionFile ?? null,
4245
+ },
4246
+ move.previousCwd,
4247
+ moveId,
4248
+ );
4249
+ });
4250
+ // Every committed rescope (`move_session`, `/move`, SDK/ACP `session.cwd.move`) funnels
4251
+ // through SessionManager.moveTo, so one after-move listener covers all surfaces: it
4252
+ // relocates the coordinator-shared runtime state to the new cwd (otherwise the identity
4253
+ // fence refuses every later persist) and rebinds the postmortem finalizer, which
4254
+ // otherwise keeps writing terminal state to the launch root's cwd/session file.
4255
+ this.#unregisterAfterMoveListener = this.sessionManager.registerAfterMoveListener(async move => {
4256
+ let completed = false;
4257
+ try {
4258
+ const relocated = await relocateCoordinatorRuntimeStateForRescope(
4259
+ {
4260
+ sessionId: this.sessionId,
4261
+ cwd: move.newCwd,
4262
+ sessionFile: this.sessionManager.getSessionFile() ?? null,
4263
+ previousSessionFile: move.previousSessionFile ?? null,
4264
+ },
4265
+ move.previousCwd,
4266
+ );
4267
+ if (!relocated) throw new Error("Coordinator runtime state rescope was refused.");
4268
+ await clearCoordinatorRuntimeStateRescope(
4269
+ {
4270
+ sessionId: this.sessionId,
4271
+ cwd: move.newCwd,
4272
+ sessionFile: this.sessionManager.getSessionFile() ?? null,
4273
+ },
4274
+ this.#coordinatorRescopeMoveId,
4275
+ move.previousCwd,
4276
+ );
4277
+ completed = true;
4278
+ } finally {
4279
+ this.#registerRuntimeStateFinalizer();
4280
+ if (completed) {
4281
+ this.#coordinatorRescopeMoveId = undefined;
4282
+ this.#endCoordinatorRescopeBarrier();
4283
+ }
4284
+ }
3767
4285
  });
3768
4286
  // Power assertions are taken per turn (see #beginInFlight); nothing acquired here.
3769
4287
  this.#evalKernelOwnerId = config.evalKernelOwnerId ?? `agent-session:${Snowflake.next()}`;
@@ -3772,7 +4290,19 @@ export class AgentSession {
3772
4290
  this.#disposeAsyncJobManager = config.disposeAsyncJobManager ?? true;
3773
4291
  this.#retainedMemorySampler = config.retainedMemorySampler;
3774
4292
  this.#ownedMcpManager = config.ownedMcpManager;
3775
- this.#startupTurnBarrier = config.startupTurnBarrier;
4293
+ this.#setStartupTurnBarrier(config.startupTurnBarrier);
4294
+ // Only arm the recovery barrier when a rescope journal is actually pending. Arming it
4295
+ // unconditionally flips #startupTurnBarrier from absent to a (resolved) promise for
4296
+ // every session, which makes barrier-gated post-turn work (e.g. the hidden-next-turn
4297
+ // drain) take an extra scheduler hop and reorder against terminal-abort requeue.
4298
+ const rescopeRecoveryContext = {
4299
+ sessionId: this.sessionId,
4300
+ cwd: this.sessionManager.getCwd(),
4301
+ sessionFile: this.sessionManager.getSessionFile() ?? null,
4302
+ };
4303
+ if (hasCoordinatorRuntimeStateRescopeJournal(rescopeRecoveryContext)) {
4304
+ this.extendStartupTurnBarrier(recoverCoordinatorRuntimeStateRescope(rescopeRecoveryContext));
4305
+ }
3776
4306
  this.#scopedModels = config.scopedModels ?? [];
3777
4307
  this.#thinkingLevel = config.thinkingLevel;
3778
4308
  this.#promptTemplates = config.promptTemplates ?? [];
@@ -3860,7 +4390,7 @@ export class AgentSession {
3860
4390
  // agent.prompt directly, allocates right before admission.
3861
4391
  this.agent.followUp(message);
3862
4392
  },
3863
- injectIdle: async messages => {
4393
+ injectIdle: async (messages, signal) => {
3864
4394
  // Mandated boundary comment (corrected turn semantics): same origin
3865
4395
  // split as the streaming injector — an allowed owned-completion
3866
4396
  // delivery starts a fresh turn attempt/lineage and is not a
@@ -3874,27 +4404,43 @@ export class AgentSession {
3874
4404
  if (dropped.length > 0) this.#settleDeliveredOwnedRegistrations(dropped);
3875
4405
  const first = survivors[0];
3876
4406
  if (!first) return;
3877
- await this.#awaitStartupTurnBarrier();
3878
- if (this.#isDisposed) return;
3879
- // A user prompt may have started during the barrier/scheduling
3880
- // delay: if the session is now streaming, mutating the epoch and
3881
- // lineage here would corrupt the ACTIVE user turn (and
3882
- // agent.prompt would then reject as busy, losing the drained
3883
- // completion). Route the survivors through followUp — the
3884
- // streaming injector's path — which allocates the fresh resume
3885
- // lineage at actual admission (review thread P1).
3886
- if (this.isStreaming) {
3887
- for (const message of survivors) this.agent.followUp(message);
3888
- return;
3889
- }
3890
- if (survivors.some(message => ownedCompletionResumeAction(message) === "fresh"))
3891
- this.#resumeFromOwnedCompletion();
4407
+ const settleIfDisposing = (): boolean => {
4408
+ if (!this.#isDisposed && !this.#sessionAdmissionClosing && !this.#disposeAbortController.signal.aborted)
4409
+ return false;
4410
+ this.#settleDeliveredOwnedRegistrations(survivors);
4411
+ return true;
4412
+ };
3892
4413
  try {
3893
- if (survivors.length === 1) {
3894
- await this.agent.prompt(first, this.#managedFallbackPromptOptions());
3895
- } else {
3896
- await this.agent.prompt(survivors, this.#managedFallbackPromptOptions());
3897
- }
4414
+ await this.#withSessionAdmission(
4415
+ "prompt",
4416
+ async () => {
4417
+ if (settleIfDisposing()) return;
4418
+ if (this.isStreaming) {
4419
+ await awaitPromptInvocationPreflight(this.agent.waitForIdle(), signal);
4420
+ if (settleIfDisposing()) return;
4421
+ }
4422
+ if (survivors.some(message => ownedCompletionResumeAction(message) === "fresh"))
4423
+ this.#resumeFromOwnedCompletion();
4424
+ if (survivors.length === 1) {
4425
+ await this.agent.prompt(first, {
4426
+ ...this.#managedFallbackPromptOptions(),
4427
+ onRunAccepted: (handle: AttemptRunHandle) => {
4428
+ if (handle) this.#acceptSdkAttemptRun(handle);
4429
+ },
4430
+ });
4431
+ } else {
4432
+ await this.agent.prompt(survivors, {
4433
+ ...this.#managedFallbackPromptOptions(),
4434
+ onRunAccepted: (handle: AttemptRunHandle) => {
4435
+ if (handle) this.#acceptSdkAttemptRun(handle);
4436
+ },
4437
+ });
4438
+ }
4439
+ },
4440
+ signal,
4441
+ undefined,
4442
+ { idleDelivery: true },
4443
+ );
3898
4444
  } finally {
3899
4445
  // The owned completions were delivered OR the prompt attempt
3900
4446
  // failed (e.g. provider rejection): either way the yield
@@ -3906,16 +4452,40 @@ export class AgentSession {
3906
4452
  this.#settleDeliveredOwnedRegistrations(survivors);
3907
4453
  }
3908
4454
  },
3909
- scheduleIdleFlush: run => {
4455
+ scheduleIdleFlush: (run, onSkip) => {
4456
+ // The startup barrier already gates injectIdle, so begin waiting on a
4457
+ // pending barrier immediately. Once readiness has settled, ordinary
4458
+ // idle wakes retain the fixed merge window.
4459
+ const delayMs = this.#startupTurnBarrierPending ? 0 : FOLD_WAKE_MERGE_WINDOW_MS;
3910
4460
  this.#schedulePostPromptTask(
3911
- async () => {
3912
- await run();
4461
+ async signal => {
4462
+ await run(signal);
3913
4463
  },
3914
- { delayMs: 1 },
4464
+ { delayMs, onSkip, excludeFromPostPromptRecovery: true },
3915
4465
  );
3916
4466
  },
4467
+ getIdleFlushSignal: () => this.#postPromptTasksAbortController.signal,
3917
4468
  });
3918
4469
  this.agent.setOnBeforeYield(() => this.yieldQueue.flush("streaming"));
4470
+ // Stop-after-result, never abort: a fold arms this once and the loop ends the
4471
+ // turn at its next checkpoint. Consuming the flag here keeps the pause scoped
4472
+ // to the folded turn instead of pausing everything that follows. The
4473
+ // config-provided checkpoint (subagent requestPause, RLM research) is OR-ed
4474
+ // in, never replaced.
4475
+ // sdk/session.ts threads the config's shouldPause into new Agent, so the
4476
+ // Agent's current checkpoint IS the config-provided one (subagent
4477
+ // requestPause, RLM research). Capture and OR it rather than replace it.
4478
+ const configuredShouldPause = this.agent.shouldPause;
4479
+ this.agent.setShouldPause(() => {
4480
+ if (configuredShouldPause?.() === true) return true;
4481
+ if (!this.#foldStopRequested) return false;
4482
+ this.#foldStopRequested = false;
4483
+ // The folded turn has now actually stopped: steering admission must be
4484
+ // restored so later turns drain their queues normally (the fence stays
4485
+ // armed from the fold until exactly this point).
4486
+ this.agent.setSteeringAdmissionFence(undefined);
4487
+ return true;
4488
+ });
3919
4489
  this.agent.setMaintainContext((context, lifecycle) =>
3920
4490
  this.#trackMidRunMaintenance(
3921
4491
  this.awaitPendingContextTransformations().then(() => this.#runMidRunMaintenance(context, lifecycle)),
@@ -4054,6 +4624,18 @@ export class AgentSession {
4054
4624
  if (messages.some(message => ownedCompletionResumeAction(message) === "fresh")) {
4055
4625
  this.#resumeFromOwnedCompletion();
4056
4626
  }
4627
+ const consumedSdkRunTokenForCurrentRun = messages
4628
+ .map(message => this.#sdkRunTokensByQueuedMessage.get(message))
4629
+ .find((token): token is string => token !== undefined);
4630
+ if (
4631
+ consumedSdkRunTokenForCurrentRun !== undefined &&
4632
+ this.#activeAttemptScope !== undefined &&
4633
+ promotion.startsOwnRun !== true
4634
+ ) {
4635
+ this.#sdkRunTokensByAttemptScope.set(this.#activeAttemptScope, consumedSdkRunTokenForCurrentRun);
4636
+ this.#skipPostPromptRecoveryWaitByAttemptScope.add(this.#activeAttemptScope);
4637
+ this.#activeSdkRunToken = consumedSdkRunTokenForCurrentRun;
4638
+ }
4057
4639
  // Monitor task-notifications now carry the owned-completion envelope
4058
4640
  // (see tools/monitor.ts), so the general drop + fresh-admission paths
4059
4641
  // above cover them exactly like async results — no monitor-specific
@@ -4089,6 +4671,18 @@ export class AgentSession {
4089
4671
  // submitter to the in-flight run instead of parking the correlation for
4090
4672
  // an unrelated later agent_start (#4668).
4091
4673
  this.agent.onSteeringConsumed = (messages, promotion = { startsOwnRun: false }) => {
4674
+ const consumedSdkRunToken = messages
4675
+ .map(message => this.#sdkRunTokensByQueuedMessage.get(message))
4676
+ .find((token): token is string => token !== undefined);
4677
+ if (
4678
+ consumedSdkRunToken !== undefined &&
4679
+ this.#activeAttemptScope !== undefined &&
4680
+ promotion.startsOwnRun !== true
4681
+ ) {
4682
+ this.#sdkRunTokensByAttemptScope.set(this.#activeAttemptScope, consumedSdkRunToken);
4683
+ this.#skipPostPromptRecoveryWaitByAttemptScope.add(this.#activeAttemptScope);
4684
+ this.#activeSdkRunToken = consumedSdkRunToken;
4685
+ }
4092
4686
  this.#fireQueuedPromotionHooks(messages, promotion);
4093
4687
  };
4094
4688
  this.agent.providerSessionState = this.#providerSessionState;
@@ -5001,6 +5595,10 @@ export class AgentSession {
5001
5595
  * seen.
5002
5596
  */
5003
5597
  #coordinatorToolObservations = new WeakMap<object, CoordinatorToolObservation>();
5598
+ #agentEventAdmission = new WeakMap<
5599
+ object,
5600
+ { scope?: AttemptScope; sdkRunToken?: string; persistGeneration: number; persistBarrier?: Promise<void> }
5601
+ >();
5004
5602
 
5005
5603
  /**
5006
5604
  * Capture what is true at the SYNCHRONOUS agent-event boundary, before any async work.
@@ -5056,6 +5654,12 @@ export class AgentSession {
5056
5654
  }
5057
5655
 
5058
5656
  #trackAgentEvent = (event: AgentEvent): Promise<void> => {
5657
+ this.#agentEventAdmission.set(event, {
5658
+ scope: this.#activeAttemptScope,
5659
+ sdkRunToken: this.#activeSdkRunToken,
5660
+ persistGeneration: this.#coordinatorPersistGeneration,
5661
+ persistBarrier: this.#coordinatorRescopeBarrier,
5662
+ });
5059
5663
  // First statement of the listener: the observation must precede every claim,
5060
5664
  // reservation, and async hop this handler performs.
5061
5665
  this.#observeCoordinatorToolEvent(event);
@@ -5125,10 +5729,10 @@ export class AgentSession {
5125
5729
  this.#postPromptLeases.set(eventLease.resourceRunId, eventLease);
5126
5730
  if (eventLease) {
5127
5731
  await this.#runResourceLeaseContext.run(eventLease, () =>
5128
- this.#handleAgentEvent(event, activePromptHandle, canonicalAdmission),
5732
+ this.#handleAgentEvent(event, activePromptHandle, canonicalAdmission, eventLease),
5129
5733
  );
5130
5734
  } else {
5131
- await this.#handleAgentEvent(event, activePromptHandle, canonicalAdmission);
5735
+ await this.#handleAgentEvent(event, activePromptHandle, canonicalAdmission, undefined);
5132
5736
  }
5133
5737
  } catch (error) {
5134
5738
  logger.warn("Agent event handler failed", { event: event.type, error: String(error) });
@@ -5136,11 +5740,19 @@ export class AgentSession {
5136
5740
  canonicalAdmission?.release();
5137
5741
  if (eventLease) {
5138
5742
  const pendingAgentEnd =
5139
- event.type === "agent_end" && !maintenanceCheckpoint && this.#pendingAgentEndEmit === event
5743
+ event.type === "agent_end" &&
5744
+ !maintenanceCheckpoint &&
5745
+ (this.#pendingAgentEndEmit === event || this.#deferredAgentEndLeases.has(event))
5140
5746
  ? this.#pendingAgentEndEmit
5141
5747
  : undefined;
5142
5748
  if (pendingAgentEnd) {
5143
5749
  this.#deferredAgentEndLeases.set(pendingAgentEnd, eventLease);
5750
+ } else if (
5751
+ event.type === "agent_end" &&
5752
+ !maintenanceCheckpoint &&
5753
+ this.#deferredAgentEndLeases.has(event)
5754
+ ) {
5755
+ // Fast publication took ownership of the lease before the handler unwound.
5144
5756
  } else {
5145
5757
  if (event.type === "agent_end" && this.#postPromptLeases.get(eventLease.resourceRunId) === eventLease)
5146
5758
  this.#postPromptLeases.delete(eventLease.resourceRunId);
@@ -5156,6 +5768,11 @@ export class AgentSession {
5156
5768
  this.#resolveSessionSettlement();
5157
5769
  }
5158
5770
  })();
5771
+ this.#coordinatorEventHandlers.add(handler);
5772
+ void handler.then(
5773
+ () => this.#coordinatorEventHandlers.delete(handler),
5774
+ () => this.#coordinatorEventHandlers.delete(handler),
5775
+ );
5159
5776
  if (eventLease) eventLease.track("post_prompt", "agent-session-event", handler);
5160
5777
  return handler;
5161
5778
  };
@@ -5163,46 +5780,125 @@ export class AgentSession {
5163
5780
  /** Serializes sidecar writes in publication order, independent of write latency. */
5164
5781
  #coordinatorPersistQueue: Promise<void> = Promise.resolve();
5165
5782
 
5783
+ #recordPostPublicationOutcome(
5784
+ context: { sessionId: string; cwd: string; sessionFile: string | undefined },
5785
+ correlationId: string | undefined,
5786
+ kind: "worker_integration" | "terminal_persistence",
5787
+ outcome: { status: "completed" | "failed" | "timed_out"; error?: string },
5788
+ ): void {
5789
+ if (outcome.status !== "completed") {
5790
+ const correlation = correlationId ?? context.sessionId;
5791
+ const safeError = sanitizePostPublicationError(outcome.error);
5792
+ this.#emit({
5793
+ type: "notice",
5794
+ level: "error",
5795
+ source: kind,
5796
+ message: `${kind} ${outcome.status} for ${correlation}${safeError ? `: ${safeError}` : ""}`,
5797
+ });
5798
+ }
5799
+ const generation = this.#coordinatorPersistGeneration;
5800
+ const barrier = this.#coordinatorRescopeBarrier;
5801
+ const persist = async () => {
5802
+ if (barrier) {
5803
+ await barrier;
5804
+ return await persistCoordinatorWorkerIntegrationOutcome(
5805
+ {
5806
+ sessionId: this.sessionId,
5807
+ cwd: this.sessionManager.getCwd(),
5808
+ sessionFile: this.sessionManager.getSessionFile(),
5809
+ },
5810
+ {
5811
+ ...outcome,
5812
+ kind,
5813
+ correlationId,
5814
+ error: sanitizePostPublicationError(outcome.error),
5815
+ },
5816
+ );
5817
+ }
5818
+ return generation === this.#coordinatorPersistGeneration
5819
+ ? await persistCoordinatorWorkerIntegrationOutcome(context, {
5820
+ ...outcome,
5821
+ kind,
5822
+ correlationId,
5823
+ error: sanitizePostPublicationError(outcome.error),
5824
+ })
5825
+ : undefined;
5826
+ };
5827
+ const queued = barrier
5828
+ ? barrier.then(() => this.#appendCoordinatorPersist(persist))
5829
+ : this.#appendCoordinatorPersist(persist);
5830
+ if (barrier) this.#trackReleasedBarrierPersist(queued);
5831
+ else this.#trackUnbarrieredCoordinatorPersist(queued);
5832
+ void queued.catch(error =>
5833
+ logger.warn("Failed to persist terminal reconciliation outcome", { error: String(error) }),
5834
+ );
5835
+ }
5836
+
5166
5837
  /**
5167
5838
  * Reserve this event's place in the sidecar write order.
5168
5839
  *
5169
5840
  * Called immediately BEFORE local delivery so a synchronous subscriber that re-enters
5170
5841
  * the session cannot get its own, later event persisted first. The returned promise is
5171
- * only awaited where durability must precede delivery (terminal `agent_end`).
5842
+ * awaited only by callers that explicitly require a durable handoff; interactive
5843
+ * terminal delivery remains independent of this secondary sink.
5172
5844
  *
5173
5845
  * A pairing-only tool event takes no place in that order at all — neither its start nor
5174
5846
  * its end. It describes a call that was never dispatched, and this file is read as the
5175
5847
  * answer to "what is this session doing right now".
5176
5848
  */
5177
- #queueCoordinatorRuntimeStatePersist(event: AgentSessionEvent): Promise<void> {
5849
+ #queueCoordinatorRuntimeStatePersist(event: AgentSessionEvent, propagateFailure = false): Promise<void> {
5178
5850
  if (isNonDispatchedToolEvent(event)) return Promise.resolve();
5179
5851
  const observation = this.#coordinatorToolObservations.get(event);
5180
- const run = () => this.#persistRuntimeStateInBackground(event, observation);
5181
- const queued = this.#coordinatorPersistQueue.then(run, run);
5182
- this.#coordinatorPersistQueue = queued.catch(() => {});
5852
+ const admission = this.#agentEventAdmission.get(event);
5853
+ const barrier = admission?.persistBarrier;
5854
+ if (barrier) {
5855
+ const run = async () => {
5856
+ await barrier;
5857
+ await this.#persistRuntimeStateInBackground(
5858
+ event,
5859
+ {
5860
+ sessionId: this.sessionId,
5861
+ cwd: this.sessionManager.getCwd(),
5862
+ sessionFile: this.sessionManager.getSessionFile(),
5863
+ },
5864
+ observation,
5865
+ propagateFailure,
5866
+ );
5867
+ };
5868
+ const queued = barrier.then(() => this.#appendCoordinatorPersist(run));
5869
+ this.#trackReleasedBarrierPersist(queued);
5870
+ return queued;
5871
+ }
5872
+ const context = {
5873
+ sessionId: this.sessionId,
5874
+ cwd: this.sessionManager.getCwd(),
5875
+ sessionFile: this.sessionManager.getSessionFile(),
5876
+ };
5877
+ const generation = admission?.persistGeneration ?? this.#coordinatorPersistGeneration;
5878
+ const run = () =>
5879
+ generation === this.#coordinatorPersistGeneration
5880
+ ? this.#persistRuntimeStateInBackground(event, context, observation, propagateFailure)
5881
+ : Promise.resolve();
5882
+ const queued = this.#appendCoordinatorPersist(run);
5883
+ this.#trackUnbarrieredCoordinatorPersist(queued);
5183
5884
  return queued;
5184
5885
  }
5185
5886
 
5186
5887
  async #persistRuntimeStateInBackground(
5187
5888
  event: AgentSessionEvent,
5889
+ context: { sessionId: string; cwd: string; sessionFile: string | undefined },
5188
5890
  observation: CoordinatorToolObservation | undefined,
5891
+ propagateFailure: boolean,
5189
5892
  ): Promise<void> {
5190
5893
  try {
5191
- await persistCoordinatorRuntimeStateFromEvent(
5192
- event,
5193
- {
5194
- sessionId: this.sessionId,
5195
- cwd: this.sessionManager.getCwd(),
5196
- sessionFile: this.sessionManager.getSessionFile(),
5197
- },
5198
- observation,
5199
- );
5200
- } catch {
5894
+ await persistCoordinatorRuntimeStateFromEvent(event, context, observation);
5895
+ } catch (error) {
5201
5896
  logger.warn("Failed to persist coordinator runtime state", { event: event.type });
5897
+ if (propagateFailure) throw error;
5202
5898
  }
5203
5899
  }
5204
5900
 
5205
- async #emitSessionEvent(event: AgentSessionEvent): Promise<void> {
5901
+ async #emitSessionEvent(event: AgentSessionEvent, eventLease?: RunResourceProducerLease): Promise<void> {
5206
5902
  const attemptScope = (event as AgentSessionEvent & { scope?: AttemptScope }).scope;
5207
5903
  if (event.type === "turn_start") {
5208
5904
  this.#extensionTurnGeneration++;
@@ -5236,6 +5932,18 @@ export class AgentSession {
5236
5932
  // have unwound. Subscribers treat this event as the ready signal; flushing it
5237
5933
  // from abort while either barrier is active permits a successor to race the
5238
5934
  // prior prompt's cleanup.
5935
+ const sdkTerminal =
5936
+ event.type === "agent_end" && attemptScope !== undefined && this.#sdkRunTokensByAttemptScope.has(attemptScope);
5937
+ if (
5938
+ event.type === "agent_end" &&
5939
+ sdkTerminal &&
5940
+ this.#pendingAgentEndContinuationHolds.size === 0 &&
5941
+ this.#pendingAgentEndEmit === undefined
5942
+ ) {
5943
+ if (eventLease) this.#deferredAgentEndLeases.set(event, eventLease);
5944
+ this.#startAgentEndPublication(event, eventLease);
5945
+ return;
5946
+ }
5239
5947
  if (event.type === "agent_end" && (this.#livePromptsInFlight() > 0 || this.#agentEventHandlersInFlight > 0)) {
5240
5948
  this.#pendingAgentEndEmit = event;
5241
5949
  return;
@@ -5244,8 +5952,12 @@ export class AgentSession {
5244
5952
  if (event.type === "agent_end") {
5245
5953
  // Start the durable terminal write before synchronous subscribers can
5246
5954
  // re-enter prompt(), so a successor's running transition serializes after it.
5247
- await this.#queueCoordinatorRuntimeStatePersist(event);
5955
+ // Do not let a lock-hostile sidecar suppress the terminal event itself.
5956
+ const terminalPersistence = this.#queueCoordinatorRuntimeStatePersist(event);
5248
5957
  this.#emit(event);
5958
+ void terminalPersistence.catch(error => {
5959
+ logger.warn("Failed to persist terminal coordinator runtime state", { error: String(error) });
5960
+ });
5249
5961
  await this.#emitExtensionEvent(event);
5250
5962
  return;
5251
5963
  }
@@ -5341,6 +6053,7 @@ export class AgentSession {
5341
6053
  event: AgentEvent,
5342
6054
  activePromptHandle?: string,
5343
6055
  canonicalAdmission?: CanonicalMessageAdmission,
6056
+ eventLease?: RunResourceProducerLease,
5344
6057
  ): Promise<void> => {
5345
6058
  const attemptScope = (event as AgentEvent & { scope?: AttemptScope }).scope;
5346
6059
 
@@ -5680,7 +6393,7 @@ export class AgentSession {
5680
6393
  });
5681
6394
  }
5682
6395
 
5683
- await this.#emitSessionEvent(displayEvent);
6396
+ await this.#emitSessionEvent(displayEvent, eventLease);
5684
6397
  if (event.type === "message_end" && event.message.role === "assistant") {
5685
6398
  this.#assistantAttemptScopes.set(event.message, {
5686
6399
  scope: attemptScope,
@@ -6030,6 +6743,7 @@ export class AgentSession {
6030
6743
  skipCompactionCheck: true,
6031
6744
  resourceRunId: activePromptHandle,
6032
6745
  maintenanceContinuation: true,
6746
+ sdkRunToken: attemptScope ? this.#sdkRunTokensByAttemptScope.get(attemptScope) : undefined,
6033
6747
  });
6034
6748
  }
6035
6749
  return;
@@ -6126,9 +6840,11 @@ export class AgentSession {
6126
6840
  });
6127
6841
  }
6128
6842
  this.#resolveRetry();
6843
+ if (this.#isDisposed || this.#sessionAdmissionClosing) return;
6129
6844
 
6130
6845
  const compactionTask = this.#schedulePostPromptTask(
6131
6846
  async () => {
6847
+ if (this.#isDisposed || this.#sessionAdmissionClosing) return;
6132
6848
  await this.#checkCompaction(msg, true, undefined, activePromptHandle);
6133
6849
  },
6134
6850
  { resourceRunId: activePromptHandle },
@@ -6201,9 +6917,11 @@ export class AgentSession {
6201
6917
  selectionFenceGeneration: number,
6202
6918
  lease?: RunResourceProducerLease,
6203
6919
  leaseTask: Promise<void> = task,
6920
+ excludeFromRecovery = false,
6204
6921
  ): void {
6205
6922
  this.#postPromptTasks.add(task);
6206
6923
  this.#postPromptTaskSelectionFenceGenerations.set(task, selectionFenceGeneration);
6924
+ if (excludeFromRecovery) this.#postPromptTaskRecoveryExcluded.add(task);
6207
6925
  this.#ensurePostPromptTasksPromise();
6208
6926
  if (lease) {
6209
6927
  lease.track("post_prompt", "agent-session", leaseTask);
@@ -6214,6 +6932,7 @@ export class AgentSession {
6214
6932
  .finally(() => {
6215
6933
  this.#postPromptTasks.delete(task);
6216
6934
  this.#postPromptTaskSelectionFenceGenerations.delete(task);
6935
+ this.#postPromptTaskRecoveryExcluded.delete(task);
6217
6936
  if (this.#postPromptTasks.size === 0) this.#resolvePostPromptTasks();
6218
6937
  });
6219
6938
  }
@@ -6227,6 +6946,7 @@ export class AgentSession {
6227
6946
  resourceRunId?: string;
6228
6947
  leaseTask?: Promise<void>;
6229
6948
  selectionFenceGeneration?: number;
6949
+ excludeFromPostPromptRecovery?: boolean;
6230
6950
  },
6231
6951
  ): Promise<void> {
6232
6952
  const selectionFenceGeneration =
@@ -6253,7 +6973,11 @@ export class AgentSession {
6253
6973
  options?.onSkip?.();
6254
6974
  return Promise.resolve();
6255
6975
  }
6256
- const signal = reservation?.ok ? reservation.lease.signal : this.#postPromptTasksAbortController.signal;
6976
+ const taskSignal = reservation?.ok ? reservation.lease.signal : this.#postPromptTasksAbortController.signal;
6977
+ // Disposal is a session-wide terminal fence. Compose it with the selected
6978
+ // logical-resource signal so stale leases from earlier runs cannot keep a
6979
+ // post-prompt task alive after the current run was quarantined.
6980
+ const signal = AbortSignal.any([taskSignal, this.#disposeAbortController.signal]);
6257
6981
  const runScheduled = async () => {
6258
6982
  if (delayMs > 0) {
6259
6983
  try {
@@ -6281,6 +7005,7 @@ export class AgentSession {
6281
7005
  selectionFenceGeneration,
6282
7006
  reservation?.ok ? reservation.lease : undefined,
6283
7007
  options?.leaseTask,
7008
+ options?.excludeFromPostPromptRecovery,
6284
7009
  );
6285
7010
  return scheduled;
6286
7011
  }
@@ -6317,8 +7042,11 @@ export class AgentSession {
6317
7042
  predecessorAgentEndHold?: symbol;
6318
7043
  /** Internal predecessor terminal event sequestered while waiting behind selection. */
6319
7044
  deferredPredecessorAgentEnd?: AgentSessionEvent;
7045
+ /** Internal causal SDK owner captured when this continuation was scheduled. */
7046
+ sdkRunToken?: string;
6320
7047
  }): Promise<void> {
6321
7048
  const continuationAdmission = this.#captureScheduledContinuationAdmission();
7049
+ const scheduledSdkRunToken = options?.sdkRunToken;
6322
7050
  const selectionFenceGeneration =
6323
7051
  options?.selectionFenceGeneration ??
6324
7052
  this.#selectionFenceGenerationContext.getStore() ??
@@ -6340,6 +7068,7 @@ export class AgentSession {
6340
7068
  generation: deferredPromptGeneration,
6341
7069
  selectionFenceGeneration,
6342
7070
  deferredPredecessorAgentEnd,
7071
+ sdkRunToken: scheduledSdkRunToken,
6343
7072
  });
6344
7073
  } finally {
6345
7074
  // The recursive call synchronously re-reserved its settlement
@@ -6410,7 +7139,7 @@ export class AgentSession {
6410
7139
  settleLease();
6411
7140
  return;
6412
7141
  }
6413
- await this.#awaitStartupTurnBarrier();
7142
+ await this.#awaitStartupTurnBarrier(scheduledSignal);
6414
7143
  if (!canContinue()) {
6415
7144
  settleLease();
6416
7145
  return;
@@ -6475,18 +7204,18 @@ export class AgentSession {
6475
7204
  // agent_start (review P1); their queued messages are in-run
6476
7205
  // consumptions, not own-run promotions.
6477
7206
  const startsOwn = options?.maintenanceContinuation !== true;
6478
- const consumedSdkRunToken = startsOwn
6479
- ? acceptance.consumedQueuedMessages
6480
- .map(message => this.#sdkRunTokensByQueuedMessage.get(message))
6481
- .find((token): token is string => token !== undefined)
6482
- : undefined;
7207
+ const inheritedSdkRunToken = options?.continueQueuedOnly
7208
+ ? undefined
7209
+ : (options?.sdkRunToken ?? scheduledSdkRunToken);
7210
+ const consumedSdkRunToken = acceptance.consumedQueuedMessages
7211
+ .map(message => this.#sdkRunTokensByQueuedMessage.get(message))
7212
+ .find((token): token is string => token !== undefined);
7213
+ const sdkRunToken = consumedSdkRunToken ?? inheritedSdkRunToken;
6483
7214
  this.#fireQueuedPromotionHooks(acceptance.consumedQueuedMessages, {
6484
7215
  startsOwnRun: startsOwn,
6485
7216
  });
6486
- if (consumedSdkRunToken !== undefined)
6487
- this.#sdkRunTokensByAttemptScope.set(handle.scope, consumedSdkRunToken);
6488
- if (startsOwn) this.#activeSdkRunToken = consumedSdkRunToken;
6489
- this.#acceptRunHandle(handle);
7217
+ if (startsOwn) this.#activeSdkRunToken = sdkRunToken;
7218
+ this.#acceptSdkAttemptRun(handle, sdkRunToken);
6490
7219
  options?.onRunAccepted?.(handle);
6491
7220
  // Keep the queued token available through the acceptance callback;
6492
7221
  // SDK follow-up owners bind it to the new attempt scope there.
@@ -6649,8 +7378,12 @@ export class AgentSession {
6649
7378
  resourceRunId?: string,
6650
7379
  deferredSelectionFenceGeneration?: number,
6651
7380
  deferredPredecessorAgentEnd?: AgentSessionEvent,
7381
+ sdkRunToken?: string,
6652
7382
  ): void {
6653
7383
  const scheduledGeneration = generation;
7384
+ const scheduledSdkRunToken =
7385
+ sdkRunToken ??
7386
+ (this.#activeAttemptScope ? this.#sdkRunTokensByAttemptScope.get(this.#activeAttemptScope) : undefined);
6654
7387
  const continuationAuthorized = async (
6655
7388
  signal: AbortSignal,
6656
7389
  hasPendingNextTurnMessages = false,
@@ -6703,6 +7436,7 @@ export class AgentSession {
6703
7436
  resourceRunId,
6704
7437
  selectionFenceGeneration,
6705
7438
  predecessorAgentEnd,
7439
+ scheduledSdkRunToken,
6706
7440
  );
6707
7441
  } finally {
6708
7442
  this.#endSelectionFenceDeferralTracking(selectionFenceGeneration);
@@ -6750,6 +7484,7 @@ export class AgentSession {
6750
7484
  promptText,
6751
7485
  {
6752
7486
  skipPostPromptRecoveryWait: true,
7487
+ sdkRunToken: scheduledSdkRunToken,
6753
7488
  skipCompactionCheck: true,
6754
7489
  predecessorAgentEndHold,
6755
7490
  onFinalPreflight: ({ hasPendingNextTurnMessages }) =>
@@ -6808,6 +7543,7 @@ export class AgentSession {
6808
7543
  this.#postPromptTasksAbortController = new AbortController();
6809
7544
  this.#postPromptTasks.clear();
6810
7545
  this.#postPromptTaskSelectionFenceGenerations.clear();
7546
+ this.#postPromptTaskRecoveryExcluded.clear();
6811
7547
  this.#releaseDeferredAgentEndContinuations();
6812
7548
  this.#resolveTtsrResume();
6813
7549
  this.#resolvePostPromptTasks();
@@ -6829,8 +7565,11 @@ export class AgentSession {
6829
7565
  await this.#ttsrResumePromise;
6830
7566
  continue;
6831
7567
  }
6832
- if (this.#postPromptTasksPromise) {
6833
- await this.#postPromptTasksPromise;
7568
+ const recoveryTasks = [...this.#postPromptTasks].filter(
7569
+ task => !this.#postPromptTaskRecoveryExcluded.has(task),
7570
+ );
7571
+ if (recoveryTasks.length > 0) {
7572
+ await Promise.allSettled(recoveryTasks);
6834
7573
  continue;
6835
7574
  }
6836
7575
  // Tracked post-prompt tasks cover deferred continuations scheduled from
@@ -6852,7 +7591,9 @@ export class AgentSession {
6852
7591
  */
6853
7592
  async #waitForPostPromptTasksBeforeSelectionFence(selectionFenceGeneration: number): Promise<void> {
6854
7593
  const precedingTasks = [...this.#postPromptTasks].filter(
6855
- task => (this.#postPromptTaskSelectionFenceGenerations.get(task) ?? 0) < selectionFenceGeneration,
7594
+ task =>
7595
+ !this.#postPromptTaskRecoveryExcluded.has(task) &&
7596
+ (this.#postPromptTaskSelectionFenceGenerations.get(task) ?? 0) < selectionFenceGeneration,
6856
7597
  );
6857
7598
  if (precedingTasks.length > 0) await Promise.allSettled(precedingTasks);
6858
7599
  }
@@ -7527,16 +8268,16 @@ export class AgentSession {
7527
8268
  this.#workerIntegrationScheduler?.enqueue();
7528
8269
  }
7529
8270
 
7530
- async #flushWorkerIntegrationAttempt(): Promise<void> {
7531
- await this.#workerIntegrationScheduler?.flush();
8271
+ async #flushWorkerIntegrationAttempt(): Promise<WorkerIntegrationOutcome> {
8272
+ return (await this.#workerIntegrationScheduler?.flushWithOutcome()) ?? { status: "completed" };
7532
8273
  }
7533
8274
 
7534
- async #flushWorkerIntegrationForAgentEnd(): Promise<void> {
8275
+ async #flushWorkerIntegrationForAgentEnd(): Promise<WorkerIntegrationOutcome> {
7535
8276
  if (!this.#workerIntegrationRequestedForTurn) {
7536
8277
  this.#requestWorkerIntegrationAttempt();
7537
8278
  }
7538
8279
  try {
7539
- await this.#flushWorkerIntegrationAttempt();
8280
+ return await this.#flushWorkerIntegrationAttempt();
7540
8281
  } finally {
7541
8282
  this.#workerIntegrationRequestedForTurn = false;
7542
8283
  }
@@ -7549,6 +8290,9 @@ export class AgentSession {
7549
8290
  workerIntegrationSettled = false,
7550
8291
  scope?: AttemptScopeRef,
7551
8292
  ): Promise<void> {
8293
+ const admission = this.#agentEventAdmission.get(event);
8294
+ const activeAttemptScopeAtEvent = admission === undefined ? this.#activeAttemptScope : admission.scope;
8295
+ const activeSdkRunTokenAtEvent = admission === undefined ? this.#activeSdkRunToken : admission.sdkRunToken;
7552
8296
  if (event.type === "agent_end" && !workerIntegrationSettled) {
7553
8297
  await this.#flushWorkerIntegrationForAgentEnd();
7554
8298
  }
@@ -7559,20 +8303,36 @@ export class AgentSession {
7559
8303
  ? eventToken
7560
8304
  : deliveryScope
7561
8305
  ? this.#sdkRunTokensByAttemptScope.get(deliveryScope as AttemptScope)
7562
- : this.#activeSdkRunToken;
8306
+ : undefined;
7563
8307
  const isTerminalAgentEnd =
7564
8308
  event.type === "agent_end" && !(event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted");
7565
8309
  const finishAttempt = () => {
7566
8310
  if (!isTerminalAgentEnd) return;
8311
+ // A merged wake batch whose flush fired while still streaming cleared its
8312
+ // pending flag without rescheduling. Returning to idle is exactly when it
8313
+ // becomes deliverable again, so rearm here rather than waiting for an
8314
+ // unrelated enqueue.
8315
+ this.yieldQueue.rearmIdle();
7567
8316
  const isActiveAttempt =
7568
- deliveryScope === undefined ||
7569
- this.#activeAttemptScope === undefined ||
7570
- deliveryScope === this.#activeAttemptScope;
8317
+ deliveryScope === undefined
8318
+ ? this.#activeAttemptScope === activeAttemptScopeAtEvent &&
8319
+ this.#activeSdkRunToken === activeSdkRunTokenAtEvent
8320
+ : deliveryScope === activeAttemptScopeAtEvent &&
8321
+ deliveryScope === this.#activeAttemptScope &&
8322
+ this.#activeSdkRunToken === activeSdkRunTokenAtEvent;
8323
+ if (this.#foldStopRequested) {
8324
+ this.#foldStopRequested = false;
8325
+ this.agent.setSteeringAdmissionFence(undefined);
8326
+ }
7571
8327
  if (deliveryScope) {
7572
8328
  this.#attemptRecordStore.retire(deliveryScope as AttemptScope);
7573
- this.#sdkRunTokensByAttemptScope.delete(deliveryScope as AttemptScope);
8329
+ if (
8330
+ activeSdkRunTokenAtEvent !== undefined &&
8331
+ this.#sdkRunTokensByAttemptScope.get(deliveryScope as AttemptScope) === activeSdkRunTokenAtEvent
8332
+ )
8333
+ this.#sdkRunTokensByAttemptScope.delete(deliveryScope as AttemptScope);
7574
8334
  }
7575
- if (isActiveAttempt && sdkRunToken === this.#activeSdkRunToken) this.#activeSdkRunToken = undefined;
8335
+ if (isActiveAttempt) this.#activeSdkRunToken = undefined;
7576
8336
  if (isActiveAttempt) {
7577
8337
  this.#activeAttemptScope = undefined;
7578
8338
  this.#activeLogicalRunId = undefined;
@@ -7840,8 +8600,18 @@ export class AgentSession {
7840
8600
  const sid = this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
7841
8601
  this.agent.sessionId = sid;
7842
8602
  this.agent.providerSessionId = this.#providerCacheSessionId ?? sid;
7843
- this.agent.setMetadataResolver((provider: string) =>
7844
- buildSessionMetadata(sid, provider, this.#modelRegistry.authStorage, this.credentialSessionId),
8603
+ this.agent.setMetadataResolver(context =>
8604
+ buildSessionMetadata(
8605
+ sid,
8606
+ context.provider,
8607
+ this.#modelRegistry.authStorage,
8608
+ this.credentialSessionId,
8609
+ context.model ?? this.model,
8610
+ // Optional call: the metadata resolver runs on every prompt, and `owner` is an
8611
+ // optional `buildSessionMetadata` argument. The auth-credential paths keep the
8612
+ // strict call because they genuinely require a real registry owner.
8613
+ this.#modelRegistry.getAuthStorageOwner?.(),
8614
+ ),
7845
8615
  );
7846
8616
  }
7847
8617
 
@@ -7945,13 +8715,35 @@ export class AgentSession {
7945
8715
  if (this.#disposePromise) return this.#disposePromise;
7946
8716
  const { promise, resolve, reject } = Promise.withResolvers<void>();
7947
8717
  this.#disposePromise = promise;
8718
+ this.#abortAdmissionEpoch++;
8719
+ this.#isDisposed = true;
8720
+ this.#disposeAbortController.abort();
8721
+ this.#disposeAdmissionClosed = this.#closeSessionAdmission();
8722
+ this.#disposePostPromptDrain = this.#cancelPostPromptTasks();
8723
+ this.#settleDeliveredOwnedRegistrations(this.#pendingNextTurnMessages.map(entry => entry.message));
8724
+ this.#pendingNextTurnMessages = [];
8725
+ this.#scheduledHiddenNextTurnGeneration = undefined;
8726
+ this.#abortActiveMidRunBarriers();
8727
+ this.abortCompaction();
8728
+ this.abortRetry();
8729
+ this.#quarantineAgentRunResources();
8730
+ this.agent.abort();
8731
+ this.agent.setMainAttemptScopeObserver(undefined);
8732
+ this.#disconnectFromAgent();
7948
8733
  void this.#dispose().then(resolve, reject);
7949
8734
  return promise;
7950
8735
  }
7951
8736
 
8737
+ /** Cancel the active logical run domain, including managed continuations detached from Agent's active attempt. */
8738
+ #quarantineAgentRunResources(): void {
8739
+ const resourceRunId = this.agent.currentManagedLogicalRunId ?? this.agent.activeResourceRunId;
8740
+ if (resourceRunId !== undefined) this.agent.resourceLedger.quarantine(String(resourceRunId));
8741
+ }
8742
+
7952
8743
  async #dispose(): Promise<void> {
7953
- await this.sessionManager.joinCwdTransition();
7954
- const admissionClosed = this.#closeSessionAdmission();
8744
+ const admissionClosed = this.#disposeAdmissionClosed ?? this.#closeSessionAdmission();
8745
+ await admissionClosed;
8746
+ await this.sessionManager.closeCwdMoveAdmission();
7955
8747
  this.#isDisposed = true;
7956
8748
  // Reject new direct Python starts as soon as disposal begins (synchronously,
7957
8749
  // before any await) so callers cannot race a start against teardown.
@@ -7989,18 +8781,22 @@ export class AgentSession {
7989
8781
  this.agent.forceAbort("Session disposed");
7990
8782
  }
7991
8783
  }
7992
- await admissionClosed;
7993
8784
  await this.#agentEndPublicationPromise;
7994
8785
  await this.#queuedExtensionEvents;
8786
+ await this.#agentEndHandlingPromise;
7995
8787
  // Drain the sidecar write order for the same reason the two queues above are
7996
8788
  // drained: each entry writes under the native identity-bound state-file lock, so a
7997
- // still-queued write would run after the session that owns it is gone — releasing
8789
+ // a still-queued write would run after the session that owns it is gone — releasing
7998
8790
  // a lock whose owner no longer exists, and under `bun test --isolate` calling into
7999
8791
  // the addon after the runtime tore down the context it was scheduled in. The chain
8000
8792
  // is already failure-absorbing, so this only waits.
8001
8793
  await this.#coordinatorPersistQueue;
8794
+ // Terminal publication records its reconciliation from a promise reaction, which
8795
+ // can append after the queue snapshot above was awaited. Drain the tracked writes
8796
+ // again so disposal cannot return while that late reconciliation still owns a lock.
8797
+ await this.#drainUnbarrieredCoordinatorPersists();
8002
8798
  this.#pendingBackgroundExchanges = [];
8003
- this.yieldQueue.clear();
8799
+ this.#drainTerminalOwnedYieldEntries();
8004
8800
 
8005
8801
  this.agent.setOnBeforeYield(undefined);
8006
8802
  try {
@@ -8014,12 +8810,25 @@ export class AgentSession {
8014
8810
  this.#workflowGateEmitter = undefined;
8015
8811
  this.#notifyWorkflowGateEmitterChanged(this.sessionId, undefined);
8016
8812
  await this.#flushWorkerIntegrationAttempt();
8017
- await this.#cancelPostPromptTasks();
8813
+ // Worker integration completion can enqueue terminal reconciliation while the
8814
+ // flush above is pending; drain that late producer before disposal resolves.
8815
+ await this.#coordinatorPersistQueue;
8816
+ await this.#drainUnbarrieredCoordinatorPersists();
8817
+ await (this.#disposePostPromptDrain ?? this.#cancelPostPromptTasks());
8018
8818
  // Cancel jobs this agent registered so a subagent's teardown doesn't
8019
8819
  // leak its background bash/task work into the parent's manager. Only
8020
8820
  // the session that owns the manager goes on to dispose it (which itself
8021
8821
  // nukes any leftover jobs and pending deliveries).
8022
8822
  this.#cancelOwnAsyncJobs();
8823
+ // Final drain (Codex review P2 on #5088): an async-job completion that
8824
+ // enqueued during the awaited session_shutdown window above survived the
8825
+ // first drain with a retained delivery claim, and the already-aborted
8826
+ // disposal signal makes its idle-flush task exit before YieldQueue.flush
8827
+ // can run. Producers are now cancelled, so drain once more to settle any
8828
+ // straggler envelope and release its claim before owned-manager teardown
8829
+ // — a shared manager would otherwise keep the claim past this session's
8830
+ // disposal.
8831
+ this.#drainTerminalOwnedYieldEntries();
8023
8832
  await Promise.allSettled(this.#deferredOwnerShutdownFinalizations);
8024
8833
  const ownedAsyncManager = this.#ownedAsyncJobManager;
8025
8834
  if (ownedAsyncManager && this.#disposeAsyncJobManager) {
@@ -8056,6 +8865,14 @@ export class AgentSession {
8056
8865
  this.#unregisterSessionMemorySettings = undefined;
8057
8866
  if (ownerTerminalContextFromEnvironment() === null) this.#unregisterRuntimeStateFinalizer?.();
8058
8867
  this.#unregisterRuntimeStateFinalizer = undefined;
8868
+ this.#unregisterBeforeMoveListener?.();
8869
+ this.#unregisterBeforeMoveListener = undefined;
8870
+ this.#unregisterMoveAbortListener?.();
8871
+ this.#unregisterMoveAbortListener = undefined;
8872
+ this.#unregisterMovePublicationListener?.();
8873
+ this.#unregisterMovePublicationListener = undefined;
8874
+ this.#unregisterAfterMoveListener?.();
8875
+ this.#unregisterAfterMoveListener = undefined;
8059
8876
  await releaseTabsForOwner(this.sessionManager.getSessionId()).catch((error: unknown) =>
8060
8877
  logger.warn("session dispose: releaseTabsForOwner failed", { error }),
8061
8878
  );
@@ -8220,14 +9037,24 @@ export class AgentSession {
8220
9037
  await this.#waitForPostPromptTasksBeforeSelectionFence(ignoreSelectionFenceGeneration);
8221
9038
  }
8222
9039
  await this.#waitForSessionSettlement(ignoreSelectionFenceGeneration);
9040
+ const hasBlockingPostPromptTasks =
9041
+ ignoreSelectionFenceGeneration === undefined
9042
+ ? this.#postPromptTasks.size > 0
9043
+ : [...this.#postPromptTasks].some(
9044
+ task =>
9045
+ !this.#postPromptTaskRecoveryExcluded.has(task) &&
9046
+ (this.#postPromptTaskSelectionFenceGenerations.get(task) ?? 0) < ignoreSelectionFenceGeneration,
9047
+ );
8223
9048
  if (
8224
9049
  !this.agent.state.isStreaming &&
8225
9050
  !this.#retryPromise &&
8226
9051
  !this.#ttsrResumePromise &&
8227
- !this.#postPromptTasksPromise &&
9052
+ !hasBlockingPostPromptTasks &&
8228
9053
  !this.#isSessionSettlementPending(ignoreSelectionFenceGeneration)
8229
9054
  )
8230
9055
  return;
9056
+ if (ignoreSelectionFenceGeneration === undefined && hasBlockingPostPromptTasks && this.#postPromptTasksPromise)
9057
+ await this.#postPromptTasksPromise;
8231
9058
  }
8232
9059
  }
8233
9060
 
@@ -8252,6 +9079,35 @@ export class AgentSession {
8252
9079
  await this.sessionManager.flush();
8253
9080
  }
8254
9081
 
9082
+ queueCoordinatorRuntimeStatePersistForTests(event: AgentSessionEvent, gate: Promise<void>): Promise<void> {
9083
+ this.#agentEventAdmission.set(event, {
9084
+ persistGeneration: this.#coordinatorPersistGeneration,
9085
+ persistBarrier: this.#coordinatorRescopeBarrier,
9086
+ });
9087
+ const task = (async () => {
9088
+ await gate;
9089
+ await this.#queueCoordinatorRuntimeStatePersist(event, true);
9090
+ })();
9091
+ this.#coordinatorEventHandlers.add(task);
9092
+ void task.then(
9093
+ () => this.#coordinatorEventHandlers.delete(task),
9094
+ () => this.#coordinatorEventHandlers.delete(task),
9095
+ );
9096
+ return task;
9097
+ }
9098
+
9099
+ parkAgentEndForCoordinatorPersistForTests(event: Extract<AgentSessionEvent, { type: "agent_end" }>): void {
9100
+ this.#agentEventAdmission.set(event, {
9101
+ persistGeneration: this.#coordinatorPersistGeneration,
9102
+ persistBarrier: this.#coordinatorRescopeBarrier,
9103
+ });
9104
+ this.#pendingAgentEndEmit = event;
9105
+ }
9106
+
9107
+ flushParkedAgentEndForCoordinatorPersistForTests(): void {
9108
+ this.#flushPendingAgentEnd();
9109
+ }
9110
+
8255
9111
  async drainAsyncJobDeliveriesForAcp(options?: { timeoutMs?: number }): Promise<boolean> {
8256
9112
  const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
8257
9113
  if (!manager) return false;
@@ -8454,13 +9310,39 @@ export class AgentSession {
8454
9310
  * handler, so Ctrl+B-style folding fails closed instead of aborting or
8455
9311
  * shell-suspending arbitrary work.
8456
9312
  */
8457
- registerForegroundBashBackgroundRequestHandler(handler: () => void): () => void {
8458
- this.#foregroundBashBackgroundRequestHandler = handler;
8459
- return () => {
8460
- if (this.#foregroundBashBackgroundRequestHandler === handler) {
8461
- this.#foregroundBashBackgroundRequestHandler = undefined;
8462
- }
8463
- };
9313
+ registerForegroundFoldParticipant(adapter: FoldAdapter): () => void {
9314
+ return this.#foldCoordinator.registerParticipant(adapter);
9315
+ }
9316
+
9317
+ /** The session-owned fold coordinator, for delivery-side receipt lookup. */
9318
+ get foldCoordinator(): FoldCoordinator {
9319
+ return this.#foldCoordinator;
9320
+ }
9321
+
9322
+ /**
9323
+ * Best-effort capture of what the interrupted turn still intended to do, so
9324
+ * the wake turn can finish the original task instead of only reporting output.
9325
+ */
9326
+ #captureRemainingIntentForFold(): string | undefined {
9327
+ const messages = this.agent.state.messages;
9328
+ for (let index = messages.length - 1; index >= 0; index--) {
9329
+ const message = messages[index];
9330
+ if (message?.role !== "user") continue;
9331
+ const content = message.content;
9332
+ const text =
9333
+ typeof content === "string"
9334
+ ? content
9335
+ : Array.isArray(content)
9336
+ ? content
9337
+ .map(block => (typeof block === "object" && block && "text" in block ? String(block.text) : ""))
9338
+ .filter(Boolean)
9339
+ .join("\n")
9340
+ : "";
9341
+ const trimmed = text.trim();
9342
+ if (trimmed.length === 0) return undefined;
9343
+ return trimmed.length > 2000 ? `${trimmed.slice(0, 2000)}…` : trimmed;
9344
+ }
9345
+ return undefined;
8464
9346
  }
8465
9347
 
8466
9348
  /**
@@ -8469,7 +9351,7 @@ export class AgentSession {
8469
9351
  * no fold target exists.
8470
9352
  */
8471
9353
  hasForegroundBashBackgroundRequestHandler(): boolean {
8472
- return this.#foregroundBashBackgroundRequestHandler !== undefined;
9354
+ return this.#foldCoordinator.hasFoldableParticipant();
8473
9355
  }
8474
9356
 
8475
9357
  /** Set the SDK permission policy used by guarded ACP tool execution. */
@@ -8505,11 +9387,17 @@ export class AgentSession {
8505
9387
  * Ask the active managed foreground bash call to return as a background job.
8506
9388
  * Returns false when no supported foreground tool is currently backgroundable.
8507
9389
  */
8508
- requestForegroundBashBackground(): boolean {
8509
- const handler = this.#foregroundBashBackgroundRequestHandler;
8510
- if (!handler) return false;
8511
- handler();
8512
- return true;
9390
+ async requestForegroundBashBackground(): Promise<boolean> {
9391
+ if (!this.#foldCoordinator.hasFoldableParticipant()) return false;
9392
+ try {
9393
+ const result = await this.#foldCoordinator.requestFold();
9394
+ return result.status === "folded";
9395
+ } catch (error) {
9396
+ logger.warn("Foreground fold request failed", {
9397
+ error: error instanceof Error ? error.message : String(error),
9398
+ });
9399
+ return false;
9400
+ }
8513
9401
  }
8514
9402
 
8515
9403
  /**
@@ -9450,6 +10338,7 @@ export class AgentSession {
9450
10338
  if (this.#sessionAdmissionClosed || this.#isDisposed) throw this.#sessionAdmissionBusyError();
9451
10339
  const inFlightPrompt = this.#beginInFlight();
9452
10340
  let hindsightRecall: string | undefined;
10341
+ let continuationSdkRunToken: string | undefined;
9453
10342
  try {
9454
10343
  const volatileProjectContextMessage = await this.#buildVolatileProjectContextMessage();
9455
10344
  this.agent.appendMessage(volatileProjectContextMessage);
@@ -9485,14 +10374,18 @@ export class AgentSession {
9485
10374
  await this.agent.continue({
9486
10375
  ...this.#managedFallbackPromptOptions(),
9487
10376
  onRunAccepted: (handle: AttemptRunHandle) => {
9488
- this.#acceptRunHandle(handle);
10377
+ continuationSdkRunToken = this.#activeSdkRunToken;
10378
+ this.#acceptSdkAttemptRun(handle, continuationSdkRunToken);
9489
10379
  if (hindsightRecall) hindsightState?.markRecallSnippetInjected(hindsightRecall);
9490
10380
  },
9491
10381
  });
9492
- await this.#waitForPostPromptRecovery();
10382
+ if (continuationSdkRunToken === undefined) await this.#waitForPostPromptRecovery();
9493
10383
  } finally {
9494
10384
  this.#removeEphemeralCustomMessages();
9495
- await this.#settleEndedInFlight(inFlightPrompt);
10385
+ await this.#settleEndedInFlight(
10386
+ inFlightPrompt,
10387
+ continuationSdkRunToken === undefined ? "full" : "publication",
10388
+ );
9496
10389
  }
9497
10390
  }
9498
10391
 
@@ -9620,8 +10513,9 @@ export class AgentSession {
9620
10513
  async setCredentialPin(provider: string, selector: AuthCredentialSelector): Promise<void> {
9621
10514
  const scopeId = this.credentialSessionId;
9622
10515
  const authStorage = this.#modelRegistry.authStorage;
9623
- const target = authStorage.resolveOAuthPinTarget(provider, selector);
9624
- authStorage.setSessionCredentialSelector(scopeId, provider, target.canonicalSelector);
10516
+ const authStorageOwner = this.#modelRegistry.getAuthStorageOwner();
10517
+ const target = authStorage.resolveOAuthPinTarget(provider, selector, authStorageOwner);
10518
+ authStorage.setSessionCredentialSelector(scopeId, provider, target.canonicalSelector, authStorageOwner);
9625
10519
  if (target.canonicalSelector.kind === "id" && !this.#credentialStoreIdentity) return;
9626
10520
  this.sessionManager.appendCustomEntry("auth-credential-pin", {
9627
10521
  v: 1,
@@ -9701,9 +10595,13 @@ export class AgentSession {
9701
10595
  args = "",
9702
10596
  options?: Pick<
9703
10597
  PromptOptions,
9704
- "onPreflightAccepted" | "onPreflightAcceptCommit" | "onSkillPrepared" | "preflightSignal" | "sdkRunToken"
10598
+ "onPreflightAccepted" | "onPreflightAcceptCommit" | "onSkillPrepared" | "preflightSignal" | "sdkRunCapability"
9705
10599
  >,
9706
10600
  ): Promise<{ name: string; path: string; args?: string; lineCount?: number }> {
10601
+ const internalOptions = {
10602
+ ...options,
10603
+ sdkRunToken: readSdkRunCapability(options?.sdkRunCapability),
10604
+ };
9707
10605
  if (options?.preflightSignal?.aborted) throw promptPreflightCancelledError();
9708
10606
  const skillName = name.trim();
9709
10607
  if (!skillName) throw Object.assign(new Error("skill.invoke requires a skill name."), { code: "invalid_input" });
@@ -9749,7 +10647,7 @@ export class AgentSession {
9749
10647
  lineCount: built.details.lineCount,
9750
10648
  cleanedArgs: activation.cleanedArgs || undefined,
9751
10649
  });
9752
- await this.promptCustomMessage(skillPromptMessage, options);
10650
+ await this.promptCustomMessage(skillPromptMessage, internalOptions as InternalCustomMessageOptions);
9753
10651
  return {
9754
10652
  name: skill.name,
9755
10653
  path: skill.filePath,
@@ -10439,6 +11337,27 @@ export class AgentSession {
10439
11337
  * @throws Error if no model selected or no API key available (when not streaming)
10440
11338
  */
10441
11339
  async prompt(text: string, options?: PromptOptions): Promise<void> {
11340
+ const releaseStartupPromptWaiter = this.#reserveStartupPromptWaiter();
11341
+ try {
11342
+ await this.#promptInternal(text, options, releaseStartupPromptWaiter);
11343
+ } finally {
11344
+ releaseStartupPromptWaiter();
11345
+ }
11346
+ }
11347
+
11348
+ async #promptInternal(
11349
+ text: string,
11350
+ options: PromptOptions | undefined,
11351
+ releaseStartupPromptWaiter: () => void,
11352
+ ): Promise<void> {
11353
+ const hasUsableImage =
11354
+ options?.images?.some(image => typeof image?.data === "string" && image.data.trim().length > 0) === true;
11355
+ if (typeof text !== "string" || (text.trim().length === 0 && !hasUsableImage))
11356
+ throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
11357
+ const sdkRunToken = readSdkRunCapability(options?.sdkRunCapability);
11358
+ const internalOptions: InternalPromptOptions | undefined = options
11359
+ ? { ...options, ...(sdkRunToken ? { sdkRunToken } : {}) }
11360
+ : undefined;
10442
11361
  this.#assertRecoveryHydrationPromoted();
10443
11362
  const owner = this.#sessionAdmissionContext.getStore();
10444
11363
  if (owner && !owner.released) throw this.#sessionAdmissionBusyError();
@@ -10453,14 +11372,19 @@ export class AgentSession {
10453
11372
  await this.invokeSkill(
10454
11373
  invocation.skill.name,
10455
11374
  invocation.args,
10456
- options?.onPreflightAccepted || options?.onPreflightAcceptCommit || options?.preflightSignal
11375
+ options?.onPreflightAccepted ||
11376
+ options?.onPreflightAcceptCommit ||
11377
+ options?.preflightSignal ||
11378
+ internalOptions?.sdkRunToken
10457
11379
  ? {
10458
- ...(options.onPreflightAccepted ? { onPreflightAccepted: options.onPreflightAccepted } : {}),
10459
- ...(options.onPreflightAcceptCommit
11380
+ ...(options?.onPreflightAccepted
11381
+ ? { onPreflightAccepted: options.onPreflightAccepted }
11382
+ : {}),
11383
+ ...(options?.onPreflightAcceptCommit
10460
11384
  ? { onPreflightAcceptCommit: options.onPreflightAcceptCommit }
10461
11385
  : {}),
10462
- ...(options.preflightSignal ? { preflightSignal: options.preflightSignal } : {}),
10463
- ...(options.sdkRunToken ? { sdkRunToken: options.sdkRunToken } : {}),
11386
+ ...(options?.preflightSignal ? { preflightSignal: options.preflightSignal } : {}),
11387
+ ...(options?.sdkRunCapability ? { sdkRunCapability: options.sdkRunCapability } : {}),
10464
11388
  }
10465
11389
  : undefined,
10466
11390
  );
@@ -10494,6 +11418,10 @@ export class AgentSession {
10494
11418
 
10495
11419
  // Expand file-based prompt templates if requested
10496
11420
  const expandedText = expandPromptTemplates ? expandPromptTemplate(text, [...this.#promptTemplates]) : text;
11421
+ const expandedHasUsableImage =
11422
+ options?.images?.some(image => typeof image?.data === "string" && image.data.trim().length > 0) === true;
11423
+ if (expandedText.trim().length === 0 && !expandedHasUsableImage)
11424
+ throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
10497
11425
  assertImagePlaceholdersHavePayload(expandedText, options?.images);
10498
11426
  const workflowIntentDiff = options?.synthetic ? null : buildWorkflowIntentDiff(expandedText);
10499
11427
  const claimsGenuineUserIntent = !options?.synthetic && options?.attribution !== "agent";
@@ -10567,7 +11495,12 @@ export class AgentSession {
10567
11495
  attribution: promptAttribution,
10568
11496
  timestamp: Date.now(),
10569
11497
  }
10570
- : { role: "user" as const, content: userContent, attribution: promptAttribution, timestamp: Date.now() };
11498
+ : {
11499
+ role: "user" as const,
11500
+ content: userContent,
11501
+ attribution: promptAttribution,
11502
+ timestamp: Date.now(),
11503
+ };
10571
11504
  if (deepInterviewUserIntentEpoch !== undefined)
10572
11505
  this.#deepInterviewGenuineUserMessageEpochs.set(message, deepInterviewUserIntentEpoch);
10573
11506
  await this.refreshGjcSubskillTools();
@@ -10595,6 +11528,8 @@ export class AgentSession {
10595
11528
  }
10596
11529
  },
10597
11530
  options?.preflightSignal,
11531
+ undefined,
11532
+ { startupPromptWaiterRelease: releaseStartupPromptWaiter },
10598
11533
  );
10599
11534
  }
10600
11535
 
@@ -10714,9 +11649,13 @@ export class AgentSession {
10714
11649
  | "onPreflightAccepted"
10715
11650
  | "onPreflightAcceptCommit"
10716
11651
  | "preflightSignal"
10717
- | "sdkRunToken"
11652
+ | "sdkRunCapability"
10718
11653
  >,
10719
11654
  ): Promise<void> {
11655
+ const sdkRunToken = readSdkRunCapability(options?.sdkRunCapability);
11656
+ const internalOptions: InternalCustomMessageOptions | undefined = options
11657
+ ? { ...options, ...(sdkRunToken ? { sdkRunToken } : {}) }
11658
+ : undefined;
10720
11659
  if (options?.preflightSignal?.aborted) throw promptPreflightCancelledError();
10721
11660
  const textContent =
10722
11661
  typeof message.content === "string"
@@ -10785,7 +11724,7 @@ export class AgentSession {
10785
11724
  };
10786
11725
  try {
10787
11726
  await this.#promptWithMessage(customMessage, textContent, {
10788
- ...options,
11727
+ ...internalOptions,
10789
11728
  onPreflightAccepted: undefined,
10790
11729
  onPreflightAcceptCommit: commitAcceptance,
10791
11730
  admissionLease: admission,
@@ -10816,8 +11755,8 @@ export class AgentSession {
10816
11755
  | "onPreflightAccepted"
10817
11756
  | "onPreflightAcceptCommit"
10818
11757
  | "preflightSignal"
10819
- | "sdkRunToken"
10820
11758
  > & {
11759
+ sdkRunToken?: string;
10821
11760
  prependMessages?: AgentMessage[];
10822
11761
  skipPostPromptRecoveryWait?: boolean;
10823
11762
  predecessorAgentEndHold?: symbol;
@@ -10884,6 +11823,9 @@ export class AgentSession {
10884
11823
  const rosterClaim = this.#claimIrcRosterCandidate();
10885
11824
  let hasPendingNextTurnMessages = false;
10886
11825
  let pendingNextTurnMessageCount = 0;
11826
+ let skipPostPromptRecoveryWait =
11827
+ options?.skipPostPromptRecoveryWait === true || options?.sdkRunToken !== undefined;
11828
+ let promptAttemptScope: AttemptScope | undefined;
10887
11829
  let hindsightRecall: string | undefined;
10888
11830
  try {
10889
11831
  this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
@@ -11152,9 +12094,10 @@ export class AgentSession {
11152
12094
  ...this.#managedFallbackPromptOptions(),
11153
12095
  ...(options?.sdkRunToken ? { sdkRunToken: options.sdkRunToken } : {}),
11154
12096
  onRunAccepted: (handle: AttemptRunHandle) => {
11155
- this.#acceptRunHandle(handle);
11156
- if (options?.sdkRunToken) {
11157
- this.#sdkRunTokensByAttemptScope.set(handle.scope, options.sdkRunToken);
12097
+ promptAttemptScope = handle?.scope;
12098
+ if (handle) this.#acceptSdkAttemptRun(handle, options?.sdkRunToken);
12099
+ else this.#acceptRunHandle(handle);
12100
+ if (handle && options?.sdkRunToken) {
11158
12101
  this.#activeSdkRunToken = options.sdkRunToken;
11159
12102
  }
11160
12103
  options?.onRunAccepted?.(handle);
@@ -11189,6 +12132,20 @@ export class AgentSession {
11189
12132
  options?.onPreflightAccepted?.();
11190
12133
  },
11191
12134
  });
12135
+ const activeTerminalScope = this.#activeAttemptScope;
12136
+ const terminalAttemptScope =
12137
+ activeTerminalScope !== undefined &&
12138
+ (this.#skipPostPromptRecoveryWaitByAttemptScope.has(activeTerminalScope) ||
12139
+ (options?.sdkRunToken !== undefined &&
12140
+ this.#sdkRunTokensByAttemptScope.get(activeTerminalScope) === options.sdkRunToken))
12141
+ ? activeTerminalScope
12142
+ : promptAttemptScope;
12143
+ if (
12144
+ terminalAttemptScope !== undefined &&
12145
+ this.#skipPostPromptRecoveryWaitByAttemptScope.delete(terminalAttemptScope)
12146
+ ) {
12147
+ skipPostPromptRecoveryWait = true;
12148
+ }
11192
12149
  const terminalAssistant = this.#findLastAssistantMessage();
11193
12150
  if (
11194
12151
  rosterClaim &&
@@ -11198,7 +12155,7 @@ export class AgentSession {
11198
12155
  ) {
11199
12156
  this.#commitIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
11200
12157
  }
11201
- if (!options?.skipPostPromptRecoveryWait) {
12158
+ if (!skipPostPromptRecoveryWait) {
11202
12159
  await this.#waitForPostPromptRecovery();
11203
12160
  }
11204
12161
  } catch (error) {
@@ -11223,7 +12180,7 @@ export class AgentSession {
11223
12180
  this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
11224
12181
  }
11225
12182
  this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
11226
- await this.#settleEndedInFlight(inFlightPrompt, options?.skipPostPromptRecoveryWait ? "publication" : "full");
12183
+ await this.#settleEndedInFlight(inFlightPrompt, skipPostPromptRecoveryWait ? "publication" : "full");
11227
12184
  }
11228
12185
  }
11229
12186
 
@@ -11427,12 +12384,18 @@ export class AgentSession {
11427
12384
  * Queue a steering message to interrupt the agent mid-run.
11428
12385
  */
11429
12386
  async steer(text: string, images?: ImageContent[]): Promise<void> {
12387
+ const hasUsableImage =
12388
+ images?.some(image => typeof image?.data === "string" && image.data.trim().length > 0) === true;
12389
+ if (typeof text !== "string" || (text.trim().length === 0 && !hasUsableImage))
12390
+ throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
11430
12391
  this.#assertRecoveryHydrationPromoted();
11431
12392
  if (text.startsWith("/")) {
11432
12393
  this.#throwIfExtensionCommand(text);
11433
12394
  }
11434
12395
 
11435
12396
  const expandedText = expandPromptTemplate(text, [...this.#promptTemplates]);
12397
+ if (expandedText.trim().length === 0 && !hasUsableImage)
12398
+ throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
11436
12399
  assertImagePlaceholdersHavePayload(expandedText, images);
11437
12400
  await this.#queueSteer(expandedText, images, { claimsGenuineUserIntent: true });
11438
12401
  }
@@ -11445,12 +12408,18 @@ export class AgentSession {
11445
12408
  images?: ImageContent[],
11446
12409
  options?: Pick<PromptOptions, "followUpQueuePolicy">,
11447
12410
  ): Promise<void> {
12411
+ const hasUsableImage =
12412
+ images?.some(image => typeof image?.data === "string" && image.data.trim().length > 0) === true;
12413
+ if (typeof text !== "string" || (text.trim().length === 0 && !hasUsableImage))
12414
+ throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
11448
12415
  this.#assertRecoveryHydrationPromoted();
11449
12416
  if (text.startsWith("/")) {
11450
12417
  this.#throwIfExtensionCommand(text);
11451
12418
  }
11452
12419
 
11453
12420
  const expandedText = expandPromptTemplate(text, [...this.#promptTemplates]);
12421
+ if (expandedText.trim().length === 0 && !hasUsableImage)
12422
+ throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
11454
12423
  assertImagePlaceholdersHavePayload(expandedText, images);
11455
12424
  await this.#queueFollowUp(expandedText, images, {
11456
12425
  forceOneAtATime: options?.followUpQueuePolicy === "sequential",
@@ -11468,6 +12437,7 @@ export class AgentSession {
11468
12437
  claimsGenuineUserIntent?: boolean;
11469
12438
  onPromoted?: (promotion: { startsOwnRun?: boolean; removed?: boolean }) => void;
11470
12439
  external?: boolean;
12440
+ sdkRunToken?: string;
11471
12441
  },
11472
12442
  ): Promise<void> {
11473
12443
  this.#assertNoHandoffTransition();
@@ -11482,6 +12452,7 @@ export class AgentSession {
11482
12452
  this.#externalSteerAdmissionSeq.set(message, ++this.#steeringAdmissionSeq);
11483
12453
  if (options.onPromoted) this.#steerPromotionHooks.set(message, options.onPromoted);
11484
12454
  }
12455
+ if (options?.sdkRunToken) this.#sdkRunTokensByQueuedMessage.set(message, options.sdkRunToken);
11485
12456
  if (options?.claimsGenuineUserIntent) {
11486
12457
  const epoch = this.#claimDeepInterviewUserIntent();
11487
12458
  this.#deepInterviewGenuineUserMessageEpochs.set(message, epoch);
@@ -11682,7 +12653,16 @@ export class AgentSession {
11682
12653
  }
11683
12654
  this.#scheduledHiddenNextTurnGeneration = generation;
11684
12655
  this.#schedulePostPromptTask(
11685
- async () => {
12656
+ async signal => {
12657
+ if (signal.aborted || this.#isDisposed || this.#sessionAdmissionClosing) return;
12658
+ if (this.#startupTurnBarrier) {
12659
+ try {
12660
+ await this.#awaitStartupTurnBarrier(signal);
12661
+ } catch {
12662
+ return;
12663
+ }
12664
+ }
12665
+ if (signal.aborted || this.#isDisposed || this.#sessionAdmissionClosing) return;
11686
12666
  if (this.#scheduledHiddenNextTurnGeneration === generation) {
11687
12667
  this.#scheduledHiddenNextTurnGeneration = undefined;
11688
12668
  }
@@ -11711,8 +12691,12 @@ export class AgentSession {
11711
12691
  return;
11712
12692
  }
11713
12693
  try {
11714
- await this.#promptQueuedHiddenNextTurnMessages();
12694
+ await this.#promptQueuedHiddenNextTurnMessages(signal);
11715
12695
  } catch {
12696
+ if (this.#isDisposed || this.#sessionAdmissionClosing || this.#disposeAbortController.signal.aborted) {
12697
+ this.#pendingNextTurnMessages = [];
12698
+ return;
12699
+ }
11716
12700
  // Leave the hidden next-turn messages queued for the next explicit prompt.
11717
12701
  }
11718
12702
  },
@@ -11727,7 +12711,7 @@ export class AgentSession {
11727
12711
  );
11728
12712
  }
11729
12713
 
11730
- async #promptQueuedHiddenNextTurnMessages(): Promise<void> {
12714
+ async #promptQueuedHiddenNextTurnMessages(signal?: AbortSignal): Promise<void> {
11731
12715
  if (this.#pendingNextTurnMessages.length === 0) {
11732
12716
  return;
11733
12717
  }
@@ -11757,6 +12741,14 @@ export class AgentSession {
11757
12741
  if (reclassified.length === 0) {
11758
12742
  return;
11759
12743
  }
12744
+ if (this.#isDisposed || this.#sessionAdmissionClosing || this.#disposeAbortController.signal.aborted) {
12745
+ this.#settleDeliveredOwnedRegistrations(reclassified.map(entry => entry.message));
12746
+ return;
12747
+ }
12748
+ if (signal?.aborted) {
12749
+ this.#pendingNextTurnMessages = [...reclassified, ...this.#pendingNextTurnMessages];
12750
+ return;
12751
+ }
11760
12752
  // Allocate a fresh root-turn lineage when the drained batch contains a
11761
12753
  // fresh owned envelope OR an EXTERNAL hidden trigger: an external
11762
12754
  // nextTurn message has no owned envelope, so without this it would
@@ -11778,11 +12770,24 @@ export class AgentSession {
11778
12770
  const textContent = this.#getCustomMessageTextContent(message);
11779
12771
  await this.#syncSkillPromptActiveStateSafely(message, true);
11780
12772
  try {
12773
+ if (this.#isDisposed || this.#sessionAdmissionClosing || this.#disposeAbortController.signal.aborted) {
12774
+ this.#settleDeliveredOwnedRegistrations(reclassified.map(entry => entry.message));
12775
+ return;
12776
+ }
12777
+ if (signal?.aborted) {
12778
+ this.#pendingNextTurnMessages = [...reclassified, ...this.#pendingNextTurnMessages];
12779
+ return;
12780
+ }
11781
12781
  await this.#promptWithMessage(message, textContent, {
11782
12782
  prependMessages,
11783
12783
  skipPostPromptRecoveryWait: true,
12784
+ preflightSignal: signal,
11784
12785
  });
11785
12786
  } catch (error) {
12787
+ if (this.#isDisposed || this.#sessionAdmissionClosing || this.#disposeAbortController.signal.aborted) {
12788
+ this.#settleDeliveredOwnedRegistrations(reclassified.map(entry => entry.message));
12789
+ return;
12790
+ }
11786
12791
  // Requeue only the SURVIVING reclassified entries: a completion
11787
12792
  // denied by scope:"owned" was settled (dropped) above and must
11788
12793
  // never reach a later prompt — once its scope and now-unoccupied
@@ -11809,19 +12814,55 @@ export class AgentSession {
11809
12814
  // another session's manager (which lacks the same local job id) would
11810
12815
  // see job === undefined and remove a still-live registration (review
11811
12816
  // thread P1).
11812
- const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
11813
12817
  for (const message of messages) {
11814
12818
  const details = (message as { details?: { ownedCompletions?: OwnedCompletionEnvelope[] } }).details;
11815
- for (const envelope of details?.ownedCompletions ?? []) {
11816
- const job = manager?.getJob(envelope.registration.jobId);
11817
- const status = job?.generation === envelope.registration.jobGeneration ? job?.status : undefined;
11818
- // Evicted jobs have no live record (job === undefined); terminal
11819
- // statuses settle the registration.
11820
- if (job === undefined || status === "completed" || status === "cancelled" || status === "failed") {
11821
- unregisterOwnedRegistration(envelope.registration);
12819
+ for (const envelope of details?.ownedCompletions ?? []) this.#settleOwnedCompletionEnvelope(envelope);
12820
+ }
12821
+ }
12822
+
12823
+ #settleOwnedCompletionEnvelope(envelope: OwnedCompletionEnvelope): void {
12824
+ const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
12825
+ const job = manager?.getJob(envelope.registration.jobId);
12826
+ const status = job?.generation === envelope.registration.jobGeneration ? job?.status : undefined;
12827
+ // Evicted jobs have no live record (job === undefined); terminal statuses
12828
+ // settle the registration.
12829
+ if (job === undefined || status === "completed" || status === "cancelled" || status === "failed") {
12830
+ unregisterOwnedRegistration(envelope.registration);
12831
+ }
12832
+ }
12833
+
12834
+ /**
12835
+ * Drop every queued yield entry, releasing its retained delivery claim and
12836
+ * settling terminal owned-completion envelopes so neither outlives this
12837
+ * session's disposal. Claims resolve to the manager that actually retained
12838
+ * them: owned entries via their registration endpoint (subagents inherit a
12839
+ * parent's manager, and the process-global instance may belong to a
12840
+ * different concurrent session), ordinary entries only on the session-OWNED
12841
+ * manager. Disposal runs this twice: before the awaited session_shutdown
12842
+ * emit, and again after producer cancellation — a completion enqueued
12843
+ * inside that window would otherwise survive disposal, its claim retained
12844
+ * on a shared manager and interfering with later delivery teardown (Codex
12845
+ * review P2 on #5088).
12846
+ */
12847
+ #drainTerminalOwnedYieldEntries(): void {
12848
+ this.yieldQueue.clear((kind, entries) => {
12849
+ if (kind !== "async-result") return;
12850
+ const ownedManager = this.#ownedAsyncJobManager;
12851
+ for (const entry of entries) {
12852
+ if (typeof entry !== "object" || entry === null) continue;
12853
+ const record = entry as { generation?: unknown; ownedCompletion?: unknown };
12854
+ if (typeof record.generation === "string" && record.generation !== "") {
12855
+ const envelope = isOwnedCompletionEnvelope(record.ownedCompletion) ? record.ownedCompletion : undefined;
12856
+ const endpointId = envelope?.registration.endpointId;
12857
+ const manager =
12858
+ (endpointId !== undefined ? AsyncJobManager.forEndpoint(endpointId) : undefined) ?? ownedManager;
12859
+ manager?.releaseDeliveryClaim(record.generation);
12860
+ }
12861
+ if (isOwnedCompletionEnvelope(record.ownedCompletion)) {
12862
+ this.#settleOwnedCompletionEnvelope(record.ownedCompletion);
11822
12863
  }
11823
12864
  }
11824
- }
12865
+ });
11825
12866
  }
11826
12867
 
11827
12868
  #getCustomMessageTextContent(message: Pick<CustomMessage, "content">): string {
@@ -12064,9 +13105,11 @@ export class AgentSession {
12064
13105
  /** Internal dispatch disposition used before actual queue consumption. */
12065
13106
  onDispatchDisposition?: (promotion: { startsOwnRun: boolean }) => void;
12066
13107
  preflightSignal?: AbortSignal;
12067
- sdkRunToken?: string;
13108
+ sdkRunCapability?: unknown;
12068
13109
  },
12069
13110
  ): Promise<void> {
13111
+ const sdkRunToken = readSdkRunCapability(options?.sdkRunCapability);
13112
+ const internalOptions = options ? { ...options, ...(sdkRunToken ? { sdkRunToken } : {}) } : undefined;
12070
13113
  this.#assertRecoveryHydrationPromoted();
12071
13114
  const owner = this.#sessionAdmissionContext.getStore();
12072
13115
  if (owner && !owner.released) throw this.#sessionAdmissionBusyError();
@@ -12087,7 +13130,15 @@ export class AgentSession {
12087
13130
  const textParts: string[] = [];
12088
13131
  images = [];
12089
13132
  for (const part of content) {
13133
+ if (!part || typeof part !== "object" || Array.isArray(part))
13134
+ throw Object.assign(new Error("sendUserMessage content blocks must be objects."), {
13135
+ code: "invalid_input",
13136
+ });
12090
13137
  if (part.type === "text") {
13138
+ if (typeof part.text !== "string")
13139
+ throw Object.assign(new Error("sendUserMessage text blocks must contain string text."), {
13140
+ code: "invalid_input",
13141
+ });
12091
13142
  textParts.push(part.text);
12092
13143
  } else {
12093
13144
  images.push(part);
@@ -12096,6 +13147,10 @@ export class AgentSession {
12096
13147
  text = textParts.join("\n");
12097
13148
  if (images.length === 0) images = undefined;
12098
13149
  }
13150
+ const hasUsableImage =
13151
+ images?.some(image => typeof image?.data === "string" && image.data.trim().length > 0) === true;
13152
+ if (text.trim().length === 0 && !hasUsableImage)
13153
+ throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
12099
13154
 
12100
13155
  let admissionSignal = options?.preflightSignal
12101
13156
  ? AbortSignal.any([this.#promptPreflightAbortController.signal, options.preflightSignal])
@@ -12189,7 +13244,7 @@ export class AgentSession {
12189
13244
  claimsGenuineUserIntent: true,
12190
13245
  forceOneAtATime: Boolean(options?.preflightSignal || options?.queuedAtDispatch),
12191
13246
  onPromoted: options?.onQueuedPromoted,
12192
- sdkRunToken: options?.sdkRunToken,
13247
+ sdkRunToken: internalOptions?.sdkRunToken,
12193
13248
  });
12194
13249
  const cancelQueuedFollowUp = () => queuedFollowUp.cancel();
12195
13250
  options?.preflightSignal?.addEventListener("abort", cancelQueuedFollowUp, { once: true });
@@ -12204,6 +13259,7 @@ export class AgentSession {
12204
13259
  claimsGenuineUserIntent: true,
12205
13260
  onPromoted: options?.onQueuedPromoted,
12206
13261
  external: true,
13262
+ sdkRunToken: internalOptions?.sdkRunToken,
12207
13263
  });
12208
13264
  options?.onPreflightAccepted?.();
12209
13265
  return;
@@ -12223,6 +13279,7 @@ export class AgentSession {
12223
13279
  claimsGenuineUserIntent: true,
12224
13280
  onPromoted: options?.onQueuedPromoted,
12225
13281
  external: true,
13282
+ sdkRunToken: internalOptions?.sdkRunToken,
12226
13283
  });
12227
13284
  // Dispatch-race disposition (#4668 review P1): the SDK snapshot-decided
12228
13285
  // this submission starts its own turn (idle at dispatch), but the
@@ -12248,7 +13305,7 @@ export class AgentSession {
12248
13305
  await this.prompt(text, {
12249
13306
  expandPromptTemplates: false,
12250
13307
  images,
12251
- sdkRunToken: options?.sdkRunToken,
13308
+ sdkRunToken: internalOptions?.sdkRunToken,
12252
13309
  onPreflightAccepted: () => {
12253
13310
  options?.onPreflightAccepted?.();
12254
13311
  fireQueuedPromotion();
@@ -12263,7 +13320,7 @@ export class AgentSession {
12263
13320
  }
12264
13321
  : undefined,
12265
13322
  preflightSignal: options?.preflightSignal,
12266
- });
13323
+ } as InternalPromptOptions);
12267
13324
  } finally {
12268
13325
  releaseFollowUpReservation();
12269
13326
  }
@@ -12772,6 +13829,7 @@ export class AgentSession {
12772
13829
  | "tool_abort"
12773
13830
  | "internal";
12774
13831
  silent?: boolean;
13832
+ sdkRunToken?: string;
12775
13833
  }): void {
12776
13834
  const abortGoalState = this.getGoalModeState();
12777
13835
  this.#suppressNextGoalReminderAfterAbortGoalId =
@@ -12860,12 +13918,14 @@ export class AgentSession {
12860
13918
  this.#promptPreflightCancellationGeneration++;
12861
13919
  this.#promptPreflightAbortController.abort();
12862
13920
  this.#promptPreflightAbortController = new AbortController();
13921
+ this.#drainTerminalOwnedYieldEntries();
13922
+ const overlappingPostPromptDrain = this.#cancelPostPromptTasks();
12863
13923
  // Abort visibility is per-request: a later real abort must not inherit an
12864
13924
  // earlier silent abort's suppression and swallow the user-visible notice.
12865
13925
  if (options?.silent !== true) this.#silentAbortPending = false;
12866
13926
  // Capture the unwind: the field clears once the first abort settles, and
12867
13927
  // the awaits below must keep watching THIS unwind, not a successor's.
12868
- const sharedUnwind = this.#abortUnwind;
13928
+ const sharedUnwind = Promise.all([this.#abortUnwind, overlappingPostPromptDrain]).then(() => {});
12869
13929
  if (options?.timeoutMs !== undefined) {
12870
13930
  const timeoutMs = Math.max(0, options.timeoutMs);
12871
13931
  const deadline = Date.now() + timeoutMs;
@@ -12896,6 +13956,7 @@ export class AgentSession {
12896
13956
  this.#abortUnwind = unwind.promise;
12897
13957
  try {
12898
13958
  this.#abortOptions(options);
13959
+ this.#drainTerminalOwnedYieldEntries();
12899
13960
  const postPromptDrain = this.#cancelPostPromptTasks();
12900
13961
  const managedLogicalRunId =
12901
13962
  this.#defaultFallbackChain().chain.entries.length > 1 ? this.agent.currentManagedLogicalRunId : undefined;
@@ -13473,6 +14534,9 @@ export class AgentSession {
13473
14534
  attribution: "user" as const,
13474
14535
  timestamp: Date.now(),
13475
14536
  };
14537
+ const selectedSdkRunToken = selectedMessage
14538
+ ? this.#sdkRunTokensByQueuedMessage.get(selectedMessage)
14539
+ : undefined;
13476
14540
  const messageText =
13477
14541
  message.role === "custom"
13478
14542
  ? this.#getCustomMessageTextContent(message)
@@ -13491,6 +14555,7 @@ export class AgentSession {
13491
14555
  await this.#promptWithMessage(message, messageText, {
13492
14556
  admissionLease: admission,
13493
14557
  resetRetryReplaySafety: true,
14558
+ sdkRunToken: selectedSdkRunToken,
13494
14559
  onRunAccepted: () => {
13495
14560
  runAccepted = true;
13496
14561
  if (selected) this.#fireQueuedPromotionHooks([message], { startsOwnRun: true });
@@ -18934,11 +19999,15 @@ export class AgentSession {
18934
19999
  }
18935
20000
 
18936
20001
  async #handleManagedAttemptOutcome(outcome: ManagedAttemptOutcome): Promise<ManagedAttemptDecision> {
20002
+ const attemptAbortEpoch = this.#abortAdmissionEpoch;
20003
+ const attemptCancelled = () =>
20004
+ this.#isDisposed || this.#sessionAdmissionClosing || this.#abortAdmissionEpoch !== attemptAbortEpoch;
18937
20005
  const activePromptHandle = this.activePromptHandle;
18938
20006
  const cancellationSignal = activePromptHandle
18939
20007
  ? this.#runCancellationDomains.lookup(activePromptHandle)?.signal
18940
20008
  : undefined;
18941
- if (cancellationSignal?.aborted) return { type: "terminal", terminal: { stopReason: "cancelled" } };
20009
+ if (attemptCancelled() || cancellationSignal?.aborted)
20010
+ return { type: "terminal", terminal: { stopReason: "cancelled" } };
18942
20011
  if (outcome.type === "run_terminal") {
18943
20012
  this.#defaultFallbackChain().resetAttemptBudget();
18944
20013
  return { type: "terminal", terminal: { stopReason: outcome.reason } };
@@ -18974,7 +20043,6 @@ export class AgentSession {
18974
20043
  const controllerStateBeforeFailure = controller.snapshotRuntimeState();
18975
20044
  const advanced = controller.recordEscapedArgumentsFailure(errorMessage, false);
18976
20045
  this.#escapedNonAsciiManagedRetries = 0;
18977
- const abortEpoch = this.#abortAdmissionEpoch;
18978
20046
  if (advanced) {
18979
20047
  const transitionGeneration = ++this.#fallbackTransitionGeneration;
18980
20048
  return {
@@ -18995,7 +20063,9 @@ export class AgentSession {
18995
20063
  !ownership.isCurrent() ||
18996
20064
  ownership.lease.signal.aborted ||
18997
20065
  cancellationSignal?.aborted ||
18998
- this.#abortAdmissionEpoch !== abortEpoch ||
20066
+ this.#isDisposed ||
20067
+ this.#sessionAdmissionClosing ||
20068
+ this.#abortAdmissionEpoch !== attemptAbortEpoch ||
18999
20069
  this.#fallbackTransitionGeneration !== transitionGeneration;
19000
20070
  if (continuationCancelled) {
19001
20071
  await restoreOwnedTransition();
@@ -19020,7 +20090,7 @@ export class AgentSession {
19020
20090
  "escaped_non_ascii",
19021
20091
  1,
19022
20092
  cancellationSignal,
19023
- abortEpoch,
20093
+ attemptAbortEpoch,
19024
20094
  controllerStateBeforeFailure,
19025
20095
  previousModel,
19026
20096
  previousThinkingLevel,
@@ -19029,7 +20099,9 @@ export class AgentSession {
19029
20099
  );
19030
20100
  if (!switched) {
19031
20101
  if (
19032
- this.#abortAdmissionEpoch !== abortEpoch ||
20102
+ this.#isDisposed ||
20103
+ this.#sessionAdmissionClosing ||
20104
+ this.#abortAdmissionEpoch !== attemptAbortEpoch ||
19033
20105
  cancellationSignal?.aborted ||
19034
20106
  this.#fallbackTransitionGeneration !== transitionGeneration
19035
20107
  ) {
@@ -19049,13 +20121,27 @@ export class AgentSession {
19049
20121
  });
19050
20122
  return;
19051
20123
  }
20124
+ if (
20125
+ attemptCancelled() ||
20126
+ !ownership.isCurrent() ||
20127
+ ownership.lease.signal.aborted ||
20128
+ this.#fallbackTransitionGeneration !== transitionGeneration
20129
+ ) {
20130
+ await restoreOwnedTransition();
20131
+ return;
20132
+ }
19052
20133
  const continuation = this.agent.continue({
19053
20134
  ...this.#managedFallbackPromptOptions(),
19054
20135
  transientRecoveryMessage: this.#escapedNonAsciiRecoveryMessage(),
20136
+ onRunAccepted: (handle: AttemptRunHandle) => {
20137
+ this.#acceptSdkAttemptRun(handle, this.#activeSdkRunToken);
20138
+ },
19055
20139
  });
19056
20140
  if (
19057
20141
  cancellationSignal?.aborted ||
19058
- this.#abortAdmissionEpoch !== abortEpoch ||
20142
+ this.#isDisposed ||
20143
+ this.#sessionAdmissionClosing ||
20144
+ this.#abortAdmissionEpoch !== attemptAbortEpoch ||
19059
20145
  this.#fallbackTransitionGeneration !== transitionGeneration
19060
20146
  ) {
19061
20147
  await restoreOwnedTransition();
@@ -19074,7 +20160,7 @@ export class AgentSession {
19074
20160
  };
19075
20161
  }
19076
20162
  controller.advance();
19077
- if (this.#abortAdmissionEpoch !== abortEpoch || cancellationSignal?.aborted)
20163
+ if (attemptCancelled() || cancellationSignal?.aborted)
19078
20164
  return { type: "terminal", terminal: { stopReason: "cancelled" } };
19079
20165
  this.#defaultFallbackExhaustedLastTurn = true;
19080
20166
  return this.#managedFallbackExhaustionDecision(
@@ -19086,10 +20172,13 @@ export class AgentSession {
19086
20172
  return {
19087
20173
  type: "retry",
19088
20174
  continuation: async ownership => {
19089
- if (!ownership.isCurrent() || ownership.lease.signal.aborted) return;
20175
+ if (attemptCancelled() || !ownership.isCurrent() || ownership.lease.signal.aborted) return;
19090
20176
  await this.agent.continue({
19091
20177
  ...this.#managedFallbackPromptOptions(),
19092
20178
  transientRecoveryMessage: this.#escapedNonAsciiRecoveryMessage(),
20179
+ onRunAccepted: (handle: AttemptRunHandle) => {
20180
+ this.#acceptSdkAttemptRun(handle, this.#activeSdkRunToken);
20181
+ },
19093
20182
  });
19094
20183
  },
19095
20184
  };
@@ -19102,7 +20191,7 @@ export class AgentSession {
19102
20191
  return {
19103
20192
  type: "maintenance",
19104
20193
  continuation: async ownership => {
19105
- if (!ownership.isCurrent() || ownership.lease.signal.aborted) return;
20194
+ if (attemptCancelled() || !ownership.isCurrent() || ownership.lease.signal.aborted) return;
19106
20195
  const resourceRunId = String(ownership.logicalRunId);
19107
20196
  const previousLease = this.#postPromptLeases.get(resourceRunId);
19108
20197
  this.#postPromptLeases.set(resourceRunId, ownership.lease);
@@ -19121,7 +20210,13 @@ export class AgentSession {
19121
20210
  resourceRunId,
19122
20211
  ownership.lease.signal,
19123
20212
  );
19124
- if (terminalized || successorScheduled || !ownership.isCurrent() || ownership.lease.signal.aborted)
20213
+ if (
20214
+ terminalized ||
20215
+ successorScheduled ||
20216
+ attemptCancelled() ||
20217
+ !ownership.isCurrent() ||
20218
+ ownership.lease.signal.aborted
20219
+ )
19125
20220
  return;
19126
20221
  this.agent.requestRunTerminal(ownership.handle.logicalRunId, {
19127
20222
  stopReason: "error",
@@ -19519,11 +20614,13 @@ export class AgentSession {
19519
20614
  if (!isAuthenticated(activeApiKey)) return "unchanged";
19520
20615
  remaining = await authStorage.invalidateCredentialMatching(provider, activeApiKey, {
19521
20616
  sessionId: credentialSessionId,
20617
+ owner: this.#modelRegistry.getAuthStorageOwner(),
19522
20618
  });
19523
20619
  if (!remaining) return "unchanged";
19524
20620
  } else {
19525
20621
  remaining = await authStorage.markUsageLimitReached(provider, credentialSessionId, {
19526
20622
  retryAfterMs: trigger.retryAfterMs,
20623
+ owner: this.#modelRegistry.getAuthStorageOwner(),
19527
20624
  });
19528
20625
  }
19529
20626
 
@@ -19562,6 +20659,9 @@ export class AgentSession {
19562
20659
  scope?: AttemptScope,
19563
20660
  scopeWasClean = this.#isRetryScopeClean(scope),
19564
20661
  ): Promise<boolean | ManagedAttemptDecision> {
20662
+ const retryAbortEpoch = this.#abortAdmissionEpoch;
20663
+ const retryCancelled = () =>
20664
+ this.#isDisposed || this.#sessionAdmissionClosing || this.#abortAdmissionEpoch !== retryAbortEpoch;
19565
20665
  const controller = this.#defaultFallbackChain();
19566
20666
  const managedFallback = controller.chain.entries.length > 1;
19567
20667
  const retrySettings = this.settings.getGroup("retry");
@@ -19573,6 +20673,9 @@ export class AgentSession {
19573
20673
  const classification = this.#classifyErrorForRetry(message);
19574
20674
  const localSnapshot = classification === "local_snapshot";
19575
20675
  const localBufferOverflow = classification === "local_buffer_overflow";
20676
+ if (retryCancelled()) {
20677
+ return managedOutcome ? { type: "terminal", terminal: { stopReason: "cancelled" } } : false;
20678
+ }
19576
20679
  // A local machinery failure must never stay charged against the provider
19577
20680
  // fallback budget, no matter which local exit follows (disabled retry,
19578
20681
  // visible-content surface, bounded retry, exhaustion, or the immediate
@@ -19835,7 +20938,7 @@ export class AgentSession {
19835
20938
  ownership?.domain.signal ??
19836
20939
  (activePromptHandle ? this.#runCancellationDomains.lookup(activePromptHandle)?.signal : undefined);
19837
20940
  if (ownership && (!ownership.isCurrent() || cancellationSignal?.aborted)) return;
19838
- const abortEpoch = this.#abortAdmissionEpoch;
20941
+ if (retryCancelled()) return;
19839
20942
  let quotaPoolExhausted = false;
19840
20943
  if (managedFallback && !credentialRotated && !providerRetryCeilingReached) {
19841
20944
  const mark = await this.#markFailedCredential(trigger);
@@ -19869,7 +20972,7 @@ export class AgentSession {
19869
20972
  trigger.class,
19870
20973
  attemptsUsed,
19871
20974
  cancellationSignal,
19872
- abortEpoch,
20975
+ retryAbortEpoch,
19873
20976
  controllerStateBeforeAdvance,
19874
20977
  previousModel,
19875
20978
  previousThinkingLevel,
@@ -19879,7 +20982,7 @@ export class AgentSession {
19879
20982
  }
19880
20983
  }
19881
20984
  if (ownership && (!ownership.isCurrent() || cancellationSignal?.aborted)) {
19882
- if (this.#abortAdmissionEpoch === abortEpoch) {
20985
+ if (!retryCancelled()) {
19883
20986
  await this.#restoreDefaultFallbackTransition(
19884
20987
  controller,
19885
20988
  controllerStateBeforeAdvance,
@@ -19889,7 +20992,7 @@ export class AgentSession {
19889
20992
  }
19890
20993
  return;
19891
20994
  }
19892
- if (this.#abortAdmissionEpoch !== abortEpoch) {
20995
+ if (retryCancelled()) {
19893
20996
  controller.resetForNewTurn();
19894
20997
  return;
19895
20998
  }
@@ -20018,8 +21121,9 @@ export class AgentSession {
20018
21121
 
20019
21122
  if (managedOutcome) {
20020
21123
  try {
21124
+ if (retryCancelled() || ownershipCancelled()) return;
20021
21125
  await this.#checkEstimatedContextBeforePrompt();
20022
- if (ownershipCancelled()) {
21126
+ if (retryCancelled() || ownershipCancelled()) {
20023
21127
  const attempt = this.#retryAttempt;
20024
21128
  this.#retryAttempt = 0;
20025
21129
  await this.#emitSessionEvent({
@@ -20031,7 +21135,12 @@ export class AgentSession {
20031
21135
  this.#resolveRetry();
20032
21136
  return;
20033
21137
  }
20034
- await this.agent.continue(this.#managedFallbackPromptOptions());
21138
+ await this.agent.continue({
21139
+ ...this.#managedFallbackPromptOptions(),
21140
+ onRunAccepted: (handle: AttemptRunHandle) => {
21141
+ this.#acceptSdkAttemptRun(handle, this.#activeSdkRunToken);
21142
+ },
21143
+ });
20035
21144
  return;
20036
21145
  } catch (error) {
20037
21146
  const attempt = this.#retryAttempt;
@@ -21068,6 +22177,9 @@ export class AgentSession {
21068
22177
  model.provider,
21069
22178
  this.#modelRegistry.authStorage,
21070
22179
  this.credentialSessionId,
22180
+ model,
22181
+ // Optional for the same reason as the main metadata resolver above.
22182
+ this.#modelRegistry.getAuthStorageOwner?.(),
21071
22183
  ),
21072
22184
  reasoning: toReasoningEffort(this.thinkingLevel),
21073
22185
  hideThinkingSummary: this.agent.hideThinkingSummary,