@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
@@ -44,6 +44,9 @@ import { fetchModelsDevPayload } from "@gajae-code/ai/provider-models/openai-com
44
44
  import { isDirectXaiReasoningEffortModel } from "@gajae-code/ai/providers/openai-completions-compat";
45
45
  import {
46
46
  detectDiscoveredApiFamily,
47
+ isSafeCatalogModelId,
48
+ MODELS_LIST_REQUEST_TIMEOUT_MS,
49
+ readBoundedModelsJson,
47
50
  resolveLoopbackOpenAIBaseUrl,
48
51
  } from "@gajae-code/ai/utils/discovery/openai-compatible";
49
52
 
@@ -144,6 +147,36 @@ function redactDiscoveryUrl(value: string | URL): string {
144
147
  }
145
148
  }
146
149
 
150
+ /**
151
+ * Scrub resolved credential material from a discovery failure before it is
152
+ * published to discovery state, cache provenance, or the logger. Mutates
153
+ * Error messages and stacks in place to preserve class/identity; wraps non-Errors.
154
+ */
155
+ export function scrubDiscoveryError(error: unknown, secrets: ReadonlyArray<string | undefined>): unknown {
156
+ const redact = (text: string): string => {
157
+ let scrubbed = text;
158
+ for (const secret of secrets) {
159
+ if (secret) scrubbed = scrubbed.split(secret).join("[redacted]");
160
+ }
161
+ return scrubbed;
162
+ };
163
+ if (error instanceof Error) {
164
+ const seen = new Set<Error>();
165
+ const scrubError = (current: Error): void => {
166
+ if (seen.has(current)) return;
167
+ seen.add(current);
168
+ current.message = redact(current.message);
169
+ if (typeof current.stack === "string") current.stack = redact(current.stack);
170
+ if (current.cause instanceof Error) scrubError(current.cause);
171
+ else if (typeof current.cause === "string") current.cause = redact(current.cause);
172
+ };
173
+ scrubError(error);
174
+ return error;
175
+ }
176
+ if (typeof error === "string") return redact(error);
177
+ return error;
178
+ }
179
+
147
180
  /** Whether a structured transport code proves that no listener accepted the connection. */
148
181
  function isConnectionRefusedDiscoveryCode(value: unknown): boolean {
149
182
  if (typeof value === "string") {
@@ -742,13 +775,26 @@ function registryModelMetadataWithoutApiSpecificFields(model: Model<Api>): Parti
742
775
  return metadata;
743
776
  }
744
777
 
745
- function registrySelectorResolvesToModel(selector: string, models: readonly Model<Api>[]): boolean {
746
- if (models.some(model => model.id === selector || `${model.provider}/${model.id}` === selector)) return true;
747
- const suffix = splitSelectorThinkingSuffix(selector);
748
- const baseSelector = suffix.thinkingLevel === undefined ? selector : suffix.selector;
778
+ export function registrySelectorResolvesToModel(selector: string, models: readonly Model<Api>[]): boolean {
779
+ const normalizedSelector = selector.trim().toLowerCase();
780
+ if (
781
+ models.some(
782
+ model =>
783
+ model.id.toLowerCase() === normalizedSelector ||
784
+ `${model.provider}/${model.id}`.toLowerCase() === normalizedSelector,
785
+ )
786
+ )
787
+ return true;
788
+ const suffix = splitSelectorThinkingSuffix(normalizedSelector);
789
+ const baseSelector = suffix.thinkingLevel === undefined ? normalizedSelector : suffix.selector;
749
790
  const parsed = parseModelString(baseSelector);
750
- if (parsed) return models.some(model => model.provider === parsed.provider && model.id === parsed.id);
751
- return models.some(model => model.id === baseSelector || model.id.endsWith(`/${baseSelector}`));
791
+ if (parsed)
792
+ return models.some(
793
+ model => model.provider.toLowerCase() === parsed.provider && model.id.toLowerCase() === parsed.id,
794
+ );
795
+ return models.some(
796
+ model => model.id.toLowerCase() === baseSelector || model.id.toLowerCase().endsWith(`/${baseSelector}`),
797
+ );
752
798
  }
753
799
 
754
800
  function filterMaterializedRegistryProfiles(
@@ -3249,7 +3295,7 @@ export class ModelRegistry {
3249
3295
  );
3250
3296
  if (
3251
3297
  evidence !== undefined &&
3252
- state?.status === "ok" &&
3298
+ (state?.status === "ok" || state?.status === "empty") &&
3253
3299
  discovery.fetched &&
3254
3300
  currentAuthGeneration === evidence.authGeneration &&
3255
3301
  currentEndpoint === evidence.endpoint
@@ -3287,8 +3333,8 @@ export class ModelRegistry {
3287
3333
  }
3288
3334
 
3289
3335
  #profileAvailabilityEvidenceFingerprint(): string {
3290
- return JSON.stringify(
3291
- [...this.#descriptorDiscoveryEvidence.entries()]
3336
+ return JSON.stringify({
3337
+ descriptor: [...this.#descriptorDiscoveryEvidence.entries()]
3292
3338
  .sort(([left], [right]) => left.localeCompare(right))
3293
3339
  .map(([provider, evidence]) => [
3294
3340
  provider,
@@ -3300,7 +3346,15 @@ export class ModelRegistry {
3300
3346
  [...evidence.modelIds].sort(),
3301
3347
  evidence.profileModelIds === undefined ? undefined : [...evidence.profileModelIds].sort(),
3302
3348
  ]),
3303
- );
3349
+ configured: [...this.#configuredDiscoveryEvidence.entries()]
3350
+ .sort(([left], [right]) => left.localeCompare(right))
3351
+ .map(([provider, evidence]) => [
3352
+ provider,
3353
+ evidence.authGeneration,
3354
+ evidence.endpoint,
3355
+ [...evidence.modelIds].sort(),
3356
+ ]),
3357
+ });
3304
3358
  }
3305
3359
 
3306
3360
  #mergeDiscoveredModels(discovered: readonly Model<Api>[]): void {
@@ -3496,8 +3550,14 @@ export class ModelRegistry {
3496
3550
  await this.#discoverModelsByProviderType(provider, apiKey),
3497
3551
  );
3498
3552
  } catch (error) {
3499
- discoveryFailureEvidence = error;
3500
- throw error;
3553
+ // ONE credential-safe discovery error boundary: transport
3554
+ // layers may echo request details (including the bearer)
3555
+ // in failure text. Scrub resolved credential material
3556
+ // before the error reaches discovery state, cache
3557
+ // provenance, or the logger below.
3558
+ const scrubbedError = scrubDiscoveryError(error, [apiKey, preflightApiKey]);
3559
+ discoveryFailureEvidence = scrubbedError;
3560
+ throw scrubbedError;
3501
3561
  }
3502
3562
  },
3503
3563
  getEvidenceGeneration: provider => this.#getProviderEvidenceGeneration(provider.provider, preflightApiKey),
@@ -3569,24 +3629,35 @@ export class ModelRegistry {
3569
3629
  };
3570
3630
  }
3571
3631
 
3572
- #discoverModelsByProviderType(
3632
+ async #discoverModelsByProviderType(
3573
3633
  providerConfig: DiscoveryProviderConfig,
3574
3634
  apiKey: string | undefined,
3575
3635
  ): Promise<Model<Api>[]> {
3636
+ let models: Model<Api>[];
3576
3637
  switch (providerConfig.discovery.type) {
3577
3638
  case "ollama":
3578
- return this.#discoverOllamaModels(providerConfig, apiKey);
3639
+ models = await this.#discoverOllamaModels(providerConfig, apiKey);
3640
+ break;
3579
3641
  case "llama.cpp":
3580
- return this.#discoverLlamaCppModels(providerConfig, apiKey);
3642
+ models = await this.#discoverLlamaCppModels(providerConfig, apiKey);
3643
+ break;
3581
3644
  case "lm-studio":
3582
3645
  case "omlx":
3583
3646
  case "vllm":
3584
3647
  case "sglang":
3585
3648
  case "openai-models-list":
3586
- return this.#discoverOpenAIModelsList(providerConfig, apiKey);
3649
+ models = await this.#discoverOpenAIModelsList(providerConfig, apiKey);
3650
+ break;
3587
3651
  case "models-dev":
3588
- return this.#discoverModelsDevProvider(providerConfig);
3652
+ models = await this.#discoverModelsDevProvider(providerConfig);
3653
+ break;
3654
+ }
3655
+ if (providerConfig.discovery.type === "openai-models-list" && models.length === 0) {
3656
+ throw new Error(
3657
+ `Model discovery for ${redactDiscoveryUrl(providerConfig.baseUrl ?? "")} returned no models; add --model <id> or fix the endpoint catalog.`,
3658
+ );
3589
3659
  }
3660
+ return models;
3590
3661
  }
3591
3662
 
3592
3663
  async #discoverBuiltInProviderModels(
@@ -4222,7 +4293,7 @@ export class ModelRegistry {
4222
4293
  ...(providerConfig.discovery.type === "vllm" || providerConfig.discovery.type === "sglang"
4223
4294
  ? { redirect: "error" as const }
4224
4295
  : {}),
4225
- signal: AbortSignal.timeout(hardenedLocalDiscovery ? 500 : 5_000),
4296
+ signal: AbortSignal.timeout(hardenedLocalDiscovery ? 500 : MODELS_LIST_REQUEST_TIMEOUT_MS),
4226
4297
  });
4227
4298
  if (!response.ok) {
4228
4299
  if (response.status === 401 || response.status === 403) {
@@ -4234,16 +4305,31 @@ export class ModelRegistry {
4234
4305
  }
4235
4306
  throw new Error(`HTTP ${response.status} from ${redactDiscoveryUrl(modelsUrl)}`);
4236
4307
  }
4237
- const payload: unknown = await response.json();
4308
+ // Shared 1MB bounded reader (same as the setup probe): a timeout
4309
+ // bounds wall clock, not bytes buffered.
4310
+ let payload: unknown;
4311
+ try {
4312
+ payload = await readBoundedModelsJson(response);
4313
+ } catch {
4314
+ throw new Error(`Malformed OpenAI models-list response from ${redactDiscoveryUrl(modelsUrl)}`);
4315
+ }
4238
4316
  if (!isRecord(payload) || !Array.isArray(payload.data)) {
4239
4317
  throw new Error(`Malformed OpenAI models-list response from ${redactDiscoveryUrl(modelsUrl)}`);
4240
4318
  }
4241
4319
  const models = payload.data;
4242
4320
  const discovered: Model<Api>[] = [];
4243
4321
  for (const item of models) {
4244
- if (!isRecord(item) || typeof item.id !== "string" || !item.id.trim()) continue;
4245
- const id = item.id;
4322
+ // Shared catalog admission: the setup probe applies the same
4323
+ // check, so IDs that appear only between requests cannot enter
4324
+ // the live catalog or cache unsanitized.
4325
+ if (!isRecord(item) || !isSafeCatalogModelId(item.id)) continue;
4326
+ const id = item.id.trim();
4246
4327
  const referenceModel = resolveCustomModelReference(id);
4328
+ // Names render directly in the model selector: apply the same
4329
+ // text-safety admission, falling back to the reference name or
4330
+ // the safe ID.
4331
+ const rawName = typeof item.name === "string" ? item.name.trim() : "";
4332
+ const name = rawName && isSafeCatalogModelId(rawName) ? rawName : (referenceModel?.name ?? id);
4247
4333
  const discoveredMaxTokens = firstPositiveDiscoveryNumber(
4248
4334
  item.max_completion_tokens,
4249
4335
  item.max_tokens,
@@ -4253,7 +4339,7 @@ export class ModelRegistry {
4253
4339
  discovered.push(
4254
4340
  enrichModelThinking({
4255
4341
  id,
4256
- name: typeof item.name === "string" ? item.name : (referenceModel?.name ?? id),
4342
+ name,
4257
4343
  api,
4258
4344
  provider: providerConfig.provider,
4259
4345
  baseUrl: requestBaseUrl,
@@ -5226,8 +5312,10 @@ export class ModelRegistry {
5226
5312
  }
5227
5313
 
5228
5314
  /**
5229
- * Get only models that have auth configured.
5230
- * This is a fast check that doesn't refresh OAuth tokens.
5315
+ * Get selectable models with auth configured.
5316
+ * This is a fast check that doesn't refresh OAuth tokens. A current,
5317
+ * authoritative live catalog also limits each provider to its enrolled ids;
5318
+ * bundled entries remain the fallback until that evidence exists.
5231
5319
  */
5232
5320
  getAvailable(): Model<Api>[] {
5233
5321
  this.#synchronizeEnvironmentCredentials();
@@ -5241,12 +5329,82 @@ export class ModelRegistry {
5241
5329
  ) {
5242
5330
  return this.#availableModelsCache;
5243
5331
  }
5244
- this.#availableModelsCache = this.#models.filter(model => this.#isModelAvailable(model, disabledProviders));
5332
+ const authoritativeDiscoveryIds = new Map<string, ReadonlySet<string> | undefined>();
5333
+ const bundledIdsByProvider = new Map<string, Set<string>>();
5334
+ this.#availableModelsCache = this.#models.filter(model => {
5335
+ if (!this.#isModelAvailable(model, disabledProviders)) return false;
5336
+
5337
+ let liveIds = authoritativeDiscoveryIds.get(model.provider);
5338
+ if (!authoritativeDiscoveryIds.has(model.provider)) {
5339
+ liveIds = this.#getAuthoritativeDiscoveredModelIds(model.provider);
5340
+ authoritativeDiscoveryIds.set(model.provider, liveIds);
5341
+ }
5342
+ // Undefined means discovery has not run successfully for the current
5343
+ // provider context. Keep the bundled catalog as the explicit fallback.
5344
+ if (liveIds === undefined) return true;
5345
+ if (this.#hasCustomModelOverlay(model.provider, model.id)) return true;
5346
+
5347
+ const activity = this.#providerActivity.get(model.provider);
5348
+ if (!activity?.staticModelIds.has(model.id)) return liveIds.has(model.id);
5349
+
5350
+ let bundledIds = bundledIdsByProvider.get(model.provider);
5351
+ if (!bundledIds) {
5352
+ bundledIds = new Set(
5353
+ (getBundledModels(model.provider as Parameters<typeof getBundledModels>[0]) as Model<Api>[]).map(
5354
+ candidate => candidate.id,
5355
+ ),
5356
+ );
5357
+ bundledIdsByProvider.set(model.provider, bundledIds);
5358
+ }
5359
+ return !bundledIds.has(model.id) || liveIds.has(model.id);
5360
+ });
5245
5361
  this.#availableModelsDisabledProviders = disabledProviderKey;
5246
5362
  this.#availableModelsEnvFingerprint = envFingerprint;
5247
5363
  return this.#availableModelsCache;
5248
5364
  }
5249
5365
 
5366
+ /**
5367
+ * Return the current live model ids only when discovery produced an
5368
+ * authoritative catalog for the provider and endpoint. An unavailable or
5369
+ * failed discovery returns undefined so callers retain the bundled fallback.
5370
+ */
5371
+ #getAuthoritativeDiscoveredModelIds(provider: string): ReadonlySet<string> | undefined {
5372
+ const descriptorEvidence = this.#descriptorDiscoveryEvidence.get(provider);
5373
+ if (descriptorEvidence?.profileFresh && descriptorEvidence.profileModelIds !== undefined) {
5374
+ try {
5375
+ if (
5376
+ descriptorEvidence.authGeneration === this.#getProviderEvidenceGeneration(provider) &&
5377
+ descriptorEvidence.profileEndpoint ===
5378
+ this.#normalizeDiscoveryEvidenceEndpoint(this.#getProviderBaseUrlForDiscovery(provider) ?? "")
5379
+ ) {
5380
+ return descriptorEvidence.profileModelIds;
5381
+ }
5382
+ } catch {
5383
+ // A provider context that can no longer be verified must use its fallback.
5384
+ }
5385
+ }
5386
+
5387
+ const configuredEvidence = this.#configuredDiscoveryEvidence.get(provider);
5388
+ if (!configuredEvidence) return undefined;
5389
+ const discoveryState = this.#discoveryManager.getState(provider);
5390
+ if (
5391
+ discoveryState?.status !== "ok" &&
5392
+ discoveryState?.status !== "cached" &&
5393
+ discoveryState?.status !== "empty"
5394
+ ) {
5395
+ return undefined;
5396
+ }
5397
+ try {
5398
+ return configuredEvidence.authGeneration === this.#getProviderEvidenceGeneration(provider) &&
5399
+ configuredEvidence.endpoint ===
5400
+ this.#normalizeDiscoveryEvidenceEndpoint(this.#getProviderBaseUrlForDiscovery(provider) ?? "")
5401
+ ? configuredEvidence.modelIds
5402
+ : undefined;
5403
+ } catch {
5404
+ return undefined;
5405
+ }
5406
+ }
5407
+
5250
5408
  #synchronizeEnvironmentCredentials(): void {
5251
5409
  // Runtime registrations own a provider while their credential is present.
5252
5410
  // Refresh them first so a missing runtime env key can hand ownership back to
@@ -369,6 +369,24 @@ const AUTOROUTING_PROVENANCE_JSON_SCHEMA: JsonSchemaObject = {
369
369
  required: ["schema", "source", "declarationFingerprint", "tiersFingerprint"],
370
370
  };
371
371
 
372
+ // SDK prompt deadline defaults. The `sdk.promptDeadlineMs` /
373
+ // `sdk.promptMaxRuntimeMs` schema entries below and the SDK bus/host lease
374
+ // fallbacks read these same constants, so a Settings lookup that misses — no
375
+ // settings object, an unwritten key, a non-finite value — cannot arm a deadline
376
+ // that disagrees with the declared default (#5583).
377
+ export const DEFAULT_SDK_PROMPT_DEADLINE_MS = 3_600_000;
378
+ export const DEFAULT_SDK_PROMPT_MAX_RUNTIME_MS = 21_600_000;
379
+
380
+ /** Resolve a `sdk.promptDeadlineMs` lookup, falling back on any non-finite value. */
381
+ export function resolveSdkPromptDeadlineMs(value: unknown): number {
382
+ return typeof value === "number" && Number.isFinite(value) ? value : DEFAULT_SDK_PROMPT_DEADLINE_MS;
383
+ }
384
+
385
+ /** Resolve a `sdk.promptMaxRuntimeMs` lookup, falling back on any non-finite value. */
386
+ export function resolveSdkPromptMaxRuntimeMs(value: unknown): number {
387
+ return typeof value === "number" && Number.isFinite(value) ? value : DEFAULT_SDK_PROMPT_MAX_RUNTIME_MS;
388
+ }
389
+
372
390
  export const SETTINGS_SCHEMA = {
373
391
  // ────────────────────────────────────────────────────────────────────────
374
392
  // General settings (no UI)
@@ -416,16 +434,22 @@ export const SETTINGS_SCHEMA = {
416
434
  // SDK-owned prompt deadline. Hidden from the UI; ACP has no separate timeout.
417
435
  "sdk.promptDeadlineMs": {
418
436
  type: "number",
419
- default: 3_600_000,
437
+ default: DEFAULT_SDK_PROMPT_DEADLINE_MS,
420
438
  description: "SDK-owned prompt deadline; ACP has no separate timeout.",
421
439
  validate: (value: number) => Number.isSafeInteger(value) && value >= 60_000 && value <= 86_400_000,
422
440
  },
423
441
  "sdk.promptMaxRuntimeMs": {
424
442
  type: "number",
425
- default: 21_600_000,
443
+ default: DEFAULT_SDK_PROMPT_MAX_RUNTIME_MS,
426
444
  description: "Hard maximum runtime for an SDK prompt from acceptance, bounding progress-aware renewals.",
427
445
  validate: (value: number) => Number.isSafeInteger(value) && value >= 60_000 && value <= 86_400_000,
428
446
  },
447
+ "sdk.flushWorktreeOnDeadline": {
448
+ type: "boolean",
449
+ default: true,
450
+ description:
451
+ "Autosave uncommitted changes in the session's linked worktree by default as a WIP commit when a prompt deadline retires a prompt; primary checkouts require an explicit true opt-in.",
452
+ },
429
453
  "sdk.masterOrphanGraceMs": {
430
454
  type: "number",
431
455
  default: 120_000,
@@ -275,11 +275,16 @@ export async function assertCoordinatorSessionLocations(
275
275
  ]);
276
276
  // Reauthorize both persisted locations independently. A managed worktree may
277
277
  // differ from the requested cwd, but neither may escape the current roots.
278
- if (
279
- !roots.some(root => isInsideCanonicalRoot(canonicalCwd, root, platform)) ||
280
- !roots.some(root => isInsideCanonicalRoot(canonicalWorkspace, root, platform))
281
- )
282
- throw new Error("coordinator_workdir_outside_allowed_roots");
278
+ const escaped: string[] = [];
279
+ if (!roots.some(root => isInsideCanonicalRoot(canonicalCwd, root, platform))) escaped.push(`cwd ${canonicalCwd}`);
280
+ if (!roots.some(root => isInsideCanonicalRoot(canonicalWorkspace, root, platform)))
281
+ escaped.push(`workspace ${canonicalWorkspace}`);
282
+ if (escaped.length > 0)
283
+ // The message stays exact because isSessionAuthorityError() in server.ts matches it
284
+ // verbatim; the escaping location rides in `cause` so a caller that logs can name it.
285
+ throw new Error("coordinator_workdir_outside_allowed_roots", {
286
+ cause: new Error(`outside allowed roots: ${escaped.join(", ")}`),
287
+ });
283
288
  }
284
289
 
285
290
  export async function assertCoordinatorArtifactPath(
@@ -184,6 +184,23 @@ export function buildCoordinatorAskAnswerSchema(
184
184
  return {
185
185
  title: "CoordinatorQuestionAnswerV1",
186
186
  type: "object",
187
+ // `additionalProperties: false` is scoped by the SIBLING `properties` map, not by
188
+ // the `oneOf` branches: a standards-compliant validator evaluates the keyword
189
+ // against this node alone. Without this map every answer field was "additional",
190
+ // so selection, custom-answer, and clarification payloads were all rejected
191
+ // client-side before the server saw them (#5801). Each branch stays closed too,
192
+ // so the union still rejects field combinations this map alone would allow.
193
+ properties: {
194
+ selected: selectedBase,
195
+ other: { type: "boolean", description: "set true to provide a free-text answer in `custom`" },
196
+ custom: boundedAnswerText("free-text answer; required when `other` is true"),
197
+ action: {
198
+ type: "string",
199
+ enum: ["answer", "clarify"],
200
+ description: "set to `clarify` to ask about the choices without answering the round",
201
+ },
202
+ question: boundedAnswerText("clarification question; required when action is `clarify`"),
203
+ },
187
204
  additionalProperties: false,
188
205
  oneOf: [
189
206
  {
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import * as fs from "node:fs/promises";
3
3
  import * as path from "node:path";
4
+ import { logger } from "@gajae-code/utils";
4
5
  import { withFileLock } from "../config/file-lock";
5
6
  import { ensureCoordinatorDirectory, syncCoordinatorDirectory, writeCoordinatorAtomic } from "./durability";
6
7
  import type { PrivateAskGateCodecV1, PublicReason } from "./question-gate-codec";
@@ -567,7 +568,7 @@ async function readTransactionJson<T>(file: string): Promise<T | null> {
567
568
  source = await fs.readFile(file, "utf8");
568
569
  } catch (error) {
569
570
  if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
570
- throw new Error("state_corrupt");
571
+ throw error;
571
572
  }
572
573
  try {
573
574
  const value: unknown = JSON.parse(source);
@@ -605,6 +606,71 @@ function migrateLegacyTransactionV1(transaction: CoordinatorSessionTransactionV1
605
606
  }
606
607
  const authorities = canonical.gate_authorities;
607
608
  const endpointIncarnation = typeof broker?.endpoint_incarnation === "string" ? broker.endpoint_incarnation : null;
609
+ // Report ids predate COORDINATOR_REPORT_ID_PATTERN. A legacy id is rejected by
610
+ // the reader forever, and because the reader is the namespace-wide scan, one
611
+ // historical report bricks every coordination read. Rekey legacy reports onto
612
+ // the canonical digest form instead of leaving the WAL permanently unreadable.
613
+ const reports = canonical.reports;
614
+ const migratedReportIds = new Map<string, string>();
615
+ if (reports && typeof reports === "object" && !Array.isArray(reports)) {
616
+ const table = reports as Record<string, Record<string, unknown>>;
617
+ for (const [reportId, report] of Object.entries(table)) {
618
+ if (COORDINATOR_REPORT_ID_PATTERN.test(reportId)) continue;
619
+ if (!report || typeof report !== "object" || Array.isArray(report)) continue;
620
+ const operationId = typeof report.operation_id === "string" ? report.operation_id : reportId;
621
+ let migratedId = `report-${digest(`legacy-report\0${record.session_id}\0${operationId}`)}`;
622
+ const existing = table[migratedId];
623
+ if (existing) {
624
+ const existingOperationId = typeof existing.operation_id === "string" ? existing.operation_id : null;
625
+ if (existingOperationId !== operationId) {
626
+ // A malformed legacy WAL can collide with the operation-derived digest.
627
+ // Preserve both records under distinct canonical ids rather than silently
628
+ // discarding an unrelated report.
629
+ migratedId = `report-${digest(`legacy-report\0${record.session_id}\0${operationId}\0${reportId}`)}`;
630
+ }
631
+ }
632
+ if (table[migratedId]) {
633
+ // The canonical report already wins this deterministic collision. The
634
+ // operation/outbox rewrites below keep every surviving reference coherent.
635
+ logger.warn("Coordinator legacy report id collision resolved", {
636
+ sessionId: record.session_id,
637
+ legacyReportId: reportId,
638
+ canonicalReportId: migratedId,
639
+ operationId,
640
+ });
641
+ migratedReportIds.set(reportId, migratedId);
642
+ delete table[reportId];
643
+ continue;
644
+ }
645
+ report.report_id = migratedId;
646
+ table[migratedId] = report;
647
+ migratedReportIds.set(reportId, migratedId);
648
+ delete table[reportId];
649
+ }
650
+ }
651
+ if (migratedReportIds.size > 0) {
652
+ const requests = record.requests as Record<string, unknown>;
653
+ const operations = requests.operations;
654
+ if (operations && typeof operations === "object" && !Array.isArray(operations)) {
655
+ for (const operation of Object.values(operations as Record<string, Record<string, unknown>>)) {
656
+ if (!operation || typeof operation !== "object" || Array.isArray(operation)) continue;
657
+ if (typeof operation.local_id === "string")
658
+ operation.local_id = migratedReportIds.get(operation.local_id) ?? operation.local_id;
659
+ }
660
+ }
661
+ const outbox = record.outbox as Record<string, Record<string, unknown>>;
662
+ for (const event of Object.values(outbox)) {
663
+ if (!event || typeof event !== "object" || Array.isArray(event) || event.entity !== "report") continue;
664
+ if (typeof event.entity_id === "string")
665
+ event.entity_id = migratedReportIds.get(event.entity_id) ?? event.entity_id;
666
+ const payload = event.payload;
667
+ if (payload && typeof payload === "object" && !Array.isArray(payload)) {
668
+ const reportId = (payload as Record<string, unknown>).report_id;
669
+ if (typeof reportId === "string")
670
+ (payload as Record<string, unknown>).report_id = migratedReportIds.get(reportId) ?? reportId;
671
+ }
672
+ }
673
+ }
608
674
  if (authorities && typeof authorities === "object" && !Array.isArray(authorities)) {
609
675
  for (const authority of Object.values(authorities as Record<string, unknown>)) {
610
676
  if (!authority || typeof authority !== "object" || Array.isArray(authority)) continue;
@@ -1322,9 +1388,16 @@ export async function listCanonicalActiveSessions(
1322
1388
  const active: string[] = [];
1323
1389
  for (const sessionId of sessionIds) {
1324
1390
  if (options.signal?.aborted) throw options.signal.reason ?? new Error("aborted");
1325
- const transaction = await readTransactionJson<CoordinatorSessionTransactionV1>(transactionPath(paths, sessionId));
1326
- if (!transaction) continue;
1327
- assertTransaction(transaction, path.basename(paths.root), sessionId);
1391
+ let transaction: CoordinatorSessionTransactionV1 | null;
1392
+ try {
1393
+ transaction = await readTransactionJson<CoordinatorSessionTransactionV1>(transactionPath(paths, sessionId));
1394
+ if (!transaction) continue;
1395
+ assertTransaction(transaction, path.basename(paths.root), sessionId);
1396
+ } catch (error) {
1397
+ if (!(error instanceof Error) || error.message !== "state_corrupt") throw error;
1398
+ logger.warn("Coordinator active-session scan skipped corrupt WAL", { sessionId });
1399
+ continue;
1400
+ }
1328
1401
  const hasActiveTurn = Object.values(transaction.canonical.turns).some(turn =>
1329
1402
  ["queued", "delivering", "active", "waiting_for_answer", "completing"].includes(turn.status),
1330
1403
  );
@@ -1418,14 +1491,74 @@ export async function advanceSchedulerCursor(
1418
1491
  export async function readSessionTransaction(
1419
1492
  paths: CoordinatorStatePaths,
1420
1493
  sessionId: string,
1494
+ options: { signal?: AbortSignal } = {},
1421
1495
  ): Promise<CoordinatorSessionTransactionV1 | null> {
1496
+ options.signal?.throwIfAborted();
1422
1497
  const transaction = await readTransactionJson<CoordinatorSessionTransactionV1>(transactionPath(paths, sessionId));
1498
+ options.signal?.throwIfAborted();
1423
1499
  if (!transaction) return null;
1500
+ // Atomic WAL replacement permits lock-free snapshots. Normalize only in
1501
+ // memory; durable report migration belongs to explicit projection recovery.
1424
1502
  assertTransaction(transaction, path.basename(paths.root), sessionId);
1425
- normalizeOutbox(transaction);
1426
1503
  return transaction;
1427
1504
  }
1428
1505
 
1506
+ export async function repairLegacyReportIds(
1507
+ paths: CoordinatorStatePaths,
1508
+ sessionId: string,
1509
+ options: { signal?: AbortSignal } = {},
1510
+ ): Promise<CoordinatorSessionTransactionV1 | null> {
1511
+ options.signal?.throwIfAborted();
1512
+ const file = transactionPath(paths, sessionId);
1513
+ const snapshot = await readTransactionJson<CoordinatorSessionTransactionV1>(file);
1514
+ options.signal?.throwIfAborted();
1515
+ if (!snapshot) return null;
1516
+ const hasLegacyReports = Object.keys(snapshot.canonical?.reports ?? {}).some(
1517
+ reportId => !COORDINATOR_REPORT_ID_PATTERN.test(reportId),
1518
+ );
1519
+ if (!hasLegacyReports) {
1520
+ // Atomic WAL replacement permits lock-free snapshots. In-memory schema
1521
+ // normalization alone must never turn an observation into a durable write.
1522
+ assertTransaction(snapshot, path.basename(paths.root), sessionId);
1523
+ return snapshot;
1524
+ }
1525
+ // Only legacy report rekeying needs a durable repair. Re-read under the lock
1526
+ // so a concurrent writer cannot be overwritten; never wait behind that writer
1527
+ // during namespace observation. The caller can skip a contended peer and retry.
1528
+ return await withFileLock(
1529
+ transactionLockPath(paths, sessionId),
1530
+ async () => {
1531
+ const transaction = await readTransactionJson<CoordinatorSessionTransactionV1>(file);
1532
+ if (!transaction) return null;
1533
+ const legacyReportIds = Object.keys(transaction.canonical?.reports ?? {}).filter(
1534
+ reportId => !COORDINATOR_REPORT_ID_PATTERN.test(reportId),
1535
+ );
1536
+ assertTransaction(transaction, path.basename(paths.root), sessionId);
1537
+ normalizeOutbox(transaction);
1538
+ if (legacyReportIds.some(reportId => !Object.hasOwn(transaction.canonical.reports, reportId))) {
1539
+ // Legacy report ids and their references are repaired as one durable WAL
1540
+ // transaction. Bump the revision so scheduler/projection consumers cannot
1541
+ // mistake the repaired image for the pre-migration one.
1542
+ transaction.revision += 1;
1543
+ transaction.projection.scheduler_pending_revision = transaction.revision;
1544
+ transaction.projection.scheduler_digest = digest(
1545
+ JSON.stringify({
1546
+ session_id: transaction.session_id,
1547
+ revision: transaction.revision,
1548
+ active: transaction.canonical.queue.active_turn_id !== null,
1549
+ state: transaction.canonical.desired_session_state,
1550
+ }),
1551
+ );
1552
+ assertTransaction(transaction, path.basename(paths.root), sessionId);
1553
+ options.signal?.throwIfAborted();
1554
+ await writeAtomic(file, transaction);
1555
+ }
1556
+ return transaction;
1557
+ },
1558
+ { ...lockOptions(options.signal), retries: 1, retryDelayMs: 0 },
1559
+ );
1560
+ }
1561
+
1429
1562
  export async function withSessionTransaction<T>(
1430
1563
  paths: CoordinatorStatePaths,
1431
1564
  sessionId: string,
@@ -2303,10 +2436,19 @@ export async function enumeratePublicDeliveries(
2303
2436
  after_order_key: afterOrderKey,
2304
2437
  });
2305
2438
  } catch (error) {
2306
- if (!(error instanceof Error) || error.message !== "resource_gone") throw error;
2439
+ if (error instanceof Error && (error.message === "resource_gone" || error.message === "state_corrupt")) {
2440
+ if (error.message === "state_corrupt")
2441
+ logger.warn("Coordinator delivery scan skipped corrupt WAL", { sessionId });
2442
+ } else throw error;
2443
+ }
2444
+ let endpointIncarnation: string | undefined;
2445
+ try {
2446
+ endpointIncarnation = (await readSessionTransaction(paths, sessionId))?.canonical.session.broker
2447
+ .endpoint_incarnation;
2448
+ } catch (error) {
2449
+ if (!(error instanceof Error) || error.message !== "state_corrupt") throw error;
2450
+ logger.warn("Coordinator delivery scan skipped corrupt WAL", { sessionId });
2307
2451
  }
2308
- const endpointIncarnation = (await readSessionTransaction(paths, sessionId))?.canonical.session.broker
2309
- .endpoint_incarnation;
2310
2452
  for (const claim of batch)
2311
2453
  claims.push({
2312
2454
  ...claim,
@@ -2349,6 +2491,30 @@ export function compactTransaction(transaction: CoordinatorSessionTransactionV1,
2349
2491
  );
2350
2492
  const pinnedRequests = new Set<object>(pinnedPrompts);
2351
2493
  const pinnedTurns = new Set(pinnedPrompts.map(request => request.coordinator_turn_id));
2494
+ // An active acknowledged turn proves its runtime receipt through the prompt
2495
+ // request that carries it. Dropping that request on retention leaves the WAL
2496
+ // failing its own `assertTransaction` receipt invariant on the next read, so
2497
+ // the session — and every namespace-wide scan that touches it — is corrupt
2498
+ // forever. Retention must never outrank an invariant the reader enforces.
2499
+ const activeTurnStatuses = new Set(["delivering", "active", "waiting_for_answer", "completing"]);
2500
+ const receiptBearingPromptIds = new Set(
2501
+ Object.entries(transaction.requests.prompts)
2502
+ .filter(([, request]) => {
2503
+ const turn = request.coordinator_turn_id
2504
+ ? transaction.canonical.turns[request.coordinator_turn_id]
2505
+ : undefined;
2506
+ if (!turn || !activeTurnStatuses.has(turn.status)) return false;
2507
+ const delivery = turn.delivery as Record<string, unknown>;
2508
+ if (delivery.prompt_acknowledged !== true) return false;
2509
+ return (
2510
+ ["accepted", "linked", "terminal", "completed"].includes(request.phase) &&
2511
+ request.runtime_receipt?.accepted === true &&
2512
+ request.runtime_receipt.command_id === delivery.runtime_command_id &&
2513
+ request.runtime_receipt.turn_id === delivery.runtime_turn_id
2514
+ );
2515
+ })
2516
+ .map(([id]) => id),
2517
+ );
2352
2518
  for (const [id, event] of Object.entries(transaction.outbox))
2353
2519
  if (
2354
2520
  event.emitted &&
@@ -2362,6 +2528,7 @@ export function compactTransaction(transaction: CoordinatorSessionTransactionV1,
2362
2528
  if (
2363
2529
  request.phase === "completed" &&
2364
2530
  !pinnedRequests.has(request) &&
2531
+ !receiptBearingPromptIds.has(id) &&
2365
2532
  old(request.updated_at) &&
2366
2533
  JSON.stringify(transaction.canonical).includes(id) === false
2367
2534
  )