@gajae-code/coding-agent 0.11.11 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) hide show
  1. package/CHANGELOG.md +131 -4
  2. package/dist/types/async/job-manager.d.ts +17 -3
  3. package/dist/types/cli/read-cli.d.ts +1 -0
  4. package/dist/types/commands/deep-interview.d.ts +9 -0
  5. package/dist/types/commands/harness.d.ts +22 -0
  6. package/dist/types/commands/read.d.ts +6 -0
  7. package/dist/types/config/model-registry.d.ts +4 -0
  8. package/dist/types/config/provider-auth-health.d.ts +14 -0
  9. package/dist/types/config/provider-ranking.d.ts +59 -0
  10. package/dist/types/config/settings-schema.d.ts +83 -1
  11. package/dist/types/config/settings.d.ts +2 -0
  12. package/dist/types/exa/mcp-client.d.ts +10 -1
  13. package/dist/types/exec/bash-executor.d.ts +45 -12
  14. package/dist/types/extensibility/extensions/types.d.ts +38 -4
  15. package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
  16. package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
  17. package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
  18. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
  19. package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
  20. package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
  21. package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
  22. package/dist/types/extensibility/shared-events.d.ts +6 -0
  23. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
  24. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
  25. package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
  26. package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
  27. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  28. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  29. package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
  30. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +70 -1
  31. package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
  32. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  33. package/dist/types/gjc-runtime/team-runtime.d.ts +27 -1
  34. package/dist/types/gjc-runtime/team-store.d.ts +12 -0
  35. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
  36. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
  37. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +17 -0
  38. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
  39. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +31 -80
  40. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
  41. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  42. package/dist/types/main.d.ts +1 -1
  43. package/dist/types/memories/index.d.ts +2 -0
  44. package/dist/types/modes/acp/acp-agent.d.ts +10 -19
  45. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  46. package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
  47. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  48. package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
  49. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  50. package/dist/types/modes/components/session-observer-overlay.d.ts +22 -0
  51. package/dist/types/modes/components/settings-selector.d.ts +11 -0
  52. package/dist/types/modes/components/status-line/gh.d.ts +5 -0
  53. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  54. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
  55. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  56. package/dist/types/modes/interactive-mode.d.ts +2 -0
  57. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  58. package/dist/types/modes/types.d.ts +2 -0
  59. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  60. package/dist/types/reminders/star-reminder.d.ts +3 -13
  61. package/dist/types/runtime-mcp/manager.d.ts +1 -0
  62. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  63. package/dist/types/sdk/acp/adapter.d.ts +3 -1
  64. package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
  65. package/dist/types/sdk/broker/transport.d.ts +2 -0
  66. package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
  67. package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -1
  68. package/dist/types/sdk/bus/config.d.ts +6 -2
  69. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  70. package/dist/types/sdk/bus/index.d.ts +44 -1
  71. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +44 -0
  72. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  73. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
  74. package/dist/types/sdk/bus/reconciliation-store.d.ts +69 -0
  75. package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
  76. package/dist/types/sdk/bus/rich-render.d.ts +2 -0
  77. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
  78. package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
  79. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -4
  80. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
  81. package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
  82. package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
  83. package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
  84. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  85. package/dist/types/sdk/host/host.d.ts +2 -0
  86. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  87. package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
  88. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  89. package/dist/types/sdk/prompt-status.d.ts +23 -3
  90. package/dist/types/sdk/session.d.ts +6 -0
  91. package/dist/types/session/agent-session.d.ts +82 -12
  92. package/dist/types/session/artifacts.d.ts +8 -0
  93. package/dist/types/session/blob-store.d.ts +57 -4
  94. package/dist/types/session/internal/managed-session-storage.d.ts +8 -1
  95. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  96. package/dist/types/session/session-manager.d.ts +48 -19
  97. package/dist/types/session/session-storage.d.ts +4 -0
  98. package/dist/types/session/streaming-output.d.ts +96 -12
  99. package/dist/types/setup/credential-import.d.ts +12 -0
  100. package/dist/types/skill-state/active-state.d.ts +12 -4
  101. package/dist/types/skill-state/workflow-hud.d.ts +9 -0
  102. package/dist/types/task/executor.d.ts +3 -2
  103. package/dist/types/task/index.d.ts +3 -1
  104. package/dist/types/task/provider-retry-status.d.ts +8 -0
  105. package/dist/types/task/receipt.d.ts +7 -1
  106. package/dist/types/task/types.d.ts +23 -0
  107. package/dist/types/tools/ask-answer-registry.d.ts +2 -2
  108. package/dist/types/tools/bash-interactive.d.ts +4 -1
  109. package/dist/types/tools/bash.d.ts +3 -1
  110. package/dist/types/tools/browser/launch.d.ts +7 -0
  111. package/dist/types/tools/fetch.d.ts +17 -1
  112. package/dist/types/tools/image-gen.d.ts +4 -0
  113. package/dist/types/tools/index.d.ts +4 -0
  114. package/dist/types/tools/output-meta.d.ts +44 -3
  115. package/dist/types/tools/path-utils.d.ts +1 -0
  116. package/dist/types/tools/read-internals.d.ts +12 -0
  117. package/dist/types/tools/read.d.ts +45 -7
  118. package/dist/types/tools/resource-gc.d.ts +4 -0
  119. package/dist/types/tools/review.d.ts +5 -7
  120. package/dist/types/tools/subagent.d.ts +2 -0
  121. package/dist/types/tools/tool-result.d.ts +7 -1
  122. package/dist/types/utils/gh.d.ts +13 -0
  123. package/dist/types/utils/image-resize.d.ts +1 -0
  124. package/dist/types/web/search/providers/kimi.d.ts +2 -0
  125. package/dist/types/web/search/providers/searxng.d.ts +7 -0
  126. package/dist/types/web/search/providers/xai.d.ts +2 -0
  127. package/package.json +11 -13
  128. package/scripts/acp-conformance-agent.ts +218 -0
  129. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  130. package/scripts/capture-gjc-bundle-settings.ts +306 -0
  131. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  132. package/scripts/generate-sdk-operation-inventory.ts +5 -0
  133. package/scripts/resident-memory-bench.ts +1341 -0
  134. package/scripts/run-acp-conformance.ts +285 -0
  135. package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
  136. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  137. package/src/async/job-manager.ts +78 -20
  138. package/src/capability/index.ts +8 -0
  139. package/src/cli/fast-help.ts +1 -1
  140. package/src/cli/plugin-cli.ts +176 -18
  141. package/src/cli/read-cli.ts +2 -1
  142. package/src/cli.ts +1 -0
  143. package/src/commands/deep-interview.ts +29 -9
  144. package/src/commands/harness.ts +35 -12
  145. package/src/commands/read.ts +11 -2
  146. package/src/commands/team.ts +20 -2
  147. package/src/config/file-lock.ts +11 -2
  148. package/src/config/model-registry.ts +21 -2
  149. package/src/config/provider-auth-health.ts +42 -0
  150. package/src/config/provider-ranking.ts +119 -0
  151. package/src/config/settings-schema.ts +74 -1
  152. package/src/config/settings.ts +246 -108
  153. package/src/coordinator-mcp/question-gate-codec.ts +1 -1
  154. package/src/coordinator-mcp/server.ts +5 -12
  155. package/src/defaults/gjc/skills/deep-interview/SKILL.md +230 -82
  156. package/src/defaults/gjc/skills/ralplan/SKILL.md +77 -41
  157. package/src/defaults/gjc/skills/team/SKILL.md +3 -1
  158. package/src/defaults/gjc/skills/ultragoal/SKILL.md +62 -39
  159. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
  160. package/src/defaults/gjc-defaults.ts +4 -4
  161. package/src/eval/py/executor.ts +7 -2
  162. package/src/exa/mcp-client.ts +12 -3
  163. package/src/exec/bash-executor.ts +144 -27
  164. package/src/extensibility/extensions/runner.ts +15 -0
  165. package/src/extensibility/extensions/types.ts +46 -5
  166. package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
  167. package/src/extensibility/gjc-plugins/index.ts +19 -2
  168. package/src/extensibility/gjc-plugins/installer.ts +189 -50
  169. package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
  170. package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
  171. package/src/extensibility/gjc-plugins/observability.ts +3 -3
  172. package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
  173. package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
  174. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
  175. package/src/extensibility/gjc-plugins/schema.ts +84 -12
  176. package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
  177. package/src/extensibility/gjc-plugins/types.ts +141 -0
  178. package/src/extensibility/gjc-plugins/validation.ts +9 -0
  179. package/src/extensibility/shared-events.ts +5 -0
  180. package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
  181. package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
  182. package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
  183. package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
  184. package/src/gjc-runtime/deep-interview-state.ts +12 -943
  185. package/src/gjc-runtime/gc-render.ts +1 -0
  186. package/src/gjc-runtime/gc-runtime.ts +11 -1
  187. package/src/gjc-runtime/launch-tmux.ts +25 -6
  188. package/src/gjc-runtime/managed-owner-admission.ts +1 -1
  189. package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
  190. package/src/gjc-runtime/ralplan-runtime.ts +1149 -98
  191. package/src/gjc-runtime/state-runtime.ts +2 -1
  192. package/src/gjc-runtime/state-writer.ts +70 -222
  193. package/src/gjc-runtime/team-launch.ts +42 -5
  194. package/src/gjc-runtime/team-runtime.ts +1085 -40
  195. package/src/gjc-runtime/team-store.ts +32 -0
  196. package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
  197. package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
  198. package/src/gjc-runtime/team-workers.ts +1 -0
  199. package/src/gjc-runtime/tmux-owner-isolation.ts +28 -0
  200. package/src/gjc-runtime/tmux-sessions.ts +58 -13
  201. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
  202. package/src/gjc-runtime/ultragoal-runtime.ts +414 -618
  203. package/src/gjc-runtime/workflow-manifest.generated.json +105 -449
  204. package/src/gjc-runtime/workflow-manifest.ts +38 -247
  205. package/src/hooks/native-skill-hook.ts +17 -4
  206. package/src/hooks/skill-state.ts +1 -23
  207. package/src/internal-urls/docs-index.generated.ts +21 -22
  208. package/src/internal-urls/local-root-gc.ts +127 -0
  209. package/src/main.ts +9 -5
  210. package/src/memories/index.ts +10 -0
  211. package/src/modes/DESIGN.md +98 -0
  212. package/src/modes/acp/acp-agent.ts +339 -191
  213. package/src/modes/acp/acp-event-mapper.ts +14 -7
  214. package/src/modes/acp/acp-mode.ts +39 -3
  215. package/src/modes/components/assistant-message.ts +123 -22
  216. package/src/modes/components/gjc-bundle-settings.ts +508 -0
  217. package/src/modes/components/irc-sidebar.ts +176 -62
  218. package/src/modes/components/model-selector.ts +61 -4
  219. package/src/modes/components/notifications-settings-editor.ts +12 -1
  220. package/src/modes/components/oauth-selector.ts +96 -28
  221. package/src/modes/components/read-tool-group.ts +82 -14
  222. package/src/modes/components/session-observer-overlay.ts +457 -83
  223. package/src/modes/components/settings-selector.ts +116 -23
  224. package/src/modes/components/status-line/gh.ts +16 -0
  225. package/src/modes/components/tool-execution.ts +138 -27
  226. package/src/modes/components/tool-status-header.ts +3 -14
  227. package/src/modes/components/transcript-viewer-overlay.ts +99 -24
  228. package/src/modes/controllers/command-controller.ts +4 -0
  229. package/src/modes/controllers/event-controller.ts +109 -27
  230. package/src/modes/controllers/extension-ui-controller.ts +26 -2
  231. package/src/modes/controllers/input-controller.ts +18 -3
  232. package/src/modes/controllers/selector-controller.ts +127 -36
  233. package/src/modes/interactive-mode.ts +62 -3
  234. package/src/modes/irc-observation-ledger.ts +71 -15
  235. package/src/modes/runtime-init.ts +22 -2
  236. package/src/modes/types.ts +2 -0
  237. package/src/modes/utils/ui-helpers.ts +94 -1
  238. package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
  239. package/src/prompts/agents/architect.md +9 -1
  240. package/src/prompts/agents/critic.md +9 -0
  241. package/src/prompts/system/eager-todo.md +8 -4
  242. package/src/prompts/system/system-prompt.md +11 -5
  243. package/src/prompts/tools/bash.md +5 -5
  244. package/src/prompts/tools/read.md +4 -2
  245. package/src/prompts/tools/search-tool-bm25.md +5 -0
  246. package/src/prompts/tools/task.md +1 -1
  247. package/src/reminders/star-reminder.ts +4 -42
  248. package/src/runtime-mcp/manager.ts +19 -4
  249. package/src/runtime-mcp/smithery-auth.ts +17 -2
  250. package/src/runtime-mcp/smithery-connect.ts +11 -1
  251. package/src/sdk/acp/adapter.ts +22 -6
  252. package/src/sdk/broker/lifecycle.ts +25 -6
  253. package/src/sdk/broker/transport.ts +35 -3
  254. package/src/sdk/bus/attachment-registry.ts +5 -1
  255. package/src/sdk/bus/chat-daemon-cli.ts +4 -39
  256. package/src/sdk/bus/chat-daemon-control.ts +6 -3
  257. package/src/sdk/bus/config.ts +18 -2
  258. package/src/sdk/bus/control-drain-lease.ts +104 -0
  259. package/src/sdk/bus/index.ts +984 -146
  260. package/src/sdk/bus/kind-aware-reconciliation.ts +317 -0
  261. package/src/sdk/bus/lifecycle-commands.ts +4 -1
  262. package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
  263. package/src/sdk/bus/prompt-reconciliation.ts +2 -1
  264. package/src/sdk/bus/reconciliation-store.ts +303 -0
  265. package/src/sdk/bus/rich-draft.ts +1 -1
  266. package/src/sdk/bus/rich-render.ts +2 -2
  267. package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
  268. package/src/sdk/bus/telegram-cli.ts +33 -8
  269. package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
  270. package/src/sdk/bus/telegram-daemon-contract.ts +11 -4
  271. package/src/sdk/bus/telegram-daemon.ts +916 -23
  272. package/src/sdk/bus/telegram-reference.ts +40 -3
  273. package/src/sdk/bus/threaded-render.ts +48 -3
  274. package/src/sdk/bus/topic-registry.ts +20 -0
  275. package/src/sdk/host/control/dispatch.ts +5 -1
  276. package/src/sdk/host/control/operations.ts +1 -1
  277. package/src/sdk/host/host.ts +23 -1
  278. package/src/sdk/host/query/handlers.ts +55 -0
  279. package/src/sdk/host/query/revision-store.ts +36 -2
  280. package/src/sdk/host/reverse-leases.ts +44 -7
  281. package/src/sdk/mcp/server.ts +46 -18
  282. package/src/sdk/prompt-status.ts +16 -3
  283. package/src/sdk/protocol/operation-inventory.generated.json +51 -0
  284. package/src/sdk/protocol/operation-registry.ts +7 -2
  285. package/src/sdk/session.ts +101 -10
  286. package/src/session/agent-session.ts +1286 -220
  287. package/src/session/artifacts.ts +22 -0
  288. package/src/session/auth-broker-config.ts +10 -3
  289. package/src/session/blob-store.ts +915 -24
  290. package/src/session/internal/managed-session-scope.ts +44 -6
  291. package/src/session/internal/managed-session-storage.ts +48 -6
  292. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  293. package/src/session/session-manager.ts +2364 -438
  294. package/src/session/session-storage.ts +9 -1
  295. package/src/session/streaming-output.ts +500 -111
  296. package/src/setup/credential-import.ts +75 -13
  297. package/src/setup/provider-onboarding.ts +19 -4
  298. package/src/skill-state/active-state.ts +32 -10
  299. package/src/skill-state/workflow-hud.ts +42 -2
  300. package/src/skill-state/workflow-mutation-guard.ts +560 -8
  301. package/src/task/executor.ts +122 -39
  302. package/src/task/index.ts +23 -14
  303. package/src/task/provider-retry-status.ts +16 -0
  304. package/src/task/receipt.ts +23 -7
  305. package/src/task/render.ts +6 -2
  306. package/src/task/types.ts +108 -0
  307. package/src/tools/ask-answer-registry.ts +12 -6
  308. package/src/tools/ask.ts +169 -118
  309. package/src/tools/ast-edit.ts +1 -0
  310. package/src/tools/ast-grep.ts +1 -0
  311. package/src/tools/bash-interactive.ts +8 -3
  312. package/src/tools/bash.ts +503 -60
  313. package/src/tools/browser/launch.ts +41 -6
  314. package/src/tools/fetch.ts +114 -23
  315. package/src/tools/find.ts +1 -0
  316. package/src/tools/image-gen.ts +29 -5
  317. package/src/tools/index.ts +4 -0
  318. package/src/tools/output-meta.ts +281 -20
  319. package/src/tools/path-utils.ts +8 -3
  320. package/src/tools/read-internals.ts +44 -0
  321. package/src/tools/read.ts +1560 -310
  322. package/src/tools/resource-gc.ts +182 -18
  323. package/src/tools/review.ts +5 -7
  324. package/src/tools/search.ts +1 -0
  325. package/src/tools/subagent-render.ts +6 -2
  326. package/src/tools/subagent.ts +6 -0
  327. package/src/tools/telegram-send.ts +6 -3
  328. package/src/tools/tool-result.ts +14 -1
  329. package/src/utils/gh.ts +39 -0
  330. package/src/utils/image-resize.ts +3 -0
  331. package/src/web/search/providers/anthropic.ts +4 -4
  332. package/src/web/search/providers/codex.ts +1 -1
  333. package/src/web/search/providers/kimi.ts +15 -5
  334. package/src/web/search/providers/searxng.ts +47 -4
  335. package/src/web/search/providers/xai.ts +7 -2
  336. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
  337. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
  338. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
  339. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
  340. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
  341. package/src/gjc-runtime/deep-interview-draft.ts +0 -843
  342. package/src/gjc-runtime/deep-interview-payload.ts +0 -262
  343. package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
  344. package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
@@ -25,6 +25,7 @@ import {
25
25
  PROTOCOL_VERSION,
26
26
  type PromptRequest,
27
27
  type PromptResponse,
28
+ RequestError,
28
29
  type ResumeSessionRequest,
29
30
  type ResumeSessionResponse,
30
31
  type SessionInfo,
@@ -46,6 +47,7 @@ import { resolveAcpFinalText } from "../../sdk/acp/final-text";
46
47
  import { ACP_MCP_LIFECYCLE_TIMEOUT_MS, type SessionLifecycleMcpServer } from "../../sdk/acp/mcp";
47
48
  import { ensureBroker } from "../../sdk/broker/ensure";
48
49
  import { readSdkBrokerDiscovery, SdkClient, SdkClientError } from "../../sdk/client";
50
+ import type { SdkPromptTerminalOutcome } from "../../sdk/prompt-status";
49
51
  import {
50
52
  buildToolCallStartUpdate,
51
53
  mapAgentSessionEventToAcpSessionUpdates,
@@ -65,26 +67,21 @@ const THINKING_CONFIG_ID = "thinking";
65
67
  const SESSION_PAGE_SIZE = 50;
66
68
  export const ACP_BOOTSTRAP_RACE_GUARD_MS = 50;
67
69
  const MAX_ACP_REPLAY_PAGES = 10_000;
70
+ /** Bounded retention of settled prompt correlations so late duplicates stay closed. */
71
+ const SETTLED_PROMPT_CORRELATION_RETENTION = 16;
68
72
 
69
73
  type JsonObject = Record<string, unknown>;
70
- /**
71
- * ACP prompt completion is tied to lifecycle activity observed after dispatch.
72
- * A fast host may stream and finish a turn before acknowledging `turn.prompt`,
73
- * so pre-ack activity is retained but cannot resolve the waiter until the
74
- * acknowledgement supplies its command/turn correlation.
75
- */
76
74
  interface PromptWaiter {
77
- cancelRequested: boolean;
78
75
  acknowledged: boolean;
79
- activityObserved: boolean;
80
- /** The prompt was accepted while the host was already busy, so its next valid idle ends the steer. */
81
- steeringAtAcknowledgement: boolean;
82
76
  /** Highest inbound frame sequence already observed when the prompt was acknowledged. */
83
77
  boundary: number;
84
78
  correlation: PromptCorrelation;
85
79
  messageProgress?: { textEmitted: boolean; thoughtEmitted: boolean };
86
80
  emittedAssistantText: string;
87
- pendingTerminal?: PromptCorrelation;
81
+ settled: boolean;
82
+ terminal?: { outcome: SdkPromptTerminalOutcome; correlation: PromptCorrelation };
83
+ /** Frames for an already-settled correlation held until acknowledgement resolves ownership. */
84
+ deferredFrames: JsonObject[];
88
85
  resolve: (response: PromptResponse) => void;
89
86
  reject: (error: Error) => void;
90
87
  }
@@ -109,6 +106,9 @@ type SessionRecord = {
109
106
  /** Start/update args retained because tool_execution_end does not carry them. */
110
107
  toolArgs: Map<string, unknown>;
111
108
  /** Actionable model-profile authentication failure detected before prompt dispatch. */
109
+ connectionId?: string;
110
+ /** Bounded set of correlations already settled; they stay closed for publication. */
111
+ settledPromptCorrelations: PromptCorrelation[];
112
112
  authFailure?: string;
113
113
  activePrompt?: PromptWaiter;
114
114
  };
@@ -256,31 +256,97 @@ function correlationFrom(...values: unknown[]): PromptCorrelation {
256
256
  return correlation;
257
257
  }
258
258
 
259
- function correlationsConflict(expected: PromptCorrelation, actual: PromptCorrelation): boolean {
259
+ function hasCorrelation(correlation: PromptCorrelation): boolean {
260
+ return correlation.commandId !== undefined || correlation.turnId !== undefined;
261
+ }
262
+ function correlationsMatch(expected: PromptCorrelation, actual: PromptCorrelation): boolean {
260
263
  return (
261
- (expected.commandId !== undefined && actual.commandId !== undefined && expected.commandId !== actual.commandId) ||
262
- (expected.turnId !== undefined && actual.turnId !== undefined && expected.turnId !== actual.turnId)
264
+ hasCorrelation(actual) &&
265
+ (expected.commandId === undefined || expected.commandId === actual.commandId) &&
266
+ (expected.turnId === undefined || expected.turnId === actual.turnId)
263
267
  );
264
268
  }
265
269
 
266
- function correlationsMatch(expected: PromptCorrelation, actual: PromptCorrelation): boolean {
270
+ function hasCompleteCorrelation(correlation: PromptCorrelation): correlation is { commandId: string; turnId: string } {
267
271
  return (
268
- (expected.commandId !== undefined && expected.commandId === actual.commandId) ||
269
- (expected.turnId !== undefined && expected.turnId === actual.turnId)
272
+ typeof correlation.commandId === "string" &&
273
+ correlation.commandId.trim().length > 0 &&
274
+ typeof correlation.turnId === "string" &&
275
+ correlation.turnId.trim().length > 0
270
276
  );
271
277
  }
272
278
 
273
- function isPromptActivity(eventType: string): boolean {
274
- return [
275
- "agent_start",
276
- "turn_start",
277
- "message_start",
278
- "message_update",
279
- "message_end",
280
- "tool_execution_start",
281
- "tool_execution_update",
282
- "tool_execution_end",
283
- ].includes(eventType);
279
+ function correlationsExactlyMatch(expected: PromptCorrelation, actual: PromptCorrelation): boolean {
280
+ return (
281
+ hasCompleteCorrelation(expected) &&
282
+ hasCompleteCorrelation(actual) &&
283
+ expected.commandId === actual.commandId &&
284
+ expected.turnId === actual.turnId
285
+ );
286
+ }
287
+
288
+ function promptAcknowledgement(value: unknown): PromptCorrelation | undefined {
289
+ const candidate = object(value);
290
+ if (
291
+ !candidate ||
292
+ candidate.ok === false ||
293
+ candidate.error !== undefined ||
294
+ (candidate.accepted !== undefined && candidate.accepted !== true)
295
+ )
296
+ return undefined;
297
+ const payload = object(candidate.result) ?? candidate;
298
+ if (payload.accepted !== true) return undefined;
299
+ if (typeof payload.commandId !== "string" || payload.commandId.trim().length === 0) return undefined;
300
+ if (typeof payload.turnId !== "string" || payload.turnId.trim().length === 0) return undefined;
301
+ return { commandId: payload.commandId, turnId: payload.turnId };
302
+ }
303
+ function strictCorrelationFrom(...values: unknown[]): PromptCorrelation | undefined {
304
+ const correlation: PromptCorrelation = {};
305
+ let malformed = false;
306
+ for (const value of values) {
307
+ const candidate = object(value);
308
+ if (!candidate) continue;
309
+ for (const [field, aliases] of [
310
+ ["commandId", ["commandId", "command_id"]],
311
+ ["turnId", ["turnId", "turn_id"]],
312
+ ] as const) {
313
+ for (const alias of aliases) {
314
+ if (!Object.hasOwn(candidate, alias)) continue;
315
+ const identity = candidate[alias];
316
+ if (typeof identity !== "string" || identity.trim().length === 0) {
317
+ malformed = true;
318
+ continue;
319
+ }
320
+ const previous = correlation[field];
321
+ if (previous !== undefined && previous !== identity) malformed = true;
322
+ correlation[field] = identity;
323
+ }
324
+ }
325
+ }
326
+ return malformed ? undefined : correlation;
327
+ }
328
+
329
+ function terminalOutcome(event: JsonObject): SdkPromptTerminalOutcome | undefined {
330
+ const outcome = object(event.outcome);
331
+ if (!outcome) return undefined;
332
+ if (
333
+ outcome.kind === "stopped" &&
334
+ (outcome.reason === "end_turn" ||
335
+ outcome.reason === "max_tokens" ||
336
+ outcome.reason === "max_turn_requests" ||
337
+ outcome.reason === "refusal" ||
338
+ outcome.reason === "cancelled") &&
339
+ (outcome.provenance === "agent" || outcome.provenance === "client_cancel")
340
+ )
341
+ return outcome as SdkPromptTerminalOutcome;
342
+ if (
343
+ outcome.kind === "failed" &&
344
+ (outcome.code === "prompt_failed" || outcome.code === "prompt_deadline_exceeded") &&
345
+ typeof outcome.message === "string" &&
346
+ (outcome.provenance === "agent_failed" || outcome.provenance === "deadline")
347
+ )
348
+ return outcome as SdkPromptTerminalOutcome;
349
+ return undefined;
284
350
  }
285
351
 
286
352
  export type TranscriptReplayBlock = { type: "text"; text: string } | { type: "image"; data: string; mimeType: string };
@@ -321,11 +387,9 @@ type ReceivedSdkEvent = {
321
387
  * without treating notification-specific frames as agent lifecycle truth.
322
388
  */
323
389
  function receivedSdkEvent(frame: JsonObject): ReceivedSdkEvent | undefined {
324
- if (frame.type === "activity") {
325
- const type = frame.state === "busy" ? "agent_start" : frame.state === "idle" ? "agent_end" : undefined;
326
- return type ? { event: { type, ...correlationFrom(frame) } } : undefined;
327
- }
328
- if (frame.type === "agent_start" || frame.type === "agent_end") return { event: frame };
390
+ if (frame.type === "activity") return undefined;
391
+ if (frame.type === "agent_start" || frame.type === "agent_end" || frame.type === "agent_failed")
392
+ return { event: frame };
329
393
  if (frame.type !== "event") return undefined;
330
394
  const payload = object(frame.payload);
331
395
  if (!payload) return undefined;
@@ -532,23 +596,95 @@ export function acpPromptPayload(blocks: PromptRequest["prompt"]): {
532
596
  return { text: text.join("\n"), images };
533
597
  }
534
598
 
599
+ /**
600
+ * `AcpSdkAdapterError.code` is an internal string, but the SDK only derives a
601
+ * JSON-RPC code from a `RequestError`. Everything else collapses to an opaque
602
+ * `-32603 Internal error`, which hides the reason and defeats client-side
603
+ * recovery (an ACP client cannot see that it must authenticate). Map the codes
604
+ * that have a defined ACP/JSON-RPC counterpart onto a real `RequestError`.
605
+ */
606
+ export function acpRequestFailure(error: unknown): unknown {
607
+ const code = typeof error === "object" && error !== null && "code" in error ? error.code : undefined;
608
+ if (typeof code !== "string") return error;
609
+ const message = error instanceof Error ? error.message : code;
610
+ switch (code) {
611
+ case "authentication_failed":
612
+ return RequestError.authRequired({ code, details: message }, message);
613
+ // `not_found` stays -32603 with its discriminator in `data`: ACP's
614
+ // `resourceNotFound` (-32002) is a URI-addressed resource error, and an unknown
615
+ // session id is not a resource URI. Pinned ACP core-v1 conformance also requires
616
+ // -32603/-32000 for a prompt against an unknown session.
617
+ case "invalid_input":
618
+ case "unsupported":
619
+ case "unsupported_content":
620
+ return RequestError.invalidParams({ code, details: message }, message);
621
+ default:
622
+ // The remaining internal codes (conflict, unavailable, busy, …) have no ACP
623
+ // counterpart and stay -32603. Keep the discriminator in `data` so a client can
624
+ // branch on retry/reconnect instead of parsing an English message.
625
+ return RequestError.internalError({ code, details: message }, message);
626
+ }
627
+ }
628
+
535
629
  /** Registers a permission provider only when the ACP client requires prompts. */
536
630
  export function acpProviderRegistrations(
537
631
  capabilities: ClientCapabilities | undefined,
538
632
  env: NodeJS.ProcessEnv = process.env,
539
633
  ): AcpProviderRegistration[] {
540
634
  return [
635
+ // `fs.readTextFile` and `fs.writeTextFile` are independently optional, so the
636
+ // advertised methods travel with the lease instead of being inferred as both.
541
637
  ...(capabilities?.fs?.readTextFile || capabilities?.fs?.writeTextFile
542
- ? [{ capability: "fs", definitions: [] }]
638
+ ? [
639
+ {
640
+ capability: "fs",
641
+ definitions: [
642
+ ...(capabilities.fs.readTextFile ? [{ name: "fs.readTextFile" }] : []),
643
+ ...(capabilities.fs.writeTextFile ? [{ name: "fs.writeTextFile" }] : []),
644
+ ],
645
+ },
646
+ ]
543
647
  : []),
544
648
  ...(capabilities?.terminal ? [{ capability: "terminal", definitions: [] }] : []),
545
649
  ...(resolveAcpPermissionMode(capabilities, env) === "prompt"
546
650
  ? [{ capability: "permission", definitions: [] }]
547
651
  : []),
548
- { capability: "ui", definitions: [] },
652
+ ...(capabilities?.elicitation?.form ? [{ capability: "ui", definitions: [] }] : []),
549
653
  ];
550
654
  }
551
655
 
656
+ export function createAcpReverseConnection(connection: AgentSideConnection, sessionId: string): AcpReverseConnection {
657
+ const methods: Record<string, string> = {
658
+ request: "session/request_permission",
659
+ "permission.request": "session/request_permission",
660
+ "fs.readTextFile": "fs/read_text_file",
661
+ "fs.writeTextFile": "fs/write_text_file",
662
+ "terminal.create": "terminal/create",
663
+ "ui.elicit": "elicitation/create",
664
+ };
665
+ return {
666
+ request: async (
667
+ method: string,
668
+ params: JsonObject,
669
+ options?: { cancellationSignal?: AbortSignal },
670
+ ): Promise<unknown> => {
671
+ const name = methods[method];
672
+ if (!name)
673
+ throw new AcpSdkAdapterError("acp_reverse_unavailable", `ACP reverse method is unavailable: ${method}`);
674
+ const rawRequest = (connection as unknown as Record<string, unknown>).request;
675
+ if (typeof rawRequest !== "function")
676
+ throw new AcpSdkAdapterError("acp_reverse_unavailable", "ACP reverse request surface is unavailable.");
677
+ return await (
678
+ rawRequest as (
679
+ method: string,
680
+ input: JsonObject,
681
+ options?: { cancellationSignal?: AbortSignal },
682
+ ) => Promise<unknown>
683
+ ).call(connection, name, { ...params, sessionId }, options);
684
+ },
685
+ };
686
+ }
687
+
552
688
  /** Maps ACP permission handling to the session's canonical SDK policy. */
553
689
  export async function applyAcpPermissionMode(
554
690
  adapter: Pick<AcpSdkAdapter, "control">,
@@ -568,82 +704,6 @@ export async function applyAcpStartupOptions(
568
704
  if (options?.thinkingLevel) await adapter.control("thinking.set", { level: options.thinkingLevel });
569
705
  }
570
706
 
571
- /** ACP form elicitation uses the client-facing reverse surface without owning a session runtime. */
572
- export function createAcpExtensionUiContext(
573
- connection: AgentSideConnection,
574
- getSessionId: () => string,
575
- capabilities: ClientCapabilities | undefined,
576
- ): {
577
- select: (
578
- message: string,
579
- options: string[],
580
- dialog?: { signal?: AbortSignal; timeout?: number; onTimeout?: () => void },
581
- ) => Promise<string | undefined>;
582
- confirm: (
583
- message: string,
584
- detail?: string,
585
- dialog?: { signal?: AbortSignal; timeout?: number; onTimeout?: () => void },
586
- ) => Promise<boolean>;
587
- input: (
588
- message: string,
589
- placeholder?: string,
590
- dialog?: { signal?: AbortSignal; timeout?: number; onTimeout?: () => void },
591
- ) => Promise<string | undefined>;
592
- } {
593
- const elicit = async (
594
- kind: "select" | "confirm" | "input",
595
- message: string,
596
- options: string[] | undefined,
597
- dialog: { signal?: AbortSignal; timeout?: number; onTimeout?: () => void } | undefined,
598
- ): Promise<unknown> => {
599
- if (!capabilities?.elicitation?.form || dialog?.signal?.aborted) return undefined;
600
- const request = (
601
- connection as unknown as { unstable_createElicitation(input: JsonObject): Promise<JsonObject> }
602
- ).unstable_createElicitation({
603
- sessionId: getSessionId(),
604
- message,
605
- requestedSchema: {
606
- type: "object",
607
- properties: {
608
- value:
609
- kind === "confirm" ? { type: "boolean" } : { type: "string", ...(options ? { enum: options } : {}) },
610
- },
611
- required: ["value"],
612
- },
613
- });
614
- let timer: NodeJS.Timeout | undefined;
615
- const timeout =
616
- dialog?.timeout === undefined
617
- ? undefined
618
- : new Promise<undefined>(resolve => {
619
- timer = setTimeout(() => {
620
- dialog.onTimeout?.();
621
- resolve(undefined);
622
- }, dialog.timeout);
623
- });
624
- try {
625
- const response = timeout ? await Promise.race([request, timeout]) : await request;
626
- return object(object(response)?.content)?.value;
627
- } catch {
628
- return undefined;
629
- } finally {
630
- if (timer) clearTimeout(timer);
631
- }
632
- };
633
- return {
634
- select: async (message, options, dialog) => {
635
- const value = await elicit("select", message, options, dialog);
636
- return typeof value === "string" && options.includes(value) ? value : undefined;
637
- },
638
- confirm: async (message, detail, dialog) =>
639
- (await elicit("confirm", detail ? `${message}\n\n${detail}` : message, undefined, dialog)) === true,
640
- input: async (message, placeholder, dialog) => {
641
- const value = await elicit("input", placeholder ? `${message}\n\n${placeholder}` : message, undefined, dialog);
642
- return typeof value === "string" ? value : undefined;
643
- },
644
- };
645
- }
646
-
647
707
  /**
648
708
  * ACP is a pure SDK client. Session processes are created and resumed by the
649
709
  * broker, while all per-session operations use that session's authenticated SDK
@@ -933,20 +993,19 @@ export class AcpAgent implements Agent {
933
993
 
934
994
  async prompt(params: PromptRequest): Promise<PromptResponse> {
935
995
  const record = this.#sessions.get(params.sessionId);
936
- if (!record) throw new AcpSdkAdapterError("not_found", `Unsupported ACP session: ${params.sessionId}`);
996
+ if (!record) throw new AcpSdkAdapterError("not_found", `Unknown session, not found: ${params.sessionId}`);
937
997
  if (record.activePrompt) throw new AcpSdkAdapterError("conflict", "ACP session already has an active prompt.");
938
998
  if (record.authFailure) throw new AcpSdkAdapterError("authentication_failed", record.authFailure);
939
999
  const payload = acpPromptPayload(params.prompt);
940
1000
  let waiter!: PromptWaiter;
941
1001
  const response = new Promise<PromptResponse>((resolve, reject) => {
942
1002
  waiter = {
943
- cancelRequested: false,
944
1003
  acknowledged: false,
945
- activityObserved: false,
946
- steeringAtAcknowledgement: record.busy,
947
1004
  boundary: record.inboundSequence,
948
1005
  correlation: {},
949
1006
  emittedAssistantText: "",
1007
+ settled: false,
1008
+ deferredFrames: [],
950
1009
  resolve,
951
1010
  reject,
952
1011
  };
@@ -957,14 +1016,29 @@ export class AcpAgent implements Agent {
957
1016
  text: payload.text,
958
1017
  ...(payload.images.length ? { images: payload.images } : {}),
959
1018
  });
960
- waiter.steeringAtAcknowledgement = record.busy;
961
- // Capture the ingress boundary after the command acknowledgement. Frames
962
- // queued before this point are stale with respect to this ACP prompt.
1019
+ const acknowledgementCorrelation = promptAcknowledgement(acknowledgement);
1020
+ if (!acknowledgementCorrelation)
1021
+ throw new AcpSdkAdapterError(
1022
+ "invalid_prompt_acknowledgement",
1023
+ "SDK prompt acknowledgement must accept the prompt and include commandId and turnId.",
1024
+ );
1025
+ // Retain the acknowledgement ingress boundary with its complete correlation.
963
1026
  waiter.boundary = record.inboundSequence;
964
- waiter.correlation = correlationFrom(acknowledgement);
1027
+ waiter.correlation = acknowledgementCorrelation;
965
1028
  waiter.acknowledged = true;
1029
+ // Frames held while ownership was unknown belong to this prompt only when the
1030
+ // acknowledgement proves their complete correlation matches exactly.
1031
+ const deferred = waiter.deferredFrames.splice(0);
1032
+ for (const deferredFrame of deferred)
1033
+ if (correlationsExactlyMatch(waiter.correlation, correlationFrom(deferredFrame)))
1034
+ record.frameTail = record.frameTail.then(
1035
+ async () => await this.#handleSdkFrame(params.sessionId, record.adapter, deferredFrame),
1036
+ );
966
1037
  this.#settlePrompt(record, waiter);
967
1038
  } catch (error) {
1039
+ waiter.deferredFrames.length = 0;
1040
+ waiter.terminal = undefined;
1041
+ waiter.settled = true;
968
1042
  if (record.activePrompt === waiter) record.activePrompt = undefined;
969
1043
  throw error;
970
1044
  }
@@ -973,8 +1047,7 @@ export class AcpAgent implements Agent {
973
1047
 
974
1048
  async cancel(params: { sessionId: string }): Promise<void> {
975
1049
  const record = this.#sessions.get(params.sessionId);
976
- if (!record) throw new AcpSdkAdapterError("not_found", `Unsupported ACP session: ${params.sessionId}`);
977
- const waiter = record.activePrompt;
1050
+ if (!record) throw new AcpSdkAdapterError("not_found", `Unknown session, not found: ${params.sessionId}`);
978
1051
  const acknowledgement = await record.adapter.cancel();
979
1052
  const result = object(object(acknowledgement)?.result) ?? object(acknowledgement);
980
1053
  if (result?.aborted !== true)
@@ -982,12 +1055,19 @@ export class AcpAgent implements Agent {
982
1055
  "abort_unacknowledged",
983
1056
  "SDK did not acknowledge cancellation of the active prompt.",
984
1057
  );
985
- // Do not retroactively mark a waiter that already settled while the abort
986
- // request was in flight. A cancelled response means the abort itself won.
987
- if (waiter && record.activePrompt === waiter) waiter.cancelRequested = true;
988
1058
  }
989
1059
 
990
1060
  async extMethod(method: string, params: JsonObject): Promise<JsonObject> {
1061
+ // An unrecognized extension method is a protocol failure, not an application
1062
+ // result: it must reach the client as JSON-RPC -32601 rather than a resolved
1063
+ // payload. Recognized `_gjc/*` methods keep their `{ok:false}` result contract.
1064
+ if (
1065
+ method !== "session/set_model" &&
1066
+ method !== "_gjc/sdk/global" &&
1067
+ method !== "_gjc/sdk/control" &&
1068
+ method !== "_gjc/sdk/query"
1069
+ )
1070
+ throw RequestError.methodNotFound(method);
991
1071
  try {
992
1072
  if (method === "session/set_model") {
993
1073
  const sessionId = typeof params.sessionId === "string" ? params.sessionId : undefined;
@@ -1001,13 +1081,10 @@ export class AcpAgent implements Agent {
1001
1081
  const result = await (await this.#brokerAdapter()).handle(method, params);
1002
1082
  return object(result) ?? {};
1003
1083
  }
1004
- if (method === "_gjc/sdk/control" || method === "_gjc/sdk/query") {
1005
- const id = typeof params.sessionId === "string" ? params.sessionId : undefined;
1006
- if (!id) throw new AcpSdkAdapterError("invalid_input", "sessionId is required.");
1007
- const result = await this.#adapter(id).handle(method, params);
1008
- return object(result) ?? {};
1009
- }
1010
- throw new AcpSdkAdapterError("method_not_found", `Unknown ACP ext method: ${method}`);
1084
+ const id = typeof params.sessionId === "string" ? params.sessionId : undefined;
1085
+ if (!id) throw new AcpSdkAdapterError("invalid_input", "sessionId is required.");
1086
+ const result = await this.#adapter(id).handle(method, params);
1087
+ return object(result) ?? {};
1011
1088
  } catch (error) {
1012
1089
  const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "internal";
1013
1090
  const message = error instanceof Error ? error.message : String(error);
@@ -1074,8 +1151,9 @@ export class AcpAgent implements Agent {
1074
1151
  if (attached) {
1075
1152
  if (path.resolve(attached.cwd) !== path.resolve(cwd))
1076
1153
  throw new AcpSdkAdapterError("conflict", `ACP session ${id} has conflicting cwd authority.`);
1077
- if (mcpServers.length > 0)
1078
- throw new AcpSdkAdapterError("conflict", `ACP session ${id} already has immutable MCP configuration.`);
1154
+ // ACP clients replay their declared MCP servers when reconnecting. The live
1155
+ // session host remains authoritative for its immutable configuration, so
1156
+ // attachment must not reinterpret the replay as a mutation request.
1079
1157
  return;
1080
1158
  }
1081
1159
  const knownCwd = this.#knownSessionCwds.get(id);
@@ -1113,8 +1191,8 @@ export class AcpAgent implements Agent {
1113
1191
  const indexed = await this.#scopedBrokerSession(id, cwd);
1114
1192
  this.#assertSessionEpoch(id, epoch);
1115
1193
  if (indexed?.live) {
1116
- if (mcpServers.length > 0)
1117
- throw new AcpSdkAdapterError("conflict", `ACP session ${id} already has immutable MCP configuration.`);
1194
+ // A reconnect may repeat the client's MCP declaration. Attaching to the
1195
+ // existing endpoint preserves the live host's immutable configuration.
1118
1196
  const result = await this.#brokerEndpoint(id, indexed.endpointGeneration);
1119
1197
  this.#assertSessionEpoch(id, epoch);
1120
1198
  await this.#attach(id, cwd, endpoint(result), epoch);
@@ -1193,6 +1271,19 @@ export class AcpAgent implements Agent {
1193
1271
  connection: this.#reverseConnection(id),
1194
1272
  providers: this.#providers(),
1195
1273
  });
1274
+ let capabilities: JsonObject | undefined;
1275
+ try {
1276
+ const response = object(await adapter.query("runtime.capabilities"));
1277
+ const result = object(response?.result) ?? response;
1278
+ // Q18 is a paged query surface: the capability object arrives as the single
1279
+ // page item, so fall back to the envelope only for direct-result hosts.
1280
+ capabilities = object(pageItems(result)[0]) ?? result;
1281
+ } catch {}
1282
+ if (capabilities?.promptTerminalOutcomeVersion !== 1)
1283
+ throw new AcpSdkAdapterError(
1284
+ "unavailable",
1285
+ "This ACP client requires a newer GJC SDK session; restart the session.",
1286
+ );
1196
1287
  this.#assertSessionEpoch(id, epoch);
1197
1288
  const record: SessionRecord = {
1198
1289
  cwd,
@@ -1201,7 +1292,9 @@ export class AcpAgent implements Agent {
1201
1292
  unsubscribe: () => {},
1202
1293
  reconnectUnsubscribe: () => {},
1203
1294
  frameTail: Promise.resolve(),
1295
+ settledPromptCorrelations: [],
1204
1296
  inboundSequence: 0,
1297
+ connectionId: adapter.connectionId,
1205
1298
  busy: false,
1206
1299
  toolArgs: new Map(),
1207
1300
  };
@@ -1378,7 +1471,7 @@ export class AcpAgent implements Agent {
1378
1471
 
1379
1472
  #adapter(id: string): AcpSdkAdapter {
1380
1473
  const record = this.#sessions.get(id);
1381
- if (!record) throw new AcpSdkAdapterError("not_found", `Unsupported ACP session: ${id}`);
1474
+ if (!record) throw new AcpSdkAdapterError("not_found", `Unknown session, not found: ${id}`);
1382
1475
  return record.adapter;
1383
1476
  }
1384
1477
 
@@ -1398,29 +1491,18 @@ export class AcpAgent implements Agent {
1398
1491
  }
1399
1492
 
1400
1493
  #reverseConnection(sessionId: string): AcpReverseConnection {
1401
- const methods: Record<string, string> = {
1402
- "fs.readTextFile": "readTextFile",
1403
- "fs.writeTextFile": "writeTextFile",
1404
- "terminal.create": "createTerminal",
1405
- "permission.request": "requestPermission",
1406
- "ui.elicit": "unstable_createElicitation",
1407
- };
1408
- return {
1409
- request: async (method: string, params: JsonObject): Promise<unknown> => {
1410
- const name = methods[method] ?? method;
1411
- const target = (this.#connection as unknown as Record<string, unknown>)[name];
1412
- if (typeof target !== "function")
1413
- throw new AcpSdkAdapterError("acp_reverse_unavailable", `ACP reverse method is unavailable: ${method}`);
1414
- const request = method === "permission.request" ? { ...params, sessionId } : params;
1415
- return await (target as (input: JsonObject) => Promise<unknown>)(request);
1416
- },
1417
- };
1494
+ return createAcpReverseConnection(this.#connection, sessionId);
1418
1495
  }
1419
1496
 
1420
1497
  #observeSessionActivity(record: SessionRecord, frame: JsonObject): void {
1498
+ if (frame.type === "activity") {
1499
+ if (frame.state === "busy") record.busy = true;
1500
+ else if (frame.state === "idle") record.busy = false;
1501
+ return;
1502
+ }
1421
1503
  const event = receivedSdkEvent(frame)?.event;
1422
1504
  if (event?.type === "agent_start") record.busy = true;
1423
- else if (event?.type === "agent_end") record.busy = false;
1505
+ else if (event?.type === "agent_end" || event?.type === "agent_failed") record.busy = false;
1424
1506
  }
1425
1507
 
1426
1508
  #frameProcessingFailure(error: unknown): AcpSdkAdapterError {
@@ -1432,22 +1514,72 @@ export class AcpAgent implements Agent {
1432
1514
  #enqueueSdkFrame(id: string, adapter: AcpSdkAdapter, frame: JsonObject): void {
1433
1515
  const record = this.#sessions.get(id);
1434
1516
  if (!record || record.adapter !== adapter) return;
1435
- // Sequence and busy state are captured at ingress, before queued work begins.
1436
- // A frame received before acknowledgement stays before that prompt's boundary.
1517
+ // Ingress ordering is recorded before queued work begins.
1437
1518
  this.#observeSessionActivity(record, frame);
1438
- const sequence = ++record.inboundSequence;
1439
- const task = record.frameTail.then(async () => await this.#handleSdkFrame(id, adapter, frame, sequence));
1519
+ ++record.inboundSequence;
1520
+ const task = record.frameTail.then(async () => await this.#handleSdkFrame(id, adapter, frame));
1440
1521
  record.frameTail = task.catch(
1441
1522
  async error => await this.#failSession(id, adapter, this.#frameProcessingFailure(error)),
1442
1523
  );
1443
1524
  }
1444
1525
 
1445
- async #handleSdkFrame(id: string, adapter: AcpSdkAdapter, frame: JsonObject, sequence: number): Promise<void> {
1526
+ async #handleSdkFrame(id: string, adapter: AcpSdkAdapter, frame: JsonObject): Promise<void> {
1446
1527
  const record = this.#sessions.get(id);
1447
1528
  if (!record || record.adapter !== adapter) return;
1529
+ if ((frame.type === "hello" || frame.type === "server_hello") && typeof frame.connectionId === "string") {
1530
+ const reconnected = record.connectionId !== undefined && record.connectionId !== frame.connectionId;
1531
+ record.connectionId = frame.connectionId;
1532
+ if (reconnected) {
1533
+ const waiter = record.activePrompt;
1534
+ if (waiter && !waiter.settled && !waiter.terminal) {
1535
+ record.activePrompt = undefined;
1536
+ waiter.settled = true;
1537
+ if (hasCorrelation(waiter.correlation)) {
1538
+ record.settledPromptCorrelations.push(waiter.correlation);
1539
+ while (record.settledPromptCorrelations.length > SETTLED_PROMPT_CORRELATION_RETENTION)
1540
+ record.settledPromptCorrelations.shift();
1541
+ }
1542
+ waiter.reject(
1543
+ new AcpSdkAdapterError(
1544
+ "connection_closed",
1545
+ "The prompt owner connection was lost before completion.",
1546
+ ),
1547
+ );
1548
+ }
1549
+ }
1550
+ return;
1551
+ }
1448
1552
  const received = receivedSdkEvent(frame);
1449
1553
  if (!received) return;
1450
1554
  const { event, wirePayload } = received;
1555
+ const isTerminal = event.type === "agent_end" || event.type === "agent_failed";
1556
+ const correlation = (isTerminal ? strictCorrelationFrom(frame, event) : correlationFrom(frame, event)) ?? {};
1557
+ const activePrompt = record.activePrompt;
1558
+ const outcome = isTerminal ? terminalOutcome(event) : undefined;
1559
+ if (isTerminal) {
1560
+ // Terminal ownership requires a complete identity. Unowned, partial, and
1561
+ // duplicate terminals are never allowed to publish or query anything.
1562
+ if (!hasCompleteCorrelation(correlation) || !activePrompt || activePrompt.settled) return;
1563
+ if (!activePrompt.acknowledged) {
1564
+ // Hold the entire frame until the prompt acknowledgement proves ownership.
1565
+ activePrompt.deferredFrames.push(frame);
1566
+ return;
1567
+ }
1568
+ if (!correlationsExactlyMatch(activePrompt.correlation, correlation) || activePrompt.terminal) return;
1569
+ if (!outcome) {
1570
+ const detail =
1571
+ typeof (event as { error?: { message?: unknown } }).error?.message === "string"
1572
+ ? (event as { error: { message: string } }).error.message
1573
+ : "the prompt terminal omitted a valid normalized outcome";
1574
+ this.#rejectPrompt(
1575
+ record,
1576
+ activePrompt,
1577
+ new AcpSdkAdapterError("connection_closed", `ACP prompt terminal was invalid: ${detail}`),
1578
+ );
1579
+ return;
1580
+ }
1581
+ activePrompt.terminal = { outcome, correlation };
1582
+ }
1451
1583
  const toolCallId = typeof event.toolCallId === "string" ? event.toolCallId : undefined;
1452
1584
  if (
1453
1585
  toolCallId &&
@@ -1456,31 +1588,21 @@ export class AcpAgent implements Agent {
1456
1588
  ) {
1457
1589
  record.toolArgs.set(toolCallId, event.args);
1458
1590
  }
1459
- const correlation = correlationFrom(frame, event);
1460
- const activePrompt = record.activePrompt;
1461
- // Prompt ownership is updated before publishing client notifications, but a
1462
- // terminal waiter is resolved only after acknowledgement and publication
1463
- // succeed. Fast hosts may emit the whole turn before acknowledging the
1464
- // control request; retaining that activity prevents an idle ACP prompt from
1465
- // remaining open forever.
1466
- if (
1467
- activePrompt &&
1468
- sequence > activePrompt.boundary &&
1469
- !correlationsConflict(activePrompt.correlation, correlation)
1470
- ) {
1471
- if (isPromptActivity(String(event.type))) {
1472
- activePrompt.activityObserved = true;
1473
- } else if (event.type === "agent_end") {
1474
- if (
1475
- correlationsMatch(activePrompt.correlation, correlation) ||
1476
- correlation.commandId !== undefined ||
1477
- correlation.turnId !== undefined ||
1478
- activePrompt.activityObserved ||
1479
- activePrompt.steeringAtAcknowledgement
1480
- )
1481
- activePrompt.pendingTerminal = correlation;
1591
+ const settledCorrelation = record.settledPromptCorrelations.some(settled =>
1592
+ correlationsMatch(settled, correlation),
1593
+ );
1594
+ if (settledCorrelation) {
1595
+ // Frames for an already-settled correlation stay closed until an active prompt
1596
+ // acknowledges the exact same identity.
1597
+ if (activePrompt && !activePrompt.settled && !activePrompt.acknowledged) {
1598
+ activePrompt.deferredFrames.push(frame);
1599
+ return;
1482
1600
  }
1601
+ if (!activePrompt || activePrompt.settled || !correlationsMatch(activePrompt.correlation, correlation)) return;
1483
1602
  }
1603
+ // After a correlated settlement with no active prompt, correlationless wire frames
1604
+ // have no prompt to belong to and must not publish further updates.
1605
+ if (!record.activePrompt && record.settledPromptCorrelations.length > 0 && !hasCorrelation(correlation)) return;
1484
1606
  if (wirePayload) {
1485
1607
  for (const notification of mapAgentWireEventPayloadToAcpSessionUpdates(wirePayload as never, id, {
1486
1608
  cwd: record.cwd,
@@ -1546,21 +1668,47 @@ export class AcpAgent implements Agent {
1546
1668
  }
1547
1669
  }
1548
1670
  await this.#emitEndOfTurnUpdates(id, adapter);
1671
+ } else if (event.type === "agent_failed") {
1672
+ await this.#emitEndOfTurnUpdates(id, adapter);
1549
1673
  }
1550
1674
  if (activePrompt) this.#settlePrompt(record, activePrompt);
1551
1675
  }
1552
1676
 
1677
+ #rejectPrompt(record: SessionRecord, waiter: PromptWaiter, error: AcpSdkAdapterError): void {
1678
+ if (record.activePrompt !== waiter || waiter.settled) return;
1679
+ record.activePrompt = undefined;
1680
+ waiter.settled = true;
1681
+ waiter.deferredFrames.length = 0;
1682
+ waiter.terminal = undefined;
1683
+ if (hasCompleteCorrelation(waiter.correlation)) {
1684
+ record.settledPromptCorrelations.push(waiter.correlation);
1685
+ while (record.settledPromptCorrelations.length > SETTLED_PROMPT_CORRELATION_RETENTION)
1686
+ record.settledPromptCorrelations.shift();
1687
+ }
1688
+ waiter.reject(error);
1689
+ }
1690
+
1553
1691
  #settlePrompt(record: SessionRecord, waiter: PromptWaiter): void {
1554
- if (record.activePrompt !== waiter || !waiter.acknowledged || !waiter.pendingTerminal) return;
1555
- if (correlationsConflict(waiter.correlation, waiter.pendingTerminal)) return;
1556
- if (
1557
- !correlationsMatch(waiter.correlation, waiter.pendingTerminal) &&
1558
- !waiter.activityObserved &&
1559
- !waiter.steeringAtAcknowledgement
1560
- )
1692
+ if (record.activePrompt !== waiter || waiter.settled || !waiter.acknowledged || !waiter.terminal) return;
1693
+ // A terminal captured before acknowledgement is only this prompt's terminal when the
1694
+ // eventual acknowledgement correlates with it; otherwise it belonged to an earlier prompt.
1695
+ if (!correlationsExactlyMatch(waiter.correlation, waiter.terminal.correlation)) {
1696
+ waiter.terminal = undefined;
1561
1697
  return;
1698
+ }
1562
1699
  record.activePrompt = undefined;
1563
- waiter.resolve({ stopReason: waiter.cancelRequested ? "cancelled" : "end_turn" });
1700
+ waiter.settled = true;
1701
+ if (hasCorrelation(waiter.correlation)) {
1702
+ record.settledPromptCorrelations.push(waiter.correlation);
1703
+ while (record.settledPromptCorrelations.length > SETTLED_PROMPT_CORRELATION_RETENTION)
1704
+ record.settledPromptCorrelations.shift();
1705
+ }
1706
+ const { outcome } = waiter.terminal;
1707
+ if (outcome.kind === "stopped") {
1708
+ waiter.resolve({ stopReason: outcome.reason });
1709
+ return;
1710
+ }
1711
+ waiter.reject(new AcpSdkAdapterError(outcome.code, outcome.message));
1564
1712
  }
1565
1713
 
1566
1714
  async #emitEndOfTurnUpdates(id: string, adapter: AcpSdkAdapter): Promise<void> {
@@ -1634,7 +1782,7 @@ export class AcpAgent implements Agent {
1634
1782
  rejectUnavailableStartupPreset = false,
1635
1783
  ): Promise<Pick<NewSessionResponse, "configOptions" | "modes">> {
1636
1784
  const record = this.#sessions.get(id);
1637
- if (!record) throw new AcpSdkAdapterError("not_found", `Unsupported ACP session: ${id}`);
1785
+ if (!record) throw new AcpSdkAdapterError("not_found", `Unknown session, not found: ${id}`);
1638
1786
  const modelPreset = this.#startupOptions?.modelPreset;
1639
1787
  const [config, modelCatalog] = await Promise.all([
1640
1788
  record.adapter.query("config.list/get"),