@gajae-code/coding-agent 0.18.6 → 0.18.8

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 (302) hide show
  1. package/CHANGELOG.md +318 -1
  2. package/dist/types/async/job-manager.d.ts +1 -1
  3. package/dist/types/capability/mcp.d.ts +2 -0
  4. package/dist/types/capability/skill.d.ts +2 -0
  5. package/dist/types/capability/types.d.ts +6 -0
  6. package/dist/types/cli/public-command-errors.d.ts +3 -1
  7. package/dist/types/cli/setup-cli.d.ts +1 -1
  8. package/dist/types/commands/deep-interview.d.ts +3 -0
  9. package/dist/types/config/autorouting-tier-map.d.ts +348 -9
  10. package/dist/types/config/file-lock.d.ts +1 -0
  11. package/dist/types/config/model-bindings-applier.d.ts +2 -0
  12. package/dist/types/config/model-profile-activation.d.ts +11 -3
  13. package/dist/types/config/model-registry.d.ts +4 -9
  14. package/dist/types/config/settings.d.ts +4 -0
  15. package/dist/types/config/skill-settings-defaults.d.ts +2 -0
  16. package/dist/types/cursor-delete-path.d.ts +1 -0
  17. package/dist/types/cursor-mcp-name.d.ts +8 -0
  18. package/dist/types/customization/inventory.d.ts +2 -0
  19. package/dist/types/debug/report-bundle-parent.d.ts +2 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
  21. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
  22. package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
  23. package/dist/types/discovery/helpers.d.ts +8 -3
  24. package/dist/types/export/html/css-color.d.ts +2 -0
  25. package/dist/types/export/html/template.generated.d.ts +1 -1
  26. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  27. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
  28. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
  29. package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
  30. package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
  31. package/dist/types/extensibility/skill-management.d.ts +3 -1
  32. package/dist/types/extensibility/skills.d.ts +2 -0
  33. package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
  34. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
  35. package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
  36. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
  37. package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
  38. package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
  39. package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
  40. package/dist/types/gjc-runtime/state-writer.d.ts +11 -1
  41. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
  42. package/dist/types/internal-urls/skill-path.d.ts +1 -0
  43. package/dist/types/lsp/workspace-path.d.ts +1 -0
  44. package/dist/types/modes/acp/acp-agent.d.ts +7 -0
  45. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  46. package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
  47. package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
  48. package/dist/types/modes/controllers/input-controller.d.ts +9 -0
  49. package/dist/types/modes/interactive-mode.d.ts +1 -0
  50. package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
  51. package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
  52. package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
  53. package/dist/types/progress/collect-project-progress.d.ts +38 -0
  54. package/dist/types/progress/progress-contract.d.ts +154 -0
  55. package/dist/types/progress/progress-snapshot.d.ts +11 -0
  56. package/dist/types/progress/project-progress.d.ts +141 -0
  57. package/dist/types/progress/render-progress.d.ts +28 -0
  58. package/dist/types/runtime-mcp/elicitation.d.ts +12 -0
  59. package/dist/types/runtime-mcp/oauth-public-url.d.ts +1 -0
  60. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
  61. package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
  62. package/dist/types/runtime-mcp/types.d.ts +6 -0
  63. package/dist/types/sdk/acp/adapter.d.ts +1 -1
  64. package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
  65. package/dist/types/sdk/broker/broker.d.ts +18 -1
  66. package/dist/types/sdk/broker/lifecycle.d.ts +9 -0
  67. package/dist/types/sdk/broker/session-index.d.ts +12 -6
  68. package/dist/types/sdk/broker/task-artifact-owner-validation.d.ts +55 -0
  69. package/dist/types/sdk/bus/index.d.ts +12 -1
  70. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
  71. package/dist/types/sdk/bus/telegram-daemon.d.ts +3 -2
  72. package/dist/types/sdk/client/client.d.ts +2 -0
  73. package/dist/types/sdk/host/host.d.ts +6 -0
  74. package/dist/types/sdk/host/query/handlers.d.ts +3 -0
  75. package/dist/types/sdk/index.d.ts +1 -0
  76. package/dist/types/sdk/router/session-router.d.ts +9 -0
  77. package/dist/types/sdk/session-list.d.ts +1 -1
  78. package/dist/types/sdk/session.d.ts +8 -0
  79. package/dist/types/session/agent-session.d.ts +38 -9
  80. package/dist/types/session/artifacts.d.ts +2 -2
  81. package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
  82. package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
  83. package/dist/types/session/internal/managed-session-storage.d.ts +36 -1
  84. package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
  85. package/dist/types/session/internal/task-artifact-owner-access.d.ts +19 -0
  86. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +11 -0
  87. package/dist/types/session/session-manager.d.ts +31 -5
  88. package/dist/types/session/session-retirement.d.ts +71 -0
  89. package/dist/types/session/session-storage.d.ts +62 -48
  90. package/dist/types/session/session-transcript-header.d.ts +2 -0
  91. package/dist/types/session/task-artifact-owner-codec.d.ts +109 -0
  92. package/dist/types/session/task-artifact-owner-retirement.d.ts +7 -0
  93. package/dist/types/session/task-artifact-owner.d.ts +10 -0
  94. package/dist/types/setup/credential-auto-import.d.ts +1 -1
  95. package/dist/types/setup/credential-import.d.ts +6 -4
  96. package/dist/types/skill-state/active-state.d.ts +38 -0
  97. package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
  98. package/dist/types/task/executor.d.ts +3 -0
  99. package/dist/types/tools/bash-skill-urls.d.ts +1 -1
  100. package/dist/types/tools/index.d.ts +3 -0
  101. package/dist/types/tools/python.d.ts +9 -1
  102. package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
  103. package/dist/types/tools/restricted-cwd.d.ts +1 -0
  104. package/dist/types/tools/todo-write.d.ts +6 -0
  105. package/dist/types/utils/tools-checksum.d.ts +7 -0
  106. package/package.json +7 -7
  107. package/scripts/generate-sdk-operation-inventory.ts +21 -0
  108. package/scripts/resident-memory-bench.ts +174 -20
  109. package/scripts/verify-managed-empty-stop.ts +28 -0
  110. package/src/async/job-manager.ts +228 -122
  111. package/src/capability/index.ts +18 -0
  112. package/src/capability/mcp.ts +2 -0
  113. package/src/capability/skill.ts +2 -0
  114. package/src/capability/types.ts +6 -0
  115. package/src/cli/auth-broker-cli.ts +1 -1
  116. package/src/cli/config-cli.ts +8 -18
  117. package/src/cli/customize-doctor.ts +15 -2
  118. package/src/cli/public-command-errors.ts +21 -1
  119. package/src/cli/setup-cli.ts +4 -4
  120. package/src/cli/skills-cli.ts +5 -0
  121. package/src/cli-main.ts +7 -8
  122. package/src/commands/deep-interview.ts +47 -8
  123. package/src/commands/sdk.ts +140 -29
  124. package/src/config/autorouting-tier-map.ts +117 -7
  125. package/src/config/file-lock.ts +307 -68
  126. package/src/config/mcp-schema.json +4 -0
  127. package/src/config/model-bindings-applier.ts +20 -0
  128. package/src/config/model-profile-activation.ts +240 -46
  129. package/src/config/model-registry.ts +47 -153
  130. package/src/config/settings.ts +44 -4
  131. package/src/config/skill-settings-defaults.ts +7 -0
  132. package/src/coordinator-mcp/policy.ts +3 -2
  133. package/src/cursor-delete-path.ts +10 -0
  134. package/src/cursor-mcp-name.ts +15 -0
  135. package/src/cursor.ts +13 -2
  136. package/src/customization/inventory.ts +3 -0
  137. package/src/debug/report-bundle-parent.ts +22 -0
  138. package/src/debug/report-bundle.ts +3 -0
  139. package/src/debug/system-info.ts +13 -1
  140. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
  141. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
  142. package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
  143. package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
  144. package/src/defaults/gjc-defaults.test.ts +18 -1
  145. package/src/defaults/gjc-defaults.ts +3 -0
  146. package/src/defaults/gjc-skills.generated.ts +58 -36
  147. package/src/discovery/agents.ts +6 -1
  148. package/src/discovery/builtin.ts +10 -0
  149. package/src/discovery/helpers.ts +168 -23
  150. package/src/discovery/mcp-json.ts +6 -0
  151. package/src/discovery/ssh.ts +9 -4
  152. package/src/discovery/substitute-plugin-root.ts +3 -1
  153. package/src/edit/file-read-cache.ts +1 -1
  154. package/src/eval/py/executor.ts +380 -120
  155. package/src/export/html/css-color.ts +9 -0
  156. package/src/export/html/index.ts +5 -4
  157. package/src/export/html/template.generated.ts +1 -1
  158. package/src/extensibility/extensions/runner.ts +4 -0
  159. package/src/extensibility/extensions/types.ts +7 -0
  160. package/src/extensibility/gjc-plugins/runtime-adapters.ts +73 -8
  161. package/src/extensibility/plugins/manager.ts +3 -2
  162. package/src/extensibility/plugins/marketplace/cache.ts +15 -0
  163. package/src/extensibility/plugins/marketplace/manager.ts +11 -3
  164. package/src/extensibility/plugins/plugin-link-path.ts +12 -0
  165. package/src/extensibility/runtime-skill-discovery.ts +20 -2
  166. package/src/extensibility/skill-management.ts +6 -2
  167. package/src/extensibility/skills.ts +6 -1
  168. package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
  169. package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
  170. package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
  171. package/src/gjc-runtime/deep-interview-stage.ts +98 -21
  172. package/src/gjc-runtime/gc-runtime.ts +206 -8
  173. package/src/gjc-runtime/launch-worktree.ts +14 -4
  174. package/src/gjc-runtime/ralplan-runtime.ts +447 -52
  175. package/src/gjc-runtime/state-argv.ts +2 -0
  176. package/src/gjc-runtime/state-migrations.ts +91 -29
  177. package/src/gjc-runtime/state-runtime.ts +3716 -135
  178. package/src/gjc-runtime/state-writer.ts +83 -8
  179. package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
  180. package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
  181. package/src/gjc-runtime/workflow-manifest.ts +54 -21
  182. package/src/harness-control-plane/operate.ts +12 -4
  183. package/src/harness-control-plane/owner.ts +5 -1
  184. package/src/hashline/parser.ts +48 -9
  185. package/src/hashline/recovery.ts +13 -4
  186. package/src/hindsight/config.ts +9 -3
  187. package/src/internal-urls/docs-index.generated.ts +19 -18
  188. package/src/internal-urls/skill-path.ts +19 -0
  189. package/src/internal-urls/skill-protocol.ts +2 -0
  190. package/src/lsp/edits.ts +6 -0
  191. package/src/lsp/workspace-path.ts +51 -0
  192. package/src/main.ts +195 -31
  193. package/src/modes/acp/acp-agent.ts +163 -7
  194. package/src/modes/components/custom-editor.ts +14 -0
  195. package/src/modes/components/customization/customization-dashboard.ts +12 -0
  196. package/src/modes/components/extensions/extension-dashboard.ts +2 -2
  197. package/src/modes/components/extensions/state-manager.ts +15 -5
  198. package/src/modes/components/model-selector.ts +1 -4
  199. package/src/modes/components/provider-onboarding-selector.ts +1 -1
  200. package/src/modes/components/status-line/segments.ts +4 -0
  201. package/src/modes/controllers/event-controller.ts +11 -4
  202. package/src/modes/controllers/input-controller.ts +74 -29
  203. package/src/modes/controllers/runtime-mcp-command-controller.ts +8 -3
  204. package/src/modes/controllers/selector-controller.ts +28 -15
  205. package/src/modes/interactive-mode.ts +4 -0
  206. package/src/modes/runtime-init.ts +4 -0
  207. package/src/modes/utils/abort-message.ts +3 -1
  208. package/src/modes/utils/interrupt-ownership.ts +12 -0
  209. package/src/modes/utils/keybinding-matchers.ts +4 -0
  210. package/src/modes/utils/macos-option-forwarding.ts +80 -0
  211. package/src/progress/collect-project-progress.ts +153 -0
  212. package/src/progress/progress-contract.ts +172 -0
  213. package/src/progress/progress-snapshot.ts +140 -0
  214. package/src/progress/project-progress.ts +446 -0
  215. package/src/progress/render-progress.ts +209 -0
  216. package/src/prompts/system/master-mode.md +1 -0
  217. package/src/prompts/tools/skill-discovery.md +1 -1
  218. package/src/runtime-mcp/client.ts +13 -4
  219. package/src/runtime-mcp/config-writer.ts +10 -6
  220. package/src/runtime-mcp/config.ts +1 -0
  221. package/src/runtime-mcp/elicitation.ts +142 -0
  222. package/src/runtime-mcp/oauth-flow.ts +5 -0
  223. package/src/runtime-mcp/oauth-public-url.ts +8 -0
  224. package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
  225. package/src/runtime-mcp/smithery-registry.ts +22 -3
  226. package/src/runtime-mcp/transports/http.ts +2 -2
  227. package/src/runtime-mcp/types.ts +6 -0
  228. package/src/sdk/acp/adapter.ts +93 -8
  229. package/src/sdk/broker/broker-exit.ts +52 -23
  230. package/src/sdk/broker/broker.ts +83 -7
  231. package/src/sdk/broker/ensure.ts +2 -0
  232. package/src/sdk/broker/lifecycle.ts +820 -73
  233. package/src/sdk/broker/session-index.ts +191 -81
  234. package/src/sdk/broker/task-artifact-owner-validation.ts +259 -0
  235. package/src/sdk/bus/chat-daemon-cli.ts +38 -1
  236. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  237. package/src/sdk/bus/chat-daemon-runtime.ts +3 -0
  238. package/src/sdk/bus/index.ts +241 -46
  239. package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
  240. package/src/sdk/bus/telegram-daemon.ts +4 -2
  241. package/src/sdk/cli/session-cli.ts +17 -3
  242. package/src/sdk/client/client.ts +28 -8
  243. package/src/sdk/host/host.ts +6 -0
  244. package/src/sdk/host/query/handlers.ts +9 -1
  245. package/src/sdk/host/session-runtime.test.ts +2805 -411
  246. package/src/sdk/host/session-runtime.ts +428 -111
  247. package/src/sdk/host/surface-policy.ts +1 -0
  248. package/src/sdk/index.ts +8 -0
  249. package/src/sdk/lifecycle-session.ts +3 -1
  250. package/src/sdk/mcp/server.ts +15 -3
  251. package/src/sdk/prompt-deadline-manager.ts +15 -0
  252. package/src/sdk/protocol/operation-inventory.generated.json +146 -0
  253. package/src/sdk/protocol/operation-registry.ts +9 -2
  254. package/src/sdk/router/session-router.ts +94 -14
  255. package/src/sdk/session-list.ts +1 -0
  256. package/src/sdk/session.ts +81 -38
  257. package/src/session/agent-session.ts +674 -224
  258. package/src/session/artifacts.ts +174 -70
  259. package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
  260. package/src/session/internal/managed-session-scope.ts +3457 -462
  261. package/src/session/internal/managed-session-storage.ts +482 -61
  262. package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
  263. package/src/session/internal/task-artifact-owner-access.ts +245 -0
  264. package/src/session/internal/task-artifact-owner-transcript.ts +614 -0
  265. package/src/session/session-manager.ts +939 -133
  266. package/src/session/session-retirement.ts +714 -0
  267. package/src/session/session-storage.ts +418 -174
  268. package/src/session/session-transcript-header.ts +17 -0
  269. package/src/session/task-artifact-owner-codec.ts +641 -0
  270. package/src/session/task-artifact-owner-retirement.ts +477 -0
  271. package/src/session/task-artifact-owner.ts +289 -0
  272. package/src/setup/credential-auto-import.ts +12 -3
  273. package/src/setup/credential-import.ts +176 -5
  274. package/src/setup/paseo/announce.ts +6 -1
  275. package/src/skill-state/active-state.ts +132 -2
  276. package/src/slash-commands/builtin-registry.ts +39 -9
  277. package/src/slash-commands/helpers/progress-report.ts +23 -0
  278. package/src/stt/setup.ts +3 -5
  279. package/src/task/commands.ts +3 -1
  280. package/src/task/executor.ts +4 -0
  281. package/src/task/index.ts +6 -0
  282. package/src/tools/ask.ts +205 -6
  283. package/src/tools/ast-edit.ts +4 -0
  284. package/src/tools/bash-allowed-prefixes.ts +15 -0
  285. package/src/tools/bash-skill-urls.ts +56 -3
  286. package/src/tools/bash.ts +48 -79
  287. package/src/tools/descriptors.ts +5 -1
  288. package/src/tools/eval.ts +56 -36
  289. package/src/tools/image-gen.ts +1 -1
  290. package/src/tools/index.ts +3 -0
  291. package/src/tools/python.ts +187 -66
  292. package/src/tools/recipe/index.ts +9 -0
  293. package/src/tools/recipe/recipe-guard.ts +19 -0
  294. package/src/tools/restricted-cwd.ts +10 -0
  295. package/src/tools/skill-discovery.ts +5 -0
  296. package/src/tools/skill.ts +28 -1
  297. package/src/tools/todo-write.ts +10 -1
  298. package/src/tools/tool-catalog.generated.ts +1 -1
  299. package/src/tools/write.ts +1 -0
  300. package/src/utils/tools-checksum.ts +34 -0
  301. package/src/utils/tools-manager.ts +24 -6
  302. 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,2507 @@ 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
+ function managedGcRetirementTranscriptKey(transcriptPath: string): string {
2547
+ return createHash("sha256").update(path.resolve(transcriptPath), "utf8").digest("hex");
2548
+ }
2549
+ function managedGcRetirementReceiptRelativePath(
2550
+ transcriptPath: string,
2551
+ state: ManagedGcSessionRetirementState,
2552
+ attempt?: number,
2553
+ ): string {
2554
+ if (state === "owner_pending" && (!Number.isSafeInteger(attempt) || attempt === undefined || attempt < 1))
2555
+ throw new Error("task_artifact_owner_continuation_attempt_invalid");
2556
+ const suffix = state === "owner_pending" ? `${state}-${String(attempt).padStart(8, "0")}` : state;
2557
+ return `${MANAGED_GC_RETIREMENT_RECEIPTS}/${MANAGED_GC_RETIREMENT_PREFIX}${managedGcRetirementTranscriptKey(transcriptPath)}-${suffix}.json`;
2558
+ }
2559
+
2560
+ const MANAGED_GC_RECEIPT_MAX_BYTES = MANAGED_SESSION_READ_RANGE_MAX_BYTES;
2561
+
2562
+ interface ManagedGcDirectoryEvidence {
2563
+ readonly dev: string;
2564
+ readonly ino: string;
2565
+ readonly mtimeNs: string;
2566
+ readonly ctimeNs: string;
2567
+ readonly mode: string;
2568
+ }
2569
+
2570
+ interface ManagedGcDirectoryInventory {
2571
+ readonly evidence: ManagedGcDirectoryEvidence;
2572
+ readonly entryCount: number;
2573
+ readonly entries: readonly fs.Dirent[];
2574
+ }
2575
+
2576
+ interface ManagedGcReceiptHistory {
2577
+ readonly entryCount: number;
2578
+ readonly encodedBytes: number;
2579
+ readonly directoryEntryCount: number;
2580
+ readonly directoryEvidence: ManagedGcDirectoryEvidence;
2581
+ readonly directoryInventory: ManagedGcDirectoryInventory;
2582
+ }
2583
+
2584
+ interface ManagedGcDiscoveryScopeFence {
2585
+ readonly scope: ManagedScope;
2586
+ readonly scopeIdentity: { readonly dev: string; readonly ino: string };
2587
+ readonly binding: ManagedFileSnapshot;
2588
+ readonly internalDirectoryIdentity: { readonly dev: string; readonly ino: string };
2589
+ readonly receiptInventory: ManagedGcDirectoryInventory | undefined;
2590
+ readonly openReader: () => ManagedSessionDescendantStore;
2591
+ }
2592
+
2593
+ interface MutableManagedGcReceiptHistory {
2594
+ encodedBytes: number;
2595
+ readonly admittedFiles: Set<string>;
2596
+ }
2597
+
2598
+ function managedGcJournalCapacity(): never {
2599
+ throw new Error("managed_gc_journal_capacity_exceeded");
2600
+ }
2601
+
2602
+ function managedGcDirectoryEvidence(stat: fs.BigIntStats): ManagedGcDirectoryEvidence {
2603
+ return {
2604
+ dev: stat.dev.toString(),
2605
+ ino: stat.ino.toString(),
2606
+ mtimeNs: stat.mtimeNs.toString(),
2607
+ ctimeNs: stat.ctimeNs.toString(),
2608
+ mode: stat.mode.toString(),
2609
+ };
2610
+ }
2611
+
2612
+ function managedGcSameDirectoryEvidence(left: ManagedGcDirectoryEvidence, right: ManagedGcDirectoryEvidence): boolean {
2613
+ return (
2614
+ left.dev === right.dev &&
2615
+ left.ino === right.ino &&
2616
+ left.mtimeNs === right.mtimeNs &&
2617
+ left.ctimeNs === right.ctimeNs &&
2618
+ left.mode === right.mode
2619
+ );
2620
+ }
2621
+
2622
+ /** Count the selected family before retaining a bounded inventory, preserving unrelated role entries. */
2623
+ function managedGcStreamDirectoryInventory(
2624
+ pathname: string,
2625
+ countEntry: (entry: fs.Dirent) => boolean,
2626
+ retain: (entry: fs.Dirent) => boolean,
2627
+ capacity: () => never = managedGcJournalCapacity,
2628
+ ): ManagedGcDirectoryInventory {
2629
+ const beforeStat = fs.lstatSync(pathname, { bigint: true });
2630
+ if (!beforeStat.isDirectory() || beforeStat.isSymbolicLink()) throw new Error("managed_gc_scope_authority_mismatch");
2631
+ const before = managedGcDirectoryEvidence(beforeStat);
2632
+ const scan = (collect: boolean): { count: number; entries: fs.Dirent[] } => {
2633
+ const entries: fs.Dirent[] = [];
2634
+ let count = 0;
2635
+ let retained = 0;
2636
+ const directory = fs.opendirSync(pathname);
2637
+ try {
2638
+ for (let entry = directory.readSync(); entry; entry = directory.readSync()) {
2639
+ if (countEntry(entry) && ++count > MANAGED_ARTIFACT_MAX_FILES) capacity();
2640
+ if (!retain(entry)) continue;
2641
+ if (++retained > MANAGED_ARTIFACT_MAX_FILES) capacity();
2642
+ if (collect) entries.push(entry);
2643
+ }
2644
+ } finally {
2645
+ directory.closeSync();
2646
+ }
2647
+ return { count, entries };
2648
+ };
2649
+ const counted = scan(false);
2650
+ const afterCount = fs.lstatSync(pathname, { bigint: true });
2651
+ if (
2652
+ !afterCount.isDirectory() ||
2653
+ afterCount.isSymbolicLink() ||
2654
+ !managedGcSameDirectoryEvidence(before, managedGcDirectoryEvidence(afterCount))
2655
+ )
2656
+ throw new Error("managed_gc_scope_authority_mismatch");
2657
+ const collected = scan(true);
2658
+ const afterCollect = fs.lstatSync(pathname, { bigint: true });
2659
+ if (
2660
+ collected.count !== counted.count ||
2661
+ !afterCollect.isDirectory() ||
2662
+ afterCollect.isSymbolicLink() ||
2663
+ !managedGcSameDirectoryEvidence(before, managedGcDirectoryEvidence(afterCollect))
2664
+ )
2665
+ throw new Error("managed_gc_scope_authority_mismatch");
2666
+ collected.entries.sort((left, right) => left.name.localeCompare(right.name));
2667
+ return { evidence: before, entryCount: counted.count, entries: collected.entries };
2668
+ }
2669
+
2670
+ function managedGcReceiptDirectoryInventory(
2671
+ scope: ManagedScope,
2672
+ store: ManagedSessionDescendantStore,
2673
+ ): ManagedGcDirectoryInventory {
2674
+ const relativePath = MANAGED_GC_RETIREMENT_RECEIPTS;
2675
+ const pathname = path.join(scope.directoryPath, relativePath);
2676
+ const before = store.captureDirectoryIdentity(relativePath);
2677
+ const inventory = managedGcStreamDirectoryInventory(
2678
+ pathname,
2679
+ entry => entry.name.startsWith(MANAGED_GC_RETIREMENT_PREFIX),
2680
+ entry => entry.name.startsWith(MANAGED_GC_RETIREMENT_PREFIX),
2681
+ );
2682
+ const after = store.captureDirectoryIdentity(relativePath);
2683
+ if (
2684
+ before.dev !== after.dev ||
2685
+ before.ino !== after.ino ||
2686
+ before.dev !== inventory.evidence.dev ||
2687
+ before.ino !== inventory.evidence.ino
2688
+ )
2689
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
2690
+ store.assertBound();
2691
+ return inventory;
2692
+ }
2693
+
2694
+ function managedGcReceiptCapacity(): never {
2695
+ throw new Error("managed_gc_receipt_capacity_exceeded");
2696
+ }
2697
+
2698
+ function managedGcJsonStringByteLength(value: string, add: (amount: number) => void): void {
2699
+ add(2);
2700
+ for (let index = 0; index < value.length; index++) {
2701
+ const unit = value.charCodeAt(index);
2702
+ if (unit === 0x22 || unit === 0x5c) add(2);
2703
+ else if (unit < 0x20)
2704
+ add(unit === 0x08 || unit === 0x09 || unit === 0x0a || unit === 0x0c || unit === 0x0d ? 2 : 6);
2705
+ else if (unit >= 0xd800 && unit <= 0xdbff) {
2706
+ const low = value.charCodeAt(index + 1);
2707
+ if (low >= 0xdc00 && low <= 0xdfff) {
2708
+ add(4);
2709
+ index++;
2710
+ } else add(6);
2711
+ } else if (unit >= 0xdc00 && unit <= 0xdfff) add(6);
2712
+ else if (unit <= 0x7f) add(1);
2713
+ else if (unit <= 0x7ff) add(2);
2714
+ else add(3);
2715
+ }
2716
+ }
2717
+
2718
+ /** Measure the canonical JSON encoding before JSON.stringify or Buffer allocation. */
2719
+ function managedGcJsonByteLength(value: unknown): number {
2720
+ let bytes = 0;
2721
+ const active = new Set<object>();
2722
+ const add = (amount: number): void => {
2723
+ if (!Number.isSafeInteger(amount) || amount < 0 || amount > MANAGED_GC_RECEIPT_MAX_BYTES - bytes)
2724
+ managedGcReceiptCapacity();
2725
+ bytes += amount;
2726
+ };
2727
+ const visit = (item: unknown, depth: number, inArray = false): void => {
2728
+ if (depth > 32) throw new Error("task_artifact_owner_continuation_corrupt");
2729
+ if (item === null) {
2730
+ add(4);
2731
+ return;
2732
+ }
2733
+ if (typeof item === "string") {
2734
+ managedGcJsonStringByteLength(item, add);
2735
+ return;
2736
+ }
2737
+ if (typeof item === "bigint") {
2738
+ managedGcJsonStringByteLength(item.toString(), add);
2739
+ return;
2740
+ }
2741
+ if (typeof item === "boolean") {
2742
+ add(item ? 4 : 5);
2743
+ return;
2744
+ }
2745
+ if (typeof item === "number") {
2746
+ add(Number.isFinite(item) ? (Object.is(item, -0) ? 1 : String(item).length) : 4);
2747
+ return;
2748
+ }
2749
+ if (typeof item === "undefined" || typeof item === "function" || typeof item === "symbol") {
2750
+ if (inArray) add(4);
2751
+ return;
2752
+ }
2753
+ if (typeof item !== "object") managedGcReceiptCapacity();
2754
+ if (active.has(item)) throw new Error("task_artifact_owner_continuation_corrupt");
2755
+ active.add(item);
2756
+ const array = Array.isArray(item);
2757
+ if (array) {
2758
+ const values = item as unknown[];
2759
+ if (values.length > MANAGED_GC_RECEIPT_MAX_BYTES) managedGcReceiptCapacity();
2760
+ add(2);
2761
+ let emitted = false;
2762
+ for (let index = 0; index < values.length; index++) {
2763
+ if (emitted) add(1);
2764
+ emitted = true;
2765
+ visit(values[index], depth + 1, true);
2766
+ }
2767
+ } else {
2768
+ add(2);
2769
+ let emitted = false;
2770
+ for (const key in item) {
2771
+ if (!Object.hasOwn(item, key)) continue;
2772
+ const child = (item as Record<string, unknown>)[key];
2773
+ if (child === undefined || typeof child === "function" || typeof child === "symbol") continue;
2774
+ if (emitted) add(1);
2775
+ emitted = true;
2776
+ managedGcJsonStringByteLength(key, add);
2777
+ add(1);
2778
+ visit(child, depth + 1);
2779
+ }
2780
+ }
2781
+ active.delete(item);
2782
+ };
2783
+ visit(value, 0);
2784
+ add(1);
2785
+ return bytes;
2786
+ }
2787
+
2788
+ function validateManagedGcTranscriptPath(scope: ManagedScope, transcriptPath: string): string {
2789
+ if (
2790
+ typeof transcriptPath !== "string" ||
2791
+ !path.isAbsolute(transcriptPath) ||
2792
+ path.resolve(transcriptPath) !== transcriptPath ||
2793
+ path.dirname(transcriptPath) !== scope.directoryPath ||
2794
+ path.extname(transcriptPath) !== ".jsonl" ||
2795
+ !pathIsWithin(scope.sessionsRoot, transcriptPath)
2796
+ )
2797
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
2798
+ return transcriptPath;
2799
+ }
2800
+ function managedGcTargetFromSnapshot(
2801
+ scope: ManagedScope,
2802
+ transcriptPath: string,
2803
+ snapshot: SessionStorageSnapshot,
2804
+ ): ManagedGcSessionRetirementTarget {
2805
+ if (
2806
+ !snapshot.stat.isFile ||
2807
+ snapshot.stat.nlink !== 1n ||
2808
+ snapshot.stat.size !== snapshot.bytes.byteLength ||
2809
+ !Number.isSafeInteger(snapshot.stat.size) ||
2810
+ snapshot.stat.size < 0
2811
+ )
2812
+ throw new Error("task_artifact_owner_continuation_identity_invalid");
2813
+ const header = parseFirstJsonlLine(snapshot.bytes);
2814
+ if (
2815
+ header?.type !== "session" ||
2816
+ typeof header.id !== "string" ||
2817
+ header.id.length === 0 ||
2818
+ typeof header.cwd !== "string" ||
2819
+ header.cwd.length === 0
2820
+ )
2821
+ throw new Error("task_artifact_owner_transcript_header_invalid");
2822
+ const cwdIdentity = identityFor(header.cwd);
2823
+ if (!cwdIdentity.ok || cwdIdentity.platform !== scope.platform || cwdIdentity.canonicalPath !== scope.canonicalCwd)
2824
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
2825
+ const locator = taskArtifactOwnerLocatorFromTranscriptBytes(snapshot.bytes, header.id);
2826
+ if (!locator) throw new Error("task_artifact_owner_locator_missing");
2827
+ return Object.freeze({
2828
+ transcriptPath,
2829
+ sessionId: header.id,
2830
+ cwd: header.cwd,
2831
+ transcriptIdentity: {
2832
+ dev: snapshot.stat.dev,
2833
+ ino: snapshot.stat.ino,
2834
+ nlink: snapshot.stat.nlink,
2835
+ size: snapshot.stat.size,
2836
+ mtimeNs: snapshot.stat.mtimeNs,
2837
+ sha256: createHash("sha256").update(snapshot.bytes).digest("hex"),
2838
+ },
2839
+ taskArtifactOwnerLocator: locator,
2840
+ });
2841
+ }
2842
+ /** Bind the first transcript header through FileSessionStorage's descriptor-bound, no-follow snapshot. */
2843
+ export function bindManagedGcSessionRetirementTarget(
2844
+ scope: ManagedScope,
2845
+ transcriptPathValue: string,
2846
+ ): ManagedGcSessionRetirementTarget {
2847
+ const trusted = managedGcTrustedScope(scope);
2848
+ const transcriptPath = validateManagedGcTranscriptPath(scope, transcriptPathValue);
2849
+ const store = managedGcScopeReader(scope, trusted);
2850
+ try {
2851
+ store.verifyRootSecurity();
2852
+ store.assertBound();
2853
+ const snapshot = new FileSessionStorage().readSnapshotSync(transcriptPath);
2854
+ store.assertBound();
2855
+ return managedGcTargetFromSnapshot(scope, transcriptPath, snapshot);
2856
+ } finally {
2857
+ store.close();
2858
+ }
2859
+ }
2860
+ function sameManagedGcTarget(left: ManagedGcSessionRetirementTarget, right: ManagedGcSessionRetirementTarget): boolean {
2861
+ return (
2862
+ left.transcriptPath === right.transcriptPath &&
2863
+ left.sessionId === right.sessionId &&
2864
+ left.cwd === right.cwd &&
2865
+ sameIdentity(left.transcriptIdentity, right.transcriptIdentity) &&
2866
+ util.isDeepStrictEqual(left.taskArtifactOwnerLocator, right.taskArtifactOwnerLocator)
2867
+ );
2868
+ }
2869
+ function assertManagedGcTranscriptUnchangedOrAbsent(
2870
+ scope: ManagedScope,
2871
+ target: ManagedGcSessionRetirementTarget,
2872
+ allowAbsent: boolean,
2873
+ ): void {
2874
+ try {
2875
+ const current = bindManagedGcSessionRetirementTarget(scope, target.transcriptPath);
2876
+ if (!sameManagedGcTarget(current, target))
2877
+ throw new Error("task_artifact_owner_continuation_transcript_replaced");
2878
+ } catch (error) {
2879
+ if (allowAbsent && hasFsCode(error, "ENOENT")) return;
2880
+ throw error;
2881
+ }
2882
+ }
2883
+ function managedGcReceiptRecord(
2884
+ scope: ManagedScope,
2885
+ receipt: ManagedGcSessionRetirementReceipt,
2886
+ attempt?: number,
2887
+ ): Record<string, unknown> {
2888
+ const state = receipt.state;
2889
+ const common = {
2890
+ schemaVersion: 1,
2891
+ state,
2892
+ scope: scopeDigest(scope.platform, scope.canonicalCwd),
2893
+ transcriptPath: receipt.transcriptPath,
2894
+ sessionId: receipt.sessionId,
2895
+ cwd: receipt.cwd,
2896
+ transcriptIdentity: managedGcRetirementIdentityRecord(receipt.transcriptIdentity),
2897
+ taskArtifactOwnerDeletionEvidence: receipt.taskArtifactOwnerDeletionEvidence,
2898
+ };
2899
+ const hasOutcomeFields =
2900
+ receipt.ownerRetirementAttempt !== undefined ||
2901
+ receipt.taskArtifactOwnerRetirementOutcome !== undefined ||
2902
+ receipt.taskArtifactOwnerRetirementContinuation !== undefined ||
2903
+ receipt.taskArtifactOwnerPayloadRetired !== undefined ||
2904
+ receipt.taskArtifactOwnerNamespaceRetained !== undefined ||
2905
+ receipt.taskArtifactOwnerRetired !== undefined;
2906
+ if (state === "prepared" || state === "artifacts_removed") {
2907
+ if (
2908
+ hasOutcomeFields ||
2909
+ (state === "prepared" && receipt.artifactsRemoved !== undefined) ||
2910
+ (state === "artifacts_removed" && receipt.artifactsRemoved !== undefined && receipt.artifactsRemoved !== true)
2911
+ )
2912
+ throw new Error("task_artifact_owner_continuation_state_mismatch");
2913
+ return state === "prepared" ? common : { ...common, artifactsRemoved: true };
2914
+ }
2915
+ if (state === "owner_pending") {
2916
+ const outcome = receipt.taskArtifactOwnerRetirementOutcome;
2917
+ if (
2918
+ !outcome ||
2919
+ outcome.kind === "completed" ||
2920
+ (receipt.artifactsRemoved !== undefined && receipt.artifactsRemoved !== true) ||
2921
+ receipt.taskArtifactOwnerRetired !== undefined ||
2922
+ (attempt !== undefined &&
2923
+ receipt.ownerRetirementAttempt !== undefined &&
2924
+ receipt.ownerRetirementAttempt !== attempt) ||
2925
+ (receipt.taskArtifactOwnerRetirementContinuation !== undefined &&
2926
+ !util.isDeepStrictEqual(receipt.taskArtifactOwnerRetirementContinuation, outcome.continuation)) ||
2927
+ (receipt.taskArtifactOwnerPayloadRetired !== undefined &&
2928
+ receipt.taskArtifactOwnerPayloadRetired !== (outcome.kind === "payload_retired")) ||
2929
+ (receipt.taskArtifactOwnerNamespaceRetained !== undefined &&
2930
+ receipt.taskArtifactOwnerNamespaceRetained !== (outcome.kind === "payload_retired"))
2931
+ )
2932
+ throw new Error("task_artifact_owner_continuation_outcome_mismatch");
2933
+ if (!Number.isSafeInteger(attempt) || attempt === undefined || attempt < 1)
2934
+ throw new Error("task_artifact_owner_continuation_attempt_invalid");
2935
+ return {
2936
+ ...common,
2937
+ ownerRetirementAttempt: attempt,
2938
+ artifactsRemoved: true,
2939
+ taskArtifactOwnerRetirementOutcome: outcome,
2940
+ taskArtifactOwnerRetirementContinuation: outcome.continuation,
2941
+ ...(outcome.kind === "payload_retired"
2942
+ ? { taskArtifactOwnerPayloadRetired: true, taskArtifactOwnerNamespaceRetained: true }
2943
+ : {}),
2944
+ };
2945
+ }
2946
+ if (
2947
+ (receipt.artifactsRemoved !== undefined && receipt.artifactsRemoved !== true) ||
2948
+ hasOutcomeFieldsExceptOutcome(receipt) ||
2949
+ (receipt.taskArtifactOwnerRetired !== undefined && receipt.taskArtifactOwnerRetired !== true) ||
2950
+ receipt.taskArtifactOwnerRetirementOutcome?.kind !== "completed"
2951
+ )
2952
+ throw new Error("task_artifact_owner_continuation_outcome_mismatch");
2953
+ return {
2954
+ ...common,
2955
+ artifactsRemoved: true,
2956
+ taskArtifactOwnerRetired: true,
2957
+ taskArtifactOwnerRetirementOutcome: receipt.taskArtifactOwnerRetirementOutcome,
2958
+ };
2959
+ }
2960
+
2961
+ function hasOutcomeFieldsExceptOutcome(receipt: ManagedGcSessionRetirementReceipt): boolean {
2962
+ return (
2963
+ receipt.ownerRetirementAttempt !== undefined ||
2964
+ receipt.taskArtifactOwnerRetirementContinuation !== undefined ||
2965
+ receipt.taskArtifactOwnerPayloadRetired !== undefined ||
2966
+ receipt.taskArtifactOwnerNamespaceRetained !== undefined
2967
+ );
2968
+ }
2969
+ function readManagedGcReceiptSnapshot(
2970
+ store: ManagedSessionDescendantStore,
2971
+ relativePath: string,
2972
+ history?: MutableManagedGcReceiptHistory,
2973
+ ): ManagedFileSnapshot | null {
2974
+ try {
2975
+ return store.readExpectedBounded(relativePath, MANAGED_GC_RECEIPT_MAX_BYTES, (size, descriptor) => {
2976
+ if (descriptor.nlink !== 1n || descriptor.size !== size) throw new Error("source_changed");
2977
+ if (history) managedGcAdmitHistoryBytes(history, descriptor.size, relativePath);
2978
+ });
2979
+ } catch (error) {
2980
+ if (error instanceof Error && error.message === "artifact_capacity_exceeded") managedGcJournalCapacity();
2981
+ throw error;
2982
+ }
2983
+ }
2984
+
2985
+ function managedGcAdmitHistoryBytes(history: MutableManagedGcReceiptHistory, size: number, relativePath: string): void {
2986
+ if (history.admittedFiles.has(relativePath)) return;
2987
+ if (history.admittedFiles.size >= MANAGED_ARTIFACT_MAX_FILES) managedGcJournalCapacity();
2988
+ if (size > MANAGED_ARTIFACT_MAX_TOTAL_BYTES - history.encodedBytes) managedGcJournalCapacity();
2989
+ history.encodedBytes += size;
2990
+ history.admittedFiles.add(relativePath);
2991
+ }
2992
+
2993
+ function parseManagedGcReceiptFile(
2994
+ store: ManagedSessionDescendantStore,
2995
+ relativePath: string,
2996
+ scope: ManagedScope,
2997
+ ownerContext: TaskArtifactOwnerStorageContext,
2998
+ target: ManagedGcSessionRetirementTarget,
2999
+ history?: MutableManagedGcReceiptHistory,
3000
+ ): ManagedGcSessionRetirementReceipt | undefined {
3001
+ const snapshot = readManagedGcReceiptSnapshot(store, relativePath, history);
3002
+ if (!snapshot) return undefined;
3003
+ let value: unknown;
3004
+ try {
3005
+ value = JSON.parse(snapshot.bytes.toString("utf8"));
3006
+ } catch {
3007
+ throw new Error("task_artifact_owner_continuation_corrupt");
3008
+ }
3009
+ return parseManagedGcRetirementReceipt(value, scopeDigest(scope.platform, scope.canonicalCwd), ownerContext, target);
3010
+ }
3011
+ function targetFromPreparedRecord(value: unknown, transcriptPath: string): ManagedGcSessionRetirementTarget {
3012
+ if (!value || typeof value !== "object" || Array.isArray(value))
3013
+ throw new Error("task_artifact_owner_continuation_corrupt");
3014
+ const record = value as Record<string, unknown>;
3015
+ if (
3016
+ record.state !== "prepared" ||
3017
+ record.transcriptPath !== transcriptPath ||
3018
+ typeof record.sessionId !== "string" ||
3019
+ typeof record.cwd !== "string"
3020
+ )
3021
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
3022
+ const evidence = parseTaskArtifactOwnerDeletionEvidence(record.taskArtifactOwnerDeletionEvidence);
3023
+ return {
3024
+ transcriptPath,
3025
+ sessionId: record.sessionId,
3026
+ cwd: record.cwd,
3027
+ transcriptIdentity: managedGcRetirementIdentityValue(record.transcriptIdentity),
3028
+ taskArtifactOwnerLocator: evidence.locator,
3029
+ };
3030
+ }
3031
+ function deepSame(left: unknown, right: unknown): boolean {
3032
+ return util.isDeepStrictEqual(left, right);
3033
+ }
3034
+ function readManagedGcPreparedAuthority(
3035
+ scope: ManagedScope,
3036
+ transcriptPath: string,
3037
+ ownerContext: TaskArtifactOwnerStorageContext,
3038
+ store: ManagedSessionDescendantStore,
3039
+ history?: MutableManagedGcReceiptHistory,
3040
+ bootstrap?: {
3041
+ readonly snapshot: ManagedFileSnapshot;
3042
+ readonly value: unknown;
3043
+ readonly history: MutableManagedGcReceiptHistory;
3044
+ },
3045
+ ): ManagedGcSessionRetirementReceipt | undefined {
3046
+ const snapshot =
3047
+ bootstrap?.snapshot ??
3048
+ readManagedGcReceiptSnapshot(store, managedGcRetirementReceiptRelativePath(transcriptPath, "prepared"), history);
3049
+ if (!snapshot) return undefined;
3050
+ let value: unknown;
3051
+ if (bootstrap) {
3052
+ value = bootstrap.value;
3053
+ if (history && history !== bootstrap.history)
3054
+ managedGcAdmitHistoryBytes(
3055
+ history,
3056
+ snapshot.identity.size,
3057
+ managedGcRetirementReceiptRelativePath(transcriptPath, "prepared"),
3058
+ );
3059
+ } else {
3060
+ try {
3061
+ value = JSON.parse(snapshot.bytes.toString("utf8"));
3062
+ } catch {
3063
+ throw new Error("task_artifact_owner_continuation_corrupt");
3064
+ }
3065
+ }
3066
+ const target = targetFromPreparedRecord(value, transcriptPath);
3067
+ const prepared = parseManagedGcRetirementReceipt(
3068
+ value,
3069
+ scopeDigest(scope.platform, scope.canonicalCwd),
3070
+ ownerContext,
3071
+ target,
3072
+ );
3073
+ if (prepared.state !== "prepared") throw new Error("task_artifact_owner_continuation_state_missing");
3074
+ return prepared;
3075
+ }
3076
+ function assertSameManagedGcImmutableAuthority(
3077
+ prepared: ManagedGcSessionRetirementReceipt,
3078
+ next: ManagedGcSessionRetirementReceipt,
3079
+ ): void {
3080
+ if (
3081
+ !sameManagedGcTarget(prepared, next) ||
3082
+ !deepSame(prepared.taskArtifactOwnerDeletionEvidence, next.taskArtifactOwnerDeletionEvidence)
3083
+ )
3084
+ throw new Error("task_artifact_owner_continuation_evidence_mismatch");
3085
+ }
3086
+ function readManagedGcSessionRetirementReceiptUnlocked(
3087
+ scope: ManagedScope,
3088
+ transcriptPathValue: string,
3089
+ trusted: ManagedGcTrustedScope,
3090
+ store: ManagedSessionDescendantStore,
3091
+ lock?: ManagedStorageLock,
3092
+ inventory?: ManagedGcDirectoryInventory,
3093
+ targetNames?: readonly string[],
3094
+ preparedBootstrap?: {
3095
+ readonly snapshot: ManagedFileSnapshot;
3096
+ readonly value: unknown;
3097
+ readonly history: MutableManagedGcReceiptHistory;
3098
+ },
3099
+ ): {
3100
+ readonly receipt: ManagedGcSessionRetirementReceipt | undefined;
3101
+ readonly prepared: ManagedGcSessionRetirementReceipt | undefined;
3102
+ readonly history: ManagedGcReceiptHistory;
3103
+ } {
3104
+ const transcriptPath = validateManagedGcTranscriptPath(scope, transcriptPathValue);
3105
+ lock?.assertOwned();
3106
+ store.assertBound();
3107
+ const receiptInventory = inventory ?? managedGcReceiptDirectoryInventory(scope, store);
3108
+ const names = targetNames ?? managedGcReceiptNames(scope, transcriptPath, store, receiptInventory);
3109
+ if (names.length > MANAGED_ARTIFACT_MAX_FILES) managedGcJournalCapacity();
3110
+ const prefix = `${MANAGED_GC_RETIREMENT_PREFIX}${managedGcRetirementTranscriptKey(transcriptPath)}-`;
3111
+ const pendingPrefix = `${prefix}owner_pending-`;
3112
+ const historyBytes = preparedBootstrap?.history ?? { encodedBytes: 0, admittedFiles: new Set<string>() };
3113
+ const history = (): ManagedGcReceiptHistory => ({
3114
+ entryCount: names.length,
3115
+ encodedBytes: historyBytes.encodedBytes,
3116
+ directoryEntryCount: receiptInventory.entryCount,
3117
+ directoryEvidence: receiptInventory.evidence,
3118
+ directoryInventory: receiptInventory,
3119
+ });
3120
+ const preparedName = `${prefix}prepared.json`;
3121
+ if (!names.includes(preparedName)) {
3122
+ if (names.length > 0) throw new Error("task_artifact_owner_continuation_state_missing");
3123
+ if (!inventory) {
3124
+ const after = managedGcReceiptDirectoryInventory(scope, store);
3125
+ if (!managedGcSameReceiptDirectoryInventory(receiptInventory, after))
3126
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
3127
+ }
3128
+ return { receipt: undefined, prepared: undefined, history: history() };
3129
+ }
3130
+ const prepared = readManagedGcPreparedAuthority(
3131
+ scope,
3132
+ transcriptPath,
3133
+ trusted.ownerContext,
3134
+ store,
3135
+ historyBytes,
3136
+ preparedBootstrap,
3137
+ );
3138
+ if (!prepared) {
3139
+ throw new Error("task_artifact_owner_continuation_state_missing");
3140
+ }
3141
+ const target = prepared;
3142
+ assertManagedGcTranscriptUnchangedOrAbsent(scope, target, true);
3143
+ const nameSet = new Set(names);
3144
+ if (!nameSet.has(preparedName)) throw new Error("task_artifact_owner_continuation_state_missing");
3145
+ const readState = (state: ManagedGcSessionRetirementState, attempt?: number) =>
3146
+ parseManagedGcReceiptFile(
3147
+ store,
3148
+ managedGcRetirementReceiptRelativePath(transcriptPath, state, attempt),
3149
+ scope,
3150
+ trusted.ownerContext,
3151
+ target,
3152
+ historyBytes,
3153
+ );
3154
+ const artifactsName = path.posix.basename(
3155
+ managedGcRetirementReceiptRelativePath(transcriptPath, "artifacts_removed"),
3156
+ );
3157
+ const retiredName = path.posix.basename(managedGcRetirementReceiptRelativePath(transcriptPath, "owner_retired"));
3158
+ const artifactsRemoved = nameSet.has(artifactsName) ? readState("artifacts_removed") : undefined;
3159
+ const ownerRetired = nameSet.has(retiredName) ? readState("owner_retired") : undefined;
3160
+ if (
3161
+ (nameSet.has(artifactsName) && artifactsRemoved?.state !== "artifacts_removed") ||
3162
+ (nameSet.has(retiredName) && ownerRetired?.state !== "owner_retired")
3163
+ )
3164
+ throw new Error("task_artifact_owner_continuation_state_missing");
3165
+ const attempts = names
3166
+ .filter(name => name.startsWith(pendingPrefix))
3167
+ .map(name => {
3168
+ const suffix = name.slice(pendingPrefix.length);
3169
+ if (!/^[0-9]{8,}\.json$/u.test(suffix)) throw new Error("task_artifact_owner_continuation_corrupt");
3170
+ const attempt = Number(suffix.slice(0, -5));
3171
+ if (!Number.isSafeInteger(attempt) || attempt < 1 || `${String(attempt).padStart(8, "0")}.json` !== suffix)
3172
+ throw new Error("task_artifact_owner_continuation_corrupt");
3173
+ return attempt;
3174
+ })
3175
+ .sort((left, right) => left - right);
3176
+ let previousPending: ManagedGcSessionRetirementReceipt | undefined;
3177
+ let latestPending: ManagedGcSessionRetirementReceipt | undefined;
3178
+ for (let index = 0; index < attempts.length; index++) {
3179
+ const attempt = attempts[index]!;
3180
+ const pending = readState("owner_pending", attempt);
3181
+ if (
3182
+ attempt !== index + 1 ||
3183
+ !pending ||
3184
+ pending.state !== "owner_pending" ||
3185
+ pending.ownerRetirementAttempt !== attempt
3186
+ )
3187
+ throw new Error("task_artifact_owner_continuation_state_missing");
3188
+ assertSameManagedGcImmutableAuthority(prepared, pending);
3189
+ if (previousPending) {
3190
+ const previous = previousPending.taskArtifactOwnerRetirementOutcome;
3191
+ const next = pending.taskArtifactOwnerRetirementOutcome;
3192
+ if (
3193
+ !previous ||
3194
+ previous.kind === "completed" ||
3195
+ !next ||
3196
+ next.kind === "completed" ||
3197
+ !continuationExtendsPrevious(previous.continuation, next.continuation)
3198
+ )
3199
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
3200
+ }
3201
+ previousPending = pending;
3202
+ latestPending = pending;
3203
+ }
3204
+ if ((attempts.length || ownerRetired) && !artifactsRemoved)
3205
+ throw new Error("task_artifact_owner_continuation_state_missing");
3206
+ if (artifactsRemoved) assertSameManagedGcImmutableAuthority(prepared, artifactsRemoved);
3207
+ if (ownerRetired) assertSameManagedGcImmutableAuthority(prepared, ownerRetired);
3208
+ if (ownerRetired) {
3209
+ const outcome = ownerRetired.taskArtifactOwnerRetirementOutcome;
3210
+ if (outcome?.kind !== "completed") throw new Error("task_artifact_owner_continuation_outcome_mismatch");
3211
+ verifyTaskArtifactOwnerPhysicalRetirement(
3212
+ trusted.ownerContext,
3213
+ ownerRetired.taskArtifactOwnerDeletionEvidence,
3214
+ outcome,
3215
+ );
3216
+ }
3217
+ if (!inventory) {
3218
+ const after = managedGcReceiptDirectoryInventory(scope, store);
3219
+ if (!managedGcSameReceiptDirectoryInventory(receiptInventory, after))
3220
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
3221
+ }
3222
+ return { receipt: ownerRetired ?? latestPending ?? artifactsRemoved ?? prepared, prepared, history: history() };
3223
+ }
3224
+
3225
+ function managedGcReceiptNames(
3226
+ scope: ManagedScope,
3227
+ transcriptPath: string,
3228
+ store: ManagedSessionDescendantStore,
3229
+ inventory?: ManagedGcDirectoryInventory,
3230
+ ): string[] {
3231
+ const prefix = `${MANAGED_GC_RETIREMENT_PREFIX}${managedGcRetirementTranscriptKey(transcriptPath)}-`;
3232
+ const matching = (inventory ?? managedGcReceiptDirectoryInventory(scope, store)).entries.filter(entry =>
3233
+ entry.name.startsWith(prefix),
3234
+ );
3235
+ if (matching.some(entry => !entry.isFile() || entry.isSymbolicLink()))
3236
+ throw new Error("task_artifact_owner_continuation_corrupt");
3237
+ const names = matching.map(entry => entry.name);
3238
+ const pendingPrefix = `${prefix}owner_pending-`;
3239
+ for (const name of names) {
3240
+ if (
3241
+ name !== `${prefix}prepared.json` &&
3242
+ name !== `${prefix}artifacts_removed.json` &&
3243
+ name !== `${prefix}owner_retired.json` &&
3244
+ !name.startsWith(pendingPrefix)
3245
+ )
3246
+ throw new Error("task_artifact_owner_continuation_corrupt");
3247
+ }
3248
+ return names;
3249
+ }
3250
+
3251
+ function managedGcSameReceiptDirectoryInventory(
3252
+ left: ManagedGcDirectoryInventory,
3253
+ right: ManagedGcDirectoryInventory,
3254
+ ): boolean {
3255
+ return (
3256
+ left.entryCount === right.entryCount &&
3257
+ managedGcSameDirectoryEvidence(left.evidence, right.evidence) &&
3258
+ left.entries.length === right.entries.length &&
3259
+ left.entries.every(
3260
+ (entry, index) =>
3261
+ entry.name === right.entries[index]?.name &&
3262
+ entry.isDirectory() === right.entries[index]?.isDirectory() &&
3263
+ entry.isFile() === right.entries[index]?.isFile() &&
3264
+ entry.isSymbolicLink() === right.entries[index]?.isSymbolicLink() &&
3265
+ entry.isBlockDevice() === right.entries[index]?.isBlockDevice() &&
3266
+ entry.isCharacterDevice() === right.entries[index]?.isCharacterDevice() &&
3267
+ entry.isFIFO() === right.entries[index]?.isFIFO() &&
3268
+ entry.isSocket() === right.entries[index]?.isSocket(),
3269
+ )
3270
+ );
3271
+ }
3272
+
3273
+ function managedGcOptionalReceiptDirectoryInventory(
3274
+ scope: ManagedScope,
3275
+ store: ManagedSessionDescendantStore,
3276
+ ): ManagedGcDirectoryInventory | undefined {
3277
+ try {
3278
+ store.captureDirectoryIdentity(MANAGED_GC_RETIREMENT_RECEIPTS);
3279
+ } catch (error) {
3280
+ if (hasFsCode(error, "ENOENT")) return undefined;
3281
+ throw error;
3282
+ }
3283
+ return managedGcReceiptDirectoryInventory(scope, store);
3284
+ }
3285
+
3286
+ function managedGcDiscoveryScopeFence(
3287
+ scope: ManagedScope,
3288
+ store: ManagedSessionDescendantStore,
3289
+ scopeIdentity: { readonly dev: string; readonly ino: string },
3290
+ binding: ManagedFileSnapshot,
3291
+ openReader: () => ManagedSessionDescendantStore,
3292
+ ): ManagedGcDiscoveryScopeFence {
3293
+ const internalDirectoryIdentity = store.captureDirectoryIdentity(MANAGED_INTERNAL_DIRECTORY);
3294
+ return {
3295
+ scope,
3296
+ scopeIdentity,
3297
+ binding,
3298
+ internalDirectoryIdentity,
3299
+ receiptInventory: managedGcOptionalReceiptDirectoryInventory(scope, store),
3300
+ openReader,
3301
+ };
3302
+ }
3303
+
3304
+ function managedGcAssertDiscoveryScopeFence(fence: ManagedGcDiscoveryScopeFence): void {
3305
+ const store = fence.openReader();
3306
+ try {
3307
+ store.verifyRootSecurity();
3308
+ const currentScope = fs.lstatSync(fence.scope.directoryPath, { bigint: true });
3309
+ if (
3310
+ !currentScope.isDirectory() ||
3311
+ currentScope.isSymbolicLink() ||
3312
+ currentScope.dev.toString() !== fence.scopeIdentity.dev ||
3313
+ currentScope.ino.toString() !== fence.scopeIdentity.ino
3314
+ )
3315
+ throw new Error("managed_gc_scope_authority_mismatch");
3316
+ const currentBinding = store.readExpected(MANAGED_SESSION_BINDING_FILE);
3317
+ if (
3318
+ !currentBinding ||
3319
+ !util.isDeepStrictEqual(currentBinding.identity, fence.binding.identity) ||
3320
+ !currentBinding.bytes.equals(fence.binding.bytes)
3321
+ )
3322
+ throw new Error("managed_gc_scope_authority_mismatch");
3323
+ const internalDirectoryIdentity = store.captureDirectoryIdentity(MANAGED_INTERNAL_DIRECTORY);
3324
+ if (
3325
+ internalDirectoryIdentity.dev !== fence.internalDirectoryIdentity.dev ||
3326
+ internalDirectoryIdentity.ino !== fence.internalDirectoryIdentity.ino
3327
+ )
3328
+ throw new Error("managed_gc_scope_authority_mismatch");
3329
+ const currentReceipts = managedGcOptionalReceiptDirectoryInventory(fence.scope, store);
3330
+ if (
3331
+ (fence.receiptInventory === undefined) !== (currentReceipts === undefined) ||
3332
+ (fence.receiptInventory !== undefined &&
3333
+ currentReceipts !== undefined &&
3334
+ !managedGcSameReceiptDirectoryInventory(fence.receiptInventory, currentReceipts))
3335
+ )
3336
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
3337
+ store.assertBound();
3338
+ } finally {
3339
+ store.close();
3340
+ }
3341
+ }
3342
+
3343
+ function assertManagedGcReceiptHistoryDirectoryUnchanged(
3344
+ scope: ManagedScope,
3345
+ store: ManagedSessionDescendantStore,
3346
+ history: ManagedGcReceiptHistory,
3347
+ ): void {
3348
+ const current = managedGcReceiptDirectoryInventory(scope, store);
3349
+ if (
3350
+ !managedGcSameReceiptDirectoryInventory(history.directoryInventory, current) ||
3351
+ current.entryCount !== history.directoryEntryCount ||
3352
+ !managedGcSameDirectoryEvidence(current.evidence, history.directoryEvidence)
3353
+ )
3354
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
3355
+ }
3356
+
3357
+ async function withManagedGcRetirementJournal<T>(
3358
+ scope: ManagedScope,
3359
+ transcriptPath: string,
3360
+ operation: (
3361
+ trusted: ManagedGcTrustedScope,
3362
+ store: ManagedSessionDescendantStore,
3363
+ lock: ManagedStorageLock,
3364
+ ) => Promise<T> | T,
3365
+ ): Promise<T> {
3366
+ const trusted = managedGcTrustedScope(scope);
3367
+ validateManagedGcTranscriptPath(scope, transcriptPath);
3368
+ const store = managedGcScopeStore(scope, trusted);
3369
+ let lock: ManagedStorageLock | undefined;
3370
+ let operationError: unknown;
3371
+ let result: T | undefined;
3372
+ let operationCompleted = false;
3373
+ try {
3374
+ store.verifyRootSecurity();
3375
+ store.ensureDirectory(MANAGED_GC_RETIREMENT_RECEIPTS);
3376
+ store.ensureDirectory(MANAGED_GC_RETIREMENT_LOCKS);
3377
+ lock = await acquireManagedLock(
3378
+ path.join(scope.directoryPath, MANAGED_GC_RETIREMENT_LOCKS),
3379
+ MANAGED_GC_RETIREMENT_SCOPE_LOCK_NAME,
3380
+ trusted.root,
3381
+ trusted.policy,
3382
+ );
3383
+ lock.assertOwned();
3384
+ store.assertBound();
3385
+ result = await operation(trusted, store, lock);
3386
+ operationCompleted = true;
3387
+ } catch (error) {
3388
+ operationError = error;
3389
+ }
3390
+ let releaseError: unknown;
3391
+ if (lock) {
3392
+ try {
3393
+ await lock.release();
3394
+ } catch (error) {
3395
+ releaseError = error;
3396
+ }
3397
+ }
3398
+ store.close();
3399
+ if (operationError !== undefined && releaseError !== undefined)
3400
+ throw new AggregateError(
3401
+ [operationError, releaseError],
3402
+ "managed_gc_retirement_operation_and_lock_release_failed",
3403
+ );
3404
+ if (operationError !== undefined) throw operationError;
3405
+ if (releaseError !== undefined) throw releaseError;
3406
+ if (!operationCompleted) throw new Error("managed_gc_retirement_result_missing");
3407
+ return result as T;
3408
+ }
3409
+
3410
+ /** Read the prepared authority first, then strictly validate every later receipt against it. */
3411
+ export async function readManagedGcSessionRetirementReceipt(
3412
+ scope: ManagedScope,
3413
+ transcriptPath: string,
3414
+ ): Promise<ManagedGcSessionRetirementReceipt | undefined> {
3415
+ const result = await withManagedGcRetirementJournal(scope, transcriptPath, (trusted, store, lock) =>
3416
+ readManagedGcSessionRetirementReceiptUnlocked(scope, transcriptPath, trusted, store, lock),
3417
+ );
3418
+ return result.receipt;
3419
+ }
3420
+
3421
+ /** Read an authenticated journal without creating lock/receipt directories or repairing security state. */
3422
+ export async function readManagedGcSessionRetirementReceiptReadOnly(
3423
+ scope: ManagedScope,
3424
+ transcriptPath: string,
3425
+ ): Promise<ManagedGcSessionRetirementReceipt | undefined> {
3426
+ const trusted = managedGcTrustedScope(scope);
3427
+ validateManagedGcTranscriptPath(scope, transcriptPath);
3428
+ const store = managedGcScopeReader(scope, trusted);
3429
+ try {
3430
+ store.verifyRootSecurity();
3431
+ store.assertBound();
3432
+ try {
3433
+ store.captureDirectoryIdentity(MANAGED_GC_RETIREMENT_RECEIPTS);
3434
+ } catch (error) {
3435
+ if (hasFsCode(error, "ENOENT")) return undefined;
3436
+ throw error;
3437
+ }
3438
+ return readManagedGcSessionRetirementReceiptUnlocked(scope, transcriptPath, trusted, store).receipt;
3439
+ } finally {
3440
+ store.close();
3441
+ }
3442
+ }
3443
+
3444
+ function hasManagedGcRetirementJournalWithoutWorkspace(
3445
+ scope: ManagedScope,
3446
+ onEmptyInventory?: (fence: ManagedGcDiscoveryScopeFence) => void,
3447
+ ): boolean {
3448
+ if (
3449
+ path.resolve(scope.canonicalCwd) !== scope.canonicalCwd ||
3450
+ scope.platform !== (process.platform === "win32" ? "win32" : "posix") ||
3451
+ scope.directoryName !== `v2-${scopeDigest(scope.platform, scope.canonicalCwd)}`
3452
+ )
3453
+ throw new Error("managed_gc_scope_authority_mismatch");
3454
+ const identity = fs.lstatSync(scope.directoryPath, { bigint: true });
3455
+ if (!identity.isDirectory() || identity.isSymbolicLink()) throw new Error("managed_gc_scope_authority_mismatch");
3456
+ const rootPath = configuredRootPath(scope);
3457
+ const rootIdentity = fs.lstatSync(rootPath, { bigint: true });
3458
+ const assertRoot = (): void => {
3459
+ const security = validateNativeSecurityResult(
3460
+ verifyExistingManagedScopeDirectory(rootPath),
3461
+ "verify",
3462
+ "directory",
3463
+ );
3464
+ const current = fs.lstatSync(rootPath, { bigint: true });
3465
+ if (
3466
+ !security.ok ||
3467
+ !current.isDirectory() ||
3468
+ current.isSymbolicLink() ||
3469
+ current.dev !== rootIdentity.dev ||
3470
+ current.ino !== rootIdentity.ino
3471
+ )
3472
+ throw new Error("managed_gc_scope_authority_mismatch");
3473
+ };
3474
+ assertRoot();
3475
+ const rootAuthority = managedDirectoryRoot(rootPath);
3476
+ if (
3477
+ rootAuthority.dev !== BigInt.asUintN(64, rootIdentity.dev) ||
3478
+ rootAuthority.ino !== BigInt.asUintN(64, rootIdentity.ino)
3479
+ )
3480
+ throw new Error("managed_gc_scope_authority_mismatch");
3481
+ const store = new ManagedSessionDescendantStore(
3482
+ rootAuthority,
3483
+ scope.directoryPath,
3484
+ undefined,
3485
+ scope.platform === "win32" ? "windows-existing-verify-first" : "default",
3486
+ scope.agentDir,
3487
+ {
3488
+ canonicalPath: scope.directoryPath,
3489
+ dev: BigInt.asUintN(64, identity.dev),
3490
+ ino: BigInt.asUintN(64, identity.ino),
3491
+ },
3492
+ "read-only",
3493
+ );
3494
+ try {
3495
+ store.verifyRootSecurity();
3496
+ const binding = store.readExpected(MANAGED_SESSION_BINDING_FILE);
3497
+ if (!binding || validateBindingRaw(scope, binding.bytes.toString("utf8")))
3498
+ throw new Error("managed_gc_scope_authority_mismatch");
3499
+ const readNames = (): ManagedGcDirectoryInventory | undefined => {
3500
+ try {
3501
+ return managedGcReceiptDirectoryInventory(scope, store);
3502
+ } catch (error) {
3503
+ if (hasFsCode(error, "ENOENT")) return undefined;
3504
+ throw error;
3505
+ }
3506
+ };
3507
+ const before = readNames();
3508
+ const currentBinding = store.readExpected(MANAGED_SESSION_BINDING_FILE);
3509
+ const after = readNames();
3510
+ if (
3511
+ !currentBinding ||
3512
+ !util.isDeepStrictEqual(binding.identity, currentBinding.identity) ||
3513
+ !binding.bytes.equals(currentBinding.bytes) ||
3514
+ (before === undefined) !== (after === undefined) ||
3515
+ (before !== undefined && after !== undefined && !managedGcSameReceiptDirectoryInventory(before, after))
3516
+ )
3517
+ throw new Error("managed_gc_scope_authority_mismatch");
3518
+ store.verifyRootSecurity();
3519
+ assertRoot();
3520
+ const hasJournal = before?.entries.some(entry => entry.name.startsWith(MANAGED_GC_RETIREMENT_PREFIX)) ?? false;
3521
+ if (!hasJournal && onEmptyInventory) {
3522
+ const internalDirectoryIdentity = store.captureDirectoryIdentity(MANAGED_INTERNAL_DIRECTORY);
3523
+ const policy: ManagedSessionSecurityPolicy =
3524
+ scope.platform === "win32" ? "windows-existing-verify-first" : "default";
3525
+ onEmptyInventory({
3526
+ scope,
3527
+ scopeIdentity: { dev: identity.dev.toString(), ino: identity.ino.toString() },
3528
+ binding,
3529
+ internalDirectoryIdentity,
3530
+ receiptInventory: before,
3531
+ openReader: () =>
3532
+ new ManagedSessionDescendantStore(
3533
+ rootAuthority,
3534
+ scope.directoryPath,
3535
+ undefined,
3536
+ policy,
3537
+ scope.agentDir,
3538
+ {
3539
+ canonicalPath: scope.directoryPath,
3540
+ dev: BigInt.asUintN(64, identity.dev),
3541
+ ino: BigInt.asUintN(64, identity.ino),
3542
+ },
3543
+ "read-only",
3544
+ ),
3545
+ });
3546
+ }
3547
+ return hasJournal;
3548
+ } finally {
3549
+ store.close();
3550
+ }
3551
+ }
3552
+
3553
+ /** Discover complete owner journals under authenticated existing v2 scopes without initializing storage. */
3554
+ export async function discoverManagedGcSessionRetirementReceipts(input: {
3555
+ agentDir: string;
3556
+ sessionsRoot: string;
3557
+ }): Promise<readonly { readonly scope: ManagedScope; readonly receipt: ManagedGcSessionRetirementReceipt }[]> {
3558
+ const agentDir = canonicalizeTrustedPath(input.agentDir);
3559
+ const sessionsRoot = canonicalizeTrustedPath(input.sessionsRoot);
3560
+ const profileStat = fs.lstatSync(agentDir, { bigint: true });
3561
+ const rootStat = fs.lstatSync(sessionsRoot, { bigint: true });
3562
+ for (const [pathname, stat] of [
3563
+ [agentDir, profileStat],
3564
+ [sessionsRoot, rootStat],
3565
+ ] as const) {
3566
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("managed_gc_scope_authority_mismatch");
3567
+ const verified = validateNativeSecurityResult(
3568
+ verifyExistingManagedScopeDirectory(pathname),
3569
+ "verify",
3570
+ "directory",
3571
+ );
3572
+ if (!verified.ok) throw new Error("managed_gc_scope_authority_mismatch");
3573
+ }
3574
+ const inventoryBefore = {
3575
+ dev: rootStat.dev,
3576
+ ino: rootStat.ino,
3577
+ profileDev: profileStat.dev,
3578
+ profileIno: profileStat.ino,
3579
+ };
3580
+ const rootInventory = managedGcStreamDirectoryInventory(
3581
+ sessionsRoot,
3582
+ entry => /^v2-[a-z2-7]{52}$/u.test(entry.name),
3583
+ entry => /^v2-[a-z2-7]{52}$/u.test(entry.name),
3584
+ );
3585
+ if (rootInventory.evidence.dev !== rootStat.dev.toString() || rootInventory.evidence.ino !== rootStat.ino.toString())
3586
+ throw new Error("managed_gc_scope_authority_mismatch");
3587
+ const entries = rootInventory.entries;
3588
+ const result: Array<{ readonly scope: ManagedScope; readonly receipt: ManagedGcSessionRetirementReceipt }> = [];
3589
+ const scopeFences: ManagedGcDiscoveryScopeFence[] = [];
3590
+ // Fail closed for the whole root: a partial inventory cannot prove live sibling absence.
3591
+ for (const entry of entries) {
3592
+ if (!/^v2-[a-z2-7]{52}$/u.test(entry.name)) continue;
3593
+ if (!entry.isDirectory() || entry.isSymbolicLink()) throw new Error("managed_gc_scope_authority_mismatch");
3594
+ const scopePath = path.join(sessionsRoot, entry.name);
3595
+ const bindingPath = path.join(scopePath, MANAGED_SESSION_BINDING_FILE);
3596
+ const bindingBytes = captureManagedFileNoFollow(bindingPath).bytes;
3597
+ let bindingValue: unknown;
3598
+ try {
3599
+ bindingValue = JSON.parse(bindingBytes.toString("utf8"));
3600
+ } catch {
3601
+ throw new Error("managed_gc_scope_authority_mismatch");
3602
+ }
3603
+ if (!isBinding(bindingValue) || bindingValue.identityDigest !== entry.name.slice(3))
3604
+ throw new Error("managed_gc_scope_authority_mismatch");
3605
+ const resolved = resolveManagedGcScopeForRead({ cwd: bindingValue.canonicalPath, agentDir, sessionsRoot });
3606
+ if (resolved.kind === "error" && resolved.code === "cwd_missing") {
3607
+ const storedScope: ManagedScope = {
3608
+ apiVersion: 1,
3609
+ layoutVersion: MANAGED_SESSION_LAYOUT_VERSION,
3610
+ identityVersion: MANAGED_SESSION_IDENTITY_VERSION,
3611
+ agentDir,
3612
+ sessionsRoot,
3613
+ canonicalCwd: bindingValue.canonicalPath,
3614
+ legacyLexicalCwd: bindingValue.canonicalPath,
3615
+ directoryName: entry.name,
3616
+ directoryPath: scopePath,
3617
+ platform: bindingValue.platform,
3618
+ };
3619
+ if (
3620
+ !hasManagedGcRetirementJournalWithoutWorkspace(storedScope, fence => {
3621
+ scopeFences.push(fence);
3622
+ })
3623
+ )
3624
+ continue;
3625
+ }
3626
+ if (resolved.kind !== "resolved" || resolved.scope.directoryPath !== scopePath)
3627
+ throw new Error("managed_gc_scope_authority_mismatch");
3628
+ const scope = resolved.scope;
3629
+ const trusted = managedGcTrustedScope(scope);
3630
+ const store = managedGcScopeReader(scope, trusted);
3631
+ try {
3632
+ const binding = store.readExpected(MANAGED_SESSION_BINDING_FILE);
3633
+ if (
3634
+ !binding ||
3635
+ validateBindingRaw(scope, binding.bytes.toString("utf8")) ||
3636
+ !binding.bytes.equals(bindingBytes)
3637
+ )
3638
+ throw new Error("managed_gc_scope_authority_mismatch");
3639
+ const scopeFence = managedGcDiscoveryScopeFence(
3640
+ scope,
3641
+ store,
3642
+ { dev: trusted.identity.dev.toString(), ino: trusted.identity.ino.toString() },
3643
+ binding,
3644
+ () => managedGcScopeReader(scope, trusted),
3645
+ );
3646
+ scopeFences.push(scopeFence);
3647
+ const receiptInventory = scopeFence.receiptInventory;
3648
+ if (!receiptInventory) continue;
3649
+ const groups = new Map<string, string[]>();
3650
+ for (const item of receiptInventory.entries) {
3651
+ if (!item.name.startsWith(MANAGED_GC_RETIREMENT_PREFIX)) continue;
3652
+ if (
3653
+ !item.isFile() ||
3654
+ item.isSymbolicLink() ||
3655
+ !/^gc-retirement-[a-f0-9]{64}-(?:prepared|artifacts_removed|owner_retired|owner_pending-[0-9]{8,})\.json$/u.test(
3656
+ item.name,
3657
+ )
3658
+ )
3659
+ throw new Error("task_artifact_owner_continuation_corrupt");
3660
+ const key = item.name.slice(MANAGED_GC_RETIREMENT_PREFIX.length, MANAGED_GC_RETIREMENT_PREFIX.length + 64);
3661
+ let group = groups.get(key);
3662
+ if (!group) {
3663
+ group = [];
3664
+ groups.set(key, group);
3665
+ }
3666
+ group.push(item.name);
3667
+ }
3668
+ for (const [key, group] of groups) {
3669
+ const preparedName = `${MANAGED_GC_RETIREMENT_PREFIX}${key}-prepared.json`;
3670
+ if (!group.includes(preparedName)) throw new Error("task_artifact_owner_continuation_state_missing");
3671
+ const bootstrapHistory: MutableManagedGcReceiptHistory = {
3672
+ encodedBytes: 0,
3673
+ admittedFiles: new Set<string>(),
3674
+ };
3675
+ const preparedFile = readManagedGcReceiptSnapshot(
3676
+ store,
3677
+ `${MANAGED_GC_RETIREMENT_RECEIPTS}/${preparedName}`,
3678
+ bootstrapHistory,
3679
+ );
3680
+ if (!preparedFile) throw new Error("task_artifact_owner_continuation_state_missing");
3681
+ let preparedValue: unknown;
3682
+ try {
3683
+ preparedValue = JSON.parse(preparedFile.bytes.toString("utf8"));
3684
+ } catch {
3685
+ throw new Error("task_artifact_owner_continuation_corrupt");
3686
+ }
3687
+ const transcriptPath =
3688
+ preparedValue && typeof preparedValue === "object"
3689
+ ? (preparedValue as Record<string, unknown>).transcriptPath
3690
+ : undefined;
3691
+ if (typeof transcriptPath !== "string" || managedGcRetirementTranscriptKey(transcriptPath) !== key)
3692
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
3693
+ const read = readManagedGcSessionRetirementReceiptUnlocked(
3694
+ scope,
3695
+ transcriptPath,
3696
+ trusted,
3697
+ store,
3698
+ undefined,
3699
+ receiptInventory,
3700
+ group,
3701
+ { snapshot: preparedFile, value: preparedValue, history: bootstrapHistory },
3702
+ );
3703
+ const receipt = read.receipt;
3704
+ if (!receipt) throw new Error("task_artifact_owner_continuation_state_missing");
3705
+ result.push({ scope, receipt });
3706
+ }
3707
+ const latestInventory = managedGcReceiptDirectoryInventory(scope, store);
3708
+ if (!managedGcSameReceiptDirectoryInventory(receiptInventory, latestInventory))
3709
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
3710
+ } finally {
3711
+ store.close();
3712
+ }
3713
+ }
3714
+ for (const fence of scopeFences) managedGcAssertDiscoveryScopeFence(fence);
3715
+ const rootInventoryAfter = managedGcStreamDirectoryInventory(
3716
+ sessionsRoot,
3717
+ entry => /^v2-[a-z2-7]{52}$/u.test(entry.name),
3718
+ entry => /^v2-[a-z2-7]{52}$/u.test(entry.name),
3719
+ );
3720
+ if (!managedGcSameReceiptDirectoryInventory(rootInventory, rootInventoryAfter))
3721
+ throw new Error("managed_gc_scope_authority_mismatch");
3722
+ const rootAfter = fs.lstatSync(sessionsRoot, { bigint: true });
3723
+ const profileAfter = fs.lstatSync(agentDir, { bigint: true });
3724
+ if (
3725
+ !rootAfter.isDirectory() ||
3726
+ rootAfter.isSymbolicLink() ||
3727
+ rootAfter.dev !== inventoryBefore.dev ||
3728
+ rootAfter.ino !== inventoryBefore.ino ||
3729
+ !profileAfter.isDirectory() ||
3730
+ profileAfter.isSymbolicLink() ||
3731
+ profileAfter.dev !== inventoryBefore.profileDev ||
3732
+ profileAfter.ino !== inventoryBefore.profileIno
3733
+ )
3734
+ throw new Error("managed_gc_scope_authority_mismatch");
3735
+ return result.sort((left, right) => left.receipt.transcriptPath.localeCompare(right.receipt.transcriptPath));
3736
+ }
3737
+
3738
+ const MANAGED_GC_PROTOCOL_ROLES: readonly ManagedGcProtocolRole[] = [
3739
+ MANAGED_LOCKS_DIRECTORY,
3740
+ MANAGED_RECEIPTS_DIRECTORY,
3741
+ MANAGED_TOMBSTONES_DIRECTORY,
3742
+ ];
3743
+ const MANAGED_GC_PROTOCOL_MAX_ENTRIES = MANAGED_ARTIFACT_MAX_FILES;
3744
+
3745
+ function managedGcProtocolDirectoryStat(pathname: string): fs.BigIntStats {
3746
+ const stat = fs.lstatSync(pathname, { bigint: true });
3747
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("managed_gc_protocol_directory_invalid");
3748
+ const security = validateNativeSecurityResult(verifyExistingManagedScopeDirectory(pathname), "verify", "directory");
3749
+ if (!security.ok) throw new Error("managed_gc_protocol_directory_invalid");
3750
+ return stat;
3751
+ }
3752
+
3753
+ function managedGcProtocolDirectorySnapshot(
3754
+ scope: ManagedScope,
3755
+ store: ManagedSessionDescendantStore,
3756
+ role: ManagedGcProtocolRole | typeof MANAGED_INTERNAL_DIRECTORY,
3757
+ ): { readonly path: string; readonly stat: fs.BigIntStats; readonly names: readonly string[] } {
3758
+ const relativePath = role === MANAGED_INTERNAL_DIRECTORY ? role : `${MANAGED_INTERNAL_DIRECTORY}/${role}`;
3759
+ const pathname = path.join(scope.directoryPath, relativePath);
3760
+ const before = managedGcProtocolDirectoryStat(pathname);
3761
+ const retained = store.captureDirectoryIdentity(relativePath);
3762
+ if (retained.dev !== before.dev.toString() || retained.ino !== before.ino.toString())
3763
+ throw new Error("managed_gc_protocol_directory_identity_mismatch");
3764
+ const inventory = managedGcStreamDirectoryInventory(
3765
+ pathname,
3766
+ entry => role === MANAGED_RECEIPTS_DIRECTORY && entry.name.startsWith(MANAGED_GC_RETIREMENT_PREFIX),
3767
+ () => true,
3768
+ );
3769
+ const names = inventory.entries.map(entry => entry.name);
3770
+ const after = managedGcProtocolDirectoryStat(pathname);
3771
+ if (
3772
+ after.dev !== before.dev ||
3773
+ after.ino !== before.ino ||
3774
+ after.mtimeNs !== before.mtimeNs ||
3775
+ after.ctimeNs !== before.ctimeNs ||
3776
+ after.mode !== before.mode ||
3777
+ !managedGcSameDirectoryEvidence(inventory.evidence, managedGcDirectoryEvidence(before))
3778
+ )
3779
+ throw new Error("managed_gc_protocol_directory_changed");
3780
+ store.assertBound();
3781
+ return { path: pathname, stat: before, names };
3782
+ }
3783
+
3784
+ function managedGcProtocolFileSnapshot(
3785
+ scope: ManagedScope,
3786
+ store: ManagedSessionDescendantStore,
3787
+ relativePath: string,
3788
+ receiptHistories?: Map<string, MutableManagedGcReceiptHistory>,
3789
+ ): {
3790
+ readonly snapshot: ManagedFileSnapshot;
3791
+ readonly identity: ManagedGcProtocolFileSnapshot;
3792
+ } {
3793
+ store.assertBound();
3794
+ const pathname = path.join(scope.directoryPath, relativePath);
3795
+ const before = fs.lstatSync(pathname, { bigint: true });
3796
+ if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n)
3797
+ throw new Error("managed_gc_protocol_file_invalid");
3798
+ const native = nativeScope();
3799
+ const security = validateNativeSecurityResult(
3800
+ process.platform === "win32"
3801
+ ? native.verifyOwnerOnlyPathSecurityExpected(pathname, "file", before.dev, before.ino)
3802
+ : native.verifyOwnerOnlyPathSecurity(pathname, "file"),
3803
+ "verify",
3804
+ "file",
3805
+ );
3806
+ if (!security.ok) throw new Error("managed_gc_protocol_file_invalid");
3807
+ let snapshot: ManagedFileSnapshot | null;
3808
+ if (isManagedGcRetirementReceiptPath(relativePath)) {
3809
+ let history: MutableManagedGcReceiptHistory | undefined;
3810
+ if (receiptHistories) {
3811
+ const match =
3812
+ /^gc-retirement-([a-f0-9]{64})-(?:prepared|artifacts_removed|owner_retired|owner_pending-[0-9]{8,})\.json$/u.exec(
3813
+ path.posix.basename(relativePath),
3814
+ );
3815
+ if (!match) throw new Error("task_artifact_owner_continuation_corrupt");
3816
+ history = receiptHistories.get(match[1]!);
3817
+ if (!history) {
3818
+ history = { encodedBytes: 0, admittedFiles: new Set<string>() };
3819
+ receiptHistories.set(match[1]!, history);
3820
+ }
3821
+ }
3822
+ snapshot = readManagedGcReceiptSnapshot(store, relativePath, history);
3823
+ } else if (isManagedGcCleanupReceiptPath(relativePath)) {
3824
+ try {
3825
+ snapshot = store.readExpectedBounded(relativePath, CLEANUP_RECEIPT_REPLAY_MAX_BYTES);
3826
+ } catch (error) {
3827
+ if (error instanceof Error && error.message === "artifact_capacity_exceeded") cleanupReceiptReplayCapacity();
3828
+ throw error;
3829
+ }
3830
+ } else {
3831
+ snapshot = store.readExpected(relativePath);
3832
+ }
3833
+ if (!snapshot) throw new Error("managed_gc_protocol_file_missing");
3834
+ const after = fs.lstatSync(pathname, { bigint: true });
3835
+ if (
3836
+ !after.isFile() ||
3837
+ after.isSymbolicLink() ||
3838
+ before.dev !== after.dev ||
3839
+ before.ino !== after.ino ||
3840
+ before.nlink !== after.nlink ||
3841
+ before.size !== after.size ||
3842
+ before.mtimeNs !== after.mtimeNs ||
3843
+ before.ctimeNs !== after.ctimeNs ||
3844
+ before.mode !== after.mode ||
3845
+ snapshot.identity.dev !== before.dev ||
3846
+ snapshot.identity.ino !== before.ino ||
3847
+ snapshot.identity.nlink !== before.nlink ||
3848
+ snapshot.identity.size !== Number(before.size) ||
3849
+ snapshot.identity.mtimeNs !== before.mtimeNs ||
3850
+ snapshot.identity.ctimeNs !== before.ctimeNs
3851
+ )
3852
+ throw new Error("managed_gc_protocol_file_changed");
3853
+ store.assertBound();
3854
+ return {
3855
+ snapshot,
3856
+ identity: {
3857
+ name: path.basename(pathname),
3858
+ dev: snapshot.identity.dev.toString(),
3859
+ ino: snapshot.identity.ino.toString(),
3860
+ nlink: snapshot.identity.nlink.toString(),
3861
+ size: snapshot.identity.size,
3862
+ mtimeNs: snapshot.identity.mtimeNs.toString(),
3863
+ ctimeNs: snapshot.identity.ctimeNs.toString(),
3864
+ mode: Number(before.mode & 0o777n),
3865
+ sha256: snapshot.identity.sha256,
3866
+ },
3867
+ };
3868
+ }
3869
+
3870
+ function isManagedGcPendingCleanupReceiptPath(relativePath: string): boolean {
3871
+ const prefix = `${MANAGED_INTERNAL_DIRECTORY}/${MANAGED_TOMBSTONES_DIRECTORY}/`;
3872
+ if (!relativePath.startsWith(prefix)) return false;
3873
+ return /\.cleanup-pending-[1-9][0-9]*\.json$/u.test(relativePath.slice(prefix.length));
3874
+ }
3875
+
3876
+ function isManagedGcCleanupReceiptPath(relativePath: string): boolean {
3877
+ const prefix = `${MANAGED_INTERNAL_DIRECTORY}/${MANAGED_TOMBSTONES_DIRECTORY}/`;
3878
+ if (!relativePath.startsWith(prefix)) return false;
3879
+ const match =
3880
+ /^[a-f0-9]{64}\.[a-f0-9]{64}\.cleanup-(pending|artifacts_removed|completed)-([1-9][0-9]*)\.json$/u.exec(
3881
+ relativePath.slice(prefix.length),
3882
+ );
3883
+ if (!match) return false;
3884
+ const attempt = Number(match[2]);
3885
+ return Number.isSafeInteger(attempt) && String(attempt) === match[2] && (match[1] !== "completed" || attempt === 1);
3886
+ }
3887
+
3888
+ function isManagedGcRetirementReceiptPath(relativePath: string): boolean {
3889
+ return relativePath.startsWith(`${MANAGED_GC_RETIREMENT_RECEIPTS}/${MANAGED_GC_RETIREMENT_PREFIX}`);
3890
+ }
3891
+
3892
+ function isManagedGcCleanupLikeName(name: string): boolean {
3893
+ return name.includes(".cleanup-") && name.endsWith(".json");
3894
+ }
3895
+
3896
+ function managedGcProtocolRecord(
3897
+ bytes: Uint8Array,
3898
+ role: ManagedGcProtocolRole,
3899
+ name: string,
3900
+ ): Record<string, unknown> {
3901
+ try {
3902
+ const value: unknown = JSON.parse(Buffer.from(bytes).toString("utf8"));
3903
+ if (value && typeof value === "object" && !Array.isArray(value)) return value as Record<string, unknown>;
3904
+ } catch {
3905
+ // A role-specific parser below supplies the bounded protocol location.
3906
+ }
3907
+ throw new Error(`managed_gc_protocol_record_invalid:${role}/${name}`);
3908
+ }
3909
+
3910
+ function managedGcProtocolFileIdentityMatches(
3911
+ left: ManagedGcProtocolFileSnapshot,
3912
+ right: ManagedGcProtocolFileSnapshot,
3913
+ ): boolean {
3914
+ return (
3915
+ left.name === right.name &&
3916
+ left.dev === right.dev &&
3917
+ left.ino === right.ino &&
3918
+ left.nlink === right.nlink &&
3919
+ left.size === right.size &&
3920
+ left.mtimeNs === right.mtimeNs &&
3921
+ left.ctimeNs === right.ctimeNs &&
3922
+ left.mode === right.mode &&
3923
+ left.sha256 === right.sha256
3924
+ );
3925
+ }
3926
+
3927
+ function managedGcProtocolPathIdentity(pathname: string): ManagedGcProtocolPathIdentity {
3928
+ const stat = managedGcProtocolDirectoryStat(pathname);
3929
+ return { path: pathname, dev: stat.dev.toString(), ino: stat.ino.toString() };
3930
+ }
3931
+
3932
+ function validateManagedGcProtocolMigrationReceipt(
3933
+ scope: ManagedScope,
3934
+ pathname: string,
3935
+ name: string,
3936
+ bytes: Uint8Array,
3937
+ ): void {
3938
+ const match = /^([a-f0-9]{64})(?:\.(prepared|detached|published))?\.json$/u.exec(name);
3939
+ if (!match || match[2] !== undefined) throw new Error("managed_gc_protocol_migration_incomplete");
3940
+ const record = managedGcProtocolRecord(bytes, MANAGED_RECEIPTS_DIRECTORY, name);
3941
+ if (record.state !== "committed") throw new Error("managed_gc_protocol_migration_incomplete");
3942
+ const source = record.source as { path?: unknown } | undefined;
3943
+ const destination = record.destination as { path?: unknown } | undefined;
3944
+ if (typeof source?.path !== "string" || typeof destination?.path !== "string")
3945
+ throw new Error("managed_gc_protocol_migration_invalid");
3946
+ const legacy = inspectCandidate(source.path, "legacy");
3947
+ const managed = inspectCandidate(destination.path, "v2");
3948
+ if ("code" in legacy || "code" in managed || !receiptMatches(pathname, legacy, managed, scope))
3949
+ throw new Error("managed_gc_protocol_migration_authority_mismatch");
3950
+ }
3951
+
3952
+ function validateManagedGcProtocolTombstoneFile(
3953
+ scope: ManagedScope,
3954
+ pathname: string,
3955
+ name: string,
3956
+ bytes: Uint8Array,
3957
+ journals: readonly { readonly scope: ManagedScope; readonly receipt: ManagedGcSessionRetirementReceipt }[],
3958
+ ): void {
3959
+ if (/^[a-f0-9]{64}\.json$/u.test(name)) {
3960
+ const targets = retiredTargets(scope, pathname);
3961
+ if (!targets || targets.length === 0) throw new Error("managed_gc_protocol_tombstone_invalid");
3962
+ return;
3963
+ }
3964
+ const match =
3965
+ /^([a-f0-9]{64})\.([a-f0-9]{64})\.cleanup-(pending|artifacts_removed|completed)-([1-9][0-9]*)\.json$/u.exec(name);
3966
+ if (!match || (match[3] === "completed" && match[4] !== "1"))
3967
+ throw new Error("managed_gc_protocol_tombstone_role_invalid");
3968
+ const tombstone = path.join(path.dirname(pathname), `${match[1]}.json`);
3969
+ const targets = retiredTargets(scope, tombstone);
3970
+ if (!targets) throw new Error("managed_gc_protocol_tombstone_authority_missing");
3971
+ const target = targets.find(candidate => stableOperationName(candidate) === match[2]);
3972
+ if (!target) throw new Error("managed_gc_protocol_tombstone_target_missing");
3973
+ const attempt = Number(match[4]);
3974
+ if (match[3] === "pending") {
3975
+ const pending = pendingCleanupReceipt(scope, tombstone, target);
3976
+ if (!pending || pending.attempt < attempt) throw new Error("managed_gc_protocol_tombstone_history_invalid");
3977
+ return;
3978
+ }
3979
+ if (match[3] === "artifacts_removed") {
3980
+ if (!cleanupArtifactsRemovedEvidence(scope, tombstone, target, attempt))
3981
+ throw new Error("managed_gc_protocol_tombstone_artifacts_invalid");
3982
+ return;
3983
+ }
3984
+ const ownerReceipt = target.taskArtifactOwnerDeletionEvidence
3985
+ ? journals.find(
3986
+ item => item.scope.directoryPath === scope.directoryPath && item.receipt.transcriptPath === target.path,
3987
+ )?.receipt
3988
+ : undefined;
3989
+ if (target.taskArtifactOwnerDeletionEvidence && !ownerReceipt)
3990
+ throw new Error("task_artifact_owner_continuation_state_missing");
3991
+ const record = managedGcProtocolRecord(bytes, MANAGED_TOMBSTONES_DIRECTORY, name);
3992
+ if (!cleanupCompletedRecordMatches(scope, tombstone, target, ownerReceipt, record))
3993
+ throw new Error("managed_gc_protocol_tombstone_completion_invalid");
3994
+ }
3995
+
3996
+ /** Inspect under a live operation lease; returned snapshots never authorize effects. */
3997
+ export function managedGcProtocolInspectorForLock(
3998
+ scope: ManagedScope,
3999
+ lock: ManagedStorageLock,
4000
+ ): ManagedGcProtocolScopeInspector {
4001
+ const inspect = managedGcProtocolScopeInspectorForScope(scope);
4002
+ return async inputs => {
4003
+ lock.assertOwned();
4004
+ managedGcTrustedScope(scope);
4005
+ const snapshots = await inspect(inputs);
4006
+ lock.assertOwned();
4007
+ managedGcTrustedScope(scope);
4008
+ return snapshots;
4009
+ };
4010
+ }
4011
+
4012
+ /** Bind a read-only protocol inventory to an already prepared scope's root/profile/session identities. */
4013
+ export function managedGcProtocolScopeInspectorForScope(scope: ManagedScope): ManagedGcProtocolScopeInspector {
4014
+ const trusted = managedGcTrustedScope(scope);
4015
+ const configuredRoot = managedGcProtocolPathIdentity(trusted.root.canonicalPath);
4016
+ const profile = managedGcProtocolPathIdentity(scope.agentDir);
4017
+ const sessionsRoot = managedGcProtocolPathIdentity(scope.sessionsRoot);
4018
+ const protocol = managedGcProtocolPathIdentity(path.join(scope.directoryPath, MANAGED_INTERNAL_DIRECTORY));
4019
+ const assertAuthority = (): void => {
4020
+ managedGcTrustedScope(scope);
4021
+ for (const expected of [configuredRoot, profile, sessionsRoot, protocol]) {
4022
+ const current = managedGcProtocolPathIdentity(expected.path);
4023
+ if (current.dev !== expected.dev || current.ino !== expected.ino)
4024
+ throw new Error("managed_gc_protocol_root_identity_mismatch");
4025
+ }
4026
+ };
4027
+ return async (inputs: readonly ManagedGcProtocolScopeInput[]) => {
4028
+ assertAuthority();
4029
+ const journals = await discoverManagedGcSessionRetirementReceipts({
4030
+ agentDir: scope.agentDir,
4031
+ sessionsRoot: scope.sessionsRoot,
4032
+ });
4033
+ assertAuthority();
4034
+ if (inputs.length > MANAGED_GC_PROTOCOL_MAX_ENTRIES) throw new Error("managed_gc_protocol_scope_limit_exceeded");
4035
+ const snapshots: ManagedGcProtocolScopeSnapshot[] = [];
4036
+ let totalFiles = 0;
4037
+ for (const input of inputs) {
4038
+ if (
4039
+ path.resolve(input.scopePath) !== input.scopePath ||
4040
+ path.dirname(input.scopePath) !== scope.sessionsRoot ||
4041
+ !/^v2-[a-z2-7]{52}$/u.test(path.basename(input.scopePath)) ||
4042
+ input.scopeIdentity.path !== input.scopePath ||
4043
+ input.protocolIdentity.path !== path.join(input.scopePath, MANAGED_INTERNAL_DIRECTORY)
4044
+ )
4045
+ throw new Error("managed_gc_protocol_scope_path_mismatch");
4046
+ const bindingPath = path.join(input.scopePath, MANAGED_SESSION_BINDING_FILE);
4047
+ const binding = captureManagedFileNoFollow(bindingPath);
4048
+ const bindingStat = fs.lstatSync(bindingPath, { bigint: true });
4049
+ const bindingIdentity: ManagedGcProtocolFileSnapshot = {
4050
+ name: MANAGED_SESSION_BINDING_FILE,
4051
+ dev: binding.identity.dev.toString(),
4052
+ ino: binding.identity.ino.toString(),
4053
+ nlink: binding.identity.nlink.toString(),
4054
+ size: binding.identity.size,
4055
+ mtimeNs: binding.identity.mtimeNs.toString(),
4056
+ ctimeNs: binding.identity.ctimeNs.toString(),
4057
+ mode: Number(bindingStat.mode & 0o777n),
4058
+ sha256: binding.identity.sha256,
4059
+ };
4060
+ if (!managedGcProtocolFileIdentityMatches(bindingIdentity, input.bindingIdentity))
4061
+ throw new Error("managed_gc_protocol_binding_identity_mismatch");
4062
+ const bindingValue: unknown = JSON.parse(binding.bytes.toString("utf8"));
4063
+ if (!isBinding(bindingValue)) throw new Error("managed_gc_scope_authority_mismatch");
4064
+ const resolved = resolveManagedGcScopeForRead({
4065
+ cwd: bindingValue.canonicalPath,
4066
+ agentDir: scope.agentDir,
4067
+ sessionsRoot: scope.sessionsRoot,
4068
+ });
4069
+ let candidateScope: ManagedScope;
4070
+ let candidateTrusted: ManagedGcTrustedScope | undefined;
4071
+ if (resolved.kind === "error" && resolved.code === "cwd_missing") {
4072
+ candidateScope = {
4073
+ apiVersion: 1,
4074
+ layoutVersion: MANAGED_SESSION_LAYOUT_VERSION,
4075
+ identityVersion: MANAGED_SESSION_IDENTITY_VERSION,
4076
+ agentDir: scope.agentDir,
4077
+ sessionsRoot: scope.sessionsRoot,
4078
+ canonicalCwd: bindingValue.canonicalPath,
4079
+ legacyLexicalCwd: bindingValue.canonicalPath,
4080
+ directoryName: path.basename(input.scopePath),
4081
+ directoryPath: input.scopePath,
4082
+ platform: bindingValue.platform,
4083
+ };
4084
+ if (hasManagedGcRetirementJournalWithoutWorkspace(candidateScope))
4085
+ throw new Error("managed_gc_protocol_scope_unrecognized");
4086
+ } else if (resolved.kind === "resolved" && resolved.scope.directoryPath === input.scopePath) {
4087
+ candidateScope = resolved.scope;
4088
+ candidateTrusted = managedGcTrustedScope(candidateScope);
4089
+ } else {
4090
+ throw new Error("managed_gc_protocol_scope_unrecognized");
4091
+ }
4092
+ if (
4093
+ configuredRootPath(candidateScope) !== trusted.root.canonicalPath ||
4094
+ (candidateTrusted &&
4095
+ (candidateTrusted.root.canonicalPath !== trusted.root.canonicalPath ||
4096
+ candidateTrusted.root.dev !== trusted.root.dev ||
4097
+ candidateTrusted.root.ino !== trusted.root.ino)) ||
4098
+ candidateScope.agentDir !== scope.agentDir ||
4099
+ candidateScope.sessionsRoot !== scope.sessionsRoot
4100
+ )
4101
+ throw new Error("managed_gc_protocol_root_mismatch");
4102
+ const scopeIdentity = managedGcProtocolPathIdentity(candidateScope.directoryPath);
4103
+ if (scopeIdentity.dev !== input.scopeIdentity.dev || scopeIdentity.ino !== input.scopeIdentity.ino)
4104
+ throw new Error("managed_gc_protocol_scope_identity_mismatch");
4105
+ const store = candidateTrusted
4106
+ ? managedGcScopeReader(candidateScope, candidateTrusted)
4107
+ : new ManagedSessionDescendantStore(
4108
+ trusted.root,
4109
+ candidateScope.directoryPath,
4110
+ undefined,
4111
+ trusted.policy,
4112
+ candidateScope.agentDir,
4113
+ {
4114
+ canonicalPath: candidateScope.directoryPath,
4115
+ dev: BigInt(scopeIdentity.dev),
4116
+ ino: BigInt(scopeIdentity.ino),
4117
+ },
4118
+ "read-only",
4119
+ );
4120
+ try {
4121
+ store.verifyRootSecurity();
4122
+ const checkedBinding = managedGcProtocolFileSnapshot(candidateScope, store, MANAGED_SESSION_BINDING_FILE);
4123
+ if (!managedGcProtocolFileIdentityMatches(checkedBinding.identity, input.bindingIdentity))
4124
+ throw new Error("managed_gc_protocol_binding_identity_mismatch");
4125
+ const protocolDirectory = managedGcProtocolDirectorySnapshot(
4126
+ candidateScope,
4127
+ store,
4128
+ MANAGED_INTERNAL_DIRECTORY,
4129
+ );
4130
+ if (
4131
+ protocolDirectory.path !== input.protocolIdentity.path ||
4132
+ protocolDirectory.stat.dev.toString() !== input.protocolIdentity.dev ||
4133
+ protocolDirectory.stat.ino.toString() !== input.protocolIdentity.ino ||
4134
+ protocolDirectory.stat.mtimeNs.toString() !== input.protocolIdentity.mtimeNs ||
4135
+ protocolDirectory.stat.ctimeNs.toString() !== input.protocolIdentity.ctimeNs ||
4136
+ Number(protocolDirectory.stat.mode & 0o777n) !== input.protocolIdentity.mode ||
4137
+ protocolDirectory.names.length !== MANAGED_GC_PROTOCOL_ROLES.length ||
4138
+ MANAGED_GC_PROTOCOL_ROLES.some(role => !protocolDirectory.names.includes(role))
4139
+ )
4140
+ throw new Error("managed_gc_protocol_roles_invalid");
4141
+ const directories: ManagedGcProtocolDirectorySnapshot[] = [];
4142
+ const receiptHistories = new Map<string, MutableManagedGcReceiptHistory>();
4143
+ for (const role of MANAGED_GC_PROTOCOL_ROLES) {
4144
+ const directory = managedGcProtocolDirectorySnapshot(candidateScope, store, role);
4145
+ const files: ManagedGcProtocolFileSnapshot[] = [];
4146
+ for (const name of directory.names) {
4147
+ if (!name || path.basename(name) !== name || name.includes(".jsonl"))
4148
+ throw new Error(`managed_gc_protocol_entry_invalid:${role}/${name}`);
4149
+ const relative = `${MANAGED_INTERNAL_DIRECTORY}/${role}/${name}`;
4150
+ const cleanupLike = role === MANAGED_TOMBSTONES_DIRECTORY && isManagedGcCleanupLikeName(name);
4151
+ if (cleanupLike && !isManagedGcCleanupReceiptPath(relative))
4152
+ throw new Error("managed_gc_protocol_tombstone_role_invalid");
4153
+ const pendingCleanup =
4154
+ role === MANAGED_TOMBSTONES_DIRECTORY && isManagedGcPendingCleanupReceiptPath(relative);
4155
+ let captured: ReturnType<typeof managedGcProtocolFileSnapshot>;
4156
+ try {
4157
+ if (pendingCleanup)
4158
+ validateManagedGcProtocolTombstoneFile(
4159
+ candidateScope,
4160
+ path.join(directory.path, name),
4161
+ name,
4162
+ new Uint8Array(),
4163
+ journals,
4164
+ );
4165
+ captured = managedGcProtocolFileSnapshot(candidateScope, store, relative, receiptHistories);
4166
+ if (role === MANAGED_LOCKS_DIRECTORY) {
4167
+ if (isManagedLockQuarantineName(name)) {
4168
+ if (
4169
+ captured.snapshot.bytes.byteLength !== 0 ||
4170
+ captured.identity.size !== 0 ||
4171
+ captured.identity.sha256 !== createHash("sha256").digest("hex")
4172
+ )
4173
+ throw new Error("managed_gc_protocol_lock_quarantine_invalid");
4174
+ } else {
4175
+ if (!/^(?:[a-f0-9]{64}|gc-retirement-[a-f0-9]{64})\.lock$/u.test(name))
4176
+ throw new Error("managed_gc_protocol_lock_role_invalid");
4177
+ if (!parseManagedLockRecord(captured.snapshot.bytes))
4178
+ throw new Error("managed_gc_protocol_lock_invalid");
4179
+ }
4180
+ } else if (role === MANAGED_RECEIPTS_DIRECTORY) {
4181
+ if (
4182
+ /^gc-retirement-[a-f0-9]{64}-(?:prepared|artifacts_removed|owner_retired|owner_pending-[0-9]{8,})\.json$/u.test(
4183
+ name,
4184
+ )
4185
+ ) {
4186
+ const key = name.slice(
4187
+ MANAGED_GC_RETIREMENT_PREFIX.length,
4188
+ MANAGED_GC_RETIREMENT_PREFIX.length + 64,
4189
+ );
4190
+ if (
4191
+ !journals.some(
4192
+ item =>
4193
+ item.scope.directoryPath === candidateScope.directoryPath &&
4194
+ managedGcRetirementTranscriptKey(item.receipt.transcriptPath) === key,
4195
+ )
4196
+ )
4197
+ throw new Error("task_artifact_owner_continuation_state_missing");
4198
+ } else {
4199
+ validateManagedGcProtocolMigrationReceipt(
4200
+ candidateScope,
4201
+ path.join(directory.path, name),
4202
+ name,
4203
+ captured.snapshot.bytes,
4204
+ );
4205
+ }
4206
+ } else {
4207
+ if (!pendingCleanup)
4208
+ validateManagedGcProtocolTombstoneFile(
4209
+ candidateScope,
4210
+ path.join(directory.path, name),
4211
+ name,
4212
+ captured.snapshot.bytes,
4213
+ journals,
4214
+ );
4215
+ }
4216
+ } catch (error) {
4217
+ if (isManagedGcJournalCapacityError(error)) throw error;
4218
+ const detail = error instanceof Error ? error.message : "unknown";
4219
+ throw new Error(`${detail} [${role}/${name}]`);
4220
+ }
4221
+ files.push(captured.identity);
4222
+ totalFiles++;
4223
+ if (totalFiles > MANAGED_GC_PROTOCOL_MAX_ENTRIES)
4224
+ throw new Error("managed_gc_protocol_entry_limit_exceeded");
4225
+ }
4226
+ const finalDirectory = managedGcProtocolDirectorySnapshot(candidateScope, store, role);
4227
+ if (
4228
+ finalDirectory.stat.dev !== directory.stat.dev ||
4229
+ finalDirectory.stat.ino !== directory.stat.ino ||
4230
+ finalDirectory.stat.mtimeNs !== directory.stat.mtimeNs ||
4231
+ finalDirectory.stat.ctimeNs !== directory.stat.ctimeNs ||
4232
+ finalDirectory.stat.mode !== directory.stat.mode ||
4233
+ finalDirectory.names.length !== directory.names.length ||
4234
+ finalDirectory.names.some((name, index) => name !== directory.names[index])
4235
+ )
4236
+ throw new Error("managed_gc_protocol_directory_changed");
4237
+ directories.push({
4238
+ role,
4239
+ path: directory.path,
4240
+ dev: directory.stat.dev.toString(),
4241
+ ino: directory.stat.ino.toString(),
4242
+ mtimeNs: directory.stat.mtimeNs.toString(),
4243
+ ctimeNs: directory.stat.ctimeNs.toString(),
4244
+ mode: Number(directory.stat.mode & 0o777n),
4245
+ files,
4246
+ });
4247
+ }
4248
+ const finalProtocol = managedGcProtocolDirectorySnapshot(candidateScope, store, MANAGED_INTERNAL_DIRECTORY);
4249
+ if (
4250
+ finalProtocol.stat.dev !== protocolDirectory.stat.dev ||
4251
+ finalProtocol.stat.ino !== protocolDirectory.stat.ino ||
4252
+ finalProtocol.stat.mtimeNs !== protocolDirectory.stat.mtimeNs ||
4253
+ finalProtocol.stat.ctimeNs !== protocolDirectory.stat.ctimeNs ||
4254
+ finalProtocol.stat.mode !== protocolDirectory.stat.mode ||
4255
+ finalProtocol.names.length !== protocolDirectory.names.length ||
4256
+ finalProtocol.names.some((name, index) => name !== protocolDirectory.names[index])
4257
+ )
4258
+ throw new Error("managed_gc_protocol_tree_changed");
4259
+ const comparisonReceiptHistories = new Map<string, MutableManagedGcReceiptHistory>();
4260
+ for (const directory of directories) {
4261
+ const relative = `${MANAGED_INTERNAL_DIRECTORY}/${directory.role}`;
4262
+ const currentDirectory = managedGcProtocolDirectorySnapshot(candidateScope, store, directory.role);
4263
+ const names = currentDirectory.names;
4264
+ if (
4265
+ currentDirectory.stat.dev !== BigInt(directory.dev) ||
4266
+ currentDirectory.stat.ino !== BigInt(directory.ino) ||
4267
+ currentDirectory.stat.mtimeNs !== BigInt(directory.mtimeNs) ||
4268
+ currentDirectory.stat.ctimeNs !== BigInt(directory.ctimeNs) ||
4269
+ names.length !== directory.files.length ||
4270
+ names.some((name, index) => name !== directory.files[index]?.name)
4271
+ )
4272
+ throw new Error("managed_gc_protocol_directory_changed");
4273
+ for (const expected of directory.files) {
4274
+ const actual = managedGcProtocolFileSnapshot(
4275
+ candidateScope,
4276
+ store,
4277
+ `${relative}/${expected.name}`,
4278
+ comparisonReceiptHistories,
4279
+ );
4280
+ if (!managedGcProtocolFileIdentityMatches(actual.identity, expected))
4281
+ throw new Error("managed_gc_protocol_file_changed");
4282
+ }
4283
+ const current = managedGcProtocolDirectoryStat(directory.path);
4284
+ if (
4285
+ current.dev.toString() !== directory.dev ||
4286
+ current.ino.toString() !== directory.ino ||
4287
+ current.mtimeNs.toString() !== directory.mtimeNs ||
4288
+ current.ctimeNs.toString() !== directory.ctimeNs ||
4289
+ Number(current.mode & 0o777n) !== directory.mode
4290
+ )
4291
+ throw new Error("managed_gc_protocol_directory_changed");
4292
+ }
4293
+ const finalBinding = managedGcProtocolFileSnapshot(candidateScope, store, MANAGED_SESSION_BINDING_FILE);
4294
+ if (!managedGcProtocolFileIdentityMatches(finalBinding.identity, input.bindingIdentity))
4295
+ throw new Error("managed_gc_protocol_binding_changed");
4296
+ snapshots.push({
4297
+ configuredRoot,
4298
+ profile,
4299
+ sessionsRoot,
4300
+ scope: scopeIdentity,
4301
+ binding: bindingIdentity,
4302
+ protocol: {
4303
+ path: protocolDirectory.path,
4304
+ dev: protocolDirectory.stat.dev.toString(),
4305
+ ino: protocolDirectory.stat.ino.toString(),
4306
+ },
4307
+ protocolMtimeNs: protocolDirectory.stat.mtimeNs.toString(),
4308
+ protocolCtimeNs: protocolDirectory.stat.ctimeNs.toString(),
4309
+ protocolMode: Number(protocolDirectory.stat.mode & 0o777n),
4310
+ directories,
4311
+ });
4312
+ } finally {
4313
+ store.close();
4314
+ }
4315
+ }
4316
+ const latestJournals = await discoverManagedGcSessionRetirementReceipts({
4317
+ agentDir: scope.agentDir,
4318
+ sessionsRoot: scope.sessionsRoot,
4319
+ });
4320
+ if (!deepSame(latestJournals, journals)) throw new Error("managed_gc_protocol_journal_changed");
4321
+ assertAuthority();
4322
+ return snapshots;
4323
+ };
4324
+ }
4325
+
4326
+ function managedGcReceiptForPublication(
4327
+ scope: ManagedScope,
4328
+ receipt: ManagedGcSessionRetirementReceipt,
4329
+ ownerContext: TaskArtifactOwnerStorageContext,
4330
+ attempt?: number,
4331
+ history?: ManagedGcReceiptHistory,
4332
+ ): {
4333
+ readonly target: ManagedGcSessionRetirementTarget;
4334
+ readonly record: Record<string, unknown>;
4335
+ readonly parsed: ManagedGcSessionRetirementReceipt;
4336
+ } {
4337
+ validateManagedGcTranscriptPath(scope, receipt.transcriptPath);
4338
+ if (
4339
+ history &&
4340
+ (history.entryCount >= MANAGED_ARTIFACT_MAX_FILES || history.directoryEntryCount >= MANAGED_ARTIFACT_MAX_FILES)
4341
+ )
4342
+ managedGcJournalCapacity();
4343
+ const candidateRecord = managedGcReceiptRecord(scope, receipt, attempt);
4344
+ const candidateByteLength = managedGcJsonByteLength(candidateRecord);
4345
+ if (history && candidateByteLength > MANAGED_ARTIFACT_MAX_TOTAL_BYTES - history.encodedBytes)
4346
+ managedGcJournalCapacity();
4347
+ const parsed = parseManagedGcRetirementReceipt(
4348
+ candidateRecord,
4349
+ scopeDigest(scope.platform, scope.canonicalCwd),
4350
+ ownerContext,
4351
+ receipt,
4352
+ );
4353
+ const record = managedGcReceiptRecord(scope, parsed, attempt);
4354
+ const encodedByteLength = managedGcJsonByteLength(record);
4355
+ if (encodedByteLength !== candidateByteLength) throw new Error("managed_gc_receipt_encoding_mismatch");
4356
+ return { target: receipt, record, parsed };
4357
+ }
4358
+
4359
+ function publishManagedGcReceiptNoReplace(
4360
+ store: ManagedSessionDescendantStore,
4361
+ lock: ManagedStorageLock,
4362
+ relativePath: string,
4363
+ record: Record<string, unknown>,
4364
+ ): void {
4365
+ lock.assertOwned();
4366
+ store.assertBound();
4367
+ const expectedByteLength = managedGcJsonByteLength(record);
4368
+ const serializedJson = JSON.stringify(record, (_key, value: unknown) =>
4369
+ typeof value === "bigint" ? value.toString() : value,
4370
+ );
4371
+ const serialized = `${serializedJson}\n`;
4372
+ const actualByteLength = Buffer.byteLength(serialized, "utf8");
4373
+ if (actualByteLength > MANAGED_GC_RECEIPT_MAX_BYTES) managedGcReceiptCapacity();
4374
+ if (actualByteLength !== expectedByteLength) throw new Error("managed_gc_receipt_encoding_mismatch");
4375
+ const bytes = Buffer.from(serialized, "utf8");
4376
+ try {
4377
+ store.publishNoReplaceSync(relativePath, bytes);
4378
+ } catch (error) {
4379
+ if (!(error instanceof Error) || error.message !== "destination_conflict") throw error;
4380
+ const existing = readManagedGcReceiptSnapshot(store, relativePath);
4381
+ if (!existing?.bytes.equals(bytes)) throw error;
4382
+ }
4383
+ lock.assertOwned();
4384
+ const persisted = readManagedGcReceiptSnapshot(store, relativePath);
4385
+ if (!persisted?.bytes.equals(bytes)) throw new Error("durability_failed");
4386
+ store.assertBound();
4387
+ }
4388
+
4389
+ /** Atomically publish one journal state under the managed scope lock; no GC effects are activated here. */
4390
+ export async function publishManagedGcSessionRetirementReceipt(
4391
+ scope: ManagedScope,
4392
+ receipt: ManagedGcSessionRetirementReceipt,
4393
+ ): Promise<ManagedGcSessionRetirementReceipt> {
4394
+ validateManagedGcTranscriptPath(scope, receipt.transcriptPath);
4395
+ return withManagedGcRetirementJournal(scope, receipt.transcriptPath, (trusted, store, lock) => {
4396
+ const currentRead = readManagedGcSessionRetirementReceiptUnlocked(
4397
+ scope,
4398
+ receipt.transcriptPath,
4399
+ trusted,
4400
+ store,
4401
+ lock,
4402
+ );
4403
+ const current = currentRead.receipt;
4404
+ const target = receipt;
4405
+ let attempt: number | undefined;
4406
+ if (!current) {
4407
+ if (receipt.state !== "prepared") throw new Error("task_artifact_owner_continuation_state_order_invalid");
4408
+ assertManagedGcTranscriptUnchangedOrAbsent(scope, target, false);
4409
+ const capturedEvidence = captureTaskArtifactOwnerDeletionEvidence(
4410
+ trusted.ownerContext,
4411
+ target.sessionId,
4412
+ target.taskArtifactOwnerLocator,
4413
+ );
4414
+ if (
4415
+ !capturedEvidence ||
4416
+ !deepSame(
4417
+ capturedEvidence,
4418
+ parseTaskArtifactOwnerDeletionEvidence(receipt.taskArtifactOwnerDeletionEvidence),
4419
+ )
4420
+ )
4421
+ throw new Error("task_artifact_owner_continuation_evidence_mismatch");
4422
+ } else {
4423
+ const prepared = currentRead.prepared;
4424
+ if (!prepared) throw new Error("task_artifact_owner_continuation_state_missing");
4425
+ assertSameManagedGcImmutableAuthority(prepared, receipt);
4426
+ assertManagedGcTranscriptUnchangedOrAbsent(scope, prepared, true);
4427
+ if (receipt.state === "prepared") {
4428
+ managedGcReceiptForPublication(scope, receipt, trusted.ownerContext);
4429
+ return current;
4430
+ }
4431
+ if (receipt.state === "artifacts_removed" && current.state !== "prepared") {
4432
+ managedGcReceiptForPublication(scope, receipt, trusted.ownerContext);
4433
+ return current;
4434
+ }
4435
+ if (receipt.state === "owner_pending") {
4436
+ if (current.state !== "artifacts_removed" && current.state !== "owner_pending")
4437
+ throw new Error("task_artifact_owner_continuation_state_order_invalid");
4438
+ attempt = current.state === "owner_pending" ? (current.ownerRetirementAttempt ?? 0) + 1 : 1;
4439
+ if (current.state === "owner_pending") {
4440
+ const previous = current.taskArtifactOwnerRetirementOutcome;
4441
+ const next = receipt.taskArtifactOwnerRetirementOutcome;
4442
+ if (
4443
+ !previous ||
4444
+ previous.kind === "completed" ||
4445
+ !next ||
4446
+ next.kind === "completed" ||
4447
+ !continuationExtendsPrevious(previous.continuation, next.continuation)
4448
+ )
4449
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
4450
+ }
4451
+ } else if (receipt.state === "owner_retired") {
4452
+ if (current.state === "owner_retired") {
4453
+ const existing = current.taskArtifactOwnerRetirementOutcome;
4454
+ const candidate = managedGcReceiptForPublication(scope, receipt, trusted.ownerContext).parsed;
4455
+ if (!existing || !deepSame(existing, candidate.taskArtifactOwnerRetirementOutcome))
4456
+ throw new Error("task_artifact_owner_continuation_outcome_mismatch");
4457
+ return current;
4458
+ }
4459
+ if (current.state !== "artifacts_removed" && current.state !== "owner_pending")
4460
+ throw new Error("task_artifact_owner_continuation_state_order_invalid");
4461
+ } else if (receipt.state !== "artifacts_removed") {
4462
+ throw new Error("task_artifact_owner_continuation_state_order_invalid");
4463
+ }
4464
+ }
4465
+ assertManagedGcReceiptHistoryDirectoryUnchanged(scope, store, currentRead.history);
4466
+ const { record, parsed } = managedGcReceiptForPublication(
4467
+ scope,
4468
+ receipt,
4469
+ trusted.ownerContext,
4470
+ attempt,
4471
+ currentRead.history,
4472
+ );
4473
+ if (receipt.state === "owner_retired") {
4474
+ const outcome = parsed.taskArtifactOwnerRetirementOutcome;
4475
+ if (outcome?.kind !== "completed") throw new Error("task_artifact_owner_continuation_outcome_mismatch");
4476
+ verifyTaskArtifactOwnerPhysicalRetirement(
4477
+ trusted.ownerContext,
4478
+ parsed.taskArtifactOwnerDeletionEvidence,
4479
+ outcome,
4480
+ );
4481
+ }
4482
+ const relativePath = managedGcRetirementReceiptRelativePath(receipt.transcriptPath, receipt.state, attempt);
4483
+ publishManagedGcReceiptNoReplace(store, lock, relativePath, record);
4484
+ const persistedRead = readManagedGcSessionRetirementReceiptUnlocked(
4485
+ scope,
4486
+ receipt.transcriptPath,
4487
+ trusted,
4488
+ store,
4489
+ lock,
4490
+ );
4491
+ const persisted = persistedRead.receipt;
4492
+ if (!persisted) throw new Error("durability_failed");
4493
+ const expectedState = receipt.state;
4494
+ if (
4495
+ persisted.state !== expectedState ||
4496
+ (expectedState === "owner_pending" && persisted.ownerRetirementAttempt !== attempt) ||
4497
+ !deepSame(persisted.taskArtifactOwnerDeletionEvidence, parsed.taskArtifactOwnerDeletionEvidence)
4498
+ )
4499
+ throw new Error("durability_failed");
4500
+ return persisted;
4501
+ });
4502
+ }
4503
+ function retiredTargets(scope: ManagedScope, pathname: string): readonly RetiredTarget[] | undefined {
4504
+ let value: unknown;
4505
+ try {
4506
+ value = JSON.parse(captureManagedFileNoFollow(pathname).bytes.toString("utf8"));
4507
+ const targets = parseRetiredTargetRecord(scope, value);
4508
+ if (!targets) assertLegacyOwnerFreeRecord(value);
4509
+ return targets;
4510
+ } catch (error) {
4511
+ assertLegacyOwnerFreeRecord(value);
4512
+ if (isOwnerBoundaryFailure(error)) throw error;
4513
+ if (error instanceof SyntaxError) throw new Error("durability_failed", { cause: error });
4514
+ return undefined;
4515
+ }
4516
+ }
4517
+
4518
+ const OWNER_CONSUMER_UNAVAILABLE = "task_artifact_owner_legacy_scope_unsupported";
4519
+
4520
+ function isOwnerBoundaryFailure(error: unknown): boolean {
4521
+ return error instanceof Error && error.message.startsWith("task_artifact_owner_");
4522
+ }
4523
+
4524
+ function hasOwnerClaims(value: unknown): boolean {
4525
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
4526
+ const record = value as Record<string, unknown>;
4527
+ return (
4528
+ Object.keys(record).some(key => key.startsWith("taskArtifactOwner")) ||
4529
+ (record.target !== undefined && hasOwnerClaims(record.target)) ||
4530
+ (Array.isArray(record.targets) && record.targets.some(hasOwnerClaims))
4531
+ );
4532
+ }
4533
+
4534
+ function assertLegacyOwnerFreeRecord(value: unknown): void {
4535
+ if (hasOwnerClaims(value)) throw new Error(OWNER_CONSUMER_UNAVAILABLE);
4536
+ }
4537
+
4538
+ function assertOwnerTranscriptLocator(
4539
+ target: Pick<
4540
+ VerifiedSessionDeleteTarget,
4541
+ "transcriptPath" | "sessionId" | "transcriptIdentity" | "detachedTranscriptPath"
4542
+ >,
4543
+ evidence?: TaskArtifactOwnerDeletionEvidence,
4544
+ ): void {
4545
+ const pathname = target.detachedTranscriptPath ?? target.transcriptPath;
4546
+ let snapshot: SessionStorageSnapshot;
4547
+ try {
4548
+ snapshot = new FileSessionStorage().readSnapshotSync(pathname);
4549
+ } catch (error) {
4550
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
4551
+ throw error;
4552
+ }
4553
+ // Descriptor-bound owner classification is data, not authorization; storage retains its exact identity fence.
4554
+ const locator = taskArtifactOwnerLocatorFromTranscriptBytes(snapshot.bytes, target.sessionId);
4555
+ if ((locator && !evidence) || (evidence && !locator)) throw new Error("task_artifact_owner_locator_missing");
4556
+ if (locator && !deepSame(locator, evidence?.locator))
4557
+ throw new Error("task_artifact_owner_continuation_evidence_mismatch");
4558
+ }
4559
+
4560
+ function parseRetiredTargetRecord(scope: ManagedScope, value: unknown): readonly RetiredTarget[] | undefined {
4561
+ if (!value || typeof value !== "object") return undefined;
4562
+ if (Object.keys(value).some(key => key.startsWith("taskArtifactOwner"))) throw new Error(OWNER_CONSUMER_UNAVAILABLE);
4563
+ const record = value as { schemaVersion?: unknown; state?: unknown; scope?: unknown; targets?: unknown };
4564
+ if (
4565
+ record.schemaVersion !== 2 ||
4566
+ record.state !== "retired" ||
4567
+ record.scope !== scopeDigest(scope.platform, scope.canonicalCwd) ||
4568
+ !Array.isArray(record.targets)
4569
+ )
4570
+ return undefined;
4571
+ const targets: RetiredTarget[] = [];
4572
+ for (const target of record.targets) {
4573
+ if (!target || typeof target !== "object" || Array.isArray(target)) return undefined;
4574
+ const item = target as Record<string, unknown>;
4575
+ if (
4576
+ Object.keys(item).some(
4577
+ key => key.startsWith("taskArtifactOwner") && key !== "taskArtifactOwnerDeletionEvidence",
4578
+ )
4579
+ )
4580
+ throw new Error(OWNER_CONSUMER_UNAVAILABLE);
4581
+ const identity = item.identity;
4582
+ if (!identity || typeof identity !== "object" || Array.isArray(identity)) return undefined;
4583
+ const fields = identity as Record<string, unknown>;
4584
+ if (
4585
+ typeof item.path !== "string" ||
4586
+ typeof item.sessionId !== "string" ||
4587
+ typeof item.cwd !== "string" ||
4588
+ !pathIsWithin(scope.sessionsRoot, item.path) ||
4589
+ (item.provenance !== undefined && item.provenance !== "v2" && item.provenance !== "legacy") ||
4590
+ typeof fields.canonicalPath !== "string" ||
4591
+ typeof fields.dev !== "string" ||
4592
+ typeof fields.ino !== "string" ||
4593
+ typeof fields.size !== "number" ||
4594
+ typeof fields.mtimeMs !== "number" ||
4595
+ typeof fields.mtimeNs !== "string" ||
4596
+ typeof fields.sha256 !== "string"
4597
+ )
4598
+ return undefined;
4599
+ const provenance =
4600
+ item.provenance === "v2" || item.provenance === "legacy"
4601
+ ? item.provenance
4602
+ : path.dirname(item.path) === scope.directoryPath
4603
+ ? "v2"
4604
+ : "legacy";
4605
+ let taskArtifactOwnerDeletionEvidence: TaskArtifactOwnerDeletionEvidence | undefined;
4606
+ if (Object.hasOwn(item, "taskArtifactOwnerDeletionEvidence")) {
4607
+ try {
4608
+ taskArtifactOwnerDeletionEvidence = parseTaskArtifactOwnerDeletionEvidence(
4609
+ item.taskArtifactOwnerDeletionEvidence,
4610
+ );
4611
+ if (taskArtifactOwnerDeletionEvidence.sessionId !== item.sessionId)
4612
+ throw new Error("owner_session_mismatch");
4613
+ } catch (cause) {
4614
+ throw new Error("task_artifact_owner_legacy_scope_unsupported", { cause });
4615
+ }
4616
+ }
4617
+ targets.push({
4618
+ ...(taskArtifactOwnerDeletionEvidence ? { taskArtifactOwnerDeletionEvidence } : {}),
4619
+ path: item.path,
4620
+ sessionId: item.sessionId,
4621
+ cwd: item.cwd,
4622
+ provenance,
4623
+ migrationState: provenance === "v2" ? "native_v2" : "legacy_unmigrated",
4624
+ identity: {
4625
+ canonicalPath: fields.canonicalPath,
4626
+ dev: BigInt(fields.dev),
4627
+ ino: BigInt(fields.ino),
4628
+ size: fields.size,
4629
+ mtimeMs: fields.mtimeMs,
4630
+ mtimeNs: BigInt(fields.mtimeNs),
4631
+ sha256: fields.sha256,
4632
+ sessionId: item.sessionId,
4633
+ },
4634
+ });
4635
+ }
4636
+ return targets;
4637
+ }
4638
+
4639
+ /**
4640
+ * Append-only cleanup state machine:
4641
+ * `pending(N)` durably authorizes only its planned quarantine names before detach;
4642
+ * a returned partial result appends `pending(N + 1)` with the observed detached
4643
+ * identity/path. Restart accepts only a contiguous, target-bound sequence.
4644
+ */
4645
+ type RetainedArtifactsRootReceipt = {
4646
+ path: string;
4647
+ identity: SessionStorageFileIdentity;
4648
+ tree: NativeDirectoryTreeSnapshot;
4649
+ };
4650
+
4651
+ function retainedArtifactsRootReceipt(receipt: CleanupReceipt): RetainedArtifactsRootReceipt | undefined {
4652
+ const pathname = receipt.detachedArtifactsPath ?? deterministicRemovalRoot(receipt.plannedArtifactsPath);
4653
+ if (!fs.existsSync(pathname)) return undefined;
4654
+ if (!isQuarantinePath(receipt.target, pathname) || !receipt.expectedArtifactsIdentity)
4655
+ throw new Error("durability_failed");
4656
+ const identity = artifactIdentityAt(pathname);
4657
+ if (
4658
+ !identity ||
4659
+ identity.dev !== receipt.expectedArtifactsIdentity.dev ||
4660
+ identity.ino !== receipt.expectedArtifactsIdentity.ino
4661
+ )
4662
+ throw new Error("durability_failed");
4663
+ const tree = snapshotArtifactTree(pathname);
4664
+ if (!artifactTreePayloadAbsent(tree)) throw new Error("durability_failed");
4665
+ return { path: pathname, identity, tree };
4666
+ }
4667
+
4668
+ function retainedArtifactsRootMatches(record: Record<string, unknown>): boolean {
4669
+ if (record.retainedArtifactsRoot === undefined) return true;
4670
+ if (!record.retainedArtifactsRoot || typeof record.retainedArtifactsRoot !== "object")
4671
+ throw new Error("durability_failed");
4672
+ const retained = record.retainedArtifactsRoot as Record<string, unknown>;
4673
+ const identity = retained.identity;
4674
+ const tree = artifactTreeSnapshot(retained.tree);
4675
+ const expectedRetainedPath =
4676
+ typeof record.detachedArtifactsPath === "string"
4677
+ ? record.detachedArtifactsPath
4678
+ : typeof record.plannedArtifactsPath === "string"
4679
+ ? deterministicRemovalRoot(record.plannedArtifactsPath)
4680
+ : undefined;
4681
+ if (
4682
+ !expectedRetainedPath ||
4683
+ retained.path !== expectedRetainedPath ||
4684
+ record.artifactsPayloadDurable !== true ||
4685
+ !identity ||
4686
+ typeof identity !== "object" ||
4687
+ Array.isArray(identity) ||
4688
+ !tree ||
4689
+ !artifactTreePayloadAbsent(tree)
4690
+ )
4691
+ throw new Error("durability_failed");
4692
+ const artifactIdentity = identity as Record<string, unknown>;
4693
+ if (
4694
+ typeof artifactIdentity.dev !== "string" ||
4695
+ typeof artifactIdentity.ino !== "string" ||
4696
+ typeof artifactIdentity.size !== "number" ||
4697
+ typeof artifactIdentity.mtimeNs !== "string" ||
4698
+ typeof artifactIdentity.sha256 !== "string"
4699
+ )
4700
+ throw new Error("durability_failed");
4701
+ if (!fs.existsSync(retained.path)) return true;
4702
+ const observed = artifactIdentityAt(retained.path);
4703
+ if (
4704
+ !observed ||
4705
+ observed.dev !== BigInt(artifactIdentity.dev) ||
4706
+ observed.ino !== BigInt(artifactIdentity.ino) ||
4707
+ !artifactTreePayloadAbsent(snapshotArtifactTree(retained.path))
4708
+ )
4709
+ throw new Error("durability_failed");
4710
+ return true;
4711
+ }
4712
+
4713
+ type CleanupReceipt = {
4714
+ attempt: number;
4715
+ target: RetiredTarget;
4716
+ taskArtifactOwnerTranscriptDeleted?: true;
4717
+ expectedArtifactsIdentity?: SessionStorageFileIdentity;
4718
+ expectedArtifactsTree?: NativeDirectoryTreeSnapshot;
4719
+ artifactsPayloadDurable?: true;
4720
+ artifactsRemovedAttempt?: number;
4721
+ detachedArtifactsPath?: string;
4722
+ detachedTranscriptPath?: string;
4723
+ transcriptPayloadDurable?: true;
4724
+ retainedArtifactsSuccessorPath?: string;
4725
+ retainedArtifactsPlaceholderPath?: string;
4726
+ retainedArtifactsUnknownPath?: string;
4727
+ retainedTranscriptSuccessorPath?: string;
4728
+ retainedTranscriptPlaceholderPath?: string;
4729
+ retainedTranscriptUnknownPath?: string;
4730
+
4731
+ plannedArtifactsPath: string;
4732
+ plannedTranscriptPath: string;
4733
+ };
4734
+
4735
+ function cleanupReceiptPath(
4736
+ tombstone: string,
4737
+ target: RetiredTarget,
4738
+ state: "pending" | "artifacts_removed" | "completed",
4739
+ attempt: number,
4740
+ ): string {
4741
+ return path.join(
4742
+ path.dirname(tombstone),
4743
+ `${path.basename(tombstone, ".json")}.${stableOperationName(target)}.cleanup-${state}-${attempt}.json`,
4744
+ );
4745
+ }
4746
+
4747
+ function cleanupReceipt(scope: ManagedScope, tombstone: string, receipt: CleanupReceipt): Record<string, unknown> {
4748
+ return {
4749
+ schemaVersion: 2,
4750
+ state: "cleanup_pending",
4751
+ scope: scopeDigest(scope.platform, scope.canonicalCwd),
4752
+ tombstone,
4753
+ attempt: receipt.attempt,
4754
+ target: {
4755
+ path: receipt.target.path,
4756
+ sessionId: receipt.target.sessionId,
4757
+ cwd: receipt.target.cwd,
4758
+ identity: receipt.target.identity,
4759
+ },
4760
+ ...(receipt.taskArtifactOwnerTranscriptDeleted ? { taskArtifactOwnerTranscriptDeleted: true } : {}),
4761
+ ...(receipt.expectedArtifactsIdentity ? { expectedArtifactsIdentity: receipt.expectedArtifactsIdentity } : {}),
4762
+ ...(receipt.expectedArtifactsTree ? { expectedArtifactsTree: receipt.expectedArtifactsTree } : {}),
4763
+ ...(receipt.artifactsPayloadDurable === true ? { artifactsPayloadDurable: true } : {}),
4764
+ ...(receipt.artifactsRemovedAttempt !== undefined
4765
+ ? { artifactsRemovedAttempt: receipt.artifactsRemovedAttempt }
4766
+ : {}),
4767
+ ...(receipt.detachedArtifactsPath ? { detachedArtifactsPath: receipt.detachedArtifactsPath } : {}),
4768
+ ...(receipt.detachedTranscriptPath ? { detachedTranscriptPath: receipt.detachedTranscriptPath } : {}),
4769
+ ...(receipt.transcriptPayloadDurable === true ? { transcriptPayloadDurable: true } : {}),
4770
+ ...(receipt.retainedArtifactsSuccessorPath
4771
+ ? { retainedArtifactsSuccessorPath: receipt.retainedArtifactsSuccessorPath }
4772
+ : {}),
4773
+ ...(receipt.retainedArtifactsPlaceholderPath
4774
+ ? { retainedArtifactsPlaceholderPath: receipt.retainedArtifactsPlaceholderPath }
4775
+ : {}),
4776
+ ...(receipt.retainedArtifactsUnknownPath
4777
+ ? { retainedArtifactsUnknownPath: receipt.retainedArtifactsUnknownPath }
4778
+ : {}),
4779
+ ...(receipt.retainedTranscriptSuccessorPath
4780
+ ? { retainedTranscriptSuccessorPath: receipt.retainedTranscriptSuccessorPath }
4781
+ : {}),
4782
+ ...(receipt.retainedTranscriptPlaceholderPath
4783
+ ? { retainedTranscriptPlaceholderPath: receipt.retainedTranscriptPlaceholderPath }
4784
+ : {}),
4785
+ ...(receipt.retainedTranscriptUnknownPath
4786
+ ? { retainedTranscriptUnknownPath: receipt.retainedTranscriptUnknownPath }
4787
+ : {}),
4788
+ plannedArtifactsPath: receipt.plannedArtifactsPath,
4789
+ plannedTranscriptPath: receipt.plannedTranscriptPath,
4790
+ };
4791
+ }
4792
+
4793
+ type CleanupArtifactsRemovedEvidence = { retainedArtifactsRootPath?: string };
4794
+
4795
+ function cleanupArtifactsRemovedEvidence(
4796
+ scope: ManagedScope,
4797
+ tombstone: string,
4798
+ target: RetiredTarget,
4799
+ attempt: number,
4800
+ ): CleanupArtifactsRemovedEvidence | undefined {
4801
+ try {
4802
+ const value: unknown = JSON.parse(
4803
+ captureManagedFileNoFollow(cleanupReceiptPath(tombstone, target, "artifacts_removed", attempt)).bytes.toString(
2308
4804
  "utf8",
2309
4805
  ),
2310
4806
  );
@@ -2875,133 +5371,231 @@ function artifactTreeSnapshot(value: unknown): NativeDirectoryTreeSnapshot | und
2875
5371
  return snapshot as unknown as NativeDirectoryTreeSnapshot;
2876
5372
  }
2877
5373
 
5374
+ const CLEANUP_RECEIPT_REPLAY_MAX_BYTES = Math.min(
5375
+ MANAGED_ARTIFACT_MAX_FILE_BYTES,
5376
+ MANAGED_SESSION_READ_RANGE_MAX_BYTES,
5377
+ );
5378
+ const CLEANUP_RECEIPT_REPLAY_MAX_WORK = MANAGED_ARTIFACT_MAX_FILES * 2;
5379
+ const CLEANUP_RECEIPT_REPLAY_CHUNK_BYTES = 64 * 1024;
5380
+
5381
+ interface CleanupReceiptReplayBudget {
5382
+ workUnits: number;
5383
+ directoryEntries: number;
5384
+ receiptReads: number;
5385
+ readBytes: number;
5386
+ }
5387
+
5388
+ function cleanupReceiptReplayCapacity(): never {
5389
+ throw new Error("managed_gc_journal_capacity_exceeded");
5390
+ }
5391
+
5392
+ function chargeCleanupReceiptReplayWork(budget: CleanupReceiptReplayBudget, amount = 1): void {
5393
+ if (!Number.isSafeInteger(amount) || amount < 0 || budget.workUnits > CLEANUP_RECEIPT_REPLAY_MAX_WORK - amount)
5394
+ cleanupReceiptReplayCapacity();
5395
+ budget.workUnits += amount;
5396
+ }
5397
+
5398
+ function admitCleanupReceiptReplayEntry(budget: CleanupReceiptReplayBudget): void {
5399
+ if (budget.directoryEntries >= MANAGED_ARTIFACT_MAX_FILES) cleanupReceiptReplayCapacity();
5400
+ chargeCleanupReceiptReplayWork(budget);
5401
+ budget.directoryEntries++;
5402
+ }
5403
+
5404
+ function admitCleanupReceiptReplayRead(budget: CleanupReceiptReplayBudget, size: number): void {
5405
+ if (
5406
+ !Number.isSafeInteger(size) ||
5407
+ size < 0 ||
5408
+ size > CLEANUP_RECEIPT_REPLAY_MAX_BYTES ||
5409
+ budget.receiptReads >= MANAGED_ARTIFACT_MAX_FILES ||
5410
+ budget.readBytes > MANAGED_ARTIFACT_MAX_TOTAL_BYTES - size
5411
+ )
5412
+ cleanupReceiptReplayCapacity();
5413
+ chargeCleanupReceiptReplayWork(budget, Math.max(1, Math.ceil(size / CLEANUP_RECEIPT_REPLAY_CHUNK_BYTES)));
5414
+ budget.receiptReads++;
5415
+ budget.readBytes += size;
5416
+ }
5417
+
2878
5418
  function pendingCleanupReceipt(
2879
5419
  scope: ManagedScope,
2880
5420
  tombstone: string,
2881
5421
  target: RetiredTarget,
2882
5422
  ): CleanupReceipt | undefined {
5423
+ const budget: CleanupReceiptReplayBudget = { workUnits: 0, directoryEntries: 0, receiptReads: 0, readBytes: 0 };
2883
5424
  try {
2884
5425
  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));
5426
+ const directory = fs.opendirSync(path.dirname(tombstone));
5427
+ const records: Array<{ readonly name: string; readonly attempt: number }> = [];
5428
+ try {
5429
+ for (;;) {
5430
+ const entry = directory.readSync();
5431
+ if (!entry) break;
5432
+ admitCleanupReceiptReplayEntry(budget);
5433
+ if (!entry.name.startsWith(prefix) || !entry.name.endsWith(".json")) continue;
5434
+ const suffix = entry.name.slice(prefix.length, -".json".length);
5435
+ if (!/^[1-9][0-9]*$/u.test(suffix)) throw new Error("durability_failed");
5436
+ const attempt = Number(suffix);
5437
+ if (!Number.isSafeInteger(attempt) || String(attempt) !== suffix) throw new Error("durability_failed");
5438
+ if (records.length >= MANAGED_ARTIFACT_MAX_FILES) cleanupReceiptReplayCapacity();
5439
+ records.push({ name: entry.name, attempt });
5440
+ }
5441
+ } finally {
5442
+ directory.closeSync();
5443
+ }
5444
+ records.sort((left, right) => left.attempt - right.attempt);
2898
5445
  let latest: CleanupReceipt | undefined;
2899
5446
  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");
5447
+ for (const { name, attempt } of records) {
5448
+ let snapshot: ManagedFileSnapshot;
5449
+ try {
5450
+ snapshot = captureManagedFileNoFollowBounded(
5451
+ path.join(path.dirname(tombstone), name),
5452
+ CLEANUP_RECEIPT_REPLAY_MAX_BYTES,
5453
+ size => admitCleanupReceiptReplayRead(budget, size),
5454
+ );
5455
+ } catch (error) {
5456
+ if (error instanceof Error && error.message === "artifact_capacity_exceeded")
5457
+ cleanupReceiptReplayCapacity();
5458
+ throw error;
5459
+ }
5460
+ const value: unknown = JSON.parse(snapshot.bytes.toString("utf8"));
5461
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("durability_failed");
5462
+ latest = parseCleanupReceiptRecord(
5463
+ scope,
5464
+ tombstone,
5465
+ target,
5466
+ value as Record<string, unknown>,
5467
+ attempt,
5468
+ latest,
5469
+ plannedPaths,
5470
+ );
5471
+ }
5472
+ return latest;
5473
+ } catch (error) {
5474
+ if (
5475
+ (error as Error).message === "durability_failed" ||
5476
+ (error as Error).message === "managed_gc_journal_capacity_exceeded" ||
5477
+ isOwnerBoundaryFailure(error)
5478
+ )
5479
+ throw error;
5480
+ return undefined;
5481
+ }
5482
+ }
5483
+
5484
+ function parseCleanupReceiptRecord(
5485
+ scope: ManagedScope,
5486
+ tombstone: string,
5487
+ target: RetiredTarget,
5488
+ record: Record<string, unknown>,
5489
+ filenameAttempt: number,
5490
+ latest: CleanupReceipt | undefined,
5491
+ plannedPaths: Set<string>,
5492
+ ): CleanupReceipt {
5493
+ if (
5494
+ Object.keys(record).some(
5495
+ key => key.startsWith("taskArtifactOwner") && key !== "taskArtifactOwnerTranscriptDeleted",
5496
+ )
5497
+ )
5498
+ throw new Error(OWNER_CONSUMER_UNAVAILABLE);
5499
+ const attempt = record.attempt;
5500
+ const recorded = record.target as Record<string, unknown> | undefined;
5501
+ const identity = recorded?.identity as Record<string, unknown> | undefined;
5502
+ if (
5503
+ record.schemaVersion !== 2 ||
5504
+ record.state !== "cleanup_pending" ||
5505
+ record.scope !== scopeDigest(scope.platform, scope.canonicalCwd) ||
5506
+ record.tombstone !== tombstone ||
5507
+ typeof attempt !== "number" ||
5508
+ !Number.isSafeInteger(attempt) ||
5509
+ attempt !== filenameAttempt ||
5510
+ attempt !== (latest?.attempt ?? 0) + 1 ||
5511
+ recorded?.path !== target.path ||
5512
+ recorded.sessionId !== target.sessionId ||
5513
+ recorded.cwd !== target.cwd ||
5514
+ identity?.dev !== String(target.identity.dev) ||
5515
+ identity.ino !== String(target.identity.ino) ||
5516
+ identity.size !== target.identity.size ||
5517
+ identity.mtimeNs !== String(target.identity.mtimeNs) ||
5518
+ identity.sha256 !== target.identity.sha256 ||
5519
+ !isQuarantinePath(target, record.plannedArtifactsPath) ||
5520
+ !isQuarantinePath(target, record.plannedTranscriptPath) ||
5521
+ record.plannedArtifactsPath === record.plannedTranscriptPath ||
5522
+ plannedPaths.has(record.plannedArtifactsPath as string) ||
5523
+ plannedPaths.has(record.plannedTranscriptPath as string) ||
5524
+ (record.detachedArtifactsPath !== undefined && !isQuarantinePath(target, record.detachedArtifactsPath)) ||
5525
+ (record.detachedTranscriptPath !== undefined && !isQuarantinePath(target, record.detachedTranscriptPath)) ||
5526
+ (record.retainedArtifactsSuccessorPath !== undefined &&
5527
+ !isRetainedNativePath(target, record.retainedArtifactsSuccessorPath)) ||
5528
+ (record.retainedArtifactsPlaceholderPath !== undefined &&
5529
+ !isRetainedNativePath(target, record.retainedArtifactsPlaceholderPath)) ||
5530
+ (record.retainedArtifactsUnknownPath !== undefined &&
5531
+ !isRetainedNativePath(target, record.retainedArtifactsUnknownPath)) ||
5532
+ (record.retainedTranscriptSuccessorPath !== undefined &&
5533
+ !isRetainedNativePath(target, record.retainedTranscriptSuccessorPath)) ||
5534
+ (record.retainedTranscriptPlaceholderPath !== undefined &&
5535
+ !isRetainedNativePath(target, record.retainedTranscriptPlaceholderPath)) ||
5536
+ (record.retainedTranscriptUnknownPath !== undefined &&
5537
+ !isRetainedNativePath(target, record.retainedTranscriptUnknownPath)) ||
5538
+ (record.artifactsPayloadDurable !== undefined && record.artifactsPayloadDurable !== true) ||
5539
+ (record.artifactsRemovedAttempt !== undefined &&
5540
+ (typeof record.artifactsRemovedAttempt !== "number" ||
5541
+ !Number.isSafeInteger(record.artifactsRemovedAttempt) ||
5542
+ record.artifactsRemovedAttempt < 1 ||
5543
+ record.artifactsRemovedAttempt > (attempt as number))) ||
5544
+ (record.transcriptPayloadDurable !== undefined && record.transcriptPayloadDurable !== true) ||
5545
+ (record.taskArtifactOwnerTranscriptDeleted !== undefined &&
5546
+ (record.taskArtifactOwnerTranscriptDeleted !== true || !target.taskArtifactOwnerDeletionEvidence))
5547
+ )
5548
+ throw new Error("durability_failed");
5549
+ const artifact = record.expectedArtifactsIdentity as Record<string, unknown> | undefined;
5550
+ const expectedArtifactsIdentity = artifact
5551
+ ? typeof artifact.dev === "string" &&
5552
+ typeof artifact.ino === "string" &&
5553
+ typeof artifact.size === "number" &&
5554
+ typeof artifact.mtimeNs === "string" &&
5555
+ typeof artifact.sha256 === "string"
5556
+ ? {
5557
+ dev: BigInt(artifact.dev),
5558
+ ino: BigInt(artifact.ino),
5559
+ size: artifact.size,
5560
+ mtimeNs: BigInt(artifact.mtimeNs),
5561
+ sha256: artifact.sha256,
5562
+ }
5563
+ : undefined
5564
+ : undefined;
5565
+ if (artifact && !expectedArtifactsIdentity) throw new Error("durability_failed");
5566
+ const expectedArtifactsTree =
5567
+ record.expectedArtifactsTree === undefined ? undefined : artifactTreeSnapshot(record.expectedArtifactsTree);
5568
+ if (record.expectedArtifactsTree !== undefined && !expectedArtifactsTree) throw new Error("durability_failed");
2968
5569
 
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
- }
5570
+ if (
5571
+ latest &&
5572
+ ((record.detachedArtifactsPath !== undefined &&
5573
+ ![...plannedPaths].some(planned => isAuthorizedArtifactRoot(target, planned, record.detachedArtifactsPath))) ||
5574
+ (record.detachedTranscriptPath !== undefined && !plannedPaths.has(record.detachedTranscriptPath)))
5575
+ )
5576
+ throw new Error("durability_failed");
5577
+ plannedPaths.add(record.plannedArtifactsPath as string);
5578
+ plannedPaths.add(record.plannedTranscriptPath as string);
5579
+ return {
5580
+ taskArtifactOwnerTranscriptDeleted: record.taskArtifactOwnerTranscriptDeleted === true ? true : undefined,
5581
+ attempt,
5582
+ target,
5583
+ expectedArtifactsIdentity,
5584
+ expectedArtifactsTree,
5585
+ artifactsPayloadDurable: record.artifactsPayloadDurable === true ? true : undefined,
5586
+ artifactsRemovedAttempt: record.artifactsRemovedAttempt as number | undefined,
5587
+ detachedArtifactsPath: record.detachedArtifactsPath as string | undefined,
5588
+ detachedTranscriptPath: record.detachedTranscriptPath as string | undefined,
5589
+ transcriptPayloadDurable: record.transcriptPayloadDurable === true ? true : undefined,
5590
+ retainedArtifactsSuccessorPath: record.retainedArtifactsSuccessorPath as string | undefined,
5591
+ retainedArtifactsPlaceholderPath: record.retainedArtifactsPlaceholderPath as string | undefined,
5592
+ retainedArtifactsUnknownPath: record.retainedArtifactsUnknownPath as string | undefined,
5593
+ retainedTranscriptSuccessorPath: record.retainedTranscriptSuccessorPath as string | undefined,
5594
+ retainedTranscriptPlaceholderPath: record.retainedTranscriptPlaceholderPath as string | undefined,
5595
+ retainedTranscriptUnknownPath: record.retainedTranscriptUnknownPath as string | undefined,
5596
+ plannedArtifactsPath: record.plannedArtifactsPath as string,
5597
+ plannedTranscriptPath: record.plannedTranscriptPath as string,
5598
+ };
3005
5599
  }
3006
5600
 
3007
5601
  function artifactIdentityForCleanup(target: RetiredTarget): SessionStorageFileIdentity | undefined {
@@ -3043,6 +5637,7 @@ function nextCleanupReceipt(target: RetiredTarget, pending: CleanupReceipt | und
3043
5637
  return {
3044
5638
  attempt,
3045
5639
  target,
5640
+ taskArtifactOwnerTranscriptDeleted: pending?.taskArtifactOwnerTranscriptDeleted,
3046
5641
  expectedArtifactsIdentity,
3047
5642
  expectedArtifactsTree,
3048
5643
  artifactsPayloadDurable: pending?.artifactsPayloadDurable,
@@ -3076,6 +5671,8 @@ function cleanupPendingEvidence(
3076
5671
  ): CleanupReceipt {
3077
5672
  return {
3078
5673
  ...retry,
5674
+ taskArtifactOwnerTranscriptDeleted:
5675
+ deletion.taskArtifactOwnerTranscriptDeleted ?? active.taskArtifactOwnerTranscriptDeleted,
3079
5676
  expectedArtifactsIdentity:
3080
5677
  deletion.phase === "artifacts" ? deletion.artifactsIdentity : active.expectedArtifactsIdentity,
3081
5678
  expectedArtifactsTree: deletion.phase === "artifacts" ? deletion.artifactsTree : active.expectedArtifactsTree,
@@ -3119,29 +5716,43 @@ async function continueDetachedArtifactCleanup(
3119
5716
  lock: ManagedStorageLock,
3120
5717
  flow: ManagedVerifiedDeleteTestEvent["flow"],
3121
5718
  ): 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
- });
5719
+ const deletion = await deleteSessionVerifiedWithFence(
5720
+ flow,
5721
+ "artifact-finalization",
5722
+ lock,
5723
+ {
5724
+ sessionsRoot: scope.sessionsRoot,
5725
+ transcriptPath: target.path,
5726
+ sessionId: target.sessionId,
5727
+ cwd: target.cwd,
5728
+ transcriptIdentity: target.identity,
5729
+ transcriptParentIdentity: (() => {
5730
+ const parent = fs.lstatSync(path.dirname(target.path), { bigint: true });
5731
+ return { dev: parent.dev, ino: parent.ino };
5732
+ })(),
5733
+ expectedArtifactsIdentity: pendingEvidence.expectedArtifactsIdentity,
5734
+ expectedArtifactsTree: pendingEvidence.expectedArtifactsTree,
5735
+ detachedArtifactsPath: pendingEvidence.detachedArtifactsPath,
5736
+ retainedArtifactsSuccessorPath: pendingEvidence.retainedArtifactsSuccessorPath,
5737
+ retainedArtifactsPlaceholderPath: pendingEvidence.retainedArtifactsPlaceholderPath,
5738
+ retainedArtifactsUnknownPath: pendingEvidence.retainedArtifactsUnknownPath,
5739
+ detachedTranscriptPath: pendingEvidence.detachedTranscriptPath ?? fallbackDetachedTranscriptPath,
5740
+ retainedTranscriptSuccessorPath: pendingEvidence.retainedTranscriptSuccessorPath,
5741
+ retainedTranscriptPlaceholderPath: pendingEvidence.retainedTranscriptPlaceholderPath,
5742
+ retainedTranscriptUnknownPath: pendingEvidence.retainedTranscriptUnknownPath,
5743
+ plannedArtifactsPath: pendingEvidence.plannedArtifactsPath,
5744
+ plannedTranscriptPath: pendingEvidence.plannedTranscriptPath,
5745
+ },
5746
+ scope,
5747
+ target,
5748
+ );
5749
+ if (deletion.kind === "cleanup_pending" && deletion.phase === "task_artifact_owner") {
5750
+ await persistManagedGcStorageOwnerDisposition(scope, target, deletion, lock);
5751
+ const retry = nextCleanupReceipt(target, pendingEvidence);
5752
+ const pending = cleanupPendingEvidence(retry, pendingEvidence, deletion);
5753
+ await publishCleanupPending(scope, tombstone, pending, lock);
5754
+ return { deletion, pendingEvidence: pending };
5755
+ }
3145
5756
  if (deletion.kind === "cleanup_pending") {
3146
5757
  if (
3147
5758
  deletion.phase !== "artifacts" ||
@@ -3180,33 +5791,110 @@ async function publishCleanupPending(
3180
5791
  if (!persisted || persisted.attempt !== receipt.attempt) throw new Error("durability_failed");
3181
5792
  }
3182
5793
 
3183
- function cleanupCompleted(scope: ManagedScope, tombstone: string, target: RetiredTarget): boolean {
5794
+ async function persistManagedOwnerTranscriptDeletion(
5795
+ scope: ManagedScope,
5796
+ tombstone: string,
5797
+ target: RetiredTarget,
5798
+ fallback: CleanupReceipt,
5799
+ lock: ManagedStorageLock,
5800
+ deletion: Extract<VerifiedSessionDeleteResult, { kind: "deleted" }>,
5801
+ ): Promise<void> {
5802
+ return persistManagedGcOwnerTranscriptDeletion({
5803
+ target,
5804
+ fallback,
5805
+ deletion,
5806
+ readLatest: () => pendingCleanupReceipt(scope, tombstone, target),
5807
+ nextReceipt: latest => nextCleanupReceipt(target, latest),
5808
+ publishPending: receipt => publishCleanupPending(scope, tombstone, receipt, lock),
5809
+ });
5810
+ }
5811
+
5812
+ async function cleanupCompleted(scope: ManagedScope, tombstone: string, target: RetiredTarget): Promise<boolean> {
3184
5813
  try {
5814
+ const ownerReceipt = target.taskArtifactOwnerDeletionEvidence
5815
+ ? await readManagedGcSessionRetirementReceipt(scope, target.path)
5816
+ : undefined;
5817
+ if (
5818
+ target.taskArtifactOwnerDeletionEvidence &&
5819
+ (ownerReceipt?.state !== "owner_retired" ||
5820
+ !deepSame(ownerReceipt.taskArtifactOwnerDeletionEvidence, target.taskArtifactOwnerDeletionEvidence))
5821
+ )
5822
+ return false;
3185
5823
  const value: unknown = JSON.parse(
3186
5824
  captureManagedFileNoFollow(cleanupReceiptPath(tombstone, target, "completed", 1)).bytes.toString("utf8"),
3187
5825
  );
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 {
5826
+ return cleanupCompletedRecordMatches(scope, tombstone, target, ownerReceipt, value);
5827
+ } catch (error) {
5828
+ if (isOwnerBoundaryFailure(error) || isManagedGcJournalCapacityError(error)) throw error;
5829
+ return false;
5830
+ }
5831
+ }
5832
+
5833
+ function cleanupCompletedRecordMatches(
5834
+ scope: ManagedScope,
5835
+ tombstone: string,
5836
+ target: RetiredTarget,
5837
+ ownerReceipt: ManagedGcSessionRetirementReceipt | undefined,
5838
+ value: unknown,
5839
+ ): boolean {
5840
+ if (
5841
+ target.taskArtifactOwnerDeletionEvidence &&
5842
+ (ownerReceipt?.state !== "owner_retired" ||
5843
+ !deepSame(ownerReceipt.taskArtifactOwnerDeletionEvidence, target.taskArtifactOwnerDeletionEvidence))
5844
+ )
3208
5845
  return false;
5846
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
5847
+ const record = value as Record<string, unknown>;
5848
+ const recorded = record.target as Record<string, unknown> | undefined;
5849
+ const identity = recorded?.identity as Record<string, unknown> | undefined;
5850
+ const expectedKeys = ownerReceipt
5851
+ ? [
5852
+ "schemaVersion",
5853
+ "state",
5854
+ "scope",
5855
+ "tombstone",
5856
+ "attempt",
5857
+ "target",
5858
+ "taskArtifactOwnerDeletionEvidence",
5859
+ "taskArtifactOwnerRetirementOutcome",
5860
+ "taskArtifactOwnerRetired",
5861
+ "taskArtifactOwnerTranscriptDeleted",
5862
+ ]
5863
+ : ["schemaVersion", "state", "scope", "tombstone", "attempt", "target"];
5864
+ if (Object.keys(record).length !== expectedKeys.length || !expectedKeys.every(key => key in record)) return false;
5865
+ if (ownerReceipt) {
5866
+ const pending = pendingCleanupReceipt(scope, tombstone, target);
5867
+ if (record.taskArtifactOwnerTranscriptDeleted !== true || !pending?.taskArtifactOwnerTranscriptDeleted)
5868
+ return false;
5869
+ const evidence = parseTaskArtifactOwnerDeletionEvidence(record.taskArtifactOwnerDeletionEvidence);
5870
+ const outcome = parseTaskArtifactOwnerRetirementOutcome(
5871
+ taskArtifactOwnerStorageContextForScope(scope),
5872
+ evidence,
5873
+ record.taskArtifactOwnerRetirementOutcome,
5874
+ );
5875
+ if (
5876
+ record.taskArtifactOwnerRetired !== true ||
5877
+ outcome.kind !== "completed" ||
5878
+ !deepSame(evidence, ownerReceipt.taskArtifactOwnerDeletionEvidence) ||
5879
+ !deepSame(outcome, ownerReceipt.taskArtifactOwnerRetirementOutcome)
5880
+ )
5881
+ return false;
3209
5882
  }
5883
+ return (
5884
+ record.schemaVersion === 1 &&
5885
+ record.state === "cleanup_completed" &&
5886
+ record.scope === scopeDigest(scope.platform, scope.canonicalCwd) &&
5887
+ record.tombstone === tombstone &&
5888
+ record.attempt === 1 &&
5889
+ recorded?.path === target.path &&
5890
+ recorded.sessionId === target.sessionId &&
5891
+ recorded.cwd === target.cwd &&
5892
+ identity?.dev === String(target.identity.dev) &&
5893
+ identity.ino === String(target.identity.ino) &&
5894
+ identity.size === target.identity.size &&
5895
+ identity.mtimeNs === String(target.identity.mtimeNs) &&
5896
+ identity.sha256 === target.identity.sha256
5897
+ );
3210
5898
  }
3211
5899
 
3212
5900
  async function publishCleanupCompleted(
@@ -3215,6 +5903,22 @@ async function publishCleanupCompleted(
3215
5903
  target: RetiredTarget,
3216
5904
  lock: ManagedStorageLock,
3217
5905
  ): Promise<void> {
5906
+ const ownerReceipt = target.taskArtifactOwnerDeletionEvidence
5907
+ ? await readManagedGcSessionRetirementReceipt(scope, target.path)
5908
+ : undefined;
5909
+ if (
5910
+ target.taskArtifactOwnerDeletionEvidence &&
5911
+ (ownerReceipt?.state !== "owner_retired" ||
5912
+ !deepSame(ownerReceipt.taskArtifactOwnerDeletionEvidence, target.taskArtifactOwnerDeletionEvidence))
5913
+ )
5914
+ throw new Error("durability_failed");
5915
+ const ownerOutcome = ownerReceipt?.taskArtifactOwnerRetirementOutcome;
5916
+ if (
5917
+ ownerReceipt &&
5918
+ (ownerOutcome?.kind !== "completed" ||
5919
+ !pendingCleanupReceipt(scope, tombstone, target)?.taskArtifactOwnerTranscriptDeleted)
5920
+ )
5921
+ throw new Error("durability_failed");
3218
5922
  try {
3219
5923
  await publishManagedTombstone(
3220
5924
  cleanupReceiptPath(tombstone, target, "completed", 1),
@@ -3225,12 +5929,21 @@ async function publishCleanupCompleted(
3225
5929
  tombstone,
3226
5930
  attempt: 1,
3227
5931
  target: { path: target.path, sessionId: target.sessionId, cwd: target.cwd, identity: target.identity },
5932
+ ...(ownerReceipt && ownerOutcome
5933
+ ? {
5934
+ taskArtifactOwnerDeletionEvidence: ownerReceipt.taskArtifactOwnerDeletionEvidence,
5935
+ taskArtifactOwnerRetirementOutcome: ownerOutcome,
5936
+ taskArtifactOwnerRetired: true,
5937
+ taskArtifactOwnerTranscriptDeleted: true,
5938
+ }
5939
+ : {}),
3228
5940
  },
3229
5941
  lock.assertOwned,
3230
5942
  );
3231
5943
  } catch (error) {
3232
5944
  if ((error as Error).message !== "destination_conflict") throw error;
3233
5945
  }
5946
+ if (!(await cleanupCompleted(scope, tombstone, target))) throw new Error("durability_failed");
3234
5947
  }
3235
5948
 
3236
5949
  function tombstonePathContaining(scope: ManagedScope, candidate: ManagedCandidate): string | undefined {
@@ -3240,7 +5953,8 @@ function tombstonePathContaining(scope: ManagedScope, candidate: ManagedCandidat
3240
5953
  const pathname = path.join(directory, name);
3241
5954
  if (retiredTargets(scope, pathname)?.some(target => sameCandidate(target, candidate))) return pathname;
3242
5955
  }
3243
- } catch {
5956
+ } catch (error) {
5957
+ if (isOwnerBoundaryFailure(error) || (error as Error).message === "durability_failed") throw error;
3244
5958
  return undefined;
3245
5959
  }
3246
5960
  return undefined;
@@ -3554,7 +6268,7 @@ export function cleanupAuthorityMatches(
3554
6268
  parentStat.ino !== cleanup.identity.parentIno
3555
6269
  )
3556
6270
  return false;
3557
- const snapshot = nativeScope().snapshotDirectoryTree(cleanup.retainedPath);
6271
+ const snapshot = nativeScope().snapshotEmptyDirectory(cleanup.retainedPath);
3558
6272
  const observedRoot = snapshot.snapshot?.entries.find(
3559
6273
  entry => entry.relativePath === "" && entry.kind === "directory",
3560
6274
  );
@@ -3618,7 +6332,7 @@ export function detachArtifactRootForMigration(
3618
6332
  const stat = fs.lstatSync(placeholder, { bigint: true });
3619
6333
  if (!stat.isDirectory() || stat.isSymbolicLink() || path.dirname(placeholder) !== path.dirname(plan.originalPath))
3620
6334
  throw new Error("durability_failed");
3621
- const snapshot = nativeScope().snapshotDirectoryTree(placeholder);
6335
+ const snapshot = nativeScope().snapshotEmptyDirectory(placeholder);
3622
6336
  if (!snapshot.ok || !snapshot.snapshot) throw new Error("durability_failed");
3623
6337
  // Windows directory size/mtime authority is the native tree root, never Bun's
3624
6338
  // zero-valued directory lstat. Capturing Bun values here would guarantee a
@@ -3983,11 +6697,23 @@ export async function reconcileManagedTombstones(
3983
6697
  expectedCandidate?: ManagedCandidate,
3984
6698
  ): Promise<void> {
3985
6699
  const directory = path.join(managedInternalDirectory(scope), MANAGED_TOMBSTONES_DIRECTORY);
6700
+ // Ensure scope authority is prepared before any operations that might access the GC protocol
6701
+ const prepared = await ensureManagedScope(
6702
+ scope,
6703
+ scope.platform === "win32" ? "windows-existing-verify-first" : "default",
6704
+ );
6705
+ if (prepared.kind === "error") throw new Error(prepared.message);
3986
6706
  for (const name of fs.readdirSync(directory)) {
3987
6707
  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;
6708
+ const discoveredTargets = retiredTargets(scope, tombstone);
6709
+ if (!discoveredTargets || discoveredTargets.some(target => hasUnsupportedLegacyOwnerTarget(scope, target)))
6710
+ continue;
6711
+ if (
6712
+ await Promise.all(discoveredTargets.map(target => cleanupCompleted(scope, tombstone, target))).then(values =>
6713
+ values.every(Boolean),
6714
+ )
6715
+ )
6716
+ continue;
3991
6717
  let lock: ManagedStorageLock | undefined;
3992
6718
  try {
3993
6719
  lock = await acquireManagedLock(
@@ -3995,11 +6721,22 @@ export async function reconcileManagedTombstones(
3995
6721
  path.basename(tombstone, ".json"),
3996
6722
  scopeRoot(scope),
3997
6723
  );
3998
- const lockedTargets = retiredTargets(scope, tombstone);
3999
- if (!lockedTargets) continue;
6724
+ const discoveredLockedTargets = retiredTargets(scope, tombstone);
6725
+ if (
6726
+ !discoveredLockedTargets ||
6727
+ discoveredLockedTargets.some(target => hasUnsupportedLegacyOwnerTarget(scope, target))
6728
+ )
6729
+ continue;
6730
+ const lockedTargets = await prepareManagedGcOwnerTargets(scope, discoveredLockedTargets, lock);
6731
+ if (
6732
+ await Promise.all(lockedTargets.map(target => cleanupCompleted(scope, tombstone, target))).then(values =>
6733
+ values.every(Boolean),
6734
+ )
6735
+ )
6736
+ continue;
4000
6737
  for (const target of lockedTargets) {
4001
6738
  lock.assertOwned();
4002
- if (cleanupCompleted(scope, tombstone, target)) continue;
6739
+ if (await cleanupCompleted(scope, tombstone, target)) continue;
4003
6740
  try {
4004
6741
  const pending = pendingCleanupReceipt(scope, tombstone, target);
4005
6742
  const observedPending = pending ? probePlannedCleanupDetach(target, pending) : undefined;
@@ -4024,8 +6761,23 @@ export async function reconcileManagedTombstones(
4024
6761
  target,
4025
6762
  replayReceipt,
4026
6763
  artifactsEvidence.retainedArtifactsRootPath,
4027
- )
6764
+ ) &&
6765
+ (!target.taskArtifactOwnerDeletionEvidence ||
6766
+ pending?.taskArtifactOwnerTranscriptDeleted === true)
4028
6767
  ) {
6768
+ // A conflicting legacy completion record is not fresh completion authority.
6769
+ if (
6770
+ !target.taskArtifactOwnerDeletionEvidence &&
6771
+ fs.existsSync(cleanupReceiptPath(tombstone, target, "completed", 1))
6772
+ )
6773
+ continue;
6774
+ const ownerProgress = await retireManagedGcOwnerAfterArtifacts(
6775
+ scope,
6776
+ target,
6777
+ lockedTargets,
6778
+ lock,
6779
+ );
6780
+ if (ownerProgress.state === "pending" || ownerProgress.state === "payload_retired") continue;
4029
6781
  fsyncManagedParent(target.path);
4030
6782
  await publishCleanupCompleted(scope, tombstone, target, lock);
4031
6783
  continue;
@@ -4050,38 +6802,11 @@ export async function reconcileManagedTombstones(
4050
6802
  ? target
4051
6803
  : validateCandidateForScope(scope, target);
4052
6804
  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(
6805
+ let ownerProgress = await managedGcOwnerProgressBeforeDelete(
6806
+ scope,
6807
+ target,
6808
+ lockedTargets,
6809
+ cleanupArtifactsRemoved(
4085
6810
  scope,
4086
6811
  tombstone,
4087
6812
  target,
@@ -4089,12 +6814,65 @@ export async function reconcileManagedTombstones(
4089
6814
  active.artifactsRemovedAttempt ??
4090
6815
  pending?.attempt ??
4091
6816
  active.attempt,
4092
- )
4093
- ? { artifactsRemoved: true as const }
4094
- : {}),
4095
- });
6817
+ ),
6818
+ lock,
6819
+ );
6820
+ if (ownerProgress.state === "pending") continue;
6821
+ let deletion = await deleteSessionVerifiedWithFence(
6822
+ "reconcile",
6823
+ "initial",
6824
+ lock,
6825
+ {
6826
+ sessionsRoot: scope.sessionsRoot,
6827
+ transcriptPath: target.path,
6828
+ sessionId: target.sessionId,
6829
+ cwd: target.cwd,
6830
+ transcriptIdentity: {
6831
+ ...initialTarget.identity,
6832
+ nlink: fs.lstatSync(observedPending?.detachedTranscriptPath ?? target.path, { bigint: true })
6833
+ .nlink,
6834
+ },
6835
+ expectedArtifactsIdentity: active.expectedArtifactsIdentity,
6836
+ expectedArtifactsTree: active.expectedArtifactsTree,
6837
+ detachedArtifactsPath:
6838
+ active.detachedArtifactsPath ??
6839
+ observedPending?.detachedArtifactsPath ??
6840
+ (fs.existsSync(active.plannedArtifactsPath) ? active.plannedArtifactsPath : undefined),
6841
+ detachedTranscriptPath:
6842
+ active.detachedTranscriptPath ??
6843
+ observedPending?.detachedTranscriptPath ??
6844
+ (pending && fs.existsSync(pending.plannedTranscriptPath)
6845
+ ? pending.plannedTranscriptPath
6846
+ : undefined) ??
6847
+ (fs.existsSync(active.plannedTranscriptPath) ? active.plannedTranscriptPath : undefined),
6848
+ retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
6849
+ retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
6850
+ retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
6851
+ retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
6852
+ retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
6853
+ retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
6854
+ plannedArtifactsPath: active.plannedArtifactsPath,
6855
+ plannedTranscriptPath: active.plannedTranscriptPath,
6856
+ ...(cleanupArtifactsRemoved(
6857
+ scope,
6858
+ tombstone,
6859
+ target,
6860
+ pending?.artifactsRemovedAttempt ??
6861
+ active.artifactsRemovedAttempt ??
6862
+ pending?.attempt ??
6863
+ active.attempt,
6864
+ )
6865
+ ? { artifactsRemoved: true as const }
6866
+ : {}),
6867
+ },
6868
+ scope,
6869
+ target,
6870
+ );
4096
6871
  if (deletion.kind === "artifacts_removed") {
6872
+ await publishManagedGcArtifactsRemoved(scope, target, lock);
4097
6873
  await publishCleanupArtifactsRemoved(scope, tombstone, active, lock);
6874
+ ownerProgress = await retireManagedGcOwnerAfterArtifacts(scope, target, lockedTargets, lock);
6875
+ if (ownerProgress.state === "pending") continue;
4098
6876
  active = { ...active, artifactsRemovedAttempt: active.attempt };
4099
6877
  const refreshedTarget = validateCandidateForScope(scope, target);
4100
6878
  if (!refreshedTarget) throw new Error("source_changed");
@@ -4121,9 +6899,15 @@ export async function reconcileManagedTombstones(
4121
6899
  retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
4122
6900
  artifactsRemoved: true,
4123
6901
  },
6902
+ scope,
6903
+ target,
4124
6904
  );
4125
6905
  }
4126
6906
  if (deletion.kind === "cleanup_pending") {
6907
+ if (deletion.phase === "task_artifact_owner") {
6908
+ ownerProgress = await persistManagedGcStorageOwnerDisposition(scope, target, deletion, lock);
6909
+ continue;
6910
+ }
4127
6911
  assertAuthorizedCleanupPending(target, active, deletion);
4128
6912
  const retry = nextCleanupReceipt(target, active);
4129
6913
  let pendingEvidence = cleanupPendingEvidence(retry, active, deletion);
@@ -4150,7 +6934,10 @@ export async function reconcileManagedTombstones(
4150
6934
  !retainedArtifactPayloadAbsent(deletion.detachedArtifactsPath))
4151
6935
  )
4152
6936
  continue;
6937
+ await publishManagedGcArtifactsRemoved(scope, target, lock);
4153
6938
  await publishCleanupArtifactsRemoved(scope, tombstone, pendingEvidence, lock);
6939
+ ownerProgress = await retireManagedGcOwnerAfterArtifacts(scope, target, lockedTargets, lock);
6940
+ if (ownerProgress.state === "pending") continue;
4154
6941
  pendingEvidence = { ...pendingEvidence, artifactsRemovedAttempt: pendingEvidence.attempt };
4155
6942
  const retainedProof = cleanupArtifactsRemovedReceipt(
4156
6943
  tombstone,
@@ -4186,6 +6973,8 @@ export async function reconcileManagedTombstones(
4186
6973
  retainedTranscriptUnknownPath: pendingEvidence.retainedTranscriptUnknownPath,
4187
6974
  artifactsRemoved: true,
4188
6975
  },
6976
+ scope,
6977
+ target,
4189
6978
  () => {
4190
6979
  if (
4191
6980
  !cleanupArtifactsRemoved(
@@ -4198,6 +6987,10 @@ export async function reconcileManagedTombstones(
4198
6987
  throw new Error("durability_failed");
4199
6988
  },
4200
6989
  );
6990
+ if (deletion.kind === "cleanup_pending" && deletion.phase === "task_artifact_owner") {
6991
+ ownerProgress = await persistManagedGcStorageOwnerDisposition(scope, target, deletion, lock);
6992
+ continue;
6993
+ }
4201
6994
  if (deletion.kind === "cleanup_pending") {
4202
6995
  if (
4203
6996
  deletion.phase !== "transcript" ||
@@ -4231,9 +7024,19 @@ export async function reconcileManagedTombstones(
4231
7024
  )
4232
7025
  continue;
4233
7026
  }
7027
+ if (deletion.kind === "deleted")
7028
+ await persistManagedOwnerTranscriptDeletion(scope, tombstone, target, active, lock, deletion);
7029
+ const ownerDisposition = await taskArtifactOwnerTranscriptResult(scope, target, lock);
7030
+ if (
7031
+ (ownerDisposition !== "none" && ownerDisposition !== "retired") ||
7032
+ (target.taskArtifactOwnerDeletionEvidence &&
7033
+ !pendingCleanupReceipt(scope, tombstone, target)?.taskArtifactOwnerTranscriptDeleted)
7034
+ )
7035
+ continue;
4234
7036
  fsyncManagedParent(target.path);
4235
7037
  await publishCleanupCompleted(scope, tombstone, target, lock);
4236
7038
  } catch (error) {
7039
+ if (isManagedGcJournalCapacityError(error)) throw error;
4237
7040
  if (!expectedCandidate || target.sessionId === expectedCandidate.sessionId) throw error;
4238
7041
  logger.warn("Tombstone reconciliation failed for one target; will retry on a future scope open", {
4239
7042
  tombstone,
@@ -4651,6 +7454,24 @@ async function deleteManagedSessionCandidateInternal(
4651
7454
  scope: ManagedScope,
4652
7455
  candidate: ManagedCandidate,
4653
7456
  ): Promise<ManagedDeleteCandidateResult> {
7457
+ try {
7458
+ if (hasUnsupportedLegacyOwnerTargetInternal(scope.directoryPath, candidate)) {
7459
+ const tombstonePath = path.join(
7460
+ managedInternalDirectory(scope),
7461
+ MANAGED_TOMBSTONES_DIRECTORY,
7462
+ `${stableOperationName(candidate)}.json`,
7463
+ );
7464
+ return {
7465
+ kind: "cleanup_pending",
7466
+ tombstonePath,
7467
+ phase: "artifacts",
7468
+ message: "task_artifact_owner_legacy_scope_unsupported",
7469
+ };
7470
+ }
7471
+ } catch (error) {
7472
+ const code = expectedFailure(error);
7473
+ return { kind: "error", code, message: error instanceof Error ? error.message : "Managed deletion failed." };
7474
+ }
4654
7475
  const prepared = await prepareManagedSessionScopeForWrite(scope);
4655
7476
  if (prepared.kind === "error")
4656
7477
  return {
@@ -4660,6 +7481,12 @@ async function deleteManagedSessionCandidateInternal(
4660
7481
  };
4661
7482
  const current = validateCandidateForScope(scope, candidate);
4662
7483
  const paired = current ? receiptPair(scope, current) : undefined;
7484
+ if (paired)
7485
+ assertOwnerTranscriptLocator({
7486
+ transcriptPath: paired.path,
7487
+ sessionId: paired.sessionId,
7488
+ transcriptIdentity: paired.identity,
7489
+ });
4663
7490
  const logical = paired?.provenance === "legacy" ? paired : (current ?? candidate);
4664
7491
  const existingTombstone = tombstonePathContaining(scope, candidate);
4665
7492
  const tombstone =
@@ -4676,9 +7503,17 @@ async function deleteManagedSessionCandidateInternal(
4676
7503
  scopeRoot(scope),
4677
7504
  );
4678
7505
  let targets = retiredTargets(scope, tombstone);
7506
+ const initialTargets = targets ?? (current ? [current, ...(paired ? [paired] : [])] : []);
7507
+ if (initialTargets.some(target => hasUnsupportedLegacyOwnerTarget(scope, target)))
7508
+ return {
7509
+ kind: "cleanup_pending",
7510
+ tombstonePath: tombstone,
7511
+ phase: "artifacts",
7512
+ message: "task_artifact_owner_legacy_scope_unsupported",
7513
+ };
4679
7514
  if (!targets) {
4680
7515
  if (!current) throw new Error("source_changed");
4681
- targets = [current, ...(paired ? [paired] : [])];
7516
+ targets = await prepareManagedGcOwnerTargets(scope, [current, ...(paired ? [paired] : [])], lock);
4682
7517
  lock.assertOwned();
4683
7518
  try {
4684
7519
  await publishManagedTombstone(
@@ -4693,6 +7528,9 @@ async function deleteManagedSessionCandidateInternal(
4693
7528
  cwd: target.cwd,
4694
7529
  provenance: target.provenance,
4695
7530
  identity: target.identity,
7531
+ ...(target.taskArtifactOwnerDeletionEvidence
7532
+ ? { taskArtifactOwnerDeletionEvidence: target.taskArtifactOwnerDeletionEvidence }
7533
+ : {}),
4696
7534
  })),
4697
7535
  },
4698
7536
  lock.assertOwned,
@@ -4703,6 +7541,7 @@ async function deleteManagedSessionCandidateInternal(
4703
7541
  targets = retiredTargets(scope, tombstone);
4704
7542
  if (!targets) throw new Error("durability_failed");
4705
7543
  }
7544
+ targets = await prepareManagedGcOwnerTargets(scope, targets, lock);
4706
7545
  let deletedAny = false;
4707
7546
  for (const target of targets) {
4708
7547
  lock.assertOwned();
@@ -4712,6 +7551,42 @@ async function deleteManagedSessionCandidateInternal(
4712
7551
  fs.lstatSync(target.path);
4713
7552
  } catch (error) {
4714
7553
  if ((error as NodeJS.ErrnoException).code === "ENOENT") {
7554
+ if (!observedPending && target.taskArtifactOwnerDeletionEvidence) {
7555
+ if (!pending?.taskArtifactOwnerTranscriptDeleted)
7556
+ return {
7557
+ kind: "cleanup_pending",
7558
+ tombstonePath: tombstone,
7559
+ phase: "artifacts",
7560
+ message: "task_artifact_owner_transcript_deletion_unverified",
7561
+ };
7562
+ const artifactsEvidence = cleanupArtifactsRemovedEvidence(
7563
+ scope,
7564
+ tombstone,
7565
+ target,
7566
+ pending.artifactsRemovedAttempt ?? pending.attempt,
7567
+ );
7568
+ if (
7569
+ !artifactsEvidence ||
7570
+ !cleanupRootsAbsent(tombstone, target, pending, artifactsEvidence.retainedArtifactsRootPath)
7571
+ )
7572
+ return {
7573
+ kind: "cleanup_pending",
7574
+ tombstonePath: tombstone,
7575
+ phase: "artifacts",
7576
+ message: "task_artifact_owner_retirement_pending",
7577
+ };
7578
+ const ownerProgress = await retireManagedGcOwnerAfterArtifacts(scope, target, targets, lock);
7579
+ if (ownerProgress.state === "pending" || ownerProgress.state === "payload_retired")
7580
+ return {
7581
+ kind: "cleanup_pending",
7582
+ tombstonePath: tombstone,
7583
+ phase: "artifacts",
7584
+ message: ownerProgress.message ?? "task_artifact_owner_retirement_pending",
7585
+ };
7586
+ fsyncManagedParent(target.path);
7587
+ await publishCleanupCompleted(scope, tombstone, target, lock);
7588
+ continue;
7589
+ }
4715
7590
  if (!observedPending) continue;
4716
7591
  const artifactsEvidence = cleanupArtifactsRemovedEvidence(
4717
7592
  scope,
@@ -4721,15 +7596,31 @@ async function deleteManagedSessionCandidateInternal(
4721
7596
  );
4722
7597
  if (
4723
7598
  artifactsEvidence &&
4724
- cleanupRootsAbsent(tombstone, target, observedPending, artifactsEvidence.retainedArtifactsRootPath)
7599
+ cleanupRootsAbsent(tombstone, target, observedPending, artifactsEvidence.retainedArtifactsRootPath) &&
7600
+ (!target.taskArtifactOwnerDeletionEvidence || pending?.taskArtifactOwnerTranscriptDeleted === true)
4725
7601
  ) {
7602
+ const ownerProgress = await retireManagedGcOwnerAfterArtifacts(scope, target, targets, lock);
7603
+ if (ownerProgress.state === "pending" || ownerProgress.state === "payload_retired")
7604
+ return {
7605
+ kind: "cleanup_pending",
7606
+ tombstonePath: tombstone,
7607
+ phase: "artifacts",
7608
+ message: ownerProgress.message ?? "task_artifact_owner_retirement_pending",
7609
+ };
4726
7610
  fsyncManagedParent(target.path);
4727
7611
  await publishCleanupCompleted(scope, tombstone, target, lock);
4728
7612
  continue;
4729
7613
  }
7614
+ if (target.taskArtifactOwnerDeletionEvidence && !observedPending.detachedTranscriptPath)
7615
+ return {
7616
+ kind: "cleanup_pending",
7617
+ tombstonePath: tombstone,
7618
+ phase: "artifacts",
7619
+ message: "task_artifact_owner_transcript_deletion_unverified",
7620
+ };
4730
7621
  } else throw error;
4731
7622
  }
4732
- if (cleanupCompleted(scope, tombstone, target)) continue;
7623
+ if (await cleanupCompleted(scope, tombstone, target)) continue;
4733
7624
  deletedAny = true;
4734
7625
  const verified = observedPending ? target : validateCandidateForScope(scope, target);
4735
7626
  if (!verified || !sameCandidate(verified, target)) throw new Error("source_changed");
@@ -4745,64 +7636,118 @@ async function deleteManagedSessionCandidateInternal(
4745
7636
  await publishCleanupPending(scope, tombstone, active, lock);
4746
7637
  const initialTarget = validateCandidateForScope(scope, target);
4747
7638
  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(
7639
+ let ownerProgress = await managedGcOwnerProgressBeforeDelete(
7640
+ scope,
7641
+ target,
7642
+ targets,
7643
+ cleanupArtifactsRemoved(
4774
7644
  scope,
4775
7645
  tombstone,
4776
7646
  target,
4777
7647
  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, {
7648
+ ),
7649
+ lock,
7650
+ );
7651
+ if (ownerProgress.state === "pending")
7652
+ return {
7653
+ kind: "cleanup_pending",
7654
+ tombstonePath: tombstone,
7655
+ phase: "artifacts",
7656
+ message: ownerProgress.message ?? "task_artifact_owner_retirement_pending",
7657
+ };
7658
+ let deletion = await deleteSessionVerifiedWithFence(
7659
+ "direct",
7660
+ "initial",
7661
+ lock,
7662
+ {
4788
7663
  sessionsRoot: scope.sessionsRoot,
4789
7664
  transcriptPath: target.path,
4790
7665
  sessionId: target.sessionId,
4791
7666
  cwd: target.cwd,
4792
- transcriptIdentity: refreshedTarget.identity,
4793
- plannedArtifactsPath: active.plannedArtifactsPath,
4794
- plannedTranscriptPath: active.plannedTranscriptPath,
4795
- detachedTranscriptPath: active.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
7667
+ transcriptIdentity: initialTarget.identity,
7668
+ transcriptParentIdentity: (() => {
7669
+ const parent = fs.lstatSync(path.dirname(target.path), { bigint: true });
7670
+ return { dev: parent.dev, ino: parent.ino };
7671
+ })(),
7672
+ expectedArtifactsIdentity: active.expectedArtifactsIdentity,
7673
+ expectedArtifactsTree: active.expectedArtifactsTree,
7674
+ detachedArtifactsPath: active.detachedArtifactsPath ?? observedPending?.detachedArtifactsPath,
7675
+ detachedTranscriptPath:
7676
+ active.detachedTranscriptPath ??
7677
+ observedPending?.detachedTranscriptPath ??
7678
+ (pending && fs.existsSync(pending.plannedTranscriptPath) ? pending.plannedTranscriptPath : undefined),
4796
7679
  retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
4797
7680
  retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
4798
7681
  retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
4799
7682
  retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
4800
7683
  retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
4801
7684
  retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
4802
- artifactsRemoved: true,
4803
- });
7685
+ plannedArtifactsPath: active.plannedArtifactsPath,
7686
+ plannedTranscriptPath: active.plannedTranscriptPath,
7687
+ ...(cleanupArtifactsRemoved(
7688
+ scope,
7689
+ tombstone,
7690
+ target,
7691
+ pending?.artifactsRemovedAttempt ??
7692
+ active.artifactsRemovedAttempt ??
7693
+ pending?.attempt ??
7694
+ active.attempt,
7695
+ )
7696
+ ? { artifactsRemoved: true as const }
7697
+ : {}),
7698
+ },
7699
+ scope,
7700
+ target,
7701
+ );
7702
+ if (deletion.kind === "artifacts_removed") {
7703
+ await publishManagedGcArtifactsRemoved(scope, target, lock);
7704
+ await publishCleanupArtifactsRemoved(scope, tombstone, active, lock);
7705
+ ownerProgress = await retireManagedGcOwnerAfterArtifacts(scope, target, targets, lock);
7706
+ if (ownerProgress.state === "pending")
7707
+ return {
7708
+ kind: "cleanup_pending",
7709
+ tombstonePath: tombstone,
7710
+ phase: "artifacts",
7711
+ message: ownerProgress.message ?? "task_artifact_owner_retirement_pending",
7712
+ };
7713
+ active = { ...active, artifactsRemovedAttempt: active.attempt };
7714
+ const refreshedTarget = validateCandidateForScope(scope, target);
7715
+ if (!refreshedTarget) throw new Error("source_changed");
7716
+ deletion = await deleteSessionVerifiedWithFence(
7717
+ "direct",
7718
+ "transcript-after-artifacts-removed",
7719
+ lock,
7720
+ {
7721
+ sessionsRoot: scope.sessionsRoot,
7722
+ transcriptPath: target.path,
7723
+ sessionId: target.sessionId,
7724
+ cwd: target.cwd,
7725
+ transcriptIdentity: refreshedTarget.identity,
7726
+ plannedArtifactsPath: active.plannedArtifactsPath,
7727
+ plannedTranscriptPath: active.plannedTranscriptPath,
7728
+ detachedTranscriptPath: active.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
7729
+ retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
7730
+ retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
7731
+ retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
7732
+ retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
7733
+ retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
7734
+ retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
7735
+ artifactsRemoved: true,
7736
+ },
7737
+ scope,
7738
+ target,
7739
+ );
4804
7740
  }
4805
7741
  if (deletion.kind === "cleanup_pending") {
7742
+ if (deletion.phase === "task_artifact_owner") {
7743
+ ownerProgress = await persistManagedGcStorageOwnerDisposition(scope, target, deletion, lock);
7744
+ return {
7745
+ kind: "cleanup_pending",
7746
+ tombstonePath: tombstone,
7747
+ phase: "artifacts",
7748
+ message: ownerProgress.message ?? deletion.error.message,
7749
+ };
7750
+ }
4806
7751
  assertAuthorizedCleanupPending(target, active, deletion);
4807
7752
  const retry = nextCleanupReceipt(target, active);
4808
7753
  let pendingEvidence = cleanupPendingEvidence(retry, active, deletion);
@@ -4836,7 +7781,16 @@ async function deleteManagedSessionCandidateInternal(
4836
7781
  phase: deletion.phase,
4837
7782
  message: "Exact cleanup remains pending because descriptor-bound final deletion is unavailable.",
4838
7783
  };
7784
+ await publishManagedGcArtifactsRemoved(scope, target, lock);
4839
7785
  await publishCleanupArtifactsRemoved(scope, tombstone, pendingEvidence, lock);
7786
+ ownerProgress = await retireManagedGcOwnerAfterArtifacts(scope, target, targets, lock);
7787
+ if (ownerProgress.state === "pending")
7788
+ return {
7789
+ kind: "cleanup_pending",
7790
+ tombstonePath: tombstone,
7791
+ phase: "artifacts",
7792
+ message: ownerProgress.message ?? "task_artifact_owner_retirement_pending",
7793
+ };
4840
7794
  pendingEvidence = { ...pendingEvidence, artifactsRemovedAttempt: pendingEvidence.attempt };
4841
7795
  const retainedProof = cleanupArtifactsRemovedReceipt(
4842
7796
  tombstone,
@@ -4872,6 +7826,8 @@ async function deleteManagedSessionCandidateInternal(
4872
7826
  retainedTranscriptUnknownPath: pendingEvidence.retainedTranscriptUnknownPath,
4873
7827
  artifactsRemoved: true,
4874
7828
  },
7829
+ scope,
7830
+ target,
4875
7831
  () => {
4876
7832
  if (
4877
7833
  !cleanupArtifactsRemoved(
@@ -4884,6 +7840,15 @@ async function deleteManagedSessionCandidateInternal(
4884
7840
  throw new Error("durability_failed");
4885
7841
  },
4886
7842
  );
7843
+ if (deletion.kind === "cleanup_pending" && deletion.phase === "task_artifact_owner") {
7844
+ ownerProgress = await persistManagedGcStorageOwnerDisposition(scope, target, deletion, lock);
7845
+ return {
7846
+ kind: "cleanup_pending",
7847
+ tombstonePath: tombstone,
7848
+ phase: "artifacts",
7849
+ message: ownerProgress.message ?? deletion.error.message,
7850
+ };
7851
+ }
4887
7852
  if (deletion.kind === "cleanup_pending") {
4888
7853
  if (
4889
7854
  deletion.phase !== "transcript" ||
@@ -4914,12 +7879,42 @@ async function deleteManagedSessionCandidateInternal(
4914
7879
  return {
4915
7880
  kind: "cleanup_pending",
4916
7881
  tombstonePath: tombstone,
4917
- phase: deletion.phase,
4918
- message: deletion.error.message,
7882
+ phase: ownerProgress.state === "payload_retired" ? "artifacts" : deletion.phase,
7883
+ message:
7884
+ ownerProgress.state === "payload_retired"
7885
+ ? "task_artifact_owner_namespace_cleanup_pending"
7886
+ : deletion.error.message,
4919
7887
  };
4920
7888
  // A retained transcript quarantine proves canonical absence and remains
4921
7889
  // identity-bound in the durable pending receipt.
4922
7890
  }
7891
+ if (deletion.kind === "deleted")
7892
+ await persistManagedOwnerTranscriptDeletion(scope, tombstone, target, active, lock, deletion);
7893
+ const ownerDisposition = await taskArtifactOwnerTranscriptResult(scope, target, lock);
7894
+ if (ownerDisposition === "payload_retired")
7895
+ return {
7896
+ kind: "cleanup_pending",
7897
+ tombstonePath: tombstone,
7898
+ phase: "artifacts",
7899
+ message: "task_artifact_owner_namespace_cleanup_pending",
7900
+ };
7901
+ if (target.taskArtifactOwnerDeletionEvidence && ownerDisposition !== "retired")
7902
+ return {
7903
+ kind: "cleanup_pending",
7904
+ tombstonePath: tombstone,
7905
+ phase: "artifacts",
7906
+ message: "task_artifact_owner_retirement_pending",
7907
+ };
7908
+ if (
7909
+ target.taskArtifactOwnerDeletionEvidence &&
7910
+ !pendingCleanupReceipt(scope, tombstone, target)?.taskArtifactOwnerTranscriptDeleted
7911
+ )
7912
+ return {
7913
+ kind: "cleanup_pending",
7914
+ tombstonePath: tombstone,
7915
+ phase: "transcript",
7916
+ message: "task_artifact_owner_transcript_deletion_unverified",
7917
+ };
4923
7918
  fsyncManagedParent(target.path);
4924
7919
  await publishCleanupCompleted(scope, tombstone, target, lock);
4925
7920
  }