@gajae-code/coding-agent 0.11.1 → 0.11.3

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 (413) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/dist/types/cli/config-cli.d.ts +11 -1
  3. package/dist/types/cli/launch-disposition.d.ts +23 -0
  4. package/dist/types/cli/notify-cli.d.ts +1 -0
  5. package/dist/types/cli/session-picker.d.ts +2 -1
  6. package/dist/types/cli/setup-cli.d.ts +1 -1
  7. package/dist/types/commands/mcp-serve.d.ts +33 -0
  8. package/dist/types/commands/mcp.d.ts +1 -1
  9. package/dist/types/commands/notify.d.ts +0 -2
  10. package/dist/types/commands/sdk.d.ts +1 -1
  11. package/dist/types/config/atomic-yaml-patch.d.ts +18 -0
  12. package/dist/types/config/file-lock.d.ts +14 -4
  13. package/dist/types/config/keybindings.d.ts +42 -1
  14. package/dist/types/config/model-auth.d.ts +3 -0
  15. package/dist/types/config/model-bindings-applier.d.ts +13 -0
  16. package/dist/types/config/model-discovery-manager.d.ts +49 -0
  17. package/dist/types/config/model-equivalence.d.ts +20 -0
  18. package/dist/types/config/model-registry.d.ts +16 -15
  19. package/dist/types/config/model-resolver.d.ts +28 -18
  20. package/dist/types/config/models-config-schema.d.ts +11 -0
  21. package/dist/types/config/settings-schema.d.ts +141 -11
  22. package/dist/types/config/settings.d.ts +14 -11
  23. package/dist/types/coordinator-mcp/policy.d.ts +5 -0
  24. package/dist/types/coordinator-mcp/question-gate-codec.d.ts +129 -0
  25. package/dist/types/coordinator-mcp/question-state.d.ts +356 -0
  26. package/dist/types/coordinator-mcp/server.d.ts +4 -0
  27. package/dist/types/discovery/helpers.d.ts +0 -6
  28. package/dist/types/edit/index.d.ts +3 -0
  29. package/dist/types/eval/py/env.d.ts +9 -0
  30. package/dist/types/extensibility/extensions/loader.d.ts +2 -1
  31. package/dist/types/extensibility/extensions/runner.d.ts +2 -2
  32. package/dist/types/extensibility/extensions/types.d.ts +34 -3
  33. package/dist/types/extensibility/hooks/runner.d.ts +1 -1
  34. package/dist/types/extensibility/shared-events.d.ts +2 -0
  35. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
  36. package/dist/types/gjc-runtime/deep-interview-state.d.ts +44 -0
  37. package/dist/types/gjc-runtime/goal-mode-request.d.ts +11 -1
  38. package/dist/types/gjc-runtime/launch-worktree.d.ts +4 -2
  39. package/dist/types/gjc-runtime/linux-proc.d.ts +29 -0
  40. package/dist/types/gjc-runtime/session-resolution.d.ts +2 -2
  41. package/dist/types/gjc-runtime/state-migrations.d.ts +1 -1
  42. package/dist/types/gjc-runtime/state-writer.d.ts +2 -1
  43. package/dist/types/gjc-runtime/team-launch.d.ts +59 -0
  44. package/dist/types/gjc-runtime/team-notify.d.ts +45 -0
  45. package/dist/types/gjc-runtime/team-runtime.d.ts +29 -121
  46. package/dist/types/gjc-runtime/team-store.d.ts +160 -0
  47. package/dist/types/gjc-runtime/team-workers.d.ts +78 -0
  48. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +8 -0
  49. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +13 -0
  50. package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +38 -0
  51. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -0
  52. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +23 -0
  53. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +55 -33
  54. package/dist/types/gjc-runtime/workflow-cli-common.d.ts +8 -0
  55. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -0
  56. package/dist/types/goals/continuation-timeout-guard.d.ts +22 -0
  57. package/dist/types/goals/runtime.d.ts +1 -0
  58. package/dist/types/goals/state.d.ts +8 -0
  59. package/dist/types/harness-control-plane/lifecycle-reconciliation.d.ts +17 -0
  60. package/dist/types/harness-control-plane/phase-rollup.d.ts +4 -7
  61. package/dist/types/harness-control-plane/receipt-ingest.d.ts +5 -0
  62. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  63. package/dist/types/hindsight/state.d.ts +11 -4
  64. package/dist/types/lsp/lspmux.d.ts +5 -3
  65. package/dist/types/lsp/path-trust.d.ts +1 -0
  66. package/dist/types/main.d.ts +4 -2
  67. package/dist/types/memories/storage.d.ts +1 -0
  68. package/dist/types/modes/action-registry.d.ts +29 -0
  69. package/dist/types/modes/components/chrome.d.ts +14 -0
  70. package/dist/types/modes/components/command-palette.d.ts +27 -0
  71. package/dist/types/modes/components/custom-provider-wizard.d.ts +1 -0
  72. package/dist/types/modes/components/index.d.ts +2 -1
  73. package/dist/types/modes/components/pet-selector.d.ts +1 -1
  74. package/dist/types/modes/components/plan-preview-overlay.d.ts +43 -0
  75. package/dist/types/modes/components/queue-mode-selector.d.ts +1 -1
  76. package/dist/types/modes/components/queue-pane.d.ts +12 -0
  77. package/dist/types/modes/components/read-tool-group.d.ts +6 -0
  78. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -4
  79. package/dist/types/modes/components/sessions-dashboard.d.ts +32 -0
  80. package/dist/types/modes/components/settings-selector.d.ts +1 -1
  81. package/dist/types/modes/components/show-images-selector.d.ts +1 -1
  82. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  83. package/dist/types/modes/components/status-line.d.ts +1 -94
  84. package/dist/types/modes/components/tasks-pane.d.ts +15 -0
  85. package/dist/types/modes/components/theme-selector.d.ts +1 -1
  86. package/dist/types/modes/components/tool-execution.d.ts +15 -0
  87. package/dist/types/modes/components/tool-status-header.d.ts +111 -0
  88. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +42 -0
  89. package/dist/types/modes/controllers/goal-mode-controller.d.ts +62 -0
  90. package/dist/types/modes/controllers/input-controller.d.ts +8 -1
  91. package/dist/types/modes/controllers/mode-gate.d.ts +8 -0
  92. package/dist/types/modes/controllers/plan-mode-controller.d.ts +65 -0
  93. package/dist/types/modes/controllers/selector-controller.d.ts +15 -3
  94. package/dist/types/modes/controllers/stt-controller.d.ts +26 -0
  95. package/dist/types/modes/interactive-mode.d.ts +19 -17
  96. package/dist/types/modes/jobs-observer.d.ts +2 -0
  97. package/dist/types/modes/prompt-action-autocomplete.d.ts +4 -0
  98. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -11
  99. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +20 -2
  100. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +31 -0
  101. package/dist/types/modes/tasks-aggregator.d.ts +39 -0
  102. package/dist/types/modes/transcript-item-registry.d.ts +60 -0
  103. package/dist/types/modes/types.d.ts +20 -13
  104. package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
  105. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  106. package/dist/types/sdk/broker/broker.d.ts +78 -12
  107. package/dist/types/sdk/broker/lifecycle-codec.d.ts +4 -0
  108. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +15 -1
  109. package/dist/types/sdk/broker/lifecycle.d.ts +6 -2
  110. package/dist/types/sdk/broker/process-incarnation.d.ts +2 -0
  111. package/dist/types/sdk/broker/session-index.d.ts +0 -1
  112. package/dist/types/sdk/broker/state-version.d.ts +2 -0
  113. package/dist/types/sdk/bus/chat-effect-journal.d.ts +1 -0
  114. package/dist/types/sdk/bus/config-commands.d.ts +1 -1
  115. package/dist/types/sdk/bus/config.d.ts +11 -0
  116. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  117. package/dist/types/sdk/bus/html-format.d.ts +3 -2
  118. package/dist/types/sdk/bus/index.d.ts +29 -0
  119. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  120. package/dist/types/sdk/bus/notification-service.d.ts +20 -0
  121. package/dist/types/sdk/bus/rate-limit-pool.d.ts +13 -2
  122. package/dist/types/sdk/bus/recent-activity.d.ts +21 -12
  123. package/dist/types/sdk/bus/rich-document.d.ts +7 -0
  124. package/dist/types/sdk/bus/rich-render.d.ts +18 -41
  125. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
  126. package/dist/types/sdk/bus/telegram-daemon.d.ts +46 -3
  127. package/dist/types/sdk/bus/threaded-inbound.d.ts +1 -1
  128. package/dist/types/sdk/bus/threaded-render.d.ts +13 -0
  129. package/dist/types/sdk/bus/topic-registry.d.ts +24 -2
  130. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  131. package/dist/types/sdk/host/host.d.ts +2 -0
  132. package/dist/types/sdk/index.d.ts +1 -0
  133. package/dist/types/sdk/session-directory.d.ts +56 -0
  134. package/dist/types/sdk/session.d.ts +2 -0
  135. package/dist/types/session/agent-session.d.ts +82 -47
  136. package/dist/types/session/agent-storage.d.ts +7 -0
  137. package/dist/types/session/fallback-chain-controller.d.ts +10 -2
  138. package/dist/types/session/internal/managed-session-scope.d.ts +102 -0
  139. package/dist/types/session/internal/managed-session-storage.d.ts +38 -0
  140. package/dist/types/session/messages.d.ts +16 -0
  141. package/dist/types/session/session-manager-internal.d.ts +14 -0
  142. package/dist/types/session/session-manager.d.ts +82 -12
  143. package/dist/types/session/session-storage.d.ts +43 -3
  144. package/dist/types/session/volatile-context-pruning.d.ts +15 -0
  145. package/dist/types/skill-state/{deep-interview-mutation-guard.d.ts → workflow-mutation-guard.d.ts} +24 -8
  146. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -3
  147. package/dist/types/slash-commands/builtin-registry.d.ts +4 -1
  148. package/dist/types/system-prompt.d.ts +2 -11
  149. package/dist/types/task/executor.d.ts +3 -1
  150. package/dist/types/thinking.d.ts +7 -0
  151. package/dist/types/tool-discovery/tool-index.d.ts +1 -50
  152. package/dist/types/tools/ask.d.ts +64 -2
  153. package/dist/types/tools/bash.d.ts +3 -0
  154. package/dist/types/tools/browser/dead-tab-recovery.d.ts +24 -0
  155. package/dist/types/tools/browser/tab-supervisor.d.ts +21 -0
  156. package/dist/types/tools/cron.d.ts +2 -0
  157. package/dist/types/tools/fetch.d.ts +1 -0
  158. package/dist/types/tools/index.d.ts +30 -15
  159. package/dist/types/tools/output-meta.d.ts +1 -0
  160. package/dist/types/tools/read.d.ts +5 -0
  161. package/dist/types/tools/search-tool-bm25.d.ts +3 -4
  162. package/dist/types/tools/subagent.d.ts +1 -0
  163. package/dist/types/utils/event-bus.d.ts +1 -1
  164. package/dist/types/utils/file-mentions.d.ts +3 -3
  165. package/dist/types/web/search/index.d.ts +3 -1
  166. package/package.json +21 -9
  167. package/scripts/build-sdk-package-smoke.ts +34 -1
  168. package/scripts/generate-hotkeys-docs.ts +74 -0
  169. package/scripts/generate-sdk-operation-inventory.ts +64 -20
  170. package/scripts/verify-gjc-sdk-canonicalization.ts +182 -2
  171. package/src/async/job-manager.ts +49 -6
  172. package/src/cli/config-cli.ts +75 -2
  173. package/src/cli/launch-disposition.ts +42 -0
  174. package/src/cli/notify-cli.ts +92 -26
  175. package/src/cli/session-picker.ts +20 -3
  176. package/src/cli/setup-cli.ts +1 -1
  177. package/src/cli.ts +1 -1
  178. package/src/commands/config.ts +1 -1
  179. package/src/commands/harness.ts +34 -106
  180. package/src/commands/launch.ts +6 -4
  181. package/src/commands/mcp-serve.ts +85 -8
  182. package/src/commands/mcp.ts +2 -2
  183. package/src/commands/notify.ts +8 -4
  184. package/src/commands/sdk.ts +61 -40
  185. package/src/commands/ultragoal.ts +4 -1
  186. package/src/config/atomic-yaml-patch.ts +80 -13
  187. package/src/config/file-lock.ts +105 -43
  188. package/src/config/keybindings.ts +64 -20
  189. package/src/config/model-auth.ts +9 -0
  190. package/src/config/model-bindings-applier.ts +139 -0
  191. package/src/config/model-discovery-manager.ts +224 -0
  192. package/src/config/model-equivalence.ts +66 -0
  193. package/src/config/model-registry.ts +169 -302
  194. package/src/config/model-resolver.ts +361 -369
  195. package/src/config/models-config-schema.ts +5 -4
  196. package/src/config/settings-schema.ts +214 -12
  197. package/src/config/settings.ts +458 -482
  198. package/src/coordinator-mcp/policy.ts +76 -11
  199. package/src/coordinator-mcp/question-gate-codec.ts +274 -0
  200. package/src/coordinator-mcp/question-state.ts +778 -0
  201. package/src/coordinator-mcp/server.ts +1702 -238
  202. package/src/debug/index.ts +1 -1
  203. package/src/defaults/gjc/skills/deep-interview/SKILL.md +13 -1
  204. package/src/defaults/gjc/skills/team/SKILL.md +16 -0
  205. package/src/discovery/helpers.ts +1 -50
  206. package/src/edit/index.ts +64 -0
  207. package/src/eval/py/env.ts +26 -0
  208. package/src/eval/py/kernel.ts +5 -3
  209. package/src/extensibility/extensions/loader.ts +9 -1
  210. package/src/extensibility/extensions/runner.ts +12 -3
  211. package/src/extensibility/extensions/types.ts +39 -1
  212. package/src/extensibility/gjc-plugins/installer.ts +16 -16
  213. package/src/extensibility/hooks/runner.ts +2 -2
  214. package/src/extensibility/runtime-skill-discovery.ts +33 -13
  215. package/src/extensibility/shared-events.ts +2 -0
  216. package/src/gjc-runtime/deep-interview-recorder.ts +63 -3
  217. package/src/gjc-runtime/deep-interview-runtime.ts +76 -27
  218. package/src/gjc-runtime/deep-interview-state.ts +317 -5
  219. package/src/gjc-runtime/goal-mode-request.ts +55 -8
  220. package/src/gjc-runtime/launch-tmux.ts +2 -5
  221. package/src/gjc-runtime/launch-worktree.ts +5 -5
  222. package/src/gjc-runtime/linux-proc.ts +71 -0
  223. package/src/gjc-runtime/ralplan-runtime.ts +74 -90
  224. package/src/gjc-runtime/session-resolution.ts +18 -3
  225. package/src/gjc-runtime/session-state-sidecar.ts +6 -107
  226. package/src/gjc-runtime/state-migrations.ts +1 -1
  227. package/src/gjc-runtime/state-runtime.ts +327 -249
  228. package/src/gjc-runtime/state-writer.ts +36 -40
  229. package/src/gjc-runtime/team-launch.ts +319 -0
  230. package/src/gjc-runtime/team-notify.ts +326 -0
  231. package/src/gjc-runtime/team-runtime.ts +1099 -1482
  232. package/src/gjc-runtime/team-store.ts +828 -0
  233. package/src/gjc-runtime/team-workers.ts +962 -0
  234. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +2 -11
  235. package/src/gjc-runtime/tmux-owner-isolation.ts +14 -9
  236. package/src/gjc-runtime/tmux-sessions.ts +4 -21
  237. package/src/gjc-runtime/ultragoal-change-set.ts +172 -0
  238. package/src/gjc-runtime/ultragoal-evidence.ts +874 -0
  239. package/src/gjc-runtime/ultragoal-guard.ts +29 -5
  240. package/src/gjc-runtime/ultragoal-pipeline.ts +306 -0
  241. package/src/gjc-runtime/ultragoal-runtime.ts +84 -1311
  242. package/src/gjc-runtime/workflow-cli-common.ts +30 -0
  243. package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
  244. package/src/gjc-runtime/workflow-manifest.ts +18 -0
  245. package/src/goals/continuation-timeout-guard.ts +79 -0
  246. package/src/goals/runtime.ts +7 -5
  247. package/src/goals/state.ts +18 -0
  248. package/src/harness-control-plane/lifecycle-reconciliation.ts +64 -0
  249. package/src/harness-control-plane/phase-rollup.ts +4 -7
  250. package/src/harness-control-plane/receipt-ingest.ts +5 -0
  251. package/src/harness-control-plane/receipt-spool.ts +51 -7
  252. package/src/harness-control-plane/session-lease.ts +12 -130
  253. package/src/hindsight/backend.ts +26 -27
  254. package/src/hindsight/state.ts +101 -32
  255. package/src/hooks/skill-state.ts +17 -11
  256. package/src/internal-urls/docs-index.generated.ts +21 -18
  257. package/src/lsp/client.ts +1 -1
  258. package/src/lsp/config.ts +145 -26
  259. package/src/lsp/index.ts +1 -1
  260. package/src/lsp/lspmux.ts +39 -10
  261. package/src/lsp/path-trust.ts +81 -0
  262. package/src/main.ts +127 -30
  263. package/src/memories/index.ts +1 -0
  264. package/src/memories/storage.ts +6 -2
  265. package/src/modes/action-registry.ts +165 -0
  266. package/src/modes/components/chrome.ts +40 -0
  267. package/src/modes/components/command-palette.ts +168 -0
  268. package/src/modes/components/custom-editor.ts +1 -1
  269. package/src/modes/components/custom-provider-wizard.ts +51 -14
  270. package/src/modes/components/index.ts +2 -1
  271. package/src/modes/components/pet-selector.ts +11 -17
  272. package/src/modes/components/plan-preview-overlay.ts +295 -0
  273. package/src/modes/components/queue-mode-selector.ts +10 -27
  274. package/src/modes/components/queue-pane.ts +79 -0
  275. package/src/modes/components/read-tool-group.ts +13 -0
  276. package/src/modes/components/session-observer-overlay.ts +207 -772
  277. package/src/modes/components/sessions-dashboard.ts +181 -0
  278. package/src/modes/components/settings-selector.ts +1 -1
  279. package/src/modes/components/show-images-selector.ts +10 -24
  280. package/src/modes/components/status-line/types.ts +1 -1
  281. package/src/modes/components/status-line.ts +1 -969
  282. package/src/modes/components/tasks-pane.ts +73 -0
  283. package/src/modes/components/theme-selector.ts +11 -31
  284. package/src/modes/components/tool-execution.ts +22 -0
  285. package/src/modes/components/tool-status-header.ts +1056 -0
  286. package/src/modes/components/transcript-viewer-overlay.ts +384 -0
  287. package/src/modes/controllers/btw-controller.ts +1 -0
  288. package/src/modes/controllers/event-controller.ts +3 -2
  289. package/src/modes/controllers/extension-ui-controller.ts +24 -1
  290. package/src/modes/controllers/goal-mode-controller.ts +474 -0
  291. package/src/modes/controllers/input-controller.ts +764 -229
  292. package/src/modes/controllers/mode-gate.ts +24 -0
  293. package/src/modes/controllers/plan-mode-controller.ts +517 -0
  294. package/src/modes/controllers/selector-controller.ts +225 -10
  295. package/src/modes/controllers/stt-controller.ts +125 -0
  296. package/src/modes/interactive-mode.ts +289 -1150
  297. package/src/modes/jobs-observer.ts +3 -0
  298. package/src/modes/prompt-action-autocomplete.ts +7 -0
  299. package/src/modes/runtime-init.ts +8 -0
  300. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -106
  301. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +71 -16
  302. package/src/modes/shared/agent-wire/workflow-gate-types.ts +177 -0
  303. package/src/modes/tasks-aggregator.ts +199 -0
  304. package/src/modes/transcript-item-registry.ts +229 -0
  305. package/src/modes/types.ts +45 -18
  306. package/src/modes/utils/ui-helpers.ts +17 -9
  307. package/src/prompts/agent-fragments/ralplan-persistence.md +6 -0
  308. package/src/prompts/agent-fragments/restricted-bash.md +1 -0
  309. package/src/prompts/agents/architect.md +2 -8
  310. package/src/prompts/agents/critic.md +2 -8
  311. package/src/prompts/agents/executor.md +3 -0
  312. package/src/prompts/agents/planner.md +4 -12
  313. package/src/prompts/system/custom-system-prompt.md +1 -23
  314. package/src/prompts/system/plan-mode-approved.md +8 -0
  315. package/src/prompts/system/system-prompt.md +17 -96
  316. package/src/prompts/tools/bash.md +4 -4
  317. package/src/prompts/tools/browser.md +15 -52
  318. package/src/prompts/tools/find.md +2 -2
  319. package/src/prompts/tools/hashline.md +13 -55
  320. package/src/prompts/tools/read.md +3 -6
  321. package/src/prompts/tools/search-tool-bm25.md +0 -2
  322. package/src/prompts/tools/search.md +2 -4
  323. package/src/prompts/tools/skill-discovery.md +1 -2
  324. package/src/prompts/tools/task.md +1 -1
  325. package/src/runtime-mcp/manager.ts +1 -1
  326. package/src/sdk/broker/broker.ts +206 -78
  327. package/src/sdk/broker/discovery.ts +1 -1
  328. package/src/sdk/broker/ensure.ts +3 -1
  329. package/src/sdk/broker/lifecycle-codec.ts +11 -0
  330. package/src/sdk/broker/lifecycle-ledger.ts +371 -74
  331. package/src/sdk/broker/lifecycle.ts +1399 -142
  332. package/src/sdk/broker/process-incarnation.ts +5 -13
  333. package/src/sdk/broker/session-index.ts +145 -56
  334. package/src/sdk/broker/state-version.ts +21 -0
  335. package/src/sdk/broker/transport.ts +1 -1
  336. package/src/sdk/bus/chat-daemon-cli.ts +2 -0
  337. package/src/sdk/bus/chat-daemon-control.ts +8 -28
  338. package/src/sdk/bus/chat-effect-journal.ts +10 -2
  339. package/src/sdk/bus/config-commands.ts +5 -4
  340. package/src/sdk/bus/config.ts +98 -0
  341. package/src/sdk/bus/conversation-store.ts +5 -29
  342. package/src/sdk/bus/daemon-paths.ts +2 -0
  343. package/src/sdk/bus/html-format.ts +20 -3
  344. package/src/sdk/bus/index.ts +782 -94
  345. package/src/sdk/bus/lifecycle-control-runtime.ts +91 -33
  346. package/src/sdk/bus/natives-augment.d.ts +26 -1
  347. package/src/sdk/bus/notification-service.ts +168 -20
  348. package/src/sdk/bus/rate-limit-pool.ts +58 -14
  349. package/src/sdk/bus/recent-activity.ts +188 -74
  350. package/src/sdk/bus/rich-document.ts +258 -0
  351. package/src/sdk/bus/rich-render.ts +62 -70
  352. package/src/sdk/bus/telegram-daemon-cli.ts +15 -59
  353. package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
  354. package/src/sdk/bus/telegram-daemon.ts +1389 -237
  355. package/src/sdk/bus/threaded-inbound.ts +8 -3
  356. package/src/sdk/bus/threaded-render.ts +57 -1
  357. package/src/sdk/bus/topic-registry.ts +120 -13
  358. package/src/sdk/host/control/dispatch.ts +14 -2
  359. package/src/sdk/host/control/operations.ts +6 -1
  360. package/src/sdk/host/host.ts +44 -2
  361. package/src/sdk/host/query/handlers.ts +20 -22
  362. package/src/sdk/host/query/revision-store.ts +80 -57
  363. package/src/sdk/index.ts +1 -0
  364. package/src/sdk/protocol/operation-inventory.generated.json +943 -1907
  365. package/src/sdk/session-directory.ts +141 -0
  366. package/src/sdk/session.ts +462 -480
  367. package/src/session/agent-session.ts +1700 -583
  368. package/src/session/agent-storage.ts +18 -0
  369. package/src/session/cache-economics.ts +25 -8
  370. package/src/session/fallback-chain-controller.ts +53 -3
  371. package/src/session/internal/managed-session-scope.ts +2226 -0
  372. package/src/session/internal/managed-session-storage.ts +453 -0
  373. package/src/session/messages.ts +91 -2
  374. package/src/session/session-manager-internal.ts +33 -0
  375. package/src/session/session-manager.ts +1112 -333
  376. package/src/session/session-storage.ts +444 -39
  377. package/src/session/volatile-context-pruning.ts +78 -0
  378. package/src/setup/credential-auto-import.ts +2 -1
  379. package/src/setup/hermes/templates/operator-instructions.v1.md +1 -1
  380. package/src/setup/provider-onboarding.ts +36 -61
  381. package/src/setup/provider-presets.json +5 -0
  382. package/src/skill-state/active-state.ts +3 -13
  383. package/src/skill-state/{deep-interview-mutation-guard.ts → workflow-mutation-guard.ts} +47 -29
  384. package/src/skill-state/workflow-state-contract.ts +6 -41
  385. package/src/slash-commands/builtin-registry.ts +46 -11
  386. package/src/system-prompt.ts +37 -44
  387. package/src/task/agents.ts +19 -1
  388. package/src/task/executor.ts +90 -6
  389. package/src/task/index.ts +36 -21
  390. package/src/thinking.ts +21 -6
  391. package/src/tool-discovery/tool-index.ts +2 -134
  392. package/src/tools/ask.ts +157 -21
  393. package/src/tools/ast-edit.ts +1 -1
  394. package/src/tools/bash.ts +38 -0
  395. package/src/tools/browser/dead-tab-recovery.ts +78 -0
  396. package/src/tools/browser/tab-supervisor.ts +282 -36
  397. package/src/tools/computer.ts +10 -3
  398. package/src/tools/cron.ts +30 -16
  399. package/src/tools/eval.ts +1 -2
  400. package/src/tools/fetch.ts +11 -4
  401. package/src/tools/index.ts +76 -24
  402. package/src/tools/output-meta.ts +19 -7
  403. package/src/tools/read.ts +219 -64
  404. package/src/tools/resource-gc.ts +2 -2
  405. package/src/tools/search-tool-bm25.ts +11 -74
  406. package/src/tools/subagent.ts +43 -2
  407. package/src/utils/event-bus.ts +9 -4
  408. package/src/utils/file-mentions.ts +4 -4
  409. package/src/web/search/index.ts +3 -3
  410. package/dist/types/discovery/plugin-dir-roots.d.ts +0 -15
  411. package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +0 -7
  412. package/src/discovery/plugin-dir-roots.ts +0 -28
  413. package/src/runtime-mcp/discoverable-tool-metadata.ts +0 -25
@@ -11,6 +11,11 @@ import {
11
11
  COORDINATOR_MCP_TOOL_NAMES,
12
12
  type CoordinatorToolName,
13
13
  } from "../coordinator/contract";
14
+ import { readLinuxProcStartTimeSync } from "../gjc-runtime/linux-proc";
15
+ import type { WorkflowGate, WorkflowGateQueryRecord } from "../modes/shared/agent-wire/workflow-gate-types";
16
+ import type { BrokerDiscovery } from "../sdk/broker/discovery";
17
+ import { type EnsureBrokerSettings, ensureBroker } from "../sdk/broker/ensure";
18
+ import { UnsupportedStateVersionError } from "../sdk/broker/state-version";
14
19
  import { SdkClient, SdkClientError } from "../sdk/client/client";
15
20
  import { readSdkBrokerDiscovery } from "../sdk/client/discovery";
16
21
  import {
@@ -23,9 +28,42 @@ import {
23
28
  assertCoordinatorWorkdir,
24
29
  buildCoordinatorMcpConfig,
25
30
  type CoordinatorMcpConfig,
26
- coordinatorNamespacePath,
27
31
  requireCoordinatorMutation,
32
+ safeOpenCoordinatorArtifact,
28
33
  } from "./policy";
34
+ import {
35
+ answerBindingMatches,
36
+ type CoordinatorQuestionDiagnosticPublicV1,
37
+ type CoordinatorQuestionPublicV1,
38
+ createAnswerBinding,
39
+ decodeAskGateV1,
40
+ type ListQuestionsSuccessV1,
41
+ type PublicReason,
42
+ projectAskGateQuestion,
43
+ translateCoordinatorAskAnswer,
44
+ validateCoordinatorAskAnswer,
45
+ } from "./question-gate-codec";
46
+ import {
47
+ advanceCreationReceipt,
48
+ advanceDeletion,
49
+ bindCreationRequest,
50
+ type CanonicalCreateIntentV1,
51
+ type CanonicalReportSnapshotV1,
52
+ type CanonicalSessionSnapshotV1,
53
+ type CoordinatorSessionTransactionV1,
54
+ claimCreationRequest,
55
+ commitCreationWal,
56
+ coordinatorStatePaths,
57
+ createSessionTransaction,
58
+ deterministicOutboxId,
59
+ initializeCoordinatorNamespace,
60
+ recordDeletionIntent,
61
+ repairProjections,
62
+ withAdmittedSessionTransaction,
63
+ withNamespaceRegistry,
64
+ withSessionTransaction,
65
+ } from "./question-state";
66
+
29
67
  import { createSessionReaper, type ReapableSession, type SessionReaper } from "./session-reaper";
30
68
 
31
69
  export type { CoordinatorToolName };
@@ -56,6 +94,41 @@ function sinkErrorCode(error: unknown): string | undefined {
56
94
  return undefined;
57
95
  }
58
96
  }
97
+
98
+ type CoordinatorBrokerStage = "ensure" | "read" | "connect" | "request" | "close";
99
+
100
+ function toCoordinatorBrokerError(stage: CoordinatorBrokerStage, error: unknown): SdkClientError {
101
+ if (stage === "request" && error instanceof SdkClientError) return error;
102
+ if (stage === "ensure") {
103
+ if (error instanceof AggregateError)
104
+ return new SdkClientError(
105
+ "broker_cleanup_unverified",
106
+ "SDK broker bootstrap failed and cleanup was not verified.",
107
+ );
108
+ if (error instanceof UnsupportedStateVersionError)
109
+ return new SdkClientError(
110
+ "broker_discovery_unsupported",
111
+ "SDK broker discovery state version is unsupported.",
112
+ );
113
+ if (sinkErrorCode(error) === "EACCES" || sinkErrorCode(error) === "EPERM")
114
+ return new SdkClientError("broker_discovery_access_denied", "SDK broker discovery cannot be accessed.");
115
+ return new SdkClientError("broker_bootstrap_failed", "SDK broker bootstrap failed.");
116
+ }
117
+ if (stage === "read") {
118
+ if (error instanceof UnsupportedStateVersionError)
119
+ return new SdkClientError(
120
+ "broker_discovery_unsupported",
121
+ "SDK broker discovery state version is unsupported.",
122
+ );
123
+ if (sinkErrorCode(error) === "EACCES" || sinkErrorCode(error) === "EPERM")
124
+ return new SdkClientError("broker_discovery_access_denied", "SDK broker discovery cannot be accessed.");
125
+ return new SdkClientError("broker_discovery_unavailable", "SDK broker discovery cannot be read.");
126
+ }
127
+ return new SdkClientError(
128
+ stage === "request" ? "broker_request_unavailable" : "broker_transport_unavailable",
129
+ stage === "request" ? "SDK broker request is unavailable." : "SDK broker transport is unavailable.",
130
+ );
131
+ }
59
132
  interface CoordinatorFinalResponse {
60
133
  text: string | null;
61
134
  format: "markdown";
@@ -78,6 +151,8 @@ interface RuntimeSessionStatePayload extends CoordinatorSessionState {
78
151
 
79
152
  interface CoordinatorServices {
80
153
  connectSdk?: (url: string, token: string) => Promise<SdkClient>;
154
+ ensureBroker?: (settings: EnsureBrokerSettings) => Promise<BrokerDiscovery>;
155
+ readSdkBrokerDiscovery?: (agentDir: string) => Promise<BrokerDiscovery | null>;
81
156
  getAgentDir?: () => string;
82
157
  resolveModelProfiles?: CoordinatorModelProfileLoader;
83
158
  canonicalizePath?: (value: string) => Promise<string>;
@@ -385,11 +460,20 @@ function toolSchema(name: CoordinatorToolName): {
385
460
  session_id: sessionId,
386
461
  turn_id: { type: "string" },
387
462
  question_id: { type: "string" },
463
+ answer_binding: { type: "string" },
388
464
  answer: {},
389
465
  idempotency_key: idempotencyKey,
390
466
  allow_mutation: allowMutation,
391
467
  },
392
- required: ["session_id", "question_id", "answer", "idempotency_key", "allow_mutation"],
468
+ required: [
469
+ "session_id",
470
+ "turn_id",
471
+ "question_id",
472
+ "answer_binding",
473
+ "answer",
474
+ "idempotency_key",
475
+ "allow_mutation",
476
+ ],
393
477
  },
394
478
  };
395
479
  }
@@ -439,7 +523,12 @@ function toolSchema(name: CoordinatorToolName): {
439
523
  return {
440
524
  name,
441
525
  description: "List bounded structured questions for coordinator coordination.",
442
- inputSchema: { type: "object", properties: { session_id: sessionId, status: { type: "string" } } },
526
+ inputSchema: {
527
+ type: "object",
528
+ properties: { session_id: sessionId, turn_id: { type: "string" }, status: { type: "string" } },
529
+
530
+ required: ["session_id"],
531
+ },
443
532
  };
444
533
  }
445
534
  if (name === "gjc_coordinator_list_artifacts") {
@@ -816,10 +905,6 @@ function publicSdkAcknowledgement(result: RuntimePromptAcknowledgement): Record<
816
905
  };
817
906
  }
818
907
 
819
- function publicSdkAccepted(result: unknown): Record<string, unknown> {
820
- return acknowledgementPayload(result)?.accepted === true ? { accepted: true } : {};
821
- }
822
-
823
908
  async function listJsonFiles(dir: string): Promise<unknown[]> {
824
909
  try {
825
910
  const entries = await fs.readdir(dir);
@@ -901,6 +986,22 @@ function publicCoordinatorSession(session: Record<string, unknown>): Record<stri
901
986
  return result;
902
987
  }
903
988
 
989
+ function publicCoordinatorStatusSession(session: Record<string, unknown>): Record<string, unknown> {
990
+ const { cwd: _cwd, ...safe } = publicCoordinatorSession(session);
991
+ return safe;
992
+ }
993
+
994
+ function capabilityFreeStatusValue(value: unknown): unknown {
995
+ if (Array.isArray(value)) return value.map(capabilityFreeStatusValue);
996
+ if (!value || typeof value !== "object") return value;
997
+ const result: Record<string, unknown> = {};
998
+ for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
999
+ if (/^(?:answer_binding|cwd|path|payload_ref|evidence_paths|artifact_path|state_root)$/i.test(key)) continue;
1000
+ result[key] = capabilityFreeStatusValue(child);
1001
+ }
1002
+ return result;
1003
+ }
1004
+
904
1005
  function publicLifecycleReceipt(result: Record<string, unknown>, sessionId: string): Record<string, unknown> {
905
1006
  const receipt: Record<string, unknown> = { session_id: sessionId };
906
1007
  const worktree = asRecord(result.worktree);
@@ -1242,19 +1343,27 @@ async function writeSessionStateUnlocked(
1242
1343
  live?: boolean | null;
1243
1344
  reason?: string | null;
1244
1345
  source?: CoordinatorSessionState["source"];
1346
+ overwrite?: boolean;
1245
1347
  } = {},
1246
1348
  ): Promise<CoordinatorSessionState> {
1247
- const previous = await readSessionState(namespaceDir, sessionId);
1349
+ const previous = options.overwrite ? null : await readSessionState(namespaceDir, sessionId);
1350
+ const hasCurrentTurn = Object.hasOwn(options, "currentTurnId");
1351
+ const hasLastTurn = Object.hasOwn(options, "lastTurnId");
1352
+ const hasLive = Object.hasOwn(options, "live");
1248
1353
  const payload: CoordinatorSessionState = {
1249
1354
  schema_version: 1,
1250
1355
  session_id: sessionId,
1251
1356
  state,
1252
1357
  ready_for_input: state === "ready_for_input" || state === "completed",
1253
- current_turn_id: options.currentTurnId ?? (state === "running" ? (previous?.current_turn_id ?? null) : null),
1254
- last_turn_id: options.lastTurnId ?? previous?.last_turn_id ?? null,
1358
+ current_turn_id: hasCurrentTurn
1359
+ ? (options.currentTurnId ?? null)
1360
+ : state === "running"
1361
+ ? (previous?.current_turn_id ?? null)
1362
+ : null,
1363
+ last_turn_id: hasLastTurn ? (options.lastTurnId ?? null) : (previous?.last_turn_id ?? null),
1255
1364
  updated_at: new Date().toISOString(),
1256
1365
  source: options.source ?? "coordinator",
1257
- live: options.live ?? previous?.live ?? null,
1366
+ live: hasLive ? (options.live ?? null) : (previous?.live ?? null),
1258
1367
  reason: options.reason ?? null,
1259
1368
  };
1260
1369
  await writeJsonFile(sessionStateFile(namespaceDir, sessionId), payload);
@@ -1290,17 +1399,7 @@ interface SessionStateLockOwner {
1290
1399
  }
1291
1400
 
1292
1401
  function processStartTime(pid: number): string | null {
1293
- try {
1294
- const stat = nodeFs.readFileSync(`/proc/${pid}/stat`, "utf8");
1295
- const close = stat.lastIndexOf(")");
1296
- const fields = stat
1297
- .slice(close + 1)
1298
- .trim()
1299
- .split(/\s+/);
1300
- return fields[19] ?? null;
1301
- } catch {
1302
- return null;
1303
- }
1402
+ return readLinuxProcStartTimeSync(pid);
1304
1403
  }
1305
1404
 
1306
1405
  function validLockOwner(value: unknown): value is SessionStateLockOwner {
@@ -1407,6 +1506,7 @@ async function writeSessionState(
1407
1506
  live?: boolean | null;
1408
1507
  reason?: string | null;
1409
1508
  source?: CoordinatorSessionState["source"];
1509
+ overwrite?: boolean;
1410
1510
  } = {},
1411
1511
  ): Promise<CoordinatorSessionState> {
1412
1512
  const file = sessionStateFile(namespaceDir, sessionId);
@@ -1416,11 +1516,7 @@ async function writeSessionState(
1416
1516
  );
1417
1517
  }
1418
1518
 
1419
- async function markTurnFailedForUnavailableSession(
1420
- namespaceDir: string,
1421
- turn: TurnRecord,
1422
- reason: string,
1423
- ): Promise<TurnRecord> {
1519
+ async function markTurnFailedForUnavailableSession(turn: TurnRecord, reason: string): Promise<TurnRecord> {
1424
1520
  const timestamp = new Date().toISOString();
1425
1521
  const failed: TurnRecord = {
1426
1522
  ...turn,
@@ -1438,18 +1534,10 @@ async function markTurnFailedForUnavailableSession(
1438
1534
  updated_at: timestamp,
1439
1535
  completed_at: timestamp,
1440
1536
  };
1441
- await writeTurnRecord(namespaceDir, failed);
1442
- await clearActiveTurn(namespaceDir, failed);
1443
- await writeSessionState(namespaceDir, failed.session_id, "stale", {
1444
- lastTurnId: failed.turn_id,
1445
- live: false,
1446
- reason,
1447
- });
1448
1537
  return failed;
1449
1538
  }
1450
1539
 
1451
1540
  async function markTurnTerminalFromSessionState(
1452
- namespaceDir: string,
1453
1541
  turn: TurnRecord,
1454
1542
  sessionState: CoordinatorSessionState,
1455
1543
  ): Promise<TurnRecord> {
@@ -1493,13 +1581,6 @@ async function markTurnTerminalFromSessionState(
1493
1581
  updated_at: timestamp,
1494
1582
  completed_at: timestamp,
1495
1583
  };
1496
- await writeTurnRecord(namespaceDir, resolved);
1497
- await clearActiveTurn(namespaceDir, resolved);
1498
- await writeSessionState(namespaceDir, resolved.session_id, sessionState.state, {
1499
- lastTurnId: resolved.turn_id,
1500
- live: sessionState.live,
1501
- reason: sessionState.reason,
1502
- });
1503
1584
  return resolved;
1504
1585
  }
1505
1586
 
@@ -1569,11 +1650,7 @@ function turnAwaitingRuntimeAckExpired(turn: TurnRecord, nowMs: number, ackTimeo
1569
1650
  return Number.isFinite(deliveredMs) && nowMs - deliveredMs >= ackTimeoutMs;
1570
1651
  }
1571
1652
 
1572
- async function markTurnFailedForUnacknowledgedDelivery(
1573
- namespaceDir: string,
1574
- turn: TurnRecord,
1575
- ackTimeoutMs: number,
1576
- ): Promise<TurnRecord> {
1653
+ async function markTurnFailedForUnacknowledgedDelivery(turn: TurnRecord, ackTimeoutMs: number): Promise<TurnRecord> {
1577
1654
  const timestamp = new Date().toISOString();
1578
1655
  const message = `Tmux key delivery succeeded, but the GJC runtime did not acknowledge the prompt or emit turn_start within ${ackTimeoutMs}ms. The turn never started; stop waiting and inspect/retry the coordinator session.`;
1579
1656
  const failed: TurnRecord = {
@@ -1618,13 +1695,6 @@ async function markTurnFailedForUnacknowledgedDelivery(
1618
1695
  updated_at: timestamp,
1619
1696
  completed_at: timestamp,
1620
1697
  };
1621
- await writeTurnRecord(namespaceDir, failed);
1622
- await clearActiveTurn(namespaceDir, failed);
1623
- await writeSessionState(namespaceDir, failed.session_id, "stale", {
1624
- lastTurnId: failed.turn_id,
1625
- live: failed.liveness.live,
1626
- reason: PROMPT_ACK_TIMEOUT_REASON,
1627
- });
1628
1698
  return failed;
1629
1699
  }
1630
1700
 
@@ -1639,7 +1709,7 @@ async function reconcileRuntimeAcknowledgement(
1639
1709
  return await markTurnAcknowledgedFromRuntimeState(namespaceDir, turn, sessionState);
1640
1710
  }
1641
1711
  if (options.failOnTimeout && turnAwaitingRuntimeAckExpired(turn, Date.now(), ackTimeoutMs)) {
1642
- return await markTurnFailedForUnacknowledgedDelivery(namespaceDir, turn, ackTimeoutMs);
1712
+ return await markTurnFailedForUnacknowledgedDelivery(turn, ackTimeoutMs);
1643
1713
  }
1644
1714
  return turn;
1645
1715
  }
@@ -1813,19 +1883,18 @@ export async function readCoordinatorArtifact(
1813
1883
  ): Promise<Record<string, unknown>> {
1814
1884
  let handle: fs.FileHandle | null = null;
1815
1885
  try {
1816
- const resolved = await assertCoordinatorArtifactPath(config, args.path);
1817
- handle = await fs.open(resolved.path, "r");
1818
- const readLimit = resolved.byteCap + 1;
1886
+ handle = await safeOpenCoordinatorArtifact(config, args.path);
1887
+ const readLimit = config.artifactByteCap + 1;
1819
1888
  const buffer = Buffer.alloc(readLimit);
1820
1889
  const { bytesRead } = await handle.read(buffer, 0, readLimit, 0);
1821
- const boundedBytes = buffer.subarray(0, Math.min(bytesRead, resolved.byteCap));
1822
- const text = decodeUtf8WithinByteCap(boundedBytes, resolved.byteCap);
1890
+ const boundedBytes = buffer.subarray(0, Math.min(bytesRead, config.artifactByteCap));
1891
+ const text = decodeUtf8WithinByteCap(boundedBytes, config.artifactByteCap);
1823
1892
  return {
1824
1893
  ok: true,
1825
- path: resolved.path,
1894
+ path: typeof args.path === "string" ? path.resolve(args.path) : "",
1826
1895
  text,
1827
1896
  bytes: Buffer.byteLength(text),
1828
- truncated: bytesRead > resolved.byteCap,
1897
+ truncated: bytesRead > config.artifactByteCap,
1829
1898
  };
1830
1899
  } catch (error) {
1831
1900
  return {
@@ -1844,7 +1913,436 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1844
1913
  const services = options.services ?? {};
1845
1914
  const platform = options.platform ?? process.platform;
1846
1915
  const loadModelProfiles = services.resolveModelProfiles ?? loadCoordinatorModelProfiles;
1847
- const namespaceDir = coordinatorNamespacePath(config);
1916
+ const namespaceDir = path.join(
1917
+ config.stateRoot,
1918
+ config.namespace.profile ?? "unscoped-profile",
1919
+ config.namespace.repo ?? "unscoped-repo",
1920
+ );
1921
+ const questionPaths = coordinatorStatePaths(config.stateRoot, config.namespace.identity);
1922
+ let questionStateReady: Promise<void> | null = null;
1923
+
1924
+ function ensureQuestionStateReady(): Promise<void> {
1925
+ questionStateReady ??= initializeCoordinatorNamespace(questionPaths);
1926
+ return questionStateReady;
1927
+ }
1928
+
1929
+ function creationDigests(
1930
+ tool: string,
1931
+ idempotencyKey: string,
1932
+ canonicalArgs: Record<string, unknown>,
1933
+ ): {
1934
+ keyDigest: string;
1935
+ requestDigest: string;
1936
+ } {
1937
+ return {
1938
+ keyDigest: createHash("sha256").update(`${tool}\0${idempotencyKey}`).digest("hex"),
1939
+ requestDigest: createHash("sha256")
1940
+ .update(canonicalJson({ tool, args: canonicalArgs }))
1941
+ .digest("hex"),
1942
+ };
1943
+ }
1944
+
1945
+ function canonicalCreationSnapshot(session: Record<string, unknown>): CanonicalSessionSnapshotV1 {
1946
+ const now = new Date().toISOString();
1947
+ const sessionId = safeExternalId("session", session.session_id ?? session.sessionId);
1948
+ const cwd = optionalString(session.cwd);
1949
+ const incarnation = optionalString(session.endpoint_incarnation);
1950
+ if (!cwd || !incarnation) throw new Error("state_corrupt");
1951
+ return {
1952
+ schema_version: 1,
1953
+ namespace_id: config.namespace.identity,
1954
+ session_id: sessionId,
1955
+ cwd: path.resolve(cwd),
1956
+ created_at: optionalString(session.created_at) ?? now,
1957
+ updated_at: now,
1958
+ mpreset: optionalString(session.mpreset),
1959
+ source: optionalString(session.source),
1960
+ model: optionalString(session.model),
1961
+ tmux: {
1962
+ session: optionalString(session.tmux_session),
1963
+ window: null,
1964
+ pane: optionalString(session.tmux_target),
1965
+ },
1966
+ broker: {
1967
+ workspace: optionalString(session.broker_workspace),
1968
+ endpoint_url: "",
1969
+ endpoint_generation: typeof session.endpoint_generation === "number" ? session.endpoint_generation : 0,
1970
+ endpoint_incarnation: incarnation,
1971
+ },
1972
+ ephemeral: session.ephemeral === true,
1973
+ visible: session.visible !== false,
1974
+ };
1975
+ }
1976
+
1977
+ function sessionFromCreationSnapshot(snapshot: CanonicalSessionSnapshotV1): Record<string, unknown> {
1978
+ return normalizeSession({
1979
+ session_id: snapshot.session_id,
1980
+ cwd: snapshot.cwd,
1981
+ created_at: snapshot.created_at,
1982
+ ...(snapshot.mpreset ? { mpreset: snapshot.mpreset } : {}),
1983
+ ...(snapshot.source ? { source: snapshot.source } : {}),
1984
+ ...(snapshot.model ? { model: snapshot.model } : {}),
1985
+ tmux_session: snapshot.tmux.session,
1986
+ tmux_target: snapshot.tmux.pane,
1987
+ ephemeral: snapshot.ephemeral,
1988
+ visible: snapshot.visible,
1989
+ broker_workspace: snapshot.broker.workspace,
1990
+ endpoint_generation: snapshot.broker.endpoint_generation,
1991
+ endpoint_incarnation: snapshot.broker.endpoint_incarnation,
1992
+ });
1993
+ }
1994
+
1995
+ async function claimProductionCreation(
1996
+ tool: string,
1997
+ idempotencyKey: string,
1998
+ canonicalArgs: Record<string, unknown>,
1999
+ ) {
2000
+ await ensureQuestionStateReady();
2001
+ const { keyDigest, requestDigest } = creationDigests(tool, idempotencyKey, canonicalArgs);
2002
+ return {
2003
+ keyDigest,
2004
+ request: await claimCreationRequest(questionPaths, {
2005
+ key_digest: keyDigest,
2006
+ request_digest: requestDigest,
2007
+ tool,
2008
+ }),
2009
+ };
2010
+ }
2011
+
2012
+ async function ensureQuestionTransaction(sessionId: string): Promise<void> {
2013
+ await ensureQuestionStateReady();
2014
+ try {
2015
+ await withSessionTransaction(questionPaths, sessionId, async () => undefined);
2016
+ return;
2017
+ } catch (error) {
2018
+ if (!(error instanceof Error) || error.message !== "resource_gone") throw error;
2019
+ }
2020
+ const session = asRecord(await readJsonFile(sessionFile(sessionId)));
2021
+ if (!session) throw new Error("resource_gone");
2022
+ const incarnation = optionalString(session.endpoint_incarnation);
2023
+ const cwd = optionalString(session.cwd);
2024
+ if (!incarnation || !cwd) throw new Error("resource_gone");
2025
+ const now = new Date().toISOString();
2026
+ await createSessionTransaction(questionPaths, {
2027
+ kind: "register",
2028
+ session: {
2029
+ schema_version: 1,
2030
+ namespace_id: config.namespace.identity,
2031
+ session_id: sessionId,
2032
+ cwd: path.resolve(cwd),
2033
+ created_at: optionalString(session.created_at) ?? now,
2034
+ updated_at: now,
2035
+ mpreset: optionalString(session.mpreset),
2036
+ source: optionalString(session.source),
2037
+ model: optionalString(session.model),
2038
+ tmux: {
2039
+ session: optionalString(session.tmux_session),
2040
+ window: null,
2041
+ pane: optionalString(session.tmux_target),
2042
+ },
2043
+ broker: {
2044
+ workspace: optionalString(session.broker_workspace),
2045
+ endpoint_url: "",
2046
+ endpoint_generation: typeof session.endpoint_generation === "number" ? session.endpoint_generation : 0,
2047
+ endpoint_incarnation: incarnation,
2048
+ },
2049
+ ephemeral: session.ephemeral === true,
2050
+ visible: session.visible !== false,
2051
+ },
2052
+ initial_state: "ready_for_input",
2053
+ initial_events: [],
2054
+ });
2055
+ }
2056
+
2057
+ function publicQuestions(
2058
+ transaction: CoordinatorSessionTransactionV1,
2059
+ status: string | null,
2060
+ ): CoordinatorQuestionPublicV1[] {
2061
+ return Object.values(transaction.canonical.questions)
2062
+ .filter(
2063
+ question => !status || question.status === status || (status === "open" && question.status === "pending"),
2064
+ )
2065
+ .map(question =>
2066
+ projectAskGateQuestion({
2067
+ question_id: question.question_id,
2068
+ session_id: question.session_id,
2069
+ turn_id: question.turn_id,
2070
+ status: question.status === "resolving" ? "pending" : question.status,
2071
+ stage: question.stage,
2072
+ kind: question.kind,
2073
+ prompt: question.prompt,
2074
+ codec: question.codec,
2075
+ created_at: question.created_at,
2076
+ updated_at: question.updated_at,
2077
+ answered_at: question.answered_at,
2078
+ reason: question.history.at(-1)?.reason ?? null,
2079
+ ...(question.status === "pending" ? { answer_binding: question.binding_plaintext } : {}),
2080
+ }),
2081
+ );
2082
+ }
2083
+
2084
+ async function reconcileQuestions(sessionId: string): Promise<ListQuestionsSuccessV1> {
2085
+ const observedAt = new Date().toISOString();
2086
+ const diagnostics: CoordinatorQuestionDiagnosticPublicV1[] = [];
2087
+ const diagnostic = (
2088
+ reason: CoordinatorQuestionDiagnosticPublicV1["reason"],
2089
+ turnId: string | null = null,
2090
+ gateId: string | null = null,
2091
+ ) => {
2092
+ if (diagnostics.length < 64)
2093
+ diagnostics.push({
2094
+ schema_version: 1,
2095
+ session_id: sessionId,
2096
+ turn_id: turnId,
2097
+ gate_id: gateId,
2098
+ reason,
2099
+ observed_at: observedAt,
2100
+ });
2101
+ };
2102
+ try {
2103
+ await ensureQuestionTransaction(sessionId);
2104
+ const session = asRecord(await readJsonFile(sessionFile(sessionId)));
2105
+ if (!session) throw new Error("resource_gone");
2106
+ const snapshot = await readCompleteQ12Snapshot(session);
2107
+ const { items, complete, revision } = snapshot;
2108
+ if (!complete) {
2109
+ diagnostic(snapshot.reason ?? "query_unavailable");
2110
+ return {
2111
+ ok: true,
2112
+ schema_version: 1,
2113
+ questions: await withSessionTransaction(questionPaths, sessionId, async tx => publicQuestions(tx, null)),
2114
+ diagnostics,
2115
+ reconciliation: {
2116
+ attempted: true,
2117
+ complete: false,
2118
+ revision,
2119
+ observed_at: observedAt,
2120
+ reason: snapshot.reason ?? "query_unavailable",
2121
+ },
2122
+ };
2123
+ }
2124
+ const projectedTurnQuestions = new Map<string, string[]>();
2125
+ await withAdmittedSessionTransaction(questionPaths, sessionId, async transaction => {
2126
+ const seen = new Set<string>();
2127
+ const byRuntimeTurn = new Map<string, Array<(typeof transaction.canonical.turns)[string]>>();
2128
+ for (const turn of Object.values(transaction.canonical.turns)) {
2129
+ const runtimeTurnId = turn.delivery.runtime_turn_id;
2130
+ if (typeof runtimeTurnId !== "string") continue;
2131
+ const owners = byRuntimeTurn.get(runtimeTurnId) ?? [];
2132
+ owners.push(turn);
2133
+ byRuntimeTurn.set(runtimeTurnId, owners);
2134
+ }
2135
+ for (const row of items as WorkflowGateQueryRecord[]) {
2136
+ if (!row || typeof row !== "object" || row.tag !== "pending" || typeof row.gate_id !== "string") {
2137
+ diagnostic("invalid_gate_row");
2138
+ continue;
2139
+ }
2140
+ const gate = row as WorkflowGateQueryRecord & WorkflowGate;
2141
+ const authorityId = createHash("sha256")
2142
+ .update(
2143
+ `${config.namespace.identity}\0${sessionId}\0${transaction.canonical.session.broker.endpoint_incarnation}\0${gate.gate_id}`,
2144
+ )
2145
+ .digest("hex");
2146
+ seen.add(authorityId);
2147
+ const runtimeTurnId = gate.runtime_turn_id;
2148
+ const existing = transaction.canonical.gate_authorities[authorityId];
2149
+ const authority = existing ?? {
2150
+ authority: {
2151
+ namespace_id: config.namespace.identity,
2152
+ session_id: sessionId,
2153
+ endpoint_incarnation: transaction.canonical.session.broker.endpoint_incarnation,
2154
+ gate_id: gate.gate_id,
2155
+ },
2156
+ observation:
2157
+ typeof runtimeTurnId === "string" && runtimeTurnId
2158
+ ? {
2159
+ kind: "valid" as const,
2160
+ first_provenance: {
2161
+ runtime_turn_id: runtimeTurnId,
2162
+ gate_created_at: gate.created_at,
2163
+ schema_hash: gate.schema_hash,
2164
+ stage: gate.stage,
2165
+ kind: gate.kind,
2166
+ },
2167
+ }
2168
+ : {
2169
+ kind: "malformed" as const,
2170
+ immutable_observation_digest: createHash("sha256")
2171
+ .update(canonicalJson(row))
2172
+ .digest("hex"),
2173
+ malformed: "missing_runtime_turn" as const,
2174
+ },
2175
+ outcome: { state: "deferred_link" as const, first_seen_at: observedAt },
2176
+ first_seen_at: observedAt,
2177
+ updated_at: observedAt,
2178
+ };
2179
+ transaction.canonical.gate_authorities[authorityId] = authority;
2180
+ if (typeof runtimeTurnId !== "string" || !runtimeTurnId) {
2181
+ authority.outcome = { state: "stale", reason: "missing_runtime_turn" };
2182
+ authority.updated_at = observedAt;
2183
+ diagnostic("missing_runtime_turn", null, gate.gate_id);
2184
+ continue;
2185
+ }
2186
+ if (
2187
+ existing?.observation.kind === "malformed" ||
2188
+ (existing?.observation.kind === "valid" &&
2189
+ (existing.observation.first_provenance.runtime_turn_id !== runtimeTurnId ||
2190
+ existing.observation.first_provenance.gate_created_at !== gate.created_at ||
2191
+ existing.observation.first_provenance.schema_hash !== gate.schema_hash ||
2192
+ existing.observation.first_provenance.stage !== gate.stage ||
2193
+ existing.observation.first_provenance.kind !== gate.kind))
2194
+ ) {
2195
+ authority.outcome = { state: "stale", reason: "gate_provenance_changed" };
2196
+ authority.updated_at = observedAt;
2197
+ diagnostic("gate_provenance_changed", null, gate.gate_id);
2198
+ continue;
2199
+ }
2200
+ const owners = byRuntimeTurn.get(runtimeTurnId) ?? [];
2201
+ if (owners.length !== 1) {
2202
+ const watermarkAt = transaction.recovery.prompt_watermark_at;
2203
+ const gateCreatedAt = Date.parse(gate.created_at);
2204
+ const watermarkBeyondGate =
2205
+ watermarkAt !== null && Number.isFinite(gateCreatedAt) && Date.parse(watermarkAt) > gateCreatedAt;
2206
+ const boundedAbsenceProved =
2207
+ watermarkBeyondGate &&
2208
+ Number.isFinite(gateCreatedAt) &&
2209
+ Date.now() - gateCreatedAt >= 5 * 60 * 1000;
2210
+ if (owners.length === 0 && !boundedAbsenceProved) {
2211
+ authority.outcome = { state: "deferred_link", first_seen_at: authority.first_seen_at };
2212
+ authority.updated_at = observedAt;
2213
+ continue;
2214
+ }
2215
+ authority.outcome =
2216
+ owners.length === 0
2217
+ ? { state: "ownership_unavailable", reason: "ownership_unavailable" }
2218
+ : { state: "ownership_conflict", reason: "ownership_conflict" };
2219
+ authority.updated_at = observedAt;
2220
+ diagnostic(owners.length === 0 ? "ownership_unavailable" : "ownership_conflict", null, gate.gate_id);
2221
+ continue;
2222
+ }
2223
+ const turn = owners[0]!;
2224
+ if (TERMINAL_TURN_STATUSES.has(turn.status as TurnStatus)) {
2225
+ authority.outcome = { state: "stale", reason: "turn_terminal", turn_id: turn.turn_id };
2226
+ authority.updated_at = observedAt;
2227
+ diagnostic("turn_terminal", turn.turn_id, gate.gate_id);
2228
+ continue;
2229
+ }
2230
+ const codec = decodeAskGateV1(gate);
2231
+ if (!codec) {
2232
+ authority.outcome = { state: "stale", reason: "unsupported_gate", turn_id: turn.turn_id };
2233
+ authority.updated_at = observedAt;
2234
+ diagnostic("unsupported_gate", turn.turn_id, gate.gate_id);
2235
+ continue;
2236
+ }
2237
+ if (
2238
+ existing &&
2239
+ existing.outcome.state !== "pending" &&
2240
+ existing.outcome.state !== "answered" &&
2241
+ existing.outcome.state !== "deferred_link"
2242
+ )
2243
+ continue;
2244
+ const questionId =
2245
+ existing?.outcome.state === "pending" || existing?.outcome.state === "answered"
2246
+ ? existing.outcome.question_id
2247
+ : gate.gate_id;
2248
+ if (!existing || authority.outcome.state === "deferred_link") {
2249
+ authority.observation = {
2250
+ kind: "valid",
2251
+ first_provenance: {
2252
+ runtime_turn_id: runtimeTurnId,
2253
+ gate_created_at: gate.created_at,
2254
+ schema_hash: gate.schema_hash,
2255
+ stage: gate.stage,
2256
+ kind: gate.kind,
2257
+ },
2258
+ };
2259
+ authority.outcome = { state: "pending", turn_id: turn.turn_id, question_id: questionId };
2260
+ }
2261
+ authority.updated_at = observedAt;
2262
+ transaction.canonical.gate_authorities[authorityId] = authority;
2263
+ if (!transaction.canonical.questions[questionId]) {
2264
+ const binding = createAnswerBinding();
2265
+ transaction.canonical.questions[questionId] = {
2266
+ question_id: questionId,
2267
+ authority_id: authorityId,
2268
+ session_id: sessionId,
2269
+ turn_id: turn.turn_id,
2270
+ endpoint_incarnation: transaction.canonical.session.broker.endpoint_incarnation,
2271
+ stage: gate.stage,
2272
+ kind: gate.kind,
2273
+ prompt: typeof gate.context.prompt === "string" ? gate.context.prompt : "",
2274
+ status: "pending",
2275
+ binding_plaintext: binding,
2276
+ binding_sha256: createHash("sha256").update(binding).digest("hex"),
2277
+ codec,
2278
+ claim_fence_epoch: null,
2279
+ answer_request_id: null,
2280
+ created_at: observedAt,
2281
+ updated_at: observedAt,
2282
+ answered_at: null,
2283
+ history: [{ at: observedAt, status: "pending", reason: null }],
2284
+ };
2285
+ turn.question_ids = [...new Set([...turn.question_ids, questionId])];
2286
+ projectedTurnQuestions.set(turn.turn_id, turn.question_ids);
2287
+ }
2288
+ }
2289
+ if (complete)
2290
+ for (const [authorityId, authority] of Object.entries(transaction.canonical.gate_authorities))
2291
+ if (!seen.has(authorityId) && authority.outcome.state === "pending") {
2292
+ const question = transaction.canonical.questions[authority.outcome.question_id];
2293
+ if (question?.status === "pending") {
2294
+ question.status = "stale";
2295
+ question.updated_at = observedAt;
2296
+ question.history.push({ at: observedAt, status: "stale", reason: "terminal_uncertain" });
2297
+ authority.outcome = {
2298
+ state: "stale",
2299
+ reason: "terminal_uncertain",
2300
+ turn_id: question.turn_id,
2301
+ question_id: question.question_id,
2302
+ };
2303
+ authority.updated_at = observedAt;
2304
+ }
2305
+ }
2306
+ });
2307
+ for (const [turnId, questionIds] of projectedTurnQuestions) {
2308
+ const legacyTurn = await readTurnRecord(namespaceDir, turnId);
2309
+ if (!legacyTurn) continue;
2310
+ legacyTurn.question_ids = questionIds;
2311
+ await writeTurnRecord(namespaceDir, legacyTurn);
2312
+ }
2313
+ return {
2314
+ ok: true,
2315
+ schema_version: 1,
2316
+ questions: await withSessionTransaction(questionPaths, sessionId, async tx => publicQuestions(tx, null)),
2317
+ diagnostics,
2318
+ reconciliation: {
2319
+ attempted: true,
2320
+ complete,
2321
+ revision,
2322
+ observed_at: observedAt,
2323
+ reason: complete ? null : "query_unavailable",
2324
+ },
2325
+ };
2326
+ } catch (error) {
2327
+ if (error instanceof Error && (error.message === "state_corrupt" || error.message === "resource_gone"))
2328
+ throw error;
2329
+ diagnostic("query_unavailable");
2330
+ await ensureQuestionTransaction(sessionId);
2331
+ return {
2332
+ ok: true,
2333
+ schema_version: 1,
2334
+ questions: await withSessionTransaction(questionPaths, sessionId, async tx => publicQuestions(tx, null)),
2335
+ diagnostics,
2336
+ reconciliation: {
2337
+ attempted: true,
2338
+ complete: false,
2339
+ revision: null,
2340
+ observed_at: observedAt,
2341
+ reason: "query_unavailable",
2342
+ },
2343
+ };
2344
+ }
2345
+ }
1848
2346
  const sessionTransitionTails = new Map<string, Promise<void>>();
1849
2347
 
1850
2348
  async function withSessionTransition<T>(sessionId: string, operation: () => Promise<T>): Promise<T> {
@@ -1884,14 +2382,19 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1884
2382
  }
1885
2383
  }
1886
2384
 
1887
- async function querySession(session: Record<string, unknown>, query: string): Promise<Record<string, unknown>> {
2385
+ async function querySession(
2386
+ session: Record<string, unknown>,
2387
+ query: string,
2388
+ input: Record<string, unknown> = {},
2389
+ cursor?: string,
2390
+ ): Promise<Record<string, unknown>> {
1888
2391
  const endpoint = await resolveSessionEndpoint(session);
1889
2392
  const client = await (services.connectSdk ?? ((url, token) => SdkClient.connect(url, token)))(
1890
2393
  endpoint.url,
1891
2394
  endpoint.token,
1892
2395
  );
1893
2396
  try {
1894
- const response = asRecord(await client.query(query));
2397
+ const response = asRecord(await client.query(query, input, cursor));
1895
2398
  if (response?.ok !== true) {
1896
2399
  const error = asRecord(response?.error);
1897
2400
  throw new SdkClientError(
@@ -1905,6 +2408,83 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1905
2408
  }
1906
2409
  }
1907
2410
 
2411
+ async function readCompleteQ12Snapshot(session: Record<string, unknown>): Promise<{
2412
+ items: unknown[];
2413
+ revision: string | null;
2414
+ complete: boolean;
2415
+ reason: "pagination_malformed" | "query_unavailable" | null;
2416
+ }> {
2417
+ const deadline = Date.now() + 5_000;
2418
+ const items: unknown[] = [];
2419
+ const cursors = new Set<string>();
2420
+ let cursor: string | undefined;
2421
+ let revision: string | null = null;
2422
+ let bytes = 0;
2423
+ let client: SdkClient;
2424
+ try {
2425
+ const endpoint = await resolveSessionEndpoint(session);
2426
+ client = await (services.connectSdk ?? ((url, token) => SdkClient.connect(url, token)))(
2427
+ endpoint.url,
2428
+ endpoint.token,
2429
+ );
2430
+ } catch {
2431
+ return { items: [], revision, complete: false, reason: "query_unavailable" };
2432
+ }
2433
+ try {
2434
+ for (let pageCount = 0; pageCount < 8 && Date.now() <= deadline; pageCount++) {
2435
+ const response = asRecord(await client.query("Q12", {}, cursor));
2436
+ if (response?.ok !== true) return { items: [], revision, complete: false, reason: "query_unavailable" };
2437
+ const page = asRecord(response.page);
2438
+ const pageItems = page?.items;
2439
+ const pageRevision = typeof page?.revision === "string" ? page.revision : null;
2440
+ const complete = page?.complete === true;
2441
+ const preview = page?.preview;
2442
+ const nextCursor = page?.continuationCursor;
2443
+ if (
2444
+ !Array.isArray(pageItems) ||
2445
+ typeof page?.complete !== "boolean" ||
2446
+ !pageRevision ||
2447
+ (complete && (preview === true || (nextCursor !== undefined && nextCursor !== null))) ||
2448
+ (!complete && (preview !== true || typeof nextCursor !== "string" || nextCursor.length === 0))
2449
+ )
2450
+ return { items: [], revision: pageRevision, complete: false, reason: "pagination_malformed" };
2451
+ const validatedCursor = typeof nextCursor === "string" ? nextCursor : undefined;
2452
+ if (revision !== null && revision !== pageRevision)
2453
+ return { items: [], revision: pageRevision, complete: false, reason: "pagination_malformed" };
2454
+ revision = pageRevision;
2455
+ bytes += Buffer.byteLength(JSON.stringify(pageItems));
2456
+ if (items.length + pageItems.length > 64 || bytes > 256 * 1024)
2457
+ return { items: [], revision, complete: false, reason: "pagination_malformed" };
2458
+ items.push(...pageItems);
2459
+ if (complete) {
2460
+ const valid = items.every(item => {
2461
+ if (!item || typeof item !== "object" || Buffer.byteLength(JSON.stringify(item)) > 16 * 1024)
2462
+ return false;
2463
+ const gate = item as WorkflowGateQueryRecord & WorkflowGate;
2464
+ return (
2465
+ gate.tag === "pending" &&
2466
+ typeof gate.gate_id === "string" &&
2467
+ gate.gate_id.length > 0 &&
2468
+ !!decodeAskGateV1(gate)
2469
+ );
2470
+ });
2471
+ return valid
2472
+ ? { items, revision, complete: true, reason: null }
2473
+ : { items: [], revision, complete: false, reason: "pagination_malformed" };
2474
+ }
2475
+ if (!validatedCursor || cursors.has(validatedCursor))
2476
+ return { items: [], revision, complete: false, reason: "pagination_malformed" };
2477
+ cursors.add(validatedCursor);
2478
+ cursor = validatedCursor;
2479
+ }
2480
+ return { items: [], revision, complete: false, reason: "pagination_malformed" };
2481
+ } catch {
2482
+ return { items: [], revision, complete: false, reason: "query_unavailable" };
2483
+ } finally {
2484
+ await client.close().catch(() => undefined);
2485
+ }
2486
+ }
2487
+
1908
2488
  function sdkQueryPageItem(response: Record<string, unknown>, query: string): unknown {
1909
2489
  const items = asRecord(response.page)?.items;
1910
2490
  if (!Array.isArray(items) || items.length !== 1)
@@ -1960,6 +2540,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1960
2540
  idempotencyKey: string,
1961
2541
  canonicalArgs: Record<string, unknown>,
1962
2542
  operation: () => Promise<Record<string, unknown>>,
2543
+ recoverInProgress = false,
1963
2544
  ): Promise<Record<string, unknown>> {
1964
2545
  const keyDigest = createHash("sha256").update(idempotencyKey).digest("hex");
1965
2546
  const requestDigest = createHash("sha256")
@@ -2000,7 +2581,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2000
2581
  },
2001
2582
  };
2002
2583
  }
2003
- if (existing.state === "in_progress")
2584
+ if (existing.state === "in_progress" && !recoverInProgress)
2004
2585
  return {
2005
2586
  ok: false,
2006
2587
  error: {
@@ -2008,6 +2589,16 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2008
2589
  message: "prior coordinator mutation outcome is not replayable",
2009
2590
  },
2010
2591
  };
2592
+ if (existing.state === "in_progress") {
2593
+ const response = boundedPublicResponse(await operation().catch(error => sdkError(error)));
2594
+ await writeCoordinatorIdempotencyFile(file, {
2595
+ ...existing,
2596
+ state: "completed",
2597
+ response,
2598
+ completed_at: new Date().toISOString(),
2599
+ } as CoordinatorToolIdempotencyRecord);
2600
+ return response;
2601
+ }
2011
2602
  return {
2012
2603
  ok: false,
2013
2604
  error: {
@@ -2042,17 +2633,45 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2042
2633
  input: Record<string, unknown>,
2043
2634
  idempotencyKey?: string,
2044
2635
  ): Promise<unknown> {
2045
- const discovery = await readSdkBrokerDiscovery(services.getAgentDir?.() ?? getAgentDir());
2046
- if (!discovery) throw new SdkClientError("not_found", "SDK broker discovery is unavailable.");
2047
- const client = await (services.connectSdk ?? ((url, token) => SdkClient.connect(url, token)))(
2048
- discovery.url,
2049
- discovery.token,
2050
- );
2636
+ const agentDir = services.getAgentDir?.() ?? getAgentDir();
2637
+ try {
2638
+ await (services.ensureBroker ?? ensureBroker)({ agentDir });
2639
+ } catch (error) {
2640
+ throw toCoordinatorBrokerError("ensure", error);
2641
+ }
2642
+
2643
+ let discovery: BrokerDiscovery | null;
2644
+ try {
2645
+ discovery = await (services.readSdkBrokerDiscovery ?? readSdkBrokerDiscovery)(agentDir);
2646
+ } catch (error) {
2647
+ throw toCoordinatorBrokerError("read", error);
2648
+ }
2649
+ if (!discovery) throw new SdkClientError("broker_unavailable", "SDK broker is unavailable after bootstrap.");
2650
+
2651
+ let client: SdkClient;
2652
+ try {
2653
+ client = await (services.connectSdk ?? ((url, token) => SdkClient.connect(url, token)))(
2654
+ discovery.url,
2655
+ discovery.token,
2656
+ );
2657
+ } catch (error) {
2658
+ throw toCoordinatorBrokerError("connect", error);
2659
+ }
2660
+
2661
+ let requestError: SdkClientError | undefined;
2662
+ let result: unknown;
2663
+ try {
2664
+ result = await client.global(operation, input, { ...(idempotencyKey ? { idempotencyKey } : {}) });
2665
+ } catch (error) {
2666
+ requestError = toCoordinatorBrokerError("request", error);
2667
+ }
2051
2668
  try {
2052
- return await client.global(operation, input, { ...(idempotencyKey ? { idempotencyKey } : {}) });
2053
- } finally {
2054
2669
  await client.close();
2670
+ } catch (error) {
2671
+ if (!requestError) throw toCoordinatorBrokerError("close", error);
2055
2672
  }
2673
+ if (requestError) throw requestError;
2674
+ return result;
2056
2675
  }
2057
2676
 
2058
2677
  function brokerResult(value: unknown): Record<string, unknown> {
@@ -2211,7 +2830,19 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2211
2830
  const id = safeExternalId("session", rawId);
2212
2831
  return await withSessionTransition(id, async () => {
2213
2832
  const session = asRecord(await readJsonFile(sessionFile(id)));
2214
- if (!session) return { ok: false, reason: "unknown_session", closed: false };
2833
+ if (!session) {
2834
+ await ensureQuestionStateReady();
2835
+ const deletion = await withNamespaceRegistry(
2836
+ questionPaths,
2837
+ async registry =>
2838
+ Object.values(registry.deletions).find(
2839
+ entry => entry.session_id === id && entry.phase === "completed",
2840
+ ) ?? null,
2841
+ );
2842
+ if (deletion?.safe_response) return deletion.safe_response as { ok: boolean; closed: boolean };
2843
+ return { ok: false, reason: "unknown_session", closed: false };
2844
+ }
2845
+
2215
2846
  if (session.ephemeral !== true && opts.force !== true)
2216
2847
  return { ok: false, reason: "not_ephemeral", closed: false };
2217
2848
  const activeTurn = await readActiveTurn(namespaceDir, id);
@@ -2227,6 +2858,39 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2227
2858
  const persistedIncarnation = optionalString(session.endpoint_incarnation);
2228
2859
  if (!cwd || !persistedWorkspace || persistedGeneration === null || !persistedIncarnation)
2229
2860
  return { ok: false, reason: "endpoint_stale", closed: false };
2861
+ const deletionId = `delete:${id}:${persistedIncarnation}`;
2862
+ const deletionKey = createHash("sha256").update(deletionId).digest("hex");
2863
+ await ensureQuestionStateReady();
2864
+ await ensureQuestionTransaction(id);
2865
+ await withSessionTransaction(questionPaths, id, async transaction => {
2866
+ const now = new Date().toISOString();
2867
+ transaction.requests.operations[deletionId] = {
2868
+ operation_id: deletionId,
2869
+ tool: "gjc_coordinator_stop_session",
2870
+ key_digest: deletionKey,
2871
+ request_digest: deletionKey,
2872
+ local_id: id,
2873
+ phase: "remote_started",
2874
+ intent: { kind: "reap", endpoint_incarnation: persistedIncarnation },
2875
+ created_at: now,
2876
+ updated_at: now,
2877
+ };
2878
+ });
2879
+ await recordDeletionIntent(questionPaths, {
2880
+ deletion_id: deletionId,
2881
+ session_id: id,
2882
+ endpoint_incarnation: persistedIncarnation,
2883
+ operation_id: deletionId,
2884
+ key_digest: deletionKey,
2885
+ request_digest: deletionKey,
2886
+ close_key: deletionId,
2887
+ phase: "intent",
2888
+ cleanup: { wal: false, turns: false, reports: false, session: false, events: false },
2889
+ authority_digest: deletionKey,
2890
+ created_at: new Date().toISOString(),
2891
+ updated_at: new Date().toISOString(),
2892
+ });
2893
+
2230
2894
  let workspace = "";
2231
2895
  try {
2232
2896
  workspace = await canonicalBrokerWorkspace(cwd);
@@ -2257,6 +2921,8 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2257
2921
  closed: false,
2258
2922
  };
2259
2923
  }
2924
+ await advanceDeletion(questionPaths, deletionId, "broker_closed");
2925
+
2260
2926
  try {
2261
2927
  await exactBrokerSessionAuthority(id, workspace);
2262
2928
  return { ok: false, reason: "endpoint_stale", closed: false };
@@ -2278,6 +2944,19 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2278
2944
  summary: `Session ${id} closed and reaped${opts.reason ? ` (${opts.reason})` : ""}`,
2279
2945
  metadata: { reason: opts.reason ?? null, force: opts.force === true, closed: true },
2280
2946
  });
2947
+ await advanceDeletion(
2948
+ questionPaths,
2949
+ deletionId,
2950
+ "completed",
2951
+ {
2952
+ wal: true,
2953
+ turns: true,
2954
+ reports: false,
2955
+ session: true,
2956
+ events: true,
2957
+ },
2958
+ { ok: true, closed: true },
2959
+ );
2281
2960
  return { ok: true, closed: true };
2282
2961
  });
2283
2962
  }
@@ -2311,16 +2990,17 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2311
2990
  },
2312
2991
  { idleTtlMs: config.sessionIdleTtlMs, sweepIntervalMs: config.sessionSweepIntervalMs },
2313
2992
  );
2314
- async function listQuestions(args: Record<string, unknown>): Promise<unknown[]> {
2315
- const sessionId = args.session_id == null ? null : safeExternalId("session", args.session_id);
2316
- const status = typeof args.status === "string" && args.status.length > 0 ? args.status : null;
2317
- return (await listJsonFiles(path.join(namespaceDir, "questions"))).filter(question => {
2318
- const record = asRecord(question);
2319
- if (!record) return false;
2320
- if (sessionId && record.session_id !== sessionId) return false;
2321
- if (status && record.status !== status) return false;
2322
- return true;
2323
- });
2993
+ async function listQuestions(args: Record<string, unknown>): Promise<ListQuestionsSuccessV1> {
2994
+ const sessionId = safeExternalId("session", args.session_id);
2995
+ const reconciled = await reconcileQuestions(sessionId);
2996
+ const status = typeof args.status === "string" && args.status.length > 0 ? args.status : "pending";
2997
+ const turnId = args.turn_id == null ? null : safeTurnId(args.turn_id);
2998
+ return {
2999
+ ...reconciled,
3000
+ questions: reconciled.questions
3001
+ .filter(question => question.status === status || (status === "open" && question.status === "pending"))
3002
+ .filter(question => turnId === null || question.turn_id === turnId),
3003
+ };
2324
3004
  }
2325
3005
 
2326
3006
  async function validateEvidencePaths(value: unknown): Promise<Array<{ path: string }>> {
@@ -2340,6 +3020,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2340
3020
  if (sessionId != null && turn.session_id !== safeExternalId("session", sessionId)) {
2341
3021
  return { ok: false, reason: "turn_session_mismatch" };
2342
3022
  }
3023
+ await reconcileQuestions(turn.session_id);
2343
3024
  const session = asRecord(await readJsonFile(sessionFile(turn.session_id)));
2344
3025
  let resolvedTurn = turn;
2345
3026
  let advisoryStatus: Record<string, unknown> = {
@@ -2389,10 +3070,20 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2389
3070
  sessionState.current_turn_id == null)) &&
2390
3071
  (sessionState.state === "completed" || sessionState.state === "errored")
2391
3072
  ) {
2392
- resolvedTurn = await markTurnTerminalFromSessionState(namespaceDir, resolvedTurn, sessionState);
3073
+ resolvedTurn = await markTurnTerminalFromSessionState(resolvedTurn, sessionState);
3074
+ await projectTerminalTransition(resolvedTurn, {
3075
+ desiredState: sessionState.state,
3076
+ reason: sessionState.reason ? "terminal_uncertain" : null,
3077
+ live: sessionState.live,
3078
+ });
2393
3079
  sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
2394
3080
  } else if (!session && ACTIVE_TURN_STATUSES.has(resolvedTurn.status)) {
2395
- resolvedTurn = await markTurnFailedForUnavailableSession(namespaceDir, resolvedTurn, "session_record_missing");
3081
+ resolvedTurn = await markTurnFailedForUnavailableSession(resolvedTurn, "session_record_missing");
3082
+ await projectTerminalTransition(resolvedTurn, {
3083
+ desiredState: "stale",
3084
+ reason: "session_unavailable",
3085
+ live: false,
3086
+ });
2396
3087
  sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
2397
3088
  }
2398
3089
  if (ACTIVE_TURN_STATUSES.has(resolvedTurn.status)) {
@@ -2403,6 +3094,11 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2403
3094
  promptAckTimeoutMs,
2404
3095
  );
2405
3096
  if (!ACTIVE_TURN_STATUSES.has(resolvedTurn.status)) {
3097
+ await projectTerminalTransition(resolvedTurn, {
3098
+ desiredState: "stale",
3099
+ reason: "terminal_uncertain",
3100
+ live: resolvedTurn.liveness.live,
3101
+ });
2406
3102
  sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
2407
3103
  }
2408
3104
  }
@@ -2458,12 +3154,302 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2458
3154
  return payload;
2459
3155
  }
2460
3156
 
3157
+ async function claimCanonicalPrompt(
3158
+ sessionId: string,
3159
+ prompt: string,
3160
+ operation: "turn.prompt" | "turn.follow_up" | "turn.abort_and_prompt",
3161
+ idempotencyKey: string,
3162
+ ): Promise<string> {
3163
+ await ensureQuestionTransaction(sessionId);
3164
+ const keyDigest = createHash("sha256").update(`${idempotencyKey}\0${operation}`).digest("hex");
3165
+ const requestDigest = createHash("sha256").update(`${operation}\0${prompt}`).digest("hex");
3166
+ await withAdmittedSessionTransaction(questionPaths, sessionId, async transaction => {
3167
+ const existing = transaction.requests.prompts[keyDigest];
3168
+ if (existing) {
3169
+ if (existing.request_digest !== requestDigest) throw new Error("idempotency_conflict");
3170
+ if (existing.phase === "remote_started" || existing.phase === "uncertain")
3171
+ throw new Error("terminal_uncertain");
3172
+ return;
3173
+ }
3174
+ const now = new Date().toISOString();
3175
+ transaction.requests.prompts[keyDigest] = {
3176
+ request_id: `prompt:${keyDigest}`,
3177
+ key_digest: keyDigest,
3178
+ request_digest: requestDigest,
3179
+ operation,
3180
+ canonical_prompt: { text: prompt },
3181
+ sdk_idempotency_key: idempotencyKey,
3182
+ phase: "remote_started",
3183
+ created_at: now,
3184
+ updated_at: now,
3185
+ };
3186
+ });
3187
+ return keyDigest;
3188
+ }
3189
+
3190
+ /** Commits terminal fencing, question revocation, report, and promotion together before legacy projection. */
3191
+ async function commitTerminalTransition(
3192
+ turn: TurnRecord,
3193
+ input: {
3194
+ desiredState: CoordinatorSessionStateValue;
3195
+ reason: PublicReason | null;
3196
+ report?: CanonicalReportSnapshotV1;
3197
+ promoteQueuedTurn?: boolean;
3198
+ },
3199
+ ): Promise<{ promotedTurnId: string | null; desiredState: CoordinatorSessionStateValue }> {
3200
+ await ensureQuestionTransaction(turn.session_id);
3201
+ return await withAdmittedSessionTransaction(questionPaths, turn.session_id, async transaction => {
3202
+ const terminalEpoch = transaction.revision + 1;
3203
+ transaction.canonical.turns[turn.turn_id] = {
3204
+ schema_version: 1,
3205
+ turn_id: turn.turn_id,
3206
+ session_id: turn.session_id,
3207
+ namespace_id: config.namespace.identity,
3208
+ status: turn.status,
3209
+ prompt: turn.prompt,
3210
+ delivery: { ...turn.delivery },
3211
+ question_ids: Object.values(transaction.canonical.questions)
3212
+ .filter(question => question.turn_id === turn.turn_id)
3213
+ .map(question => question.question_id),
3214
+ final_response: { ...turn.final_response },
3215
+ evidence: turn.evidence,
3216
+ error: turn.error ? { ...turn.error } : null,
3217
+ liveness: { ...turn.liveness },
3218
+ created_at: turn.created_at,
3219
+ updated_at: turn.updated_at,
3220
+ started_at: turn.started_at,
3221
+ completed_at: turn.completed_at,
3222
+ terminal_fence: {
3223
+ epoch: terminalEpoch,
3224
+ status: turn.status,
3225
+ reason: input.reason,
3226
+ at: turn.completed_at ?? turn.updated_at,
3227
+ },
3228
+ };
3229
+ for (const question of Object.values(transaction.canonical.questions)) {
3230
+ if (question.turn_id !== turn.turn_id || question.status === "answered") continue;
3231
+ question.status = "stale";
3232
+ question.claim_fence_epoch = terminalEpoch;
3233
+ question.updated_at = turn.updated_at;
3234
+ question.history.push({
3235
+ at: turn.updated_at,
3236
+ status: "stale",
3237
+ reason: input.reason ?? "terminal_uncertain",
3238
+ });
3239
+ }
3240
+ if (input.report) transaction.canonical.reports[input.report.report_id] = input.report;
3241
+ const next =
3242
+ input.promoteQueuedTurn === false
3243
+ ? null
3244
+ : (Object.values(transaction.canonical.turns)
3245
+ .filter(candidate => candidate.status === "queued")
3246
+ .sort((left, right) => left.created_at.localeCompare(right.created_at))[0] ?? null);
3247
+ if (next) {
3248
+ const timestamp = new Date().toISOString();
3249
+ next.status = "active";
3250
+ next.started_at = timestamp;
3251
+ next.updated_at = timestamp;
3252
+ }
3253
+ transaction.canonical.queue.ordered_turn_ids = Object.values(transaction.canonical.turns)
3254
+ .filter(candidate => candidate.status === "queued")
3255
+ .map(candidate => candidate.turn_id);
3256
+ transaction.canonical.queue.active_turn_id = next?.turn_id ?? null;
3257
+ transaction.canonical.queue.selected_promotion = next
3258
+ ? { from_turn_id: turn.turn_id, to_turn_id: next.turn_id, revision: terminalEpoch }
3259
+ : null;
3260
+ transaction.canonical.desired_session_state = next ? "running" : input.desiredState;
3261
+ const eventKind = turnEventKind(turn.status) ?? "turn.terminal";
3262
+ const eventId = deterministicOutboxId(turn.session_id, terminalEpoch, eventKind, "turn", turn.turn_id);
3263
+ transaction.outbox[eventId] ??= {
3264
+ id: eventId,
3265
+ transaction_revision: terminalEpoch,
3266
+ kind: eventKind,
3267
+ entity: "turn",
3268
+ entity_id: turn.turn_id,
3269
+ payload: {
3270
+ session_id: turn.session_id,
3271
+ turn_id: turn.turn_id,
3272
+ status: turn.status,
3273
+ created_at: turn.updated_at,
3274
+ },
3275
+ emitted: false,
3276
+ };
3277
+ if (input.report) {
3278
+ const reportEventId = deterministicOutboxId(
3279
+ turn.session_id,
3280
+ terminalEpoch,
3281
+ "report.written",
3282
+ "report",
3283
+ input.report.report_id,
3284
+ );
3285
+ transaction.outbox[reportEventId] ??= {
3286
+ id: reportEventId,
3287
+ transaction_revision: terminalEpoch,
3288
+ kind: "report.written",
3289
+ entity: "report",
3290
+ entity_id: input.report.report_id,
3291
+ payload: {
3292
+ session_id: turn.session_id,
3293
+ turn_id: turn.turn_id,
3294
+ report_id: input.report.report_id,
3295
+ status: input.report.status,
3296
+ created_at: input.report.created_at,
3297
+ },
3298
+ emitted: false,
3299
+ };
3300
+ }
3301
+ return { promotedTurnId: next?.turn_id ?? null, desiredState: next ? "running" : input.desiredState };
3302
+ });
3303
+ }
3304
+
3305
+ function turnFromCanonical(turn: CoordinatorSessionTransactionV1["canonical"]["turns"][string]): TurnRecord {
3306
+ return {
3307
+ schema_version: 1,
3308
+ turn_id: turn.turn_id,
3309
+ session_id: turn.session_id,
3310
+ namespace: config.namespace,
3311
+ status: turn.status as TurnStatus,
3312
+ prompt: turn.prompt as TurnRecord["prompt"],
3313
+ delivery: turn.delivery as TurnRecord["delivery"],
3314
+ question_ids: [...turn.question_ids],
3315
+ final_response: turn.final_response as TurnRecord["final_response"],
3316
+ evidence: [...turn.evidence],
3317
+ error: turn.error as TurnRecord["error"],
3318
+ liveness: turn.liveness as TurnRecord["liveness"],
3319
+ created_at: turn.created_at,
3320
+ updated_at: turn.updated_at,
3321
+ started_at: turn.started_at,
3322
+ completed_at: turn.completed_at,
3323
+ };
3324
+ }
3325
+
3326
+ /** Rebuild every legacy projection from the committed canonical snapshot. */
3327
+ async function repairCanonicalProjections(sessionId: string): Promise<void> {
3328
+ await repairProjections(questionPaths, sessionId, async canonical => {
3329
+ await writeJsonFile(sessionFile(sessionId), sessionFromCreationSnapshot(canonical.session));
3330
+ for (const turn of Object.values(canonical.turns))
3331
+ await writeTurnRecord(namespaceDir, turnFromCanonical(turn));
3332
+ for (const report of Object.values(canonical.reports))
3333
+ await writeJsonFile(path.join(namespaceDir, "reports", `${report.report_id}.json`), report);
3334
+ const canonicalTurnIds = new Set(Object.keys(canonical.turns));
3335
+ for (const entry of await fs.readdir(turnsDir(namespaceDir)).catch(() => [])) {
3336
+ if (!entry.endsWith(".json")) continue;
3337
+ const turnId = entry.slice(0, -5);
3338
+ if (canonicalTurnIds.has(turnId)) continue;
3339
+ const projected = asRecord(await readJsonFile(path.join(turnsDir(namespaceDir), entry)));
3340
+ if (projected?.session_id === sessionId)
3341
+ await fs.rm(path.join(turnsDir(namespaceDir), entry), { force: true });
3342
+ }
3343
+ const reportsDirectory = path.join(namespaceDir, "reports");
3344
+ const canonicalReportIds = new Set(Object.keys(canonical.reports));
3345
+ for (const entry of await fs.readdir(reportsDirectory).catch(() => [])) {
3346
+ if (!entry.endsWith(".json")) continue;
3347
+ const reportId = entry.slice(0, -5);
3348
+ if (canonicalReportIds.has(reportId)) continue;
3349
+ const projected = asRecord(await readJsonFile(path.join(reportsDirectory, entry)));
3350
+ if (projected?.session_id === sessionId) await fs.rm(path.join(reportsDirectory, entry), { force: true });
3351
+ }
3352
+ const activeId = canonical.queue.selected_promotion?.to_turn_id ?? canonical.queue.active_turn_id;
3353
+ const active = activeId ? canonical.turns[activeId] : null;
3354
+ if (active) await writeActiveTurn(namespaceDir, turnFromCanonical(active));
3355
+ else {
3356
+ const prior = await readActiveTurn(namespaceDir, sessionId);
3357
+ if (prior) await clearActiveTurn(namespaceDir, prior);
3358
+ }
3359
+ await writeSessionState(namespaceDir, sessionId, canonical.desired_session_state, {
3360
+ currentTurnId: active?.turn_id ?? null,
3361
+ lastTurnId: canonical.queue.selected_promotion?.from_turn_id ?? null,
3362
+ live: null,
3363
+ reason: null,
3364
+ overwrite: true,
3365
+ });
3366
+ });
3367
+ }
3368
+
3369
+ async function projectTerminalTransition(
3370
+ turn: TurnRecord,
3371
+
3372
+ input: Parameters<typeof commitTerminalTransition>[1] & { live?: boolean | null },
3373
+ ): Promise<void> {
3374
+ await commitTerminalTransition(turn, input);
3375
+ await repairCanonicalProjections(turn.session_id);
3376
+ }
3377
+
3378
+ async function commitCanonicalTurn(
3379
+ sessionId: string,
3380
+ turn: TurnRecord,
3381
+ promptKey: string | null = null,
3382
+ ): Promise<void> {
3383
+ await ensureQuestionTransaction(sessionId);
3384
+ await withAdmittedSessionTransaction(questionPaths, sessionId, async transaction => {
3385
+ const terminal = TERMINAL_TURN_STATUSES.has(turn.status);
3386
+ transaction.canonical.turns[turn.turn_id] = {
3387
+ schema_version: 1,
3388
+ turn_id: turn.turn_id,
3389
+ session_id: sessionId,
3390
+ namespace_id: config.namespace.identity,
3391
+ status: turn.status,
3392
+ prompt: turn.prompt,
3393
+ delivery: { ...turn.delivery },
3394
+ question_ids: Object.values(transaction.canonical.questions)
3395
+ .filter(question => question.turn_id === turn.turn_id)
3396
+ .map(question => question.question_id),
3397
+ final_response: { ...turn.final_response },
3398
+ evidence: turn.evidence,
3399
+ error: turn.error ? { ...turn.error } : null,
3400
+ liveness: { ...turn.liveness },
3401
+ created_at: turn.created_at,
3402
+ updated_at: turn.updated_at,
3403
+ started_at: turn.started_at,
3404
+ completed_at: turn.completed_at,
3405
+ terminal_fence: terminal
3406
+ ? {
3407
+ epoch: transaction.revision + 1,
3408
+ status: turn.status,
3409
+ reason: null,
3410
+ at: turn.completed_at ?? turn.updated_at,
3411
+ }
3412
+ : null,
3413
+ };
3414
+ transaction.canonical.queue.ordered_turn_ids = Object.values(transaction.canonical.turns)
3415
+ .filter(candidate => candidate.status === "queued")
3416
+ .map(candidate => candidate.turn_id);
3417
+ transaction.canonical.queue.active_turn_id = terminal
3418
+ ? null
3419
+ : turn.delivery.queued
3420
+ ? transaction.canonical.queue.active_turn_id
3421
+ : turn.turn_id;
3422
+ transaction.recovery.prompt_watermark_at = turn.updated_at;
3423
+ if (terminal)
3424
+ for (const question of Object.values(transaction.canonical.questions)) {
3425
+ if (question.turn_id !== turn.turn_id || question.status === "answered") continue;
3426
+ question.status = "stale";
3427
+ question.claim_fence_epoch = transaction.revision + 1;
3428
+ question.updated_at = turn.updated_at;
3429
+ question.history.push({ at: turn.updated_at, status: "stale", reason: "terminal_uncertain" });
3430
+ }
3431
+ if (promptKey) {
3432
+ const request = transaction.requests.prompts[promptKey];
3433
+ if (!request) throw new Error("state_corrupt");
3434
+ request.phase = "completed";
3435
+ request.coordinator_turn_id = turn.turn_id;
3436
+ request.runtime_receipt = {
3437
+ accepted: true,
3438
+ command_id: String(turn.delivery.runtime_command_id),
3439
+ turn_id: String(turn.delivery.runtime_turn_id),
3440
+ };
3441
+ request.updated_at = turn.updated_at;
3442
+ }
3443
+ });
3444
+ }
3445
+
2461
3446
  async function recordAcceptedPrompt(
2462
3447
  sessionId: string,
2463
3448
  prompt: string,
2464
3449
  operation: "turn.prompt" | "turn.follow_up" | "turn.abort_and_prompt",
2465
3450
  previousActiveTurn: TurnRecord | null,
2466
3451
  acknowledgement: RuntimePromptAcknowledgement,
3452
+ promptKey: string | null = null,
2467
3453
  ): Promise<TurnRecord> {
2468
3454
  const timestamp = new Date().toISOString();
2469
3455
  if (operation === "turn.abort_and_prompt" && previousActiveTurn) {
@@ -2473,8 +3459,11 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2473
3459
  updated_at: timestamp,
2474
3460
  completed_at: timestamp,
2475
3461
  };
2476
- await writeTurnRecord(namespaceDir, superseded);
2477
- await clearActiveTurn(namespaceDir, superseded);
3462
+ await projectTerminalTransition(superseded, {
3463
+ desiredState: "running",
3464
+ reason: null,
3465
+ promoteQueuedTurn: false,
3466
+ });
2478
3467
  }
2479
3468
  const queued = operation === "turn.follow_up";
2480
3469
  const turn = makeTurnRecord(config, sessionId, prompt, queued ? "queued" : "active");
@@ -2488,6 +3477,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2488
3477
  state: "acknowledged",
2489
3478
  attempts: [{ delivered: true, channel: "runtime_ack", created_at: timestamp, reason: null }],
2490
3479
  };
3480
+ await commitCanonicalTurn(sessionId, turn, promptKey);
2491
3481
  await writeTurnRecord(namespaceDir, turn);
2492
3482
  if (!queued) {
2493
3483
  await writeActiveTurn(namespaceDir, turn);
@@ -2500,48 +3490,50 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2500
3490
  return turn;
2501
3491
  }
2502
3492
 
2503
- async function promoteNextQueuedTurn(sessionId: string): Promise<TurnRecord | null> {
2504
- const queuedTurns = (await listJsonFiles(turnsDir(namespaceDir)))
2505
- .map(turn => asRecord(turn) as TurnRecord | null)
2506
- .filter(
2507
- (turn): turn is TurnRecord => turn !== null && turn.session_id === sessionId && turn.status === "queued",
2508
- )
2509
- .sort((left, right) => left.created_at.localeCompare(right.created_at));
2510
- const next = queuedTurns[0];
2511
- if (!next) return null;
2512
- const timestamp = new Date().toISOString();
2513
- const active: TurnRecord = { ...next, status: "active", started_at: timestamp, updated_at: timestamp };
2514
- await writeTurnRecord(namespaceDir, active);
2515
- await writeActiveTurn(namespaceDir, active);
2516
- await writeSessionState(namespaceDir, sessionId, "running", {
2517
- currentTurnId: active.turn_id,
2518
- live: null,
2519
- reason: null,
2520
- });
2521
- return active;
2522
- }
2523
-
2524
3493
  async function reconcileActiveTurnAcknowledgements(): Promise<void> {
2525
3494
  const turns = (await listJsonFiles(turnsDir(namespaceDir)))
2526
3495
  .map(turn => asRecord(turn) as TurnRecord | null)
2527
3496
  .filter((turn): turn is TurnRecord => turn !== null && ACTIVE_TURN_STATUSES.has(turn.status));
2528
3497
  for (const turn of turns) {
2529
3498
  let sessionState = await readSessionState(namespaceDir, turn.session_id);
2530
- const resolvedTurn = await reconcileRuntimeAcknowledgement(
3499
+ let resolvedTurn = await reconcileRuntimeAcknowledgement(
2531
3500
  namespaceDir,
2532
3501
  turn,
2533
3502
  sessionState,
2534
3503
  promptAckTimeoutMs,
2535
3504
  { failOnTimeout: false },
2536
3505
  );
2537
- if (!ACTIVE_TURN_STATUSES.has(resolvedTurn.status)) continue;
3506
+ if (!ACTIVE_TURN_STATUSES.has(resolvedTurn.status)) {
3507
+ await projectTerminalTransition(resolvedTurn, {
3508
+ desiredState: "stale",
3509
+ reason: "terminal_uncertain",
3510
+ live: resolvedTurn.liveness.live,
3511
+ });
3512
+ continue;
3513
+ }
2538
3514
  if (resolvedTurn !== turn) sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
2539
3515
  const session = asRecord(await readJsonFile(sessionFile(resolvedTurn.session_id)));
2540
3516
  if (!session) {
2541
- await markTurnFailedForUnavailableSession(namespaceDir, resolvedTurn, "session_record_missing");
3517
+ resolvedTurn = await markTurnFailedForUnavailableSession(resolvedTurn, "session_record_missing");
3518
+ await projectTerminalTransition(resolvedTurn, {
3519
+ desiredState: "stale",
3520
+ reason: "session_unavailable",
3521
+ live: false,
3522
+ });
2542
3523
  continue;
2543
3524
  }
2544
- await reconcileRuntimeAcknowledgement(namespaceDir, resolvedTurn, sessionState, promptAckTimeoutMs);
3525
+ resolvedTurn = await reconcileRuntimeAcknowledgement(
3526
+ namespaceDir,
3527
+ resolvedTurn,
3528
+ sessionState,
3529
+ promptAckTimeoutMs,
3530
+ );
3531
+ if (!ACTIVE_TURN_STATUSES.has(resolvedTurn.status))
3532
+ await projectTerminalTransition(resolvedTurn, {
3533
+ desiredState: "stale",
3534
+ reason: "terminal_uncertain",
3535
+ live: resolvedTurn.liveness.live,
3536
+ });
2545
3537
  }
2546
3538
  }
2547
3539
 
@@ -2556,20 +3548,24 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2556
3548
  const cwd = await canonicalBrokerWorkspace(await assertCoordinatorWorkdir(config, args.cwd));
2557
3549
  const tmuxSession = optionalString(args.tmux_session) ? safeTmuxSessionName(args.tmux_session) : undefined;
2558
3550
  const tmuxTarget = optionalString(args.tmux_target) ? safeTmuxTarget(args.tmux_target) : undefined;
3551
+ const canonicalArgs = {
3552
+ session_id: sessionId,
3553
+ cwd,
3554
+ ...(tmuxSession ? { tmux_session: tmuxSession } : {}),
3555
+ ...(tmuxTarget ? { tmux_target: tmuxTarget } : {}),
3556
+ visible: args.visible !== false,
3557
+ source: optionalString(args.source) ?? "register_session",
3558
+ model: optionalString(args.model),
3559
+ allow_mutation: true,
3560
+ };
2559
3561
  return await withToolIdempotency(
2560
3562
  name,
2561
3563
  idempotencyKey,
2562
- {
2563
- session_id: sessionId,
2564
- cwd,
2565
- ...(tmuxSession ? { tmux_session: tmuxSession } : {}),
2566
- ...(tmuxTarget ? { tmux_target: tmuxTarget } : {}),
2567
- visible: args.visible !== false,
2568
- source: optionalString(args.source) ?? "register_session",
2569
- model: optionalString(args.model),
2570
- allow_mutation: true,
2571
- },
3564
+ canonicalArgs,
2572
3565
  async () => {
3566
+ const creation = await claimProductionCreation(name, idempotencyKey, canonicalArgs);
3567
+ if (creation.request.phase === "completed" && creation.request.safe_response)
3568
+ return creation.request.safe_response;
2573
3569
  const binding = await exactBrokerSessionBinding(sessionId, cwd, idempotencyKey);
2574
3570
  const session = normalizeSession({
2575
3571
  session_id: sessionId,
@@ -2583,6 +3579,14 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2583
3579
  endpoint_generation: binding.endpointGeneration,
2584
3580
  endpoint_incarnation: binding.endpointIncarnation,
2585
3581
  });
3582
+ const intent: CanonicalCreateIntentV1 = {
3583
+ kind: "register",
3584
+ session: canonicalCreationSnapshot(session),
3585
+ initial_state: "ready_for_input",
3586
+ initial_events: [],
3587
+ };
3588
+ await bindCreationRequest(questionPaths, creation.keyDigest, intent);
3589
+ await commitCreationWal(questionPaths, creation.keyDigest, intent);
2586
3590
  await writeJsonFile(sessionFile(sessionId), session);
2587
3591
  const sessionState = await writeSessionState(namespaceDir, sessionId, "ready_for_input", {
2588
3592
  live: null,
@@ -2598,13 +3602,17 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2598
3602
  visible: args.visible !== false,
2599
3603
  },
2600
3604
  });
2601
- return {
3605
+ const response = {
2602
3606
  ok: true,
2603
3607
  session: publicCoordinatorSession(session),
2604
3608
  session_state: publicCoordinatorSessionState(sessionState),
2605
3609
  registered: true,
2606
3610
  };
3611
+ await advanceCreationReceipt(questionPaths, creation.keyDigest, "projected", response);
3612
+ await advanceCreationReceipt(questionPaths, creation.keyDigest, "completed", response);
3613
+ return response;
2607
3614
  },
3615
+ true,
2608
3616
  );
2609
3617
  }
2610
3618
  if (name === "gjc_coordinator_read_status") {
@@ -2624,7 +3632,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2624
3632
  );
2625
3633
  return {
2626
3634
  ok: true,
2627
- session: publicCoordinatorSession(session),
3635
+ session: publicCoordinatorStatusSession(session),
2628
3636
  status: brokerLiveness(indexedSession ?? null),
2629
3637
  session_state: publicCoordinatorSessionState(
2630
3638
  await readSessionState(namespaceDir, canonicalSessionId),
@@ -2668,7 +3676,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2668
3676
  return sdkError(error);
2669
3677
  }
2670
3678
  }
2671
- if (name === "gjc_coordinator_list_questions") return { ok: true, questions: await listQuestions(args) };
3679
+ if (name === "gjc_coordinator_list_questions") return await listQuestions(args);
2672
3680
  if (name === "gjc_coordinator_list_artifacts") return { ok: true, roots: config.allowedRoots };
2673
3681
  if (name === "gjc_coordinator_read_artifact")
2674
3682
  return await readCoordinatorArtifact(config, { path: args.path });
@@ -2677,14 +3685,18 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2677
3685
  const brokerSessions = await listSessions();
2678
3686
  const sessionStates = jsonRecords(await listJsonFiles(path.join(namespaceDir, "session-states")));
2679
3687
  const turns = jsonRecords(await listJsonFiles(turnsDir(namespaceDir)));
2680
- const questions = jsonRecords(await listQuestions(args));
3688
+ const questions =
3689
+ args.session_id == null
3690
+ ? []
3691
+ : (await listQuestions(args)).questions.map(
3692
+ ({ answer_binding: _answerBinding, ...question }) => question,
3693
+ );
2681
3694
  const reports = jsonRecords(await listJsonFiles(path.join(namespaceDir, "reports")));
2682
3695
  const events = await readCoordinatorEvents(namespaceDir);
2683
- return {
3696
+ return capabilityFreeStatusValue({
2684
3697
  ok: true,
2685
3698
  schema_version: 1,
2686
3699
  namespace: config.namespace,
2687
- state_root: namespaceDir,
2688
3700
  transport: { mcp: "polling", push_subscriptions: false },
2689
3701
  summary: {
2690
3702
  sessions: brokerSessions.length,
@@ -2693,7 +3705,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2693
3705
  active_turns: turns.filter(turn => ACTIVE_TURN_STATUSES.has(turn.status as TurnStatus)).length,
2694
3706
  queued_turns: turns.filter(turn => turn.status === "queued").length,
2695
3707
  terminal_turns: turns.filter(turn => TERMINAL_TURN_STATUSES.has(turn.status as TurnStatus)).length,
2696
- open_questions: questions.filter(question => question.status === "open").length,
3708
+ open_questions: questions.filter(question => question.status === "pending").length,
2697
3709
  reports: reports.length,
2698
3710
  },
2699
3711
  sessions: brokerSessions.map(publicBrokerSession),
@@ -2712,10 +3724,11 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2712
3724
  events: buildCanonicalCoordinatorEvents({ sessionStates, turns, questions, reports }),
2713
3725
  latest_event_seq: await readLatestEventSeq(namespaceDir),
2714
3726
  recent_events: eventSummaries(events.slice(-10)),
2715
- };
3727
+ }) as Record<string, unknown>;
2716
3728
  }
2717
3729
  if (name === "gjc_coordinator_watch_events") {
2718
3730
  await reconcileActiveTurnAcknowledgements();
3731
+ if (args.session_id != null) await reconcileQuestions(safeExternalId("session", args.session_id));
2719
3732
  const limit = boundedEventLimit(args.limit);
2720
3733
  const timeoutMs = boundedEventWatchTimeoutMs(args.timeout_ms);
2721
3734
  let events = await readCoordinatorEvents(namespaceDir);
@@ -2762,29 +3775,31 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2762
3775
  model: typeof args.model === "string" ? args.model : null,
2763
3776
  });
2764
3777
  const reusedSessionId = args.session_id == null ? undefined : safeExternalId("session", args.session_id);
3778
+ const canonicalArgs = {
3779
+ cwd: canonicalCwd,
3780
+ task,
3781
+ ...(reusedSessionId ? { session_id: reusedSessionId } : {}),
3782
+ queue: args.queue === true,
3783
+ force: args.force === true,
3784
+ mpreset: mpresetResolution.mpreset,
3785
+ model: typeof args.model === "string" ? args.model : null,
3786
+ await_completion: args.await_completion === true,
3787
+ ...(args.await_completion === true
3788
+ ? { timeout_ms: args.timeout_ms, poll_interval_ms: args.poll_interval_ms }
3789
+ : {}),
3790
+ prompt_alias_ignored: hasTask && hasPrompt,
3791
+ allow_mutation: true,
3792
+ };
2765
3793
  return await withToolIdempotency(
2766
3794
  name,
2767
3795
  idempotencyKey,
2768
- {
2769
- cwd: canonicalCwd,
2770
- task,
2771
- ...(reusedSessionId ? { session_id: reusedSessionId } : {}),
2772
- queue: args.queue === true,
2773
- force: args.force === true,
2774
- mpreset: mpresetResolution.mpreset,
2775
- model: typeof args.model === "string" ? args.model : null,
2776
- await_completion: args.await_completion === true,
2777
- ...(args.await_completion === true
2778
- ? { timeout_ms: args.timeout_ms, poll_interval_ms: args.poll_interval_ms }
2779
- : {}),
2780
- prompt_alias_ignored: hasTask && hasPrompt,
2781
- allow_mutation: true,
2782
- },
3796
+ canonicalArgs,
2783
3797
  async () => {
2784
3798
  const delegate = async () => {
2785
3799
  let sessionId: string;
2786
3800
  let session: Record<string, unknown>;
2787
3801
  let reusedSession = false;
3802
+ let creationKey: string | null = null;
2788
3803
  if (reusedSessionId) {
2789
3804
  sessionId = reusedSessionId;
2790
3805
  const existing = asRecord(await readJsonFile(sessionFile(sessionId)));
@@ -2842,31 +3857,66 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2842
3857
  });
2843
3858
  reusedSession = true;
2844
3859
  } else {
2845
- const created = brokerResult(
2846
- await brokerSession(
2847
- canonicalCwd,
2848
- "session.create",
2849
- {
2850
- cwd: canonicalCwd,
2851
- target: coordinatorLifecycleTarget(config.sessionCommand, canonicalCwd),
2852
- ...(mpresetResolution.mpreset ? { modelPreset: mpresetResolution.mpreset } : {}),
3860
+ const creation = await claimProductionCreation(name, idempotencyKey, canonicalArgs);
3861
+ if (creation.request.phase === "completed" && creation.request.safe_response)
3862
+ return creation.request.safe_response;
3863
+ creationKey = creation.keyDigest;
3864
+ if (creation.request.canonical_create_intent) {
3865
+ session = sessionFromCreationSnapshot(creation.request.canonical_create_intent.session);
3866
+ await bindCreationRequest(
3867
+ questionPaths,
3868
+ creation.keyDigest,
3869
+ creation.request.canonical_create_intent,
3870
+ );
3871
+ await commitCreationWal(
3872
+ questionPaths,
3873
+ creation.keyDigest,
3874
+ creation.request.canonical_create_intent,
3875
+ );
3876
+ sessionId = creation.request.canonical_create_intent.session.session_id;
3877
+ } else {
3878
+ const created = brokerResult(
3879
+ await brokerSession(
3880
+ canonicalCwd,
3881
+ "session.create",
3882
+ {
3883
+ cwd: canonicalCwd,
3884
+ target: coordinatorLifecycleTarget(config.sessionCommand, canonicalCwd),
3885
+ ...(mpresetResolution.mpreset ? { modelPreset: mpresetResolution.mpreset } : {}),
3886
+ },
3887
+ idempotencyKey,
3888
+ ),
3889
+ );
3890
+ sessionId = safeExternalId("session", created.sessionId ?? created.session_id);
3891
+ const createdCwd = await canonicalBrokerWorkspace(
3892
+ optionalString(created.cwd) ?? canonicalCwd,
3893
+ );
3894
+ const binding = await exactBrokerSessionBinding(sessionId, createdCwd, idempotencyKey);
3895
+ session = normalizeSession({
3896
+ session_id: sessionId,
3897
+ cwd: createdCwd,
3898
+ ephemeral: true,
3899
+ created_at: new Date().toISOString(),
3900
+ ...(mpresetResolution.mpreset ? { mpreset: mpresetResolution.mpreset } : {}),
3901
+ broker_workspace: binding.workspace,
3902
+ endpoint_generation: binding.endpointGeneration,
3903
+ endpoint_incarnation: binding.endpointIncarnation,
3904
+ });
3905
+ const intent: CanonicalCreateIntentV1 = {
3906
+ kind: "delegate",
3907
+ workflow: delegateWorkflow,
3908
+ session: canonicalCreationSnapshot(session),
3909
+ remote_create_key: creation.request.remote_create_key,
3910
+ initial_state: "running",
3911
+ initial_prompt: {
3912
+ text: taggedPrompt,
3913
+ caller_key_digest: createHash("sha256").update(idempotencyKey).digest("hex"),
2853
3914
  },
2854
- idempotencyKey,
2855
- ),
2856
- );
2857
- sessionId = safeExternalId("session", created.sessionId ?? created.session_id);
2858
- const createdCwd = await canonicalBrokerWorkspace(optionalString(created.cwd) ?? canonicalCwd);
2859
- const binding = await exactBrokerSessionBinding(sessionId, createdCwd, idempotencyKey);
2860
- session = normalizeSession({
2861
- session_id: sessionId,
2862
- cwd: createdCwd,
2863
- ephemeral: true,
2864
- created_at: new Date().toISOString(),
2865
- ...(mpresetResolution.mpreset ? { mpreset: mpresetResolution.mpreset } : {}),
2866
- broker_workspace: binding.workspace,
2867
- endpoint_generation: binding.endpointGeneration,
2868
- endpoint_incarnation: binding.endpointIncarnation,
2869
- });
3915
+ initial_events: [],
3916
+ };
3917
+ await bindCreationRequest(questionPaths, creation.keyDigest, intent);
3918
+ await commitCreationWal(questionPaths, creation.keyDigest, intent);
3919
+ }
2870
3920
  }
2871
3921
  await writeJsonFile(sessionFile(sessionId), session);
2872
3922
  const previousActiveTurn = await readActiveTurn(namespaceDir, sessionId);
@@ -2886,6 +3936,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2886
3936
  : args.queue === true
2887
3937
  ? "turn.follow_up"
2888
3938
  : "turn.prompt";
3939
+ const promptKey = await claimCanonicalPrompt(sessionId, taggedPrompt, operation, idempotencyKey);
2889
3940
  const result = await controlSession(session, operation, { text: taggedPrompt }, idempotencyKey);
2890
3941
  const acknowledgement = requirePromptAcknowledgement(result);
2891
3942
  const turn = await recordAcceptedPrompt(
@@ -2894,6 +3945,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2894
3945
  operation,
2895
3946
  previousActiveTurn,
2896
3947
  acknowledgement,
3948
+ promptKey,
2897
3949
  );
2898
3950
  await appendCoordinatorEvent(namespaceDir, {
2899
3951
  kind: "delegation.started",
@@ -2924,6 +3976,10 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2924
3976
  result: publicSdkAcknowledgement(acknowledgement),
2925
3977
  ...(hasTask && hasPrompt ? { prompt_alias_ignored: true } : {}),
2926
3978
  };
3979
+ if (creationKey) {
3980
+ await advanceCreationReceipt(questionPaths, creationKey, "projected", response);
3981
+ await advanceCreationReceipt(questionPaths, creationKey, "completed", response);
3982
+ }
2927
3983
  return args.await_completion === true
2928
3984
  ? {
2929
3985
  ...response,
@@ -2938,6 +3994,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2938
3994
  };
2939
3995
  return reusedSessionId ? await withSessionTransition(reusedSessionId, delegate) : await delegate();
2940
3996
  },
3997
+ true,
2941
3998
  );
2942
3999
  }
2943
4000
  if (name === "gjc_coordinator_stop_session") {
@@ -2976,46 +4033,79 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
2976
4033
  };
2977
4034
  }
2978
4035
  const prompt = typeof args.prompt === "string" && args.prompt.length > 0 ? args.prompt : null;
4036
+ const canonicalArgs = {
4037
+ cwd,
4038
+ mpreset: mpresetResolution.mpreset,
4039
+ ...(prompt ? { prompt } : {}),
4040
+ allow_mutation: true,
4041
+ };
2979
4042
  return await withToolIdempotency(
2980
4043
  name,
2981
4044
  idempotencyKey,
2982
- {
2983
- cwd,
2984
- mpreset: mpresetResolution.mpreset,
2985
- ...(prompt ? { prompt } : {}),
2986
- allow_mutation: true,
2987
- },
4045
+ canonicalArgs,
2988
4046
  async () => {
2989
- const created = brokerResult(
2990
- await brokerSession(
2991
- cwd,
2992
- "session.create",
2993
- {
4047
+ const creation = await claimProductionCreation(name, idempotencyKey, canonicalArgs);
4048
+ if (creation.request.phase === "completed" && creation.request.safe_response)
4049
+ return creation.request.safe_response;
4050
+ let created: Record<string, unknown>;
4051
+ let session: Record<string, unknown>;
4052
+ let sessionId: string;
4053
+ if (creation.request.canonical_create_intent) {
4054
+ session = sessionFromCreationSnapshot(creation.request.canonical_create_intent.session);
4055
+ sessionId = creation.request.canonical_create_intent.session.session_id;
4056
+ created = { session_id: sessionId };
4057
+ } else {
4058
+ created = brokerResult(
4059
+ await brokerSession(
2994
4060
  cwd,
2995
- target: coordinatorLifecycleTarget(config.sessionCommand, cwd),
2996
- ...(mpresetResolution.mpreset ? { modelPreset: mpresetResolution.mpreset } : {}),
2997
- },
2998
- idempotencyKey,
2999
- ),
3000
- );
3001
- const sessionId = safeExternalId("session", created.sessionId ?? created.session_id);
3002
- const sessionCwd = await canonicalBrokerWorkspace(optionalString(created.cwd) ?? cwd);
3003
- const binding = await exactBrokerSessionBinding(sessionId, sessionCwd, idempotencyKey);
3004
- const session = normalizeSession({
3005
- session_id: sessionId,
3006
- cwd: sessionCwd,
3007
- ...(mpresetResolution.mpreset ? { mpreset: mpresetResolution.mpreset } : {}),
3008
- broker_workspace: binding.workspace,
3009
- endpoint_generation: binding.endpointGeneration,
3010
- endpoint_incarnation: binding.endpointIncarnation,
3011
- });
4061
+ "session.create",
4062
+ {
4063
+ cwd,
4064
+ target: coordinatorLifecycleTarget(config.sessionCommand, cwd),
4065
+ ...(mpresetResolution.mpreset ? { modelPreset: mpresetResolution.mpreset } : {}),
4066
+ },
4067
+ idempotencyKey,
4068
+ ),
4069
+ );
4070
+ sessionId = safeExternalId("session", created.sessionId ?? created.session_id);
4071
+ const sessionCwd = await canonicalBrokerWorkspace(optionalString(created.cwd) ?? cwd);
4072
+ const binding = await exactBrokerSessionBinding(sessionId, sessionCwd, idempotencyKey);
4073
+ session = normalizeSession({
4074
+ session_id: sessionId,
4075
+ cwd: sessionCwd,
4076
+ ...(mpresetResolution.mpreset ? { mpreset: mpresetResolution.mpreset } : {}),
4077
+ broker_workspace: binding.workspace,
4078
+ endpoint_generation: binding.endpointGeneration,
4079
+ endpoint_incarnation: binding.endpointIncarnation,
4080
+ });
4081
+ }
4082
+ const intent: CanonicalCreateIntentV1 = {
4083
+ kind: "start",
4084
+ session: canonicalCreationSnapshot(session),
4085
+ remote_create_key: creation.request.remote_create_key,
4086
+ initial_state: prompt ? "running" : "ready_for_input",
4087
+ initial_prompt: prompt
4088
+ ? { text: prompt, caller_key_digest: createHash("sha256").update(idempotencyKey).digest("hex") }
4089
+ : null,
4090
+ initial_events: [],
4091
+ };
4092
+ await bindCreationRequest(questionPaths, creation.keyDigest, intent);
4093
+ await commitCreationWal(questionPaths, creation.keyDigest, intent);
3012
4094
  await writeJsonFile(sessionFile(sessionId), session);
3013
4095
  const lifecycle = publicLifecycleReceipt(created, sessionId);
3014
4096
  if (prompt) {
4097
+ const promptKey = await claimCanonicalPrompt(sessionId, prompt, "turn.prompt", idempotencyKey);
3015
4098
  const result = await controlSession(session, "turn.prompt", { text: prompt }, idempotencyKey);
3016
4099
  const acknowledgement = requirePromptAcknowledgement(result);
3017
- const turn = await recordAcceptedPrompt(sessionId, prompt, "turn.prompt", null, acknowledgement);
3018
- return {
4100
+ const turn = await recordAcceptedPrompt(
4101
+ sessionId,
4102
+ prompt,
4103
+ "turn.prompt",
4104
+ null,
4105
+ acknowledgement,
4106
+ promptKey,
4107
+ );
4108
+ const response = {
3019
4109
  ok: true,
3020
4110
  session: publicCoordinatorSession(session),
3021
4111
  session_id: sessionId,
@@ -3030,6 +4120,9 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
3030
4120
  result: publicSdkAcknowledgement(acknowledgement),
3031
4121
  session_state: publicCoordinatorSessionState(await readSessionState(namespaceDir, sessionId)),
3032
4122
  };
4123
+ await advanceCreationReceipt(questionPaths, creation.keyDigest, "projected", response);
4124
+ await advanceCreationReceipt(questionPaths, creation.keyDigest, "completed", response);
4125
+ return response;
3033
4126
  }
3034
4127
  const sessionState = await writeSessionState(namespaceDir, sessionId, "ready_for_input", {
3035
4128
  live: null,
@@ -3041,13 +4134,17 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
3041
4134
  summary: `Session ${sessionId} started through SDK lifecycle control`,
3042
4135
  payloadRef: path.relative(namespaceDir, sessionFile(sessionId)),
3043
4136
  });
3044
- return {
4137
+ const response = {
3045
4138
  ok: true,
3046
4139
  session: publicCoordinatorSession(session),
3047
4140
  session_state: publicCoordinatorSessionState(sessionState),
3048
4141
  lifecycle,
3049
4142
  };
4143
+ await advanceCreationReceipt(questionPaths, creation.keyDigest, "projected", response);
4144
+ await advanceCreationReceipt(questionPaths, creation.keyDigest, "completed", response);
4145
+ return response;
3050
4146
  },
4147
+ true,
3051
4148
  );
3052
4149
  }
3053
4150
  if (name === "gjc_coordinator_send_prompt") {
@@ -3093,6 +4190,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
3093
4190
  : args.queue === true
3094
4191
  ? "turn.follow_up"
3095
4192
  : "turn.prompt";
4193
+ const promptKey = await claimCanonicalPrompt(sessionId, prompt, operation, idempotencyKey);
3096
4194
  const result = await controlSession(currentSession, operation, { text: prompt }, idempotencyKey);
3097
4195
  const acknowledgement = requirePromptAcknowledgement(result);
3098
4196
  const turn = await recordAcceptedPrompt(
@@ -3101,6 +4199,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
3101
4199
  operation,
3102
4200
  previousActiveTurn,
3103
4201
  acknowledgement,
4202
+ promptKey,
3104
4203
  );
3105
4204
  return {
3106
4205
  ok: true,
@@ -3128,37 +4227,383 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
3128
4227
  requireCoordinatorMutation(config, "questions", args);
3129
4228
  const idempotencyKey = requiredIdempotencyKey(args);
3130
4229
  const sessionId = safeExternalId("session", args.session_id);
3131
- const questionId = safeExternalId("question", args.question_id);
4230
+ const turnId = safeTurnId(args.turn_id);
4231
+ const questionId = typeof args.question_id === "string" ? args.question_id : "";
4232
+ const answerBinding = typeof args.answer_binding === "string" ? args.answer_binding : "";
3132
4233
  return await withToolIdempotency(
3133
4234
  name,
3134
4235
  idempotencyKey,
3135
4236
  {
3136
4237
  session_id: sessionId,
4238
+ turn_id: turnId,
3137
4239
  question_id: questionId,
4240
+ answer_binding: answerBinding,
3138
4241
  answer: args.answer,
3139
- ...(args.turn_id == null ? {} : { turn_id: args.turn_id }),
3140
4242
  allow_mutation: true,
3141
4243
  },
3142
4244
  async () => {
3143
- const session = asRecord(await readJsonFile(sessionFile(sessionId)));
3144
- if (!session)
4245
+ const keyDigest = createHash("sha256").update(idempotencyKey).digest("hex");
4246
+ const requestDigest = createHash("sha256")
4247
+ .update(
4248
+ canonicalJson({
4249
+ session_id: sessionId,
4250
+ turn_id: turnId,
4251
+ question_id: questionId,
4252
+ answer_binding: answerBinding,
4253
+ answer: args.answer,
4254
+ }),
4255
+ )
4256
+ .digest("hex");
4257
+ await ensureQuestionTransaction(sessionId);
4258
+ const replay = await withSessionTransaction(questionPaths, sessionId, async transaction => {
4259
+ const request = transaction.requests.answers[keyDigest];
4260
+ if (!request) return null;
4261
+ if (request.request_digest !== requestDigest)
4262
+ return {
4263
+ ok: false,
4264
+ error: {
4265
+ code: "idempotency_conflict",
4266
+ message: "Idempotency key was reused with a different answer request.",
4267
+ },
4268
+ };
4269
+ if (request.phase === "completed" && request.safe_receipt) {
4270
+ const receipt = request.safe_receipt;
4271
+ if (
4272
+ receipt.answer_hash !== request.answer_hash ||
4273
+ receipt.answer_binding_sha256 !== request.answer_binding_sha256 ||
4274
+ receipt.authority_id !== request.authority_id ||
4275
+ receipt.turn_id !== request.turn_id ||
4276
+ receipt.endpoint_incarnation !== request.endpoint_incarnation ||
4277
+ receipt.claim_fence_epoch !== request.claim_fence_epoch
4278
+ )
4279
+ return {
4280
+ ok: false,
4281
+ error: {
4282
+ code: "terminal_uncertain",
4283
+ message: "The stored answer receipt is incomplete.",
4284
+ },
4285
+ };
4286
+ return {
4287
+ ok: true,
4288
+ schema_version: 1,
4289
+ session_id: sessionId,
4290
+ turn_id: turnId,
4291
+ question_id: questionId,
4292
+ operation: "workflow.gate_answer",
4293
+ status: receipt.status,
4294
+ replayed: true,
4295
+ resolved_at: receipt.resolved_at,
4296
+ };
4297
+ }
4298
+ if (request.phase === "uncertain")
4299
+ return {
4300
+ ok: false,
4301
+ error: { code: "terminal_uncertain", message: "The previous answer outcome is uncertain." },
4302
+ };
4303
+ if (request.phase === "rejected" && request.safe_receipt)
4304
+ return {
4305
+ ok: false,
4306
+ schema_version: 1,
4307
+ session_id: sessionId,
4308
+ turn_id: turnId,
4309
+ question_id: questionId,
4310
+ error: { code: "validation_rejected", message: "Answer was rejected by the workflow gate." },
4311
+ question_status: "pending",
4312
+ };
4313
+ return null;
4314
+ });
4315
+ if (replay) return replay;
4316
+
4317
+ const reconciliation = await reconcileQuestions(sessionId);
4318
+ if (!reconciliation.reconciliation.complete)
3145
4319
  return {
3146
4320
  ok: false,
3147
- error: { code: "not_found", message: `Coordinator session not found: ${sessionId}` },
4321
+ error: { code: "terminal_uncertain", message: "Workflow gate snapshot is incomplete." },
3148
4322
  };
3149
- const result = await controlSession(
3150
- session,
3151
- "ask.answer",
3152
- { id: questionId, answer: args.answer },
3153
- idempotencyKey,
3154
- );
3155
- return {
3156
- ok: true,
3157
- session_id: sessionId,
3158
- operation: "ask.answer",
3159
- result: publicSdkAccepted(result),
3160
- };
4323
+ let translated: unknown;
4324
+ let session: Record<string, unknown> | null = null;
4325
+ const claimed = await withAdmittedSessionTransaction(questionPaths, sessionId, async transaction => {
4326
+ const question = transaction.canonical.questions[questionId];
4327
+ if (!question)
4328
+ return {
4329
+ response: { ok: false, error: { code: "not_found", message: "Question was not found." } },
4330
+ };
4331
+ if (
4332
+ question.session_id !== sessionId ||
4333
+ question.turn_id !== turnId ||
4334
+ question.endpoint_incarnation !== transaction.canonical.session.broker.endpoint_incarnation
4335
+ )
4336
+ return {
4337
+ response: {
4338
+ ok: false,
4339
+ error: {
4340
+ code: "ownership_mismatch",
4341
+ message: "Question ownership does not match this answer.",
4342
+ },
4343
+ },
4344
+ };
4345
+ const existingRequest = transaction.requests.answers[keyDigest];
4346
+ const recovering =
4347
+ (existingRequest?.phase === "remote_started" || existingRequest?.phase === "accepted") &&
4348
+ existingRequest.request_digest === requestDigest &&
4349
+ existingRequest.question_id === question.question_id &&
4350
+ existingRequest.authority_id === question.authority_id &&
4351
+ existingRequest.turn_id === question.turn_id &&
4352
+ existingRequest.endpoint_incarnation === question.endpoint_incarnation &&
4353
+ existingRequest.answer_binding_sha256 === question.binding_sha256 &&
4354
+ existingRequest.claim_fence_epoch === question.claim_fence_epoch &&
4355
+ question.answer_request_id === existingRequest.request_id;
4356
+ if (question.status === "answered" && !recovering)
4357
+ return {
4358
+ response: {
4359
+ ok: false,
4360
+ error: {
4361
+ code: "idempotency_conflict",
4362
+ message: "Question has already been answered by a different request.",
4363
+ },
4364
+ },
4365
+ };
4366
+
4367
+ if (question.status !== "pending" && !recovering)
4368
+ return {
4369
+ response: {
4370
+ ok: false,
4371
+ error: { code: "resource_gone", message: "Question is no longer answerable." },
4372
+ },
4373
+ };
4374
+ if (!answerBindingMatches(answerBinding, question.binding_plaintext))
4375
+ return {
4376
+ response: {
4377
+ ok: false,
4378
+ error: { code: "ownership_mismatch", message: "Question binding does not match." },
4379
+ },
4380
+ };
4381
+ const answer = validateCoordinatorAskAnswer(question.codec, args.answer);
4382
+ if (!answer)
4383
+ return {
4384
+ response: {
4385
+ ok: false,
4386
+ schema_version: 1,
4387
+ session_id: sessionId,
4388
+ turn_id: turnId,
4389
+ question_id: questionId,
4390
+ error: {
4391
+ code: "validation_rejected",
4392
+ message: "Answer does not match the question schema.",
4393
+ },
4394
+ question_status: "pending",
4395
+ },
4396
+ };
4397
+ const turn = await readTurnRecord(namespaceDir, turnId);
4398
+ const canonicalTurn = transaction.canonical.turns[turnId];
4399
+ const authority = transaction.canonical.gate_authorities[question.authority_id];
4400
+ const runtimeTurnId =
4401
+ authority?.observation.kind === "valid"
4402
+ ? authority.observation.first_provenance.runtime_turn_id
4403
+ : null;
4404
+ if (
4405
+ !authority ||
4406
+ !turn ||
4407
+ canonicalTurn?.terminal_fence ||
4408
+ TERMINAL_TURN_STATUSES.has(canonicalTurn?.status as TurnStatus) ||
4409
+ TERMINAL_TURN_STATUSES.has(turn.status) ||
4410
+ turn.delivery.runtime_turn_id !== runtimeTurnId
4411
+ ) {
4412
+ if (recovering && existingRequest) {
4413
+ existingRequest.phase = "uncertain";
4414
+ existingRequest.error_code = "terminal_uncertain";
4415
+ existingRequest.updated_at = new Date().toISOString();
4416
+ }
4417
+ return {
4418
+ response: {
4419
+ ok: false,
4420
+ error: {
4421
+ code: recovering ? "terminal_uncertain" : "resource_gone",
4422
+ message: recovering
4423
+ ? "The previous answer outcome is uncertain."
4424
+ : "Turn is no longer answerable.",
4425
+ },
4426
+ },
4427
+ };
4428
+ }
4429
+ translated = translateCoordinatorAskAnswer(question.codec, answer);
4430
+ session = asRecord(await readJsonFile(sessionFile(sessionId)));
4431
+ if (!session)
4432
+ return {
4433
+ response: {
4434
+ ok: false,
4435
+ error: { code: "not_found", message: "Coordinator session was not found." },
4436
+ },
4437
+ };
4438
+ if (recovering) {
4439
+ return {
4440
+ response: null,
4441
+ fence: existingRequest.claim_fence_epoch,
4442
+ gateId: authority.authority.gate_id,
4443
+ requestId: existingRequest.sdk_idempotency_key,
4444
+ };
4445
+ }
4446
+ const requestId = `c07:${createHash("sha256")
4447
+ .update(`coordinator-question-v1${question.authority_id}${requestDigest}`)
4448
+ .digest("hex")}`;
4449
+ question.status = "resolving";
4450
+ question.claim_fence_epoch = transaction.revision + 1;
4451
+ question.answer_request_id = requestId;
4452
+ question.updated_at = new Date().toISOString();
4453
+ transaction.requests.answers[keyDigest] = {
4454
+ request_id: requestId,
4455
+ key_digest: keyDigest,
4456
+ request_digest: requestDigest,
4457
+ answer_hash: createHash("sha256").update(canonicalJson(args.answer)).digest("hex"),
4458
+ answer_binding_sha256: question.binding_sha256,
4459
+ authority_id: question.authority_id,
4460
+ question_id: question.question_id,
4461
+ turn_id: question.turn_id,
4462
+ endpoint_incarnation: question.endpoint_incarnation,
4463
+ sdk_idempotency_key: requestId,
4464
+ claim_fence_epoch: question.claim_fence_epoch,
4465
+ phase: "claimed",
4466
+ created_at: question.updated_at,
4467
+ updated_at: question.updated_at,
4468
+ };
4469
+ return {
4470
+ response: null,
4471
+ fence: question.claim_fence_epoch,
4472
+ gateId: authority.authority.gate_id,
4473
+ requestId,
4474
+ };
4475
+ });
4476
+ if (claimed.response) return claimed.response;
4477
+ const gateId = (claimed as { gateId: string }).gateId;
4478
+ await withAdmittedSessionTransaction(questionPaths, sessionId, async transaction => {
4479
+ const request = transaction.requests.answers[keyDigest];
4480
+ if (!request || request.claim_fence_epoch !== (claimed as { fence: number }).fence)
4481
+ throw new Error("terminal_uncertain");
4482
+ if (request.phase === "claimed") request.phase = "remote_started";
4483
+ if (request.phase !== "remote_started" && request.phase !== "accepted")
4484
+ throw new Error("terminal_uncertain");
4485
+ request.updated_at = new Date().toISOString();
4486
+ });
4487
+
4488
+ try {
4489
+ const result = await controlSession(
4490
+ session!,
4491
+ "workflow.gate_answer",
4492
+ { id: gateId, response: translated, expectedSessionId: sessionId },
4493
+ (claimed as { requestId: string }).requestId,
4494
+ );
4495
+ const resolution = asRecord(result);
4496
+ const status = resolution?.status;
4497
+ if (status === "rejected") {
4498
+ await withAdmittedSessionTransaction(questionPaths, sessionId, async transaction => {
4499
+ const question = transaction.canonical.questions[questionId];
4500
+ if (
4501
+ question?.status === "resolving" &&
4502
+ question.claim_fence_epoch === (claimed as { fence: number }).fence
4503
+ ) {
4504
+ question.status = "pending";
4505
+ question.claim_fence_epoch = null;
4506
+ question.updated_at = new Date().toISOString();
4507
+ question.history.push({
4508
+ at: question.updated_at,
4509
+ status: "pending",
4510
+ reason: "validation_rejected",
4511
+ });
4512
+ }
4513
+ const request = transaction.requests.answers[keyDigest];
4514
+ if (request && question) {
4515
+ request.phase = "rejected";
4516
+ request.safe_receipt = {
4517
+ status: "rejected",
4518
+ answer_hash: request.answer_hash,
4519
+ answer_binding_sha256: request.answer_binding_sha256,
4520
+ authority_id: request.authority_id,
4521
+ turn_id: request.turn_id,
4522
+ endpoint_incarnation: request.endpoint_incarnation,
4523
+ claim_fence_epoch: request.claim_fence_epoch,
4524
+ resolved_at: question.updated_at,
4525
+ };
4526
+ request.updated_at = question.updated_at;
4527
+ }
4528
+ });
4529
+ return {
4530
+ ok: false,
4531
+ schema_version: 1,
4532
+ session_id: sessionId,
4533
+ turn_id: turnId,
4534
+ question_id: questionId,
4535
+ error: { code: "validation_rejected", message: "Answer was rejected by the workflow gate." },
4536
+ question_status: "pending",
4537
+ };
4538
+ }
4539
+ if (status !== "accepted")
4540
+ throw new SdkClientError("terminal_uncertain", "Workflow gate answer was not accepted.");
4541
+ const resolvedAt =
4542
+ typeof resolution?.resolved_at === "string" ? resolution.resolved_at : new Date().toISOString();
4543
+ await withAdmittedSessionTransaction(questionPaths, sessionId, async transaction => {
4544
+ const question = transaction.canonical.questions[questionId];
4545
+ if (!question || question.claim_fence_epoch !== (claimed as { fence: number }).fence)
4546
+ throw new Error("terminal_uncertain");
4547
+ question.status = "answered";
4548
+ question.answered_at = resolvedAt;
4549
+ question.updated_at = resolvedAt;
4550
+ question.history.push({ at: resolvedAt, status: "answered", reason: null });
4551
+ const authority = transaction.canonical.gate_authorities[question.authority_id];
4552
+ if (authority)
4553
+ authority.outcome = { state: "answered", turn_id: turnId, question_id: questionId };
4554
+ const request = transaction.requests.answers[keyDigest];
4555
+ if (!request) throw new Error("terminal_uncertain");
4556
+ request.phase = "accepted";
4557
+ request.safe_receipt = {
4558
+ status: "accepted",
4559
+ answer_hash: request.answer_hash,
4560
+ answer_binding_sha256: request.answer_binding_sha256,
4561
+ authority_id: request.authority_id,
4562
+ turn_id: request.turn_id,
4563
+ endpoint_incarnation: request.endpoint_incarnation,
4564
+ claim_fence_epoch: request.claim_fence_epoch,
4565
+ resolved_at: resolvedAt,
4566
+ };
4567
+ request.phase = "completed";
4568
+ request.updated_at = resolvedAt;
4569
+ });
4570
+ return {
4571
+ ok: true,
4572
+ schema_version: 1,
4573
+ session_id: sessionId,
4574
+ turn_id: turnId,
4575
+ question_id: questionId,
4576
+ operation: "workflow.gate_answer",
4577
+ status: "accepted",
4578
+ replayed: false,
4579
+ resolved_at: resolvedAt,
4580
+ };
4581
+ } catch (error) {
4582
+ await withAdmittedSessionTransaction(questionPaths, sessionId, async transaction => {
4583
+ const question = transaction.canonical.questions[questionId];
4584
+ if (
4585
+ question?.status === "resolving" &&
4586
+ question.claim_fence_epoch === (claimed as { fence: number }).fence
4587
+ ) {
4588
+ question.status = "uncertain";
4589
+ question.updated_at = new Date().toISOString();
4590
+ question.history.push({
4591
+ at: question.updated_at,
4592
+ status: "uncertain",
4593
+ reason: "terminal_uncertain",
4594
+ });
4595
+ }
4596
+ const request = transaction.requests.answers[keyDigest];
4597
+ if (request && question) {
4598
+ request.phase = "uncertain";
4599
+ request.error_code = "terminal_uncertain";
4600
+ request.updated_at = question.updated_at;
4601
+ }
4602
+ });
4603
+ return sdkError(error);
4604
+ }
3161
4605
  },
4606
+ true,
3162
4607
  );
3163
4608
  }
3164
4609
  if (name === "gjc_coordinator_report_status") {
@@ -3180,6 +4625,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
3180
4625
  allow_mutation: true,
3181
4626
  },
3182
4627
  async () => {
4628
+ const reportId = `report-${randomUUID()}`;
3183
4629
  const report = {
3184
4630
  session_id: sessionId,
3185
4631
  turn_id: args.turn_id,
@@ -3202,11 +4648,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
3202
4648
  turn = {
3203
4649
  ...turn,
3204
4650
  status: terminalStatus,
3205
- delivery: {
3206
- ...turn.delivery,
3207
- prompt_acknowledged: true,
3208
- state: "acknowledged",
3209
- },
4651
+ delivery: { ...turn.delivery, prompt_acknowledged: true, state: "acknowledged" },
3210
4652
  final_response: {
3211
4653
  text:
3212
4654
  typeof args.summary === "string"
@@ -3234,22 +4676,44 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
3234
4676
  updated_at: timestamp,
3235
4677
  completed_at: timestamp,
3236
4678
  };
3237
- await writeTurnRecord(namespaceDir, turn);
3238
- await clearActiveTurn(namespaceDir, turn);
3239
- await writeSessionState(
3240
- namespaceDir,
3241
- turn.session_id,
3242
- terminalStatus === "failed" ? "errored" : "completed",
3243
- {
3244
- lastTurnId: turn.turn_id,
3245
- live: null,
3246
- reason: terminalStatus === "failed" ? "reported_failure" : null,
3247
- },
3248
- );
3249
- await promoteNextQueuedTurn(turn.session_id);
4679
+ const canonicalReport: CanonicalReportSnapshotV1 = {
4680
+ schema_version: 1,
4681
+ report_id: reportId,
4682
+ operation_id: `report:${idempotencyKey}`,
4683
+ session_id: turn.session_id,
4684
+ turn_id: turn.turn_id,
4685
+ status: String(args.status ?? "unknown"),
4686
+ summary: typeof args.summary === "string" ? args.summary : "",
4687
+ blocker: optionalString(args.blocker),
4688
+ pr_url: optionalString(args.pr_url),
4689
+ evidence_paths: evidence.map(item => item.path),
4690
+ created_at: report.created_at,
4691
+ };
4692
+ await projectTerminalTransition(turn, {
4693
+ desiredState: terminalStatus === "failed" ? "errored" : "completed",
4694
+ reason: terminalStatus === "failed" ? "reported_failure" : null,
4695
+ report: canonicalReport,
4696
+ });
3250
4697
  }
3251
4698
  }
3252
- const reportId = `report-${randomUUID()}`;
4699
+ if (sessionId && (!args.turn_id || !asTerminalTurnStatus(args.status))) {
4700
+ await ensureQuestionTransaction(sessionId);
4701
+ await withAdmittedSessionTransaction(questionPaths, sessionId, async transaction => {
4702
+ transaction.canonical.reports[reportId] = {
4703
+ schema_version: 1,
4704
+ report_id: reportId,
4705
+ operation_id: `report:${idempotencyKey}`,
4706
+ session_id: sessionId,
4707
+ turn_id: typeof args.turn_id === "string" ? args.turn_id : "",
4708
+ status: String(args.status ?? "unknown"),
4709
+ summary: typeof args.summary === "string" ? args.summary : "",
4710
+ blocker: optionalString(args.blocker),
4711
+ pr_url: optionalString(args.pr_url),
4712
+ evidence_paths: evidence.map(item => item.path),
4713
+ created_at: report.created_at,
4714
+ };
4715
+ });
4716
+ }
3253
4717
  const reportPath = path.join(namespaceDir, "reports", `${reportId}.json`);
3254
4718
  await writeJsonFile(reportPath, report);
3255
4719
  await appendCoordinatorEvent(namespaceDir, {