@gajae-code/coding-agent 0.15.5 → 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 (155) hide show
  1. package/CHANGELOG.md +26 -1
  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 +2 -0
  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 +96 -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 +2 -5
  22. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
  23. package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
  24. package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
  25. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +6 -0
  26. package/dist/types/index.d.ts +1 -0
  27. package/dist/types/main.d.ts +3 -1
  28. package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
  29. package/dist/types/modes/jobs-observer.d.ts +36 -0
  30. package/dist/types/sdk/broker/broker.d.ts +3 -0
  31. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
  32. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
  33. package/dist/types/sdk/client/discovery.d.ts +1 -0
  34. package/dist/types/sdk/host/model-pin.d.ts +7 -2
  35. package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
  36. package/dist/types/sdk/host/session-runtime.d.ts +15 -1
  37. package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
  38. package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
  39. package/dist/types/sdk/router/session-router.d.ts +9 -0
  40. package/dist/types/session/account-inventory.d.ts +1 -0
  41. package/dist/types/session/agent-session.d.ts +19 -7
  42. package/dist/types/session/fold-coordinator.d.ts +201 -0
  43. package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
  44. package/dist/types/session/sdk-run-capability.d.ts +2 -0
  45. package/dist/types/telemetry/control.d.ts +11 -0
  46. package/dist/types/telemetry/events.d.ts +20 -0
  47. package/dist/types/telemetry/index.d.ts +3 -0
  48. package/dist/types/telemetry/transport.d.ts +22 -0
  49. package/dist/types/tools/bash-interactive.d.ts +23 -0
  50. package/dist/types/tools/index.d.ts +6 -5
  51. package/dist/types/web/search/providers/base.d.ts +9 -4
  52. package/dist/types/web/search/providers/xai.d.ts +2 -1
  53. package/dist/types/web/search/types.d.ts +14 -0
  54. package/package.json +10 -7
  55. package/scripts/generate-hotkeys-docs.ts +4 -0
  56. package/scripts/generate-sdk-operation-inventory.ts +3 -3
  57. package/src/async/job-manager.ts +671 -39
  58. package/src/cli/auth-broker-cli.ts +40 -12
  59. package/src/cli/auth-gateway-cli.ts +273 -97
  60. package/src/cli/config-cli.ts +2 -2
  61. package/src/cli/fast-help.ts +3 -0
  62. package/src/cli/model-presets-cli.ts +100 -0
  63. package/src/cli/update-cli.ts +40 -5
  64. package/src/cli.ts +6 -2
  65. package/src/commands/auth-gateway.ts +12 -8
  66. package/src/commands/gc.ts +19 -0
  67. package/src/commands/model-presets.ts +24 -0
  68. package/src/config/autorouting-tier-map.ts +32 -0
  69. package/src/config/file-lock-gc.ts +9 -4
  70. package/src/config/file-lock.ts +775 -59
  71. package/src/config/internal/model-preset-registry-test-state.ts +28 -0
  72. package/src/config/internal/model-preset-registry-test-support.ts +11 -0
  73. package/src/config/keybindings.ts +6 -2
  74. package/src/config/model-preset-registry.ts +2799 -0
  75. package/src/config/model-profile-activation.ts +115 -28
  76. package/src/config/model-profile-contract.ts +4 -1
  77. package/src/config/model-profiles.ts +51 -9
  78. package/src/config/model-registry.ts +1021 -122
  79. package/src/config/settings-schema.ts +11 -0
  80. package/src/config/settings.ts +6 -1
  81. package/src/coordinator-mcp/model-preset.ts +32 -17
  82. package/src/coordinator-mcp/projection-scan.ts +571 -0
  83. package/src/coordinator-mcp/server.ts +19 -24
  84. package/src/coordinator-mcp/session-reaper.ts +9 -3
  85. package/src/extensibility/extensions/loader.ts +0 -1
  86. package/src/extensibility/extensions/types.ts +2 -3
  87. package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
  88. package/src/gjc-runtime/empty-delete-gc.ts +301 -0
  89. package/src/gjc-runtime/gc-render.ts +29 -0
  90. package/src/gjc-runtime/gc-runtime.ts +174 -11
  91. package/src/gjc-runtime/session-state-lock.ts +1242 -141
  92. package/src/gjc-runtime/session-state-sidecar.ts +76 -3
  93. package/src/index.ts +1 -0
  94. package/src/internal-urls/docs-index.generated.ts +9 -8
  95. package/src/lsp/config.ts +4 -3
  96. package/src/lsp/path-trust.ts +12 -6
  97. package/src/main.ts +9 -2
  98. package/src/modes/acp/acp-agent.ts +11 -5
  99. package/src/modes/components/jobs-overlay-model.ts +99 -16
  100. package/src/modes/components/model-selector.ts +158 -18
  101. package/src/modes/components/status-line/segments.ts +6 -1
  102. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  103. package/src/modes/controllers/input-controller.ts +23 -6
  104. package/src/modes/interactive-mode.ts +14 -1
  105. package/src/modes/jobs-observer.ts +149 -16
  106. package/src/modes/runtime-init.ts +1 -1
  107. package/src/modes/utils/injected-user-submission.ts +15 -1
  108. package/src/prompts/tools/bash.md +2 -2
  109. package/src/prompts/tools/fold-receipt.md +3 -0
  110. package/src/prompts/tools/job.md +1 -1
  111. package/src/sdk/acp/adapter.ts +17 -2
  112. package/src/sdk/broker/broker.ts +48 -21
  113. package/src/sdk/broker/lifecycle.ts +32 -6
  114. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  115. package/src/sdk/bus/chat-daemon-runtime.ts +11 -1
  116. package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
  117. package/src/sdk/bus/prompt-reconciliation.ts +28 -4
  118. package/src/sdk/bus/slack-conversation.ts +5 -1
  119. package/src/sdk/cli/session-cli.ts +12 -1
  120. package/src/sdk/client/discovery.ts +3 -1
  121. package/src/sdk/host/control/dispatch.ts +3 -0
  122. package/src/sdk/host/model-pin.ts +185 -25
  123. package/src/sdk/host/sdk-run-capability.ts +1 -0
  124. package/src/sdk/host/session-runtime.test.ts +371 -37
  125. package/src/sdk/host/session-runtime.ts +364 -52
  126. package/src/sdk/prompt-deadline-manager.ts +16 -1
  127. package/src/sdk/protocol/adapter-validation.ts +27 -0
  128. package/src/sdk/protocol/operation-inventory.generated.json +2 -2
  129. package/src/sdk/protocol/operation-registry.ts +4 -1
  130. package/src/sdk/router/session-router.ts +228 -96
  131. package/src/sdk/session.ts +83 -12
  132. package/src/session/account-inventory.ts +27 -11
  133. package/src/session/agent-session.ts +678 -103
  134. package/src/session/fold-coordinator.ts +433 -0
  135. package/src/session/sdk-run-capability-internal.ts +15 -0
  136. package/src/session/sdk-run-capability.ts +2 -0
  137. package/src/session/session-manager.ts +5 -4
  138. package/src/setup/provider-presets.json +1 -1
  139. package/src/slash-commands/helpers/credential-switch.ts +6 -1
  140. package/src/task/executor.ts +19 -3
  141. package/src/telemetry/control.ts +26 -0
  142. package/src/telemetry/events.ts +142 -0
  143. package/src/telemetry/index.ts +3 -0
  144. package/src/telemetry/transport.ts +87 -0
  145. package/src/tools/bash-interactive.ts +182 -85
  146. package/src/tools/bash.ts +772 -67
  147. package/src/tools/index.ts +7 -5
  148. package/src/tools/tool-catalog.generated.ts +1 -1
  149. package/src/web/search/provider.ts +120 -11
  150. package/src/web/search/providers/anthropic.ts +89 -17
  151. package/src/web/search/providers/base.ts +9 -4
  152. package/src/web/search/providers/gemini.ts +87 -15
  153. package/src/web/search/providers/openai-compatible.ts +23 -8
  154. package/src/web/search/providers/xai.ts +98 -12
  155. package/src/web/search/types.ts +16 -0
@@ -117,6 +117,7 @@ import {
117
117
  modelsAreEqual,
118
118
  streamSimple,
119
119
  } from "@gajae-code/ai/core";
120
+ import { normalizeAnthropicBaseUrl } from "@gajae-code/ai/providers/anthropic";
120
121
  import {
121
122
  type AuthDisposition,
122
123
  beginAttempt,
@@ -181,8 +182,10 @@ export interface ForkContextSeedOptions {
181
182
  }
182
183
 
183
184
  import type { AuthCredentialSelector } from "@gajae-code/ai/core";
185
+ import { isFoundryEnabled } from "@gajae-code/ai/utils/foundry";
184
186
  import type { MacOSPowerAssertion } from "@gajae-code/natives";
185
187
  import {
188
+ $pickCredentialEnv,
186
189
  extractRetryHint,
187
190
  hasFsCode,
188
191
  isEacces,
@@ -314,6 +317,7 @@ import {
314
317
  type CoordinatorToolObservation,
315
318
  ownerTerminalContextFromEnvironment,
316
319
  persistCoordinatorRuntimeStateFromEvent,
320
+ persistCoordinatorWorkerIntegrationOutcome,
317
321
  registerCoordinatorRuntimeStateFinalizer,
318
322
  UNPROVEN_TOOL_LABEL,
319
323
  } from "../gjc-runtime/session-state-sidecar";
@@ -443,6 +447,7 @@ import {
443
447
  } from "./contribution-prep";
444
448
  import { canonicalCoordinatorToolLabel } from "./coordinator-tool-label";
445
449
  import { pruneStaleFileMentions } from "./file-mention-pruning";
450
+ import { describeFoldReceipt, FOLD_WAKE_MERGE_WINDOW_MS, type FoldAdapter, FoldCoordinator } from "./fold-coordinator";
446
451
  import type { MemoryGuardRestoreResult } from "./memory-guard-checkpoint-participant";
447
452
  import {
448
453
  type BashExecutionMessage,
@@ -457,6 +462,7 @@ import {
457
462
  SKILL_PROMPT_MESSAGE_TYPE,
458
463
  } from "./messages";
459
464
  import { isLegacyProviderSafetyStopMessage } from "./provider-safety-stop";
465
+ import { readSdkRunCapability } from "./sdk-run-capability";
460
466
  import { formatSessionDumpText } from "./session-dump-format";
461
467
  import type {
462
468
  BranchSummaryEntry,
@@ -471,7 +477,6 @@ import type {
471
477
  SessionManagerCloseOutcome,
472
478
  SessionMemoryStats,
473
479
  } from "./session-manager";
474
-
475
480
  import {
476
481
  createReadonlySessionManager,
477
482
  getLatestCompactionEntry,
@@ -488,6 +493,8 @@ import {
488
493
  bindToolLineage,
489
494
  classifyOwnedEnvelope,
490
495
  isOwnedCompletionEnvelope,
496
+ isOwnedCompletionEnvelopeAllowed,
497
+ lookupOwnedRegistration,
491
498
  lookupTerminalScope,
492
499
  mintTurnLineageIdHash,
493
500
  type OwnedCompletionEnvelope,
@@ -496,11 +503,35 @@ import {
496
503
  settleToolLineageRegistrationWindow,
497
504
  unregisterOwnedRegistration,
498
505
  } from "./terminal-abort";
499
-
500
506
  import { ToolChoiceQueue } from "./tool-choice-queue";
501
507
  import { pruneSupersededMaintenanceReminders, pruneSupersededVolatileProjectContext } from "./volatile-context-pruning";
502
508
  import { YieldQueue } from "./yield-queue";
503
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
+
504
535
  /**
505
536
  * #4560: structured workflow recovery projection from canonical durable
506
537
  * Ralplan/Ultragoal state, consumed by the compaction summary context and
@@ -1048,10 +1079,23 @@ export interface PromptOptions {
1048
1079
  onSkillPrepared?: (meta: { name: string; path: string; lineCount?: number; cleanedArgs?: string }) => void;
1049
1080
  /** Optional invocation-scoped cancellation fence used before an accepted skill starts execution. */
1050
1081
  preflightSignal?: AbortSignal;
1051
- /** Internal SDK lifecycle owner token. */
1052
- sdkRunToken?: string;
1082
+ /** Internal host capability; ordinary package callers cannot construct the branded value. */
1083
+ sdkRunCapability?: unknown;
1053
1084
  }
1054
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
+
1055
1099
  function promptPreflightCancelledError(): Error {
1056
1100
  const error = Object.assign(new Error("Prompt preflight was cancelled before execution."), { code: "busy" });
1057
1101
  error.name = "PromptPreflightCancelledError";
@@ -1626,14 +1670,44 @@ function buildSessionMetadata(
1626
1670
  provider: string,
1627
1671
  authStorage: AuthStorage | undefined,
1628
1672
  credentialSessionId = sessionId,
1673
+ model?: Model,
1674
+ owner?: object,
1629
1675
  ): Record<string, unknown> {
1630
1676
  const userId: Record<string, string> = { session_id: sessionId };
1631
1677
  // Only look up account_uuid when the request is going to Anthropic. Injecting
1632
1678
  // a Anthropic model OAuth account_uuid into requests bound for other providers (including
1633
1679
  // Anthropic-format-compatible proxies like cloudflare-ai-gateway or gitlab-duo)
1634
1680
  // would leak the user's Anthropic identity to unrelated third-party APIs.
1635
- if (provider === "anthropic") {
1636
- 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 });
1637
1711
  if (typeof accountUuid === "string" && accountUuid.length > 0) {
1638
1712
  userId.account_uuid = accountUuid;
1639
1713
  // Derive device_id from account_uuid so the payload matches the real CC
@@ -1914,9 +1988,24 @@ export type BeforeAgentStartContributor = (event: {
1914
1988
  }) => Promise<BeforeAgentStartInternalMessage | undefined>;
1915
1989
 
1916
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
+ }
1917
2006
 
1918
2007
  export class WorkerIntegrationRequestScheduler {
1919
- #inFlight: Promise<void> | undefined = undefined;
2008
+ #inFlight: Promise<WorkerIntegrationOutcome> | undefined = undefined;
1920
2009
  #pending = false;
1921
2010
 
1922
2011
  constructor(
@@ -1933,9 +2022,15 @@ export class WorkerIntegrationRequestScheduler {
1933
2022
  }
1934
2023
 
1935
2024
  async flush(): Promise<void> {
2025
+ await this.flushWithOutcome();
2026
+ }
2027
+
2028
+ async flushWithOutcome(): Promise<WorkerIntegrationOutcome> {
2029
+ let outcome: WorkerIntegrationOutcome = { status: "completed" };
1936
2030
  while (this.#inFlight) {
1937
- await this.#inFlight;
2031
+ outcome = await this.#inFlight;
1938
2032
  }
2033
+ return outcome;
1939
2034
  }
1940
2035
 
1941
2036
  #start(): void {
@@ -1945,16 +2040,22 @@ export class WorkerIntegrationRequestScheduler {
1945
2040
  try {
1946
2041
  request = this.request(controller.signal);
1947
2042
  } catch {
1948
- request = Promise.resolve();
2043
+ request = Promise.reject(new Error("Worker integration request failed before dispatch."));
1949
2044
  }
1950
2045
  let timeout: ReturnType<typeof setTimeout> | undefined;
1951
- const deadline = new Promise<void>(resolve => {
2046
+ const deadline = new Promise<WorkerIntegrationOutcome>(resolve => {
1952
2047
  timeout = setTimeout(() => {
1953
2048
  controller.abort(new Error("Worker integration request timed out"));
1954
- resolve();
2049
+ resolve({ status: "timed_out" });
1955
2050
  }, this.timeoutMs);
1956
2051
  });
1957
- 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(() => {
1958
2059
  if (timeout) clearTimeout(timeout);
1959
2060
  controller.abort();
1960
2061
  this.#inFlight = undefined;
@@ -2475,6 +2576,22 @@ export class AgentSession {
2475
2576
  this.#activeAttemptScope = undefined;
2476
2577
  this.#activeLogicalRunId = undefined;
2477
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
+ }
2478
2595
  /** Messages queued to be included with the next user prompt as context ("asides"). */
2479
2596
  #pendingNextTurnMessages: Array<{ message: CustomMessage; origin: "turn" | "external" }> = [];
2480
2597
  #scheduledHiddenNextTurnGeneration: number | undefined = undefined;
@@ -2608,7 +2725,88 @@ export class AgentSession {
2608
2725
  onPersisted?: () => void;
2609
2726
  appendedToAgent: boolean;
2610
2727
  }> = [];
2611
- #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
+ });
2612
2810
 
2613
2811
  // Python execution state
2614
2812
  #evalAbortControllers = new Set<AbortController>();
@@ -2658,6 +2856,7 @@ export class AgentSession {
2658
2856
  #extensionRunner: ExtensionRunner | undefined = undefined;
2659
2857
  /** SDK follow-up ownership by queued message and the attempt that dequeues it. */
2660
2858
  #sdkRunTokensByQueuedMessage = new WeakMap<AgentMessage, string>();
2859
+ #skipPostPromptRecoveryWaitByAttemptScope = new WeakSet<AttemptScope>();
2661
2860
  #sdkRunTokensByAttemptScope = new WeakMap<AttemptScope, string>();
2662
2861
  #activeSdkRunToken: string | undefined;
2663
2862
  #activeAttemptScope: AttemptScope | undefined;
@@ -3006,6 +3205,7 @@ export class AgentSession {
3006
3205
  * orchestrator does not hold it), so there is no self-deadlock.
3007
3206
  */
3008
3207
  #sessionTransitionKind: string | undefined;
3208
+ #coordinatorPersistGeneration = 0;
3009
3209
 
3010
3210
  #beginSessionTransition(kind: string): void {
3011
3211
  if (this.#sessionTransitionKind !== undefined) {
@@ -3015,6 +3215,7 @@ export class AgentSession {
3015
3215
  );
3016
3216
  }
3017
3217
  this.#sessionTransitionKind = kind;
3218
+ this.#coordinatorPersistGeneration += 1;
3018
3219
  }
3019
3220
 
3020
3221
  #endSessionTransition(): void {
@@ -3614,7 +3815,7 @@ export class AgentSession {
3614
3815
  // Deferrals behind still-pending selection fences are excluded here: they
3615
3816
  // cannot settle before their fence resolves, and the fence owner may be
3616
3817
  // waiting on this very settlement (#4519).
3617
- if (!predecessorPromptStillInFlight) {
3818
+ if (promptWait !== "publication" && !predecessorPromptStillInFlight) {
3618
3819
  await this.#waitForSessionSettlement(this.#turnSettlementDeferralFloor());
3619
3820
  }
3620
3821
  if (flushError) throw flushError;
@@ -3656,28 +3857,50 @@ export class AgentSession {
3656
3857
  this.#pendingAgentEndEmit = undefined;
3657
3858
  const lease = this.#deferredAgentEndLeases.get(pending);
3658
3859
  if (lease) this.#deferredAgentEndLeases.delete(pending);
3860
+ this.#startAgentEndPublication(pending, lease);
3861
+ }
3862
+ #startAgentEndPublication(pending: AgentSessionEvent, lease?: RunResourceProducerLease): void {
3659
3863
  this.#agentEndPublicationInFlight++;
3660
- 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);
3661
3869
  void this.#agentEndPublicationPromise;
3662
3870
  }
3663
3871
 
3664
3872
  async #publishDeferredAgentEnd(
3665
3873
  pending: AgentSessionEvent,
3666
3874
  lease: RunResourceProducerLease | undefined,
3875
+ workerIntegrationSettled: boolean,
3667
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;
3668
3888
  let extensionDelivery: Promise<void> | undefined;
3669
3889
  const releaseLease = () => {
3670
3890
  if (!lease) return;
3891
+ this.#deferredAgentEndLeases.delete(pending);
3671
3892
  if (this.#postPromptLeases.get(lease.resourceRunId) === lease) {
3672
3893
  this.#postPromptLeases.delete(lease.resourceRunId);
3673
3894
  }
3674
3895
  lease.closeDiscovery();
3675
3896
  };
3676
3897
  const publish = async () => {
3677
- // Worker integration is first-party lifecycle persistence, not an extension
3678
- // hook. Settle it before publishing the terminal boundary while user extension
3679
- // delivery and coordinator sidecar persistence remain asynchronous.
3680
- await this.#flushWorkerIntegrationForAgentEnd();
3898
+ let workerIntegration: Promise<WorkerIntegrationOutcome> | undefined;
3899
+ let workerIntegrationOutcome: WorkerIntegrationOutcome | undefined;
3900
+ if (!workerIntegrationSettled) {
3901
+ workerIntegration = this.#flushWorkerIntegrationForAgentEnd();
3902
+ workerIntegrationOutcome = await workerIntegration;
3903
+ }
3681
3904
  // Reserve persistence before notifying synchronous subscribers: a subscriber
3682
3905
  // may start a successor prompt from agent_end, whose running state must
3683
3906
  // serialize after this terminal boundary rather than be overwritten by it.
@@ -3686,16 +3909,72 @@ export class AgentSession {
3686
3909
  // behind this terminal transition, but do not make the interactive terminal
3687
3910
  // wait for the filesystem-backed lock/write to settle. On slow or lock-hostile
3688
3911
  // mounts such as WSL drvfs, waiting here leaves the foreground activity loader
3689
- // and busy input state visible after the model has already finished.
3690
- const terminalPersistence = this.#queueCoordinatorRuntimeStatePersist(pending);
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);
3691
3916
  this.#emit(pending);
3692
- 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
+ );
3693
3955
  extensionDelivery = this.#queueExtensionEvent(
3694
3956
  pending,
3695
3957
  undefined,
3696
3958
  true,
3697
3959
  (pending as AgentSessionEvent & { scope?: AttemptScopeRef }).scope,
3698
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
+ }
3699
3978
  };
3700
3979
  try {
3701
3980
  if (lease) await this.#runResourceLeaseContext.run(lease, publish);
@@ -3891,9 +4170,19 @@ export class AgentSession {
3891
4170
  this.#resumeFromOwnedCompletion();
3892
4171
  try {
3893
4172
  if (survivors.length === 1) {
3894
- 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
+ });
3895
4179
  } else {
3896
- 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
+ });
3897
4186
  }
3898
4187
  } finally {
3899
4188
  // The owned completions were delivered OR the prompt attempt
@@ -3911,11 +4200,32 @@ export class AgentSession {
3911
4200
  async () => {
3912
4201
  await run();
3913
4202
  },
3914
- { 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 },
3915
4206
  );
3916
4207
  },
3917
4208
  });
3918
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
+ });
3919
4229
  this.agent.setMaintainContext((context, lifecycle) =>
3920
4230
  this.#trackMidRunMaintenance(
3921
4231
  this.awaitPendingContextTransformations().then(() => this.#runMidRunMaintenance(context, lifecycle)),
@@ -4054,6 +4364,18 @@ export class AgentSession {
4054
4364
  if (messages.some(message => ownedCompletionResumeAction(message) === "fresh")) {
4055
4365
  this.#resumeFromOwnedCompletion();
4056
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
+ }
4057
4379
  // Monitor task-notifications now carry the owned-completion envelope
4058
4380
  // (see tools/monitor.ts), so the general drop + fresh-admission paths
4059
4381
  // above cover them exactly like async results — no monitor-specific
@@ -4089,6 +4411,18 @@ export class AgentSession {
4089
4411
  // submitter to the in-flight run instead of parking the correlation for
4090
4412
  // an unrelated later agent_start (#4668).
4091
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
+ }
4092
4426
  this.#fireQueuedPromotionHooks(messages, promotion);
4093
4427
  };
4094
4428
  this.agent.providerSessionState = this.#providerSessionState;
@@ -5001,6 +5335,10 @@ export class AgentSession {
5001
5335
  * seen.
5002
5336
  */
5003
5337
  #coordinatorToolObservations = new WeakMap<object, CoordinatorToolObservation>();
5338
+ #agentEventAdmission = new WeakMap<
5339
+ object,
5340
+ { scope?: AttemptScope; sdkRunToken?: string; persistGeneration: number }
5341
+ >();
5004
5342
 
5005
5343
  /**
5006
5344
  * Capture what is true at the SYNCHRONOUS agent-event boundary, before any async work.
@@ -5056,6 +5394,11 @@ export class AgentSession {
5056
5394
  }
5057
5395
 
5058
5396
  #trackAgentEvent = (event: AgentEvent): Promise<void> => {
5397
+ this.#agentEventAdmission.set(event, {
5398
+ scope: this.#activeAttemptScope,
5399
+ sdkRunToken: this.#activeSdkRunToken,
5400
+ persistGeneration: this.#coordinatorPersistGeneration,
5401
+ });
5059
5402
  // First statement of the listener: the observation must precede every claim,
5060
5403
  // reservation, and async hop this handler performs.
5061
5404
  this.#observeCoordinatorToolEvent(event);
@@ -5125,10 +5468,10 @@ export class AgentSession {
5125
5468
  this.#postPromptLeases.set(eventLease.resourceRunId, eventLease);
5126
5469
  if (eventLease) {
5127
5470
  await this.#runResourceLeaseContext.run(eventLease, () =>
5128
- this.#handleAgentEvent(event, activePromptHandle, canonicalAdmission),
5471
+ this.#handleAgentEvent(event, activePromptHandle, canonicalAdmission, eventLease),
5129
5472
  );
5130
5473
  } else {
5131
- await this.#handleAgentEvent(event, activePromptHandle, canonicalAdmission);
5474
+ await this.#handleAgentEvent(event, activePromptHandle, canonicalAdmission, undefined);
5132
5475
  }
5133
5476
  } catch (error) {
5134
5477
  logger.warn("Agent event handler failed", { event: event.type, error: String(error) });
@@ -5136,11 +5479,19 @@ export class AgentSession {
5136
5479
  canonicalAdmission?.release();
5137
5480
  if (eventLease) {
5138
5481
  const pendingAgentEnd =
5139
- event.type === "agent_end" && !maintenanceCheckpoint && this.#pendingAgentEndEmit === event
5482
+ event.type === "agent_end" &&
5483
+ !maintenanceCheckpoint &&
5484
+ (this.#pendingAgentEndEmit === event || this.#deferredAgentEndLeases.has(event))
5140
5485
  ? this.#pendingAgentEndEmit
5141
5486
  : undefined;
5142
5487
  if (pendingAgentEnd) {
5143
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.
5144
5495
  } else {
5145
5496
  if (event.type === "agent_end" && this.#postPromptLeases.get(eventLease.resourceRunId) === eventLease)
5146
5497
  this.#postPromptLeases.delete(eventLease.resourceRunId);
@@ -5163,21 +5514,61 @@ export class AgentSession {
5163
5514
  /** Serializes sidecar writes in publication order, independent of write latency. */
5164
5515
  #coordinatorPersistQueue: Promise<void> = Promise.resolve();
5165
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
+
5166
5547
  /**
5167
5548
  * Reserve this event's place in the sidecar write order.
5168
5549
  *
5169
5550
  * Called immediately BEFORE local delivery so a synchronous subscriber that re-enters
5170
5551
  * the session cannot get its own, later event persisted first. The returned promise is
5171
- * only awaited where durability must precede delivery (terminal `agent_end`).
5552
+ * awaited only by callers that explicitly require a durable handoff; interactive
5553
+ * terminal delivery remains independent of this secondary sink.
5172
5554
  *
5173
5555
  * A pairing-only tool event takes no place in that order at all — neither its start nor
5174
5556
  * its end. It describes a call that was never dispatched, and this file is read as the
5175
5557
  * answer to "what is this session doing right now".
5176
5558
  */
5177
- #queueCoordinatorRuntimeStatePersist(event: AgentSessionEvent): Promise<void> {
5559
+ #queueCoordinatorRuntimeStatePersist(event: AgentSessionEvent, propagateFailure = false): Promise<void> {
5178
5560
  if (isNonDispatchedToolEvent(event)) return Promise.resolve();
5179
5561
  const observation = this.#coordinatorToolObservations.get(event);
5180
- 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();
5181
5572
  const queued = this.#coordinatorPersistQueue.then(run, run);
5182
5573
  this.#coordinatorPersistQueue = queued.catch(() => {});
5183
5574
  return queued;
@@ -5185,24 +5576,19 @@ export class AgentSession {
5185
5576
 
5186
5577
  async #persistRuntimeStateInBackground(
5187
5578
  event: AgentSessionEvent,
5579
+ context: { sessionId: string; cwd: string; sessionFile: string | undefined },
5188
5580
  observation: CoordinatorToolObservation | undefined,
5581
+ propagateFailure: boolean,
5189
5582
  ): Promise<void> {
5190
5583
  try {
5191
- await persistCoordinatorRuntimeStateFromEvent(
5192
- event,
5193
- {
5194
- sessionId: this.sessionId,
5195
- cwd: this.sessionManager.getCwd(),
5196
- sessionFile: this.sessionManager.getSessionFile(),
5197
- },
5198
- observation,
5199
- );
5200
- } catch {
5584
+ await persistCoordinatorRuntimeStateFromEvent(event, context, observation);
5585
+ } catch (error) {
5201
5586
  logger.warn("Failed to persist coordinator runtime state", { event: event.type });
5587
+ if (propagateFailure) throw error;
5202
5588
  }
5203
5589
  }
5204
5590
 
5205
- async #emitSessionEvent(event: AgentSessionEvent): Promise<void> {
5591
+ async #emitSessionEvent(event: AgentSessionEvent, eventLease?: RunResourceProducerLease): Promise<void> {
5206
5592
  const attemptScope = (event as AgentSessionEvent & { scope?: AttemptScope }).scope;
5207
5593
  if (event.type === "turn_start") {
5208
5594
  this.#extensionTurnGeneration++;
@@ -5236,6 +5622,18 @@ export class AgentSession {
5236
5622
  // have unwound. Subscribers treat this event as the ready signal; flushing it
5237
5623
  // from abort while either barrier is active permits a successor to race the
5238
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
+ }
5239
5637
  if (event.type === "agent_end" && (this.#livePromptsInFlight() > 0 || this.#agentEventHandlersInFlight > 0)) {
5240
5638
  this.#pendingAgentEndEmit = event;
5241
5639
  return;
@@ -5244,8 +5642,12 @@ export class AgentSession {
5244
5642
  if (event.type === "agent_end") {
5245
5643
  // Start the durable terminal write before synchronous subscribers can
5246
5644
  // re-enter prompt(), so a successor's running transition serializes after it.
5247
- await this.#queueCoordinatorRuntimeStatePersist(event);
5645
+ // Do not let a lock-hostile sidecar suppress the terminal event itself.
5646
+ const terminalPersistence = this.#queueCoordinatorRuntimeStatePersist(event);
5248
5647
  this.#emit(event);
5648
+ void terminalPersistence.catch(error => {
5649
+ logger.warn("Failed to persist terminal coordinator runtime state", { error: String(error) });
5650
+ });
5249
5651
  await this.#emitExtensionEvent(event);
5250
5652
  return;
5251
5653
  }
@@ -5341,6 +5743,7 @@ export class AgentSession {
5341
5743
  event: AgentEvent,
5342
5744
  activePromptHandle?: string,
5343
5745
  canonicalAdmission?: CanonicalMessageAdmission,
5746
+ eventLease?: RunResourceProducerLease,
5344
5747
  ): Promise<void> => {
5345
5748
  const attemptScope = (event as AgentEvent & { scope?: AttemptScope }).scope;
5346
5749
 
@@ -5680,7 +6083,7 @@ export class AgentSession {
5680
6083
  });
5681
6084
  }
5682
6085
 
5683
- await this.#emitSessionEvent(displayEvent);
6086
+ await this.#emitSessionEvent(displayEvent, eventLease);
5684
6087
  if (event.type === "message_end" && event.message.role === "assistant") {
5685
6088
  this.#assistantAttemptScopes.set(event.message, {
5686
6089
  scope: attemptScope,
@@ -6030,6 +6433,7 @@ export class AgentSession {
6030
6433
  skipCompactionCheck: true,
6031
6434
  resourceRunId: activePromptHandle,
6032
6435
  maintenanceContinuation: true,
6436
+ sdkRunToken: attemptScope ? this.#sdkRunTokensByAttemptScope.get(attemptScope) : undefined,
6033
6437
  });
6034
6438
  }
6035
6439
  return;
@@ -6317,8 +6721,11 @@ export class AgentSession {
6317
6721
  predecessorAgentEndHold?: symbol;
6318
6722
  /** Internal predecessor terminal event sequestered while waiting behind selection. */
6319
6723
  deferredPredecessorAgentEnd?: AgentSessionEvent;
6724
+ /** Internal causal SDK owner captured when this continuation was scheduled. */
6725
+ sdkRunToken?: string;
6320
6726
  }): Promise<void> {
6321
6727
  const continuationAdmission = this.#captureScheduledContinuationAdmission();
6728
+ const scheduledSdkRunToken = options?.sdkRunToken;
6322
6729
  const selectionFenceGeneration =
6323
6730
  options?.selectionFenceGeneration ??
6324
6731
  this.#selectionFenceGenerationContext.getStore() ??
@@ -6340,6 +6747,7 @@ export class AgentSession {
6340
6747
  generation: deferredPromptGeneration,
6341
6748
  selectionFenceGeneration,
6342
6749
  deferredPredecessorAgentEnd,
6750
+ sdkRunToken: scheduledSdkRunToken,
6343
6751
  });
6344
6752
  } finally {
6345
6753
  // The recursive call synchronously re-reserved its settlement
@@ -6475,18 +6883,18 @@ export class AgentSession {
6475
6883
  // agent_start (review P1); their queued messages are in-run
6476
6884
  // consumptions, not own-run promotions.
6477
6885
  const startsOwn = options?.maintenanceContinuation !== true;
6478
- const consumedSdkRunToken = startsOwn
6479
- ? acceptance.consumedQueuedMessages
6480
- .map(message => this.#sdkRunTokensByQueuedMessage.get(message))
6481
- .find((token): token is string => token !== undefined)
6482
- : undefined;
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;
6483
6893
  this.#fireQueuedPromotionHooks(acceptance.consumedQueuedMessages, {
6484
6894
  startsOwnRun: startsOwn,
6485
6895
  });
6486
- if (consumedSdkRunToken !== undefined)
6487
- this.#sdkRunTokensByAttemptScope.set(handle.scope, consumedSdkRunToken);
6488
- if (startsOwn) this.#activeSdkRunToken = consumedSdkRunToken;
6489
- this.#acceptRunHandle(handle);
6896
+ if (startsOwn) this.#activeSdkRunToken = sdkRunToken;
6897
+ this.#acceptSdkAttemptRun(handle, sdkRunToken);
6490
6898
  options?.onRunAccepted?.(handle);
6491
6899
  // Keep the queued token available through the acceptance callback;
6492
6900
  // SDK follow-up owners bind it to the new attempt scope there.
@@ -6649,8 +7057,12 @@ export class AgentSession {
6649
7057
  resourceRunId?: string,
6650
7058
  deferredSelectionFenceGeneration?: number,
6651
7059
  deferredPredecessorAgentEnd?: AgentSessionEvent,
7060
+ sdkRunToken?: string,
6652
7061
  ): void {
6653
7062
  const scheduledGeneration = generation;
7063
+ const scheduledSdkRunToken =
7064
+ sdkRunToken ??
7065
+ (this.#activeAttemptScope ? this.#sdkRunTokensByAttemptScope.get(this.#activeAttemptScope) : undefined);
6654
7066
  const continuationAuthorized = async (
6655
7067
  signal: AbortSignal,
6656
7068
  hasPendingNextTurnMessages = false,
@@ -6703,6 +7115,7 @@ export class AgentSession {
6703
7115
  resourceRunId,
6704
7116
  selectionFenceGeneration,
6705
7117
  predecessorAgentEnd,
7118
+ scheduledSdkRunToken,
6706
7119
  );
6707
7120
  } finally {
6708
7121
  this.#endSelectionFenceDeferralTracking(selectionFenceGeneration);
@@ -6750,6 +7163,7 @@ export class AgentSession {
6750
7163
  promptText,
6751
7164
  {
6752
7165
  skipPostPromptRecoveryWait: true,
7166
+ sdkRunToken: scheduledSdkRunToken,
6753
7167
  skipCompactionCheck: true,
6754
7168
  predecessorAgentEndHold,
6755
7169
  onFinalPreflight: ({ hasPendingNextTurnMessages }) =>
@@ -7527,16 +7941,16 @@ export class AgentSession {
7527
7941
  this.#workerIntegrationScheduler?.enqueue();
7528
7942
  }
7529
7943
 
7530
- async #flushWorkerIntegrationAttempt(): Promise<void> {
7531
- await this.#workerIntegrationScheduler?.flush();
7944
+ async #flushWorkerIntegrationAttempt(): Promise<WorkerIntegrationOutcome> {
7945
+ return (await this.#workerIntegrationScheduler?.flushWithOutcome()) ?? { status: "completed" };
7532
7946
  }
7533
7947
 
7534
- async #flushWorkerIntegrationForAgentEnd(): Promise<void> {
7948
+ async #flushWorkerIntegrationForAgentEnd(): Promise<WorkerIntegrationOutcome> {
7535
7949
  if (!this.#workerIntegrationRequestedForTurn) {
7536
7950
  this.#requestWorkerIntegrationAttempt();
7537
7951
  }
7538
7952
  try {
7539
- await this.#flushWorkerIntegrationAttempt();
7953
+ return await this.#flushWorkerIntegrationAttempt();
7540
7954
  } finally {
7541
7955
  this.#workerIntegrationRequestedForTurn = false;
7542
7956
  }
@@ -7549,6 +7963,9 @@ export class AgentSession {
7549
7963
  workerIntegrationSettled = false,
7550
7964
  scope?: AttemptScopeRef,
7551
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;
7552
7969
  if (event.type === "agent_end" && !workerIntegrationSettled) {
7553
7970
  await this.#flushWorkerIntegrationForAgentEnd();
7554
7971
  }
@@ -7559,20 +7976,36 @@ export class AgentSession {
7559
7976
  ? eventToken
7560
7977
  : deliveryScope
7561
7978
  ? this.#sdkRunTokensByAttemptScope.get(deliveryScope as AttemptScope)
7562
- : this.#activeSdkRunToken;
7979
+ : undefined;
7563
7980
  const isTerminalAgentEnd =
7564
7981
  event.type === "agent_end" && !(event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted");
7565
7982
  const finishAttempt = () => {
7566
7983
  if (!isTerminalAgentEnd) return;
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();
7567
7989
  const isActiveAttempt =
7568
- deliveryScope === undefined ||
7569
- this.#activeAttemptScope === undefined ||
7570
- deliveryScope === this.#activeAttemptScope;
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
+ }
7571
8000
  if (deliveryScope) {
7572
8001
  this.#attemptRecordStore.retire(deliveryScope as AttemptScope);
7573
- this.#sdkRunTokensByAttemptScope.delete(deliveryScope as AttemptScope);
8002
+ if (
8003
+ activeSdkRunTokenAtEvent !== undefined &&
8004
+ this.#sdkRunTokensByAttemptScope.get(deliveryScope as AttemptScope) === activeSdkRunTokenAtEvent
8005
+ )
8006
+ this.#sdkRunTokensByAttemptScope.delete(deliveryScope as AttemptScope);
7574
8007
  }
7575
- if (isActiveAttempt && sdkRunToken === this.#activeSdkRunToken) this.#activeSdkRunToken = undefined;
8008
+ if (isActiveAttempt) this.#activeSdkRunToken = undefined;
7576
8009
  if (isActiveAttempt) {
7577
8010
  this.#activeAttemptScope = undefined;
7578
8011
  this.#activeLogicalRunId = undefined;
@@ -7840,8 +8273,18 @@ export class AgentSession {
7840
8273
  const sid = this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
7841
8274
  this.agent.sessionId = sid;
7842
8275
  this.agent.providerSessionId = this.#providerCacheSessionId ?? sid;
7843
- this.agent.setMetadataResolver((provider: string) =>
7844
- 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
+ ),
7845
8288
  );
7846
8289
  }
7847
8290
 
@@ -8454,13 +8897,39 @@ export class AgentSession {
8454
8897
  * handler, so Ctrl+B-style folding fails closed instead of aborting or
8455
8898
  * shell-suspending arbitrary work.
8456
8899
  */
8457
- registerForegroundBashBackgroundRequestHandler(handler: () => void): () => void {
8458
- this.#foregroundBashBackgroundRequestHandler = handler;
8459
- return () => {
8460
- if (this.#foregroundBashBackgroundRequestHandler === handler) {
8461
- this.#foregroundBashBackgroundRequestHandler = undefined;
8462
- }
8463
- };
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;
8464
8933
  }
8465
8934
 
8466
8935
  /**
@@ -8469,7 +8938,7 @@ export class AgentSession {
8469
8938
  * no fold target exists.
8470
8939
  */
8471
8940
  hasForegroundBashBackgroundRequestHandler(): boolean {
8472
- return this.#foregroundBashBackgroundRequestHandler !== undefined;
8941
+ return this.#foldCoordinator.hasFoldableParticipant();
8473
8942
  }
8474
8943
 
8475
8944
  /** Set the SDK permission policy used by guarded ACP tool execution. */
@@ -8505,11 +8974,17 @@ export class AgentSession {
8505
8974
  * Ask the active managed foreground bash call to return as a background job.
8506
8975
  * Returns false when no supported foreground tool is currently backgroundable.
8507
8976
  */
8508
- requestForegroundBashBackground(): boolean {
8509
- const handler = this.#foregroundBashBackgroundRequestHandler;
8510
- if (!handler) return false;
8511
- handler();
8512
- 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
+ }
8513
8988
  }
8514
8989
 
8515
8990
  /**
@@ -9450,6 +9925,7 @@ export class AgentSession {
9450
9925
  if (this.#sessionAdmissionClosed || this.#isDisposed) throw this.#sessionAdmissionBusyError();
9451
9926
  const inFlightPrompt = this.#beginInFlight();
9452
9927
  let hindsightRecall: string | undefined;
9928
+ let continuationSdkRunToken: string | undefined;
9453
9929
  try {
9454
9930
  const volatileProjectContextMessage = await this.#buildVolatileProjectContextMessage();
9455
9931
  this.agent.appendMessage(volatileProjectContextMessage);
@@ -9485,14 +9961,18 @@ export class AgentSession {
9485
9961
  await this.agent.continue({
9486
9962
  ...this.#managedFallbackPromptOptions(),
9487
9963
  onRunAccepted: (handle: AttemptRunHandle) => {
9488
- this.#acceptRunHandle(handle);
9964
+ continuationSdkRunToken = this.#activeSdkRunToken;
9965
+ this.#acceptSdkAttemptRun(handle, continuationSdkRunToken);
9489
9966
  if (hindsightRecall) hindsightState?.markRecallSnippetInjected(hindsightRecall);
9490
9967
  },
9491
9968
  });
9492
- await this.#waitForPostPromptRecovery();
9969
+ if (continuationSdkRunToken === undefined) await this.#waitForPostPromptRecovery();
9493
9970
  } finally {
9494
9971
  this.#removeEphemeralCustomMessages();
9495
- await this.#settleEndedInFlight(inFlightPrompt);
9972
+ await this.#settleEndedInFlight(
9973
+ inFlightPrompt,
9974
+ continuationSdkRunToken === undefined ? "full" : "publication",
9975
+ );
9496
9976
  }
9497
9977
  }
9498
9978
 
@@ -9620,8 +10100,9 @@ export class AgentSession {
9620
10100
  async setCredentialPin(provider: string, selector: AuthCredentialSelector): Promise<void> {
9621
10101
  const scopeId = this.credentialSessionId;
9622
10102
  const authStorage = this.#modelRegistry.authStorage;
9623
- const target = authStorage.resolveOAuthPinTarget(provider, selector);
9624
- 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);
9625
10106
  if (target.canonicalSelector.kind === "id" && !this.#credentialStoreIdentity) return;
9626
10107
  this.sessionManager.appendCustomEntry("auth-credential-pin", {
9627
10108
  v: 1,
@@ -9701,9 +10182,13 @@ export class AgentSession {
9701
10182
  args = "",
9702
10183
  options?: Pick<
9703
10184
  PromptOptions,
9704
- "onPreflightAccepted" | "onPreflightAcceptCommit" | "onSkillPrepared" | "preflightSignal" | "sdkRunToken"
10185
+ "onPreflightAccepted" | "onPreflightAcceptCommit" | "onSkillPrepared" | "preflightSignal" | "sdkRunCapability"
9705
10186
  >,
9706
10187
  ): Promise<{ name: string; path: string; args?: string; lineCount?: number }> {
10188
+ const internalOptions = {
10189
+ ...options,
10190
+ sdkRunToken: readSdkRunCapability(options?.sdkRunCapability),
10191
+ };
9707
10192
  if (options?.preflightSignal?.aborted) throw promptPreflightCancelledError();
9708
10193
  const skillName = name.trim();
9709
10194
  if (!skillName) throw Object.assign(new Error("skill.invoke requires a skill name."), { code: "invalid_input" });
@@ -9749,7 +10234,7 @@ export class AgentSession {
9749
10234
  lineCount: built.details.lineCount,
9750
10235
  cleanedArgs: activation.cleanedArgs || undefined,
9751
10236
  });
9752
- await this.promptCustomMessage(skillPromptMessage, options);
10237
+ await this.promptCustomMessage(skillPromptMessage, internalOptions as InternalCustomMessageOptions);
9753
10238
  return {
9754
10239
  name: skill.name,
9755
10240
  path: skill.filePath,
@@ -10439,6 +10924,14 @@ export class AgentSession {
10439
10924
  * @throws Error if no model selected or no API key available (when not streaming)
10440
10925
  */
10441
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;
10442
10935
  this.#assertRecoveryHydrationPromoted();
10443
10936
  const owner = this.#sessionAdmissionContext.getStore();
10444
10937
  if (owner && !owner.released) throw this.#sessionAdmissionBusyError();
@@ -10453,14 +10946,19 @@ export class AgentSession {
10453
10946
  await this.invokeSkill(
10454
10947
  invocation.skill.name,
10455
10948
  invocation.args,
10456
- options?.onPreflightAccepted || options?.onPreflightAcceptCommit || options?.preflightSignal
10949
+ options?.onPreflightAccepted ||
10950
+ options?.onPreflightAcceptCommit ||
10951
+ options?.preflightSignal ||
10952
+ internalOptions?.sdkRunToken
10457
10953
  ? {
10458
- ...(options.onPreflightAccepted ? { onPreflightAccepted: options.onPreflightAccepted } : {}),
10459
- ...(options.onPreflightAcceptCommit
10954
+ ...(options?.onPreflightAccepted
10955
+ ? { onPreflightAccepted: options.onPreflightAccepted }
10956
+ : {}),
10957
+ ...(options?.onPreflightAcceptCommit
10460
10958
  ? { onPreflightAcceptCommit: options.onPreflightAcceptCommit }
10461
10959
  : {}),
10462
- ...(options.preflightSignal ? { preflightSignal: options.preflightSignal } : {}),
10463
- ...(options.sdkRunToken ? { sdkRunToken: options.sdkRunToken } : {}),
10960
+ ...(options?.preflightSignal ? { preflightSignal: options.preflightSignal } : {}),
10961
+ ...(options?.sdkRunCapability ? { sdkRunCapability: options.sdkRunCapability } : {}),
10464
10962
  }
10465
10963
  : undefined,
10466
10964
  );
@@ -10494,6 +10992,10 @@ export class AgentSession {
10494
10992
 
10495
10993
  // Expand file-based prompt templates if requested
10496
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" });
10497
10999
  assertImagePlaceholdersHavePayload(expandedText, options?.images);
10498
11000
  const workflowIntentDiff = options?.synthetic ? null : buildWorkflowIntentDiff(expandedText);
10499
11001
  const claimsGenuineUserIntent = !options?.synthetic && options?.attribution !== "agent";
@@ -10714,9 +11216,13 @@ export class AgentSession {
10714
11216
  | "onPreflightAccepted"
10715
11217
  | "onPreflightAcceptCommit"
10716
11218
  | "preflightSignal"
10717
- | "sdkRunToken"
11219
+ | "sdkRunCapability"
10718
11220
  >,
10719
11221
  ): Promise<void> {
11222
+ const sdkRunToken = readSdkRunCapability(options?.sdkRunCapability);
11223
+ const internalOptions: InternalCustomMessageOptions | undefined = options
11224
+ ? { ...options, ...(sdkRunToken ? { sdkRunToken } : {}) }
11225
+ : undefined;
10720
11226
  if (options?.preflightSignal?.aborted) throw promptPreflightCancelledError();
10721
11227
  const textContent =
10722
11228
  typeof message.content === "string"
@@ -10785,7 +11291,7 @@ export class AgentSession {
10785
11291
  };
10786
11292
  try {
10787
11293
  await this.#promptWithMessage(customMessage, textContent, {
10788
- ...options,
11294
+ ...internalOptions,
10789
11295
  onPreflightAccepted: undefined,
10790
11296
  onPreflightAcceptCommit: commitAcceptance,
10791
11297
  admissionLease: admission,
@@ -10816,8 +11322,8 @@ export class AgentSession {
10816
11322
  | "onPreflightAccepted"
10817
11323
  | "onPreflightAcceptCommit"
10818
11324
  | "preflightSignal"
10819
- | "sdkRunToken"
10820
11325
  > & {
11326
+ sdkRunToken?: string;
10821
11327
  prependMessages?: AgentMessage[];
10822
11328
  skipPostPromptRecoveryWait?: boolean;
10823
11329
  predecessorAgentEndHold?: symbol;
@@ -10884,6 +11390,9 @@ export class AgentSession {
10884
11390
  const rosterClaim = this.#claimIrcRosterCandidate();
10885
11391
  let hasPendingNextTurnMessages = false;
10886
11392
  let pendingNextTurnMessageCount = 0;
11393
+ let skipPostPromptRecoveryWait =
11394
+ options?.skipPostPromptRecoveryWait === true || options?.sdkRunToken !== undefined;
11395
+ let promptAttemptScope: AttemptScope | undefined;
10887
11396
  let hindsightRecall: string | undefined;
10888
11397
  try {
10889
11398
  this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
@@ -11152,9 +11661,10 @@ export class AgentSession {
11152
11661
  ...this.#managedFallbackPromptOptions(),
11153
11662
  ...(options?.sdkRunToken ? { sdkRunToken: options.sdkRunToken } : {}),
11154
11663
  onRunAccepted: (handle: AttemptRunHandle) => {
11155
- this.#acceptRunHandle(handle);
11156
- if (options?.sdkRunToken) {
11157
- this.#sdkRunTokensByAttemptScope.set(handle.scope, options.sdkRunToken);
11664
+ promptAttemptScope = handle?.scope;
11665
+ if (handle) this.#acceptSdkAttemptRun(handle, options?.sdkRunToken);
11666
+ else this.#acceptRunHandle(handle);
11667
+ if (handle && options?.sdkRunToken) {
11158
11668
  this.#activeSdkRunToken = options.sdkRunToken;
11159
11669
  }
11160
11670
  options?.onRunAccepted?.(handle);
@@ -11189,6 +11699,20 @@ export class AgentSession {
11189
11699
  options?.onPreflightAccepted?.();
11190
11700
  },
11191
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
+ }
11192
11716
  const terminalAssistant = this.#findLastAssistantMessage();
11193
11717
  if (
11194
11718
  rosterClaim &&
@@ -11198,7 +11722,7 @@ export class AgentSession {
11198
11722
  ) {
11199
11723
  this.#commitIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
11200
11724
  }
11201
- if (!options?.skipPostPromptRecoveryWait) {
11725
+ if (!skipPostPromptRecoveryWait) {
11202
11726
  await this.#waitForPostPromptRecovery();
11203
11727
  }
11204
11728
  } catch (error) {
@@ -11223,7 +11747,7 @@ export class AgentSession {
11223
11747
  this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
11224
11748
  }
11225
11749
  this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
11226
- await this.#settleEndedInFlight(inFlightPrompt, options?.skipPostPromptRecoveryWait ? "publication" : "full");
11750
+ await this.#settleEndedInFlight(inFlightPrompt, skipPostPromptRecoveryWait ? "publication" : "full");
11227
11751
  }
11228
11752
  }
11229
11753
 
@@ -11427,12 +11951,18 @@ export class AgentSession {
11427
11951
  * Queue a steering message to interrupt the agent mid-run.
11428
11952
  */
11429
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" });
11430
11958
  this.#assertRecoveryHydrationPromoted();
11431
11959
  if (text.startsWith("/")) {
11432
11960
  this.#throwIfExtensionCommand(text);
11433
11961
  }
11434
11962
 
11435
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" });
11436
11966
  assertImagePlaceholdersHavePayload(expandedText, images);
11437
11967
  await this.#queueSteer(expandedText, images, { claimsGenuineUserIntent: true });
11438
11968
  }
@@ -11445,12 +11975,18 @@ export class AgentSession {
11445
11975
  images?: ImageContent[],
11446
11976
  options?: Pick<PromptOptions, "followUpQueuePolicy">,
11447
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" });
11448
11982
  this.#assertRecoveryHydrationPromoted();
11449
11983
  if (text.startsWith("/")) {
11450
11984
  this.#throwIfExtensionCommand(text);
11451
11985
  }
11452
11986
 
11453
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" });
11454
11990
  assertImagePlaceholdersHavePayload(expandedText, images);
11455
11991
  await this.#queueFollowUp(expandedText, images, {
11456
11992
  forceOneAtATime: options?.followUpQueuePolicy === "sequential",
@@ -11468,6 +12004,7 @@ export class AgentSession {
11468
12004
  claimsGenuineUserIntent?: boolean;
11469
12005
  onPromoted?: (promotion: { startsOwnRun?: boolean; removed?: boolean }) => void;
11470
12006
  external?: boolean;
12007
+ sdkRunToken?: string;
11471
12008
  },
11472
12009
  ): Promise<void> {
11473
12010
  this.#assertNoHandoffTransition();
@@ -11482,6 +12019,7 @@ export class AgentSession {
11482
12019
  this.#externalSteerAdmissionSeq.set(message, ++this.#steeringAdmissionSeq);
11483
12020
  if (options.onPromoted) this.#steerPromotionHooks.set(message, options.onPromoted);
11484
12021
  }
12022
+ if (options?.sdkRunToken) this.#sdkRunTokensByQueuedMessage.set(message, options.sdkRunToken);
11485
12023
  if (options?.claimsGenuineUserIntent) {
11486
12024
  const epoch = this.#claimDeepInterviewUserIntent();
11487
12025
  this.#deepInterviewGenuineUserMessageEpochs.set(message, epoch);
@@ -12064,9 +12602,11 @@ export class AgentSession {
12064
12602
  /** Internal dispatch disposition used before actual queue consumption. */
12065
12603
  onDispatchDisposition?: (promotion: { startsOwnRun: boolean }) => void;
12066
12604
  preflightSignal?: AbortSignal;
12067
- sdkRunToken?: string;
12605
+ sdkRunCapability?: unknown;
12068
12606
  },
12069
12607
  ): Promise<void> {
12608
+ const sdkRunToken = readSdkRunCapability(options?.sdkRunCapability);
12609
+ const internalOptions = options ? { ...options, ...(sdkRunToken ? { sdkRunToken } : {}) } : undefined;
12070
12610
  this.#assertRecoveryHydrationPromoted();
12071
12611
  const owner = this.#sessionAdmissionContext.getStore();
12072
12612
  if (owner && !owner.released) throw this.#sessionAdmissionBusyError();
@@ -12087,7 +12627,15 @@ export class AgentSession {
12087
12627
  const textParts: string[] = [];
12088
12628
  images = [];
12089
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
+ });
12090
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
+ });
12091
12639
  textParts.push(part.text);
12092
12640
  } else {
12093
12641
  images.push(part);
@@ -12096,6 +12644,10 @@ export class AgentSession {
12096
12644
  text = textParts.join("\n");
12097
12645
  if (images.length === 0) images = undefined;
12098
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" });
12099
12651
 
12100
12652
  let admissionSignal = options?.preflightSignal
12101
12653
  ? AbortSignal.any([this.#promptPreflightAbortController.signal, options.preflightSignal])
@@ -12189,7 +12741,7 @@ export class AgentSession {
12189
12741
  claimsGenuineUserIntent: true,
12190
12742
  forceOneAtATime: Boolean(options?.preflightSignal || options?.queuedAtDispatch),
12191
12743
  onPromoted: options?.onQueuedPromoted,
12192
- sdkRunToken: options?.sdkRunToken,
12744
+ sdkRunToken: internalOptions?.sdkRunToken,
12193
12745
  });
12194
12746
  const cancelQueuedFollowUp = () => queuedFollowUp.cancel();
12195
12747
  options?.preflightSignal?.addEventListener("abort", cancelQueuedFollowUp, { once: true });
@@ -12204,6 +12756,7 @@ export class AgentSession {
12204
12756
  claimsGenuineUserIntent: true,
12205
12757
  onPromoted: options?.onQueuedPromoted,
12206
12758
  external: true,
12759
+ sdkRunToken: internalOptions?.sdkRunToken,
12207
12760
  });
12208
12761
  options?.onPreflightAccepted?.();
12209
12762
  return;
@@ -12223,6 +12776,7 @@ export class AgentSession {
12223
12776
  claimsGenuineUserIntent: true,
12224
12777
  onPromoted: options?.onQueuedPromoted,
12225
12778
  external: true,
12779
+ sdkRunToken: internalOptions?.sdkRunToken,
12226
12780
  });
12227
12781
  // Dispatch-race disposition (#4668 review P1): the SDK snapshot-decided
12228
12782
  // this submission starts its own turn (idle at dispatch), but the
@@ -12248,7 +12802,7 @@ export class AgentSession {
12248
12802
  await this.prompt(text, {
12249
12803
  expandPromptTemplates: false,
12250
12804
  images,
12251
- sdkRunToken: options?.sdkRunToken,
12805
+ sdkRunToken: internalOptions?.sdkRunToken,
12252
12806
  onPreflightAccepted: () => {
12253
12807
  options?.onPreflightAccepted?.();
12254
12808
  fireQueuedPromotion();
@@ -12263,7 +12817,7 @@ export class AgentSession {
12263
12817
  }
12264
12818
  : undefined,
12265
12819
  preflightSignal: options?.preflightSignal,
12266
- });
12820
+ } as InternalPromptOptions);
12267
12821
  } finally {
12268
12822
  releaseFollowUpReservation();
12269
12823
  }
@@ -12772,6 +13326,7 @@ export class AgentSession {
12772
13326
  | "tool_abort"
12773
13327
  | "internal";
12774
13328
  silent?: boolean;
13329
+ sdkRunToken?: string;
12775
13330
  }): void {
12776
13331
  const abortGoalState = this.getGoalModeState();
12777
13332
  this.#suppressNextGoalReminderAfterAbortGoalId =
@@ -13473,6 +14028,9 @@ export class AgentSession {
13473
14028
  attribution: "user" as const,
13474
14029
  timestamp: Date.now(),
13475
14030
  };
14031
+ const selectedSdkRunToken = selectedMessage
14032
+ ? this.#sdkRunTokensByQueuedMessage.get(selectedMessage)
14033
+ : undefined;
13476
14034
  const messageText =
13477
14035
  message.role === "custom"
13478
14036
  ? this.#getCustomMessageTextContent(message)
@@ -13491,6 +14049,7 @@ export class AgentSession {
13491
14049
  await this.#promptWithMessage(message, messageText, {
13492
14050
  admissionLease: admission,
13493
14051
  resetRetryReplaySafety: true,
14052
+ sdkRunToken: selectedSdkRunToken,
13494
14053
  onRunAccepted: () => {
13495
14054
  runAccepted = true;
13496
14055
  if (selected) this.#fireQueuedPromotionHooks([message], { startsOwnRun: true });
@@ -19052,6 +19611,9 @@ export class AgentSession {
19052
19611
  const continuation = this.agent.continue({
19053
19612
  ...this.#managedFallbackPromptOptions(),
19054
19613
  transientRecoveryMessage: this.#escapedNonAsciiRecoveryMessage(),
19614
+ onRunAccepted: (handle: AttemptRunHandle) => {
19615
+ this.#acceptSdkAttemptRun(handle, this.#activeSdkRunToken);
19616
+ },
19055
19617
  });
19056
19618
  if (
19057
19619
  cancellationSignal?.aborted ||
@@ -19090,6 +19652,9 @@ export class AgentSession {
19090
19652
  await this.agent.continue({
19091
19653
  ...this.#managedFallbackPromptOptions(),
19092
19654
  transientRecoveryMessage: this.#escapedNonAsciiRecoveryMessage(),
19655
+ onRunAccepted: (handle: AttemptRunHandle) => {
19656
+ this.#acceptSdkAttemptRun(handle, this.#activeSdkRunToken);
19657
+ },
19093
19658
  });
19094
19659
  },
19095
19660
  };
@@ -19519,11 +20084,13 @@ export class AgentSession {
19519
20084
  if (!isAuthenticated(activeApiKey)) return "unchanged";
19520
20085
  remaining = await authStorage.invalidateCredentialMatching(provider, activeApiKey, {
19521
20086
  sessionId: credentialSessionId,
20087
+ owner: this.#modelRegistry.getAuthStorageOwner(),
19522
20088
  });
19523
20089
  if (!remaining) return "unchanged";
19524
20090
  } else {
19525
20091
  remaining = await authStorage.markUsageLimitReached(provider, credentialSessionId, {
19526
20092
  retryAfterMs: trigger.retryAfterMs,
20093
+ owner: this.#modelRegistry.getAuthStorageOwner(),
19527
20094
  });
19528
20095
  }
19529
20096
 
@@ -20031,7 +20598,12 @@ export class AgentSession {
20031
20598
  this.#resolveRetry();
20032
20599
  return;
20033
20600
  }
20034
- 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
+ });
20035
20607
  return;
20036
20608
  } catch (error) {
20037
20609
  const attempt = this.#retryAttempt;
@@ -21068,6 +21640,9 @@ export class AgentSession {
21068
21640
  model.provider,
21069
21641
  this.#modelRegistry.authStorage,
21070
21642
  this.credentialSessionId,
21643
+ model,
21644
+ // Optional for the same reason as the main metadata resolver above.
21645
+ this.#modelRegistry.getAuthStorageOwner?.(),
21071
21646
  ),
21072
21647
  reasoning: toReasoningEffort(this.thinkingLevel),
21073
21648
  hideThinkingSummary: this.agent.hideThinkingSummary,