@gajae-code/coding-agent 0.17.2 → 0.17.5

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 (390) hide show
  1. package/CHANGELOG.md +267 -0
  2. package/dist/types/cli/daemon-cli.d.ts +3 -1
  3. package/dist/types/cli/github-release.d.ts +17 -0
  4. package/dist/types/cli/public-command-entry.d.ts +56 -0
  5. package/dist/types/cli/public-command-errors.d.ts +126 -0
  6. package/dist/types/cli/public-command-evidence.d.ts +99 -0
  7. package/dist/types/cli/public-command-help.d.ts +65 -0
  8. package/dist/types/cli/public-command-registry.d.ts +69 -0
  9. package/dist/types/cli/session-picker.d.ts +1 -1
  10. package/dist/types/cli/setup-cli.d.ts +1 -0
  11. package/dist/types/cli/skills-cli.d.ts +5 -0
  12. package/dist/types/cli.d.ts +3 -0
  13. package/dist/types/commands/daemon.d.ts +4 -50
  14. package/dist/types/commands/sdk.d.ts +20 -37
  15. package/dist/types/commands/setup.d.ts +3 -0
  16. package/dist/types/commands/skills.d.ts +9 -0
  17. package/dist/types/config/autorouting-tier-map.d.ts +1049 -3
  18. package/dist/types/config/file-lock.d.ts +13 -3
  19. package/dist/types/config/model-profile-activation.d.ts +45 -1
  20. package/dist/types/config/model-registry.d.ts +14 -3
  21. package/dist/types/config/model-resolver.d.ts +1 -1
  22. package/dist/types/config/settings-schema.d.ts +11 -0
  23. package/dist/types/coordinator-mcp/question-state.d.ts +6 -1
  24. package/dist/types/daemon/operator-contract.d.ts +5 -1
  25. package/dist/types/eval/js/worker-protocol.d.ts +2 -0
  26. package/dist/types/exec/bash-executor.d.ts +2 -0
  27. package/dist/types/extensibility/extensions/types.d.ts +12 -0
  28. package/dist/types/extensibility/gjc-plugins/registry.d.ts +4 -0
  29. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +11 -2
  30. package/dist/types/extensibility/runtime-skill-discovery.d.ts +18 -0
  31. package/dist/types/extensibility/shared-events.d.ts +3 -1
  32. package/dist/types/gjc-runtime/session-state-lock.d.ts +61 -0
  33. package/dist/types/gjc-runtime/state-writer.d.ts +14 -1
  34. package/dist/types/harness-control-plane/control-endpoint.d.ts +4 -1
  35. package/dist/types/harness-control-plane/finalize.d.ts +14 -3
  36. package/dist/types/internal-urls/embedded-protocol.d.ts +9 -0
  37. package/dist/types/internal-urls/index.d.ts +1 -0
  38. package/dist/types/internal-urls/parse.d.ts +9 -1
  39. package/dist/types/main.d.ts +23 -1
  40. package/dist/types/modes/acp/acp-agent.d.ts +55 -1
  41. package/dist/types/modes/acp/startup-provenance.d.ts +16 -0
  42. package/dist/types/modes/components/custom-provider-wizard.d.ts +19 -1
  43. package/dist/types/modes/components/session-selector.d.ts +5 -2
  44. package/dist/types/modes/components/user-message-selector.d.ts +5 -2
  45. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  46. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  47. package/dist/types/modes/interactive-mode.d.ts +1 -1
  48. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +11 -1
  49. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +14 -1
  50. package/dist/types/modes/types.d.ts +2 -2
  51. package/dist/types/runtime/process-lifecycle.d.ts +2 -0
  52. package/dist/types/runtime-mcp/loader.d.ts +2 -0
  53. package/dist/types/runtime-mcp/manager.d.ts +69 -6
  54. package/dist/types/runtime-mcp/pool.d.ts +17 -0
  55. package/dist/types/runtime-mcp/redaction.d.ts +20 -0
  56. package/dist/types/runtime-mcp/startup-policy.d.ts +6 -0
  57. package/dist/types/runtime-mcp/tool-bridge.d.ts +9 -0
  58. package/dist/types/runtime-mcp/transports/http.d.ts +11 -0
  59. package/dist/types/sdk/broker/broker.d.ts +17 -4
  60. package/dist/types/sdk/broker/ensure.d.ts +3 -0
  61. package/dist/types/sdk/broker/identity.d.ts +5 -1
  62. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +23 -3
  63. package/dist/types/sdk/broker/lifecycle.d.ts +15 -8
  64. package/dist/types/sdk/broker/managed-task-dag.d.ts +1335 -0
  65. package/dist/types/sdk/broker/managed-task-verification.d.ts +37 -0
  66. package/dist/types/sdk/broker/runtime.d.ts +1 -1
  67. package/dist/types/sdk/broker/session-index.d.ts +22 -3
  68. package/dist/types/sdk/broker/spawn-authority.d.ts +12 -0
  69. package/dist/types/sdk/broker/startup-failure.d.ts +4 -0
  70. package/dist/types/sdk/broker/stderr-drainer.d.ts +13 -0
  71. package/dist/types/sdk/bus/index.d.ts +19 -0
  72. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -1
  73. package/dist/types/sdk/bus/telegram-daemon.d.ts +5 -0
  74. package/dist/types/sdk/cli/master-cli.d.ts +6 -10
  75. package/dist/types/sdk/cli/rows.d.ts +19 -1
  76. package/dist/types/sdk/cli/session-cli.d.ts +22 -5
  77. package/dist/types/sdk/client/client.d.ts +2 -0
  78. package/dist/types/sdk/guides/cli.d.ts +3 -4
  79. package/dist/types/sdk/guides/verify.d.ts +2 -0
  80. package/dist/types/sdk/host/host.d.ts +9 -0
  81. package/dist/types/sdk/host/session-runtime.d.ts +31 -0
  82. package/dist/types/sdk/host/types.d.ts +10 -0
  83. package/dist/types/sdk/index.d.ts +1 -0
  84. package/dist/types/sdk/prompt-deadline-flush.d.ts +85 -0
  85. package/dist/types/sdk/prompt-deadline-manager.d.ts +37 -0
  86. package/dist/types/sdk/prompt-tool-boundary.d.ts +73 -0
  87. package/dist/types/sdk/router/session-router.d.ts +2 -0
  88. package/dist/types/sdk/session.d.ts +3 -3
  89. package/dist/types/sdk/startup-capability.d.ts +2 -0
  90. package/dist/types/sdk/transport/relay.d.ts +1 -0
  91. package/dist/types/sdk/transport/serve-cli.d.ts +48 -4
  92. package/dist/types/sdk/transport/stdio.d.ts +5 -1
  93. package/dist/types/session/agent-session.d.ts +175 -23
  94. package/dist/types/session/fallback-chain-controller.d.ts +6 -6
  95. package/dist/types/session/fold-coordinator.d.ts +17 -3
  96. package/dist/types/session/internal/managed-session-storage.d.ts +7 -1
  97. package/dist/types/session/session-manager.d.ts +64 -1
  98. package/dist/types/session/session-work-lease.d.ts +14 -0
  99. package/dist/types/setup/model-onboarding-guidance.d.ts +1 -1
  100. package/dist/types/setup/provider-onboarding.d.ts +81 -1
  101. package/dist/types/task/types.d.ts +2 -0
  102. package/dist/types/tools/bash-interactive.d.ts +1 -0
  103. package/dist/types/tools/bash.d.ts +37 -11
  104. package/dist/types/tools/fetch.d.ts +10 -1
  105. package/dist/types/tools/job.d.ts +1 -1
  106. package/dist/types/tools/steer-fold.d.ts +31 -0
  107. package/dist/types/tools/subagent.d.ts +8 -1
  108. package/dist/types/utils/git.d.ts +28 -2
  109. package/dist/types/utils/mupdf-embedded.d.ts +2 -0
  110. package/dist/types/utils/mupdf.d.ts +4 -0
  111. package/dist/types/utils/session-title-generation.d.ts +6 -0
  112. package/dist/types/utils/title-generator.d.ts +11 -2
  113. package/dist/types/web/scrapers/types.d.ts +3 -0
  114. package/package.json +18 -10
  115. package/scripts/build-binary.ts +9 -3
  116. package/scripts/compile-args.ts +0 -9
  117. package/scripts/embed-mupdf.ts +52 -0
  118. package/scripts/generate-sdk-operation-inventory.ts +31 -1
  119. package/scripts/vendor-markit.ts +196 -0
  120. package/src/async/job-manager.ts +22 -1
  121. package/src/cli/daemon-cli.ts +53 -18
  122. package/src/cli/github-release.ts +87 -5
  123. package/src/cli/mcp-cli.ts +38 -4
  124. package/src/cli/public-command-entry.ts +450 -0
  125. package/src/cli/public-command-errors.ts +581 -0
  126. package/src/cli/public-command-evidence.ts +717 -0
  127. package/src/cli/public-command-help.ts +360 -0
  128. package/src/cli/public-command-registry.ts +541 -0
  129. package/src/cli/session-picker.ts +2 -1
  130. package/src/cli/setup-cli.ts +10 -1
  131. package/src/cli/skills-cli.ts +46 -2
  132. package/src/cli/stats-cli.ts +11 -1
  133. package/src/cli.ts +61 -1
  134. package/src/commands/daemon.ts +65 -69
  135. package/src/commands/sdk.ts +175 -117
  136. package/src/commands/setup.ts +2 -0
  137. package/src/commands/skills.ts +11 -0
  138. package/src/config/autorouting-tier-map.ts +543 -3
  139. package/src/config/file-lock.ts +206 -65
  140. package/src/config/model-discovery-manager.ts +4 -4
  141. package/src/config/model-profile-activation.ts +417 -66
  142. package/src/config/model-registry.ts +254 -36
  143. package/src/config/model-resolver.ts +6 -1
  144. package/src/config/settings-schema.ts +26 -2
  145. package/src/coordinator-mcp/policy.ts +10 -5
  146. package/src/coordinator-mcp/question-gate-codec.ts +17 -0
  147. package/src/coordinator-mcp/question-state.ts +179 -9
  148. package/src/coordinator-mcp/server.ts +521 -87
  149. package/src/coordinator-mcp/session-reaper.ts +6 -4
  150. package/src/crash/record-loader.ts +6 -2
  151. package/src/daemon/operator-contract.ts +26 -12
  152. package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
  153. package/src/eval/js/context-manager.ts +8 -8
  154. package/src/eval/js/worker-core.ts +9 -4
  155. package/src/eval/js/worker-protocol.ts +2 -0
  156. package/src/exec/bash-executor.ts +3 -0
  157. package/src/extensibility/extensions/runner.ts +52 -4
  158. package/src/extensibility/extensions/types.ts +10 -0
  159. package/src/extensibility/gjc-plugins/registry.ts +196 -37
  160. package/src/extensibility/gjc-plugins/runtime-adapters.ts +46 -8
  161. package/src/extensibility/runtime-skill-discovery.ts +61 -3
  162. package/src/extensibility/shared-events.ts +3 -1
  163. package/src/gjc-runtime/session-state-lock.ts +165 -24
  164. package/src/gjc-runtime/state-writer.ts +317 -6
  165. package/src/gjc-runtime/ultragoal-succession.ts +31 -3
  166. package/src/harness-control-plane/control-endpoint.ts +24 -6
  167. package/src/harness-control-plane/finalize.ts +312 -25
  168. package/src/harness-control-plane/owner.ts +323 -89
  169. package/src/internal-urls/docs-index.generated.ts +23 -20
  170. package/src/internal-urls/embedded-protocol.ts +83 -0
  171. package/src/internal-urls/index.ts +1 -0
  172. package/src/internal-urls/parse.ts +21 -1
  173. package/src/internal-urls/router.ts +4 -2
  174. package/src/main.ts +192 -74
  175. package/src/modes/acp/acp-agent.ts +973 -56
  176. package/src/modes/acp/startup-provenance.ts +59 -0
  177. package/src/modes/components/custom-provider-wizard.ts +308 -7
  178. package/src/modes/components/gjc-bundle-settings.ts +21 -1
  179. package/src/modes/components/oauth-selector.ts +9 -0
  180. package/src/modes/components/session-selector.ts +47 -4
  181. package/src/modes/components/tool-status-header.ts +72 -6
  182. package/src/modes/components/user-message-selector.ts +57 -15
  183. package/src/modes/controllers/command-controller.ts +58 -32
  184. package/src/modes/controllers/extension-ui-controller.ts +33 -7
  185. package/src/modes/controllers/input-controller.ts +4 -0
  186. package/src/modes/controllers/runtime-mcp-command-controller.ts +1 -1
  187. package/src/modes/controllers/selector-controller.ts +161 -15
  188. package/src/modes/interactive-mode.ts +21 -7
  189. package/src/modes/runtime-init.ts +1 -0
  190. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +124 -7
  191. package/src/modes/shared/agent-wire/workflow-gate-types.ts +16 -2
  192. package/src/modes/types.ts +2 -2
  193. package/src/prompts/agent-fragments/browser-aside.md +1 -0
  194. package/src/prompts/system/title-system.md +1 -1
  195. package/src/prompts/tools/bash.md +1 -1
  196. package/src/prompts/tools/job.md +1 -1
  197. package/src/prompts/tools/read.md +2 -0
  198. package/src/runtime/network-prewarm-service.test.ts +36 -1
  199. package/src/runtime/network-prewarm-service.ts +28 -3
  200. package/src/runtime/process-lifecycle.ts +22 -11
  201. package/src/runtime-mcp/config.ts +16 -1
  202. package/src/runtime-mcp/loader.ts +6 -3
  203. package/src/runtime-mcp/manager.ts +845 -109
  204. package/src/runtime-mcp/pool.test.ts +31 -0
  205. package/src/runtime-mcp/pool.ts +304 -28
  206. package/src/runtime-mcp/redaction.ts +42 -0
  207. package/src/runtime-mcp/startup-policy.ts +7 -0
  208. package/src/runtime-mcp/tool-bridge.ts +71 -0
  209. package/src/runtime-mcp/transports/http.ts +108 -25
  210. package/src/sdk/acp/adapter.ts +27 -8
  211. package/src/sdk/broker/broker.ts +1431 -198
  212. package/src/sdk/broker/ensure.ts +21 -3
  213. package/src/sdk/broker/identity.ts +22 -1
  214. package/src/sdk/broker/lifecycle-ledger.ts +95 -31
  215. package/src/sdk/broker/lifecycle.ts +690 -141
  216. package/src/sdk/broker/managed-task-dag.ts +1927 -0
  217. package/src/sdk/broker/managed-task-verification.ts +642 -0
  218. package/src/sdk/broker/runtime.ts +1 -1
  219. package/src/sdk/broker/session-index.ts +97 -4
  220. package/src/sdk/broker/spawn-authority.ts +175 -14
  221. package/src/sdk/broker/startup-failure.ts +80 -1
  222. package/src/sdk/broker/stderr-drainer.ts +59 -0
  223. package/src/sdk/broker/transport.ts +1 -0
  224. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  225. package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
  226. package/src/sdk/bus/index.ts +707 -81
  227. package/src/sdk/bus/telegram-daemon-cli.ts +31 -2
  228. package/src/sdk/bus/telegram-daemon-contract.ts +7 -1
  229. package/src/sdk/bus/telegram-daemon.ts +10 -3
  230. package/src/sdk/cli/master-cli.ts +47 -36
  231. package/src/sdk/cli/rows.ts +30 -7
  232. package/src/sdk/cli/session-cli.ts +705 -184
  233. package/src/sdk/client/client.ts +18 -0
  234. package/src/sdk/guides/catalog.ts +2 -2
  235. package/src/sdk/guides/cli.ts +20 -62
  236. package/src/sdk/guides/verify.ts +14 -0
  237. package/src/sdk/host/host.ts +49 -5
  238. package/src/sdk/host/session-runtime.test.ts +184 -2
  239. package/src/sdk/host/session-runtime.ts +322 -101
  240. package/src/sdk/host/types.ts +12 -1
  241. package/src/sdk/host/websocket-transport.lifecycle.test.ts +84 -0
  242. package/src/sdk/host/websocket-transport.ts +40 -6
  243. package/src/sdk/index.ts +11 -0
  244. package/src/sdk/lifecycle/service.ts +2 -0
  245. package/src/sdk/prompt-deadline-flush.ts +356 -0
  246. package/src/sdk/prompt-deadline-manager.ts +133 -1
  247. package/src/sdk/prompt-tool-boundary.ts +121 -0
  248. package/src/sdk/protocol/operation-inventory.generated.json +106 -0
  249. package/src/sdk/router/session-router.ts +9 -1
  250. package/src/sdk/session.ts +763 -116
  251. package/src/sdk/startup-capability.ts +1 -1
  252. package/src/sdk/transport/relay.ts +71 -6
  253. package/src/sdk/transport/serve-cli.ts +397 -42
  254. package/src/sdk/transport/socket.ts +12 -11
  255. package/src/sdk/transport/stdio.ts +7 -3
  256. package/src/session/agent-session.ts +2472 -407
  257. package/src/session/fallback-chain-controller.ts +6 -7
  258. package/src/session/fold-coordinator.ts +24 -5
  259. package/src/session/internal/managed-session-scope.ts +68 -18
  260. package/src/session/internal/managed-session-storage.ts +334 -12
  261. package/src/session/session-manager.ts +382 -44
  262. package/src/session/session-work-lease.ts +34 -0
  263. package/src/setup/model-onboarding-guidance.ts +1 -1
  264. package/src/setup/provider-onboarding.ts +397 -24
  265. package/src/setup/provider-presets.json +2 -1
  266. package/src/slash-commands/builtin-registry.ts +259 -15
  267. package/src/task/executor.ts +46 -17
  268. package/src/task/index.ts +14 -2
  269. package/src/task/types.ts +2 -0
  270. package/src/tools/bash-interactive.ts +2 -0
  271. package/src/tools/bash.ts +118 -78
  272. package/src/tools/fetch.ts +140 -25
  273. package/src/tools/job.ts +151 -25
  274. package/src/tools/output-meta.ts +15 -2
  275. package/src/tools/path-utils.ts +27 -3
  276. package/src/tools/read.ts +3 -3
  277. package/src/tools/steer-fold.ts +107 -0
  278. package/src/tools/subagent-render.ts +24 -1
  279. package/src/tools/subagent.ts +27 -1
  280. package/src/tools/tool-catalog.generated.ts +29 -2
  281. package/src/tools/tool-errors.ts +4 -0
  282. package/src/utils/git.ts +53 -3
  283. package/src/utils/markit.ts +55 -41
  284. package/src/utils/mupdf-assets.d.ts +4 -0
  285. package/src/utils/mupdf-embedded.ts +4 -0
  286. package/src/utils/mupdf.ts +136 -0
  287. package/src/utils/session-title-generation.ts +29 -0
  288. package/src/utils/title-generator.ts +46 -2
  289. package/src/web/scrapers/types.ts +24 -3
  290. package/vendor/markit-ai/LICENSE +21 -0
  291. package/vendor/markit-ai/MANIFEST.json +412 -0
  292. package/vendor/markit-ai/dist/commands/config.d.ts +4 -0
  293. package/vendor/markit-ai/dist/commands/config.js +139 -0
  294. package/vendor/markit-ai/dist/commands/convert.d.ts +6 -0
  295. package/vendor/markit-ai/dist/commands/convert.js +113 -0
  296. package/vendor/markit-ai/dist/commands/formats.d.ts +2 -0
  297. package/vendor/markit-ai/dist/commands/formats.js +92 -0
  298. package/vendor/markit-ai/dist/commands/init.d.ts +2 -0
  299. package/vendor/markit-ai/dist/commands/init.js +29 -0
  300. package/vendor/markit-ai/dist/commands/onboard.d.ts +2 -0
  301. package/vendor/markit-ai/dist/commands/onboard.js +61 -0
  302. package/vendor/markit-ai/dist/commands/plugin.d.ts +4 -0
  303. package/vendor/markit-ai/dist/commands/plugin.js +58 -0
  304. package/vendor/markit-ai/dist/config.d.ts +26 -0
  305. package/vendor/markit-ai/dist/config.js +42 -0
  306. package/vendor/markit-ai/dist/converters/audio.d.ts +7 -0
  307. package/vendor/markit-ai/dist/converters/audio.js +104 -0
  308. package/vendor/markit-ai/dist/converters/csv.d.ts +7 -0
  309. package/vendor/markit-ai/dist/converters/csv.js +83 -0
  310. package/vendor/markit-ai/dist/converters/docx.d.ts +6 -0
  311. package/vendor/markit-ai/dist/converters/docx.js +57 -0
  312. package/vendor/markit-ai/dist/converters/epub.d.ts +8 -0
  313. package/vendor/markit-ai/dist/converters/epub.js +128 -0
  314. package/vendor/markit-ai/dist/converters/github.d.ts +18 -0
  315. package/vendor/markit-ai/dist/converters/github.js +148 -0
  316. package/vendor/markit-ai/dist/converters/html.d.ts +6 -0
  317. package/vendor/markit-ai/dist/converters/html.js +30 -0
  318. package/vendor/markit-ai/dist/converters/image.d.ts +6 -0
  319. package/vendor/markit-ai/dist/converters/image.js +124 -0
  320. package/vendor/markit-ai/dist/converters/ipynb.d.ts +6 -0
  321. package/vendor/markit-ai/dist/converters/ipynb.js +75 -0
  322. package/vendor/markit-ai/dist/converters/iwork.d.ts +20 -0
  323. package/vendor/markit-ai/dist/converters/iwork.js +391 -0
  324. package/vendor/markit-ai/dist/converters/json.d.ts +6 -0
  325. package/vendor/markit-ai/dist/converters/json.js +21 -0
  326. package/vendor/markit-ai/dist/converters/pdf/columns.d.ts +35 -0
  327. package/vendor/markit-ai/dist/converters/pdf/columns.js +93 -0
  328. package/vendor/markit-ai/dist/converters/pdf/extract.d.ts +19 -0
  329. package/vendor/markit-ai/dist/converters/pdf/extract.js +514 -0
  330. package/vendor/markit-ai/dist/converters/pdf/grid.d.ts +25 -0
  331. package/vendor/markit-ai/dist/converters/pdf/grid.js +795 -0
  332. package/vendor/markit-ai/dist/converters/pdf/headers.d.ts +24 -0
  333. package/vendor/markit-ai/dist/converters/pdf/headers.js +108 -0
  334. package/vendor/markit-ai/dist/converters/pdf/index.d.ts +19 -0
  335. package/vendor/markit-ai/dist/converters/pdf/index.js +136 -0
  336. package/vendor/markit-ai/dist/converters/pdf/mupdf-loader.js +9 -0
  337. package/vendor/markit-ai/dist/converters/pdf/render.d.ts +24 -0
  338. package/vendor/markit-ai/dist/converters/pdf/render.js +513 -0
  339. package/vendor/markit-ai/dist/converters/pdf/types.d.ts +75 -0
  340. package/vendor/markit-ai/dist/converters/pdf/types.js +1 -0
  341. package/vendor/markit-ai/dist/converters/plain-text.d.ts +6 -0
  342. package/vendor/markit-ai/dist/converters/plain-text.js +87 -0
  343. package/vendor/markit-ai/dist/converters/pptx.d.ts +8 -0
  344. package/vendor/markit-ai/dist/converters/pptx.js +289 -0
  345. package/vendor/markit-ai/dist/converters/rss.d.ts +11 -0
  346. package/vendor/markit-ai/dist/converters/rss.js +138 -0
  347. package/vendor/markit-ai/dist/converters/wikipedia.d.ts +6 -0
  348. package/vendor/markit-ai/dist/converters/wikipedia.js +33 -0
  349. package/vendor/markit-ai/dist/converters/xlsx.d.ts +8 -0
  350. package/vendor/markit-ai/dist/converters/xlsx.js +147 -0
  351. package/vendor/markit-ai/dist/converters/xml.d.ts +6 -0
  352. package/vendor/markit-ai/dist/converters/xml.js +18 -0
  353. package/vendor/markit-ai/dist/converters/yaml.d.ts +6 -0
  354. package/vendor/markit-ai/dist/converters/yaml.js +17 -0
  355. package/vendor/markit-ai/dist/converters/zip.d.ts +8 -0
  356. package/vendor/markit-ai/dist/converters/zip.js +57 -0
  357. package/vendor/markit-ai/dist/index.d.ts +30 -0
  358. package/vendor/markit-ai/dist/index.js +26 -0
  359. package/vendor/markit-ai/dist/main.d.ts +2 -0
  360. package/vendor/markit-ai/dist/main.js +191 -0
  361. package/vendor/markit-ai/dist/markit.d.ts +45 -0
  362. package/vendor/markit-ai/dist/markit.js +267 -0
  363. package/vendor/markit-ai/dist/plugins/api.d.ts +7 -0
  364. package/vendor/markit-ai/dist/plugins/api.js +42 -0
  365. package/vendor/markit-ai/dist/plugins/index.d.ts +4 -0
  366. package/vendor/markit-ai/dist/plugins/index.js +3 -0
  367. package/vendor/markit-ai/dist/plugins/installer.d.ts +25 -0
  368. package/vendor/markit-ai/dist/plugins/installer.js +176 -0
  369. package/vendor/markit-ai/dist/plugins/loader.d.ts +6 -0
  370. package/vendor/markit-ai/dist/plugins/loader.js +61 -0
  371. package/vendor/markit-ai/dist/plugins/types.d.ts +25 -0
  372. package/vendor/markit-ai/dist/plugins/types.js +1 -0
  373. package/vendor/markit-ai/dist/providers/anthropic.d.ts +2 -0
  374. package/vendor/markit-ai/dist/providers/anthropic.js +47 -0
  375. package/vendor/markit-ai/dist/providers/index.d.ts +21 -0
  376. package/vendor/markit-ai/dist/providers/index.js +56 -0
  377. package/vendor/markit-ai/dist/providers/openai.d.ts +2 -0
  378. package/vendor/markit-ai/dist/providers/openai.js +76 -0
  379. package/vendor/markit-ai/dist/providers/types.d.ts +26 -0
  380. package/vendor/markit-ai/dist/providers/types.js +1 -0
  381. package/vendor/markit-ai/dist/types.d.ts +36 -0
  382. package/vendor/markit-ai/dist/types.js +1 -0
  383. package/vendor/markit-ai/dist/utils/exit-codes.d.ts +4 -0
  384. package/vendor/markit-ai/dist/utils/exit-codes.js +4 -0
  385. package/vendor/markit-ai/dist/utils/output.d.ts +22 -0
  386. package/vendor/markit-ai/dist/utils/output.js +31 -0
  387. package/vendor/markit-ai/dist/utils/turndown.d.ts +8 -0
  388. package/vendor/markit-ai/dist/utils/turndown.js +70 -0
  389. package/vendor/markit-ai/package.json +74 -0
  390. package/vendor/markit-ai.patch +49 -0
@@ -60,7 +60,11 @@ import {
60
60
  promptFailureRetryability,
61
61
  rephaseFailedOutcome,
62
62
  } from "../../sdk/prompt-failure";
63
- import type { SdkPromptTerminalOutcome } from "../../sdk/prompt-status";
63
+ import type {
64
+ SdkPromptFailureCategory,
65
+ SdkPromptFailurePhase,
66
+ SdkPromptTerminalOutcome,
67
+ } from "../../sdk/prompt-status";
64
68
  import { PromptActivity, type PromptWatchdogClock, systemPromptWatchdogClock } from "../../sdk/prompt-watchdog";
65
69
  import { validateRequiredPromptText } from "../../sdk/protocol/adapter-validation";
66
70
  import { type SessionAttachment, SessionRouter, type SessionRouterFrame } from "../../sdk/router";
@@ -74,6 +78,7 @@ import {
74
78
  } from "./acp-event-mapper";
75
79
  import { resolveAcpPermissionMode } from "./permission-mode";
76
80
  import type { AcpStartupOptions } from "./startup-options";
81
+ import { resolveStartupProvenance } from "./startup-provenance";
77
82
  import { ACP_TERMINAL_AUTH_FLAG } from "./terminal-auth";
78
83
 
79
84
  const ACP_DEFAULT_MODE_ID = "default";
@@ -87,6 +92,8 @@ const SESSION_PAGE_SIZE = 50;
87
92
  const MAX_ACP_REPLAY_PAGES = 10_000;
88
93
  /** Bounded retention of settled prompt correlations so late duplicates stay closed. */
89
94
  const SETTLED_PROMPT_CORRELATION_RETENTION = 16;
95
+ /** Bounded valid terminal candidates retained while an uncertain prompt has only clientRef identity. */
96
+ const PROVISIONAL_ABORT_TERMINAL_CANDIDATE_RETENTION = 16;
90
97
  /**
91
98
  * A cancelled prompt must still settle. The SDK acknowledges `turn.abort` before the
92
99
  * aborted run publishes its normalized terminal, and agent-owned async work that
@@ -159,6 +166,9 @@ const ACP_FIRST_PROMPT_RETRY_BASE_DELAY_MS = 250;
159
166
  type JsonObject = Record<string, unknown>;
160
167
  interface PromptWaiter {
161
168
  acknowledged: boolean;
169
+ invocationKind: "prompt" | "skill";
170
+ /** ACP-owned identity, never inherited from caller metadata or reused for replay. */
171
+ clientRef: string;
162
172
  /** True once turn.prompt / skill.invoke has been sent; cancel must not fake-settle after this. */
163
173
  dispatched: boolean;
164
174
  /** True only while the dispatched control request can still reveal its correlation. */
@@ -217,6 +227,15 @@ interface PromptWaiter {
217
227
  }
218
228
 
219
229
  type PromptCorrelation = { commandId?: string; turnId?: string };
230
+ type UncertainAbortOwner = {
231
+ kind: PromptWaiter["invocationKind"];
232
+ clientRef: string;
233
+ correlation?: { commandId: string; turnId: string };
234
+ };
235
+ type UncertainPromptRecoveryResult =
236
+ | { kind: "terminal" }
237
+ | { kind: "uncertain"; error: AcpSdkAdapterError }
238
+ | { kind: "ownership_lost" };
220
239
  type PromptPhaseOwner = PromptWaiter | "background" | undefined;
221
240
 
222
241
  type BrokerConnection = { adapter: AcpSdkAdapter; client: SdkClient };
@@ -253,6 +272,11 @@ type SessionRecord = {
253
272
  /** Replayable startup notice captured before ACP bootstrap; emitted once after the session id is known. */
254
273
  routingInactiveNotice?: string;
255
274
  activePrompt?: PromptWaiter;
275
+ /** One bounded read-only recovery owner for this attachment and waiter. */
276
+ statusRecovery?: PromptWaiter;
277
+ statusRecoveryTask?: Promise<UncertainPromptRecoveryResult>;
278
+ /** An uncertain abort's exact late terminal is required before a successor may start. */
279
+ uncertainAbortOwner?: UncertainAbortOwner;
256
280
  /** Set by `session/cancel` so an in-flight prompt settles as `cancelled`, never as an error. */
257
281
  cancelRequested?: boolean;
258
282
  /** True once the session's first logical prompt has settled; gates first-turn readiness retries. */
@@ -597,6 +621,14 @@ function sdkFrameCorrelation(frame: JsonObject, event?: JsonObject): PromptCorre
597
621
  return strictCorrelationFrom(frame, event);
598
622
  }
599
623
 
624
+ function sdkFrameBelongsToSession(frame: JsonObject, event: JsonObject | undefined, sessionId: string): boolean {
625
+ return (
626
+ frame.routerSessionId === sessionId &&
627
+ (frame.sessionId === undefined || frame.sessionId === sessionId) &&
628
+ (event?.sessionId === undefined || event.sessionId === sessionId)
629
+ );
630
+ }
631
+
600
632
  /**
601
633
  * Conflicting envelope/event identities own no prompt and therefore cannot
602
634
  * refresh a watchdog or publish into either turn.
@@ -608,7 +640,7 @@ function watchdogCorrelationFrom(frame: JsonObject, event?: JsonObject): PromptC
608
640
  function logDroppedPromptTerminal(
609
641
  sessionId: string,
610
642
  event: JsonObject,
611
- reason: "incomplete_correlation" | "correlation_mismatch",
643
+ reason: "incomplete_correlation" | "correlation_mismatch" | "session_mismatch",
612
644
  actual: PromptCorrelation,
613
645
  expected?: PromptCorrelation,
614
646
  ): void {
@@ -640,6 +672,46 @@ export class AcpPromptFailureError extends AcpSdkAdapterError {
640
672
  }
641
673
  }
642
674
 
675
+ /**
676
+ * Recover the shared prompt-failure projection for a direct SDK rejection.
677
+ *
678
+ * A terminal frame normally reaches {@link AcpPromptFailureError} through
679
+ * `#settlePrompt`, but a host may reject the `turn.prompt` control request
680
+ * itself with `code: "prompt_failed"`. That path never has a terminal outcome
681
+ * to retain, so translating the raw adapter error used to bypass
682
+ * `promptFailureWireData` and publish only the bare `code`/`details` pair.
683
+ * Keep the fallback deliberately narrow: only the two prompt terminal codes
684
+ * are upgraded, and any optional provider classifier is re-validated before
685
+ * it enters the existing classifier/projection seam.
686
+ */
687
+ function promptFailureFromDirectError(error: unknown): SdkPromptFailedOutcome | undefined {
688
+ if (error instanceof AcpPromptFailureError) return error.failure;
689
+ let code: unknown;
690
+ let providerCode: unknown;
691
+ let phase: unknown;
692
+ try {
693
+ const candidate = error as {
694
+ code?: unknown;
695
+ providerCode?: unknown;
696
+ transportFailure?: { providerCode?: unknown };
697
+ phase?: unknown;
698
+ };
699
+ code = candidate?.code;
700
+ providerCode = candidate?.providerCode ?? candidate?.transportFailure?.providerCode;
701
+ phase = candidate?.phase;
702
+ } catch {
703
+ return undefined;
704
+ }
705
+ if (code !== "prompt_failed" && code !== "prompt_deadline_exceeded") return undefined;
706
+ return failedPromptOutcome({
707
+ code,
708
+ provenance: code === "prompt_deadline_exceeded" ? "deadline" : "agent_failed",
709
+ ...(isSafePromptFailureCode(providerCode) ? { providerCode } : {}),
710
+ ...(isSdkPromptFailurePhase(phase) ? { phase } : {}),
711
+ evidence: {},
712
+ });
713
+ }
714
+
643
715
  /**
644
716
  * The wire projection of a prompt terminal's classification (issue #5615).
645
717
  *
@@ -655,12 +727,71 @@ export class AcpPromptFailureError extends AcpSdkAdapterError {
655
727
  * dropped instead of becoming a leak of provider text (the redaction contract in
656
728
  * `sanitizePromptFailure`). A dropped or absent code is omitted, never nulled.
657
729
  */
730
+ export const OPERATOR_UPSTREAM_LABEL = "Upstream provider failure";
731
+ export const OPERATOR_UPSTREAM_SUFFIX = ": the model provider ended this turn.";
732
+ /**
733
+ * `post_start` proves execution began, so tools may already have run and had effects before the
734
+ * provider died. The wording therefore qualifies the turn rather than absolving the task: saying
735
+ * the task was unaffected would assert something the bounded classifiers do not establish.
736
+ */
737
+ export const OPERATOR_PARTIAL_WORK_NOTE =
738
+ " Work this turn had already performed may have taken effect; check before retrying.";
739
+
740
+ /**
741
+ * Operator-facing wording for a post-start terminal, assembled ONLY from bounded safe tokens: the
742
+ * classifier `phase`, the `category`, and a `providerCode` that passes `isSafePromptFailureCode`.
743
+ * Raw provider text never reaches this function and must never reach it.
744
+ *
745
+ * Gated on BOTH `phase === "post_start"` and `category === "provider_transport"`. A submission-phase
746
+ * provider failure is rejected by the phase gate: nothing executed, so wording about a turn the
747
+ * provider ended would describe an event that never occurred.
748
+ *
749
+ * It claims only what those tokens prove — that the upstream provider ended the turn — and says
750
+ * nothing about whether the operator's work survived. `post_start` proves execution began, so the
751
+ * wording qualifies that earlier work may already have taken effect instead of absolving the task.
752
+ *
753
+ * This is additive. The wire `code`/`details` pair and every `PROMPT_FAILURE_MESSAGE_*` constant
754
+ * stay exactly as they are — pinned ACP core-v1 conformance asserts on them — so this wording
755
+ * travels alongside the redacted message instead of repurposing it.
756
+ *
757
+ * It says NOTHING about the worktree, deliberately. Nothing on this path inspects the worktree, so
758
+ * any sentence about uncommitted work would assert a check that never ran — reporting an
759
+ * uninspected tree as clean is the absence-of-evidence-as-evidence-of-absence error, and an
760
+ * operator who believed it would sweep work away. Silence is the only honest option until something
761
+ * actually looks.
762
+ *
763
+ * Returns `undefined` for every other category: `phase` and `category` are already on the wire, so
764
+ * restating them in prose tells an operator nothing they do not already have.
765
+ */
766
+ export function postStartOperatorMessage(input: {
767
+ phase: SdkPromptFailurePhase;
768
+ category: SdkPromptFailureCategory;
769
+ providerCode?: string;
770
+ }): string | undefined {
771
+ // Phase first: a submission-phase provider failure never started executing, so wording about a
772
+ // turn that ended mid-flight would describe something that did not happen. `phase` is a bounded
773
+ // classifier already on the wire, so gating on it widens nothing.
774
+ if (input.phase !== "post_start") return undefined;
775
+ if (input.category !== "provider_transport") return undefined;
776
+ const code = isSafePromptFailureCode(input.providerCode) ? ` (${input.providerCode})` : "";
777
+ return `${OPERATOR_UPSTREAM_LABEL}${code}${OPERATOR_UPSTREAM_SUFFIX}${OPERATOR_PARTIAL_WORK_NOTE}`;
778
+ }
779
+
658
780
  function promptFailureWireData(failure: SdkPromptFailedOutcome): Record<string, string> {
781
+ // `operatorMessage` says, in words, what the classifier tokens already say in codes: that the
782
+ // cause was an upstream provider problem rather than a failure of the operator's task. Built
783
+ // only from those same bounded tokens, so it widens nothing that reaches the wire.
784
+ const operatorMessage = postStartOperatorMessage({
785
+ phase: failure.phase,
786
+ category: failure.category,
787
+ ...(failure.providerCode === undefined ? {} : { providerCode: failure.providerCode }),
788
+ });
659
789
  return {
660
790
  phase: failure.phase,
661
791
  category: failure.category,
662
792
  retryability: promptFailureRetryability(failure.category),
663
793
  ...(isSafePromptFailureCode(failure.providerCode) ? { providerCode: failure.providerCode } : {}),
794
+ ...(operatorMessage === undefined ? {} : { operatorMessage }),
664
795
  };
665
796
  }
666
797
 
@@ -939,18 +1070,20 @@ const ROUTER_PASSTHROUGH_FRAME_TYPES = new Set([
939
1070
  "reverse_request_cancelled",
940
1071
  ]);
941
1072
 
942
- function acpFrameFromRouted(frame: SessionRouterFrame): JsonObject {
1073
+ function acpFrameFromRouted(frame: SessionRouterFrame, attachmentSessionId: string): JsonObject {
943
1074
  if (
944
1075
  frame.body.type === "activity" ||
945
1076
  frame.body.type === "agent_start" ||
946
1077
  frame.body.type === "agent_end" ||
947
1078
  frame.body.type === "agent_failed"
948
1079
  )
949
- return frame.body;
950
- if (typeof frame.body.type === "string" && ROUTER_PASSTHROUGH_FRAME_TYPES.has(frame.body.type)) return frame.body;
1080
+ return { ...frame.body, routerSessionId: attachmentSessionId };
1081
+ if (typeof frame.body.type === "string" && ROUTER_PASSTHROUGH_FRAME_TYPES.has(frame.body.type))
1082
+ return { ...frame.body, routerSessionId: attachmentSessionId };
951
1083
  const connectionId = typeof frame.body.connectionId === "string" ? frame.body.connectionId : undefined;
952
1084
  return {
953
1085
  type: "event",
1086
+ routerSessionId: attachmentSessionId,
954
1087
  ...(frame.name === undefined ? {} : { kind: frame.name }),
955
1088
  ...(frame.sessionId === undefined ? {} : { sessionId: frame.sessionId }),
956
1089
  ...(frame.commandId === undefined ? {} : { commandId: frame.commandId }),
@@ -1366,9 +1499,10 @@ export function acpRequestFailure(error: unknown): unknown {
1366
1499
  // spread is empty for every other error, so their payloads are byte-identical to
1367
1500
  // before. `code`/`details` and the JSON-RPC code itself are untouched either way:
1368
1501
  // this enriches `data`, it does not restate the redacted message.
1502
+ const promptFailure = promptFailureFromDirectError(error);
1369
1503
  const data =
1370
- error instanceof AcpPromptFailureError
1371
- ? { code, details: message, ...promptFailureWireData(error.failure) }
1504
+ promptFailure !== undefined
1505
+ ? { code, details: message, ...promptFailureWireData(promptFailure) }
1372
1506
  : { code, details: message };
1373
1507
  // An abandoned prompt additionally publishes the plan it never finished (issue #5669).
1374
1508
  // Same rule as above: `code`/`details` and the JSON-RPC code are untouched, and an abandon
@@ -1507,10 +1641,12 @@ export class AcpAgent implements Agent {
1507
1641
  readonly #pendingRouterFrames = new Map<string, Record<string, unknown>[]>();
1508
1642
  #routerStartPromise: Promise<void> | undefined;
1509
1643
  readonly #sessions = new Map<string, SessionRecord>();
1644
+ /** Exact abort owners survive SessionRecord replacement until terminal proof or retirement. */
1645
+ readonly #uncertainAbortOwners = new Map<string, UncertainAbortOwner>();
1510
1646
  /** Retain settled prompt identities across automatic transport reattachment. */
1511
1647
  readonly #retiredPromptCorrelations = new Map<string, PromptCorrelation[]>();
1512
1648
  /** Prevent a successor admission while a retired prompt can still reveal its identity. */
1513
- readonly #retiredPromptAcknowledgements = new Set<string>();
1649
+ readonly #retiredPromptAcknowledgements = new Map<string, PromptWaiter>();
1514
1650
  /** Ready terminal metadata writes retain ownership across same-id record replacement. */
1515
1651
  readonly #terminalMetadataTails = new Map<string, Promise<void>>();
1516
1652
  /** Unstreamed terminal text retains transcript ownership across same-id replacement. */
@@ -1579,7 +1715,7 @@ export class AcpAgent implements Agent {
1579
1715
  await adapter.attachmentReady(attachment);
1580
1716
  },
1581
1717
  onFrame: (attachment, frame) => {
1582
- const acpFrame = acpFrameFromRouted(frame);
1718
+ const acpFrame = acpFrameFromRouted(frame, attachment.sessionId);
1583
1719
  const adapter =
1584
1720
  this.#sessions.get(attachment.sessionId)?.adapter ??
1585
1721
  this.#pendingRouterAdapters.get(attachment.sessionId);
@@ -1824,6 +1960,7 @@ export class AcpAgent implements Agent {
1824
1960
  { sessionId: params.sessionId },
1825
1961
  this.#lifecycleIdempotencyKey(params.sessionId, "session.delete"),
1826
1962
  );
1963
+ this.#uncertainAbortOwners.delete(params.sessionId);
1827
1964
  return {};
1828
1965
  }
1829
1966
  this.#beginTeardown(params.sessionId);
@@ -1855,6 +1992,7 @@ export class AcpAgent implements Agent {
1855
1992
  this.#knownSessionMetadata.delete(params.sessionId);
1856
1993
  this.#retiredPromptCorrelations.delete(params.sessionId);
1857
1994
  this.#retiredPromptAcknowledgements.delete(params.sessionId);
1995
+ this.#uncertainAbortOwners.delete(params.sessionId);
1858
1996
  return {};
1859
1997
  }
1860
1998
  throw error;
@@ -1866,6 +2004,7 @@ export class AcpAgent implements Agent {
1866
2004
  { sessionId: params.sessionId, sessionPath: saved, cwd, target: { path: cwd } },
1867
2005
  this.#lifecycleIdempotencyKey(params.sessionId, "session.delete"),
1868
2006
  );
2007
+ this.#uncertainAbortOwners.delete(params.sessionId);
1869
2008
  this.#knownSessionCwds.delete(params.sessionId);
1870
2009
  this.#ownedSessionIds.delete(params.sessionId);
1871
2010
  this.#knownSessionMcpServers.delete(params.sessionId);
@@ -2094,6 +2233,11 @@ export class AcpAgent implements Agent {
2094
2233
  if (record.pendingFirstPromptRetry && record.pendingFirstPromptRetry !== retryReservation)
2095
2234
  throw new AcpSdkAdapterError("conflict", "ACP session is retrying its first prompt.");
2096
2235
  if (record.activePrompt) throw new AcpSdkAdapterError("conflict", "ACP session already has an active prompt.");
2236
+ if (record.uncertainAbortOwner)
2237
+ throw new AcpSdkAdapterError(
2238
+ "conflict",
2239
+ "ACP session is still awaiting the uncertainly aborted turn's terminal.",
2240
+ );
2097
2241
  if (record.authFailure) throw new AcpSdkAdapterError("authentication_failed", record.authFailure);
2098
2242
  if (this.#retiredPromptAcknowledgements.has(params.sessionId))
2099
2243
  throw new AcpSdkAdapterError(
@@ -2112,6 +2256,7 @@ export class AcpAgent implements Agent {
2112
2256
 
2113
2257
  const payload = acpPromptPayload(params.prompt);
2114
2258
  const skillInvocation = acpSkillInvocation(params.prompt);
2259
+ const clientRef = randomUUID();
2115
2260
  if (!skillInvocation) {
2116
2261
  const promptError = validateRequiredPromptText("turn.prompt", {
2117
2262
  text: payload.text,
@@ -2146,7 +2291,7 @@ export class AcpAgent implements Agent {
2146
2291
  JSON.stringify({
2147
2292
  type: "control_request",
2148
2293
  operation: skillInvocation ? "skill.invoke" : "turn.prompt",
2149
- input: skillInvocation ?? { text: payload.text, images: payload.images },
2294
+ input: { ...(skillInvocation ?? { text: payload.text, images: payload.images }), clientRef },
2150
2295
  // AcpSdkAdapter.control passes `confirm: false` to SdkClient, which serializes
2151
2296
  // the field even though it is not part of the skill input payload.
2152
2297
  ...(skillInvocation ? { confirm: false } : {}),
@@ -2168,6 +2313,8 @@ export class AcpAgent implements Agent {
2168
2313
  record.promptObservedAssistantOutput = false;
2169
2314
  const { promise: response, resolve, reject } = Promise.withResolvers<PromptResponse>();
2170
2315
  const waiter: PromptWaiter = {
2316
+ invocationKind: skillInvocation ? "skill" : "prompt",
2317
+ clientRef,
2171
2318
  acknowledged: false,
2172
2319
  dispatched: false,
2173
2320
  acknowledgementPending: false,
@@ -2212,11 +2359,11 @@ export class AcpAgent implements Agent {
2212
2359
  } catch (error) {
2213
2360
  waiter.awaitingProviderPreflight = false;
2214
2361
  if (waiter.settled || record.activePrompt !== waiter) {
2215
- this.#retiredPromptAcknowledgements.delete(params.sessionId);
2362
+ this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
2216
2363
  return await response;
2217
2364
  }
2218
2365
  if (record.cancelRequested && waiter.cancelAcknowledged) {
2219
- this.#retiredPromptAcknowledgements.delete(params.sessionId);
2366
+ this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
2220
2367
  await this.#settleCancelledPrompt(params.sessionId, record, waiter);
2221
2368
  return await response;
2222
2369
  }
@@ -2224,18 +2371,22 @@ export class AcpAgent implements Agent {
2224
2371
  record.activePrompt = undefined;
2225
2372
  record.busy = record.backgroundBusy;
2226
2373
  clearPromptWatchdog(waiter);
2227
- this.#retiredPromptAcknowledgements.delete(params.sessionId);
2374
+ // #5526's guarded release replaces the unguarded delete: it only clears
2375
+ // the entry when it still belongs to THIS waiter, so a successor's
2376
+ // acknowledgement is never dropped. Strictly narrower than the delete
2377
+ // it supersedes; dev's watchdog clear above is unaffected.
2378
+ this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
2228
2379
  void this.#publishPromptPhaseIdle(params.sessionId, record.adapter);
2229
2380
  }
2230
2381
  throw error;
2231
2382
  }
2232
2383
  waiter.awaitingProviderPreflight = false;
2233
2384
  if (waiter.settled || record.activePrompt !== waiter) {
2234
- this.#retiredPromptAcknowledgements.delete(params.sessionId);
2385
+ this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
2235
2386
  return await response;
2236
2387
  }
2237
2388
  if (record.cancelRequested && waiter.cancelAcknowledged) {
2238
- this.#retiredPromptAcknowledgements.delete(params.sessionId);
2389
+ this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
2239
2390
  await this.#settleCancelledPrompt(params.sessionId, record, waiter);
2240
2391
  return await response;
2241
2392
  }
@@ -2278,14 +2429,18 @@ export class AcpAgent implements Agent {
2278
2429
  }
2279
2430
 
2280
2431
  waiter.acknowledgementPending = true;
2432
+ const promptAdapter = record.adapter;
2281
2433
  const acknowledgementTask = (async (): Promise<PromptResponse> => {
2282
2434
  if (waiter.settled || record.activePrompt !== waiter) return await response;
2283
2435
  const acknowledgement = skillInvocation
2284
- ? await record.adapter.control("skill.invoke", skillInvocation)
2285
- : await record.adapter.prompt({
2436
+ ? await promptAdapter.control("skill.invoke", { ...skillInvocation, clientRef })
2437
+ : await promptAdapter.prompt({
2286
2438
  text: payload.text,
2439
+ clientRef,
2287
2440
  ...(payload.images.length ? { images: payload.images } : {}),
2288
2441
  });
2442
+ // A recovered waiter already owns its exact identity; a late ack cannot rebind it.
2443
+ if (waiter.settled && waiter.acknowledged) return await response;
2289
2444
 
2290
2445
  const acknowledgementCorrelation = promptAcknowledgement(acknowledgement);
2291
2446
  if (!acknowledgementCorrelation)
@@ -2293,6 +2448,74 @@ export class AcpAgent implements Agent {
2293
2448
  "invalid_prompt_acknowledgement",
2294
2449
  "SDK prompt acknowledgement must accept the prompt and include commandId and turnId.",
2295
2450
  );
2451
+ if (
2452
+ this.#sessions.get(params.sessionId) !== record ||
2453
+ record.adapter !== promptAdapter ||
2454
+ record.activePrompt !== waiter
2455
+ ) {
2456
+ const current = this.#sessions.get(params.sessionId);
2457
+ const provisionalOwner = current?.uncertainAbortOwner;
2458
+ if (
2459
+ provisionalOwner?.kind === waiter.invocationKind &&
2460
+ provisionalOwner.clientRef === waiter.clientRef &&
2461
+ provisionalOwner.correlation === undefined &&
2462
+ this.#hasRetiredPromptCorrelation(params.sessionId, record, acknowledgementCorrelation)
2463
+ ) {
2464
+ // A stale ACK cannot bind a provisional abort owner. Keep the fence
2465
+ // until this session is retired or new exact evidence is available.
2466
+ waiter.deferredFrames.length = 0;
2467
+ return await response;
2468
+ }
2469
+ this.#bindUncertainAbortOwner(params.sessionId, waiter, acknowledgementCorrelation);
2470
+ this.#rememberSettledPromptCorrelation(params.sessionId, record, acknowledgementCorrelation);
2471
+ const retainedOwner = current?.uncertainAbortOwner;
2472
+ if (
2473
+ current &&
2474
+ retainedOwner?.kind === waiter.invocationKind &&
2475
+ retainedOwner.clientRef === waiter.clientRef &&
2476
+ retainedOwner.correlation &&
2477
+ correlationsExactlyMatch(retainedOwner.correlation, acknowledgementCorrelation)
2478
+ ) {
2479
+ await current.frameTail;
2480
+ const deferredFrames = waiter.deferredFrames.splice(0);
2481
+ let reconciledDeferredTerminal = false;
2482
+ for (const deferred of deferredFrames) {
2483
+ const deferredEvent = receivedSdkEvent(deferred.frame)?.event;
2484
+ if (!deferredEvent) continue;
2485
+ const deferredOutcome = terminalOutcome(deferredEvent);
2486
+ const validDeferredTerminal =
2487
+ (deferredEvent.type === "agent_end" && deferredOutcome !== undefined) ||
2488
+ (deferredEvent.type === "agent_failed" && deferredOutcome?.kind === "failed");
2489
+ const deferredCorrelation = sdkFrameCorrelation(deferred.frame, deferredEvent);
2490
+ if (
2491
+ !validDeferredTerminal ||
2492
+ !sdkFrameBelongsToSession(deferred.frame, deferredEvent, params.sessionId) ||
2493
+ !deferredCorrelation ||
2494
+ !hasCompleteCorrelation(deferredCorrelation) ||
2495
+ !correlationsExactlyMatch(deferredCorrelation, acknowledgementCorrelation)
2496
+ )
2497
+ continue;
2498
+ waiter.terminalReserved = true;
2499
+ await this.#handleSdkFrame(
2500
+ params.sessionId,
2501
+ current.adapter,
2502
+ deferred.frame,
2503
+ current.publicationGeneration,
2504
+ );
2505
+ reconciledDeferredTerminal = true;
2506
+ break;
2507
+ }
2508
+ if (!reconciledDeferredTerminal)
2509
+ void this.#recoverUncertainAbortAfterLateAcknowledgement(
2510
+ params.sessionId,
2511
+ current.adapter,
2512
+ retainedOwner,
2513
+ );
2514
+ } else {
2515
+ waiter.deferredFrames.length = 0;
2516
+ }
2517
+ return await response;
2518
+ }
2296
2519
  if (this.#hasRetiredPromptCorrelation(params.sessionId, record, acknowledgementCorrelation)) {
2297
2520
  try {
2298
2521
  const abortAcknowledgement = await record.adapter.cancel("turn");
@@ -2318,6 +2541,7 @@ export class AcpAgent implements Agent {
2318
2541
  waiter.boundary = record.inboundSequence;
2319
2542
  waiter.correlation = acknowledgementCorrelation;
2320
2543
  waiter.acknowledged = true;
2544
+ this.#bindUncertainAbortOwner(params.sessionId, waiter, acknowledgementCorrelation);
2321
2545
  if (promptWaiterRetired(record, waiter)) {
2322
2546
  this.#rememberSettledPromptCorrelation(params.sessionId, record, acknowledgementCorrelation);
2323
2547
  return await response;
@@ -2427,10 +2651,20 @@ export class AcpAgent implements Agent {
2427
2651
  }
2428
2652
  this.#settlePrompt(params.sessionId, record, waiter);
2429
2653
  return await response;
2430
- })().finally(() => {
2431
- waiter.acknowledgementPending = false;
2432
- this.#retiredPromptAcknowledgements.delete(params.sessionId);
2433
- });
2654
+ })()
2655
+ .catch(async error => {
2656
+ if (
2657
+ !(error instanceof SdkClientError || error instanceof AcpSdkAdapterError) ||
2658
+ error.code !== "uncertain_after_send"
2659
+ )
2660
+ throw error;
2661
+ if (record.adapter === promptAdapter) this.#startUncertainPromptRecovery(params.sessionId, record, waiter);
2662
+ return await response;
2663
+ })
2664
+ .finally(() => {
2665
+ waiter.acknowledgementPending = false;
2666
+ this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
2667
+ });
2434
2668
  // Settlement can win before the SDK answers `turn.prompt`; acknowledgement processing
2435
2669
  // must remain alive to tombstone its eventual correlation without holding the ACP caller.
2436
2670
  void acknowledgementTask.catch(() => undefined);
@@ -2541,6 +2775,49 @@ export class AcpAgent implements Agent {
2541
2775
  }
2542
2776
  waiter?.cancelAttemptResolve?.(true);
2543
2777
  } catch (error) {
2778
+ let cancellationError = error;
2779
+ const abortDetails = error instanceof SdkClientError ? object(error.details) : undefined;
2780
+ if (
2781
+ error instanceof SdkClientError &&
2782
+ error.code === "uncertain_after_send" &&
2783
+ abortDetails?.operation === "turn.abort" &&
2784
+ waiter &&
2785
+ record.activePrompt === waiter &&
2786
+ waiter.dispatched &&
2787
+ !waiter.settled
2788
+ ) {
2789
+ if (waiter.terminalReserved) {
2790
+ waiter.cancelAttemptResolve?.(true);
2791
+ return;
2792
+ }
2793
+ this.#retainUncertainAbortOwner(params.sessionId, record, waiter);
2794
+ let recovery = record.statusRecovery === waiter ? record.statusRecoveryTask : undefined;
2795
+ if (!recovery && this.#startUncertainPromptRecovery(params.sessionId, record, waiter))
2796
+ recovery = record.statusRecoveryTask;
2797
+ if (recovery) {
2798
+ const recoveryResult = await recovery;
2799
+ if (
2800
+ recoveryResult.kind === "terminal" ||
2801
+ (recoveryResult.kind === "ownership_lost" && waiter.terminalReserved)
2802
+ ) {
2803
+ record.cancelRequested = false;
2804
+ waiter.cancelAttemptResolve?.(true);
2805
+ return;
2806
+ }
2807
+ cancellationError =
2808
+ recoveryResult.kind === "uncertain"
2809
+ ? recoveryResult.error
2810
+ : new AcpSdkAdapterError(
2811
+ "terminal_uncertain",
2812
+ "ACP abort recovery lost ownership before exact terminal evidence was resolved. No mutation was replayed.",
2813
+ );
2814
+ } else {
2815
+ cancellationError = new AcpSdkAdapterError(
2816
+ "terminal_uncertain",
2817
+ "ACP abort could not retain recovery ownership. No mutation was replayed.",
2818
+ );
2819
+ }
2820
+ }
2544
2821
  // With no active prompt the cancel intent normally clears — except when a first-turn
2545
2822
  // retry is reserved across its backoff gap. That reservation owner has not yet observed
2546
2823
  // the cancel; clearing it here would let the retry resubmit a turn the client cancelled
@@ -2565,7 +2842,7 @@ export class AcpAgent implements Agent {
2565
2842
  waiter.cancelAttempt = undefined;
2566
2843
  waiter.cancelAttemptResolve = undefined;
2567
2844
  }
2568
- throw error;
2845
+ throw cancellationError;
2569
2846
  } finally {
2570
2847
  if (waiter) {
2571
2848
  waiter.pendingCancelAttempts = Math.max(0, (waiter.pendingCancelAttempts ?? 1) - 1);
@@ -2596,6 +2873,22 @@ export class AcpAgent implements Agent {
2596
2873
  if (!waiter.cancelAcknowledged) return;
2597
2874
  if (this.#sessions.get(id) !== record || record.activePrompt !== waiter || waiter.settled) return;
2598
2875
  if (waiter.terminalReserved) return;
2876
+ // An overlapping acknowledged cancel does not supersede the earlier
2877
+ // uncertain abort's bounded terminal lookup. Let that exact read either
2878
+ // prove a terminal or retire the waiter as still uncertain before grace
2879
+ // settlement can discard its foreground recovery owner.
2880
+ if (record.statusRecovery === waiter && this.#ownsUncertainAbort(record, waiter)) return;
2881
+ const uncertainAbortOwner = this.#uncertainAbortOwners.get(id);
2882
+ if (uncertainAbortOwner && this.#ownsUncertainAbort(record, waiter)) {
2883
+ record.uncertainAbortOwner = uncertainAbortOwner;
2884
+ const ownerCorrelation = uncertainAbortOwner.correlation;
2885
+ if (
2886
+ ownerCorrelation &&
2887
+ !record.backgroundCorrelations.some(owner => correlationsExactlyMatch(owner, ownerCorrelation))
2888
+ )
2889
+ record.backgroundCorrelations.push(ownerCorrelation);
2890
+ record.backgroundBusy = true;
2891
+ }
2599
2892
  record.activePrompt = undefined;
2600
2893
  if (!record.backgroundBusy) record.busy = false;
2601
2894
  clearPromptWatchdog(waiter);
@@ -2862,11 +3155,7 @@ export class AcpAgent implements Agent {
2862
3155
  this.#pendingRouterFrames.set(id, bufferedFrames);
2863
3156
  try {
2864
3157
  await this.#ensureRouterReady();
2865
- const attachment = lifecycleResult
2866
- ? await this.#router.adoptLifecycleResult(lifecycleResult, { sessionId: id, cwd })
2867
- : this.#router.attachment(id);
2868
- if (!attachment)
2869
- throw new AcpSdkAdapterError("unavailable", `ACP session ${id} has no current Router attachment.`);
3158
+ if (lifecycleResult) await this.#router.adoptLifecycleResult(lifecycleResult, { sessionId: id, cwd });
2870
3159
  let currentAttachment = this.#router.attachment(id);
2871
3160
  for (let attempt = 0; !currentAttachment && attempt < 40; attempt++) {
2872
3161
  await Bun.sleep(50);
@@ -2874,7 +3163,7 @@ export class AcpAgent implements Agent {
2874
3163
  currentAttachment = this.#router.attachment(id);
2875
3164
  }
2876
3165
  if (!currentAttachment)
2877
- throw new AcpSdkAdapterError("unavailable", `ACP session ${id} lost exact Router authority.`);
3166
+ throw new AcpSdkAdapterError("unavailable", `ACP session ${id} has no current Router attachment.`);
2878
3167
  adapter = new AcpSdkAdapter({
2879
3168
  router: this.#router,
2880
3169
  attachment: currentAttachment,
@@ -2889,24 +3178,23 @@ export class AcpAgent implements Agent {
2889
3178
  // a broker-launched SDK host keeps ACP ownership across reconnects.
2890
3179
  await adapter.start({ activateProviders: false });
2891
3180
  let capabilities: JsonObject | undefined;
3181
+ // A query that never answered and a host that answered without provenance are
3182
+ // different operator problems, so the failure is retained instead of discarded.
3183
+ let capabilitiesFailure: unknown;
2892
3184
  try {
2893
3185
  const response = object(await adapter.query("runtime.capabilities"));
2894
3186
  const result = object(response?.result) ?? response;
2895
3187
  // Q18 is a paged query surface: the capability object arrives as the single
2896
3188
  // page item, so fall back to the envelope only for direct-result hosts.
2897
3189
  capabilities = object(pageItems(result)[0]) ?? result;
2898
- } catch {}
2899
- const primaryControlSurface =
2900
- capabilities?.primaryControlSurface === "sdk"
2901
- ? "sdk"
2902
- : capabilities?.primaryControlSurface === "cli"
2903
- ? "cli"
2904
- : undefined;
2905
- if (capabilities?.promptTerminalOutcomeVersion !== 1 || primaryControlSurface === undefined)
2906
- throw new AcpSdkAdapterError(
2907
- "unavailable",
2908
- "This ACP client requires a newer GJC SDK session with startup control provenance; restart the session.",
2909
- );
3190
+ } catch (error) {
3191
+ capabilitiesFailure = error;
3192
+ }
3193
+ const primaryControlSurface = resolveStartupProvenance({
3194
+ sessionId: id,
3195
+ capabilities,
3196
+ queryFailure: capabilitiesFailure,
3197
+ });
2910
3198
  if (primaryControlSurface === "sdk") {
2911
3199
  adapter.authorizeProviderActivation();
2912
3200
  await adapter.ensureProviders();
@@ -2916,6 +3204,7 @@ export class AcpAgent implements Agent {
2916
3204
  const exactAttachment = this.#router.attachment(id) ?? currentAttachment;
2917
3205
  if (!exactAttachment.isCurrent())
2918
3206
  throw new AcpSdkAdapterError("unavailable", `ACP session ${id} lost exact Router authority.`);
3207
+ const retainedAbortOwner = this.#uncertainAbortOwners.get(id);
2919
3208
  const record: SessionRecord = {
2920
3209
  cwd,
2921
3210
  adapter,
@@ -2929,11 +3218,12 @@ export class AcpAgent implements Agent {
2929
3218
  settledPromptCorrelations: this.#retiredPromptCorrelations.get(id) ?? [],
2930
3219
  inboundSequence: 0,
2931
3220
  connectionId: adapter.connectionId,
2932
- busy: false,
2933
- backgroundBusy: false,
3221
+ busy: retainedAbortOwner !== undefined,
3222
+ backgroundBusy: retainedAbortOwner !== undefined,
2934
3223
  backgroundAnonymousCount: 0,
2935
- backgroundCorrelations: [],
3224
+ backgroundCorrelations: retainedAbortOwner?.correlation ? [retainedAbortOwner.correlation] : [],
2936
3225
  toolArgs: new Map(),
3226
+ uncertainAbortOwner: retainedAbortOwner,
2937
3227
  };
2938
3228
  // Preserve first-turn state across reattachment (issue #5574). The startup-readiness
2939
3229
  // retry only guards a session's very first logical prompt; a fresh record built on
@@ -2970,9 +3260,11 @@ export class AcpAgent implements Agent {
2970
3260
  try {
2971
3261
  await this.#teardownSession(id, "attachment failed", false);
2972
3262
  } finally {
2973
- this.#knownSessionCwds.delete(id);
3263
+ if (!this.#uncertainAbortOwners.has(id)) {
3264
+ this.#knownSessionCwds.delete(id);
3265
+ this.#knownSessionMcpServers.delete(id);
3266
+ }
2974
3267
  this.#ownedSessionIds.delete(id);
2975
- this.#knownSessionMcpServers.delete(id);
2976
3268
  }
2977
3269
  } else if (adapter) {
2978
3270
  try {
@@ -3017,7 +3309,186 @@ export class AcpAgent implements Agent {
3017
3309
 
3018
3310
  #fenceRetiredPromptAcknowledgement(id: string, waiter: PromptWaiter): void {
3019
3311
  if (waiter.dispatched && waiter.acknowledgementPending && !waiter.acknowledged)
3020
- this.#retiredPromptAcknowledgements.add(id);
3312
+ this.#retiredPromptAcknowledgements.set(id, waiter);
3313
+ }
3314
+
3315
+ #releaseRetiredPromptAcknowledgement(id: string, waiter: PromptWaiter): void {
3316
+ if (this.#retiredPromptAcknowledgements.get(id) === waiter) this.#retiredPromptAcknowledgements.delete(id);
3317
+ }
3318
+
3319
+ #retainUncertainAbortOwner(id: string, record: SessionRecord, waiter: PromptWaiter): void {
3320
+ let owner: UncertainAbortOwner = {
3321
+ kind: waiter.invocationKind,
3322
+ clientRef: waiter.clientRef,
3323
+ };
3324
+ if (hasCompleteCorrelation(waiter.correlation))
3325
+ owner = {
3326
+ ...owner,
3327
+ correlation: { commandId: waiter.correlation.commandId, turnId: waiter.correlation.turnId },
3328
+ };
3329
+ const retained = this.#uncertainAbortOwners.get(id);
3330
+ if (
3331
+ retained &&
3332
+ (retained.kind !== owner.kind ||
3333
+ retained.clientRef !== owner.clientRef ||
3334
+ (retained.correlation !== undefined &&
3335
+ owner.correlation !== undefined &&
3336
+ !correlationsExactlyMatch(retained.correlation, owner.correlation)))
3337
+ ) {
3338
+ record.uncertainAbortOwner = retained;
3339
+ return;
3340
+ }
3341
+ if (retained?.correlation && !owner.correlation) owner = retained;
3342
+ this.#uncertainAbortOwners.set(id, owner);
3343
+ record.uncertainAbortOwner = owner;
3344
+ }
3345
+
3346
+ #bindUncertainAbortOwner(id: string, waiter: PromptWaiter, correlation: PromptCorrelation): void {
3347
+ if (!hasCompleteCorrelation(correlation)) return;
3348
+ const retained = this.#uncertainAbortOwners.get(id);
3349
+ if (!retained || retained.kind !== waiter.invocationKind || retained.clientRef !== waiter.clientRef) return;
3350
+ if (retained.correlation && !correlationsExactlyMatch(retained.correlation, correlation)) return;
3351
+ const boundCorrelation = { commandId: correlation.commandId, turnId: correlation.turnId };
3352
+ const owner: UncertainAbortOwner = {
3353
+ kind: retained.kind,
3354
+ clientRef: retained.clientRef,
3355
+ correlation: boundCorrelation,
3356
+ };
3357
+ this.#uncertainAbortOwners.set(id, owner);
3358
+ const current = this.#sessions.get(id);
3359
+ if (!current) return;
3360
+ current.uncertainAbortOwner = owner;
3361
+ if (current.activePrompt !== waiter || waiter.settled) {
3362
+ if (!current.backgroundCorrelations.some(background => correlationsExactlyMatch(background, correlation)))
3363
+ current.backgroundCorrelations.push(boundCorrelation);
3364
+ current.backgroundBusy = true;
3365
+ current.busy = true;
3366
+ }
3367
+ }
3368
+
3369
+ async #recoverUncertainAbortAfterLateAcknowledgement(
3370
+ id: string,
3371
+ adapter: AcpSdkAdapter,
3372
+ owner: UncertainAbortOwner,
3373
+ ): Promise<void> {
3374
+ const correlation = owner.correlation;
3375
+ if (!correlation) return;
3376
+ let timer: NodeJS.Timeout | undefined;
3377
+ let timedOut = false;
3378
+ try {
3379
+ const deadline = Promise.withResolvers<never>();
3380
+ timer = setTimeout(() => {
3381
+ timedOut = true;
3382
+ deadline.reject(new Error("status query timeout"));
3383
+ }, 5_000);
3384
+ const response = object(
3385
+ await Promise.race([
3386
+ adapter.query("turn.result", {
3387
+ kind: owner.kind,
3388
+ commandId: correlation.commandId,
3389
+ turnId: correlation.turnId,
3390
+ }),
3391
+ deadline.promise,
3392
+ ]),
3393
+ );
3394
+ const status = object(response?.result) ?? response;
3395
+ const reportedCorrelation = strictCorrelationFrom(response, status);
3396
+ const outcome = status ? terminalOutcome(status) : undefined;
3397
+ const failure = object(status?.error);
3398
+ const conflictingIdentity = [response, status].some(
3399
+ value =>
3400
+ (value?.kind !== undefined && value.kind !== owner.kind) ||
3401
+ (value?.sessionId !== undefined && value.sessionId !== id) ||
3402
+ (value?.clientRef !== undefined && value.clientRef !== owner.clientRef),
3403
+ );
3404
+ const failedReceipt =
3405
+ status?.status === "failed" &&
3406
+ ((outcome?.kind === "failed" &&
3407
+ (status.receiptState === "present" ||
3408
+ status.receiptState === "missing" ||
3409
+ status.receiptState === "unknown")) ||
3410
+ (outcome === undefined &&
3411
+ (status.receiptState === "present" ||
3412
+ status.receiptState === "missing" ||
3413
+ status.receiptState === "unknown") &&
3414
+ typeof failure?.code === "string" &&
3415
+ /^[a-zA-Z0-9_.-]{1,64}$/.test(failure.code) &&
3416
+ typeof failure.message === "string" &&
3417
+ failure.message.trim().length > 0 &&
3418
+ failure.message.length <= 512));
3419
+ const terminal =
3420
+ (status?.status === "terminal_ok" &&
3421
+ outcome?.kind === "stopped" &&
3422
+ (status.receiptState === "present" || status.receiptState === "missing")) ||
3423
+ failedReceipt;
3424
+ // The original prompt has already settled as terminal_uncertain on this
3425
+ // detached follow-up path. Missing receipt remains a failure classification;
3426
+ // this proof retires only the session ownership fence, never the old result
3427
+ // as a successful stop reason.
3428
+ if (
3429
+ !terminal ||
3430
+ conflictingIdentity ||
3431
+ status?.kind !== owner.kind ||
3432
+ !reportedCorrelation ||
3433
+ !hasCompleteCorrelation(reportedCorrelation) ||
3434
+ !correlationsExactlyMatch(correlation, reportedCorrelation)
3435
+ )
3436
+ return;
3437
+ const record = this.#sessions.get(id);
3438
+ const retained = this.#uncertainAbortOwners.get(id);
3439
+ if (
3440
+ !record ||
3441
+ record.adapter !== adapter ||
3442
+ retained?.kind !== owner.kind ||
3443
+ retained.clientRef !== owner.clientRef ||
3444
+ !retained.correlation ||
3445
+ !correlationsExactlyMatch(retained.correlation, correlation)
3446
+ )
3447
+ return;
3448
+ this.#clearUncertainAbortOwner(id, record, owner.kind, correlation);
3449
+ record.backgroundCorrelations = record.backgroundCorrelations.filter(
3450
+ background => !correlationsExactlyMatch(background, correlation),
3451
+ );
3452
+ record.backgroundBusy = record.backgroundAnonymousCount > 0 || record.backgroundCorrelations.length > 0;
3453
+ record.busy = record.backgroundBusy;
3454
+ void this.#publishPromptPhaseIdle(id, adapter);
3455
+ } catch {
3456
+ logger.warn("acp_uncertain_abort_late_ack_lookup_unresolved", {
3457
+ sessionId: id,
3458
+ reason: timedOut ? "timeout" : "unavailable",
3459
+ });
3460
+ } finally {
3461
+ if (timer !== undefined) clearTimeout(timer);
3462
+ }
3463
+ }
3464
+
3465
+ #clearUncertainAbortOwner(
3466
+ id: string,
3467
+ record: SessionRecord,
3468
+ kind: PromptWaiter["invocationKind"],
3469
+ correlation: PromptCorrelation,
3470
+ ): void {
3471
+ const owner = this.#uncertainAbortOwners.get(id);
3472
+ if (owner?.kind === kind && owner.correlation && correlationsExactlyMatch(owner.correlation, correlation))
3473
+ this.#uncertainAbortOwners.delete(id);
3474
+ const recordOwner = record.uncertainAbortOwner;
3475
+ if (
3476
+ recordOwner?.kind === kind &&
3477
+ recordOwner.correlation &&
3478
+ correlationsExactlyMatch(recordOwner.correlation, correlation)
3479
+ )
3480
+ record.uncertainAbortOwner = undefined;
3481
+ }
3482
+
3483
+ #ownsUncertainAbort(record: SessionRecord, waiter: PromptWaiter): boolean {
3484
+ const owner = record.uncertainAbortOwner;
3485
+ return (
3486
+ owner?.kind === waiter.invocationKind &&
3487
+ owner.clientRef === waiter.clientRef &&
3488
+ (!owner.correlation ||
3489
+ !hasCompleteCorrelation(waiter.correlation) ||
3490
+ correlationsExactlyMatch(owner.correlation, waiter.correlation))
3491
+ );
3021
3492
  }
3022
3493
 
3023
3494
  #advanceTerminalGeneration(record: SessionRecord): void {
@@ -3027,6 +3498,23 @@ export class AcpAgent implements Agent {
3027
3498
  #recoverSessionAfterTransportFailure(id: string, adapter: AcpSdkAdapter, error: Error): void {
3028
3499
  const record = this.#sessions.get(id);
3029
3500
  if (!record || record.adapter !== adapter) return;
3501
+ if (error instanceof SdkClientError && error.code === "uncertain_after_send") {
3502
+ const details = object(error.details);
3503
+ const abortWaiter = record.activePrompt;
3504
+ if (details?.operation === "turn.abort" && abortWaiter?.dispatched && !abortWaiter.settled) {
3505
+ if (abortWaiter.terminalReserved) return;
3506
+ this.#retainUncertainAbortOwner(id, record, abortWaiter);
3507
+ if (record.statusRecovery !== abortWaiter) this.#startUncertainPromptRecovery(id, record, abortWaiter);
3508
+ return;
3509
+ }
3510
+ // Provider-registration requests share this transport error. Recover only when the
3511
+ // prompt's own dispatched acknowledgement is still pending; ordinary acknowledged
3512
+ // turns continue until their terminal frame arrives.
3513
+ const waiter = record.activePrompt;
3514
+ if (waiter?.dispatched && waiter.acknowledgementPending && !waiter.acknowledged)
3515
+ this.#startUncertainPromptRecovery(id, record, waiter);
3516
+ return;
3517
+ }
3030
3518
  if (
3031
3519
  error instanceof SdkClientError &&
3032
3520
  error.code !== "reconnect_exhausted" &&
@@ -3041,11 +3529,276 @@ export class AcpAgent implements Agent {
3041
3529
  logger.warn(`ACP session ${id} non-transport error reached reconnect handler; ignoring terminal recovery.`);
3042
3530
  return;
3043
3531
  }
3532
+ const waiter = record.activePrompt;
3533
+ if (waiter?.terminalReserved && this.#ownsUncertainAbort(record, waiter)) return;
3534
+ if (waiter?.dispatched) {
3535
+ // A prior uncertainty notification already owns recovery for this waiter. Keep
3536
+ // the in-flight lookup alive; only declined, non-recovering waiters fall through
3537
+ // to the ordinary transport-loss teardown and reattachment path.
3538
+ if (record.statusRecovery === waiter || this.#startUncertainPromptRecovery(id, record, waiter)) return;
3539
+ }
3044
3540
  const detail = error.message || "SDK transport reconnect failed.";
3045
3541
  const terminal = new AcpSdkAdapterError("connection_closed", `ACP session transport was lost: ${detail}`);
3046
3542
  void this.#recoverSessionAfterTransportFailureAsync(id, adapter, record.cwd, terminal);
3047
3543
  }
3048
3544
 
3545
+ #startUncertainPromptRecovery(id: string, record: SessionRecord, waiter: PromptWaiter): boolean {
3546
+ if (
3547
+ this.#sessions.get(id) !== record ||
3548
+ promptWaiterRetired(record, waiter) ||
3549
+ !waiter.dispatched ||
3550
+ waiter.terminalReserved ||
3551
+ record.statusRecovery === waiter
3552
+ )
3553
+ return false;
3554
+ record.statusRecovery = waiter;
3555
+ clearPromptWatchdog(waiter);
3556
+ const recovery = this.#recoverUncertainPrompt(id, record, record.adapter, waiter);
3557
+ record.statusRecoveryTask = recovery;
3558
+ const clearRecovery = (): void => {
3559
+ if (record.statusRecovery !== waiter) return;
3560
+ record.statusRecovery = undefined;
3561
+ record.statusRecoveryTask = undefined;
3562
+ };
3563
+ void recovery.then(clearRecovery, clearRecovery);
3564
+ return true;
3565
+ }
3566
+
3567
+ async #recoverUncertainPrompt(
3568
+ id: string,
3569
+ record: SessionRecord,
3570
+ adapter: AcpSdkAdapter,
3571
+ waiter: PromptWaiter,
3572
+ ): Promise<UncertainPromptRecoveryResult> {
3573
+ const ownsRecovery = (): boolean =>
3574
+ this.#sessions.get(id) === record &&
3575
+ record.adapter === adapter &&
3576
+ record.statusRecovery === waiter &&
3577
+ !promptWaiterRetired(record, waiter) &&
3578
+ !waiter.terminalReserved;
3579
+ // Keep the lookup selector's authority even if an ack arrives during the read.
3580
+ const acknowledgedAtLookup = waiter.acknowledged;
3581
+ let detail = "status query unavailable";
3582
+ let timer: NodeJS.Timeout | undefined;
3583
+ let timedOut = false;
3584
+ try {
3585
+ const deadline = Promise.withResolvers<never>();
3586
+ timer = setTimeout(() => {
3587
+ timedOut = true;
3588
+ deadline.reject(new Error("status query timeout"));
3589
+ }, 5_000);
3590
+ const refreshAttachment = async (): Promise<void> => {
3591
+ if (this.#sessions.get(id) !== record || record.adapter !== adapter) return;
3592
+ // A reconnect can revoke the adapter's opaque attachment before Router has
3593
+ // published its replacement. Force an authority pass first, then hand the
3594
+ // exact current capability back to the adapter before issuing the read-only
3595
+ // lookup. This pass does not replay the prompt mutation.
3596
+ await this.#router.reconcile({ waitForReplay: false });
3597
+ if (!ownsRecovery()) return;
3598
+ const refreshed = this.#router.attachment(id);
3599
+ if (!refreshed?.isCurrent())
3600
+ throw new AcpSdkAdapterError(
3601
+ "unavailable",
3602
+ "ACP session has no current Router attachment for recovery.",
3603
+ );
3604
+ if (refreshed !== record.attachment || !record.attachment.isCurrent()) {
3605
+ await adapter.attachmentReady(refreshed);
3606
+ record.attachment = refreshed;
3607
+ }
3608
+ };
3609
+ await Promise.race([refreshAttachment(), deadline.promise]);
3610
+ if (!ownsRecovery()) return { kind: "ownership_lost" };
3611
+ // Bound observation only: the mutation and the read are never replayed or aborted.
3612
+ const response = object(
3613
+ await Promise.race([
3614
+ adapter.query("turn.result", {
3615
+ kind: waiter.invocationKind,
3616
+ ...(acknowledgedAtLookup ? waiter.correlation : { clientRef: waiter.clientRef }),
3617
+ }),
3618
+ deadline.promise,
3619
+ ]),
3620
+ );
3621
+ if (!ownsRecovery()) return { kind: "ownership_lost" };
3622
+ const status = object(response?.result) ?? response;
3623
+ const correlation = strictCorrelationFrom(response, status);
3624
+ const outcome = status ? terminalOutcome(status) : undefined;
3625
+ const content = object(status?.content);
3626
+ const readableText =
3627
+ content?.version === 1 &&
3628
+ content.type === "text" &&
3629
+ typeof content.text === "string" &&
3630
+ content.text.trim().length > 0;
3631
+ const failure = object(status?.error);
3632
+ const conflictingIdentity = [response, status].some(
3633
+ value =>
3634
+ (value?.kind !== undefined && value.kind !== waiter.invocationKind) ||
3635
+ (value?.sessionId !== undefined && value.sessionId !== id) ||
3636
+ (value?.clientRef !== undefined && value.clientRef !== waiter.clientRef),
3637
+ );
3638
+ if (
3639
+ status?.kind !== waiter.invocationKind ||
3640
+ conflictingIdentity ||
3641
+ (!acknowledgedAtLookup && status.clientRef !== waiter.clientRef) ||
3642
+ !correlation ||
3643
+ !hasCompleteCorrelation(correlation) ||
3644
+ (waiter.acknowledged && !correlationsExactlyMatch(waiter.correlation, correlation)) ||
3645
+ this.#hasRetiredPromptCorrelation(id, record, correlation)
3646
+ ) {
3647
+ detail = "status correlation is missing, retired, or mismatched";
3648
+ } else if (
3649
+ status.status === "failed" &&
3650
+ status.outcome === undefined &&
3651
+ (status.receiptState === "present" ||
3652
+ status.receiptState === "missing" ||
3653
+ status.receiptState === "unknown") &&
3654
+ typeof failure?.code === "string" &&
3655
+ /^[a-zA-Z0-9_.-]{1,64}$/.test(failure.code) &&
3656
+ typeof failure.message === "string" &&
3657
+ failure.message.trim().length > 0 &&
3658
+ failure.message.length <= 512
3659
+ ) {
3660
+ waiter.correlation = correlation;
3661
+ waiter.acknowledged = true;
3662
+ this.#bindUncertainAbortOwner(id, waiter, correlation);
3663
+ this.#clearUncertainAbortOwner(id, record, waiter.invocationKind, correlation);
3664
+ record.busy = record.backgroundBusy;
3665
+ this.#advanceTerminalGeneration(record);
3666
+ void this.#rejectPrompt(record, id, waiter, new AcpSdkAdapterError(failure.code, failure.message));
3667
+ return { kind: "terminal" };
3668
+ } else if (
3669
+ this.#ownsUncertainAbort(record, waiter) &&
3670
+ status.status === "terminal_ok" &&
3671
+ outcome?.kind === "stopped" &&
3672
+ outcome.reason === "end_turn" &&
3673
+ status.receiptState === "missing"
3674
+ ) {
3675
+ waiter.correlation = correlation;
3676
+ waiter.acknowledged = true;
3677
+ this.#bindUncertainAbortOwner(id, waiter, correlation);
3678
+ this.#clearUncertainAbortOwner(id, record, waiter.invocationKind, correlation);
3679
+ record.busy = record.backgroundBusy;
3680
+ this.#advanceTerminalGeneration(record);
3681
+ void this.#rejectPrompt(
3682
+ record,
3683
+ id,
3684
+ waiter,
3685
+ new AcpSdkAdapterError(
3686
+ "prompt_failed",
3687
+ "Runtime completed without reportable final response text or artifact_path.",
3688
+ ),
3689
+ );
3690
+ return { kind: "terminal" };
3691
+ } else if (
3692
+ (status.status === "terminal_ok" &&
3693
+ outcome?.kind === "stopped" &&
3694
+ (status.receiptState === "present" || status.receiptState === "missing") &&
3695
+ (outcome.reason !== "end_turn" || (status.receiptState === "present" && readableText))) ||
3696
+ (status.status === "failed" &&
3697
+ outcome?.kind === "failed" &&
3698
+ (status.receiptState === "present" ||
3699
+ status.receiptState === "missing" ||
3700
+ status.receiptState === "unknown"))
3701
+ ) {
3702
+ waiter.correlation = correlation;
3703
+ waiter.acknowledged = true;
3704
+ this.#bindUncertainAbortOwner(id, waiter, correlation);
3705
+ this.#clearUncertainAbortOwner(id, record, waiter.invocationKind, correlation);
3706
+ record.busy = record.backgroundBusy;
3707
+ this.#advanceTerminalGeneration(record);
3708
+ const settledOutcome =
3709
+ record.cancelRequested && outcome.kind === "stopped" && outcome.reason === "end_turn"
3710
+ ? { ...outcome, reason: "cancelled" as const, provenance: "client_cancel" as const }
3711
+ : outcome;
3712
+ const publicationBarrier = record.frameTail;
3713
+ waiter.terminal = { outcome: settledOutcome, correlation };
3714
+ // Recovered final text is published on the async terminal tail below. Record it
3715
+ // before settling so a first-turn startup-readiness failure cannot be retried
3716
+ // after this retained answer has already been committed for publication.
3717
+ if (readableText) record.promptObservedAssistantOutput = true;
3718
+ this.#settlePrompt(id, record, waiter);
3719
+ this.#scheduleTerminalUpdates(
3720
+ id,
3721
+ adapter,
3722
+ record.publicationGeneration,
3723
+ {
3724
+ type: settledOutcome.kind === "failed" ? "agent_failed" : "agent_end",
3725
+ outcome: settledOutcome,
3726
+ finalText: readableText ? content.text : undefined,
3727
+ finalTextTruncated: content?.truncated === true,
3728
+ },
3729
+ waiter,
3730
+ publicationBarrier,
3731
+ );
3732
+ return { kind: "terminal" };
3733
+ } else {
3734
+ if (!acknowledgedAtLookup) {
3735
+ waiter.correlation = correlation;
3736
+ waiter.acknowledged = true;
3737
+ this.#bindUncertainAbortOwner(id, waiter, correlation);
3738
+ const deferred = waiter.deferredFrames.find(({ frame }) => {
3739
+ const candidate = receivedSdkEvent(frame)?.event;
3740
+ if (!candidate) return false;
3741
+ const candidateOutcome = terminalOutcome(candidate);
3742
+ const validTerminal =
3743
+ (candidate.type === "agent_end" && candidateOutcome !== undefined) ||
3744
+ (candidate.type === "agent_failed" && candidateOutcome?.kind === "failed");
3745
+ const candidateCorrelation = sdkFrameCorrelation(frame, candidate);
3746
+ return (
3747
+ validTerminal &&
3748
+ sdkFrameBelongsToSession(frame, candidate, id) &&
3749
+ candidateCorrelation !== undefined &&
3750
+ correlationsExactlyMatch(candidateCorrelation, correlation)
3751
+ );
3752
+ });
3753
+ if (deferred) {
3754
+ waiter.deferredFrames.length = 0;
3755
+ waiter.terminalReserved = true;
3756
+ clearPromptWatchdog(waiter);
3757
+ await record.frameTail;
3758
+ if (
3759
+ this.#sessions.get(id) !== record ||
3760
+ record.adapter !== adapter ||
3761
+ record.activePrompt !== waiter ||
3762
+ waiter.settled
3763
+ )
3764
+ return { kind: "ownership_lost" };
3765
+ await this.#handleSdkFrame(id, adapter, deferred.frame, record.publicationGeneration);
3766
+ return { kind: "terminal" };
3767
+ }
3768
+ }
3769
+ detail = "status has no usable retained terminal outcome and receipt";
3770
+ }
3771
+ } catch {
3772
+ detail = timedOut ? "status query timed out after 5000ms" : "status query unavailable";
3773
+ } finally {
3774
+ if (timer !== undefined) clearTimeout(timer);
3775
+ }
3776
+ if (!ownsRecovery()) return { kind: "ownership_lost" };
3777
+ const uncertain = new AcpSdkAdapterError(
3778
+ "terminal_uncertain",
3779
+ `ACP prompt outcome remains uncertain: ${detail}. No mutation was replayed. Inspect turn.result for kind=${waiter.invocationKind}, clientRef=${waiter.clientRef}, commandId=${waiter.correlation.commandId ?? "unknown"}, turnId=${waiter.correlation.turnId ?? "unknown"} before submitting more work.`,
3780
+ );
3781
+ const uncertainAbortOwner = this.#uncertainAbortOwners.get(id);
3782
+ if (uncertainAbortOwner?.kind === waiter.invocationKind && uncertainAbortOwner.clientRef === waiter.clientRef) {
3783
+ record.uncertainAbortOwner = uncertainAbortOwner;
3784
+ const ownerCorrelation = uncertainAbortOwner.correlation;
3785
+ if (
3786
+ ownerCorrelation &&
3787
+ !record.backgroundCorrelations.some(owner => correlationsExactlyMatch(owner, ownerCorrelation))
3788
+ )
3789
+ record.backgroundCorrelations.push(ownerCorrelation);
3790
+ record.backgroundBusy = true;
3791
+ record.busy = true;
3792
+ record.cancelRequested = false;
3793
+ record.statusRecovery = undefined;
3794
+ record.statusRecoveryTask = undefined;
3795
+ await this.#rejectPrompt(record, id, waiter, uncertain);
3796
+ return { kind: "uncertain", error: uncertain };
3797
+ }
3798
+ await this.#failSession(id, adapter, uncertain);
3799
+ return { kind: "uncertain", error: uncertain };
3800
+ }
3801
+
3049
3802
  async #recoverSessionAfterTransportFailureAsync(
3050
3803
  id: string,
3051
3804
  adapter: AcpSdkAdapter,
@@ -3093,13 +3846,15 @@ export class AcpAgent implements Agent {
3093
3846
  if (attaching) await Promise.allSettled([attaching.task]);
3094
3847
  // A CLI-primary host owns its own process lifecycle: release this ACP
3095
3848
  // attachment locally and never ask the broker to close the session.
3096
- await this.#teardownSession(id, "closed", this.#ownedSessionIds.has(id));
3849
+ const closesHost = this.#ownedSessionIds.has(id);
3850
+ await this.#teardownSession(id, "closed", closesHost);
3097
3851
  this.#knownSessionCwds.delete(id);
3098
3852
  this.#ownedSessionIds.delete(id);
3099
3853
  this.#knownSessionMcpServers.delete(id);
3100
3854
  this.#knownSessionMetadata.delete(id);
3101
3855
  this.#retiredPromptCorrelations.delete(id);
3102
- this.#retiredPromptAcknowledgements.delete(id);
3856
+ // Detaching locally cannot retire an outstanding host acknowledgement.
3857
+ if (closesHost) this.#retiredPromptAcknowledgements.delete(id);
3103
3858
  return {};
3104
3859
  } finally {
3105
3860
  this.#finishTeardown(id);
@@ -3187,7 +3942,10 @@ export class AcpAgent implements Agent {
3187
3942
  .join("; ");
3188
3943
  throw aggregateAcpFailure("terminal_uncertain", `ACP session cleanup is uncertain: ${detail}`, failures);
3189
3944
  }
3190
- if (closeRemote) this.#pendingCloseIdempotencyKeys.delete(id);
3945
+ if (closeRemote) {
3946
+ this.#pendingCloseIdempotencyKeys.delete(id);
3947
+ this.#uncertainAbortOwners.delete(id);
3948
+ }
3191
3949
  } finally {
3192
3950
  this.#finishTeardown(id);
3193
3951
  }
@@ -3337,7 +4095,7 @@ export class AcpAgent implements Agent {
3337
4095
  * running forever behind a dead session.
3338
4096
  */
3339
4097
  #armPromptWatchdog(id: string, record: SessionRecord, waiter: PromptWaiter): void {
3340
- if (waiter.settled) return;
4098
+ if (waiter.settled || record.statusRecovery === waiter) return;
3341
4099
  waiter.cancelWatchdog?.();
3342
4100
  // No frame can still arrive once the transport is known to be gone, so waiting out
3343
4101
  // the full bound would only add dead time to an outcome that is already decided.
@@ -3360,6 +4118,7 @@ export class AcpAgent implements Agent {
3360
4118
  const waiter = record.activePrompt;
3361
4119
  if (!waiter || waiter.settled || waiter.terminalReserved) return;
3362
4120
  const event = receivedSdkEvent(frame)?.event;
4121
+ if (event && !sdkFrameBelongsToSession(frame, event, id)) return;
3363
4122
  const correlation = watchdogCorrelationFrom(frame, event);
3364
4123
  if (!waiter.acknowledged) {
3365
4124
  if (hasCorrelation(correlation)) waiter.deferredActivityFrames.push(frame);
@@ -3400,7 +4159,13 @@ export class AcpAgent implements Agent {
3400
4159
  * turn can no longer be observed, it does not cancel or tear down the session.
3401
4160
  */
3402
4161
  async #expirePromptWatchdog(id: string, record: SessionRecord, waiter: PromptWaiter): Promise<void> {
3403
- if (record.activePrompt !== waiter || waiter.settled || waiter.terminalReserved) return;
4162
+ if (
4163
+ record.activePrompt !== waiter ||
4164
+ waiter.settled ||
4165
+ waiter.terminalReserved ||
4166
+ record.statusRecovery === waiter
4167
+ )
4168
+ return;
3404
4169
  const silenceMs = Math.max(0, this.#promptWatchdogClock.now() - waiter.lastFrameAt);
3405
4170
  // A prompt still in preflight has never reached the host, so "stopped producing frames"
3406
4171
  // would point the reader at the wrong phase: nothing was ever dispatched to produce them.
@@ -3448,6 +4213,20 @@ export class AcpAgent implements Agent {
3448
4213
  #enqueueSdkFrame(id: string, adapter: AcpSdkAdapter, frame: JsonObject): void {
3449
4214
  const record = this.#sessions.get(id);
3450
4215
  if (!record || record.adapter !== adapter) return;
4216
+ const received = receivedSdkEvent(frame);
4217
+ const ingressEvent = received?.event;
4218
+ if (!sdkFrameBelongsToSession(frame, ingressEvent, id)) {
4219
+ if (ingressEvent?.type === "agent_end" || ingressEvent?.type === "agent_failed")
4220
+ logDroppedPromptTerminal(
4221
+ id,
4222
+ ingressEvent,
4223
+ "session_mismatch",
4224
+ sdkFrameCorrelation(frame, ingressEvent) ?? {},
4225
+ record.activePrompt?.correlation,
4226
+ );
4227
+ else logger.warn(`ACP session ${id} dropped an event from a foreign session identity.`);
4228
+ return;
4229
+ }
3451
4230
  if (frame.type !== "hello" && frame.type !== "server_hello" && typeof frame.connectionId === "string")
3452
4231
  record.connectionId = frame.connectionId;
3453
4232
  // Ingress ordering is recorded before queued work begins.
@@ -3455,8 +4234,6 @@ export class AcpAgent implements Agent {
3455
4234
  // Correlation is checked at ingress before a prompt-owned frame may refresh the
3456
4235
  // watchdog, so queued processing cannot turn unrelated host traffic into turn liveness.
3457
4236
  this.#refreshPromptWatchdog(id, record, frame);
3458
- const received = receivedSdkEvent(frame);
3459
- const ingressEvent = received?.event;
3460
4237
  const ingressOutcome =
3461
4238
  ingressEvent?.type === "agent_end" || ingressEvent?.type === "agent_failed"
3462
4239
  ? terminalOutcome(ingressEvent)
@@ -3506,6 +4283,19 @@ export class AcpAgent implements Agent {
3506
4283
  await this.#settleCancelledPrompt(id, record, waiter);
3507
4284
  return;
3508
4285
  }
4286
+ if (waiter.terminalReserved) return;
4287
+ if (record.statusRecovery === waiter && this.#ownsUncertainAbort(record, waiter)) return;
4288
+ if (record.statusRecovery === waiter || (waiter.dispatched && !waiter.acknowledged)) {
4289
+ await this.#failSession(
4290
+ id,
4291
+ adapter,
4292
+ new AcpSdkAdapterError(
4293
+ "terminal_uncertain",
4294
+ `Prompt owner connection changed before retained evidence was resolved. No mutation was replayed. Inspect turn.result for kind=${waiter.invocationKind}, clientRef=${waiter.clientRef}.`,
4295
+ ),
4296
+ );
4297
+ return;
4298
+ }
3509
4299
  record.activePrompt = undefined;
3510
4300
  record.busy = record.backgroundBusy;
3511
4301
  clearPromptWatchdog(waiter);
@@ -3549,14 +4339,75 @@ export class AcpAgent implements Agent {
3549
4339
  const ownsBackgroundTerminal =
3550
4340
  ownedBackgroundCorrelation !== undefined ||
3551
4341
  (!hasCorrelation(correlation) && record.backgroundAnonymousCount > 0);
4342
+ const provisionalAbortOwner = this.#uncertainAbortOwners.get(id);
4343
+ const retiredPromptAcknowledgement = this.#retiredPromptAcknowledgements.get(id);
4344
+ const validTerminalProof =
4345
+ (event.type === "agent_end" && outcome !== undefined) ||
4346
+ (event.type === "agent_failed" && outcome?.kind === "failed");
4347
+ if (isTerminal && !sdkFrameBelongsToSession(frame, event, id)) {
4348
+ logDroppedPromptTerminal(id, event, "session_mismatch", correlation, activePrompt?.correlation);
4349
+ return;
4350
+ }
4351
+ if (
4352
+ isTerminal &&
4353
+ !activePrompt &&
4354
+ provisionalAbortOwner &&
4355
+ !provisionalAbortOwner.correlation &&
4356
+ retiredPromptAcknowledgement?.settled &&
4357
+ retiredPromptAcknowledgement.acknowledgementPending &&
4358
+ !retiredPromptAcknowledgement.acknowledged &&
4359
+ provisionalAbortOwner.kind === retiredPromptAcknowledgement.invocationKind &&
4360
+ provisionalAbortOwner.clientRef === retiredPromptAcknowledgement.clientRef &&
4361
+ validTerminalProof &&
4362
+ hasCompleteCorrelation(correlation) &&
4363
+ sdkFrameBelongsToSession(frame, event, id) &&
4364
+ !this.#hasRetiredPromptCorrelation(id, record, correlation)
4365
+ ) {
4366
+ // Query-by-clientRef has not yet yielded the command/turn identity. Retain
4367
+ // a bounded set of distinct candidates without publishing them until the
4368
+ // delayed prompt acknowledgement proves which exact correlation belongs.
4369
+ const duplicateCandidate = retiredPromptAcknowledgement.deferredFrames.some(({ frame: candidateFrame }) => {
4370
+ const candidateEvent = receivedSdkEvent(candidateFrame)?.event;
4371
+ const candidateCorrelation = candidateEvent
4372
+ ? sdkFrameCorrelation(candidateFrame, candidateEvent)
4373
+ : undefined;
4374
+ return candidateCorrelation !== undefined && correlationsExactlyMatch(candidateCorrelation, correlation);
4375
+ });
4376
+ if (
4377
+ !duplicateCandidate &&
4378
+ retiredPromptAcknowledgement.deferredFrames.length < PROVISIONAL_ABORT_TERMINAL_CANDIDATE_RETENTION
4379
+ )
4380
+ retiredPromptAcknowledgement.deferredFrames.push({ frame, publicationGeneration });
4381
+ return;
4382
+ }
3552
4383
  if (isTerminal) {
3553
4384
  if (!activePrompt) {
3554
- if (!record.backgroundBusy || !ownsBackgroundTerminal || settledCorrelation) return;
4385
+ if (
4386
+ !record.backgroundBusy ||
4387
+ !ownsBackgroundTerminal ||
4388
+ (settledCorrelation && !ownedBackgroundCorrelation)
4389
+ )
4390
+ return;
4391
+ const uncertainAbortOwner = this.#uncertainAbortOwners.get(id);
4392
+ if (
4393
+ uncertainAbortOwner?.correlation &&
4394
+ ownedBackgroundCorrelation &&
4395
+ correlationsExactlyMatch(uncertainAbortOwner.correlation, ownedBackgroundCorrelation) &&
4396
+ !validTerminalProof
4397
+ )
4398
+ return;
3555
4399
  if (ownedBackgroundCorrelation)
3556
4400
  record.backgroundCorrelations = record.backgroundCorrelations.filter(
3557
4401
  owner => !correlationsExactlyMatch(owner, ownedBackgroundCorrelation),
3558
4402
  );
3559
4403
  else record.backgroundAnonymousCount--;
4404
+ if (
4405
+ uncertainAbortOwner?.correlation &&
4406
+ ownedBackgroundCorrelation &&
4407
+ validTerminalProof &&
4408
+ correlationsExactlyMatch(uncertainAbortOwner.correlation, ownedBackgroundCorrelation)
4409
+ )
4410
+ this.#clearUncertainAbortOwner(id, record, uncertainAbortOwner.kind, ownedBackgroundCorrelation);
3560
4411
  record.backgroundBusy = record.backgroundAnonymousCount > 0 || record.backgroundCorrelations.length > 0;
3561
4412
  record.busy = record.backgroundBusy;
3562
4413
  await this.#publishPromptPhase(id, record.adapter, undefined);
@@ -3605,6 +4456,18 @@ export class AcpAgent implements Agent {
3605
4456
  this.#advanceTerminalGeneration(record);
3606
4457
  publicationGeneration = record.publicationGeneration;
3607
4458
  if (!outcome) {
4459
+ const uncertainAbortOwner = this.#uncertainAbortOwners.get(id);
4460
+ if (
4461
+ uncertainAbortOwner?.kind === activePrompt.invocationKind &&
4462
+ uncertainAbortOwner.correlation &&
4463
+ correlationsExactlyMatch(uncertainAbortOwner.correlation, correlation)
4464
+ ) {
4465
+ record.uncertainAbortOwner = uncertainAbortOwner;
4466
+ if (!record.backgroundCorrelations.some(owner => correlationsExactlyMatch(owner, correlation)))
4467
+ record.backgroundCorrelations.push(uncertainAbortOwner.correlation);
4468
+ record.backgroundBusy = true;
4469
+ record.busy = true;
4470
+ }
3608
4471
  await this.#rejectPrompt(
3609
4472
  record,
3610
4473
  id,
@@ -3613,6 +4476,13 @@ export class AcpAgent implements Agent {
3613
4476
  );
3614
4477
  return;
3615
4478
  }
4479
+ const uncertainAbortOwner = this.#uncertainAbortOwners.get(id);
4480
+ if (
4481
+ uncertainAbortOwner?.kind === activePrompt.invocationKind &&
4482
+ uncertainAbortOwner.correlation &&
4483
+ correlationsExactlyMatch(uncertainAbortOwner.correlation, correlation)
4484
+ )
4485
+ this.#clearUncertainAbortOwner(id, record, uncertainAbortOwner.kind, correlation);
3616
4486
  // When a client cancel has been requested, a trailing stopped terminal may
3617
4487
  // still carry the normal `end_turn` reason (the model finished its response as
3618
4488
  // the cancel arrived mid-stream, or the cancel was processed before the prompt
@@ -3796,13 +4666,15 @@ export class AcpAgent implements Agent {
3796
4666
  publicationGeneration: number,
3797
4667
  event: JsonObject,
3798
4668
  promptOwner: PromptWaiter | undefined,
4669
+ publicationBarrier?: Promise<void>,
3799
4670
  ): void {
3800
4671
  const failedTerminal = event.type === "agent_failed";
3801
4672
  if (promptOwner) this.#flushFailureDiagnostics(id, promptOwner, adapter);
3802
- let decorationStart = Promise.resolve();
4673
+ let decorationStart = publicationBarrier ?? Promise.resolve();
3803
4674
  const finalText = typeof event.finalText === "string" ? event.finalText : "";
3804
4675
  if (promptOwner && hasAcpFinalTextContent(finalText)) {
3805
4676
  const finalTextTask = (async () => {
4677
+ if (publicationBarrier) await publicationBarrier;
3806
4678
  await Bun.sleep(0);
3807
4679
  const resolution = resolveAcpFinalText(promptOwner.emittedAssistantText, finalText);
3808
4680
  if (resolution.kind === "emit") {
@@ -3814,7 +4686,9 @@ export class AcpAgent implements Agent {
3814
4686
  update: {
3815
4687
  sessionUpdate: "agent_message_chunk",
3816
4688
  content: { type: "text", text: resolution.text },
3817
- ...(resolution.final.truncated ? { _meta: { gjcFinalTextTruncated: true } } : {}),
4689
+ ...(resolution.final.truncated || event.finalTextTruncated === true
4690
+ ? { _meta: { gjcFinalTextTruncated: true } }
4691
+ : {}),
3818
4692
  },
3819
4693
  },
3820
4694
  adapter,
@@ -3829,6 +4703,24 @@ export class AcpAgent implements Agent {
3829
4703
  finalLength: resolution.final.text.length,
3830
4704
  });
3831
4705
  }
4706
+ if (resolution.kind !== "emit" && event.finalTextTruncated === true) {
4707
+ // A retained prefix may already equal the streamed text, leaving no suffix for
4708
+ // `resolveAcpFinalText` to emit. Still publish a metadata-only chunk so the
4709
+ // caller can distinguish an incomplete retained result from a complete answer.
4710
+ await this.#publishSessionUpdate(
4711
+ id,
4712
+ {
4713
+ sessionId: id,
4714
+ update: {
4715
+ sessionUpdate: "agent_message_chunk",
4716
+ content: { type: "text", text: "" },
4717
+ _meta: { gjcFinalTextTruncated: true },
4718
+ },
4719
+ },
4720
+ adapter,
4721
+ publicationGeneration,
4722
+ );
4723
+ }
3832
4724
  })();
3833
4725
  let finalTextTail: Promise<void>;
3834
4726
  finalTextTail = finalTextTask
@@ -3865,7 +4757,32 @@ export class AcpAgent implements Agent {
3865
4757
  clearPromptWatchdog(waiter);
3866
4758
  waiter.settled = true;
3867
4759
  this.#fenceRetiredPromptAcknowledgement(id, waiter);
4760
+ const abortOwner = this.#uncertainAbortOwners.get(id);
4761
+ const preserveAbortTerminal =
4762
+ abortOwner?.kind === waiter.invocationKind &&
4763
+ abortOwner.clientRef === waiter.clientRef &&
4764
+ abortOwner.correlation === undefined &&
4765
+ waiter.acknowledgementPending &&
4766
+ !waiter.acknowledged;
4767
+ const candidateTerminal = preserveAbortTerminal
4768
+ ? waiter.deferredFrames.find(({ frame }) => {
4769
+ const candidate = receivedSdkEvent(frame)?.event;
4770
+ if (!candidate) return false;
4771
+ const candidateOutcome = terminalOutcome(candidate);
4772
+ const validTerminal =
4773
+ (candidate.type === "agent_end" && candidateOutcome !== undefined) ||
4774
+ (candidate.type === "agent_failed" && candidateOutcome?.kind === "failed");
4775
+ const candidateCorrelation = sdkFrameCorrelation(frame, candidate);
4776
+ return (
4777
+ validTerminal &&
4778
+ sdkFrameBelongsToSession(frame, candidate, id) &&
4779
+ hasCompleteCorrelation(candidateCorrelation ?? {}) &&
4780
+ !this.#hasRetiredPromptCorrelation(id, record, candidateCorrelation ?? {})
4781
+ );
4782
+ })
4783
+ : undefined;
3868
4784
  waiter.deferredFrames.length = 0;
4785
+ if (candidateTerminal) waiter.deferredFrames.push(candidateTerminal);
3869
4786
  waiter.deferredActivityFrames.length = 0;
3870
4787
  waiter.terminal = undefined;
3871
4788
  this.#rememberSettledPromptCorrelation(id, record, waiter.correlation);