@gajae-code/coding-agent 0.15.4 → 0.15.6

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 (174) hide show
  1. package/CHANGELOG.md +46 -2
  2. package/dist/types/async/job-manager.d.ts +83 -0
  3. package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
  4. package/dist/types/cli/model-presets-cli.d.ts +9 -0
  5. package/dist/types/cli/update-cli.d.ts +27 -5
  6. package/dist/types/commands/auth-gateway.d.ts +3 -0
  7. package/dist/types/commands/gc.d.ts +13 -0
  8. package/dist/types/commands/model-presets.d.ts +21 -0
  9. package/dist/types/config/autorouting-tier-map.d.ts +99 -0
  10. package/dist/types/config/file-lock.d.ts +49 -2
  11. package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
  12. package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
  13. package/dist/types/config/keybindings.d.ts +3 -2
  14. package/dist/types/config/model-preset-registry.d.ts +368 -0
  15. package/dist/types/config/model-profile-activation.d.ts +39 -0
  16. package/dist/types/config/model-profiles.d.ts +5 -3
  17. package/dist/types/config/model-registry.d.ts +11 -5
  18. package/dist/types/config/settings-schema.d.ts +9 -0
  19. package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
  20. package/dist/types/coordinator-mcp/projection-scan.d.ts +46 -0
  21. package/dist/types/extensibility/extensions/types.d.ts +4 -4
  22. package/dist/types/extensibility/shared-events.d.ts +2 -0
  23. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
  24. package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
  25. package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
  26. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +6 -0
  27. package/dist/types/index.d.ts +1 -0
  28. package/dist/types/main.d.ts +3 -1
  29. package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
  30. package/dist/types/modes/jobs-observer.d.ts +36 -0
  31. package/dist/types/sdk/broker/broker.d.ts +3 -0
  32. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
  33. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
  34. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
  35. package/dist/types/sdk/cli/session-cli.d.ts +42 -1
  36. package/dist/types/sdk/client/discovery.d.ts +1 -0
  37. package/dist/types/sdk/host/host.d.ts +2 -0
  38. package/dist/types/sdk/host/model-pin.d.ts +7 -2
  39. package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
  40. package/dist/types/sdk/host/session-runtime.d.ts +19 -1
  41. package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
  42. package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
  43. package/dist/types/sdk/router/session-router.d.ts +9 -0
  44. package/dist/types/session/account-inventory.d.ts +1 -0
  45. package/dist/types/session/agent-session.d.ts +19 -5
  46. package/dist/types/session/fold-coordinator.d.ts +201 -0
  47. package/dist/types/session/internal/managed-session-storage.d.ts +4 -3
  48. package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
  49. package/dist/types/session/sdk-run-capability.d.ts +2 -0
  50. package/dist/types/telemetry/control.d.ts +11 -0
  51. package/dist/types/telemetry/events.d.ts +20 -0
  52. package/dist/types/telemetry/index.d.ts +3 -0
  53. package/dist/types/telemetry/transport.d.ts +22 -0
  54. package/dist/types/tools/bash-interactive.d.ts +23 -0
  55. package/dist/types/tools/index.d.ts +6 -5
  56. package/dist/types/tools/path-utils.d.ts +5 -6
  57. package/dist/types/tools/tmux-self-injection-guard.d.ts +15 -0
  58. package/dist/types/web/search/providers/base.d.ts +9 -4
  59. package/dist/types/web/search/providers/xai.d.ts +2 -1
  60. package/dist/types/web/search/types.d.ts +14 -0
  61. package/package.json +10 -7
  62. package/scripts/generate-hotkeys-docs.ts +4 -0
  63. package/scripts/generate-sdk-operation-inventory.ts +3 -3
  64. package/src/async/job-manager.ts +671 -39
  65. package/src/cli/auth-broker-cli.ts +40 -12
  66. package/src/cli/auth-gateway-cli.ts +273 -97
  67. package/src/cli/config-cli.ts +2 -2
  68. package/src/cli/fast-help.ts +3 -0
  69. package/src/cli/model-presets-cli.ts +100 -0
  70. package/src/cli/update-cli.ts +142 -6
  71. package/src/cli.ts +6 -2
  72. package/src/commands/auth-gateway.ts +12 -8
  73. package/src/commands/gc.ts +19 -0
  74. package/src/commands/model-presets.ts +24 -0
  75. package/src/commands/sdk.ts +5 -0
  76. package/src/config/autorouting-tier-map.ts +33 -0
  77. package/src/config/file-lock-gc.ts +9 -4
  78. package/src/config/file-lock.ts +775 -59
  79. package/src/config/internal/model-preset-registry-test-state.ts +28 -0
  80. package/src/config/internal/model-preset-registry-test-support.ts +11 -0
  81. package/src/config/keybindings.ts +6 -2
  82. package/src/config/model-preset-registry.ts +2799 -0
  83. package/src/config/model-profile-activation.ts +115 -28
  84. package/src/config/model-profile-contract.ts +4 -1
  85. package/src/config/model-profiles.ts +56 -14
  86. package/src/config/model-registry.ts +1028 -123
  87. package/src/config/settings-schema.ts +11 -0
  88. package/src/config/settings.ts +6 -1
  89. package/src/coordinator-mcp/model-preset.ts +32 -17
  90. package/src/coordinator-mcp/policy.ts +1 -1
  91. package/src/coordinator-mcp/projection-scan.ts +571 -0
  92. package/src/coordinator-mcp/server.ts +19 -24
  93. package/src/coordinator-mcp/session-reaper.ts +9 -3
  94. package/src/extensibility/extensions/loader.ts +0 -1
  95. package/src/extensibility/extensions/types.ts +4 -2
  96. package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
  97. package/src/extensibility/shared-events.ts +2 -0
  98. package/src/gjc-runtime/empty-delete-gc.ts +301 -0
  99. package/src/gjc-runtime/gc-render.ts +29 -0
  100. package/src/gjc-runtime/gc-runtime.ts +174 -11
  101. package/src/gjc-runtime/launch-worktree.ts +44 -7
  102. package/src/gjc-runtime/ralplan-runtime.ts +14 -1
  103. package/src/gjc-runtime/session-state-lock.ts +1242 -141
  104. package/src/gjc-runtime/session-state-sidecar.ts +76 -3
  105. package/src/gjc-runtime/tmux-sessions.ts +145 -48
  106. package/src/index.ts +1 -0
  107. package/src/internal-urls/docs-index.generated.ts +14 -13
  108. package/src/lsp/config.ts +4 -3
  109. package/src/lsp/path-trust.ts +12 -6
  110. package/src/main.ts +9 -2
  111. package/src/modes/acp/acp-agent.ts +11 -5
  112. package/src/modes/components/jobs-overlay-model.ts +99 -16
  113. package/src/modes/components/model-selector.ts +158 -18
  114. package/src/modes/components/status-line/segments.ts +6 -1
  115. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  116. package/src/modes/controllers/input-controller.ts +23 -6
  117. package/src/modes/interactive-mode.ts +14 -1
  118. package/src/modes/jobs-observer.ts +149 -16
  119. package/src/modes/prompt-action-autocomplete.ts +26 -47
  120. package/src/modes/runtime-init.ts +1 -1
  121. package/src/modes/utils/injected-user-submission.ts +15 -1
  122. package/src/prompts/tools/bash.md +2 -2
  123. package/src/prompts/tools/fold-receipt.md +3 -0
  124. package/src/prompts/tools/job.md +1 -1
  125. package/src/sdk/acp/adapter.ts +17 -2
  126. package/src/sdk/broker/broker.ts +48 -21
  127. package/src/sdk/broker/lifecycle.ts +32 -6
  128. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  129. package/src/sdk/bus/chat-daemon-runtime.ts +11 -1
  130. package/src/sdk/bus/kind-aware-reconciliation.ts +46 -8
  131. package/src/sdk/bus/prompt-reconciliation.ts +33 -6
  132. package/src/sdk/bus/slack-conversation.ts +5 -1
  133. package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
  134. package/src/sdk/cli/session-cli.ts +161 -5
  135. package/src/sdk/client/discovery.ts +3 -1
  136. package/src/sdk/host/control/dispatch.ts +3 -0
  137. package/src/sdk/host/host.ts +7 -0
  138. package/src/sdk/host/model-pin.ts +185 -25
  139. package/src/sdk/host/sdk-run-capability.ts +1 -0
  140. package/src/sdk/host/session-runtime.test.ts +1240 -107
  141. package/src/sdk/host/session-runtime.ts +1029 -115
  142. package/src/sdk/prompt-deadline-manager.ts +16 -1
  143. package/src/sdk/protocol/adapter-validation.ts +27 -0
  144. package/src/sdk/protocol/operation-inventory.generated.json +2 -2
  145. package/src/sdk/protocol/operation-registry.ts +4 -1
  146. package/src/sdk/router/session-router.ts +297 -111
  147. package/src/sdk/session.ts +83 -12
  148. package/src/session/account-inventory.ts +27 -11
  149. package/src/session/agent-session.ts +887 -95
  150. package/src/session/fold-coordinator.ts +433 -0
  151. package/src/session/internal/managed-session-storage.ts +64 -26
  152. package/src/session/sdk-run-capability-internal.ts +15 -0
  153. package/src/session/sdk-run-capability.ts +2 -0
  154. package/src/session/session-manager.ts +29 -10
  155. package/src/setup/provider-presets.json +1 -1
  156. package/src/slash-commands/helpers/credential-switch.ts +6 -1
  157. package/src/task/executor.ts +19 -3
  158. package/src/telemetry/control.ts +26 -0
  159. package/src/telemetry/events.ts +142 -0
  160. package/src/telemetry/index.ts +3 -0
  161. package/src/telemetry/transport.ts +87 -0
  162. package/src/tools/bash-interactive.ts +182 -85
  163. package/src/tools/bash.ts +786 -67
  164. package/src/tools/index.ts +7 -5
  165. package/src/tools/path-utils.ts +77 -29
  166. package/src/tools/tmux-self-injection-guard.ts +359 -0
  167. package/src/tools/tool-catalog.generated.ts +1 -1
  168. package/src/web/search/provider.ts +120 -11
  169. package/src/web/search/providers/anthropic.ts +89 -17
  170. package/src/web/search/providers/base.ts +9 -4
  171. package/src/web/search/providers/gemini.ts +87 -15
  172. package/src/web/search/providers/openai-compatible.ts +23 -8
  173. package/src/web/search/providers/xai.ts +98 -12
  174. 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,
@@ -124,6 +125,7 @@ import {
124
125
  EMPTY_RESPONSE_PROVIDER_CODE,
125
126
  type FallbackAttemptToken,
126
127
  type FallbackTriggerClass,
128
+ SERVER_OVERLOADED_PROVIDER_CODE,
127
129
  STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE,
128
130
  } from "@gajae-code/ai/utils/fallback-transport";
129
131
  import { AttemptRecordStore } from "./attempt-record-store";
@@ -180,8 +182,10 @@ export interface ForkContextSeedOptions {
180
182
  }
181
183
 
182
184
  import type { AuthCredentialSelector } from "@gajae-code/ai/core";
185
+ import { isFoundryEnabled } from "@gajae-code/ai/utils/foundry";
183
186
  import type { MacOSPowerAssertion } from "@gajae-code/natives";
184
187
  import {
188
+ $pickCredentialEnv,
185
189
  extractRetryHint,
186
190
  hasFsCode,
187
191
  isEacces,
@@ -313,6 +317,7 @@ import {
313
317
  type CoordinatorToolObservation,
314
318
  ownerTerminalContextFromEnvironment,
315
319
  persistCoordinatorRuntimeStateFromEvent,
320
+ persistCoordinatorWorkerIntegrationOutcome,
316
321
  registerCoordinatorRuntimeStateFinalizer,
317
322
  UNPROVEN_TOOL_LABEL,
318
323
  } from "../gjc-runtime/session-state-sidecar";
@@ -442,6 +447,7 @@ import {
442
447
  } from "./contribution-prep";
443
448
  import { canonicalCoordinatorToolLabel } from "./coordinator-tool-label";
444
449
  import { pruneStaleFileMentions } from "./file-mention-pruning";
450
+ import { describeFoldReceipt, FOLD_WAKE_MERGE_WINDOW_MS, type FoldAdapter, FoldCoordinator } from "./fold-coordinator";
445
451
  import type { MemoryGuardRestoreResult } from "./memory-guard-checkpoint-participant";
446
452
  import {
447
453
  type BashExecutionMessage,
@@ -456,6 +462,7 @@ import {
456
462
  SKILL_PROMPT_MESSAGE_TYPE,
457
463
  } from "./messages";
458
464
  import { isLegacyProviderSafetyStopMessage } from "./provider-safety-stop";
465
+ import { readSdkRunCapability } from "./sdk-run-capability";
459
466
  import { formatSessionDumpText } from "./session-dump-format";
460
467
  import type {
461
468
  BranchSummaryEntry,
@@ -470,7 +477,6 @@ import type {
470
477
  SessionManagerCloseOutcome,
471
478
  SessionMemoryStats,
472
479
  } from "./session-manager";
473
-
474
480
  import {
475
481
  createReadonlySessionManager,
476
482
  getLatestCompactionEntry,
@@ -487,6 +493,8 @@ import {
487
493
  bindToolLineage,
488
494
  classifyOwnedEnvelope,
489
495
  isOwnedCompletionEnvelope,
496
+ isOwnedCompletionEnvelopeAllowed,
497
+ lookupOwnedRegistration,
490
498
  lookupTerminalScope,
491
499
  mintTurnLineageIdHash,
492
500
  type OwnedCompletionEnvelope,
@@ -495,11 +503,35 @@ import {
495
503
  settleToolLineageRegistrationWindow,
496
504
  unregisterOwnedRegistration,
497
505
  } from "./terminal-abort";
498
-
499
506
  import { ToolChoiceQueue } from "./tool-choice-queue";
500
507
  import { pruneSupersededMaintenanceReminders, pruneSupersededVolatileProjectContext } from "./volatile-context-pruning";
501
508
  import { YieldQueue } from "./yield-queue";
502
509
 
510
+ const ASYNC_INLINE_RESULT_MAX_CHARS = 12_000;
511
+ const ASYNC_PREVIEW_MAX_CHARS = 4_000;
512
+
513
+ async function formatParkedAsyncResult(
514
+ sessionManager: SessionManager,
515
+ result: string,
516
+ allowArtifact = true,
517
+ ): Promise<string> {
518
+ if (result.length <= ASYNC_INLINE_RESULT_MAX_CHARS) return result;
519
+ const preview = `${result.slice(0, ASYNC_PREVIEW_MAX_CHARS)}\n\n[Output truncated. Showing first ${ASYNC_PREVIEW_MAX_CHARS.toLocaleString()} characters.]`;
520
+ try {
521
+ if (!allowArtifact) return preview;
522
+ const { path: artifactPath, id: artifactId } = await sessionManager.allocateArtifactPath("async");
523
+ if (artifactPath && artifactId) {
524
+ await Bun.write(artifactPath, result);
525
+ return `${preview}\nFull output: artifact://${artifactId}`;
526
+ }
527
+ } catch (error) {
528
+ logger.warn("Failed to persist parked async follow-up artifact", {
529
+ error: error instanceof Error ? error.message : String(error),
530
+ });
531
+ }
532
+ return preview;
533
+ }
534
+
503
535
  /**
504
536
  * #4560: structured workflow recovery projection from canonical durable
505
537
  * Ralplan/Ultragoal state, consumed by the compaction summary context and
@@ -1047,8 +1079,23 @@ export interface PromptOptions {
1047
1079
  onSkillPrepared?: (meta: { name: string; path: string; lineCount?: number; cleanedArgs?: string }) => void;
1048
1080
  /** Optional invocation-scoped cancellation fence used before an accepted skill starts execution. */
1049
1081
  preflightSignal?: AbortSignal;
1082
+ /** Internal host capability; ordinary package callers cannot construct the branded value. */
1083
+ sdkRunCapability?: unknown;
1050
1084
  }
1051
1085
 
1086
+ type InternalPromptOptions = PromptOptions & { sdkRunToken?: string };
1087
+ type InternalCustomMessageOptions = Pick<
1088
+ PromptOptions,
1089
+ | "streamingBehavior"
1090
+ | "toolChoice"
1091
+ | "followUpQueuePolicy"
1092
+ | "onPreflightAccepted"
1093
+ | "onPreflightAcceptCommit"
1094
+ | "preflightSignal"
1095
+ > & {
1096
+ sdkRunToken?: string;
1097
+ };
1098
+
1052
1099
  function promptPreflightCancelledError(): Error {
1053
1100
  const error = Object.assign(new Error("Prompt preflight was cancelled before execution."), { code: "busy" });
1054
1101
  error.name = "PromptPreflightCancelledError";
@@ -1297,11 +1344,74 @@ function isBareDefaultMessageOnlyFirstEventTimeout(message: AssistantMessage): b
1297
1344
  BARE_DEFAULT_WATCHDOG_ERROR.test(message.errorMessage ?? ""))
1298
1345
  );
1299
1346
  }
1347
+ /**
1348
+ * True when the attempt's structured evidence is exactly the provider's typed
1349
+ * capacity-overload code and nothing else: no status, no retained retry header,
1350
+ * no second typed code, no error kind, and none of the transport's other
1351
+ * observations (request size, first-event timing, endpoint class, provider retry
1352
+ * ceiling). The code arrives statuslessly (a Codex error event, an HTTP 200
1353
+ * terminal Responses envelope), so these facts ARE the overload rather than
1354
+ * evidence that the attempt did observable work; any additional fact means the
1355
+ * attempt is no longer provably that bare capacity rejection. The code is
1356
+ * compared case-sensitively, matching the parser and the transport gate.
1357
+ */
1358
+ function isExactTypedOverloadFacts(message: AssistantMessage): boolean {
1359
+ if (message.errorKind !== undefined || message.errorStatus !== undefined) return false;
1360
+ const facts = message.transportFailure;
1361
+ if (!facts) return false;
1362
+ return (
1363
+ facts.providerCode === SERVER_OVERLOADED_PROVIDER_CODE &&
1364
+ facts.status === undefined &&
1365
+ facts.anthropicErrorType === undefined &&
1366
+ (facts.openaiErrorCode === undefined || facts.openaiErrorCode === SERVER_OVERLOADED_PROVIDER_CODE) &&
1367
+ (facts.headers === undefined || Object.keys(facts.headers).length === 0) &&
1368
+ facts.requestBytes === undefined &&
1369
+ facts.firstEventElapsedMs === undefined &&
1370
+ facts.firstEventTimeoutMs === undefined &&
1371
+ facts.endpointClass === undefined &&
1372
+ facts.retryMaxAttempts === undefined
1373
+ );
1374
+ }
1375
+ /**
1376
+ * True when transport facts are exactly the provider's statusless typed
1377
+ * capacity-overload code (the provider code verbatim and, when present, the
1378
+ * OpenAI code verbatim, with no status). The agent loop must not treat the new
1379
+ * facts as managed transaction authority, and managed session fallback must
1380
+ * not gain retry/advance authority from them, so this predicate keeps both
1381
+ * guards reading only typed facts, never error prose.
1382
+ */
1383
+ function isStatuslessTypedOverloadFacts(facts: TransportFailureFacts | undefined): boolean {
1384
+ if (!facts) return false;
1385
+ return (
1386
+ facts.status === undefined &&
1387
+ facts.providerCode === SERVER_OVERLOADED_PROVIDER_CODE &&
1388
+ (facts.openaiErrorCode === undefined || facts.openaiErrorCode === SERVER_OVERLOADED_PROVIDER_CODE)
1389
+ );
1390
+ }
1391
+
1300
1392
  function isBareDefaultCodexOverload(message: AssistantMessage): boolean {
1301
1393
  return (
1302
1394
  message.api === "openai-codex-responses" &&
1303
1395
  BARE_DEFAULT_CODEX_OVERLOAD_ERROR.test(message.errorMessage ?? "") &&
1304
- !hasBareDefaultRetryDisqualifyingFacts(message) &&
1396
+ (isExactTypedOverloadFacts(message) || !hasBareDefaultRetryDisqualifyingFacts(message)) &&
1397
+ !assistantMessageHasVisibleOrToolContent(message)
1398
+ );
1399
+ }
1400
+
1401
+ /**
1402
+ * True when a generic OpenAI Responses turn failed with exactly OpenAI's typed
1403
+ * capacity-overload code. Unlike the Codex and Anthropic admissions this reads
1404
+ * no prose at all: the terminal Responses envelope carries the code in typed
1405
+ * transport facts, so the code plus a content-free attempt is the whole
1406
+ * admission. Both `openaiErrorCode` and the derived `providerCode` must be the
1407
+ * exact code, so a different transport's overload facts cannot enter through
1408
+ * this path.
1409
+ */
1410
+ function isBareDefaultOpenAIResponsesOverload(message: AssistantMessage): boolean {
1411
+ return (
1412
+ message.api === "openai-responses" &&
1413
+ message.transportFailure?.openaiErrorCode === SERVER_OVERLOADED_PROVIDER_CODE &&
1414
+ isExactTypedOverloadFacts(message) &&
1305
1415
  !assistantMessageHasVisibleOrToolContent(message)
1306
1416
  );
1307
1417
  }
@@ -1560,14 +1670,44 @@ function buildSessionMetadata(
1560
1670
  provider: string,
1561
1671
  authStorage: AuthStorage | undefined,
1562
1672
  credentialSessionId = sessionId,
1673
+ model?: Model,
1674
+ owner?: object,
1563
1675
  ): Record<string, unknown> {
1564
1676
  const userId: Record<string, string> = { session_id: sessionId };
1565
1677
  // Only look up account_uuid when the request is going to Anthropic. Injecting
1566
1678
  // a Anthropic model OAuth account_uuid into requests bound for other providers (including
1567
1679
  // Anthropic-format-compatible proxies like cloudflare-ai-gateway or gitlab-duo)
1568
1680
  // would leak the user's Anthropic identity to unrelated third-party APIs.
1569
- if (provider === "anthropic") {
1570
- const accountUuid = authStorage?.getOAuthAccountId("anthropic", credentialSessionId);
1681
+ const effectiveBaseUrl =
1682
+ model?.provider === "anthropic"
1683
+ ? (normalizeAnthropicBaseUrl(
1684
+ (isFoundryEnabled() ? $pickCredentialEnv("FOUNDRY_BASE_URL") : undefined) ?? model.baseUrl,
1685
+ ) ?? "https://api.anthropic.com")
1686
+ : undefined;
1687
+ let officialAnthropicEndpoint = false;
1688
+ if (provider === "anthropic" && model?.api === "anthropic-messages") {
1689
+ try {
1690
+ const candidate = effectiveBaseUrl ?? "https://api.anthropic.com";
1691
+ const url = new URL(candidate);
1692
+ const authority = candidate.match(/^[a-z][a-z\d+.-]*:\/\/([^/?#]*)/iu)?.[1];
1693
+ officialAnthropicEndpoint =
1694
+ url.protocol === "https:" &&
1695
+ url.hostname === "api.anthropic.com" &&
1696
+ authority?.toLowerCase() === "api.anthropic.com" &&
1697
+ !url.username &&
1698
+ !url.password &&
1699
+ !url.search &&
1700
+ !url.hash &&
1701
+ (url.pathname === "/" || url.pathname === "/v1" || url.pathname === "/v1/");
1702
+ } catch {
1703
+ officialAnthropicEndpoint = false;
1704
+ }
1705
+ }
1706
+ // Without the registry owner, do not query shared auth storage: an unscoped
1707
+ // lookup can attribute a registry-scoped API-key request to another OAuth
1708
+ // account. Compatibility facades therefore retain session_id-only metadata.
1709
+ if (officialAnthropicEndpoint && owner) {
1710
+ const accountUuid = authStorage?.getOAuthAccountId("anthropic", credentialSessionId, { owner });
1571
1711
  if (typeof accountUuid === "string" && accountUuid.length > 0) {
1572
1712
  userId.account_uuid = accountUuid;
1573
1713
  // Derive device_id from account_uuid so the payload matches the real CC
@@ -1848,9 +1988,24 @@ export type BeforeAgentStartContributor = (event: {
1848
1988
  }) => Promise<BeforeAgentStartInternalMessage | undefined>;
1849
1989
 
1850
1990
  const AGENT_END_WORKER_INTEGRATION_TIMEOUT_MS = 5_000;
1991
+ const POST_PUBLICATION_ERROR_MAX_BYTES = 512;
1992
+
1993
+ export type WorkerIntegrationOutcome =
1994
+ | { status: "completed" }
1995
+ | { status: "failed"; error: string }
1996
+ | { status: "timed_out" };
1997
+
1998
+ function sanitizePostPublicationError(error: string | undefined): string | undefined {
1999
+ if (error === undefined) return undefined;
2000
+ const normalized = error
2001
+ .replace(/[\u0000-\u001f\u007f]/gu, " ")
2002
+ .replace(/\s+/gu, " ")
2003
+ .trim();
2004
+ return truncateHeadBytes(normalized || "unknown worker integration error", POST_PUBLICATION_ERROR_MAX_BYTES).text;
2005
+ }
1851
2006
 
1852
2007
  export class WorkerIntegrationRequestScheduler {
1853
- #inFlight: Promise<void> | undefined = undefined;
2008
+ #inFlight: Promise<WorkerIntegrationOutcome> | undefined = undefined;
1854
2009
  #pending = false;
1855
2010
 
1856
2011
  constructor(
@@ -1867,9 +2022,15 @@ export class WorkerIntegrationRequestScheduler {
1867
2022
  }
1868
2023
 
1869
2024
  async flush(): Promise<void> {
2025
+ await this.flushWithOutcome();
2026
+ }
2027
+
2028
+ async flushWithOutcome(): Promise<WorkerIntegrationOutcome> {
2029
+ let outcome: WorkerIntegrationOutcome = { status: "completed" };
1870
2030
  while (this.#inFlight) {
1871
- await this.#inFlight;
2031
+ outcome = await this.#inFlight;
1872
2032
  }
2033
+ return outcome;
1873
2034
  }
1874
2035
 
1875
2036
  #start(): void {
@@ -1879,16 +2040,22 @@ export class WorkerIntegrationRequestScheduler {
1879
2040
  try {
1880
2041
  request = this.request(controller.signal);
1881
2042
  } catch {
1882
- request = Promise.resolve();
2043
+ request = Promise.reject(new Error("Worker integration request failed before dispatch."));
1883
2044
  }
1884
2045
  let timeout: ReturnType<typeof setTimeout> | undefined;
1885
- const deadline = new Promise<void>(resolve => {
2046
+ const deadline = new Promise<WorkerIntegrationOutcome>(resolve => {
1886
2047
  timeout = setTimeout(() => {
1887
2048
  controller.abort(new Error("Worker integration request timed out"));
1888
- resolve();
2049
+ resolve({ status: "timed_out" });
1889
2050
  }, this.timeoutMs);
1890
2051
  });
1891
- this.#inFlight = Promise.race([request.catch(() => {}), deadline]).finally(() => {
2052
+ this.#inFlight = Promise.race([
2053
+ request.then(
2054
+ () => ({ status: "completed" as const }),
2055
+ error => ({ status: "failed" as const, error: String(error) }),
2056
+ ),
2057
+ deadline,
2058
+ ]).finally(() => {
1892
2059
  if (timeout) clearTimeout(timeout);
1893
2060
  controller.abort();
1894
2061
  this.#inFlight = undefined;
@@ -2365,6 +2532,8 @@ export class AgentSession {
2365
2532
  this.#followUpPromotionHooks.delete(message);
2366
2533
  followUpHook({ startsOwnRun: true, removed: true });
2367
2534
  }
2535
+ this.#sdkRunTokensByQueuedMessage.delete(message);
2536
+ this.#deepInterviewGenuineUserMessageEpochs.delete(message);
2368
2537
  }
2369
2538
  }
2370
2539
  #fireQueuedPromotionHooks(messages: readonly AgentMessage[], promotion?: { startsOwnRun?: boolean }): void {
@@ -2384,6 +2553,45 @@ export class AgentSession {
2384
2553
  }
2385
2554
  }
2386
2555
  }
2556
+ #queuedMessagesForSessionTransition(): AgentMessage[] {
2557
+ return [
2558
+ ...new Set([
2559
+ ...this.agent.snapshotSteering(),
2560
+ ...this.agent.snapshotFollowUp(),
2561
+ ...this.#deferredSdkFollowUps,
2562
+ ]),
2563
+ ];
2564
+ }
2565
+ /** Drop queued SDK work when the session identity is replaced. The old
2566
+ * promotion hooks belong to the predecessor runtime; retaining the message
2567
+ * would let it execute later under the successor without an owner. */
2568
+ #terminalizeQueuedSdkWorkForSessionTransition(messages: readonly AgentMessage[]): void {
2569
+ if (messages.length === 0) return;
2570
+ this.#fireQueuedRemovalHooks(messages);
2571
+ for (const message of messages) this.#sdkRunTokensByQueuedMessage.delete(message);
2572
+ this.#deferredSdkFollowUps = this.#deferredSdkFollowUps.filter(message => !messages.includes(message));
2573
+ }
2574
+ #resetActiveSdkRunOwnership(): void {
2575
+ this.#activeSdkRunToken = undefined;
2576
+ this.#activeAttemptScope = undefined;
2577
+ this.#activeLogicalRunId = undefined;
2578
+ }
2579
+ #acceptSdkAttemptRun(handle: AttemptRunHandle, sdkRunToken?: string): void {
2580
+ const predecessorScope = this.#activeAttemptScope;
2581
+ const predecessorSdkRunToken =
2582
+ predecessorScope === undefined ? undefined : this.#sdkRunTokensByAttemptScope.get(predecessorScope);
2583
+ const carryRecoverySkip =
2584
+ sdkRunToken !== undefined &&
2585
+ predecessorSdkRunToken === sdkRunToken &&
2586
+ predecessorScope !== undefined &&
2587
+ this.#skipPostPromptRecoveryWaitByAttemptScope.delete(predecessorScope);
2588
+ this.#acceptRunHandle(handle);
2589
+ if (sdkRunToken !== undefined) {
2590
+ this.#activeSdkRunToken = sdkRunToken;
2591
+ this.#sdkRunTokensByAttemptScope.set(handle.scope, sdkRunToken);
2592
+ }
2593
+ if (carryRecoverySkip) this.#skipPostPromptRecoveryWaitByAttemptScope.add(handle.scope);
2594
+ }
2387
2595
  /** Messages queued to be included with the next user prompt as context ("asides"). */
2388
2596
  #pendingNextTurnMessages: Array<{ message: CustomMessage; origin: "turn" | "external" }> = [];
2389
2597
  #scheduledHiddenNextTurnGeneration: number | undefined = undefined;
@@ -2517,7 +2725,88 @@ export class AgentSession {
2517
2725
  onPersisted?: () => void;
2518
2726
  appendedToAgent: boolean;
2519
2727
  }> = [];
2520
- #foregroundBashBackgroundRequestHandler: (() => void) | undefined;
2728
+ /**
2729
+ * Set by a fold and consumed once by the pause checkpoint, so a fold ends its
2730
+ * own turn without pausing any later turn.
2731
+ */
2732
+ #foldStopRequested = false;
2733
+ /**
2734
+ * The single linearization point for folding a foreground wait. Arrow deps
2735
+ * capture `this` but are only invoked after construction.
2736
+ */
2737
+ readonly #foldCoordinator = new FoldCoordinator({
2738
+ hasActiveTurn: () => this.activePromptHandle !== undefined,
2739
+ armSteeringFence: () => {
2740
+ this.agent.setSteeringAdmissionFence(() => true);
2741
+ return () => this.agent.setSteeringAdmissionFence(undefined);
2742
+ },
2743
+ requestStop: () => {
2744
+ this.#foldStopRequested = true;
2745
+ },
2746
+ captureRemainingIntent: () => this.#captureRemainingIntentForFold(),
2747
+ // A parked completion replayed after the fold must schedule its own wake.
2748
+ // This mirrors exactly what the sdk delivery seam does for a live receipt
2749
+ // delivery, so the parked path cannot rely on an unrelated idle rearm.
2750
+ deliverParked: (job, disposition) => {
2751
+ const endpointId = this.#ownedAsyncJobManager
2752
+ ? AsyncJobManager.endpointIdOf(this.#ownedAsyncJobManager)
2753
+ : undefined;
2754
+ const registration = lookupOwnedRegistration(job.id, job.generation, endpointId);
2755
+ if (this.#ownedAsyncJobManager?.isDeliverySuppressed(job.id, job.generation)) {
2756
+ this.#ownedAsyncJobManager.clearParkedDelivery(job.generation);
2757
+ return;
2758
+ }
2759
+ if (this.#foldCoordinator.claimCompletionNotice(job)) {
2760
+ this.emitNotice(
2761
+ "info",
2762
+ `Folded job ${disposition.receipt.jobId} (${disposition.receipt.label}) finished.`,
2763
+ "fold",
2764
+ );
2765
+ }
2766
+ const ownedCompletion = registration
2767
+ ? {
2768
+ lineageIdHash: registration.lineageIdHash,
2769
+ promptAttemptEpoch: registration.promptAttemptEpoch,
2770
+ registration,
2771
+ }
2772
+ : undefined;
2773
+ const allowArtifact = ownedCompletion === undefined || isOwnedCompletionEnvelopeAllowed(ownedCompletion);
2774
+ void formatParkedAsyncResult(this.sessionManager, disposition.text, allowArtifact)
2775
+ .then(formattedResult => {
2776
+ const manager = this.#ownedAsyncJobManager;
2777
+ if (manager?.isDeliverySuppressed(job.id, job.generation)) {
2778
+ manager.clearParkedDelivery(job.generation);
2779
+ return;
2780
+ }
2781
+ if (!this.#foldCoordinator.claimCompletionDelivery(job)) {
2782
+ manager?.clearParkedDelivery(job.generation);
2783
+ return;
2784
+ }
2785
+ try {
2786
+ manager?.retainDeliveryClaim(job);
2787
+ if (manager?.isDeliverySuppressed(job.id, job.generation)) {
2788
+ manager.releaseDeliveryClaim(job.generation);
2789
+ manager.clearParkedDelivery(job.generation);
2790
+ return;
2791
+ }
2792
+ this.yieldQueue.enqueue("async-result", {
2793
+ jobId: disposition.receipt.jobId,
2794
+ generation: disposition.receipt.jobGeneration,
2795
+ result: `${formattedResult}\n\n${describeFoldReceipt(disposition.receipt)}`,
2796
+ job,
2797
+ durationMs: undefined,
2798
+ ownedCompletion,
2799
+ });
2800
+ } finally {
2801
+ manager?.clearParkedDelivery(job.generation);
2802
+ }
2803
+ })
2804
+ .catch(error => {
2805
+ this.#ownedAsyncJobManager?.clearParkedDelivery(job.generation);
2806
+ logger.warn("Parked folded delivery formatting failed", { error: String(error) });
2807
+ });
2808
+ },
2809
+ });
2521
2810
 
2522
2811
  // Python execution state
2523
2812
  #evalAbortControllers = new Set<AbortController>();
@@ -2567,7 +2856,10 @@ export class AgentSession {
2567
2856
  #extensionRunner: ExtensionRunner | undefined = undefined;
2568
2857
  /** SDK follow-up ownership by queued message and the attempt that dequeues it. */
2569
2858
  #sdkRunTokensByQueuedMessage = new WeakMap<AgentMessage, string>();
2859
+ #skipPostPromptRecoveryWaitByAttemptScope = new WeakSet<AttemptScope>();
2570
2860
  #sdkRunTokensByAttemptScope = new WeakMap<AttemptScope, string>();
2861
+ #activeSdkRunToken: string | undefined;
2862
+ #activeAttemptScope: AttemptScope | undefined;
2571
2863
  #attemptAuthority!: AttemptScopeAuthority;
2572
2864
  #attemptRecordStore!: AttemptRecordStore;
2573
2865
  #activeLogicalRunId: AttemptRunHandle["logicalRunId"] | undefined;
@@ -2576,6 +2868,7 @@ export class AgentSession {
2576
2868
  // previously recorded run id rather than throwing inside the callback.
2577
2869
  if (!handle) return;
2578
2870
  this.#activeLogicalRunId = handle.logicalRunId;
2871
+ this.#activeAttemptScope = handle.scope;
2579
2872
  }
2580
2873
 
2581
2874
  #turnIndex = 0;
@@ -2912,6 +3205,7 @@ export class AgentSession {
2912
3205
  * orchestrator does not hold it), so there is no self-deadlock.
2913
3206
  */
2914
3207
  #sessionTransitionKind: string | undefined;
3208
+ #coordinatorPersistGeneration = 0;
2915
3209
 
2916
3210
  #beginSessionTransition(kind: string): void {
2917
3211
  if (this.#sessionTransitionKind !== undefined) {
@@ -2921,6 +3215,7 @@ export class AgentSession {
2921
3215
  );
2922
3216
  }
2923
3217
  this.#sessionTransitionKind = kind;
3218
+ this.#coordinatorPersistGeneration += 1;
2924
3219
  }
2925
3220
 
2926
3221
  #endSessionTransition(): void {
@@ -3520,7 +3815,7 @@ export class AgentSession {
3520
3815
  // Deferrals behind still-pending selection fences are excluded here: they
3521
3816
  // cannot settle before their fence resolves, and the fence owner may be
3522
3817
  // waiting on this very settlement (#4519).
3523
- if (!predecessorPromptStillInFlight) {
3818
+ if (promptWait !== "publication" && !predecessorPromptStillInFlight) {
3524
3819
  await this.#waitForSessionSettlement(this.#turnSettlementDeferralFloor());
3525
3820
  }
3526
3821
  if (flushError) throw flushError;
@@ -3562,28 +3857,50 @@ export class AgentSession {
3562
3857
  this.#pendingAgentEndEmit = undefined;
3563
3858
  const lease = this.#deferredAgentEndLeases.get(pending);
3564
3859
  if (lease) this.#deferredAgentEndLeases.delete(pending);
3860
+ this.#startAgentEndPublication(pending, lease);
3861
+ }
3862
+ #startAgentEndPublication(pending: AgentSessionEvent, lease?: RunResourceProducerLease): void {
3565
3863
  this.#agentEndPublicationInFlight++;
3566
- this.#agentEndPublicationPromise = this.#publishDeferredAgentEnd(pending, lease);
3864
+ const pendingScope = (pending as AgentSessionEvent & { scope?: AttemptScopeRef }).scope as
3865
+ | AttemptScope
3866
+ | undefined;
3867
+ const sdkTerminal = pendingScope !== undefined && this.#sdkRunTokensByAttemptScope.has(pendingScope);
3868
+ this.#agentEndPublicationPromise = this.#publishDeferredAgentEnd(pending, lease, sdkTerminal);
3567
3869
  void this.#agentEndPublicationPromise;
3568
3870
  }
3569
3871
 
3570
3872
  async #publishDeferredAgentEnd(
3571
3873
  pending: AgentSessionEvent,
3572
3874
  lease: RunResourceProducerLease | undefined,
3875
+ workerIntegrationSettled: boolean,
3573
3876
  ): Promise<void> {
3877
+ const publicationContext = {
3878
+ sessionId: this.sessionId,
3879
+ cwd: this.sessionManager.getCwd(),
3880
+ sessionFile: this.sessionManager.getSessionFile(),
3881
+ };
3882
+ const publicationScope = (pending as AgentSessionEvent & { scope?: AttemptScopeRef }).scope as
3883
+ | AttemptScope
3884
+ | undefined;
3885
+ const publicationCorrelationId = publicationScope
3886
+ ? this.#sdkRunTokensByAttemptScope.get(publicationScope)
3887
+ : undefined;
3574
3888
  let extensionDelivery: Promise<void> | undefined;
3575
3889
  const releaseLease = () => {
3576
3890
  if (!lease) return;
3891
+ this.#deferredAgentEndLeases.delete(pending);
3577
3892
  if (this.#postPromptLeases.get(lease.resourceRunId) === lease) {
3578
3893
  this.#postPromptLeases.delete(lease.resourceRunId);
3579
3894
  }
3580
3895
  lease.closeDiscovery();
3581
3896
  };
3582
3897
  const publish = async () => {
3583
- // Worker integration is first-party lifecycle persistence, not an extension
3584
- // hook. Settle it before publishing the terminal boundary while user extension
3585
- // delivery and coordinator sidecar persistence remain asynchronous.
3586
- await this.#flushWorkerIntegrationForAgentEnd();
3898
+ let workerIntegration: Promise<WorkerIntegrationOutcome> | undefined;
3899
+ let workerIntegrationOutcome: WorkerIntegrationOutcome | undefined;
3900
+ if (!workerIntegrationSettled) {
3901
+ workerIntegration = this.#flushWorkerIntegrationForAgentEnd();
3902
+ workerIntegrationOutcome = await workerIntegration;
3903
+ }
3587
3904
  // Reserve persistence before notifying synchronous subscribers: a subscriber
3588
3905
  // may start a successor prompt from agent_end, whose running state must
3589
3906
  // serialize after this terminal boundary rather than be overwritten by it.
@@ -3592,16 +3909,72 @@ export class AgentSession {
3592
3909
  // behind this terminal transition, but do not make the interactive terminal
3593
3910
  // wait for the filesystem-backed lock/write to settle. On slow or lock-hostile
3594
3911
  // mounts such as WSL drvfs, waiting here leaves the foreground activity loader
3595
- // and busy input state visible after the model has already finished.
3596
- const terminalPersistence = this.#queueCoordinatorRuntimeStatePersist(pending);
3912
+ // and busy input state visible after the model has already finished. The queue
3913
+ // preserves transaction order; terminal publication is the user-visible
3914
+ // authority and must not be suppressed by a secondary persistence failure.
3915
+ const terminalPersistence = this.#queueCoordinatorRuntimeStatePersist(pending, true);
3597
3916
  this.#emit(pending);
3598
- await terminalPersistence;
3917
+ void terminalPersistence.then(
3918
+ () => {
3919
+ if (workerIntegrationOutcome) {
3920
+ this.#recordPostPublicationOutcome(
3921
+ publicationContext,
3922
+ publicationCorrelationId,
3923
+ "worker_integration",
3924
+ workerIntegrationOutcome,
3925
+ );
3926
+ }
3927
+ this.#recordPostPublicationOutcome(
3928
+ publicationContext,
3929
+ publicationCorrelationId,
3930
+ "terminal_persistence",
3931
+ { status: "completed" },
3932
+ );
3933
+ },
3934
+ error => {
3935
+ if (workerIntegrationOutcome) {
3936
+ this.#recordPostPublicationOutcome(
3937
+ publicationContext,
3938
+ publicationCorrelationId,
3939
+ "worker_integration",
3940
+ workerIntegrationOutcome,
3941
+ );
3942
+ }
3943
+ this.#recordPostPublicationOutcome(
3944
+ publicationContext,
3945
+ publicationCorrelationId,
3946
+ "terminal_persistence",
3947
+ {
3948
+ status: "failed",
3949
+ error: String(error),
3950
+ },
3951
+ );
3952
+ logger.warn("Failed to persist terminal coordinator runtime state", { error: String(error) });
3953
+ },
3954
+ );
3599
3955
  extensionDelivery = this.#queueExtensionEvent(
3600
3956
  pending,
3601
3957
  undefined,
3602
3958
  true,
3603
3959
  (pending as AgentSessionEvent & { scope?: AttemptScopeRef }).scope,
3604
3960
  );
3961
+ if (workerIntegrationSettled) {
3962
+ workerIntegration = this.#flushWorkerIntegrationForAgentEnd();
3963
+ void workerIntegration.then(outcome =>
3964
+ this.#recordPostPublicationOutcome(
3965
+ publicationContext,
3966
+ publicationCorrelationId,
3967
+ "worker_integration",
3968
+ outcome,
3969
+ ),
3970
+ );
3971
+ await extensionDelivery;
3972
+ void terminalPersistence.catch(error =>
3973
+ logger.warn("Terminal persistence continued after SDK publication", { error }),
3974
+ );
3975
+ } else {
3976
+ // Non-SDK/public publication has already awaited worker integration above.
3977
+ }
3605
3978
  };
3606
3979
  try {
3607
3980
  if (lease) await this.#runResourceLeaseContext.run(lease, publish);
@@ -3797,9 +4170,19 @@ export class AgentSession {
3797
4170
  this.#resumeFromOwnedCompletion();
3798
4171
  try {
3799
4172
  if (survivors.length === 1) {
3800
- await this.agent.prompt(first, this.#managedFallbackPromptOptions());
4173
+ await this.agent.prompt(first, {
4174
+ ...this.#managedFallbackPromptOptions(),
4175
+ onRunAccepted: (handle: AttemptRunHandle) => {
4176
+ if (handle) this.#acceptSdkAttemptRun(handle);
4177
+ },
4178
+ });
3801
4179
  } else {
3802
- await this.agent.prompt(survivors, this.#managedFallbackPromptOptions());
4180
+ await this.agent.prompt(survivors, {
4181
+ ...this.#managedFallbackPromptOptions(),
4182
+ onRunAccepted: (handle: AttemptRunHandle) => {
4183
+ if (handle) this.#acceptSdkAttemptRun(handle);
4184
+ },
4185
+ });
3803
4186
  }
3804
4187
  } finally {
3805
4188
  // The owned completions were delivered OR the prompt attempt
@@ -3817,11 +4200,32 @@ export class AgentSession {
3817
4200
  async () => {
3818
4201
  await run();
3819
4202
  },
3820
- { delayMs: 1 },
4203
+ // One merge window, so staggered completions share a single wake
4204
+ // turn instead of each buying its own.
4205
+ { delayMs: FOLD_WAKE_MERGE_WINDOW_MS },
3821
4206
  );
3822
4207
  },
3823
4208
  });
3824
4209
  this.agent.setOnBeforeYield(() => this.yieldQueue.flush("streaming"));
4210
+ // Stop-after-result, never abort: a fold arms this once and the loop ends the
4211
+ // turn at its next checkpoint. Consuming the flag here keeps the pause scoped
4212
+ // to the folded turn instead of pausing everything that follows. The
4213
+ // config-provided checkpoint (subagent requestPause, RLM research) is OR-ed
4214
+ // in, never replaced.
4215
+ // sdk/session.ts threads the config's shouldPause into new Agent, so the
4216
+ // Agent's current checkpoint IS the config-provided one (subagent
4217
+ // requestPause, RLM research). Capture and OR it rather than replace it.
4218
+ const configuredShouldPause = this.agent.shouldPause;
4219
+ this.agent.setShouldPause(() => {
4220
+ if (configuredShouldPause?.() === true) return true;
4221
+ if (!this.#foldStopRequested) return false;
4222
+ this.#foldStopRequested = false;
4223
+ // The folded turn has now actually stopped: steering admission must be
4224
+ // restored so later turns drain their queues normally (the fence stays
4225
+ // armed from the fold until exactly this point).
4226
+ this.agent.setSteeringAdmissionFence(undefined);
4227
+ return true;
4228
+ });
3825
4229
  this.agent.setMaintainContext((context, lifecycle) =>
3826
4230
  this.#trackMidRunMaintenance(
3827
4231
  this.awaitPendingContextTransformations().then(() => this.#runMidRunMaintenance(context, lifecycle)),
@@ -3960,6 +4364,18 @@ export class AgentSession {
3960
4364
  if (messages.some(message => ownedCompletionResumeAction(message) === "fresh")) {
3961
4365
  this.#resumeFromOwnedCompletion();
3962
4366
  }
4367
+ const consumedSdkRunTokenForCurrentRun = messages
4368
+ .map(message => this.#sdkRunTokensByQueuedMessage.get(message))
4369
+ .find((token): token is string => token !== undefined);
4370
+ if (
4371
+ consumedSdkRunTokenForCurrentRun !== undefined &&
4372
+ this.#activeAttemptScope !== undefined &&
4373
+ promotion.startsOwnRun !== true
4374
+ ) {
4375
+ this.#sdkRunTokensByAttemptScope.set(this.#activeAttemptScope, consumedSdkRunTokenForCurrentRun);
4376
+ this.#skipPostPromptRecoveryWaitByAttemptScope.add(this.#activeAttemptScope);
4377
+ this.#activeSdkRunToken = consumedSdkRunTokenForCurrentRun;
4378
+ }
3963
4379
  // Monitor task-notifications now carry the owned-completion envelope
3964
4380
  // (see tools/monitor.ts), so the general drop + fresh-admission paths
3965
4381
  // above cover them exactly like async results — no monitor-specific
@@ -3975,13 +4391,38 @@ export class AgentSession {
3975
4391
  // when the batch is drained by a continuation the message did not
3976
4392
  // schedule (a skipped continuation must never discard the correlation
3977
4393
  // of work that is still consumed; review thread P2).
4394
+ const consumedSdkRunToken =
4395
+ promotion.startsOwnRun === true
4396
+ ? [...messages, ...dropped]
4397
+ .map(message => this.#sdkRunTokensByQueuedMessage.get(message))
4398
+ .find((token): token is string => token !== undefined)
4399
+ : undefined;
3978
4400
  this.#fireQueuedPromotionHooks(messages, promotion);
4401
+ if (consumedSdkRunToken !== undefined) this.#activeSdkRunToken = consumedSdkRunToken;
4402
+ // Dropped or in-run follow-ups have no later own-run acceptance callback;
4403
+ // delete those tokens now. Own-run messages stay mapped until
4404
+ // #scheduleAgentContinue.onRunAccepted binds the new attempt scope.
4405
+ for (const message of dropped) this.#sdkRunTokensByQueuedMessage.delete(message);
4406
+ if (promotion.startsOwnRun !== true)
4407
+ for (const message of messages) this.#sdkRunTokensByQueuedMessage.delete(message);
3979
4408
  };
3980
4409
  // Steering consumed mid-run never starts its own run: fire the stored
3981
4410
  // promotion hook at the REAL dequeue boundary so the SDK attaches the
3982
4411
  // submitter to the in-flight run instead of parking the correlation for
3983
4412
  // an unrelated later agent_start (#4668).
3984
4413
  this.agent.onSteeringConsumed = (messages, promotion = { startsOwnRun: false }) => {
4414
+ const consumedSdkRunToken = messages
4415
+ .map(message => this.#sdkRunTokensByQueuedMessage.get(message))
4416
+ .find((token): token is string => token !== undefined);
4417
+ if (
4418
+ consumedSdkRunToken !== undefined &&
4419
+ this.#activeAttemptScope !== undefined &&
4420
+ promotion.startsOwnRun !== true
4421
+ ) {
4422
+ this.#sdkRunTokensByAttemptScope.set(this.#activeAttemptScope, consumedSdkRunToken);
4423
+ this.#skipPostPromptRecoveryWaitByAttemptScope.add(this.#activeAttemptScope);
4424
+ this.#activeSdkRunToken = consumedSdkRunToken;
4425
+ }
3985
4426
  this.#fireQueuedPromotionHooks(messages, promotion);
3986
4427
  };
3987
4428
  this.agent.providerSessionState = this.#providerSessionState;
@@ -4894,6 +5335,10 @@ export class AgentSession {
4894
5335
  * seen.
4895
5336
  */
4896
5337
  #coordinatorToolObservations = new WeakMap<object, CoordinatorToolObservation>();
5338
+ #agentEventAdmission = new WeakMap<
5339
+ object,
5340
+ { scope?: AttemptScope; sdkRunToken?: string; persistGeneration: number }
5341
+ >();
4897
5342
 
4898
5343
  /**
4899
5344
  * Capture what is true at the SYNCHRONOUS agent-event boundary, before any async work.
@@ -4949,6 +5394,11 @@ export class AgentSession {
4949
5394
  }
4950
5395
 
4951
5396
  #trackAgentEvent = (event: AgentEvent): Promise<void> => {
5397
+ this.#agentEventAdmission.set(event, {
5398
+ scope: this.#activeAttemptScope,
5399
+ sdkRunToken: this.#activeSdkRunToken,
5400
+ persistGeneration: this.#coordinatorPersistGeneration,
5401
+ });
4952
5402
  // First statement of the listener: the observation must precede every claim,
4953
5403
  // reservation, and async hop this handler performs.
4954
5404
  this.#observeCoordinatorToolEvent(event);
@@ -5018,10 +5468,10 @@ export class AgentSession {
5018
5468
  this.#postPromptLeases.set(eventLease.resourceRunId, eventLease);
5019
5469
  if (eventLease) {
5020
5470
  await this.#runResourceLeaseContext.run(eventLease, () =>
5021
- this.#handleAgentEvent(event, activePromptHandle, canonicalAdmission),
5471
+ this.#handleAgentEvent(event, activePromptHandle, canonicalAdmission, eventLease),
5022
5472
  );
5023
5473
  } else {
5024
- await this.#handleAgentEvent(event, activePromptHandle, canonicalAdmission);
5474
+ await this.#handleAgentEvent(event, activePromptHandle, canonicalAdmission, undefined);
5025
5475
  }
5026
5476
  } catch (error) {
5027
5477
  logger.warn("Agent event handler failed", { event: event.type, error: String(error) });
@@ -5029,11 +5479,19 @@ export class AgentSession {
5029
5479
  canonicalAdmission?.release();
5030
5480
  if (eventLease) {
5031
5481
  const pendingAgentEnd =
5032
- event.type === "agent_end" && !maintenanceCheckpoint && this.#pendingAgentEndEmit === event
5482
+ event.type === "agent_end" &&
5483
+ !maintenanceCheckpoint &&
5484
+ (this.#pendingAgentEndEmit === event || this.#deferredAgentEndLeases.has(event))
5033
5485
  ? this.#pendingAgentEndEmit
5034
5486
  : undefined;
5035
5487
  if (pendingAgentEnd) {
5036
5488
  this.#deferredAgentEndLeases.set(pendingAgentEnd, eventLease);
5489
+ } else if (
5490
+ event.type === "agent_end" &&
5491
+ !maintenanceCheckpoint &&
5492
+ this.#deferredAgentEndLeases.has(event)
5493
+ ) {
5494
+ // Fast publication took ownership of the lease before the handler unwound.
5037
5495
  } else {
5038
5496
  if (event.type === "agent_end" && this.#postPromptLeases.get(eventLease.resourceRunId) === eventLease)
5039
5497
  this.#postPromptLeases.delete(eventLease.resourceRunId);
@@ -5056,21 +5514,61 @@ export class AgentSession {
5056
5514
  /** Serializes sidecar writes in publication order, independent of write latency. */
5057
5515
  #coordinatorPersistQueue: Promise<void> = Promise.resolve();
5058
5516
 
5517
+ #recordPostPublicationOutcome(
5518
+ context: { sessionId: string; cwd: string; sessionFile: string | undefined },
5519
+ correlationId: string | undefined,
5520
+ kind: "worker_integration" | "terminal_persistence",
5521
+ outcome: { status: "completed" | "failed" | "timed_out"; error?: string },
5522
+ ): void {
5523
+ if (outcome.status !== "completed") {
5524
+ const correlation = correlationId ?? context.sessionId;
5525
+ const safeError = sanitizePostPublicationError(outcome.error);
5526
+ this.#emit({
5527
+ type: "notice",
5528
+ level: "error",
5529
+ source: kind,
5530
+ message: `${kind} ${outcome.status} for ${correlation}${safeError ? `: ${safeError}` : ""}`,
5531
+ });
5532
+ }
5533
+ const persist = () =>
5534
+ persistCoordinatorWorkerIntegrationOutcome(context, {
5535
+ ...outcome,
5536
+ kind,
5537
+ correlationId,
5538
+ error: sanitizePostPublicationError(outcome.error),
5539
+ });
5540
+ const queued = this.#coordinatorPersistQueue.then(persist, persist);
5541
+ this.#coordinatorPersistQueue = queued.catch(() => {});
5542
+ void queued.catch(error =>
5543
+ logger.warn("Failed to persist terminal reconciliation outcome", { error: String(error) }),
5544
+ );
5545
+ }
5546
+
5059
5547
  /**
5060
5548
  * Reserve this event's place in the sidecar write order.
5061
5549
  *
5062
5550
  * Called immediately BEFORE local delivery so a synchronous subscriber that re-enters
5063
5551
  * the session cannot get its own, later event persisted first. The returned promise is
5064
- * only awaited where durability must precede delivery (terminal `agent_end`).
5552
+ * awaited only by callers that explicitly require a durable handoff; interactive
5553
+ * terminal delivery remains independent of this secondary sink.
5065
5554
  *
5066
5555
  * A pairing-only tool event takes no place in that order at all — neither its start nor
5067
5556
  * its end. It describes a call that was never dispatched, and this file is read as the
5068
5557
  * answer to "what is this session doing right now".
5069
5558
  */
5070
- #queueCoordinatorRuntimeStatePersist(event: AgentSessionEvent): Promise<void> {
5559
+ #queueCoordinatorRuntimeStatePersist(event: AgentSessionEvent, propagateFailure = false): Promise<void> {
5071
5560
  if (isNonDispatchedToolEvent(event)) return Promise.resolve();
5072
5561
  const observation = this.#coordinatorToolObservations.get(event);
5073
- const run = () => this.#persistRuntimeStateInBackground(event, observation);
5562
+ const context = {
5563
+ sessionId: this.sessionId,
5564
+ cwd: this.sessionManager.getCwd(),
5565
+ sessionFile: this.sessionManager.getSessionFile(),
5566
+ };
5567
+ const generation = this.#agentEventAdmission.get(event)?.persistGeneration ?? this.#coordinatorPersistGeneration;
5568
+ const run = () =>
5569
+ generation === this.#coordinatorPersistGeneration
5570
+ ? this.#persistRuntimeStateInBackground(event, context, observation, propagateFailure)
5571
+ : Promise.resolve();
5074
5572
  const queued = this.#coordinatorPersistQueue.then(run, run);
5075
5573
  this.#coordinatorPersistQueue = queued.catch(() => {});
5076
5574
  return queued;
@@ -5078,24 +5576,19 @@ export class AgentSession {
5078
5576
 
5079
5577
  async #persistRuntimeStateInBackground(
5080
5578
  event: AgentSessionEvent,
5579
+ context: { sessionId: string; cwd: string; sessionFile: string | undefined },
5081
5580
  observation: CoordinatorToolObservation | undefined,
5581
+ propagateFailure: boolean,
5082
5582
  ): Promise<void> {
5083
5583
  try {
5084
- await persistCoordinatorRuntimeStateFromEvent(
5085
- event,
5086
- {
5087
- sessionId: this.sessionId,
5088
- cwd: this.sessionManager.getCwd(),
5089
- sessionFile: this.sessionManager.getSessionFile(),
5090
- },
5091
- observation,
5092
- );
5093
- } catch {
5584
+ await persistCoordinatorRuntimeStateFromEvent(event, context, observation);
5585
+ } catch (error) {
5094
5586
  logger.warn("Failed to persist coordinator runtime state", { event: event.type });
5587
+ if (propagateFailure) throw error;
5095
5588
  }
5096
5589
  }
5097
5590
 
5098
- async #emitSessionEvent(event: AgentSessionEvent): Promise<void> {
5591
+ async #emitSessionEvent(event: AgentSessionEvent, eventLease?: RunResourceProducerLease): Promise<void> {
5099
5592
  const attemptScope = (event as AgentSessionEvent & { scope?: AttemptScope }).scope;
5100
5593
  if (event.type === "turn_start") {
5101
5594
  this.#extensionTurnGeneration++;
@@ -5129,6 +5622,18 @@ export class AgentSession {
5129
5622
  // have unwound. Subscribers treat this event as the ready signal; flushing it
5130
5623
  // from abort while either barrier is active permits a successor to race the
5131
5624
  // prior prompt's cleanup.
5625
+ const sdkTerminal =
5626
+ event.type === "agent_end" && attemptScope !== undefined && this.#sdkRunTokensByAttemptScope.has(attemptScope);
5627
+ if (
5628
+ event.type === "agent_end" &&
5629
+ sdkTerminal &&
5630
+ this.#pendingAgentEndContinuationHolds.size === 0 &&
5631
+ this.#pendingAgentEndEmit === undefined
5632
+ ) {
5633
+ if (eventLease) this.#deferredAgentEndLeases.set(event, eventLease);
5634
+ this.#startAgentEndPublication(event, eventLease);
5635
+ return;
5636
+ }
5132
5637
  if (event.type === "agent_end" && (this.#livePromptsInFlight() > 0 || this.#agentEventHandlersInFlight > 0)) {
5133
5638
  this.#pendingAgentEndEmit = event;
5134
5639
  return;
@@ -5137,8 +5642,12 @@ export class AgentSession {
5137
5642
  if (event.type === "agent_end") {
5138
5643
  // Start the durable terminal write before synchronous subscribers can
5139
5644
  // re-enter prompt(), so a successor's running transition serializes after it.
5140
- await this.#queueCoordinatorRuntimeStatePersist(event);
5645
+ // Do not let a lock-hostile sidecar suppress the terminal event itself.
5646
+ const terminalPersistence = this.#queueCoordinatorRuntimeStatePersist(event);
5141
5647
  this.#emit(event);
5648
+ void terminalPersistence.catch(error => {
5649
+ logger.warn("Failed to persist terminal coordinator runtime state", { error: String(error) });
5650
+ });
5142
5651
  await this.#emitExtensionEvent(event);
5143
5652
  return;
5144
5653
  }
@@ -5234,6 +5743,7 @@ export class AgentSession {
5234
5743
  event: AgentEvent,
5235
5744
  activePromptHandle?: string,
5236
5745
  canonicalAdmission?: CanonicalMessageAdmission,
5746
+ eventLease?: RunResourceProducerLease,
5237
5747
  ): Promise<void> => {
5238
5748
  const attemptScope = (event as AgentEvent & { scope?: AttemptScope }).scope;
5239
5749
 
@@ -5573,7 +6083,7 @@ export class AgentSession {
5573
6083
  });
5574
6084
  }
5575
6085
 
5576
- await this.#emitSessionEvent(displayEvent);
6086
+ await this.#emitSessionEvent(displayEvent, eventLease);
5577
6087
  if (event.type === "message_end" && event.message.role === "assistant") {
5578
6088
  this.#assistantAttemptScopes.set(event.message, {
5579
6089
  scope: attemptScope,
@@ -5923,6 +6433,7 @@ export class AgentSession {
5923
6433
  skipCompactionCheck: true,
5924
6434
  resourceRunId: activePromptHandle,
5925
6435
  maintenanceContinuation: true,
6436
+ sdkRunToken: attemptScope ? this.#sdkRunTokensByAttemptScope.get(attemptScope) : undefined,
5926
6437
  });
5927
6438
  }
5928
6439
  return;
@@ -6210,8 +6721,11 @@ export class AgentSession {
6210
6721
  predecessorAgentEndHold?: symbol;
6211
6722
  /** Internal predecessor terminal event sequestered while waiting behind selection. */
6212
6723
  deferredPredecessorAgentEnd?: AgentSessionEvent;
6724
+ /** Internal causal SDK owner captured when this continuation was scheduled. */
6725
+ sdkRunToken?: string;
6213
6726
  }): Promise<void> {
6214
6727
  const continuationAdmission = this.#captureScheduledContinuationAdmission();
6728
+ const scheduledSdkRunToken = options?.sdkRunToken;
6215
6729
  const selectionFenceGeneration =
6216
6730
  options?.selectionFenceGeneration ??
6217
6731
  this.#selectionFenceGenerationContext.getStore() ??
@@ -6233,6 +6747,7 @@ export class AgentSession {
6233
6747
  generation: deferredPromptGeneration,
6234
6748
  selectionFenceGeneration,
6235
6749
  deferredPredecessorAgentEnd,
6750
+ sdkRunToken: scheduledSdkRunToken,
6236
6751
  });
6237
6752
  } finally {
6238
6753
  // The recursive call synchronously re-reserved its settlement
@@ -6368,18 +6883,23 @@ export class AgentSession {
6368
6883
  // agent_start (review P1); their queued messages are in-run
6369
6884
  // consumptions, not own-run promotions.
6370
6885
  const startsOwn = options?.maintenanceContinuation !== true;
6886
+ const inheritedSdkRunToken = options?.continueQueuedOnly
6887
+ ? undefined
6888
+ : (options?.sdkRunToken ?? scheduledSdkRunToken);
6889
+ const consumedSdkRunToken = acceptance.consumedQueuedMessages
6890
+ .map(message => this.#sdkRunTokensByQueuedMessage.get(message))
6891
+ .find((token): token is string => token !== undefined);
6892
+ const sdkRunToken = consumedSdkRunToken ?? inheritedSdkRunToken;
6371
6893
  this.#fireQueuedPromotionHooks(acceptance.consumedQueuedMessages, {
6372
6894
  startsOwnRun: startsOwn,
6373
6895
  });
6374
- for (const message of acceptance.consumedQueuedMessages) {
6375
- const sdkRunToken = this.#sdkRunTokensByQueuedMessage.get(message);
6376
- if (sdkRunToken) {
6377
- this.#sdkRunTokensByAttemptScope.set(handle.scope, sdkRunToken);
6378
- break;
6379
- }
6380
- }
6381
- this.#acceptRunHandle(handle);
6896
+ if (startsOwn) this.#activeSdkRunToken = sdkRunToken;
6897
+ this.#acceptSdkAttemptRun(handle, sdkRunToken);
6382
6898
  options?.onRunAccepted?.(handle);
6899
+ // Keep the queued token available through the acceptance callback;
6900
+ // SDK follow-up owners bind it to the new attempt scope there.
6901
+ for (const message of acceptance.consumedQueuedMessages)
6902
+ this.#sdkRunTokensByQueuedMessage.delete(message);
6383
6903
  settleLease();
6384
6904
  releasePredecessor();
6385
6905
  if (startsQueuedSuccessor) {
@@ -6537,8 +7057,12 @@ export class AgentSession {
6537
7057
  resourceRunId?: string,
6538
7058
  deferredSelectionFenceGeneration?: number,
6539
7059
  deferredPredecessorAgentEnd?: AgentSessionEvent,
7060
+ sdkRunToken?: string,
6540
7061
  ): void {
6541
7062
  const scheduledGeneration = generation;
7063
+ const scheduledSdkRunToken =
7064
+ sdkRunToken ??
7065
+ (this.#activeAttemptScope ? this.#sdkRunTokensByAttemptScope.get(this.#activeAttemptScope) : undefined);
6542
7066
  const continuationAuthorized = async (
6543
7067
  signal: AbortSignal,
6544
7068
  hasPendingNextTurnMessages = false,
@@ -6591,6 +7115,7 @@ export class AgentSession {
6591
7115
  resourceRunId,
6592
7116
  selectionFenceGeneration,
6593
7117
  predecessorAgentEnd,
7118
+ scheduledSdkRunToken,
6594
7119
  );
6595
7120
  } finally {
6596
7121
  this.#endSelectionFenceDeferralTracking(selectionFenceGeneration);
@@ -6638,6 +7163,7 @@ export class AgentSession {
6638
7163
  promptText,
6639
7164
  {
6640
7165
  skipPostPromptRecoveryWait: true,
7166
+ sdkRunToken: scheduledSdkRunToken,
6641
7167
  skipCompactionCheck: true,
6642
7168
  predecessorAgentEndHold,
6643
7169
  onFinalPreflight: ({ hasPendingNextTurnMessages }) =>
@@ -7415,16 +7941,16 @@ export class AgentSession {
7415
7941
  this.#workerIntegrationScheduler?.enqueue();
7416
7942
  }
7417
7943
 
7418
- async #flushWorkerIntegrationAttempt(): Promise<void> {
7419
- await this.#workerIntegrationScheduler?.flush();
7944
+ async #flushWorkerIntegrationAttempt(): Promise<WorkerIntegrationOutcome> {
7945
+ return (await this.#workerIntegrationScheduler?.flushWithOutcome()) ?? { status: "completed" };
7420
7946
  }
7421
7947
 
7422
- async #flushWorkerIntegrationForAgentEnd(): Promise<void> {
7948
+ async #flushWorkerIntegrationForAgentEnd(): Promise<WorkerIntegrationOutcome> {
7423
7949
  if (!this.#workerIntegrationRequestedForTurn) {
7424
7950
  this.#requestWorkerIntegrationAttempt();
7425
7951
  }
7426
7952
  try {
7427
- await this.#flushWorkerIntegrationAttempt();
7953
+ return await this.#flushWorkerIntegrationAttempt();
7428
7954
  } finally {
7429
7955
  this.#workerIntegrationRequestedForTurn = false;
7430
7956
  }
@@ -7437,16 +7963,53 @@ export class AgentSession {
7437
7963
  workerIntegrationSettled = false,
7438
7964
  scope?: AttemptScopeRef,
7439
7965
  ): Promise<void> {
7966
+ const admission = this.#agentEventAdmission.get(event);
7967
+ const activeAttemptScopeAtEvent = admission === undefined ? this.#activeAttemptScope : admission.scope;
7968
+ const activeSdkRunTokenAtEvent = admission === undefined ? this.#activeSdkRunToken : admission.sdkRunToken;
7440
7969
  if (event.type === "agent_end" && !workerIntegrationSettled) {
7441
7970
  await this.#flushWorkerIntegrationForAgentEnd();
7442
7971
  }
7443
7972
  const deliveryScope = scope ?? (event as AgentSessionEvent & { scope?: AttemptScopeRef }).scope;
7973
+ const eventToken = (event as AgentSessionEvent & { sdkRunToken?: unknown }).sdkRunToken;
7974
+ const sdkRunToken =
7975
+ typeof eventToken === "string"
7976
+ ? eventToken
7977
+ : deliveryScope
7978
+ ? this.#sdkRunTokensByAttemptScope.get(deliveryScope as AttemptScope)
7979
+ : undefined;
7444
7980
  const isTerminalAgentEnd =
7445
7981
  event.type === "agent_end" && !(event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted");
7446
7982
  const finishAttempt = () => {
7447
7983
  if (!isTerminalAgentEnd) return;
7448
- if (deliveryScope) this.#attemptRecordStore.retire(deliveryScope as AttemptScope);
7449
- this.#activeLogicalRunId = undefined;
7984
+ // A merged wake batch whose flush fired while still streaming cleared its
7985
+ // pending flag without rescheduling. Returning to idle is exactly when it
7986
+ // becomes deliverable again, so rearm here rather than waiting for an
7987
+ // unrelated enqueue.
7988
+ this.yieldQueue.rearmIdle();
7989
+ const isActiveAttempt =
7990
+ deliveryScope === undefined
7991
+ ? this.#activeAttemptScope === activeAttemptScopeAtEvent &&
7992
+ this.#activeSdkRunToken === activeSdkRunTokenAtEvent
7993
+ : deliveryScope === activeAttemptScopeAtEvent &&
7994
+ deliveryScope === this.#activeAttemptScope &&
7995
+ this.#activeSdkRunToken === activeSdkRunTokenAtEvent;
7996
+ if (this.#foldStopRequested) {
7997
+ this.#foldStopRequested = false;
7998
+ this.agent.setSteeringAdmissionFence(undefined);
7999
+ }
8000
+ if (deliveryScope) {
8001
+ this.#attemptRecordStore.retire(deliveryScope as AttemptScope);
8002
+ if (
8003
+ activeSdkRunTokenAtEvent !== undefined &&
8004
+ this.#sdkRunTokensByAttemptScope.get(deliveryScope as AttemptScope) === activeSdkRunTokenAtEvent
8005
+ )
8006
+ this.#sdkRunTokensByAttemptScope.delete(deliveryScope as AttemptScope);
8007
+ }
8008
+ if (isActiveAttempt) this.#activeSdkRunToken = undefined;
8009
+ if (isActiveAttempt) {
8010
+ this.#activeAttemptScope = undefined;
8011
+ this.#activeLogicalRunId = undefined;
8012
+ }
7450
8013
  };
7451
8014
  if (!this.#extensionRunner) {
7452
8015
  finishAttempt();
@@ -7459,9 +8022,7 @@ export class AgentSession {
7459
8022
  await this.#extensionRunner.emit(
7460
8023
  {
7461
8024
  type: "agent_start",
7462
- ...(deliveryScope
7463
- ? { sdkRunToken: this.#sdkRunTokensByAttemptScope.get(deliveryScope as AttemptScope) }
7464
- : {}),
8025
+ ...(sdkRunToken ? { sdkRunToken } : {}),
7465
8026
  },
7466
8027
  undefined,
7467
8028
  deliveryScope,
@@ -7472,6 +8033,7 @@ export class AgentSession {
7472
8033
  type: "agent_failed",
7473
8034
  error: sanitizePromptFailure(event.error),
7474
8035
  scope: event.scope,
8036
+ ...(sdkRunToken ? { sdkRunToken } : {}),
7475
8037
  },
7476
8038
  undefined,
7477
8039
  deliveryScope,
@@ -7483,6 +8045,7 @@ export class AgentSession {
7483
8045
  messages: event.messages,
7484
8046
  stopReason: event.stopReason,
7485
8047
  maintenanceOutcome: event.maintenanceOutcome,
8048
+ ...(sdkRunToken ? { sdkRunToken } : {}),
7486
8049
  },
7487
8050
  undefined,
7488
8051
  deliveryScope,
@@ -7710,8 +8273,18 @@ export class AgentSession {
7710
8273
  const sid = this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
7711
8274
  this.agent.sessionId = sid;
7712
8275
  this.agent.providerSessionId = this.#providerCacheSessionId ?? sid;
7713
- this.agent.setMetadataResolver((provider: string) =>
7714
- buildSessionMetadata(sid, provider, this.#modelRegistry.authStorage, this.credentialSessionId),
8276
+ this.agent.setMetadataResolver(context =>
8277
+ buildSessionMetadata(
8278
+ sid,
8279
+ context.provider,
8280
+ this.#modelRegistry.authStorage,
8281
+ this.credentialSessionId,
8282
+ context.model ?? this.model,
8283
+ // Optional call: the metadata resolver runs on every prompt, and `owner` is an
8284
+ // optional `buildSessionMetadata` argument. The auth-credential paths keep the
8285
+ // strict call because they genuinely require a real registry owner.
8286
+ this.#modelRegistry.getAuthStorageOwner?.(),
8287
+ ),
7715
8288
  );
7716
8289
  }
7717
8290
 
@@ -8324,13 +8897,39 @@ export class AgentSession {
8324
8897
  * handler, so Ctrl+B-style folding fails closed instead of aborting or
8325
8898
  * shell-suspending arbitrary work.
8326
8899
  */
8327
- registerForegroundBashBackgroundRequestHandler(handler: () => void): () => void {
8328
- this.#foregroundBashBackgroundRequestHandler = handler;
8329
- return () => {
8330
- if (this.#foregroundBashBackgroundRequestHandler === handler) {
8331
- this.#foregroundBashBackgroundRequestHandler = undefined;
8332
- }
8333
- };
8900
+ registerForegroundFoldParticipant(adapter: FoldAdapter): () => void {
8901
+ return this.#foldCoordinator.registerParticipant(adapter);
8902
+ }
8903
+
8904
+ /** The session-owned fold coordinator, for delivery-side receipt lookup. */
8905
+ get foldCoordinator(): FoldCoordinator {
8906
+ return this.#foldCoordinator;
8907
+ }
8908
+
8909
+ /**
8910
+ * Best-effort capture of what the interrupted turn still intended to do, so
8911
+ * the wake turn can finish the original task instead of only reporting output.
8912
+ */
8913
+ #captureRemainingIntentForFold(): string | undefined {
8914
+ const messages = this.agent.state.messages;
8915
+ for (let index = messages.length - 1; index >= 0; index--) {
8916
+ const message = messages[index];
8917
+ if (message?.role !== "user") continue;
8918
+ const content = message.content;
8919
+ const text =
8920
+ typeof content === "string"
8921
+ ? content
8922
+ : Array.isArray(content)
8923
+ ? content
8924
+ .map(block => (typeof block === "object" && block && "text" in block ? String(block.text) : ""))
8925
+ .filter(Boolean)
8926
+ .join("\n")
8927
+ : "";
8928
+ const trimmed = text.trim();
8929
+ if (trimmed.length === 0) return undefined;
8930
+ return trimmed.length > 2000 ? `${trimmed.slice(0, 2000)}…` : trimmed;
8931
+ }
8932
+ return undefined;
8334
8933
  }
8335
8934
 
8336
8935
  /**
@@ -8339,7 +8938,7 @@ export class AgentSession {
8339
8938
  * no fold target exists.
8340
8939
  */
8341
8940
  hasForegroundBashBackgroundRequestHandler(): boolean {
8342
- return this.#foregroundBashBackgroundRequestHandler !== undefined;
8941
+ return this.#foldCoordinator.hasFoldableParticipant();
8343
8942
  }
8344
8943
 
8345
8944
  /** Set the SDK permission policy used by guarded ACP tool execution. */
@@ -8375,11 +8974,17 @@ export class AgentSession {
8375
8974
  * Ask the active managed foreground bash call to return as a background job.
8376
8975
  * Returns false when no supported foreground tool is currently backgroundable.
8377
8976
  */
8378
- requestForegroundBashBackground(): boolean {
8379
- const handler = this.#foregroundBashBackgroundRequestHandler;
8380
- if (!handler) return false;
8381
- handler();
8382
- return true;
8977
+ async requestForegroundBashBackground(): Promise<boolean> {
8978
+ if (!this.#foldCoordinator.hasFoldableParticipant()) return false;
8979
+ try {
8980
+ const result = await this.#foldCoordinator.requestFold();
8981
+ return result.status === "folded";
8982
+ } catch (error) {
8983
+ logger.warn("Foreground fold request failed", {
8984
+ error: error instanceof Error ? error.message : String(error),
8985
+ });
8986
+ return false;
8987
+ }
8383
8988
  }
8384
8989
 
8385
8990
  /**
@@ -9320,6 +9925,7 @@ export class AgentSession {
9320
9925
  if (this.#sessionAdmissionClosed || this.#isDisposed) throw this.#sessionAdmissionBusyError();
9321
9926
  const inFlightPrompt = this.#beginInFlight();
9322
9927
  let hindsightRecall: string | undefined;
9928
+ let continuationSdkRunToken: string | undefined;
9323
9929
  try {
9324
9930
  const volatileProjectContextMessage = await this.#buildVolatileProjectContextMessage();
9325
9931
  this.agent.appendMessage(volatileProjectContextMessage);
@@ -9355,14 +9961,18 @@ export class AgentSession {
9355
9961
  await this.agent.continue({
9356
9962
  ...this.#managedFallbackPromptOptions(),
9357
9963
  onRunAccepted: (handle: AttemptRunHandle) => {
9358
- this.#acceptRunHandle(handle);
9964
+ continuationSdkRunToken = this.#activeSdkRunToken;
9965
+ this.#acceptSdkAttemptRun(handle, continuationSdkRunToken);
9359
9966
  if (hindsightRecall) hindsightState?.markRecallSnippetInjected(hindsightRecall);
9360
9967
  },
9361
9968
  });
9362
- await this.#waitForPostPromptRecovery();
9969
+ if (continuationSdkRunToken === undefined) await this.#waitForPostPromptRecovery();
9363
9970
  } finally {
9364
9971
  this.#removeEphemeralCustomMessages();
9365
- await this.#settleEndedInFlight(inFlightPrompt);
9972
+ await this.#settleEndedInFlight(
9973
+ inFlightPrompt,
9974
+ continuationSdkRunToken === undefined ? "full" : "publication",
9975
+ );
9366
9976
  }
9367
9977
  }
9368
9978
 
@@ -9490,8 +10100,9 @@ export class AgentSession {
9490
10100
  async setCredentialPin(provider: string, selector: AuthCredentialSelector): Promise<void> {
9491
10101
  const scopeId = this.credentialSessionId;
9492
10102
  const authStorage = this.#modelRegistry.authStorage;
9493
- const target = authStorage.resolveOAuthPinTarget(provider, selector);
9494
- authStorage.setSessionCredentialSelector(scopeId, provider, target.canonicalSelector);
10103
+ const authStorageOwner = this.#modelRegistry.getAuthStorageOwner();
10104
+ const target = authStorage.resolveOAuthPinTarget(provider, selector, authStorageOwner);
10105
+ authStorage.setSessionCredentialSelector(scopeId, provider, target.canonicalSelector, authStorageOwner);
9495
10106
  if (target.canonicalSelector.kind === "id" && !this.#credentialStoreIdentity) return;
9496
10107
  this.sessionManager.appendCustomEntry("auth-credential-pin", {
9497
10108
  v: 1,
@@ -9571,9 +10182,13 @@ export class AgentSession {
9571
10182
  args = "",
9572
10183
  options?: Pick<
9573
10184
  PromptOptions,
9574
- "onPreflightAccepted" | "onPreflightAcceptCommit" | "onSkillPrepared" | "preflightSignal"
10185
+ "onPreflightAccepted" | "onPreflightAcceptCommit" | "onSkillPrepared" | "preflightSignal" | "sdkRunCapability"
9575
10186
  >,
9576
10187
  ): Promise<{ name: string; path: string; args?: string; lineCount?: number }> {
10188
+ const internalOptions = {
10189
+ ...options,
10190
+ sdkRunToken: readSdkRunCapability(options?.sdkRunCapability),
10191
+ };
9577
10192
  if (options?.preflightSignal?.aborted) throw promptPreflightCancelledError();
9578
10193
  const skillName = name.trim();
9579
10194
  if (!skillName) throw Object.assign(new Error("skill.invoke requires a skill name."), { code: "invalid_input" });
@@ -9619,7 +10234,7 @@ export class AgentSession {
9619
10234
  lineCount: built.details.lineCount,
9620
10235
  cleanedArgs: activation.cleanedArgs || undefined,
9621
10236
  });
9622
- await this.promptCustomMessage(skillPromptMessage, options);
10237
+ await this.promptCustomMessage(skillPromptMessage, internalOptions as InternalCustomMessageOptions);
9623
10238
  return {
9624
10239
  name: skill.name,
9625
10240
  path: skill.filePath,
@@ -10309,6 +10924,14 @@ export class AgentSession {
10309
10924
  * @throws Error if no model selected or no API key available (when not streaming)
10310
10925
  */
10311
10926
  async prompt(text: string, options?: PromptOptions): Promise<void> {
10927
+ const hasUsableImage =
10928
+ options?.images?.some(image => typeof image?.data === "string" && image.data.trim().length > 0) === true;
10929
+ if (typeof text !== "string" || (text.trim().length === 0 && !hasUsableImage))
10930
+ throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
10931
+ const sdkRunToken = readSdkRunCapability(options?.sdkRunCapability);
10932
+ const internalOptions: InternalPromptOptions | undefined = options
10933
+ ? { ...options, ...(sdkRunToken ? { sdkRunToken } : {}) }
10934
+ : undefined;
10312
10935
  this.#assertRecoveryHydrationPromoted();
10313
10936
  const owner = this.#sessionAdmissionContext.getStore();
10314
10937
  if (owner && !owner.released) throw this.#sessionAdmissionBusyError();
@@ -10323,13 +10946,19 @@ export class AgentSession {
10323
10946
  await this.invokeSkill(
10324
10947
  invocation.skill.name,
10325
10948
  invocation.args,
10326
- options?.onPreflightAccepted || options?.onPreflightAcceptCommit || options?.preflightSignal
10949
+ options?.onPreflightAccepted ||
10950
+ options?.onPreflightAcceptCommit ||
10951
+ options?.preflightSignal ||
10952
+ internalOptions?.sdkRunToken
10327
10953
  ? {
10328
- ...(options.onPreflightAccepted ? { onPreflightAccepted: options.onPreflightAccepted } : {}),
10329
- ...(options.onPreflightAcceptCommit
10954
+ ...(options?.onPreflightAccepted
10955
+ ? { onPreflightAccepted: options.onPreflightAccepted }
10956
+ : {}),
10957
+ ...(options?.onPreflightAcceptCommit
10330
10958
  ? { onPreflightAcceptCommit: options.onPreflightAcceptCommit }
10331
10959
  : {}),
10332
- ...(options.preflightSignal ? { preflightSignal: options.preflightSignal } : {}),
10960
+ ...(options?.preflightSignal ? { preflightSignal: options.preflightSignal } : {}),
10961
+ ...(options?.sdkRunCapability ? { sdkRunCapability: options.sdkRunCapability } : {}),
10333
10962
  }
10334
10963
  : undefined,
10335
10964
  );
@@ -10363,6 +10992,10 @@ export class AgentSession {
10363
10992
 
10364
10993
  // Expand file-based prompt templates if requested
10365
10994
  const expandedText = expandPromptTemplates ? expandPromptTemplate(text, [...this.#promptTemplates]) : text;
10995
+ const expandedHasUsableImage =
10996
+ options?.images?.some(image => typeof image?.data === "string" && image.data.trim().length > 0) === true;
10997
+ if (expandedText.trim().length === 0 && !expandedHasUsableImage)
10998
+ throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
10366
10999
  assertImagePlaceholdersHavePayload(expandedText, options?.images);
10367
11000
  const workflowIntentDiff = options?.synthetic ? null : buildWorkflowIntentDiff(expandedText);
10368
11001
  const claimsGenuineUserIntent = !options?.synthetic && options?.attribution !== "agent";
@@ -10583,8 +11216,13 @@ export class AgentSession {
10583
11216
  | "onPreflightAccepted"
10584
11217
  | "onPreflightAcceptCommit"
10585
11218
  | "preflightSignal"
11219
+ | "sdkRunCapability"
10586
11220
  >,
10587
11221
  ): Promise<void> {
11222
+ const sdkRunToken = readSdkRunCapability(options?.sdkRunCapability);
11223
+ const internalOptions: InternalCustomMessageOptions | undefined = options
11224
+ ? { ...options, ...(sdkRunToken ? { sdkRunToken } : {}) }
11225
+ : undefined;
10588
11226
  if (options?.preflightSignal?.aborted) throw promptPreflightCancelledError();
10589
11227
  const textContent =
10590
11228
  typeof message.content === "string"
@@ -10653,7 +11291,7 @@ export class AgentSession {
10653
11291
  };
10654
11292
  try {
10655
11293
  await this.#promptWithMessage(customMessage, textContent, {
10656
- ...options,
11294
+ ...internalOptions,
10657
11295
  onPreflightAccepted: undefined,
10658
11296
  onPreflightAcceptCommit: commitAcceptance,
10659
11297
  admissionLease: admission,
@@ -10685,6 +11323,7 @@ export class AgentSession {
10685
11323
  | "onPreflightAcceptCommit"
10686
11324
  | "preflightSignal"
10687
11325
  > & {
11326
+ sdkRunToken?: string;
10688
11327
  prependMessages?: AgentMessage[];
10689
11328
  skipPostPromptRecoveryWait?: boolean;
10690
11329
  predecessorAgentEndHold?: symbol;
@@ -10751,6 +11390,9 @@ export class AgentSession {
10751
11390
  const rosterClaim = this.#claimIrcRosterCandidate();
10752
11391
  let hasPendingNextTurnMessages = false;
10753
11392
  let pendingNextTurnMessageCount = 0;
11393
+ let skipPostPromptRecoveryWait =
11394
+ options?.skipPostPromptRecoveryWait === true || options?.sdkRunToken !== undefined;
11395
+ let promptAttemptScope: AttemptScope | undefined;
10754
11396
  let hindsightRecall: string | undefined;
10755
11397
  try {
10756
11398
  this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
@@ -11017,8 +11659,14 @@ export class AgentSession {
11017
11659
  const agentPromptOptions = {
11018
11660
  ...(options?.toolChoice ? { toolChoice: options.toolChoice } : undefined),
11019
11661
  ...this.#managedFallbackPromptOptions(),
11662
+ ...(options?.sdkRunToken ? { sdkRunToken: options.sdkRunToken } : {}),
11020
11663
  onRunAccepted: (handle: AttemptRunHandle) => {
11021
- this.#acceptRunHandle(handle);
11664
+ promptAttemptScope = handle?.scope;
11665
+ if (handle) this.#acceptSdkAttemptRun(handle, options?.sdkRunToken);
11666
+ else this.#acceptRunHandle(handle);
11667
+ if (handle && options?.sdkRunToken) {
11668
+ this.#activeSdkRunToken = options.sdkRunToken;
11669
+ }
11022
11670
  options?.onRunAccepted?.(handle);
11023
11671
  options?.admissionLease?.release();
11024
11672
  // R3.3: the accepted-run wrapper is the exact acceptance boundary —
@@ -11051,6 +11699,20 @@ export class AgentSession {
11051
11699
  options?.onPreflightAccepted?.();
11052
11700
  },
11053
11701
  });
11702
+ const activeTerminalScope = this.#activeAttemptScope;
11703
+ const terminalAttemptScope =
11704
+ activeTerminalScope !== undefined &&
11705
+ (this.#skipPostPromptRecoveryWaitByAttemptScope.has(activeTerminalScope) ||
11706
+ (options?.sdkRunToken !== undefined &&
11707
+ this.#sdkRunTokensByAttemptScope.get(activeTerminalScope) === options.sdkRunToken))
11708
+ ? activeTerminalScope
11709
+ : promptAttemptScope;
11710
+ if (
11711
+ terminalAttemptScope !== undefined &&
11712
+ this.#skipPostPromptRecoveryWaitByAttemptScope.delete(terminalAttemptScope)
11713
+ ) {
11714
+ skipPostPromptRecoveryWait = true;
11715
+ }
11054
11716
  const terminalAssistant = this.#findLastAssistantMessage();
11055
11717
  if (
11056
11718
  rosterClaim &&
@@ -11060,7 +11722,7 @@ export class AgentSession {
11060
11722
  ) {
11061
11723
  this.#commitIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
11062
11724
  }
11063
- if (!options?.skipPostPromptRecoveryWait) {
11725
+ if (!skipPostPromptRecoveryWait) {
11064
11726
  await this.#waitForPostPromptRecovery();
11065
11727
  }
11066
11728
  } catch (error) {
@@ -11085,7 +11747,7 @@ export class AgentSession {
11085
11747
  this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
11086
11748
  }
11087
11749
  this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
11088
- await this.#settleEndedInFlight(inFlightPrompt, options?.skipPostPromptRecoveryWait ? "publication" : "full");
11750
+ await this.#settleEndedInFlight(inFlightPrompt, skipPostPromptRecoveryWait ? "publication" : "full");
11089
11751
  }
11090
11752
  }
11091
11753
 
@@ -11289,12 +11951,18 @@ export class AgentSession {
11289
11951
  * Queue a steering message to interrupt the agent mid-run.
11290
11952
  */
11291
11953
  async steer(text: string, images?: ImageContent[]): Promise<void> {
11954
+ const hasUsableImage =
11955
+ images?.some(image => typeof image?.data === "string" && image.data.trim().length > 0) === true;
11956
+ if (typeof text !== "string" || (text.trim().length === 0 && !hasUsableImage))
11957
+ throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
11292
11958
  this.#assertRecoveryHydrationPromoted();
11293
11959
  if (text.startsWith("/")) {
11294
11960
  this.#throwIfExtensionCommand(text);
11295
11961
  }
11296
11962
 
11297
11963
  const expandedText = expandPromptTemplate(text, [...this.#promptTemplates]);
11964
+ if (expandedText.trim().length === 0 && !hasUsableImage)
11965
+ throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
11298
11966
  assertImagePlaceholdersHavePayload(expandedText, images);
11299
11967
  await this.#queueSteer(expandedText, images, { claimsGenuineUserIntent: true });
11300
11968
  }
@@ -11307,12 +11975,18 @@ export class AgentSession {
11307
11975
  images?: ImageContent[],
11308
11976
  options?: Pick<PromptOptions, "followUpQueuePolicy">,
11309
11977
  ): Promise<void> {
11978
+ const hasUsableImage =
11979
+ images?.some(image => typeof image?.data === "string" && image.data.trim().length > 0) === true;
11980
+ if (typeof text !== "string" || (text.trim().length === 0 && !hasUsableImage))
11981
+ throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
11310
11982
  this.#assertRecoveryHydrationPromoted();
11311
11983
  if (text.startsWith("/")) {
11312
11984
  this.#throwIfExtensionCommand(text);
11313
11985
  }
11314
11986
 
11315
11987
  const expandedText = expandPromptTemplate(text, [...this.#promptTemplates]);
11988
+ if (expandedText.trim().length === 0 && !hasUsableImage)
11989
+ throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
11316
11990
  assertImagePlaceholdersHavePayload(expandedText, images);
11317
11991
  await this.#queueFollowUp(expandedText, images, {
11318
11992
  forceOneAtATime: options?.followUpQueuePolicy === "sequential",
@@ -11330,6 +12004,7 @@ export class AgentSession {
11330
12004
  claimsGenuineUserIntent?: boolean;
11331
12005
  onPromoted?: (promotion: { startsOwnRun?: boolean; removed?: boolean }) => void;
11332
12006
  external?: boolean;
12007
+ sdkRunToken?: string;
11333
12008
  },
11334
12009
  ): Promise<void> {
11335
12010
  this.#assertNoHandoffTransition();
@@ -11344,6 +12019,7 @@ export class AgentSession {
11344
12019
  this.#externalSteerAdmissionSeq.set(message, ++this.#steeringAdmissionSeq);
11345
12020
  if (options.onPromoted) this.#steerPromotionHooks.set(message, options.onPromoted);
11346
12021
  }
12022
+ if (options?.sdkRunToken) this.#sdkRunTokensByQueuedMessage.set(message, options.sdkRunToken);
11347
12023
  if (options?.claimsGenuineUserIntent) {
11348
12024
  const epoch = this.#claimDeepInterviewUserIntent();
11349
12025
  this.#deepInterviewGenuineUserMessageEpochs.set(message, epoch);
@@ -11926,9 +12602,11 @@ export class AgentSession {
11926
12602
  /** Internal dispatch disposition used before actual queue consumption. */
11927
12603
  onDispatchDisposition?: (promotion: { startsOwnRun: boolean }) => void;
11928
12604
  preflightSignal?: AbortSignal;
11929
- sdkRunToken?: string;
12605
+ sdkRunCapability?: unknown;
11930
12606
  },
11931
12607
  ): Promise<void> {
12608
+ const sdkRunToken = readSdkRunCapability(options?.sdkRunCapability);
12609
+ const internalOptions = options ? { ...options, ...(sdkRunToken ? { sdkRunToken } : {}) } : undefined;
11932
12610
  this.#assertRecoveryHydrationPromoted();
11933
12611
  const owner = this.#sessionAdmissionContext.getStore();
11934
12612
  if (owner && !owner.released) throw this.#sessionAdmissionBusyError();
@@ -11949,7 +12627,15 @@ export class AgentSession {
11949
12627
  const textParts: string[] = [];
11950
12628
  images = [];
11951
12629
  for (const part of content) {
12630
+ if (!part || typeof part !== "object" || Array.isArray(part))
12631
+ throw Object.assign(new Error("sendUserMessage content blocks must be objects."), {
12632
+ code: "invalid_input",
12633
+ });
11952
12634
  if (part.type === "text") {
12635
+ if (typeof part.text !== "string")
12636
+ throw Object.assign(new Error("sendUserMessage text blocks must contain string text."), {
12637
+ code: "invalid_input",
12638
+ });
11953
12639
  textParts.push(part.text);
11954
12640
  } else {
11955
12641
  images.push(part);
@@ -11958,6 +12644,10 @@ export class AgentSession {
11958
12644
  text = textParts.join("\n");
11959
12645
  if (images.length === 0) images = undefined;
11960
12646
  }
12647
+ const hasUsableImage =
12648
+ images?.some(image => typeof image?.data === "string" && image.data.trim().length > 0) === true;
12649
+ if (text.trim().length === 0 && !hasUsableImage)
12650
+ throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
11961
12651
 
11962
12652
  let admissionSignal = options?.preflightSignal
11963
12653
  ? AbortSignal.any([this.#promptPreflightAbortController.signal, options.preflightSignal])
@@ -12051,7 +12741,7 @@ export class AgentSession {
12051
12741
  claimsGenuineUserIntent: true,
12052
12742
  forceOneAtATime: Boolean(options?.preflightSignal || options?.queuedAtDispatch),
12053
12743
  onPromoted: options?.onQueuedPromoted,
12054
- sdkRunToken: options?.sdkRunToken,
12744
+ sdkRunToken: internalOptions?.sdkRunToken,
12055
12745
  });
12056
12746
  const cancelQueuedFollowUp = () => queuedFollowUp.cancel();
12057
12747
  options?.preflightSignal?.addEventListener("abort", cancelQueuedFollowUp, { once: true });
@@ -12066,6 +12756,7 @@ export class AgentSession {
12066
12756
  claimsGenuineUserIntent: true,
12067
12757
  onPromoted: options?.onQueuedPromoted,
12068
12758
  external: true,
12759
+ sdkRunToken: internalOptions?.sdkRunToken,
12069
12760
  });
12070
12761
  options?.onPreflightAccepted?.();
12071
12762
  return;
@@ -12085,6 +12776,7 @@ export class AgentSession {
12085
12776
  claimsGenuineUserIntent: true,
12086
12777
  onPromoted: options?.onQueuedPromoted,
12087
12778
  external: true,
12779
+ sdkRunToken: internalOptions?.sdkRunToken,
12088
12780
  });
12089
12781
  // Dispatch-race disposition (#4668 review P1): the SDK snapshot-decided
12090
12782
  // this submission starts its own turn (idle at dispatch), but the
@@ -12110,6 +12802,7 @@ export class AgentSession {
12110
12802
  await this.prompt(text, {
12111
12803
  expandPromptTemplates: false,
12112
12804
  images,
12805
+ sdkRunToken: internalOptions?.sdkRunToken,
12113
12806
  onPreflightAccepted: () => {
12114
12807
  options?.onPreflightAccepted?.();
12115
12808
  fireQueuedPromotion();
@@ -12124,7 +12817,7 @@ export class AgentSession {
12124
12817
  }
12125
12818
  : undefined,
12126
12819
  preflightSignal: options?.preflightSignal,
12127
- });
12820
+ } as InternalPromptOptions);
12128
12821
  } finally {
12129
12822
  releaseFollowUpReservation();
12130
12823
  }
@@ -12633,6 +13326,7 @@ export class AgentSession {
12633
13326
  | "tool_abort"
12634
13327
  | "internal";
12635
13328
  silent?: boolean;
13329
+ sdkRunToken?: string;
12636
13330
  }): void {
12637
13331
  const abortGoalState = this.getGoalModeState();
12638
13332
  this.#suppressNextGoalReminderAfterAbortGoalId =
@@ -13334,6 +14028,9 @@ export class AgentSession {
13334
14028
  attribution: "user" as const,
13335
14029
  timestamp: Date.now(),
13336
14030
  };
14031
+ const selectedSdkRunToken = selectedMessage
14032
+ ? this.#sdkRunTokensByQueuedMessage.get(selectedMessage)
14033
+ : undefined;
13337
14034
  const messageText =
13338
14035
  message.role === "custom"
13339
14036
  ? this.#getCustomMessageTextContent(message)
@@ -13352,6 +14049,7 @@ export class AgentSession {
13352
14049
  await this.#promptWithMessage(message, messageText, {
13353
14050
  admissionLease: admission,
13354
14051
  resetRetryReplaySafety: true,
14052
+ sdkRunToken: selectedSdkRunToken,
13355
14053
  onRunAccepted: () => {
13356
14054
  runAccepted = true;
13357
14055
  if (selected) this.#fireQueuedPromotionHooks([message], { startsOwnRun: true });
@@ -13461,6 +14159,8 @@ export class AgentSession {
13461
14159
  this.#cancelOwnAsyncJobs();
13462
14160
  this.#closeAllProviderSessions("new session");
13463
14161
  this.#rebindProviderSessionState(new Map());
14162
+ this.#terminalizeQueuedSdkWorkForSessionTransition(this.#queuedMessagesForSessionTransition());
14163
+ this.#resetActiveSdkRunOwnership();
13464
14164
  this.agent.reset();
13465
14165
  if (!options?.drop) await this.sessionManager.flush();
13466
14166
  const noLeasePreviousSessionIdentity = this.sessionManager.getSessionId();
@@ -13558,6 +14258,8 @@ export class AgentSession {
13558
14258
  this.#disconnectFromAgent();
13559
14259
  this.#closeAllProviderSessions("new session");
13560
14260
  this.#rebindProviderSessionState(new Map());
14261
+ this.#terminalizeQueuedSdkWorkForSessionTransition(this.#queuedMessagesForSessionTransition());
14262
+ this.#resetActiveSdkRunOwnership();
13561
14263
  this.agent.reset();
13562
14264
  this.setTodoPhases([]);
13563
14265
  this.#syncAgentSessionId();
@@ -13680,6 +14382,8 @@ export class AgentSession {
13680
14382
  this.yieldQueue.clear();
13681
14383
  this.#pendingBackgroundExchanges = [];
13682
14384
  this.#closeAllProviderSessions("context clear");
14385
+ this.#terminalizeQueuedSdkWorkForSessionTransition(this.#queuedMessagesForSessionTransition());
14386
+ this.#resetActiveSdkRunOwnership();
13683
14387
  this.agent.reset();
13684
14388
  await this.sessionManager.flush();
13685
14389
  this.sessionManager.appendContextClearEntry({ sessionId });
@@ -16104,6 +16808,7 @@ export class AgentSession {
16104
16808
  const rollbackPendingNextTurnMessages = [...this.#pendingNextTurnMessages];
16105
16809
  const rollbackScheduledHiddenNextTurnGeneration = this.#scheduledHiddenNextTurnGeneration;
16106
16810
  const rollbackTodoReminderCount = this.#todoReminderCount;
16811
+ const rollbackDeferredSdkFollowUps = [...this.#deferredSdkFollowUps];
16107
16812
  // Snapshot the agent's executable queues so a rollback restores queued
16108
16813
  // user work that agent.reset() would otherwise clear.
16109
16814
  const rollbackAgentSteeringQueue = this.agent.snapshotSteering();
@@ -16144,6 +16849,12 @@ export class AgentSession {
16144
16849
  this.#rekeyJobManagerForSessionIdentity(rollbackSessionState.sessionId, rollbackSessionState.sessionFile);
16145
16850
  committed = true;
16146
16851
  await this.#runToolSessionTransitionCleanups();
16852
+ this.#terminalizeQueuedSdkWorkForSessionTransition([
16853
+ ...rollbackAgentSteeringQueue,
16854
+ ...rollbackAgentFollowUpQueue,
16855
+ ...rollbackDeferredSdkFollowUps,
16856
+ ]);
16857
+ this.#resetActiveSdkRunOwnership();
16147
16858
  this.agent.reset();
16148
16859
  this.#syncAgentSessionId();
16149
16860
  this.#rekeyHindsightMemoryForCurrentSessionId();
@@ -16227,6 +16938,7 @@ export class AgentSession {
16227
16938
  this.#pendingNextTurnMessages = rollbackPendingNextTurnMessages;
16228
16939
  this.#scheduledHiddenNextTurnGeneration = rollbackScheduledHiddenNextTurnGeneration;
16229
16940
  this.#todoReminderCount = rollbackTodoReminderCount;
16941
+ this.#deferredSdkFollowUps = rollbackDeferredSdkFollowUps;
16230
16942
  this.#syncTodoPhasesFromBranch();
16231
16943
  // Exact-discard only the staged successor; predecessor state was never adopted.
16232
16944
  const rollbackError = prepared
@@ -18899,6 +19611,9 @@ export class AgentSession {
18899
19611
  const continuation = this.agent.continue({
18900
19612
  ...this.#managedFallbackPromptOptions(),
18901
19613
  transientRecoveryMessage: this.#escapedNonAsciiRecoveryMessage(),
19614
+ onRunAccepted: (handle: AttemptRunHandle) => {
19615
+ this.#acceptSdkAttemptRun(handle, this.#activeSdkRunToken);
19616
+ },
18902
19617
  });
18903
19618
  if (
18904
19619
  cancellationSignal?.aborted ||
@@ -18937,6 +19652,9 @@ export class AgentSession {
18937
19652
  await this.agent.continue({
18938
19653
  ...this.#managedFallbackPromptOptions(),
18939
19654
  transientRecoveryMessage: this.#escapedNonAsciiRecoveryMessage(),
19655
+ onRunAccepted: (handle: AttemptRunHandle) => {
19656
+ this.#acceptSdkAttemptRun(handle, this.#activeSdkRunToken);
19657
+ },
18940
19658
  });
18941
19659
  },
18942
19660
  };
@@ -18995,6 +19713,18 @@ export class AgentSession {
18995
19713
  },
18996
19714
  };
18997
19715
  }
19716
+ // Issue #5018 preserves managed behavior for the typed statusless
19717
+ // Responses overload: before the code survived transport, this failure
19718
+ // reached the session as an ordinary committed error, so the chain never
19719
+ // discarded or advanced on it. Route it to the exhaustion decision
19720
+ // directly, before the retryable path can classify its new facts.
19721
+ if (isStatuslessTypedOverloadFacts(outcome.failure.transportFailure)) {
19722
+ this.#defaultFallbackChain().resetAttemptBudget();
19723
+ return this.#managedFallbackExhaustionDecision(
19724
+ outcome.failure.message,
19725
+ outcome.failure.message.errorMessage || "Model fallback attempt failed",
19726
+ );
19727
+ }
18998
19728
  return this.#handleRetryableError(
18999
19729
  outcome.failure.message,
19000
19730
  true,
@@ -19354,11 +20084,13 @@ export class AgentSession {
19354
20084
  if (!isAuthenticated(activeApiKey)) return "unchanged";
19355
20085
  remaining = await authStorage.invalidateCredentialMatching(provider, activeApiKey, {
19356
20086
  sessionId: credentialSessionId,
20087
+ owner: this.#modelRegistry.getAuthStorageOwner(),
19357
20088
  });
19358
20089
  if (!remaining) return "unchanged";
19359
20090
  } else {
19360
20091
  remaining = await authStorage.markUsageLimitReached(provider, credentialSessionId, {
19361
20092
  retryAfterMs: trigger.retryAfterMs,
20093
+ owner: this.#modelRegistry.getAuthStorageOwner(),
19362
20094
  });
19363
20095
  }
19364
20096
 
@@ -19453,7 +20185,10 @@ export class AgentSession {
19453
20185
  }
19454
20186
  const firstEventTimeout = classification === "first_event_timeout";
19455
20187
  const emptyResponse = classification === "empty_response";
19456
- const canReplayProviderOverload = isBareDefaultCodexOverload(message) || isBareDefaultAnthropicOverload(message);
20188
+ const canReplayProviderOverload =
20189
+ isBareDefaultCodexOverload(message) ||
20190
+ isBareDefaultAnthropicOverload(message) ||
20191
+ isBareDefaultOpenAIResponsesOverload(message);
19457
20192
  const reportedRetryMaxAttempts = transportFailure?.retryMaxAttempts;
19458
20193
  if (reportedRetryMaxAttempts !== undefined) {
19459
20194
  this.#providerRetryMaxAttempts = Math.min(
@@ -19496,6 +20231,17 @@ export class AgentSession {
19496
20231
  const trigger:
19497
20232
  | { class: FallbackTriggerClass; retryAfterMs?: number; authDisposition?: AuthDisposition }
19498
20233
  | undefined = this.#fallbackTriggerFor(message, !managedFallback, transportFailure);
20234
+ // OpenAI's typed statusless capacity-overload code (issue #5018) must not
20235
+ // gain managed-chain retry/advance authority from its new facts. Before
20236
+ // the code survived transport, this failure reached the session as an
20237
+ // ordinary committed error and surfaced immediately, so mirror that
20238
+ // behavior with the existing exhaustion decision.
20239
+ if (managedFallback && isStatuslessTypedOverloadFacts(transportFailure)) {
20240
+ return this.#managedFallbackExhaustionDecision(
20241
+ message,
20242
+ message.errorMessage || "Model fallback attempt failed",
20243
+ );
20244
+ }
19499
20245
  if (!trigger) {
19500
20246
  return managedOutcome
19501
20247
  ? this.#managedFallbackExhaustionDecision(message, message.errorMessage || "Model fallback attempt failed")
@@ -19852,7 +20598,12 @@ export class AgentSession {
19852
20598
  this.#resolveRetry();
19853
20599
  return;
19854
20600
  }
19855
- await this.agent.continue(this.#managedFallbackPromptOptions());
20601
+ await this.agent.continue({
20602
+ ...this.#managedFallbackPromptOptions(),
20603
+ onRunAccepted: (handle: AttemptRunHandle) => {
20604
+ this.#acceptSdkAttemptRun(handle, this.#activeSdkRunToken);
20605
+ },
20606
+ });
19856
20607
  return;
19857
20608
  } catch (error) {
19858
20609
  const attempt = this.#retryAttempt;
@@ -19934,6 +20685,7 @@ export class AgentSession {
19934
20685
  preSubmit: PreSubmitBuilder,
19935
20686
  options?: {
19936
20687
  toolChoice?: ToolChoice;
20688
+ sdkRunToken?: string;
19937
20689
  fallbackManaged?: boolean;
19938
20690
  onRunAccepted?: (handle: AttemptRunHandle) => void;
19939
20691
  },
@@ -20888,6 +21640,9 @@ export class AgentSession {
20888
21640
  model.provider,
20889
21641
  this.#modelRegistry.authStorage,
20890
21642
  this.credentialSessionId,
21643
+ model,
21644
+ // Optional for the same reason as the main metadata resolver above.
21645
+ this.#modelRegistry.getAuthStorageOwner?.(),
20891
21646
  ),
20892
21647
  reasoning: toReasoningEffort(this.thinkingLevel),
20893
21648
  hideThinkingSummary: this.agent.hideThinkingSummary,
@@ -21233,6 +21988,10 @@ export class AgentSession {
21233
21988
  const previousSystemPrompt = this.agent.state.systemPrompt;
21234
21989
  const previousAgentSteeringQueue = this.agent.snapshotSteering();
21235
21990
  const previousAgentFollowUpQueue = this.agent.snapshotFollowUp();
21991
+ const previousDeferredSdkFollowUps = [...this.#deferredSdkFollowUps];
21992
+ const previousActiveSdkRunToken = this.#activeSdkRunToken;
21993
+ const previousActiveAttemptScope = this.#activeAttemptScope;
21994
+ const previousActiveLogicalRunId = this.#activeLogicalRunId;
21236
21995
 
21237
21996
  this.#steeringMessages = [];
21238
21997
  this.#followUpMessages = [];
@@ -21280,6 +22039,7 @@ export class AgentSession {
21280
22039
  // The target session is loaded and MCP selections are restored: discard
21281
22040
  // pre-switch delivery queues before completing the restored agent state.
21282
22041
  this.agent.clearAllQueues();
22042
+ this.#resetActiveSdkRunOwnership();
21283
22043
 
21284
22044
  if (historyRewriteReason) {
21285
22045
  this.agent.replaceMessages(sessionContext.messages, {
@@ -21464,6 +22224,12 @@ export class AgentSession {
21464
22224
  ...(options?.transition ? { transition: options.transition } : {}),
21465
22225
  });
21466
22226
  }
22227
+ this.#terminalizeQueuedSdkWorkForSessionTransition([
22228
+ ...previousAgentSteeringQueue,
22229
+ ...previousAgentFollowUpQueue,
22230
+ ...previousDeferredSdkFollowUps,
22231
+ ]);
22232
+ this.#deferredSdkFollowUps = [];
21467
22233
  return true;
21468
22234
  } catch (error) {
21469
22235
  if (transitionCleanupCommitted) throw error;
@@ -21529,6 +22295,10 @@ export class AgentSession {
21529
22295
  this.#followUpMessages = previousFollowUpMessages;
21530
22296
  this.#pendingNextTurnMessages = previousPendingNextTurnMessages;
21531
22297
  this.#scheduledHiddenNextTurnGeneration = previousScheduledHiddenNextTurnGeneration;
22298
+ this.#deferredSdkFollowUps = previousDeferredSdkFollowUps;
22299
+ this.#activeSdkRunToken = previousActiveSdkRunToken;
22300
+ this.#activeAttemptScope = previousActiveAttemptScope;
22301
+ this.#activeLogicalRunId = previousActiveLogicalRunId;
21532
22302
  this.agent.clearAllQueues();
21533
22303
  this.agent.restoreSteering(previousAgentSteeringQueue);
21534
22304
  this.agent.restoreFollowUp(previousAgentFollowUpQueue);
@@ -21595,6 +22365,9 @@ export class AgentSession {
21595
22365
  }
21596
22366
 
21597
22367
  const selectedText = this.#extractUserMessageText(selectedEntry.message.content);
22368
+ const previousAgentSteeringQueue = this.agent.snapshotSteering();
22369
+ const previousAgentFollowUpQueue = this.agent.snapshotFollowUp();
22370
+ const previousDeferredSdkFollowUps = [...this.#deferredSdkFollowUps];
21598
22371
 
21599
22372
  let skipConversationRestore = false;
21600
22373
 
@@ -21630,6 +22403,16 @@ export class AgentSession {
21630
22403
  }
21631
22404
  this.#pendingNextTurnMessages = [];
21632
22405
  this.#scheduledHiddenNextTurnGeneration = undefined;
22406
+ this.#terminalizeQueuedSdkWorkForSessionTransition([
22407
+ ...previousAgentSteeringQueue,
22408
+ ...previousAgentFollowUpQueue,
22409
+ ...previousDeferredSdkFollowUps,
22410
+ ]);
22411
+ this.#deferredSdkFollowUps = [];
22412
+ this.#resetActiveSdkRunOwnership();
22413
+ this.agent.clearAllQueues();
22414
+ this.#steeringMessages = [];
22415
+ this.#followUpMessages = [];
21633
22416
 
21634
22417
  this.#syncTodoPhasesFromBranch();
21635
22418
  this.#syncAgentSessionId();
@@ -21835,6 +22618,15 @@ export class AgentSession {
21835
22618
  this.sessionManager.branch(newLeafId);
21836
22619
  }
21837
22620
 
22621
+ // The history rewrite is now committed. Drop predecessor-owned queued SDK
22622
+ // work at this boundary; cancelled or failed preparation above preserves it.
22623
+ const queuedSdkWork = this.#queuedMessagesForSessionTransition();
22624
+ this.#terminalizeQueuedSdkWorkForSessionTransition(queuedSdkWork);
22625
+ this.#deferredSdkFollowUps = [];
22626
+ this.agent.clearAllQueues();
22627
+ this.#steeringMessages = [];
22628
+ this.#followUpMessages = [];
22629
+
21838
22630
  // Update agent state through the canonical filtered display context so legacy
21839
22631
  // request-scoped entries cannot re-enter live history after tree navigation.
21840
22632
  const displayContext = this.buildDisplaySessionContext();