@gajae-code/coding-agent 0.18.7 → 0.18.9

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 (344) hide show
  1. package/CHANGELOG.md +377 -1
  2. package/dist/types/async/endpoint-id.d.ts +16 -0
  3. package/dist/types/async/job-manager.d.ts +1 -1
  4. package/dist/types/async/support.d.ts +0 -14
  5. package/dist/types/capability/mcp.d.ts +2 -0
  6. package/dist/types/capability/skill.d.ts +2 -0
  7. package/dist/types/capability/types.d.ts +2 -0
  8. package/dist/types/cli/public-command-errors.d.ts +3 -1
  9. package/dist/types/cli/setup-cli.d.ts +1 -1
  10. package/dist/types/commands/deep-interview.d.ts +3 -0
  11. package/dist/types/commands/mcp-serve.d.ts +4 -0
  12. package/dist/types/config/autorouting-tier-map.d.ts +348 -9
  13. package/dist/types/config/file-lock.d.ts +1 -0
  14. package/dist/types/config/model-bindings-applier.d.ts +2 -0
  15. package/dist/types/config/model-profile-activation.d.ts +11 -3
  16. package/dist/types/config/model-registry.d.ts +4 -9
  17. package/dist/types/config/skill-settings-defaults.d.ts +2 -0
  18. package/dist/types/cursor-delete-path.d.ts +1 -0
  19. package/dist/types/cursor-mcp-name.d.ts +8 -0
  20. package/dist/types/customization/inventory.d.ts +2 -0
  21. package/dist/types/debug/report-bundle-parent.d.ts +2 -0
  22. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
  23. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
  24. package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
  25. package/dist/types/discovery/helpers.d.ts +6 -1
  26. package/dist/types/discovery/mcp-json.d.ts +1 -0
  27. package/dist/types/eval/py/kernel.d.ts +18 -0
  28. package/dist/types/export/html/css-color.d.ts +2 -0
  29. package/dist/types/export/html/html-export-destination.d.ts +2 -0
  30. package/dist/types/export/html/template.generated.d.ts +1 -1
  31. package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +5 -0
  32. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  33. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
  34. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
  35. package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
  36. package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
  37. package/dist/types/extensibility/skill-management.d.ts +4 -1
  38. package/dist/types/extensibility/skills.d.ts +2 -0
  39. package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
  40. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
  41. package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
  42. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -3
  43. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
  44. package/dist/types/gjc-runtime/sidecar-bootstrap-env.d.ts +6 -0
  45. package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
  46. package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
  47. package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
  48. package/dist/types/gjc-runtime/state-writer.d.ts +14 -1
  49. package/dist/types/gjc-runtime/tmux-common.d.ts +8 -5
  50. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
  51. package/dist/types/internal-urls/skill-path.d.ts +1 -0
  52. package/dist/types/lsp/config.d.ts +8 -5
  53. package/dist/types/lsp/workspace-path.d.ts +1 -0
  54. package/dist/types/modes/acp/acp-agent.d.ts +7 -0
  55. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  56. package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
  57. package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
  58. package/dist/types/modes/controllers/input-controller.d.ts +9 -0
  59. package/dist/types/modes/interactive-mode.d.ts +1 -0
  60. package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
  61. package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
  62. package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
  63. package/dist/types/progress/collect-project-progress.d.ts +38 -0
  64. package/dist/types/progress/progress-contract.d.ts +154 -0
  65. package/dist/types/progress/progress-snapshot.d.ts +11 -0
  66. package/dist/types/progress/project-progress.d.ts +141 -0
  67. package/dist/types/progress/render-progress.d.ts +28 -0
  68. package/dist/types/runtime-mcp/manager.d.ts +2 -1
  69. package/dist/types/runtime-mcp/mcp-redirect.d.ts +9 -0
  70. package/dist/types/runtime-mcp/oauth-public-url.d.ts +3 -0
  71. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
  72. package/dist/types/runtime-mcp/project-host-values.d.ts +27 -0
  73. package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
  74. package/dist/types/runtime-mcp/types.d.ts +6 -0
  75. package/dist/types/sdk/acp/adapter.d.ts +1 -1
  76. package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
  77. package/dist/types/sdk/broker/broker.d.ts +24 -2
  78. package/dist/types/sdk/broker/doctor-restart.d.ts +5 -0
  79. package/dist/types/sdk/broker/ensure.d.ts +14 -1
  80. package/dist/types/sdk/broker/session-index.d.ts +12 -6
  81. package/dist/types/sdk/bus/index.d.ts +12 -1
  82. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
  83. package/dist/types/sdk/client/client.d.ts +2 -0
  84. package/dist/types/sdk/host/query/handlers.d.ts +3 -0
  85. package/dist/types/sdk/index.d.ts +1 -0
  86. package/dist/types/sdk/router/session-router.d.ts +2 -0
  87. package/dist/types/sdk/session-list.d.ts +1 -1
  88. package/dist/types/sdk/session.d.ts +8 -0
  89. package/dist/types/session/agent-session.d.ts +38 -9
  90. package/dist/types/session/artifacts.d.ts +2 -2
  91. package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
  92. package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
  93. package/dist/types/session/internal/managed-session-storage.d.ts +26 -1
  94. package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
  95. package/dist/types/session/internal/task-artifact-owner-access.d.ts +2 -0
  96. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +9 -1
  97. package/dist/types/session/session-manager.d.ts +31 -5
  98. package/dist/types/session/session-retirement.d.ts +31 -7
  99. package/dist/types/session/session-storage.d.ts +69 -2
  100. package/dist/types/session/task-artifact-owner-codec.d.ts +2 -0
  101. package/dist/types/setup/credential-auto-import.d.ts +1 -1
  102. package/dist/types/setup/credential-import.d.ts +6 -4
  103. package/dist/types/skill-state/active-state.d.ts +38 -0
  104. package/dist/types/skill-state/workflow-mutation-guard.d.ts +3 -0
  105. package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
  106. package/dist/types/task/executor.d.ts +3 -0
  107. package/dist/types/task/isolation-dir.d.ts +26 -0
  108. package/dist/types/tools/ast-edit-write-path.d.ts +13 -0
  109. package/dist/types/tools/bash-skill-urls.d.ts +1 -1
  110. package/dist/types/tools/browser/cdp-url.d.ts +2 -0
  111. package/dist/types/tools/github-side-effect.d.ts +22 -0
  112. package/dist/types/tools/index.d.ts +3 -0
  113. package/dist/types/tools/monitor-mutation.d.ts +9 -0
  114. package/dist/types/tools/python.d.ts +9 -1
  115. package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
  116. package/dist/types/tools/restricted-cwd.d.ts +1 -0
  117. package/dist/types/tools/todo-write.d.ts +6 -0
  118. package/dist/types/utils/tools-checksum.d.ts +7 -0
  119. package/package.json +7 -7
  120. package/scripts/generate-sdk-operation-inventory.ts +21 -0
  121. package/scripts/verify-managed-empty-stop.ts +28 -0
  122. package/src/async/endpoint-id.ts +26 -0
  123. package/src/async/job-manager.ts +247 -132
  124. package/src/async/support.ts +0 -25
  125. package/src/capability/index.ts +15 -0
  126. package/src/capability/mcp.ts +2 -0
  127. package/src/capability/skill.ts +2 -0
  128. package/src/capability/types.ts +2 -0
  129. package/src/cli/auth-broker-cli.ts +1 -1
  130. package/src/cli/customize-doctor.ts +15 -2
  131. package/src/cli/doctor/repairs.ts +4 -1
  132. package/src/cli/doctor-supervisor.ts +3 -0
  133. package/src/cli/public-command-errors.ts +21 -1
  134. package/src/cli/public-command-registry.ts +28 -6
  135. package/src/cli/setup-cli.ts +4 -4
  136. package/src/cli/skills-cli.ts +5 -0
  137. package/src/commands/deep-interview.ts +47 -8
  138. package/src/commands/mcp-serve.ts +7 -0
  139. package/src/commands/sdk.ts +455 -397
  140. package/src/config/autorouting-tier-map.ts +117 -7
  141. package/src/config/file-lock.ts +316 -79
  142. package/src/config/mcp-schema.json +4 -0
  143. package/src/config/model-bindings-applier.ts +20 -0
  144. package/src/config/model-profile-activation.ts +240 -46
  145. package/src/config/model-registry.ts +47 -153
  146. package/src/config/skill-settings-defaults.ts +7 -0
  147. package/src/coordinator-mcp/policy.ts +3 -2
  148. package/src/coordinator-mcp/server.ts +2 -0
  149. package/src/cursor-delete-path.ts +10 -0
  150. package/src/cursor-mcp-name.ts +15 -0
  151. package/src/cursor.ts +13 -2
  152. package/src/customization/inventory.ts +4 -13
  153. package/src/dap/config.ts +2 -2
  154. package/src/debug/report-bundle-parent.ts +22 -0
  155. package/src/debug/report-bundle.ts +3 -0
  156. package/src/debug/system-info.ts +13 -1
  157. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +10 -6
  158. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
  159. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
  160. package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
  161. package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
  162. package/src/defaults/gjc-defaults.test.ts +18 -1
  163. package/src/defaults/gjc-defaults.ts +3 -0
  164. package/src/defaults/gjc-skills.generated.ts +58 -36
  165. package/src/discovery/agents.ts +6 -1
  166. package/src/discovery/builtin.ts +13 -1
  167. package/src/discovery/helpers.ts +171 -29
  168. package/src/discovery/mcp-json.ts +26 -12
  169. package/src/discovery/ssh.ts +1 -1
  170. package/src/discovery/substitute-plugin-root.ts +3 -1
  171. package/src/edit/file-read-cache.ts +1 -1
  172. package/src/eval/py/executor.ts +796 -154
  173. package/src/eval/py/kernel.ts +79 -13
  174. package/src/eval/py/tool-bridge.ts +4 -0
  175. package/src/export/html/css-color.ts +9 -0
  176. package/src/export/html/html-export-destination.ts +16 -0
  177. package/src/export/html/index.ts +7 -10
  178. package/src/export/html/template.generated.ts +1 -1
  179. package/src/extensibility/extensions/function-hooks-internal.ts +27 -0
  180. package/src/extensibility/extensions/runner.ts +15 -1
  181. package/src/extensibility/extensions/types.ts +7 -0
  182. package/src/extensibility/gjc-plugins/mcp-policy.ts +2 -1
  183. package/src/extensibility/gjc-plugins/runtime-adapters.ts +85 -8
  184. package/src/extensibility/plugins/manager.ts +3 -2
  185. package/src/extensibility/plugins/marketplace/cache.ts +15 -0
  186. package/src/extensibility/plugins/marketplace/manager.ts +11 -3
  187. package/src/extensibility/plugins/plugin-link-path.ts +12 -0
  188. package/src/extensibility/runtime-skill-discovery.ts +20 -2
  189. package/src/extensibility/skill-management.ts +9 -2
  190. package/src/extensibility/skills.ts +6 -1
  191. package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
  192. package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
  193. package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
  194. package/src/gjc-runtime/deep-interview-stage.ts +98 -21
  195. package/src/gjc-runtime/gc-runtime.ts +206 -9
  196. package/src/gjc-runtime/launch-worktree.ts +70 -9
  197. package/src/gjc-runtime/psmux-detect.ts +28 -4
  198. package/src/gjc-runtime/ralplan-runtime.ts +447 -52
  199. package/src/gjc-runtime/session-state-sidecar.ts +3 -2
  200. package/src/gjc-runtime/sidecar-bootstrap-env.ts +19 -0
  201. package/src/gjc-runtime/state-argv.ts +2 -0
  202. package/src/gjc-runtime/state-migrations.ts +91 -29
  203. package/src/gjc-runtime/state-runtime.ts +3716 -135
  204. package/src/gjc-runtime/state-writer.ts +105 -16
  205. package/src/gjc-runtime/tmux-common.ts +8 -5
  206. package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
  207. package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
  208. package/src/gjc-runtime/workflow-manifest.ts +54 -21
  209. package/src/harness-control-plane/operate.ts +12 -4
  210. package/src/harness-control-plane/owner.ts +5 -1
  211. package/src/hashline/parser.ts +48 -9
  212. package/src/hashline/recovery.ts +13 -4
  213. package/src/internal-urls/docs-index.generated.ts +20 -19
  214. package/src/internal-urls/skill-path.ts +19 -0
  215. package/src/internal-urls/skill-protocol.ts +2 -0
  216. package/src/lsp/config.ts +12 -63
  217. package/src/lsp/edits.ts +6 -0
  218. package/src/lsp/workspace-path.ts +51 -0
  219. package/src/main.ts +195 -31
  220. package/src/memories/index.ts +88 -7
  221. package/src/modes/acp/acp-agent.ts +88 -4
  222. package/src/modes/components/custom-editor.ts +14 -0
  223. package/src/modes/components/customization/customization-dashboard.ts +12 -0
  224. package/src/modes/components/extensions/extension-dashboard.ts +2 -2
  225. package/src/modes/components/extensions/state-manager.ts +15 -5
  226. package/src/modes/components/hook-editor.ts +1 -1
  227. package/src/modes/components/hook-selector.ts +1 -1
  228. package/src/modes/components/model-selector.ts +1 -4
  229. package/src/modes/components/provider-onboarding-selector.ts +1 -1
  230. package/src/modes/components/status-line/segments.ts +4 -0
  231. package/src/modes/controllers/event-controller.ts +11 -4
  232. package/src/modes/controllers/input-controller.ts +76 -31
  233. package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -16
  234. package/src/modes/controllers/selector-controller.ts +24 -15
  235. package/src/modes/controllers/todo-command-controller.ts +1 -1
  236. package/src/modes/interactive-mode.ts +5 -1
  237. package/src/modes/runtime-init.ts +4 -0
  238. package/src/modes/utils/abort-message.ts +3 -1
  239. package/src/modes/utils/interrupt-ownership.ts +12 -0
  240. package/src/modes/utils/keybinding-matchers.ts +4 -0
  241. package/src/modes/utils/macos-option-forwarding.ts +80 -0
  242. package/src/progress/collect-project-progress.ts +153 -0
  243. package/src/progress/progress-contract.ts +172 -0
  244. package/src/progress/progress-snapshot.ts +140 -0
  245. package/src/progress/project-progress.ts +446 -0
  246. package/src/progress/render-progress.ts +209 -0
  247. package/src/prompts/system/master-mode.md +1 -0
  248. package/src/prompts/tools/skill-discovery.md +1 -1
  249. package/src/runtime-mcp/client.ts +4 -2
  250. package/src/runtime-mcp/config-writer.ts +10 -6
  251. package/src/runtime-mcp/config.ts +1 -0
  252. package/src/runtime-mcp/json-rpc.ts +2 -1
  253. package/src/runtime-mcp/manager.ts +29 -11
  254. package/src/runtime-mcp/mcp-redirect.ts +73 -0
  255. package/src/runtime-mcp/oauth-flow.ts +14 -2
  256. package/src/runtime-mcp/oauth-public-url.ts +14 -0
  257. package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
  258. package/src/runtime-mcp/pool.ts +6 -0
  259. package/src/runtime-mcp/project-host-values.ts +46 -0
  260. package/src/runtime-mcp/smithery-registry.ts +22 -3
  261. package/src/runtime-mcp/transports/http.ts +4 -3
  262. package/src/runtime-mcp/types.ts +6 -0
  263. package/src/sdk/acp/adapter.ts +95 -8
  264. package/src/sdk/broker/broker-exit.ts +52 -23
  265. package/src/sdk/broker/broker.ts +81 -6
  266. package/src/sdk/broker/doctor-restart.ts +4 -0
  267. package/src/sdk/broker/ensure.ts +31 -2
  268. package/src/sdk/broker/lifecycle.ts +711 -66
  269. package/src/sdk/broker/session-index.ts +191 -81
  270. package/src/sdk/bus/chat-daemon-cli.ts +38 -1
  271. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  272. package/src/sdk/bus/index.ts +238 -43
  273. package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
  274. package/src/sdk/cli/session-cli.ts +17 -3
  275. package/src/sdk/client/client.ts +28 -8
  276. package/src/sdk/host/query/handlers.ts +9 -1
  277. package/src/sdk/host/session-runtime.test.ts +2705 -303
  278. package/src/sdk/host/session-runtime.ts +490 -143
  279. package/src/sdk/host/surface-policy.ts +1 -0
  280. package/src/sdk/index.ts +8 -0
  281. package/src/sdk/lifecycle-session.ts +3 -1
  282. package/src/sdk/prompt-deadline-manager.ts +15 -0
  283. package/src/sdk/protocol/operation-inventory.generated.json +146 -0
  284. package/src/sdk/protocol/operation-registry.ts +9 -2
  285. package/src/sdk/router/session-router.ts +82 -18
  286. package/src/sdk/session-list.ts +1 -0
  287. package/src/sdk/session.ts +69 -45
  288. package/src/session/agent-session.ts +679 -238
  289. package/src/session/artifacts.ts +176 -71
  290. package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
  291. package/src/session/internal/managed-session-scope.ts +3779 -462
  292. package/src/session/internal/managed-session-storage.ts +501 -78
  293. package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
  294. package/src/session/internal/task-artifact-owner-access.ts +19 -3
  295. package/src/session/internal/task-artifact-owner-transcript.ts +547 -1
  296. package/src/session/session-manager.ts +951 -133
  297. package/src/session/session-retirement.ts +573 -14
  298. package/src/session/session-storage.ts +447 -15
  299. package/src/session/task-artifact-owner-codec.ts +71 -8
  300. package/src/session/task-artifact-owner-retirement.ts +21 -19
  301. package/src/setup/credential-auto-import.ts +12 -3
  302. package/src/setup/credential-import.ts +176 -5
  303. package/src/setup/paseo/announce.ts +6 -1
  304. package/src/skill-state/active-state.ts +145 -2
  305. package/src/skill-state/workflow-mutation-guard.ts +102 -10
  306. package/src/slash-commands/builtin-registry.ts +39 -9
  307. package/src/slash-commands/helpers/mcp.ts +8 -4
  308. package/src/slash-commands/helpers/progress-report.ts +23 -0
  309. package/src/stt/setup.ts +3 -5
  310. package/src/task/commands.ts +3 -1
  311. package/src/task/executor.ts +4 -0
  312. package/src/task/index.ts +6 -0
  313. package/src/task/isolation-dir.ts +143 -0
  314. package/src/task/worktree.ts +16 -7
  315. package/src/tools/ask.ts +205 -6
  316. package/src/tools/ast-edit-write-path.ts +68 -0
  317. package/src/tools/ast-edit.ts +10 -1
  318. package/src/tools/bash-allowed-prefixes.ts +15 -0
  319. package/src/tools/bash-skill-urls.ts +56 -3
  320. package/src/tools/bash.ts +48 -79
  321. package/src/tools/browser/cdp-url.ts +18 -0
  322. package/src/tools/browser.ts +8 -1
  323. package/src/tools/descriptors.ts +5 -1
  324. package/src/tools/eval.ts +56 -36
  325. package/src/tools/gh.ts +10 -0
  326. package/src/tools/github-side-effect.ts +112 -0
  327. package/src/tools/image-gen.ts +1 -1
  328. package/src/tools/index.ts +3 -0
  329. package/src/tools/monitor-mutation.ts +27 -0
  330. package/src/tools/monitor.ts +7 -0
  331. package/src/tools/python.ts +188 -66
  332. package/src/tools/recipe/index.ts +9 -0
  333. package/src/tools/recipe/recipe-guard.ts +19 -0
  334. package/src/tools/restricted-cwd.ts +10 -0
  335. package/src/tools/skill-discovery.ts +5 -0
  336. package/src/tools/skill.ts +28 -1
  337. package/src/tools/todo-write.ts +10 -1
  338. package/src/tools/tool-catalog.generated.ts +1 -1
  339. package/src/tools/write.ts +1 -0
  340. package/src/utils/external-editor.ts +3 -2
  341. package/src/utils/tools-checksum.ts +34 -0
  342. package/src/utils/tools-manager.ts +24 -6
  343. package/src/web/insane/url-guard.ts +2 -1
  344. 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";
@@ -34,6 +35,7 @@ import {
34
35
  validateSettingPatch,
35
36
  } from "../../config/settings";
36
37
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../../extensibility/extensions";
38
+ import { tagSdkLifecycleObserver } from "../../extensibility/extensions/function-hooks-internal";
37
39
  import type { AgentEndEvent } from "../../extensibility/shared-events";
38
40
  import { normalizeGoal } from "../../goals/state";
39
41
  import { toAgentWireEventPayload } from "../../modes/shared/agent-wire/event-envelope";
@@ -229,6 +231,8 @@ class SdkOnlyIdempotencyConflictError extends Error {
229
231
  * the outcome synchronously on its capture slot, while this runtime observes
230
232
  * the publication from the separate `agent_end` handler. */
231
233
  const SDK_ONLY_TERMINAL_PUBLICATION_WAIT_MS = 1_000;
234
+ /** Queue-owner cancellation is synchronous in AgentSession; this bound fails closed if its disposition is lost. */
235
+ const SDK_ONLY_QUEUE_CANCELLATION_WAIT_MS = 1_000;
232
236
  /** Bounded wait for in-flight workflow gate resolutions to settle during SDK
233
237
  * runtime shutdown before proceeding with cleanup. Unresolved resolutions
234
238
  * after this bound are abandoned — their durable broker state is the recovery
@@ -746,6 +750,49 @@ export interface InvocationCorrelation {
746
750
  turnId: string;
747
751
  }
748
752
 
753
+ type AcceptedQueueDisposition = "preflight" | "queued" | "consumed" | "promoted" | "removed" | "teardown";
754
+
755
+ interface AcceptedQueueCancellationResult {
756
+ removed: boolean;
757
+ consumed: boolean;
758
+ unconfirmed: boolean;
759
+ }
760
+
761
+ interface AcceptedQueueCancellation {
762
+ correlation: InvocationCorrelation;
763
+ connectionId: string | undefined;
764
+ controller: AbortController;
765
+ accepted: boolean;
766
+ queueCandidate: boolean;
767
+ disposition: AcceptedQueueDisposition;
768
+ dispositionPromise: Promise<AcceptedQueueDisposition>;
769
+ resolveDisposition: (disposition: AcceptedQueueDisposition) => void;
770
+ removalTerminalization?: Promise<boolean>;
771
+ removalFailure?: unknown;
772
+ }
773
+
774
+ function retireAcceptedQueueCancellation(
775
+ cancellations: Map<string, AcceptedQueueCancellation>,
776
+ correlation: InvocationCorrelation,
777
+ durableTerminalConfirmed = false,
778
+ ): void {
779
+ const key = `${correlation.commandId}:${correlation.turnId}`;
780
+ const cancellation = cancellations.get(key);
781
+ if (!cancellation) return;
782
+ if (!cancellation.controller.signal.aborted) cancellation.controller.abort();
783
+ if (!durableTerminalConfirmed && cancellation.disposition === "removed" && cancellation.removalTerminalization) {
784
+ void cancellation.removalTerminalization.then(published => {
785
+ if (published && cancellations.get(key) === cancellation) cancellations.delete(key);
786
+ });
787
+ return;
788
+ }
789
+ if (cancellations.get(key) === cancellation) cancellations.delete(key);
790
+ if (cancellation.disposition === "queued") {
791
+ cancellation.disposition = "teardown";
792
+ cancellation.resolveDisposition("teardown");
793
+ }
794
+ }
795
+
749
796
  export type InvocationKind = "prompt" | "skill" | "steer";
750
797
  type InvocationStatus = "accepted" | "in_flight" | "terminal_ok" | "failed" | "uncertain";
751
798
  type InvocationOutcome = SdkPromptTerminalOutcome;
@@ -2063,12 +2110,7 @@ function createQuerySurface(
2063
2110
  };
2064
2111
  const getProfileCredentialSessionId = () => ctx.credentialSessionId ?? id;
2065
2112
  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
- };
2113
+ const getProfileAvailableModels = (): Model<Api>[] => ctx.modelRegistry.getAvailable();
2072
2114
  const resolveProfileAvailability = async (
2073
2115
  profile: ModelProfileDefinition,
2074
2116
  authenticatedProviders: ReadonlySet<string>,
@@ -2422,6 +2464,9 @@ function createQuerySurface(
2422
2464
  getExtensions: () => ctx.getExtensions(),
2423
2465
  getArtifactRange: (artifactId, offset, length) => ctx.getArtifactRange?.(artifactId, offset, length),
2424
2466
  getJobs: () => ctx.getJobs(),
2467
+ ...(typeof (ctx as Partial<ExtensionContext>).getProjectProgress === "function"
2468
+ ? { getProjectProgress: () => ctx.getProjectProgress!() }
2469
+ : {}),
2425
2470
  getPromptStatus: (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
2426
2471
  reconciliation.lookup("prompt", selector),
2427
2472
  getSkillInvokeStatus: (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
@@ -2694,6 +2739,11 @@ export function providerFailureFromAgentEnd(
2694
2739
  return undefined;
2695
2740
  }
2696
2741
  if (errorKind === "local_snapshot_failure" || errorKind === "local_buffer_overflow") return undefined;
2742
+ if (errorKind === "local_empty_response")
2743
+ return {
2744
+ code: "empty_response",
2745
+ message: PROMPT_FAILURE_MESSAGE_SUBMISSION,
2746
+ };
2697
2747
  let errorMessage: unknown;
2698
2748
  try {
2699
2749
  errorMessage = assistant.errorMessage;
@@ -2874,7 +2924,7 @@ function createControlSurface(
2874
2924
  connectionId: string | undefined,
2875
2925
  sdkRunToken: string,
2876
2926
  promotion?: { startsOwnRun?: boolean; removed?: boolean },
2877
- ) => void,
2927
+ ) => undefined | Promise<boolean>,
2878
2928
  policy?: SdkSurfacePolicy,
2879
2929
  settings?: Settings,
2880
2930
  configOverrides?: Map<string, unknown>,
@@ -2895,6 +2945,7 @@ function createControlSurface(
2895
2945
  trackGateResolution: <T>(resolution: Promise<T>) => Promise<T> = async resolution => await resolution,
2896
2946
  onInvocationCompletionReconciledForTests?: (kind: InvocationKind, correlation: InvocationCorrelation) => void,
2897
2947
  publishLifecycleFrame?: (frame: SdkFrame) => void,
2948
+ acceptedQueueCancellations: Map<string, AcceptedQueueCancellation> = new Map(),
2898
2949
  ): ControlSurface {
2899
2950
  const normalizePromptImages = (value: unknown): ImageContent[] => {
2900
2951
  if (!Array.isArray(value)) return [];
@@ -2998,6 +3049,49 @@ function createControlSurface(
2998
3049
  }
2999
3050
  return pending;
3000
3051
  };
3052
+ const cancelAcceptedQueueSubmissions = async (
3053
+ connectionId: string | undefined,
3054
+ admittedRequests: readonly AcceptedQueueCancellation[] = [...acceptedQueueCancellations.values()],
3055
+ ): Promise<AcceptedQueueCancellationResult> => {
3056
+ const result: AcceptedQueueCancellationResult = { removed: false, consumed: false, unconfirmed: false };
3057
+ if (connectionId === undefined) return result;
3058
+ const owned = admittedRequests.filter(
3059
+ request => request.connectionId === connectionId && request.accepted && request.queueCandidate,
3060
+ );
3061
+ for (const request of owned) {
3062
+ let disposition = request.disposition;
3063
+ if (disposition === "queued") {
3064
+ request.controller.abort();
3065
+ disposition = request.disposition;
3066
+ if (disposition === "queued") {
3067
+ const timedOut = Promise.withResolvers<AcceptedQueueDisposition>();
3068
+ const timer = setTimeout(() => timedOut.resolve("teardown"), SDK_ONLY_QUEUE_CANCELLATION_WAIT_MS);
3069
+ timer.unref();
3070
+ try {
3071
+ disposition = await Promise.race([request.dispositionPromise, timedOut.promise]);
3072
+ } finally {
3073
+ clearTimeout(timer);
3074
+ }
3075
+ }
3076
+ }
3077
+ if (disposition === "removed") {
3078
+ if (await request.removalTerminalization) result.removed = true;
3079
+ else result.unconfirmed = true;
3080
+ } else if (disposition === "consumed" || disposition === "promoted") {
3081
+ result.consumed = true;
3082
+ } else if (disposition === "teardown") {
3083
+ result.unconfirmed = true;
3084
+ }
3085
+ }
3086
+ return result;
3087
+ };
3088
+ const requesterOwnsActiveRun = (connectionId: string | undefined): boolean => {
3089
+ if (connectionId === undefined) return false;
3090
+ const seamOwner = terminalAbortSeams?.getActivePromptOwnerConnectionId?.();
3091
+ return seamOwner === undefined
3092
+ ? (activePromptOwner.connectionIds?.has(connectionId) ?? false)
3093
+ : seamOwner === connectionId;
3094
+ };
3001
3095
  const normalizeClientRef = (clientRef: string | undefined): string | undefined => {
3002
3096
  if (clientRef === undefined) return undefined;
3003
3097
  const trimmed = clientRef.trim();
@@ -3012,6 +3106,7 @@ function createControlSurface(
3012
3106
  clientRef: string | undefined,
3013
3107
  run: (options: {
3014
3108
  sdkRunCapability: SdkRunCapability;
3109
+ preflightSignal: AbortSignal;
3015
3110
  onPreflightAccepted: () => void;
3016
3111
  onPreflightAcceptCommit: () => Promise<void>;
3017
3112
  /** Internal disposition before a queued submission is actually consumed. */
@@ -3034,6 +3129,7 @@ function createControlSurface(
3034
3129
  const sdkRunToken = `${correlation.commandId}:${correlation.turnId}`;
3035
3130
  const sdkRunCapability = createSdkRunCapability(sdkRunToken);
3036
3131
  const publishTerminal = (outcome: InvocationOutcome): void => {
3132
+ retireAcceptedQueueCancellation(acceptedQueueCancellations, correlation);
3037
3133
  publishLifecycleFrame?.({
3038
3134
  type: "agent_end",
3039
3135
  sessionId: ctx.sessionManager.getSessionId(),
@@ -3052,9 +3148,22 @@ function createControlSurface(
3052
3148
  publishTerminal(canonicalFailedOutcome(failure));
3053
3149
  };
3054
3150
  const preflight = Promise.withResolvers<void>();
3151
+ const preflightController = new AbortController();
3152
+ const queueDisposition = Promise.withResolvers<AcceptedQueueDisposition>();
3153
+ const queueCancellation: AcceptedQueueCancellation = {
3154
+ correlation,
3155
+ connectionId: requesterConnectionId,
3156
+ controller: preflightController,
3157
+ accepted: false,
3158
+ queueCandidate: false,
3159
+ disposition: "preflight",
3160
+ dispositionPromise: queueDisposition.promise,
3161
+ resolveDisposition: queueDisposition.resolve,
3162
+ };
3055
3163
  let accepted = false;
3056
3164
  let settled = false;
3057
3165
  const cancelPreflight = () => {
3166
+ preflightController.abort();
3058
3167
  if (settled) return;
3059
3168
  settled = true;
3060
3169
  preflight.reject(
@@ -3073,6 +3182,8 @@ function createControlSurface(
3073
3182
  try {
3074
3183
  await reconciliation.noteAccepted(kind, correlation, retainedClientRef);
3075
3184
  accepted = true;
3185
+ queueCancellation.accepted = true;
3186
+ if (queueCancellation.queueCandidate) queueCancellation.disposition = "queued";
3076
3187
  settled = true;
3077
3188
  if (kind === "prompt" && startsOwnTurn) armPromptDeadline(correlation);
3078
3189
  // The accepted submission does NOT own the active turn until its run
@@ -3105,16 +3216,23 @@ function createControlSurface(
3105
3216
  // Skills always start their own invocation; a plain prompt starts one
3106
3217
  // only when idle at dispatch time.
3107
3218
  const startsOwnTurn = kind === "skill" || (kind === "prompt" && !alwaysQueued && !queuedAtDispatch);
3219
+ queueCancellation.queueCandidate = queuedAtDispatch;
3220
+ if (queuedAtDispatch && queueCancellation.accepted) queueCancellation.disposition = "queued";
3221
+ if (kind === "prompt")
3222
+ acceptedQueueCancellations.set(`${correlation.commandId}:${correlation.turnId}`, queueCancellation);
3108
3223
  let promotionStartsOwnRun: boolean | undefined;
3109
3224
  try {
3110
3225
  const submission = Promise.resolve(
3111
3226
  run({
3112
3227
  onPreflightAccepted: () => void accept().catch(() => undefined),
3228
+ preflightSignal: preflightController.signal,
3113
3229
  sdkRunCapability,
3114
3230
  onPreflightAcceptCommit: accept,
3115
3231
  onDispatchDisposition: promotion => {
3116
3232
  promotionStartsOwnRun = promotion.startsOwnRun;
3117
3233
  if (promotion.startsOwnRun === false) {
3234
+ queueCancellation.queueCandidate = true;
3235
+ if (queueCancellation.accepted) queueCancellation.disposition = "queued";
3118
3236
  // Dispatch-race diversion (#4668 review P1): the idle snapshot leased
3119
3237
  // this prompt at acceptance, but it was actually diverted into the
3120
3238
  // in-flight run's steering queue. While it sits queued — legitimately
@@ -3131,7 +3249,37 @@ function createControlSurface(
3131
3249
  // terminal-abort that turn (review threads P1/P2).
3132
3250
  onQueuedPromoted: (promotion?: { startsOwnRun?: boolean; removed?: boolean }) => {
3133
3251
  promotionStartsOwnRun = promotion?.startsOwnRun;
3134
- onPromotedTurn?.(kind, correlation, requesterConnectionId, sdkRunToken, promotion);
3252
+ if (promotion?.removed) {
3253
+ queueCancellation.disposition = "removed";
3254
+ queueCancellation.removalTerminalization = (async () => {
3255
+ try {
3256
+ return (
3257
+ (await onPromotedTurn?.(
3258
+ kind,
3259
+ correlation,
3260
+ requesterConnectionId,
3261
+ sdkRunToken,
3262
+ promotion,
3263
+ )) === true
3264
+ );
3265
+ } catch (error) {
3266
+ queueCancellation.removalFailure = error;
3267
+ logger.error("SDK queued prompt terminal publication failed", {
3268
+ commandId: correlation.commandId,
3269
+ turnId: correlation.turnId,
3270
+ error: String(error),
3271
+ });
3272
+ return false;
3273
+ }
3274
+ })();
3275
+ queueCancellation.resolveDisposition("removed");
3276
+ return;
3277
+ }
3278
+ if (queueCancellation.queueCandidate) {
3279
+ queueCancellation.disposition = promotion?.startsOwnRun === false ? "consumed" : "promoted";
3280
+ queueCancellation.resolveDisposition(queueCancellation.disposition);
3281
+ }
3282
+ void onPromotedTurn?.(kind, correlation, requesterConnectionId, sdkRunToken, promotion);
3135
3283
  },
3136
3284
  queuedAtDispatch,
3137
3285
  }),
@@ -3218,6 +3366,7 @@ function createControlSurface(
3218
3366
  },
3219
3367
  error => {
3220
3368
  if (settled) {
3369
+ retireAcceptedQueueCancellation(acceptedQueueCancellations, correlation);
3221
3370
  // The submission promise rejects after preflight acceptance only when the
3222
3371
  // work itself is over (provider stream interrupt, abort, queue failure).
3223
3372
  // The accepted run never started (agent_start never fired), so its pending
@@ -3338,6 +3487,7 @@ function createControlSurface(
3338
3487
  ...(acceptedFields?.() ?? {}),
3339
3488
  };
3340
3489
  } catch (error) {
3490
+ retireAcceptedQueueCancellation(acceptedQueueCancellations, correlation);
3341
3491
  if (!accepted) reconciliation.release(kind, retainedClientRef);
3342
3492
  throw error;
3343
3493
  } finally {
@@ -3705,6 +3855,36 @@ function createControlSurface(
3705
3855
  return boundTerminalRetentionState(state.keys, mutate(state.scopes), terminalReservationLimit);
3706
3856
  });
3707
3857
  };
3858
+ const unconfirmedQueueTerminal = async (): Promise<unknown> => {
3859
+ const result = {
3860
+ ok: true,
3861
+ selection: scope,
3862
+ turn: "uncertain",
3863
+ ownedWork: scope === "turn" ? "left_running" : "uncertain",
3864
+ automaticDelivery: scope === "turn" ? "enabled" : "none",
3865
+ resumeOnOwnedCompletion: scope === "turn",
3866
+ reason: "queue_terminal_unconfirmed",
3867
+ };
3868
+ const payloadHash = hashResult(result);
3869
+ await transactBoundedTerminalScopes(scopes =>
3870
+ scopes.map(record =>
3871
+ (keyHash
3872
+ ? record.idempotencyKeyHash === keyHash
3873
+ : record.turnContinuationFence.abortedAttemptEpoch === epoch) &&
3874
+ record.turnDisposition === "no_effect_reserved"
3875
+ ? {
3876
+ ...record,
3877
+ turnDisposition: "uncertain",
3878
+ responsePayloadHash: payloadHash,
3879
+ replayPayloadHash: replayShapedHash(record, result, payloadHash),
3880
+ terminalAt: Date.now(),
3881
+ }
3882
+ : record,
3883
+ ),
3884
+ );
3885
+ return result;
3886
+ };
3887
+ const abortingConnectionId = sdkControlRequesterContext.getStore();
3708
3888
  let handle = terminalAbortSeams.getActivePromptHandle();
3709
3889
  let epoch = terminalAbortSeams.getTerminalTurnEpoch();
3710
3890
  // Set when the no-effect reservation found an existing SAME-input row or
@@ -3796,9 +3976,12 @@ function createControlSurface(
3796
3976
  const recheckedHandle = terminalAbortSeams.getActivePromptHandle();
3797
3977
  const recheckedEpoch = terminalAbortSeams.getTerminalTurnEpoch();
3798
3978
  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).
3979
+ // An accepted SDK queue owner is independent of the root prompt. Remove
3980
+ // only this requester's exact queued submissions, and do not claim the
3981
+ // root stopped; terminal success waits for each correlated removal
3982
+ // failure+terminal transaction to become durable.
3983
+ const queueCancellation = await cancelAcceptedQueueSubmissions(abortingConnectionId);
3984
+ if (queueCancellation.unconfirmed) return await unconfirmedQueueTerminal();
3802
3985
  return await returnNoActiveTurn();
3803
3986
  }
3804
3987
  handle = recheckedHandle;
@@ -3816,7 +3999,6 @@ function createControlSurface(
3816
3999
  // per-connection selection of the full bus path. The owner is re-read
3817
4000
  // through the seam when provided (deterministic tests) and otherwise
3818
4001
  // from the runtime-tracked accepting connection.
3819
- const abortingConnectionId = sdkControlRequesterContext.getStore();
3820
4002
  const currentOwnerConnectionIds = (): ReadonlySet<string> => {
3821
4003
  const seam = terminalAbortSeams.getActivePromptOwnerConnectionId?.();
3822
4004
  // The seam reports a single deterministic owner (test harnesses); the
@@ -3864,9 +4046,10 @@ function createControlSurface(
3864
4046
  abortingConnectionId === undefined ||
3865
4047
  !recheckedOwners.has(abortingConnectionId)
3866
4048
  ) {
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).
4049
+ // Request-owned steering cannot borrow the unrelated root's abort
4050
+ // authority. It may still be removed by its exact queue capability.
4051
+ const queueCancellation = await cancelAcceptedQueueSubmissions(abortingConnectionId);
4052
+ if (queueCancellation.unconfirmed) return await unconfirmedQueueTerminal();
3870
4053
  const noActiveTurnResult = {
3871
4054
  ok: true,
3872
4055
  selection: scope,
@@ -4289,6 +4472,26 @@ function createControlSurface(
4289
4472
  );
4290
4473
  },
4291
4474
  abort: async () => {
4475
+ const connectionId = sdkControlRequesterContext.getStore();
4476
+ const ownedAdmissions = [...acceptedQueueCancellations.values()].filter(
4477
+ request => connectionId !== undefined && request.connectionId === connectionId,
4478
+ );
4479
+ const ownedPreflights = connectionId === undefined ? [] : [...(pendingPreflights.get(connectionId) ?? [])];
4480
+ // Capture both phases before cancellation or any awaited terminal work:
4481
+ // acceptance can settle its callback before agent_start owns the run.
4482
+ for (const cancel of ownedPreflights) cancel();
4483
+ let cancelledAdmission = ownedPreflights.length > 0;
4484
+ for (const request of ownedAdmissions) {
4485
+ if (request.disposition !== "preflight" || request.controller.signal.aborted) continue;
4486
+ request.controller.abort();
4487
+ cancelledAdmission = true;
4488
+ }
4489
+ if (connectionId !== undefined && !requesterOwnsActiveRun(connectionId)) {
4490
+ const queueCancellation = await cancelAcceptedQueueSubmissions(connectionId, ownedAdmissions);
4491
+ if (queueCancellation.unconfirmed) return { aborted: false, reason: "queue_terminal_unconfirmed" };
4492
+ if (queueCancellation.removed || cancelledAdmission) return { aborted: true };
4493
+ return { aborted: false, turn: "no_active_turn" };
4494
+ }
4292
4495
  await Promise.resolve(ctx.abort()).catch(() => undefined);
4293
4496
  return { aborted: true };
4294
4497
  },
@@ -4628,6 +4831,61 @@ function quiescingFrame(frame: Record<string, unknown>): Record<string, unknown>
4628
4831
 
4629
4832
  /** Install a complete SDK host for a session when notifications are inactive. */
4630
4833
  export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: CreateSdkSessionRuntimeOptions): void {
4834
+ const acceptedQueueCancellations = new Map<string, AcceptedQueueCancellation>();
4835
+ const disposeAcceptedQueueCancellations = async (): Promise<void> => {
4836
+ const cancellations = [...acceptedQueueCancellations.values()];
4837
+ const queuedRemovals = cancellations.filter(
4838
+ cancellation =>
4839
+ cancellation.accepted &&
4840
+ cancellation.queueCandidate &&
4841
+ (cancellation.disposition === "queued" || cancellation.disposition === "removed"),
4842
+ );
4843
+ for (const cancellation of cancellations)
4844
+ if (!cancellation.controller.signal.aborted) cancellation.controller.abort();
4845
+
4846
+ const failures: unknown[] = [];
4847
+ const timedOut = Promise.withResolvers<void>();
4848
+ const timer = setTimeout(timedOut.resolve, SDK_ONLY_QUEUE_CANCELLATION_WAIT_MS);
4849
+ timer.unref();
4850
+ try {
4851
+ await Promise.all(
4852
+ queuedRemovals.map(async cancellation => {
4853
+ const publication = (async (): Promise<boolean> => {
4854
+ const disposition =
4855
+ cancellation.disposition === "queued"
4856
+ ? await cancellation.dispositionPromise
4857
+ : cancellation.disposition;
4858
+ if (disposition === "promoted" || disposition === "consumed") return true;
4859
+ return disposition === "removed" && (await cancellation.removalTerminalization) === true;
4860
+ })();
4861
+ const published = await Promise.race([publication, timedOut.promise.then(() => false)]);
4862
+ const key = `${cancellation.correlation.commandId}:${cancellation.correlation.turnId}`;
4863
+ if (published || acceptedQueueCancellations.get(key) !== cancellation) {
4864
+ if (acceptedQueueCancellations.get(key) === cancellation) acceptedQueueCancellations.delete(key);
4865
+ } else {
4866
+ failures.push(
4867
+ cancellation.removalFailure ??
4868
+ new Error(
4869
+ `Queued prompt ${cancellation.correlation.commandId}:${cancellation.correlation.turnId} terminal publication was not confirmed during shutdown.`,
4870
+ ),
4871
+ );
4872
+ }
4873
+ }),
4874
+ );
4875
+ } finally {
4876
+ clearTimeout(timer);
4877
+ }
4878
+ for (const cancellation of cancellations) {
4879
+ if (queuedRemovals.includes(cancellation)) continue;
4880
+ const key = `${cancellation.correlation.commandId}:${cancellation.correlation.turnId}`;
4881
+ if (acceptedQueueCancellations.get(key) === cancellation) acceptedQueueCancellations.delete(key);
4882
+ }
4883
+ if (failures.length > 0)
4884
+ throw Object.assign(
4885
+ new AggregateError(failures, "SDK runtime could not durably publish queued prompt cancellations."),
4886
+ { code: "sdk_reconciliation_teardown_failed" },
4887
+ );
4888
+ };
4631
4889
  let active:
4632
4890
  | {
4633
4891
  sessionId: string;
@@ -4658,6 +4916,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4658
4916
  }>;
4659
4917
  openLifecycleBatches: Array<{
4660
4918
  epoch: number;
4919
+ /** Session-initiated starts still reserve their own unmatched end. */
4920
+ unownedStart: boolean;
4661
4921
  invocations: Array<{
4662
4922
  kind: InvocationKind;
4663
4923
  correlation: InvocationCorrelation;
@@ -4713,6 +4973,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4713
4973
  string,
4714
4974
  { state: RuntimeState; batch?: LifecycleBatch; correlationKey?: string }
4715
4975
  >();
4976
+ const lifecycleScopeOwners = new WeakMap<AttemptScope, { state: RuntimeState; batch: LifecycleBatch }>();
4716
4977
  const lifecycleCorrelationKey = (correlation: InvocationCorrelation): string =>
4717
4978
  `${correlation.commandId}:${correlation.turnId}`;
4718
4979
  const sessionIdentityForContext = (ctx: ExtensionContext): string | undefined => {
@@ -4782,13 +5043,18 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4782
5043
  owner.unrecordedFailureReasons?.clear();
4783
5044
  for (const [token, binding] of lifecycleRunOwners) if (binding.state === owner) lifecycleRunOwners.delete(token);
4784
5045
  };
5046
+ const hasQueuedTerminalRecovery = (owner: RuntimeState): boolean =>
5047
+ [...acceptedQueueCancellations.values()].some(
5048
+ cancellation => cancellation.disposition === "removed" && owner.deadlineManager.has(cancellation.correlation),
5049
+ );
4785
5050
  const maybeRetireLifecycleOwner = (owner: RuntimeState): void => {
4786
5051
  if (
4787
5052
  owner.pending.length > 0 ||
4788
5053
  owner.openLifecycleBatches.length > 0 ||
4789
5054
  (owner.attachedInvocations?.length ?? 0) > 0 ||
4790
5055
  (owner.drainedInvocations?.length ?? 0) > 0 ||
4791
- owner.lifecycleTasks.size > 0
5056
+ owner.lifecycleTasks.size > 0 ||
5057
+ hasQueuedTerminalRecovery(owner)
4792
5058
  )
4793
5059
  return;
4794
5060
  removeRetiredLifecycleOwner(owner);
@@ -4809,8 +5075,10 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4809
5075
  entry => lifecycleCorrelationKey(entry.correlation) !== target,
4810
5076
  );
4811
5077
  }
5078
+ // Retiring an SDK correlation must not discard an unrelated session-
5079
+ // initiated start. Its delayed end still belongs to that empty batch.
4812
5080
  owner.openLifecycleBatches = owner.openLifecycleBatches.filter(
4813
- batch => batch.invocations.length > 0 || batch.attachedInvocations.length > 0,
5081
+ batch => batch.unownedStart || batch.invocations.length > 0 || batch.attachedInvocations.length > 0,
4814
5082
  );
4815
5083
  owner.drainedInvocations = owner.drainedInvocations?.filter(
4816
5084
  entry => lifecycleCorrelationKey(entry.correlation) !== target,
@@ -5089,32 +5357,24 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5089
5357
  stopReason?: AgentEndEvent["stopReason"],
5090
5358
  startToken?: string,
5091
5359
  startTokens?: string[],
5360
+ startLifecycleScope?: AttemptScope,
5092
5361
  ): Promise<void> => {
5093
5362
  const current = lifecycleOwner?.state ?? active;
5094
5363
  if (!current) return;
5095
5364
  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;
5365
+ const adoptLifecycleBatch = (batch: LifecycleBatch | undefined): void => {
5366
+ current.activeInvocation = batch?.invocations[0];
5367
+ current.drainedInvocations = batch
5368
+ ? [...batch.invocations, ...batch.attachedInvocations].map(({ kind, correlation }) => ({
5369
+ kind,
5370
+ correlation,
5371
+ }))
5372
+ : undefined;
5373
+ current.attachedInvocations = batch?.attachedInvocations.slice();
5115
5374
  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;
5375
+ for (const entry of batch?.invocations ?? [])
5376
+ if (entry.connectionId !== undefined) owners.add(entry.connectionId);
5377
+ if (current === active) activePromptOwnerHolder.connectionIds = owners.size > 0 ? owners : undefined;
5118
5378
  };
5119
5379
  let transitions: Array<{ kind: InvocationKind; correlation: InvocationCorrelation }> = [];
5120
5380
  if (type === "agent_failed") {
@@ -5148,13 +5408,18 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5148
5408
  );
5149
5409
  transitions = [...fallback, ...attached].map(({ kind, correlation }) => ({ kind, correlation }));
5150
5410
  } else if (type === "agent_start") {
5151
- current.lifecycleEpoch = ++nextLifecycleEpoch;
5411
+ const scopeOwner = startLifecycleScope ? lifecycleScopeOwners.get(startLifecycleScope) : undefined;
5412
+ const scopeBatch =
5413
+ scopeOwner?.state === current && current.openLifecycleBatches.includes(scopeOwner.batch)
5414
+ ? scopeOwner.batch
5415
+ : undefined;
5416
+ current.lifecycleEpoch = scopeBatch?.epoch ?? ++nextLifecycleEpoch;
5152
5417
  if (current === active) activePromptOwnerHolder.lifecycleEpoch = current.lifecycleEpoch;
5153
5418
  // Mark lifecycle active even when the drain is empty: a monitor/cron
5154
5419
  // run started by the session has no SDK pending entry but is still a
5155
5420
  // 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.
5421
+ // of falling back to pending (review P1). Only an explicitly bound
5422
+ // continuation retains a previous public lifecycle boundary.
5158
5423
  current.lifecycleActive = true;
5159
5424
  // Drain EVERY entry admitted for this run: a continuation may promote
5160
5425
  // several follow-ups (each with its own requester correlation) into one
@@ -5175,40 +5440,47 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5175
5440
  typeof startToken === "string" && startToken.length > 0
5176
5441
  ? lifecycleRunOwners.get(startToken)?.batch
5177
5442
  : undefined;
5178
- if (drained.length > 0) {
5179
- const batch: LifecycleBatch = {
5443
+ const continuationBatch =
5444
+ scopeBatch ??
5445
+ (drained.length === 0 && existingTokenBatch && current.openLifecycleBatches.includes(existingTokenBatch)
5446
+ ? existingTokenBatch
5447
+ : undefined);
5448
+ let batch: LifecycleBatch;
5449
+ if (!continuationBatch) {
5450
+ // Every new run owns an end, including interactive/monitor runs
5451
+ // without SDK invocations. Their delayed ends must never consume
5452
+ // the next SDK run's terminal receipt.
5453
+ batch = {
5180
5454
  epoch: current.lifecycleEpoch,
5455
+ unownedStart: drained.length === 0,
5181
5456
  invocations: drained,
5182
5457
  attachedInvocations: [],
5183
- startPublished: false,
5458
+ startPublished: drained.length === 0,
5184
5459
  heldContentDropped: false,
5185
5460
  heldContent: [],
5186
5461
  };
5187
5462
  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
5463
  } 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);
5464
+ // The producer consumed the predecessor end, or this is the same
5465
+ // token-owned run. Preserve its attached owners and single final end.
5466
+ batch = continuationBatch;
5467
+ batch.invocations.push(...drained);
5468
+ if (drained.length > 0) batch.startPublished = false;
5469
+ }
5470
+ if (startLifecycleScope) lifecycleScopeOwners.set(startLifecycleScope, { state: current, batch });
5471
+ for (const entry of drained) {
5472
+ lifecycleRunOwners.set(entry.sdkRunToken, {
5473
+ state: current,
5474
+ batch,
5475
+ correlationKey: lifecycleCorrelationKey(entry.correlation),
5476
+ });
5477
+ }
5478
+ adoptLifecycleBatch(batch);
5479
+ for (const entry of drained) {
5480
+ if (entry.kind !== "prompt") continue;
5481
+ if (current.deadlineManager.isExpiring(entry.correlation))
5482
+ current.deadlineManager.captureExpiringRun(entry.correlation);
5483
+ else current.deadlineManager.onRunStarted(entry.correlation);
5212
5484
  }
5213
5485
  transitions = drained.map(({ kind, correlation }) => ({ kind, correlation }));
5214
5486
  } else {
@@ -5284,7 +5556,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5284
5556
  transitions.map(({ correlation }) => `${correlation.commandId}:${correlation.turnId}`),
5285
5557
  );
5286
5558
  if (
5287
- !ended.invocations.some(({ correlation }) =>
5559
+ (ended.invocations.length > 0 || ended.attachedInvocations.length > 0) &&
5560
+ ![...ended.invocations, ...ended.attachedInvocations].some(({ correlation }) =>
5288
5561
  transitionKeys.has(`${correlation.commandId}:${correlation.turnId}`),
5289
5562
  )
5290
5563
  )
@@ -5331,7 +5604,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5331
5604
  );
5332
5605
  }
5333
5606
  current.openLifecycleBatches = current.openLifecycleBatches.filter(
5334
- batch => batch.invocations.length > 0,
5607
+ batch => batch.unownedStart || batch.invocations.length > 0 || batch.attachedInvocations.length > 0,
5335
5608
  );
5336
5609
  current.drainedInvocations = current.drainedInvocations?.filter(
5337
5610
  entry =>
@@ -5342,7 +5615,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5342
5615
  current.activeInvocation?.correlation.commandId === invocation.correlation.commandId &&
5343
5616
  current.activeInvocation.correlation.turnId === invocation.correlation.turnId
5344
5617
  )
5345
- adoptLifecycleBatch(current.openLifecycleBatches[0]?.invocations);
5618
+ adoptLifecycleBatch(current.openLifecycleBatches[0]);
5346
5619
  if (current.openLifecycleBatches.length === 0) current.lifecycleActive = false;
5347
5620
  } catch (error) {
5348
5621
  if (controller.signal.aborted) {
@@ -5714,6 +5987,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5714
5987
  }
5715
5988
  if (type === "agent_end" && isContinuingMidRunMaintenanceOutcome(maintenanceOutcome)) return;
5716
5989
  if (type === "agent_end") {
5990
+ for (const invocation of transitions)
5991
+ retireAcceptedQueueCancellation(acceptedQueueCancellations, invocation.correlation);
5717
5992
  if (current.lifecycleEpoch !== eventLifecycleEpoch) {
5718
5993
  // A successor agent_start won the lifecycle race while this event's
5719
5994
  // durable transitions were awaiting persistence. Retire the ended
@@ -5763,7 +6038,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5763
6038
  !current.deadlineManager.shouldDeferTerminalTransition(invocation.correlation)
5764
6039
  )
5765
6040
  current.deadlineManager.clear(invocation.correlation);
5766
- adoptLifecycleBatch(current.openLifecycleBatches[0]?.invocations);
6041
+ adoptLifecycleBatch(current.openLifecycleBatches[0]);
5767
6042
  if (current.openLifecycleBatches.length === 0) current.lifecycleActive = false;
5768
6043
  // Resolve EVERY concurrent waiter for the aborted turn: the turn emits
5769
6044
  // exactly one agent_end, and each admitted abort of it must observe the
@@ -5772,39 +6047,43 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5772
6047
  resolveTerminalPublicationWaiters(observed, terminalPublicationByCorrelation);
5773
6048
  }
5774
6049
  };
5775
- api.on("agent_start", (event, ctx) => {
5776
- const owner = lifecycleStateForEvent(ctx, "agent_start", event.sdkRunToken);
5777
- // The activity checkpoint is already fire-and-forget and does not depend on
5778
- // the handler awaiting trackLifecycle, so it composes with #5683 unchanged.
5779
- void (owner?.runtime ?? lifecycleStateForContext(ctx, "agent_start")?.runtime)
5780
- ?.reportActivity("active")
5781
- .catch(error => logger.warn(`sdk: active activity checkpoint failed: ${String(error)}`));
5782
- // Interactive/skill turns must not wait on durable start persist. Keep
5783
- // trackLifecycle so drain, persist-then-publish, content-hold release, and
5784
- // shutdown still run; do not return that promise to the extension runner
5785
- // (EXTENSION_HANDLER_TIMEOUT_MS would otherwise stall the prompt).
5786
- void trackLifecycle(
5787
- async () =>
5788
- emitLifecycle(
5789
- "agent_start",
5790
- ctx,
5791
- undefined,
5792
- undefined,
5793
- owner ? { state: owner, sessionId: owner.sessionId } : undefined,
5794
- undefined,
5795
- false,
5796
- undefined,
5797
- undefined,
5798
- typeof event.sdkRunToken === "string" ? event.sdkRunToken : undefined,
5799
- event.sdkRunTokens,
5800
- ),
5801
- owner,
5802
- ).catch(error => {
5803
- logger.error("SDK agent_start lifecycle task failed", {
5804
- error: sanitizePromptFailure(error),
6050
+ api.on(
6051
+ "agent_start",
6052
+ tagSdkLifecycleObserver((event, ctx) => {
6053
+ const owner = lifecycleStateForEvent(ctx, "agent_start", event.sdkRunToken);
6054
+ // The activity checkpoint is already fire-and-forget and does not depend on
6055
+ // the handler awaiting trackLifecycle, so it composes with #5683 unchanged.
6056
+ void (owner?.runtime ?? lifecycleStateForContext(ctx, "agent_start")?.runtime)
6057
+ ?.reportActivity("active")
6058
+ .catch(error => logger.warn(`sdk: active activity checkpoint failed: ${String(error)}`));
6059
+ // Interactive/skill turns must not wait on durable start persist. Keep
6060
+ // trackLifecycle so drain, persist-then-publish, content-hold release, and
6061
+ // shutdown still run; do not return that promise to the extension runner
6062
+ // (EXTENSION_HANDLER_TIMEOUT_MS would otherwise stall the prompt).
6063
+ void trackLifecycle(
6064
+ async () =>
6065
+ emitLifecycle(
6066
+ "agent_start",
6067
+ ctx,
6068
+ undefined,
6069
+ undefined,
6070
+ owner ? { state: owner, sessionId: owner.sessionId } : undefined,
6071
+ undefined,
6072
+ false,
6073
+ undefined,
6074
+ undefined,
6075
+ typeof event.sdkRunToken === "string" ? event.sdkRunToken : undefined,
6076
+ event.sdkRunTokens,
6077
+ event.lifecycleScope,
6078
+ ),
6079
+ owner,
6080
+ ).catch(error => {
6081
+ logger.error("SDK agent_start lifecycle task failed", {
6082
+ error: sanitizePromptFailure(error),
6083
+ });
5805
6084
  });
5806
- });
5807
- });
6085
+ }),
6086
+ );
5808
6087
  api.on("agent_end", (event, ctx) => {
5809
6088
  const tokenBinding =
5810
6089
  typeof event.sdkRunToken === "string" ? lifecycleRunOwners.get(event.sdkRunToken) : undefined;
@@ -5965,7 +6244,10 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5965
6244
  api.on("turn_start", async (_event, ctx) => {
5966
6245
  const current = lifecycleStateForContext(ctx, "agent_start");
5967
6246
  if (!current) return;
5968
- await current.registerBroker();
6247
+ // Optional broker discovery must not hold an interactive extension event open.
6248
+ // registerBroker owns single-flight recovery and optional-failure diagnostics.
6249
+ const registration = current.registerBroker();
6250
+ if (options.brokerRegistrationRequired) await registration;
5969
6251
  current.runtime.emitEvent({ type: "turn_start", sessionId: ctx.sessionManager.getSessionId() });
5970
6252
  });
5971
6253
  const consumedMasterNonces = new Map<string, number>();
@@ -6124,6 +6406,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6124
6406
  return code !== undefined && /^[A-Za-z0-9_.-]{1,64}$/u.test(code) ? code : "unavailable";
6125
6407
  };
6126
6408
  const startRuntime = async (ctx: ExtensionContext): Promise<void> => {
6409
+ if (active) return;
6410
+ if (acceptedQueueCancellations.size > 0) await disposeAcceptedQueueCancellations();
6127
6411
  if (active) return;
6128
6412
  const sessionId = ctx.sessionManager.getSessionId();
6129
6413
  const stateRoot = path.join(ctx.cwd, ".gjc", "state");
@@ -6164,7 +6448,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6164
6448
  const seams = options.terminalAbortSeams;
6165
6449
  const handle = seams?.getActivePromptHandle();
6166
6450
  const epoch = seams?.getTerminalTurnEpoch();
6167
- if (!seams || !handle || epoch === undefined || !seams.pendingToolExecutions) return;
6451
+ if (!seams || !handle || epoch === undefined) return;
6168
6452
  const existing = deadlineTerminalizationObservations.get(key);
6169
6453
  if (existing) return () => cleanupDeadlineTerminalizationObservation(key, existing);
6170
6454
 
@@ -6364,6 +6648,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6364
6648
  });
6365
6649
  },
6366
6650
  onExpired: correlation => {
6651
+ retireAcceptedQueueCancellation(acceptedQueueCancellations, correlation, true);
6367
6652
  const owner = lifecycleOwnerHolder.state;
6368
6653
  if (!owner) return;
6369
6654
  removeLifecycleReferences(owner, correlation);
@@ -6400,26 +6685,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6400
6685
  true,
6401
6686
  );
6402
6687
  }
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
- }> = [];
6688
+ const openLifecycleBatches: LifecycleBatch[] = [];
6423
6689
  const configRevision = { current: 0 };
6424
6690
  let acceptingGateResolutions = true;
6425
6691
  const inFlightGateResolutions = new Set<Promise<unknown>>();
@@ -6521,9 +6787,9 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6521
6787
  kind: InvocationKind,
6522
6788
  correlation: InvocationCorrelation,
6523
6789
  error: { code: string; message: string },
6524
- ): void => {
6790
+ ): Promise<boolean> => {
6525
6791
  if (lifecycleOwnerHolder.state) removeLifecycleTokenAliases(lifecycleOwnerHolder.state, correlation);
6526
- const attempt = async (remaining: number): Promise<void> => {
6792
+ const attempt = async (remaining: number): Promise<boolean> => {
6527
6793
  try {
6528
6794
  // 1. Record the failure reason durably FIRST. If this write fails,
6529
6795
  // never fall through to agent_end: the record still has no error,
@@ -6546,7 +6812,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6546
6812
  turnId: correlation.turnId,
6547
6813
  error: sanitizePromptFailure(reasonError),
6548
6814
  });
6549
- return;
6815
+ return false;
6550
6816
  }
6551
6817
  await Bun.sleep(1_000);
6552
6818
  return attempt(remaining - 1);
@@ -6570,6 +6836,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6570
6836
  });
6571
6837
  // 3. Release recovery ownership ONLY after durable terminalization.
6572
6838
  deadlineManager.clear(correlation);
6839
+ if (kind === "prompt") retireAcceptedQueueCancellation(acceptedQueueCancellations, correlation);
6840
+ return true;
6573
6841
  } catch (transitionError) {
6574
6842
  // Keep prompt recovery leased; skill recovery has no prompt lease.
6575
6843
  if (kind === "skill") scheduleSkillRecovery(correlation);
@@ -6580,9 +6848,10 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6580
6848
  turnId: correlation.turnId,
6581
6849
  error: sanitizePromptFailure(transitionError),
6582
6850
  });
6851
+ return false;
6583
6852
  }
6584
6853
  };
6585
- void attempt(3);
6854
+ return attempt(3);
6586
6855
  };
6587
6856
 
6588
6857
  const controlSurface = createControlSurface(
@@ -6632,11 +6901,10 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6632
6901
  });
6633
6902
  };
6634
6903
  if (lifecycleOwnerHolder.quiescing) {
6635
- terminalizeAbandonedSubmission(kind, correlation, {
6904
+ return terminalizeAbandonedSubmission(kind, correlation, {
6636
6905
  code: "session_quiescing",
6637
6906
  message: "Session endpoint was replaced before the invocation started.",
6638
6907
  });
6639
- return;
6640
6908
  }
6641
6909
  // Lease at the ACTUAL promotion boundary (#4668 review): a promoted
6642
6910
  // submission that wedges before its run's agent_start must still
@@ -6656,11 +6924,10 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6656
6924
  entry.correlation.turnId === correlation.turnId,
6657
6925
  );
6658
6926
  if (pendingIdx >= 0) pending.splice(pendingIdx, 1);
6659
- terminalizeAbandonedSubmission(kind, correlation, {
6927
+ return terminalizeAbandonedSubmission(kind, correlation, {
6660
6928
  code: "cancelled",
6661
6929
  message: "Queued prompt was removed before consumption.",
6662
6930
  });
6663
- return;
6664
6931
  }
6665
6932
  if (promotion?.startsOwnRun !== false) {
6666
6933
  // A submission PROMOTED to its own run (finished prompt unwinding)
@@ -6706,7 +6973,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6706
6973
  entry.correlation.turnId === current.activeInvocation?.correlation.turnId,
6707
6974
  ),
6708
6975
  )
6709
- : undefined;
6976
+ : current.openLifecycleBatches.at(-1);
6710
6977
  const alreadyAttached = current.drainedInvocations.some(
6711
6978
  entry =>
6712
6979
  entry.correlation.commandId === correlation.commandId &&
@@ -6812,7 +7079,34 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6812
7079
  () => acceptingGateResolutions,
6813
7080
  trackGateResolution,
6814
7081
  options.onInvocationCompletionReconciledForTests,
6815
- frame => runtime.emitEvent(frame),
7082
+ frame => {
7083
+ // Completion can win the durable transaction while a lifecycle end is
7084
+ // still waiting on it. Both publishers must share the same claim.
7085
+ if (typeof frame.commandId !== "string" || typeof frame.turnId !== "string") return;
7086
+ const correlation = { commandId: frame.commandId, turnId: frame.turnId };
7087
+ if (frame.type !== "agent_end") {
7088
+ if (!hasClaimedTerminalBoundary(correlation)) runtime.emitEvent(frame);
7089
+ return;
7090
+ }
7091
+ const key = lifecycleCorrelationKey(correlation);
7092
+ if (!claimTerminalBoundary(correlation)) {
7093
+ const previouslyPublished = terminalBoundaryClaims.publicationResult(key) === true;
7094
+ if (previouslyPublished) return;
7095
+ terminalBoundaryClaims.releaseClaim(key);
7096
+ if (!claimTerminalBoundary(correlation)) return;
7097
+ }
7098
+ const sequenceBefore = runtime.host.events.sequence;
7099
+ try {
7100
+ runtime.emitEvent(frame);
7101
+ terminalBoundaryClaims.setPublicationResult(key, true);
7102
+ } catch (error) {
7103
+ if (runtime.host.events.sequence > sequenceBefore)
7104
+ terminalBoundaryClaims.setPublicationResult(key, true);
7105
+ else terminalBoundaryClaims.releaseClaim(key);
7106
+ throw error;
7107
+ }
7108
+ },
7109
+ acceptedQueueCancellations,
6816
7110
  );
6817
7111
  const installProviderDefinitions = (capability: string, definitions: unknown): void => {
6818
7112
  if (capability === "permission") {
@@ -7074,7 +7368,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
7074
7368
  };
7075
7369
  const startupImageCapture = captureStartupRuntimeImage();
7076
7370
  const registerBroker = async (): Promise<void> => {
7077
- if (brokerRegistered) return;
7371
+ if (brokerRecoveryStopped || brokerRegistered) return;
7078
7372
  if (brokerRegistrationInFlight !== undefined) {
7079
7373
  await brokerRegistrationInFlight;
7080
7374
  return;
@@ -7107,6 +7401,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
7107
7401
  throw new Error("SDK transport endpoint was not published before broker registration.");
7108
7402
  const endpointPath = path.join(input.stateRoot, "sdk", `${input.sessionId}.json`);
7109
7403
  const file = await readEndpointFile(endpointPath);
7404
+ if (brokerRecoveryStopped) return;
7110
7405
  if (!file) throw new Error("SDK endpoint could not be read as a stable regular file.");
7111
7406
  const endpoint = JSON.parse(file.source) as Record<string, unknown>;
7112
7407
  if (
@@ -7310,13 +7605,32 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
7310
7605
  active = runtimeOwner;
7311
7606
  try {
7312
7607
  publishedEndpointUrl = (await runtime.start()).url;
7313
- await registerBroker();
7608
+ // Required lifecycle hosts remain fail-closed; optional hosts publish locally
7609
+ // and recover broker availability outside the extension watchdog.
7610
+ const registration = registerBroker();
7611
+ if (options.brokerRegistrationRequired) await registration;
7314
7612
  // Await the startup runtime image capture before arming recovery.
7315
7613
  // This ensures replacement detection works correctly on the first recovery check.
7316
7614
  await startupImageCapture;
7317
- if (!brokerRecoveryStopped) startBrokerRecovery();
7615
+ // Recovery arms only after the startup attempt settles, so no tick overlaps it, and
7616
+ // a failed startup attempt holds the backoff like a failed recovery registration.
7617
+ const armBrokerRecovery = (): void => {
7618
+ if (brokerRecoveryStopped) return;
7619
+ if (!brokerRegistered) brokerRecoveryBackoff.recordFailure(options.agentDir);
7620
+ startBrokerRecovery();
7621
+ };
7622
+ if (options.brokerRegistrationRequired) armBrokerRecovery();
7623
+ else void registration.then(armBrokerRecovery);
7318
7624
  } catch (error) {
7625
+ runtimeOwner.quiesceInput();
7626
+ runtimeOwner.fenceGateResolutions();
7319
7627
  active = undefined;
7628
+ let queueCancellationFailure: unknown;
7629
+ try {
7630
+ await disposeAcceptedQueueCancellations();
7631
+ } catch (cleanupError) {
7632
+ queueCancellationFailure = cleanupError;
7633
+ }
7320
7634
  stopBrokerRecovery();
7321
7635
  disposeGate?.();
7322
7636
  try {
@@ -7356,16 +7670,40 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
7356
7670
  };
7357
7671
  lifecycleOwnerHolder.state = failedRuntimeOwner;
7358
7672
  active = failedRuntimeOwner;
7359
- throw new AggregateError([error, cleanupError], "SDK runtime startup failed and cleanup failed.");
7673
+ const failures = [
7674
+ error,
7675
+ ...(queueCancellationFailure === undefined ? [] : [queueCancellationFailure]),
7676
+ cleanupError,
7677
+ ];
7678
+ const startupFailure = new AggregateError(failures, "SDK runtime startup failed and cleanup failed.");
7679
+ if (queueCancellationFailure !== undefined)
7680
+ Object.assign(startupFailure, { code: "sdk_reconciliation_teardown_failed" });
7681
+ throw startupFailure;
7360
7682
  }
7361
7683
  cursors.close();
7362
7684
  await revisions.close().catch(() => undefined);
7685
+ if (queueCancellationFailure !== undefined)
7686
+ throw Object.assign(
7687
+ new AggregateError(
7688
+ [error, queueCancellationFailure],
7689
+ "SDK runtime startup failed with queued cancellation publication errors.",
7690
+ ),
7691
+ { code: "sdk_reconciliation_teardown_failed" },
7692
+ );
7363
7693
  throw error;
7364
7694
  }
7365
7695
  };
7366
7696
  const stopActive = async (cancelSkillRecovery = false, unregisterReason?: "detached_idle"): Promise<void> => {
7367
7697
  const current = active;
7368
7698
  if (!current) return;
7699
+ current.quiesceInput();
7700
+ current.fenceGateResolutions();
7701
+ let queueCancellationFailure: unknown;
7702
+ try {
7703
+ await disposeAcceptedQueueCancellations();
7704
+ } catch (cleanupError) {
7705
+ queueCancellationFailure = cleanupError;
7706
+ }
7369
7707
  if (cancelSkillRecovery) {
7370
7708
  for (const controller of skillRecoveryControllers.values()) controller.abort();
7371
7709
  for (const controller of skillTerminalRecoveryControllers.values()) controller.abort();
@@ -7373,8 +7711,6 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
7373
7711
  activePromptOwnerHolder.connectionIds = undefined;
7374
7712
  activePromptOwnerHolder.lifecycleEpoch = undefined;
7375
7713
  current.stopBrokerRecovery();
7376
- current.quiesceInput();
7377
- current.fenceGateResolutions();
7378
7714
  try {
7379
7715
  await current.waitForGateResolutionQuiescence();
7380
7716
  if (current.lifecycleTasks.size > 0) {
@@ -7395,7 +7731,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
7395
7731
  current.openLifecycleBatches.length > 0 ||
7396
7732
  (current.attachedInvocations?.length ?? 0) > 0 ||
7397
7733
  (current.drainedInvocations?.length ?? 0) > 0 ||
7398
- current.lifecycleTasks.size > 0;
7734
+ current.lifecycleTasks.size > 0 ||
7735
+ hasQueuedTerminalRecovery(current);
7399
7736
  if (retainsLifecycleWork) {
7400
7737
  const owners = retiredLifecycleOwners.get(current.sessionId) ?? [];
7401
7738
  if (!owners.includes(current)) owners.push(current);
@@ -7407,7 +7744,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
7407
7744
  current.openLifecycleBatches.length > 0 ||
7408
7745
  (current.attachedInvocations?.length ?? 0) > 0 ||
7409
7746
  (current.drainedInvocations?.length ?? 0) > 0 ||
7410
- current.lifecycleTasks.size > 0
7747
+ current.lifecycleTasks.size > 0 ||
7748
+ hasQueuedTerminalRecovery(current)
7411
7749
  ) {
7412
7750
  const retry = setTimeout(retryCleanup, LIFECYCLE_QUIESCENCE_MS);
7413
7751
  retry.unref();
@@ -7431,10 +7769,19 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
7431
7769
  // shutdown from retrying the failed endpoint removal.
7432
7770
  if (active === undefined) active = current;
7433
7771
  logger.error("sdk runtime stop failed", { code: errorCode(error), error: String(error) });
7772
+ if (queueCancellationFailure !== undefined)
7773
+ throw Object.assign(
7774
+ new AggregateError(
7775
+ [queueCancellationFailure, error],
7776
+ "SDK runtime stop and queued cancellation publication both failed.",
7777
+ ),
7778
+ { code: "sdk_reconciliation_teardown_failed" },
7779
+ );
7434
7780
  throw error;
7435
7781
  }
7436
7782
  current.cursors.close();
7437
7783
  await current.revisions.close();
7784
+ if (queueCancellationFailure !== undefined) throw queueCancellationFailure;
7438
7785
  };
7439
7786
  api.on("session_start", async (_event, ctx) => {
7440
7787
  await startRuntime(ctx);