@gajae-code/coding-agent 0.17.1 → 0.17.4

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 (375) hide show
  1. package/CHANGELOG.md +266 -0
  2. package/dist/types/async/job-manager.d.ts +7 -6
  3. package/dist/types/cli/daemon-cli.d.ts +3 -1
  4. package/dist/types/cli/github-release.d.ts +17 -0
  5. package/dist/types/cli/public-command-entry.d.ts +56 -0
  6. package/dist/types/cli/public-command-errors.d.ts +126 -0
  7. package/dist/types/cli/public-command-evidence.d.ts +99 -0
  8. package/dist/types/cli/public-command-help.d.ts +65 -0
  9. package/dist/types/cli/public-command-registry.d.ts +69 -0
  10. package/dist/types/cli/session-picker.d.ts +1 -1
  11. package/dist/types/cli/setup-cli.d.ts +1 -0
  12. package/dist/types/cli/skills-cli.d.ts +5 -0
  13. package/dist/types/cli.d.ts +3 -0
  14. package/dist/types/commands/daemon.d.ts +4 -50
  15. package/dist/types/commands/sdk.d.ts +20 -37
  16. package/dist/types/commands/setup.d.ts +3 -0
  17. package/dist/types/commands/skills.d.ts +9 -0
  18. package/dist/types/config/autorouting-tier-map.d.ts +24 -0
  19. package/dist/types/config/file-lock.d.ts +10 -1
  20. package/dist/types/config/model-profile-activation.d.ts +63 -4
  21. package/dist/types/config/model-registry.d.ts +20 -2
  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/coordinator-mcp/server.d.ts +3 -0
  25. package/dist/types/daemon/operator-contract.d.ts +5 -1
  26. package/dist/types/eval/js/worker-protocol.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/runtime-skill-discovery.d.ts +18 -0
  30. package/dist/types/extensibility/shared-events.d.ts +3 -1
  31. package/dist/types/gjc-runtime/session-state-lock.d.ts +61 -0
  32. package/dist/types/internal-urls/embedded-protocol.d.ts +9 -0
  33. package/dist/types/internal-urls/index.d.ts +1 -0
  34. package/dist/types/internal-urls/parse.d.ts +9 -1
  35. package/dist/types/main.d.ts +22 -0
  36. package/dist/types/modes/acp/acp-agent.d.ts +94 -1
  37. package/dist/types/modes/acp/startup-provenance.d.ts +16 -0
  38. package/dist/types/modes/components/custom-provider-wizard.d.ts +19 -1
  39. package/dist/types/modes/components/session-selector.d.ts +5 -2
  40. package/dist/types/modes/components/thinking-selector.d.ts +2 -1
  41. package/dist/types/modes/components/user-message-selector.d.ts +5 -2
  42. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  43. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  44. package/dist/types/modes/interactive-mode.d.ts +1 -1
  45. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +11 -1
  46. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +14 -1
  47. package/dist/types/modes/types.d.ts +2 -2
  48. package/dist/types/runtime/process-lifecycle.d.ts +2 -0
  49. package/dist/types/runtime-mcp/manager.d.ts +56 -0
  50. package/dist/types/runtime-mcp/pool.d.ts +17 -0
  51. package/dist/types/runtime-mcp/redaction.d.ts +20 -0
  52. package/dist/types/runtime-mcp/startup-policy.d.ts +6 -0
  53. package/dist/types/runtime-mcp/tool-bridge.d.ts +9 -0
  54. package/dist/types/runtime-mcp/transports/http.d.ts +11 -0
  55. package/dist/types/sdk/acp/adapter.d.ts +27 -0
  56. package/dist/types/sdk/acp/final-text.d.ts +8 -0
  57. package/dist/types/sdk/broker/broker.d.ts +13 -2
  58. package/dist/types/sdk/broker/ensure.d.ts +46 -1
  59. package/dist/types/sdk/broker/identity.d.ts +3 -1
  60. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +23 -3
  61. package/dist/types/sdk/broker/lifecycle.d.ts +15 -8
  62. package/dist/types/sdk/broker/runtime.d.ts +1 -1
  63. package/dist/types/sdk/broker/session-index.d.ts +22 -3
  64. package/dist/types/sdk/broker/spawn-authority.d.ts +4 -0
  65. package/dist/types/sdk/broker/stderr-drainer.d.ts +13 -0
  66. package/dist/types/sdk/bus/index.d.ts +19 -0
  67. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
  68. package/dist/types/sdk/bus/telegram-daemon.d.ts +5 -0
  69. package/dist/types/sdk/cli/master-cli.d.ts +6 -10
  70. package/dist/types/sdk/cli/rows.d.ts +19 -1
  71. package/dist/types/sdk/cli/session-cli.d.ts +22 -5
  72. package/dist/types/sdk/client/client.d.ts +2 -0
  73. package/dist/types/sdk/guides/cli.d.ts +3 -4
  74. package/dist/types/sdk/guides/verify.d.ts +2 -0
  75. package/dist/types/sdk/host/host.d.ts +9 -0
  76. package/dist/types/sdk/host/session-runtime.d.ts +31 -0
  77. package/dist/types/sdk/host/types.d.ts +10 -0
  78. package/dist/types/sdk/index.d.ts +1 -0
  79. package/dist/types/sdk/prompt-deadline-flush.d.ts +83 -0
  80. package/dist/types/sdk/prompt-deadline-manager.d.ts +38 -1
  81. package/dist/types/sdk/prompt-failure.d.ts +30 -1
  82. package/dist/types/sdk/prompt-status.d.ts +8 -0
  83. package/dist/types/sdk/prompt-tool-boundary.d.ts +73 -0
  84. package/dist/types/sdk/router/replay-retention-gap-cache.d.ts +13 -0
  85. package/dist/types/sdk/router/session-router.d.ts +2 -0
  86. package/dist/types/sdk/startup-capability.d.ts +2 -0
  87. package/dist/types/sdk/transport/relay.d.ts +1 -0
  88. package/dist/types/sdk/transport/serve-cli.d.ts +73 -2
  89. package/dist/types/sdk/transport/stdio.d.ts +5 -1
  90. package/dist/types/session/agent-session.d.ts +168 -21
  91. package/dist/types/session/fallback-chain-controller.d.ts +6 -6
  92. package/dist/types/session/fold-coordinator.d.ts +17 -3
  93. package/dist/types/session/internal/managed-session-storage.d.ts +7 -1
  94. package/dist/types/session/session-manager.d.ts +64 -1
  95. package/dist/types/session/session-work-lease.d.ts +14 -0
  96. package/dist/types/setup/model-onboarding-guidance.d.ts +1 -1
  97. package/dist/types/setup/provider-onboarding.d.ts +81 -1
  98. package/dist/types/thinking.d.ts +3 -1
  99. package/dist/types/tools/bash.d.ts +37 -11
  100. package/dist/types/tools/job.d.ts +1 -1
  101. package/dist/types/tools/steer-fold.d.ts +31 -0
  102. package/dist/types/utils/git.d.ts +28 -2
  103. package/dist/types/utils/mupdf-embedded.d.ts +2 -0
  104. package/dist/types/utils/mupdf-wasm-embedded.d.ts +2 -0
  105. package/dist/types/utils/mupdf.d.ts +4 -0
  106. package/dist/types/utils/session-title-generation.d.ts +6 -0
  107. package/dist/types/utils/title-generator.d.ts +11 -2
  108. package/dist/types/web/scrapers/types.d.ts +3 -0
  109. package/package.json +18 -10
  110. package/scripts/build-binary.ts +9 -3
  111. package/scripts/compile-args.ts +0 -9
  112. package/scripts/embed-mupdf.ts +52 -0
  113. package/scripts/generate-sdk-operation-inventory.ts +31 -1
  114. package/scripts/vendor-markit.ts +196 -0
  115. package/src/async/job-manager.ts +60 -9
  116. package/src/cli/daemon-cli.ts +53 -18
  117. package/src/cli/github-release.ts +87 -5
  118. package/src/cli/mcp-cli.ts +38 -4
  119. package/src/cli/public-command-entry.ts +450 -0
  120. package/src/cli/public-command-errors.ts +581 -0
  121. package/src/cli/public-command-evidence.ts +717 -0
  122. package/src/cli/public-command-help.ts +360 -0
  123. package/src/cli/public-command-registry.ts +535 -0
  124. package/src/cli/session-picker.ts +2 -1
  125. package/src/cli/setup-cli.ts +9 -1
  126. package/src/cli/skills-cli.ts +46 -2
  127. package/src/cli.ts +61 -1
  128. package/src/commands/daemon.ts +65 -69
  129. package/src/commands/sdk.ts +232 -100
  130. package/src/commands/setup.ts +2 -0
  131. package/src/commands/skills.ts +11 -0
  132. package/src/config/autorouting-tier-map.ts +16 -0
  133. package/src/config/file-lock.ts +199 -68
  134. package/src/config/model-profile-activation.ts +406 -53
  135. package/src/config/model-registry.ts +204 -25
  136. package/src/config/settings-schema.ts +26 -2
  137. package/src/coordinator-mcp/policy.ts +10 -5
  138. package/src/coordinator-mcp/question-gate-codec.ts +17 -0
  139. package/src/coordinator-mcp/question-state.ts +175 -8
  140. package/src/coordinator-mcp/server.ts +528 -87
  141. package/src/coordinator-mcp/session-reaper.ts +6 -4
  142. package/src/crash/record-loader.ts +6 -2
  143. package/src/daemon/operator-contract.ts +26 -12
  144. package/src/debug/report-bundle.ts +114 -24
  145. package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
  146. package/src/eval/js/context-manager.ts +8 -8
  147. package/src/eval/js/worker-core.ts +9 -4
  148. package/src/eval/js/worker-protocol.ts +2 -0
  149. package/src/extensibility/extensions/runner.ts +52 -4
  150. package/src/extensibility/extensions/types.ts +10 -0
  151. package/src/extensibility/gjc-plugins/registry.ts +196 -37
  152. package/src/extensibility/runtime-skill-discovery.ts +61 -3
  153. package/src/extensibility/shared-events.ts +3 -1
  154. package/src/gjc-runtime/managed-owner-admission.ts +67 -50
  155. package/src/gjc-runtime/session-state-lock.ts +165 -24
  156. package/src/gjc-runtime/ultragoal-succession.ts +31 -3
  157. package/src/internal-urls/docs-index.generated.ts +21 -19
  158. package/src/internal-urls/embedded-protocol.ts +83 -0
  159. package/src/internal-urls/index.ts +1 -0
  160. package/src/internal-urls/parse.ts +21 -1
  161. package/src/internal-urls/router.ts +4 -2
  162. package/src/main.ts +123 -55
  163. package/src/modes/acp/acp-agent.ts +593 -47
  164. package/src/modes/acp/acp-event-mapper.ts +9 -0
  165. package/src/modes/acp/startup-provenance.ts +59 -0
  166. package/src/modes/components/custom-provider-wizard.ts +308 -7
  167. package/src/modes/components/model-selector.ts +31 -15
  168. package/src/modes/components/oauth-selector.ts +9 -0
  169. package/src/modes/components/session-selector.ts +47 -4
  170. package/src/modes/components/status-line/segments.ts +6 -1
  171. package/src/modes/components/thinking-selector.ts +7 -3
  172. package/src/modes/components/user-message-selector.ts +57 -15
  173. package/src/modes/controllers/command-controller.ts +58 -32
  174. package/src/modes/controllers/extension-ui-controller.ts +33 -7
  175. package/src/modes/controllers/input-controller.ts +4 -0
  176. package/src/modes/controllers/selector-controller.ts +177 -18
  177. package/src/modes/interactive-mode.ts +21 -7
  178. package/src/modes/runtime-init.ts +1 -0
  179. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +124 -7
  180. package/src/modes/shared/agent-wire/workflow-gate-types.ts +16 -2
  181. package/src/modes/types.ts +2 -2
  182. package/src/prompts/agent-fragments/browser-aside.md +1 -0
  183. package/src/prompts/system/title-system.md +1 -1
  184. package/src/prompts/tools/bash.md +1 -1
  185. package/src/prompts/tools/job.md +1 -1
  186. package/src/prompts/tools/read.md +2 -0
  187. package/src/runtime/network-prewarm-service.test.ts +36 -1
  188. package/src/runtime/network-prewarm-service.ts +28 -3
  189. package/src/runtime/process-lifecycle.ts +22 -11
  190. package/src/runtime-mcp/config.ts +16 -1
  191. package/src/runtime-mcp/manager.ts +689 -55
  192. package/src/runtime-mcp/pool.test.ts +31 -0
  193. package/src/runtime-mcp/pool.ts +304 -28
  194. package/src/runtime-mcp/redaction.ts +42 -0
  195. package/src/runtime-mcp/startup-policy.ts +7 -0
  196. package/src/runtime-mcp/tool-bridge.ts +71 -0
  197. package/src/runtime-mcp/transports/http.ts +108 -25
  198. package/src/sdk/acp/adapter.ts +90 -9
  199. package/src/sdk/acp/final-text.ts +19 -2
  200. package/src/sdk/broker/broker.ts +378 -52
  201. package/src/sdk/broker/ensure.ts +97 -19
  202. package/src/sdk/broker/identity.ts +9 -1
  203. package/src/sdk/broker/lifecycle-ledger.ts +95 -31
  204. package/src/sdk/broker/lifecycle.ts +690 -141
  205. package/src/sdk/broker/runtime.ts +1 -1
  206. package/src/sdk/broker/session-index.ts +97 -4
  207. package/src/sdk/broker/spawn-authority.ts +35 -7
  208. package/src/sdk/broker/stderr-drainer.ts +59 -0
  209. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  210. package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
  211. package/src/sdk/bus/index.ts +721 -84
  212. package/src/sdk/bus/telegram-daemon-cli.ts +31 -2
  213. package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
  214. package/src/sdk/bus/telegram-daemon.ts +10 -3
  215. package/src/sdk/cli/master-cli.ts +47 -36
  216. package/src/sdk/cli/rows.ts +30 -7
  217. package/src/sdk/cli/session-cli.ts +687 -184
  218. package/src/sdk/client/client.ts +18 -0
  219. package/src/sdk/guides/catalog.ts +2 -2
  220. package/src/sdk/guides/cli.ts +20 -62
  221. package/src/sdk/guides/verify.ts +14 -0
  222. package/src/sdk/host/host.ts +49 -5
  223. package/src/sdk/host/session-runtime.test.ts +636 -9
  224. package/src/sdk/host/session-runtime.ts +499 -118
  225. package/src/sdk/host/types.ts +12 -1
  226. package/src/sdk/host/websocket-transport.lifecycle.test.ts +84 -0
  227. package/src/sdk/host/websocket-transport.ts +40 -6
  228. package/src/sdk/index.ts +11 -0
  229. package/src/sdk/prompt-deadline-flush.ts +334 -0
  230. package/src/sdk/prompt-deadline-manager.ts +136 -4
  231. package/src/sdk/prompt-failure.ts +43 -1
  232. package/src/sdk/prompt-status.ts +8 -0
  233. package/src/sdk/prompt-tool-boundary.ts +121 -0
  234. package/src/sdk/protocol/operation-inventory.generated.json +106 -0
  235. package/src/sdk/router/replay-retention-gap-cache.ts +27 -0
  236. package/src/sdk/router/session-router.ts +31 -4
  237. package/src/sdk/session.ts +194 -16
  238. package/src/sdk/startup-capability.ts +1 -1
  239. package/src/sdk/transport/relay.ts +71 -6
  240. package/src/sdk/transport/serve-cli.ts +494 -49
  241. package/src/sdk/transport/socket.ts +12 -11
  242. package/src/sdk/transport/stdio.ts +7 -3
  243. package/src/session/agent-session.ts +2437 -400
  244. package/src/session/fallback-chain-controller.ts +6 -7
  245. package/src/session/fold-coordinator.ts +24 -5
  246. package/src/session/internal/managed-session-scope.ts +58 -18
  247. package/src/session/internal/managed-session-storage.ts +304 -11
  248. package/src/session/session-manager.ts +382 -44
  249. package/src/session/session-work-lease.ts +34 -0
  250. package/src/session-import/codex.ts +12 -3
  251. package/src/setup/model-onboarding-guidance.ts +1 -1
  252. package/src/setup/provider-onboarding.ts +397 -24
  253. package/src/slash-commands/builtin-registry.ts +258 -14
  254. package/src/task/index.ts +14 -2
  255. package/src/thinking.ts +21 -2
  256. package/src/tools/bash.ts +98 -78
  257. package/src/tools/fetch.ts +119 -20
  258. package/src/tools/job.ts +151 -25
  259. package/src/tools/output-meta.ts +15 -2
  260. package/src/tools/path-utils.ts +27 -3
  261. package/src/tools/read.ts +3 -3
  262. package/src/tools/steer-fold.ts +107 -0
  263. package/src/tools/tool-catalog.generated.ts +29 -2
  264. package/src/tools/tool-errors.ts +4 -0
  265. package/src/utils/git.ts +53 -3
  266. package/src/utils/markit.ts +55 -41
  267. package/src/utils/mupdf-assets.d.ts +4 -0
  268. package/src/utils/mupdf-embedded.ts +4 -0
  269. package/src/utils/mupdf-wasm-embedded.ts +3 -0
  270. package/src/utils/mupdf-wasm.ts +24 -2
  271. package/src/utils/mupdf.ts +129 -0
  272. package/src/utils/session-title-generation.ts +29 -0
  273. package/src/utils/title-generator.ts +46 -2
  274. package/src/web/scrapers/types.ts +24 -3
  275. package/vendor/markit-ai/LICENSE +21 -0
  276. package/vendor/markit-ai/MANIFEST.json +412 -0
  277. package/vendor/markit-ai/dist/commands/config.d.ts +4 -0
  278. package/vendor/markit-ai/dist/commands/config.js +139 -0
  279. package/vendor/markit-ai/dist/commands/convert.d.ts +6 -0
  280. package/vendor/markit-ai/dist/commands/convert.js +113 -0
  281. package/vendor/markit-ai/dist/commands/formats.d.ts +2 -0
  282. package/vendor/markit-ai/dist/commands/formats.js +92 -0
  283. package/vendor/markit-ai/dist/commands/init.d.ts +2 -0
  284. package/vendor/markit-ai/dist/commands/init.js +29 -0
  285. package/vendor/markit-ai/dist/commands/onboard.d.ts +2 -0
  286. package/vendor/markit-ai/dist/commands/onboard.js +61 -0
  287. package/vendor/markit-ai/dist/commands/plugin.d.ts +4 -0
  288. package/vendor/markit-ai/dist/commands/plugin.js +58 -0
  289. package/vendor/markit-ai/dist/config.d.ts +26 -0
  290. package/vendor/markit-ai/dist/config.js +42 -0
  291. package/vendor/markit-ai/dist/converters/audio.d.ts +7 -0
  292. package/vendor/markit-ai/dist/converters/audio.js +104 -0
  293. package/vendor/markit-ai/dist/converters/csv.d.ts +7 -0
  294. package/vendor/markit-ai/dist/converters/csv.js +83 -0
  295. package/vendor/markit-ai/dist/converters/docx.d.ts +6 -0
  296. package/vendor/markit-ai/dist/converters/docx.js +57 -0
  297. package/vendor/markit-ai/dist/converters/epub.d.ts +8 -0
  298. package/vendor/markit-ai/dist/converters/epub.js +128 -0
  299. package/vendor/markit-ai/dist/converters/github.d.ts +18 -0
  300. package/vendor/markit-ai/dist/converters/github.js +148 -0
  301. package/vendor/markit-ai/dist/converters/html.d.ts +6 -0
  302. package/vendor/markit-ai/dist/converters/html.js +30 -0
  303. package/vendor/markit-ai/dist/converters/image.d.ts +6 -0
  304. package/vendor/markit-ai/dist/converters/image.js +124 -0
  305. package/vendor/markit-ai/dist/converters/ipynb.d.ts +6 -0
  306. package/vendor/markit-ai/dist/converters/ipynb.js +75 -0
  307. package/vendor/markit-ai/dist/converters/iwork.d.ts +20 -0
  308. package/vendor/markit-ai/dist/converters/iwork.js +391 -0
  309. package/vendor/markit-ai/dist/converters/json.d.ts +6 -0
  310. package/vendor/markit-ai/dist/converters/json.js +21 -0
  311. package/vendor/markit-ai/dist/converters/pdf/columns.d.ts +35 -0
  312. package/vendor/markit-ai/dist/converters/pdf/columns.js +93 -0
  313. package/vendor/markit-ai/dist/converters/pdf/extract.d.ts +19 -0
  314. package/vendor/markit-ai/dist/converters/pdf/extract.js +514 -0
  315. package/vendor/markit-ai/dist/converters/pdf/grid.d.ts +25 -0
  316. package/vendor/markit-ai/dist/converters/pdf/grid.js +795 -0
  317. package/vendor/markit-ai/dist/converters/pdf/headers.d.ts +24 -0
  318. package/vendor/markit-ai/dist/converters/pdf/headers.js +108 -0
  319. package/vendor/markit-ai/dist/converters/pdf/index.d.ts +19 -0
  320. package/vendor/markit-ai/dist/converters/pdf/index.js +136 -0
  321. package/vendor/markit-ai/dist/converters/pdf/mupdf-loader.js +9 -0
  322. package/vendor/markit-ai/dist/converters/pdf/render.d.ts +24 -0
  323. package/vendor/markit-ai/dist/converters/pdf/render.js +513 -0
  324. package/vendor/markit-ai/dist/converters/pdf/types.d.ts +75 -0
  325. package/vendor/markit-ai/dist/converters/pdf/types.js +1 -0
  326. package/vendor/markit-ai/dist/converters/plain-text.d.ts +6 -0
  327. package/vendor/markit-ai/dist/converters/plain-text.js +87 -0
  328. package/vendor/markit-ai/dist/converters/pptx.d.ts +8 -0
  329. package/vendor/markit-ai/dist/converters/pptx.js +289 -0
  330. package/vendor/markit-ai/dist/converters/rss.d.ts +11 -0
  331. package/vendor/markit-ai/dist/converters/rss.js +138 -0
  332. package/vendor/markit-ai/dist/converters/wikipedia.d.ts +6 -0
  333. package/vendor/markit-ai/dist/converters/wikipedia.js +33 -0
  334. package/vendor/markit-ai/dist/converters/xlsx.d.ts +8 -0
  335. package/vendor/markit-ai/dist/converters/xlsx.js +147 -0
  336. package/vendor/markit-ai/dist/converters/xml.d.ts +6 -0
  337. package/vendor/markit-ai/dist/converters/xml.js +18 -0
  338. package/vendor/markit-ai/dist/converters/yaml.d.ts +6 -0
  339. package/vendor/markit-ai/dist/converters/yaml.js +17 -0
  340. package/vendor/markit-ai/dist/converters/zip.d.ts +8 -0
  341. package/vendor/markit-ai/dist/converters/zip.js +57 -0
  342. package/vendor/markit-ai/dist/index.d.ts +30 -0
  343. package/vendor/markit-ai/dist/index.js +26 -0
  344. package/vendor/markit-ai/dist/main.d.ts +2 -0
  345. package/vendor/markit-ai/dist/main.js +191 -0
  346. package/vendor/markit-ai/dist/markit.d.ts +45 -0
  347. package/vendor/markit-ai/dist/markit.js +267 -0
  348. package/vendor/markit-ai/dist/plugins/api.d.ts +7 -0
  349. package/vendor/markit-ai/dist/plugins/api.js +42 -0
  350. package/vendor/markit-ai/dist/plugins/index.d.ts +4 -0
  351. package/vendor/markit-ai/dist/plugins/index.js +3 -0
  352. package/vendor/markit-ai/dist/plugins/installer.d.ts +25 -0
  353. package/vendor/markit-ai/dist/plugins/installer.js +176 -0
  354. package/vendor/markit-ai/dist/plugins/loader.d.ts +6 -0
  355. package/vendor/markit-ai/dist/plugins/loader.js +61 -0
  356. package/vendor/markit-ai/dist/plugins/types.d.ts +25 -0
  357. package/vendor/markit-ai/dist/plugins/types.js +1 -0
  358. package/vendor/markit-ai/dist/providers/anthropic.d.ts +2 -0
  359. package/vendor/markit-ai/dist/providers/anthropic.js +47 -0
  360. package/vendor/markit-ai/dist/providers/index.d.ts +21 -0
  361. package/vendor/markit-ai/dist/providers/index.js +56 -0
  362. package/vendor/markit-ai/dist/providers/openai.d.ts +2 -0
  363. package/vendor/markit-ai/dist/providers/openai.js +76 -0
  364. package/vendor/markit-ai/dist/providers/types.d.ts +26 -0
  365. package/vendor/markit-ai/dist/providers/types.js +1 -0
  366. package/vendor/markit-ai/dist/types.d.ts +36 -0
  367. package/vendor/markit-ai/dist/types.js +1 -0
  368. package/vendor/markit-ai/dist/utils/exit-codes.d.ts +4 -0
  369. package/vendor/markit-ai/dist/utils/exit-codes.js +4 -0
  370. package/vendor/markit-ai/dist/utils/output.d.ts +22 -0
  371. package/vendor/markit-ai/dist/utils/output.js +31 -0
  372. package/vendor/markit-ai/dist/utils/turndown.d.ts +8 -0
  373. package/vendor/markit-ai/dist/utils/turndown.js +70 -0
  374. package/vendor/markit-ai/package.json +74 -0
  375. package/vendor/markit-ai.patch +49 -0
@@ -46,15 +46,25 @@ import {
46
46
  AcpSdkAdapterError,
47
47
  acpMcpLaunchFailure,
48
48
  } from "../../sdk/acp";
49
- import { resolveAcpFinalText } from "../../sdk/acp/final-text";
49
+ import { hasAcpFinalTextContent, resolveAcpFinalText } from "../../sdk/acp/final-text";
50
50
  import type { SessionLifecycleMcpServer } from "../../sdk/acp/mcp";
51
51
  import { ensureBroker } from "../../sdk/broker/ensure";
52
52
  import { canonicalSessionCwd } from "../../sdk/broker/session-index";
53
53
  import { readSdkBrokerDiscovery, SdkClient, SdkClientError } from "../../sdk/client";
54
54
  import type { AbortScope } from "../../sdk/host/control/operations";
55
55
  import { SYNTHETIC_PROVIDER_ID } from "../../sdk/model-profile-namespace";
56
- import { failedPromptOutcome, isSdkPromptFailurePhase, rephaseFailedOutcome } from "../../sdk/prompt-failure";
57
- import type { SdkPromptTerminalOutcome } from "../../sdk/prompt-status";
56
+ import {
57
+ failedPromptOutcome,
58
+ isSafePromptFailureCode,
59
+ isSdkPromptFailurePhase,
60
+ promptFailureRetryability,
61
+ rephaseFailedOutcome,
62
+ } from "../../sdk/prompt-failure";
63
+ import type {
64
+ SdkPromptFailureCategory,
65
+ SdkPromptFailurePhase,
66
+ SdkPromptTerminalOutcome,
67
+ } from "../../sdk/prompt-status";
58
68
  import { PromptActivity, type PromptWatchdogClock, systemPromptWatchdogClock } from "../../sdk/prompt-watchdog";
59
69
  import { validateRequiredPromptText } from "../../sdk/protocol/adapter-validation";
60
70
  import { type SessionAttachment, SessionRouter, type SessionRouterFrame } from "../../sdk/router";
@@ -68,6 +78,7 @@ import {
68
78
  } from "./acp-event-mapper";
69
79
  import { resolveAcpPermissionMode } from "./permission-mode";
70
80
  import type { AcpStartupOptions } from "./startup-options";
81
+ import { resolveStartupProvenance } from "./startup-provenance";
71
82
  import { ACP_TERMINAL_AUTH_FLAG } from "./terminal-auth";
72
83
 
73
84
  const ACP_DEFAULT_MODE_ID = "default";
@@ -153,6 +164,9 @@ const ACP_FIRST_PROMPT_RETRY_BASE_DELAY_MS = 250;
153
164
  type JsonObject = Record<string, unknown>;
154
165
  interface PromptWaiter {
155
166
  acknowledged: boolean;
167
+ invocationKind: "prompt" | "skill";
168
+ /** ACP-owned identity, never inherited from caller metadata or reused for replay. */
169
+ clientRef: string;
156
170
  /** True once turn.prompt / skill.invoke has been sent; cancel must not fake-settle after this. */
157
171
  dispatched: boolean;
158
172
  /** True only while the dispatched control request can still reveal its correlation. */
@@ -177,6 +191,8 @@ interface PromptWaiter {
177
191
  lastFrameAt: number;
178
192
  /** Type of that prompt-owned frame, reported when the watchdog expires. */
179
193
  lastFrameType: string;
194
+ /** True while the prompt is blocked in provider preflight, before the turn is dispatched. */
195
+ awaitingProviderPreflight: boolean;
180
196
  /** Cancels the armed inactivity watchdog; re-armed by prompt-owned frames. */
181
197
  cancelWatchdog?: () => void;
182
198
  /** What the host is observably doing — a tool running, a model call unanswered — and the bound that follows from it. */
@@ -185,6 +201,12 @@ interface PromptWaiter {
185
201
  observedTurnActivity: boolean;
186
202
  /** True once a prompt-owned tool execution frame was observed; vetoes a first-turn retry that would re-run it. */
187
203
  observedToolExecution: boolean;
204
+ /**
205
+ * Newest `plan` update published for this prompt, retained so an abandoned turn can report
206
+ * what it had left to do (issue #5669). Absent means no plan was ever observed, which is
207
+ * evidence of nothing — distinct from an observed plan whose entries are all `completed`.
208
+ */
209
+ planSnapshot?: AcpPlanSnapshot;
188
210
  /** Coordinates a prompt-control rejection racing an acknowledged ACP cancellation. */
189
211
  cancelAttempt?: Promise<boolean>;
190
212
  /** Whether ANY overlapping cancel attempt for this prompt was acknowledged:
@@ -239,6 +261,8 @@ type SessionRecord = {
239
261
  /** Replayable startup notice captured before ACP bootstrap; emitted once after the session id is known. */
240
262
  routingInactiveNotice?: string;
241
263
  activePrompt?: PromptWaiter;
264
+ /** One bounded read-only recovery owner for this attachment and waiter. */
265
+ statusRecovery?: PromptWaiter;
242
266
  /** Set by `session/cancel` so an in-flight prompt settles as `cancelled`, never as an error. */
243
267
  cancelRequested?: boolean;
244
268
  /** True once the session's first logical prompt has settled; gates first-turn readiness retries. */
@@ -618,7 +642,7 @@ type SdkPromptFailedOutcome = Extract<SdkPromptTerminalOutcome, { kind: "failed"
618
642
  * category — so settlement carries that classification instead of discarding it into a bare
619
643
  * `AcpSdkAdapterError` (review P1, issue #5574).
620
644
  */
621
- class AcpPromptFailureError extends AcpSdkAdapterError {
645
+ export class AcpPromptFailureError extends AcpSdkAdapterError {
622
646
  readonly failure: SdkPromptFailedOutcome;
623
647
  constructor(failure: SdkPromptFailedOutcome) {
624
648
  super(failure.code, failure.message);
@@ -626,6 +650,202 @@ class AcpPromptFailureError extends AcpSdkAdapterError {
626
650
  }
627
651
  }
628
652
 
653
+ /**
654
+ * Recover the shared prompt-failure projection for a direct SDK rejection.
655
+ *
656
+ * A terminal frame normally reaches {@link AcpPromptFailureError} through
657
+ * `#settlePrompt`, but a host may reject the `turn.prompt` control request
658
+ * itself with `code: "prompt_failed"`. That path never has a terminal outcome
659
+ * to retain, so translating the raw adapter error used to bypass
660
+ * `promptFailureWireData` and publish only the bare `code`/`details` pair.
661
+ * Keep the fallback deliberately narrow: only the two prompt terminal codes
662
+ * are upgraded, and any optional provider classifier is re-validated before
663
+ * it enters the existing classifier/projection seam.
664
+ */
665
+ function promptFailureFromDirectError(error: unknown): SdkPromptFailedOutcome | undefined {
666
+ if (error instanceof AcpPromptFailureError) return error.failure;
667
+ let code: unknown;
668
+ let providerCode: unknown;
669
+ let phase: unknown;
670
+ try {
671
+ const candidate = error as {
672
+ code?: unknown;
673
+ providerCode?: unknown;
674
+ transportFailure?: { providerCode?: unknown };
675
+ phase?: unknown;
676
+ };
677
+ code = candidate?.code;
678
+ providerCode = candidate?.providerCode ?? candidate?.transportFailure?.providerCode;
679
+ phase = candidate?.phase;
680
+ } catch {
681
+ return undefined;
682
+ }
683
+ if (code !== "prompt_failed" && code !== "prompt_deadline_exceeded") return undefined;
684
+ return failedPromptOutcome({
685
+ code,
686
+ provenance: code === "prompt_deadline_exceeded" ? "deadline" : "agent_failed",
687
+ ...(isSafePromptFailureCode(providerCode) ? { providerCode } : {}),
688
+ ...(isSdkPromptFailurePhase(phase) ? { phase } : {}),
689
+ evidence: {},
690
+ });
691
+ }
692
+
693
+ /**
694
+ * The wire projection of a prompt terminal's classification (issue #5615).
695
+ *
696
+ * `super(failure.code, failure.message)` above narrows the error to the generic
697
+ * `prompt_failed` plus its fixed redacted message, so the classification the
698
+ * terminal already computed used to die at the JSON-RPC boundary: an ACP client
699
+ * saw only `-32603 {code, details}` and could not tell a transient provider blip
700
+ * from a dead session without parsing English. These fields carry it across.
701
+ *
702
+ * `providerCode` is re-checked against the safe-token rule rather than trusted:
703
+ * `terminalOutcome` accepts the host's `providerCode` on a `typeof` check alone,
704
+ * and this is the first path that puts it on the wire, so an unbounded value is
705
+ * dropped instead of becoming a leak of provider text (the redaction contract in
706
+ * `sanitizePromptFailure`). A dropped or absent code is omitted, never nulled.
707
+ */
708
+ export const OPERATOR_UPSTREAM_LABEL = "Upstream provider failure";
709
+ export const OPERATOR_UPSTREAM_SUFFIX = ": the model provider ended this turn.";
710
+ /**
711
+ * `post_start` proves execution began, so tools may already have run and had effects before the
712
+ * provider died. The wording therefore qualifies the turn rather than absolving the task: saying
713
+ * the task was unaffected would assert something the bounded classifiers do not establish.
714
+ */
715
+ export const OPERATOR_PARTIAL_WORK_NOTE =
716
+ " Work this turn had already performed may have taken effect; check before retrying.";
717
+
718
+ /**
719
+ * Operator-facing wording for a post-start terminal, assembled ONLY from bounded safe tokens: the
720
+ * classifier `phase`, the `category`, and a `providerCode` that passes `isSafePromptFailureCode`.
721
+ * Raw provider text never reaches this function and must never reach it.
722
+ *
723
+ * Gated on BOTH `phase === "post_start"` and `category === "provider_transport"`. A submission-phase
724
+ * provider failure is rejected by the phase gate: nothing executed, so wording about a turn the
725
+ * provider ended would describe an event that never occurred.
726
+ *
727
+ * It claims only what those tokens prove — that the upstream provider ended the turn — and says
728
+ * nothing about whether the operator's work survived. `post_start` proves execution began, so the
729
+ * wording qualifies that earlier work may already have taken effect instead of absolving the task.
730
+ *
731
+ * This is additive. The wire `code`/`details` pair and every `PROMPT_FAILURE_MESSAGE_*` constant
732
+ * stay exactly as they are — pinned ACP core-v1 conformance asserts on them — so this wording
733
+ * travels alongside the redacted message instead of repurposing it.
734
+ *
735
+ * It says NOTHING about the worktree, deliberately. Nothing on this path inspects the worktree, so
736
+ * any sentence about uncommitted work would assert a check that never ran — reporting an
737
+ * uninspected tree as clean is the absence-of-evidence-as-evidence-of-absence error, and an
738
+ * operator who believed it would sweep work away. Silence is the only honest option until something
739
+ * actually looks.
740
+ *
741
+ * Returns `undefined` for every other category: `phase` and `category` are already on the wire, so
742
+ * restating them in prose tells an operator nothing they do not already have.
743
+ */
744
+ export function postStartOperatorMessage(input: {
745
+ phase: SdkPromptFailurePhase;
746
+ category: SdkPromptFailureCategory;
747
+ providerCode?: string;
748
+ }): string | undefined {
749
+ // Phase first: a submission-phase provider failure never started executing, so wording about a
750
+ // turn that ended mid-flight would describe something that did not happen. `phase` is a bounded
751
+ // classifier already on the wire, so gating on it widens nothing.
752
+ if (input.phase !== "post_start") return undefined;
753
+ if (input.category !== "provider_transport") return undefined;
754
+ const code = isSafePromptFailureCode(input.providerCode) ? ` (${input.providerCode})` : "";
755
+ return `${OPERATOR_UPSTREAM_LABEL}${code}${OPERATOR_UPSTREAM_SUFFIX}${OPERATOR_PARTIAL_WORK_NOTE}`;
756
+ }
757
+
758
+ function promptFailureWireData(failure: SdkPromptFailedOutcome): Record<string, string> {
759
+ // `operatorMessage` says, in words, what the classifier tokens already say in codes: that the
760
+ // cause was an upstream provider problem rather than a failure of the operator's task. Built
761
+ // only from those same bounded tokens, so it widens nothing that reaches the wire.
762
+ const operatorMessage = postStartOperatorMessage({
763
+ phase: failure.phase,
764
+ category: failure.category,
765
+ ...(failure.providerCode === undefined ? {} : { providerCode: failure.providerCode }),
766
+ });
767
+ return {
768
+ phase: failure.phase,
769
+ category: failure.category,
770
+ retryability: promptFailureRetryability(failure.category),
771
+ ...(isSafePromptFailureCode(failure.providerCode) ? { providerCode: failure.providerCode } : {}),
772
+ ...(operatorMessage === undefined ? {} : { operatorMessage }),
773
+ };
774
+ }
775
+
776
+ /**
777
+ * What an ACP `plan` update said the turn intended to do. Structural rather than the SDK's
778
+ * `PlanEntry` so only the fields this evidence reads are depended on.
779
+ */
780
+ type AcpPlanSnapshot = Array<{
781
+ content: string;
782
+ status: "pending" | "in_progress" | "completed";
783
+ _meta?: { gjcTodoStatus?: unknown } | null;
784
+ }>;
785
+
786
+ /**
787
+ * An abandoned prompt that still carries the plan its turn was working through (issue #5669).
788
+ *
789
+ * The watchdog settles a prompt whose host stopped producing frames, and that rejection used to
790
+ * be a bare `AcpSdkAdapterError`: `{code, details}` and nothing else. A wrapper could not tell
791
+ * "the agent finished" from "the agent stopped with work left", so an abandoned turn's unverified
792
+ * diff was published as if it were complete. The last observed plan is the evidence that settles
793
+ * that question, so settlement carries it instead of dropping it at the JSON-RPC boundary.
794
+ */
795
+ export class AcpPromptAbandonedError extends AcpSdkAdapterError {
796
+ readonly plan: AcpPlanSnapshot | undefined;
797
+ constructor(code: string, message: string, plan: AcpPlanSnapshot | undefined) {
798
+ super(code, message);
799
+ this.plan = plan;
800
+ }
801
+ }
802
+
803
+ /** Most pending plan entries that may reach the wire. */
804
+ const ACP_ABANDON_PLAN_MAX_ENTRIES = 10;
805
+ /** Longest one pending entry's text may be on the wire. */
806
+ const ACP_ABANDON_PLAN_MAX_CONTENT_CHARS = 200;
807
+
808
+ /**
809
+ * Whether a plan entry describes work the turn never finished (issue #5669).
810
+ *
811
+ * An entry whose ACP status is `completed` can still be unfinished: ACP's `PlanEntryStatus` has no
812
+ * `abandoned` member, so a task dropped via `/todo drop` is projected as `completed` for the
813
+ * client's plan UI while the mapper keeps the internal status in `_meta.gjcTodoStatus`. Completion
814
+ * evidence has to read that internal truth — reading the wire status alone would report a plan of
815
+ * nothing but dropped tasks as a finished turn. `_meta` crosses a JSON boundary, so the internal
816
+ * status is compared as `unknown` rather than asserted into a type.
817
+ */
818
+ function planEntryUnfinished(entry: AcpPlanSnapshot[number]): boolean {
819
+ return entry.status !== "completed" || entry._meta?.gjcTodoStatus === "abandoned";
820
+ }
821
+
822
+ /**
823
+ * The wire projection of an abandoned prompt's plan (issue #5669).
824
+ *
825
+ * Plan text is model-authored, so it is bounded the same way `promptFailureWireData` bounds a
826
+ * provider classifier: a capped number of entries, each capped in length, and never restated in
827
+ * the human message. The counts are what a wrapper actually gates on; the contents are there so
828
+ * the reader can name the unfinished steps without a log dive.
829
+ *
830
+ * An absent plan yields no keys at all. "No plan was ever observed" is not the same claim as
831
+ * "the plan was complete", and a client must be able to tell them apart, so the fields are
832
+ * omitted rather than nulled or reported as unknown.
833
+ */
834
+ function promptAbandonWireData(plan: AcpPlanSnapshot | undefined): Record<string, string> {
835
+ if (!plan) return {};
836
+ const pending = plan.filter(planEntryUnfinished);
837
+ return {
838
+ planIncomplete: pending.length > 0 ? "true" : "false",
839
+ planPendingCount: String(pending.length),
840
+ planTotalCount: String(plan.length),
841
+ planPending: JSON.stringify(
842
+ pending
843
+ .slice(0, ACP_ABANDON_PLAN_MAX_ENTRIES)
844
+ .map(entry => entry.content.slice(0, ACP_ABANDON_PLAN_MAX_CONTENT_CHARS)),
845
+ ),
846
+ };
847
+ }
848
+
629
849
  /**
630
850
  * The startup-readiness failure class (issue #5574): the turn failed AFTER it started, with a
631
851
  * bounded provider/transport classifier — the signature of a host that accepted the turn before
@@ -1251,9 +1471,22 @@ export function acpRequestFailure(error: unknown): unknown {
1251
1471
  const code = typeof error === "object" && error !== null && "code" in error ? error.code : undefined;
1252
1472
  if (typeof code !== "string") return error;
1253
1473
  const message = error instanceof Error ? error.message : code;
1474
+ // A prompt terminal additionally publishes its classification (issue #5615). The
1475
+ // spread is empty for every other error, so their payloads are byte-identical to
1476
+ // before. `code`/`details` and the JSON-RPC code itself are untouched either way:
1477
+ // this enriches `data`, it does not restate the redacted message.
1478
+ const promptFailure = promptFailureFromDirectError(error);
1479
+ const data =
1480
+ promptFailure !== undefined
1481
+ ? { code, details: message, ...promptFailureWireData(promptFailure) }
1482
+ : { code, details: message };
1483
+ // An abandoned prompt additionally publishes the plan it never finished (issue #5669).
1484
+ // Same rule as above: `code`/`details` and the JSON-RPC code are untouched, and an abandon
1485
+ // that observed no plan adds no keys, so its payload stays byte-identical to before.
1486
+ if (error instanceof AcpPromptAbandonedError) Object.assign(data, promptAbandonWireData(error.plan));
1254
1487
  switch (code) {
1255
1488
  case "authentication_failed":
1256
- return RequestError.authRequired({ code, details: message }, message);
1489
+ return RequestError.authRequired(data, message);
1257
1490
  // `not_found` stays -32603 with its discriminator in `data`: ACP's
1258
1491
  // `resourceNotFound` (-32002) is a URI-addressed resource error, and an unknown
1259
1492
  // session id is not a resource URI. Pinned ACP core-v1 conformance also requires
@@ -1261,12 +1494,15 @@ export function acpRequestFailure(error: unknown): unknown {
1261
1494
  case "invalid_input":
1262
1495
  case "unsupported":
1263
1496
  case "unsupported_content":
1264
- return RequestError.invalidParams({ code, details: message }, message);
1497
+ return RequestError.invalidParams(data, message);
1265
1498
  default:
1266
1499
  // The remaining internal codes (conflict, unavailable, busy, …) have no ACP
1267
1500
  // counterpart and stay -32603. Keep the discriminator in `data` so a client can
1268
- // branch on retry/reconnect instead of parsing an English message.
1269
- return RequestError.internalError({ code, details: message }, message);
1501
+ // branch on retry/reconnect instead of parsing an English message. `prompt_failed`
1502
+ // and `prompt_deadline_exceeded` land here and keep -32603 by design: pinned ACP
1503
+ // core-v1 conformance expects -32603/-32000 for this class, so the added
1504
+ // classification travels in `data` rather than in a renumbered code.
1505
+ return RequestError.internalError(data, message);
1270
1506
  }
1271
1507
  }
1272
1508
 
@@ -1384,7 +1620,7 @@ export class AcpAgent implements Agent {
1384
1620
  /** Retain settled prompt identities across automatic transport reattachment. */
1385
1621
  readonly #retiredPromptCorrelations = new Map<string, PromptCorrelation[]>();
1386
1622
  /** Prevent a successor admission while a retired prompt can still reveal its identity. */
1387
- readonly #retiredPromptAcknowledgements = new Set<string>();
1623
+ readonly #retiredPromptAcknowledgements = new Map<string, PromptWaiter>();
1388
1624
  /** Ready terminal metadata writes retain ownership across same-id record replacement. */
1389
1625
  readonly #terminalMetadataTails = new Map<string, Promise<void>>();
1390
1626
  /** Unstreamed terminal text retains transcript ownership across same-id replacement. */
@@ -1986,6 +2222,7 @@ export class AcpAgent implements Agent {
1986
2222
 
1987
2223
  const payload = acpPromptPayload(params.prompt);
1988
2224
  const skillInvocation = acpSkillInvocation(params.prompt);
2225
+ const clientRef = randomUUID();
1989
2226
  if (!skillInvocation) {
1990
2227
  const promptError = validateRequiredPromptText("turn.prompt", {
1991
2228
  text: payload.text,
@@ -2020,7 +2257,7 @@ export class AcpAgent implements Agent {
2020
2257
  JSON.stringify({
2021
2258
  type: "control_request",
2022
2259
  operation: skillInvocation ? "skill.invoke" : "turn.prompt",
2023
- input: skillInvocation ?? { text: payload.text, images: payload.images },
2260
+ input: { ...(skillInvocation ?? { text: payload.text, images: payload.images }), clientRef },
2024
2261
  // AcpSdkAdapter.control passes `confirm: false` to SdkClient, which serializes
2025
2262
  // the field even though it is not part of the skill input payload.
2026
2263
  ...(skillInvocation ? { confirm: false } : {}),
@@ -2042,6 +2279,8 @@ export class AcpAgent implements Agent {
2042
2279
  record.promptObservedAssistantOutput = false;
2043
2280
  const { promise: response, resolve, reject } = Promise.withResolvers<PromptResponse>();
2044
2281
  const waiter: PromptWaiter = {
2282
+ invocationKind: skillInvocation ? "skill" : "prompt",
2283
+ clientRef,
2045
2284
  acknowledged: false,
2046
2285
  dispatched: false,
2047
2286
  acknowledgementPending: false,
@@ -2055,6 +2294,7 @@ export class AcpAgent implements Agent {
2055
2294
  deferredActivityFrames: [],
2056
2295
  lastFrameAt: this.#promptWatchdogClock.now(),
2057
2296
  lastFrameType: "prompt_dispatch",
2297
+ awaitingProviderPreflight: true,
2058
2298
  activity: new PromptActivity(),
2059
2299
  observedTurnActivity: false,
2060
2300
  observedToolExecution: false,
@@ -2066,38 +2306,60 @@ export class AcpAgent implements Agent {
2066
2306
  // pending. Retain the original promise for the caller, but mark that delayed rejection
2067
2307
  // as observed until prompt() can resume and await it.
2068
2308
  void response.catch(() => undefined);
2309
+ // Silence has to be bounded from the moment the prompt owns the session: a host that
2310
+ // dies before it ever answers is exactly the failure that leaves the client running.
2311
+ // Provider preflight is part of that window — it awaits a lease the host may never win
2312
+ // — so the bound is armed before it, not after (issue #5658). The arm below re-arms
2313
+ // this same waiter once preflight clears, and #armPromptWatchdog cancels the previous
2314
+ // timer first, so only one timer is ever live.
2315
+ this.#armPromptWatchdog(params.sessionId, record, waiter);
2316
+ // The watchdog settles `response`, not this await, so a preflight that never returns
2317
+ // would pin session/prompt open with the caller's promise already rejected. Racing the
2318
+ // settlement lets the re-checks below hand that rejection back to the caller.
2319
+ const settlement = response.then(
2320
+ () => undefined,
2321
+ () => undefined,
2322
+ );
2069
2323
  try {
2070
- await record.adapter.ensureProviders();
2324
+ await Promise.race([record.adapter.ensureProviders(), settlement]);
2071
2325
  } catch (error) {
2326
+ waiter.awaitingProviderPreflight = false;
2072
2327
  if (waiter.settled || record.activePrompt !== waiter) {
2073
- this.#retiredPromptAcknowledgements.delete(params.sessionId);
2328
+ this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
2074
2329
  return await response;
2075
2330
  }
2076
2331
  if (record.cancelRequested && waiter.cancelAcknowledged) {
2077
- this.#retiredPromptAcknowledgements.delete(params.sessionId);
2332
+ this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
2078
2333
  await this.#settleCancelledPrompt(params.sessionId, record, waiter);
2079
2334
  return await response;
2080
2335
  }
2081
2336
  if (record.activePrompt === waiter) {
2082
2337
  record.activePrompt = undefined;
2083
2338
  record.busy = record.backgroundBusy;
2084
- this.#retiredPromptAcknowledgements.delete(params.sessionId);
2339
+ clearPromptWatchdog(waiter);
2340
+ // #5526's guarded release replaces the unguarded delete: it only clears
2341
+ // the entry when it still belongs to THIS waiter, so a successor's
2342
+ // acknowledgement is never dropped. Strictly narrower than the delete
2343
+ // it supersedes; dev's watchdog clear above is unaffected.
2344
+ this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
2085
2345
  void this.#publishPromptPhaseIdle(params.sessionId, record.adapter);
2086
2346
  }
2087
2347
  throw error;
2088
2348
  }
2349
+ waiter.awaitingProviderPreflight = false;
2089
2350
  if (waiter.settled || record.activePrompt !== waiter) {
2090
- this.#retiredPromptAcknowledgements.delete(params.sessionId);
2351
+ this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
2091
2352
  return await response;
2092
2353
  }
2093
2354
  if (record.cancelRequested && waiter.cancelAcknowledged) {
2094
- this.#retiredPromptAcknowledgements.delete(params.sessionId);
2355
+ this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
2095
2356
  await this.#settleCancelledPrompt(params.sessionId, record, waiter);
2096
2357
  return await response;
2097
2358
  }
2098
2359
 
2099
- // Silence has to be bounded from the moment the prompt owns the session: a host that
2100
- // dies before it ever answers is exactly the failure that leaves the client running.
2360
+ // Re-arm now that preflight is behind us: the bound armed before it already covered
2361
+ // this prompt's ownership window, and this call replaces that timer rather than
2362
+ // racing it, so the dispatched turn gets the full inactivity bound of its own.
2101
2363
  this.#armPromptWatchdog(params.sessionId, record, waiter);
2102
2364
  // Echo the user's own message back as `user_message_chunk`. Clients render their
2103
2365
  // transcript from session/update, so without this a prompt's text and any attached
@@ -2133,14 +2395,18 @@ export class AcpAgent implements Agent {
2133
2395
  }
2134
2396
 
2135
2397
  waiter.acknowledgementPending = true;
2398
+ const promptAdapter = record.adapter;
2136
2399
  const acknowledgementTask = (async (): Promise<PromptResponse> => {
2137
2400
  if (waiter.settled || record.activePrompt !== waiter) return await response;
2138
2401
  const acknowledgement = skillInvocation
2139
- ? await record.adapter.control("skill.invoke", skillInvocation)
2140
- : await record.adapter.prompt({
2402
+ ? await promptAdapter.control("skill.invoke", { ...skillInvocation, clientRef })
2403
+ : await promptAdapter.prompt({
2141
2404
  text: payload.text,
2405
+ clientRef,
2142
2406
  ...(payload.images.length ? { images: payload.images } : {}),
2143
2407
  });
2408
+ // A recovered waiter already owns its exact identity; a late ack cannot rebind it.
2409
+ if (waiter.settled && waiter.acknowledged) return await response;
2144
2410
 
2145
2411
  const acknowledgementCorrelation = promptAcknowledgement(acknowledgement);
2146
2412
  if (!acknowledgementCorrelation)
@@ -2148,6 +2414,14 @@ export class AcpAgent implements Agent {
2148
2414
  "invalid_prompt_acknowledgement",
2149
2415
  "SDK prompt acknowledgement must accept the prompt and include commandId and turnId.",
2150
2416
  );
2417
+ if (
2418
+ this.#sessions.get(params.sessionId) !== record ||
2419
+ record.adapter !== promptAdapter ||
2420
+ record.activePrompt !== waiter
2421
+ ) {
2422
+ this.#rememberSettledPromptCorrelation(params.sessionId, record, acknowledgementCorrelation);
2423
+ return await response;
2424
+ }
2151
2425
  if (this.#hasRetiredPromptCorrelation(params.sessionId, record, acknowledgementCorrelation)) {
2152
2426
  try {
2153
2427
  const abortAcknowledgement = await record.adapter.cancel("turn");
@@ -2282,10 +2556,20 @@ export class AcpAgent implements Agent {
2282
2556
  }
2283
2557
  this.#settlePrompt(params.sessionId, record, waiter);
2284
2558
  return await response;
2285
- })().finally(() => {
2286
- waiter.acknowledgementPending = false;
2287
- this.#retiredPromptAcknowledgements.delete(params.sessionId);
2288
- });
2559
+ })()
2560
+ .catch(async error => {
2561
+ if (
2562
+ !(error instanceof SdkClientError || error instanceof AcpSdkAdapterError) ||
2563
+ error.code !== "uncertain_after_send"
2564
+ )
2565
+ throw error;
2566
+ if (record.adapter === promptAdapter) this.#startUncertainPromptRecovery(params.sessionId, record, waiter);
2567
+ return await response;
2568
+ })
2569
+ .finally(() => {
2570
+ waiter.acknowledgementPending = false;
2571
+ this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
2572
+ });
2289
2573
  // Settlement can win before the SDK answers `turn.prompt`; acknowledgement processing
2290
2574
  // must remain alive to tombstone its eventual correlation without holding the ACP caller.
2291
2575
  void acknowledgementTask.catch(() => undefined);
@@ -2744,24 +3028,23 @@ export class AcpAgent implements Agent {
2744
3028
  // a broker-launched SDK host keeps ACP ownership across reconnects.
2745
3029
  await adapter.start({ activateProviders: false });
2746
3030
  let capabilities: JsonObject | undefined;
3031
+ // A query that never answered and a host that answered without provenance are
3032
+ // different operator problems, so the failure is retained instead of discarded.
3033
+ let capabilitiesFailure: unknown;
2747
3034
  try {
2748
3035
  const response = object(await adapter.query("runtime.capabilities"));
2749
3036
  const result = object(response?.result) ?? response;
2750
3037
  // Q18 is a paged query surface: the capability object arrives as the single
2751
3038
  // page item, so fall back to the envelope only for direct-result hosts.
2752
3039
  capabilities = object(pageItems(result)[0]) ?? result;
2753
- } catch {}
2754
- const primaryControlSurface =
2755
- capabilities?.primaryControlSurface === "sdk"
2756
- ? "sdk"
2757
- : capabilities?.primaryControlSurface === "cli"
2758
- ? "cli"
2759
- : undefined;
2760
- if (capabilities?.promptTerminalOutcomeVersion !== 1 || primaryControlSurface === undefined)
2761
- throw new AcpSdkAdapterError(
2762
- "unavailable",
2763
- "This ACP client requires a newer GJC SDK session with startup control provenance; restart the session.",
2764
- );
3040
+ } catch (error) {
3041
+ capabilitiesFailure = error;
3042
+ }
3043
+ const primaryControlSurface = resolveStartupProvenance({
3044
+ sessionId: id,
3045
+ capabilities,
3046
+ queryFailure: capabilitiesFailure,
3047
+ });
2765
3048
  if (primaryControlSurface === "sdk") {
2766
3049
  adapter.authorizeProviderActivation();
2767
3050
  await adapter.ensureProviders();
@@ -2872,7 +3155,11 @@ export class AcpAgent implements Agent {
2872
3155
 
2873
3156
  #fenceRetiredPromptAcknowledgement(id: string, waiter: PromptWaiter): void {
2874
3157
  if (waiter.dispatched && waiter.acknowledgementPending && !waiter.acknowledged)
2875
- this.#retiredPromptAcknowledgements.add(id);
3158
+ this.#retiredPromptAcknowledgements.set(id, waiter);
3159
+ }
3160
+
3161
+ #releaseRetiredPromptAcknowledgement(id: string, waiter: PromptWaiter): void {
3162
+ if (this.#retiredPromptAcknowledgements.get(id) === waiter) this.#retiredPromptAcknowledgements.delete(id);
2876
3163
  }
2877
3164
 
2878
3165
  #advanceTerminalGeneration(record: SessionRecord): void {
@@ -2882,6 +3169,15 @@ export class AcpAgent implements Agent {
2882
3169
  #recoverSessionAfterTransportFailure(id: string, adapter: AcpSdkAdapter, error: Error): void {
2883
3170
  const record = this.#sessions.get(id);
2884
3171
  if (!record || record.adapter !== adapter) return;
3172
+ if (error instanceof SdkClientError && error.code === "uncertain_after_send") {
3173
+ // Provider-registration requests share this transport error. Recover only when the
3174
+ // prompt's own dispatched acknowledgement is still pending; an acknowledged turn
3175
+ // must continue until its terminal frame arrives.
3176
+ const waiter = record.activePrompt;
3177
+ if (waiter?.dispatched && waiter.acknowledgementPending && !waiter.acknowledged)
3178
+ this.#startUncertainPromptRecovery(id, record, waiter);
3179
+ return;
3180
+ }
2885
3181
  if (
2886
3182
  error instanceof SdkClientError &&
2887
3183
  error.code !== "reconnect_exhausted" &&
@@ -2896,11 +3192,191 @@ export class AcpAgent implements Agent {
2896
3192
  logger.warn(`ACP session ${id} non-transport error reached reconnect handler; ignoring terminal recovery.`);
2897
3193
  return;
2898
3194
  }
3195
+ const waiter = record.activePrompt;
3196
+ if (waiter?.dispatched) {
3197
+ // A prior uncertainty notification already owns recovery for this waiter. Keep
3198
+ // the in-flight lookup alive; only declined, non-recovering waiters fall through
3199
+ // to the ordinary transport-loss teardown and reattachment path.
3200
+ if (record.statusRecovery === waiter || this.#startUncertainPromptRecovery(id, record, waiter)) return;
3201
+ }
2899
3202
  const detail = error.message || "SDK transport reconnect failed.";
2900
3203
  const terminal = new AcpSdkAdapterError("connection_closed", `ACP session transport was lost: ${detail}`);
2901
3204
  void this.#recoverSessionAfterTransportFailureAsync(id, adapter, record.cwd, terminal);
2902
3205
  }
2903
3206
 
3207
+ #startUncertainPromptRecovery(id: string, record: SessionRecord, waiter: PromptWaiter): boolean {
3208
+ if (
3209
+ this.#sessions.get(id) !== record ||
3210
+ promptWaiterRetired(record, waiter) ||
3211
+ !waiter.dispatched ||
3212
+ waiter.terminalReserved ||
3213
+ record.statusRecovery === waiter
3214
+ )
3215
+ return false;
3216
+ record.statusRecovery = waiter;
3217
+ clearPromptWatchdog(waiter);
3218
+ void this.#recoverUncertainPrompt(id, record, record.adapter, waiter);
3219
+ return true;
3220
+ }
3221
+
3222
+ async #recoverUncertainPrompt(
3223
+ id: string,
3224
+ record: SessionRecord,
3225
+ adapter: AcpSdkAdapter,
3226
+ waiter: PromptWaiter,
3227
+ ): Promise<void> {
3228
+ const ownsRecovery = (): boolean =>
3229
+ this.#sessions.get(id) === record &&
3230
+ record.adapter === adapter &&
3231
+ record.statusRecovery === waiter &&
3232
+ !promptWaiterRetired(record, waiter) &&
3233
+ !waiter.terminalReserved;
3234
+ // Keep the lookup selector's authority even if an ack arrives during the read.
3235
+ const acknowledgedAtLookup = waiter.acknowledged;
3236
+ let detail = "status query unavailable";
3237
+ let timer: NodeJS.Timeout | undefined;
3238
+ let timedOut = false;
3239
+ try {
3240
+ const deadline = Promise.withResolvers<never>();
3241
+ timer = setTimeout(() => {
3242
+ timedOut = true;
3243
+ deadline.reject(new Error("status query timeout"));
3244
+ }, 5_000);
3245
+ const refreshAttachment = async (): Promise<void> => {
3246
+ if (this.#sessions.get(id) !== record || record.adapter !== adapter) return;
3247
+ // A reconnect can revoke the adapter's opaque attachment before Router has
3248
+ // published its replacement. Force an authority pass first, then hand the
3249
+ // exact current capability back to the adapter before issuing the read-only
3250
+ // lookup. This pass does not replay the prompt mutation.
3251
+ await this.#router.reconcile({ waitForReplay: false });
3252
+ if (!ownsRecovery()) return;
3253
+ const refreshed = this.#router.attachment(id);
3254
+ if (!refreshed?.isCurrent())
3255
+ throw new AcpSdkAdapterError(
3256
+ "unavailable",
3257
+ "ACP session has no current Router attachment for recovery.",
3258
+ );
3259
+ if (refreshed !== record.attachment || !record.attachment.isCurrent()) {
3260
+ await adapter.attachmentReady(refreshed);
3261
+ record.attachment = refreshed;
3262
+ }
3263
+ };
3264
+ await Promise.race([refreshAttachment(), deadline.promise]);
3265
+ if (!ownsRecovery()) return;
3266
+ // Bound observation only: the mutation and the read are never replayed or aborted.
3267
+ const response = object(
3268
+ await Promise.race([
3269
+ adapter.query("turn.result", {
3270
+ kind: waiter.invocationKind,
3271
+ ...(acknowledgedAtLookup ? waiter.correlation : { clientRef: waiter.clientRef }),
3272
+ }),
3273
+ deadline.promise,
3274
+ ]),
3275
+ );
3276
+ if (!ownsRecovery()) return;
3277
+ const status = object(response?.result) ?? response;
3278
+ const correlation = strictCorrelationFrom(response, status);
3279
+ const outcome = status ? terminalOutcome(status) : undefined;
3280
+ const content = object(status?.content);
3281
+ const readableText =
3282
+ content?.version === 1 &&
3283
+ content.type === "text" &&
3284
+ typeof content.text === "string" &&
3285
+ content.text.trim().length > 0;
3286
+ const failure = object(status?.error);
3287
+ const conflictingIdentity = [response, status].some(
3288
+ value =>
3289
+ (value?.kind !== undefined && value.kind !== waiter.invocationKind) ||
3290
+ (value?.sessionId !== undefined && value.sessionId !== id) ||
3291
+ (value?.clientRef !== undefined && value.clientRef !== waiter.clientRef),
3292
+ );
3293
+ if (
3294
+ status?.kind !== waiter.invocationKind ||
3295
+ conflictingIdentity ||
3296
+ (!acknowledgedAtLookup && status.clientRef !== waiter.clientRef) ||
3297
+ !correlation ||
3298
+ !hasCompleteCorrelation(correlation) ||
3299
+ (waiter.acknowledged && !correlationsExactlyMatch(waiter.correlation, correlation)) ||
3300
+ this.#hasRetiredPromptCorrelation(id, record, correlation)
3301
+ ) {
3302
+ detail = "status correlation is missing, retired, or mismatched";
3303
+ } else if (
3304
+ status.status === "failed" &&
3305
+ status.outcome === undefined &&
3306
+ (status.receiptState === "present" ||
3307
+ status.receiptState === "missing" ||
3308
+ status.receiptState === "unknown") &&
3309
+ typeof failure?.code === "string" &&
3310
+ /^[a-zA-Z0-9_.-]{1,64}$/.test(failure.code) &&
3311
+ typeof failure.message === "string" &&
3312
+ failure.message.trim().length > 0 &&
3313
+ failure.message.length <= 512
3314
+ ) {
3315
+ waiter.correlation = correlation;
3316
+ waiter.acknowledged = true;
3317
+ record.busy = record.backgroundBusy;
3318
+ this.#advanceTerminalGeneration(record);
3319
+ void this.#rejectPrompt(record, id, waiter, new AcpSdkAdapterError(failure.code, failure.message));
3320
+ return;
3321
+ } else if (
3322
+ (status.status === "terminal_ok" &&
3323
+ outcome?.kind === "stopped" &&
3324
+ (status.receiptState === "present" || status.receiptState === "missing") &&
3325
+ (outcome.reason !== "end_turn" || (status.receiptState === "present" && readableText))) ||
3326
+ (status.status === "failed" &&
3327
+ outcome?.kind === "failed" &&
3328
+ (status.receiptState === "present" ||
3329
+ status.receiptState === "missing" ||
3330
+ status.receiptState === "unknown"))
3331
+ ) {
3332
+ waiter.correlation = correlation;
3333
+ waiter.acknowledged = true;
3334
+ record.busy = record.backgroundBusy;
3335
+ this.#advanceTerminalGeneration(record);
3336
+ const settledOutcome =
3337
+ record.cancelRequested && outcome.kind === "stopped" && outcome.reason === "end_turn"
3338
+ ? { ...outcome, reason: "cancelled" as const, provenance: "client_cancel" as const }
3339
+ : outcome;
3340
+ const publicationBarrier = record.frameTail;
3341
+ waiter.terminal = { outcome: settledOutcome, correlation };
3342
+ // Recovered final text is published on the async terminal tail below. Record it
3343
+ // before settling so a first-turn startup-readiness failure cannot be retried
3344
+ // after this retained answer has already been committed for publication.
3345
+ if (readableText) record.promptObservedAssistantOutput = true;
3346
+ this.#settlePrompt(id, record, waiter);
3347
+ this.#scheduleTerminalUpdates(
3348
+ id,
3349
+ adapter,
3350
+ record.publicationGeneration,
3351
+ {
3352
+ type: settledOutcome.kind === "failed" ? "agent_failed" : "agent_end",
3353
+ outcome: settledOutcome,
3354
+ finalText: readableText ? content.text : undefined,
3355
+ finalTextTruncated: content?.truncated === true,
3356
+ },
3357
+ waiter,
3358
+ publicationBarrier,
3359
+ );
3360
+ return;
3361
+ } else {
3362
+ detail = "status has no usable retained terminal outcome and receipt";
3363
+ }
3364
+ } catch {
3365
+ detail = timedOut ? "status query timed out after 5000ms" : "status query unavailable";
3366
+ } finally {
3367
+ if (timer !== undefined) clearTimeout(timer);
3368
+ }
3369
+ if (!ownsRecovery()) return;
3370
+ await this.#failSession(
3371
+ id,
3372
+ adapter,
3373
+ new AcpSdkAdapterError(
3374
+ "terminal_uncertain",
3375
+ `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.`,
3376
+ ),
3377
+ );
3378
+ }
3379
+
2904
3380
  async #recoverSessionAfterTransportFailureAsync(
2905
3381
  id: string,
2906
3382
  adapter: AcpSdkAdapter,
@@ -2948,13 +3424,15 @@ export class AcpAgent implements Agent {
2948
3424
  if (attaching) await Promise.allSettled([attaching.task]);
2949
3425
  // A CLI-primary host owns its own process lifecycle: release this ACP
2950
3426
  // attachment locally and never ask the broker to close the session.
2951
- await this.#teardownSession(id, "closed", this.#ownedSessionIds.has(id));
3427
+ const closesHost = this.#ownedSessionIds.has(id);
3428
+ await this.#teardownSession(id, "closed", closesHost);
2952
3429
  this.#knownSessionCwds.delete(id);
2953
3430
  this.#ownedSessionIds.delete(id);
2954
3431
  this.#knownSessionMcpServers.delete(id);
2955
3432
  this.#knownSessionMetadata.delete(id);
2956
3433
  this.#retiredPromptCorrelations.delete(id);
2957
- this.#retiredPromptAcknowledgements.delete(id);
3434
+ // Detaching locally cannot retire an outstanding host acknowledgement.
3435
+ if (closesHost) this.#retiredPromptAcknowledgements.delete(id);
2958
3436
  return {};
2959
3437
  } finally {
2960
3438
  this.#finishTeardown(id);
@@ -3192,7 +3670,7 @@ export class AcpAgent implements Agent {
3192
3670
  * running forever behind a dead session.
3193
3671
  */
3194
3672
  #armPromptWatchdog(id: string, record: SessionRecord, waiter: PromptWaiter): void {
3195
- if (waiter.settled) return;
3673
+ if (waiter.settled || record.statusRecovery === waiter) return;
3196
3674
  waiter.cancelWatchdog?.();
3197
3675
  // No frame can still arrive once the transport is known to be gone, so waiting out
3198
3676
  // the full bound would only add dead time to an outcome that is already decided.
@@ -3255,17 +3733,40 @@ export class AcpAgent implements Agent {
3255
3733
  * turn can no longer be observed, it does not cancel or tear down the session.
3256
3734
  */
3257
3735
  async #expirePromptWatchdog(id: string, record: SessionRecord, waiter: PromptWaiter): Promise<void> {
3258
- if (record.activePrompt !== waiter || waiter.settled || waiter.terminalReserved) return;
3736
+ if (
3737
+ record.activePrompt !== waiter ||
3738
+ waiter.settled ||
3739
+ waiter.terminalReserved ||
3740
+ record.statusRecovery === waiter
3741
+ )
3742
+ return;
3259
3743
  const silenceMs = Math.max(0, this.#promptWatchdogClock.now() - waiter.lastFrameAt);
3260
- const cause = this.#promptTransportGone(id, record) ?? "the SDK session host stopped producing frames";
3744
+ // A prompt still in preflight has never reached the host, so "stopped producing frames"
3745
+ // would point the reader at the wrong phase: nothing was ever dispatched to produce them.
3746
+ const cause =
3747
+ this.#promptTransportGone(id, record) ??
3748
+ (waiter.awaitingProviderPreflight
3749
+ ? "the SDK session host never finished provider preflight"
3750
+ : "the SDK session host stopped producing frames");
3751
+ // Counts only: the plan's contents are model-authored and belong on the bounded wire
3752
+ // projection, not in the log line (issue #5669).
3753
+ const plan = waiter.planSnapshot;
3261
3754
  logger.error("acp_prompt_watchdog_expired", {
3262
3755
  sessionId: id,
3263
3756
  cause,
3264
3757
  silenceMs,
3265
3758
  lastFrameType: waiter.lastFrameType,
3759
+ awaitingProviderPreflight: waiter.awaitingProviderPreflight,
3266
3760
  inactivityBoundMs: waiter.activity.inactivityBoundMs,
3267
3761
  toolRunning: waiter.activity.running,
3268
3762
  awaitingModel: waiter.activity.awaitingModel,
3763
+ ...(plan
3764
+ ? {
3765
+ planIncomplete: plan.some(planEntryUnfinished),
3766
+ planPendingCount: plan.filter(planEntryUnfinished).length,
3767
+ planTotalCount: plan.length,
3768
+ }
3769
+ : {}),
3269
3770
  ...(waiter.correlation.commandId ? { commandId: waiter.correlation.commandId } : {}),
3270
3771
  ...(waiter.correlation.turnId ? { turnId: waiter.correlation.turnId } : {}),
3271
3772
  });
@@ -3273,11 +3774,12 @@ export class AcpAgent implements Agent {
3273
3774
  record,
3274
3775
  id,
3275
3776
  waiter,
3276
- new AcpSdkAdapterError(
3777
+ new AcpPromptAbandonedError(
3277
3778
  "prompt_abandoned",
3278
3779
  `ACP prompt was abandoned after ${Math.round(silenceMs / 1_000)}s of silence: ${cause}. Last frame was ` +
3279
3780
  `"${waiter.lastFrameType}" (${describeCorrelation(waiter.correlation)}). The turn was settled so the ` +
3280
3781
  `client stops waiting; the session still accepts the next prompt.`,
3782
+ plan,
3281
3783
  ),
3282
3784
  );
3283
3785
  }
@@ -3343,6 +3845,18 @@ export class AcpAgent implements Agent {
3343
3845
  await this.#settleCancelledPrompt(id, record, waiter);
3344
3846
  return;
3345
3847
  }
3848
+ if (waiter.terminalReserved) return;
3849
+ if (record.statusRecovery === waiter || (waiter.dispatched && !waiter.acknowledged)) {
3850
+ await this.#failSession(
3851
+ id,
3852
+ adapter,
3853
+ new AcpSdkAdapterError(
3854
+ "terminal_uncertain",
3855
+ `Prompt owner connection changed before retained evidence was resolved. No mutation was replayed. Inspect turn.result for kind=${waiter.invocationKind}, clientRef=${waiter.clientRef}.`,
3856
+ ),
3857
+ );
3858
+ return;
3859
+ }
3346
3860
  record.activePrompt = undefined;
3347
3861
  record.busy = record.backgroundBusy;
3348
3862
  clearPromptWatchdog(waiter);
@@ -3467,7 +3981,11 @@ export class AcpAgent implements Agent {
3467
3981
  // the first-turn retry gate — which the settlement below releases — would see
3468
3982
  // "started, no output", resubmit, and let BOTH this terminal's final text and the
3469
3983
  // retry's answer reach ACP consumers. Record the output before settling (review P1).
3470
- if (typeof event.finalText === "string" && event.finalText) record.promptObservedAssistantOutput = true;
3984
+ // Whitespace-only final text carries no assistant content. `hasAcpFinalTextContent` is the
3985
+ // single presence predicate shared with the terminal publication gate below, so a terminal
3986
+ // this gate treats as "no output" can never publish a chunk alongside the retry's answer.
3987
+ if (typeof event.finalText === "string" && hasAcpFinalTextContent(event.finalText))
3988
+ record.promptObservedAssistantOutput = true;
3471
3989
  // Failure diagnostics are useful but advisory. Settle before any mapped
3472
3990
  // session update can await a backpressured client transport; otherwise an
3473
3991
  // already-decided failure can still lose to the inactivity watchdog.
@@ -3534,6 +4052,12 @@ export class AcpAgent implements Agent {
3534
4052
  notification.update.content.type === "text"
3535
4053
  )
3536
4054
  promptOwner.emittedAssistantText += notification.update.content.text;
4055
+ // The newest plan REPLACES the retained one: a plan update is the whole list, not a
4056
+ // delta, so `todo_auto_clear`'s empty `entries` correctly leaves no pending work behind
4057
+ // (issue #5669). Retained here rather than at frame ingress because this is where the
4058
+ // mapper has already decided what the client is being told the plan is.
4059
+ if (promptOwner && notification.update.sessionUpdate === "plan")
4060
+ promptOwner.planSnapshot = notification.update.entries;
3537
4061
  // A live assistant text/thought chunk for this prompt owner has now been exposed to
3538
4062
  // ACP consumers. Record it so the first-turn readiness retry is vetoed: a re-submit
3539
4063
  // would deliver a second attempt's output on top of these chunks (issue #5574).
@@ -3623,13 +4147,15 @@ export class AcpAgent implements Agent {
3623
4147
  publicationGeneration: number,
3624
4148
  event: JsonObject,
3625
4149
  promptOwner: PromptWaiter | undefined,
4150
+ publicationBarrier?: Promise<void>,
3626
4151
  ): void {
3627
4152
  const failedTerminal = event.type === "agent_failed";
3628
4153
  if (promptOwner) this.#flushFailureDiagnostics(id, promptOwner, adapter);
3629
- let decorationStart = Promise.resolve();
4154
+ let decorationStart = publicationBarrier ?? Promise.resolve();
3630
4155
  const finalText = typeof event.finalText === "string" ? event.finalText : "";
3631
- if (promptOwner && finalText) {
4156
+ if (promptOwner && hasAcpFinalTextContent(finalText)) {
3632
4157
  const finalTextTask = (async () => {
4158
+ if (publicationBarrier) await publicationBarrier;
3633
4159
  await Bun.sleep(0);
3634
4160
  const resolution = resolveAcpFinalText(promptOwner.emittedAssistantText, finalText);
3635
4161
  if (resolution.kind === "emit") {
@@ -3641,7 +4167,9 @@ export class AcpAgent implements Agent {
3641
4167
  update: {
3642
4168
  sessionUpdate: "agent_message_chunk",
3643
4169
  content: { type: "text", text: resolution.text },
3644
- ...(resolution.final.truncated ? { _meta: { gjcFinalTextTruncated: true } } : {}),
4170
+ ...(resolution.final.truncated || event.finalTextTruncated === true
4171
+ ? { _meta: { gjcFinalTextTruncated: true } }
4172
+ : {}),
3645
4173
  },
3646
4174
  },
3647
4175
  adapter,
@@ -3656,6 +4184,24 @@ export class AcpAgent implements Agent {
3656
4184
  finalLength: resolution.final.text.length,
3657
4185
  });
3658
4186
  }
4187
+ if (resolution.kind !== "emit" && event.finalTextTruncated === true) {
4188
+ // A retained prefix may already equal the streamed text, leaving no suffix for
4189
+ // `resolveAcpFinalText` to emit. Still publish a metadata-only chunk so the
4190
+ // caller can distinguish an incomplete retained result from a complete answer.
4191
+ await this.#publishSessionUpdate(
4192
+ id,
4193
+ {
4194
+ sessionId: id,
4195
+ update: {
4196
+ sessionUpdate: "agent_message_chunk",
4197
+ content: { type: "text", text: "" },
4198
+ _meta: { gjcFinalTextTruncated: true },
4199
+ },
4200
+ },
4201
+ adapter,
4202
+ publicationGeneration,
4203
+ );
4204
+ }
3659
4205
  })();
3660
4206
  let finalTextTail: Promise<void>;
3661
4207
  finalTextTail = finalTextTask