@gajae-code/coding-agent 0.14.2 → 0.15.0

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 (345) hide show
  1. package/CHANGELOG.md +120 -1
  2. package/dist/types/async/job-manager.d.ts +4 -1
  3. package/dist/types/autoresearch/dashboard.d.ts +29 -4
  4. package/dist/types/autoresearch/data-context.d.ts +25 -0
  5. package/dist/types/autoresearch/git.d.ts +65 -7
  6. package/dist/types/autoresearch/harness.d.ts +58 -0
  7. package/dist/types/autoresearch/prompts.d.ts +51 -0
  8. package/dist/types/autoresearch/runs.d.ts +173 -0
  9. package/dist/types/autoresearch/types.d.ts +9 -139
  10. package/dist/types/capability/types.d.ts +20 -0
  11. package/dist/types/cli/accounts-cli.d.ts +1 -0
  12. package/dist/types/commands/{rlm.d.ts → autoresearch.d.ts} +2 -4
  13. package/dist/types/config/autorouting-contract.d.ts +146 -0
  14. package/dist/types/config/autorouting-generator.d.ts +40 -0
  15. package/dist/types/config/autorouting-tier-map.d.ts +16844 -0
  16. package/dist/types/config/autorouting.d.ts +35 -0
  17. package/dist/types/config/model-discovery-manager.d.ts +8 -1
  18. package/dist/types/config/model-registry.d.ts +30 -1
  19. package/dist/types/config/provider-selection-policy.d.ts +23 -0
  20. package/dist/types/config/settings-schema.d.ts +61 -3
  21. package/dist/types/config/settings.d.ts +3 -0
  22. package/dist/types/coordinator/contract.d.ts +1 -1
  23. package/dist/types/coordinator-mcp/durability.d.ts +29 -0
  24. package/dist/types/coordinator-mcp/event-webhook.d.ts +50 -0
  25. package/dist/types/coordinator-mcp/question-state.d.ts +1 -1
  26. package/dist/types/coordinator-mcp/server.d.ts +14 -0
  27. package/dist/types/crash/index-store.d.ts +3 -0
  28. package/dist/types/crash/record-loader.d.ts +2 -0
  29. package/dist/types/crash/upstream/relay.d.ts +5 -3
  30. package/dist/types/cursor.d.ts +12 -0
  31. package/dist/types/defaults/gjc-defaults.d.ts +35 -1
  32. package/dist/types/defaults/gjc-skills.generated.d.ts +1 -1
  33. package/dist/types/discovery/helpers.d.ts +1 -1
  34. package/dist/types/edit/index.d.ts +2 -1
  35. package/dist/types/eval/py/executor.d.ts +2 -0
  36. package/dist/types/extensibility/extensions/loader.d.ts +32 -0
  37. package/dist/types/extensibility/gjc-plugins/types.d.ts +1 -1
  38. package/dist/types/extensibility/skills.d.ts +2 -0
  39. package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +194 -0
  40. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  41. package/dist/types/gjc-runtime/psmux-detect.d.ts +1 -1
  42. package/dist/types/gjc-runtime/python-transcript.d.ts +19 -0
  43. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +2 -8
  44. package/dist/types/gjc-runtime/session-layout.d.ts +6 -3
  45. package/dist/types/gjc-runtime/state-renderer.d.ts +0 -22
  46. package/dist/types/gjc-runtime/state-schema.d.ts +7 -7
  47. package/dist/types/gjc-runtime/tmux-common.d.ts +2 -2
  48. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +38 -1
  49. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -24
  50. package/dist/types/gjc-runtime/ultragoal-validation-policy.d.ts +73 -0
  51. package/dist/types/gjc-runtime/workflow-recovery-projection.d.ts +100 -0
  52. package/dist/types/hooks/skill-keywords.d.ts +1 -1
  53. package/dist/types/main.d.ts +0 -10
  54. package/dist/types/modes/acp/acp-agent.d.ts +13 -6
  55. package/dist/types/modes/components/model-selector.d.ts +14 -0
  56. package/dist/types/modes/components/skill-hud/render.d.ts +6 -1
  57. package/dist/types/modes/components/smart-routing-panel.d.ts +82 -0
  58. package/dist/types/modes/controllers/selector-controller.d.ts +14 -0
  59. package/dist/types/modes/interactive-mode.d.ts +3 -0
  60. package/dist/types/modes/types.d.ts +9 -0
  61. package/dist/types/rlm/artifacts.d.ts +4 -4
  62. package/dist/types/runtime/optional-runtime-services.d.ts +2 -1
  63. package/dist/types/runtime/workspace-tree-service.d.ts +5 -1
  64. package/dist/types/runtime-mcp/config.d.ts +6 -0
  65. package/dist/types/runtime-mcp/manager.d.ts +10 -0
  66. package/dist/types/sdk/acp/adapter.d.ts +0 -4
  67. package/dist/types/sdk/broker/broker.d.ts +1 -1
  68. package/dist/types/sdk/broker/lifecycle.d.ts +57 -2
  69. package/dist/types/sdk/broker/session-index.d.ts +15 -0
  70. package/dist/types/sdk/bus/chat-daemon-control.d.ts +6 -0
  71. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -0
  72. package/dist/types/sdk/bus/index.d.ts +8 -0
  73. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +2 -0
  74. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -2
  75. package/dist/types/sdk/client/client.d.ts +39 -4
  76. package/dist/types/sdk/host/host.d.ts +1 -0
  77. package/dist/types/sdk/host/internal-autorouting-state.d.ts +3 -0
  78. package/dist/types/sdk/host/model-pin.d.ts +29 -0
  79. package/dist/types/sdk/host/session-runtime.d.ts +2 -1
  80. package/dist/types/sdk/lifecycle-session.d.ts +13 -0
  81. package/dist/types/sdk/router/session-router.d.ts +27 -1
  82. package/dist/types/sdk/session.d.ts +11 -1
  83. package/dist/types/session/agent-session.d.ts +36 -6
  84. package/dist/types/session/artifacts.d.ts +16 -1
  85. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  86. package/dist/types/session/session-manager.d.ts +84 -2
  87. package/dist/types/session/session-staging-paths.d.ts +3 -0
  88. package/dist/types/skill-state/canonical-skills.d.ts +1 -1
  89. package/dist/types/skill-state/workflow-hud.d.ts +12 -21
  90. package/dist/types/skill-state/workflow-mutation-guard.d.ts +1 -0
  91. package/dist/types/slash-commands/helpers/autorouting-status.d.ts +8 -0
  92. package/dist/types/system-prompt.d.ts +23 -0
  93. package/dist/types/task/executor.d.ts +35 -4
  94. package/dist/types/task/index.d.ts +17 -3
  95. package/dist/types/task/receipt.d.ts +3 -1
  96. package/dist/types/task/types.d.ts +144 -0
  97. package/dist/types/tools/atomic-file-write.d.ts +72 -0
  98. package/dist/types/tools/descriptors.d.ts +1 -0
  99. package/dist/types/tools/index.d.ts +36 -1
  100. package/dist/types/tools/move-session.d.ts +40 -0
  101. package/dist/types/tools/python.d.ts +12 -0
  102. package/dist/types/tools/resource-gc.d.ts +0 -3
  103. package/dist/types/tools/search.d.ts +8 -1
  104. package/dist/types/tools/skill.d.ts +9 -4
  105. package/dist/types/tools/subagent.d.ts +33 -3
  106. package/dist/types/utils/xml-escape.d.ts +2 -0
  107. package/dist/types/workflow/workflow-intent-diff.d.ts +2 -2
  108. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +1 -1
  109. package/package.json +11 -12
  110. package/scripts/build-sdk-package-smoke.ts +7 -5
  111. package/scripts/check-autorouting-tier-map.ts +183 -0
  112. package/scripts/generate-sdk-operation-inventory.ts +12 -0
  113. package/scripts/generate-tool-catalog.ts +40 -5
  114. package/src/async/job-manager.ts +5 -2
  115. package/src/autoresearch/dashboard.ts +134 -314
  116. package/src/autoresearch/data-context.ts +35 -0
  117. package/src/autoresearch/git.ts +186 -26
  118. package/src/autoresearch/{helpers.ts → harness.ts} +65 -40
  119. package/src/autoresearch/{prompt-setup.md → prompts/prompt-setup.md} +7 -7
  120. package/src/autoresearch/prompts/prompt.md +103 -0
  121. package/src/autoresearch/prompts.ts +126 -0
  122. package/src/autoresearch/runs.ts +708 -0
  123. package/src/autoresearch/types.ts +9 -150
  124. package/src/capability/index.ts +4 -4
  125. package/src/capability/types.ts +20 -0
  126. package/src/cli/accounts-cli.ts +21 -5
  127. package/src/cli/config-cli.ts +25 -1
  128. package/src/cli/fast-help.ts +1 -2
  129. package/src/cli/list-models.ts +5 -8
  130. package/src/cli/setup-cli.ts +8 -0
  131. package/src/cli.ts +2 -21
  132. package/src/commands/autoresearch.ts +33 -0
  133. package/src/commands/coordinator.ts +15 -0
  134. package/src/commands/launch.ts +2 -4
  135. package/src/commands/quick-lane.ts +1 -1
  136. package/src/commands/sdk.ts +24 -0
  137. package/src/commands/state.ts +1 -1
  138. package/src/config/autorouting-contract.ts +474 -0
  139. package/src/config/autorouting-generator.ts +238 -0
  140. package/src/config/autorouting-tier-map.ts +6108 -0
  141. package/src/config/autorouting.ts +120 -0
  142. package/src/config/model-discovery-manager.ts +42 -11
  143. package/src/config/model-registry.ts +348 -68
  144. package/src/config/provider-selection-policy.ts +60 -9
  145. package/src/config/settings-schema.ts +195 -6
  146. package/src/config/settings.ts +62 -1
  147. package/src/config.ts +2 -3
  148. package/src/coordinator/contract.ts +0 -1
  149. package/src/coordinator-mcp/codex-handoff.ts +68 -42
  150. package/src/coordinator-mcp/durability.ts +200 -0
  151. package/src/coordinator-mcp/event-webhook.ts +278 -0
  152. package/src/coordinator-mcp/question-state.ts +109 -46
  153. package/src/coordinator-mcp/server.ts +932 -504
  154. package/src/crash/index-store.ts +126 -22
  155. package/src/crash/record-loader.ts +7 -0
  156. package/src/crash/upstream/relay.ts +82 -34
  157. package/src/cursor.ts +84 -1
  158. package/src/debug/index.ts +26 -0
  159. package/src/defaults/gjc/skills/autoresearch/SKILL.md +116 -0
  160. package/src/defaults/gjc/skills/autoresearch/auto-critic.md +46 -0
  161. package/src/defaults/gjc/skills/autoresearch/auto-iterate.md +42 -0
  162. package/src/defaults/gjc/skills/deep-interview/SKILL.md +17 -37
  163. package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -18
  164. package/src/defaults/gjc/skills/ultragoal/SKILL.md +6 -17
  165. package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +2 -2
  166. package/src/defaults/gjc-defaults.ts +89 -2
  167. package/src/defaults/gjc-skills.generated.ts +24 -17
  168. package/src/discovery/builtin.ts +18 -5
  169. package/src/discovery/helpers.ts +6 -6
  170. package/src/edit/index.ts +5 -2
  171. package/src/eval/py/executor.ts +8 -0
  172. package/src/extensibility/extensions/loader.ts +135 -13
  173. package/src/extensibility/extensions/wrapper.ts +1 -2
  174. package/src/extensibility/gjc-plugins/schema.ts +1 -1
  175. package/src/extensibility/runtime-skill-discovery.ts +2 -2
  176. package/src/extensibility/skill-management.ts +2 -3
  177. package/src/extensibility/skills.ts +16 -8
  178. package/src/gjc-runtime/autoresearch-runtime.ts +1509 -0
  179. package/src/gjc-runtime/gc-render.ts +0 -1
  180. package/src/gjc-runtime/gc-runtime.ts +1 -11
  181. package/src/gjc-runtime/launch-tmux.ts +3 -3
  182. package/src/gjc-runtime/psmux-detect.ts +2 -2
  183. package/src/gjc-runtime/python-transcript.ts +58 -0
  184. package/src/gjc-runtime/ralplan-runtime.ts +6 -18
  185. package/src/gjc-runtime/session-layout.ts +18 -7
  186. package/src/gjc-runtime/session-state-sidecar.ts +58 -9
  187. package/src/gjc-runtime/state-renderer.ts +0 -38
  188. package/src/gjc-runtime/state-runtime.ts +43 -22
  189. package/src/gjc-runtime/state-schema.ts +1 -1
  190. package/src/gjc-runtime/tmux-common.ts +7 -7
  191. package/src/gjc-runtime/ultragoal-change-set.ts +232 -11
  192. package/src/gjc-runtime/ultragoal-runtime.ts +309 -114
  193. package/src/gjc-runtime/ultragoal-validation-policy.ts +247 -0
  194. package/src/gjc-runtime/workflow-command-ref.ts +15 -11
  195. package/src/gjc-runtime/workflow-manifest.generated.json +413 -532
  196. package/src/gjc-runtime/workflow-manifest.ts +43 -83
  197. package/src/gjc-runtime/workflow-recovery-projection.ts +616 -0
  198. package/src/hooks/skill-keywords.ts +3 -3
  199. package/src/hooks/skill-state.ts +2 -2
  200. package/src/internal-urls/docs-index.generated.ts +26 -24
  201. package/src/lsp/index.ts +25 -5
  202. package/src/main.ts +2 -27
  203. package/src/modes/acp/acp-agent.ts +200 -75
  204. package/src/modes/components/model-selector.ts +216 -13
  205. package/src/modes/components/oauth-selector.ts +5 -5
  206. package/src/modes/components/skill-hud/render.ts +173 -35
  207. package/src/modes/components/smart-routing-panel.ts +531 -0
  208. package/src/modes/components/tool-status-header.ts +6 -3
  209. package/src/modes/controllers/command-controller.ts +17 -7
  210. package/src/modes/controllers/input-controller.ts +44 -9
  211. package/src/modes/controllers/runtime-mcp-command-controller.ts +4 -0
  212. package/src/modes/controllers/selector-controller.ts +298 -65
  213. package/src/modes/interactive-mode.ts +8 -1
  214. package/src/modes/shared/agent-wire/workflow-gate-types.ts +1 -1
  215. package/src/modes/types.ts +9 -1
  216. package/src/prompts/agents/planner.md +1 -1
  217. package/src/prompts/system/system-prompt.md +1 -1
  218. package/src/prompts/system/volatile-project-context.md +2 -1
  219. package/src/prompts/tools/move-session.md +15 -0
  220. package/src/prompts/tools/python.md +12 -0
  221. package/src/prompts/tools/skill-discovery.md +1 -1
  222. package/src/prompts/tools/skill.md +2 -2
  223. package/src/prompts/tools/task-summary.md +1 -0
  224. package/src/prompts/tools/task.md +6 -0
  225. package/src/rlm/artifacts.ts +12 -10
  226. package/src/runtime/optional-runtime-services.ts +3 -2
  227. package/src/runtime/workspace-tree-service.ts +11 -3
  228. package/src/runtime-mcp/config.ts +8 -1
  229. package/src/runtime-mcp/manager.ts +33 -0
  230. package/src/sdk/acp/adapter.ts +48 -1
  231. package/src/sdk/broker/broker.ts +30 -2
  232. package/src/sdk/broker/discovery.ts +217 -2
  233. package/src/sdk/broker/lifecycle.ts +385 -59
  234. package/src/sdk/broker/session-index.ts +243 -48
  235. package/src/sdk/broker/transport.ts +1 -0
  236. package/src/sdk/bus/chat-daemon-control.ts +8 -2
  237. package/src/sdk/bus/chat-daemon-runtime.ts +5 -0
  238. package/src/sdk/bus/index.ts +208 -11
  239. package/src/sdk/bus/kind-aware-reconciliation.ts +16 -0
  240. package/src/sdk/bus/reconciliation-store.ts +27 -5
  241. package/src/sdk/bus/telegram-daemon-contract.ts +12 -2
  242. package/src/sdk/client/client.ts +204 -22
  243. package/src/sdk/host/host.ts +16 -0
  244. package/src/sdk/host/internal-autorouting-state.ts +13 -0
  245. package/src/sdk/host/model-pin.ts +101 -0
  246. package/src/sdk/host/reverse-leases.ts +2 -1
  247. package/src/sdk/host/session-runtime.test.ts +224 -1
  248. package/src/sdk/host/session-runtime.ts +125 -37
  249. package/src/sdk/lifecycle/service.ts +3 -1
  250. package/src/sdk/lifecycle-session.ts +42 -1
  251. package/src/sdk/protocol/operation-inventory.generated.json +66 -0
  252. package/src/sdk/router/session-router.ts +375 -36
  253. package/src/sdk/session.ts +547 -47
  254. package/src/session/account-inventory.ts +1 -1
  255. package/src/session/agent-session.ts +765 -189
  256. package/src/session/artifacts.ts +234 -4
  257. package/src/session/internal/managed-session-storage.ts +91 -4
  258. package/src/session/provider-safety-stop-hint.ts +3 -2
  259. package/src/session/session-manager.ts +775 -16
  260. package/src/session/session-staging-paths.ts +8 -0
  261. package/src/setup/hermes/templates/operator-instructions.v1.md +0 -1
  262. package/src/setup/host-plugin-setup.ts +2 -2
  263. package/src/skill-state/active-state.ts +2 -2
  264. package/src/skill-state/canonical-skills.ts +1 -1
  265. package/src/skill-state/initial-phase.ts +1 -1
  266. package/src/skill-state/workflow-hud.ts +41 -41
  267. package/src/skill-state/workflow-mutation-guard.ts +26 -7
  268. package/src/slash-commands/builtin-registry.ts +77 -0
  269. package/src/slash-commands/helpers/autorouting-status.ts +55 -0
  270. package/src/system-prompt.ts +102 -5
  271. package/src/task/discovery.ts +2 -3
  272. package/src/task/executor.ts +617 -43
  273. package/src/task/index.ts +262 -15
  274. package/src/task/receipt.ts +30 -0
  275. package/src/task/render.ts +11 -1
  276. package/src/task/types.ts +316 -0
  277. package/src/tools/atomic-file-write.ts +594 -0
  278. package/src/tools/bisect.ts +3 -2
  279. package/src/tools/descriptors.test.ts +3 -0
  280. package/src/tools/descriptors.ts +24 -0
  281. package/src/tools/image-gen.ts +5 -1
  282. package/src/tools/index.ts +62 -5
  283. package/src/tools/move-session.ts +107 -0
  284. package/src/tools/python.ts +205 -0
  285. package/src/tools/read.ts +88 -15
  286. package/src/tools/renderers.ts +2 -0
  287. package/src/tools/resource-gc.ts +8 -177
  288. package/src/tools/search.ts +30 -6
  289. package/src/tools/skill-discovery.ts +1 -0
  290. package/src/tools/skill.ts +37 -7
  291. package/src/tools/subagent-render.ts +119 -13
  292. package/src/tools/subagent.ts +91 -74
  293. package/src/tools/tool-catalog.generated.ts +64 -2
  294. package/src/tools/write.ts +16 -12
  295. package/src/utils/xml-escape.ts +14 -0
  296. package/src/web/search/providers/codex.ts +2 -2
  297. package/src/workflow/workflow-intent-diff.ts +13 -12
  298. package/src/workflow/workflow-intent-report-consumer.ts +6 -6
  299. package/dist/types/autoresearch/helpers.d.ts +0 -24
  300. package/dist/types/autoresearch/index.d.ts +0 -2
  301. package/dist/types/autoresearch/state.d.ts +0 -17
  302. package/dist/types/autoresearch/storage.d.ts +0 -142
  303. package/dist/types/autoresearch/tools/init-experiment.d.ts +0 -31
  304. package/dist/types/autoresearch/tools/log-experiment.d.ts +0 -23
  305. package/dist/types/autoresearch/tools/run-experiment.d.ts +0 -8
  306. package/dist/types/autoresearch/tools/update-notes.d.ts +0 -12
  307. package/dist/types/commands/team.d.ts +0 -24
  308. package/dist/types/gjc-runtime/team-gc.d.ts +0 -7
  309. package/dist/types/gjc-runtime/team-launch.d.ts +0 -61
  310. package/dist/types/gjc-runtime/team-notify.d.ts +0 -45
  311. package/dist/types/gjc-runtime/team-runtime.d.ts +0 -336
  312. package/dist/types/gjc-runtime/team-store.d.ts +0 -168
  313. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +0 -43
  314. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +0 -92
  315. package/dist/types/gjc-runtime/team-workers.d.ts +0 -83
  316. package/dist/types/rlm/index.d.ts +0 -47
  317. package/dist/types/rlm/preset.d.ts +0 -23
  318. package/dist/types/rlm/python-tool.d.ts +0 -16
  319. package/src/autoresearch/command-resume.md +0 -14
  320. package/src/autoresearch/index.ts +0 -536
  321. package/src/autoresearch/prompt.md +0 -103
  322. package/src/autoresearch/resume-message.md +0 -10
  323. package/src/autoresearch/state.ts +0 -273
  324. package/src/autoresearch/storage.ts +0 -699
  325. package/src/autoresearch/tools/init-experiment.ts +0 -272
  326. package/src/autoresearch/tools/log-experiment.ts +0 -524
  327. package/src/autoresearch/tools/run-experiment.ts +0 -475
  328. package/src/autoresearch/tools/update-notes.ts +0 -109
  329. package/src/commands/rlm.ts +0 -19
  330. package/src/commands/team.ts +0 -238
  331. package/src/defaults/gjc/skills/deep-interview/auto-research-greenfield.md +0 -42
  332. package/src/defaults/gjc/skills/team/SKILL.md +0 -467
  333. package/src/gjc-runtime/team-gc.ts +0 -49
  334. package/src/gjc-runtime/team-launch.ts +0 -404
  335. package/src/gjc-runtime/team-notify.ts +0 -326
  336. package/src/gjc-runtime/team-runtime.ts +0 -5923
  337. package/src/gjc-runtime/team-store.ts +0 -878
  338. package/src/gjc-runtime/team-worker-heartbeat.ts +0 -149
  339. package/src/gjc-runtime/team-worker-memory-guard.ts +0 -363
  340. package/src/gjc-runtime/team-workers.ts +0 -1123
  341. package/src/prompts/system/rlm-report-command.md +0 -1
  342. package/src/prompts/system/rlm-research.md +0 -23
  343. package/src/rlm/index.ts +0 -365
  344. package/src/rlm/preset.ts +0 -76
  345. package/src/rlm/python-tool.ts +0 -68
@@ -34,6 +34,13 @@ function isSafeFilename(filename: string): boolean {
34
34
  return /^[a-zA-Z0-9_.-]+$/.test(filename);
35
35
  }
36
36
 
37
+ const MAX_ATTEMPT_ID_LENGTH = 128;
38
+
39
+ function assertSafeAttemptId(attemptId: string): void {
40
+ if (!/^[A-Za-z0-9_-]{1,128}$/.test(attemptId) || attemptId.length > MAX_ATTEMPT_ID_LENGTH)
41
+ throw new Error("Unsafe artifact attempt id");
42
+ }
43
+
37
44
  function parseManagedOutputGeneration(value: Uint8Array, outputFilenamePrefix: string): ManagedOutputGeneration | null {
38
45
  try {
39
46
  const parsed = JSON.parse(Buffer.from(value).toString("utf8")) as Partial<ManagedOutputGeneration>;
@@ -99,13 +106,29 @@ export class ArtifactManager {
99
106
  #dirCreated = false;
100
107
  #initialized: Promise<void> | undefined;
101
108
  #initializedComplete = false;
109
+ readonly #attemptId: string | undefined;
110
+ readonly #allocatedIds = new Set<string>();
111
+ readonly #retiredIds = new Set<string>();
112
+ #reservations = new Map<string, { start: number; count: number; names: string[] }>();
113
+ readonly #stagingParentStore: ManagedSessionDescendantStore | undefined;
114
+ readonly #stagingRelativePath: string | undefined;
102
115
 
103
116
  /**
104
117
  * @param dir Directory that will hold artifact files. Created lazily on first save.
105
118
  */
106
- constructor(target: string | ManagedSessionDescendantStore) {
119
+ constructor(
120
+ target: string | ManagedSessionDescendantStore,
121
+ options?: {
122
+ readonly attemptId?: string;
123
+ readonly stagingParentStore?: ManagedSessionDescendantStore;
124
+ readonly stagingRelativePath?: string;
125
+ },
126
+ ) {
107
127
  this.#store = typeof target === "string" ? undefined : target;
108
128
  this.#dir = typeof target === "string" ? target : target.dir;
129
+ this.#attemptId = options?.attemptId;
130
+ this.#stagingParentStore = options?.stagingParentStore;
131
+ this.#stagingRelativePath = options?.stagingRelativePath;
109
132
  }
110
133
 
111
134
  /**
@@ -188,8 +211,12 @@ export class ArtifactManager {
188
211
  }
189
212
 
190
213
  async #publish(content: string, filename: string): Promise<void> {
191
- if (this.#store) await this.#store.publishNoReplace(filename, Buffer.from(content, "utf8"));
192
- else await publishManagedFileNoReplace(path.join(this.#dir, filename), Buffer.from(content, "utf8"));
214
+ await this.#publishBytes(Buffer.from(content, "utf8"), filename);
215
+ }
216
+
217
+ async #publishBytes(bytes: Uint8Array, filename: string): Promise<void> {
218
+ if (this.#store) await this.#store.publishNoReplace(filename, bytes);
219
+ else await publishManagedFileNoReplace(path.join(this.#dir, filename), bytes);
193
220
  }
194
221
 
195
222
  async replaceNamed(filename: string, content: string): Promise<void> {
@@ -289,8 +316,19 @@ export class ArtifactManager {
289
316
  if (!/^[a-zA-Z0-9_.-]+$/.test(filename)) return false;
290
317
  try {
291
318
  if (this.#store) {
319
+ const beforePaths = new Set(await fs.readdir(this.#store.dir));
292
320
  const staged = this.#store.readExpected(filename);
293
321
  if (staged) this.#store.removeExpected(filename, staged);
322
+ // Capture only root names outside retained authority so foreign sibling
323
+ // placeholders cannot make rollback fail before exact removal runs.
324
+ for (const basename of await fs.readdir(this.#store.dir)) {
325
+ const nativeResidue = /^\.gjc-/u.test(basename);
326
+ const ownQuarantine = basename === `${filename}.removing` || basename.startsWith(`${filename}.`);
327
+ if (beforePaths.has(basename) || (!nativeResidue && !ownQuarantine)) continue;
328
+ const residuePath = path.join(this.#store.dir, basename);
329
+ const stat = await fs.lstat(residuePath);
330
+ await fs.rm(residuePath, { recursive: stat.isDirectory(), force: true });
331
+ }
294
332
  } else {
295
333
  await fs.unlink(path.join(this.#dir, filename));
296
334
  }
@@ -326,7 +364,10 @@ export class ArtifactManager {
326
364
  * Prefer `allocatePath` or `save`; this synchronous seam exists for pruning callbacks.
327
365
  */
328
366
  allocateId(): number {
329
- return this.#claimNextIdSync();
367
+ while (this.#retiredIds.has(String(this.#nextId))) this.#nextId++;
368
+ const id = this.#claimNextIdSync();
369
+ this.#allocatedIds.add(String(id));
370
+ return id;
330
371
  }
331
372
 
332
373
  /**
@@ -338,6 +379,7 @@ export class ArtifactManager {
338
379
  async allocatePath(toolType: string): Promise<{ id: string; path?: string }> {
339
380
  await this.#ensureDir();
340
381
  const id = String(await this.#claimNextId());
382
+ this.#allocatedIds.add(id);
341
383
  if (this.#store) return { id };
342
384
  return { id, path: path.join(this.#dir, this.#filename(id, toolType)) };
343
385
  }
@@ -350,6 +392,7 @@ export class ArtifactManager {
350
392
  async save(content: string, toolType: string, options: ArtifactSaveOptions = {}): Promise<string> {
351
393
  await this.#ensureDir();
352
394
  const id = String(await this.#claimNextId());
395
+ this.#allocatedIds.add(id);
353
396
  const maxBytes = Math.max(0, options.maxBytes ?? DEFAULT_ARTIFACT_MAX_BYTES);
354
397
  const contentBytes = Buffer.byteLength(content, "utf-8");
355
398
  const published =
@@ -378,11 +421,198 @@ export class ArtifactManager {
378
421
  */
379
422
  async listFiles(): Promise<string[]> {
380
423
  try {
424
+ if (this.#store) {
425
+ return this.#store
426
+ .captureTree("")
427
+ .entries.filter(entry => entry.kind === "file" && entry.relativePath.length > 0)
428
+ .map(entry => entry.relativePath);
429
+ }
381
430
  return await fs.readdir(this.#dir);
382
431
  } catch {
383
432
  return [];
384
433
  }
385
434
  }
435
+ getAttemptId(): string | undefined {
436
+ return this.#attemptId;
437
+ }
438
+
439
+ getAllocatedIds(): readonly string[] {
440
+ return [...this.#allocatedIds].sort((a, b) => Number(a) - Number(b));
441
+ }
442
+
443
+ /** Create an isolated artifact manager rooted below this manager's staging area. */
444
+ createAttemptStaging(attemptId: string): ArtifactManager {
445
+ assertSafeAttemptId(attemptId);
446
+ const stagingRelativePath = path.posix.join(".staging", attemptId);
447
+ const target = this.#store
448
+ ? this.#store.deriveSubtree(stagingRelativePath)
449
+ : path.join(this.#dir, stagingRelativePath);
450
+ return new ArtifactManager(target, {
451
+ attemptId,
452
+ ...(this.#store ? { stagingParentStore: this.#store, stagingRelativePath } : {}),
453
+ });
454
+ }
455
+
456
+ /** Reserve and publish a candidate's staged artifacts with a contiguous parent ID block. */
457
+ async commitAttemptStaging(
458
+ staging: ArtifactManager,
459
+ attemptId: string,
460
+ options?: { beforePublish?: (mapping: ReadonlyMap<string, string>) => Promise<void> | void },
461
+ ): Promise<ReadonlyMap<string, string>> {
462
+ if (staging.#attemptId !== attemptId) throw new Error("Artifact staging ownership mismatch");
463
+ assertSafeAttemptId(attemptId);
464
+ await this.#ensureDir();
465
+ await staging.#ensureDir();
466
+ const ids = staging.getAllocatedIds();
467
+ const start = this.#nextId;
468
+ this.#nextId += ids.length;
469
+ const mapping = new Map<string, string>();
470
+ for (let index = 0; index < ids.length; index++) mapping.set(ids[index]!, String(start + index));
471
+ const frozenMap = new Map(mapping) as Map<string, string> & ReadonlyMap<string, string>;
472
+ Object.defineProperties(frozenMap, {
473
+ set: {
474
+ value: () => {
475
+ throw new Error("Artifact ID map is immutable");
476
+ },
477
+ },
478
+ delete: {
479
+ value: () => {
480
+ throw new Error("Artifact ID map is immutable");
481
+ },
482
+ },
483
+ clear: {
484
+ value: () => {
485
+ throw new Error("Artifact ID map is immutable");
486
+ },
487
+ },
488
+ });
489
+ Object.freeze(frozenMap);
490
+ const publishedNames: string[] = [];
491
+ try {
492
+ await options?.beforePublish?.(frozenMap);
493
+ for (const filename of await staging.listFiles()) {
494
+ if (/^\.artifact-id-\d+$/.test(filename)) continue;
495
+ const bytes = staging.#store
496
+ ? staging.#store.readExpected(filename)?.bytes
497
+ : await fs.readFile(path.join(staging.#dir, filename));
498
+ if (!bytes) continue;
499
+ const match = filename.match(/^(\d+)(\..*)$/);
500
+ const mappedFilename = match ? `${mapping.get(match[1]!) ?? match[1]}${match[2]}` : filename;
501
+ await this.#publishBytes(bytes, mappedFilename);
502
+ publishedNames.push(mappedFilename);
503
+ }
504
+ this.#reservations.set(attemptId, { start, count: ids.length, names: [...publishedNames] });
505
+ await staging.discardAttemptStaging();
506
+ return frozenMap;
507
+ } catch (error) {
508
+ // Cleanup is best-effort, but a durable removal failure must never be silent: it leaves a
509
+ // published artifact behind under an id we are about to retire. Surface it alongside the
510
+ // original publication error rather than dropping the boolean.
511
+ const unremoved: string[] = [];
512
+ for (const filename of publishedNames.reverse())
513
+ if (!(await this.removeNamedBestEffort(filename))) unremoved.push(filename);
514
+ // Only rewind the tail when every published artifact was actually removed. If any removal
515
+ // failed, the file still occupies its id, so retire the whole block instead of handing the
516
+ // ids out again.
517
+ if (unremoved.length === 0 && this.#nextId === start + ids.length) this.#nextId = start;
518
+ else for (const id of mapping.values()) this.#retiredIds.add(id);
519
+ if (unremoved.length > 0)
520
+ throw new AggregateError(
521
+ [error, new Error(`Failed to roll back published artifacts: ${unremoved.join(", ")}`)],
522
+ "Attempt-staging publication failed and rollback left artifacts behind.",
523
+ );
524
+ throw error;
525
+ }
526
+ }
527
+ async rollbackLastAttemptCommit(attemptId?: string): Promise<void> {
528
+ if (attemptId === undefined) return;
529
+ const reservation = this.#reservations.get(attemptId);
530
+ if (!reservation) return;
531
+ const unremoved: string[] = [];
532
+ for (const filename of [...reservation.names].reverse())
533
+ if (!(await this.removeNamedBestEffort(filename))) unremoved.push(filename);
534
+ // Only rewind the tail when every published artifact was actually removed; otherwise retire the
535
+ // whole block so a leaked file's id can never be reallocated.
536
+ if (unremoved.length === 0 && this.#nextId === reservation.start + reservation.count)
537
+ this.#nextId = reservation.start;
538
+ else
539
+ for (let index = 0; index < reservation.count; index++)
540
+ this.#retiredIds.add(String(reservation.start + index));
541
+ this.#reservations.delete(attemptId);
542
+ // The reservation is always released so ids can never be reused, but a failed durable removal
543
+ // is reported rather than swallowed.
544
+ if (unremoved.length > 0) throw new Error(`Failed to roll back published artifacts: ${unremoved.join(", ")}`);
545
+ }
546
+
547
+ finalizeLastAttemptCommit(attemptId?: string): void {
548
+ if (attemptId !== undefined) this.#reservations.delete(attemptId);
549
+ }
550
+
551
+ async discardAttemptStaging(): Promise<void> {
552
+ if (this.#store) {
553
+ const cleanupStore = this.#stagingParentStore ?? this.#store;
554
+ const cleanupPath = this.#stagingParentStore ? this.#stagingRelativePath! : "";
555
+ const parentCleanupPath = cleanupPath ? path.posix.dirname(cleanupPath) : "";
556
+ let parentBefore: ReturnType<ManagedSessionDescendantStore["captureTree"]> | undefined;
557
+ if (this.#stagingParentStore) {
558
+ try {
559
+ parentBefore = cleanupStore.captureTree(parentCleanupPath);
560
+ } catch {
561
+ parentBefore = undefined;
562
+ }
563
+ }
564
+ try {
565
+ const snapshot = cleanupStore.captureTree(cleanupPath);
566
+ cleanupStore.removeTreeExpected(cleanupPath, snapshot);
567
+ } catch (error) {
568
+ if (!(error instanceof Error && (error.message === "not_found" || error.message === "cleanup_pending")))
569
+ throw error;
570
+ }
571
+ if (this.#stagingParentStore && parentBefore) {
572
+ try {
573
+ const after = cleanupStore.captureTree(parentCleanupPath);
574
+ const beforePaths = new Set(parentBefore.entries.map(entry => entry.relativePath));
575
+ for (const entry of after.entries) {
576
+ if (
577
+ entry.kind === "directory" &&
578
+ entry.relativePath.length > 0 &&
579
+ !beforePaths.has(entry.relativePath) &&
580
+ /\.removing$/u.test(path.posix.basename(entry.relativePath))
581
+ ) {
582
+ await fs.rm(path.join(cleanupStore.dir, parentCleanupPath, entry.relativePath), {
583
+ recursive: true,
584
+ force: true,
585
+ });
586
+ continue;
587
+ }
588
+ if (
589
+ entry.kind !== "file" ||
590
+ beforePaths.has(entry.relativePath) ||
591
+ !/^\\.gjc-(?:exact-unlink-placeholder|remove)-/u.test(path.posix.basename(entry.relativePath))
592
+ )
593
+ continue;
594
+ const relative = path.posix.join(parentCleanupPath, entry.relativePath);
595
+ const expected = cleanupStore.readExpected(relative);
596
+ if (expected) {
597
+ try {
598
+ cleanupStore.removeExpected(relative, expected);
599
+ } catch (cleanupError) {
600
+ if (!(cleanupError instanceof Error && cleanupError.message === "cleanup_pending"))
601
+ throw cleanupError;
602
+ }
603
+ }
604
+ await fs.rm(path.join(cleanupStore.dir, relative), { force: true }).catch(() => undefined);
605
+ }
606
+ } catch {
607
+ // Retained cleanup evidence is safe to leave for a later maintenance pass.
608
+ }
609
+ }
610
+ this.#store.close();
611
+ } else {
612
+ await fs.rm(this.#dir, { recursive: true, force: true });
613
+ }
614
+ this.#dirCreated = false;
615
+ }
386
616
 
387
617
  /** Persist exact UTF-8 text for heap-eviction rehydration. */
388
618
  async publishExactText(text: string, options: ArtifactPublishOptions = {}): Promise<ArtifactPublishOutcome> {
@@ -176,6 +176,7 @@ function sameDirectoryTreeSnapshotAfterRename(
176
176
  );
177
177
  }
178
178
 
179
+ /** A managed move outcome that reports whether the mutation is known not to have committed. */
179
180
  export class ManagedTreeMoveOutcomeError extends Error {
180
181
  constructor(
181
182
  message: string,
@@ -1391,14 +1392,21 @@ export class ManagedSessionDescendantStore {
1391
1392
  try {
1392
1393
  const directory = fs.opendirSync(this.#baseDir);
1393
1394
  try {
1395
+ // The limit bounds receipt reconciliation work, so it counts receipts —
1396
+ // not every dirent in the directory. Counting all entries conflated
1397
+ // "this scope has too many receipts to reconcile" with "this directory
1398
+ // has many files", so a scope whose bulk was inert zero-byte remnants
1399
+ // aborted before examining a single receipt (observed: 50,003 dirents,
1400
+ // 0 receipts) and every mutation failed permanently. Unbounded receipt
1401
+ // growth is still rejected, which is the case the limit exists for.
1394
1402
  let examined = 0;
1395
1403
  for (let entry = directory.readSync(); entry; entry = directory.readSync()) {
1396
- examined++;
1397
- if (examined > REPLACEMENT_CLEANUP_RECEIPT_SCAN_LIMIT)
1398
- throw new Error("managed_replace_cleanup_receipt_limit_exceeded");
1399
1404
  const name = entry.name;
1400
1405
  if (!name.startsWith(".gjc-replace-receipt-pending-") && !name.startsWith(".gjc-replace-cleanup-"))
1401
1406
  continue;
1407
+ examined++;
1408
+ if (examined > REPLACEMENT_CLEANUP_RECEIPT_SCAN_LIMIT)
1409
+ throw new Error("managed_replace_cleanup_receipt_limit_exceeded");
1402
1410
  if (name.startsWith(".gjc-replace-receipt-pending-")) {
1403
1411
  this.#reconcilePendingReplacementReceipt(path.join(this.#baseDir, name));
1404
1412
  continue;
@@ -1427,9 +1435,21 @@ export class ManagedSessionDescendantStore {
1427
1435
 
1428
1436
  #beforeMutation(): void {
1429
1437
  this.#assertBound();
1438
+ // Reaping is scheduled BEFORE reconciliation, not after. The receipt scan
1439
+ // throws `managed_replace_cleanup_receipt_limit_exceeded` once the bound
1440
+ // directory exceeds the dirent scan limit, and the only thing that brings a
1441
+ // directory back under that limit is this reaper. Scheduling it after the
1442
+ // scan made the recovery unreachable exactly when it was needed: a scope
1443
+ // that crossed the limit failed every mutation forever, so tool-output
1444
+ // eviction could never persist and its originals stayed resident in heap
1445
+ // until the non-disableable emergency compaction floor cut the session
1446
+ // (observed: 50,003 dirents, 47,043 of them zero-byte remnants, 55 failed
1447
+ // evictions and 9 heap compactions in one day). Reaping is throttled,
1448
+ // asynchronous, and never fails the triggering mutation, so hoisting it
1449
+ // cannot make a healthy scope worse.
1450
+ this.#scheduleRemnantReap();
1430
1451
  this.#reconcileReplacementCleanupReceipts();
1431
1452
  this.#assertBound();
1432
- this.#scheduleRemnantReap();
1433
1453
  }
1434
1454
 
1435
1455
  /**
@@ -2371,6 +2391,73 @@ export class ManagedSessionDescendantStore {
2371
2391
  return movedSnapshot.snapshot;
2372
2392
  }
2373
2393
 
2394
+ /** Move one exact managed regular file to an absent destination through retained authority. */
2395
+ moveFileNoReplace(
2396
+ sourceRelativePath: string,
2397
+ destinationRelativePath: string,
2398
+ expected: ManagedFileSnapshot,
2399
+ options?: {
2400
+ /** Subtree store bound to the source's directory; keeps the verification read basename-scoped when this store lacks retained authority. */
2401
+ sourceStore: ManagedSessionDescendantStore;
2402
+ sourceStoreRelativePath: string;
2403
+ },
2404
+ ): ManagedFileSnapshot {
2405
+ this.#assertBound();
2406
+ const sourceResolved = this.#resolve(sourceRelativePath);
2407
+ const destinationResolved = this.#resolve(destinationRelativePath);
2408
+ if (
2409
+ options &&
2410
+ path.resolve(options.sourceStore.#resolve(options.sourceStoreRelativePath)) !== path.resolve(sourceResolved)
2411
+ )
2412
+ throw new ManagedTreeMoveOutcomeError("artifact_source_changed", false);
2413
+ const source = options
2414
+ ? options.sourceStore.readExpected(options.sourceStoreRelativePath)
2415
+ : this.readExpected(sourceRelativePath);
2416
+ if (
2417
+ !source ||
2418
+ !sameReplacementIdentity(source.identity, expected.identity) ||
2419
+ source.identity.sha256 !== expected.identity.sha256 ||
2420
+ !source.bytes.equals(expected.bytes)
2421
+ )
2422
+ throw new ManagedTreeMoveOutcomeError("artifact_source_changed", false);
2423
+
2424
+ const moved = this.#authority
2425
+ ? this.#authority.renameManagedFileNoReplace(
2426
+ this.#relative(sourceResolved),
2427
+ this.#relative(destinationResolved),
2428
+ expected.identity.dev.toString(),
2429
+ expected.identity.ino.toString(),
2430
+ expected.identity.size.toString(),
2431
+ expected.identity.mtimeNs.toString(),
2432
+ expected.identity.ctimeNs.toString(),
2433
+ expected.identity.sha256,
2434
+ )
2435
+ : nativeSessionStorage().renameNoReplacePath(sourceResolved, destinationResolved);
2436
+ const outcome = classifyNativePublishOutcome(moved, this.#authority ? "retained_file" : "direct_rename");
2437
+ if (!outcome.ok)
2438
+ throw new ManagedTreeMoveOutcomeError(publishFailure(outcome).message, mayCleanCurrentStaging(outcome));
2439
+
2440
+ const movedSnapshot = this.readExpected(destinationRelativePath);
2441
+ if (
2442
+ !movedSnapshot ||
2443
+ !sameReplacementIdentity(movedSnapshot.identity, expected.identity) ||
2444
+ movedSnapshot.identity.sha256 !== expected.identity.sha256 ||
2445
+ !movedSnapshot.bytes.equals(expected.bytes)
2446
+ )
2447
+ throw new ManagedTreeMoveOutcomeError("artifact_destination_mismatch", false);
2448
+ if (!this.#authority) {
2449
+ try {
2450
+ fsyncDirectory(path.dirname(sourceResolved));
2451
+ if (path.dirname(sourceResolved) !== path.dirname(destinationResolved))
2452
+ fsyncDirectory(path.dirname(destinationResolved));
2453
+ } catch {
2454
+ throw new ManagedTreeMoveOutcomeError("managed_publish_fsync_failed", false);
2455
+ }
2456
+ }
2457
+ this.#assertBound();
2458
+ return movedSnapshot;
2459
+ }
2460
+
2374
2461
  removeTreeExpected(relativePath: string, expected: NativeDirectoryTreeSnapshot): void {
2375
2462
  this.#beforeMutation();
2376
2463
  this.#assertBound();
@@ -54,8 +54,9 @@ export interface ProviderSafetyStopHintSession {
54
54
  /** Whether an assistant message terminated as a provider safety stop (typed or legacy-persisted). */
55
55
  export function isProviderSafetyStop(message: AssistantMessage): boolean {
56
56
  return (
57
- message.errorKind === "provider_safety_stop" ||
58
- (message.errorMessage !== undefined && isLegacyProviderSafetyStopMessage(message.errorMessage))
57
+ message.stopReason === "error" &&
58
+ (message.errorKind === "provider_safety_stop" ||
59
+ (message.errorMessage !== undefined && isLegacyProviderSafetyStopMessage(message.errorMessage)))
59
60
  );
60
61
  }
61
62