@gajae-code/coding-agent 0.17.2 → 0.17.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (349) hide show
  1. package/CHANGELOG.md +194 -0
  2. package/dist/types/cli/daemon-cli.d.ts +3 -1
  3. package/dist/types/cli/github-release.d.ts +17 -0
  4. package/dist/types/cli/public-command-entry.d.ts +56 -0
  5. package/dist/types/cli/public-command-errors.d.ts +126 -0
  6. package/dist/types/cli/public-command-evidence.d.ts +99 -0
  7. package/dist/types/cli/public-command-help.d.ts +65 -0
  8. package/dist/types/cli/public-command-registry.d.ts +69 -0
  9. package/dist/types/cli/session-picker.d.ts +1 -1
  10. package/dist/types/cli/setup-cli.d.ts +1 -0
  11. package/dist/types/cli/skills-cli.d.ts +5 -0
  12. package/dist/types/cli.d.ts +3 -0
  13. package/dist/types/commands/daemon.d.ts +4 -50
  14. package/dist/types/commands/sdk.d.ts +20 -37
  15. package/dist/types/commands/setup.d.ts +3 -0
  16. package/dist/types/commands/skills.d.ts +9 -0
  17. package/dist/types/config/autorouting-tier-map.d.ts +18 -0
  18. package/dist/types/config/file-lock.d.ts +9 -1
  19. package/dist/types/config/model-profile-activation.d.ts +45 -1
  20. package/dist/types/config/model-registry.d.ts +11 -2
  21. package/dist/types/config/settings-schema.d.ts +11 -0
  22. package/dist/types/coordinator-mcp/question-state.d.ts +6 -1
  23. package/dist/types/daemon/operator-contract.d.ts +5 -1
  24. package/dist/types/eval/js/worker-protocol.d.ts +2 -0
  25. package/dist/types/extensibility/extensions/types.d.ts +12 -0
  26. package/dist/types/extensibility/gjc-plugins/registry.d.ts +4 -0
  27. package/dist/types/extensibility/runtime-skill-discovery.d.ts +18 -0
  28. package/dist/types/extensibility/shared-events.d.ts +3 -1
  29. package/dist/types/gjc-runtime/session-state-lock.d.ts +61 -0
  30. package/dist/types/internal-urls/embedded-protocol.d.ts +9 -0
  31. package/dist/types/internal-urls/index.d.ts +1 -0
  32. package/dist/types/internal-urls/parse.d.ts +9 -1
  33. package/dist/types/main.d.ts +22 -0
  34. package/dist/types/modes/acp/acp-agent.d.ts +55 -1
  35. package/dist/types/modes/acp/startup-provenance.d.ts +16 -0
  36. package/dist/types/modes/components/custom-provider-wizard.d.ts +19 -1
  37. package/dist/types/modes/components/session-selector.d.ts +5 -2
  38. package/dist/types/modes/components/user-message-selector.d.ts +5 -2
  39. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  40. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  41. package/dist/types/modes/interactive-mode.d.ts +1 -1
  42. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +11 -1
  43. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +14 -1
  44. package/dist/types/modes/types.d.ts +2 -2
  45. package/dist/types/runtime/process-lifecycle.d.ts +2 -0
  46. package/dist/types/runtime-mcp/manager.d.ts +56 -0
  47. package/dist/types/runtime-mcp/pool.d.ts +17 -0
  48. package/dist/types/runtime-mcp/redaction.d.ts +20 -0
  49. package/dist/types/runtime-mcp/startup-policy.d.ts +6 -0
  50. package/dist/types/runtime-mcp/tool-bridge.d.ts +9 -0
  51. package/dist/types/runtime-mcp/transports/http.d.ts +11 -0
  52. package/dist/types/sdk/broker/broker.d.ts +13 -2
  53. package/dist/types/sdk/broker/identity.d.ts +3 -1
  54. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +23 -3
  55. package/dist/types/sdk/broker/lifecycle.d.ts +15 -8
  56. package/dist/types/sdk/broker/runtime.d.ts +1 -1
  57. package/dist/types/sdk/broker/session-index.d.ts +22 -3
  58. package/dist/types/sdk/broker/spawn-authority.d.ts +4 -0
  59. package/dist/types/sdk/broker/stderr-drainer.d.ts +13 -0
  60. package/dist/types/sdk/bus/index.d.ts +19 -0
  61. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
  62. package/dist/types/sdk/bus/telegram-daemon.d.ts +5 -0
  63. package/dist/types/sdk/cli/master-cli.d.ts +6 -10
  64. package/dist/types/sdk/cli/rows.d.ts +19 -1
  65. package/dist/types/sdk/cli/session-cli.d.ts +22 -5
  66. package/dist/types/sdk/client/client.d.ts +2 -0
  67. package/dist/types/sdk/guides/cli.d.ts +3 -4
  68. package/dist/types/sdk/guides/verify.d.ts +2 -0
  69. package/dist/types/sdk/host/host.d.ts +9 -0
  70. package/dist/types/sdk/host/session-runtime.d.ts +31 -0
  71. package/dist/types/sdk/host/types.d.ts +10 -0
  72. package/dist/types/sdk/index.d.ts +1 -0
  73. package/dist/types/sdk/prompt-deadline-flush.d.ts +83 -0
  74. package/dist/types/sdk/prompt-deadline-manager.d.ts +37 -0
  75. package/dist/types/sdk/prompt-tool-boundary.d.ts +73 -0
  76. package/dist/types/sdk/router/session-router.d.ts +2 -0
  77. package/dist/types/sdk/startup-capability.d.ts +2 -0
  78. package/dist/types/sdk/transport/relay.d.ts +1 -0
  79. package/dist/types/sdk/transport/serve-cli.d.ts +48 -4
  80. package/dist/types/sdk/transport/stdio.d.ts +5 -1
  81. package/dist/types/session/agent-session.d.ts +168 -21
  82. package/dist/types/session/fallback-chain-controller.d.ts +6 -6
  83. package/dist/types/session/fold-coordinator.d.ts +17 -3
  84. package/dist/types/session/internal/managed-session-storage.d.ts +7 -1
  85. package/dist/types/session/session-manager.d.ts +64 -1
  86. package/dist/types/session/session-work-lease.d.ts +14 -0
  87. package/dist/types/setup/model-onboarding-guidance.d.ts +1 -1
  88. package/dist/types/setup/provider-onboarding.d.ts +81 -1
  89. package/dist/types/tools/bash.d.ts +37 -11
  90. package/dist/types/tools/job.d.ts +1 -1
  91. package/dist/types/tools/steer-fold.d.ts +31 -0
  92. package/dist/types/utils/git.d.ts +28 -2
  93. package/dist/types/utils/mupdf-embedded.d.ts +2 -0
  94. package/dist/types/utils/mupdf.d.ts +4 -0
  95. package/dist/types/utils/session-title-generation.d.ts +6 -0
  96. package/dist/types/utils/title-generator.d.ts +11 -2
  97. package/dist/types/web/scrapers/types.d.ts +3 -0
  98. package/package.json +18 -10
  99. package/scripts/build-binary.ts +9 -3
  100. package/scripts/compile-args.ts +0 -9
  101. package/scripts/embed-mupdf.ts +52 -0
  102. package/scripts/generate-sdk-operation-inventory.ts +31 -1
  103. package/scripts/vendor-markit.ts +196 -0
  104. package/src/async/job-manager.ts +22 -1
  105. package/src/cli/daemon-cli.ts +53 -18
  106. package/src/cli/github-release.ts +87 -5
  107. package/src/cli/mcp-cli.ts +38 -4
  108. package/src/cli/public-command-entry.ts +450 -0
  109. package/src/cli/public-command-errors.ts +581 -0
  110. package/src/cli/public-command-evidence.ts +717 -0
  111. package/src/cli/public-command-help.ts +360 -0
  112. package/src/cli/public-command-registry.ts +535 -0
  113. package/src/cli/session-picker.ts +2 -1
  114. package/src/cli/setup-cli.ts +9 -1
  115. package/src/cli/skills-cli.ts +46 -2
  116. package/src/cli.ts +61 -1
  117. package/src/commands/daemon.ts +65 -69
  118. package/src/commands/sdk.ts +175 -117
  119. package/src/commands/setup.ts +2 -0
  120. package/src/commands/skills.ts +11 -0
  121. package/src/config/autorouting-tier-map.ts +14 -0
  122. package/src/config/file-lock.ts +147 -51
  123. package/src/config/model-profile-activation.ts +334 -43
  124. package/src/config/model-registry.ts +183 -25
  125. package/src/config/settings-schema.ts +26 -2
  126. package/src/coordinator-mcp/policy.ts +10 -5
  127. package/src/coordinator-mcp/question-gate-codec.ts +17 -0
  128. package/src/coordinator-mcp/question-state.ts +175 -8
  129. package/src/coordinator-mcp/server.ts +507 -81
  130. package/src/coordinator-mcp/session-reaper.ts +6 -4
  131. package/src/crash/record-loader.ts +6 -2
  132. package/src/daemon/operator-contract.ts +26 -12
  133. package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
  134. package/src/eval/js/context-manager.ts +8 -8
  135. package/src/eval/js/worker-core.ts +9 -4
  136. package/src/eval/js/worker-protocol.ts +2 -0
  137. package/src/extensibility/extensions/runner.ts +52 -4
  138. package/src/extensibility/extensions/types.ts +10 -0
  139. package/src/extensibility/gjc-plugins/registry.ts +196 -37
  140. package/src/extensibility/runtime-skill-discovery.ts +61 -3
  141. package/src/extensibility/shared-events.ts +3 -1
  142. package/src/gjc-runtime/session-state-lock.ts +165 -24
  143. package/src/gjc-runtime/ultragoal-succession.ts +31 -3
  144. package/src/internal-urls/docs-index.generated.ts +20 -19
  145. package/src/internal-urls/embedded-protocol.ts +83 -0
  146. package/src/internal-urls/index.ts +1 -0
  147. package/src/internal-urls/parse.ts +21 -1
  148. package/src/internal-urls/router.ts +4 -2
  149. package/src/main.ts +123 -55
  150. package/src/modes/acp/acp-agent.ts +408 -35
  151. package/src/modes/acp/startup-provenance.ts +59 -0
  152. package/src/modes/components/custom-provider-wizard.ts +308 -7
  153. package/src/modes/components/oauth-selector.ts +9 -0
  154. package/src/modes/components/session-selector.ts +47 -4
  155. package/src/modes/components/user-message-selector.ts +57 -15
  156. package/src/modes/controllers/command-controller.ts +58 -32
  157. package/src/modes/controllers/extension-ui-controller.ts +33 -7
  158. package/src/modes/controllers/input-controller.ts +4 -0
  159. package/src/modes/controllers/selector-controller.ts +161 -15
  160. package/src/modes/interactive-mode.ts +21 -7
  161. package/src/modes/runtime-init.ts +1 -0
  162. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +124 -7
  163. package/src/modes/shared/agent-wire/workflow-gate-types.ts +16 -2
  164. package/src/modes/types.ts +2 -2
  165. package/src/prompts/agent-fragments/browser-aside.md +1 -0
  166. package/src/prompts/system/title-system.md +1 -1
  167. package/src/prompts/tools/bash.md +1 -1
  168. package/src/prompts/tools/job.md +1 -1
  169. package/src/prompts/tools/read.md +2 -0
  170. package/src/runtime/network-prewarm-service.test.ts +36 -1
  171. package/src/runtime/network-prewarm-service.ts +28 -3
  172. package/src/runtime/process-lifecycle.ts +22 -11
  173. package/src/runtime-mcp/config.ts +16 -1
  174. package/src/runtime-mcp/manager.ts +689 -55
  175. package/src/runtime-mcp/pool.test.ts +31 -0
  176. package/src/runtime-mcp/pool.ts +304 -28
  177. package/src/runtime-mcp/redaction.ts +42 -0
  178. package/src/runtime-mcp/startup-policy.ts +7 -0
  179. package/src/runtime-mcp/tool-bridge.ts +71 -0
  180. package/src/runtime-mcp/transports/http.ts +108 -25
  181. package/src/sdk/acp/adapter.ts +27 -8
  182. package/src/sdk/broker/broker.ts +378 -52
  183. package/src/sdk/broker/identity.ts +9 -1
  184. package/src/sdk/broker/lifecycle-ledger.ts +95 -31
  185. package/src/sdk/broker/lifecycle.ts +690 -141
  186. package/src/sdk/broker/runtime.ts +1 -1
  187. package/src/sdk/broker/session-index.ts +97 -4
  188. package/src/sdk/broker/spawn-authority.ts +35 -7
  189. package/src/sdk/broker/stderr-drainer.ts +59 -0
  190. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  191. package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
  192. package/src/sdk/bus/index.ts +690 -80
  193. package/src/sdk/bus/telegram-daemon-cli.ts +31 -2
  194. package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
  195. package/src/sdk/bus/telegram-daemon.ts +10 -3
  196. package/src/sdk/cli/master-cli.ts +47 -36
  197. package/src/sdk/cli/rows.ts +30 -7
  198. package/src/sdk/cli/session-cli.ts +687 -184
  199. package/src/sdk/client/client.ts +18 -0
  200. package/src/sdk/guides/catalog.ts +2 -2
  201. package/src/sdk/guides/cli.ts +20 -62
  202. package/src/sdk/guides/verify.ts +14 -0
  203. package/src/sdk/host/host.ts +49 -5
  204. package/src/sdk/host/session-runtime.test.ts +138 -2
  205. package/src/sdk/host/session-runtime.ts +311 -97
  206. package/src/sdk/host/types.ts +12 -1
  207. package/src/sdk/host/websocket-transport.lifecycle.test.ts +84 -0
  208. package/src/sdk/host/websocket-transport.ts +40 -6
  209. package/src/sdk/index.ts +11 -0
  210. package/src/sdk/prompt-deadline-flush.ts +334 -0
  211. package/src/sdk/prompt-deadline-manager.ts +133 -1
  212. package/src/sdk/prompt-tool-boundary.ts +121 -0
  213. package/src/sdk/protocol/operation-inventory.generated.json +106 -0
  214. package/src/sdk/router/session-router.ts +9 -1
  215. package/src/sdk/session.ts +182 -7
  216. package/src/sdk/startup-capability.ts +1 -1
  217. package/src/sdk/transport/relay.ts +71 -6
  218. package/src/sdk/transport/serve-cli.ts +397 -42
  219. package/src/sdk/transport/socket.ts +12 -11
  220. package/src/sdk/transport/stdio.ts +7 -3
  221. package/src/session/agent-session.ts +2437 -400
  222. package/src/session/fallback-chain-controller.ts +6 -7
  223. package/src/session/fold-coordinator.ts +24 -5
  224. package/src/session/internal/managed-session-scope.ts +58 -18
  225. package/src/session/internal/managed-session-storage.ts +304 -11
  226. package/src/session/session-manager.ts +382 -44
  227. package/src/session/session-work-lease.ts +34 -0
  228. package/src/setup/model-onboarding-guidance.ts +1 -1
  229. package/src/setup/provider-onboarding.ts +397 -24
  230. package/src/slash-commands/builtin-registry.ts +258 -14
  231. package/src/task/index.ts +14 -2
  232. package/src/tools/bash.ts +98 -78
  233. package/src/tools/fetch.ts +119 -20
  234. package/src/tools/job.ts +151 -25
  235. package/src/tools/output-meta.ts +15 -2
  236. package/src/tools/path-utils.ts +27 -3
  237. package/src/tools/read.ts +3 -3
  238. package/src/tools/steer-fold.ts +107 -0
  239. package/src/tools/tool-catalog.generated.ts +29 -2
  240. package/src/tools/tool-errors.ts +4 -0
  241. package/src/utils/git.ts +53 -3
  242. package/src/utils/markit.ts +55 -41
  243. package/src/utils/mupdf-assets.d.ts +4 -0
  244. package/src/utils/mupdf-embedded.ts +4 -0
  245. package/src/utils/mupdf.ts +129 -0
  246. package/src/utils/session-title-generation.ts +29 -0
  247. package/src/utils/title-generator.ts +46 -2
  248. package/src/web/scrapers/types.ts +24 -3
  249. package/vendor/markit-ai/LICENSE +21 -0
  250. package/vendor/markit-ai/MANIFEST.json +412 -0
  251. package/vendor/markit-ai/dist/commands/config.d.ts +4 -0
  252. package/vendor/markit-ai/dist/commands/config.js +139 -0
  253. package/vendor/markit-ai/dist/commands/convert.d.ts +6 -0
  254. package/vendor/markit-ai/dist/commands/convert.js +113 -0
  255. package/vendor/markit-ai/dist/commands/formats.d.ts +2 -0
  256. package/vendor/markit-ai/dist/commands/formats.js +92 -0
  257. package/vendor/markit-ai/dist/commands/init.d.ts +2 -0
  258. package/vendor/markit-ai/dist/commands/init.js +29 -0
  259. package/vendor/markit-ai/dist/commands/onboard.d.ts +2 -0
  260. package/vendor/markit-ai/dist/commands/onboard.js +61 -0
  261. package/vendor/markit-ai/dist/commands/plugin.d.ts +4 -0
  262. package/vendor/markit-ai/dist/commands/plugin.js +58 -0
  263. package/vendor/markit-ai/dist/config.d.ts +26 -0
  264. package/vendor/markit-ai/dist/config.js +42 -0
  265. package/vendor/markit-ai/dist/converters/audio.d.ts +7 -0
  266. package/vendor/markit-ai/dist/converters/audio.js +104 -0
  267. package/vendor/markit-ai/dist/converters/csv.d.ts +7 -0
  268. package/vendor/markit-ai/dist/converters/csv.js +83 -0
  269. package/vendor/markit-ai/dist/converters/docx.d.ts +6 -0
  270. package/vendor/markit-ai/dist/converters/docx.js +57 -0
  271. package/vendor/markit-ai/dist/converters/epub.d.ts +8 -0
  272. package/vendor/markit-ai/dist/converters/epub.js +128 -0
  273. package/vendor/markit-ai/dist/converters/github.d.ts +18 -0
  274. package/vendor/markit-ai/dist/converters/github.js +148 -0
  275. package/vendor/markit-ai/dist/converters/html.d.ts +6 -0
  276. package/vendor/markit-ai/dist/converters/html.js +30 -0
  277. package/vendor/markit-ai/dist/converters/image.d.ts +6 -0
  278. package/vendor/markit-ai/dist/converters/image.js +124 -0
  279. package/vendor/markit-ai/dist/converters/ipynb.d.ts +6 -0
  280. package/vendor/markit-ai/dist/converters/ipynb.js +75 -0
  281. package/vendor/markit-ai/dist/converters/iwork.d.ts +20 -0
  282. package/vendor/markit-ai/dist/converters/iwork.js +391 -0
  283. package/vendor/markit-ai/dist/converters/json.d.ts +6 -0
  284. package/vendor/markit-ai/dist/converters/json.js +21 -0
  285. package/vendor/markit-ai/dist/converters/pdf/columns.d.ts +35 -0
  286. package/vendor/markit-ai/dist/converters/pdf/columns.js +93 -0
  287. package/vendor/markit-ai/dist/converters/pdf/extract.d.ts +19 -0
  288. package/vendor/markit-ai/dist/converters/pdf/extract.js +514 -0
  289. package/vendor/markit-ai/dist/converters/pdf/grid.d.ts +25 -0
  290. package/vendor/markit-ai/dist/converters/pdf/grid.js +795 -0
  291. package/vendor/markit-ai/dist/converters/pdf/headers.d.ts +24 -0
  292. package/vendor/markit-ai/dist/converters/pdf/headers.js +108 -0
  293. package/vendor/markit-ai/dist/converters/pdf/index.d.ts +19 -0
  294. package/vendor/markit-ai/dist/converters/pdf/index.js +136 -0
  295. package/vendor/markit-ai/dist/converters/pdf/mupdf-loader.js +9 -0
  296. package/vendor/markit-ai/dist/converters/pdf/render.d.ts +24 -0
  297. package/vendor/markit-ai/dist/converters/pdf/render.js +513 -0
  298. package/vendor/markit-ai/dist/converters/pdf/types.d.ts +75 -0
  299. package/vendor/markit-ai/dist/converters/pdf/types.js +1 -0
  300. package/vendor/markit-ai/dist/converters/plain-text.d.ts +6 -0
  301. package/vendor/markit-ai/dist/converters/plain-text.js +87 -0
  302. package/vendor/markit-ai/dist/converters/pptx.d.ts +8 -0
  303. package/vendor/markit-ai/dist/converters/pptx.js +289 -0
  304. package/vendor/markit-ai/dist/converters/rss.d.ts +11 -0
  305. package/vendor/markit-ai/dist/converters/rss.js +138 -0
  306. package/vendor/markit-ai/dist/converters/wikipedia.d.ts +6 -0
  307. package/vendor/markit-ai/dist/converters/wikipedia.js +33 -0
  308. package/vendor/markit-ai/dist/converters/xlsx.d.ts +8 -0
  309. package/vendor/markit-ai/dist/converters/xlsx.js +147 -0
  310. package/vendor/markit-ai/dist/converters/xml.d.ts +6 -0
  311. package/vendor/markit-ai/dist/converters/xml.js +18 -0
  312. package/vendor/markit-ai/dist/converters/yaml.d.ts +6 -0
  313. package/vendor/markit-ai/dist/converters/yaml.js +17 -0
  314. package/vendor/markit-ai/dist/converters/zip.d.ts +8 -0
  315. package/vendor/markit-ai/dist/converters/zip.js +57 -0
  316. package/vendor/markit-ai/dist/index.d.ts +30 -0
  317. package/vendor/markit-ai/dist/index.js +26 -0
  318. package/vendor/markit-ai/dist/main.d.ts +2 -0
  319. package/vendor/markit-ai/dist/main.js +191 -0
  320. package/vendor/markit-ai/dist/markit.d.ts +45 -0
  321. package/vendor/markit-ai/dist/markit.js +267 -0
  322. package/vendor/markit-ai/dist/plugins/api.d.ts +7 -0
  323. package/vendor/markit-ai/dist/plugins/api.js +42 -0
  324. package/vendor/markit-ai/dist/plugins/index.d.ts +4 -0
  325. package/vendor/markit-ai/dist/plugins/index.js +3 -0
  326. package/vendor/markit-ai/dist/plugins/installer.d.ts +25 -0
  327. package/vendor/markit-ai/dist/plugins/installer.js +176 -0
  328. package/vendor/markit-ai/dist/plugins/loader.d.ts +6 -0
  329. package/vendor/markit-ai/dist/plugins/loader.js +61 -0
  330. package/vendor/markit-ai/dist/plugins/types.d.ts +25 -0
  331. package/vendor/markit-ai/dist/plugins/types.js +1 -0
  332. package/vendor/markit-ai/dist/providers/anthropic.d.ts +2 -0
  333. package/vendor/markit-ai/dist/providers/anthropic.js +47 -0
  334. package/vendor/markit-ai/dist/providers/index.d.ts +21 -0
  335. package/vendor/markit-ai/dist/providers/index.js +56 -0
  336. package/vendor/markit-ai/dist/providers/openai.d.ts +2 -0
  337. package/vendor/markit-ai/dist/providers/openai.js +76 -0
  338. package/vendor/markit-ai/dist/providers/types.d.ts +26 -0
  339. package/vendor/markit-ai/dist/providers/types.js +1 -0
  340. package/vendor/markit-ai/dist/types.d.ts +36 -0
  341. package/vendor/markit-ai/dist/types.js +1 -0
  342. package/vendor/markit-ai/dist/utils/exit-codes.d.ts +4 -0
  343. package/vendor/markit-ai/dist/utils/exit-codes.js +4 -0
  344. package/vendor/markit-ai/dist/utils/output.d.ts +22 -0
  345. package/vendor/markit-ai/dist/utils/output.js +31 -0
  346. package/vendor/markit-ai/dist/utils/turndown.d.ts +8 -0
  347. package/vendor/markit-ai/dist/utils/turndown.js +70 -0
  348. package/vendor/markit-ai/package.json +74 -0
  349. package/vendor/markit-ai.patch +49 -0
@@ -4,7 +4,7 @@ import * as fs from "node:fs/promises";
4
4
  import * as path from "node:path";
5
5
  import { getAgentDir, isKnownSinkPeerClosedError, logger } from "@gajae-code/utils";
6
6
  import { normalizePathForComparison, VERSION } from "@gajae-code/utils/dirs";
7
- import { withFileLock } from "../config/file-lock";
7
+ import { isFileLockAcquireTimeout, withFileLock } from "../config/file-lock";
8
8
  import {
9
9
  COORDINATOR_MCP_PROTOCOL_VERSION,
10
10
  COORDINATOR_MCP_SERVER_NAME,
@@ -27,6 +27,7 @@ import type { WorkflowGate, WorkflowGateQueryRecord } from "../modes/shared/agen
27
27
  import type { BrokerDiscovery } from "../sdk/broker/discovery";
28
28
  import { endpointIncarnation } from "../sdk/broker/endpoint-authority";
29
29
  import { type EnsureBrokerSettings, ensureBroker } from "../sdk/broker/ensure";
30
+ import { DEAD_HOST_HEARTBEAT_SILENCE_MS } from "../sdk/broker/session-index";
30
31
  import { lifecycleRequestTimeoutMs } from "../sdk/broker/startup-budget";
31
32
  import { UnsupportedStateVersionError } from "../sdk/broker/state-version";
32
33
  import { SdkClient, SdkClientError } from "../sdk/client/client";
@@ -39,6 +40,7 @@ import {
39
40
  SessionActivationError,
40
41
  } from "../sdk/session-activation";
41
42
  import { SessionListTraversalError, sessionListPageFromResponse, traverseSessionList } from "../sdk/session-list";
43
+ import { sanitizeSdkStartupMessage } from "../sdk/startup-capability";
42
44
  import {
43
45
  ackCodexWakeEvent,
44
46
  bindDelegateCodexHandoff,
@@ -144,6 +146,7 @@ import {
144
146
  recoverExpiredPublicDelivery,
145
147
  releasePublicDeliveryClaim,
146
148
  removeSessionTransaction,
149
+ repairLegacyReportIds,
147
150
  repairProjections,
148
151
  replaceCreationRetirementIntent,
149
152
  rotateClaimedCreationVerifier,
@@ -516,6 +519,11 @@ const OBSERVED_BROKER_FAILURE_CODES = new Set([
516
519
  const MISSING_FINAL_RESPONSE_ADVISORY = "completion_missing_final_response";
517
520
  const PROMPT_ACK_TIMEOUT_REASON = "runtime_prompt_ack_timeout";
518
521
  const DEFAULT_RUNTIME_PROMPT_ACK_TIMEOUT_MS = 10_000;
522
+ function coordinatorKnownSecrets(): string[] {
523
+ return Object.entries(process.env)
524
+ .filter(([name, value]) => value && /(?:token|secret|password|credential|api[_-]?key|auth)/iu.test(name))
525
+ .map(([, value]) => value!);
526
+ }
519
527
  /**
520
528
  * Pagination window for `readCompleteQ12Snapshot`: it stops starting pages once
521
529
  * this much time has passed and gives each page only the remainder as its reply
@@ -527,6 +535,26 @@ const Q12_SNAPSHOT_BUDGET_MS = 5_000;
527
535
  const MAX_RUNTIME_SESSIONS_PER_WATCH_PASS = 4;
528
536
  const MAX_Q12_ATTEMPTS_PER_WATCH_PASS = 2;
529
537
  const MAX_RUNTIME_PROMPT_ACK_TIMEOUT_MS = 5 * 60 * 1000;
538
+ /**
539
+ * A delete intent is superseded once another deletion for the same session and
540
+ * endpoint incarnation has completed: force eviction retires a stale endpoint under
541
+ * its own record and leaves behind the intent an earlier idle reap admitted.
542
+ */
543
+ function isSupersededDeletionIntent(
544
+ entry: NamespaceDeletionEntryV1,
545
+ deletions: readonly NamespaceDeletionEntryV1[],
546
+ ): boolean {
547
+ return (
548
+ entry.phase === "intent" &&
549
+ deletions.some(
550
+ other =>
551
+ other !== entry &&
552
+ other.session_id === entry.session_id &&
553
+ other.endpoint_incarnation === entry.endpoint_incarnation &&
554
+ other.phase === "completed",
555
+ )
556
+ );
557
+ }
530
558
  const ACTIVE_TURN_STATUSES = new Set<TurnStatus>(["delivering", "active", "waiting_for_answer", "completing"]);
531
559
  const TERMINAL_TURN_STATUSES = new Set<TurnStatus>(["completed", "failed", "cancelled", "superseded"]);
532
560
  const TURN_ID_PATTERN = /^turn-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -741,7 +769,8 @@ function toolSchema(
741
769
  if (name === "gjc_coordinator_await_turn") {
742
770
  return {
743
771
  name,
744
- description: "Poll a durable turn for a bounded time and return the same shape as read_turn.",
772
+ description:
773
+ "Poll a durable turn for a bounded time. Observation expiry returns wait_expired:true (legacy ok:false, reason:timeout) without an MCP error. Inspect turn.status for completion; await again without resending the prompt.",
745
774
  inputSchema: {
746
775
  type: "object",
747
776
  properties: {
@@ -1314,6 +1343,11 @@ interface CoordinatorToolIdempotencyRecord {
1314
1343
  request_digest: string;
1315
1344
  state: "in_progress" | "completed";
1316
1345
  response?: Record<string, unknown>;
1346
+ /** Reused-session delegate prompt-claim provenance, written under the key lock:
1347
+ * `false` when the attempt starts, `true` immediately before the canonical prompt
1348
+ * claim. A same-key retry may claim afresh only on an explicit `false`; `true` or
1349
+ * an absent marker (unknown provenance) keeps missing live state fail-closed. */
1350
+ delegate_prompt_claim_started?: boolean;
1317
1351
  admission?: DelegateAdmissionCheckpoint;
1318
1352
  delegate_response_pin?: DelegateResponsePinV1;
1319
1353
  created_at: string;
@@ -1393,7 +1427,24 @@ function boundedPublicValue(value: unknown, budget: { remaining: number }, depth
1393
1427
  : undefined;
1394
1428
  const code =
1395
1429
  typeof rawCode === "string" && Object.hasOwn(PUBLIC_ERROR_MESSAGES, rawCode) ? rawCode : "unavailable";
1396
- output[key] = { code, message: PUBLIC_ERROR_MESSAGES[code] };
1430
+ const rawDiagnostic =
1431
+ field && typeof field === "object" && !Array.isArray(field)
1432
+ ? (field as Record<string, unknown>).diagnostic
1433
+ : undefined;
1434
+ const boundedDiagnostic =
1435
+ typeof rawDiagnostic === "string" ? boundedPublicValue(rawDiagnostic, budget, depth + 1) : undefined;
1436
+ const rawMessage =
1437
+ field && typeof field === "object" && !Array.isArray(field)
1438
+ ? (field as Record<string, unknown>).message
1439
+ : undefined;
1440
+ output[key] = {
1441
+ code,
1442
+ message:
1443
+ code === "spawn_failed" && typeof rawMessage === "string" && boundedDiagnostic !== undefined
1444
+ ? boundedPublicValue(rawMessage, budget, depth + 1)
1445
+ : PUBLIC_ERROR_MESSAGES[code],
1446
+ ...(boundedDiagnostic === undefined ? {} : { diagnostic: boundedDiagnostic }),
1447
+ };
1397
1448
  } else {
1398
1449
  output[key] = sensitivePublicField(key) ? "[redacted]" : boundedPublicValue(field, budget, depth + 1);
1399
1450
  }
@@ -3855,7 +3906,25 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
3855
3906
  ): Promise<Map<string, string>> {
3856
3907
  const authorized = new Map<string, string>();
3857
3908
  for (const sessionId of new Set(sessionIds)) {
3858
- const transaction = await readSessionTransaction(questionPaths, sessionId);
3909
+ // A corrupt or unreadable peer session must never abort authorization for
3910
+ // every other session. Only the explicitly scoped session may propagate,
3911
+ // otherwise one bad WAL record makes namespace-wide reads permanently
3912
+ // unavailable instead of degrading to the sessions that are still sound.
3913
+ let transaction: CoordinatorSessionTransactionV1 | null;
3914
+ try {
3915
+ transaction = await readSessionTransaction(questionPaths, sessionId);
3916
+ } catch (error) {
3917
+ logger.warn("Coordinator authorization skipped unreadable session", {
3918
+ sessionId,
3919
+ error: error instanceof Error ? error.message : String(error),
3920
+ });
3921
+ // Only a structurally corrupt WAL is degradable for an unscoped
3922
+ // namespace sweep. Cancellation, I/O, and unexpected invariant errors
3923
+ // must remain visible instead of silently shrinking authorization.
3924
+ if (scopedSessionId === sessionId || !(error instanceof Error && error.message === "state_corrupt"))
3925
+ throw error;
3926
+ continue;
3927
+ }
3859
3928
  if (!transaction) {
3860
3929
  if (scopedSessionId === sessionId && !allowMissingSessionIds.has(sessionId))
3861
3930
  throw new Error("resource_gone");
@@ -4454,6 +4523,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
4454
4523
  questionId: string;
4455
4524
  eventId: string;
4456
4525
  }> = [];
4526
+ let pendingGateCount = 0;
4457
4527
  let q12Admitted = true;
4458
4528
  let liveInFlightAskAdmitted = false;
4459
4529
  let liveInFlightCandidate = false;
@@ -4543,10 +4613,19 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
4543
4613
  byRuntimeTurn.set(runtimeTurnId, owners);
4544
4614
  }
4545
4615
  for (const row of items as WorkflowGateQueryRecord[]) {
4546
- if (!row || typeof row !== "object" || row.tag !== "pending" || typeof row.gate_id !== "string") {
4616
+ if (!row || typeof row !== "object" || typeof row.gate_id !== "string") {
4617
+ diagnostic("invalid_gate_row");
4618
+ continue;
4619
+ }
4620
+ // Accepted and quarantined rows are durable diagnostics, not actionable
4621
+ // questions. They remain in Q12 so SDK clients can explain a completed or
4622
+ // lost gate, but reconciliation must not classify them as malformed.
4623
+ if (row.tag === "accepted" || row.tag === "quarantined") continue;
4624
+ if (row.tag !== "pending") {
4547
4625
  diagnostic("invalid_gate_row");
4548
4626
  continue;
4549
4627
  }
4628
+ pendingGateCount++;
4550
4629
  const gate = row as WorkflowGateQueryRecord & WorkflowGate;
4551
4630
  const authorityId = createHash("sha256")
4552
4631
  .update(
@@ -4792,7 +4871,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
4792
4871
  // even while an authenticated, acknowledged turn is still running.
4793
4872
  // Nonempty snapshots still require an admitted ask; all writer, waiting
4794
4873
  // token and terminal fences above remain authoritative.
4795
- if (!q12Admitted || (liveInFlightCandidate && items.length > 0 && !liveInFlightAskAdmitted))
4874
+ if (!q12Admitted || (liveInFlightCandidate && pendingGateCount > 0 && !liveInFlightAskAdmitted))
4796
4875
  return {
4797
4876
  ok: true,
4798
4877
  schema_version: 1,
@@ -5137,6 +5216,11 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
5137
5216
  return asRecord(response.error)?.code === "ambiguous";
5138
5217
  }
5139
5218
 
5219
+ function isTransientDelegateAuthorityFailure(response: Record<string, unknown>): boolean {
5220
+ if (response.ok !== false) return false;
5221
+ return asRecord(response.error)?.code === "unavailable";
5222
+ }
5223
+
5140
5224
  function publicErrorCode(code: unknown): string {
5141
5225
  return typeof code === "string" && Object.hasOwn(PUBLIC_ERROR_MESSAGES, code) ? code : "unavailable";
5142
5226
  }
@@ -5147,7 +5231,22 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
5147
5231
  const messageCode =
5148
5232
  error instanceof Error && Object.hasOwn(PUBLIC_ERROR_MESSAGES, error.message) ? error.message : undefined;
5149
5233
  const code = publicErrorCode(directCode ?? messageCode);
5150
- return { ok: false, error: { code, message: PUBLIC_ERROR_MESSAGES[code] } };
5234
+ const diagnostic =
5235
+ error instanceof SdkClientError && code === "spawn_failed"
5236
+ ? sanitizeSdkStartupMessage(error.message, coordinatorKnownSecrets())
5237
+ : undefined;
5238
+ const message =
5239
+ diagnostic && diagnostic !== PUBLIC_ERROR_MESSAGES[code]
5240
+ ? `${PUBLIC_ERROR_MESSAGES[code]} Cause: ${diagnostic}`
5241
+ : PUBLIC_ERROR_MESSAGES[code];
5242
+ return {
5243
+ ok: false,
5244
+ error: {
5245
+ code,
5246
+ message,
5247
+ ...(diagnostic && diagnostic !== PUBLIC_ERROR_MESSAGES[code] ? { diagnostic } : {}),
5248
+ },
5249
+ };
5151
5250
  }
5152
5251
  function sdkError(error: unknown): Record<string, unknown> {
5153
5252
  return publicError(error);
@@ -5728,10 +5827,55 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
5728
5827
  );
5729
5828
  }
5730
5829
 
5830
+ async function findPersistedBrokerAuthority(
5831
+ sessions: Array<Record<string, unknown>>,
5832
+ expected: {
5833
+ sessionId: string;
5834
+ workspace: string;
5835
+ endpointGeneration: number | null;
5836
+ endpointIncarnation: string | null;
5837
+ },
5838
+ ): Promise<Record<string, unknown> | undefined> {
5839
+ if (expected.endpointGeneration === null || expected.endpointIncarnation === null) return undefined;
5840
+ let canonicalExpectedWorkspace: string;
5841
+ try {
5842
+ canonicalExpectedWorkspace = await canonicalBrokerWorkspace(expected.workspace);
5843
+ } catch {
5844
+ return undefined;
5845
+ }
5846
+ for (const session of sessions) {
5847
+ if (brokerSessionId(session) !== expected.sessionId) continue;
5848
+ if (
5849
+ brokerEndpointGeneration(session) !== expected.endpointGeneration ||
5850
+ brokerEndpointIncarnation(session, expected.sessionId) !== expected.endpointIncarnation
5851
+ )
5852
+ continue;
5853
+ const declaredWorkspace = brokerSessionScope(session);
5854
+ if (declaredWorkspace === null) continue;
5855
+ try {
5856
+ const canonicalDeclaredWorkspace = await canonicalBrokerWorkspace(declaredWorkspace);
5857
+ if (sameCanonicalPath(canonicalDeclaredWorkspace, canonicalExpectedWorkspace, platform)) return session;
5858
+ } catch {
5859
+ // A missing or unreadable locator is not authority for this session.
5860
+ }
5861
+ }
5862
+ return undefined;
5863
+ }
5864
+
5731
5865
  type BrokerSessionAuthority = {
5732
5866
  workspace: string;
5733
5867
  endpointGeneration: number;
5734
5868
  endpointIncarnation: string;
5869
+ /** Host liveness the broker reports for the indexed row; absent when unreported. */
5870
+ live?: boolean;
5871
+ /** Last host heartbeat the broker recorded for the row (epoch ms); absent when unreported. */
5872
+ lastHeartbeatAt?: number;
5873
+ /** True when more than one unresolved state-root authority claims this session. */
5874
+ ambiguous?: boolean;
5875
+ /** True when teardown ownership is unresolved and must not be force-retired. */
5876
+ terminalUncertain?: boolean;
5877
+ /** True when the exact broker row is already terminal. */
5878
+ terminal?: boolean;
5735
5879
  };
5736
5880
 
5737
5881
  async function exactBrokerSessionAuthority(sessionId: string, workspace: string): Promise<BrokerSessionAuthority> {
@@ -5751,17 +5895,35 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
5751
5895
  if (sameCanonicalPath(canonicalWorkspace, workspace, platform))
5752
5896
  matches.push({ session, workspace: canonicalWorkspace });
5753
5897
  }
5754
- if (matches.length !== 1)
5898
+ if (matches.length === 0)
5755
5899
  throw new SdkClientError(
5756
5900
  "not_found",
5757
5901
  "Session is not uniquely indexed in the requested coordinator workspace.",
5758
5902
  );
5903
+ if (matches.length !== 1)
5904
+ throw new SdkClientError(
5905
+ "ambiguous",
5906
+ "Session has multiple broker authority rows in the requested coordinator workspace.",
5907
+ );
5759
5908
  const match = matches[0]!;
5760
5909
  const endpointGeneration = brokerEndpointGeneration(match.session);
5761
5910
  const endpointIncarnation = brokerEndpointIncarnation(match.session, sessionId);
5762
5911
  if (endpointGeneration === null || endpointIncarnation === null)
5763
5912
  throw new SdkClientError("endpoint_stale", "Broker session has no usable endpoint incarnation.");
5764
- return { workspace: match.workspace, endpointGeneration, endpointIncarnation };
5913
+ return {
5914
+ workspace: match.workspace,
5915
+ endpointGeneration,
5916
+ endpointIncarnation,
5917
+ ...(typeof match.session.live === "boolean" ? { live: match.session.live } : {}),
5918
+ ...(typeof match.session.lastHeartbeatAt === "number"
5919
+ ? { lastHeartbeatAt: match.session.lastHeartbeatAt }
5920
+ : {}),
5921
+ ...(typeof match.session.ambiguous === "boolean" ? { ambiguous: match.session.ambiguous } : {}),
5922
+ ...(typeof match.session.terminalUncertain === "boolean"
5923
+ ? { terminalUncertain: match.session.terminalUncertain }
5924
+ : {}),
5925
+ ...(typeof match.session.terminal === "boolean" ? { terminal: match.session.terminal } : {}),
5926
+ };
5765
5927
  }
5766
5928
 
5767
5929
  async function exactBrokerSessionBinding(sessionId: string, workspace: string): Promise<BrokerSessionAuthority> {
@@ -5850,13 +6012,49 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
5850
6012
  const sessionId = brokerSessionId(session);
5851
6013
  return sessionId !== null && registered.has(sessionId);
5852
6014
  }
6015
+ async function reportProjectionFilesForSession(sessionId: string): Promise<string[]> {
6016
+ const reportsDirectory = path.join(namespaceDir, "reports");
6017
+ const entries = await fs.readdir(reportsDirectory, { withFileTypes: true }).catch(error => {
6018
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
6019
+ throw error;
6020
+ });
6021
+ const files: string[] = [];
6022
+ for (const entry of entries) {
6023
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
6024
+ const file = path.join(reportsDirectory, entry.name);
6025
+ try {
6026
+ const report = asRecord(await readJsonFile(file));
6027
+ if (report?.session_id === sessionId) files.push(file);
6028
+ } catch (error) {
6029
+ logger.warn("Coordinator report projection scan skipped unreadable file", {
6030
+ file,
6031
+ error: error instanceof Error ? error.message : String(error),
6032
+ });
6033
+ }
6034
+ }
6035
+ return files;
6036
+ }
6037
+ async function removeStaleReportProjections(
6038
+ sessionId: string,
6039
+ retainedReportIds: ReadonlySet<string>,
6040
+ ): Promise<void> {
6041
+ for (const file of await reportProjectionFilesForSession(sessionId)) {
6042
+ const reportId = path.basename(file, ".json");
6043
+ if (retainedReportIds.has(reportId)) continue;
6044
+ await fs.rm(file, { force: true });
6045
+ }
6046
+ }
5853
6047
  async function removeReapedProjection(
5854
6048
  sessionId: string,
5855
6049
  turnIds: readonly string[],
5856
6050
  reportIds: readonly string[],
5857
6051
  ): Promise<void> {
5858
6052
  for (const turnId of turnIds) await fs.rm(turnFile(namespaceDir, turnId), { force: true });
5859
- for (const reportId of reportIds) await fs.rm(reportProjectionFile(namespaceDir, reportId), { force: true });
6053
+ for (const reportId of reportIds) {
6054
+ if (COORDINATOR_REPORT_ID_PATTERN.test(reportId))
6055
+ await fs.rm(reportProjectionFile(namespaceDir, reportId), { force: true });
6056
+ }
6057
+ for (const file of await reportProjectionFilesForSession(sessionId)) await fs.rm(file, { force: true });
5860
6058
  await fs.rm(sessionFile(sessionId), { force: true });
5861
6059
  await fs.rm(sessionStateFile(namespaceDir, sessionId), { force: true });
5862
6060
  await fs.rm(activeTurnFile(namespaceDir, sessionId), { force: true });
@@ -6046,19 +6244,21 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
6046
6244
  // manifest first, even while the session projection remains readable, so a
6047
6245
  // verification-uncertain retry never reaches broker close again.
6048
6246
  await ensureQuestionStateReady();
6049
- let pendingDeletion = await withNamespaceRegistry(
6050
- questionPaths,
6051
- async registry =>
6052
- Object.values(registry.deletions)
6247
+ let pendingDeletion = await withNamespaceRegistry(questionPaths, async registry => {
6248
+ const deletions = Object.values(registry.deletions);
6249
+ return (
6250
+ deletions
6053
6251
  .filter(
6054
6252
  entry =>
6055
6253
  entry.session_id === id &&
6056
6254
  (entry.phase === "intent" ||
6057
6255
  entry.phase === "broker_closed" ||
6058
- entry.phase === "cleanup_pending"),
6256
+ entry.phase === "cleanup_pending") &&
6257
+ !isSupersededDeletionIntent(entry, deletions),
6059
6258
  )
6060
- .sort((left, right) => right.updated_at.localeCompare(left.updated_at))[0] ?? null,
6061
- );
6259
+ .sort((left, right) => right.updated_at.localeCompare(left.updated_at))[0] ?? null
6260
+ );
6261
+ });
6062
6262
  if (pendingDeletion?.phase === "intent") {
6063
6263
  try {
6064
6264
  await recoverIntentDeletion(pendingDeletion);
@@ -6086,7 +6286,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
6086
6286
  const deletion = await withNamespaceRegistry(questionPaths, async registry => {
6087
6287
  const matching = Object.values(registry.deletions).filter(entry => entry.session_id === id);
6088
6288
  return (
6089
- matching.find(entry => entry.phase !== "completed") ??
6289
+ matching.find(entry => entry.phase !== "completed" && !isSupersededDeletionIntent(entry, matching)) ??
6090
6290
  matching.find(entry => entry.safe_response !== undefined) ??
6091
6291
  null
6092
6292
  );
@@ -6215,6 +6415,9 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
6215
6415
  }
6216
6416
  const deletionId = `delete:${id}:${persistedIncarnation}`;
6217
6417
  const deletionKey = createHash("sha256").update(deletionId).digest("hex");
6418
+ const projectedReportIds = (await reportProjectionFilesForSession(id)).map(file =>
6419
+ path.basename(file, ".json"),
6420
+ );
6218
6421
  const deletionEntry = {
6219
6422
  deletion_id: deletionId,
6220
6423
  session_id: id,
@@ -6231,7 +6434,12 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
6231
6434
  session: false,
6232
6435
  events: false,
6233
6436
  turn_ids: canonicalTransaction ? Object.keys(canonicalTransaction.canonical.turns) : [],
6234
- report_ids: canonicalTransaction ? Object.keys(canonicalTransaction.canonical.reports) : [],
6437
+ report_ids: [
6438
+ ...new Set([
6439
+ ...(canonicalTransaction ? Object.keys(canonicalTransaction.canonical.reports) : []),
6440
+ ...projectedReportIds,
6441
+ ]),
6442
+ ],
6235
6443
  },
6236
6444
  authority_digest: deletionKey,
6237
6445
  created_at: new Date().toISOString(),
@@ -6252,7 +6460,12 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
6252
6460
  }
6253
6461
  const projectionIds = {
6254
6462
  turnIds: Object.keys(admitted.canonical.turns),
6255
- reportIds: Object.keys(admitted.canonical.reports),
6463
+ reportIds: [
6464
+ ...new Set([
6465
+ ...Object.keys(admitted.canonical.reports),
6466
+ ...(await reportProjectionFilesForSession(id)).map(file => path.basename(file, ".json")),
6467
+ ]),
6468
+ ],
6256
6469
  };
6257
6470
  await ensureQuestionStateReady();
6258
6471
  const deletionPhase = await withNamespaceRegistry(
@@ -6409,8 +6622,10 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
6409
6622
  /**
6410
6623
  * Prove a stored endpoint identity (workspace + generation + incarnation) is no
6411
6624
  * longer current against the live broker index. Returns true only when the
6412
- * broker row is absent (not_found) or its generation/incarnation/workspace has
6413
- * rotated away from the stored triple. A transient authority-read failure
6625
+ * broker row is absent (not_found), its generation/incarnation/workspace has
6626
+ * rotated away from the stored triple, or the broker reports the row not live
6627
+ * and its host has been silent for DEAD_HOST_HEARTBEAT_SILENCE_MS. A transient
6628
+ * authority-read failure
6414
6629
  * returns false: we cannot prove the endpoint recovered, but we also must not
6415
6630
  * force-evict on a flake — the normal reap path will retry.
6416
6631
  */
@@ -6424,6 +6639,17 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
6424
6639
  const workspace = await canonicalBrokerWorkspace(persistedWorkspace);
6425
6640
  const authority = await exactBrokerSessionAuthority(id, workspace);
6426
6641
  return (
6642
+ // The broker can keep indexing a session's exact identity after its host is
6643
+ // gone; it then reports the row as not live and answers close with
6644
+ // endpoint_stale, so an identity match alone does not prove a live endpoint.
6645
+ // `live` alone is heartbeat-derived and flips after a short gap, so also
6646
+ // require the host to have been silent far beyond the broker's freshness
6647
+ // window before treating the row as dead.
6648
+ (authority.ambiguous !== true &&
6649
+ authority.terminalUncertain !== true &&
6650
+ authority.live === false &&
6651
+ authority.lastHeartbeatAt !== undefined &&
6652
+ Date.now() - authority.lastHeartbeatAt >= DEAD_HOST_HEARTBEAT_SILENCE_MS) ||
6427
6653
  !sameCanonicalPath(authority.workspace, persistedWorkspace, platform) ||
6428
6654
  authority.endpointGeneration !== persistedGeneration ||
6429
6655
  authority.endpointIncarnation !== persistedIncarnation
@@ -6535,8 +6761,50 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
6535
6761
  // projection-only delete.
6536
6762
  return;
6537
6763
  }
6764
+ const brokerSessionSnapshot = canonicalTransaction.canonical.session.broker;
6765
+ const brokerWorkspace = optionalString(brokerSessionSnapshot.workspace);
6766
+ const brokerGeneration =
6767
+ typeof brokerSessionSnapshot.endpoint_generation === "number" &&
6768
+ Number.isSafeInteger(brokerSessionSnapshot.endpoint_generation) &&
6769
+ brokerSessionSnapshot.endpoint_generation > 0
6770
+ ? brokerSessionSnapshot.endpoint_generation
6771
+ : null;
6772
+ if (
6773
+ !brokerWorkspace ||
6774
+ brokerGeneration === null ||
6775
+ brokerSessionSnapshot.endpoint_incarnation !== persistedIncarnation
6776
+ )
6777
+ return;
6778
+ // The stale proof above is a read. Before deleting coordinator state, ask the
6779
+ // broker to atomically fence the exact generation/incarnation under its index
6780
+ // lock. A successor, fresh heartbeat, ambiguous row, or terminal-uncertain
6781
+ // authority refuses this request. A missing row is already fenced and is safe
6782
+ // to treat as retired: the preceding reap close observed the same endpoint.
6783
+ try {
6784
+ const fence = strictBrokerSessionClose(
6785
+ await brokerSession(
6786
+ brokerWorkspace,
6787
+ "session.close",
6788
+ {
6789
+ cwd: brokerWorkspace,
6790
+ sessionId,
6791
+ endpointGeneration: brokerGeneration,
6792
+ endpointIncarnation: persistedIncarnation,
6793
+ forceRetireStale: true,
6794
+ },
6795
+ `coordinator-reaper-fence:${sessionId}:${persistedIncarnation}`,
6796
+ ),
6797
+ sessionId,
6798
+ );
6799
+ if (fence.retired !== true) return;
6800
+ } catch (error) {
6801
+ if (!(error instanceof SdkClientError) || error.code !== "not_found") return;
6802
+ }
6538
6803
  const deletionId = `force-evict:${sessionId}:${persistedIncarnation}`;
6539
6804
  const deletionKey = createHash("sha256").update(deletionId).digest("hex");
6805
+ const projectedReportIds = (await reportProjectionFilesForSession(sessionId)).map(file =>
6806
+ path.basename(file, ".json"),
6807
+ );
6540
6808
  const now = new Date().toISOString();
6541
6809
  const entry: NamespaceDeletionEntryV1 = {
6542
6810
  deletion_id: deletionId,
@@ -6554,7 +6822,12 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
6554
6822
  session: false,
6555
6823
  events: false,
6556
6824
  turn_ids: canonicalTransaction ? Object.keys(canonicalTransaction.canonical.turns) : [],
6557
- report_ids: canonicalTransaction ? Object.keys(canonicalTransaction.canonical.reports) : [],
6825
+ report_ids: [
6826
+ ...new Set([
6827
+ ...(canonicalTransaction ? Object.keys(canonicalTransaction.canonical.reports) : []),
6828
+ ...projectedReportIds,
6829
+ ]),
6830
+ ],
6558
6831
  },
6559
6832
  authority_digest: deletionKey,
6560
6833
  created_at: now,
@@ -6576,6 +6849,24 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
6576
6849
  try {
6577
6850
  await ensureQuestionTransaction(sessionId);
6578
6851
  } catch (error) {
6852
+ // A session whose persisted location the current policy no longer authorizes
6853
+ // must not be reaped (that would mutate an unauthorized location), but it must
6854
+ // not abort enumeration either: rethrowing refuses the whole sweep, so one such
6855
+ // session would stop reaping for every other session in the namespace. An empty
6856
+ // root list is a namespace-wide misconfiguration, not a per-session defect, so it
6857
+ // still refuses the sweep once instead of warning for every session.
6858
+ if (
6859
+ error instanceof Error &&
6860
+ isSessionAuthorityError(error) &&
6861
+ error.message !== "coordinator_workdir_roots_required"
6862
+ ) {
6863
+ logger.warn("Coordinator session reaper skipped an unauthorized session", {
6864
+ sessionId,
6865
+ reason: error.message,
6866
+ detail: error.cause instanceof Error ? error.cause.message : undefined,
6867
+ });
6868
+ continue;
6869
+ }
6579
6870
  if (!(error instanceof Error) || error.message !== "resource_gone") throw error;
6580
6871
  }
6581
6872
  try {
@@ -6612,7 +6903,15 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
6612
6903
  },
6613
6904
  reapSession: async (sessionId: string): Promise<void> => {
6614
6905
  const result = await reapSession(sessionId, { reason: "idle_reaper" });
6615
- if (!result.ok) throw new Error(result.reason ?? "session_reap_failed");
6906
+ if (result.ok) return;
6907
+ // reapSession reports a broker close the endpoint could not serve as
6908
+ // close_failed with the broker code in `detail`. The reaper only counts
6909
+ // endpoint_stale toward force eviction, so pass that code through;
6910
+ // markSessionDead still re-proves staleness under the session lock.
6911
+ const staleClose =
6912
+ result.reason === "close_failed" &&
6913
+ (result.detail === "endpoint_stale" || result.detail === "not_found");
6914
+ throw new Error(staleClose ? "endpoint_stale" : (result.reason ?? "session_reap_failed"));
6616
6915
  },
6617
6916
  markSessionDead: async (sessionId: string): Promise<void> => {
6618
6917
  // Force-evict a session that cannot be reaped normally (endpoint_stale
@@ -6788,6 +7087,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
6788
7087
  return {
6789
7088
  ok: false,
6790
7089
  reason: "timeout",
7090
+ wait_expired: true,
6791
7091
  turn: payload.turn,
6792
7092
  advisory_status: payload.advisory_status,
6793
7093
  session_state: payload.session_state,
@@ -7411,6 +7711,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
7411
7711
  await writeTurnRecord(namespaceDir, turnFromCanonical(turn));
7412
7712
  for (const report of Object.values(canonical.reports))
7413
7713
  await writeJsonFile(reportProjectionFile(namespaceDir, report.report_id), report);
7714
+ await removeStaleReportProjections(sessionId, new Set(Object.keys(canonical.reports)));
7414
7715
  const activeId = canonical.queue.selected_promotion?.to_turn_id ?? canonical.queue.active_turn_id;
7415
7716
  const active = activeId ? canonical.turns[activeId] : null;
7416
7717
  if (active) await writeActiveTurn(namespaceDir, turnFromCanonical(active));
@@ -7489,8 +7790,11 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
7489
7790
  await exportRetainedDeliveries(32, options.signal);
7490
7791
  }
7491
7792
 
7492
- async function recoverCanonicalSessionProjection(sessionId: string): Promise<void> {
7493
- const transaction = await readSessionTransaction(questionPaths, sessionId);
7793
+ async function recoverCanonicalSessionProjection(
7794
+ sessionId: string,
7795
+ options: { signal?: AbortSignal } = {},
7796
+ ): Promise<void> {
7797
+ const transaction = await repairLegacyReportIds(questionPaths, sessionId, options);
7494
7798
  if (!transaction) return;
7495
7799
  const applied = Math.min(
7496
7800
  transaction.projection.applied_turns_revision,
@@ -7499,10 +7803,13 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
7499
7803
  transaction.projection.applied_active_revision,
7500
7804
  transaction.projection.applied_events_revision,
7501
7805
  );
7502
- if (applied < transaction.revision) await repairCanonicalProjections(sessionId);
7806
+ if (applied < transaction.revision) await repairCanonicalProjections(sessionId, options);
7503
7807
  }
7504
7808
 
7505
- async function recoverCanonicalNamespaceProjections(scopedSessionId: string | null = null): Promise<void> {
7809
+ async function recoverCanonicalNamespaceProjections(
7810
+ scopedSessionId: string | null = null,
7811
+ options: { signal?: AbortSignal } = {},
7812
+ ): Promise<void> {
7506
7813
  await ensureQuestionStateReady();
7507
7814
  const roster = await readSchedulerRoster(questionPaths);
7508
7815
  const persisted = await fs.readdir(questionPaths.sessions).catch(() => []);
@@ -7517,13 +7824,21 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
7517
7824
  const transaction = await readSessionTransaction(questionPaths, sessionId);
7518
7825
  if (!transaction) continue;
7519
7826
  await assertPersistedSessionAuthority(transaction.canonical.session);
7520
- await recoverCanonicalSessionProjection(sessionId);
7827
+ await recoverCanonicalSessionProjection(sessionId, options);
7521
7828
  } catch (error) {
7522
- if (
7523
- !(error instanceof Error) ||
7524
- (error.message !== "state_corrupt" && (scopedSessionId !== null || !isSessionAuthorityError(error)))
7525
- )
7526
- throw error;
7829
+ // Degrade per session instead of aborting the namespace sweep: one
7830
+ // unreadable record must not make every coordination read fail.
7831
+ // Log before classifying so the skipped session is always nameable
7832
+ // from the outside — state_corrupt is precisely the shape the two
7833
+ // WAL defects surface as, and silencing it hides the cause.
7834
+ logger.warn("Coordinator projection recovery skipped session", {
7835
+ sessionId,
7836
+ error: error instanceof Error ? error.message : String(error),
7837
+ });
7838
+ const isCorruptWal = error instanceof Error && error.message === "state_corrupt";
7839
+ const isAuthorityFailure = isSessionAuthorityError(error);
7840
+ if (!isCorruptWal && !isAuthorityFailure && !isFileLockAcquireTimeout(error)) throw error;
7841
+ if (scopedSessionId === sessionId && !isCorruptWal) throw error;
7527
7842
  }
7528
7843
  }
7529
7844
  }
@@ -7845,12 +8160,12 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
7845
8160
  try {
7846
8161
  await reconcileSessionRuntime(entry.session_id, options);
7847
8162
  } catch (error) {
7848
- if (
7849
- !(error instanceof Error) ||
7850
- (error.message !== "resource_gone" &&
7851
- (prioritySessionId !== undefined || !isSessionAuthorityError(error)))
7852
- )
7853
- throw error;
8163
+ const degradable =
8164
+ error instanceof Error &&
8165
+ (error.message === "resource_gone" ||
8166
+ isSessionAuthorityError(error) ||
8167
+ (error.message === "state_corrupt" && prioritySessionId === undefined));
8168
+ if (!degradable) throw error;
7854
8169
  }
7855
8170
  }
7856
8171
  if (processedCursor && !options.signal?.aborted)
@@ -7879,7 +8194,12 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
7879
8194
  await reconcileSessionRuntime(sessionId, options);
7880
8195
  await reconcileQuestions(sessionId, options);
7881
8196
  } catch (error) {
7882
- if (!(error instanceof Error) || (error.message !== "resource_gone" && !isSessionAuthorityError(error)))
8197
+ if (
8198
+ !(error instanceof Error) ||
8199
+ (error.message !== "resource_gone" &&
8200
+ error.message !== "state_corrupt" &&
8201
+ !isSessionAuthorityError(error))
8202
+ )
7883
8203
  throw error;
7884
8204
  }
7885
8205
  }
@@ -7913,12 +8233,12 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
7913
8233
  try {
7914
8234
  await reconcileQuestions(entry.session_id, options);
7915
8235
  } catch (error) {
7916
- if (
7917
- !(error instanceof Error) ||
7918
- (error.message !== "resource_gone" &&
7919
- (prioritySessionId !== undefined || !isSessionAuthorityError(error)))
7920
- )
7921
- throw error;
8236
+ const degradable =
8237
+ error instanceof Error &&
8238
+ (error.message === "resource_gone" ||
8239
+ isSessionAuthorityError(error) ||
8240
+ (error.message === "state_corrupt" && prioritySessionId === undefined));
8241
+ if (!degradable) throw error;
7922
8242
  }
7923
8243
  }
7924
8244
  if (processedCursor && !options.signal?.aborted)
@@ -7958,14 +8278,12 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
7958
8278
  await assertPersistedSessionAuthority(transaction.canonical.session);
7959
8279
  turn = await readActiveTurn(namespaceDir, entry.session_id);
7960
8280
  } catch (error) {
7961
- if (
7962
- !(error instanceof Error) ||
7963
- (error.message !== "resource_gone" &&
7964
- error.message !== "coordinator_workdir_outside_allowed_roots" &&
7965
- error.message !== "coordinator_workdir_roots_required" &&
7966
- error.message !== "coordinator_workspace_required")
7967
- )
7968
- throw error;
8281
+ const degradable =
8282
+ error instanceof Error &&
8283
+ (error.message === "resource_gone" ||
8284
+ isSessionAuthorityError(error) ||
8285
+ (error.message === "state_corrupt" && prioritySessionId === undefined));
8286
+ if (!degradable) throw error;
7969
8287
  }
7970
8288
  processedCursor = entry.session_id;
7971
8289
  if (!turn) continue;
@@ -8054,9 +8372,11 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
8054
8372
  };
8055
8373
  const binding = await exactBrokerSessionBinding(sessionId, cwd);
8056
8374
  const priorSession = asRecord(await readJsonFile(sessionFile(sessionId)));
8375
+ const priorWorkspace = optionalString(priorSession?.broker_workspace);
8057
8376
  const priorAuthority =
8058
8377
  priorSession &&
8059
- priorSession.broker_workspace === binding.workspace &&
8378
+ priorWorkspace !== null &&
8379
+ sameCanonicalPath(priorWorkspace, binding.workspace, platform) &&
8060
8380
  priorSession.endpoint_generation === binding.endpointGeneration &&
8061
8381
  optionalString(priorSession.endpoint_incarnation) === binding.endpointIncarnation
8062
8382
  ? (priorSession.sidecar_verifier as { key_id: string; public_key: string } | undefined)
@@ -8294,18 +8614,49 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
8294
8614
  };
8295
8615
  await reconcileSessionRuntime(canonicalSessionId, { observeQuestions: false });
8296
8616
  try {
8297
- const brokerWorkspace = optionalString(session.broker_workspace) ?? cwd;
8298
- let indexedSession = (await listSessions(brokerWorkspace)).find(
8299
- candidate => brokerSessionId(candidate) === canonicalSessionId,
8617
+ // A projection can be partially written after its canonical WAL commit.
8618
+ // Recover the durable broker identity before proving the live endpoint so
8619
+ // a missing projection field does not turn an indexed session into a
8620
+ // false `not_indexed` result.
8621
+ const canonicalBroker = (await readSessionTransaction(questionPaths, canonicalSessionId))?.canonical
8622
+ .session.broker;
8623
+ const brokerWorkspace =
8624
+ optionalString(session.broker_workspace) ?? optionalString(canonicalBroker?.workspace) ?? cwd;
8625
+ const persistedEndpointGeneration =
8626
+ typeof session.endpoint_generation === "number" &&
8627
+ Number.isSafeInteger(session.endpoint_generation) &&
8628
+ session.endpoint_generation > 0
8629
+ ? session.endpoint_generation
8630
+ : typeof canonicalBroker?.endpoint_generation === "number" &&
8631
+ Number.isSafeInteger(canonicalBroker.endpoint_generation) &&
8632
+ canonicalBroker.endpoint_generation > 0
8633
+ ? canonicalBroker.endpoint_generation
8634
+ : null;
8635
+ const persistedEndpointIncarnation =
8636
+ optionalString(session.endpoint_incarnation) ??
8637
+ optionalString(canonicalBroker?.endpoint_incarnation);
8638
+ const expectedAuthority = {
8639
+ sessionId: canonicalSessionId,
8640
+ workspace: brokerWorkspace,
8641
+ endpointGeneration: persistedEndpointGeneration,
8642
+ endpointIncarnation: persistedEndpointIncarnation,
8643
+ };
8644
+ // A matching session id is not enough: the broker row must still
8645
+ // prove the projection's exact worktree endpoint authority.
8646
+ let indexedSession = await findPersistedBrokerAuthority(
8647
+ await listSessions(brokerWorkspace),
8648
+ expectedAuthority,
8300
8649
  );
8301
- // Windows broker locators may differ in drive-letter casing or separator
8302
- // spelling even after the injected canonical workspace seam has resolved
8303
- // the coordinator path. The scoped listing request is still authoritative;
8304
- // only its local path filter is relaxed for the exact requested session.
8305
- if (!indexedSession && platform === "win32") {
8650
+ // A broker locator can retain a valid worktree under a spelling that
8651
+ // the platform-local scope filter cannot normalize (for example a
8652
+ // symlinked worktree or a Windows alias). Retry the unfiltered page,
8653
+ // but keep canonical workspace and endpoint authority checks so this
8654
+ // cannot turn a foreign or rotated row into an indexed session.
8655
+ if (!indexedSession) {
8306
8656
  const listing = await paginatedBrokerSessionList(brokerWorkspace, { cwd: brokerWorkspace });
8307
- indexedSession = jsonRecords(Array.isArray(listing.sessions) ? listing.sessions : []).find(
8308
- candidate => brokerSessionId(candidate) === canonicalSessionId,
8657
+ indexedSession = await findPersistedBrokerAuthority(
8658
+ jsonRecords(Array.isArray(listing.sessions) ? listing.sessions : []),
8659
+ expectedAuthority,
8309
8660
  );
8310
8661
  }
8311
8662
  const sessionState = publicCoordinatorSessionState(
@@ -8314,7 +8665,10 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
8314
8665
  return {
8315
8666
  ok: true,
8316
8667
  session: publicCoordinatorStatusSession(session),
8317
- status: { ...brokerLiveness(indexedSession ?? null), ...(sessionState ?? {}) },
8668
+ // Broker indexing is the authority for endpoint liveness. A runtime
8669
+ // sidecar can retain a stale `live:true` projection after its
8670
+ // endpoint incarnation rotates, so it must not mask `not_indexed`.
8671
+ status: { ...(sessionState ?? {}), ...brokerLiveness(indexedSession ?? null) },
8318
8672
  session_state: sessionState,
8319
8673
  };
8320
8674
  } catch (error) {
@@ -8547,14 +8901,14 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
8547
8901
  // repair. The fresh journal snapshot below is the first public read after
8548
8902
  // that repair/export sequence, so events are visible after the caller's
8549
8903
  // cursor without allowing recovery to erase a terminal observation.
8550
- await recoverCanonicalNamespaceProjections(prioritySessionId ?? null);
8904
+ await recoverCanonicalNamespaceProjections(prioritySessionId ?? null, reconcileOptions);
8551
8905
  await exportRetainedDeliveries(32);
8552
8906
  } catch (error) {
8553
8907
  if (!(error instanceof Error && error.message === "resource_gone")) throw error;
8554
8908
  }
8555
8909
  } else if (!watchController.signal.aborted && Date.now() < absoluteDeadline) {
8556
- await recoverCanonicalNamespaceProjections(prioritySessionId ?? null);
8557
8910
  try {
8911
+ await recoverCanonicalNamespaceProjections(prioritySessionId ?? null, reconcileOptions);
8558
8912
  await exportRetainedDeliveries(32, watchController.signal);
8559
8913
  await reconcileWatchAdmissions(prioritySessionId, reconcileOptions);
8560
8914
  await reconcileActiveTurnAcknowledgements(
@@ -8873,6 +9227,14 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
8873
9227
  let initialDelegateRequest: CoordinatorSessionTransactionV1["recovery"]["initial_delegate_request"];
8874
9228
  if (reusedSessionId) {
8875
9229
  sessionId = reusedSessionId;
9230
+ // Record that this attempt has not claimed a prompt before any step
9231
+ // that can fail transiently. Only this explicit marker lets a same-key
9232
+ // retry claim afresh; a receipt without it has unknown provenance.
9233
+ if (!recovering && outer.value.delegate_prompt_claim_started === undefined)
9234
+ await writeCoordinatorIdempotencyFile(idempotencyFile(idempotencyKey), {
9235
+ ...(outer.value as unknown as CoordinatorToolIdempotencyRecord),
9236
+ delegate_prompt_claim_started: false,
9237
+ });
8876
9238
  const prior = await readSessionTransaction(questionPaths, sessionId);
8877
9239
  const prompt = prior?.requests.prompts[requestKey];
8878
9240
  delegateEffectStarted ||= Boolean(prompt && prompt.phase !== "claimed");
@@ -8904,13 +9266,58 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
8904
9266
  message: "Coordinator session is bound to another workspace.",
8905
9267
  },
8906
9268
  };
8907
- const binding = await exactBrokerSessionBinding(sessionId, canonicalCwd);
8908
- if (
8909
- !sameCanonicalPath(
8910
- optionalString(existing.broker_workspace) ?? "",
8911
- canonicalCwd,
9269
+ // Delegate-created managed-worktree sessions retain the caller's
9270
+ // repository cwd separately from the broker's execution worktree.
9271
+ // Resolve endpoint authority in that persisted broker workspace;
9272
+ // using canonicalCwd here searches the parent repository and makes
9273
+ // every follow-up appear unindexed even while its endpoint is live.
9274
+ const persistedBrokerWorkspace = optionalString(existing.broker_workspace);
9275
+ if (!persistedBrokerWorkspace)
9276
+ return {
9277
+ ok: false,
9278
+ error: {
9279
+ code: "endpoint_stale",
9280
+ message: "Coordinator session endpoint authority is stale.",
9281
+ },
9282
+ };
9283
+ let bindingWorkspace: string;
9284
+ try {
9285
+ bindingWorkspace = await canonicalBrokerWorkspace(persistedBrokerWorkspace);
9286
+ } catch (error) {
9287
+ if (!(error instanceof SdkClientError) || error.code !== "not_found") throw error;
9288
+ return {
9289
+ ok: false,
9290
+ error: {
9291
+ code: "endpoint_stale",
9292
+ message: "Coordinator session endpoint authority is stale.",
9293
+ },
9294
+ };
9295
+ }
9296
+ // The caller cwd guard only proves args.cwd is inside the current roots.
9297
+ // The persisted pair is what the delegate actually dispatches through, so
9298
+ // reauthorize it against the current policy before it becomes binding scope:
9299
+ // a narrowed managed-worktree root or a redirected projection must not let
9300
+ // a reused delegate reach a workspace the coordinator no longer owns.
9301
+ try {
9302
+ await assertCoordinatorSessionLocations(config, existingCwd, bindingWorkspace, {
9303
+ canonicalizePath: services.canonicalizePath,
8912
9304
  platform,
8913
- ) ||
9305
+ });
9306
+ } catch (error) {
9307
+ if (!isSessionAuthorityError(error)) throw error;
9308
+ return {
9309
+ ok: false,
9310
+ error: {
9311
+ code: "workspace_mismatch",
9312
+ message: "Coordinator session workspace is outside the allowed roots.",
9313
+ },
9314
+ };
9315
+ }
9316
+ const binding = await exactBrokerSessionBinding(sessionId, bindingWorkspace);
9317
+ if (
9318
+ // The caller cwd guard above prevents cross-workspace reuse; this
9319
+ // comparison fences the persisted endpoint identity itself.
9320
+ !sameCanonicalPath(binding.workspace, bindingWorkspace, platform) ||
8914
9321
  existing.endpoint_generation !== binding.endpointGeneration ||
8915
9322
  optionalString(existing.endpoint_incarnation) !== binding.endpointIncarnation
8916
9323
  )
@@ -9040,8 +9447,17 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
9040
9447
  (await readCanonicalActiveTurn(sessionId));
9041
9448
  const priorTransaction = await readSessionTransaction(questionPaths, sessionId);
9042
9449
  const priorPrompt = priorTransaction?.requests.prompts[requestKey];
9450
+ // A reused session has no creation authority to recover through, so a
9451
+ // same-key retry after a pre-admission failure (for example a transient
9452
+ // workspace canonicalization error) would otherwise seal as uncertain. The
9453
+ // outer receipt records whether a prompt claim began: an explicit `false`
9454
+ // proves none was claimed and this retry may claim afresh. `true` or an
9455
+ // absent marker means missing live state is still not proof that nothing
9456
+ // was dispatched.
9457
+ const requireClaimedPrompt =
9458
+ recovering && (!reusedSessionId || outer.value.delegate_prompt_claim_started !== false);
9043
9459
  if (
9044
- recovering &&
9460
+ requireClaimedPrompt &&
9045
9461
  !priorPrompt &&
9046
9462
  !hasInitialDelegatePromptAuthority(priorTransaction, initialDelegateRequest)
9047
9463
  )
@@ -9071,13 +9487,18 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
9071
9487
  turn_id: previousActiveTurn.turn_id,
9072
9488
  };
9073
9489
  }
9490
+ if (outer.value.delegate_prompt_claim_started !== true)
9491
+ await writeCoordinatorIdempotencyFile(idempotencyFile(idempotencyKey), {
9492
+ ...(outer.value as unknown as CoordinatorToolIdempotencyRecord),
9493
+ delegate_prompt_claim_started: true,
9494
+ });
9074
9495
  const promptKey = await claimCanonicalPrompt(
9075
9496
  sessionId,
9076
9497
  taggedPrompt,
9077
9498
  operation,
9078
9499
  idempotencyKey,
9079
9500
  true,
9080
- recovering,
9501
+ requireClaimedPrompt,
9081
9502
  initialDelegateRequest,
9082
9503
  );
9083
9504
  if (reserved?.terminal_fence && !(await promptReceipt(sessionId, promptKey))) {
@@ -9224,6 +9645,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
9224
9645
  true,
9225
9646
  response =>
9226
9647
  (!delegateResponseComplete && (creationRemoteStarted || delegateEffectStarted)) ||
9648
+ isTransientDelegateAuthorityFailure(response) ||
9227
9649
  isRouterRequestAmbiguous(response),
9228
9650
  );
9229
9651
  }
@@ -11015,7 +11437,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
11015
11437
  if (request.method === "tools/call") {
11016
11438
  const params = (request.params ?? {}) as { name?: string; arguments?: Record<string, unknown> };
11017
11439
  const payload = await callTool(params.name ?? "", params.arguments ?? {});
11018
- return { jsonrpc: "2.0", id, result: textResult(payload, payload.ok === false) };
11440
+ return { jsonrpc: "2.0", id, result: coordinatorToolResult(payload) };
11019
11441
  }
11020
11442
  return { jsonrpc: "2.0", id, error: { code: -32601, message: `unknown_method:${request.method}` } };
11021
11443
  }
@@ -11037,8 +11459,12 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
11037
11459
  };
11038
11460
  }
11039
11461
 
11040
- function legacyToolResult(payload: unknown): { content: Array<{ type: "text"; text: string }>; isError: boolean } {
11041
- const failed = typeof payload === "object" && payload !== null && (payload as { ok?: unknown }).ok === false;
11462
+ function coordinatorToolResult(payload: unknown): { content: Array<{ type: "text"; text: string }>; isError: boolean } {
11463
+ const record = asRecord(payload);
11464
+ // The legacy ok:false means the observation window ended, not that the RPC
11465
+ // failed. Keep its payload contract while preventing host error/retry breakers.
11466
+ const observationExpired = record?.wait_expired === true && record.reason === "timeout" && !record.error;
11467
+ const failed = record?.ok === false && !observationExpired;
11042
11468
  return textResult(payload, failed);
11043
11469
  }
11044
11470
 
@@ -11083,7 +11509,7 @@ export async function handleCoordinatorMcpRequest(
11083
11509
  return {
11084
11510
  jsonrpc: "2.0",
11085
11511
  id: request.id ?? null,
11086
- result: legacyToolResult(await server.callTool(params.name ?? "", args)),
11512
+ result: coordinatorToolResult(await server.callTool(params.name ?? "", args)),
11087
11513
  };
11088
11514
  } finally {
11089
11515
  await server.close();