@gajae-code/coding-agent 0.18.7 → 0.18.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) hide show
  1. package/CHANGELOG.md +377 -1
  2. package/dist/types/async/endpoint-id.d.ts +16 -0
  3. package/dist/types/async/job-manager.d.ts +1 -1
  4. package/dist/types/async/support.d.ts +0 -14
  5. package/dist/types/capability/mcp.d.ts +2 -0
  6. package/dist/types/capability/skill.d.ts +2 -0
  7. package/dist/types/capability/types.d.ts +2 -0
  8. package/dist/types/cli/public-command-errors.d.ts +3 -1
  9. package/dist/types/cli/setup-cli.d.ts +1 -1
  10. package/dist/types/commands/deep-interview.d.ts +3 -0
  11. package/dist/types/commands/mcp-serve.d.ts +4 -0
  12. package/dist/types/config/autorouting-tier-map.d.ts +348 -9
  13. package/dist/types/config/file-lock.d.ts +1 -0
  14. package/dist/types/config/model-bindings-applier.d.ts +2 -0
  15. package/dist/types/config/model-profile-activation.d.ts +11 -3
  16. package/dist/types/config/model-registry.d.ts +4 -9
  17. package/dist/types/config/skill-settings-defaults.d.ts +2 -0
  18. package/dist/types/cursor-delete-path.d.ts +1 -0
  19. package/dist/types/cursor-mcp-name.d.ts +8 -0
  20. package/dist/types/customization/inventory.d.ts +2 -0
  21. package/dist/types/debug/report-bundle-parent.d.ts +2 -0
  22. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
  23. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
  24. package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
  25. package/dist/types/discovery/helpers.d.ts +6 -1
  26. package/dist/types/discovery/mcp-json.d.ts +1 -0
  27. package/dist/types/eval/py/kernel.d.ts +18 -0
  28. package/dist/types/export/html/css-color.d.ts +2 -0
  29. package/dist/types/export/html/html-export-destination.d.ts +2 -0
  30. package/dist/types/export/html/template.generated.d.ts +1 -1
  31. package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +5 -0
  32. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  33. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
  34. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
  35. package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
  36. package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
  37. package/dist/types/extensibility/skill-management.d.ts +4 -1
  38. package/dist/types/extensibility/skills.d.ts +2 -0
  39. package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
  40. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
  41. package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
  42. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -3
  43. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
  44. package/dist/types/gjc-runtime/sidecar-bootstrap-env.d.ts +6 -0
  45. package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
  46. package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
  47. package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
  48. package/dist/types/gjc-runtime/state-writer.d.ts +14 -1
  49. package/dist/types/gjc-runtime/tmux-common.d.ts +8 -5
  50. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
  51. package/dist/types/internal-urls/skill-path.d.ts +1 -0
  52. package/dist/types/lsp/config.d.ts +8 -5
  53. package/dist/types/lsp/workspace-path.d.ts +1 -0
  54. package/dist/types/modes/acp/acp-agent.d.ts +7 -0
  55. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  56. package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
  57. package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
  58. package/dist/types/modes/controllers/input-controller.d.ts +9 -0
  59. package/dist/types/modes/interactive-mode.d.ts +1 -0
  60. package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
  61. package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
  62. package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
  63. package/dist/types/progress/collect-project-progress.d.ts +38 -0
  64. package/dist/types/progress/progress-contract.d.ts +154 -0
  65. package/dist/types/progress/progress-snapshot.d.ts +11 -0
  66. package/dist/types/progress/project-progress.d.ts +141 -0
  67. package/dist/types/progress/render-progress.d.ts +28 -0
  68. package/dist/types/runtime-mcp/manager.d.ts +2 -1
  69. package/dist/types/runtime-mcp/mcp-redirect.d.ts +9 -0
  70. package/dist/types/runtime-mcp/oauth-public-url.d.ts +3 -0
  71. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
  72. package/dist/types/runtime-mcp/project-host-values.d.ts +27 -0
  73. package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
  74. package/dist/types/runtime-mcp/types.d.ts +6 -0
  75. package/dist/types/sdk/acp/adapter.d.ts +1 -1
  76. package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
  77. package/dist/types/sdk/broker/broker.d.ts +24 -2
  78. package/dist/types/sdk/broker/doctor-restart.d.ts +5 -0
  79. package/dist/types/sdk/broker/ensure.d.ts +14 -1
  80. package/dist/types/sdk/broker/session-index.d.ts +12 -6
  81. package/dist/types/sdk/bus/index.d.ts +12 -1
  82. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
  83. package/dist/types/sdk/client/client.d.ts +2 -0
  84. package/dist/types/sdk/host/query/handlers.d.ts +3 -0
  85. package/dist/types/sdk/index.d.ts +1 -0
  86. package/dist/types/sdk/router/session-router.d.ts +2 -0
  87. package/dist/types/sdk/session-list.d.ts +1 -1
  88. package/dist/types/sdk/session.d.ts +8 -0
  89. package/dist/types/session/agent-session.d.ts +38 -9
  90. package/dist/types/session/artifacts.d.ts +2 -2
  91. package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
  92. package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
  93. package/dist/types/session/internal/managed-session-storage.d.ts +26 -1
  94. package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
  95. package/dist/types/session/internal/task-artifact-owner-access.d.ts +2 -0
  96. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +9 -1
  97. package/dist/types/session/session-manager.d.ts +31 -5
  98. package/dist/types/session/session-retirement.d.ts +31 -7
  99. package/dist/types/session/session-storage.d.ts +69 -2
  100. package/dist/types/session/task-artifact-owner-codec.d.ts +2 -0
  101. package/dist/types/setup/credential-auto-import.d.ts +1 -1
  102. package/dist/types/setup/credential-import.d.ts +6 -4
  103. package/dist/types/skill-state/active-state.d.ts +38 -0
  104. package/dist/types/skill-state/workflow-mutation-guard.d.ts +3 -0
  105. package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
  106. package/dist/types/task/executor.d.ts +3 -0
  107. package/dist/types/task/isolation-dir.d.ts +26 -0
  108. package/dist/types/tools/ast-edit-write-path.d.ts +13 -0
  109. package/dist/types/tools/bash-skill-urls.d.ts +1 -1
  110. package/dist/types/tools/browser/cdp-url.d.ts +2 -0
  111. package/dist/types/tools/github-side-effect.d.ts +22 -0
  112. package/dist/types/tools/index.d.ts +3 -0
  113. package/dist/types/tools/monitor-mutation.d.ts +9 -0
  114. package/dist/types/tools/python.d.ts +9 -1
  115. package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
  116. package/dist/types/tools/restricted-cwd.d.ts +1 -0
  117. package/dist/types/tools/todo-write.d.ts +6 -0
  118. package/dist/types/utils/tools-checksum.d.ts +7 -0
  119. package/package.json +7 -7
  120. package/scripts/generate-sdk-operation-inventory.ts +21 -0
  121. package/scripts/verify-managed-empty-stop.ts +28 -0
  122. package/src/async/endpoint-id.ts +26 -0
  123. package/src/async/job-manager.ts +247 -132
  124. package/src/async/support.ts +0 -25
  125. package/src/capability/index.ts +15 -0
  126. package/src/capability/mcp.ts +2 -0
  127. package/src/capability/skill.ts +2 -0
  128. package/src/capability/types.ts +2 -0
  129. package/src/cli/auth-broker-cli.ts +1 -1
  130. package/src/cli/customize-doctor.ts +15 -2
  131. package/src/cli/doctor/repairs.ts +4 -1
  132. package/src/cli/doctor-supervisor.ts +3 -0
  133. package/src/cli/public-command-errors.ts +21 -1
  134. package/src/cli/public-command-registry.ts +28 -6
  135. package/src/cli/setup-cli.ts +4 -4
  136. package/src/cli/skills-cli.ts +5 -0
  137. package/src/commands/deep-interview.ts +47 -8
  138. package/src/commands/mcp-serve.ts +7 -0
  139. package/src/commands/sdk.ts +455 -397
  140. package/src/config/autorouting-tier-map.ts +117 -7
  141. package/src/config/file-lock.ts +316 -79
  142. package/src/config/mcp-schema.json +4 -0
  143. package/src/config/model-bindings-applier.ts +20 -0
  144. package/src/config/model-profile-activation.ts +240 -46
  145. package/src/config/model-registry.ts +47 -153
  146. package/src/config/skill-settings-defaults.ts +7 -0
  147. package/src/coordinator-mcp/policy.ts +3 -2
  148. package/src/coordinator-mcp/server.ts +2 -0
  149. package/src/cursor-delete-path.ts +10 -0
  150. package/src/cursor-mcp-name.ts +15 -0
  151. package/src/cursor.ts +13 -2
  152. package/src/customization/inventory.ts +4 -13
  153. package/src/dap/config.ts +2 -2
  154. package/src/debug/report-bundle-parent.ts +22 -0
  155. package/src/debug/report-bundle.ts +3 -0
  156. package/src/debug/system-info.ts +13 -1
  157. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +10 -6
  158. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
  159. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
  160. package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
  161. package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
  162. package/src/defaults/gjc-defaults.test.ts +18 -1
  163. package/src/defaults/gjc-defaults.ts +3 -0
  164. package/src/defaults/gjc-skills.generated.ts +58 -36
  165. package/src/discovery/agents.ts +6 -1
  166. package/src/discovery/builtin.ts +13 -1
  167. package/src/discovery/helpers.ts +171 -29
  168. package/src/discovery/mcp-json.ts +26 -12
  169. package/src/discovery/ssh.ts +1 -1
  170. package/src/discovery/substitute-plugin-root.ts +3 -1
  171. package/src/edit/file-read-cache.ts +1 -1
  172. package/src/eval/py/executor.ts +796 -154
  173. package/src/eval/py/kernel.ts +79 -13
  174. package/src/eval/py/tool-bridge.ts +4 -0
  175. package/src/export/html/css-color.ts +9 -0
  176. package/src/export/html/html-export-destination.ts +16 -0
  177. package/src/export/html/index.ts +7 -10
  178. package/src/export/html/template.generated.ts +1 -1
  179. package/src/extensibility/extensions/function-hooks-internal.ts +27 -0
  180. package/src/extensibility/extensions/runner.ts +15 -1
  181. package/src/extensibility/extensions/types.ts +7 -0
  182. package/src/extensibility/gjc-plugins/mcp-policy.ts +2 -1
  183. package/src/extensibility/gjc-plugins/runtime-adapters.ts +85 -8
  184. package/src/extensibility/plugins/manager.ts +3 -2
  185. package/src/extensibility/plugins/marketplace/cache.ts +15 -0
  186. package/src/extensibility/plugins/marketplace/manager.ts +11 -3
  187. package/src/extensibility/plugins/plugin-link-path.ts +12 -0
  188. package/src/extensibility/runtime-skill-discovery.ts +20 -2
  189. package/src/extensibility/skill-management.ts +9 -2
  190. package/src/extensibility/skills.ts +6 -1
  191. package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
  192. package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
  193. package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
  194. package/src/gjc-runtime/deep-interview-stage.ts +98 -21
  195. package/src/gjc-runtime/gc-runtime.ts +206 -9
  196. package/src/gjc-runtime/launch-worktree.ts +70 -9
  197. package/src/gjc-runtime/psmux-detect.ts +28 -4
  198. package/src/gjc-runtime/ralplan-runtime.ts +447 -52
  199. package/src/gjc-runtime/session-state-sidecar.ts +3 -2
  200. package/src/gjc-runtime/sidecar-bootstrap-env.ts +19 -0
  201. package/src/gjc-runtime/state-argv.ts +2 -0
  202. package/src/gjc-runtime/state-migrations.ts +91 -29
  203. package/src/gjc-runtime/state-runtime.ts +3716 -135
  204. package/src/gjc-runtime/state-writer.ts +105 -16
  205. package/src/gjc-runtime/tmux-common.ts +8 -5
  206. package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
  207. package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
  208. package/src/gjc-runtime/workflow-manifest.ts +54 -21
  209. package/src/harness-control-plane/operate.ts +12 -4
  210. package/src/harness-control-plane/owner.ts +5 -1
  211. package/src/hashline/parser.ts +48 -9
  212. package/src/hashline/recovery.ts +13 -4
  213. package/src/internal-urls/docs-index.generated.ts +20 -19
  214. package/src/internal-urls/skill-path.ts +19 -0
  215. package/src/internal-urls/skill-protocol.ts +2 -0
  216. package/src/lsp/config.ts +12 -63
  217. package/src/lsp/edits.ts +6 -0
  218. package/src/lsp/workspace-path.ts +51 -0
  219. package/src/main.ts +195 -31
  220. package/src/memories/index.ts +88 -7
  221. package/src/modes/acp/acp-agent.ts +88 -4
  222. package/src/modes/components/custom-editor.ts +14 -0
  223. package/src/modes/components/customization/customization-dashboard.ts +12 -0
  224. package/src/modes/components/extensions/extension-dashboard.ts +2 -2
  225. package/src/modes/components/extensions/state-manager.ts +15 -5
  226. package/src/modes/components/hook-editor.ts +1 -1
  227. package/src/modes/components/hook-selector.ts +1 -1
  228. package/src/modes/components/model-selector.ts +1 -4
  229. package/src/modes/components/provider-onboarding-selector.ts +1 -1
  230. package/src/modes/components/status-line/segments.ts +4 -0
  231. package/src/modes/controllers/event-controller.ts +11 -4
  232. package/src/modes/controllers/input-controller.ts +76 -31
  233. package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -16
  234. package/src/modes/controllers/selector-controller.ts +24 -15
  235. package/src/modes/controllers/todo-command-controller.ts +1 -1
  236. package/src/modes/interactive-mode.ts +5 -1
  237. package/src/modes/runtime-init.ts +4 -0
  238. package/src/modes/utils/abort-message.ts +3 -1
  239. package/src/modes/utils/interrupt-ownership.ts +12 -0
  240. package/src/modes/utils/keybinding-matchers.ts +4 -0
  241. package/src/modes/utils/macos-option-forwarding.ts +80 -0
  242. package/src/progress/collect-project-progress.ts +153 -0
  243. package/src/progress/progress-contract.ts +172 -0
  244. package/src/progress/progress-snapshot.ts +140 -0
  245. package/src/progress/project-progress.ts +446 -0
  246. package/src/progress/render-progress.ts +209 -0
  247. package/src/prompts/system/master-mode.md +1 -0
  248. package/src/prompts/tools/skill-discovery.md +1 -1
  249. package/src/runtime-mcp/client.ts +4 -2
  250. package/src/runtime-mcp/config-writer.ts +10 -6
  251. package/src/runtime-mcp/config.ts +1 -0
  252. package/src/runtime-mcp/json-rpc.ts +2 -1
  253. package/src/runtime-mcp/manager.ts +29 -11
  254. package/src/runtime-mcp/mcp-redirect.ts +73 -0
  255. package/src/runtime-mcp/oauth-flow.ts +14 -2
  256. package/src/runtime-mcp/oauth-public-url.ts +14 -0
  257. package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
  258. package/src/runtime-mcp/pool.ts +6 -0
  259. package/src/runtime-mcp/project-host-values.ts +46 -0
  260. package/src/runtime-mcp/smithery-registry.ts +22 -3
  261. package/src/runtime-mcp/transports/http.ts +4 -3
  262. package/src/runtime-mcp/types.ts +6 -0
  263. package/src/sdk/acp/adapter.ts +95 -8
  264. package/src/sdk/broker/broker-exit.ts +52 -23
  265. package/src/sdk/broker/broker.ts +81 -6
  266. package/src/sdk/broker/doctor-restart.ts +4 -0
  267. package/src/sdk/broker/ensure.ts +31 -2
  268. package/src/sdk/broker/lifecycle.ts +711 -66
  269. package/src/sdk/broker/session-index.ts +191 -81
  270. package/src/sdk/bus/chat-daemon-cli.ts +38 -1
  271. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  272. package/src/sdk/bus/index.ts +238 -43
  273. package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
  274. package/src/sdk/cli/session-cli.ts +17 -3
  275. package/src/sdk/client/client.ts +28 -8
  276. package/src/sdk/host/query/handlers.ts +9 -1
  277. package/src/sdk/host/session-runtime.test.ts +2705 -303
  278. package/src/sdk/host/session-runtime.ts +490 -143
  279. package/src/sdk/host/surface-policy.ts +1 -0
  280. package/src/sdk/index.ts +8 -0
  281. package/src/sdk/lifecycle-session.ts +3 -1
  282. package/src/sdk/prompt-deadline-manager.ts +15 -0
  283. package/src/sdk/protocol/operation-inventory.generated.json +146 -0
  284. package/src/sdk/protocol/operation-registry.ts +9 -2
  285. package/src/sdk/router/session-router.ts +82 -18
  286. package/src/sdk/session-list.ts +1 -0
  287. package/src/sdk/session.ts +69 -45
  288. package/src/session/agent-session.ts +679 -238
  289. package/src/session/artifacts.ts +176 -71
  290. package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
  291. package/src/session/internal/managed-session-scope.ts +3779 -462
  292. package/src/session/internal/managed-session-storage.ts +501 -78
  293. package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
  294. package/src/session/internal/task-artifact-owner-access.ts +19 -3
  295. package/src/session/internal/task-artifact-owner-transcript.ts +547 -1
  296. package/src/session/session-manager.ts +951 -133
  297. package/src/session/session-retirement.ts +573 -14
  298. package/src/session/session-storage.ts +447 -15
  299. package/src/session/task-artifact-owner-codec.ts +71 -8
  300. package/src/session/task-artifact-owner-retirement.ts +21 -19
  301. package/src/setup/credential-auto-import.ts +12 -3
  302. package/src/setup/credential-import.ts +176 -5
  303. package/src/setup/paseo/announce.ts +6 -1
  304. package/src/skill-state/active-state.ts +145 -2
  305. package/src/skill-state/workflow-mutation-guard.ts +102 -10
  306. package/src/slash-commands/builtin-registry.ts +39 -9
  307. package/src/slash-commands/helpers/mcp.ts +8 -4
  308. package/src/slash-commands/helpers/progress-report.ts +23 -0
  309. package/src/stt/setup.ts +3 -5
  310. package/src/task/commands.ts +3 -1
  311. package/src/task/executor.ts +4 -0
  312. package/src/task/index.ts +6 -0
  313. package/src/task/isolation-dir.ts +143 -0
  314. package/src/task/worktree.ts +16 -7
  315. package/src/tools/ask.ts +205 -6
  316. package/src/tools/ast-edit-write-path.ts +68 -0
  317. package/src/tools/ast-edit.ts +10 -1
  318. package/src/tools/bash-allowed-prefixes.ts +15 -0
  319. package/src/tools/bash-skill-urls.ts +56 -3
  320. package/src/tools/bash.ts +48 -79
  321. package/src/tools/browser/cdp-url.ts +18 -0
  322. package/src/tools/browser.ts +8 -1
  323. package/src/tools/descriptors.ts +5 -1
  324. package/src/tools/eval.ts +56 -36
  325. package/src/tools/gh.ts +10 -0
  326. package/src/tools/github-side-effect.ts +112 -0
  327. package/src/tools/image-gen.ts +1 -1
  328. package/src/tools/index.ts +3 -0
  329. package/src/tools/monitor-mutation.ts +27 -0
  330. package/src/tools/monitor.ts +7 -0
  331. package/src/tools/python.ts +188 -66
  332. package/src/tools/recipe/index.ts +9 -0
  333. package/src/tools/recipe/recipe-guard.ts +19 -0
  334. package/src/tools/restricted-cwd.ts +10 -0
  335. package/src/tools/skill-discovery.ts +5 -0
  336. package/src/tools/skill.ts +28 -1
  337. package/src/tools/todo-write.ts +10 -1
  338. package/src/tools/tool-catalog.generated.ts +1 -1
  339. package/src/tools/write.ts +1 -0
  340. package/src/utils/external-editor.ts +3 -2
  341. package/src/utils/tools-checksum.ts +34 -0
  342. package/src/utils/tools-manager.ts +24 -6
  343. package/src/web/insane/url-guard.ts +2 -1
  344. package/src/web/search/providers/codex.ts +3 -2
@@ -15,6 +15,7 @@ import {
15
15
  publishManagedFileNoReplace,
16
16
  publishManagedFileNoReplaceSync,
17
17
  } from "./internal/managed-session-storage";
18
+ import { type SessionStorageStat, writeExplicitOwnerOnlyTextSync } from "./session-storage";
18
19
  import { DEFAULT_ARTIFACT_MAX_BYTES, truncateHeadBytes } from "./streaming-output";
19
20
 
20
21
  export interface ManagedOutputGeneration {
@@ -67,6 +68,28 @@ function parseManagedOutputGeneration(value: Uint8Array, outputFilenamePrefix: s
67
68
  function sameGeneration(left: ManagedOutputGeneration, right: ManagedOutputGeneration): boolean {
68
69
  return left.outputFilename === right.outputFilename && left.metadataFilename === right.metadataFilename;
69
70
  }
71
+
72
+ function normalizeByteRange(size: number, range: ArtifactByteRange): { start: number; endExclusive: number } {
73
+ const start = Math.max(0, Math.min(size, range.start ?? 0));
74
+ const endExclusive = Math.max(start, Math.min(size, range.endExclusive ?? size));
75
+ const normalizedStart = Number.isNaN(start) ? 0 : Math.trunc(start);
76
+ const normalizedEnd = Number.isNaN(endExclusive) ? 0 : Math.trunc(endExclusive);
77
+ return {
78
+ start: normalizedStart,
79
+ endExclusive: Math.max(normalizedStart, normalizedEnd),
80
+ };
81
+ }
82
+
83
+ const ARTIFACT_STREAM_CHUNK_BYTES = 64 * 1024;
84
+ const ARTIFACT_RANGE_READ_CHUNK_BYTES = 64 * 1024 * 1024;
85
+
86
+ type AttemptStagingCleanupClassification = { kind: "authorized_quarantine" } | { kind: "failure"; error: unknown };
87
+
88
+ function classifyAttemptStagingCleanupFailure(error: unknown): AttemptStagingCleanupClassification {
89
+ return error instanceof Error && error.message === "cleanup_pending"
90
+ ? { kind: "authorized_quarantine" }
91
+ : { kind: "failure", error };
92
+ }
70
93
  export interface ArtifactSaveOptions {
71
94
  maxBytes?: number;
72
95
  }
@@ -223,7 +246,8 @@ export class ArtifactManager {
223
246
  if (!/^[a-zA-Z0-9_.-]+$/.test(filename)) throw new Error("Unsafe named artifact");
224
247
  await this.#ensureDir();
225
248
  if (this.#store) await this.#store.replace(filename, Buffer.from(content, "utf8"));
226
- else await Bun.write(path.join(this.#dir, filename), content);
249
+ // Named files, including draft.txt, are created here.
250
+ else writeExplicitOwnerOnlyTextSync(path.join(this.#dir, filename), content);
227
251
  }
228
252
 
229
253
  async replaceNamedBytes(filename: string, bytes: Uint8Array): Promise<void> {
@@ -309,26 +333,16 @@ export class ArtifactManager {
309
333
  * Best-effort removal of a previously published named artifact. Used to roll
310
334
  * back staged publications when a transactional operation (e.g. gated
311
335
  * maintenance pruning) is rejected after publication succeeded. Returns false
312
- * when the artifact could not be removed so callers can log the failure
313
- * instead of silently treating the rollback as complete.
336
+ * when the exact named artifact is absent or could not be removed; success does
337
+ * not claim that unrelated native cleanup residue was reclaimed.
314
338
  */
315
339
  async removeNamedBestEffort(filename: string): Promise<boolean> {
316
340
  if (!/^[a-zA-Z0-9_.-]+$/.test(filename)) return false;
317
341
  try {
318
342
  if (this.#store) {
319
- const beforePaths = new Set(await fs.readdir(this.#store.dir));
320
343
  const staged = this.#store.readExpected(filename);
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
- }
344
+ if (!staged) return false;
345
+ this.#store.removeExpected(filename, staged);
332
346
  } else {
333
347
  await fs.unlink(path.join(this.#dir, filename));
334
348
  }
@@ -502,7 +516,13 @@ export class ArtifactManager {
502
516
  publishedNames.push(mappedFilename);
503
517
  }
504
518
  this.#reservations.set(attemptId, { start, count: ids.length, names: [...publishedNames] });
505
- await staging.discardAttemptStaging();
519
+ try {
520
+ await staging.discardAttemptStaging();
521
+ } catch (error) {
522
+ const cleanup = classifyAttemptStagingCleanupFailure(error);
523
+ if (cleanup.kind === "authorized_quarantine") return frozenMap;
524
+ throw cleanup.error;
525
+ }
506
526
  return frozenMap;
507
527
  } catch (error) {
508
528
  // Cleanup is best-effort, but a durable removal failure must never be silent: it leaves a
@@ -550,64 +570,34 @@ export class ArtifactManager {
550
570
 
551
571
  async discardAttemptStaging(): Promise<void> {
552
572
  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
- }
573
+ // Only an attempt manager owns a managed subtree that it may retire. A
574
+ // normal manager's store is borrowed by this cleanup method and its root
575
+ // must never become a deletion target.
576
+ if (!this.#stagingParentStore || !this.#stagingRelativePath) return;
577
+ const cleanupStore = this.#stagingParentStore;
564
578
  try {
565
- const snapshot = cleanupStore.captureTree(cleanupPath);
566
- cleanupStore.removeTreeExpected(cleanupPath, snapshot);
579
+ const snapshot = cleanupStore.captureTree(this.#stagingRelativePath);
580
+ const issuedRoot = this.#store.subtreeRootAuthority;
581
+ if (
582
+ issuedRoot.canonicalPath !== path.resolve(this.#dir) ||
583
+ snapshot.rootDev !== issuedRoot.dev.toString() ||
584
+ snapshot.rootIno !== issuedRoot.ino.toString()
585
+ )
586
+ throw new Error("artifact_staging_root_changed");
587
+ // Stop using and release this attempt's retained writable root before
588
+ // asking the parent authority to retire the tree. The exact tree snapshot
589
+ // above is still checked by native removal; a concurrent change therefore
590
+ // remains a refusal rather than being adopted. Never close the borrowed
591
+ // parent store.
592
+ this.#store.close();
593
+ cleanupStore.removeTreeExpected(this.#stagingRelativePath, snapshot);
567
594
  } catch (error) {
568
- if (!(error instanceof Error && (error.message === "not_found" || error.message === "cleanup_pending")))
569
- throw error;
595
+ if (!(error instanceof Error && error.message === "not_found")) throw error;
596
+ } finally {
597
+ // Replacement, snapshot, and native cleanup failures also release only
598
+ // the capability owned by this attempt manager.
599
+ this.#store.close();
570
600
  }
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
601
  } else {
612
602
  await fs.rm(this.#dir, { recursive: true, force: true });
613
603
  }
@@ -650,7 +640,69 @@ export class ArtifactManager {
650
640
  }
651
641
  }
652
642
 
643
+ #captureManagedArtifact(id: string): {
644
+ filename: string;
645
+ size: number;
646
+ expectedDescriptor: Pick<SessionStorageStat, "dev" | "ino" | "nlink" | "size" | "mtimeNs" | "ctimeNs">;
647
+ } | null {
648
+ if (!this.#store) return null;
649
+ try {
650
+ const entry = this.#store
651
+ .captureTree("")
652
+ .entries.find(candidate => candidate.kind === "file" && candidate.relativePath.startsWith(`${id}.`));
653
+ if (!entry) return null;
654
+ const size = Number(BigInt(entry.size));
655
+ if (!Number.isSafeInteger(size) || size < 0) return null;
656
+ return {
657
+ filename: entry.relativePath,
658
+ size,
659
+ expectedDescriptor: {
660
+ dev: BigInt(entry.dev),
661
+ ino: BigInt(entry.ino),
662
+ nlink: BigInt(entry.nlink),
663
+ size,
664
+ mtimeNs: BigInt(entry.mtimeNs),
665
+ ctimeNs: BigInt(entry.ctimeNs),
666
+ },
667
+ };
668
+ } catch {
669
+ // listFiles historically hides an unavailable managed tree as no artifacts.
670
+ return null;
671
+ }
672
+ }
673
+
653
674
  async readRange(id: string, range: ArtifactByteRange = {}): Promise<string> {
675
+ if (this.#store) {
676
+ const artifact = this.#captureManagedArtifact(id);
677
+ if (!artifact) throw new Error(`artifact://${id} not found`);
678
+ const { start, endExclusive } = normalizeByteRange(artifact.size, range);
679
+ const length = Math.max(0, endExclusive - start);
680
+ let bytes: Uint8Array;
681
+ if (length <= ARTIFACT_RANGE_READ_CHUNK_BYTES) {
682
+ bytes = this.#store.readRangeExpectedSync(
683
+ artifact.filename,
684
+ start,
685
+ length,
686
+ artifact.expectedDescriptor,
687
+ ).bytes;
688
+ } else {
689
+ const complete = Buffer.allocUnsafe(length);
690
+ let offset = 0;
691
+ while (offset < length) {
692
+ const chunkLength = Math.min(ARTIFACT_RANGE_READ_CHUNK_BYTES, length - offset);
693
+ const chunk = this.#store.readRangeExpectedSync(
694
+ artifact.filename,
695
+ start + offset,
696
+ chunkLength,
697
+ artifact.expectedDescriptor,
698
+ ).bytes;
699
+ complete.set(chunk, offset);
700
+ offset += chunk.byteLength;
701
+ }
702
+ bytes = complete;
703
+ }
704
+ return new TextDecoder().decode(bytes);
705
+ }
654
706
  const artifactPath = await this.getPath(id);
655
707
  if (!artifactPath) throw new Error(`artifact://${id} not found`);
656
708
  const file = Bun.file(artifactPath);
@@ -661,6 +713,59 @@ export class ArtifactManager {
661
713
  }
662
714
 
663
715
  async openReadStream(id: string, range: ArtifactByteRange = {}): Promise<ReadableStream<Uint8Array>> {
716
+ if (this.#store) {
717
+ const artifact = this.#captureManagedArtifact(id);
718
+ if (!artifact) throw new Error(`artifact://${id} not found`);
719
+ const { start, endExclusive } = normalizeByteRange(artifact.size, range);
720
+ const lease = this.#store.openReadLease(artifact.filename, artifact.expectedDescriptor);
721
+ let offset = start;
722
+ let closed = false;
723
+ const closeLease = (): void => {
724
+ if (closed) return;
725
+ closed = true;
726
+ lease.close();
727
+ };
728
+ const closeAfterFailure = (): void => {
729
+ try {
730
+ closeLease();
731
+ } catch {
732
+ // Keep the read failure as the stream's observable error.
733
+ }
734
+ };
735
+ return new ReadableStream<Uint8Array>(
736
+ {
737
+ start: controller => {
738
+ if (offset !== endExclusive) return;
739
+ try {
740
+ closeLease();
741
+ controller.close();
742
+ } catch (error) {
743
+ closeAfterFailure();
744
+ controller.error(error);
745
+ }
746
+ },
747
+ pull: controller => {
748
+ if (closed) return;
749
+ try {
750
+ const length = Math.min(ARTIFACT_STREAM_CHUNK_BYTES, endExclusive - offset);
751
+ const bytes = lease.readRange(offset, length);
752
+ if (bytes.byteLength !== length) throw new Error("range_not_present");
753
+ offset += bytes.byteLength;
754
+ if (offset === endExclusive) closeLease();
755
+ controller.enqueue(bytes);
756
+ if (offset === endExclusive) controller.close();
757
+ } catch (error) {
758
+ closeAfterFailure();
759
+ controller.error(error);
760
+ }
761
+ },
762
+ cancel: () => {
763
+ closeLease();
764
+ },
765
+ },
766
+ { highWaterMark: 0 },
767
+ );
768
+ }
664
769
  const artifactPath = await this.getPath(id);
665
770
  if (!artifactPath) throw new Error(`artifact://${id} not found`);
666
771
  const file = Bun.file(artifactPath);
@@ -0,0 +1,319 @@
1
+ import * as path from "node:path";
2
+ import type { SessionStorageFileIdentity } from "../session-storage";
3
+ import {
4
+ assertSessionRoot,
5
+ EMPTY_PAYLOAD_SHA256,
6
+ parseTaskArtifactOwnerDeletionEvidence,
7
+ parseTaskArtifactOwnerLocator,
8
+ parseTaskArtifactOwnerRetirementContinuation,
9
+ parseTaskArtifactOwnerRetirementOutcome,
10
+ type TaskArtifactOwnerDeletionEvidence,
11
+ type TaskArtifactOwnerLocator,
12
+ type TaskArtifactOwnerRetirementContinuation,
13
+ type TaskArtifactOwnerRetirementOutcome,
14
+ type TaskArtifactOwnerStorageContext,
15
+ } from "../task-artifact-owner-codec";
16
+
17
+ export type ManagedGcSessionRetirementState = "prepared" | "artifacts_removed" | "owner_pending" | "owner_retired";
18
+
19
+ export interface ManagedGcSessionRetirementTarget {
20
+ readonly transcriptPath: string;
21
+ readonly sessionId: string;
22
+ readonly cwd: string;
23
+ readonly transcriptIdentity: SessionStorageFileIdentity;
24
+ readonly taskArtifactOwnerLocator: TaskArtifactOwnerLocator;
25
+ }
26
+
27
+ export interface ManagedGcSessionRetirementReceipt extends ManagedGcSessionRetirementTarget {
28
+ readonly state: ManagedGcSessionRetirementState;
29
+ readonly taskArtifactOwnerDeletionEvidence: TaskArtifactOwnerDeletionEvidence;
30
+ readonly artifactsRemoved?: true;
31
+ readonly ownerRetirementAttempt?: number;
32
+ readonly taskArtifactOwnerRetirementOutcome?: TaskArtifactOwnerRetirementOutcome;
33
+ readonly taskArtifactOwnerRetirementContinuation?: TaskArtifactOwnerRetirementContinuation;
34
+ readonly taskArtifactOwnerPayloadRetired?: true;
35
+ readonly taskArtifactOwnerNamespaceRetained?: true;
36
+ readonly taskArtifactOwnerRetired?: true;
37
+ }
38
+
39
+ function isRecord(value: unknown): value is Record<string, unknown> {
40
+ return typeof value === "object" && value !== null && !Array.isArray(value);
41
+ }
42
+
43
+ export function managedGcRetirementIdentityRecord(identity: SessionStorageFileIdentity): Record<string, unknown> {
44
+ return {
45
+ dev: identity.dev.toString(),
46
+ ino: identity.ino.toString(),
47
+ nlink: identity.nlink?.toString(),
48
+ size: identity.size,
49
+ mtimeNs: identity.mtimeNs.toString(),
50
+ sha256: identity.sha256,
51
+ };
52
+ }
53
+
54
+ export function sameIdentity(left: SessionStorageFileIdentity, right: SessionStorageFileIdentity): boolean {
55
+ return (
56
+ left.dev === right.dev &&
57
+ left.ino === right.ino &&
58
+ left.nlink === right.nlink &&
59
+ left.size === right.size &&
60
+ left.mtimeNs === right.mtimeNs &&
61
+ left.sha256 === right.sha256
62
+ );
63
+ }
64
+
65
+ export function managedGcRetirementIdentityValue(value: unknown): SessionStorageFileIdentity {
66
+ if (!isRecord(value)) throw new Error("task_artifact_owner_continuation_corrupt");
67
+ if (
68
+ Object.keys(value).length !== 6 ||
69
+ !["dev", "ino", "nlink", "size", "mtimeNs", "sha256"].every(key => key in value) ||
70
+ typeof value.dev !== "string" ||
71
+ !/^(0|[1-9][0-9]*)$/u.test(value.dev) ||
72
+ typeof value.ino !== "string" ||
73
+ !/^(0|[1-9][0-9]*)$/u.test(value.ino) ||
74
+ typeof value.nlink !== "string" ||
75
+ !/^(0|[1-9][0-9]*)$/u.test(value.nlink) ||
76
+ value.nlink !== "1" ||
77
+ typeof value.size !== "number" ||
78
+ !Number.isSafeInteger(value.size) ||
79
+ value.size < 0 ||
80
+ typeof value.mtimeNs !== "string" ||
81
+ !/^(0|-?[1-9][0-9]*)$/u.test(value.mtimeNs) ||
82
+ typeof value.sha256 !== "string" ||
83
+ !/^[0-9a-f]{64}$/u.test(value.sha256)
84
+ )
85
+ throw new Error("task_artifact_owner_continuation_corrupt");
86
+ return {
87
+ dev: BigInt(value.dev),
88
+ ino: BigInt(value.ino),
89
+ nlink: BigInt(value.nlink),
90
+ size: value.size,
91
+ mtimeNs: BigInt(value.mtimeNs),
92
+ sha256: value.sha256,
93
+ };
94
+ }
95
+
96
+ function validateTarget(
97
+ ownerContext: TaskArtifactOwnerStorageContext,
98
+ target: ManagedGcSessionRetirementTarget,
99
+ ): { locator: TaskArtifactOwnerLocator; identity: SessionStorageFileIdentity } {
100
+ if (
101
+ !target ||
102
+ typeof target.transcriptPath !== "string" ||
103
+ typeof target.sessionId !== "string" ||
104
+ typeof target.cwd !== "string"
105
+ )
106
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
107
+ assertSessionRoot(ownerContext);
108
+ const relativeTranscript = path.relative(ownerContext.sessionsRoot, target.transcriptPath);
109
+ if (
110
+ target.sessionId.length === 0 ||
111
+ !path.isAbsolute(target.transcriptPath) ||
112
+ path.resolve(target.transcriptPath) !== target.transcriptPath ||
113
+ relativeTranscript === "" ||
114
+ path.isAbsolute(relativeTranscript) ||
115
+ relativeTranscript === ".." ||
116
+ relativeTranscript.startsWith(`..${path.sep}`)
117
+ )
118
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
119
+ let locator: TaskArtifactOwnerLocator | undefined;
120
+ try {
121
+ locator = parseTaskArtifactOwnerLocator(target.taskArtifactOwnerLocator);
122
+ } catch {
123
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
124
+ }
125
+ if (!locator) throw new Error("task_artifact_owner_continuation_authority_mismatch");
126
+ let identity: SessionStorageFileIdentity;
127
+ try {
128
+ identity = managedGcRetirementIdentityValue(managedGcRetirementIdentityRecord(target.transcriptIdentity));
129
+ } catch {
130
+ throw new Error("task_artifact_owner_continuation_identity_invalid");
131
+ }
132
+ if (
133
+ identity.nlink !== 1n ||
134
+ identity.size < 0 ||
135
+ !Number.isSafeInteger(identity.size) ||
136
+ !/^[0-9a-f]{64}$/u.test(identity.sha256)
137
+ )
138
+ throw new Error("task_artifact_owner_continuation_identity_invalid");
139
+ return { locator, identity };
140
+ }
141
+
142
+ /**
143
+ * Strictly decode one persisted managed-GC receipt. A decoded `owner_retired` outcome is persisted DTO data only;
144
+ * this pure parser does not verify current filesystem state, payload destruction, or writer quiescence. A future
145
+ * journal reader must perform the live physical-retirement proof for the relevant state before treating it as done.
146
+ */
147
+ export function parseManagedGcRetirementReceipt(
148
+ value: unknown,
149
+ scopeDigest: string,
150
+ ownerContext: TaskArtifactOwnerStorageContext,
151
+ target: ManagedGcSessionRetirementTarget,
152
+ ): ManagedGcSessionRetirementReceipt {
153
+ if (!isRecord(value)) throw new Error("task_artifact_owner_continuation_corrupt");
154
+ const { locator: targetLocator, identity: targetIdentity } = validateTarget(ownerContext, target);
155
+ const record = value;
156
+ const state = record.state;
157
+ const commonKeys = [
158
+ "schemaVersion",
159
+ "state",
160
+ "scope",
161
+ "transcriptPath",
162
+ "sessionId",
163
+ "cwd",
164
+ "transcriptIdentity",
165
+ "taskArtifactOwnerDeletionEvidence",
166
+ ];
167
+ const outcomeValue = record.taskArtifactOwnerRetirementOutcome;
168
+ const outcomeKind = isRecord(outcomeValue) ? outcomeValue.kind : undefined;
169
+ const expectedKeys =
170
+ state === "prepared"
171
+ ? commonKeys
172
+ : state === "artifacts_removed"
173
+ ? [...commonKeys, "artifactsRemoved"]
174
+ : state === "owner_pending"
175
+ ? [
176
+ ...commonKeys,
177
+ "ownerRetirementAttempt",
178
+ "artifactsRemoved",
179
+ "taskArtifactOwnerRetirementOutcome",
180
+ "taskArtifactOwnerRetirementContinuation",
181
+ ...(outcomeKind === "payload_retired"
182
+ ? ["taskArtifactOwnerPayloadRetired", "taskArtifactOwnerNamespaceRetained"]
183
+ : []),
184
+ ]
185
+ : state === "owner_retired"
186
+ ? [
187
+ ...commonKeys,
188
+ "artifactsRemoved",
189
+ "taskArtifactOwnerRetired",
190
+ "taskArtifactOwnerRetirementOutcome",
191
+ ]
192
+ : undefined;
193
+ if (
194
+ !expectedKeys ||
195
+ Object.keys(record).length !== expectedKeys.length ||
196
+ !expectedKeys.every(key => key in record) ||
197
+ record.schemaVersion !== 1 ||
198
+ record.scope !== scopeDigest ||
199
+ record.transcriptPath !== target.transcriptPath ||
200
+ record.sessionId !== target.sessionId ||
201
+ record.cwd !== target.cwd ||
202
+ (state !== "prepared" && record.artifactsRemoved !== true) ||
203
+ (state === "owner_retired" && record.taskArtifactOwnerRetired !== true) ||
204
+ (state === "owner_pending" &&
205
+ (!Number.isSafeInteger(record.ownerRetirementAttempt) || (record.ownerRetirementAttempt as number) < 1))
206
+ )
207
+ throw new Error("task_artifact_owner_continuation_mismatch");
208
+ const identity = managedGcRetirementIdentityValue(record.transcriptIdentity);
209
+ if (!sameIdentity(identity, targetIdentity)) throw new Error("task_artifact_owner_continuation_transcript_replaced");
210
+ let evidence: TaskArtifactOwnerDeletionEvidence;
211
+ try {
212
+ evidence = parseTaskArtifactOwnerDeletionEvidence(record.taskArtifactOwnerDeletionEvidence);
213
+ } catch {
214
+ throw new Error("task_artifact_owner_continuation_evidence_corrupt");
215
+ }
216
+ if (evidence.sessionId !== target.sessionId || JSON.stringify(evidence.locator) !== JSON.stringify(targetLocator))
217
+ throw new Error("task_artifact_owner_continuation_locator_mismatch");
218
+ let outcome: TaskArtifactOwnerRetirementOutcome | undefined;
219
+ if (state === "owner_pending" || state === "owner_retired") {
220
+ outcome = parseTaskArtifactOwnerRetirementOutcome(ownerContext, evidence, outcomeValue);
221
+ if (
222
+ (state === "owner_pending" && outcome.kind === "completed") ||
223
+ (state === "owner_retired" && outcome.kind !== "completed")
224
+ )
225
+ throw new Error("task_artifact_owner_continuation_outcome_mismatch");
226
+ if (outcome.kind !== "completed") {
227
+ let receiptContinuation: TaskArtifactOwnerRetirementContinuation;
228
+ try {
229
+ receiptContinuation = parseTaskArtifactOwnerRetirementContinuation(
230
+ ownerContext,
231
+ evidence,
232
+ record.taskArtifactOwnerRetirementContinuation,
233
+ );
234
+ } catch {
235
+ throw new Error("task_artifact_owner_continuation_outcome_mismatch");
236
+ }
237
+ if (JSON.stringify(receiptContinuation) !== JSON.stringify(outcome.continuation))
238
+ throw new Error("task_artifact_owner_continuation_outcome_mismatch");
239
+ if (
240
+ (outcome.kind === "payload_retired" &&
241
+ (record.taskArtifactOwnerPayloadRetired !== true ||
242
+ record.taskArtifactOwnerNamespaceRetained !== true)) ||
243
+ (outcome.kind !== "payload_retired" &&
244
+ ("taskArtifactOwnerPayloadRetired" in record || "taskArtifactOwnerNamespaceRetained" in record))
245
+ )
246
+ throw new Error("task_artifact_owner_continuation_outcome_mismatch");
247
+ }
248
+ }
249
+ return {
250
+ state: state as ManagedGcSessionRetirementState,
251
+ transcriptPath: target.transcriptPath,
252
+ sessionId: target.sessionId,
253
+ cwd: target.cwd,
254
+ transcriptIdentity: identity,
255
+ taskArtifactOwnerLocator: targetLocator,
256
+ taskArtifactOwnerDeletionEvidence: evidence,
257
+ ...(state !== "prepared" ? { artifactsRemoved: true as const } : {}),
258
+ ...(state === "owner_pending" ? { ownerRetirementAttempt: record.ownerRetirementAttempt as number } : {}),
259
+ ...(outcome ? { taskArtifactOwnerRetirementOutcome: outcome } : {}),
260
+ ...(outcome && outcome.kind !== "completed"
261
+ ? { taskArtifactOwnerRetirementContinuation: outcome.continuation }
262
+ : {}),
263
+ ...(outcome?.kind === "payload_retired"
264
+ ? { taskArtifactOwnerPayloadRetired: true as const, taskArtifactOwnerNamespaceRetained: true as const }
265
+ : {}),
266
+ ...(state === "owner_retired" ? { taskArtifactOwnerRetired: true as const } : {}),
267
+ };
268
+ }
269
+
270
+ /** Check only monotone DTO history; this does not establish physical cleanup or current authority. */
271
+ export function continuationExtendsPrevious(
272
+ previous: TaskArtifactOwnerRetirementContinuation,
273
+ next: TaskArtifactOwnerRetirementContinuation,
274
+ ): boolean {
275
+ const previousTree = previous.retainedTreeSnapshot;
276
+ const nextTree = next.retainedTreeSnapshot;
277
+ if (
278
+ previous.parentIdentity.dev !== next.parentIdentity.dev ||
279
+ previous.parentIdentity.ino !== next.parentIdentity.ino ||
280
+ previousTree.rootDev !== nextTree.rootDev ||
281
+ previousTree.rootIno !== nextTree.rootIno ||
282
+ nextTree.entries.length > previousTree.entries.length
283
+ )
284
+ return false;
285
+ const previousEntries = new Map(previousTree.entries.map(entry => [entry.relativePath, entry]));
286
+ for (const entry of nextTree.entries) {
287
+ const old = previousEntries.get(entry.relativePath);
288
+ if (
289
+ !old ||
290
+ old.kind !== entry.kind ||
291
+ old.dev !== entry.dev ||
292
+ old.ino !== entry.ino ||
293
+ old.nlink !== entry.nlink
294
+ )
295
+ return false;
296
+ if (
297
+ entry.kind === "file" &&
298
+ !(
299
+ (entry.size === old.size && entry.sha256 === old.sha256) ||
300
+ (entry.size === "0" && entry.sha256 === EMPTY_PAYLOAD_SHA256)
301
+ )
302
+ )
303
+ return false;
304
+ if (entry.kind !== "file" && entry.size !== old.size) return false;
305
+ }
306
+ for (const key of [
307
+ "detachedPaths",
308
+ "nativeCodes",
309
+ "retainedSuccessorPaths",
310
+ "retainedPlaceholderPaths",
311
+ "retainedUnknownPaths",
312
+ "windowsErrorCodes",
313
+ ] as const) {
314
+ const oldValues = previous[key] ?? [];
315
+ const nextValues = next[key] ?? [];
316
+ if (!oldValues.every(item => nextValues.includes(item))) return false;
317
+ }
318
+ return previous.payloadDurable !== true || next.payloadDurable === true;
319
+ }