@gajae-code/coding-agent 0.18.11 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (462) hide show
  1. package/CHANGELOG.md +760 -0
  2. package/dist/types/async/job-manager.d.ts +7 -3
  3. package/dist/types/async/result-formatting.d.ts +2 -0
  4. package/dist/types/autoresearch/git.d.ts +1 -1
  5. package/dist/types/capability/fs.d.ts +2 -2
  6. package/dist/types/capability/rule.d.ts +7 -2
  7. package/dist/types/cli/accounts-cli.d.ts +2 -2
  8. package/dist/types/cli/append-system-prompt.d.ts +6 -0
  9. package/dist/types/cli/args.d.ts +7 -2
  10. package/dist/types/cli/customize-doctor-precedence.d.ts +16 -0
  11. package/dist/types/cli/customize-doctor.d.ts +4 -3
  12. package/dist/types/cli/doctor/install-repairs.d.ts +4 -1
  13. package/dist/types/cli/install-activation.d.ts +10 -4
  14. package/dist/types/cli/public-command-errors.d.ts +2 -0
  15. package/dist/types/cli/root-flags.d.ts +0 -12
  16. package/dist/types/cli/update-cli.d.ts +21 -3
  17. package/dist/types/cli-main.d.ts +0 -6
  18. package/dist/types/cli-ordinary.d.ts +2 -2
  19. package/dist/types/commands/accounts.d.ts +2 -1
  20. package/dist/types/commands/launch.d.ts +0 -6
  21. package/dist/types/commands/migrate.d.ts +1 -1
  22. package/dist/types/commands/sdk.d.ts +13 -0
  23. package/dist/types/commit/changelog/index.d.ts +1 -0
  24. package/dist/types/commit/map-reduce/index.d.ts +4 -4
  25. package/dist/types/config/autorouting-tier-map.d.ts +53 -1
  26. package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
  27. package/dist/types/config/config-hot-reload.d.ts +44 -0
  28. package/dist/types/config/file-lock.d.ts +52 -3
  29. package/dist/types/config/model-discovery-manager.d.ts +2 -0
  30. package/dist/types/config/model-profile-activation.d.ts +79 -9
  31. package/dist/types/config/model-registry.d.ts +72 -8
  32. package/dist/types/config/model-resolver.d.ts +7 -4
  33. package/dist/types/config/models-config-schema.d.ts +10 -0
  34. package/dist/types/config/settings-schema.d.ts +57 -4
  35. package/dist/types/config/settings.d.ts +33 -1
  36. package/dist/types/coordinator-mcp/server.d.ts +4 -0
  37. package/dist/types/customization/import.d.ts +2 -0
  38. package/dist/types/customization/types.d.ts +1 -1
  39. package/dist/types/dap/config.d.ts +8 -4
  40. package/dist/types/dap/session.d.ts +11 -2
  41. package/dist/types/debug/system-info.d.ts +6 -1
  42. package/dist/types/discovery/helpers.d.ts +7 -2
  43. package/dist/types/discovery/mcp-compat.d.ts +3 -1
  44. package/dist/types/edit/index.d.ts +5 -0
  45. package/dist/types/edit/modes/patch.d.ts +14 -0
  46. package/dist/types/edit/modes/replace.d.ts +3 -0
  47. package/dist/types/eval/py/runtime.d.ts +9 -0
  48. package/dist/types/exec/bash-executor.d.ts +12 -0
  49. package/dist/types/export/ttsr.d.ts +19 -0
  50. package/dist/types/extensibility/extensions/types.d.ts +5 -1
  51. package/dist/types/extensibility/slash-commands.d.ts +1 -0
  52. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -0
  53. package/dist/types/gjc-runtime/ralplan-current-final.d.ts +54 -0
  54. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -0
  55. package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
  56. package/dist/types/gjc-runtime/state-runtime.d.ts +0 -2
  57. package/dist/types/gjc-runtime/tmux-sessions.d.ts +24 -0
  58. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +6 -0
  59. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  60. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  61. package/dist/types/hindsight/content.d.ts +5 -6
  62. package/dist/types/internal-urls/types.d.ts +2 -0
  63. package/dist/types/lsp/clients/swiftlint-client.d.ts +0 -4
  64. package/dist/types/main.d.ts +23 -18
  65. package/dist/types/migrate/action-planner.d.ts +10 -1
  66. package/dist/types/migrate/adapters/codex.d.ts +2 -1
  67. package/dist/types/migrate/adapters/index.d.ts +42 -1
  68. package/dist/types/migrate/executor.d.ts +3 -1
  69. package/dist/types/migrate/hook-module.d.ts +23 -0
  70. package/dist/types/migrate/report.d.ts +2 -1
  71. package/dist/types/migrate/types.d.ts +82 -3
  72. package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
  73. package/dist/types/modes/components/model-selector.d.ts +43 -0
  74. package/dist/types/modes/components/settings-selector.d.ts +6 -0
  75. package/dist/types/modes/controllers/selector-controller.d.ts +2 -0
  76. package/dist/types/modes/prompt-suggestion-controller.d.ts +1 -1
  77. package/dist/types/modes/theme/theme.d.ts +1 -0
  78. package/dist/types/modes/utils/host-status.d.ts +15 -11
  79. package/dist/types/runtime-mcp/content-limits.d.ts +4 -0
  80. package/dist/types/runtime-mcp/manager.d.ts +7 -0
  81. package/dist/types/runtime-mcp/plugin-mcp-name-filter.d.ts +22 -0
  82. package/dist/types/runtime-mcp/project-host-values.d.ts +11 -5
  83. package/dist/types/runtime-mcp/tool-bridge.d.ts +4 -0
  84. package/dist/types/sdk/broker/broker-exit.d.ts +3 -3
  85. package/dist/types/sdk/broker/broker.d.ts +29 -2
  86. package/dist/types/sdk/broker/daemon-entry.d.ts +11 -1
  87. package/dist/types/sdk/broker/doctor-restart.d.ts +1 -1
  88. package/dist/types/sdk/broker/ensure.d.ts +141 -9
  89. package/dist/types/sdk/broker/hop.d.ts +45 -0
  90. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
  91. package/dist/types/sdk/broker/lifecycle.d.ts +22 -0
  92. package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
  93. package/dist/types/sdk/broker/session-index.d.ts +6 -1
  94. package/dist/types/sdk/broker/transport.d.ts +2 -0
  95. package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
  96. package/dist/types/sdk/bus/index.d.ts +44 -0
  97. package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
  98. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -1
  99. package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
  100. package/dist/types/sdk/bus/threaded-render.d.ts +2 -1
  101. package/dist/types/sdk/cli/session-cli.d.ts +0 -1
  102. package/dist/types/sdk/host/control/operations.d.ts +5 -1
  103. package/dist/types/sdk/host/events.d.ts +4 -0
  104. package/dist/types/sdk/host/host.d.ts +2 -1
  105. package/dist/types/sdk/host/prompt-image-upload.d.ts +43 -0
  106. package/dist/types/sdk/host/session-runtime.d.ts +40 -8
  107. package/dist/types/sdk/host/surface-policy.d.ts +2 -1
  108. package/dist/types/sdk/lifecycle/service.d.ts +12 -0
  109. package/dist/types/sdk/prompt-deadline-flush.d.ts +42 -4
  110. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -0
  111. package/dist/types/sdk/prompt-status.d.ts +11 -4
  112. package/dist/types/sdk/protocol/adapter-validation.d.ts +10 -4
  113. package/dist/types/sdk/reconciliation-extensions.d.ts +1 -1
  114. package/dist/types/sdk/session-list.d.ts +1 -1
  115. package/dist/types/session/agent-session.d.ts +107 -9
  116. package/dist/types/session/internal/managed-session-scope.d.ts +2 -2
  117. package/dist/types/session/internal/managed-session-storage.d.ts +1 -1
  118. package/dist/types/session/internal/native-publish-outcome.d.ts +1 -1
  119. package/dist/types/session/messages.d.ts +5 -1
  120. package/dist/types/session/session-manager.d.ts +29 -3
  121. package/dist/types/session/startup-auth-config.d.ts +24 -2
  122. package/dist/types/session/streaming-output.d.ts +11 -0
  123. package/dist/types/session/terminal-abort.d.ts +3 -1
  124. package/dist/types/session-import/service.d.ts +1 -1
  125. package/dist/types/skill-state/workflow-mutation-guard.d.ts +20 -4
  126. package/dist/types/slash-commands/helpers/mcp.d.ts +5 -0
  127. package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
  128. package/dist/types/ssh/ssh-executor.d.ts +8 -1
  129. package/dist/types/system-prompt.d.ts +3 -2
  130. package/dist/types/task/executor.d.ts +19 -2
  131. package/dist/types/task/role-body.d.ts +11 -0
  132. package/dist/types/tools/ast-edit-write-path.d.ts +10 -0
  133. package/dist/types/tools/atomic-file-write.d.ts +9 -7
  134. package/dist/types/tools/awaiting-input.d.ts +28 -0
  135. package/dist/types/tools/bash-allowed-prefixes.d.ts +17 -0
  136. package/dist/types/tools/bash-skill-urls.d.ts +4 -0
  137. package/dist/types/tools/bash.d.ts +7 -0
  138. package/dist/types/tools/descriptors.d.ts +15 -2
  139. package/dist/types/tools/fetch.d.ts +7 -1
  140. package/dist/types/tools/index.d.ts +2 -0
  141. package/dist/types/tools/irc.d.ts +5 -3
  142. package/dist/types/tools/python.d.ts +1 -1
  143. package/dist/types/tools/report-issue.d.ts +41 -0
  144. package/dist/types/tools/subagent.d.ts +23 -0
  145. package/dist/types/tools/tmux-self-injection-guard.d.ts +2 -0
  146. package/dist/types/tools/ultragoal-ask-guard.d.ts +1 -1
  147. package/dist/types/tools/write.d.ts +2 -0
  148. package/dist/types/utils/file-mentions.d.ts +5 -0
  149. package/dist/types/utils/image-limits.d.ts +7 -0
  150. package/dist/types/utils/image-loading.d.ts +0 -1
  151. package/dist/types/utils/open.d.ts +0 -1
  152. package/dist/types/utils/pasted-image-loading.d.ts +1 -5
  153. package/dist/types/utils/prompt-suggestion.d.ts +2 -2
  154. package/dist/types/utils/shell-snapshot.d.ts +2 -0
  155. package/dist/types/utils/title-generator.d.ts +13 -3
  156. package/dist/types/web/scrapers/types.d.ts +8 -1
  157. package/dist/types/web/search/providers/codex.d.ts +6 -7
  158. package/dist/types/web/search/providers/insane.d.ts +7 -1
  159. package/dist/types/web/search/providers/searxng.d.ts +5 -0
  160. package/package.json +10 -9
  161. package/scripts/acp-conformance-agent.ts +12 -2
  162. package/scripts/build-binary.ts +2 -1
  163. package/scripts/capture-subagent-compact-panel-showcase.ts +105 -0
  164. package/scripts/generate-docs-index.ts +8 -1
  165. package/scripts/generate-sdk-operation-inventory.ts +38 -4
  166. package/src/async/job-manager.ts +356 -56
  167. package/src/async/result-formatting.ts +40 -0
  168. package/src/autoresearch/git.ts +9 -2
  169. package/src/autoresearch/prompts/prompt-setup.md +1 -1
  170. package/src/capability/fs.ts +58 -22
  171. package/src/capability/index.ts +2 -2
  172. package/src/capability/rule.ts +11 -2
  173. package/src/cli/accounts-cli.ts +176 -8
  174. package/src/cli/append-system-prompt.ts +12 -0
  175. package/src/cli/args.ts +11 -7
  176. package/src/cli/auth-broker-cli.ts +21 -11
  177. package/src/cli/customize-doctor-precedence.ts +31 -0
  178. package/src/cli/customize-doctor.ts +49 -39
  179. package/src/cli/doctor/install-repairs.ts +35 -5
  180. package/src/cli/fast-help.ts +1 -3
  181. package/src/cli/install-activation.ts +47 -4
  182. package/src/cli/migrate-cli.ts +19 -6
  183. package/src/cli/public-command-errors.ts +6 -0
  184. package/src/cli/root-flags.ts +2 -4
  185. package/src/cli/stats-cli.ts +9 -5
  186. package/src/cli/update-cli.ts +471 -13
  187. package/src/cli-ordinary.ts +7 -2
  188. package/src/commands/accounts.ts +10 -5
  189. package/src/commands/harness.ts +25 -15
  190. package/src/commands/migrate.ts +3 -3
  191. package/src/commands/sdk.ts +277 -50
  192. package/src/commands/ultragoal.ts +16 -8
  193. package/src/commit/agentic/agent.ts +6 -6
  194. package/src/commit/analysis/conventional.ts +14 -5
  195. package/src/commit/changelog/index.ts +39 -1
  196. package/src/commit/map-reduce/index.ts +7 -7
  197. package/src/commit/message.ts +10 -3
  198. package/src/commit/pipeline.ts +12 -12
  199. package/src/config/autorouting-tier-map.ts +50 -1
  200. package/src/config/config-file.ts +106 -3
  201. package/src/config/config-hot-reload-trust.ts +148 -0
  202. package/src/config/config-hot-reload.ts +742 -0
  203. package/src/config/file-lock.ts +1615 -328
  204. package/src/config/keybindings.ts +154 -7
  205. package/src/config/model-bindings-applier.ts +85 -47
  206. package/src/config/model-discovery-manager.ts +10 -0
  207. package/src/config/model-profile-activation.ts +889 -148
  208. package/src/config/model-registry.ts +1706 -205
  209. package/src/config/model-resolver.ts +52 -22
  210. package/src/config/models-config-schema.ts +6 -0
  211. package/src/config/settings-schema.ts +73 -4
  212. package/src/config/settings.ts +460 -24
  213. package/src/coordinator-mcp/policy.ts +26 -1
  214. package/src/coordinator-mcp/server.ts +37 -2
  215. package/src/crash/sanitize.ts +7 -1
  216. package/src/customization/import.ts +279 -8
  217. package/src/customization/inventory.ts +24 -8
  218. package/src/customization/types.ts +1 -0
  219. package/src/dap/client.ts +3 -2
  220. package/src/dap/config.ts +31 -11
  221. package/src/dap/defaults.json +1 -1
  222. package/src/dap/session.ts +161 -36
  223. package/src/debug/crash-diagnostics.ts +89 -6
  224. package/src/debug/report-bundle.ts +90 -2
  225. package/src/debug/system-info.ts +8 -3
  226. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +21 -7
  227. package/src/discovery/agents-md.ts +1 -1
  228. package/src/discovery/builtin.ts +4 -3
  229. package/src/discovery/helpers.ts +25 -16
  230. package/src/discovery/mcp-compat.ts +120 -17
  231. package/src/edit/apply-patch/index.ts +12 -1
  232. package/src/edit/index.ts +204 -22
  233. package/src/edit/modes/patch.ts +214 -8
  234. package/src/edit/modes/replace.ts +136 -0
  235. package/src/eval/js/context-manager.ts +29 -3
  236. package/src/eval/js/worker-entry.ts +51 -18
  237. package/src/eval/py/executor.ts +14 -4
  238. package/src/eval/py/runner.py +144 -5
  239. package/src/eval/py/runtime.ts +44 -4
  240. package/src/exec/bash-executor.ts +81 -4
  241. package/src/exec/bash-shell-guardian.ts +4 -1
  242. package/src/exec/bash-shell-supervisor.ts +8 -4
  243. package/src/exec/isolated-shell.ts +5 -1
  244. package/src/exec/non-interactive-env.ts +0 -1
  245. package/src/export/html/index.ts +86 -6
  246. package/src/export/ttsr.ts +176 -1
  247. package/src/extensibility/custom-tools/wrapper.ts +3 -1
  248. package/src/extensibility/extensions/loader.ts +10 -1
  249. package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +63 -1
  250. package/src/extensibility/extensions/types.ts +5 -1
  251. package/src/extensibility/hooks/loader.ts +10 -2
  252. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
  253. package/src/extensibility/plugins/marketplace/manager.ts +36 -10
  254. package/src/extensibility/slash-commands.ts +5 -0
  255. package/src/gjc-runtime/gc-runtime.ts +37 -7
  256. package/src/gjc-runtime/launch-tmux.ts +30 -15
  257. package/src/gjc-runtime/launch-worktree.test.ts +148 -0
  258. package/src/gjc-runtime/launch-worktree.ts +101 -14
  259. package/src/gjc-runtime/managed-owner-admission.ts +46 -2
  260. package/src/gjc-runtime/ralplan-current-final.ts +294 -0
  261. package/src/gjc-runtime/ralplan-runtime.ts +95 -1
  262. package/src/gjc-runtime/session-resolution.ts +38 -6
  263. package/src/gjc-runtime/session-state-lock.ts +59 -14
  264. package/src/gjc-runtime/state-runtime.ts +23 -19
  265. package/src/gjc-runtime/state-writer.ts +109 -5
  266. package/src/gjc-runtime/tmux-sessions.ts +93 -13
  267. package/src/gjc-runtime/ultragoal-change-set.ts +90 -11
  268. package/src/gjc-runtime/ultragoal-guard.ts +9 -1
  269. package/src/gjc-runtime/ultragoal-runtime.ts +15 -5
  270. package/src/harness-control-plane/finalize.ts +11 -0
  271. package/src/harness-control-plane/receipt-spool.ts +105 -28
  272. package/src/harness-control-plane/storage.ts +124 -6
  273. package/src/hindsight/content.ts +116 -16
  274. package/src/hindsight/mental-models.ts +22 -10
  275. package/src/hindsight/state.ts +7 -4
  276. package/src/hooks/native-skill-hook.ts +17 -12
  277. package/src/internal-urls/artifact-protocol.ts +3 -1
  278. package/src/internal-urls/docs-index.generated.ts +23 -22
  279. package/src/internal-urls/local-protocol.ts +167 -8
  280. package/src/internal-urls/types.ts +2 -0
  281. package/src/lsp/client.ts +2 -1
  282. package/src/lsp/clients/biome-client.ts +2 -0
  283. package/src/lsp/clients/swiftlint-client.ts +2 -0
  284. package/src/lsp/index.ts +29 -0
  285. package/src/lsp/workspace-path.ts +23 -0
  286. package/src/main.ts +330 -92
  287. package/src/memories/index.ts +25 -2
  288. package/src/migrate/action-planner.ts +574 -2
  289. package/src/migrate/adapters/claude-code.ts +162 -5
  290. package/src/migrate/adapters/codex.ts +77 -7
  291. package/src/migrate/adapters/index.ts +258 -5
  292. package/src/migrate/adapters/opencode.ts +10 -1
  293. package/src/migrate/executor.ts +222 -7
  294. package/src/migrate/hook-module.ts +193 -0
  295. package/src/migrate/report.ts +63 -2
  296. package/src/migrate/types.ts +84 -4
  297. package/src/modes/DESIGN.md +51 -0
  298. package/src/modes/acp/acp-agent.ts +10 -1
  299. package/src/modes/acp/permission-mode.ts +18 -1
  300. package/src/modes/components/agent-dashboard.ts +20 -4
  301. package/src/modes/components/assistant-message.ts +55 -1
  302. package/src/modes/components/customization/customization-dashboard.ts +7 -0
  303. package/src/modes/components/customization/import-wizard.ts +29 -3
  304. package/src/modes/components/model-selector.ts +157 -103
  305. package/src/modes/components/settings-selector.ts +103 -37
  306. package/src/modes/components/welcome.ts +3 -1
  307. package/src/modes/controllers/command-controller.ts +300 -9
  308. package/src/modes/controllers/event-controller.ts +1 -1
  309. package/src/modes/controllers/extension-ui-controller.ts +31 -1
  310. package/src/modes/controllers/goal-mode-controller.ts +79 -10
  311. package/src/modes/controllers/input-controller.ts +2 -0
  312. package/src/modes/controllers/plan-mode-controller.ts +66 -11
  313. package/src/modes/controllers/runtime-mcp-command-controller.ts +7 -6
  314. package/src/modes/controllers/selector-controller.ts +76 -7
  315. package/src/modes/interactive-mode.ts +123 -4
  316. package/src/modes/prompt-suggestion-controller.ts +1 -1
  317. package/src/modes/runtime-init.ts +5 -1
  318. package/src/modes/theme/theme.ts +15 -0
  319. package/src/modes/utils/host-status.ts +21 -10
  320. package/src/prompts/agent-fragments/browser-aside.md +2 -2
  321. package/src/prompts/system/irc-incoming.md +3 -1
  322. package/src/prompts/tools/debug.md +1 -1
  323. package/src/prompts/tools/irc.md +1 -1
  324. package/src/prompts/tools/read.md +1 -1
  325. package/src/prompts/tools/report-issue.md +13 -0
  326. package/src/prompts/tools/task.md +1 -1
  327. package/src/runtime-credential-selector.ts +2 -2
  328. package/src/runtime-mcp/content-limits.ts +7 -0
  329. package/src/runtime-mcp/manager-pool.test.ts +5 -4
  330. package/src/runtime-mcp/manager.ts +40 -3
  331. package/src/runtime-mcp/oauth-flow.ts +32 -6
  332. package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
  333. package/src/runtime-mcp/project-host-values.ts +12 -8
  334. package/src/runtime-mcp/render.ts +16 -1
  335. package/src/runtime-mcp/tool-bridge.ts +194 -21
  336. package/src/runtime-mcp/transports/stdio.ts +2 -1
  337. package/src/sdk/broker/broker-exit.ts +10 -5
  338. package/src/sdk/broker/broker.ts +484 -85
  339. package/src/sdk/broker/daemon-entry.ts +338 -116
  340. package/src/sdk/broker/doctor-restart.ts +2 -1
  341. package/src/sdk/broker/ensure.ts +1214 -268
  342. package/src/sdk/broker/hop.ts +286 -0
  343. package/src/sdk/broker/lifecycle-ledger.ts +38 -11
  344. package/src/sdk/broker/lifecycle.ts +102 -6
  345. package/src/sdk/broker/lock-host-identity.ts +29 -0
  346. package/src/sdk/broker/session-index.ts +91 -16
  347. package/src/sdk/broker/transport.ts +17 -1
  348. package/src/sdk/bus/chat-command-policy.ts +16 -3
  349. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  350. package/src/sdk/bus/index.ts +460 -35
  351. package/src/sdk/bus/reconciliation-store.ts +201 -8
  352. package/src/sdk/bus/session-control.ts +2 -0
  353. package/src/sdk/bus/slack-daemon.ts +105 -9
  354. package/src/sdk/bus/telegram-daemon-contract.ts +9 -1
  355. package/src/sdk/bus/telegram-daemon.ts +10 -2
  356. package/src/sdk/bus/threaded-render.ts +20 -1
  357. package/src/sdk/cli/session-cli.ts +23 -6
  358. package/src/sdk/host/control/dispatch.ts +13 -1
  359. package/src/sdk/host/control/operations.ts +10 -1
  360. package/src/sdk/host/events.ts +67 -4
  361. package/src/sdk/host/host.ts +8 -2
  362. package/src/sdk/host/prompt-image-upload.ts +438 -0
  363. package/src/sdk/host/sdk-surface-parity.test.ts +15 -0
  364. package/src/sdk/host/session-runtime.test.ts +1737 -129
  365. package/src/sdk/host/session-runtime.ts +684 -83
  366. package/src/sdk/host/surface-policy.ts +4 -0
  367. package/src/sdk/host/websocket-transport.lifecycle.test.ts +90 -2
  368. package/src/sdk/host/websocket-transport.ts +19 -7
  369. package/src/sdk/lifecycle/service.ts +15 -1
  370. package/src/sdk/prompt-deadline-flush.ts +78 -5
  371. package/src/sdk/prompt-deadline-manager.ts +68 -14
  372. package/src/sdk/prompt-status.ts +6 -4
  373. package/src/sdk/protocol/adapter-validation.ts +81 -12
  374. package/src/sdk/protocol/operation-inventory.generated.json +261 -2
  375. package/src/sdk/protocol/operation-registry.ts +33 -3
  376. package/src/sdk/reconciliation-extensions.ts +1 -0
  377. package/src/sdk/session-list.ts +1 -0
  378. package/src/sdk/session.ts +237 -97
  379. package/src/session/agent-session.ts +2726 -479
  380. package/src/session/history-storage.ts +182 -12
  381. package/src/session/internal/managed-session-scope.ts +173 -57
  382. package/src/session/internal/managed-session-storage.ts +85 -12
  383. package/src/session/internal/native-publish-outcome.ts +5 -1
  384. package/src/session/messages.ts +47 -6
  385. package/src/session/session-manager.ts +326 -19
  386. package/src/session/startup-auth-config.ts +116 -8
  387. package/src/session/streaming-output.ts +262 -4
  388. package/src/session/terminal-abort.ts +14 -2
  389. package/src/session-import/redact.ts +53 -2
  390. package/src/session-import/service.ts +1 -0
  391. package/src/skill-state/workflow-mutation-guard.ts +2145 -206
  392. package/src/slash-commands/builtin-registry.ts +21 -6
  393. package/src/slash-commands/helpers/credential-switch.ts +10 -5
  394. package/src/slash-commands/helpers/mcp.ts +11 -3
  395. package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
  396. package/src/ssh/ssh-executor.ts +9 -2
  397. package/src/stt/recorder.ts +26 -9
  398. package/src/system-prompt.ts +61 -16
  399. package/src/task/executor.ts +115 -20
  400. package/src/task/index.ts +389 -175
  401. package/src/task/role-body.ts +84 -0
  402. package/src/task/token-log-mode.test.ts +162 -0
  403. package/src/task/token-log.ts +91 -2
  404. package/src/tools/archive-reader.ts +650 -37
  405. package/src/tools/ask.ts +1 -0
  406. package/src/tools/ast-edit-write-path.ts +64 -0
  407. package/src/tools/ast-edit.ts +18 -1
  408. package/src/tools/ast-grep.ts +7 -2
  409. package/src/tools/atomic-file-write.ts +46 -15
  410. package/src/tools/auto-generated-guard.ts +15 -3
  411. package/src/tools/awaiting-input.ts +114 -0
  412. package/src/tools/bash-allowed-prefixes.ts +214 -6
  413. package/src/tools/bash-interactive.ts +14 -2
  414. package/src/tools/bash-skill-urls.ts +58 -5
  415. package/src/tools/bash.ts +113 -26
  416. package/src/tools/browser/tab-worker.ts +39 -13
  417. package/src/tools/browser.ts +17 -7
  418. package/src/tools/cron.ts +10 -1
  419. package/src/tools/debug.ts +16 -8
  420. package/src/tools/descriptors.test.ts +44 -0
  421. package/src/tools/descriptors.ts +50 -6
  422. package/src/tools/eval.ts +15 -0
  423. package/src/tools/fetch.ts +93 -4
  424. package/src/tools/gh.ts +27 -12
  425. package/src/tools/image-gen.ts +20 -6
  426. package/src/tools/index.ts +2 -0
  427. package/src/tools/irc.ts +5 -3
  428. package/src/tools/monitor.ts +9 -1
  429. package/src/tools/output-meta.ts +74 -5
  430. package/src/tools/python.ts +1 -1
  431. package/src/tools/read.ts +26 -37
  432. package/src/tools/recipe/runner.ts +23 -12
  433. package/src/tools/render-utils.ts +2 -2
  434. package/src/tools/report-issue.ts +184 -0
  435. package/src/tools/search.ts +3 -2
  436. package/src/tools/ssh.ts +274 -8
  437. package/src/tools/subagent-render.ts +114 -12
  438. package/src/tools/subagent.ts +118 -5
  439. package/src/tools/tmux-self-injection-guard.ts +3301 -122
  440. package/src/tools/todo-write.ts +41 -3
  441. package/src/tools/tool-catalog.generated.ts +32 -4
  442. package/src/tools/ultragoal-ask-guard.ts +13 -1
  443. package/src/tools/write.ts +42 -3
  444. package/src/tui/tree-list.ts +15 -1
  445. package/src/utils/commit-message-generator.ts +12 -4
  446. package/src/utils/external-editor.ts +21 -7
  447. package/src/utils/file-mentions.ts +75 -7
  448. package/src/utils/git.ts +38 -7
  449. package/src/utils/image-limits.ts +8 -0
  450. package/src/utils/image-loading.ts +1 -1
  451. package/src/utils/open.ts +73 -4
  452. package/src/utils/pasted-image-loading.ts +9 -8
  453. package/src/utils/prompt-suggestion.ts +5 -4
  454. package/src/utils/shell-snapshot.ts +9 -1
  455. package/src/utils/title-generator.ts +72 -22
  456. package/src/web/scrapers/index.ts +3 -1
  457. package/src/web/scrapers/types.ts +8 -0
  458. package/src/web/scrapers/youtube.ts +3 -2
  459. package/src/web/search/index.ts +2 -1
  460. package/src/web/search/providers/codex.ts +29 -32
  461. package/src/web/search/providers/insane.ts +91 -28
  462. package/src/web/search/providers/searxng.ts +62 -10
@@ -4,7 +4,12 @@ import * as crypto from "node:crypto";
4
4
  import * as fs from "node:fs/promises";
5
5
  import * as path from "node:path";
6
6
  import { promisify } from "node:util";
7
- import { isContinuingMidRunMaintenanceOutcome, isNonDispatchedToolEvent, ThinkingLevel } from "@gajae-code/agent-core";
7
+ import {
8
+ isContinuingMidRunMaintenanceOutcome,
9
+ isNonDispatchedToolEvent,
10
+ type RunSettlementProof,
11
+ ThinkingLevel,
12
+ } from "@gajae-code/agent-core";
8
13
  import type { AttemptScope } from "@gajae-code/agent-core/attempt-scope";
9
14
  import type { Api, ImageContent, Model, ProviderDiagnostic } from "@gajae-code/ai/core";
10
15
 
@@ -39,7 +44,7 @@ import { tagSdkLifecycleObserver } from "../../extensibility/extensions/function
39
44
  import type { AgentEndEvent } from "../../extensibility/shared-events";
40
45
  import { normalizeGoal } from "../../goals/state";
41
46
  import { toAgentWireEventPayload } from "../../modes/shared/agent-wire/event-envelope";
42
- import type { AgentSessionEvent } from "../../session/agent-session";
47
+ import type { AgentSessionEvent, QueuedRunStartFailure } from "../../session/agent-session";
43
48
  import type { SdkRunCapability } from "../../session/sdk-run-capability";
44
49
  import {
45
50
  boundTerminalRetentionState,
@@ -73,7 +78,7 @@ import {
73
78
  syntheticNamespaceCollision,
74
79
  } from "../model-profile-model";
75
80
  import { projectQ10Models } from "../models.js";
76
- import { flushWorktreeOnPromptDeadline } from "../prompt-deadline-flush";
81
+ import { autosaveWorktreeOnPromptDeadline } from "../prompt-deadline-flush";
77
82
  import {
78
83
  PromptDeadlineManager,
79
84
  type PromptDeadlinePublicationResult,
@@ -103,6 +108,7 @@ import {
103
108
  createKindAwareReconciliation,
104
109
  createReconciliationStore,
105
110
  type KindAwareReconciliation,
111
+ type ReconciliationStoreInstrumentationEvent,
106
112
  resolveReconciliationSessionFile,
107
113
  } from "../reconciliation-extensions";
108
114
  import {
@@ -124,6 +130,7 @@ import {
124
130
  TURN_STREAM_CAPABILITY,
125
131
  } from "./host";
126
132
  import { clearAutoroutingInactive, isAutoroutingInactive, markAutoroutingInactive } from "./internal-autorouting-state";
133
+ import { PromptImageUploadStore, retirePromptImageReservation } from "./prompt-image-upload";
127
134
  import { CursorRegistry, QueryHandlers, RevisionStore, type SessionSurface } from "./query";
128
135
  import { createSdkRunCapability } from "./sdk-run-capability";
129
136
  import {
@@ -349,6 +356,8 @@ export interface SessionSdkTransport {
349
356
  stop(): Promise<void>;
350
357
  broadcastFrame?(frame: SdkFrame): void;
351
358
  onConnectionClose?(handler: (connectionId: string) => void): undefined | (() => void);
359
+ /** Authoritative live socket membership, not a cache of previously observed frames. */
360
+ isConnectionOpen?(connectionId: string): boolean;
352
361
  onNegotiatedCapabilities?(
353
362
  handler: (connectionId: string, capabilities: readonly string[]) => void,
354
363
  ): undefined | (() => void);
@@ -356,6 +365,7 @@ export interface SessionSdkTransport {
356
365
 
357
366
  export interface SessionSdkRuntimeOptions
358
367
  extends Omit<SessionSdkHostOptions, "sessionId" | "stateRoot" | "token" | "sendFrame" | "onFrame"> {
368
+ onDisconnected?: (connectionId: string) => void;
359
369
  transport: SessionSdkTransport;
360
370
  /** Session settings; enables `config.patch` application on this runtime. */
361
371
  settings?: Settings;
@@ -442,9 +452,13 @@ export interface SdkOnlyReconciliationStore {
442
452
  }
443
453
 
444
454
  export interface SdkOnlyTerminalAbortSeams {
455
+ /** Definitive event-driven observation: never abort, seal or quarantine a run to reclaim images. */
456
+ waitForRunSettlement?: (handle: string) => Promise<RunSettlementProof>;
445
457
  getReconciliationStore?: () => SdkOnlyReconciliationStore | undefined;
446
458
  getTerminalTurnEpoch: () => number | undefined;
447
459
  getActivePromptHandle: () => string | undefined;
460
+ onDeadlineRecoveryCheckpointForTests?: (checkpoint: SdkOnlyDeadlineRecoveryCheckpoint) => void;
461
+ onPersistenceInstrumentationForTests?: (event: PersistenceInstrumentationEvent) => void;
448
462
  /** Re-read the active prompt's owning SDK connection for the owner-mismatch
449
463
  * recheck; falls back to the runtime-tracked owner when absent (review
450
464
  * thread P1). */
@@ -481,6 +495,35 @@ export interface SdkOnlyTerminalAbortSeams {
481
495
  maxDurableTerminalReservationsForTests?: number;
482
496
  }
483
497
 
498
+ export interface SdkOnlyDeadlineRecoveryCheckpoint {
499
+ at: number;
500
+ stage:
501
+ | "terminalization-start"
502
+ | "terminalization-result"
503
+ | "publication-start"
504
+ | "publication-result"
505
+ | "note-transition-start"
506
+ | "note-transition-awaiting-finalization"
507
+ | "note-transition-finalization-ready"
508
+ | "note-transition-before-persist"
509
+ | "note-transition-persisted"
510
+ | "note-transition-persist-failed"
511
+ | "deadline-recovery-retired";
512
+ correlation: InvocationCorrelation;
513
+ result?: "settled" | "uncertain" | "published" | "not-published" | "rejected";
514
+ reason?: string;
515
+ recordStatus?: string;
516
+ recordRevision?: number;
517
+ recordTerminalAt?: number;
518
+ pendingFinalization?: boolean;
519
+ eventCaptured?: boolean;
520
+ eventPrepared?: boolean;
521
+ terminalCommitted?: boolean;
522
+ leaseCurrent?: boolean;
523
+ pendingToolCount?: number;
524
+ errorCode?: string;
525
+ }
526
+
484
527
  /**
485
528
  * The transport-neutral SDK session runtime.
486
529
  *
@@ -489,6 +532,10 @@ export interface SdkOnlyTerminalAbortSeams {
489
532
  * dispatch, replay/event publication, and reverse-provider lifecycle without
490
533
  * importing any notification adapter or native notification class.
491
534
  */
535
+ function isStringArray(value: unknown): value is string[] {
536
+ return Array.isArray(value) && value.every((item: unknown) => typeof item === "string");
537
+ }
538
+
492
539
  export class SessionSdkSessionRuntime {
493
540
  readonly host: SessionSdkHost;
494
541
  readonly transport: SessionSdkTransport;
@@ -519,18 +566,32 @@ export class SessionSdkSessionRuntime {
519
566
  onFrame: handler =>
520
567
  options.transport.onFrame((connectionId, frame) => {
521
568
  this.#connectionIds.add(connectionId);
569
+ if (
570
+ !options.connectionCapabilities &&
571
+ frame.type === "event_replay" &&
572
+ isStringArray(frame.capabilities)
573
+ ) {
574
+ this.#connectionCapabilities.set(
575
+ connectionId,
576
+ new Set([...(this.#connectionCapabilities.get(connectionId) ?? []), ...frame.capabilities]),
577
+ );
578
+ }
522
579
  handler(connectionId, frame);
523
580
  }),
524
581
  });
525
582
  this.#connectionDisposer = options.transport.onConnectionClose?.(connectionId => {
526
583
  this.#connectionIds.delete(connectionId);
527
584
  this.#connectionCapabilities.delete(connectionId);
585
+ options.onDisconnected?.(connectionId);
528
586
  this.host.handleDisconnect(connectionId);
529
587
  });
530
588
  this.#capabilitiesDisposer = options.transport.onNegotiatedCapabilities?.((connectionId, negotiated) => {
531
589
  if (options.connectionCapabilities && this.#connectionCapabilitiesProvider(connectionId) === undefined) return;
532
590
  this.#connectionIds.add(connectionId);
533
- this.#connectionCapabilities.set(connectionId, new Set(negotiated));
591
+ this.#connectionCapabilities.set(
592
+ connectionId,
593
+ new Set([...(this.#connectionCapabilities.get(connectionId) ?? []), ...negotiated]),
594
+ );
534
595
  });
535
596
  this.#malformedDisposer = options.transport.onMalformedFrame?.((connectionId, message) => {
536
597
  this.host.handleMalformedFrame(connectionId, message);
@@ -1024,8 +1085,26 @@ function canonicalizeHydratedDiagnostics(record: InvocationRecord): InvocationRe
1024
1085
  return canonical;
1025
1086
  }
1026
1087
 
1088
+ export interface PersistenceInstrumentationEvent {
1089
+ at: number;
1090
+ phase: "enqueued" | "started" | "completed" | "failed";
1091
+ operation?: string;
1092
+ substep?: string;
1093
+ error?: string;
1094
+ /** Detailed information about lock acquisition, promise identity, db state, etc. */
1095
+ detail?: string;
1096
+ /** Time spent waiting (ms) before the operation started. */
1097
+ waitMs?: number;
1098
+ }
1099
+
1027
1100
  export function createInvocationReconciliation(
1028
- options: { stateRoot?: string; sessionId?: string; store?: SdkOnlyReconciliationStore } = {},
1101
+ options: {
1102
+ stateRoot?: string;
1103
+ sessionId?: string;
1104
+ store?: SdkOnlyReconciliationStore;
1105
+ onDeadlineRecoveryCheckpointForTests?: (checkpoint: SdkOnlyDeadlineRecoveryCheckpoint) => void;
1106
+ onPersistenceInstrumentationForTests?: (event: PersistenceInstrumentationEvent) => void;
1107
+ } = {},
1029
1108
  ): InvocationReconciliation {
1030
1109
  const ACTIVE_CAPACITY = 256;
1031
1110
  const TERMINAL_CAPACITY = 512;
@@ -1063,6 +1142,17 @@ export function createInvocationReconciliation(
1063
1142
  }
1064
1143
  >();
1065
1144
  const pendingTerminalVisibility = new Map<string, { visibleRecord: InvocationRecord; writes: number }>();
1145
+ const recordDeadlineRecoveryCheckpoint = (
1146
+ stage: SdkOnlyDeadlineRecoveryCheckpoint["stage"],
1147
+ correlation: InvocationCorrelation,
1148
+ details: Omit<SdkOnlyDeadlineRecoveryCheckpoint, "at" | "correlation" | "stage"> = {},
1149
+ ): void => {
1150
+ try {
1151
+ options.onDeadlineRecoveryCheckpointForTests?.({ at: Date.now(), stage, correlation, ...details });
1152
+ } catch {
1153
+ // A test observer must never participate in durable reconciliation.
1154
+ }
1155
+ };
1066
1156
  const retainPendingTerminalVisibility = (recordKey: string, visibleRecord: InvocationRecord): (() => void) => {
1067
1157
  const existing = pendingTerminalVisibility.get(recordKey);
1068
1158
  if (existing) existing.writes += 1;
@@ -1079,36 +1169,120 @@ export function createInvocationReconciliation(
1079
1169
  };
1080
1170
  const persist = async (): Promise<void> => {
1081
1171
  const run = async (): Promise<void> => {
1172
+ try {
1173
+ options.onPersistenceInstrumentationForTests?.({ at: Date.now(), phase: "enqueued" });
1174
+ } catch {
1175
+ // A test observer must never participate in durable reconciliation.
1176
+ }
1177
+ try {
1178
+ options.onPersistenceInstrumentationForTests?.({ at: Date.now(), phase: "started" });
1179
+ } catch {
1180
+ // Ignore observer errors.
1181
+ }
1082
1182
  // Construct the candidate only when this serialized write starts. A
1083
1183
  // pre-await full snapshot lets a later agent_start/agent_end transition
1084
1184
  // be overwritten on disk by an older queued write even though the live
1085
1185
  // map has already converged.
1086
1186
  const snapshot = [...records.values()].map(record => ({ ...record }));
1087
1187
  if (store) {
1088
- await store.transact(current => [
1089
- ...current.filter(record => record.kind !== "prompt" && record.kind !== "skill"),
1090
- ...snapshot.map(record => ({ ...record })),
1091
- ]);
1188
+ const promiseId = `transact-${crypto.randomUUID().slice(0, 8)}`;
1189
+ const lockWaitStart = Date.now();
1190
+ try {
1191
+ options.onPersistenceInstrumentationForTests?.({
1192
+ at: lockWaitStart,
1193
+ phase: "started",
1194
+ operation: "store.transact",
1195
+ detail: `promise=${promiseId} awaiting-lock`,
1196
+ });
1197
+ } catch {}
1198
+ const lockAcquiredTime = Date.now();
1199
+ try {
1200
+ await store.transact(current => [
1201
+ ...current.filter(record => record.kind !== "prompt" && record.kind !== "skill"),
1202
+ ...snapshot.map(record => ({ ...record })),
1203
+ ]);
1204
+ } catch (error) {
1205
+ try {
1206
+ options.onPersistenceInstrumentationForTests?.({
1207
+ at: Date.now(),
1208
+ phase: "failed",
1209
+ operation: "store.transact",
1210
+ detail: `promise=${promiseId} lock-wait=${Date.now() - lockWaitStart}ms`,
1211
+ error: error instanceof Error ? error.message : String(error),
1212
+ });
1213
+ } catch {}
1214
+ throw error;
1215
+ }
1216
+ try {
1217
+ options.onPersistenceInstrumentationForTests?.({
1218
+ at: Date.now(),
1219
+ phase: "completed",
1220
+ operation: "store.transact",
1221
+ detail: `promise=${promiseId} lock-acquire=${lockAcquiredTime - lockWaitStart}ms`,
1222
+ });
1223
+ } catch {}
1092
1224
  return;
1093
1225
  }
1094
1226
  if (!reconciliationFile) return;
1095
1227
  const directory = path.dirname(reconciliationFile);
1096
1228
  const temporary = `${reconciliationFile}.${process.pid}.${crypto.randomUUID()}.tmp`;
1229
+ try {
1230
+ options.onPersistenceInstrumentationForTests?.({ at: Date.now(), phase: "started", substep: "mkdir" });
1231
+ } catch {}
1097
1232
  await fs.mkdir(directory, { recursive: true, mode: 0o700 });
1233
+ try {
1234
+ options.onPersistenceInstrumentationForTests?.({ at: Date.now(), phase: "completed", substep: "mkdir" });
1235
+ } catch {}
1236
+ try {
1237
+ options.onPersistenceInstrumentationForTests?.({ at: Date.now(), phase: "started", substep: "writeFile" });
1238
+ } catch {}
1098
1239
  await fs.writeFile(
1099
1240
  temporary,
1100
1241
  JSON.stringify({ version: 1, sessionId: options.sessionId, records: [...snapshot] }),
1101
1242
  { encoding: "utf8", mode: 0o600 },
1102
1243
  );
1244
+ try {
1245
+ options.onPersistenceInstrumentationForTests?.({
1246
+ at: Date.now(),
1247
+ phase: "completed",
1248
+ substep: "writeFile",
1249
+ });
1250
+ } catch {}
1251
+ try {
1252
+ options.onPersistenceInstrumentationForTests?.({ at: Date.now(), phase: "started", substep: "chmod" });
1253
+ } catch {}
1103
1254
  await fs.chmod(temporary, 0o600);
1255
+ try {
1256
+ options.onPersistenceInstrumentationForTests?.({ at: Date.now(), phase: "completed", substep: "chmod" });
1257
+ } catch {}
1258
+ try {
1259
+ options.onPersistenceInstrumentationForTests?.({ at: Date.now(), phase: "started", substep: "rename" });
1260
+ } catch {}
1104
1261
  await fs.rename(temporary, reconciliationFile);
1262
+ try {
1263
+ options.onPersistenceInstrumentationForTests?.({ at: Date.now(), phase: "completed", substep: "rename" });
1264
+ } catch {}
1105
1265
  };
1106
1266
  const pending = persistenceChain.then(run, run);
1107
1267
  persistenceChain = pending.then(
1108
1268
  () => undefined,
1109
1269
  () => undefined,
1110
1270
  );
1111
- await pending;
1271
+ try {
1272
+ await pending;
1273
+ try {
1274
+ options.onPersistenceInstrumentationForTests?.({ at: Date.now(), phase: "completed" });
1275
+ } catch {}
1276
+ } catch (error) {
1277
+ try {
1278
+ options.onPersistenceInstrumentationForTests?.({
1279
+ at: Date.now(),
1280
+ phase: "failed",
1281
+ error: error instanceof Error ? error.message : String(error),
1282
+ });
1283
+ } catch {}
1284
+ throw error;
1285
+ }
1112
1286
  };
1113
1287
  // Retention contract (#4547): terminal records are never age-evicted; only
1114
1288
  // the per-kind oldest-terminal-first capacity trim removes them, so a
@@ -1257,7 +1431,22 @@ export function createInvocationReconciliation(
1257
1431
  let record = records.get(recordKey);
1258
1432
  if (!record) return;
1259
1433
  const pending = pendingFinalizations.get(recordKey);
1434
+ const isPromptEnd = kind === "prompt" && frame.type === "agent_end";
1435
+ if (isPromptEnd)
1436
+ recordDeadlineRecoveryCheckpoint("note-transition-start", correlation, {
1437
+ recordStatus: record.status,
1438
+ recordRevision: record.revision,
1439
+ recordTerminalAt: record.terminalAt,
1440
+ pendingFinalization: pending?.finalizedRecord === record,
1441
+ });
1260
1442
  if (pending?.finalizedRecord === record) {
1443
+ if (isPromptEnd)
1444
+ recordDeadlineRecoveryCheckpoint("note-transition-awaiting-finalization", correlation, {
1445
+ recordStatus: record.status,
1446
+ recordRevision: record.revision,
1447
+ recordTerminalAt: record.terminalAt,
1448
+ pendingFinalization: true,
1449
+ });
1261
1450
  const upgrade = Promise.withResolvers<void>();
1262
1451
  pending.upgrades.add(upgrade);
1263
1452
  let upgradeError: unknown;
@@ -1269,6 +1458,13 @@ export function createInvocationReconciliation(
1269
1458
  // so a real lifecycle event is never swallowed.
1270
1459
  }
1271
1460
  const current = records.get(recordKey);
1461
+ if (isPromptEnd)
1462
+ recordDeadlineRecoveryCheckpoint("note-transition-finalization-ready", correlation, {
1463
+ recordStatus: current?.status,
1464
+ recordRevision: current?.revision,
1465
+ recordTerminalAt: current?.terminalAt,
1466
+ pendingFinalization: pendingFinalizations.get(recordKey) === pending,
1467
+ });
1272
1468
  const incomingOutcome = frame.type === "agent_end" ? canonicalTerminalOutcome(frame.outcome) : undefined;
1273
1469
  const providerFailure =
1274
1470
  current?.error !== undefined && current.error.code !== "prompt_deadline_exceeded";
@@ -1650,10 +1846,31 @@ export function createInvocationReconciliation(
1650
1846
  record.terminalAt === undefined && next.terminalAt !== undefined
1651
1847
  ? retainPendingTerminalVisibility(recordKey, record)
1652
1848
  : undefined;
1849
+ if (isPromptEnd)
1850
+ recordDeadlineRecoveryCheckpoint("note-transition-before-persist", correlation, {
1851
+ recordStatus: next.status,
1852
+ recordRevision: next.revision,
1853
+ recordTerminalAt: next.terminalAt,
1854
+ pendingFinalization: pendingFinalizations.has(recordKey),
1855
+ });
1653
1856
  records.set(recordKey, next);
1654
1857
  try {
1655
1858
  await persist();
1859
+ if (isPromptEnd)
1860
+ recordDeadlineRecoveryCheckpoint("note-transition-persisted", correlation, {
1861
+ recordStatus: next.status,
1862
+ recordRevision: next.revision,
1863
+ recordTerminalAt: next.terminalAt,
1864
+ pendingFinalization: pendingFinalizations.has(recordKey),
1865
+ });
1656
1866
  } catch (error) {
1867
+ if (isPromptEnd)
1868
+ recordDeadlineRecoveryCheckpoint("note-transition-persist-failed", correlation, {
1869
+ recordStatus: next.status,
1870
+ recordRevision: next.revision,
1871
+ recordTerminalAt: next.terminalAt,
1872
+ pendingFinalization: pendingFinalizations.has(recordKey),
1873
+ });
1657
1874
  if (records.get(recordKey) === next) records.set(recordKey, record);
1658
1875
  throw error;
1659
1876
  } finally {
@@ -2027,6 +2244,8 @@ export interface SdkSurfaceFactoryOptions {
2027
2244
  api: ExtensionAPI;
2028
2245
  /** Startup surface that owns host-level permission and lifecycle authority. */
2029
2246
  primaryControlSurface?: "cli" | "sdk";
2247
+ /** Private staging controls are advertised only with authoritative transport liveness. */
2248
+ imageUploadsAvailable?: boolean;
2030
2249
  policy?: SdkSurfacePolicy;
2031
2250
  getInstalledDefinitions?: (capability: string) => unknown | undefined;
2032
2251
  getLiveState?: () => { isStreaming: boolean; steeringQueueDepth: number; followupQueueDepth: number };
@@ -2110,7 +2329,8 @@ function createQuerySurface(
2110
2329
  };
2111
2330
  const getProfileCredentialSessionId = () => ctx.credentialSessionId ?? id;
2112
2331
  const profileSettings = (options.settings ?? ctx.settings) as Pick<Settings, "get"> | undefined;
2113
- const getProfileAvailableModels = (): Model<Api>[] => ctx.modelRegistry.getAvailable();
2332
+ const getProfileAvailableModels = (): Model<Api>[] =>
2333
+ ctx.modelRegistry.getAvailable(getProfileCredentialSessionId());
2114
2334
  const resolveProfileAvailability = async (
2115
2335
  profile: ModelProfileDefinition,
2116
2336
  authenticatedProviders: ReadonlySet<string>,
@@ -2331,7 +2551,7 @@ function createQuerySurface(
2331
2551
  getDiff,
2332
2552
  getUsage: () => ctx.sessionManager.getUsageStatistics(),
2333
2553
  getModels: async () => {
2334
- const models = ctx.modelRegistry.getAll();
2554
+ const models = ctx.modelRegistry.getAll(getProfileCredentialSessionId());
2335
2555
  const currentModel = ctx.model;
2336
2556
  const currentThinkingLevel = api.getThinkingLevel();
2337
2557
  const activeProfile =
@@ -2454,7 +2674,9 @@ function createQuerySurface(
2454
2674
  getBranchCandidates: () => ctx.getBranchCandidates(),
2455
2675
  getLastAssistant: lastAssistant,
2456
2676
  getCapabilities: () => createSdkCapabilities(policy, hasHostTools(), options.primaryControlSurface),
2457
- getAuthProviders: () => [...new Set(ctx.modelRegistry.getAll().map(model => model.provider))],
2677
+ getAuthProviders: () => [
2678
+ ...new Set(ctx.modelRegistry.getAll(getProfileCredentialSessionId()).map(model => model.provider)),
2679
+ ],
2458
2680
  getActiveProviders: () => ctx.modelRegistry.getActiveProviders(),
2459
2681
  getTools: () => {
2460
2682
  const tools = typeof (ctx as Partial<ExtensionContext>).getAllTools === "function" ? ctx.getAllTools() : [];
@@ -2532,7 +2754,11 @@ export function createSdkSurfaceFactory(
2532
2754
  ): SdkSurfaceFactory {
2533
2755
  const policy =
2534
2756
  options.policy ??
2535
- createSdkSurfacePolicyForContext(options.ctx, hasSdkWorkflowGateCapability(options.ctx.workflowGate));
2757
+ createSdkSurfacePolicyForContext(
2758
+ options.ctx,
2759
+ hasSdkWorkflowGateCapability(options.ctx.workflowGate),
2760
+ options.imageUploadsAvailable,
2761
+ );
2536
2762
  const reconciliation =
2537
2763
  options.reconciliation ??
2538
2764
  createInvocationReconciliation({
@@ -2918,6 +3144,7 @@ function createControlSurface(
2918
3144
  ) => void,
2919
3145
  armPromptDeadline: (correlation: InvocationCorrelation) => void,
2920
3146
  steerReconciliation: KindAwareReconciliation,
3147
+ imageUploads: PromptImageUploadStore | undefined,
2921
3148
  onPromotedTurn?: (
2922
3149
  kind: InvocationKind,
2923
3150
  correlation: InvocationCorrelation,
@@ -2945,7 +3172,15 @@ function createControlSurface(
2945
3172
  trackGateResolution: <T>(resolution: Promise<T>) => Promise<T> = async resolution => await resolution,
2946
3173
  onInvocationCompletionReconciledForTests?: (kind: InvocationKind, correlation: InvocationCorrelation) => void,
2947
3174
  publishLifecycleFrame?: (frame: SdkFrame) => void,
3175
+ retainAcceptedImage?: (correlation: InvocationCorrelation, release: () => void) => void,
3176
+ settleAcceptedImageSubmission?: (correlation: InvocationCorrelation) => void,
3177
+ releaseAcceptedImage?: (correlation: InvocationCorrelation) => void,
2948
3178
  acceptedQueueCancellations: Map<string, AcceptedQueueCancellation> = new Map(),
3179
+ onQueuedRunStartFailed?: (
3180
+ kind: InvocationKind,
3181
+ correlation: InvocationCorrelation,
3182
+ failure: QueuedRunStartFailure,
3183
+ ) => boolean,
2949
3184
  ): ControlSurface {
2950
3185
  const normalizePromptImages = (value: unknown): ImageContent[] => {
2951
3186
  if (!Array.isArray(value)) return [];
@@ -2966,6 +3201,7 @@ function createControlSurface(
2966
3201
  type InternalSendOptions = NonNullable<Parameters<ExtensionAPI["sendUserMessage"]>[1]> & {
2967
3202
  sdkRunCapability?: SdkRunCapability;
2968
3203
  expectedSdkRunToken?: string;
3204
+ onQueuedRunStartFailed?: (failure: QueuedRunStartFailure) => void;
2969
3205
  };
2970
3206
  type InternalSdkApi = Omit<ExtensionAPI, "sendUserMessage"> & {
2971
3207
  sendUserMessage: (
@@ -2976,7 +3212,7 @@ function createControlSurface(
2976
3212
  const internalApi = api as InternalSdkApi;
2977
3213
  const sendSdkUserMessage = (
2978
3214
  content: Parameters<ExtensionAPI["sendUserMessage"]>[0],
2979
- options?: Record<string, unknown>,
3215
+ options?: InternalSendOptions,
2980
3216
  ): Promise<void> => internalApi.sendUserMessage(content, options);
2981
3217
  const surfacePolicy =
2982
3218
  policy ?? createSdkSurfacePolicyForContext(ctx, hasSdkWorkflowGateCapability(ctx.workflowGate));
@@ -2986,8 +3222,14 @@ function createControlSurface(
2986
3222
  const [provider, ...modelId] = id.split("/");
2987
3223
  const model =
2988
3224
  modelId.length > 0
2989
- ? ctx.modelRegistry.find(provider, modelId.join("/"))
2990
- : ctx.modelRegistry.getAll().find(candidate => candidate.id === id);
3225
+ ? ctx.modelRegistry.find(
3226
+ provider,
3227
+ modelId.join("/"),
3228
+ ctx.credentialSessionId ?? ctx.sessionManager.getSessionId(),
3229
+ )
3230
+ : ctx.modelRegistry
3231
+ .getAll(ctx.credentialSessionId ?? ctx.sessionManager.getSessionId())
3232
+ .find(candidate => candidate.id === id);
2991
3233
  if (!model) throw Object.assign(new Error(`Model ${id} was not found.`), { code: "invalid_input" });
2992
3234
  return model;
2993
3235
  };
@@ -3113,11 +3355,13 @@ function createControlSurface(
3113
3355
  onDispatchDisposition: (promotion: { startsOwnRun: boolean }) => void;
3114
3356
  /** Fired when a queued submission (steering or follow-up) is promoted to its own run (SDK ownership correlation). */
3115
3357
  onQueuedPromoted: (promotion?: { startsOwnRun?: boolean; removed?: boolean }) => void;
3358
+ onQueuedRunStartFailed: (failure: QueuedRunStartFailure) => void;
3116
3359
  queuedAtDispatch: boolean;
3117
3360
  }) => Promise<unknown>,
3118
3361
  acceptedFields?: () => Record<string, unknown>,
3119
3362
  allowCompletionFallback = false,
3120
3363
  alwaysQueued = false,
3364
+ onCreated?: (correlation: InvocationCorrelation) => void,
3121
3365
  ): Promise<unknown> => {
3122
3366
  // Capture the REQUESTING connection at admission: a terminal abort from
3123
3367
  // another SDK connection must never stop the prompt this one accepts
@@ -3126,9 +3370,11 @@ function createControlSurface(
3126
3370
  const retainedClientRef = normalizeClientRef(clientRef);
3127
3371
  reconciliation.admit(kind, retainedClientRef);
3128
3372
  const correlation = newCorrelation();
3373
+ onCreated?.(correlation);
3129
3374
  const sdkRunToken = `${correlation.commandId}:${correlation.turnId}`;
3130
3375
  const sdkRunCapability = createSdkRunCapability(sdkRunToken);
3131
3376
  const publishTerminal = (outcome: InvocationOutcome): void => {
3377
+ releaseAcceptedImage?.(correlation);
3132
3378
  retireAcceptedQueueCancellation(acceptedQueueCancellations, correlation);
3133
3379
  publishLifecycleFrame?.({
3134
3380
  type: "agent_end",
@@ -3162,6 +3408,7 @@ function createControlSurface(
3162
3408
  };
3163
3409
  let accepted = false;
3164
3410
  let settled = false;
3411
+ let submission: Promise<unknown> | undefined;
3165
3412
  const cancelPreflight = () => {
3166
3413
  preflightController.abort();
3167
3414
  if (settled) return;
@@ -3221,14 +3468,16 @@ function createControlSurface(
3221
3468
  if (kind === "prompt")
3222
3469
  acceptedQueueCancellations.set(`${correlation.commandId}:${correlation.turnId}`, queueCancellation);
3223
3470
  let promotionStartsOwnRun: boolean | undefined;
3471
+ let queuedRunStartRefused = false;
3224
3472
  try {
3225
- const submission = Promise.resolve(
3473
+ submission = Promise.resolve(
3226
3474
  run({
3227
3475
  onPreflightAccepted: () => void accept().catch(() => undefined),
3228
3476
  preflightSignal: preflightController.signal,
3229
3477
  sdkRunCapability,
3230
3478
  onPreflightAcceptCommit: accept,
3231
3479
  onDispatchDisposition: promotion => {
3480
+ if (queuedRunStartRefused) return;
3232
3481
  promotionStartsOwnRun = promotion.startsOwnRun;
3233
3482
  if (promotion.startsOwnRun === false) {
3234
3483
  queueCancellation.queueCandidate = true;
@@ -3248,6 +3497,7 @@ function createControlSurface(
3248
3497
  // created at promotion so the submitting connection can
3249
3498
  // terminal-abort that turn (review threads P1/P2).
3250
3499
  onQueuedPromoted: (promotion?: { startsOwnRun?: boolean; removed?: boolean }) => {
3500
+ if (queuedRunStartRefused) return;
3251
3501
  promotionStartsOwnRun = promotion?.startsOwnRun;
3252
3502
  if (promotion?.removed) {
3253
3503
  queueCancellation.disposition = "removed";
@@ -3281,11 +3531,29 @@ function createControlSurface(
3281
3531
  }
3282
3532
  void onPromotedTurn?.(kind, correlation, requesterConnectionId, sdkRunToken, promotion);
3283
3533
  },
3534
+ onQueuedRunStartFailed: failure => {
3535
+ if (
3536
+ queuedRunStartRefused ||
3537
+ !accepted ||
3538
+ !queueCancellation.queueCandidate ||
3539
+ failure.disposition !== "not-started"
3540
+ )
3541
+ return;
3542
+ // Only the private Core decision for this selected queue entry is proof.
3543
+ // The lifecycle owner rejects contradictory decisions after adoption.
3544
+ queuedRunStartRefused = onQueuedRunStartFailed?.(kind, correlation, failure) === true;
3545
+ if (queuedRunStartRefused) {
3546
+ queueCancellation.disposition = "consumed";
3547
+ queueCancellation.resolveDisposition("consumed");
3548
+ }
3549
+ },
3284
3550
  queuedAtDispatch,
3285
3551
  }),
3286
3552
  );
3287
3553
  void submission.then(
3288
3554
  result => {
3555
+ settleAcceptedImageSubmission?.(correlation);
3556
+ if (queuedRunStartRefused) return;
3289
3557
  if (settled) {
3290
3558
  // A resolved submission after preflight acceptance means the work is over
3291
3559
  // for every kind. `noteTransition` ignores an already-terminal record, so
@@ -3296,7 +3564,9 @@ function createControlSurface(
3296
3564
  // a delayed preflight diverted to steering fires onQueuedPromoted
3297
3565
  // with startsOwnRun:false and is now attached to the in-flight run.
3298
3566
  // Do not terminalize from the stale snapshot — the run will.
3299
- if (!queuedAtDispatch && promotionStartsOwnRun !== false) {
3567
+ // Remember actual diversion even after own-run promotion changes
3568
+ // promotionStartsOwnRun: enqueue completion is still not run completion.
3569
+ if (!queueCancellation.queueCandidate && promotionStartsOwnRun !== false) {
3300
3570
  // The accepted work settled without its own run still pending:
3301
3571
  // retire the pending ownership entry (and with it the
3302
3572
  // acceptance-anchored deadline lease, #4668 review) BEFORE
@@ -3365,7 +3635,10 @@ function createControlSurface(
3365
3635
  );
3366
3636
  },
3367
3637
  error => {
3638
+ settleAcceptedImageSubmission?.(correlation);
3639
+ if (queuedRunStartRefused) return;
3368
3640
  if (settled) {
3641
+ releaseAcceptedImage?.(correlation);
3369
3642
  retireAcceptedQueueCancellation(acceptedQueueCancellations, correlation);
3370
3643
  // The submission promise rejects after preflight acceptance only when the
3371
3644
  // work itself is over (provider stream interrupt, abort, queue failure).
@@ -3487,6 +3760,9 @@ function createControlSurface(
3487
3760
  ...(acceptedFields?.() ?? {}),
3488
3761
  };
3489
3762
  } catch (error) {
3763
+ if (!accepted) preflightController.abort();
3764
+ if (!submission) settleAcceptedImageSubmission?.(correlation);
3765
+ releaseAcceptedImage?.(correlation);
3490
3766
  retireAcceptedQueueCancellation(acceptedQueueCancellations, correlation);
3491
3767
  if (!accepted) reconciliation.release(kind, retainedClientRef);
3492
3768
  throw error;
@@ -4405,28 +4681,57 @@ function createControlSurface(
4405
4681
  }
4406
4682
  };
4407
4683
  return {
4408
- prompt: async (text, images, clientRef) => {
4409
- const invalid = validateRequiredPromptText("turn.prompt", { text, images });
4684
+ prompt: async (text, images, clientRef, stagedImages) => {
4685
+ const invalid = validateRequiredPromptText("turn.prompt", { text, images, stagedImages });
4410
4686
  if (invalid) throw Object.assign(new Error(invalid.message), { code: invalid.code });
4411
- return await submit("prompt", clientRef, ({ queuedAtDispatch, sdkRunCapability, ...options }) =>
4412
- sendSdkUserMessage(
4413
- typeof images === "undefined"
4414
- ? text
4415
- : ([{ type: "text", text }, ...normalizePromptImages(images)] as [
4416
- { type: "text"; text: string },
4417
- ...ImageContent[],
4418
- ]),
4419
- {
4420
- ...options,
4421
- sdkRunCapability,
4422
- // ACP terminal settlement is owned by the correlated agent_end
4423
- // publication. Post-prompt recovery may include independent
4424
- // subagent work and must not hold that client-facing boundary.
4425
- ...(queuedAtDispatch ? { queuedAtDispatch: true } : {}),
4687
+ if (stagedImages !== undefined && images !== undefined)
4688
+ throw Object.assign(new Error("Direct and staged images cannot be mixed."), { code: "invalid_input" });
4689
+ if (stagedImages !== undefined && !imageUploads)
4690
+ throw Object.assign(new Error("Image uploads are unavailable."), { code: "operation_prohibited" });
4691
+ let reservation: { images: ImageContent[]; release: () => void } | undefined;
4692
+ let imageCorrelation: InvocationCorrelation | undefined;
4693
+ try {
4694
+ return await submit(
4695
+ "prompt",
4696
+ clientRef,
4697
+ ({ queuedAtDispatch, sdkRunCapability, ...options }) => {
4698
+ // submit has reserved reconciliation/clientRef before consuming staged IDs.
4699
+ if (stagedImages !== undefined) {
4700
+ reservation = imageUploads!.redeem(sdkControlRequesterContext.getStore(), stagedImages);
4701
+ if (imageCorrelation && retainAcceptedImage)
4702
+ retainAcceptedImage(imageCorrelation, reservation.release);
4703
+ }
4704
+ return sendSdkUserMessage(
4705
+ typeof images === "undefined" && !reservation
4706
+ ? text
4707
+ : ([{ type: "text", text }, ...(reservation?.images ?? normalizePromptImages(images))] as [
4708
+ { type: "text"; text: string },
4709
+ ...ImageContent[],
4710
+ ]),
4711
+ { ...options, sdkRunCapability, ...(queuedAtDispatch ? { queuedAtDispatch: true } : {}) },
4712
+ );
4426
4713
  },
4427
- ),
4428
- );
4714
+ undefined,
4715
+ false,
4716
+ false,
4717
+ correlation => {
4718
+ if (retainAcceptedImage) imageCorrelation = correlation;
4719
+ },
4720
+ );
4721
+ } catch (error) {
4722
+ if (imageCorrelation) releaseAcceptedImage?.(imageCorrelation);
4723
+ else reservation?.release();
4724
+ throw error;
4725
+ }
4429
4726
  },
4727
+ ...(imageUploads
4728
+ ? {
4729
+ imageBegin: (input: unknown) => imageUploads.begin(sdkControlRequesterContext.getStore(), input),
4730
+ imageAppend: (input: unknown) => imageUploads.append(sdkControlRequesterContext.getStore(), input),
4731
+ imageFinish: (input: unknown) => imageUploads.finish(sdkControlRequesterContext.getStore(), input),
4732
+ imageDiscard: (input: unknown) => imageUploads.discard(sdkControlRequesterContext.getStore(), input),
4733
+ }
4734
+ : {}),
4430
4735
  steer: async (text, clientRef, expectedSdkRunToken) => {
4431
4736
  const invalid = validateRequiredPromptText("turn.steer", { text });
4432
4737
  if (invalid) throw Object.assign(new Error(invalid.message), { code: invalid.code });
@@ -4681,7 +4986,9 @@ function createControlSurface(
4681
4986
  retryLast: () => typed("retry.last"),
4682
4987
  retryNow: () => typed("retry.now"),
4683
4988
  backgroundBash: () => typed("bash.background"),
4684
- installedOperations: surfacePolicy.installedControls,
4989
+ installedOperations: imageUploads
4990
+ ? surfacePolicy.installedControls
4991
+ : new Set([...surfacePolicy.installedControls].filter(operation => !operation.startsWith("turn.image."))),
4685
4992
  revisionProvider: resource => (resource === "config" ? String(configRevision.current) : undefined),
4686
4993
  };
4687
4994
  }
@@ -4831,6 +5138,89 @@ function quiescingFrame(frame: Record<string, unknown>): Record<string, unknown>
4831
5138
 
4832
5139
  /** Install a complete SDK host for a session when notifications are inactive. */
4833
5140
  export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: CreateSdkSessionRuntimeOptions): void {
5141
+ const acceptedImages = new Map<
5142
+ string,
5143
+ {
5144
+ store: PromptImageUploadStore;
5145
+ release: () => void;
5146
+ handle?: string;
5147
+ executionStarted?: boolean;
5148
+ submissionPending?: boolean;
5149
+ awaitingAdoption?: boolean;
5150
+ releaseRequested?: boolean;
5151
+ retiring?: boolean;
5152
+ }
5153
+ >();
5154
+ const imageKey = (correlation: InvocationCorrelation): string => `${correlation.commandId}:${correlation.turnId}`;
5155
+ const retireAcceptedImage = (key: string): void => {
5156
+ const reservation = acceptedImages.get(key);
5157
+ if (!reservation) return;
5158
+ if ((reservation.submissionPending || reservation.awaitingAdoption) && !reservation.executionStarted) {
5159
+ reservation.releaseRequested = true;
5160
+ return;
5161
+ }
5162
+ if (reservation.retiring) return;
5163
+ const release = (): void => {
5164
+ if (acceptedImages.get(key) !== reservation) return;
5165
+ acceptedImages.delete(key);
5166
+ reservation.release();
5167
+ };
5168
+ if (!reservation.handle) {
5169
+ if (reservation.executionStarted) {
5170
+ reservation.retiring = true;
5171
+ logger.warn("SDK image capacity retained for execution without an exact resource handle");
5172
+ return;
5173
+ }
5174
+ release();
5175
+ return;
5176
+ }
5177
+ const waitForSettlement = options.terminalAbortSeams?.waitForRunSettlement;
5178
+ if (!waitForSettlement) return;
5179
+ reservation.retiring = true;
5180
+ retirePromptImageReservation(
5181
+ reservation.handle,
5182
+ waitForSettlement,
5183
+ () => acceptedImages.get(key) === reservation,
5184
+ release,
5185
+ );
5186
+ };
5187
+ const releaseAcceptedImage = (correlation: InvocationCorrelation): void =>
5188
+ retireAcceptedImage(imageKey(correlation));
5189
+ const settleAcceptedImageSubmission = (correlation: InvocationCorrelation): void => {
5190
+ const key = imageKey(correlation);
5191
+ const reservation = acceptedImages.get(key);
5192
+ if (!reservation) return;
5193
+ reservation.submissionPending = false;
5194
+ if (reservation.releaseRequested) {
5195
+ reservation.releaseRequested = false;
5196
+ retireAcceptedImage(key);
5197
+ }
5198
+ };
5199
+ const bindAcceptedImage = (correlation: InvocationCorrelation, handle: string | undefined): void => {
5200
+ const reservation = acceptedImages.get(imageKey(correlation));
5201
+ if (!reservation) return;
5202
+ reservation.awaitingAdoption = false;
5203
+ reservation.executionStarted = true;
5204
+ if (handle && reservation.handle === undefined) reservation.handle = handle;
5205
+ if (reservation.releaseRequested) retireAcceptedImage(imageKey(correlation));
5206
+ };
5207
+ // Promotion commits a queued image to its next own-run before agent_start supplies the exact resource handle.
5208
+ const awaitAcceptedImageAdoption = (correlation: InvocationCorrelation): void => {
5209
+ const reservation = acceptedImages.get(imageKey(correlation));
5210
+ if (reservation) reservation.awaitingAdoption = true;
5211
+ };
5212
+ const releaseImagesForStore = (store: PromptImageUploadStore | undefined): void => {
5213
+ for (const [key, reservation] of acceptedImages) {
5214
+ if (reservation.store !== store) continue;
5215
+ retireAcceptedImage(key);
5216
+ }
5217
+ };
5218
+ const pendingImageCleanups = new Set<PromptImageUploadStore>();
5219
+ const completeImageCleanups = (): void => {
5220
+ for (const store of pendingImageCleanups) releaseImagesForStore(store);
5221
+ pendingImageCleanups.clear();
5222
+ };
5223
+ let currentImageUploads: PromptImageUploadStore | undefined;
4834
5224
  const acceptedQueueCancellations = new Map<string, AcceptedQueueCancellation>();
4835
5225
  const disposeAcceptedQueueCancellations = async (): Promise<void> => {
4836
5226
  const cancellations = [...acceptedQueueCancellations.values()];
@@ -5361,6 +5751,10 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5361
5751
  ): Promise<void> => {
5362
5752
  const current = lifecycleOwner?.state ?? active;
5363
5753
  if (!current) return;
5754
+ const imageRunHandle =
5755
+ type === "agent_start" && acceptedImages.size > 0
5756
+ ? options.terminalAbortSeams?.getActivePromptHandle()
5757
+ : undefined;
5364
5758
  const failureBatch = lifecycleOwner?.batch;
5365
5759
  const adoptLifecycleBatch = (batch: LifecycleBatch | undefined): void => {
5366
5760
  current.activeInvocation = batch?.invocations[0];
@@ -5478,6 +5872,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5478
5872
  adoptLifecycleBatch(batch);
5479
5873
  for (const entry of drained) {
5480
5874
  if (entry.kind !== "prompt") continue;
5875
+ bindAcceptedImage(entry.correlation, imageRunHandle);
5481
5876
  if (current.deadlineManager.isExpiring(entry.correlation))
5482
5877
  current.deadlineManager.captureExpiringRun(entry.correlation);
5483
5878
  else current.deadlineManager.onRunStarted(entry.correlation);
@@ -5987,8 +6382,13 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5987
6382
  }
5988
6383
  if (type === "agent_end" && isContinuingMidRunMaintenanceOutcome(maintenanceOutcome)) return;
5989
6384
  if (type === "agent_end") {
5990
- for (const invocation of transitions)
6385
+ for (const invocation of transitions) {
6386
+ // A deadline can capture this event while the exact run still owns
6387
+ // pending tools. Its settlement owner, not event capture, releases quota.
6388
+ if (!current.deadlineManager.shouldDeferTerminalTransition(invocation.correlation))
6389
+ releaseAcceptedImage(invocation.correlation);
5991
6390
  retireAcceptedQueueCancellation(acceptedQueueCancellations, invocation.correlation);
6391
+ }
5992
6392
  if (current.lifecycleEpoch !== eventLifecycleEpoch) {
5993
6393
  // A successor agent_start won the lifecycle race while this event's
5994
6394
  // durable transitions were awaiting persistence. Retire the ended
@@ -6408,6 +6808,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6408
6808
  const startRuntime = async (ctx: ExtensionContext): Promise<void> => {
6409
6809
  if (active) return;
6410
6810
  if (acceptedQueueCancellations.size > 0) await disposeAcceptedQueueCancellations();
6811
+ completeImageCleanups();
6411
6812
  if (active) return;
6412
6813
  const sessionId = ctx.sessionManager.getSessionId();
6413
6814
  const stateRoot = path.join(ctx.cwd, ".gjc", "state");
@@ -6419,16 +6820,38 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6419
6820
  (typeof ctx.sessionManager.getSessionFile === "function" ? ctx.sessionManager.getSessionFile() : undefined) ??
6420
6821
  resolveReconciliationSessionFile(undefined, stateRoot, sessionId);
6421
6822
  const sessionIdentity = `${sessionId}\u0000${sessionFile}`;
6823
+ const reconciliationInstrumentation = options.terminalAbortSeams?.onPersistenceInstrumentationForTests
6824
+ ? (event: ReconciliationStoreInstrumentationEvent) => {
6825
+ try {
6826
+ options.terminalAbortSeams?.onPersistenceInstrumentationForTests?.(event as never);
6827
+ } catch {}
6828
+ }
6829
+ : undefined;
6422
6830
  const reconciliationStore =
6423
6831
  options.terminalAbortSeams?.getReconciliationStore?.() ??
6424
- createReconciliationStore({ sessionFile, sessionId });
6425
- const reconciliation = createInvocationReconciliation({ store: reconciliationStore });
6832
+ createReconciliationStore({
6833
+ sessionFile,
6834
+ sessionId,
6835
+ onInstrumentationForTests: reconciliationInstrumentation,
6836
+ });
6837
+ const reconciliation = createInvocationReconciliation({
6838
+ store: reconciliationStore,
6839
+ onDeadlineRecoveryCheckpointForTests: options.terminalAbortSeams?.onDeadlineRecoveryCheckpointForTests,
6840
+ onPersistenceInstrumentationForTests: options.terminalAbortSeams?.onPersistenceInstrumentationForTests,
6841
+ });
6426
6842
  await reconciliation.hydrate();
6427
6843
  const steerReconciliation = createKindAwareReconciliation({
6428
6844
  store: reconciliationStore as never,
6429
6845
  ownedKinds: ["steer"],
6430
6846
  });
6431
6847
  await steerReconciliation.hydrateFromStore();
6848
+ const recordDeadlineRecoveryCheckpoint = (checkpoint: Omit<SdkOnlyDeadlineRecoveryCheckpoint, "at">): void => {
6849
+ try {
6850
+ options.terminalAbortSeams?.onDeadlineRecoveryCheckpointForTests?.({ ...checkpoint, at: Date.now() });
6851
+ } catch {
6852
+ // A test observer must never participate in deadline correctness.
6853
+ }
6854
+ };
6432
6855
  const deadlineManager = new PromptDeadlineManager({
6433
6856
  reconciliation,
6434
6857
  getLeaseMs: () => resolveSdkPromptDeadlineMs(options.settings?.get("sdk.promptDeadlineMs" as never)),
@@ -6448,6 +6871,9 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6448
6871
  const seams = options.terminalAbortSeams;
6449
6872
  const handle = seams?.getActivePromptHandle();
6450
6873
  const epoch = seams?.getTerminalTurnEpoch();
6874
+ // Capture lifecycle ownership independently of settlement proof. Without
6875
+ // a tool-set observer the later terminalization hook still fails closed,
6876
+ // but a matching real end must remain private and recoverable meanwhile.
6451
6877
  if (!seams || !handle || epoch === undefined) return;
6452
6878
  const existing = deadlineTerminalizationObservations.get(key);
6453
6879
  if (existing) return () => cleanupDeadlineTerminalizationObservation(key, existing);
@@ -6495,8 +6921,36 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6495
6921
  const observation = deadlineTerminalizationObservations.get(target);
6496
6922
  const seams = options.terminalAbortSeams;
6497
6923
  const pendingToolExecutions = seams?.pendingToolExecutions;
6498
- if (!observation || !seams || !pendingToolExecutions) return "uncertain";
6499
- const pendingTools = () => pendingToolExecutions(observation.handle);
6924
+ recordDeadlineRecoveryCheckpoint({
6925
+ stage: "terminalization-start",
6926
+ correlation,
6927
+ eventCaptured: observation?.eventCaptured,
6928
+ eventPrepared: observation?.eventPrepared,
6929
+ terminalCommitted: observation?.terminalCommitted,
6930
+ });
6931
+ let pendingToolCount: number | undefined;
6932
+ const finish = (result: PromptDeadlineTerminalization, reason: string): PromptDeadlineTerminalization => {
6933
+ recordDeadlineRecoveryCheckpoint({
6934
+ stage: "terminalization-result",
6935
+ correlation,
6936
+ result,
6937
+ reason,
6938
+ eventCaptured: observation?.eventCaptured,
6939
+ eventPrepared: observation?.eventPrepared,
6940
+ terminalCommitted: observation?.terminalCommitted,
6941
+ pendingToolCount,
6942
+ });
6943
+ // Notify deadline manager about termination result to adjust retry strategy
6944
+ deadlineManager.notifyTerminationResult(correlation, result, reason);
6945
+ return result;
6946
+ };
6947
+ if (!observation || !seams || !pendingToolExecutions)
6948
+ return finish("uncertain", "missing-observation-or-tool-observer");
6949
+ const pendingTools = () => {
6950
+ const pending = pendingToolExecutions(observation.handle);
6951
+ pendingToolCount = pending.length;
6952
+ return pending;
6953
+ };
6500
6954
  const awaitEventPreparation = async (): Promise<boolean> => {
6501
6955
  if (observation.eventPrepared) return true;
6502
6956
  const captured = await Promise.race([
@@ -6507,7 +6961,15 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6507
6961
  };
6508
6962
  if (observation.eventCaptured) {
6509
6963
  const prepared = await awaitEventPreparation();
6510
- return prepared && pendingTools().length === 0 ? "settled" : "uncertain";
6964
+ pendingToolCount = pendingTools().length;
6965
+ return finish(
6966
+ prepared && pendingToolCount === 0 ? "settled" : "uncertain",
6967
+ !prepared
6968
+ ? "captured-event-not-prepared"
6969
+ : pendingToolCount > 0
6970
+ ? "tools-still-pending"
6971
+ : "captured-event-and-tools-settled",
6972
+ );
6511
6973
  }
6512
6974
 
6513
6975
  if (!observation.eventCaptured) {
@@ -6521,13 +6983,13 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6521
6983
  entry => lifecycleCorrelationKey(entry.correlation) === target,
6522
6984
  )
6523
6985
  )
6524
- return "uncertain";
6986
+ return finish("uncertain", "lifecycle-batch-no-longer-owns-correlation");
6525
6987
  if (
6526
6988
  !isCurrent() ||
6527
6989
  seams.getActivePromptHandle() !== observation.handle ||
6528
6990
  seams.getTerminalTurnEpoch() !== observation.epoch
6529
6991
  )
6530
- return "uncertain";
6992
+ return finish("uncertain", "lease-or-active-run-mismatch");
6531
6993
  }
6532
6994
  await waitForToolCallBoundary({
6533
6995
  pending: pendingTools,
@@ -6536,14 +6998,22 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6536
6998
  });
6537
6999
  if (observation.eventCaptured) {
6538
7000
  const prepared = await awaitEventPreparation();
6539
- return prepared && pendingTools().length === 0 ? "settled" : "uncertain";
7001
+ pendingToolCount = pendingTools().length;
7002
+ return finish(
7003
+ prepared && pendingToolCount === 0 ? "settled" : "uncertain",
7004
+ !prepared
7005
+ ? "captured-event-not-prepared"
7006
+ : pendingToolCount > 0
7007
+ ? "tools-still-pending"
7008
+ : "captured-event-and-tools-settled",
7009
+ );
6540
7010
  }
6541
7011
  if (
6542
7012
  !isCurrent() ||
6543
7013
  seams.getActivePromptHandle() !== observation.handle ||
6544
7014
  seams.getTerminalTurnEpoch() !== observation.epoch
6545
7015
  )
6546
- return "uncertain";
7016
+ return finish("uncertain", "lease-or-active-run-mismatch-after-tool-boundary");
6547
7017
 
6548
7018
  let steeringSnapshotToken: number | undefined;
6549
7019
  let proof: { status: string; terminalScope?: unknown } | undefined;
@@ -6560,7 +7030,15 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6560
7030
  });
6561
7031
  if (proof.status !== "settled") {
6562
7032
  const prepared = await awaitEventPreparation();
6563
- return prepared && pendingTools().length === 0 ? "settled" : "uncertain";
7033
+ pendingToolCount = pendingTools().length;
7034
+ return finish(
7035
+ prepared && pendingToolCount === 0 ? "settled" : "uncertain",
7036
+ !prepared
7037
+ ? "abort-proof-not-settled-and-event-not-prepared"
7038
+ : pendingToolCount > 0
7039
+ ? "tools-still-pending"
7040
+ : "captured-event-and-tools-settled",
7041
+ );
6564
7042
  }
6565
7043
  const terminalScope = proof.terminalScope as
6566
7044
  | { abortedAttemptEpoch?: unknown; lineageIdHash?: unknown }
@@ -6570,17 +7048,26 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6570
7048
  typeof terminalScope.lineageIdHash !== "string" ||
6571
7049
  isOwnedAttemptRegistrationIncomplete(terminalScope.lineageIdHash, observation.epoch)
6572
7050
  )
6573
- return "uncertain";
7051
+ return finish("uncertain", "terminal-scope-mismatch");
6574
7052
  const exactJobs = findOwnedRegistrationsForTurn(terminalScope.lineageIdHash, observation.epoch);
6575
7053
  if (exactJobs.length > 0) {
6576
7054
  const endpointId = exactJobs[0]?.endpointId;
6577
7055
  const manager = AsyncJobManager.forEndpoint(endpointId) ?? AsyncJobManager.instance();
6578
- if (!manager || (await settleOwnedWork(manager, exactJobs, 500)) !== "stopped") return "uncertain";
7056
+ if (!manager || (await settleOwnedWork(manager, exactJobs, 500)) !== "stopped")
7057
+ return finish("uncertain", "owned-work-not-stopped");
6579
7058
  }
6580
7059
  const prepared = await awaitEventPreparation();
6581
- return prepared && pendingTools().length === 0 ? "settled" : "uncertain";
7060
+ pendingToolCount = pendingTools().length;
7061
+ return finish(
7062
+ prepared && pendingToolCount === 0 ? "settled" : "uncertain",
7063
+ !prepared
7064
+ ? "captured-event-not-prepared"
7065
+ : pendingToolCount > 0
7066
+ ? "tools-still-pending"
7067
+ : "captured-event-and-tools-settled",
7068
+ );
6582
7069
  } catch {
6583
- return "uncertain";
7070
+ return finish("uncertain", "terminal-abort-error");
6584
7071
  } finally {
6585
7072
  if (steeringSnapshotToken !== undefined && proof?.status !== "settled")
6586
7073
  seams.discardTerminalAbortSteeringSnapshot?.(steeringSnapshotToken);
@@ -6592,21 +7079,57 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6592
7079
  const terminalOutcome = observation
6593
7080
  ? deadlineOutcomeAfterAbort(correlation, observation.terminalOutcome, observation.terminalHasActivity)
6594
7081
  : undefined;
6595
- if (!observation?.eventCaptured || !observation.eventPrepared || terminalOutcome === undefined)
7082
+ let terminalIsCurrent: boolean | undefined;
7083
+ let pendingToolCount = -1;
7084
+ const recordPublicationResult = (
7085
+ result: SdkOnlyDeadlineRecoveryCheckpoint["result"],
7086
+ reason: string,
7087
+ errorCodeValue?: string,
7088
+ ): void => {
7089
+ recordDeadlineRecoveryCheckpoint({
7090
+ stage: "publication-result",
7091
+ correlation,
7092
+ result,
7093
+ reason,
7094
+ eventCaptured: observation?.eventCaptured,
7095
+ eventPrepared: observation?.eventPrepared,
7096
+ terminalCommitted: observation?.terminalCommitted,
7097
+ leaseCurrent: terminalIsCurrent,
7098
+ pendingToolCount,
7099
+ errorCode: errorCodeValue,
7100
+ });
7101
+ };
7102
+ recordDeadlineRecoveryCheckpoint({
7103
+ stage: "publication-start",
7104
+ correlation,
7105
+ eventCaptured: observation?.eventCaptured,
7106
+ eventPrepared: observation?.eventPrepared,
7107
+ terminalCommitted: observation?.terminalCommitted,
7108
+ });
7109
+ if (!observation?.eventCaptured || !observation.eventPrepared || terminalOutcome === undefined) {
7110
+ recordPublicationResult("rejected", "missing-captured-terminal-outcome");
6596
7111
  return false;
7112
+ }
6597
7113
  if (!observation.terminalCommitted) {
6598
7114
  const pendingTools = options.terminalAbortSeams?.pendingToolExecutions;
6599
- let terminalIsCurrent = false;
6600
- let pendingToolCount = -1;
6601
7115
  try {
6602
7116
  terminalIsCurrent = isCurrent();
6603
7117
  if (pendingTools) pendingToolCount = pendingTools(observation.handle).length;
6604
- } catch {
7118
+ } catch (error) {
7119
+ recordPublicationResult("rejected", "publication-precheck-threw", errorCode(error));
7120
+ return false;
7121
+ }
7122
+ if (!terminalIsCurrent || !pendingTools || pendingToolCount > 0) {
7123
+ recordPublicationResult("rejected", "publication-precheck-rejected");
6605
7124
  return false;
6606
7125
  }
6607
- if (!terminalIsCurrent || !pendingTools || pendingToolCount > 0) return false;
6608
7126
  try {
6609
- if (!isCurrent() || pendingTools(observation.handle).length > 0) return false;
7127
+ terminalIsCurrent = isCurrent();
7128
+ pendingToolCount = pendingTools(observation.handle).length;
7129
+ if (!terminalIsCurrent || pendingToolCount > 0) {
7130
+ recordPublicationResult("rejected", "publication-recheck-rejected");
7131
+ return false;
7132
+ }
6610
7133
  // Commit the authoritative Q26 result before exposing its terminal
6611
7134
  // boundary; a crash between publication and this write would strand
6612
7135
  // restart recovery with only a hidden pending claim.
@@ -6619,40 +7142,39 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6619
7142
  observation.terminalOutcome = terminalOutcome;
6620
7143
  observation.terminalCommitted = true;
6621
7144
  observation.clearUnrecordedFailure?.();
6622
- } catch {
7145
+ } catch (error) {
7146
+ recordPublicationResult("rejected", "terminal-transition-persist-failed", errorCode(error));
6623
7147
  return false;
6624
7148
  }
6625
7149
  }
6626
7150
  const published = publishDeadlineTerminalBoundary(observation, correlation, terminalOutcome);
7151
+ recordPublicationResult(published ? "published" : "not-published", "terminal-boundary-result");
6627
7152
  return { outcome: terminalOutcome, published } satisfies PromptDeadlinePublicationResult;
6628
7153
  },
6629
7154
  // Persist the agent's uncommitted work before the retirement below tears
6630
7155
  // the session down (#5583). Best effort by contract: failures are logged
6631
7156
  // inside the flush and the deadline outcome is unaffected.
6632
7157
  onDeadlineExceeded: async (_correlation, signal, isCurrent) => {
6633
- if (options.settings?.get("sdk.flushWorktreeOnDeadline" as never) === false) return;
6634
- // `has` is true only for a value the user actually wrote, so this
6635
- // separates an explicit opt-in from the schema default. The flush only
6636
- // honours the default inside a linked worktree the session owns.
6637
- const configured = options.settings?.get("sdk.flushWorktreeOnDeadline" as never);
6638
- const hasExplicitSetting =
6639
- typeof options.settings?.has === "function"
6640
- ? options.settings.has("sdk.flushWorktreeOnDeadline" as never)
6641
- : configured === true;
6642
- const explicitOptIn = hasExplicitSetting === true && configured === true;
6643
- await flushWorktreeOnPromptDeadline(ctx.cwd, {
6644
- agentDir: options.settings?.getAgentDir(),
6645
- explicitOptIn,
6646
- isCurrent,
6647
- signal,
6648
- });
7158
+ // Project `.gjc` files are not an opt-in. The shared helper reads the
7159
+ // user/global layer and runtime overrides only (#5583).
7160
+ await autosaveWorktreeOnPromptDeadline(ctx.cwd, options.settings, { isCurrent, signal });
6649
7161
  },
6650
7162
  onExpired: correlation => {
7163
+ // The manager invokes retirement only after confirmed settlement and
7164
+ // durable terminalization (including recovered queue removals), never
7165
+ // merely because an inactivity timer fired or execution is uncertain.
7166
+ releaseAcceptedImage(correlation);
6651
7167
  retireAcceptedQueueCancellation(acceptedQueueCancellations, correlation, true);
6652
7168
  const owner = lifecycleOwnerHolder.state;
6653
- if (!owner) return;
6654
- removeLifecycleReferences(owner, correlation);
6655
- maybeRetireLifecycleOwner(owner);
7169
+ if (owner) {
7170
+ removeLifecycleReferences(owner, correlation);
7171
+ maybeRetireLifecycleOwner(owner);
7172
+ }
7173
+ recordDeadlineRecoveryCheckpoint({
7174
+ stage: "deadline-recovery-retired",
7175
+ correlation,
7176
+ terminalCommitted: true,
7177
+ });
6656
7178
  },
6657
7179
  });
6658
7180
  const pending: Array<{
@@ -6706,6 +7228,9 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6706
7228
  ctx,
6707
7229
  id: sessionId,
6708
7230
  api,
7231
+ imageUploadsAvailable:
7232
+ typeof transport.isConnectionOpen === "function" &&
7233
+ typeof options.terminalAbortSeams?.waitForRunSettlement === "function",
6709
7234
  reconciliation,
6710
7235
  turnResultLookup: selector => reconciliation.lookupResult(selector.kind, selector),
6711
7236
  steerStatusLookup: selector => steerReconciliation.lookupSteer(selector),
@@ -6836,7 +7361,10 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6836
7361
  });
6837
7362
  // 3. Release recovery ownership ONLY after durable terminalization.
6838
7363
  deadlineManager.clear(correlation);
6839
- if (kind === "prompt") retireAcceptedQueueCancellation(acceptedQueueCancellations, correlation);
7364
+ if (kind === "prompt") {
7365
+ releaseAcceptedImage(correlation);
7366
+ retireAcceptedQueueCancellation(acceptedQueueCancellations, correlation);
7367
+ }
6840
7368
  return true;
6841
7369
  } catch (transitionError) {
6842
7370
  // Keep prompt recovery leased; skill recovery has no prompt lease.
@@ -6854,6 +7382,14 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6854
7382
  return attempt(3);
6855
7383
  };
6856
7384
 
7385
+ // This is a private SDK transport facility. A frame-observer cache cannot
7386
+ // establish that the authenticated connection still exists at redemption.
7387
+ const imageUploads =
7388
+ typeof transport.isConnectionOpen === "function" &&
7389
+ typeof options.terminalAbortSeams?.waitForRunSettlement === "function"
7390
+ ? new PromptImageUploadStore(connectionId => transport.isConnectionOpen?.(connectionId) === true)
7391
+ : undefined;
7392
+ currentImageUploads = imageUploads;
6857
7393
  const controlSurface = createControlSurface(
6858
7394
  ctx,
6859
7395
  api,
@@ -6890,7 +7426,13 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6890
7426
  },
6891
7427
  correlation => deadlineManager.onAccepted(correlation),
6892
7428
  steerReconciliation,
7429
+ imageUploads,
6893
7430
  (kind, correlation, connectionId, sdkRunToken, promotion) => {
7431
+ if (!promotion?.removed) {
7432
+ if (promotion?.startsOwnRun === false)
7433
+ bindAcceptedImage(correlation, options.terminalAbortSeams?.getActivePromptHandle());
7434
+ else awaitAcceptedImageAdoption(correlation);
7435
+ }
6894
7436
  const bindPromotedToken = (batch?: LifecycleBatch): void => {
6895
7437
  const owner = lifecycleOwnerHolder.state;
6896
7438
  if (sdkRunToken && owner)
@@ -6946,6 +7488,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
6946
7488
  // with it.
6947
7489
  const current = active;
6948
7490
  if (current?.lifecycleActive) {
7491
+ bindAcceptedImage(correlation, options.terminalAbortSeams?.getActivePromptHandle());
6949
7492
  // If this correlation was already admitted as own-run via the
6950
7493
  // idle snapshot (onAccepted -> pending) but later diverted to
6951
7494
  // steering and consumed in-run, move it from pending to the
@@ -7106,7 +7649,47 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
7106
7649
  throw error;
7107
7650
  }
7108
7651
  },
7652
+ (correlation, release) => {
7653
+ if (imageUploads)
7654
+ acceptedImages.set(imageKey(correlation), {
7655
+ store: imageUploads,
7656
+ release,
7657
+ submissionPending: true,
7658
+ });
7659
+ },
7660
+ settleAcceptedImageSubmission,
7661
+ releaseAcceptedImage,
7109
7662
  acceptedQueueCancellations,
7663
+ (kind, correlation, failure) => {
7664
+ const record = reconciliation.lookup(kind, correlation) as { status?: string };
7665
+ const reservation = acceptedImages.get(imageKey(correlation));
7666
+ const terminal = record.status === "terminal_ok" || record.status === "failed";
7667
+ if (record.status !== "accepted" && !(terminal && reservation)) return false;
7668
+ // A known execution/handle retains exact-ledger authority even if a
7669
+ // contradictory callback arrives later. Never downgrade adoption.
7670
+ if (reservation?.executionStarted || reservation?.handle) return false;
7671
+ const adopted = active?.openLifecycleBatches.some(batch =>
7672
+ [...batch.invocations, ...batch.attachedInvocations].some(
7673
+ entry => entry.kind === kind && imageKey(entry.correlation) === imageKey(correlation),
7674
+ ),
7675
+ );
7676
+ if (adopted) return false;
7677
+ const index = pending.findIndex(entry => entry.kind === kind && entry.correlation === correlation);
7678
+ if (index >= 0) {
7679
+ const [removed] = pending.splice(index, 1);
7680
+ if (removed) lifecycleRunOwners.delete(removed.sdkRunToken);
7681
+ }
7682
+ if (reservation) {
7683
+ reservation.awaitingAdoption = false;
7684
+ // Ordinary retirement still waits for an outstanding enqueue producer.
7685
+ retireAcceptedImage(imageKey(correlation));
7686
+ }
7687
+ // Keep the existing durable-first failure/recovery owner; this is not
7688
+ // queue removal, and cannot publish cancellation or synthetic success.
7689
+ if (!terminal)
7690
+ void terminalizeAbandonedSubmission(kind, correlation, rejectionRecoveryIntent(failure.error));
7691
+ return true;
7692
+ },
7110
7693
  );
7111
7694
  const installProviderDefinitions = (capability: string, definitions: unknown): void => {
7112
7695
  if (capability === "permission") {
@@ -7174,6 +7757,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
7174
7757
  };
7175
7758
  runtime = new SessionSdkSessionRuntime({
7176
7759
  transport,
7760
+ onDisconnected: connectionId => imageUploads?.disconnect(connectionId),
7177
7761
  eventRevision: () =>
7178
7762
  typeof (ctx as Partial<ExtensionContext>).getTranscript === "function"
7179
7763
  ? ctx.getTranscript().length
@@ -7624,6 +8208,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
7624
8208
  } catch (error) {
7625
8209
  runtimeOwner.quiesceInput();
7626
8210
  runtimeOwner.fenceGateResolutions();
8211
+ imageUploads?.close();
7627
8212
  active = undefined;
7628
8213
  let queueCancellationFailure: unknown;
7629
8214
  try {
@@ -7682,7 +8267,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
7682
8267
  }
7683
8268
  cursors.close();
7684
8269
  await revisions.close().catch(() => undefined);
7685
- if (queueCancellationFailure !== undefined)
8270
+ if (queueCancellationFailure !== undefined) {
8271
+ if (imageUploads) pendingImageCleanups.add(imageUploads);
7686
8272
  throw Object.assign(
7687
8273
  new AggregateError(
7688
8274
  [error, queueCancellationFailure],
@@ -7690,14 +8276,25 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
7690
8276
  ),
7691
8277
  { code: "sdk_reconciliation_teardown_failed" },
7692
8278
  );
8279
+ }
8280
+ if (currentImageUploads === imageUploads) currentImageUploads = undefined;
8281
+ releaseImagesForStore(imageUploads);
7693
8282
  throw error;
7694
8283
  }
7695
8284
  };
7696
8285
  const stopActive = async (cancelSkillRecovery = false, unregisterReason?: "detached_idle"): Promise<void> => {
7697
8286
  const current = active;
7698
- if (!current) return;
8287
+ if (!current) {
8288
+ if (pendingImageCleanups.size > 0) {
8289
+ await disposeAcceptedQueueCancellations();
8290
+ completeImageCleanups();
8291
+ }
8292
+ return;
8293
+ }
7699
8294
  current.quiesceInput();
7700
8295
  current.fenceGateResolutions();
8296
+ const imageUploads = currentImageUploads;
8297
+ imageUploads?.close();
7701
8298
  let queueCancellationFailure: unknown;
7702
8299
  try {
7703
8300
  await disposeAcceptedQueueCancellations();
@@ -7763,6 +8360,10 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
7763
8360
  allowLockContention: cancelSkillRecovery,
7764
8361
  ...(unregisterReason === undefined ? {} : { unregisterReason }),
7765
8362
  });
8363
+ if (queueCancellationFailure === undefined) {
8364
+ if (currentImageUploads === imageUploads) currentImageUploads = undefined;
8365
+ releaseImagesForStore(imageUploads);
8366
+ } else if (imageUploads) pendingImageCleanups.add(imageUploads);
7766
8367
  } catch (error) {
7767
8368
  // Keep the immutable owner available for a retry when transport teardown
7768
8369
  // fails after quiescing. Clearing `active` before stop prevents a second