@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
@@ -15,6 +15,7 @@ import {
15
15
  publishManagedFileNoReplace,
16
16
  publishManagedFileNoReplaceSync,
17
17
  } from "./internal/managed-session-storage";
18
+ import type { SessionStorageStat } from "./session-storage";
18
19
  import { DEFAULT_ARTIFACT_MAX_BYTES, truncateHeadBytes } from "./streaming-output";
19
20
 
20
21
  export interface ManagedOutputGeneration {
@@ -67,6 +68,28 @@ function parseManagedOutputGeneration(value: Uint8Array, outputFilenamePrefix: s
67
68
  function sameGeneration(left: ManagedOutputGeneration, right: ManagedOutputGeneration): boolean {
68
69
  return left.outputFilename === right.outputFilename && left.metadataFilename === right.metadataFilename;
69
70
  }
71
+
72
+ function normalizeByteRange(size: number, range: ArtifactByteRange): { start: number; endExclusive: number } {
73
+ const start = Math.max(0, Math.min(size, range.start ?? 0));
74
+ const endExclusive = Math.max(start, Math.min(size, range.endExclusive ?? size));
75
+ const normalizedStart = Number.isNaN(start) ? 0 : Math.trunc(start);
76
+ const normalizedEnd = Number.isNaN(endExclusive) ? 0 : Math.trunc(endExclusive);
77
+ return {
78
+ start: normalizedStart,
79
+ endExclusive: Math.max(normalizedStart, normalizedEnd),
80
+ };
81
+ }
82
+
83
+ const ARTIFACT_STREAM_CHUNK_BYTES = 64 * 1024;
84
+ const ARTIFACT_RANGE_READ_CHUNK_BYTES = 64 * 1024 * 1024;
85
+
86
+ type AttemptStagingCleanupClassification = { kind: "authorized_quarantine" } | { kind: "failure"; error: unknown };
87
+
88
+ function classifyAttemptStagingCleanupFailure(error: unknown): AttemptStagingCleanupClassification {
89
+ return error instanceof Error && error.message === "cleanup_pending"
90
+ ? { kind: "authorized_quarantine" }
91
+ : { kind: "failure", error };
92
+ }
70
93
  export interface ArtifactSaveOptions {
71
94
  maxBytes?: number;
72
95
  }
@@ -309,26 +332,16 @@ export class ArtifactManager {
309
332
  * Best-effort removal of a previously published named artifact. Used to roll
310
333
  * back staged publications when a transactional operation (e.g. gated
311
334
  * maintenance pruning) is rejected after publication succeeded. Returns false
312
- * when the artifact could not be removed so callers can log the failure
313
- * instead of silently treating the rollback as complete.
335
+ * when the exact named artifact is absent or could not be removed; success does
336
+ * not claim that unrelated native cleanup residue was reclaimed.
314
337
  */
315
338
  async removeNamedBestEffort(filename: string): Promise<boolean> {
316
339
  if (!/^[a-zA-Z0-9_.-]+$/.test(filename)) return false;
317
340
  try {
318
341
  if (this.#store) {
319
- const beforePaths = new Set(await fs.readdir(this.#store.dir));
320
342
  const staged = this.#store.readExpected(filename);
321
- if (staged) this.#store.removeExpected(filename, staged);
322
- // Capture only root names outside retained authority so foreign sibling
323
- // placeholders cannot make rollback fail before exact removal runs.
324
- for (const basename of await fs.readdir(this.#store.dir)) {
325
- const nativeResidue = /^\.gjc-/u.test(basename);
326
- const ownQuarantine = basename === `${filename}.removing` || basename.startsWith(`${filename}.`);
327
- if (beforePaths.has(basename) || (!nativeResidue && !ownQuarantine)) continue;
328
- const residuePath = path.join(this.#store.dir, basename);
329
- const stat = await fs.lstat(residuePath);
330
- await fs.rm(residuePath, { recursive: stat.isDirectory(), force: true });
331
- }
343
+ if (!staged) return false;
344
+ this.#store.removeExpected(filename, staged);
332
345
  } else {
333
346
  await fs.unlink(path.join(this.#dir, filename));
334
347
  }
@@ -502,7 +515,13 @@ export class ArtifactManager {
502
515
  publishedNames.push(mappedFilename);
503
516
  }
504
517
  this.#reservations.set(attemptId, { start, count: ids.length, names: [...publishedNames] });
505
- await staging.discardAttemptStaging();
518
+ try {
519
+ await staging.discardAttemptStaging();
520
+ } catch (error) {
521
+ const cleanup = classifyAttemptStagingCleanupFailure(error);
522
+ if (cleanup.kind === "authorized_quarantine") return frozenMap;
523
+ throw cleanup.error;
524
+ }
506
525
  return frozenMap;
507
526
  } catch (error) {
508
527
  // Cleanup is best-effort, but a durable removal failure must never be silent: it leaves a
@@ -550,64 +569,34 @@ export class ArtifactManager {
550
569
 
551
570
  async discardAttemptStaging(): Promise<void> {
552
571
  if (this.#store) {
553
- const cleanupStore = this.#stagingParentStore ?? this.#store;
554
- const cleanupPath = this.#stagingParentStore ? this.#stagingRelativePath! : "";
555
- const parentCleanupPath = cleanupPath ? path.posix.dirname(cleanupPath) : "";
556
- let parentBefore: ReturnType<ManagedSessionDescendantStore["captureTree"]> | undefined;
557
- if (this.#stagingParentStore) {
558
- try {
559
- parentBefore = cleanupStore.captureTree(parentCleanupPath);
560
- } catch {
561
- parentBefore = undefined;
562
- }
563
- }
572
+ // Only an attempt manager owns a managed subtree that it may retire. A
573
+ // normal manager's store is borrowed by this cleanup method and its root
574
+ // must never become a deletion target.
575
+ if (!this.#stagingParentStore || !this.#stagingRelativePath) return;
576
+ const cleanupStore = this.#stagingParentStore;
564
577
  try {
565
- const snapshot = cleanupStore.captureTree(cleanupPath);
566
- cleanupStore.removeTreeExpected(cleanupPath, snapshot);
578
+ const snapshot = cleanupStore.captureTree(this.#stagingRelativePath);
579
+ const issuedRoot = this.#store.subtreeRootAuthority;
580
+ if (
581
+ issuedRoot.canonicalPath !== path.resolve(this.#dir) ||
582
+ snapshot.rootDev !== issuedRoot.dev.toString() ||
583
+ snapshot.rootIno !== issuedRoot.ino.toString()
584
+ )
585
+ throw new Error("artifact_staging_root_changed");
586
+ // Stop using and release this attempt's retained writable root before
587
+ // asking the parent authority to retire the tree. The exact tree snapshot
588
+ // above is still checked by native removal; a concurrent change therefore
589
+ // remains a refusal rather than being adopted. Never close the borrowed
590
+ // parent store.
591
+ this.#store.close();
592
+ cleanupStore.removeTreeExpected(this.#stagingRelativePath, snapshot);
567
593
  } catch (error) {
568
- if (!(error instanceof Error && (error.message === "not_found" || error.message === "cleanup_pending")))
569
- throw error;
594
+ if (!(error instanceof Error && error.message === "not_found")) throw error;
595
+ } finally {
596
+ // Replacement, snapshot, and native cleanup failures also release only
597
+ // the capability owned by this attempt manager.
598
+ this.#store.close();
570
599
  }
571
- if (this.#stagingParentStore && parentBefore) {
572
- try {
573
- const after = cleanupStore.captureTree(parentCleanupPath);
574
- const beforePaths = new Set(parentBefore.entries.map(entry => entry.relativePath));
575
- for (const entry of after.entries) {
576
- if (
577
- entry.kind === "directory" &&
578
- entry.relativePath.length > 0 &&
579
- !beforePaths.has(entry.relativePath) &&
580
- /\.removing$/u.test(path.posix.basename(entry.relativePath))
581
- ) {
582
- await fs.rm(path.join(cleanupStore.dir, parentCleanupPath, entry.relativePath), {
583
- recursive: true,
584
- force: true,
585
- });
586
- continue;
587
- }
588
- if (
589
- entry.kind !== "file" ||
590
- beforePaths.has(entry.relativePath) ||
591
- !/^\\.gjc-(?:exact-unlink-placeholder|remove)-/u.test(path.posix.basename(entry.relativePath))
592
- )
593
- continue;
594
- const relative = path.posix.join(parentCleanupPath, entry.relativePath);
595
- const expected = cleanupStore.readExpected(relative);
596
- if (expected) {
597
- try {
598
- cleanupStore.removeExpected(relative, expected);
599
- } catch (cleanupError) {
600
- if (!(cleanupError instanceof Error && cleanupError.message === "cleanup_pending"))
601
- throw cleanupError;
602
- }
603
- }
604
- await fs.rm(path.join(cleanupStore.dir, relative), { force: true }).catch(() => undefined);
605
- }
606
- } catch {
607
- // Retained cleanup evidence is safe to leave for a later maintenance pass.
608
- }
609
- }
610
- this.#store.close();
611
600
  } else {
612
601
  await fs.rm(this.#dir, { recursive: true, force: true });
613
602
  }
@@ -650,7 +639,69 @@ export class ArtifactManager {
650
639
  }
651
640
  }
652
641
 
642
+ #captureManagedArtifact(id: string): {
643
+ filename: string;
644
+ size: number;
645
+ expectedDescriptor: Pick<SessionStorageStat, "dev" | "ino" | "nlink" | "size" | "mtimeNs" | "ctimeNs">;
646
+ } | null {
647
+ if (!this.#store) return null;
648
+ try {
649
+ const entry = this.#store
650
+ .captureTree("")
651
+ .entries.find(candidate => candidate.kind === "file" && candidate.relativePath.startsWith(`${id}.`));
652
+ if (!entry) return null;
653
+ const size = Number(BigInt(entry.size));
654
+ if (!Number.isSafeInteger(size) || size < 0) return null;
655
+ return {
656
+ filename: entry.relativePath,
657
+ size,
658
+ expectedDescriptor: {
659
+ dev: BigInt(entry.dev),
660
+ ino: BigInt(entry.ino),
661
+ nlink: BigInt(entry.nlink),
662
+ size,
663
+ mtimeNs: BigInt(entry.mtimeNs),
664
+ ctimeNs: BigInt(entry.ctimeNs),
665
+ },
666
+ };
667
+ } catch {
668
+ // listFiles historically hides an unavailable managed tree as no artifacts.
669
+ return null;
670
+ }
671
+ }
672
+
653
673
  async readRange(id: string, range: ArtifactByteRange = {}): Promise<string> {
674
+ if (this.#store) {
675
+ const artifact = this.#captureManagedArtifact(id);
676
+ if (!artifact) throw new Error(`artifact://${id} not found`);
677
+ const { start, endExclusive } = normalizeByteRange(artifact.size, range);
678
+ const length = Math.max(0, endExclusive - start);
679
+ let bytes: Uint8Array;
680
+ if (length <= ARTIFACT_RANGE_READ_CHUNK_BYTES) {
681
+ bytes = this.#store.readRangeExpectedSync(
682
+ artifact.filename,
683
+ start,
684
+ length,
685
+ artifact.expectedDescriptor,
686
+ ).bytes;
687
+ } else {
688
+ const complete = Buffer.allocUnsafe(length);
689
+ let offset = 0;
690
+ while (offset < length) {
691
+ const chunkLength = Math.min(ARTIFACT_RANGE_READ_CHUNK_BYTES, length - offset);
692
+ const chunk = this.#store.readRangeExpectedSync(
693
+ artifact.filename,
694
+ start + offset,
695
+ chunkLength,
696
+ artifact.expectedDescriptor,
697
+ ).bytes;
698
+ complete.set(chunk, offset);
699
+ offset += chunk.byteLength;
700
+ }
701
+ bytes = complete;
702
+ }
703
+ return new TextDecoder().decode(bytes);
704
+ }
654
705
  const artifactPath = await this.getPath(id);
655
706
  if (!artifactPath) throw new Error(`artifact://${id} not found`);
656
707
  const file = Bun.file(artifactPath);
@@ -661,6 +712,59 @@ export class ArtifactManager {
661
712
  }
662
713
 
663
714
  async openReadStream(id: string, range: ArtifactByteRange = {}): Promise<ReadableStream<Uint8Array>> {
715
+ if (this.#store) {
716
+ const artifact = this.#captureManagedArtifact(id);
717
+ if (!artifact) throw new Error(`artifact://${id} not found`);
718
+ const { start, endExclusive } = normalizeByteRange(artifact.size, range);
719
+ const lease = this.#store.openReadLease(artifact.filename, artifact.expectedDescriptor);
720
+ let offset = start;
721
+ let closed = false;
722
+ const closeLease = (): void => {
723
+ if (closed) return;
724
+ closed = true;
725
+ lease.close();
726
+ };
727
+ const closeAfterFailure = (): void => {
728
+ try {
729
+ closeLease();
730
+ } catch {
731
+ // Keep the read failure as the stream's observable error.
732
+ }
733
+ };
734
+ return new ReadableStream<Uint8Array>(
735
+ {
736
+ start: controller => {
737
+ if (offset !== endExclusive) return;
738
+ try {
739
+ closeLease();
740
+ controller.close();
741
+ } catch (error) {
742
+ closeAfterFailure();
743
+ controller.error(error);
744
+ }
745
+ },
746
+ pull: controller => {
747
+ if (closed) return;
748
+ try {
749
+ const length = Math.min(ARTIFACT_STREAM_CHUNK_BYTES, endExclusive - offset);
750
+ const bytes = lease.readRange(offset, length);
751
+ if (bytes.byteLength !== length) throw new Error("range_not_present");
752
+ offset += bytes.byteLength;
753
+ if (offset === endExclusive) closeLease();
754
+ controller.enqueue(bytes);
755
+ if (offset === endExclusive) controller.close();
756
+ } catch (error) {
757
+ closeAfterFailure();
758
+ controller.error(error);
759
+ }
760
+ },
761
+ cancel: () => {
762
+ closeLease();
763
+ },
764
+ },
765
+ { highWaterMark: 0 },
766
+ );
767
+ }
664
768
  const artifactPath = await this.getPath(id);
665
769
  if (!artifactPath) throw new Error(`artifact://${id} not found`);
666
770
  const file = Bun.file(artifactPath);
@@ -0,0 +1,319 @@
1
+ import * as path from "node:path";
2
+ import type { SessionStorageFileIdentity } from "../session-storage";
3
+ import {
4
+ assertSessionRoot,
5
+ EMPTY_PAYLOAD_SHA256,
6
+ parseTaskArtifactOwnerDeletionEvidence,
7
+ parseTaskArtifactOwnerLocator,
8
+ parseTaskArtifactOwnerRetirementContinuation,
9
+ parseTaskArtifactOwnerRetirementOutcome,
10
+ type TaskArtifactOwnerDeletionEvidence,
11
+ type TaskArtifactOwnerLocator,
12
+ type TaskArtifactOwnerRetirementContinuation,
13
+ type TaskArtifactOwnerRetirementOutcome,
14
+ type TaskArtifactOwnerStorageContext,
15
+ } from "../task-artifact-owner-codec";
16
+
17
+ export type ManagedGcSessionRetirementState = "prepared" | "artifacts_removed" | "owner_pending" | "owner_retired";
18
+
19
+ export interface ManagedGcSessionRetirementTarget {
20
+ readonly transcriptPath: string;
21
+ readonly sessionId: string;
22
+ readonly cwd: string;
23
+ readonly transcriptIdentity: SessionStorageFileIdentity;
24
+ readonly taskArtifactOwnerLocator: TaskArtifactOwnerLocator;
25
+ }
26
+
27
+ export interface ManagedGcSessionRetirementReceipt extends ManagedGcSessionRetirementTarget {
28
+ readonly state: ManagedGcSessionRetirementState;
29
+ readonly taskArtifactOwnerDeletionEvidence: TaskArtifactOwnerDeletionEvidence;
30
+ readonly artifactsRemoved?: true;
31
+ readonly ownerRetirementAttempt?: number;
32
+ readonly taskArtifactOwnerRetirementOutcome?: TaskArtifactOwnerRetirementOutcome;
33
+ readonly taskArtifactOwnerRetirementContinuation?: TaskArtifactOwnerRetirementContinuation;
34
+ readonly taskArtifactOwnerPayloadRetired?: true;
35
+ readonly taskArtifactOwnerNamespaceRetained?: true;
36
+ readonly taskArtifactOwnerRetired?: true;
37
+ }
38
+
39
+ function isRecord(value: unknown): value is Record<string, unknown> {
40
+ return typeof value === "object" && value !== null && !Array.isArray(value);
41
+ }
42
+
43
+ export function managedGcRetirementIdentityRecord(identity: SessionStorageFileIdentity): Record<string, unknown> {
44
+ return {
45
+ dev: identity.dev.toString(),
46
+ ino: identity.ino.toString(),
47
+ nlink: identity.nlink?.toString(),
48
+ size: identity.size,
49
+ mtimeNs: identity.mtimeNs.toString(),
50
+ sha256: identity.sha256,
51
+ };
52
+ }
53
+
54
+ export function sameIdentity(left: SessionStorageFileIdentity, right: SessionStorageFileIdentity): boolean {
55
+ return (
56
+ left.dev === right.dev &&
57
+ left.ino === right.ino &&
58
+ left.nlink === right.nlink &&
59
+ left.size === right.size &&
60
+ left.mtimeNs === right.mtimeNs &&
61
+ left.sha256 === right.sha256
62
+ );
63
+ }
64
+
65
+ export function managedGcRetirementIdentityValue(value: unknown): SessionStorageFileIdentity {
66
+ if (!isRecord(value)) throw new Error("task_artifact_owner_continuation_corrupt");
67
+ if (
68
+ Object.keys(value).length !== 6 ||
69
+ !["dev", "ino", "nlink", "size", "mtimeNs", "sha256"].every(key => key in value) ||
70
+ typeof value.dev !== "string" ||
71
+ !/^(0|[1-9][0-9]*)$/u.test(value.dev) ||
72
+ typeof value.ino !== "string" ||
73
+ !/^(0|[1-9][0-9]*)$/u.test(value.ino) ||
74
+ typeof value.nlink !== "string" ||
75
+ !/^(0|[1-9][0-9]*)$/u.test(value.nlink) ||
76
+ value.nlink !== "1" ||
77
+ typeof value.size !== "number" ||
78
+ !Number.isSafeInteger(value.size) ||
79
+ value.size < 0 ||
80
+ typeof value.mtimeNs !== "string" ||
81
+ !/^(0|-?[1-9][0-9]*)$/u.test(value.mtimeNs) ||
82
+ typeof value.sha256 !== "string" ||
83
+ !/^[0-9a-f]{64}$/u.test(value.sha256)
84
+ )
85
+ throw new Error("task_artifact_owner_continuation_corrupt");
86
+ return {
87
+ dev: BigInt(value.dev),
88
+ ino: BigInt(value.ino),
89
+ nlink: BigInt(value.nlink),
90
+ size: value.size,
91
+ mtimeNs: BigInt(value.mtimeNs),
92
+ sha256: value.sha256,
93
+ };
94
+ }
95
+
96
+ function validateTarget(
97
+ ownerContext: TaskArtifactOwnerStorageContext,
98
+ target: ManagedGcSessionRetirementTarget,
99
+ ): { locator: TaskArtifactOwnerLocator; identity: SessionStorageFileIdentity } {
100
+ if (
101
+ !target ||
102
+ typeof target.transcriptPath !== "string" ||
103
+ typeof target.sessionId !== "string" ||
104
+ typeof target.cwd !== "string"
105
+ )
106
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
107
+ assertSessionRoot(ownerContext);
108
+ const relativeTranscript = path.relative(ownerContext.sessionsRoot, target.transcriptPath);
109
+ if (
110
+ target.sessionId.length === 0 ||
111
+ !path.isAbsolute(target.transcriptPath) ||
112
+ path.resolve(target.transcriptPath) !== target.transcriptPath ||
113
+ relativeTranscript === "" ||
114
+ path.isAbsolute(relativeTranscript) ||
115
+ relativeTranscript === ".." ||
116
+ relativeTranscript.startsWith(`..${path.sep}`)
117
+ )
118
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
119
+ let locator: TaskArtifactOwnerLocator | undefined;
120
+ try {
121
+ locator = parseTaskArtifactOwnerLocator(target.taskArtifactOwnerLocator);
122
+ } catch {
123
+ throw new Error("task_artifact_owner_continuation_authority_mismatch");
124
+ }
125
+ if (!locator) throw new Error("task_artifact_owner_continuation_authority_mismatch");
126
+ let identity: SessionStorageFileIdentity;
127
+ try {
128
+ identity = managedGcRetirementIdentityValue(managedGcRetirementIdentityRecord(target.transcriptIdentity));
129
+ } catch {
130
+ throw new Error("task_artifact_owner_continuation_identity_invalid");
131
+ }
132
+ if (
133
+ identity.nlink !== 1n ||
134
+ identity.size < 0 ||
135
+ !Number.isSafeInteger(identity.size) ||
136
+ !/^[0-9a-f]{64}$/u.test(identity.sha256)
137
+ )
138
+ throw new Error("task_artifact_owner_continuation_identity_invalid");
139
+ return { locator, identity };
140
+ }
141
+
142
+ /**
143
+ * Strictly decode one persisted managed-GC receipt. A decoded `owner_retired` outcome is persisted DTO data only;
144
+ * this pure parser does not verify current filesystem state, payload destruction, or writer quiescence. A future
145
+ * journal reader must perform the live physical-retirement proof for the relevant state before treating it as done.
146
+ */
147
+ export function parseManagedGcRetirementReceipt(
148
+ value: unknown,
149
+ scopeDigest: string,
150
+ ownerContext: TaskArtifactOwnerStorageContext,
151
+ target: ManagedGcSessionRetirementTarget,
152
+ ): ManagedGcSessionRetirementReceipt {
153
+ if (!isRecord(value)) throw new Error("task_artifact_owner_continuation_corrupt");
154
+ const { locator: targetLocator, identity: targetIdentity } = validateTarget(ownerContext, target);
155
+ const record = value;
156
+ const state = record.state;
157
+ const commonKeys = [
158
+ "schemaVersion",
159
+ "state",
160
+ "scope",
161
+ "transcriptPath",
162
+ "sessionId",
163
+ "cwd",
164
+ "transcriptIdentity",
165
+ "taskArtifactOwnerDeletionEvidence",
166
+ ];
167
+ const outcomeValue = record.taskArtifactOwnerRetirementOutcome;
168
+ const outcomeKind = isRecord(outcomeValue) ? outcomeValue.kind : undefined;
169
+ const expectedKeys =
170
+ state === "prepared"
171
+ ? commonKeys
172
+ : state === "artifacts_removed"
173
+ ? [...commonKeys, "artifactsRemoved"]
174
+ : state === "owner_pending"
175
+ ? [
176
+ ...commonKeys,
177
+ "ownerRetirementAttempt",
178
+ "artifactsRemoved",
179
+ "taskArtifactOwnerRetirementOutcome",
180
+ "taskArtifactOwnerRetirementContinuation",
181
+ ...(outcomeKind === "payload_retired"
182
+ ? ["taskArtifactOwnerPayloadRetired", "taskArtifactOwnerNamespaceRetained"]
183
+ : []),
184
+ ]
185
+ : state === "owner_retired"
186
+ ? [
187
+ ...commonKeys,
188
+ "artifactsRemoved",
189
+ "taskArtifactOwnerRetired",
190
+ "taskArtifactOwnerRetirementOutcome",
191
+ ]
192
+ : undefined;
193
+ if (
194
+ !expectedKeys ||
195
+ Object.keys(record).length !== expectedKeys.length ||
196
+ !expectedKeys.every(key => key in record) ||
197
+ record.schemaVersion !== 1 ||
198
+ record.scope !== scopeDigest ||
199
+ record.transcriptPath !== target.transcriptPath ||
200
+ record.sessionId !== target.sessionId ||
201
+ record.cwd !== target.cwd ||
202
+ (state !== "prepared" && record.artifactsRemoved !== true) ||
203
+ (state === "owner_retired" && record.taskArtifactOwnerRetired !== true) ||
204
+ (state === "owner_pending" &&
205
+ (!Number.isSafeInteger(record.ownerRetirementAttempt) || (record.ownerRetirementAttempt as number) < 1))
206
+ )
207
+ throw new Error("task_artifact_owner_continuation_mismatch");
208
+ const identity = managedGcRetirementIdentityValue(record.transcriptIdentity);
209
+ if (!sameIdentity(identity, targetIdentity)) throw new Error("task_artifact_owner_continuation_transcript_replaced");
210
+ let evidence: TaskArtifactOwnerDeletionEvidence;
211
+ try {
212
+ evidence = parseTaskArtifactOwnerDeletionEvidence(record.taskArtifactOwnerDeletionEvidence);
213
+ } catch {
214
+ throw new Error("task_artifact_owner_continuation_evidence_corrupt");
215
+ }
216
+ if (evidence.sessionId !== target.sessionId || JSON.stringify(evidence.locator) !== JSON.stringify(targetLocator))
217
+ throw new Error("task_artifact_owner_continuation_locator_mismatch");
218
+ let outcome: TaskArtifactOwnerRetirementOutcome | undefined;
219
+ if (state === "owner_pending" || state === "owner_retired") {
220
+ outcome = parseTaskArtifactOwnerRetirementOutcome(ownerContext, evidence, outcomeValue);
221
+ if (
222
+ (state === "owner_pending" && outcome.kind === "completed") ||
223
+ (state === "owner_retired" && outcome.kind !== "completed")
224
+ )
225
+ throw new Error("task_artifact_owner_continuation_outcome_mismatch");
226
+ if (outcome.kind !== "completed") {
227
+ let receiptContinuation: TaskArtifactOwnerRetirementContinuation;
228
+ try {
229
+ receiptContinuation = parseTaskArtifactOwnerRetirementContinuation(
230
+ ownerContext,
231
+ evidence,
232
+ record.taskArtifactOwnerRetirementContinuation,
233
+ );
234
+ } catch {
235
+ throw new Error("task_artifact_owner_continuation_outcome_mismatch");
236
+ }
237
+ if (JSON.stringify(receiptContinuation) !== JSON.stringify(outcome.continuation))
238
+ throw new Error("task_artifact_owner_continuation_outcome_mismatch");
239
+ if (
240
+ (outcome.kind === "payload_retired" &&
241
+ (record.taskArtifactOwnerPayloadRetired !== true ||
242
+ record.taskArtifactOwnerNamespaceRetained !== true)) ||
243
+ (outcome.kind !== "payload_retired" &&
244
+ ("taskArtifactOwnerPayloadRetired" in record || "taskArtifactOwnerNamespaceRetained" in record))
245
+ )
246
+ throw new Error("task_artifact_owner_continuation_outcome_mismatch");
247
+ }
248
+ }
249
+ return {
250
+ state: state as ManagedGcSessionRetirementState,
251
+ transcriptPath: target.transcriptPath,
252
+ sessionId: target.sessionId,
253
+ cwd: target.cwd,
254
+ transcriptIdentity: identity,
255
+ taskArtifactOwnerLocator: targetLocator,
256
+ taskArtifactOwnerDeletionEvidence: evidence,
257
+ ...(state !== "prepared" ? { artifactsRemoved: true as const } : {}),
258
+ ...(state === "owner_pending" ? { ownerRetirementAttempt: record.ownerRetirementAttempt as number } : {}),
259
+ ...(outcome ? { taskArtifactOwnerRetirementOutcome: outcome } : {}),
260
+ ...(outcome && outcome.kind !== "completed"
261
+ ? { taskArtifactOwnerRetirementContinuation: outcome.continuation }
262
+ : {}),
263
+ ...(outcome?.kind === "payload_retired"
264
+ ? { taskArtifactOwnerPayloadRetired: true as const, taskArtifactOwnerNamespaceRetained: true as const }
265
+ : {}),
266
+ ...(state === "owner_retired" ? { taskArtifactOwnerRetired: true as const } : {}),
267
+ };
268
+ }
269
+
270
+ /** Check only monotone DTO history; this does not establish physical cleanup or current authority. */
271
+ export function continuationExtendsPrevious(
272
+ previous: TaskArtifactOwnerRetirementContinuation,
273
+ next: TaskArtifactOwnerRetirementContinuation,
274
+ ): boolean {
275
+ const previousTree = previous.retainedTreeSnapshot;
276
+ const nextTree = next.retainedTreeSnapshot;
277
+ if (
278
+ previous.parentIdentity.dev !== next.parentIdentity.dev ||
279
+ previous.parentIdentity.ino !== next.parentIdentity.ino ||
280
+ previousTree.rootDev !== nextTree.rootDev ||
281
+ previousTree.rootIno !== nextTree.rootIno ||
282
+ nextTree.entries.length > previousTree.entries.length
283
+ )
284
+ return false;
285
+ const previousEntries = new Map(previousTree.entries.map(entry => [entry.relativePath, entry]));
286
+ for (const entry of nextTree.entries) {
287
+ const old = previousEntries.get(entry.relativePath);
288
+ if (
289
+ !old ||
290
+ old.kind !== entry.kind ||
291
+ old.dev !== entry.dev ||
292
+ old.ino !== entry.ino ||
293
+ old.nlink !== entry.nlink
294
+ )
295
+ return false;
296
+ if (
297
+ entry.kind === "file" &&
298
+ !(
299
+ (entry.size === old.size && entry.sha256 === old.sha256) ||
300
+ (entry.size === "0" && entry.sha256 === EMPTY_PAYLOAD_SHA256)
301
+ )
302
+ )
303
+ return false;
304
+ if (entry.kind !== "file" && entry.size !== old.size) return false;
305
+ }
306
+ for (const key of [
307
+ "detachedPaths",
308
+ "nativeCodes",
309
+ "retainedSuccessorPaths",
310
+ "retainedPlaceholderPaths",
311
+ "retainedUnknownPaths",
312
+ "windowsErrorCodes",
313
+ ] as const) {
314
+ const oldValues = previous[key] ?? [];
315
+ const nextValues = next[key] ?? [];
316
+ if (!oldValues.every(item => nextValues.includes(item))) return false;
317
+ }
318
+ return previous.payloadDurable !== true || next.payloadDurable === true;
319
+ }