@gajae-code/coding-agent 0.18.7 → 0.18.8

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 (279) hide show
  1. package/CHANGELOG.md +279 -1
  2. package/dist/types/async/job-manager.d.ts +1 -1
  3. package/dist/types/capability/mcp.d.ts +2 -0
  4. package/dist/types/capability/skill.d.ts +2 -0
  5. package/dist/types/capability/types.d.ts +2 -0
  6. package/dist/types/cli/public-command-errors.d.ts +3 -1
  7. package/dist/types/cli/setup-cli.d.ts +1 -1
  8. package/dist/types/commands/deep-interview.d.ts +3 -0
  9. package/dist/types/config/autorouting-tier-map.d.ts +348 -9
  10. package/dist/types/config/file-lock.d.ts +1 -0
  11. package/dist/types/config/model-bindings-applier.d.ts +2 -0
  12. package/dist/types/config/model-profile-activation.d.ts +11 -3
  13. package/dist/types/config/model-registry.d.ts +4 -9
  14. package/dist/types/config/skill-settings-defaults.d.ts +2 -0
  15. package/dist/types/cursor-delete-path.d.ts +1 -0
  16. package/dist/types/cursor-mcp-name.d.ts +8 -0
  17. package/dist/types/customization/inventory.d.ts +2 -0
  18. package/dist/types/debug/report-bundle-parent.d.ts +2 -0
  19. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
  21. package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
  22. package/dist/types/discovery/helpers.d.ts +6 -1
  23. package/dist/types/export/html/css-color.d.ts +2 -0
  24. package/dist/types/export/html/template.generated.d.ts +1 -1
  25. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  26. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
  27. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
  28. package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
  29. package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
  30. package/dist/types/extensibility/skill-management.d.ts +3 -1
  31. package/dist/types/extensibility/skills.d.ts +2 -0
  32. package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
  33. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
  34. package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
  35. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
  36. package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
  37. package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
  38. package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
  39. package/dist/types/gjc-runtime/state-writer.d.ts +11 -1
  40. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
  41. package/dist/types/internal-urls/skill-path.d.ts +1 -0
  42. package/dist/types/lsp/workspace-path.d.ts +1 -0
  43. package/dist/types/modes/acp/acp-agent.d.ts +7 -0
  44. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  45. package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
  46. package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
  47. package/dist/types/modes/controllers/input-controller.d.ts +9 -0
  48. package/dist/types/modes/interactive-mode.d.ts +1 -0
  49. package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
  50. package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
  51. package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
  52. package/dist/types/progress/collect-project-progress.d.ts +38 -0
  53. package/dist/types/progress/progress-contract.d.ts +154 -0
  54. package/dist/types/progress/progress-snapshot.d.ts +11 -0
  55. package/dist/types/progress/project-progress.d.ts +141 -0
  56. package/dist/types/progress/render-progress.d.ts +28 -0
  57. package/dist/types/runtime-mcp/oauth-public-url.d.ts +1 -0
  58. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
  59. package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
  60. package/dist/types/runtime-mcp/types.d.ts +6 -0
  61. package/dist/types/sdk/acp/adapter.d.ts +1 -1
  62. package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
  63. package/dist/types/sdk/broker/broker.d.ts +18 -1
  64. package/dist/types/sdk/broker/session-index.d.ts +12 -6
  65. package/dist/types/sdk/bus/index.d.ts +12 -1
  66. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
  67. package/dist/types/sdk/client/client.d.ts +2 -0
  68. package/dist/types/sdk/host/query/handlers.d.ts +3 -0
  69. package/dist/types/sdk/index.d.ts +1 -0
  70. package/dist/types/sdk/router/session-router.d.ts +2 -0
  71. package/dist/types/sdk/session-list.d.ts +1 -1
  72. package/dist/types/sdk/session.d.ts +8 -0
  73. package/dist/types/session/agent-session.d.ts +38 -9
  74. package/dist/types/session/artifacts.d.ts +2 -2
  75. package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
  76. package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
  77. package/dist/types/session/internal/managed-session-storage.d.ts +26 -1
  78. package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
  79. package/dist/types/session/internal/task-artifact-owner-access.d.ts +2 -0
  80. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +9 -1
  81. package/dist/types/session/session-manager.d.ts +31 -5
  82. package/dist/types/session/session-retirement.d.ts +31 -7
  83. package/dist/types/session/session-storage.d.ts +62 -2
  84. package/dist/types/session/task-artifact-owner-codec.d.ts +2 -0
  85. package/dist/types/setup/credential-auto-import.d.ts +1 -1
  86. package/dist/types/setup/credential-import.d.ts +6 -4
  87. package/dist/types/skill-state/active-state.d.ts +38 -0
  88. package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
  89. package/dist/types/task/executor.d.ts +3 -0
  90. package/dist/types/tools/bash-skill-urls.d.ts +1 -1
  91. package/dist/types/tools/index.d.ts +3 -0
  92. package/dist/types/tools/python.d.ts +9 -1
  93. package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
  94. package/dist/types/tools/restricted-cwd.d.ts +1 -0
  95. package/dist/types/tools/todo-write.d.ts +6 -0
  96. package/dist/types/utils/tools-checksum.d.ts +7 -0
  97. package/package.json +7 -7
  98. package/scripts/generate-sdk-operation-inventory.ts +21 -0
  99. package/scripts/verify-managed-empty-stop.ts +28 -0
  100. package/src/async/job-manager.ts +228 -122
  101. package/src/capability/index.ts +17 -0
  102. package/src/capability/mcp.ts +2 -0
  103. package/src/capability/skill.ts +2 -0
  104. package/src/capability/types.ts +2 -0
  105. package/src/cli/auth-broker-cli.ts +1 -1
  106. package/src/cli/customize-doctor.ts +15 -2
  107. package/src/cli/public-command-errors.ts +21 -1
  108. package/src/cli/setup-cli.ts +4 -4
  109. package/src/cli/skills-cli.ts +5 -0
  110. package/src/commands/deep-interview.ts +47 -8
  111. package/src/commands/sdk.ts +28 -8
  112. package/src/config/autorouting-tier-map.ts +117 -7
  113. package/src/config/file-lock.ts +307 -68
  114. package/src/config/mcp-schema.json +4 -0
  115. package/src/config/model-bindings-applier.ts +20 -0
  116. package/src/config/model-profile-activation.ts +240 -46
  117. package/src/config/model-registry.ts +47 -153
  118. package/src/config/skill-settings-defaults.ts +7 -0
  119. package/src/coordinator-mcp/policy.ts +3 -2
  120. package/src/cursor-delete-path.ts +10 -0
  121. package/src/cursor-mcp-name.ts +15 -0
  122. package/src/cursor.ts +13 -2
  123. package/src/customization/inventory.ts +3 -0
  124. package/src/debug/report-bundle-parent.ts +22 -0
  125. package/src/debug/report-bundle.ts +3 -0
  126. package/src/debug/system-info.ts +13 -1
  127. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
  128. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
  129. package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
  130. package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
  131. package/src/defaults/gjc-defaults.test.ts +18 -1
  132. package/src/defaults/gjc-defaults.ts +3 -0
  133. package/src/defaults/gjc-skills.generated.ts +58 -36
  134. package/src/discovery/agents.ts +6 -1
  135. package/src/discovery/builtin.ts +10 -0
  136. package/src/discovery/helpers.ts +159 -20
  137. package/src/discovery/mcp-json.ts +6 -0
  138. package/src/discovery/ssh.ts +1 -1
  139. package/src/discovery/substitute-plugin-root.ts +3 -1
  140. package/src/edit/file-read-cache.ts +1 -1
  141. package/src/eval/py/executor.ts +380 -120
  142. package/src/export/html/css-color.ts +9 -0
  143. package/src/export/html/index.ts +5 -4
  144. package/src/export/html/template.generated.ts +1 -1
  145. package/src/extensibility/extensions/runner.ts +4 -0
  146. package/src/extensibility/extensions/types.ts +7 -0
  147. package/src/extensibility/gjc-plugins/runtime-adapters.ts +73 -8
  148. package/src/extensibility/plugins/manager.ts +3 -2
  149. package/src/extensibility/plugins/marketplace/cache.ts +15 -0
  150. package/src/extensibility/plugins/marketplace/manager.ts +11 -3
  151. package/src/extensibility/plugins/plugin-link-path.ts +12 -0
  152. package/src/extensibility/runtime-skill-discovery.ts +20 -2
  153. package/src/extensibility/skill-management.ts +6 -2
  154. package/src/extensibility/skills.ts +6 -1
  155. package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
  156. package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
  157. package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
  158. package/src/gjc-runtime/deep-interview-stage.ts +98 -21
  159. package/src/gjc-runtime/gc-runtime.ts +206 -9
  160. package/src/gjc-runtime/launch-worktree.ts +14 -4
  161. package/src/gjc-runtime/ralplan-runtime.ts +447 -52
  162. package/src/gjc-runtime/state-argv.ts +2 -0
  163. package/src/gjc-runtime/state-migrations.ts +91 -29
  164. package/src/gjc-runtime/state-runtime.ts +3716 -135
  165. package/src/gjc-runtime/state-writer.ts +83 -8
  166. package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
  167. package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
  168. package/src/gjc-runtime/workflow-manifest.ts +54 -21
  169. package/src/harness-control-plane/operate.ts +12 -4
  170. package/src/harness-control-plane/owner.ts +5 -1
  171. package/src/hashline/parser.ts +48 -9
  172. package/src/hashline/recovery.ts +13 -4
  173. package/src/internal-urls/docs-index.generated.ts +18 -17
  174. package/src/internal-urls/skill-path.ts +19 -0
  175. package/src/internal-urls/skill-protocol.ts +2 -0
  176. package/src/lsp/edits.ts +6 -0
  177. package/src/lsp/workspace-path.ts +51 -0
  178. package/src/main.ts +195 -31
  179. package/src/modes/acp/acp-agent.ts +88 -4
  180. package/src/modes/components/custom-editor.ts +14 -0
  181. package/src/modes/components/customization/customization-dashboard.ts +12 -0
  182. package/src/modes/components/extensions/extension-dashboard.ts +2 -2
  183. package/src/modes/components/extensions/state-manager.ts +15 -5
  184. package/src/modes/components/model-selector.ts +1 -4
  185. package/src/modes/components/provider-onboarding-selector.ts +1 -1
  186. package/src/modes/components/status-line/segments.ts +4 -0
  187. package/src/modes/controllers/event-controller.ts +11 -4
  188. package/src/modes/controllers/input-controller.ts +74 -29
  189. package/src/modes/controllers/runtime-mcp-command-controller.ts +8 -3
  190. package/src/modes/controllers/selector-controller.ts +28 -15
  191. package/src/modes/interactive-mode.ts +4 -0
  192. package/src/modes/runtime-init.ts +4 -0
  193. package/src/modes/utils/abort-message.ts +3 -1
  194. package/src/modes/utils/interrupt-ownership.ts +12 -0
  195. package/src/modes/utils/keybinding-matchers.ts +4 -0
  196. package/src/modes/utils/macos-option-forwarding.ts +80 -0
  197. package/src/progress/collect-project-progress.ts +153 -0
  198. package/src/progress/progress-contract.ts +172 -0
  199. package/src/progress/progress-snapshot.ts +140 -0
  200. package/src/progress/project-progress.ts +446 -0
  201. package/src/progress/render-progress.ts +209 -0
  202. package/src/prompts/system/master-mode.md +1 -0
  203. package/src/prompts/tools/skill-discovery.md +1 -1
  204. package/src/runtime-mcp/config-writer.ts +10 -6
  205. package/src/runtime-mcp/config.ts +1 -0
  206. package/src/runtime-mcp/oauth-flow.ts +5 -0
  207. package/src/runtime-mcp/oauth-public-url.ts +8 -0
  208. package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
  209. package/src/runtime-mcp/smithery-registry.ts +22 -3
  210. package/src/runtime-mcp/transports/http.ts +2 -2
  211. package/src/runtime-mcp/types.ts +6 -0
  212. package/src/sdk/acp/adapter.ts +93 -8
  213. package/src/sdk/broker/broker-exit.ts +52 -23
  214. package/src/sdk/broker/broker.ts +75 -6
  215. package/src/sdk/broker/ensure.ts +2 -0
  216. package/src/sdk/broker/lifecycle.ts +711 -66
  217. package/src/sdk/broker/session-index.ts +191 -81
  218. package/src/sdk/bus/chat-daemon-cli.ts +38 -1
  219. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  220. package/src/sdk/bus/index.ts +238 -43
  221. package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
  222. package/src/sdk/cli/session-cli.ts +17 -3
  223. package/src/sdk/client/client.ts +28 -8
  224. package/src/sdk/host/query/handlers.ts +9 -1
  225. package/src/sdk/host/session-runtime.test.ts +2647 -294
  226. package/src/sdk/host/session-runtime.ts +426 -109
  227. package/src/sdk/host/surface-policy.ts +1 -0
  228. package/src/sdk/index.ts +8 -0
  229. package/src/sdk/lifecycle-session.ts +3 -1
  230. package/src/sdk/prompt-deadline-manager.ts +15 -0
  231. package/src/sdk/protocol/operation-inventory.generated.json +146 -0
  232. package/src/sdk/protocol/operation-registry.ts +9 -2
  233. package/src/sdk/router/session-router.ts +75 -12
  234. package/src/sdk/session-list.ts +1 -0
  235. package/src/sdk/session.ts +67 -38
  236. package/src/session/agent-session.ts +623 -224
  237. package/src/session/artifacts.ts +174 -70
  238. package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
  239. package/src/session/internal/managed-session-scope.ts +3457 -462
  240. package/src/session/internal/managed-session-storage.ts +376 -61
  241. package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
  242. package/src/session/internal/task-artifact-owner-access.ts +19 -3
  243. package/src/session/internal/task-artifact-owner-transcript.ts +547 -1
  244. package/src/session/session-manager.ts +939 -133
  245. package/src/session/session-retirement.ts +573 -14
  246. package/src/session/session-storage.ts +417 -14
  247. package/src/session/task-artifact-owner-codec.ts +71 -8
  248. package/src/session/task-artifact-owner-retirement.ts +21 -19
  249. package/src/setup/credential-auto-import.ts +12 -3
  250. package/src/setup/credential-import.ts +176 -5
  251. package/src/setup/paseo/announce.ts +6 -1
  252. package/src/skill-state/active-state.ts +132 -2
  253. package/src/slash-commands/builtin-registry.ts +39 -9
  254. package/src/slash-commands/helpers/progress-report.ts +23 -0
  255. package/src/stt/setup.ts +3 -5
  256. package/src/task/commands.ts +3 -1
  257. package/src/task/executor.ts +4 -0
  258. package/src/task/index.ts +6 -0
  259. package/src/tools/ask.ts +205 -6
  260. package/src/tools/ast-edit.ts +4 -0
  261. package/src/tools/bash-allowed-prefixes.ts +15 -0
  262. package/src/tools/bash-skill-urls.ts +56 -3
  263. package/src/tools/bash.ts +48 -79
  264. package/src/tools/descriptors.ts +5 -1
  265. package/src/tools/eval.ts +56 -36
  266. package/src/tools/image-gen.ts +1 -1
  267. package/src/tools/index.ts +3 -0
  268. package/src/tools/python.ts +187 -66
  269. package/src/tools/recipe/index.ts +9 -0
  270. package/src/tools/recipe/recipe-guard.ts +19 -0
  271. package/src/tools/restricted-cwd.ts +10 -0
  272. package/src/tools/skill-discovery.ts +5 -0
  273. package/src/tools/skill.ts +28 -1
  274. package/src/tools/todo-write.ts +10 -1
  275. package/src/tools/tool-catalog.generated.ts +1 -1
  276. package/src/tools/write.ts +1 -0
  277. package/src/utils/tools-checksum.ts +34 -0
  278. package/src/utils/tools-manager.ts +24 -6
  279. package/src/web/search/providers/codex.ts +3 -2
@@ -5,6 +5,7 @@ import * as fs from "node:fs/promises";
5
5
  import * as path from "node:path";
6
6
  import { promisify } from "node:util";
7
7
  import { isContinuingMidRunMaintenanceOutcome, isNonDispatchedToolEvent, ThinkingLevel } from "@gajae-code/agent-core";
8
+ import type { AttemptScope } from "@gajae-code/agent-core/attempt-scope";
8
9
  import type { Api, ImageContent, Model, ProviderDiagnostic } from "@gajae-code/ai/core";
9
10
 
10
11
  import { logger } from "@gajae-code/utils";
@@ -229,6 +230,8 @@ class SdkOnlyIdempotencyConflictError extends Error {
229
230
  * the outcome synchronously on its capture slot, while this runtime observes
230
231
  * the publication from the separate `agent_end` handler. */
231
232
  const SDK_ONLY_TERMINAL_PUBLICATION_WAIT_MS = 1_000;
233
+ /** Queue-owner cancellation is synchronous in AgentSession; this bound fails closed if its disposition is lost. */
234
+ const SDK_ONLY_QUEUE_CANCELLATION_WAIT_MS = 1_000;
232
235
  /** Bounded wait for in-flight workflow gate resolutions to settle during SDK
233
236
  * runtime shutdown before proceeding with cleanup. Unresolved resolutions
234
237
  * after this bound are abandoned — their durable broker state is the recovery
@@ -746,6 +749,49 @@ export interface InvocationCorrelation {
746
749
  turnId: string;
747
750
  }
748
751
 
752
+ type AcceptedQueueDisposition = "preflight" | "queued" | "consumed" | "promoted" | "removed" | "teardown";
753
+
754
+ interface AcceptedQueueCancellationResult {
755
+ removed: boolean;
756
+ consumed: boolean;
757
+ unconfirmed: boolean;
758
+ }
759
+
760
+ interface AcceptedQueueCancellation {
761
+ correlation: InvocationCorrelation;
762
+ connectionId: string | undefined;
763
+ controller: AbortController;
764
+ accepted: boolean;
765
+ queueCandidate: boolean;
766
+ disposition: AcceptedQueueDisposition;
767
+ dispositionPromise: Promise<AcceptedQueueDisposition>;
768
+ resolveDisposition: (disposition: AcceptedQueueDisposition) => void;
769
+ removalTerminalization?: Promise<boolean>;
770
+ removalFailure?: unknown;
771
+ }
772
+
773
+ function retireAcceptedQueueCancellation(
774
+ cancellations: Map<string, AcceptedQueueCancellation>,
775
+ correlation: InvocationCorrelation,
776
+ durableTerminalConfirmed = false,
777
+ ): void {
778
+ const key = `${correlation.commandId}:${correlation.turnId}`;
779
+ const cancellation = cancellations.get(key);
780
+ if (!cancellation) return;
781
+ if (!cancellation.controller.signal.aborted) cancellation.controller.abort();
782
+ if (!durableTerminalConfirmed && cancellation.disposition === "removed" && cancellation.removalTerminalization) {
783
+ void cancellation.removalTerminalization.then(published => {
784
+ if (published && cancellations.get(key) === cancellation) cancellations.delete(key);
785
+ });
786
+ return;
787
+ }
788
+ if (cancellations.get(key) === cancellation) cancellations.delete(key);
789
+ if (cancellation.disposition === "queued") {
790
+ cancellation.disposition = "teardown";
791
+ cancellation.resolveDisposition("teardown");
792
+ }
793
+ }
794
+
749
795
  export type InvocationKind = "prompt" | "skill" | "steer";
750
796
  type InvocationStatus = "accepted" | "in_flight" | "terminal_ok" | "failed" | "uncertain";
751
797
  type InvocationOutcome = SdkPromptTerminalOutcome;
@@ -2063,12 +2109,7 @@ function createQuerySurface(
2063
2109
  };
2064
2110
  const getProfileCredentialSessionId = () => ctx.credentialSessionId ?? id;
2065
2111
  const profileSettings = (options.settings ?? ctx.settings) as Pick<Settings, "get"> | undefined;
2066
- const getProfileAvailableModels = (): Model<Api>[] => {
2067
- const getAvailableForProfileActivation = ctx.modelRegistry.getAvailableForProfileActivation;
2068
- return typeof getAvailableForProfileActivation === "function"
2069
- ? getAvailableForProfileActivation.call(ctx.modelRegistry)
2070
- : ctx.modelRegistry.getAvailable();
2071
- };
2112
+ const getProfileAvailableModels = (): Model<Api>[] => ctx.modelRegistry.getAvailable();
2072
2113
  const resolveProfileAvailability = async (
2073
2114
  profile: ModelProfileDefinition,
2074
2115
  authenticatedProviders: ReadonlySet<string>,
@@ -2422,6 +2463,9 @@ function createQuerySurface(
2422
2463
  getExtensions: () => ctx.getExtensions(),
2423
2464
  getArtifactRange: (artifactId, offset, length) => ctx.getArtifactRange?.(artifactId, offset, length),
2424
2465
  getJobs: () => ctx.getJobs(),
2466
+ ...(typeof (ctx as Partial<ExtensionContext>).getProjectProgress === "function"
2467
+ ? { getProjectProgress: () => ctx.getProjectProgress!() }
2468
+ : {}),
2425
2469
  getPromptStatus: (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
2426
2470
  reconciliation.lookup("prompt", selector),
2427
2471
  getSkillInvokeStatus: (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
@@ -2694,6 +2738,11 @@ export function providerFailureFromAgentEnd(
2694
2738
  return undefined;
2695
2739
  }
2696
2740
  if (errorKind === "local_snapshot_failure" || errorKind === "local_buffer_overflow") return undefined;
2741
+ if (errorKind === "local_empty_response")
2742
+ return {
2743
+ code: "empty_response",
2744
+ message: PROMPT_FAILURE_MESSAGE_SUBMISSION,
2745
+ };
2697
2746
  let errorMessage: unknown;
2698
2747
  try {
2699
2748
  errorMessage = assistant.errorMessage;
@@ -2874,7 +2923,7 @@ function createControlSurface(
2874
2923
  connectionId: string | undefined,
2875
2924
  sdkRunToken: string,
2876
2925
  promotion?: { startsOwnRun?: boolean; removed?: boolean },
2877
- ) => void,
2926
+ ) => undefined | Promise<boolean>,
2878
2927
  policy?: SdkSurfacePolicy,
2879
2928
  settings?: Settings,
2880
2929
  configOverrides?: Map<string, unknown>,
@@ -2895,6 +2944,7 @@ function createControlSurface(
2895
2944
  trackGateResolution: <T>(resolution: Promise<T>) => Promise<T> = async resolution => await resolution,
2896
2945
  onInvocationCompletionReconciledForTests?: (kind: InvocationKind, correlation: InvocationCorrelation) => void,
2897
2946
  publishLifecycleFrame?: (frame: SdkFrame) => void,
2947
+ acceptedQueueCancellations: Map<string, AcceptedQueueCancellation> = new Map(),
2898
2948
  ): ControlSurface {
2899
2949
  const normalizePromptImages = (value: unknown): ImageContent[] => {
2900
2950
  if (!Array.isArray(value)) return [];
@@ -2998,6 +3048,49 @@ function createControlSurface(
2998
3048
  }
2999
3049
  return pending;
3000
3050
  };
3051
+ const cancelAcceptedQueueSubmissions = async (
3052
+ connectionId: string | undefined,
3053
+ admittedRequests: readonly AcceptedQueueCancellation[] = [...acceptedQueueCancellations.values()],
3054
+ ): Promise<AcceptedQueueCancellationResult> => {
3055
+ const result: AcceptedQueueCancellationResult = { removed: false, consumed: false, unconfirmed: false };
3056
+ if (connectionId === undefined) return result;
3057
+ const owned = admittedRequests.filter(
3058
+ request => request.connectionId === connectionId && request.accepted && request.queueCandidate,
3059
+ );
3060
+ for (const request of owned) {
3061
+ let disposition = request.disposition;
3062
+ if (disposition === "queued") {
3063
+ request.controller.abort();
3064
+ disposition = request.disposition;
3065
+ if (disposition === "queued") {
3066
+ const timedOut = Promise.withResolvers<AcceptedQueueDisposition>();
3067
+ const timer = setTimeout(() => timedOut.resolve("teardown"), SDK_ONLY_QUEUE_CANCELLATION_WAIT_MS);
3068
+ timer.unref();
3069
+ try {
3070
+ disposition = await Promise.race([request.dispositionPromise, timedOut.promise]);
3071
+ } finally {
3072
+ clearTimeout(timer);
3073
+ }
3074
+ }
3075
+ }
3076
+ if (disposition === "removed") {
3077
+ if (await request.removalTerminalization) result.removed = true;
3078
+ else result.unconfirmed = true;
3079
+ } else if (disposition === "consumed" || disposition === "promoted") {
3080
+ result.consumed = true;
3081
+ } else if (disposition === "teardown") {
3082
+ result.unconfirmed = true;
3083
+ }
3084
+ }
3085
+ return result;
3086
+ };
3087
+ const requesterOwnsActiveRun = (connectionId: string | undefined): boolean => {
3088
+ if (connectionId === undefined) return false;
3089
+ const seamOwner = terminalAbortSeams?.getActivePromptOwnerConnectionId?.();
3090
+ return seamOwner === undefined
3091
+ ? (activePromptOwner.connectionIds?.has(connectionId) ?? false)
3092
+ : seamOwner === connectionId;
3093
+ };
3001
3094
  const normalizeClientRef = (clientRef: string | undefined): string | undefined => {
3002
3095
  if (clientRef === undefined) return undefined;
3003
3096
  const trimmed = clientRef.trim();
@@ -3012,6 +3105,7 @@ function createControlSurface(
3012
3105
  clientRef: string | undefined,
3013
3106
  run: (options: {
3014
3107
  sdkRunCapability: SdkRunCapability;
3108
+ preflightSignal: AbortSignal;
3015
3109
  onPreflightAccepted: () => void;
3016
3110
  onPreflightAcceptCommit: () => Promise<void>;
3017
3111
  /** Internal disposition before a queued submission is actually consumed. */
@@ -3034,6 +3128,7 @@ function createControlSurface(
3034
3128
  const sdkRunToken = `${correlation.commandId}:${correlation.turnId}`;
3035
3129
  const sdkRunCapability = createSdkRunCapability(sdkRunToken);
3036
3130
  const publishTerminal = (outcome: InvocationOutcome): void => {
3131
+ retireAcceptedQueueCancellation(acceptedQueueCancellations, correlation);
3037
3132
  publishLifecycleFrame?.({
3038
3133
  type: "agent_end",
3039
3134
  sessionId: ctx.sessionManager.getSessionId(),
@@ -3052,9 +3147,22 @@ function createControlSurface(
3052
3147
  publishTerminal(canonicalFailedOutcome(failure));
3053
3148
  };
3054
3149
  const preflight = Promise.withResolvers<void>();
3150
+ const preflightController = new AbortController();
3151
+ const queueDisposition = Promise.withResolvers<AcceptedQueueDisposition>();
3152
+ const queueCancellation: AcceptedQueueCancellation = {
3153
+ correlation,
3154
+ connectionId: requesterConnectionId,
3155
+ controller: preflightController,
3156
+ accepted: false,
3157
+ queueCandidate: false,
3158
+ disposition: "preflight",
3159
+ dispositionPromise: queueDisposition.promise,
3160
+ resolveDisposition: queueDisposition.resolve,
3161
+ };
3055
3162
  let accepted = false;
3056
3163
  let settled = false;
3057
3164
  const cancelPreflight = () => {
3165
+ preflightController.abort();
3058
3166
  if (settled) return;
3059
3167
  settled = true;
3060
3168
  preflight.reject(
@@ -3073,6 +3181,8 @@ function createControlSurface(
3073
3181
  try {
3074
3182
  await reconciliation.noteAccepted(kind, correlation, retainedClientRef);
3075
3183
  accepted = true;
3184
+ queueCancellation.accepted = true;
3185
+ if (queueCancellation.queueCandidate) queueCancellation.disposition = "queued";
3076
3186
  settled = true;
3077
3187
  if (kind === "prompt" && startsOwnTurn) armPromptDeadline(correlation);
3078
3188
  // The accepted submission does NOT own the active turn until its run
@@ -3105,16 +3215,23 @@ function createControlSurface(
3105
3215
  // Skills always start their own invocation; a plain prompt starts one
3106
3216
  // only when idle at dispatch time.
3107
3217
  const startsOwnTurn = kind === "skill" || (kind === "prompt" && !alwaysQueued && !queuedAtDispatch);
3218
+ queueCancellation.queueCandidate = queuedAtDispatch;
3219
+ if (queuedAtDispatch && queueCancellation.accepted) queueCancellation.disposition = "queued";
3220
+ if (kind === "prompt")
3221
+ acceptedQueueCancellations.set(`${correlation.commandId}:${correlation.turnId}`, queueCancellation);
3108
3222
  let promotionStartsOwnRun: boolean | undefined;
3109
3223
  try {
3110
3224
  const submission = Promise.resolve(
3111
3225
  run({
3112
3226
  onPreflightAccepted: () => void accept().catch(() => undefined),
3227
+ preflightSignal: preflightController.signal,
3113
3228
  sdkRunCapability,
3114
3229
  onPreflightAcceptCommit: accept,
3115
3230
  onDispatchDisposition: promotion => {
3116
3231
  promotionStartsOwnRun = promotion.startsOwnRun;
3117
3232
  if (promotion.startsOwnRun === false) {
3233
+ queueCancellation.queueCandidate = true;
3234
+ if (queueCancellation.accepted) queueCancellation.disposition = "queued";
3118
3235
  // Dispatch-race diversion (#4668 review P1): the idle snapshot leased
3119
3236
  // this prompt at acceptance, but it was actually diverted into the
3120
3237
  // in-flight run's steering queue. While it sits queued — legitimately
@@ -3131,7 +3248,37 @@ function createControlSurface(
3131
3248
  // terminal-abort that turn (review threads P1/P2).
3132
3249
  onQueuedPromoted: (promotion?: { startsOwnRun?: boolean; removed?: boolean }) => {
3133
3250
  promotionStartsOwnRun = promotion?.startsOwnRun;
3134
- onPromotedTurn?.(kind, correlation, requesterConnectionId, sdkRunToken, promotion);
3251
+ if (promotion?.removed) {
3252
+ queueCancellation.disposition = "removed";
3253
+ queueCancellation.removalTerminalization = (async () => {
3254
+ try {
3255
+ return (
3256
+ (await onPromotedTurn?.(
3257
+ kind,
3258
+ correlation,
3259
+ requesterConnectionId,
3260
+ sdkRunToken,
3261
+ promotion,
3262
+ )) === true
3263
+ );
3264
+ } catch (error) {
3265
+ queueCancellation.removalFailure = error;
3266
+ logger.error("SDK queued prompt terminal publication failed", {
3267
+ commandId: correlation.commandId,
3268
+ turnId: correlation.turnId,
3269
+ error: String(error),
3270
+ });
3271
+ return false;
3272
+ }
3273
+ })();
3274
+ queueCancellation.resolveDisposition("removed");
3275
+ return;
3276
+ }
3277
+ if (queueCancellation.queueCandidate) {
3278
+ queueCancellation.disposition = promotion?.startsOwnRun === false ? "consumed" : "promoted";
3279
+ queueCancellation.resolveDisposition(queueCancellation.disposition);
3280
+ }
3281
+ void onPromotedTurn?.(kind, correlation, requesterConnectionId, sdkRunToken, promotion);
3135
3282
  },
3136
3283
  queuedAtDispatch,
3137
3284
  }),
@@ -3218,6 +3365,7 @@ function createControlSurface(
3218
3365
  },
3219
3366
  error => {
3220
3367
  if (settled) {
3368
+ retireAcceptedQueueCancellation(acceptedQueueCancellations, correlation);
3221
3369
  // The submission promise rejects after preflight acceptance only when the
3222
3370
  // work itself is over (provider stream interrupt, abort, queue failure).
3223
3371
  // The accepted run never started (agent_start never fired), so its pending
@@ -3338,6 +3486,7 @@ function createControlSurface(
3338
3486
  ...(acceptedFields?.() ?? {}),
3339
3487
  };
3340
3488
  } catch (error) {
3489
+ retireAcceptedQueueCancellation(acceptedQueueCancellations, correlation);
3341
3490
  if (!accepted) reconciliation.release(kind, retainedClientRef);
3342
3491
  throw error;
3343
3492
  } finally {
@@ -3705,6 +3854,36 @@ function createControlSurface(
3705
3854
  return boundTerminalRetentionState(state.keys, mutate(state.scopes), terminalReservationLimit);
3706
3855
  });
3707
3856
  };
3857
+ const unconfirmedQueueTerminal = async (): Promise<unknown> => {
3858
+ const result = {
3859
+ ok: true,
3860
+ selection: scope,
3861
+ turn: "uncertain",
3862
+ ownedWork: scope === "turn" ? "left_running" : "uncertain",
3863
+ automaticDelivery: scope === "turn" ? "enabled" : "none",
3864
+ resumeOnOwnedCompletion: scope === "turn",
3865
+ reason: "queue_terminal_unconfirmed",
3866
+ };
3867
+ const payloadHash = hashResult(result);
3868
+ await transactBoundedTerminalScopes(scopes =>
3869
+ scopes.map(record =>
3870
+ (keyHash
3871
+ ? record.idempotencyKeyHash === keyHash
3872
+ : record.turnContinuationFence.abortedAttemptEpoch === epoch) &&
3873
+ record.turnDisposition === "no_effect_reserved"
3874
+ ? {
3875
+ ...record,
3876
+ turnDisposition: "uncertain",
3877
+ responsePayloadHash: payloadHash,
3878
+ replayPayloadHash: replayShapedHash(record, result, payloadHash),
3879
+ terminalAt: Date.now(),
3880
+ }
3881
+ : record,
3882
+ ),
3883
+ );
3884
+ return result;
3885
+ };
3886
+ const abortingConnectionId = sdkControlRequesterContext.getStore();
3708
3887
  let handle = terminalAbortSeams.getActivePromptHandle();
3709
3888
  let epoch = terminalAbortSeams.getTerminalTurnEpoch();
3710
3889
  // Set when the no-effect reservation found an existing SAME-input row or
@@ -3796,9 +3975,12 @@ function createControlSurface(
3796
3975
  const recheckedHandle = terminalAbortSeams.getActivePromptHandle();
3797
3976
  const recheckedEpoch = terminalAbortSeams.getTerminalTurnEpoch();
3798
3977
  if (!recheckedHandle || recheckedEpoch === undefined) {
3799
- // No prompt won the race: finalize the reserved row so a later
3800
- // same-key retry replays this deterministic no_active_turn result
3801
- // (review thread P2).
3978
+ // An accepted SDK queue owner is independent of the root prompt. Remove
3979
+ // only this requester's exact queued submissions, and do not claim the
3980
+ // root stopped; terminal success waits for each correlated removal
3981
+ // failure+terminal transaction to become durable.
3982
+ const queueCancellation = await cancelAcceptedQueueSubmissions(abortingConnectionId);
3983
+ if (queueCancellation.unconfirmed) return await unconfirmedQueueTerminal();
3802
3984
  return await returnNoActiveTurn();
3803
3985
  }
3804
3986
  handle = recheckedHandle;
@@ -3816,7 +3998,6 @@ function createControlSurface(
3816
3998
  // per-connection selection of the full bus path. The owner is re-read
3817
3999
  // through the seam when provided (deterministic tests) and otherwise
3818
4000
  // from the runtime-tracked accepting connection.
3819
- const abortingConnectionId = sdkControlRequesterContext.getStore();
3820
4001
  const currentOwnerConnectionIds = (): ReadonlySet<string> => {
3821
4002
  const seam = terminalAbortSeams.getActivePromptOwnerConnectionId?.();
3822
4003
  // The seam reports a single deterministic owner (test harnesses); the
@@ -3864,9 +4045,10 @@ function createControlSurface(
3864
4045
  abortingConnectionId === undefined ||
3865
4046
  !recheckedOwners.has(abortingConnectionId)
3866
4047
  ) {
3867
- // The turn is still not the aborting connection's: finalize the
3868
- // reserved row so a later same-key retry replays no_active_turn
3869
- // deterministically (review thread P2).
4048
+ // Request-owned steering cannot borrow the unrelated root's abort
4049
+ // authority. It may still be removed by its exact queue capability.
4050
+ const queueCancellation = await cancelAcceptedQueueSubmissions(abortingConnectionId);
4051
+ if (queueCancellation.unconfirmed) return await unconfirmedQueueTerminal();
3870
4052
  const noActiveTurnResult = {
3871
4053
  ok: true,
3872
4054
  selection: scope,
@@ -4289,6 +4471,26 @@ function createControlSurface(
4289
4471
  );
4290
4472
  },
4291
4473
  abort: async () => {
4474
+ const connectionId = sdkControlRequesterContext.getStore();
4475
+ const ownedAdmissions = [...acceptedQueueCancellations.values()].filter(
4476
+ request => connectionId !== undefined && request.connectionId === connectionId,
4477
+ );
4478
+ const ownedPreflights = connectionId === undefined ? [] : [...(pendingPreflights.get(connectionId) ?? [])];
4479
+ // Capture both phases before cancellation or any awaited terminal work:
4480
+ // acceptance can settle its callback before agent_start owns the run.
4481
+ for (const cancel of ownedPreflights) cancel();
4482
+ let cancelledAdmission = ownedPreflights.length > 0;
4483
+ for (const request of ownedAdmissions) {
4484
+ if (request.disposition !== "preflight" || request.controller.signal.aborted) continue;
4485
+ request.controller.abort();
4486
+ cancelledAdmission = true;
4487
+ }
4488
+ if (connectionId !== undefined && !requesterOwnsActiveRun(connectionId)) {
4489
+ const queueCancellation = await cancelAcceptedQueueSubmissions(connectionId, ownedAdmissions);
4490
+ if (queueCancellation.unconfirmed) return { aborted: false, reason: "queue_terminal_unconfirmed" };
4491
+ if (queueCancellation.removed || cancelledAdmission) return { aborted: true };
4492
+ return { aborted: false, turn: "no_active_turn" };
4493
+ }
4292
4494
  await Promise.resolve(ctx.abort()).catch(() => undefined);
4293
4495
  return { aborted: true };
4294
4496
  },
@@ -4628,6 +4830,61 @@ function quiescingFrame(frame: Record<string, unknown>): Record<string, unknown>
4628
4830
 
4629
4831
  /** Install a complete SDK host for a session when notifications are inactive. */
4630
4832
  export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: CreateSdkSessionRuntimeOptions): void {
4833
+ const acceptedQueueCancellations = new Map<string, AcceptedQueueCancellation>();
4834
+ const disposeAcceptedQueueCancellations = async (): Promise<void> => {
4835
+ const cancellations = [...acceptedQueueCancellations.values()];
4836
+ const queuedRemovals = cancellations.filter(
4837
+ cancellation =>
4838
+ cancellation.accepted &&
4839
+ cancellation.queueCandidate &&
4840
+ (cancellation.disposition === "queued" || cancellation.disposition === "removed"),
4841
+ );
4842
+ for (const cancellation of cancellations)
4843
+ if (!cancellation.controller.signal.aborted) cancellation.controller.abort();
4844
+
4845
+ const failures: unknown[] = [];
4846
+ const timedOut = Promise.withResolvers<void>();
4847
+ const timer = setTimeout(timedOut.resolve, SDK_ONLY_QUEUE_CANCELLATION_WAIT_MS);
4848
+ timer.unref();
4849
+ try {
4850
+ await Promise.all(
4851
+ queuedRemovals.map(async cancellation => {
4852
+ const publication = (async (): Promise<boolean> => {
4853
+ const disposition =
4854
+ cancellation.disposition === "queued"
4855
+ ? await cancellation.dispositionPromise
4856
+ : cancellation.disposition;
4857
+ if (disposition === "promoted" || disposition === "consumed") return true;
4858
+ return disposition === "removed" && (await cancellation.removalTerminalization) === true;
4859
+ })();
4860
+ const published = await Promise.race([publication, timedOut.promise.then(() => false)]);
4861
+ const key = `${cancellation.correlation.commandId}:${cancellation.correlation.turnId}`;
4862
+ if (published || acceptedQueueCancellations.get(key) !== cancellation) {
4863
+ if (acceptedQueueCancellations.get(key) === cancellation) acceptedQueueCancellations.delete(key);
4864
+ } else {
4865
+ failures.push(
4866
+ cancellation.removalFailure ??
4867
+ new Error(
4868
+ `Queued prompt ${cancellation.correlation.commandId}:${cancellation.correlation.turnId} terminal publication was not confirmed during shutdown.`,
4869
+ ),
4870
+ );
4871
+ }
4872
+ }),
4873
+ );
4874
+ } finally {
4875
+ clearTimeout(timer);
4876
+ }
4877
+ for (const cancellation of cancellations) {
4878
+ if (queuedRemovals.includes(cancellation)) continue;
4879
+ const key = `${cancellation.correlation.commandId}:${cancellation.correlation.turnId}`;
4880
+ if (acceptedQueueCancellations.get(key) === cancellation) acceptedQueueCancellations.delete(key);
4881
+ }
4882
+ if (failures.length > 0)
4883
+ throw Object.assign(
4884
+ new AggregateError(failures, "SDK runtime could not durably publish queued prompt cancellations."),
4885
+ { code: "sdk_reconciliation_teardown_failed" },
4886
+ );
4887
+ };
4631
4888
  let active:
4632
4889
  | {
4633
4890
  sessionId: string;
@@ -4658,6 +4915,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4658
4915
  }>;
4659
4916
  openLifecycleBatches: Array<{
4660
4917
  epoch: number;
4918
+ /** Session-initiated starts still reserve their own unmatched end. */
4919
+ unownedStart: boolean;
4661
4920
  invocations: Array<{
4662
4921
  kind: InvocationKind;
4663
4922
  correlation: InvocationCorrelation;
@@ -4713,6 +4972,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4713
4972
  string,
4714
4973
  { state: RuntimeState; batch?: LifecycleBatch; correlationKey?: string }
4715
4974
  >();
4975
+ const lifecycleScopeOwners = new WeakMap<AttemptScope, { state: RuntimeState; batch: LifecycleBatch }>();
4716
4976
  const lifecycleCorrelationKey = (correlation: InvocationCorrelation): string =>
4717
4977
  `${correlation.commandId}:${correlation.turnId}`;
4718
4978
  const sessionIdentityForContext = (ctx: ExtensionContext): string | undefined => {
@@ -4782,13 +5042,18 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4782
5042
  owner.unrecordedFailureReasons?.clear();
4783
5043
  for (const [token, binding] of lifecycleRunOwners) if (binding.state === owner) lifecycleRunOwners.delete(token);
4784
5044
  };
5045
+ const hasQueuedTerminalRecovery = (owner: RuntimeState): boolean =>
5046
+ [...acceptedQueueCancellations.values()].some(
5047
+ cancellation => cancellation.disposition === "removed" && owner.deadlineManager.has(cancellation.correlation),
5048
+ );
4785
5049
  const maybeRetireLifecycleOwner = (owner: RuntimeState): void => {
4786
5050
  if (
4787
5051
  owner.pending.length > 0 ||
4788
5052
  owner.openLifecycleBatches.length > 0 ||
4789
5053
  (owner.attachedInvocations?.length ?? 0) > 0 ||
4790
5054
  (owner.drainedInvocations?.length ?? 0) > 0 ||
4791
- owner.lifecycleTasks.size > 0
5055
+ owner.lifecycleTasks.size > 0 ||
5056
+ hasQueuedTerminalRecovery(owner)
4792
5057
  )
4793
5058
  return;
4794
5059
  removeRetiredLifecycleOwner(owner);
@@ -4809,8 +5074,10 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4809
5074
  entry => lifecycleCorrelationKey(entry.correlation) !== target,
4810
5075
  );
4811
5076
  }
5077
+ // Retiring an SDK correlation must not discard an unrelated session-
5078
+ // initiated start. Its delayed end still belongs to that empty batch.
4812
5079
  owner.openLifecycleBatches = owner.openLifecycleBatches.filter(
4813
- batch => batch.invocations.length > 0 || batch.attachedInvocations.length > 0,
5080
+ batch => batch.unownedStart || batch.invocations.length > 0 || batch.attachedInvocations.length > 0,
4814
5081
  );
4815
5082
  owner.drainedInvocations = owner.drainedInvocations?.filter(
4816
5083
  entry => lifecycleCorrelationKey(entry.correlation) !== target,
@@ -5089,32 +5356,24 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5089
5356
  stopReason?: AgentEndEvent["stopReason"],
5090
5357
  startToken?: string,
5091
5358
  startTokens?: string[],
5359
+ startLifecycleScope?: AttemptScope,
5092
5360
  ): Promise<void> => {
5093
5361
  const current = lifecycleOwner?.state ?? active;
5094
5362
  if (!current) return;
5095
5363
  const failureBatch = lifecycleOwner?.batch;
5096
- const adoptLifecycleBatch = (
5097
- batch:
5098
- | Array<{
5099
- kind: InvocationKind;
5100
- correlation: InvocationCorrelation;
5101
- connectionId: string | undefined;
5102
- }>
5103
- | undefined,
5104
- ): void => {
5105
- if (!batch || batch.length === 0) {
5106
- current.activeInvocation = undefined;
5107
- current.drainedInvocations = undefined;
5108
- current.attachedInvocations = undefined;
5109
- if (current === active) activePromptOwnerHolder.connectionIds = undefined;
5110
- return;
5111
- }
5112
- current.activeInvocation = batch[0];
5113
- current.drainedInvocations = batch.map(({ kind, correlation }) => ({ kind, correlation }));
5114
- current.attachedInvocations = undefined;
5364
+ const adoptLifecycleBatch = (batch: LifecycleBatch | undefined): void => {
5365
+ current.activeInvocation = batch?.invocations[0];
5366
+ current.drainedInvocations = batch
5367
+ ? [...batch.invocations, ...batch.attachedInvocations].map(({ kind, correlation }) => ({
5368
+ kind,
5369
+ correlation,
5370
+ }))
5371
+ : undefined;
5372
+ current.attachedInvocations = batch?.attachedInvocations.slice();
5115
5373
  const owners = new Set<string>();
5116
- for (const entry of batch) if (entry.connectionId !== undefined) owners.add(entry.connectionId);
5117
- if (current === active) activePromptOwnerHolder.connectionIds = owners;
5374
+ for (const entry of batch?.invocations ?? [])
5375
+ if (entry.connectionId !== undefined) owners.add(entry.connectionId);
5376
+ if (current === active) activePromptOwnerHolder.connectionIds = owners.size > 0 ? owners : undefined;
5118
5377
  };
5119
5378
  let transitions: Array<{ kind: InvocationKind; correlation: InvocationCorrelation }> = [];
5120
5379
  if (type === "agent_failed") {
@@ -5148,13 +5407,18 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5148
5407
  );
5149
5408
  transitions = [...fallback, ...attached].map(({ kind, correlation }) => ({ kind, correlation }));
5150
5409
  } else if (type === "agent_start") {
5151
- current.lifecycleEpoch = ++nextLifecycleEpoch;
5410
+ const scopeOwner = startLifecycleScope ? lifecycleScopeOwners.get(startLifecycleScope) : undefined;
5411
+ const scopeBatch =
5412
+ scopeOwner?.state === current && current.openLifecycleBatches.includes(scopeOwner.batch)
5413
+ ? scopeOwner.batch
5414
+ : undefined;
5415
+ current.lifecycleEpoch = scopeBatch?.epoch ?? ++nextLifecycleEpoch;
5152
5416
  if (current === active) activePromptOwnerHolder.lifecycleEpoch = current.lifecycleEpoch;
5153
5417
  // Mark lifecycle active even when the drain is empty: a monitor/cron
5154
5418
  // run started by the session has no SDK pending entry but is still a
5155
5419
  // real active run that later in-run promotions must attach to instead
5156
- // of falling back to pending (review P1). Empty drains leave the
5157
- // previous SDK owner untouched.
5420
+ // of falling back to pending (review P1). Only an explicitly bound
5421
+ // continuation retains a previous public lifecycle boundary.
5158
5422
  current.lifecycleActive = true;
5159
5423
  // Drain EVERY entry admitted for this run: a continuation may promote
5160
5424
  // several follow-ups (each with its own requester correlation) into one
@@ -5175,40 +5439,47 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5175
5439
  typeof startToken === "string" && startToken.length > 0
5176
5440
  ? lifecycleRunOwners.get(startToken)?.batch
5177
5441
  : undefined;
5178
- if (drained.length > 0) {
5179
- const batch: LifecycleBatch = {
5442
+ const continuationBatch =
5443
+ scopeBatch ??
5444
+ (drained.length === 0 && existingTokenBatch && current.openLifecycleBatches.includes(existingTokenBatch)
5445
+ ? existingTokenBatch
5446
+ : undefined);
5447
+ let batch: LifecycleBatch;
5448
+ if (!continuationBatch) {
5449
+ // Every new run owns an end, including interactive/monitor runs
5450
+ // without SDK invocations. Their delayed ends must never consume
5451
+ // the next SDK run's terminal receipt.
5452
+ batch = {
5180
5453
  epoch: current.lifecycleEpoch,
5454
+ unownedStart: drained.length === 0,
5181
5455
  invocations: drained,
5182
5456
  attachedInvocations: [],
5183
- startPublished: false,
5457
+ startPublished: drained.length === 0,
5184
5458
  heldContentDropped: false,
5185
5459
  heldContent: [],
5186
5460
  };
5187
5461
  current.openLifecycleBatches.push(batch);
5188
- for (const entry of drained) {
5189
- lifecycleRunOwners.set(entry.sdkRunToken, {
5190
- state: current,
5191
- batch,
5192
- correlationKey: lifecycleCorrelationKey(entry.correlation),
5193
- });
5194
- }
5195
- adoptLifecycleBatch(drained);
5196
- for (const entry of drained) {
5197
- if (entry.kind !== "prompt") continue;
5198
- if (current.deadlineManager.isExpiring(entry.correlation))
5199
- current.deadlineManager.captureExpiringRun(entry.correlation);
5200
- else current.deadlineManager.onRunStarted(entry.correlation);
5201
- }
5202
- } else if (existingTokenBatch && current.openLifecycleBatches.includes(existingTokenBatch)) {
5203
- // A continuation within the same SDK-owned run carries the same token
5204
- // but has no newly pending invocation. Preserve the established owner so
5205
- // later chunks and the final terminal remain directed to its submitters.
5206
- adoptLifecycleBatch(existingTokenBatch.invocations);
5207
5462
  } else {
5208
- // An empty drain is an agent-initiated successor run. Clear the
5209
- // predecessor's SDK owner and active invocation so abort ownership and
5210
- // tool-progress renewal cannot leak into the successor.
5211
- adoptLifecycleBatch(undefined);
5463
+ // The producer consumed the predecessor end, or this is the same
5464
+ // token-owned run. Preserve its attached owners and single final end.
5465
+ batch = continuationBatch;
5466
+ batch.invocations.push(...drained);
5467
+ if (drained.length > 0) batch.startPublished = false;
5468
+ }
5469
+ if (startLifecycleScope) lifecycleScopeOwners.set(startLifecycleScope, { state: current, batch });
5470
+ for (const entry of drained) {
5471
+ lifecycleRunOwners.set(entry.sdkRunToken, {
5472
+ state: current,
5473
+ batch,
5474
+ correlationKey: lifecycleCorrelationKey(entry.correlation),
5475
+ });
5476
+ }
5477
+ adoptLifecycleBatch(batch);
5478
+ for (const entry of drained) {
5479
+ if (entry.kind !== "prompt") continue;
5480
+ if (current.deadlineManager.isExpiring(entry.correlation))
5481
+ current.deadlineManager.captureExpiringRun(entry.correlation);
5482
+ else current.deadlineManager.onRunStarted(entry.correlation);
5212
5483
  }
5213
5484
  transitions = drained.map(({ kind, correlation }) => ({ kind, correlation }));
5214
5485
  } else {
@@ -5284,7 +5555,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5284
5555
  transitions.map(({ correlation }) => `${correlation.commandId}:${correlation.turnId}`),
5285
5556
  );
5286
5557
  if (
5287
- !ended.invocations.some(({ correlation }) =>
5558
+ (ended.invocations.length > 0 || ended.attachedInvocations.length > 0) &&
5559
+ ![...ended.invocations, ...ended.attachedInvocations].some(({ correlation }) =>
5288
5560
  transitionKeys.has(`${correlation.commandId}:${correlation.turnId}`),
5289
5561
  )
5290
5562
  )
@@ -5331,7 +5603,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5331
5603
  );
5332
5604
  }
5333
5605
  current.openLifecycleBatches = current.openLifecycleBatches.filter(
5334
- batch => batch.invocations.length > 0,
5606
+ batch => batch.unownedStart || batch.invocations.length > 0 || batch.attachedInvocations.length > 0,
5335
5607
  );
5336
5608
  current.drainedInvocations = current.drainedInvocations?.filter(
5337
5609
  entry =>
@@ -5342,7 +5614,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5342
5614
  current.activeInvocation?.correlation.commandId === invocation.correlation.commandId &&
5343
5615
  current.activeInvocation.correlation.turnId === invocation.correlation.turnId
5344
5616
  )
5345
- adoptLifecycleBatch(current.openLifecycleBatches[0]?.invocations);
5617
+ adoptLifecycleBatch(current.openLifecycleBatches[0]);
5346
5618
  if (current.openLifecycleBatches.length === 0) current.lifecycleActive = false;
5347
5619
  } catch (error) {
5348
5620
  if (controller.signal.aborted) {
@@ -5714,6 +5986,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5714
5986
  }
5715
5987
  if (type === "agent_end" && isContinuingMidRunMaintenanceOutcome(maintenanceOutcome)) return;
5716
5988
  if (type === "agent_end") {
5989
+ for (const invocation of transitions)
5990
+ retireAcceptedQueueCancellation(acceptedQueueCancellations, invocation.correlation);
5717
5991
  if (current.lifecycleEpoch !== eventLifecycleEpoch) {
5718
5992
  // A successor agent_start won the lifecycle race while this event's
5719
5993
  // durable transitions were awaiting persistence. Retire the ended
@@ -5763,7 +6037,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5763
6037
  !current.deadlineManager.shouldDeferTerminalTransition(invocation.correlation)
5764
6038
  )
5765
6039
  current.deadlineManager.clear(invocation.correlation);
5766
- adoptLifecycleBatch(current.openLifecycleBatches[0]?.invocations);
6040
+ adoptLifecycleBatch(current.openLifecycleBatches[0]);
5767
6041
  if (current.openLifecycleBatches.length === 0) current.lifecycleActive = false;
5768
6042
  // Resolve EVERY concurrent waiter for the aborted turn: the turn emits
5769
6043
  // exactly one agent_end, and each admitted abort of it must observe the
@@ -5797,6 +6071,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5797
6071
  undefined,
5798
6072
  typeof event.sdkRunToken === "string" ? event.sdkRunToken : undefined,
5799
6073
  event.sdkRunTokens,
6074
+ event.lifecycleScope,
5800
6075
  ),
5801
6076
  owner,
5802
6077
  ).catch(error => {
@@ -5965,7 +6240,10 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5965
6240
  api.on("turn_start", async (_event, ctx) => {
5966
6241
  const current = lifecycleStateForContext(ctx, "agent_start");
5967
6242
  if (!current) return;
5968
- await current.registerBroker();
6243
+ // Optional broker discovery must not hold an interactive extension event open.
6244
+ // registerBroker owns single-flight recovery and optional-failure diagnostics.
6245
+ const registration = current.registerBroker();
6246
+ if (options.brokerRegistrationRequired) await registration;
5969
6247
  current.runtime.emitEvent({ type: "turn_start", sessionId: ctx.sessionManager.getSessionId() });
5970
6248
  });
5971
6249
  const consumedMasterNonces = new Map<string, number>();
@@ -6124,6 +6402,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6124
6402
  return code !== undefined && /^[A-Za-z0-9_.-]{1,64}$/u.test(code) ? code : "unavailable";
6125
6403
  };
6126
6404
  const startRuntime = async (ctx: ExtensionContext): Promise<void> => {
6405
+ if (active) return;
6406
+ if (acceptedQueueCancellations.size > 0) await disposeAcceptedQueueCancellations();
6127
6407
  if (active) return;
6128
6408
  const sessionId = ctx.sessionManager.getSessionId();
6129
6409
  const stateRoot = path.join(ctx.cwd, ".gjc", "state");
@@ -6364,6 +6644,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6364
6644
  });
6365
6645
  },
6366
6646
  onExpired: correlation => {
6647
+ retireAcceptedQueueCancellation(acceptedQueueCancellations, correlation, true);
6367
6648
  const owner = lifecycleOwnerHolder.state;
6368
6649
  if (!owner) return;
6369
6650
  removeLifecycleReferences(owner, correlation);
@@ -6400,26 +6681,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6400
6681
  true,
6401
6682
  );
6402
6683
  }
6403
- const openLifecycleBatches: Array<{
6404
- epoch: number;
6405
- invocations: Array<{
6406
- kind: InvocationKind;
6407
- correlation: InvocationCorrelation;
6408
- connectionId: string | undefined;
6409
- sdkRunToken: string;
6410
- }>;
6411
- attachedInvocations: Array<{
6412
- kind: InvocationKind;
6413
- correlation: InvocationCorrelation;
6414
- connectionId: string | undefined;
6415
- }>;
6416
- startPublished: boolean;
6417
- heldContentDropped: boolean;
6418
- heldContent: Array<{
6419
- event: AgentSessionEvent;
6420
- recipients: Array<{ correlation: InvocationCorrelation; connectionId: string | undefined }>;
6421
- }>;
6422
- }> = [];
6684
+ const openLifecycleBatches: LifecycleBatch[] = [];
6423
6685
  const configRevision = { current: 0 };
6424
6686
  let acceptingGateResolutions = true;
6425
6687
  const inFlightGateResolutions = new Set<Promise<unknown>>();
@@ -6521,9 +6783,9 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6521
6783
  kind: InvocationKind,
6522
6784
  correlation: InvocationCorrelation,
6523
6785
  error: { code: string; message: string },
6524
- ): void => {
6786
+ ): Promise<boolean> => {
6525
6787
  if (lifecycleOwnerHolder.state) removeLifecycleTokenAliases(lifecycleOwnerHolder.state, correlation);
6526
- const attempt = async (remaining: number): Promise<void> => {
6788
+ const attempt = async (remaining: number): Promise<boolean> => {
6527
6789
  try {
6528
6790
  // 1. Record the failure reason durably FIRST. If this write fails,
6529
6791
  // never fall through to agent_end: the record still has no error,
@@ -6546,7 +6808,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6546
6808
  turnId: correlation.turnId,
6547
6809
  error: sanitizePromptFailure(reasonError),
6548
6810
  });
6549
- return;
6811
+ return false;
6550
6812
  }
6551
6813
  await Bun.sleep(1_000);
6552
6814
  return attempt(remaining - 1);
@@ -6570,6 +6832,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6570
6832
  });
6571
6833
  // 3. Release recovery ownership ONLY after durable terminalization.
6572
6834
  deadlineManager.clear(correlation);
6835
+ if (kind === "prompt") retireAcceptedQueueCancellation(acceptedQueueCancellations, correlation);
6836
+ return true;
6573
6837
  } catch (transitionError) {
6574
6838
  // Keep prompt recovery leased; skill recovery has no prompt lease.
6575
6839
  if (kind === "skill") scheduleSkillRecovery(correlation);
@@ -6580,9 +6844,10 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6580
6844
  turnId: correlation.turnId,
6581
6845
  error: sanitizePromptFailure(transitionError),
6582
6846
  });
6847
+ return false;
6583
6848
  }
6584
6849
  };
6585
- void attempt(3);
6850
+ return attempt(3);
6586
6851
  };
6587
6852
 
6588
6853
  const controlSurface = createControlSurface(
@@ -6632,11 +6897,10 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6632
6897
  });
6633
6898
  };
6634
6899
  if (lifecycleOwnerHolder.quiescing) {
6635
- terminalizeAbandonedSubmission(kind, correlation, {
6900
+ return terminalizeAbandonedSubmission(kind, correlation, {
6636
6901
  code: "session_quiescing",
6637
6902
  message: "Session endpoint was replaced before the invocation started.",
6638
6903
  });
6639
- return;
6640
6904
  }
6641
6905
  // Lease at the ACTUAL promotion boundary (#4668 review): a promoted
6642
6906
  // submission that wedges before its run's agent_start must still
@@ -6656,11 +6920,10 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6656
6920
  entry.correlation.turnId === correlation.turnId,
6657
6921
  );
6658
6922
  if (pendingIdx >= 0) pending.splice(pendingIdx, 1);
6659
- terminalizeAbandonedSubmission(kind, correlation, {
6923
+ return terminalizeAbandonedSubmission(kind, correlation, {
6660
6924
  code: "cancelled",
6661
6925
  message: "Queued prompt was removed before consumption.",
6662
6926
  });
6663
- return;
6664
6927
  }
6665
6928
  if (promotion?.startsOwnRun !== false) {
6666
6929
  // A submission PROMOTED to its own run (finished prompt unwinding)
@@ -6706,7 +6969,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6706
6969
  entry.correlation.turnId === current.activeInvocation?.correlation.turnId,
6707
6970
  ),
6708
6971
  )
6709
- : undefined;
6972
+ : current.openLifecycleBatches.at(-1);
6710
6973
  const alreadyAttached = current.drainedInvocations.some(
6711
6974
  entry =>
6712
6975
  entry.correlation.commandId === correlation.commandId &&
@@ -6813,6 +7076,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6813
7076
  trackGateResolution,
6814
7077
  options.onInvocationCompletionReconciledForTests,
6815
7078
  frame => runtime.emitEvent(frame),
7079
+ acceptedQueueCancellations,
6816
7080
  );
6817
7081
  const installProviderDefinitions = (capability: string, definitions: unknown): void => {
6818
7082
  if (capability === "permission") {
@@ -7074,7 +7338,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
7074
7338
  };
7075
7339
  const startupImageCapture = captureStartupRuntimeImage();
7076
7340
  const registerBroker = async (): Promise<void> => {
7077
- if (brokerRegistered) return;
7341
+ if (brokerRecoveryStopped || brokerRegistered) return;
7078
7342
  if (brokerRegistrationInFlight !== undefined) {
7079
7343
  await brokerRegistrationInFlight;
7080
7344
  return;
@@ -7107,6 +7371,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
7107
7371
  throw new Error("SDK transport endpoint was not published before broker registration.");
7108
7372
  const endpointPath = path.join(input.stateRoot, "sdk", `${input.sessionId}.json`);
7109
7373
  const file = await readEndpointFile(endpointPath);
7374
+ if (brokerRecoveryStopped) return;
7110
7375
  if (!file) throw new Error("SDK endpoint could not be read as a stable regular file.");
7111
7376
  const endpoint = JSON.parse(file.source) as Record<string, unknown>;
7112
7377
  if (
@@ -7310,13 +7575,32 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
7310
7575
  active = runtimeOwner;
7311
7576
  try {
7312
7577
  publishedEndpointUrl = (await runtime.start()).url;
7313
- await registerBroker();
7578
+ // Required lifecycle hosts remain fail-closed; optional hosts publish locally
7579
+ // and recover broker availability outside the extension watchdog.
7580
+ const registration = registerBroker();
7581
+ if (options.brokerRegistrationRequired) await registration;
7314
7582
  // Await the startup runtime image capture before arming recovery.
7315
7583
  // This ensures replacement detection works correctly on the first recovery check.
7316
7584
  await startupImageCapture;
7317
- if (!brokerRecoveryStopped) startBrokerRecovery();
7585
+ // Recovery arms only after the startup attempt settles, so no tick overlaps it, and
7586
+ // a failed startup attempt holds the backoff like a failed recovery registration.
7587
+ const armBrokerRecovery = (): void => {
7588
+ if (brokerRecoveryStopped) return;
7589
+ if (!brokerRegistered) brokerRecoveryBackoff.recordFailure(options.agentDir);
7590
+ startBrokerRecovery();
7591
+ };
7592
+ if (options.brokerRegistrationRequired) armBrokerRecovery();
7593
+ else void registration.then(armBrokerRecovery);
7318
7594
  } catch (error) {
7595
+ runtimeOwner.quiesceInput();
7596
+ runtimeOwner.fenceGateResolutions();
7319
7597
  active = undefined;
7598
+ let queueCancellationFailure: unknown;
7599
+ try {
7600
+ await disposeAcceptedQueueCancellations();
7601
+ } catch (cleanupError) {
7602
+ queueCancellationFailure = cleanupError;
7603
+ }
7320
7604
  stopBrokerRecovery();
7321
7605
  disposeGate?.();
7322
7606
  try {
@@ -7356,16 +7640,40 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
7356
7640
  };
7357
7641
  lifecycleOwnerHolder.state = failedRuntimeOwner;
7358
7642
  active = failedRuntimeOwner;
7359
- throw new AggregateError([error, cleanupError], "SDK runtime startup failed and cleanup failed.");
7643
+ const failures = [
7644
+ error,
7645
+ ...(queueCancellationFailure === undefined ? [] : [queueCancellationFailure]),
7646
+ cleanupError,
7647
+ ];
7648
+ const startupFailure = new AggregateError(failures, "SDK runtime startup failed and cleanup failed.");
7649
+ if (queueCancellationFailure !== undefined)
7650
+ Object.assign(startupFailure, { code: "sdk_reconciliation_teardown_failed" });
7651
+ throw startupFailure;
7360
7652
  }
7361
7653
  cursors.close();
7362
7654
  await revisions.close().catch(() => undefined);
7655
+ if (queueCancellationFailure !== undefined)
7656
+ throw Object.assign(
7657
+ new AggregateError(
7658
+ [error, queueCancellationFailure],
7659
+ "SDK runtime startup failed with queued cancellation publication errors.",
7660
+ ),
7661
+ { code: "sdk_reconciliation_teardown_failed" },
7662
+ );
7363
7663
  throw error;
7364
7664
  }
7365
7665
  };
7366
7666
  const stopActive = async (cancelSkillRecovery = false, unregisterReason?: "detached_idle"): Promise<void> => {
7367
7667
  const current = active;
7368
7668
  if (!current) return;
7669
+ current.quiesceInput();
7670
+ current.fenceGateResolutions();
7671
+ let queueCancellationFailure: unknown;
7672
+ try {
7673
+ await disposeAcceptedQueueCancellations();
7674
+ } catch (cleanupError) {
7675
+ queueCancellationFailure = cleanupError;
7676
+ }
7369
7677
  if (cancelSkillRecovery) {
7370
7678
  for (const controller of skillRecoveryControllers.values()) controller.abort();
7371
7679
  for (const controller of skillTerminalRecoveryControllers.values()) controller.abort();
@@ -7373,8 +7681,6 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
7373
7681
  activePromptOwnerHolder.connectionIds = undefined;
7374
7682
  activePromptOwnerHolder.lifecycleEpoch = undefined;
7375
7683
  current.stopBrokerRecovery();
7376
- current.quiesceInput();
7377
- current.fenceGateResolutions();
7378
7684
  try {
7379
7685
  await current.waitForGateResolutionQuiescence();
7380
7686
  if (current.lifecycleTasks.size > 0) {
@@ -7395,7 +7701,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
7395
7701
  current.openLifecycleBatches.length > 0 ||
7396
7702
  (current.attachedInvocations?.length ?? 0) > 0 ||
7397
7703
  (current.drainedInvocations?.length ?? 0) > 0 ||
7398
- current.lifecycleTasks.size > 0;
7704
+ current.lifecycleTasks.size > 0 ||
7705
+ hasQueuedTerminalRecovery(current);
7399
7706
  if (retainsLifecycleWork) {
7400
7707
  const owners = retiredLifecycleOwners.get(current.sessionId) ?? [];
7401
7708
  if (!owners.includes(current)) owners.push(current);
@@ -7407,7 +7714,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
7407
7714
  current.openLifecycleBatches.length > 0 ||
7408
7715
  (current.attachedInvocations?.length ?? 0) > 0 ||
7409
7716
  (current.drainedInvocations?.length ?? 0) > 0 ||
7410
- current.lifecycleTasks.size > 0
7717
+ current.lifecycleTasks.size > 0 ||
7718
+ hasQueuedTerminalRecovery(current)
7411
7719
  ) {
7412
7720
  const retry = setTimeout(retryCleanup, LIFECYCLE_QUIESCENCE_MS);
7413
7721
  retry.unref();
@@ -7431,10 +7739,19 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
7431
7739
  // shutdown from retrying the failed endpoint removal.
7432
7740
  if (active === undefined) active = current;
7433
7741
  logger.error("sdk runtime stop failed", { code: errorCode(error), error: String(error) });
7742
+ if (queueCancellationFailure !== undefined)
7743
+ throw Object.assign(
7744
+ new AggregateError(
7745
+ [queueCancellationFailure, error],
7746
+ "SDK runtime stop and queued cancellation publication both failed.",
7747
+ ),
7748
+ { code: "sdk_reconciliation_teardown_failed" },
7749
+ );
7434
7750
  throw error;
7435
7751
  }
7436
7752
  current.cursors.close();
7437
7753
  await current.revisions.close();
7754
+ if (queueCancellationFailure !== undefined) throw queueCancellationFailure;
7438
7755
  };
7439
7756
  api.on("session_start", async (_event, ctx) => {
7440
7757
  await startRuntime(ctx);