@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
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
+ import * as util from "node:util";
5
6
 
6
7
  import type { RecoveryFsRoot } from "@gajae-code/natives";
7
8
 
@@ -12,6 +13,7 @@ type NativeManagedScope = Pick<
12
13
  | "exactRestore"
13
14
  | "exactUnlink"
14
15
  | "snapshotDirectoryTree"
16
+ | "snapshotEmptyDirectory"
15
17
  | "verifyOwnerOnlyPathSecurity"
16
18
  | "verifyOwnerOnlyPathSecurityExpected"
17
19
  >;
@@ -26,28 +28,53 @@ import {
26
28
  FileSessionStorage,
27
29
  type NativeDirectoryTreeSnapshot,
28
30
  type SessionStorageFileIdentity,
31
+ type SessionStorageSnapshot,
29
32
  type VerifiedSessionDeleteResult,
30
33
  type VerifiedSessionDeleteTarget,
31
34
  } from "../session-storage";
35
+ import { parseFirstJsonlLine } from "../session-transcript-header";
36
+ import {
37
+ parseTaskArtifactOwnerDeletionEvidence,
38
+ parseTaskArtifactOwnerRetirementOutcome,
39
+ type TaskArtifactOwnerDeletionEvidence,
40
+ type TaskArtifactOwnerStorageContext,
41
+ } from "../task-artifact-owner-codec";
42
+ import { verifyTaskArtifactOwnerPhysicalRetirement } from "../task-artifact-owner-retirement";
43
+ import {
44
+ continuationExtendsPrevious,
45
+ type ManagedGcSessionRetirementReceipt,
46
+ type ManagedGcSessionRetirementState,
47
+ type ManagedGcSessionRetirementTarget,
48
+ managedGcRetirementIdentityRecord,
49
+ managedGcRetirementIdentityValue,
50
+ parseManagedGcRetirementReceipt,
51
+ sameIdentity,
52
+ } from "./managed-gc-retirement-codec";
32
53
  import {
33
54
  acquireManagedLock,
34
55
  assertManagedDirectoryRoot,
35
56
  captureManagedFileNoFollow,
57
+ captureManagedFileNoFollowBounded,
36
58
  captureManagedFilePrefixNoFollow,
37
59
  copyManagedFileNoReplace,
38
60
  ensureManagedDirectory,
39
61
  fsyncManagedArtifactTree,
40
62
  inspectManagedFileNoFollow,
63
+ isManagedLockQuarantineName,
41
64
  MANAGED_ARTIFACT_COPY_BATCH_SIZE,
65
+ MANAGED_ARTIFACT_MAX_FILE_BYTES,
42
66
  MANAGED_ARTIFACT_MAX_FILES,
43
67
  MANAGED_ARTIFACT_MAX_TOTAL_BYTES,
68
+ MANAGED_SESSION_READ_RANGE_MAX_BYTES,
44
69
  type ManagedDirectoryRoot,
45
70
  type ManagedFileSnapshot,
46
71
  ManagedPublishError,
47
72
  ManagedSessionDescendantStore,
48
73
  type ManagedSessionSecurityPolicy,
49
74
  type ManagedStorageLock,
75
+ managedDirectoryRoot,
50
76
  managedSecurityFailureClassification,
77
+ parseManagedLockRecord,
51
78
  prepareManagedDirectoryRoot,
52
79
  publishManagedFileNoReplace,
53
80
  publishManagedTombstone,
@@ -56,6 +83,22 @@ import {
56
83
  validateManagedArtifactTree,
57
84
  validateNativeSecurityResult,
58
85
  } from "./managed-session-storage";
86
+ import {
87
+ hasUnsupportedLegacyOwnerTarget as hasUnsupportedLegacyOwnerTargetInternal,
88
+ type ManagedGcOwnerCleanupAuthority,
89
+ type ManagedGcOwnerDeleteFields,
90
+ type ManagedGcOwnerProgress,
91
+ managedGcOwnerDeleteFields as managedGcOwnerDeleteFieldsInternal,
92
+ managedGcOwnerProgressBeforeDelete as managedGcOwnerProgressBeforeDeleteInternal,
93
+ persistManagedGcOwnerTranscriptDeletion,
94
+ persistManagedGcStorageOwnerDisposition as persistManagedGcStorageOwnerDispositionInternal,
95
+ prepareManagedGcOwnerTargets as prepareManagedGcOwnerTargetsInternal,
96
+ publishManagedGcArtifactsRemoved as publishManagedGcArtifactsRemovedInternal,
97
+ retireManagedGcOwnerAfterArtifacts as retireManagedGcOwnerAfterArtifactsInternal,
98
+ taskArtifactOwnerTranscriptResult as taskArtifactOwnerTranscriptResultInternal,
99
+ } from "./managed-task-owner-cleanup";
100
+ import { captureTaskArtifactOwnerDeletionEvidence } from "./task-artifact-owner-access";
101
+ import { taskArtifactOwnerLocatorFromTranscriptBytes } from "./task-artifact-owner-transcript";
59
102
 
60
103
  export const MANAGED_SESSION_LAYOUT_VERSION = 2 as const;
61
104
  export const MANAGED_SESSION_IDENTITY_VERSION = 1 as const;
@@ -74,6 +117,66 @@ export interface ManagedScope {
74
117
  platform: "posix" | "win32";
75
118
  }
76
119
 
120
+ export interface ManagedGcProtocolPathIdentity {
121
+ readonly path: string;
122
+ readonly dev: string;
123
+ readonly ino: string;
124
+ }
125
+
126
+ export interface ManagedGcProtocolFileSnapshot {
127
+ readonly name: string;
128
+ readonly dev: string;
129
+ readonly ino: string;
130
+ readonly nlink: string;
131
+ readonly size: number;
132
+ readonly mtimeNs: string;
133
+ readonly ctimeNs: string;
134
+ readonly mode: number;
135
+ readonly sha256: string;
136
+ }
137
+
138
+ export type ManagedGcProtocolRole = "locks" | "receipts" | "tombstones";
139
+
140
+ export interface ManagedGcProtocolDirectorySnapshot {
141
+ readonly role: ManagedGcProtocolRole;
142
+ readonly path: string;
143
+ readonly dev: string;
144
+ readonly ino: string;
145
+ readonly mtimeNs: string;
146
+ readonly ctimeNs: string;
147
+ readonly mode: number;
148
+ readonly files: readonly ManagedGcProtocolFileSnapshot[];
149
+ }
150
+
151
+ /** Read-only protocol data; never a deletion capability. */
152
+ export interface ManagedGcProtocolScopeSnapshot {
153
+ readonly configuredRoot: ManagedGcProtocolPathIdentity;
154
+ readonly profile: ManagedGcProtocolPathIdentity;
155
+ readonly sessionsRoot: ManagedGcProtocolPathIdentity;
156
+ readonly scope: ManagedGcProtocolPathIdentity;
157
+ readonly binding: ManagedGcProtocolFileSnapshot;
158
+ readonly protocol: ManagedGcProtocolPathIdentity;
159
+ readonly protocolMtimeNs: string;
160
+ readonly protocolCtimeNs: string;
161
+ readonly protocolMode: number;
162
+ readonly directories: readonly ManagedGcProtocolDirectorySnapshot[];
163
+ }
164
+
165
+ export interface ManagedGcProtocolScopeInput {
166
+ readonly scopePath: string;
167
+ readonly scopeIdentity: ManagedGcProtocolPathIdentity;
168
+ readonly bindingIdentity: ManagedGcProtocolFileSnapshot;
169
+ readonly protocolIdentity: ManagedGcProtocolScopeSnapshot["protocol"] & {
170
+ readonly mtimeNs: string;
171
+ readonly ctimeNs: string;
172
+ readonly mode: number;
173
+ };
174
+ }
175
+
176
+ export type ManagedGcProtocolScopeInspector = (
177
+ inputs: readonly ManagedGcProtocolScopeInput[],
178
+ ) => Promise<readonly ManagedGcProtocolScopeSnapshot[]>;
179
+
77
180
  /**
78
181
  * Opaque managed writer authority captured by a trusted destination. The open
79
182
  * transaction must use this authority rather than reacquiring its root from a
@@ -86,9 +189,23 @@ export interface ManagedCandidateWriteAuthority {
86
189
  }
87
190
 
88
191
  const managedRoots = new WeakMap<ManagedScope, ReturnType<typeof prepareManagedDirectoryRoot>>();
192
+ const managedScopeConfigurations = new WeakMap<
193
+ ManagedScope,
194
+ Readonly<
195
+ Pick<ManagedScope, "agentDir" | "sessionsRoot" | "canonicalCwd" | "directoryName" | "directoryPath" | "platform">
196
+ >
197
+ >();
89
198
  const managedDirectoryIdentities = new WeakMap<ManagedScope, { dev: bigint; ino: bigint }>();
90
199
  const managedDirectoryAuthorities = new WeakMap<ManagedScope, RecoveryFsRoot | undefined>();
91
200
  const boundManagedWriteAuthorities = new WeakMap<ManagedScope, ManagedCandidateWriteAuthority>();
201
+ const readManagedGcScopeIdentities = new WeakMap<
202
+ ManagedScope,
203
+ {
204
+ readonly configuredRoot: { readonly path: string; readonly dev: bigint; readonly ino: bigint };
205
+ readonly profile: { readonly path: string; readonly dev: bigint; readonly ino: bigint };
206
+ readonly sessions: { readonly path: string; readonly dev: bigint; readonly ino: bigint };
207
+ }
208
+ >();
92
209
 
93
210
  function bindManagedWriteAuthority(scope: ManagedScope, authority: ManagedCandidateWriteAuthority): void {
94
211
  if (
@@ -179,6 +296,7 @@ export type ManagedScopeErrorCode =
179
296
  | "binding_conflict"
180
297
  | "binding_invalid"
181
298
  | "migration_busy"
299
+ | "managed_gc_journal_capacity_exceeded"
182
300
  | "atomic_unavailable"
183
301
  | "invalid_request"
184
302
  | "durability_failed"
@@ -227,9 +345,14 @@ const managedScopeFailureCodes = new Set<ManagedScopeErrorCode>([
227
345
  "durability_failed",
228
346
  "durability_not_provable",
229
347
  "migration_busy",
348
+ "managed_gc_journal_capacity_exceeded",
230
349
  "capacity_exceeded",
231
350
  ]);
232
351
 
352
+ function isManagedGcJournalCapacityError(error: unknown): boolean {
353
+ return error instanceof Error && error.message === "managed_gc_journal_capacity_exceeded";
354
+ }
355
+
233
356
  /**
234
357
  * A scope whose directory outgrew a scan budget. The binding is untouched and
235
358
  * canonical; only the surrounding entry count is over budget, so this must not
@@ -315,6 +438,7 @@ export type ManagedOpenFailure =
315
438
  | "source_changed"
316
439
  | "unsafe_artifacts"
317
440
  | "artifact_capacity_exceeded"
441
+ | "managed_gc_journal_capacity_exceeded"
318
442
  | "durability_failed"
319
443
  | "atomic_unavailable"
320
444
  | "invalid_request"
@@ -355,6 +479,8 @@ async function deleteSessionVerifiedWithFence(
355
479
  stage: ManagedVerifiedDeleteTestEvent["stage"],
356
480
  lock: ManagedStorageLock,
357
481
  target: VerifiedSessionDeleteTarget,
482
+ scope: ManagedScope,
483
+ retiredTarget: RetiredTarget,
358
484
  verifyAuthority?: () => void,
359
485
  ): Promise<VerifiedSessionDeleteResult> {
360
486
  lock.assertOwned();
@@ -363,7 +489,14 @@ async function deleteSessionVerifiedWithFence(
363
489
  if (hook) await hook({ flow, stage });
364
490
  lock.assertOwned();
365
491
  verifyAuthority?.();
366
- return new FileSessionStorage().deleteSessionVerified(target);
492
+ const ownerFields = await managedGcOwnerDeleteFields(scope, retiredTarget, lock);
493
+ assertOwnerTranscriptLocator(target, ownerFields.taskArtifactOwnerDeletionEvidence);
494
+ lock.assertOwned();
495
+ verifyAuthority?.();
496
+ return new FileSessionStorage().deleteSessionVerified(
497
+ { ...target, ...ownerFields },
498
+ managedGcProtocolInspectorForLock(scope, lock),
499
+ );
367
500
  }
368
501
 
369
502
  type NativeIdentity =
@@ -615,6 +748,17 @@ function resolveManagedScopeInternal(
615
748
  directoryPath: path.join(sessionsRoot, `v2-${digest}`),
616
749
  platform: identity.platform,
617
750
  };
751
+ managedScopeConfigurations.set(
752
+ scope,
753
+ Object.freeze({
754
+ agentDir: scope.agentDir,
755
+ sessionsRoot: scope.sessionsRoot,
756
+ canonicalCwd: scope.canonicalCwd,
757
+ directoryName: scope.directoryName,
758
+ directoryPath: scope.directoryPath,
759
+ platform: scope.platform,
760
+ }),
761
+ );
618
762
  try {
619
763
  const root = fs.lstatSync(sessionsRoot);
620
764
  if (!root.isDirectory() || root.isSymbolicLink()) {
@@ -649,7 +793,10 @@ function resolveManagedScopeInternal(
649
793
  "verify",
650
794
  "directory",
651
795
  );
652
- if (!security.ok && (!allowRepairableAclFailure || security.code !== "acl_verify_failed")) {
796
+ if (
797
+ !security.ok &&
798
+ (!allowRepairableAclFailure || (security.code !== "acl_verify_failed" && security.code !== "owner_mismatch"))
799
+ ) {
653
800
  return {
654
801
  kind: "error",
655
802
  code: "binding_invalid",
@@ -677,6 +824,71 @@ export function resolveManagedScopeForWrite(input: ManagedScopeInput): ManagedSc
677
824
  return resolveManagedScopeInternal(input, true);
678
825
  }
679
826
 
827
+ /** Resolve only an existing, authenticated v2 scope; this path never initializes or repairs storage. */
828
+ export function resolveManagedGcScopeForRead(input: ManagedScopeInput): ManagedScopeResolution {
829
+ const resolved = resolveManagedScope(input);
830
+ if (resolved.kind === "error") return resolved;
831
+ const scope = resolved.scope;
832
+ try {
833
+ const rootPath = configuredRootPath(scope);
834
+ if (
835
+ path.resolve(scope.agentDir) !== scope.agentDir ||
836
+ path.resolve(scope.sessionsRoot) !== scope.sessionsRoot ||
837
+ path.dirname(scope.directoryPath) !== scope.sessionsRoot ||
838
+ scope.directoryName !== `v2-${scopeDigest(scope.platform, scope.canonicalCwd)}`
839
+ )
840
+ throw new Error("managed_gc_scope_authority_mismatch");
841
+ const captureDirectory = (pathname: string) => {
842
+ const stat = fs.lstatSync(pathname, { bigint: true });
843
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("managed_gc_scope_authority_mismatch");
844
+ const security = validateNativeSecurityResult(
845
+ verifyExistingManagedScopeDirectory(pathname),
846
+ "verify",
847
+ "directory",
848
+ );
849
+ if (!security.ok) throw new Error("managed_gc_scope_authority_mismatch");
850
+ return { path: pathname, dev: stat.dev, ino: stat.ino };
851
+ };
852
+ const configuredRoot = captureDirectory(rootPath);
853
+ const profile = captureDirectory(scope.agentDir);
854
+ const sessions = captureDirectory(scope.sessionsRoot);
855
+ const scopeDirectory = captureDirectory(scope.directoryPath);
856
+ const rootAuthority = managedDirectoryRoot(rootPath);
857
+ if (
858
+ rootAuthority.canonicalPath !== rootPath ||
859
+ rootAuthority.dev !== BigInt.asUintN(64, configuredRoot.dev) ||
860
+ rootAuthority.ino !== BigInt.asUintN(64, configuredRoot.ino)
861
+ )
862
+ throw new Error("managed_gc_scope_authority_mismatch");
863
+ const bindingPath = path.join(scope.directoryPath, MANAGED_SESSION_BINDING_FILE);
864
+ const binding = captureManagedFileNoFollow(bindingPath);
865
+ if (validateBindingRaw(scope, binding.bytes.toString("utf8")))
866
+ throw new Error("managed_gc_scope_authority_mismatch");
867
+ for (const expected of [configuredRoot, profile, sessions, scopeDirectory]) {
868
+ const current = fs.lstatSync(expected.path, { bigint: true });
869
+ if (
870
+ !current.isDirectory() ||
871
+ current.isSymbolicLink() ||
872
+ current.dev !== expected.dev ||
873
+ current.ino !== expected.ino
874
+ )
875
+ throw new Error("managed_gc_scope_authority_mismatch");
876
+ }
877
+ managedRoots.set(scope, rootAuthority);
878
+ managedDirectoryIdentities.set(scope, { dev: scopeDirectory.dev, ino: scopeDirectory.ino });
879
+ managedDirectoryAuthorities.set(scope, undefined);
880
+ readManagedGcScopeIdentities.set(scope, { configuredRoot, profile, sessions });
881
+ return resolved;
882
+ } catch (error) {
883
+ return {
884
+ kind: "error",
885
+ code: "binding_invalid",
886
+ message: "The existing managed GC read authority could not be verified.",
887
+ cause: { classification: managedScopeResidualClassification(error) ?? "managed_gc_scope_authority_mismatch" },
888
+ };
889
+ }
890
+ }
891
+
680
892
  function legacyDirectoryNames(
681
893
  platform: ManagedScope["platform"],
682
894
  canonicalCwd: string,
@@ -1015,6 +1227,17 @@ export async function ensureManagedScope(
1015
1227
  const preparedDirectory = fs.lstatSync(scope.directoryPath, { bigint: true });
1016
1228
  if (!preparedDirectory.isDirectory() || preparedDirectory.isSymbolicLink()) throw new Error("reparse_point");
1017
1229
  managedDirectoryIdentities.set(scope, { dev: preparedDirectory.dev, ino: preparedDirectory.ino });
1230
+ if (!managedDirectoryAuthorities.has(scope)) {
1231
+ const authority = boundManagedWriteAuthorities.get(scope);
1232
+ const retainedAuthority =
1233
+ authority?.retainedAuthority &&
1234
+ authority.retainedDirectory !== undefined &&
1235
+ path.resolve(authority.retainedDirectory) === path.resolve(scope.directoryPath)
1236
+ ? authority.retainedAuthority
1237
+ : retainManagedDirectoryAuthority(root, scope.directoryPath, managedDirectoryIdentityForScope(scope));
1238
+ assertRetainedManagedDirectoryIdentity(scope);
1239
+ managedDirectoryAuthorities.set(scope, retainedAuthority);
1240
+ }
1018
1241
  return { kind: "resolved", scope };
1019
1242
  } catch (error) {
1020
1243
  const publication = error instanceof ManagedPublishError ? error : undefined;
@@ -1815,6 +2038,7 @@ function expectedFailure(error: unknown): ManagedOpenFailure {
1815
2038
  message === "source_changed" ||
1816
2039
  message === "unsafe_artifacts" ||
1817
2040
  message === "artifact_capacity_exceeded" ||
2041
+ message === "managed_gc_journal_capacity_exceeded" ||
1818
2042
  message === "durability_failed" ||
1819
2043
  message === "atomic_unavailable" ||
1820
2044
  message === "invalid_request" ||
@@ -2076,235 +2300,2829 @@ function preparedReceiptMatches(
2076
2300
  }
2077
2301
  }
2078
2302
 
2079
- type RetiredTarget = ManagedCandidate;
2303
+ type RetiredTarget = ManagedCandidate & {
2304
+ readonly taskArtifactOwnerDeletionEvidence?: TaskArtifactOwnerDeletionEvidence;
2305
+ };
2080
2306
 
2081
- function retiredTargets(scope: ManagedScope, pathname: string): readonly RetiredTarget[] | undefined {
2082
- try {
2083
- const value: unknown = JSON.parse(captureManagedFileNoFollow(pathname).bytes.toString("utf8"));
2084
- if (!value || typeof value !== "object") return undefined;
2085
- const record = value as { schemaVersion?: unknown; state?: unknown; scope?: unknown; targets?: unknown };
2086
- if (
2087
- record.schemaVersion !== 2 ||
2088
- record.state !== "retired" ||
2089
- record.scope !== scopeDigest(scope.platform, scope.canonicalCwd) ||
2090
- !Array.isArray(record.targets)
2091
- )
2092
- return undefined;
2093
- const targets: RetiredTarget[] = [];
2094
- for (const target of record.targets) {
2095
- if (!target || typeof target !== "object" || Array.isArray(target)) return undefined;
2096
- const item = target as Record<string, unknown>;
2097
- const identity = item.identity;
2098
- if (!identity || typeof identity !== "object" || Array.isArray(identity)) return undefined;
2099
- const fields = identity as Record<string, unknown>;
2307
+ const MANAGED_GC_RETIREMENT_PREFIX = "gc-retirement-";
2308
+ const MANAGED_GC_RETIREMENT_RECEIPTS = `${MANAGED_INTERNAL_DIRECTORY}/${MANAGED_RECEIPTS_DIRECTORY}`;
2309
+ const MANAGED_GC_RETIREMENT_LOCKS = `${MANAGED_INTERNAL_DIRECTORY}/${MANAGED_LOCKS_DIRECTORY}`;
2310
+ // History capacity is shared by every transcript in one scope, so publishers must share one lease.
2311
+ const MANAGED_GC_RETIREMENT_SCOPE_LOCK_NAME = `${MANAGED_GC_RETIREMENT_PREFIX}${createHash("sha256")
2312
+ .update("managed-gc-retirement-scope-lock-v1", "utf8")
2313
+ .digest("hex")}`;
2314
+ interface ManagedGcTrustedScope {
2315
+ readonly root: ManagedDirectoryRoot;
2316
+ readonly retainedAuthority: RecoveryFsRoot | undefined;
2317
+ readonly identity: { readonly dev: bigint; readonly ino: bigint };
2318
+ readonly policy: ManagedSessionSecurityPolicy;
2319
+ readonly ownerContext: TaskArtifactOwnerStorageContext;
2320
+ }
2321
+ function managedGcTrustedScope(scope: ManagedScope): ManagedGcTrustedScope {
2322
+ const root = managedRoots.get(scope);
2323
+ const identity = managedDirectoryIdentities.get(scope);
2324
+ const configuration = managedScopeConfigurations.get(scope);
2325
+ if (!root || !identity || !configuration || !managedDirectoryAuthorities.has(scope))
2326
+ throw new Error("managed_gc_scope_authority_unavailable");
2327
+ if (
2328
+ scope.agentDir !== configuration.agentDir ||
2329
+ scope.sessionsRoot !== configuration.sessionsRoot ||
2330
+ scope.canonicalCwd !== configuration.canonicalCwd ||
2331
+ scope.directoryName !== configuration.directoryName ||
2332
+ scope.directoryPath !== configuration.directoryPath ||
2333
+ scope.platform !== configuration.platform ||
2334
+ scope.apiVersion !== 1 ||
2335
+ scope.layoutVersion !== MANAGED_SESSION_LAYOUT_VERSION ||
2336
+ scope.identityVersion !== MANAGED_SESSION_IDENTITY_VERSION
2337
+ )
2338
+ throw new Error("managed_gc_scope_authority_mismatch");
2339
+ const policy: ManagedSessionSecurityPolicy =
2340
+ scope.platform === "win32" ? "windows-existing-verify-first" : "default";
2341
+ assertManagedDirectoryRoot(root);
2342
+ managedDirectoryIdentityForScope(scope);
2343
+ assertRetainedManagedDirectoryIdentity(scope);
2344
+ const readIdentities = readManagedGcScopeIdentities.get(scope);
2345
+ if (readIdentities) {
2346
+ for (const expected of [readIdentities.configuredRoot, readIdentities.profile, readIdentities.sessions]) {
2347
+ const current = fs.lstatSync(expected.path, { bigint: true });
2100
2348
  if (
2101
- typeof item.path !== "string" ||
2102
- typeof item.sessionId !== "string" ||
2103
- typeof item.cwd !== "string" ||
2104
- !pathIsWithin(scope.sessionsRoot, item.path) ||
2105
- (item.provenance !== undefined && item.provenance !== "v2" && item.provenance !== "legacy") ||
2106
- typeof fields.canonicalPath !== "string" ||
2107
- typeof fields.dev !== "string" ||
2108
- typeof fields.ino !== "string" ||
2109
- typeof fields.size !== "number" ||
2110
- typeof fields.mtimeMs !== "number" ||
2111
- typeof fields.mtimeNs !== "string" ||
2112
- typeof fields.sha256 !== "string"
2349
+ !current.isDirectory() ||
2350
+ current.isSymbolicLink() ||
2351
+ current.dev !== expected.dev ||
2352
+ current.ino !== expected.ino
2113
2353
  )
2114
- return undefined;
2115
- const provenance =
2116
- item.provenance === "v2" || item.provenance === "legacy"
2117
- ? item.provenance
2118
- : path.dirname(item.path) === scope.directoryPath
2119
- ? "v2"
2120
- : "legacy";
2121
- targets.push({
2122
- path: item.path,
2123
- sessionId: item.sessionId,
2124
- cwd: item.cwd,
2125
- provenance,
2126
- migrationState: provenance === "v2" ? "native_v2" : "legacy_unmigrated",
2127
- identity: {
2128
- canonicalPath: fields.canonicalPath,
2129
- dev: BigInt(fields.dev),
2130
- ino: BigInt(fields.ino),
2131
- size: fields.size,
2132
- mtimeMs: fields.mtimeMs,
2133
- mtimeNs: BigInt(fields.mtimeNs),
2134
- sha256: fields.sha256,
2135
- sessionId: item.sessionId,
2136
- },
2137
- });
2354
+ throw new Error("managed_gc_scope_authority_mismatch");
2355
+ }
2356
+ for (const pathname of [
2357
+ readIdentities.configuredRoot.path,
2358
+ readIdentities.profile.path,
2359
+ readIdentities.sessions.path,
2360
+ ]) {
2361
+ const verified = validateNativeSecurityResult(
2362
+ verifyExistingManagedScopeDirectory(pathname),
2363
+ "verify",
2364
+ "directory",
2365
+ );
2366
+ if (!verified.ok) throw new Error("managed_gc_scope_authority_mismatch");
2138
2367
  }
2139
- return targets;
2140
- } catch {
2141
- return undefined;
2142
2368
  }
2369
+ return {
2370
+ root,
2371
+ retainedAuthority: managedDirectoryAuthorities.get(scope),
2372
+ identity,
2373
+ policy,
2374
+ ownerContext: Object.freeze({
2375
+ rootAuthority: root,
2376
+ sessionsRoot: scope.sessionsRoot,
2377
+ securityPolicy: policy,
2378
+ profileAgentDir: scope.agentDir,
2379
+ }),
2380
+ };
2381
+ }
2382
+ /** Build owner-storage authority only from a previously prepared trusted managed scope. */
2383
+ export function taskArtifactOwnerStorageContextForScope(scope: ManagedScope): TaskArtifactOwnerStorageContext {
2384
+ return managedGcTrustedScope(scope).ownerContext;
2143
2385
  }
2144
2386
 
2145
- /**
2146
- * Append-only cleanup state machine:
2147
- * `pending(N)` durably authorizes only its planned quarantine names before detach;
2148
- * a returned partial result appends `pending(N + 1)` with the observed detached
2149
- * identity/path. Restart accepts only a contiguous, target-bound sequence.
2150
- */
2151
- type RetainedArtifactsRootReceipt = {
2152
- path: string;
2153
- identity: SessionStorageFileIdentity;
2154
- tree: NativeDirectoryTreeSnapshot;
2155
- };
2387
+ function managedGcOwnerCleanupAuthority(scope: ManagedScope, lock: ManagedStorageLock): ManagedGcOwnerCleanupAuthority {
2388
+ return {
2389
+ agentDir: scope.agentDir,
2390
+ sessionsRoot: scope.sessionsRoot,
2391
+ directoryPath: scope.directoryPath,
2392
+ storageContext: taskArtifactOwnerStorageContextForScope(scope),
2393
+ inspectProtocol: managedGcProtocolInspectorForLock(scope, lock),
2394
+ assertOwned: () => lock.assertOwned(),
2395
+ bindTarget: transcriptPath => bindManagedGcSessionRetirementTarget(scope, transcriptPath),
2396
+ readReceipt: async transcriptPath => {
2397
+ lock.assertOwned();
2398
+ const receipt = await readManagedGcSessionRetirementReceipt(scope, transcriptPath);
2399
+ lock.assertOwned();
2400
+ return receipt;
2401
+ },
2402
+ publishReceipt: async receipt => {
2403
+ lock.assertOwned();
2404
+ const published = await publishManagedGcSessionRetirementReceipt(scope, receipt);
2405
+ lock.assertOwned();
2406
+ return published;
2407
+ },
2408
+ findCompletedRetirement: async (evidence, exceptTranscriptPath) => {
2409
+ lock.assertOwned();
2410
+ const receipts = await discoverManagedGcSessionRetirementReceipts({
2411
+ agentDir: scope.agentDir,
2412
+ sessionsRoot: scope.sessionsRoot,
2413
+ });
2414
+ lock.assertOwned();
2415
+ const match = receipts.find(
2416
+ ({ scope: source, receipt }) =>
2417
+ source.agentDir === scope.agentDir &&
2418
+ source.sessionsRoot === scope.sessionsRoot &&
2419
+ source.directoryPath !== scope.directoryPath &&
2420
+ receipt.transcriptPath !== exceptTranscriptPath &&
2421
+ receipt.state === "owner_retired" &&
2422
+ receipt.taskArtifactOwnerRetirementOutcome?.kind === "completed" &&
2423
+ deepSame(receipt.taskArtifactOwnerDeletionEvidence, evidence),
2424
+ );
2425
+ return match
2426
+ ? {
2427
+ scope: {
2428
+ agentDir: match.scope.agentDir,
2429
+ sessionsRoot: match.scope.sessionsRoot,
2430
+ directoryPath: match.scope.directoryPath,
2431
+ },
2432
+ receipt: match.receipt,
2433
+ }
2434
+ : undefined;
2435
+ },
2436
+ };
2437
+ }
2156
2438
 
2157
- function retainedArtifactsRootReceipt(receipt: CleanupReceipt): RetainedArtifactsRootReceipt | undefined {
2158
- const pathname = receipt.detachedArtifactsPath ?? deterministicRemovalRoot(receipt.plannedArtifactsPath);
2159
- if (!fs.existsSync(pathname)) return undefined;
2160
- if (!isQuarantinePath(receipt.target, pathname) || !receipt.expectedArtifactsIdentity)
2161
- throw new Error("durability_failed");
2162
- const identity = artifactIdentityAt(pathname);
2163
- if (
2164
- !identity ||
2165
- identity.dev !== receipt.expectedArtifactsIdentity.dev ||
2166
- identity.ino !== receipt.expectedArtifactsIdentity.ino
2167
- )
2168
- throw new Error("durability_failed");
2169
- const tree = snapshotArtifactTree(pathname);
2170
- if (!artifactTreePayloadAbsent(tree)) throw new Error("durability_failed");
2171
- return { path: pathname, identity, tree };
2439
+ function hasUnsupportedLegacyOwnerTarget(scope: ManagedScope, target: RetiredTarget): boolean {
2440
+ return hasUnsupportedLegacyOwnerTargetInternal(scope.directoryPath, target);
2172
2441
  }
2173
2442
 
2174
- function retainedArtifactsRootMatches(record: Record<string, unknown>): boolean {
2175
- if (record.retainedArtifactsRoot === undefined) return true;
2176
- if (!record.retainedArtifactsRoot || typeof record.retainedArtifactsRoot !== "object")
2177
- throw new Error("durability_failed");
2178
- const retained = record.retainedArtifactsRoot as Record<string, unknown>;
2179
- const identity = retained.identity;
2180
- const tree = artifactTreeSnapshot(retained.tree);
2181
- const expectedRetainedPath =
2182
- typeof record.detachedArtifactsPath === "string"
2183
- ? record.detachedArtifactsPath
2184
- : typeof record.plannedArtifactsPath === "string"
2185
- ? deterministicRemovalRoot(record.plannedArtifactsPath)
2186
- : undefined;
2187
- if (
2188
- !expectedRetainedPath ||
2189
- retained.path !== expectedRetainedPath ||
2190
- record.artifactsPayloadDurable !== true ||
2191
- !identity ||
2192
- typeof identity !== "object" ||
2193
- Array.isArray(identity) ||
2194
- !tree ||
2195
- !artifactTreePayloadAbsent(tree)
2196
- )
2197
- throw new Error("durability_failed");
2198
- const artifactIdentity = identity as Record<string, unknown>;
2199
- if (
2200
- typeof artifactIdentity.dev !== "string" ||
2201
- typeof artifactIdentity.ino !== "string" ||
2202
- typeof artifactIdentity.size !== "number" ||
2203
- typeof artifactIdentity.mtimeNs !== "string" ||
2204
- typeof artifactIdentity.sha256 !== "string"
2205
- )
2206
- throw new Error("durability_failed");
2207
- if (!fs.existsSync(retained.path)) return true;
2208
- const observed = artifactIdentityAt(retained.path);
2209
- if (
2210
- !observed ||
2211
- observed.dev !== BigInt(artifactIdentity.dev) ||
2212
- observed.ino !== BigInt(artifactIdentity.ino) ||
2213
- !artifactTreePayloadAbsent(snapshotArtifactTree(retained.path))
2214
- )
2215
- throw new Error("durability_failed");
2216
- return true;
2443
+ async function prepareManagedGcOwnerTargets(
2444
+ scope: ManagedScope,
2445
+ targets: readonly RetiredTarget[],
2446
+ lock: ManagedStorageLock,
2447
+ ): Promise<RetiredTarget[]> {
2448
+ return prepareManagedGcOwnerTargetsInternal(managedGcOwnerCleanupAuthority(scope, lock), targets);
2217
2449
  }
2218
2450
 
2219
- type CleanupReceipt = {
2220
- attempt: number;
2221
- target: RetiredTarget;
2222
- expectedArtifactsIdentity?: SessionStorageFileIdentity;
2223
- expectedArtifactsTree?: NativeDirectoryTreeSnapshot;
2224
- artifactsPayloadDurable?: true;
2225
- artifactsRemovedAttempt?: number;
2226
- detachedArtifactsPath?: string;
2227
- detachedTranscriptPath?: string;
2228
- transcriptPayloadDurable?: true;
2229
- retainedArtifactsSuccessorPath?: string;
2230
- retainedArtifactsPlaceholderPath?: string;
2231
- retainedArtifactsUnknownPath?: string;
2232
- retainedTranscriptSuccessorPath?: string;
2233
- retainedTranscriptPlaceholderPath?: string;
2234
- retainedTranscriptUnknownPath?: string;
2451
+ async function managedGcOwnerDeleteFields(
2452
+ scope: ManagedScope,
2453
+ target: RetiredTarget,
2454
+ lock: ManagedStorageLock,
2455
+ ): Promise<ManagedGcOwnerDeleteFields> {
2456
+ return managedGcOwnerDeleteFieldsInternal(managedGcOwnerCleanupAuthority(scope, lock), target);
2457
+ }
2235
2458
 
2236
- plannedArtifactsPath: string;
2237
- plannedTranscriptPath: string;
2238
- };
2459
+ async function publishManagedGcArtifactsRemoved(
2460
+ scope: ManagedScope,
2461
+ target: RetiredTarget,
2462
+ lock: ManagedStorageLock,
2463
+ ): Promise<ManagedGcSessionRetirementReceipt | undefined> {
2464
+ return publishManagedGcArtifactsRemovedInternal(managedGcOwnerCleanupAuthority(scope, lock), target);
2465
+ }
2239
2466
 
2240
- function cleanupReceiptPath(
2241
- tombstone: string,
2467
+ async function retireManagedGcOwnerAfterArtifacts(
2468
+ scope: ManagedScope,
2242
2469
  target: RetiredTarget,
2243
- state: "pending" | "artifacts_removed" | "completed",
2244
- attempt: number,
2245
- ): string {
2246
- return path.join(
2247
- path.dirname(tombstone),
2248
- `${path.basename(tombstone, ".json")}.${stableOperationName(target)}.cleanup-${state}-${attempt}.json`,
2470
+ tombstoneTargets: readonly RetiredTarget[],
2471
+ lock: ManagedStorageLock,
2472
+ ): Promise<ManagedGcOwnerProgress> {
2473
+ return retireManagedGcOwnerAfterArtifactsInternal(
2474
+ managedGcOwnerCleanupAuthority(scope, lock),
2475
+ target,
2476
+ tombstoneTargets,
2249
2477
  );
2250
2478
  }
2251
2479
 
2252
- function cleanupReceipt(scope: ManagedScope, tombstone: string, receipt: CleanupReceipt): Record<string, unknown> {
2253
- return {
2254
- schemaVersion: 2,
2255
- state: "cleanup_pending",
2256
- scope: scopeDigest(scope.platform, scope.canonicalCwd),
2257
- tombstone,
2258
- attempt: receipt.attempt,
2259
- target: {
2260
- path: receipt.target.path,
2261
- sessionId: receipt.target.sessionId,
2262
- cwd: receipt.target.cwd,
2263
- identity: receipt.target.identity,
2264
- },
2265
- ...(receipt.expectedArtifactsIdentity ? { expectedArtifactsIdentity: receipt.expectedArtifactsIdentity } : {}),
2266
- ...(receipt.expectedArtifactsTree ? { expectedArtifactsTree: receipt.expectedArtifactsTree } : {}),
2267
- ...(receipt.artifactsPayloadDurable === true ? { artifactsPayloadDurable: true } : {}),
2268
- ...(receipt.artifactsRemovedAttempt !== undefined
2269
- ? { artifactsRemovedAttempt: receipt.artifactsRemovedAttempt }
2270
- : {}),
2271
- ...(receipt.detachedArtifactsPath ? { detachedArtifactsPath: receipt.detachedArtifactsPath } : {}),
2272
- ...(receipt.detachedTranscriptPath ? { detachedTranscriptPath: receipt.detachedTranscriptPath } : {}),
2273
- ...(receipt.transcriptPayloadDurable === true ? { transcriptPayloadDurable: true } : {}),
2274
- ...(receipt.retainedArtifactsSuccessorPath
2275
- ? { retainedArtifactsSuccessorPath: receipt.retainedArtifactsSuccessorPath }
2276
- : {}),
2277
- ...(receipt.retainedArtifactsPlaceholderPath
2278
- ? { retainedArtifactsPlaceholderPath: receipt.retainedArtifactsPlaceholderPath }
2279
- : {}),
2280
- ...(receipt.retainedArtifactsUnknownPath
2281
- ? { retainedArtifactsUnknownPath: receipt.retainedArtifactsUnknownPath }
2282
- : {}),
2283
- ...(receipt.retainedTranscriptSuccessorPath
2284
- ? { retainedTranscriptSuccessorPath: receipt.retainedTranscriptSuccessorPath }
2285
- : {}),
2286
- ...(receipt.retainedTranscriptPlaceholderPath
2287
- ? { retainedTranscriptPlaceholderPath: receipt.retainedTranscriptPlaceholderPath }
2288
- : {}),
2289
- ...(receipt.retainedTranscriptUnknownPath
2290
- ? { retainedTranscriptUnknownPath: receipt.retainedTranscriptUnknownPath }
2291
- : {}),
2292
- plannedArtifactsPath: receipt.plannedArtifactsPath,
2293
- plannedTranscriptPath: receipt.plannedTranscriptPath,
2294
- };
2480
+ async function persistManagedGcStorageOwnerDisposition(
2481
+ scope: ManagedScope,
2482
+ target: RetiredTarget,
2483
+ deletion: Extract<VerifiedSessionDeleteResult, { kind: "cleanup_pending"; phase: "task_artifact_owner" }>,
2484
+ lock: ManagedStorageLock,
2485
+ ): Promise<ManagedGcOwnerProgress> {
2486
+ return persistManagedGcStorageOwnerDispositionInternal(
2487
+ managedGcOwnerCleanupAuthority(scope, lock),
2488
+ target,
2489
+ deletion,
2490
+ );
2295
2491
  }
2296
2492
 
2297
- type CleanupArtifactsRemovedEvidence = { retainedArtifactsRootPath?: string };
2493
+ async function managedGcOwnerProgressBeforeDelete(
2494
+ scope: ManagedScope,
2495
+ target: RetiredTarget,
2496
+ tombstoneTargets: readonly RetiredTarget[],
2497
+ artifactsRemoved: boolean,
2498
+ lock: ManagedStorageLock,
2499
+ ): Promise<ManagedGcOwnerProgress> {
2500
+ return managedGcOwnerProgressBeforeDeleteInternal(
2501
+ managedGcOwnerCleanupAuthority(scope, lock),
2502
+ target,
2503
+ tombstoneTargets,
2504
+ artifactsRemoved,
2505
+ );
2506
+ }
2298
2507
 
2299
- function cleanupArtifactsRemovedEvidence(
2508
+ async function taskArtifactOwnerTranscriptResult(
2300
2509
  scope: ManagedScope,
2301
- tombstone: string,
2302
2510
  target: RetiredTarget,
2303
- attempt: number,
2304
- ): CleanupArtifactsRemovedEvidence | undefined {
2305
- try {
2306
- const value: unknown = JSON.parse(
2307
- captureManagedFileNoFollow(cleanupReceiptPath(tombstone, target, "artifacts_removed", attempt)).bytes.toString(
2511
+ lock: ManagedStorageLock,
2512
+ ): Promise<"none" | "retired" | "payload_retired"> {
2513
+ return taskArtifactOwnerTranscriptResultInternal(managedGcOwnerCleanupAuthority(scope, lock), target);
2514
+ }
2515
+ function managedGcScopeStore(scope: ManagedScope, trusted: ManagedGcTrustedScope): ManagedSessionDescendantStore {
2516
+ return new ManagedSessionDescendantStore(
2517
+ trusted.root,
2518
+ scope.directoryPath,
2519
+ trusted.retainedAuthority
2520
+ ? { authority: trusted.retainedAuthority, authorityBaseDir: scope.directoryPath }
2521
+ : undefined,
2522
+ trusted.policy,
2523
+ scope.agentDir,
2524
+ {
2525
+ canonicalPath: scope.directoryPath,
2526
+ dev: BigInt.asUintN(64, trusted.identity.dev),
2527
+ ino: BigInt.asUintN(64, trusted.identity.ino),
2528
+ },
2529
+ );
2530
+ }
2531
+ function managedGcScopeReader(scope: ManagedScope, trusted: ManagedGcTrustedScope): ManagedSessionDescendantStore {
2532
+ return new ManagedSessionDescendantStore(
2533
+ trusted.root,
2534
+ scope.directoryPath,
2535
+ undefined,
2536
+ trusted.policy,
2537
+ scope.agentDir,
2538
+ {
2539
+ canonicalPath: scope.directoryPath,
2540
+ dev: BigInt.asUintN(64, trusted.identity.dev),
2541
+ ino: BigInt.asUintN(64, trusted.identity.ino),
2542
+ },
2543
+ "read-only",
2544
+ );
2545
+ }
2546
+
2547
+ function managedGcWithClosedReader<T>(store: ManagedSessionDescendantStore, operation: () => T): T {
2548
+ let result: T | undefined;
2549
+ let operationError: unknown;
2550
+ let hasOperationError = false;
2551
+ try {
2552
+ result = operation();
2553
+ } catch (error) {
2554
+ operationError = error;
2555
+ hasOperationError = true;
2556
+ }
2557
+ let closeError: unknown;
2558
+ let hasCloseError = false;
2559
+ try {
2560
+ store.close();
2561
+ } catch (error) {
2562
+ closeError = error;
2563
+ hasCloseError = true;
2564
+ }
2565
+ if (hasOperationError) throw operationError;
2566
+ if (hasCloseError) throw closeError;
2567
+ return result as T;
2568
+ }
2569
+
2570
+ function managedGcRetirementTranscriptKey(transcriptPath: string): string {
2571
+ return createHash("sha256").update(path.resolve(transcriptPath), "utf8").digest("hex");
2572
+ }
2573
+ function managedGcRetirementReceiptRelativePath(
2574
+ transcriptPath: string,
2575
+ state: ManagedGcSessionRetirementState,
2576
+ attempt?: number,
2577
+ ): string {
2578
+ if (state === "owner_pending" && (!Number.isSafeInteger(attempt) || attempt === undefined || attempt < 1))
2579
+ throw new Error("task_artifact_owner_continuation_attempt_invalid");
2580
+ const suffix = state === "owner_pending" ? `${state}-${String(attempt).padStart(8, "0")}` : state;
2581
+ return `${MANAGED_GC_RETIREMENT_RECEIPTS}/${MANAGED_GC_RETIREMENT_PREFIX}${managedGcRetirementTranscriptKey(transcriptPath)}-${suffix}.json`;
2582
+ }
2583
+
2584
+ const MANAGED_GC_RECEIPT_MAX_BYTES = MANAGED_SESSION_READ_RANGE_MAX_BYTES;
2585
+
2586
+ interface ManagedGcDirectoryEvidence {
2587
+ readonly dev: string;
2588
+ readonly ino: string;
2589
+ readonly mtimeNs: string;
2590
+ readonly ctimeNs: string;
2591
+ readonly mode: string;
2592
+ }
2593
+
2594
+ interface ManagedGcDirectoryInventory {
2595
+ readonly evidence: ManagedGcDirectoryEvidence;
2596
+ readonly entryCount: number;
2597
+ readonly physicalEntryCount: number;
2598
+ readonly entries: readonly fs.Dirent[];
2599
+ }
2600
+
2601
+ const MANAGED_GC_INVENTORY_MAX_WORK_UNITS = MANAGED_ARTIFACT_MAX_FILES * 2;
2602
+
2603
+ type ManagedGcInventoryOperation =
2604
+ | { readonly kind: "inspection"; workUnits: number; observedEntries: number }
2605
+ | { readonly kind: "leased-journal"; readonly lock: ManagedStorageLock };
2606
+
2607
+ function newManagedGcInspectionOperation(): ManagedGcInventoryOperation {
2608
+ return { kind: "inspection", workUnits: 0, observedEntries: 0 };
2609
+ }
2610
+
2611
+ function managedGcReserveInventoryWork(operation: ManagedGcInventoryOperation, amount = 1): void {
2612
+ if (!Number.isSafeInteger(amount) || amount < 0) managedGcJournalCapacity();
2613
+ if (operation.kind === "leased-journal") {
2614
+ operation.lock.assertOwned();
2615
+ return;
2616
+ }
2617
+ if (!Number.isSafeInteger(operation.workUnits) || operation.workUnits > MANAGED_GC_INVENTORY_MAX_WORK_UNITS - amount)
2618
+ managedGcJournalCapacity();
2619
+ operation.workUnits += amount;
2620
+ }
2621
+
2622
+ function managedGcComponentInputAllowance(pathname: string): number {
2623
+ if (typeof pathname !== "string") managedGcJournalCapacity();
2624
+ const allowance = pathname.length + 1;
2625
+ if (!Number.isSafeInteger(allowance)) managedGcJournalCapacity();
2626
+ return allowance;
2627
+ }
2628
+
2629
+ function managedGcReserveScopeComponentInputs(operation: ManagedGcInventoryOperation, scope: ManagedScope): void {
2630
+ managedGcReserveInventoryWork(operation, managedGcComponentInputAllowance(scope.canonicalCwd));
2631
+ managedGcReserveInventoryWork(operation, managedGcComponentInputAllowance(scope.agentDir));
2632
+ managedGcReserveInventoryWork(operation, managedGcComponentInputAllowance(scope.sessionsRoot));
2633
+ managedGcReserveInventoryWork(operation, managedGcComponentInputAllowance(scope.directoryPath));
2634
+ }
2635
+
2636
+ function managedGcAdmitInventoryEntry(operation: ManagedGcInventoryOperation): void {
2637
+ if (operation.kind === "inspection" && operation.observedEntries >= MANAGED_ARTIFACT_MAX_FILES)
2638
+ managedGcJournalCapacity();
2639
+ managedGcReserveInventoryWork(operation);
2640
+ if (operation.kind === "inspection") operation.observedEntries++;
2641
+ }
2642
+
2643
+ interface ManagedGcReceiptHistory {
2644
+ readonly entryCount: number;
2645
+ readonly encodedBytes: number;
2646
+ readonly directoryEntryCount: number;
2647
+ readonly directoryEvidence: ManagedGcDirectoryEvidence;
2648
+ readonly directoryInventory: ManagedGcDirectoryInventory;
2649
+ }
2650
+
2651
+ interface ManagedGcDiscoveryScopeFence {
2652
+ readonly scope: ManagedScope;
2653
+ readonly scopeIdentity: { readonly dev: string; readonly ino: string };
2654
+ readonly binding: ManagedFileSnapshot;
2655
+ readonly internalDirectoryIdentity: { readonly dev: string; readonly ino: string };
2656
+ readonly receiptInventory: ManagedGcDirectoryInventory | undefined;
2657
+ readonly openReader: () => ManagedSessionDescendantStore;
2658
+ }
2659
+
2660
+ interface MutableManagedGcReceiptHistory {
2661
+ encodedBytes: number;
2662
+ readonly admittedFiles: Set<string>;
2663
+ }
2664
+
2665
+ function managedGcJournalCapacity(): never {
2666
+ throw new Error("managed_gc_journal_capacity_exceeded");
2667
+ }
2668
+
2669
+ function managedGcDirectoryEvidence(stat: fs.BigIntStats): ManagedGcDirectoryEvidence {
2670
+ return {
2671
+ dev: stat.dev.toString(),
2672
+ ino: stat.ino.toString(),
2673
+ mtimeNs: stat.mtimeNs.toString(),
2674
+ ctimeNs: stat.ctimeNs.toString(),
2675
+ mode: stat.mode.toString(),
2676
+ };
2677
+ }
2678
+
2679
+ function managedGcSameDirectoryEvidence(left: ManagedGcDirectoryEvidence, right: ManagedGcDirectoryEvidence): boolean {
2680
+ return (
2681
+ left.dev === right.dev &&
2682
+ left.ino === right.ino &&
2683
+ left.mtimeNs === right.mtimeNs &&
2684
+ left.ctimeNs === right.ctimeNs &&
2685
+ left.mode === right.mode
2686
+ );
2687
+ }
2688
+
2689
+ /** Count the selected family before retaining a bounded inventory, preserving unrelated role entries. */
2690
+ function managedGcStreamDirectoryInventory(
2691
+ pathname: string,
2692
+ countEntry: (entry: fs.Dirent) => boolean,
2693
+ retain: (entry: fs.Dirent) => boolean,
2694
+ operation: ManagedGcInventoryOperation,
2695
+ ): ManagedGcDirectoryInventory {
2696
+ managedGcReserveInventoryWork(operation);
2697
+ const beforeStat = fs.lstatSync(pathname, { bigint: true });
2698
+ if (!beforeStat.isDirectory() || beforeStat.isSymbolicLink()) throw new Error("managed_gc_scope_authority_mismatch");
2699
+ const before = managedGcDirectoryEvidence(beforeStat);
2700
+ const scan = (collect: boolean): { count: number; physicalEntryCount: number; entries: fs.Dirent[] } => {
2701
+ managedGcReserveInventoryWork(operation);
2702
+ const entries: fs.Dirent[] = [];
2703
+ let count = 0;
2704
+ let physicalEntryCount = 0;
2705
+ let retained = 0;
2706
+ // Reserve open/close plus conservative processing for the dot entries Node suppresses.
2707
+ managedGcReserveInventoryWork(operation, 4);
2708
+ let directory: fs.Dir | undefined;
2709
+ let primaryError: unknown;
2710
+ let hasPrimaryError = false;
2711
+ try {
2712
+ directory = fs.opendirSync(pathname, { bufferSize: 1 });
2713
+ for (;;) {
2714
+ managedGcReserveInventoryWork(operation);
2715
+ const entry = directory.readSync();
2716
+ if (!entry) break;
2717
+ managedGcAdmitInventoryEntry(operation);
2718
+ if (!Number.isSafeInteger(physicalEntryCount + 1)) managedGcJournalCapacity();
2719
+ physicalEntryCount++;
2720
+ if (countEntry(entry) && ++count > MANAGED_ARTIFACT_MAX_FILES) managedGcJournalCapacity();
2721
+ if (!retain(entry)) continue;
2722
+ if (++retained > MANAGED_ARTIFACT_MAX_FILES) managedGcJournalCapacity();
2723
+ if (collect) {
2724
+ managedGcReserveInventoryWork(operation);
2725
+ entries.push(entry);
2726
+ }
2727
+ }
2728
+ } catch (error) {
2729
+ primaryError = error;
2730
+ hasPrimaryError = true;
2731
+ }
2732
+ let closeError: unknown;
2733
+ if (directory) {
2734
+ try {
2735
+ directory.closeSync();
2736
+ } catch (error) {
2737
+ closeError = error;
2738
+ }
2739
+ }
2740
+ if (hasPrimaryError) throw primaryError;
2741
+ if (closeError !== undefined) throw closeError;
2742
+ managedGcReserveInventoryWork(operation);
2743
+ return { count, physicalEntryCount, entries };
2744
+ };
2745
+ const counted = scan(false);
2746
+ managedGcReserveInventoryWork(operation);
2747
+ const afterCount = fs.lstatSync(pathname, { bigint: true });
2748
+ if (
2749
+ !afterCount.isDirectory() ||
2750
+ afterCount.isSymbolicLink() ||
2751
+ !managedGcSameDirectoryEvidence(before, managedGcDirectoryEvidence(afterCount))
2752
+ )
2753
+ throw new Error("managed_gc_scope_authority_mismatch");
2754
+ const collected = scan(true);
2755
+ managedGcReserveInventoryWork(operation);
2756
+ const afterCollect = fs.lstatSync(pathname, { bigint: true });
2757
+ if (
2758
+ collected.count !== counted.count ||
2759
+ collected.physicalEntryCount !== counted.physicalEntryCount ||
2760
+ !afterCollect.isDirectory() ||
2761
+ afterCollect.isSymbolicLink() ||
2762
+ !managedGcSameDirectoryEvidence(before, managedGcDirectoryEvidence(afterCollect))
2763
+ )
2764
+ throw new Error("managed_gc_scope_authority_mismatch");
2765
+ managedGcReserveInventoryWork(operation, collected.entries.length);
2766
+ collected.entries.sort((left, right) => left.name.localeCompare(right.name));
2767
+ return {
2768
+ evidence: before,
2769
+ entryCount: counted.count,
2770
+ physicalEntryCount: counted.physicalEntryCount,
2771
+ entries: collected.entries,
2772
+ };
2773
+ }
2774
+
2775
+ function managedGcReceiptDirectoryInventory(
2776
+ scope: ManagedScope,
2777
+ store: ManagedSessionDescendantStore,
2778
+ operation: ManagedGcInventoryOperation,
2779
+ ): ManagedGcDirectoryInventory {
2780
+ const relativePath = MANAGED_GC_RETIREMENT_RECEIPTS;
2781
+ managedGcReserveInventoryWork(
2782
+ operation,
2783
+ managedGcComponentInputAllowance(scope.directoryPath) + relativePath.length,
2784
+ );
2785
+ const pathname = path.join(scope.directoryPath, relativePath);
2786
+ const before = store.captureDirectoryIdentity(relativePath);
2787
+ const inventory = managedGcStreamDirectoryInventory(
2788
+ pathname,
2789
+ entry => entry.name.startsWith(MANAGED_GC_RETIREMENT_PREFIX),
2790
+ entry => entry.name.startsWith(MANAGED_GC_RETIREMENT_PREFIX),
2791
+ operation,
2792
+ );
2793
+ managedGcReserveInventoryWork(operation);
2794
+ const after = store.captureDirectoryIdentity(relativePath);
2795
+ if (
2796
+ before.dev !== after.dev ||
2797
+ before.ino !== after.ino ||
2798
+ before.dev !== inventory.evidence.dev ||
2799
+ before.ino !== inventory.evidence.ino
2800
+ )
2801
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
2802
+ managedGcReserveInventoryWork(operation);
2803
+ store.assertBound();
2804
+ return inventory;
2805
+ }
2806
+
2807
+ function managedGcReceiptCapacity(): never {
2808
+ throw new Error("managed_gc_receipt_capacity_exceeded");
2809
+ }
2810
+
2811
+ function managedGcJsonStringByteLength(value: string, add: (amount: number) => void): void {
2812
+ add(2);
2813
+ for (let index = 0; index < value.length; index++) {
2814
+ const unit = value.charCodeAt(index);
2815
+ if (unit === 0x22 || unit === 0x5c) add(2);
2816
+ else if (unit < 0x20)
2817
+ add(unit === 0x08 || unit === 0x09 || unit === 0x0a || unit === 0x0c || unit === 0x0d ? 2 : 6);
2818
+ else if (unit >= 0xd800 && unit <= 0xdbff) {
2819
+ const low = value.charCodeAt(index + 1);
2820
+ if (low >= 0xdc00 && low <= 0xdfff) {
2821
+ add(4);
2822
+ index++;
2823
+ } else add(6);
2824
+ } else if (unit >= 0xdc00 && unit <= 0xdfff) add(6);
2825
+ else if (unit <= 0x7f) add(1);
2826
+ else if (unit <= 0x7ff) add(2);
2827
+ else add(3);
2828
+ }
2829
+ }
2830
+
2831
+ /** Measure the canonical JSON encoding before JSON.stringify or Buffer allocation. */
2832
+ function managedGcJsonByteLength(value: unknown): number {
2833
+ let bytes = 0;
2834
+ const active = new Set<object>();
2835
+ const add = (amount: number): void => {
2836
+ if (!Number.isSafeInteger(amount) || amount < 0 || amount > MANAGED_GC_RECEIPT_MAX_BYTES - bytes)
2837
+ managedGcReceiptCapacity();
2838
+ bytes += amount;
2839
+ };
2840
+ const visit = (item: unknown, depth: number, inArray = false): void => {
2841
+ if (depth > 32) throw new Error("task_artifact_owner_continuation_corrupt");
2842
+ if (item === null) {
2843
+ add(4);
2844
+ return;
2845
+ }
2846
+ if (typeof item === "string") {
2847
+ managedGcJsonStringByteLength(item, add);
2848
+ return;
2849
+ }
2850
+ if (typeof item === "bigint") {
2851
+ managedGcJsonStringByteLength(item.toString(), add);
2852
+ return;
2853
+ }
2854
+ if (typeof item === "boolean") {
2855
+ add(item ? 4 : 5);
2856
+ return;
2857
+ }
2858
+ if (typeof item === "number") {
2859
+ add(Number.isFinite(item) ? (Object.is(item, -0) ? 1 : String(item).length) : 4);
2860
+ return;
2861
+ }
2862
+ if (typeof item === "undefined" || typeof item === "function" || typeof item === "symbol") {
2863
+ if (inArray) add(4);
2864
+ return;
2865
+ }
2866
+ if (typeof item !== "object") managedGcReceiptCapacity();
2867
+ if (active.has(item)) throw new Error("task_artifact_owner_continuation_corrupt");
2868
+ active.add(item);
2869
+ const array = Array.isArray(item);
2870
+ if (array) {
2871
+ const values = item as unknown[];
2872
+ if (values.length > MANAGED_GC_RECEIPT_MAX_BYTES) managedGcReceiptCapacity();
2873
+ add(2);
2874
+ let emitted = false;
2875
+ for (let index = 0; index < values.length; index++) {
2876
+ if (emitted) add(1);
2877
+ emitted = true;
2878
+ visit(values[index], depth + 1, true);
2879
+ }
2880
+ } else {
2881
+ add(2);
2882
+ let emitted = false;
2883
+ for (const key in item) {
2884
+ if (!Object.hasOwn(item, key)) continue;
2885
+ const child = (item as Record<string, unknown>)[key];
2886
+ if (child === undefined || typeof child === "function" || typeof child === "symbol") continue;
2887
+ if (emitted) add(1);
2888
+ emitted = true;
2889
+ managedGcJsonStringByteLength(key, add);
2890
+ add(1);
2891
+ visit(child, depth + 1);
2892
+ }
2893
+ }
2894
+ active.delete(item);
2895
+ };
2896
+ visit(value, 0);
2897
+ add(1);
2898
+ return bytes;
2899
+ }
2900
+
2901
+ function validateManagedGcTranscriptPath(scope: ManagedScope, transcriptPath: string): string {
2902
+ if (
2903
+ typeof transcriptPath !== "string" ||
2904
+ !path.isAbsolute(transcriptPath) ||
2905
+ path.resolve(transcriptPath) !== transcriptPath ||
2906
+ path.dirname(transcriptPath) !== scope.directoryPath ||
2907
+ path.extname(transcriptPath) !== ".jsonl" ||
2908
+ !pathIsWithin(scope.sessionsRoot, transcriptPath)
2909
+ )
2910
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
2911
+ return transcriptPath;
2912
+ }
2913
+ function managedGcTargetFromSnapshot(
2914
+ scope: ManagedScope,
2915
+ transcriptPath: string,
2916
+ snapshot: SessionStorageSnapshot,
2917
+ ): ManagedGcSessionRetirementTarget {
2918
+ if (
2919
+ !snapshot.stat.isFile ||
2920
+ snapshot.stat.nlink !== 1n ||
2921
+ snapshot.stat.size !== snapshot.bytes.byteLength ||
2922
+ !Number.isSafeInteger(snapshot.stat.size) ||
2923
+ snapshot.stat.size < 0
2924
+ )
2925
+ throw new Error("task_artifact_owner_continuation_identity_invalid");
2926
+ const header = parseFirstJsonlLine(snapshot.bytes);
2927
+ if (
2928
+ header?.type !== "session" ||
2929
+ typeof header.id !== "string" ||
2930
+ header.id.length === 0 ||
2931
+ typeof header.cwd !== "string" ||
2932
+ header.cwd.length === 0
2933
+ )
2934
+ throw new Error("task_artifact_owner_transcript_header_invalid");
2935
+ const cwdIdentity = identityFor(header.cwd);
2936
+ if (!cwdIdentity.ok || cwdIdentity.platform !== scope.platform || cwdIdentity.canonicalPath !== scope.canonicalCwd)
2937
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
2938
+ const locator = taskArtifactOwnerLocatorFromTranscriptBytes(snapshot.bytes, header.id);
2939
+ if (!locator) throw new Error("task_artifact_owner_locator_missing");
2940
+ return Object.freeze({
2941
+ transcriptPath,
2942
+ sessionId: header.id,
2943
+ cwd: header.cwd,
2944
+ transcriptIdentity: {
2945
+ dev: snapshot.stat.dev,
2946
+ ino: snapshot.stat.ino,
2947
+ nlink: snapshot.stat.nlink,
2948
+ size: snapshot.stat.size,
2949
+ mtimeNs: snapshot.stat.mtimeNs,
2950
+ sha256: createHash("sha256").update(snapshot.bytes).digest("hex"),
2951
+ },
2952
+ taskArtifactOwnerLocator: locator,
2953
+ });
2954
+ }
2955
+ /** Bind the first transcript header through FileSessionStorage's descriptor-bound, no-follow snapshot. */
2956
+ export function bindManagedGcSessionRetirementTarget(
2957
+ scope: ManagedScope,
2958
+ transcriptPathValue: string,
2959
+ ): ManagedGcSessionRetirementTarget {
2960
+ const trusted = managedGcTrustedScope(scope);
2961
+ const transcriptPath = validateManagedGcTranscriptPath(scope, transcriptPathValue);
2962
+ const store = managedGcScopeReader(scope, trusted);
2963
+ try {
2964
+ store.verifyRootSecurity();
2965
+ store.assertBound();
2966
+ const snapshot = new FileSessionStorage().readSnapshotSync(transcriptPath);
2967
+ store.assertBound();
2968
+ return managedGcTargetFromSnapshot(scope, transcriptPath, snapshot);
2969
+ } finally {
2970
+ store.close();
2971
+ }
2972
+ }
2973
+ function sameManagedGcTarget(left: ManagedGcSessionRetirementTarget, right: ManagedGcSessionRetirementTarget): boolean {
2974
+ return (
2975
+ left.transcriptPath === right.transcriptPath &&
2976
+ left.sessionId === right.sessionId &&
2977
+ left.cwd === right.cwd &&
2978
+ sameIdentity(left.transcriptIdentity, right.transcriptIdentity) &&
2979
+ util.isDeepStrictEqual(left.taskArtifactOwnerLocator, right.taskArtifactOwnerLocator)
2980
+ );
2981
+ }
2982
+ function assertManagedGcTranscriptUnchangedOrAbsent(
2983
+ scope: ManagedScope,
2984
+ target: ManagedGcSessionRetirementTarget,
2985
+ allowAbsent: boolean,
2986
+ ): void {
2987
+ try {
2988
+ const current = bindManagedGcSessionRetirementTarget(scope, target.transcriptPath);
2989
+ if (!sameManagedGcTarget(current, target))
2990
+ throw new Error("task_artifact_owner_continuation_transcript_replaced");
2991
+ } catch (error) {
2992
+ if (allowAbsent && hasFsCode(error, "ENOENT")) return;
2993
+ throw error;
2994
+ }
2995
+ }
2996
+ function managedGcReceiptRecord(
2997
+ scope: ManagedScope,
2998
+ receipt: ManagedGcSessionRetirementReceipt,
2999
+ attempt?: number,
3000
+ ): Record<string, unknown> {
3001
+ const state = receipt.state;
3002
+ const common = {
3003
+ schemaVersion: 1,
3004
+ state,
3005
+ scope: scopeDigest(scope.platform, scope.canonicalCwd),
3006
+ transcriptPath: receipt.transcriptPath,
3007
+ sessionId: receipt.sessionId,
3008
+ cwd: receipt.cwd,
3009
+ transcriptIdentity: managedGcRetirementIdentityRecord(receipt.transcriptIdentity),
3010
+ taskArtifactOwnerDeletionEvidence: receipt.taskArtifactOwnerDeletionEvidence,
3011
+ };
3012
+ const hasOutcomeFields =
3013
+ receipt.ownerRetirementAttempt !== undefined ||
3014
+ receipt.taskArtifactOwnerRetirementOutcome !== undefined ||
3015
+ receipt.taskArtifactOwnerRetirementContinuation !== undefined ||
3016
+ receipt.taskArtifactOwnerPayloadRetired !== undefined ||
3017
+ receipt.taskArtifactOwnerNamespaceRetained !== undefined ||
3018
+ receipt.taskArtifactOwnerRetired !== undefined;
3019
+ if (state === "prepared" || state === "artifacts_removed") {
3020
+ if (
3021
+ hasOutcomeFields ||
3022
+ (state === "prepared" && receipt.artifactsRemoved !== undefined) ||
3023
+ (state === "artifacts_removed" && receipt.artifactsRemoved !== undefined && receipt.artifactsRemoved !== true)
3024
+ )
3025
+ throw new Error("task_artifact_owner_continuation_state_mismatch");
3026
+ return state === "prepared" ? common : { ...common, artifactsRemoved: true };
3027
+ }
3028
+ if (state === "owner_pending") {
3029
+ const outcome = receipt.taskArtifactOwnerRetirementOutcome;
3030
+ if (
3031
+ !outcome ||
3032
+ outcome.kind === "completed" ||
3033
+ (receipt.artifactsRemoved !== undefined && receipt.artifactsRemoved !== true) ||
3034
+ receipt.taskArtifactOwnerRetired !== undefined ||
3035
+ (attempt !== undefined &&
3036
+ receipt.ownerRetirementAttempt !== undefined &&
3037
+ receipt.ownerRetirementAttempt !== attempt) ||
3038
+ (receipt.taskArtifactOwnerRetirementContinuation !== undefined &&
3039
+ !util.isDeepStrictEqual(receipt.taskArtifactOwnerRetirementContinuation, outcome.continuation)) ||
3040
+ (receipt.taskArtifactOwnerPayloadRetired !== undefined &&
3041
+ receipt.taskArtifactOwnerPayloadRetired !== (outcome.kind === "payload_retired")) ||
3042
+ (receipt.taskArtifactOwnerNamespaceRetained !== undefined &&
3043
+ receipt.taskArtifactOwnerNamespaceRetained !== (outcome.kind === "payload_retired"))
3044
+ )
3045
+ throw new Error("task_artifact_owner_continuation_outcome_mismatch");
3046
+ if (!Number.isSafeInteger(attempt) || attempt === undefined || attempt < 1)
3047
+ throw new Error("task_artifact_owner_continuation_attempt_invalid");
3048
+ return {
3049
+ ...common,
3050
+ ownerRetirementAttempt: attempt,
3051
+ artifactsRemoved: true,
3052
+ taskArtifactOwnerRetirementOutcome: outcome,
3053
+ taskArtifactOwnerRetirementContinuation: outcome.continuation,
3054
+ ...(outcome.kind === "payload_retired"
3055
+ ? { taskArtifactOwnerPayloadRetired: true, taskArtifactOwnerNamespaceRetained: true }
3056
+ : {}),
3057
+ };
3058
+ }
3059
+ if (
3060
+ (receipt.artifactsRemoved !== undefined && receipt.artifactsRemoved !== true) ||
3061
+ hasOutcomeFieldsExceptOutcome(receipt) ||
3062
+ (receipt.taskArtifactOwnerRetired !== undefined && receipt.taskArtifactOwnerRetired !== true) ||
3063
+ receipt.taskArtifactOwnerRetirementOutcome?.kind !== "completed"
3064
+ )
3065
+ throw new Error("task_artifact_owner_continuation_outcome_mismatch");
3066
+ return {
3067
+ ...common,
3068
+ artifactsRemoved: true,
3069
+ taskArtifactOwnerRetired: true,
3070
+ taskArtifactOwnerRetirementOutcome: receipt.taskArtifactOwnerRetirementOutcome,
3071
+ };
3072
+ }
3073
+
3074
+ function hasOutcomeFieldsExceptOutcome(receipt: ManagedGcSessionRetirementReceipt): boolean {
3075
+ return (
3076
+ receipt.ownerRetirementAttempt !== undefined ||
3077
+ receipt.taskArtifactOwnerRetirementContinuation !== undefined ||
3078
+ receipt.taskArtifactOwnerPayloadRetired !== undefined ||
3079
+ receipt.taskArtifactOwnerNamespaceRetained !== undefined
3080
+ );
3081
+ }
3082
+ function readManagedGcReceiptSnapshot(
3083
+ store: ManagedSessionDescendantStore,
3084
+ relativePath: string,
3085
+ history?: MutableManagedGcReceiptHistory,
3086
+ ): ManagedFileSnapshot | null {
3087
+ try {
3088
+ return store.readExpectedBounded(relativePath, MANAGED_GC_RECEIPT_MAX_BYTES, (size, descriptor) => {
3089
+ if (descriptor.nlink !== 1n || descriptor.size !== size) throw new Error("source_changed");
3090
+ if (history) managedGcAdmitHistoryBytes(history, descriptor.size, relativePath);
3091
+ });
3092
+ } catch (error) {
3093
+ if (error instanceof Error && error.message === "artifact_capacity_exceeded") managedGcJournalCapacity();
3094
+ throw error;
3095
+ }
3096
+ }
3097
+
3098
+ function managedGcAdmitHistoryBytes(history: MutableManagedGcReceiptHistory, size: number, relativePath: string): void {
3099
+ if (history.admittedFiles.has(relativePath)) return;
3100
+ if (history.admittedFiles.size >= MANAGED_ARTIFACT_MAX_FILES) managedGcJournalCapacity();
3101
+ if (size > MANAGED_ARTIFACT_MAX_TOTAL_BYTES - history.encodedBytes) managedGcJournalCapacity();
3102
+ history.encodedBytes += size;
3103
+ history.admittedFiles.add(relativePath);
3104
+ }
3105
+
3106
+ function parseManagedGcReceiptFile(
3107
+ store: ManagedSessionDescendantStore,
3108
+ relativePath: string,
3109
+ scope: ManagedScope,
3110
+ ownerContext: TaskArtifactOwnerStorageContext,
3111
+ target: ManagedGcSessionRetirementTarget,
3112
+ history?: MutableManagedGcReceiptHistory,
3113
+ ): ManagedGcSessionRetirementReceipt | undefined {
3114
+ const snapshot = readManagedGcReceiptSnapshot(store, relativePath, history);
3115
+ if (!snapshot) return undefined;
3116
+ let value: unknown;
3117
+ try {
3118
+ value = JSON.parse(snapshot.bytes.toString("utf8"));
3119
+ } catch {
3120
+ throw new Error("task_artifact_owner_continuation_corrupt");
3121
+ }
3122
+ return parseManagedGcRetirementReceipt(value, scopeDigest(scope.platform, scope.canonicalCwd), ownerContext, target);
3123
+ }
3124
+ function targetFromPreparedRecord(value: unknown, transcriptPath: string): ManagedGcSessionRetirementTarget {
3125
+ if (!value || typeof value !== "object" || Array.isArray(value))
3126
+ throw new Error("task_artifact_owner_continuation_corrupt");
3127
+ const record = value as Record<string, unknown>;
3128
+ if (
3129
+ record.state !== "prepared" ||
3130
+ record.transcriptPath !== transcriptPath ||
3131
+ typeof record.sessionId !== "string" ||
3132
+ typeof record.cwd !== "string"
3133
+ )
3134
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
3135
+ const evidence = parseTaskArtifactOwnerDeletionEvidence(record.taskArtifactOwnerDeletionEvidence);
3136
+ return {
3137
+ transcriptPath,
3138
+ sessionId: record.sessionId,
3139
+ cwd: record.cwd,
3140
+ transcriptIdentity: managedGcRetirementIdentityValue(record.transcriptIdentity),
3141
+ taskArtifactOwnerLocator: evidence.locator,
3142
+ };
3143
+ }
3144
+ function deepSame(left: unknown, right: unknown): boolean {
3145
+ return util.isDeepStrictEqual(left, right);
3146
+ }
3147
+ function readManagedGcPreparedAuthority(
3148
+ scope: ManagedScope,
3149
+ transcriptPath: string,
3150
+ ownerContext: TaskArtifactOwnerStorageContext,
3151
+ store: ManagedSessionDescendantStore,
3152
+ history?: MutableManagedGcReceiptHistory,
3153
+ bootstrap?: {
3154
+ readonly snapshot: ManagedFileSnapshot;
3155
+ readonly value: unknown;
3156
+ readonly history: MutableManagedGcReceiptHistory;
3157
+ },
3158
+ ): ManagedGcSessionRetirementReceipt | undefined {
3159
+ const snapshot =
3160
+ bootstrap?.snapshot ??
3161
+ readManagedGcReceiptSnapshot(store, managedGcRetirementReceiptRelativePath(transcriptPath, "prepared"), history);
3162
+ if (!snapshot) return undefined;
3163
+ let value: unknown;
3164
+ if (bootstrap) {
3165
+ value = bootstrap.value;
3166
+ if (history && history !== bootstrap.history)
3167
+ managedGcAdmitHistoryBytes(
3168
+ history,
3169
+ snapshot.identity.size,
3170
+ managedGcRetirementReceiptRelativePath(transcriptPath, "prepared"),
3171
+ );
3172
+ } else {
3173
+ try {
3174
+ value = JSON.parse(snapshot.bytes.toString("utf8"));
3175
+ } catch {
3176
+ throw new Error("task_artifact_owner_continuation_corrupt");
3177
+ }
3178
+ }
3179
+ const target = targetFromPreparedRecord(value, transcriptPath);
3180
+ const prepared = parseManagedGcRetirementReceipt(
3181
+ value,
3182
+ scopeDigest(scope.platform, scope.canonicalCwd),
3183
+ ownerContext,
3184
+ target,
3185
+ );
3186
+ if (prepared.state !== "prepared") throw new Error("task_artifact_owner_continuation_state_missing");
3187
+ return prepared;
3188
+ }
3189
+ function assertSameManagedGcImmutableAuthority(
3190
+ prepared: ManagedGcSessionRetirementReceipt,
3191
+ next: ManagedGcSessionRetirementReceipt,
3192
+ ): void {
3193
+ if (
3194
+ !sameManagedGcTarget(prepared, next) ||
3195
+ !deepSame(prepared.taskArtifactOwnerDeletionEvidence, next.taskArtifactOwnerDeletionEvidence)
3196
+ )
3197
+ throw new Error("task_artifact_owner_continuation_evidence_mismatch");
3198
+ }
3199
+ function readManagedGcSessionRetirementReceiptUnlocked(
3200
+ scope: ManagedScope,
3201
+ transcriptPathValue: string,
3202
+ trusted: ManagedGcTrustedScope,
3203
+ store: ManagedSessionDescendantStore,
3204
+ operation: ManagedGcInventoryOperation,
3205
+ inventory?: ManagedGcDirectoryInventory,
3206
+ targetNames?: readonly string[],
3207
+ preparedBootstrap?: {
3208
+ readonly snapshot: ManagedFileSnapshot;
3209
+ readonly value: unknown;
3210
+ readonly history: MutableManagedGcReceiptHistory;
3211
+ },
3212
+ ): {
3213
+ readonly receipt: ManagedGcSessionRetirementReceipt | undefined;
3214
+ readonly prepared: ManagedGcSessionRetirementReceipt | undefined;
3215
+ readonly history: ManagedGcReceiptHistory;
3216
+ } {
3217
+ managedGcReserveInventoryWork(operation, managedGcComponentInputAllowance(transcriptPathValue));
3218
+ const transcriptPath = validateManagedGcTranscriptPath(scope, transcriptPathValue);
3219
+ managedGcReserveInventoryWork(operation);
3220
+ store.assertBound();
3221
+ const receiptInventory = inventory ?? managedGcReceiptDirectoryInventory(scope, store, operation);
3222
+ const names = targetNames ?? managedGcReceiptNames(scope, transcriptPath, store, operation, receiptInventory);
3223
+ if (names.length > MANAGED_ARTIFACT_MAX_FILES) managedGcJournalCapacity();
3224
+ const prefix = `${MANAGED_GC_RETIREMENT_PREFIX}${managedGcRetirementTranscriptKey(transcriptPath)}-`;
3225
+ const pendingPrefix = `${prefix}owner_pending-`;
3226
+ const historyBytes = preparedBootstrap?.history ?? { encodedBytes: 0, admittedFiles: new Set<string>() };
3227
+ const history = (): ManagedGcReceiptHistory => ({
3228
+ entryCount: names.length,
3229
+ encodedBytes: historyBytes.encodedBytes,
3230
+ directoryEntryCount: receiptInventory.entryCount,
3231
+ directoryEvidence: receiptInventory.evidence,
3232
+ directoryInventory: receiptInventory,
3233
+ });
3234
+ const preparedName = `${prefix}prepared.json`;
3235
+ if (!names.includes(preparedName)) {
3236
+ if (names.length > 0) throw new Error("task_artifact_owner_continuation_state_missing");
3237
+ if (!inventory) {
3238
+ const after = managedGcReceiptDirectoryInventory(scope, store, operation);
3239
+ if (!managedGcSameReceiptDirectoryInventory(receiptInventory, after))
3240
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
3241
+ }
3242
+ return { receipt: undefined, prepared: undefined, history: history() };
3243
+ }
3244
+ const prepared = readManagedGcPreparedAuthority(
3245
+ scope,
3246
+ transcriptPath,
3247
+ trusted.ownerContext,
3248
+ store,
3249
+ historyBytes,
3250
+ preparedBootstrap,
3251
+ );
3252
+ if (!prepared) {
3253
+ throw new Error("task_artifact_owner_continuation_state_missing");
3254
+ }
3255
+ const target = prepared;
3256
+ assertManagedGcTranscriptUnchangedOrAbsent(scope, target, true);
3257
+ managedGcReserveInventoryWork(operation, names.length);
3258
+ const nameSet = new Set(names);
3259
+ if (!nameSet.has(preparedName)) throw new Error("task_artifact_owner_continuation_state_missing");
3260
+ const readState = (state: ManagedGcSessionRetirementState, attempt?: number) =>
3261
+ parseManagedGcReceiptFile(
3262
+ store,
3263
+ managedGcRetirementReceiptRelativePath(transcriptPath, state, attempt),
3264
+ scope,
3265
+ trusted.ownerContext,
3266
+ target,
3267
+ historyBytes,
3268
+ );
3269
+ const artifactsName = path.posix.basename(
3270
+ managedGcRetirementReceiptRelativePath(transcriptPath, "artifacts_removed"),
3271
+ );
3272
+ const retiredName = path.posix.basename(managedGcRetirementReceiptRelativePath(transcriptPath, "owner_retired"));
3273
+ const artifactsRemoved = nameSet.has(artifactsName) ? readState("artifacts_removed") : undefined;
3274
+ const ownerRetired = nameSet.has(retiredName) ? readState("owner_retired") : undefined;
3275
+ if (
3276
+ (nameSet.has(artifactsName) && artifactsRemoved?.state !== "artifacts_removed") ||
3277
+ (nameSet.has(retiredName) && ownerRetired?.state !== "owner_retired")
3278
+ )
3279
+ throw new Error("task_artifact_owner_continuation_state_missing");
3280
+ managedGcReserveInventoryWork(operation, names.length);
3281
+ const attempts = names
3282
+ .filter(name => name.startsWith(pendingPrefix))
3283
+ .map(name => {
3284
+ const suffix = name.slice(pendingPrefix.length);
3285
+ if (!/^[0-9]{8,}\.json$/u.test(suffix)) throw new Error("task_artifact_owner_continuation_corrupt");
3286
+ const attempt = Number(suffix.slice(0, -5));
3287
+ if (!Number.isSafeInteger(attempt) || attempt < 1 || `${String(attempt).padStart(8, "0")}.json` !== suffix)
3288
+ throw new Error("task_artifact_owner_continuation_corrupt");
3289
+ return attempt;
3290
+ })
3291
+ .sort((left, right) => left - right);
3292
+ let previousPending: ManagedGcSessionRetirementReceipt | undefined;
3293
+ let latestPending: ManagedGcSessionRetirementReceipt | undefined;
3294
+ for (let index = 0; index < attempts.length; index++) {
3295
+ const attempt = attempts[index]!;
3296
+ const pending = readState("owner_pending", attempt);
3297
+ if (
3298
+ attempt !== index + 1 ||
3299
+ !pending ||
3300
+ pending.state !== "owner_pending" ||
3301
+ pending.ownerRetirementAttempt !== attempt
3302
+ )
3303
+ throw new Error("task_artifact_owner_continuation_state_missing");
3304
+ assertSameManagedGcImmutableAuthority(prepared, pending);
3305
+ if (previousPending) {
3306
+ const previous = previousPending.taskArtifactOwnerRetirementOutcome;
3307
+ const next = pending.taskArtifactOwnerRetirementOutcome;
3308
+ if (
3309
+ !previous ||
3310
+ previous.kind === "completed" ||
3311
+ !next ||
3312
+ next.kind === "completed" ||
3313
+ !continuationExtendsPrevious(previous.continuation, next.continuation)
3314
+ )
3315
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
3316
+ }
3317
+ previousPending = pending;
3318
+ latestPending = pending;
3319
+ }
3320
+ if ((attempts.length || ownerRetired) && !artifactsRemoved)
3321
+ throw new Error("task_artifact_owner_continuation_state_missing");
3322
+ if (artifactsRemoved) assertSameManagedGcImmutableAuthority(prepared, artifactsRemoved);
3323
+ if (ownerRetired) assertSameManagedGcImmutableAuthority(prepared, ownerRetired);
3324
+ if (ownerRetired) {
3325
+ const outcome = ownerRetired.taskArtifactOwnerRetirementOutcome;
3326
+ if (outcome?.kind !== "completed") throw new Error("task_artifact_owner_continuation_outcome_mismatch");
3327
+ verifyTaskArtifactOwnerPhysicalRetirement(
3328
+ trusted.ownerContext,
3329
+ ownerRetired.taskArtifactOwnerDeletionEvidence,
3330
+ outcome,
3331
+ );
3332
+ }
3333
+ if (!inventory) {
3334
+ const after = managedGcReceiptDirectoryInventory(scope, store, operation);
3335
+ if (!managedGcSameReceiptDirectoryInventory(receiptInventory, after))
3336
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
3337
+ }
3338
+ return { receipt: ownerRetired ?? latestPending ?? artifactsRemoved ?? prepared, prepared, history: history() };
3339
+ }
3340
+
3341
+ function managedGcReceiptNames(
3342
+ scope: ManagedScope,
3343
+ transcriptPath: string,
3344
+ store: ManagedSessionDescendantStore,
3345
+ operation: ManagedGcInventoryOperation,
3346
+ inventory?: ManagedGcDirectoryInventory,
3347
+ ): string[] {
3348
+ const prefix = `${MANAGED_GC_RETIREMENT_PREFIX}${managedGcRetirementTranscriptKey(transcriptPath)}-`;
3349
+ const directoryInventory = inventory ?? managedGcReceiptDirectoryInventory(scope, store, operation);
3350
+ managedGcReserveInventoryWork(operation, directoryInventory.entries.length);
3351
+ const matching = directoryInventory.entries.filter(entry => entry.name.startsWith(prefix));
3352
+ if (matching.some(entry => !entry.isFile() || entry.isSymbolicLink()))
3353
+ throw new Error("task_artifact_owner_continuation_corrupt");
3354
+ managedGcReserveInventoryWork(operation, matching.length);
3355
+ const names = matching.map(entry => entry.name);
3356
+ const pendingPrefix = `${prefix}owner_pending-`;
3357
+ for (const name of names) {
3358
+ if (
3359
+ name !== `${prefix}prepared.json` &&
3360
+ name !== `${prefix}artifacts_removed.json` &&
3361
+ name !== `${prefix}owner_retired.json` &&
3362
+ !name.startsWith(pendingPrefix)
3363
+ )
3364
+ throw new Error("task_artifact_owner_continuation_corrupt");
3365
+ }
3366
+ return names;
3367
+ }
3368
+
3369
+ function managedGcSameReceiptDirectoryInventory(
3370
+ left: ManagedGcDirectoryInventory,
3371
+ right: ManagedGcDirectoryInventory,
3372
+ ): boolean {
3373
+ return (
3374
+ left.entryCount === right.entryCount &&
3375
+ left.physicalEntryCount === right.physicalEntryCount &&
3376
+ managedGcSameDirectoryEvidence(left.evidence, right.evidence) &&
3377
+ left.entries.length === right.entries.length &&
3378
+ left.entries.every(
3379
+ (entry, index) =>
3380
+ entry.name === right.entries[index]?.name &&
3381
+ entry.isDirectory() === right.entries[index]?.isDirectory() &&
3382
+ entry.isFile() === right.entries[index]?.isFile() &&
3383
+ entry.isSymbolicLink() === right.entries[index]?.isSymbolicLink() &&
3384
+ entry.isBlockDevice() === right.entries[index]?.isBlockDevice() &&
3385
+ entry.isCharacterDevice() === right.entries[index]?.isCharacterDevice() &&
3386
+ entry.isFIFO() === right.entries[index]?.isFIFO() &&
3387
+ entry.isSocket() === right.entries[index]?.isSocket(),
3388
+ )
3389
+ );
3390
+ }
3391
+
3392
+ function managedGcOptionalReceiptDirectoryInventory(
3393
+ scope: ManagedScope,
3394
+ store: ManagedSessionDescendantStore,
3395
+ operation: ManagedGcInventoryOperation,
3396
+ ): ManagedGcDirectoryInventory | undefined {
3397
+ try {
3398
+ managedGcReserveInventoryWork(operation);
3399
+ store.captureDirectoryIdentity(MANAGED_GC_RETIREMENT_RECEIPTS);
3400
+ } catch (error) {
3401
+ if (hasFsCode(error, "ENOENT")) return undefined;
3402
+ throw error;
3403
+ }
3404
+ return managedGcReceiptDirectoryInventory(scope, store, operation);
3405
+ }
3406
+
3407
+ function managedGcDiscoveryScopeFence(
3408
+ scope: ManagedScope,
3409
+ store: ManagedSessionDescendantStore,
3410
+ scopeIdentity: { readonly dev: string; readonly ino: string },
3411
+ binding: ManagedFileSnapshot,
3412
+ openReader: () => ManagedSessionDescendantStore,
3413
+ operation: ManagedGcInventoryOperation,
3414
+ ): ManagedGcDiscoveryScopeFence {
3415
+ managedGcReserveInventoryWork(operation);
3416
+ const internalDirectoryIdentity = store.captureDirectoryIdentity(MANAGED_INTERNAL_DIRECTORY);
3417
+ managedGcReserveInventoryWork(operation);
3418
+ return {
3419
+ scope,
3420
+ scopeIdentity,
3421
+ binding,
3422
+ internalDirectoryIdentity,
3423
+ receiptInventory: managedGcOptionalReceiptDirectoryInventory(scope, store, operation),
3424
+ openReader,
3425
+ };
3426
+ }
3427
+
3428
+ function managedGcAssertDiscoveryScopeFence(
3429
+ fence: ManagedGcDiscoveryScopeFence,
3430
+ operation: ManagedGcInventoryOperation,
3431
+ ): void {
3432
+ managedGcReserveInventoryWork(operation);
3433
+ const store = fence.openReader();
3434
+ let primaryError: unknown;
3435
+ let hasPrimaryError = false;
3436
+ try {
3437
+ managedGcReserveInventoryWork(operation);
3438
+ store.verifyRootSecurity();
3439
+ managedGcReserveInventoryWork(operation);
3440
+ const currentScope = fs.lstatSync(fence.scope.directoryPath, { bigint: true });
3441
+ if (
3442
+ !currentScope.isDirectory() ||
3443
+ currentScope.isSymbolicLink() ||
3444
+ currentScope.dev.toString() !== fence.scopeIdentity.dev ||
3445
+ currentScope.ino.toString() !== fence.scopeIdentity.ino
3446
+ )
3447
+ throw new Error("managed_gc_scope_authority_mismatch");
3448
+ managedGcReserveInventoryWork(operation);
3449
+ const currentBinding = store.readExpected(MANAGED_SESSION_BINDING_FILE);
3450
+ if (
3451
+ !currentBinding ||
3452
+ !util.isDeepStrictEqual(currentBinding.identity, fence.binding.identity) ||
3453
+ !currentBinding.bytes.equals(fence.binding.bytes)
3454
+ )
3455
+ throw new Error("managed_gc_scope_authority_mismatch");
3456
+ managedGcReserveInventoryWork(operation);
3457
+ const internalDirectoryIdentity = store.captureDirectoryIdentity(MANAGED_INTERNAL_DIRECTORY);
3458
+ if (
3459
+ internalDirectoryIdentity.dev !== fence.internalDirectoryIdentity.dev ||
3460
+ internalDirectoryIdentity.ino !== fence.internalDirectoryIdentity.ino
3461
+ )
3462
+ throw new Error("managed_gc_scope_authority_mismatch");
3463
+ const currentReceipts = managedGcOptionalReceiptDirectoryInventory(fence.scope, store, operation);
3464
+ if (
3465
+ (fence.receiptInventory === undefined) !== (currentReceipts === undefined) ||
3466
+ (fence.receiptInventory !== undefined &&
3467
+ currentReceipts !== undefined &&
3468
+ !managedGcSameReceiptDirectoryInventory(fence.receiptInventory, currentReceipts))
3469
+ )
3470
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
3471
+ managedGcReserveInventoryWork(operation);
3472
+ store.assertBound();
3473
+ } catch (error) {
3474
+ primaryError = error;
3475
+ hasPrimaryError = true;
3476
+ }
3477
+ let closeError: unknown;
3478
+ try {
3479
+ store.close();
3480
+ } catch (error) {
3481
+ closeError = error;
3482
+ }
3483
+ if (hasPrimaryError) throw primaryError;
3484
+ if (closeError !== undefined) throw closeError;
3485
+ }
3486
+
3487
+ function assertManagedGcReceiptHistoryDirectoryUnchanged(
3488
+ scope: ManagedScope,
3489
+ store: ManagedSessionDescendantStore,
3490
+ history: ManagedGcReceiptHistory,
3491
+ operation: ManagedGcInventoryOperation,
3492
+ ): void {
3493
+ const current = managedGcReceiptDirectoryInventory(scope, store, operation);
3494
+ if (
3495
+ !managedGcSameReceiptDirectoryInventory(history.directoryInventory, current) ||
3496
+ current.entryCount !== history.directoryEntryCount ||
3497
+ !managedGcSameDirectoryEvidence(current.evidence, history.directoryEvidence)
3498
+ )
3499
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
3500
+ }
3501
+
3502
+ async function withManagedGcRetirementJournal<T>(
3503
+ scope: ManagedScope,
3504
+ transcriptPath: string,
3505
+ operation: (
3506
+ trusted: ManagedGcTrustedScope,
3507
+ store: ManagedSessionDescendantStore,
3508
+ lock: ManagedStorageLock,
3509
+ ) => Promise<T> | T,
3510
+ ): Promise<T> {
3511
+ const trusted = managedGcTrustedScope(scope);
3512
+ validateManagedGcTranscriptPath(scope, transcriptPath);
3513
+ const store = managedGcScopeStore(scope, trusted);
3514
+ let lock: ManagedStorageLock | undefined;
3515
+ let operationError: unknown;
3516
+ let result: T | undefined;
3517
+ let operationCompleted = false;
3518
+ let hasOperationError = false;
3519
+ try {
3520
+ store.verifyRootSecurity();
3521
+ store.ensureDirectory(MANAGED_GC_RETIREMENT_RECEIPTS);
3522
+ store.ensureDirectory(MANAGED_GC_RETIREMENT_LOCKS);
3523
+ lock = await acquireManagedLock(
3524
+ path.join(scope.directoryPath, MANAGED_GC_RETIREMENT_LOCKS),
3525
+ MANAGED_GC_RETIREMENT_SCOPE_LOCK_NAME,
3526
+ trusted.root,
3527
+ trusted.policy,
3528
+ );
3529
+ lock.assertOwned();
3530
+ store.assertBound();
3531
+ result = await operation(trusted, store, lock);
3532
+ operationCompleted = true;
3533
+ } catch (error) {
3534
+ operationError = error;
3535
+ hasOperationError = true;
3536
+ }
3537
+ let releaseError: unknown;
3538
+ let hasReleaseError = false;
3539
+ if (lock) {
3540
+ try {
3541
+ await lock.release();
3542
+ } catch (error) {
3543
+ releaseError = error;
3544
+ hasReleaseError = true;
3545
+ }
3546
+ }
3547
+ let closeError: unknown;
3548
+ let hasCloseError = false;
3549
+ try {
3550
+ store.close();
3551
+ } catch (error) {
3552
+ closeError = error;
3553
+ hasCloseError = true;
3554
+ }
3555
+ if (hasOperationError && (hasReleaseError || hasCloseError))
3556
+ throw new AggregateError(
3557
+ [operationError, ...(hasReleaseError ? [releaseError] : []), ...(hasCloseError ? [closeError] : [])],
3558
+ "managed_gc_retirement_operation_and_cleanup_failed",
3559
+ );
3560
+ if (hasOperationError) throw operationError;
3561
+ if (hasReleaseError) throw releaseError;
3562
+ if (hasCloseError) throw closeError;
3563
+ if (!operationCompleted) throw new Error("managed_gc_retirement_result_missing");
3564
+ return result as T;
3565
+ }
3566
+
3567
+ /** Read the prepared authority first, then strictly validate every later receipt against it. */
3568
+ export async function readManagedGcSessionRetirementReceipt(
3569
+ scope: ManagedScope,
3570
+ transcriptPath: string,
3571
+ ): Promise<ManagedGcSessionRetirementReceipt | undefined> {
3572
+ const result = await withManagedGcRetirementJournal(scope, transcriptPath, (trusted, store, lock) =>
3573
+ readManagedGcSessionRetirementReceiptUnlocked(scope, transcriptPath, trusted, store, {
3574
+ kind: "leased-journal",
3575
+ lock,
3576
+ }),
3577
+ );
3578
+ return result.receipt;
3579
+ }
3580
+
3581
+ /** Read an authenticated journal without creating lock/receipt directories or repairing security state. */
3582
+ export async function readManagedGcSessionRetirementReceiptReadOnly(
3583
+ scope: ManagedScope,
3584
+ transcriptPath: string,
3585
+ ): Promise<ManagedGcSessionRetirementReceipt | undefined> {
3586
+ const operation = newManagedGcInspectionOperation();
3587
+ managedGcReserveScopeComponentInputs(operation, scope);
3588
+ managedGcReserveInventoryWork(operation, managedGcComponentInputAllowance(transcriptPath));
3589
+ const trusted = managedGcTrustedScope(scope);
3590
+ validateManagedGcTranscriptPath(scope, transcriptPath);
3591
+ managedGcReserveInventoryWork(operation, 2);
3592
+ const store = managedGcScopeReader(scope, trusted);
3593
+ let receipt: ManagedGcSessionRetirementReceipt | undefined;
3594
+ let primaryError: unknown;
3595
+ let hasPrimaryError = false;
3596
+ try {
3597
+ managedGcReserveInventoryWork(operation);
3598
+ store.verifyRootSecurity();
3599
+ managedGcReserveInventoryWork(operation);
3600
+ store.assertBound();
3601
+ let receiptDirectoryPresent = true;
3602
+ try {
3603
+ managedGcReserveInventoryWork(operation);
3604
+ store.captureDirectoryIdentity(MANAGED_GC_RETIREMENT_RECEIPTS);
3605
+ } catch (error) {
3606
+ if (!hasFsCode(error, "ENOENT")) throw error;
3607
+ receiptDirectoryPresent = false;
3608
+ }
3609
+ if (receiptDirectoryPresent)
3610
+ receipt = readManagedGcSessionRetirementReceiptUnlocked(
3611
+ scope,
3612
+ transcriptPath,
3613
+ trusted,
3614
+ store,
3615
+ operation,
3616
+ ).receipt;
3617
+ } catch (error) {
3618
+ primaryError = error;
3619
+ hasPrimaryError = true;
3620
+ }
3621
+ let closeError: unknown;
3622
+ try {
3623
+ store.close();
3624
+ } catch (error) {
3625
+ closeError = error;
3626
+ }
3627
+ if (hasPrimaryError) throw primaryError;
3628
+ if (closeError !== undefined) throw closeError;
3629
+ return receipt;
3630
+ }
3631
+
3632
+ function hasManagedGcRetirementJournalWithoutWorkspace(
3633
+ scope: ManagedScope,
3634
+ operation: ManagedGcInventoryOperation,
3635
+ onEmptyInventory?: (fence: ManagedGcDiscoveryScopeFence) => void,
3636
+ ): boolean {
3637
+ managedGcReserveScopeComponentInputs(operation, scope);
3638
+ if (
3639
+ path.resolve(scope.canonicalCwd) !== scope.canonicalCwd ||
3640
+ scope.platform !== (process.platform === "win32" ? "win32" : "posix") ||
3641
+ scope.directoryName !== `v2-${scopeDigest(scope.platform, scope.canonicalCwd)}`
3642
+ )
3643
+ throw new Error("managed_gc_scope_authority_mismatch");
3644
+ managedGcReserveInventoryWork(operation);
3645
+ const identity = fs.lstatSync(scope.directoryPath, { bigint: true });
3646
+ if (!identity.isDirectory() || identity.isSymbolicLink()) throw new Error("managed_gc_scope_authority_mismatch");
3647
+ const rootPath = configuredRootPath(scope);
3648
+ managedGcReserveInventoryWork(operation);
3649
+ const rootIdentity = fs.lstatSync(rootPath, { bigint: true });
3650
+ const assertRoot = (): void => {
3651
+ managedGcReserveInventoryWork(operation);
3652
+ const security = validateNativeSecurityResult(
3653
+ verifyExistingManagedScopeDirectory(rootPath),
3654
+ "verify",
3655
+ "directory",
3656
+ );
3657
+ managedGcReserveInventoryWork(operation);
3658
+ const current = fs.lstatSync(rootPath, { bigint: true });
3659
+ if (
3660
+ !security.ok ||
3661
+ !current.isDirectory() ||
3662
+ current.isSymbolicLink() ||
3663
+ current.dev !== rootIdentity.dev ||
3664
+ current.ino !== rootIdentity.ino
3665
+ )
3666
+ throw new Error("managed_gc_scope_authority_mismatch");
3667
+ };
3668
+ assertRoot();
3669
+ managedGcReserveInventoryWork(operation);
3670
+ const rootAuthority = managedDirectoryRoot(rootPath);
3671
+ if (
3672
+ rootAuthority.dev !== BigInt.asUintN(64, rootIdentity.dev) ||
3673
+ rootAuthority.ino !== BigInt.asUintN(64, rootIdentity.ino)
3674
+ )
3675
+ throw new Error("managed_gc_scope_authority_mismatch");
3676
+ managedGcReserveInventoryWork(operation, 2);
3677
+ const store = new ManagedSessionDescendantStore(
3678
+ rootAuthority,
3679
+ scope.directoryPath,
3680
+ undefined,
3681
+ scope.platform === "win32" ? "windows-existing-verify-first" : "default",
3682
+ scope.agentDir,
3683
+ {
3684
+ canonicalPath: scope.directoryPath,
3685
+ dev: BigInt.asUintN(64, identity.dev),
3686
+ ino: BigInt.asUintN(64, identity.ino),
3687
+ },
3688
+ "read-only",
3689
+ );
3690
+ let result: boolean | undefined;
3691
+ let primaryError: unknown;
3692
+ let hasPrimaryError = false;
3693
+ try {
3694
+ managedGcReserveInventoryWork(operation);
3695
+ store.verifyRootSecurity();
3696
+ managedGcReserveInventoryWork(operation);
3697
+ const binding = store.readExpected(MANAGED_SESSION_BINDING_FILE);
3698
+ if (!binding || validateBindingRaw(scope, binding.bytes.toString("utf8")))
3699
+ throw new Error("managed_gc_scope_authority_mismatch");
3700
+ const readNames = (): ManagedGcDirectoryInventory | undefined => {
3701
+ try {
3702
+ return managedGcReceiptDirectoryInventory(scope, store, operation);
3703
+ } catch (error) {
3704
+ if (hasFsCode(error, "ENOENT")) return undefined;
3705
+ throw error;
3706
+ }
3707
+ };
3708
+ const before = readNames();
3709
+ managedGcReserveInventoryWork(operation);
3710
+ const currentBinding = store.readExpected(MANAGED_SESSION_BINDING_FILE);
3711
+ const after = readNames();
3712
+ if (
3713
+ !currentBinding ||
3714
+ !util.isDeepStrictEqual(binding.identity, currentBinding.identity) ||
3715
+ !binding.bytes.equals(currentBinding.bytes) ||
3716
+ (before === undefined) !== (after === undefined) ||
3717
+ (before !== undefined && after !== undefined && !managedGcSameReceiptDirectoryInventory(before, after))
3718
+ )
3719
+ throw new Error("managed_gc_scope_authority_mismatch");
3720
+ managedGcReserveInventoryWork(operation);
3721
+ store.verifyRootSecurity();
3722
+ assertRoot();
3723
+ const hasJournal = before?.entries.some(entry => entry.name.startsWith(MANAGED_GC_RETIREMENT_PREFIX)) ?? false;
3724
+ if (!hasJournal && onEmptyInventory) {
3725
+ managedGcReserveInventoryWork(operation);
3726
+ const internalDirectoryIdentity = store.captureDirectoryIdentity(MANAGED_INTERNAL_DIRECTORY);
3727
+ const policy: ManagedSessionSecurityPolicy =
3728
+ scope.platform === "win32" ? "windows-existing-verify-first" : "default";
3729
+ managedGcReserveInventoryWork(operation, 2);
3730
+ onEmptyInventory({
3731
+ scope,
3732
+ scopeIdentity: { dev: identity.dev.toString(), ino: identity.ino.toString() },
3733
+ binding,
3734
+ internalDirectoryIdentity,
3735
+ receiptInventory: before,
3736
+ openReader: () => {
3737
+ managedGcReserveInventoryWork(operation, 2);
3738
+ return new ManagedSessionDescendantStore(
3739
+ rootAuthority,
3740
+ scope.directoryPath,
3741
+ undefined,
3742
+ policy,
3743
+ scope.agentDir,
3744
+ {
3745
+ canonicalPath: scope.directoryPath,
3746
+ dev: BigInt.asUintN(64, identity.dev),
3747
+ ino: BigInt.asUintN(64, identity.ino),
3748
+ },
3749
+ "read-only",
3750
+ );
3751
+ },
3752
+ });
3753
+ }
3754
+ result = hasJournal;
3755
+ } catch (error) {
3756
+ primaryError = error;
3757
+ hasPrimaryError = true;
3758
+ }
3759
+ let closeError: unknown;
3760
+ try {
3761
+ store.close();
3762
+ } catch (error) {
3763
+ closeError = error;
3764
+ }
3765
+ if (hasPrimaryError) throw primaryError;
3766
+ if (closeError !== undefined) throw closeError;
3767
+ return result as boolean;
3768
+ }
3769
+
3770
+ /** Discover complete owner journals under authenticated existing v2 scopes without initializing storage. */
3771
+ export async function discoverManagedGcSessionRetirementReceipts(input: {
3772
+ agentDir: string;
3773
+ sessionsRoot: string;
3774
+ }): Promise<readonly { readonly scope: ManagedScope; readonly receipt: ManagedGcSessionRetirementReceipt }[]> {
3775
+ const operation = newManagedGcInspectionOperation();
3776
+ return discoverManagedGcSessionRetirementReceiptsWithOperation(input, operation);
3777
+ }
3778
+
3779
+ async function discoverManagedGcSessionRetirementReceiptsWithOperation(
3780
+ input: { agentDir: string; sessionsRoot: string },
3781
+ operation: ManagedGcInventoryOperation,
3782
+ ): Promise<readonly { readonly scope: ManagedScope; readonly receipt: ManagedGcSessionRetirementReceipt }[]> {
3783
+ managedGcReserveInventoryWork(operation, managedGcComponentInputAllowance(input.agentDir));
3784
+ const agentDir = canonicalizeTrustedPath(input.agentDir);
3785
+ managedGcReserveInventoryWork(operation, managedGcComponentInputAllowance(input.sessionsRoot));
3786
+ const sessionsRoot = canonicalizeTrustedPath(input.sessionsRoot);
3787
+ managedGcReserveInventoryWork(operation);
3788
+ const profileStat = fs.lstatSync(agentDir, { bigint: true });
3789
+ managedGcReserveInventoryWork(operation);
3790
+ const rootStat = fs.lstatSync(sessionsRoot, { bigint: true });
3791
+ for (const [pathname, stat] of [
3792
+ [agentDir, profileStat],
3793
+ [sessionsRoot, rootStat],
3794
+ ] as const) {
3795
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("managed_gc_scope_authority_mismatch");
3796
+ managedGcReserveInventoryWork(operation);
3797
+ const verified = validateNativeSecurityResult(
3798
+ verifyExistingManagedScopeDirectory(pathname),
3799
+ "verify",
3800
+ "directory",
3801
+ );
3802
+ if (!verified.ok) throw new Error("managed_gc_scope_authority_mismatch");
3803
+ }
3804
+ const inventoryBefore = {
3805
+ dev: rootStat.dev,
3806
+ ino: rootStat.ino,
3807
+ profileDev: profileStat.dev,
3808
+ profileIno: profileStat.ino,
3809
+ };
3810
+ managedGcReserveInventoryWork(operation);
3811
+ const rootInventory = managedGcStreamDirectoryInventory(
3812
+ sessionsRoot,
3813
+ entry => /^v2-[a-z2-7]{52}$/u.test(entry.name),
3814
+ entry => /^v2-[a-z2-7]{52}$/u.test(entry.name),
3815
+ operation,
3816
+ );
3817
+ if (rootInventory.evidence.dev !== rootStat.dev.toString() || rootInventory.evidence.ino !== rootStat.ino.toString())
3818
+ throw new Error("managed_gc_scope_authority_mismatch");
3819
+ const entries = rootInventory.entries;
3820
+ managedGcReserveInventoryWork(operation, entries.length);
3821
+ const result: Array<{ readonly scope: ManagedScope; readonly receipt: ManagedGcSessionRetirementReceipt }> = [];
3822
+ managedGcReserveInventoryWork(operation, entries.length);
3823
+ const scopeFences: ManagedGcDiscoveryScopeFence[] = [];
3824
+ // Fail closed for the whole root: a partial inventory cannot prove live sibling absence.
3825
+ for (const entry of entries) {
3826
+ if (!/^v2-[a-z2-7]{52}$/u.test(entry.name)) continue;
3827
+ if (!entry.isDirectory() || entry.isSymbolicLink()) throw new Error("managed_gc_scope_authority_mismatch");
3828
+ managedGcReserveInventoryWork(operation, 2);
3829
+ const scopePath = path.join(sessionsRoot, entry.name);
3830
+ const bindingPath = path.join(scopePath, MANAGED_SESSION_BINDING_FILE);
3831
+ managedGcReserveInventoryWork(operation);
3832
+ const bindingBytes = captureManagedFileNoFollow(bindingPath).bytes;
3833
+ let bindingValue: unknown;
3834
+ try {
3835
+ bindingValue = JSON.parse(bindingBytes.toString("utf8"));
3836
+ } catch {
3837
+ throw new Error("managed_gc_scope_authority_mismatch");
3838
+ }
3839
+ if (!isBinding(bindingValue) || bindingValue.identityDigest !== entry.name.slice(3))
3840
+ throw new Error("managed_gc_scope_authority_mismatch");
3841
+ managedGcReserveInventoryWork(operation, managedGcComponentInputAllowance(bindingValue.canonicalPath));
3842
+ const resolved = resolveManagedGcScopeForRead({ cwd: bindingValue.canonicalPath, agentDir, sessionsRoot });
3843
+ if (resolved.kind === "error" && resolved.code === "cwd_missing") {
3844
+ const storedScope: ManagedScope = {
3845
+ apiVersion: 1,
3846
+ layoutVersion: MANAGED_SESSION_LAYOUT_VERSION,
3847
+ identityVersion: MANAGED_SESSION_IDENTITY_VERSION,
3848
+ agentDir,
3849
+ sessionsRoot,
3850
+ canonicalCwd: bindingValue.canonicalPath,
3851
+ legacyLexicalCwd: bindingValue.canonicalPath,
3852
+ directoryName: entry.name,
3853
+ directoryPath: scopePath,
3854
+ platform: bindingValue.platform,
3855
+ };
3856
+ if (
3857
+ !hasManagedGcRetirementJournalWithoutWorkspace(storedScope, operation, fence => {
3858
+ managedGcReserveInventoryWork(operation);
3859
+ scopeFences.push(fence);
3860
+ })
3861
+ )
3862
+ continue;
3863
+ }
3864
+ if (resolved.kind !== "resolved" || resolved.scope.directoryPath !== scopePath)
3865
+ throw new Error("managed_gc_scope_authority_mismatch");
3866
+ const scope = resolved.scope;
3867
+ managedGcReserveScopeComponentInputs(operation, scope);
3868
+ const trusted = managedGcTrustedScope(scope);
3869
+ managedGcReserveInventoryWork(operation, 2);
3870
+ const store = managedGcScopeReader(scope, trusted);
3871
+ const hasReceiptDirectory = managedGcWithClosedReader(store, () => {
3872
+ managedGcReserveInventoryWork(operation);
3873
+ const binding = store.readExpected(MANAGED_SESSION_BINDING_FILE);
3874
+ if (
3875
+ !binding ||
3876
+ validateBindingRaw(scope, binding.bytes.toString("utf8")) ||
3877
+ !binding.bytes.equals(bindingBytes)
3878
+ )
3879
+ throw new Error("managed_gc_scope_authority_mismatch");
3880
+ const scopeFence = managedGcDiscoveryScopeFence(
3881
+ scope,
3882
+ store,
3883
+ { dev: trusted.identity.dev.toString(), ino: trusted.identity.ino.toString() },
3884
+ binding,
3885
+ () => {
3886
+ managedGcReserveInventoryWork(operation, 2);
3887
+ return managedGcScopeReader(scope, trusted);
3888
+ },
3889
+ operation,
3890
+ );
3891
+ managedGcReserveInventoryWork(operation);
3892
+ scopeFences.push(scopeFence);
3893
+ const receiptInventory = scopeFence.receiptInventory;
3894
+ if (!receiptInventory) return false;
3895
+ managedGcReserveInventoryWork(operation, receiptInventory.physicalEntryCount);
3896
+ const groups = new Map<string, string[]>();
3897
+ for (const item of receiptInventory.entries) {
3898
+ if (!item.name.startsWith(MANAGED_GC_RETIREMENT_PREFIX)) continue;
3899
+ if (
3900
+ !item.isFile() ||
3901
+ item.isSymbolicLink() ||
3902
+ !/^gc-retirement-[a-f0-9]{64}-(?:prepared|artifacts_removed|owner_retired|owner_pending-[0-9]{8,})\.json$/u.test(
3903
+ item.name,
3904
+ )
3905
+ )
3906
+ throw new Error("task_artifact_owner_continuation_corrupt");
3907
+ managedGcReserveInventoryWork(operation);
3908
+ const key = item.name.slice(MANAGED_GC_RETIREMENT_PREFIX.length, MANAGED_GC_RETIREMENT_PREFIX.length + 64);
3909
+ let group = groups.get(key);
3910
+ if (!group) {
3911
+ managedGcReserveInventoryWork(operation);
3912
+ group = [];
3913
+ groups.set(key, group);
3914
+ }
3915
+ managedGcReserveInventoryWork(operation);
3916
+ group.push(item.name);
3917
+ }
3918
+ for (const [key, group] of groups) {
3919
+ const preparedName = `${MANAGED_GC_RETIREMENT_PREFIX}${key}-prepared.json`;
3920
+ if (!group.includes(preparedName)) throw new Error("task_artifact_owner_continuation_state_missing");
3921
+ managedGcReserveInventoryWork(operation);
3922
+ const bootstrapHistory: MutableManagedGcReceiptHistory = {
3923
+ encodedBytes: 0,
3924
+ admittedFiles: new Set<string>(),
3925
+ };
3926
+ const preparedFile = readManagedGcReceiptSnapshot(
3927
+ store,
3928
+ `${MANAGED_GC_RETIREMENT_RECEIPTS}/${preparedName}`,
3929
+ bootstrapHistory,
3930
+ );
3931
+ if (!preparedFile) throw new Error("task_artifact_owner_continuation_state_missing");
3932
+ let preparedValue: unknown;
3933
+ try {
3934
+ preparedValue = JSON.parse(preparedFile.bytes.toString("utf8"));
3935
+ } catch {
3936
+ throw new Error("task_artifact_owner_continuation_corrupt");
3937
+ }
3938
+ const transcriptPath =
3939
+ preparedValue && typeof preparedValue === "object"
3940
+ ? (preparedValue as Record<string, unknown>).transcriptPath
3941
+ : undefined;
3942
+ if (typeof transcriptPath === "string") {
3943
+ managedGcReserveInventoryWork(operation, managedGcComponentInputAllowance(transcriptPath));
3944
+ }
3945
+ if (typeof transcriptPath !== "string" || managedGcRetirementTranscriptKey(transcriptPath) !== key)
3946
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
3947
+ const read = readManagedGcSessionRetirementReceiptUnlocked(
3948
+ scope,
3949
+ transcriptPath,
3950
+ trusted,
3951
+ store,
3952
+ operation,
3953
+ receiptInventory,
3954
+ group,
3955
+ { snapshot: preparedFile, value: preparedValue, history: bootstrapHistory },
3956
+ );
3957
+ const receipt = read.receipt;
3958
+ if (!receipt) throw new Error("task_artifact_owner_continuation_state_missing");
3959
+ managedGcReserveInventoryWork(operation);
3960
+ result.push({ scope, receipt });
3961
+ }
3962
+ const latestInventory = managedGcReceiptDirectoryInventory(scope, store, operation);
3963
+ if (!managedGcSameReceiptDirectoryInventory(receiptInventory, latestInventory))
3964
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
3965
+ return true;
3966
+ });
3967
+ if (!hasReceiptDirectory) continue;
3968
+ }
3969
+ for (const fence of scopeFences) managedGcAssertDiscoveryScopeFence(fence, operation);
3970
+ const rootInventoryAfter = managedGcStreamDirectoryInventory(
3971
+ sessionsRoot,
3972
+ entry => /^v2-[a-z2-7]{52}$/u.test(entry.name),
3973
+ entry => /^v2-[a-z2-7]{52}$/u.test(entry.name),
3974
+ operation,
3975
+ );
3976
+ if (!managedGcSameReceiptDirectoryInventory(rootInventory, rootInventoryAfter))
3977
+ throw new Error("managed_gc_scope_authority_mismatch");
3978
+ managedGcReserveInventoryWork(operation);
3979
+ const rootAfter = fs.lstatSync(sessionsRoot, { bigint: true });
3980
+ managedGcReserveInventoryWork(operation);
3981
+ const profileAfter = fs.lstatSync(agentDir, { bigint: true });
3982
+ if (
3983
+ !rootAfter.isDirectory() ||
3984
+ rootAfter.isSymbolicLink() ||
3985
+ rootAfter.dev !== inventoryBefore.dev ||
3986
+ rootAfter.ino !== inventoryBefore.ino ||
3987
+ !profileAfter.isDirectory() ||
3988
+ profileAfter.isSymbolicLink() ||
3989
+ profileAfter.dev !== inventoryBefore.profileDev ||
3990
+ profileAfter.ino !== inventoryBefore.profileIno
3991
+ )
3992
+ throw new Error("managed_gc_scope_authority_mismatch");
3993
+ managedGcReserveInventoryWork(operation, result.length);
3994
+ return result.sort((left, right) => left.receipt.transcriptPath.localeCompare(right.receipt.transcriptPath));
3995
+ }
3996
+
3997
+ const MANAGED_GC_PROTOCOL_ROLES: readonly ManagedGcProtocolRole[] = [
3998
+ MANAGED_LOCKS_DIRECTORY,
3999
+ MANAGED_RECEIPTS_DIRECTORY,
4000
+ MANAGED_TOMBSTONES_DIRECTORY,
4001
+ ];
4002
+ const MANAGED_GC_PROTOCOL_MAX_ENTRIES = MANAGED_ARTIFACT_MAX_FILES;
4003
+
4004
+ function managedGcProtocolDirectoryStat(pathname: string): fs.BigIntStats {
4005
+ const stat = fs.lstatSync(pathname, { bigint: true });
4006
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("managed_gc_protocol_directory_invalid");
4007
+ const security = validateNativeSecurityResult(verifyExistingManagedScopeDirectory(pathname), "verify", "directory");
4008
+ if (!security.ok) throw new Error("managed_gc_protocol_directory_invalid");
4009
+ return stat;
4010
+ }
4011
+
4012
+ function managedGcProtocolDirectorySnapshot(
4013
+ scope: ManagedScope,
4014
+ store: ManagedSessionDescendantStore,
4015
+ role: ManagedGcProtocolRole | typeof MANAGED_INTERNAL_DIRECTORY,
4016
+ operation: ManagedGcInventoryOperation,
4017
+ ): { readonly path: string; readonly stat: fs.BigIntStats; readonly names: readonly string[] } {
4018
+ const relativePath = role === MANAGED_INTERNAL_DIRECTORY ? role : `${MANAGED_INTERNAL_DIRECTORY}/${role}`;
4019
+ managedGcReserveInventoryWork(
4020
+ operation,
4021
+ managedGcComponentInputAllowance(scope.directoryPath) + relativePath.length,
4022
+ );
4023
+ const pathname = path.join(scope.directoryPath, relativePath);
4024
+ managedGcReserveInventoryWork(operation);
4025
+ const before = managedGcProtocolDirectoryStat(pathname);
4026
+ managedGcReserveInventoryWork(operation);
4027
+ const retained = store.captureDirectoryIdentity(relativePath);
4028
+ if (retained.dev !== before.dev.toString() || retained.ino !== before.ino.toString())
4029
+ throw new Error("managed_gc_protocol_directory_identity_mismatch");
4030
+ const inventory = managedGcStreamDirectoryInventory(
4031
+ pathname,
4032
+ entry => role === MANAGED_RECEIPTS_DIRECTORY && entry.name.startsWith(MANAGED_GC_RETIREMENT_PREFIX),
4033
+ () => true,
4034
+ operation,
4035
+ );
4036
+ managedGcReserveInventoryWork(operation, inventory.entries.length);
4037
+ const names = inventory.entries.map(entry => entry.name);
4038
+ managedGcReserveInventoryWork(operation);
4039
+ const after = managedGcProtocolDirectoryStat(pathname);
4040
+ if (
4041
+ after.dev !== before.dev ||
4042
+ after.ino !== before.ino ||
4043
+ after.mtimeNs !== before.mtimeNs ||
4044
+ after.ctimeNs !== before.ctimeNs ||
4045
+ after.mode !== before.mode ||
4046
+ !managedGcSameDirectoryEvidence(inventory.evidence, managedGcDirectoryEvidence(before))
4047
+ )
4048
+ throw new Error("managed_gc_protocol_directory_changed");
4049
+ managedGcReserveInventoryWork(operation);
4050
+ store.assertBound();
4051
+ return { path: pathname, stat: before, names };
4052
+ }
4053
+
4054
+ function managedGcProtocolFileSnapshot(
4055
+ scope: ManagedScope,
4056
+ store: ManagedSessionDescendantStore,
4057
+ relativePath: string,
4058
+ receiptHistories?: Map<string, MutableManagedGcReceiptHistory>,
4059
+ ): {
4060
+ readonly snapshot: ManagedFileSnapshot;
4061
+ readonly identity: ManagedGcProtocolFileSnapshot;
4062
+ } {
4063
+ store.assertBound();
4064
+ const pathname = path.join(scope.directoryPath, relativePath);
4065
+ const before = fs.lstatSync(pathname, { bigint: true });
4066
+ if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n)
4067
+ throw new Error("managed_gc_protocol_file_invalid");
4068
+ const native = nativeScope();
4069
+ const security = validateNativeSecurityResult(
4070
+ process.platform === "win32"
4071
+ ? native.verifyOwnerOnlyPathSecurityExpected(pathname, "file", before.dev, before.ino)
4072
+ : native.verifyOwnerOnlyPathSecurity(pathname, "file"),
4073
+ "verify",
4074
+ "file",
4075
+ );
4076
+ if (!security.ok) throw new Error("managed_gc_protocol_file_invalid");
4077
+ let snapshot: ManagedFileSnapshot | null;
4078
+ if (isManagedGcRetirementReceiptPath(relativePath)) {
4079
+ let history: MutableManagedGcReceiptHistory | undefined;
4080
+ if (receiptHistories) {
4081
+ const match =
4082
+ /^gc-retirement-([a-f0-9]{64})-(?:prepared|artifacts_removed|owner_retired|owner_pending-[0-9]{8,})\.json$/u.exec(
4083
+ path.posix.basename(relativePath),
4084
+ );
4085
+ if (!match) throw new Error("task_artifact_owner_continuation_corrupt");
4086
+ history = receiptHistories.get(match[1]!);
4087
+ if (!history) {
4088
+ history = { encodedBytes: 0, admittedFiles: new Set<string>() };
4089
+ receiptHistories.set(match[1]!, history);
4090
+ }
4091
+ }
4092
+ snapshot = readManagedGcReceiptSnapshot(store, relativePath, history);
4093
+ } else if (isManagedGcCleanupReceiptPath(relativePath)) {
4094
+ try {
4095
+ snapshot = store.readExpectedBounded(relativePath, CLEANUP_RECEIPT_REPLAY_MAX_BYTES);
4096
+ } catch (error) {
4097
+ if (error instanceof Error && error.message === "artifact_capacity_exceeded") cleanupReceiptReplayCapacity();
4098
+ throw error;
4099
+ }
4100
+ } else {
4101
+ snapshot = store.readExpected(relativePath);
4102
+ }
4103
+ if (!snapshot) throw new Error("managed_gc_protocol_file_missing");
4104
+ const after = fs.lstatSync(pathname, { bigint: true });
4105
+ if (
4106
+ !after.isFile() ||
4107
+ after.isSymbolicLink() ||
4108
+ before.dev !== after.dev ||
4109
+ before.ino !== after.ino ||
4110
+ before.nlink !== after.nlink ||
4111
+ before.size !== after.size ||
4112
+ before.mtimeNs !== after.mtimeNs ||
4113
+ before.ctimeNs !== after.ctimeNs ||
4114
+ before.mode !== after.mode ||
4115
+ snapshot.identity.dev !== before.dev ||
4116
+ snapshot.identity.ino !== before.ino ||
4117
+ snapshot.identity.nlink !== before.nlink ||
4118
+ snapshot.identity.size !== Number(before.size) ||
4119
+ snapshot.identity.mtimeNs !== before.mtimeNs ||
4120
+ snapshot.identity.ctimeNs !== before.ctimeNs
4121
+ )
4122
+ throw new Error("managed_gc_protocol_file_changed");
4123
+ store.assertBound();
4124
+ return {
4125
+ snapshot,
4126
+ identity: {
4127
+ name: path.basename(pathname),
4128
+ dev: snapshot.identity.dev.toString(),
4129
+ ino: snapshot.identity.ino.toString(),
4130
+ nlink: snapshot.identity.nlink.toString(),
4131
+ size: snapshot.identity.size,
4132
+ mtimeNs: snapshot.identity.mtimeNs.toString(),
4133
+ ctimeNs: snapshot.identity.ctimeNs.toString(),
4134
+ mode: Number(before.mode & 0o777n),
4135
+ sha256: snapshot.identity.sha256,
4136
+ },
4137
+ };
4138
+ }
4139
+
4140
+ function isManagedGcPendingCleanupReceiptPath(relativePath: string): boolean {
4141
+ const prefix = `${MANAGED_INTERNAL_DIRECTORY}/${MANAGED_TOMBSTONES_DIRECTORY}/`;
4142
+ if (!relativePath.startsWith(prefix)) return false;
4143
+ return /\.cleanup-pending-[1-9][0-9]*\.json$/u.test(relativePath.slice(prefix.length));
4144
+ }
4145
+
4146
+ function isManagedGcCleanupReceiptPath(relativePath: string): boolean {
4147
+ const prefix = `${MANAGED_INTERNAL_DIRECTORY}/${MANAGED_TOMBSTONES_DIRECTORY}/`;
4148
+ if (!relativePath.startsWith(prefix)) return false;
4149
+ const match =
4150
+ /^[a-f0-9]{64}\.[a-f0-9]{64}\.cleanup-(pending|artifacts_removed|completed)-([1-9][0-9]*)\.json$/u.exec(
4151
+ relativePath.slice(prefix.length),
4152
+ );
4153
+ if (!match) return false;
4154
+ const attempt = Number(match[2]);
4155
+ return Number.isSafeInteger(attempt) && String(attempt) === match[2] && (match[1] !== "completed" || attempt === 1);
4156
+ }
4157
+
4158
+ function isManagedGcRetirementReceiptPath(relativePath: string): boolean {
4159
+ return relativePath.startsWith(`${MANAGED_GC_RETIREMENT_RECEIPTS}/${MANAGED_GC_RETIREMENT_PREFIX}`);
4160
+ }
4161
+
4162
+ function isManagedGcCleanupLikeName(name: string): boolean {
4163
+ return name.includes(".cleanup-") && name.endsWith(".json");
4164
+ }
4165
+
4166
+ function managedGcProtocolRecord(
4167
+ bytes: Uint8Array,
4168
+ role: ManagedGcProtocolRole,
4169
+ name: string,
4170
+ ): Record<string, unknown> {
4171
+ try {
4172
+ const value: unknown = JSON.parse(Buffer.from(bytes).toString("utf8"));
4173
+ if (value && typeof value === "object" && !Array.isArray(value)) return value as Record<string, unknown>;
4174
+ } catch {
4175
+ // A role-specific parser below supplies the bounded protocol location.
4176
+ }
4177
+ throw new Error(`managed_gc_protocol_record_invalid:${role}/${name}`);
4178
+ }
4179
+
4180
+ function managedGcProtocolFileIdentityMatches(
4181
+ left: ManagedGcProtocolFileSnapshot,
4182
+ right: ManagedGcProtocolFileSnapshot,
4183
+ ): boolean {
4184
+ return (
4185
+ left.name === right.name &&
4186
+ left.dev === right.dev &&
4187
+ left.ino === right.ino &&
4188
+ left.nlink === right.nlink &&
4189
+ left.size === right.size &&
4190
+ left.mtimeNs === right.mtimeNs &&
4191
+ left.ctimeNs === right.ctimeNs &&
4192
+ left.mode === right.mode &&
4193
+ left.sha256 === right.sha256
4194
+ );
4195
+ }
4196
+
4197
+ function managedGcProtocolPathIdentity(pathname: string): ManagedGcProtocolPathIdentity {
4198
+ const stat = managedGcProtocolDirectoryStat(pathname);
4199
+ return { path: pathname, dev: stat.dev.toString(), ino: stat.ino.toString() };
4200
+ }
4201
+
4202
+ function validateManagedGcProtocolMigrationReceipt(
4203
+ scope: ManagedScope,
4204
+ pathname: string,
4205
+ name: string,
4206
+ bytes: Uint8Array,
4207
+ ): void {
4208
+ const match = /^([a-f0-9]{64})(?:\.(prepared|detached|published))?\.json$/u.exec(name);
4209
+ if (!match || match[2] !== undefined) throw new Error("managed_gc_protocol_migration_incomplete");
4210
+ const record = managedGcProtocolRecord(bytes, MANAGED_RECEIPTS_DIRECTORY, name);
4211
+ if (record.state !== "committed") throw new Error("managed_gc_protocol_migration_incomplete");
4212
+ const source = record.source as { path?: unknown } | undefined;
4213
+ const destination = record.destination as { path?: unknown } | undefined;
4214
+ if (typeof source?.path !== "string" || typeof destination?.path !== "string")
4215
+ throw new Error("managed_gc_protocol_migration_invalid");
4216
+ const legacy = inspectCandidate(source.path, "legacy");
4217
+ const managed = inspectCandidate(destination.path, "v2");
4218
+ if ("code" in legacy || "code" in managed || !receiptMatches(pathname, legacy, managed, scope))
4219
+ throw new Error("managed_gc_protocol_migration_authority_mismatch");
4220
+ }
4221
+
4222
+ function validateManagedGcProtocolTombstoneFile(
4223
+ scope: ManagedScope,
4224
+ pathname: string,
4225
+ name: string,
4226
+ bytes: Uint8Array,
4227
+ journals: readonly { readonly scope: ManagedScope; readonly receipt: ManagedGcSessionRetirementReceipt }[],
4228
+ ): void {
4229
+ if (/^[a-f0-9]{64}\.json$/u.test(name)) {
4230
+ const targets = retiredTargets(scope, pathname);
4231
+ if (!targets || targets.length === 0) throw new Error("managed_gc_protocol_tombstone_invalid");
4232
+ return;
4233
+ }
4234
+ const match =
4235
+ /^([a-f0-9]{64})\.([a-f0-9]{64})\.cleanup-(pending|artifacts_removed|completed)-([1-9][0-9]*)\.json$/u.exec(name);
4236
+ if (!match || (match[3] === "completed" && match[4] !== "1"))
4237
+ throw new Error("managed_gc_protocol_tombstone_role_invalid");
4238
+ const tombstone = path.join(path.dirname(pathname), `${match[1]}.json`);
4239
+ const targets = retiredTargets(scope, tombstone);
4240
+ if (!targets) throw new Error("managed_gc_protocol_tombstone_authority_missing");
4241
+ const target = targets.find(candidate => stableOperationName(candidate) === match[2]);
4242
+ if (!target) throw new Error("managed_gc_protocol_tombstone_target_missing");
4243
+ const attempt = Number(match[4]);
4244
+ if (match[3] === "pending") {
4245
+ const pending = pendingCleanupReceipt(scope, tombstone, target);
4246
+ if (!pending || pending.attempt < attempt) throw new Error("managed_gc_protocol_tombstone_history_invalid");
4247
+ return;
4248
+ }
4249
+ if (match[3] === "artifacts_removed") {
4250
+ if (!cleanupArtifactsRemovedEvidence(scope, tombstone, target, attempt))
4251
+ throw new Error("managed_gc_protocol_tombstone_artifacts_invalid");
4252
+ return;
4253
+ }
4254
+ const ownerReceipt = target.taskArtifactOwnerDeletionEvidence
4255
+ ? journals.find(
4256
+ item => item.scope.directoryPath === scope.directoryPath && item.receipt.transcriptPath === target.path,
4257
+ )?.receipt
4258
+ : undefined;
4259
+ if (target.taskArtifactOwnerDeletionEvidence && !ownerReceipt)
4260
+ throw new Error("task_artifact_owner_continuation_state_missing");
4261
+ const record = managedGcProtocolRecord(bytes, MANAGED_TOMBSTONES_DIRECTORY, name);
4262
+ if (!cleanupCompletedRecordMatches(scope, tombstone, target, ownerReceipt, record))
4263
+ throw new Error("managed_gc_protocol_tombstone_completion_invalid");
4264
+ }
4265
+
4266
+ /** Inspect under a live operation lease; returned snapshots never authorize effects. */
4267
+ export function managedGcProtocolInspectorForLock(
4268
+ scope: ManagedScope,
4269
+ lock: ManagedStorageLock,
4270
+ ): ManagedGcProtocolScopeInspector {
4271
+ const inspect = managedGcProtocolScopeInspectorWithOperation(scope);
4272
+ return async inputs => {
4273
+ const operation = newManagedGcInspectionOperation();
4274
+ managedGcReserveScopeComponentInputs(operation, scope);
4275
+ managedGcReserveInventoryWork(operation);
4276
+ lock.assertOwned();
4277
+ managedGcTrustedScope(scope);
4278
+ const snapshots = await inspect(inputs, operation);
4279
+ lock.assertOwned();
4280
+ managedGcReserveScopeComponentInputs(operation, scope);
4281
+ managedGcTrustedScope(scope);
4282
+ return snapshots;
4283
+ };
4284
+ }
4285
+
4286
+ /** Bind a read-only protocol inventory to an already prepared scope's root/profile/session identities. */
4287
+ export function managedGcProtocolScopeInspectorForScope(scope: ManagedScope): ManagedGcProtocolScopeInspector {
4288
+ const inspect = managedGcProtocolScopeInspectorWithOperation(scope);
4289
+ return async inputs => {
4290
+ const operation = newManagedGcInspectionOperation();
4291
+ return inspect(inputs, operation);
4292
+ };
4293
+ }
4294
+
4295
+ function managedGcProtocolScopeInspectorWithOperation(
4296
+ scope: ManagedScope,
4297
+ ): (
4298
+ inputs: readonly ManagedGcProtocolScopeInput[],
4299
+ operation: ManagedGcInventoryOperation,
4300
+ ) => Promise<readonly ManagedGcProtocolScopeSnapshot[]> {
4301
+ const trusted = managedGcTrustedScope(scope);
4302
+ const configuredRoot = managedGcProtocolPathIdentity(trusted.root.canonicalPath);
4303
+ const profile = managedGcProtocolPathIdentity(scope.agentDir);
4304
+ const sessionsRoot = managedGcProtocolPathIdentity(scope.sessionsRoot);
4305
+ const protocol = managedGcProtocolPathIdentity(path.join(scope.directoryPath, MANAGED_INTERNAL_DIRECTORY));
4306
+ const authorityIdentities = [configuredRoot, profile, sessionsRoot, protocol] as const;
4307
+ const assertAuthority = (operation: ManagedGcInventoryOperation): void => {
4308
+ managedGcReserveScopeComponentInputs(operation, scope);
4309
+ managedGcReserveInventoryWork(operation);
4310
+ managedGcTrustedScope(scope);
4311
+ for (const expected of authorityIdentities) {
4312
+ managedGcReserveInventoryWork(operation);
4313
+ const current = managedGcProtocolPathIdentity(expected.path);
4314
+ if (current.dev !== expected.dev || current.ino !== expected.ino)
4315
+ throw new Error("managed_gc_protocol_root_identity_mismatch");
4316
+ }
4317
+ };
4318
+ return async (inputs: readonly ManagedGcProtocolScopeInput[], operation: ManagedGcInventoryOperation) => {
4319
+ if (inputs.length > MANAGED_GC_PROTOCOL_MAX_ENTRIES) throw new Error("managed_gc_protocol_scope_limit_exceeded");
4320
+ managedGcReserveInventoryWork(operation, inputs.length);
4321
+ assertAuthority(operation);
4322
+ const journals = await discoverManagedGcSessionRetirementReceiptsWithOperation(
4323
+ { agentDir: scope.agentDir, sessionsRoot: scope.sessionsRoot },
4324
+ operation,
4325
+ );
4326
+ assertAuthority(operation);
4327
+ managedGcReserveInventoryWork(operation);
4328
+ managedGcReserveInventoryWork(operation);
4329
+ const snapshots: ManagedGcProtocolScopeSnapshot[] = [];
4330
+ let totalFiles = 0;
4331
+ for (const input of inputs) {
4332
+ if (typeof input.scopePath !== "string" || typeof input.protocolIdentity?.path !== "string")
4333
+ throw new Error("managed_gc_protocol_scope_path_mismatch");
4334
+ managedGcReserveInventoryWork(operation, managedGcComponentInputAllowance(input.scopePath));
4335
+ managedGcReserveInventoryWork(operation, managedGcComponentInputAllowance(input.protocolIdentity.path));
4336
+ if (
4337
+ path.resolve(input.scopePath) !== input.scopePath ||
4338
+ path.dirname(input.scopePath) !== scope.sessionsRoot ||
4339
+ !/^v2-[a-z2-7]{52}$/u.test(path.basename(input.scopePath)) ||
4340
+ input.scopeIdentity.path !== input.scopePath ||
4341
+ input.protocolIdentity.path !== path.join(input.scopePath, MANAGED_INTERNAL_DIRECTORY)
4342
+ )
4343
+ throw new Error("managed_gc_protocol_scope_path_mismatch");
4344
+ managedGcReserveInventoryWork(operation, 3);
4345
+ const bindingPath = path.join(input.scopePath, MANAGED_SESSION_BINDING_FILE);
4346
+ const binding = captureManagedFileNoFollow(bindingPath);
4347
+ managedGcReserveInventoryWork(operation);
4348
+ const bindingStat = fs.lstatSync(bindingPath, { bigint: true });
4349
+ const bindingIdentity: ManagedGcProtocolFileSnapshot = {
4350
+ name: MANAGED_SESSION_BINDING_FILE,
4351
+ dev: binding.identity.dev.toString(),
4352
+ ino: binding.identity.ino.toString(),
4353
+ nlink: binding.identity.nlink.toString(),
4354
+ size: binding.identity.size,
4355
+ mtimeNs: binding.identity.mtimeNs.toString(),
4356
+ ctimeNs: binding.identity.ctimeNs.toString(),
4357
+ mode: Number(bindingStat.mode & 0o777n),
4358
+ sha256: binding.identity.sha256,
4359
+ };
4360
+ if (!managedGcProtocolFileIdentityMatches(bindingIdentity, input.bindingIdentity))
4361
+ throw new Error("managed_gc_protocol_binding_identity_mismatch");
4362
+ const bindingValue: unknown = JSON.parse(binding.bytes.toString("utf8"));
4363
+ if (!isBinding(bindingValue)) throw new Error("managed_gc_scope_authority_mismatch");
4364
+ managedGcReserveInventoryWork(operation, managedGcComponentInputAllowance(bindingValue.canonicalPath));
4365
+ const resolved = resolveManagedGcScopeForRead({
4366
+ cwd: bindingValue.canonicalPath,
4367
+ agentDir: scope.agentDir,
4368
+ sessionsRoot: scope.sessionsRoot,
4369
+ });
4370
+ let candidateScope: ManagedScope;
4371
+ let candidateTrusted: ManagedGcTrustedScope | undefined;
4372
+ if (resolved.kind === "error" && resolved.code === "cwd_missing") {
4373
+ candidateScope = {
4374
+ apiVersion: 1,
4375
+ layoutVersion: MANAGED_SESSION_LAYOUT_VERSION,
4376
+ identityVersion: MANAGED_SESSION_IDENTITY_VERSION,
4377
+ agentDir: scope.agentDir,
4378
+ sessionsRoot: scope.sessionsRoot,
4379
+ canonicalCwd: bindingValue.canonicalPath,
4380
+ legacyLexicalCwd: bindingValue.canonicalPath,
4381
+ directoryName: path.basename(input.scopePath),
4382
+ directoryPath: input.scopePath,
4383
+ platform: bindingValue.platform,
4384
+ };
4385
+ if (hasManagedGcRetirementJournalWithoutWorkspace(candidateScope, operation))
4386
+ throw new Error("managed_gc_protocol_scope_unrecognized");
4387
+ } else if (resolved.kind === "resolved" && resolved.scope.directoryPath === input.scopePath) {
4388
+ candidateScope = resolved.scope;
4389
+ managedGcReserveScopeComponentInputs(operation, candidateScope);
4390
+ candidateTrusted = managedGcTrustedScope(candidateScope);
4391
+ } else {
4392
+ throw new Error("managed_gc_protocol_scope_unrecognized");
4393
+ }
4394
+ if (
4395
+ configuredRootPath(candidateScope) !== trusted.root.canonicalPath ||
4396
+ (candidateTrusted &&
4397
+ (candidateTrusted.root.canonicalPath !== trusted.root.canonicalPath ||
4398
+ candidateTrusted.root.dev !== trusted.root.dev ||
4399
+ candidateTrusted.root.ino !== trusted.root.ino)) ||
4400
+ candidateScope.agentDir !== scope.agentDir ||
4401
+ candidateScope.sessionsRoot !== scope.sessionsRoot
4402
+ )
4403
+ throw new Error("managed_gc_protocol_root_mismatch");
4404
+ managedGcReserveInventoryWork(operation);
4405
+ const scopeIdentity = managedGcProtocolPathIdentity(candidateScope.directoryPath);
4406
+ if (scopeIdentity.dev !== input.scopeIdentity.dev || scopeIdentity.ino !== input.scopeIdentity.ino)
4407
+ throw new Error("managed_gc_protocol_scope_identity_mismatch");
4408
+ managedGcReserveInventoryWork(operation, 2);
4409
+ const store = candidateTrusted
4410
+ ? managedGcScopeReader(candidateScope, candidateTrusted)
4411
+ : new ManagedSessionDescendantStore(
4412
+ trusted.root,
4413
+ candidateScope.directoryPath,
4414
+ undefined,
4415
+ trusted.policy,
4416
+ candidateScope.agentDir,
4417
+ {
4418
+ canonicalPath: candidateScope.directoryPath,
4419
+ dev: BigInt(scopeIdentity.dev),
4420
+ ino: BigInt(scopeIdentity.ino),
4421
+ },
4422
+ "read-only",
4423
+ );
4424
+ managedGcWithClosedReader(store, () => {
4425
+ managedGcReserveInventoryWork(operation);
4426
+ store.verifyRootSecurity();
4427
+ const checkedBinding = managedGcProtocolFileSnapshot(candidateScope, store, MANAGED_SESSION_BINDING_FILE);
4428
+ if (!managedGcProtocolFileIdentityMatches(checkedBinding.identity, input.bindingIdentity))
4429
+ throw new Error("managed_gc_protocol_binding_identity_mismatch");
4430
+ const protocolDirectory = managedGcProtocolDirectorySnapshot(
4431
+ candidateScope,
4432
+ store,
4433
+ MANAGED_INTERNAL_DIRECTORY,
4434
+ operation,
4435
+ );
4436
+ if (
4437
+ protocolDirectory.path !== input.protocolIdentity.path ||
4438
+ protocolDirectory.stat.dev.toString() !== input.protocolIdentity.dev ||
4439
+ protocolDirectory.stat.ino.toString() !== input.protocolIdentity.ino ||
4440
+ protocolDirectory.stat.mtimeNs.toString() !== input.protocolIdentity.mtimeNs ||
4441
+ protocolDirectory.stat.ctimeNs.toString() !== input.protocolIdentity.ctimeNs ||
4442
+ Number(protocolDirectory.stat.mode & 0o777n) !== input.protocolIdentity.mode ||
4443
+ protocolDirectory.names.length !== MANAGED_GC_PROTOCOL_ROLES.length ||
4444
+ MANAGED_GC_PROTOCOL_ROLES.some(role => !protocolDirectory.names.includes(role))
4445
+ )
4446
+ throw new Error("managed_gc_protocol_roles_invalid");
4447
+ managedGcReserveInventoryWork(operation, 2);
4448
+ const directories: ManagedGcProtocolDirectorySnapshot[] = [];
4449
+ const receiptHistories = new Map<string, MutableManagedGcReceiptHistory>();
4450
+ for (const role of MANAGED_GC_PROTOCOL_ROLES) {
4451
+ const directory = managedGcProtocolDirectorySnapshot(candidateScope, store, role, operation);
4452
+ managedGcReserveInventoryWork(operation, directory.names.length);
4453
+ const files: ManagedGcProtocolFileSnapshot[] = [];
4454
+ for (const name of directory.names) {
4455
+ if (!name || path.basename(name) !== name || name.includes(".jsonl"))
4456
+ throw new Error(`managed_gc_protocol_entry_invalid:${role}/${name}`);
4457
+ const relative = `${MANAGED_INTERNAL_DIRECTORY}/${role}/${name}`;
4458
+ const cleanupLike = role === MANAGED_TOMBSTONES_DIRECTORY && isManagedGcCleanupLikeName(name);
4459
+ if (cleanupLike && !isManagedGcCleanupReceiptPath(relative))
4460
+ throw new Error("managed_gc_protocol_tombstone_role_invalid");
4461
+ const pendingCleanup =
4462
+ role === MANAGED_TOMBSTONES_DIRECTORY && isManagedGcPendingCleanupReceiptPath(relative);
4463
+ let captured: ReturnType<typeof managedGcProtocolFileSnapshot>;
4464
+ try {
4465
+ if (pendingCleanup)
4466
+ validateManagedGcProtocolTombstoneFile(
4467
+ candidateScope,
4468
+ path.join(directory.path, name),
4469
+ name,
4470
+ new Uint8Array(),
4471
+ journals,
4472
+ );
4473
+ captured = managedGcProtocolFileSnapshot(candidateScope, store, relative, receiptHistories);
4474
+ if (role === MANAGED_LOCKS_DIRECTORY) {
4475
+ if (isManagedLockQuarantineName(name)) {
4476
+ if (
4477
+ captured.snapshot.bytes.byteLength !== 0 ||
4478
+ captured.identity.size !== 0 ||
4479
+ captured.identity.sha256 !== createHash("sha256").digest("hex")
4480
+ )
4481
+ throw new Error("managed_gc_protocol_lock_quarantine_invalid");
4482
+ } else {
4483
+ if (!/^(?:[a-f0-9]{64}|gc-retirement-[a-f0-9]{64})\.lock$/u.test(name))
4484
+ throw new Error("managed_gc_protocol_lock_role_invalid");
4485
+ if (!parseManagedLockRecord(captured.snapshot.bytes))
4486
+ throw new Error("managed_gc_protocol_lock_invalid");
4487
+ }
4488
+ } else if (role === MANAGED_RECEIPTS_DIRECTORY) {
4489
+ if (
4490
+ /^gc-retirement-[a-f0-9]{64}-(?:prepared|artifacts_removed|owner_retired|owner_pending-[0-9]{8,})\.json$/u.test(
4491
+ name,
4492
+ )
4493
+ ) {
4494
+ const key = name.slice(
4495
+ MANAGED_GC_RETIREMENT_PREFIX.length,
4496
+ MANAGED_GC_RETIREMENT_PREFIX.length + 64,
4497
+ );
4498
+ if (
4499
+ !journals.some(
4500
+ item =>
4501
+ item.scope.directoryPath === candidateScope.directoryPath &&
4502
+ managedGcRetirementTranscriptKey(item.receipt.transcriptPath) === key,
4503
+ )
4504
+ )
4505
+ throw new Error("task_artifact_owner_continuation_state_missing");
4506
+ } else {
4507
+ validateManagedGcProtocolMigrationReceipt(
4508
+ candidateScope,
4509
+ path.join(directory.path, name),
4510
+ name,
4511
+ captured.snapshot.bytes,
4512
+ );
4513
+ }
4514
+ } else {
4515
+ if (!pendingCleanup)
4516
+ validateManagedGcProtocolTombstoneFile(
4517
+ candidateScope,
4518
+ path.join(directory.path, name),
4519
+ name,
4520
+ captured.snapshot.bytes,
4521
+ journals,
4522
+ );
4523
+ }
4524
+ } catch (error) {
4525
+ if (isManagedGcJournalCapacityError(error)) throw error;
4526
+ const detail = error instanceof Error ? error.message : "unknown";
4527
+ throw new Error(`${detail} [${role}/${name}]`);
4528
+ }
4529
+ managedGcReserveInventoryWork(operation);
4530
+ files.push(captured.identity);
4531
+ totalFiles++;
4532
+ if (totalFiles > MANAGED_GC_PROTOCOL_MAX_ENTRIES)
4533
+ throw new Error("managed_gc_protocol_entry_limit_exceeded");
4534
+ }
4535
+ const finalDirectory = managedGcProtocolDirectorySnapshot(candidateScope, store, role, operation);
4536
+ if (
4537
+ finalDirectory.stat.dev !== directory.stat.dev ||
4538
+ finalDirectory.stat.ino !== directory.stat.ino ||
4539
+ finalDirectory.stat.mtimeNs !== directory.stat.mtimeNs ||
4540
+ finalDirectory.stat.ctimeNs !== directory.stat.ctimeNs ||
4541
+ finalDirectory.stat.mode !== directory.stat.mode ||
4542
+ finalDirectory.names.length !== directory.names.length ||
4543
+ finalDirectory.names.some((name, index) => name !== directory.names[index])
4544
+ )
4545
+ throw new Error("managed_gc_protocol_directory_changed");
4546
+ managedGcReserveInventoryWork(operation);
4547
+ directories.push({
4548
+ role,
4549
+ path: directory.path,
4550
+ dev: directory.stat.dev.toString(),
4551
+ ino: directory.stat.ino.toString(),
4552
+ mtimeNs: directory.stat.mtimeNs.toString(),
4553
+ ctimeNs: directory.stat.ctimeNs.toString(),
4554
+ mode: Number(directory.stat.mode & 0o777n),
4555
+ files,
4556
+ });
4557
+ }
4558
+ const finalProtocol = managedGcProtocolDirectorySnapshot(
4559
+ candidateScope,
4560
+ store,
4561
+ MANAGED_INTERNAL_DIRECTORY,
4562
+ operation,
4563
+ );
4564
+ if (
4565
+ finalProtocol.stat.dev !== protocolDirectory.stat.dev ||
4566
+ finalProtocol.stat.ino !== protocolDirectory.stat.ino ||
4567
+ finalProtocol.stat.mtimeNs !== protocolDirectory.stat.mtimeNs ||
4568
+ finalProtocol.stat.ctimeNs !== protocolDirectory.stat.ctimeNs ||
4569
+ finalProtocol.stat.mode !== protocolDirectory.stat.mode ||
4570
+ finalProtocol.names.length !== protocolDirectory.names.length ||
4571
+ finalProtocol.names.some((name, index) => name !== protocolDirectory.names[index])
4572
+ )
4573
+ throw new Error("managed_gc_protocol_tree_changed");
4574
+ managedGcReserveInventoryWork(operation);
4575
+ const comparisonReceiptHistories = new Map<string, MutableManagedGcReceiptHistory>();
4576
+ for (const directory of directories) {
4577
+ const relative = `${MANAGED_INTERNAL_DIRECTORY}/${directory.role}`;
4578
+ const currentDirectory = managedGcProtocolDirectorySnapshot(
4579
+ candidateScope,
4580
+ store,
4581
+ directory.role,
4582
+ operation,
4583
+ );
4584
+ const names = currentDirectory.names;
4585
+ if (
4586
+ currentDirectory.stat.dev !== BigInt(directory.dev) ||
4587
+ currentDirectory.stat.ino !== BigInt(directory.ino) ||
4588
+ currentDirectory.stat.mtimeNs !== BigInt(directory.mtimeNs) ||
4589
+ currentDirectory.stat.ctimeNs !== BigInt(directory.ctimeNs) ||
4590
+ names.length !== directory.files.length ||
4591
+ names.some((name, index) => name !== directory.files[index]?.name)
4592
+ )
4593
+ throw new Error("managed_gc_protocol_directory_changed");
4594
+ for (const expected of directory.files) {
4595
+ const actual = managedGcProtocolFileSnapshot(
4596
+ candidateScope,
4597
+ store,
4598
+ `${relative}/${expected.name}`,
4599
+ comparisonReceiptHistories,
4600
+ );
4601
+ if (!managedGcProtocolFileIdentityMatches(actual.identity, expected))
4602
+ throw new Error("managed_gc_protocol_file_changed");
4603
+ }
4604
+ managedGcReserveInventoryWork(operation);
4605
+ const current = managedGcProtocolDirectoryStat(directory.path);
4606
+ if (
4607
+ current.dev.toString() !== directory.dev ||
4608
+ current.ino.toString() !== directory.ino ||
4609
+ current.mtimeNs.toString() !== directory.mtimeNs ||
4610
+ current.ctimeNs.toString() !== directory.ctimeNs ||
4611
+ Number(current.mode & 0o777n) !== directory.mode
4612
+ )
4613
+ throw new Error("managed_gc_protocol_directory_changed");
4614
+ }
4615
+ const finalBinding = managedGcProtocolFileSnapshot(candidateScope, store, MANAGED_SESSION_BINDING_FILE);
4616
+ if (!managedGcProtocolFileIdentityMatches(finalBinding.identity, input.bindingIdentity))
4617
+ throw new Error("managed_gc_protocol_binding_changed");
4618
+ managedGcReserveInventoryWork(operation);
4619
+ snapshots.push({
4620
+ configuredRoot,
4621
+ profile,
4622
+ sessionsRoot,
4623
+ scope: scopeIdentity,
4624
+ binding: bindingIdentity,
4625
+ protocol: {
4626
+ path: protocolDirectory.path,
4627
+ dev: protocolDirectory.stat.dev.toString(),
4628
+ ino: protocolDirectory.stat.ino.toString(),
4629
+ },
4630
+ protocolMtimeNs: protocolDirectory.stat.mtimeNs.toString(),
4631
+ protocolCtimeNs: protocolDirectory.stat.ctimeNs.toString(),
4632
+ protocolMode: Number(protocolDirectory.stat.mode & 0o777n),
4633
+ directories,
4634
+ });
4635
+ });
4636
+ }
4637
+ const latestJournals = await discoverManagedGcSessionRetirementReceiptsWithOperation(
4638
+ { agentDir: scope.agentDir, sessionsRoot: scope.sessionsRoot },
4639
+ operation,
4640
+ );
4641
+ if (!deepSame(latestJournals, journals)) throw new Error("managed_gc_protocol_journal_changed");
4642
+ assertAuthority(operation);
4643
+ return snapshots;
4644
+ };
4645
+ }
4646
+
4647
+ function managedGcReceiptForPublication(
4648
+ scope: ManagedScope,
4649
+ receipt: ManagedGcSessionRetirementReceipt,
4650
+ ownerContext: TaskArtifactOwnerStorageContext,
4651
+ attempt?: number,
4652
+ history?: ManagedGcReceiptHistory,
4653
+ ): {
4654
+ readonly target: ManagedGcSessionRetirementTarget;
4655
+ readonly record: Record<string, unknown>;
4656
+ readonly parsed: ManagedGcSessionRetirementReceipt;
4657
+ } {
4658
+ validateManagedGcTranscriptPath(scope, receipt.transcriptPath);
4659
+ if (
4660
+ history &&
4661
+ (history.entryCount >= MANAGED_ARTIFACT_MAX_FILES || history.directoryEntryCount >= MANAGED_ARTIFACT_MAX_FILES)
4662
+ )
4663
+ managedGcJournalCapacity();
4664
+ const candidateRecord = managedGcReceiptRecord(scope, receipt, attempt);
4665
+ const candidateByteLength = managedGcJsonByteLength(candidateRecord);
4666
+ if (history && candidateByteLength > MANAGED_ARTIFACT_MAX_TOTAL_BYTES - history.encodedBytes)
4667
+ managedGcJournalCapacity();
4668
+ const parsed = parseManagedGcRetirementReceipt(
4669
+ candidateRecord,
4670
+ scopeDigest(scope.platform, scope.canonicalCwd),
4671
+ ownerContext,
4672
+ receipt,
4673
+ );
4674
+ const record = managedGcReceiptRecord(scope, parsed, attempt);
4675
+ const encodedByteLength = managedGcJsonByteLength(record);
4676
+ if (encodedByteLength !== candidateByteLength) throw new Error("managed_gc_receipt_encoding_mismatch");
4677
+ return { target: receipt, record, parsed };
4678
+ }
4679
+
4680
+ function publishManagedGcReceiptNoReplace(
4681
+ store: ManagedSessionDescendantStore,
4682
+ lock: ManagedStorageLock,
4683
+ relativePath: string,
4684
+ record: Record<string, unknown>,
4685
+ ): void {
4686
+ lock.assertOwned();
4687
+ store.assertBound();
4688
+ const expectedByteLength = managedGcJsonByteLength(record);
4689
+ const serializedJson = JSON.stringify(record, (_key, value: unknown) =>
4690
+ typeof value === "bigint" ? value.toString() : value,
4691
+ );
4692
+ const serialized = `${serializedJson}\n`;
4693
+ const actualByteLength = Buffer.byteLength(serialized, "utf8");
4694
+ if (actualByteLength > MANAGED_GC_RECEIPT_MAX_BYTES) managedGcReceiptCapacity();
4695
+ if (actualByteLength !== expectedByteLength) throw new Error("managed_gc_receipt_encoding_mismatch");
4696
+ const bytes = Buffer.from(serialized, "utf8");
4697
+ try {
4698
+ store.publishNoReplaceSync(relativePath, bytes);
4699
+ } catch (error) {
4700
+ if (!(error instanceof Error) || error.message !== "destination_conflict") throw error;
4701
+ const existing = readManagedGcReceiptSnapshot(store, relativePath);
4702
+ if (!existing?.bytes.equals(bytes)) throw error;
4703
+ }
4704
+ lock.assertOwned();
4705
+ const persisted = readManagedGcReceiptSnapshot(store, relativePath);
4706
+ if (!persisted?.bytes.equals(bytes)) throw new Error("durability_failed");
4707
+ store.assertBound();
4708
+ }
4709
+
4710
+ /** Atomically publish one journal state under the managed scope lock; no GC effects are activated here. */
4711
+ export async function publishManagedGcSessionRetirementReceipt(
4712
+ scope: ManagedScope,
4713
+ receipt: ManagedGcSessionRetirementReceipt,
4714
+ ): Promise<ManagedGcSessionRetirementReceipt> {
4715
+ validateManagedGcTranscriptPath(scope, receipt.transcriptPath);
4716
+ return withManagedGcRetirementJournal(scope, receipt.transcriptPath, (trusted, store, lock) => {
4717
+ const operation: ManagedGcInventoryOperation = { kind: "leased-journal", lock };
4718
+ const currentRead = readManagedGcSessionRetirementReceiptUnlocked(
4719
+ scope,
4720
+ receipt.transcriptPath,
4721
+ trusted,
4722
+ store,
4723
+ operation,
4724
+ );
4725
+ const current = currentRead.receipt;
4726
+ const target = receipt;
4727
+ let attempt: number | undefined;
4728
+ if (!current) {
4729
+ if (receipt.state !== "prepared") throw new Error("task_artifact_owner_continuation_state_order_invalid");
4730
+ assertManagedGcTranscriptUnchangedOrAbsent(scope, target, false);
4731
+ const capturedEvidence = captureTaskArtifactOwnerDeletionEvidence(
4732
+ trusted.ownerContext,
4733
+ target.sessionId,
4734
+ target.taskArtifactOwnerLocator,
4735
+ );
4736
+ if (
4737
+ !capturedEvidence ||
4738
+ !deepSame(
4739
+ capturedEvidence,
4740
+ parseTaskArtifactOwnerDeletionEvidence(receipt.taskArtifactOwnerDeletionEvidence),
4741
+ )
4742
+ )
4743
+ throw new Error("task_artifact_owner_continuation_evidence_mismatch");
4744
+ } else {
4745
+ const prepared = currentRead.prepared;
4746
+ if (!prepared) throw new Error("task_artifact_owner_continuation_state_missing");
4747
+ assertSameManagedGcImmutableAuthority(prepared, receipt);
4748
+ assertManagedGcTranscriptUnchangedOrAbsent(scope, prepared, true);
4749
+ if (receipt.state === "prepared") {
4750
+ managedGcReceiptForPublication(scope, receipt, trusted.ownerContext);
4751
+ return current;
4752
+ }
4753
+ if (receipt.state === "artifacts_removed" && current.state !== "prepared") {
4754
+ managedGcReceiptForPublication(scope, receipt, trusted.ownerContext);
4755
+ return current;
4756
+ }
4757
+ if (receipt.state === "owner_pending") {
4758
+ if (current.state !== "artifacts_removed" && current.state !== "owner_pending")
4759
+ throw new Error("task_artifact_owner_continuation_state_order_invalid");
4760
+ attempt = current.state === "owner_pending" ? (current.ownerRetirementAttempt ?? 0) + 1 : 1;
4761
+ if (current.state === "owner_pending") {
4762
+ const previous = current.taskArtifactOwnerRetirementOutcome;
4763
+ const next = receipt.taskArtifactOwnerRetirementOutcome;
4764
+ if (
4765
+ !previous ||
4766
+ previous.kind === "completed" ||
4767
+ !next ||
4768
+ next.kind === "completed" ||
4769
+ !continuationExtendsPrevious(previous.continuation, next.continuation)
4770
+ )
4771
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
4772
+ }
4773
+ } else if (receipt.state === "owner_retired") {
4774
+ if (current.state === "owner_retired") {
4775
+ const existing = current.taskArtifactOwnerRetirementOutcome;
4776
+ const candidate = managedGcReceiptForPublication(scope, receipt, trusted.ownerContext).parsed;
4777
+ if (!existing || !deepSame(existing, candidate.taskArtifactOwnerRetirementOutcome))
4778
+ throw new Error("task_artifact_owner_continuation_outcome_mismatch");
4779
+ return current;
4780
+ }
4781
+ if (current.state !== "artifacts_removed" && current.state !== "owner_pending")
4782
+ throw new Error("task_artifact_owner_continuation_state_order_invalid");
4783
+ } else if (receipt.state !== "artifacts_removed") {
4784
+ throw new Error("task_artifact_owner_continuation_state_order_invalid");
4785
+ }
4786
+ }
4787
+ assertManagedGcReceiptHistoryDirectoryUnchanged(scope, store, currentRead.history, operation);
4788
+ const { record, parsed } = managedGcReceiptForPublication(
4789
+ scope,
4790
+ receipt,
4791
+ trusted.ownerContext,
4792
+ attempt,
4793
+ currentRead.history,
4794
+ );
4795
+ if (receipt.state === "owner_retired") {
4796
+ const outcome = parsed.taskArtifactOwnerRetirementOutcome;
4797
+ if (outcome?.kind !== "completed") throw new Error("task_artifact_owner_continuation_outcome_mismatch");
4798
+ verifyTaskArtifactOwnerPhysicalRetirement(
4799
+ trusted.ownerContext,
4800
+ parsed.taskArtifactOwnerDeletionEvidence,
4801
+ outcome,
4802
+ );
4803
+ }
4804
+ const relativePath = managedGcRetirementReceiptRelativePath(receipt.transcriptPath, receipt.state, attempt);
4805
+ publishManagedGcReceiptNoReplace(store, lock, relativePath, record);
4806
+ const persistedRead = readManagedGcSessionRetirementReceiptUnlocked(
4807
+ scope,
4808
+ receipt.transcriptPath,
4809
+ trusted,
4810
+ store,
4811
+ operation,
4812
+ );
4813
+ const persisted = persistedRead.receipt;
4814
+ if (!persisted) throw new Error("durability_failed");
4815
+ const expectedState = receipt.state;
4816
+ if (
4817
+ persisted.state !== expectedState ||
4818
+ (expectedState === "owner_pending" && persisted.ownerRetirementAttempt !== attempt) ||
4819
+ !deepSame(persisted.taskArtifactOwnerDeletionEvidence, parsed.taskArtifactOwnerDeletionEvidence)
4820
+ )
4821
+ throw new Error("durability_failed");
4822
+ return persisted;
4823
+ });
4824
+ }
4825
+ function retiredTargets(scope: ManagedScope, pathname: string): readonly RetiredTarget[] | undefined {
4826
+ let value: unknown;
4827
+ try {
4828
+ value = JSON.parse(captureManagedFileNoFollow(pathname).bytes.toString("utf8"));
4829
+ const targets = parseRetiredTargetRecord(scope, value);
4830
+ if (!targets) assertLegacyOwnerFreeRecord(value);
4831
+ return targets;
4832
+ } catch (error) {
4833
+ assertLegacyOwnerFreeRecord(value);
4834
+ if (isOwnerBoundaryFailure(error)) throw error;
4835
+ if (error instanceof SyntaxError) throw new Error("durability_failed", { cause: error });
4836
+ return undefined;
4837
+ }
4838
+ }
4839
+
4840
+ const OWNER_CONSUMER_UNAVAILABLE = "task_artifact_owner_legacy_scope_unsupported";
4841
+
4842
+ function isOwnerBoundaryFailure(error: unknown): boolean {
4843
+ return error instanceof Error && error.message.startsWith("task_artifact_owner_");
4844
+ }
4845
+
4846
+ function hasOwnerClaims(value: unknown): boolean {
4847
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
4848
+ const record = value as Record<string, unknown>;
4849
+ return (
4850
+ Object.keys(record).some(key => key.startsWith("taskArtifactOwner")) ||
4851
+ (record.target !== undefined && hasOwnerClaims(record.target)) ||
4852
+ (Array.isArray(record.targets) && record.targets.some(hasOwnerClaims))
4853
+ );
4854
+ }
4855
+
4856
+ function assertLegacyOwnerFreeRecord(value: unknown): void {
4857
+ if (hasOwnerClaims(value)) throw new Error(OWNER_CONSUMER_UNAVAILABLE);
4858
+ }
4859
+
4860
+ function assertOwnerTranscriptLocator(
4861
+ target: Pick<
4862
+ VerifiedSessionDeleteTarget,
4863
+ "transcriptPath" | "sessionId" | "transcriptIdentity" | "detachedTranscriptPath"
4864
+ >,
4865
+ evidence?: TaskArtifactOwnerDeletionEvidence,
4866
+ ): void {
4867
+ const pathname = target.detachedTranscriptPath ?? target.transcriptPath;
4868
+ let snapshot: SessionStorageSnapshot;
4869
+ try {
4870
+ snapshot = new FileSessionStorage().readSnapshotSync(pathname);
4871
+ } catch (error) {
4872
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
4873
+ throw error;
4874
+ }
4875
+ // Descriptor-bound owner classification is data, not authorization; storage retains its exact identity fence.
4876
+ const locator = taskArtifactOwnerLocatorFromTranscriptBytes(snapshot.bytes, target.sessionId);
4877
+ if ((locator && !evidence) || (evidence && !locator)) throw new Error("task_artifact_owner_locator_missing");
4878
+ if (locator && !deepSame(locator, evidence?.locator))
4879
+ throw new Error("task_artifact_owner_continuation_evidence_mismatch");
4880
+ }
4881
+
4882
+ function parseRetiredTargetRecord(scope: ManagedScope, value: unknown): readonly RetiredTarget[] | undefined {
4883
+ if (!value || typeof value !== "object") return undefined;
4884
+ if (Object.keys(value).some(key => key.startsWith("taskArtifactOwner"))) throw new Error(OWNER_CONSUMER_UNAVAILABLE);
4885
+ const record = value as { schemaVersion?: unknown; state?: unknown; scope?: unknown; targets?: unknown };
4886
+ if (
4887
+ record.schemaVersion !== 2 ||
4888
+ record.state !== "retired" ||
4889
+ record.scope !== scopeDigest(scope.platform, scope.canonicalCwd) ||
4890
+ !Array.isArray(record.targets)
4891
+ )
4892
+ return undefined;
4893
+ const targets: RetiredTarget[] = [];
4894
+ for (const target of record.targets) {
4895
+ if (!target || typeof target !== "object" || Array.isArray(target)) return undefined;
4896
+ const item = target as Record<string, unknown>;
4897
+ if (
4898
+ Object.keys(item).some(
4899
+ key => key.startsWith("taskArtifactOwner") && key !== "taskArtifactOwnerDeletionEvidence",
4900
+ )
4901
+ )
4902
+ throw new Error(OWNER_CONSUMER_UNAVAILABLE);
4903
+ const identity = item.identity;
4904
+ if (!identity || typeof identity !== "object" || Array.isArray(identity)) return undefined;
4905
+ const fields = identity as Record<string, unknown>;
4906
+ if (
4907
+ typeof item.path !== "string" ||
4908
+ typeof item.sessionId !== "string" ||
4909
+ typeof item.cwd !== "string" ||
4910
+ !pathIsWithin(scope.sessionsRoot, item.path) ||
4911
+ (item.provenance !== undefined && item.provenance !== "v2" && item.provenance !== "legacy") ||
4912
+ typeof fields.canonicalPath !== "string" ||
4913
+ typeof fields.dev !== "string" ||
4914
+ typeof fields.ino !== "string" ||
4915
+ typeof fields.size !== "number" ||
4916
+ typeof fields.mtimeMs !== "number" ||
4917
+ typeof fields.mtimeNs !== "string" ||
4918
+ typeof fields.sha256 !== "string"
4919
+ )
4920
+ return undefined;
4921
+ const provenance =
4922
+ item.provenance === "v2" || item.provenance === "legacy"
4923
+ ? item.provenance
4924
+ : path.dirname(item.path) === scope.directoryPath
4925
+ ? "v2"
4926
+ : "legacy";
4927
+ let taskArtifactOwnerDeletionEvidence: TaskArtifactOwnerDeletionEvidence | undefined;
4928
+ if (Object.hasOwn(item, "taskArtifactOwnerDeletionEvidence")) {
4929
+ try {
4930
+ taskArtifactOwnerDeletionEvidence = parseTaskArtifactOwnerDeletionEvidence(
4931
+ item.taskArtifactOwnerDeletionEvidence,
4932
+ );
4933
+ if (taskArtifactOwnerDeletionEvidence.sessionId !== item.sessionId)
4934
+ throw new Error("owner_session_mismatch");
4935
+ } catch (cause) {
4936
+ throw new Error("task_artifact_owner_legacy_scope_unsupported", { cause });
4937
+ }
4938
+ }
4939
+ targets.push({
4940
+ ...(taskArtifactOwnerDeletionEvidence ? { taskArtifactOwnerDeletionEvidence } : {}),
4941
+ path: item.path,
4942
+ sessionId: item.sessionId,
4943
+ cwd: item.cwd,
4944
+ provenance,
4945
+ migrationState: provenance === "v2" ? "native_v2" : "legacy_unmigrated",
4946
+ identity: {
4947
+ canonicalPath: fields.canonicalPath,
4948
+ dev: BigInt(fields.dev),
4949
+ ino: BigInt(fields.ino),
4950
+ size: fields.size,
4951
+ mtimeMs: fields.mtimeMs,
4952
+ mtimeNs: BigInt(fields.mtimeNs),
4953
+ sha256: fields.sha256,
4954
+ sessionId: item.sessionId,
4955
+ },
4956
+ });
4957
+ }
4958
+ return targets;
4959
+ }
4960
+
4961
+ /**
4962
+ * Append-only cleanup state machine:
4963
+ * `pending(N)` durably authorizes only its planned quarantine names before detach;
4964
+ * a returned partial result appends `pending(N + 1)` with the observed detached
4965
+ * identity/path. Restart accepts only a contiguous, target-bound sequence.
4966
+ */
4967
+ type RetainedArtifactsRootReceipt = {
4968
+ path: string;
4969
+ identity: SessionStorageFileIdentity;
4970
+ tree: NativeDirectoryTreeSnapshot;
4971
+ };
4972
+
4973
+ function retainedArtifactsRootReceipt(receipt: CleanupReceipt): RetainedArtifactsRootReceipt | undefined {
4974
+ const pathname = receipt.detachedArtifactsPath ?? deterministicRemovalRoot(receipt.plannedArtifactsPath);
4975
+ if (!fs.existsSync(pathname)) return undefined;
4976
+ if (!isQuarantinePath(receipt.target, pathname) || !receipt.expectedArtifactsIdentity)
4977
+ throw new Error("durability_failed");
4978
+ const identity = artifactIdentityAt(pathname);
4979
+ if (
4980
+ !identity ||
4981
+ identity.dev !== receipt.expectedArtifactsIdentity.dev ||
4982
+ identity.ino !== receipt.expectedArtifactsIdentity.ino
4983
+ )
4984
+ throw new Error("durability_failed");
4985
+ const tree = snapshotArtifactTree(pathname);
4986
+ if (!artifactTreePayloadAbsent(tree)) throw new Error("durability_failed");
4987
+ return { path: pathname, identity, tree };
4988
+ }
4989
+
4990
+ function retainedArtifactsRootMatches(record: Record<string, unknown>): boolean {
4991
+ if (record.retainedArtifactsRoot === undefined) return true;
4992
+ if (!record.retainedArtifactsRoot || typeof record.retainedArtifactsRoot !== "object")
4993
+ throw new Error("durability_failed");
4994
+ const retained = record.retainedArtifactsRoot as Record<string, unknown>;
4995
+ const identity = retained.identity;
4996
+ const tree = artifactTreeSnapshot(retained.tree);
4997
+ const expectedRetainedPath =
4998
+ typeof record.detachedArtifactsPath === "string"
4999
+ ? record.detachedArtifactsPath
5000
+ : typeof record.plannedArtifactsPath === "string"
5001
+ ? deterministicRemovalRoot(record.plannedArtifactsPath)
5002
+ : undefined;
5003
+ if (
5004
+ !expectedRetainedPath ||
5005
+ retained.path !== expectedRetainedPath ||
5006
+ record.artifactsPayloadDurable !== true ||
5007
+ !identity ||
5008
+ typeof identity !== "object" ||
5009
+ Array.isArray(identity) ||
5010
+ !tree ||
5011
+ !artifactTreePayloadAbsent(tree)
5012
+ )
5013
+ throw new Error("durability_failed");
5014
+ const artifactIdentity = identity as Record<string, unknown>;
5015
+ if (
5016
+ typeof artifactIdentity.dev !== "string" ||
5017
+ typeof artifactIdentity.ino !== "string" ||
5018
+ typeof artifactIdentity.size !== "number" ||
5019
+ typeof artifactIdentity.mtimeNs !== "string" ||
5020
+ typeof artifactIdentity.sha256 !== "string"
5021
+ )
5022
+ throw new Error("durability_failed");
5023
+ if (!fs.existsSync(retained.path)) return true;
5024
+ const observed = artifactIdentityAt(retained.path);
5025
+ if (
5026
+ !observed ||
5027
+ observed.dev !== BigInt(artifactIdentity.dev) ||
5028
+ observed.ino !== BigInt(artifactIdentity.ino) ||
5029
+ !artifactTreePayloadAbsent(snapshotArtifactTree(retained.path))
5030
+ )
5031
+ throw new Error("durability_failed");
5032
+ return true;
5033
+ }
5034
+
5035
+ type CleanupReceipt = {
5036
+ attempt: number;
5037
+ target: RetiredTarget;
5038
+ taskArtifactOwnerTranscriptDeleted?: true;
5039
+ expectedArtifactsIdentity?: SessionStorageFileIdentity;
5040
+ expectedArtifactsTree?: NativeDirectoryTreeSnapshot;
5041
+ artifactsPayloadDurable?: true;
5042
+ artifactsRemovedAttempt?: number;
5043
+ detachedArtifactsPath?: string;
5044
+ detachedTranscriptPath?: string;
5045
+ transcriptPayloadDurable?: true;
5046
+ retainedArtifactsSuccessorPath?: string;
5047
+ retainedArtifactsPlaceholderPath?: string;
5048
+ retainedArtifactsUnknownPath?: string;
5049
+ retainedTranscriptSuccessorPath?: string;
5050
+ retainedTranscriptPlaceholderPath?: string;
5051
+ retainedTranscriptUnknownPath?: string;
5052
+
5053
+ plannedArtifactsPath: string;
5054
+ plannedTranscriptPath: string;
5055
+ };
5056
+
5057
+ function cleanupReceiptPath(
5058
+ tombstone: string,
5059
+ target: RetiredTarget,
5060
+ state: "pending" | "artifacts_removed" | "completed",
5061
+ attempt: number,
5062
+ ): string {
5063
+ return path.join(
5064
+ path.dirname(tombstone),
5065
+ `${path.basename(tombstone, ".json")}.${stableOperationName(target)}.cleanup-${state}-${attempt}.json`,
5066
+ );
5067
+ }
5068
+
5069
+ function cleanupReceipt(scope: ManagedScope, tombstone: string, receipt: CleanupReceipt): Record<string, unknown> {
5070
+ return {
5071
+ schemaVersion: 2,
5072
+ state: "cleanup_pending",
5073
+ scope: scopeDigest(scope.platform, scope.canonicalCwd),
5074
+ tombstone,
5075
+ attempt: receipt.attempt,
5076
+ target: {
5077
+ path: receipt.target.path,
5078
+ sessionId: receipt.target.sessionId,
5079
+ cwd: receipt.target.cwd,
5080
+ identity: receipt.target.identity,
5081
+ },
5082
+ ...(receipt.taskArtifactOwnerTranscriptDeleted ? { taskArtifactOwnerTranscriptDeleted: true } : {}),
5083
+ ...(receipt.expectedArtifactsIdentity ? { expectedArtifactsIdentity: receipt.expectedArtifactsIdentity } : {}),
5084
+ ...(receipt.expectedArtifactsTree ? { expectedArtifactsTree: receipt.expectedArtifactsTree } : {}),
5085
+ ...(receipt.artifactsPayloadDurable === true ? { artifactsPayloadDurable: true } : {}),
5086
+ ...(receipt.artifactsRemovedAttempt !== undefined
5087
+ ? { artifactsRemovedAttempt: receipt.artifactsRemovedAttempt }
5088
+ : {}),
5089
+ ...(receipt.detachedArtifactsPath ? { detachedArtifactsPath: receipt.detachedArtifactsPath } : {}),
5090
+ ...(receipt.detachedTranscriptPath ? { detachedTranscriptPath: receipt.detachedTranscriptPath } : {}),
5091
+ ...(receipt.transcriptPayloadDurable === true ? { transcriptPayloadDurable: true } : {}),
5092
+ ...(receipt.retainedArtifactsSuccessorPath
5093
+ ? { retainedArtifactsSuccessorPath: receipt.retainedArtifactsSuccessorPath }
5094
+ : {}),
5095
+ ...(receipt.retainedArtifactsPlaceholderPath
5096
+ ? { retainedArtifactsPlaceholderPath: receipt.retainedArtifactsPlaceholderPath }
5097
+ : {}),
5098
+ ...(receipt.retainedArtifactsUnknownPath
5099
+ ? { retainedArtifactsUnknownPath: receipt.retainedArtifactsUnknownPath }
5100
+ : {}),
5101
+ ...(receipt.retainedTranscriptSuccessorPath
5102
+ ? { retainedTranscriptSuccessorPath: receipt.retainedTranscriptSuccessorPath }
5103
+ : {}),
5104
+ ...(receipt.retainedTranscriptPlaceholderPath
5105
+ ? { retainedTranscriptPlaceholderPath: receipt.retainedTranscriptPlaceholderPath }
5106
+ : {}),
5107
+ ...(receipt.retainedTranscriptUnknownPath
5108
+ ? { retainedTranscriptUnknownPath: receipt.retainedTranscriptUnknownPath }
5109
+ : {}),
5110
+ plannedArtifactsPath: receipt.plannedArtifactsPath,
5111
+ plannedTranscriptPath: receipt.plannedTranscriptPath,
5112
+ };
5113
+ }
5114
+
5115
+ type CleanupArtifactsRemovedEvidence = { retainedArtifactsRootPath?: string };
5116
+
5117
+ function cleanupArtifactsRemovedEvidence(
5118
+ scope: ManagedScope,
5119
+ tombstone: string,
5120
+ target: RetiredTarget,
5121
+ attempt: number,
5122
+ ): CleanupArtifactsRemovedEvidence | undefined {
5123
+ try {
5124
+ const value: unknown = JSON.parse(
5125
+ captureManagedFileNoFollow(cleanupReceiptPath(tombstone, target, "artifacts_removed", attempt)).bytes.toString(
2308
5126
  "utf8",
2309
5127
  ),
2310
5128
  );
@@ -2875,133 +5693,231 @@ function artifactTreeSnapshot(value: unknown): NativeDirectoryTreeSnapshot | und
2875
5693
  return snapshot as unknown as NativeDirectoryTreeSnapshot;
2876
5694
  }
2877
5695
 
5696
+ const CLEANUP_RECEIPT_REPLAY_MAX_BYTES = Math.min(
5697
+ MANAGED_ARTIFACT_MAX_FILE_BYTES,
5698
+ MANAGED_SESSION_READ_RANGE_MAX_BYTES,
5699
+ );
5700
+ const CLEANUP_RECEIPT_REPLAY_MAX_WORK = MANAGED_ARTIFACT_MAX_FILES * 2;
5701
+ const CLEANUP_RECEIPT_REPLAY_CHUNK_BYTES = 64 * 1024;
5702
+
5703
+ interface CleanupReceiptReplayBudget {
5704
+ workUnits: number;
5705
+ directoryEntries: number;
5706
+ receiptReads: number;
5707
+ readBytes: number;
5708
+ }
5709
+
5710
+ function cleanupReceiptReplayCapacity(): never {
5711
+ throw new Error("managed_gc_journal_capacity_exceeded");
5712
+ }
5713
+
5714
+ function chargeCleanupReceiptReplayWork(budget: CleanupReceiptReplayBudget, amount = 1): void {
5715
+ if (!Number.isSafeInteger(amount) || amount < 0 || budget.workUnits > CLEANUP_RECEIPT_REPLAY_MAX_WORK - amount)
5716
+ cleanupReceiptReplayCapacity();
5717
+ budget.workUnits += amount;
5718
+ }
5719
+
5720
+ function admitCleanupReceiptReplayEntry(budget: CleanupReceiptReplayBudget): void {
5721
+ if (budget.directoryEntries >= MANAGED_ARTIFACT_MAX_FILES) cleanupReceiptReplayCapacity();
5722
+ chargeCleanupReceiptReplayWork(budget);
5723
+ budget.directoryEntries++;
5724
+ }
5725
+
5726
+ function admitCleanupReceiptReplayRead(budget: CleanupReceiptReplayBudget, size: number): void {
5727
+ if (
5728
+ !Number.isSafeInteger(size) ||
5729
+ size < 0 ||
5730
+ size > CLEANUP_RECEIPT_REPLAY_MAX_BYTES ||
5731
+ budget.receiptReads >= MANAGED_ARTIFACT_MAX_FILES ||
5732
+ budget.readBytes > MANAGED_ARTIFACT_MAX_TOTAL_BYTES - size
5733
+ )
5734
+ cleanupReceiptReplayCapacity();
5735
+ chargeCleanupReceiptReplayWork(budget, Math.max(1, Math.ceil(size / CLEANUP_RECEIPT_REPLAY_CHUNK_BYTES)));
5736
+ budget.receiptReads++;
5737
+ budget.readBytes += size;
5738
+ }
5739
+
2878
5740
  function pendingCleanupReceipt(
2879
5741
  scope: ManagedScope,
2880
5742
  tombstone: string,
2881
5743
  target: RetiredTarget,
2882
5744
  ): CleanupReceipt | undefined {
5745
+ const budget: CleanupReceiptReplayBudget = { workUnits: 0, directoryEntries: 0, receiptReads: 0, readBytes: 0 };
2883
5746
  try {
2884
5747
  const prefix = `${path.basename(tombstone, ".json")}.${stableOperationName(target)}.cleanup-pending-`;
2885
- const records = fs
2886
- .readdirSync(path.dirname(tombstone))
2887
- .filter(name => name.startsWith(prefix) && name.endsWith(".json"))
2888
- .map(
2889
- name =>
2890
- JSON.parse(
2891
- captureManagedFileNoFollow(path.join(path.dirname(tombstone), name)).bytes.toString("utf8"),
2892
- ) as unknown,
2893
- )
2894
- .filter(
2895
- (value): value is Record<string, unknown> => !!value && typeof value === "object" && !Array.isArray(value),
2896
- )
2897
- .sort((left, right) => Number(left.attempt) - Number(right.attempt));
5748
+ const directory = fs.opendirSync(path.dirname(tombstone));
5749
+ const records: Array<{ readonly name: string; readonly attempt: number }> = [];
5750
+ try {
5751
+ for (;;) {
5752
+ const entry = directory.readSync();
5753
+ if (!entry) break;
5754
+ admitCleanupReceiptReplayEntry(budget);
5755
+ if (!entry.name.startsWith(prefix) || !entry.name.endsWith(".json")) continue;
5756
+ const suffix = entry.name.slice(prefix.length, -".json".length);
5757
+ if (!/^[1-9][0-9]*$/u.test(suffix)) throw new Error("durability_failed");
5758
+ const attempt = Number(suffix);
5759
+ if (!Number.isSafeInteger(attempt) || String(attempt) !== suffix) throw new Error("durability_failed");
5760
+ if (records.length >= MANAGED_ARTIFACT_MAX_FILES) cleanupReceiptReplayCapacity();
5761
+ records.push({ name: entry.name, attempt });
5762
+ }
5763
+ } finally {
5764
+ directory.closeSync();
5765
+ }
5766
+ records.sort((left, right) => left.attempt - right.attempt);
2898
5767
  let latest: CleanupReceipt | undefined;
2899
5768
  const plannedPaths = new Set<string>();
2900
- for (const record of records) {
2901
- const attempt = record.attempt;
2902
- const recorded = record.target as Record<string, unknown> | undefined;
2903
- const identity = recorded?.identity as Record<string, unknown> | undefined;
2904
- if (
2905
- record.schemaVersion !== 2 ||
2906
- record.state !== "cleanup_pending" ||
2907
- record.scope !== scopeDigest(scope.platform, scope.canonicalCwd) ||
2908
- record.tombstone !== tombstone ||
2909
- typeof attempt !== "number" ||
2910
- !Number.isSafeInteger(attempt) ||
2911
- attempt !== (latest?.attempt ?? 0) + 1 ||
2912
- recorded?.path !== target.path ||
2913
- recorded.sessionId !== target.sessionId ||
2914
- recorded.cwd !== target.cwd ||
2915
- identity?.dev !== String(target.identity.dev) ||
2916
- identity.ino !== String(target.identity.ino) ||
2917
- identity.size !== target.identity.size ||
2918
- identity.mtimeNs !== String(target.identity.mtimeNs) ||
2919
- identity.sha256 !== target.identity.sha256 ||
2920
- !isQuarantinePath(target, record.plannedArtifactsPath) ||
2921
- !isQuarantinePath(target, record.plannedTranscriptPath) ||
2922
- record.plannedArtifactsPath === record.plannedTranscriptPath ||
2923
- plannedPaths.has(record.plannedArtifactsPath as string) ||
2924
- plannedPaths.has(record.plannedTranscriptPath as string) ||
2925
- (record.detachedArtifactsPath !== undefined && !isQuarantinePath(target, record.detachedArtifactsPath)) ||
2926
- (record.detachedTranscriptPath !== undefined && !isQuarantinePath(target, record.detachedTranscriptPath)) ||
2927
- (record.retainedArtifactsSuccessorPath !== undefined &&
2928
- !isRetainedNativePath(target, record.retainedArtifactsSuccessorPath)) ||
2929
- (record.retainedArtifactsPlaceholderPath !== undefined &&
2930
- !isRetainedNativePath(target, record.retainedArtifactsPlaceholderPath)) ||
2931
- (record.retainedArtifactsUnknownPath !== undefined &&
2932
- !isRetainedNativePath(target, record.retainedArtifactsUnknownPath)) ||
2933
- (record.retainedTranscriptSuccessorPath !== undefined &&
2934
- !isRetainedNativePath(target, record.retainedTranscriptSuccessorPath)) ||
2935
- (record.retainedTranscriptPlaceholderPath !== undefined &&
2936
- !isRetainedNativePath(target, record.retainedTranscriptPlaceholderPath)) ||
2937
- (record.retainedTranscriptUnknownPath !== undefined &&
2938
- !isRetainedNativePath(target, record.retainedTranscriptUnknownPath)) ||
2939
- (record.artifactsPayloadDurable !== undefined && record.artifactsPayloadDurable !== true) ||
2940
- (record.artifactsRemovedAttempt !== undefined &&
2941
- (typeof record.artifactsRemovedAttempt !== "number" ||
2942
- !Number.isSafeInteger(record.artifactsRemovedAttempt) ||
2943
- record.artifactsRemovedAttempt < 1 ||
2944
- record.artifactsRemovedAttempt > (attempt as number))) ||
2945
- (record.transcriptPayloadDurable !== undefined && record.transcriptPayloadDurable !== true)
2946
- )
2947
- throw new Error("durability_failed");
2948
- const artifact = record.expectedArtifactsIdentity as Record<string, unknown> | undefined;
2949
- const expectedArtifactsIdentity = artifact
2950
- ? typeof artifact.dev === "string" &&
2951
- typeof artifact.ino === "string" &&
2952
- typeof artifact.size === "number" &&
2953
- typeof artifact.mtimeNs === "string" &&
2954
- typeof artifact.sha256 === "string"
2955
- ? {
2956
- dev: BigInt(artifact.dev),
2957
- ino: BigInt(artifact.ino),
2958
- size: artifact.size,
2959
- mtimeNs: BigInt(artifact.mtimeNs),
2960
- sha256: artifact.sha256,
2961
- }
2962
- : undefined
2963
- : undefined;
2964
- if (artifact && !expectedArtifactsIdentity) throw new Error("durability_failed");
2965
- const expectedArtifactsTree =
2966
- record.expectedArtifactsTree === undefined ? undefined : artifactTreeSnapshot(record.expectedArtifactsTree);
2967
- if (record.expectedArtifactsTree !== undefined && !expectedArtifactsTree) throw new Error("durability_failed");
5769
+ for (const { name, attempt } of records) {
5770
+ let snapshot: ManagedFileSnapshot;
5771
+ try {
5772
+ snapshot = captureManagedFileNoFollowBounded(
5773
+ path.join(path.dirname(tombstone), name),
5774
+ CLEANUP_RECEIPT_REPLAY_MAX_BYTES,
5775
+ size => admitCleanupReceiptReplayRead(budget, size),
5776
+ );
5777
+ } catch (error) {
5778
+ if (error instanceof Error && error.message === "artifact_capacity_exceeded")
5779
+ cleanupReceiptReplayCapacity();
5780
+ throw error;
5781
+ }
5782
+ const value: unknown = JSON.parse(snapshot.bytes.toString("utf8"));
5783
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("durability_failed");
5784
+ latest = parseCleanupReceiptRecord(
5785
+ scope,
5786
+ tombstone,
5787
+ target,
5788
+ value as Record<string, unknown>,
5789
+ attempt,
5790
+ latest,
5791
+ plannedPaths,
5792
+ );
5793
+ }
5794
+ return latest;
5795
+ } catch (error) {
5796
+ if (
5797
+ (error as Error).message === "durability_failed" ||
5798
+ (error as Error).message === "managed_gc_journal_capacity_exceeded" ||
5799
+ isOwnerBoundaryFailure(error)
5800
+ )
5801
+ throw error;
5802
+ return undefined;
5803
+ }
5804
+ }
5805
+
5806
+ function parseCleanupReceiptRecord(
5807
+ scope: ManagedScope,
5808
+ tombstone: string,
5809
+ target: RetiredTarget,
5810
+ record: Record<string, unknown>,
5811
+ filenameAttempt: number,
5812
+ latest: CleanupReceipt | undefined,
5813
+ plannedPaths: Set<string>,
5814
+ ): CleanupReceipt {
5815
+ if (
5816
+ Object.keys(record).some(
5817
+ key => key.startsWith("taskArtifactOwner") && key !== "taskArtifactOwnerTranscriptDeleted",
5818
+ )
5819
+ )
5820
+ throw new Error(OWNER_CONSUMER_UNAVAILABLE);
5821
+ const attempt = record.attempt;
5822
+ const recorded = record.target as Record<string, unknown> | undefined;
5823
+ const identity = recorded?.identity as Record<string, unknown> | undefined;
5824
+ if (
5825
+ record.schemaVersion !== 2 ||
5826
+ record.state !== "cleanup_pending" ||
5827
+ record.scope !== scopeDigest(scope.platform, scope.canonicalCwd) ||
5828
+ record.tombstone !== tombstone ||
5829
+ typeof attempt !== "number" ||
5830
+ !Number.isSafeInteger(attempt) ||
5831
+ attempt !== filenameAttempt ||
5832
+ attempt !== (latest?.attempt ?? 0) + 1 ||
5833
+ recorded?.path !== target.path ||
5834
+ recorded.sessionId !== target.sessionId ||
5835
+ recorded.cwd !== target.cwd ||
5836
+ identity?.dev !== String(target.identity.dev) ||
5837
+ identity.ino !== String(target.identity.ino) ||
5838
+ identity.size !== target.identity.size ||
5839
+ identity.mtimeNs !== String(target.identity.mtimeNs) ||
5840
+ identity.sha256 !== target.identity.sha256 ||
5841
+ !isQuarantinePath(target, record.plannedArtifactsPath) ||
5842
+ !isQuarantinePath(target, record.plannedTranscriptPath) ||
5843
+ record.plannedArtifactsPath === record.plannedTranscriptPath ||
5844
+ plannedPaths.has(record.plannedArtifactsPath as string) ||
5845
+ plannedPaths.has(record.plannedTranscriptPath as string) ||
5846
+ (record.detachedArtifactsPath !== undefined && !isQuarantinePath(target, record.detachedArtifactsPath)) ||
5847
+ (record.detachedTranscriptPath !== undefined && !isQuarantinePath(target, record.detachedTranscriptPath)) ||
5848
+ (record.retainedArtifactsSuccessorPath !== undefined &&
5849
+ !isRetainedNativePath(target, record.retainedArtifactsSuccessorPath)) ||
5850
+ (record.retainedArtifactsPlaceholderPath !== undefined &&
5851
+ !isRetainedNativePath(target, record.retainedArtifactsPlaceholderPath)) ||
5852
+ (record.retainedArtifactsUnknownPath !== undefined &&
5853
+ !isRetainedNativePath(target, record.retainedArtifactsUnknownPath)) ||
5854
+ (record.retainedTranscriptSuccessorPath !== undefined &&
5855
+ !isRetainedNativePath(target, record.retainedTranscriptSuccessorPath)) ||
5856
+ (record.retainedTranscriptPlaceholderPath !== undefined &&
5857
+ !isRetainedNativePath(target, record.retainedTranscriptPlaceholderPath)) ||
5858
+ (record.retainedTranscriptUnknownPath !== undefined &&
5859
+ !isRetainedNativePath(target, record.retainedTranscriptUnknownPath)) ||
5860
+ (record.artifactsPayloadDurable !== undefined && record.artifactsPayloadDurable !== true) ||
5861
+ (record.artifactsRemovedAttempt !== undefined &&
5862
+ (typeof record.artifactsRemovedAttempt !== "number" ||
5863
+ !Number.isSafeInteger(record.artifactsRemovedAttempt) ||
5864
+ record.artifactsRemovedAttempt < 1 ||
5865
+ record.artifactsRemovedAttempt > (attempt as number))) ||
5866
+ (record.transcriptPayloadDurable !== undefined && record.transcriptPayloadDurable !== true) ||
5867
+ (record.taskArtifactOwnerTranscriptDeleted !== undefined &&
5868
+ (record.taskArtifactOwnerTranscriptDeleted !== true || !target.taskArtifactOwnerDeletionEvidence))
5869
+ )
5870
+ throw new Error("durability_failed");
5871
+ const artifact = record.expectedArtifactsIdentity as Record<string, unknown> | undefined;
5872
+ const expectedArtifactsIdentity = artifact
5873
+ ? typeof artifact.dev === "string" &&
5874
+ typeof artifact.ino === "string" &&
5875
+ typeof artifact.size === "number" &&
5876
+ typeof artifact.mtimeNs === "string" &&
5877
+ typeof artifact.sha256 === "string"
5878
+ ? {
5879
+ dev: BigInt(artifact.dev),
5880
+ ino: BigInt(artifact.ino),
5881
+ size: artifact.size,
5882
+ mtimeNs: BigInt(artifact.mtimeNs),
5883
+ sha256: artifact.sha256,
5884
+ }
5885
+ : undefined
5886
+ : undefined;
5887
+ if (artifact && !expectedArtifactsIdentity) throw new Error("durability_failed");
5888
+ const expectedArtifactsTree =
5889
+ record.expectedArtifactsTree === undefined ? undefined : artifactTreeSnapshot(record.expectedArtifactsTree);
5890
+ if (record.expectedArtifactsTree !== undefined && !expectedArtifactsTree) throw new Error("durability_failed");
2968
5891
 
2969
- if (
2970
- latest &&
2971
- ((record.detachedArtifactsPath !== undefined &&
2972
- ![...plannedPaths].some(planned =>
2973
- isAuthorizedArtifactRoot(target, planned, record.detachedArtifactsPath),
2974
- )) ||
2975
- (record.detachedTranscriptPath !== undefined && !plannedPaths.has(record.detachedTranscriptPath)))
2976
- )
2977
- throw new Error("durability_failed");
2978
- plannedPaths.add(record.plannedArtifactsPath as string);
2979
- plannedPaths.add(record.plannedTranscriptPath as string);
2980
- latest = {
2981
- attempt,
2982
- target,
2983
- expectedArtifactsIdentity,
2984
- expectedArtifactsTree,
2985
- artifactsPayloadDurable: record.artifactsPayloadDurable === true ? true : undefined,
2986
- artifactsRemovedAttempt: record.artifactsRemovedAttempt as number | undefined,
2987
- detachedArtifactsPath: record.detachedArtifactsPath as string | undefined,
2988
- detachedTranscriptPath: record.detachedTranscriptPath as string | undefined,
2989
- transcriptPayloadDurable: record.transcriptPayloadDurable === true ? true : undefined,
2990
- retainedArtifactsSuccessorPath: record.retainedArtifactsSuccessorPath as string | undefined,
2991
- retainedArtifactsPlaceholderPath: record.retainedArtifactsPlaceholderPath as string | undefined,
2992
- retainedArtifactsUnknownPath: record.retainedArtifactsUnknownPath as string | undefined,
2993
- retainedTranscriptSuccessorPath: record.retainedTranscriptSuccessorPath as string | undefined,
2994
- retainedTranscriptPlaceholderPath: record.retainedTranscriptPlaceholderPath as string | undefined,
2995
- retainedTranscriptUnknownPath: record.retainedTranscriptUnknownPath as string | undefined,
2996
- plannedArtifactsPath: record.plannedArtifactsPath as string,
2997
- plannedTranscriptPath: record.plannedTranscriptPath as string,
2998
- };
2999
- }
3000
- return latest;
3001
- } catch (error) {
3002
- if ((error as Error).message === "durability_failed") throw error;
3003
- return undefined;
3004
- }
5892
+ if (
5893
+ latest &&
5894
+ ((record.detachedArtifactsPath !== undefined &&
5895
+ ![...plannedPaths].some(planned => isAuthorizedArtifactRoot(target, planned, record.detachedArtifactsPath))) ||
5896
+ (record.detachedTranscriptPath !== undefined && !plannedPaths.has(record.detachedTranscriptPath)))
5897
+ )
5898
+ throw new Error("durability_failed");
5899
+ plannedPaths.add(record.plannedArtifactsPath as string);
5900
+ plannedPaths.add(record.plannedTranscriptPath as string);
5901
+ return {
5902
+ taskArtifactOwnerTranscriptDeleted: record.taskArtifactOwnerTranscriptDeleted === true ? true : undefined,
5903
+ attempt,
5904
+ target,
5905
+ expectedArtifactsIdentity,
5906
+ expectedArtifactsTree,
5907
+ artifactsPayloadDurable: record.artifactsPayloadDurable === true ? true : undefined,
5908
+ artifactsRemovedAttempt: record.artifactsRemovedAttempt as number | undefined,
5909
+ detachedArtifactsPath: record.detachedArtifactsPath as string | undefined,
5910
+ detachedTranscriptPath: record.detachedTranscriptPath as string | undefined,
5911
+ transcriptPayloadDurable: record.transcriptPayloadDurable === true ? true : undefined,
5912
+ retainedArtifactsSuccessorPath: record.retainedArtifactsSuccessorPath as string | undefined,
5913
+ retainedArtifactsPlaceholderPath: record.retainedArtifactsPlaceholderPath as string | undefined,
5914
+ retainedArtifactsUnknownPath: record.retainedArtifactsUnknownPath as string | undefined,
5915
+ retainedTranscriptSuccessorPath: record.retainedTranscriptSuccessorPath as string | undefined,
5916
+ retainedTranscriptPlaceholderPath: record.retainedTranscriptPlaceholderPath as string | undefined,
5917
+ retainedTranscriptUnknownPath: record.retainedTranscriptUnknownPath as string | undefined,
5918
+ plannedArtifactsPath: record.plannedArtifactsPath as string,
5919
+ plannedTranscriptPath: record.plannedTranscriptPath as string,
5920
+ };
3005
5921
  }
3006
5922
 
3007
5923
  function artifactIdentityForCleanup(target: RetiredTarget): SessionStorageFileIdentity | undefined {
@@ -3043,6 +5959,7 @@ function nextCleanupReceipt(target: RetiredTarget, pending: CleanupReceipt | und
3043
5959
  return {
3044
5960
  attempt,
3045
5961
  target,
5962
+ taskArtifactOwnerTranscriptDeleted: pending?.taskArtifactOwnerTranscriptDeleted,
3046
5963
  expectedArtifactsIdentity,
3047
5964
  expectedArtifactsTree,
3048
5965
  artifactsPayloadDurable: pending?.artifactsPayloadDurable,
@@ -3076,6 +5993,8 @@ function cleanupPendingEvidence(
3076
5993
  ): CleanupReceipt {
3077
5994
  return {
3078
5995
  ...retry,
5996
+ taskArtifactOwnerTranscriptDeleted:
5997
+ deletion.taskArtifactOwnerTranscriptDeleted ?? active.taskArtifactOwnerTranscriptDeleted,
3079
5998
  expectedArtifactsIdentity:
3080
5999
  deletion.phase === "artifacts" ? deletion.artifactsIdentity : active.expectedArtifactsIdentity,
3081
6000
  expectedArtifactsTree: deletion.phase === "artifacts" ? deletion.artifactsTree : active.expectedArtifactsTree,
@@ -3119,29 +6038,43 @@ async function continueDetachedArtifactCleanup(
3119
6038
  lock: ManagedStorageLock,
3120
6039
  flow: ManagedVerifiedDeleteTestEvent["flow"],
3121
6040
  ): Promise<{ deletion: VerifiedSessionDeleteResult; pendingEvidence: CleanupReceipt }> {
3122
- const deletion = await deleteSessionVerifiedWithFence(flow, "artifact-finalization", lock, {
3123
- sessionsRoot: scope.sessionsRoot,
3124
- transcriptPath: target.path,
3125
- sessionId: target.sessionId,
3126
- cwd: target.cwd,
3127
- transcriptIdentity: target.identity,
3128
- transcriptParentIdentity: (() => {
3129
- const parent = fs.lstatSync(path.dirname(target.path), { bigint: true });
3130
- return { dev: parent.dev, ino: parent.ino };
3131
- })(),
3132
- expectedArtifactsIdentity: pendingEvidence.expectedArtifactsIdentity,
3133
- expectedArtifactsTree: pendingEvidence.expectedArtifactsTree,
3134
- detachedArtifactsPath: pendingEvidence.detachedArtifactsPath,
3135
- retainedArtifactsSuccessorPath: pendingEvidence.retainedArtifactsSuccessorPath,
3136
- retainedArtifactsPlaceholderPath: pendingEvidence.retainedArtifactsPlaceholderPath,
3137
- retainedArtifactsUnknownPath: pendingEvidence.retainedArtifactsUnknownPath,
3138
- detachedTranscriptPath: pendingEvidence.detachedTranscriptPath ?? fallbackDetachedTranscriptPath,
3139
- retainedTranscriptSuccessorPath: pendingEvidence.retainedTranscriptSuccessorPath,
3140
- retainedTranscriptPlaceholderPath: pendingEvidence.retainedTranscriptPlaceholderPath,
3141
- retainedTranscriptUnknownPath: pendingEvidence.retainedTranscriptUnknownPath,
3142
- plannedArtifactsPath: pendingEvidence.plannedArtifactsPath,
3143
- plannedTranscriptPath: pendingEvidence.plannedTranscriptPath,
3144
- });
6041
+ const deletion = await deleteSessionVerifiedWithFence(
6042
+ flow,
6043
+ "artifact-finalization",
6044
+ lock,
6045
+ {
6046
+ sessionsRoot: scope.sessionsRoot,
6047
+ transcriptPath: target.path,
6048
+ sessionId: target.sessionId,
6049
+ cwd: target.cwd,
6050
+ transcriptIdentity: target.identity,
6051
+ transcriptParentIdentity: (() => {
6052
+ const parent = fs.lstatSync(path.dirname(target.path), { bigint: true });
6053
+ return { dev: parent.dev, ino: parent.ino };
6054
+ })(),
6055
+ expectedArtifactsIdentity: pendingEvidence.expectedArtifactsIdentity,
6056
+ expectedArtifactsTree: pendingEvidence.expectedArtifactsTree,
6057
+ detachedArtifactsPath: pendingEvidence.detachedArtifactsPath,
6058
+ retainedArtifactsSuccessorPath: pendingEvidence.retainedArtifactsSuccessorPath,
6059
+ retainedArtifactsPlaceholderPath: pendingEvidence.retainedArtifactsPlaceholderPath,
6060
+ retainedArtifactsUnknownPath: pendingEvidence.retainedArtifactsUnknownPath,
6061
+ detachedTranscriptPath: pendingEvidence.detachedTranscriptPath ?? fallbackDetachedTranscriptPath,
6062
+ retainedTranscriptSuccessorPath: pendingEvidence.retainedTranscriptSuccessorPath,
6063
+ retainedTranscriptPlaceholderPath: pendingEvidence.retainedTranscriptPlaceholderPath,
6064
+ retainedTranscriptUnknownPath: pendingEvidence.retainedTranscriptUnknownPath,
6065
+ plannedArtifactsPath: pendingEvidence.plannedArtifactsPath,
6066
+ plannedTranscriptPath: pendingEvidence.plannedTranscriptPath,
6067
+ },
6068
+ scope,
6069
+ target,
6070
+ );
6071
+ if (deletion.kind === "cleanup_pending" && deletion.phase === "task_artifact_owner") {
6072
+ await persistManagedGcStorageOwnerDisposition(scope, target, deletion, lock);
6073
+ const retry = nextCleanupReceipt(target, pendingEvidence);
6074
+ const pending = cleanupPendingEvidence(retry, pendingEvidence, deletion);
6075
+ await publishCleanupPending(scope, tombstone, pending, lock);
6076
+ return { deletion, pendingEvidence: pending };
6077
+ }
3145
6078
  if (deletion.kind === "cleanup_pending") {
3146
6079
  if (
3147
6080
  deletion.phase !== "artifacts" ||
@@ -3180,33 +6113,110 @@ async function publishCleanupPending(
3180
6113
  if (!persisted || persisted.attempt !== receipt.attempt) throw new Error("durability_failed");
3181
6114
  }
3182
6115
 
3183
- function cleanupCompleted(scope: ManagedScope, tombstone: string, target: RetiredTarget): boolean {
6116
+ async function persistManagedOwnerTranscriptDeletion(
6117
+ scope: ManagedScope,
6118
+ tombstone: string,
6119
+ target: RetiredTarget,
6120
+ fallback: CleanupReceipt,
6121
+ lock: ManagedStorageLock,
6122
+ deletion: Extract<VerifiedSessionDeleteResult, { kind: "deleted" }>,
6123
+ ): Promise<void> {
6124
+ return persistManagedGcOwnerTranscriptDeletion({
6125
+ target,
6126
+ fallback,
6127
+ deletion,
6128
+ readLatest: () => pendingCleanupReceipt(scope, tombstone, target),
6129
+ nextReceipt: latest => nextCleanupReceipt(target, latest),
6130
+ publishPending: receipt => publishCleanupPending(scope, tombstone, receipt, lock),
6131
+ });
6132
+ }
6133
+
6134
+ async function cleanupCompleted(scope: ManagedScope, tombstone: string, target: RetiredTarget): Promise<boolean> {
3184
6135
  try {
6136
+ const ownerReceipt = target.taskArtifactOwnerDeletionEvidence
6137
+ ? await readManagedGcSessionRetirementReceipt(scope, target.path)
6138
+ : undefined;
6139
+ if (
6140
+ target.taskArtifactOwnerDeletionEvidence &&
6141
+ (ownerReceipt?.state !== "owner_retired" ||
6142
+ !deepSame(ownerReceipt.taskArtifactOwnerDeletionEvidence, target.taskArtifactOwnerDeletionEvidence))
6143
+ )
6144
+ return false;
3185
6145
  const value: unknown = JSON.parse(
3186
6146
  captureManagedFileNoFollow(cleanupReceiptPath(tombstone, target, "completed", 1)).bytes.toString("utf8"),
3187
6147
  );
3188
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
3189
- const record = value as Record<string, unknown>;
3190
- const recorded = record.target as Record<string, unknown> | undefined;
3191
- const identity = recorded?.identity as Record<string, unknown> | undefined;
3192
- return (
3193
- record.schemaVersion === 1 &&
3194
- record.state === "cleanup_completed" &&
3195
- record.scope === scopeDigest(scope.platform, scope.canonicalCwd) &&
3196
- record.tombstone === tombstone &&
3197
- record.attempt === 1 &&
3198
- recorded?.path === target.path &&
3199
- recorded.sessionId === target.sessionId &&
3200
- recorded.cwd === target.cwd &&
3201
- identity?.dev === String(target.identity.dev) &&
3202
- identity.ino === String(target.identity.ino) &&
3203
- identity.size === target.identity.size &&
3204
- identity.mtimeNs === String(target.identity.mtimeNs) &&
3205
- identity.sha256 === target.identity.sha256
3206
- );
3207
- } catch {
6148
+ return cleanupCompletedRecordMatches(scope, tombstone, target, ownerReceipt, value);
6149
+ } catch (error) {
6150
+ if (isOwnerBoundaryFailure(error) || isManagedGcJournalCapacityError(error)) throw error;
6151
+ return false;
6152
+ }
6153
+ }
6154
+
6155
+ function cleanupCompletedRecordMatches(
6156
+ scope: ManagedScope,
6157
+ tombstone: string,
6158
+ target: RetiredTarget,
6159
+ ownerReceipt: ManagedGcSessionRetirementReceipt | undefined,
6160
+ value: unknown,
6161
+ ): boolean {
6162
+ if (
6163
+ target.taskArtifactOwnerDeletionEvidence &&
6164
+ (ownerReceipt?.state !== "owner_retired" ||
6165
+ !deepSame(ownerReceipt.taskArtifactOwnerDeletionEvidence, target.taskArtifactOwnerDeletionEvidence))
6166
+ )
3208
6167
  return false;
6168
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
6169
+ const record = value as Record<string, unknown>;
6170
+ const recorded = record.target as Record<string, unknown> | undefined;
6171
+ const identity = recorded?.identity as Record<string, unknown> | undefined;
6172
+ const expectedKeys = ownerReceipt
6173
+ ? [
6174
+ "schemaVersion",
6175
+ "state",
6176
+ "scope",
6177
+ "tombstone",
6178
+ "attempt",
6179
+ "target",
6180
+ "taskArtifactOwnerDeletionEvidence",
6181
+ "taskArtifactOwnerRetirementOutcome",
6182
+ "taskArtifactOwnerRetired",
6183
+ "taskArtifactOwnerTranscriptDeleted",
6184
+ ]
6185
+ : ["schemaVersion", "state", "scope", "tombstone", "attempt", "target"];
6186
+ if (Object.keys(record).length !== expectedKeys.length || !expectedKeys.every(key => key in record)) return false;
6187
+ if (ownerReceipt) {
6188
+ const pending = pendingCleanupReceipt(scope, tombstone, target);
6189
+ if (record.taskArtifactOwnerTranscriptDeleted !== true || !pending?.taskArtifactOwnerTranscriptDeleted)
6190
+ return false;
6191
+ const evidence = parseTaskArtifactOwnerDeletionEvidence(record.taskArtifactOwnerDeletionEvidence);
6192
+ const outcome = parseTaskArtifactOwnerRetirementOutcome(
6193
+ taskArtifactOwnerStorageContextForScope(scope),
6194
+ evidence,
6195
+ record.taskArtifactOwnerRetirementOutcome,
6196
+ );
6197
+ if (
6198
+ record.taskArtifactOwnerRetired !== true ||
6199
+ outcome.kind !== "completed" ||
6200
+ !deepSame(evidence, ownerReceipt.taskArtifactOwnerDeletionEvidence) ||
6201
+ !deepSame(outcome, ownerReceipt.taskArtifactOwnerRetirementOutcome)
6202
+ )
6203
+ return false;
3209
6204
  }
6205
+ return (
6206
+ record.schemaVersion === 1 &&
6207
+ record.state === "cleanup_completed" &&
6208
+ record.scope === scopeDigest(scope.platform, scope.canonicalCwd) &&
6209
+ record.tombstone === tombstone &&
6210
+ record.attempt === 1 &&
6211
+ recorded?.path === target.path &&
6212
+ recorded.sessionId === target.sessionId &&
6213
+ recorded.cwd === target.cwd &&
6214
+ identity?.dev === String(target.identity.dev) &&
6215
+ identity.ino === String(target.identity.ino) &&
6216
+ identity.size === target.identity.size &&
6217
+ identity.mtimeNs === String(target.identity.mtimeNs) &&
6218
+ identity.sha256 === target.identity.sha256
6219
+ );
3210
6220
  }
3211
6221
 
3212
6222
  async function publishCleanupCompleted(
@@ -3215,6 +6225,22 @@ async function publishCleanupCompleted(
3215
6225
  target: RetiredTarget,
3216
6226
  lock: ManagedStorageLock,
3217
6227
  ): Promise<void> {
6228
+ const ownerReceipt = target.taskArtifactOwnerDeletionEvidence
6229
+ ? await readManagedGcSessionRetirementReceipt(scope, target.path)
6230
+ : undefined;
6231
+ if (
6232
+ target.taskArtifactOwnerDeletionEvidence &&
6233
+ (ownerReceipt?.state !== "owner_retired" ||
6234
+ !deepSame(ownerReceipt.taskArtifactOwnerDeletionEvidence, target.taskArtifactOwnerDeletionEvidence))
6235
+ )
6236
+ throw new Error("durability_failed");
6237
+ const ownerOutcome = ownerReceipt?.taskArtifactOwnerRetirementOutcome;
6238
+ if (
6239
+ ownerReceipt &&
6240
+ (ownerOutcome?.kind !== "completed" ||
6241
+ !pendingCleanupReceipt(scope, tombstone, target)?.taskArtifactOwnerTranscriptDeleted)
6242
+ )
6243
+ throw new Error("durability_failed");
3218
6244
  try {
3219
6245
  await publishManagedTombstone(
3220
6246
  cleanupReceiptPath(tombstone, target, "completed", 1),
@@ -3225,12 +6251,21 @@ async function publishCleanupCompleted(
3225
6251
  tombstone,
3226
6252
  attempt: 1,
3227
6253
  target: { path: target.path, sessionId: target.sessionId, cwd: target.cwd, identity: target.identity },
6254
+ ...(ownerReceipt && ownerOutcome
6255
+ ? {
6256
+ taskArtifactOwnerDeletionEvidence: ownerReceipt.taskArtifactOwnerDeletionEvidence,
6257
+ taskArtifactOwnerRetirementOutcome: ownerOutcome,
6258
+ taskArtifactOwnerRetired: true,
6259
+ taskArtifactOwnerTranscriptDeleted: true,
6260
+ }
6261
+ : {}),
3228
6262
  },
3229
6263
  lock.assertOwned,
3230
6264
  );
3231
6265
  } catch (error) {
3232
6266
  if ((error as Error).message !== "destination_conflict") throw error;
3233
6267
  }
6268
+ if (!(await cleanupCompleted(scope, tombstone, target))) throw new Error("durability_failed");
3234
6269
  }
3235
6270
 
3236
6271
  function tombstonePathContaining(scope: ManagedScope, candidate: ManagedCandidate): string | undefined {
@@ -3240,7 +6275,8 @@ function tombstonePathContaining(scope: ManagedScope, candidate: ManagedCandidat
3240
6275
  const pathname = path.join(directory, name);
3241
6276
  if (retiredTargets(scope, pathname)?.some(target => sameCandidate(target, candidate))) return pathname;
3242
6277
  }
3243
- } catch {
6278
+ } catch (error) {
6279
+ if (isOwnerBoundaryFailure(error) || (error as Error).message === "durability_failed") throw error;
3244
6280
  return undefined;
3245
6281
  }
3246
6282
  return undefined;
@@ -3554,7 +6590,7 @@ export function cleanupAuthorityMatches(
3554
6590
  parentStat.ino !== cleanup.identity.parentIno
3555
6591
  )
3556
6592
  return false;
3557
- const snapshot = nativeScope().snapshotDirectoryTree(cleanup.retainedPath);
6593
+ const snapshot = nativeScope().snapshotEmptyDirectory(cleanup.retainedPath);
3558
6594
  const observedRoot = snapshot.snapshot?.entries.find(
3559
6595
  entry => entry.relativePath === "" && entry.kind === "directory",
3560
6596
  );
@@ -3618,7 +6654,7 @@ export function detachArtifactRootForMigration(
3618
6654
  const stat = fs.lstatSync(placeholder, { bigint: true });
3619
6655
  if (!stat.isDirectory() || stat.isSymbolicLink() || path.dirname(placeholder) !== path.dirname(plan.originalPath))
3620
6656
  throw new Error("durability_failed");
3621
- const snapshot = nativeScope().snapshotDirectoryTree(placeholder);
6657
+ const snapshot = nativeScope().snapshotEmptyDirectory(placeholder);
3622
6658
  if (!snapshot.ok || !snapshot.snapshot) throw new Error("durability_failed");
3623
6659
  // Windows directory size/mtime authority is the native tree root, never Bun's
3624
6660
  // zero-valued directory lstat. Capturing Bun values here would guarantee a
@@ -3983,11 +7019,23 @@ export async function reconcileManagedTombstones(
3983
7019
  expectedCandidate?: ManagedCandidate,
3984
7020
  ): Promise<void> {
3985
7021
  const directory = path.join(managedInternalDirectory(scope), MANAGED_TOMBSTONES_DIRECTORY);
7022
+ // Ensure scope authority is prepared before any operations that might access the GC protocol
7023
+ const prepared = await ensureManagedScope(
7024
+ scope,
7025
+ scope.platform === "win32" ? "windows-existing-verify-first" : "default",
7026
+ );
7027
+ if (prepared.kind === "error") throw new Error(prepared.message);
3986
7028
  for (const name of fs.readdirSync(directory)) {
3987
7029
  const tombstone = path.join(directory, name);
3988
- const targets = retiredTargets(scope, tombstone);
3989
- if (!targets) continue;
3990
- if (targets.every(target => cleanupCompleted(scope, tombstone, target))) continue;
7030
+ const discoveredTargets = retiredTargets(scope, tombstone);
7031
+ if (!discoveredTargets || discoveredTargets.some(target => hasUnsupportedLegacyOwnerTarget(scope, target)))
7032
+ continue;
7033
+ if (
7034
+ await Promise.all(discoveredTargets.map(target => cleanupCompleted(scope, tombstone, target))).then(values =>
7035
+ values.every(Boolean),
7036
+ )
7037
+ )
7038
+ continue;
3991
7039
  let lock: ManagedStorageLock | undefined;
3992
7040
  try {
3993
7041
  lock = await acquireManagedLock(
@@ -3995,11 +7043,22 @@ export async function reconcileManagedTombstones(
3995
7043
  path.basename(tombstone, ".json"),
3996
7044
  scopeRoot(scope),
3997
7045
  );
3998
- const lockedTargets = retiredTargets(scope, tombstone);
3999
- if (!lockedTargets) continue;
7046
+ const discoveredLockedTargets = retiredTargets(scope, tombstone);
7047
+ if (
7048
+ !discoveredLockedTargets ||
7049
+ discoveredLockedTargets.some(target => hasUnsupportedLegacyOwnerTarget(scope, target))
7050
+ )
7051
+ continue;
7052
+ const lockedTargets = await prepareManagedGcOwnerTargets(scope, discoveredLockedTargets, lock);
7053
+ if (
7054
+ await Promise.all(lockedTargets.map(target => cleanupCompleted(scope, tombstone, target))).then(values =>
7055
+ values.every(Boolean),
7056
+ )
7057
+ )
7058
+ continue;
4000
7059
  for (const target of lockedTargets) {
4001
7060
  lock.assertOwned();
4002
- if (cleanupCompleted(scope, tombstone, target)) continue;
7061
+ if (await cleanupCompleted(scope, tombstone, target)) continue;
4003
7062
  try {
4004
7063
  const pending = pendingCleanupReceipt(scope, tombstone, target);
4005
7064
  const observedPending = pending ? probePlannedCleanupDetach(target, pending) : undefined;
@@ -4024,8 +7083,23 @@ export async function reconcileManagedTombstones(
4024
7083
  target,
4025
7084
  replayReceipt,
4026
7085
  artifactsEvidence.retainedArtifactsRootPath,
4027
- )
7086
+ ) &&
7087
+ (!target.taskArtifactOwnerDeletionEvidence ||
7088
+ pending?.taskArtifactOwnerTranscriptDeleted === true)
4028
7089
  ) {
7090
+ // A conflicting legacy completion record is not fresh completion authority.
7091
+ if (
7092
+ !target.taskArtifactOwnerDeletionEvidence &&
7093
+ fs.existsSync(cleanupReceiptPath(tombstone, target, "completed", 1))
7094
+ )
7095
+ continue;
7096
+ const ownerProgress = await retireManagedGcOwnerAfterArtifacts(
7097
+ scope,
7098
+ target,
7099
+ lockedTargets,
7100
+ lock,
7101
+ );
7102
+ if (ownerProgress.state === "pending" || ownerProgress.state === "payload_retired") continue;
4029
7103
  fsyncManagedParent(target.path);
4030
7104
  await publishCleanupCompleted(scope, tombstone, target, lock);
4031
7105
  continue;
@@ -4050,38 +7124,11 @@ export async function reconcileManagedTombstones(
4050
7124
  ? target
4051
7125
  : validateCandidateForScope(scope, target);
4052
7126
  if (!initialTarget) throw new Error("source_changed");
4053
- let deletion = await deleteSessionVerifiedWithFence("reconcile", "initial", lock, {
4054
- sessionsRoot: scope.sessionsRoot,
4055
- transcriptPath: target.path,
4056
- sessionId: target.sessionId,
4057
- cwd: target.cwd,
4058
- transcriptIdentity: {
4059
- ...initialTarget.identity,
4060
- nlink: fs.lstatSync(observedPending?.detachedTranscriptPath ?? target.path, { bigint: true })
4061
- .nlink,
4062
- },
4063
- expectedArtifactsIdentity: active.expectedArtifactsIdentity,
4064
- expectedArtifactsTree: active.expectedArtifactsTree,
4065
- detachedArtifactsPath:
4066
- active.detachedArtifactsPath ??
4067
- observedPending?.detachedArtifactsPath ??
4068
- (fs.existsSync(active.plannedArtifactsPath) ? active.plannedArtifactsPath : undefined),
4069
- detachedTranscriptPath:
4070
- active.detachedTranscriptPath ??
4071
- observedPending?.detachedTranscriptPath ??
4072
- (pending && fs.existsSync(pending.plannedTranscriptPath)
4073
- ? pending.plannedTranscriptPath
4074
- : undefined) ??
4075
- (fs.existsSync(active.plannedTranscriptPath) ? active.plannedTranscriptPath : undefined),
4076
- retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
4077
- retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
4078
- retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
4079
- retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
4080
- retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
4081
- retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
4082
- plannedArtifactsPath: active.plannedArtifactsPath,
4083
- plannedTranscriptPath: active.plannedTranscriptPath,
4084
- ...(cleanupArtifactsRemoved(
7127
+ let ownerProgress = await managedGcOwnerProgressBeforeDelete(
7128
+ scope,
7129
+ target,
7130
+ lockedTargets,
7131
+ cleanupArtifactsRemoved(
4085
7132
  scope,
4086
7133
  tombstone,
4087
7134
  target,
@@ -4089,12 +7136,65 @@ export async function reconcileManagedTombstones(
4089
7136
  active.artifactsRemovedAttempt ??
4090
7137
  pending?.attempt ??
4091
7138
  active.attempt,
4092
- )
4093
- ? { artifactsRemoved: true as const }
4094
- : {}),
4095
- });
7139
+ ),
7140
+ lock,
7141
+ );
7142
+ if (ownerProgress.state === "pending") continue;
7143
+ let deletion = await deleteSessionVerifiedWithFence(
7144
+ "reconcile",
7145
+ "initial",
7146
+ lock,
7147
+ {
7148
+ sessionsRoot: scope.sessionsRoot,
7149
+ transcriptPath: target.path,
7150
+ sessionId: target.sessionId,
7151
+ cwd: target.cwd,
7152
+ transcriptIdentity: {
7153
+ ...initialTarget.identity,
7154
+ nlink: fs.lstatSync(observedPending?.detachedTranscriptPath ?? target.path, { bigint: true })
7155
+ .nlink,
7156
+ },
7157
+ expectedArtifactsIdentity: active.expectedArtifactsIdentity,
7158
+ expectedArtifactsTree: active.expectedArtifactsTree,
7159
+ detachedArtifactsPath:
7160
+ active.detachedArtifactsPath ??
7161
+ observedPending?.detachedArtifactsPath ??
7162
+ (fs.existsSync(active.plannedArtifactsPath) ? active.plannedArtifactsPath : undefined),
7163
+ detachedTranscriptPath:
7164
+ active.detachedTranscriptPath ??
7165
+ observedPending?.detachedTranscriptPath ??
7166
+ (pending && fs.existsSync(pending.plannedTranscriptPath)
7167
+ ? pending.plannedTranscriptPath
7168
+ : undefined) ??
7169
+ (fs.existsSync(active.plannedTranscriptPath) ? active.plannedTranscriptPath : undefined),
7170
+ retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
7171
+ retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
7172
+ retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
7173
+ retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
7174
+ retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
7175
+ retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
7176
+ plannedArtifactsPath: active.plannedArtifactsPath,
7177
+ plannedTranscriptPath: active.plannedTranscriptPath,
7178
+ ...(cleanupArtifactsRemoved(
7179
+ scope,
7180
+ tombstone,
7181
+ target,
7182
+ pending?.artifactsRemovedAttempt ??
7183
+ active.artifactsRemovedAttempt ??
7184
+ pending?.attempt ??
7185
+ active.attempt,
7186
+ )
7187
+ ? { artifactsRemoved: true as const }
7188
+ : {}),
7189
+ },
7190
+ scope,
7191
+ target,
7192
+ );
4096
7193
  if (deletion.kind === "artifacts_removed") {
7194
+ await publishManagedGcArtifactsRemoved(scope, target, lock);
4097
7195
  await publishCleanupArtifactsRemoved(scope, tombstone, active, lock);
7196
+ ownerProgress = await retireManagedGcOwnerAfterArtifacts(scope, target, lockedTargets, lock);
7197
+ if (ownerProgress.state === "pending") continue;
4098
7198
  active = { ...active, artifactsRemovedAttempt: active.attempt };
4099
7199
  const refreshedTarget = validateCandidateForScope(scope, target);
4100
7200
  if (!refreshedTarget) throw new Error("source_changed");
@@ -4121,9 +7221,15 @@ export async function reconcileManagedTombstones(
4121
7221
  retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
4122
7222
  artifactsRemoved: true,
4123
7223
  },
7224
+ scope,
7225
+ target,
4124
7226
  );
4125
7227
  }
4126
7228
  if (deletion.kind === "cleanup_pending") {
7229
+ if (deletion.phase === "task_artifact_owner") {
7230
+ ownerProgress = await persistManagedGcStorageOwnerDisposition(scope, target, deletion, lock);
7231
+ continue;
7232
+ }
4127
7233
  assertAuthorizedCleanupPending(target, active, deletion);
4128
7234
  const retry = nextCleanupReceipt(target, active);
4129
7235
  let pendingEvidence = cleanupPendingEvidence(retry, active, deletion);
@@ -4150,7 +7256,10 @@ export async function reconcileManagedTombstones(
4150
7256
  !retainedArtifactPayloadAbsent(deletion.detachedArtifactsPath))
4151
7257
  )
4152
7258
  continue;
7259
+ await publishManagedGcArtifactsRemoved(scope, target, lock);
4153
7260
  await publishCleanupArtifactsRemoved(scope, tombstone, pendingEvidence, lock);
7261
+ ownerProgress = await retireManagedGcOwnerAfterArtifacts(scope, target, lockedTargets, lock);
7262
+ if (ownerProgress.state === "pending") continue;
4154
7263
  pendingEvidence = { ...pendingEvidence, artifactsRemovedAttempt: pendingEvidence.attempt };
4155
7264
  const retainedProof = cleanupArtifactsRemovedReceipt(
4156
7265
  tombstone,
@@ -4186,6 +7295,8 @@ export async function reconcileManagedTombstones(
4186
7295
  retainedTranscriptUnknownPath: pendingEvidence.retainedTranscriptUnknownPath,
4187
7296
  artifactsRemoved: true,
4188
7297
  },
7298
+ scope,
7299
+ target,
4189
7300
  () => {
4190
7301
  if (
4191
7302
  !cleanupArtifactsRemoved(
@@ -4198,6 +7309,10 @@ export async function reconcileManagedTombstones(
4198
7309
  throw new Error("durability_failed");
4199
7310
  },
4200
7311
  );
7312
+ if (deletion.kind === "cleanup_pending" && deletion.phase === "task_artifact_owner") {
7313
+ ownerProgress = await persistManagedGcStorageOwnerDisposition(scope, target, deletion, lock);
7314
+ continue;
7315
+ }
4201
7316
  if (deletion.kind === "cleanup_pending") {
4202
7317
  if (
4203
7318
  deletion.phase !== "transcript" ||
@@ -4231,9 +7346,19 @@ export async function reconcileManagedTombstones(
4231
7346
  )
4232
7347
  continue;
4233
7348
  }
7349
+ if (deletion.kind === "deleted")
7350
+ await persistManagedOwnerTranscriptDeletion(scope, tombstone, target, active, lock, deletion);
7351
+ const ownerDisposition = await taskArtifactOwnerTranscriptResult(scope, target, lock);
7352
+ if (
7353
+ (ownerDisposition !== "none" && ownerDisposition !== "retired") ||
7354
+ (target.taskArtifactOwnerDeletionEvidence &&
7355
+ !pendingCleanupReceipt(scope, tombstone, target)?.taskArtifactOwnerTranscriptDeleted)
7356
+ )
7357
+ continue;
4234
7358
  fsyncManagedParent(target.path);
4235
7359
  await publishCleanupCompleted(scope, tombstone, target, lock);
4236
7360
  } catch (error) {
7361
+ if (isManagedGcJournalCapacityError(error)) throw error;
4237
7362
  if (!expectedCandidate || target.sessionId === expectedCandidate.sessionId) throw error;
4238
7363
  logger.warn("Tombstone reconciliation failed for one target; will retry on a future scope open", {
4239
7364
  tombstone,
@@ -4651,6 +7776,24 @@ async function deleteManagedSessionCandidateInternal(
4651
7776
  scope: ManagedScope,
4652
7777
  candidate: ManagedCandidate,
4653
7778
  ): Promise<ManagedDeleteCandidateResult> {
7779
+ try {
7780
+ if (hasUnsupportedLegacyOwnerTargetInternal(scope.directoryPath, candidate)) {
7781
+ const tombstonePath = path.join(
7782
+ managedInternalDirectory(scope),
7783
+ MANAGED_TOMBSTONES_DIRECTORY,
7784
+ `${stableOperationName(candidate)}.json`,
7785
+ );
7786
+ return {
7787
+ kind: "cleanup_pending",
7788
+ tombstonePath,
7789
+ phase: "artifacts",
7790
+ message: "task_artifact_owner_legacy_scope_unsupported",
7791
+ };
7792
+ }
7793
+ } catch (error) {
7794
+ const code = expectedFailure(error);
7795
+ return { kind: "error", code, message: error instanceof Error ? error.message : "Managed deletion failed." };
7796
+ }
4654
7797
  const prepared = await prepareManagedSessionScopeForWrite(scope);
4655
7798
  if (prepared.kind === "error")
4656
7799
  return {
@@ -4660,6 +7803,12 @@ async function deleteManagedSessionCandidateInternal(
4660
7803
  };
4661
7804
  const current = validateCandidateForScope(scope, candidate);
4662
7805
  const paired = current ? receiptPair(scope, current) : undefined;
7806
+ if (paired)
7807
+ assertOwnerTranscriptLocator({
7808
+ transcriptPath: paired.path,
7809
+ sessionId: paired.sessionId,
7810
+ transcriptIdentity: paired.identity,
7811
+ });
4663
7812
  const logical = paired?.provenance === "legacy" ? paired : (current ?? candidate);
4664
7813
  const existingTombstone = tombstonePathContaining(scope, candidate);
4665
7814
  const tombstone =
@@ -4676,9 +7825,17 @@ async function deleteManagedSessionCandidateInternal(
4676
7825
  scopeRoot(scope),
4677
7826
  );
4678
7827
  let targets = retiredTargets(scope, tombstone);
7828
+ const initialTargets = targets ?? (current ? [current, ...(paired ? [paired] : [])] : []);
7829
+ if (initialTargets.some(target => hasUnsupportedLegacyOwnerTarget(scope, target)))
7830
+ return {
7831
+ kind: "cleanup_pending",
7832
+ tombstonePath: tombstone,
7833
+ phase: "artifacts",
7834
+ message: "task_artifact_owner_legacy_scope_unsupported",
7835
+ };
4679
7836
  if (!targets) {
4680
7837
  if (!current) throw new Error("source_changed");
4681
- targets = [current, ...(paired ? [paired] : [])];
7838
+ targets = await prepareManagedGcOwnerTargets(scope, [current, ...(paired ? [paired] : [])], lock);
4682
7839
  lock.assertOwned();
4683
7840
  try {
4684
7841
  await publishManagedTombstone(
@@ -4693,6 +7850,9 @@ async function deleteManagedSessionCandidateInternal(
4693
7850
  cwd: target.cwd,
4694
7851
  provenance: target.provenance,
4695
7852
  identity: target.identity,
7853
+ ...(target.taskArtifactOwnerDeletionEvidence
7854
+ ? { taskArtifactOwnerDeletionEvidence: target.taskArtifactOwnerDeletionEvidence }
7855
+ : {}),
4696
7856
  })),
4697
7857
  },
4698
7858
  lock.assertOwned,
@@ -4703,6 +7863,7 @@ async function deleteManagedSessionCandidateInternal(
4703
7863
  targets = retiredTargets(scope, tombstone);
4704
7864
  if (!targets) throw new Error("durability_failed");
4705
7865
  }
7866
+ targets = await prepareManagedGcOwnerTargets(scope, targets, lock);
4706
7867
  let deletedAny = false;
4707
7868
  for (const target of targets) {
4708
7869
  lock.assertOwned();
@@ -4712,6 +7873,42 @@ async function deleteManagedSessionCandidateInternal(
4712
7873
  fs.lstatSync(target.path);
4713
7874
  } catch (error) {
4714
7875
  if ((error as NodeJS.ErrnoException).code === "ENOENT") {
7876
+ if (!observedPending && target.taskArtifactOwnerDeletionEvidence) {
7877
+ if (!pending?.taskArtifactOwnerTranscriptDeleted)
7878
+ return {
7879
+ kind: "cleanup_pending",
7880
+ tombstonePath: tombstone,
7881
+ phase: "artifacts",
7882
+ message: "task_artifact_owner_transcript_deletion_unverified",
7883
+ };
7884
+ const artifactsEvidence = cleanupArtifactsRemovedEvidence(
7885
+ scope,
7886
+ tombstone,
7887
+ target,
7888
+ pending.artifactsRemovedAttempt ?? pending.attempt,
7889
+ );
7890
+ if (
7891
+ !artifactsEvidence ||
7892
+ !cleanupRootsAbsent(tombstone, target, pending, artifactsEvidence.retainedArtifactsRootPath)
7893
+ )
7894
+ return {
7895
+ kind: "cleanup_pending",
7896
+ tombstonePath: tombstone,
7897
+ phase: "artifacts",
7898
+ message: "task_artifact_owner_retirement_pending",
7899
+ };
7900
+ const ownerProgress = await retireManagedGcOwnerAfterArtifacts(scope, target, targets, lock);
7901
+ if (ownerProgress.state === "pending" || ownerProgress.state === "payload_retired")
7902
+ return {
7903
+ kind: "cleanup_pending",
7904
+ tombstonePath: tombstone,
7905
+ phase: "artifacts",
7906
+ message: ownerProgress.message ?? "task_artifact_owner_retirement_pending",
7907
+ };
7908
+ fsyncManagedParent(target.path);
7909
+ await publishCleanupCompleted(scope, tombstone, target, lock);
7910
+ continue;
7911
+ }
4715
7912
  if (!observedPending) continue;
4716
7913
  const artifactsEvidence = cleanupArtifactsRemovedEvidence(
4717
7914
  scope,
@@ -4721,15 +7918,31 @@ async function deleteManagedSessionCandidateInternal(
4721
7918
  );
4722
7919
  if (
4723
7920
  artifactsEvidence &&
4724
- cleanupRootsAbsent(tombstone, target, observedPending, artifactsEvidence.retainedArtifactsRootPath)
7921
+ cleanupRootsAbsent(tombstone, target, observedPending, artifactsEvidence.retainedArtifactsRootPath) &&
7922
+ (!target.taskArtifactOwnerDeletionEvidence || pending?.taskArtifactOwnerTranscriptDeleted === true)
4725
7923
  ) {
7924
+ const ownerProgress = await retireManagedGcOwnerAfterArtifacts(scope, target, targets, lock);
7925
+ if (ownerProgress.state === "pending" || ownerProgress.state === "payload_retired")
7926
+ return {
7927
+ kind: "cleanup_pending",
7928
+ tombstonePath: tombstone,
7929
+ phase: "artifacts",
7930
+ message: ownerProgress.message ?? "task_artifact_owner_retirement_pending",
7931
+ };
4726
7932
  fsyncManagedParent(target.path);
4727
7933
  await publishCleanupCompleted(scope, tombstone, target, lock);
4728
7934
  continue;
4729
7935
  }
7936
+ if (target.taskArtifactOwnerDeletionEvidence && !observedPending.detachedTranscriptPath)
7937
+ return {
7938
+ kind: "cleanup_pending",
7939
+ tombstonePath: tombstone,
7940
+ phase: "artifacts",
7941
+ message: "task_artifact_owner_transcript_deletion_unverified",
7942
+ };
4730
7943
  } else throw error;
4731
7944
  }
4732
- if (cleanupCompleted(scope, tombstone, target)) continue;
7945
+ if (await cleanupCompleted(scope, tombstone, target)) continue;
4733
7946
  deletedAny = true;
4734
7947
  const verified = observedPending ? target : validateCandidateForScope(scope, target);
4735
7948
  if (!verified || !sameCandidate(verified, target)) throw new Error("source_changed");
@@ -4745,64 +7958,118 @@ async function deleteManagedSessionCandidateInternal(
4745
7958
  await publishCleanupPending(scope, tombstone, active, lock);
4746
7959
  const initialTarget = validateCandidateForScope(scope, target);
4747
7960
  if (!initialTarget) throw new Error("source_changed");
4748
- let deletion = await deleteSessionVerifiedWithFence("direct", "initial", lock, {
4749
- sessionsRoot: scope.sessionsRoot,
4750
- transcriptPath: target.path,
4751
- sessionId: target.sessionId,
4752
- cwd: target.cwd,
4753
- transcriptIdentity: initialTarget.identity,
4754
- transcriptParentIdentity: (() => {
4755
- const parent = fs.lstatSync(path.dirname(target.path), { bigint: true });
4756
- return { dev: parent.dev, ino: parent.ino };
4757
- })(),
4758
- expectedArtifactsIdentity: active.expectedArtifactsIdentity,
4759
- expectedArtifactsTree: active.expectedArtifactsTree,
4760
- detachedArtifactsPath: active.detachedArtifactsPath ?? observedPending?.detachedArtifactsPath,
4761
- detachedTranscriptPath:
4762
- active.detachedTranscriptPath ??
4763
- observedPending?.detachedTranscriptPath ??
4764
- (pending && fs.existsSync(pending.plannedTranscriptPath) ? pending.plannedTranscriptPath : undefined),
4765
- retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
4766
- retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
4767
- retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
4768
- retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
4769
- retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
4770
- retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
4771
- plannedArtifactsPath: active.plannedArtifactsPath,
4772
- plannedTranscriptPath: active.plannedTranscriptPath,
4773
- ...(cleanupArtifactsRemoved(
7961
+ let ownerProgress = await managedGcOwnerProgressBeforeDelete(
7962
+ scope,
7963
+ target,
7964
+ targets,
7965
+ cleanupArtifactsRemoved(
4774
7966
  scope,
4775
7967
  tombstone,
4776
7968
  target,
4777
7969
  pending?.artifactsRemovedAttempt ?? active.artifactsRemovedAttempt ?? pending?.attempt ?? active.attempt,
4778
- )
4779
- ? { artifactsRemoved: true as const }
4780
- : {}),
4781
- });
4782
- if (deletion.kind === "artifacts_removed") {
4783
- await publishCleanupArtifactsRemoved(scope, tombstone, active, lock);
4784
- active = { ...active, artifactsRemovedAttempt: active.attempt };
4785
- const refreshedTarget = validateCandidateForScope(scope, target);
4786
- if (!refreshedTarget) throw new Error("source_changed");
4787
- deletion = await deleteSessionVerifiedWithFence("direct", "transcript-after-artifacts-removed", lock, {
7970
+ ),
7971
+ lock,
7972
+ );
7973
+ if (ownerProgress.state === "pending")
7974
+ return {
7975
+ kind: "cleanup_pending",
7976
+ tombstonePath: tombstone,
7977
+ phase: "artifacts",
7978
+ message: ownerProgress.message ?? "task_artifact_owner_retirement_pending",
7979
+ };
7980
+ let deletion = await deleteSessionVerifiedWithFence(
7981
+ "direct",
7982
+ "initial",
7983
+ lock,
7984
+ {
4788
7985
  sessionsRoot: scope.sessionsRoot,
4789
7986
  transcriptPath: target.path,
4790
7987
  sessionId: target.sessionId,
4791
7988
  cwd: target.cwd,
4792
- transcriptIdentity: refreshedTarget.identity,
4793
- plannedArtifactsPath: active.plannedArtifactsPath,
4794
- plannedTranscriptPath: active.plannedTranscriptPath,
4795
- detachedTranscriptPath: active.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
7989
+ transcriptIdentity: initialTarget.identity,
7990
+ transcriptParentIdentity: (() => {
7991
+ const parent = fs.lstatSync(path.dirname(target.path), { bigint: true });
7992
+ return { dev: parent.dev, ino: parent.ino };
7993
+ })(),
7994
+ expectedArtifactsIdentity: active.expectedArtifactsIdentity,
7995
+ expectedArtifactsTree: active.expectedArtifactsTree,
7996
+ detachedArtifactsPath: active.detachedArtifactsPath ?? observedPending?.detachedArtifactsPath,
7997
+ detachedTranscriptPath:
7998
+ active.detachedTranscriptPath ??
7999
+ observedPending?.detachedTranscriptPath ??
8000
+ (pending && fs.existsSync(pending.plannedTranscriptPath) ? pending.plannedTranscriptPath : undefined),
4796
8001
  retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
4797
8002
  retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
4798
8003
  retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
4799
8004
  retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
4800
8005
  retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
4801
8006
  retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
4802
- artifactsRemoved: true,
4803
- });
8007
+ plannedArtifactsPath: active.plannedArtifactsPath,
8008
+ plannedTranscriptPath: active.plannedTranscriptPath,
8009
+ ...(cleanupArtifactsRemoved(
8010
+ scope,
8011
+ tombstone,
8012
+ target,
8013
+ pending?.artifactsRemovedAttempt ??
8014
+ active.artifactsRemovedAttempt ??
8015
+ pending?.attempt ??
8016
+ active.attempt,
8017
+ )
8018
+ ? { artifactsRemoved: true as const }
8019
+ : {}),
8020
+ },
8021
+ scope,
8022
+ target,
8023
+ );
8024
+ if (deletion.kind === "artifacts_removed") {
8025
+ await publishManagedGcArtifactsRemoved(scope, target, lock);
8026
+ await publishCleanupArtifactsRemoved(scope, tombstone, active, lock);
8027
+ ownerProgress = await retireManagedGcOwnerAfterArtifacts(scope, target, targets, lock);
8028
+ if (ownerProgress.state === "pending")
8029
+ return {
8030
+ kind: "cleanup_pending",
8031
+ tombstonePath: tombstone,
8032
+ phase: "artifacts",
8033
+ message: ownerProgress.message ?? "task_artifact_owner_retirement_pending",
8034
+ };
8035
+ active = { ...active, artifactsRemovedAttempt: active.attempt };
8036
+ const refreshedTarget = validateCandidateForScope(scope, target);
8037
+ if (!refreshedTarget) throw new Error("source_changed");
8038
+ deletion = await deleteSessionVerifiedWithFence(
8039
+ "direct",
8040
+ "transcript-after-artifacts-removed",
8041
+ lock,
8042
+ {
8043
+ sessionsRoot: scope.sessionsRoot,
8044
+ transcriptPath: target.path,
8045
+ sessionId: target.sessionId,
8046
+ cwd: target.cwd,
8047
+ transcriptIdentity: refreshedTarget.identity,
8048
+ plannedArtifactsPath: active.plannedArtifactsPath,
8049
+ plannedTranscriptPath: active.plannedTranscriptPath,
8050
+ detachedTranscriptPath: active.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
8051
+ retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
8052
+ retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
8053
+ retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
8054
+ retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
8055
+ retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
8056
+ retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
8057
+ artifactsRemoved: true,
8058
+ },
8059
+ scope,
8060
+ target,
8061
+ );
4804
8062
  }
4805
8063
  if (deletion.kind === "cleanup_pending") {
8064
+ if (deletion.phase === "task_artifact_owner") {
8065
+ ownerProgress = await persistManagedGcStorageOwnerDisposition(scope, target, deletion, lock);
8066
+ return {
8067
+ kind: "cleanup_pending",
8068
+ tombstonePath: tombstone,
8069
+ phase: "artifacts",
8070
+ message: ownerProgress.message ?? deletion.error.message,
8071
+ };
8072
+ }
4806
8073
  assertAuthorizedCleanupPending(target, active, deletion);
4807
8074
  const retry = nextCleanupReceipt(target, active);
4808
8075
  let pendingEvidence = cleanupPendingEvidence(retry, active, deletion);
@@ -4836,7 +8103,16 @@ async function deleteManagedSessionCandidateInternal(
4836
8103
  phase: deletion.phase,
4837
8104
  message: "Exact cleanup remains pending because descriptor-bound final deletion is unavailable.",
4838
8105
  };
8106
+ await publishManagedGcArtifactsRemoved(scope, target, lock);
4839
8107
  await publishCleanupArtifactsRemoved(scope, tombstone, pendingEvidence, lock);
8108
+ ownerProgress = await retireManagedGcOwnerAfterArtifacts(scope, target, targets, lock);
8109
+ if (ownerProgress.state === "pending")
8110
+ return {
8111
+ kind: "cleanup_pending",
8112
+ tombstonePath: tombstone,
8113
+ phase: "artifacts",
8114
+ message: ownerProgress.message ?? "task_artifact_owner_retirement_pending",
8115
+ };
4840
8116
  pendingEvidence = { ...pendingEvidence, artifactsRemovedAttempt: pendingEvidence.attempt };
4841
8117
  const retainedProof = cleanupArtifactsRemovedReceipt(
4842
8118
  tombstone,
@@ -4872,6 +8148,8 @@ async function deleteManagedSessionCandidateInternal(
4872
8148
  retainedTranscriptUnknownPath: pendingEvidence.retainedTranscriptUnknownPath,
4873
8149
  artifactsRemoved: true,
4874
8150
  },
8151
+ scope,
8152
+ target,
4875
8153
  () => {
4876
8154
  if (
4877
8155
  !cleanupArtifactsRemoved(
@@ -4884,6 +8162,15 @@ async function deleteManagedSessionCandidateInternal(
4884
8162
  throw new Error("durability_failed");
4885
8163
  },
4886
8164
  );
8165
+ if (deletion.kind === "cleanup_pending" && deletion.phase === "task_artifact_owner") {
8166
+ ownerProgress = await persistManagedGcStorageOwnerDisposition(scope, target, deletion, lock);
8167
+ return {
8168
+ kind: "cleanup_pending",
8169
+ tombstonePath: tombstone,
8170
+ phase: "artifacts",
8171
+ message: ownerProgress.message ?? deletion.error.message,
8172
+ };
8173
+ }
4887
8174
  if (deletion.kind === "cleanup_pending") {
4888
8175
  if (
4889
8176
  deletion.phase !== "transcript" ||
@@ -4914,12 +8201,42 @@ async function deleteManagedSessionCandidateInternal(
4914
8201
  return {
4915
8202
  kind: "cleanup_pending",
4916
8203
  tombstonePath: tombstone,
4917
- phase: deletion.phase,
4918
- message: deletion.error.message,
8204
+ phase: ownerProgress.state === "payload_retired" ? "artifacts" : deletion.phase,
8205
+ message:
8206
+ ownerProgress.state === "payload_retired"
8207
+ ? "task_artifact_owner_namespace_cleanup_pending"
8208
+ : deletion.error.message,
4919
8209
  };
4920
8210
  // A retained transcript quarantine proves canonical absence and remains
4921
8211
  // identity-bound in the durable pending receipt.
4922
8212
  }
8213
+ if (deletion.kind === "deleted")
8214
+ await persistManagedOwnerTranscriptDeletion(scope, tombstone, target, active, lock, deletion);
8215
+ const ownerDisposition = await taskArtifactOwnerTranscriptResult(scope, target, lock);
8216
+ if (ownerDisposition === "payload_retired")
8217
+ return {
8218
+ kind: "cleanup_pending",
8219
+ tombstonePath: tombstone,
8220
+ phase: "artifacts",
8221
+ message: "task_artifact_owner_namespace_cleanup_pending",
8222
+ };
8223
+ if (target.taskArtifactOwnerDeletionEvidence && ownerDisposition !== "retired")
8224
+ return {
8225
+ kind: "cleanup_pending",
8226
+ tombstonePath: tombstone,
8227
+ phase: "artifacts",
8228
+ message: "task_artifact_owner_retirement_pending",
8229
+ };
8230
+ if (
8231
+ target.taskArtifactOwnerDeletionEvidence &&
8232
+ !pendingCleanupReceipt(scope, tombstone, target)?.taskArtifactOwnerTranscriptDeleted
8233
+ )
8234
+ return {
8235
+ kind: "cleanup_pending",
8236
+ tombstonePath: tombstone,
8237
+ phase: "transcript",
8238
+ message: "task_artifact_owner_transcript_deletion_unverified",
8239
+ };
4923
8240
  fsyncManagedParent(target.path);
4924
8241
  await publishCleanupCompleted(scope, tombstone, target, lock);
4925
8242
  }