@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
@@ -8,7 +8,9 @@ import type {
8
8
  NativeExactFileIdentity,
9
9
  NativeExactUnlinkResult,
10
10
  NativeOwnerOnlySecurityResult,
11
+ RecoveryFsFile,
11
12
  RecoveryFsIdentity,
13
+ RecoveryFsResult,
12
14
  RecoveryFsRoot,
13
15
  } from "@gajae-code/natives";
14
16
  import { isEnoent, logger } from "@gajae-code/utils";
@@ -49,6 +51,7 @@ export const MANAGED_ARTIFACT_MAX_DEPTH = 32;
49
51
  export const MANAGED_ARTIFACT_MAX_FILES = 50_000;
50
52
  export const MANAGED_ARTIFACT_MAX_FILE_BYTES = 128 * 1024 * 1024;
51
53
  export const MANAGED_ARTIFACT_MAX_TOTAL_BYTES = 512 * 1024 * 1024;
54
+ export const MANAGED_SESSION_READ_RANGE_MAX_BYTES = 64 * 1024 * 1024;
52
55
  const REPLACEMENT_CLEANUP_RECEIPT_MAX_BYTES = 64 * 1024;
53
56
  const REPLACEMENT_CLEANUP_RECEIPT_SCAN_LIMIT = MANAGED_ARTIFACT_MAX_FILES;
54
57
  export const MANAGED_ARTIFACT_COPY_BATCH_SIZE = 256;
@@ -390,6 +393,15 @@ const replacementPublisherOwnerId = randomUUID();
390
393
  const activeReplacementAttempts = new Set<string>();
391
394
  const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
392
395
 
396
+ /** Exact quarantine names used when native exact-unlink leaves a scrubbed lock inode. */
397
+ export function isManagedLockQuarantineName(name: string): boolean {
398
+ const prefix = ".gjc-lock-";
399
+ const suffix = ".stale";
400
+ return (
401
+ name.startsWith(prefix) && name.endsWith(suffix) && UUID_PATTERN.test(name.slice(prefix.length, -suffix.length))
402
+ );
403
+ }
404
+
393
405
  function replacementReceiptPublisher(bytes: Uint8Array): ReplacementReceiptPublisher | undefined {
394
406
  let record: Record<string, unknown> | undefined;
395
407
  try {
@@ -874,43 +886,24 @@ async function hashReaperFile(pathname: string, expected: fs.BigIntStats): Promi
874
886
  }
875
887
  }
876
888
 
877
- function exactReaperUnlinkSync(
878
- pathname: string,
879
- stat: fs.BigIntStats,
880
- sha256: string,
881
- allowHardLink: boolean,
882
- ): NativeExactUnlinkResult {
883
- const parent = fs.lstatSync(path.dirname(pathname), { bigint: true });
884
- if (!parent.isDirectory() || parent.isSymbolicLink()) return { ok: false, code: "parent_mismatch" };
885
- const identity: NativeExactFileIdentity = {
886
- dev: stat.dev,
887
- ino: stat.ino,
888
- nlink: stat.nlink,
889
- parentDev: parent.dev,
890
- parentIno: parent.ino,
891
- size: stat.size,
892
- mtimeNs: stat.mtimeNs,
893
- sha256,
894
- quarantineName: remnantReapQuarantineName(pathname),
895
- ...(allowHardLink ? { allowHardLink: true, requireHardLink: true } : {}),
896
- };
897
- return nativeSessionStorage().exactUnlinkDirect(pathname, identity);
889
+ interface ReaperParentIdentity {
890
+ readonly dev: bigint;
891
+ readonly ino: bigint;
898
892
  }
899
893
 
900
- async function exactReaperUnlink(
894
+ function exactReaperUnlinkSync(
901
895
  pathname: string,
902
896
  stat: fs.BigIntStats,
903
897
  sha256: string,
904
898
  allowHardLink: boolean,
905
- ): Promise<NativeExactUnlinkResult> {
906
- const parent = await fsp.lstat(path.dirname(pathname), { bigint: true });
907
- if (!parent.isDirectory() || parent.isSymbolicLink()) return { ok: false, code: "parent_mismatch" };
899
+ parentIdentity: ReaperParentIdentity,
900
+ ): NativeExactUnlinkResult {
908
901
  const identity: NativeExactFileIdentity = {
909
902
  dev: stat.dev,
910
903
  ino: stat.ino,
911
904
  nlink: stat.nlink,
912
- parentDev: parent.dev,
913
- parentIno: parent.ino,
905
+ parentDev: parentIdentity.dev,
906
+ parentIno: parentIdentity.ino,
914
907
  size: stat.size,
915
908
  mtimeNs: stat.mtimeNs,
916
909
  sha256,
@@ -957,10 +950,12 @@ export function reapScrubbedProtocolRemnantsSync(
957
950
  const cutoff = Date.now() - minAgeMs;
958
951
  let reaped = 0;
959
952
  let failures = 0;
953
+ // Immutable expected identity only; native reopens and revalidates the parent per unlink.
954
+ let parentIdentity: ReaperParentIdentity | undefined;
960
955
  for (const name of names) {
961
956
  const terminalRemnant = SCRUBBED_REMNANT_PREFIXES.some(prefix => name.startsWith(prefix));
962
- const replacementCandidate = REPLACEMENT_STAGING_NAME.test(name);
963
- const quarantineCandidate = REAPER_QUARANTINE_NAME.test(name);
957
+ const replacementCandidate = !terminalRemnant && REPLACEMENT_STAGING_NAME.test(name);
958
+ const quarantineCandidate = !terminalRemnant && REAPER_QUARANTINE_NAME.test(name);
964
959
  if (!terminalRemnant && !replacementCandidate && !quarantineCandidate) continue;
965
960
  const alias = terminalRemnant
966
961
  ? undefined
@@ -980,9 +975,29 @@ export function reapScrubbedProtocolRemnantsSync(
980
975
  if (named.mtimeMs > cutoff) continue;
981
976
  const sha256 = terminalRemnant ? EMPTY_FILE_SHA256 : hashReaperFileSync(pathname, named);
982
977
  if (!sha256) continue;
983
- const removed = exactReaperUnlinkSync(pathname, named, sha256, !terminalRemnant);
978
+ if (!parentIdentity) {
979
+ const parent = fs.lstatSync(path.dirname(pathname), { bigint: true });
980
+ if (!parent.isDirectory() || parent.isSymbolicLink()) {
981
+ failures += 1;
982
+ continue;
983
+ }
984
+ parentIdentity = { dev: parent.dev, ino: parent.ino };
985
+ }
986
+ const removed = exactReaperUnlinkSync(pathname, named, sha256, !terminalRemnant, parentIdentity);
984
987
  if (removed.ok) reaped += 1;
985
- else if (!isBenignReaperRace(removed)) failures += 1;
988
+ else {
989
+ let parentKindMismatch = false;
990
+ if (removed.code === "reparse_point") {
991
+ try {
992
+ const parent = fs.lstatSync(path.dirname(pathname), { bigint: true });
993
+ parentKindMismatch = !parent.isDirectory() || parent.isSymbolicLink();
994
+ } catch (error) {
995
+ // Preserve lookup-error accounting without changing or retrying the native result.
996
+ if (!isEnoent(error)) failures += 1;
997
+ }
998
+ }
999
+ if (parentKindMismatch || !isBenignReaperRace(removed)) failures += 1;
1000
+ }
986
1001
  } catch (error) {
987
1002
  if (!isEnoent(error)) failures += 1;
988
1003
  }
@@ -1016,10 +1031,11 @@ export async function reapScrubbedProtocolRemnants(
1016
1031
  let reaped = 0;
1017
1032
  let failures = 0;
1018
1033
  let scanned = 0;
1034
+ let parentIdentity: ReaperParentIdentity | undefined;
1019
1035
  for (const name of names) {
1020
1036
  const terminalRemnant = SCRUBBED_REMNANT_PREFIXES.some(prefix => name.startsWith(prefix));
1021
- const replacementCandidate = REPLACEMENT_STAGING_NAME.test(name);
1022
- const quarantineCandidate = REAPER_QUARANTINE_NAME.test(name);
1037
+ const replacementCandidate = !terminalRemnant && REPLACEMENT_STAGING_NAME.test(name);
1038
+ const quarantineCandidate = !terminalRemnant && REAPER_QUARANTINE_NAME.test(name);
1023
1039
  if (!terminalRemnant && !replacementCandidate && !quarantineCandidate) continue;
1024
1040
  if (++scanned % SCRUBBED_REMNANT_REAP_BATCH_SIZE === 0) await Bun.sleep(0);
1025
1041
  const alias = terminalRemnant
@@ -1038,9 +1054,29 @@ export async function reapScrubbedProtocolRemnants(
1038
1054
  if (named.mtimeMs > cutoff) continue;
1039
1055
  const sha256 = terminalRemnant ? EMPTY_FILE_SHA256 : await hashReaperFile(pathname, named);
1040
1056
  if (!sha256) continue;
1041
- const removed = await exactReaperUnlink(pathname, named, sha256, !terminalRemnant);
1057
+ if (!parentIdentity) {
1058
+ const parent = await fsp.lstat(path.dirname(pathname), { bigint: true });
1059
+ if (!parent.isDirectory() || parent.isSymbolicLink()) {
1060
+ failures += 1;
1061
+ continue;
1062
+ }
1063
+ parentIdentity = { dev: parent.dev, ino: parent.ino };
1064
+ }
1065
+ const removed = await exactReaperUnlinkSync(pathname, named, sha256, !terminalRemnant, parentIdentity);
1042
1066
  if (removed.ok) reaped += 1;
1043
- else if (!isBenignReaperRace(removed)) failures += 1;
1067
+ else {
1068
+ let parentKindMismatch = false;
1069
+ if (removed.code === "reparse_point") {
1070
+ try {
1071
+ const parent = await fsp.lstat(path.dirname(pathname), { bigint: true });
1072
+ parentKindMismatch = !parent.isDirectory() || parent.isSymbolicLink();
1073
+ } catch (error) {
1074
+ // Preserve lookup-error accounting without changing or retrying the native result.
1075
+ if (!isEnoent(error)) failures += 1;
1076
+ }
1077
+ }
1078
+ if (parentKindMismatch || !isBenignReaperRace(removed)) failures += 1;
1079
+ }
1044
1080
  } catch (error) {
1045
1081
  if (!isEnoent(error)) failures += 1;
1046
1082
  }
@@ -1163,7 +1199,7 @@ type RetainedManagedReplacer = {
1163
1199
  expectedSha256: string,
1164
1200
  ): { ok: boolean; code?: string };
1165
1201
  };
1166
- type LockRecord = {
1202
+ export type LockRecord = {
1167
1203
  attemptId: string;
1168
1204
  pid: number;
1169
1205
  bootId?: string;
@@ -1429,7 +1465,12 @@ function secureExistingManagedDirectory(pathname: string, kind: "directory" | "f
1429
1465
  );
1430
1466
  assertManagedPathIdentity(pathname, kind, named);
1431
1467
  if (verified.ok) return;
1432
- if (verified.code !== "acl_verify_failed") throw securityError(pathname, verified);
1468
+ // owner_mismatch: directory owner is not the current user. Repair attempts ownership transfer.
1469
+ // If the user lacks SeTakeOwnershipPrivilege (non-elevated), the repair will fail with
1470
+ // io_error, which is caught as a security error with clear context below.
1471
+ if (verified.code !== "acl_verify_failed" && verified.code !== "owner_mismatch") {
1472
+ throw securityError(pathname, verified);
1473
+ }
1433
1474
  const repaired = validateNativeSecurityResult(
1434
1475
  nativeSessionStorage().repairOwnerOnlyPathSecurityExpected(
1435
1476
  pathname,
@@ -1440,7 +1481,16 @@ function secureExistingManagedDirectory(pathname: string, kind: "directory" | "f
1440
1481
  "verify",
1441
1482
  kind,
1442
1483
  );
1443
- if (!repaired.ok) throw securityError(pathname, repaired);
1484
+ if (!repaired.ok) {
1485
+ // If initial verification was owner_mismatch and repair failed (typically io_error
1486
+ // when the user lacks SeTakeOwnershipPrivilege), provide clear context.
1487
+ if (verified.code === "owner_mismatch" && repaired.code === "io_error") {
1488
+ throw new Error(
1489
+ `${kind} owner mismatch: unable to take ownership (need administrator privileges or file ownership): ${pathname}`,
1490
+ );
1491
+ }
1492
+ throw securityError(pathname, repaired);
1493
+ }
1444
1494
  assertManagedPathIdentity(pathname, kind, named);
1445
1495
  }
1446
1496
 
@@ -1452,11 +1502,18 @@ function secureManagedDirectory(pathname: string, created: boolean, policy: Mana
1452
1502
  secure(pathname, "directory");
1453
1503
  }
1454
1504
 
1505
+ /** Exact, identity-bound read access retained for the lifetime of one managed stream. */
1506
+ export interface ManagedSessionReadLease {
1507
+ readRange(start: number, length: number): Uint8Array;
1508
+ close(): void;
1509
+ }
1510
+
1455
1511
  /** Internal retained-root capability for one managed descendant subtree. */
1456
1512
  export class ManagedSessionDescendantStore {
1457
1513
  readonly #root: ManagedDirectoryRoot;
1458
1514
  readonly #baseDir: string;
1459
1515
  readonly #policy: ManagedSessionSecurityPolicy;
1516
+ readonly #access: "read-only" | "read-write";
1460
1517
  readonly #authority: RecoveryFsRoot | undefined;
1461
1518
  #ownsAuthority = false;
1462
1519
  #closed = false;
@@ -1474,12 +1531,19 @@ export class ManagedSessionDescendantStore {
1474
1531
  retained?: { authority: RecoveryFsRoot; authorityBaseDir: string },
1475
1532
  policy?: ManagedSessionSecurityPolicy,
1476
1533
  profileAgentDir?: string,
1534
+ expectedSubtreeRoot?: ManagedDirectoryRoot,
1535
+ access: "read-only" | "read-write" = "read-write",
1477
1536
  ) {
1537
+ if (access !== "read-only" && access !== "read-write") throw new Error("managed_store_access_invalid");
1538
+ if (access === "read-only" && retained) throw new Error("managed_read_store_cannot_borrow_authority");
1539
+ if (access === "read-only" && !expectedSubtreeRoot)
1540
+ throw new Error("managed_read_store_requires_existing_identity");
1478
1541
  managedRelativePath(root, baseDir);
1479
1542
 
1480
1543
  this.#root = root;
1481
1544
  this.#baseDir = path.resolve(baseDir);
1482
1545
  this.#policy = policy ?? "default";
1546
+ this.#access = access;
1483
1547
  this.#profileAgentDir = profileAgentDir ?? root.canonicalPath;
1484
1548
  this.#authorityBaseDir = retained?.authorityBaseDir ?? this.#baseDir;
1485
1549
  if (retained) {
@@ -1512,19 +1576,52 @@ export class ManagedSessionDescendantStore {
1512
1576
  });
1513
1577
  }
1514
1578
  this.#authority = retained.authority;
1579
+ if (
1580
+ expectedSubtreeRoot &&
1581
+ (expectedSubtreeRoot.canonicalPath !== this.#baseDir ||
1582
+ expectedSubtreeRoot.dev !== this.#subtreeRoot.dev ||
1583
+ expectedSubtreeRoot.ino !== this.#subtreeRoot.ino)
1584
+ )
1585
+ throw new Error("Managed subtree authority changed during establishment");
1515
1586
  this.#assertBound();
1516
1587
 
1517
1588
  return;
1518
1589
  }
1519
1590
  assertManagedDirectoryRoot(root);
1520
- ensureManagedDirectory(this.#baseDir, root, this.#policy);
1591
+ if (expectedSubtreeRoot) {
1592
+ if (expectedSubtreeRoot.canonicalPath !== this.#baseDir)
1593
+ throw new Error("Managed subtree authority path mismatch");
1594
+ assertManagedDirectoryRoot(expectedSubtreeRoot);
1595
+ const verified = validateNativeSecurityResult(
1596
+ process.platform === "win32"
1597
+ ? nativeSessionStorage().verifyOwnerOnlyPathSecurityExpected(
1598
+ this.#baseDir,
1599
+ "directory",
1600
+ expectedSubtreeRoot.dev,
1601
+ expectedSubtreeRoot.ino,
1602
+ )
1603
+ : nativeSessionStorage().verifyOwnerOnlyPathSecurity(this.#baseDir, "directory"),
1604
+ "verify",
1605
+ "directory",
1606
+ );
1607
+ if (!verified.ok) throw securityError(this.#baseDir, verified);
1608
+ assertManagedDirectoryRoot(expectedSubtreeRoot);
1609
+ } else {
1610
+ ensureManagedDirectory(this.#baseDir, root, this.#policy);
1611
+ }
1521
1612
  const subtreeStat = fs.lstatSync(this.#baseDir, { bigint: true });
1613
+ if (
1614
+ expectedSubtreeRoot &&
1615
+ (canonicalFileId(subtreeStat.dev) !== expectedSubtreeRoot.dev ||
1616
+ canonicalFileId(subtreeStat.ino) !== expectedSubtreeRoot.ino)
1617
+ )
1618
+ throw new Error("Managed subtree authority changed during establishment");
1522
1619
  this.#subtreeRoot = Object.freeze({
1523
1620
  canonicalPath: this.#baseDir,
1524
1621
  dev: canonicalFileId(subtreeStat.dev),
1525
1622
  ino: canonicalFileId(subtreeStat.ino),
1526
1623
  });
1527
- if (process.platform === "linux") {
1624
+ if (process.platform === "linux" && access === "read-write") {
1528
1625
  const before = fs.lstatSync(this.#baseDir, { bigint: true });
1529
1626
  const authority = nativeSessionStorage().openRecoveryFsRoot(this.#baseDir);
1530
1627
  const retained = authority.identity();
@@ -1572,19 +1669,27 @@ export class ManagedSessionDescendantStore {
1572
1669
  child.dev.toString(),
1573
1670
  child.ino.toString(),
1574
1671
  );
1575
- return new ManagedSessionDescendantStore(
1576
- this.#root,
1577
- resolved,
1578
- {
1579
- authority: retainedChild,
1580
- authorityBaseDir: resolved,
1581
- },
1582
- this.#policy,
1583
- this.#profileAgentDir,
1584
- );
1672
+ try {
1673
+ const derived = new ManagedSessionDescendantStore(
1674
+ this.#root,
1675
+ resolved,
1676
+ {
1677
+ authority: retainedChild,
1678
+ authorityBaseDir: resolved,
1679
+ },
1680
+ this.#policy,
1681
+ this.#profileAgentDir,
1682
+ );
1683
+ derived.#ownsAuthority = true;
1684
+ return derived;
1685
+ } catch (error) {
1686
+ retainedChild.close();
1687
+ throw error;
1688
+ }
1585
1689
  }
1586
1690
 
1587
1691
  retainAuthority(): RecoveryFsRoot | undefined {
1692
+ this.#assertWritable();
1588
1693
  if (!this.#authority) return undefined;
1589
1694
  return this.#authority.retainManagedDirectory(
1590
1695
  "",
@@ -1634,11 +1739,56 @@ export class ManagedSessionDescendantStore {
1634
1739
  }
1635
1740
  this.#assertBound();
1636
1741
  }
1742
+
1743
+ /** Capture a directory identity through this store's retained managed root without repairing it. */
1744
+ captureDirectoryIdentity(relativePath: string): { dev: string; ino: string } {
1745
+ this.#assertBound();
1746
+ const resolved = this.#resolve(relativePath);
1747
+ if (this.#access === "read-only" || !this.#authority) this.#assertPathBackedDirectoryChain(resolved);
1748
+ const named = fs.lstatSync(resolved, { bigint: true });
1749
+ if (!named.isDirectory() || named.isSymbolicLink()) throw new Error("managed_directory_identity_unavailable");
1750
+ const dev = canonicalFileId(named.dev);
1751
+ const ino = canonicalFileId(named.ino);
1752
+ if (dev !== this.#subtreeRoot.dev) throw new Error("managed_directory_identity_unavailable");
1753
+ if (this.#authority) {
1754
+ const retainedRelative = this.#relative(resolved);
1755
+ if (retainedRelative === "") {
1756
+ if (dev !== this.#subtreeRoot.dev || ino !== this.#subtreeRoot.ino)
1757
+ throw new Error("Managed descendant root binding changed");
1758
+ } else {
1759
+ const retained = this.#authority.retainManagedDirectory(retainedRelative, dev.toString(), ino.toString());
1760
+ try {
1761
+ const identity = retained.identity();
1762
+ if (
1763
+ !identity.ok ||
1764
+ !identity.identity ||
1765
+ identity.identity.dev !== dev.toString() ||
1766
+ identity.identity.ino !== ino.toString()
1767
+ )
1768
+ throw new Error(identity.code ?? "managed_directory_identity_unavailable");
1769
+ } finally {
1770
+ retained.close();
1771
+ }
1772
+ }
1773
+ }
1774
+ const after = fs.lstatSync(resolved, { bigint: true });
1775
+ if (
1776
+ !after.isDirectory() ||
1777
+ after.isSymbolicLink() ||
1778
+ canonicalFileId(after.dev) !== dev ||
1779
+ canonicalFileId(after.ino) !== ino
1780
+ )
1781
+ throw new Error("managed_directory_identity_changed");
1782
+ this.#assertBound();
1783
+ return { dev: dev.toString(), ino: ino.toString() };
1784
+ }
1785
+
1637
1786
  #assertBound(): void {
1638
1787
  if (!this.#authority) {
1639
- const named = fs.statSync(this.#baseDir, { bigint: true });
1788
+ const named = fs.lstatSync(this.#baseDir, { bigint: true });
1640
1789
  if (
1641
1790
  !named.isDirectory() ||
1791
+ named.isSymbolicLink() ||
1642
1792
  canonicalFileId(named.dev) !== this.#subtreeRoot.dev ||
1643
1793
  canonicalFileId(named.ino) !== this.#subtreeRoot.ino
1644
1794
  )
@@ -1941,7 +2091,12 @@ export class ManagedSessionDescendantStore {
1941
2091
  }
1942
2092
  }
1943
2093
 
2094
+ #assertWritable(): void {
2095
+ if (this.#access === "read-only") throw new Error("managed_store_read_only");
2096
+ }
2097
+
1944
2098
  #beforeMutation(): void {
2099
+ this.#assertWritable();
1945
2100
  this.#assertBound();
1946
2101
  // Reaping is scheduled BEFORE reconciliation, not after. The receipt scan
1947
2102
  // throws `managed_replace_cleanup_receipt_limit_exceeded` once the bound
@@ -2579,7 +2734,8 @@ export class ManagedSessionDescendantStore {
2579
2734
  if (!Number.isSafeInteger(start) || start < 0 || !Number.isSafeInteger(length) || length < 0)
2580
2735
  throw new RangeError("Invalid managed range read");
2581
2736
  if (start > Number.MAX_SAFE_INTEGER - length) throw new RangeError("Managed range read start overflows");
2582
- if (length > 64 * 1024 * 1024) throw new RangeError("Managed range read exceeds the bounded maximum");
2737
+ if (length > MANAGED_SESSION_READ_RANGE_MAX_BYTES)
2738
+ throw new RangeError("Managed range read exceeds the bounded maximum");
2583
2739
  this.#assertPathBackedReadRelative(relativePath);
2584
2740
  this.#assertBound();
2585
2741
  const rootBefore = fs.lstatSync(this.#baseDir, { bigint: true });
@@ -2649,7 +2805,10 @@ export class ManagedSessionDescendantStore {
2649
2805
  named.isSymbolicLink() ||
2650
2806
  named.dev !== before.dev ||
2651
2807
  named.ino !== before.ino ||
2652
- named.nlink !== before.nlink
2808
+ named.nlink !== before.nlink ||
2809
+ named.size !== before.size ||
2810
+ named.mtimeNs !== before.mtimeNs ||
2811
+ named.ctimeNs !== before.ctimeNs
2653
2812
  )
2654
2813
  throw new Error("source_changed");
2655
2814
  const rootAfter = fs.lstatSync(this.#baseDir, { bigint: true });
@@ -2679,6 +2838,124 @@ export class ManagedSessionDescendantStore {
2679
2838
  fs.closeSync(fd);
2680
2839
  }
2681
2840
  }
2841
+
2842
+ /** Open an identity-bound stream lease; Darwin uses exact bounded range reads per chunk. */
2843
+ openReadLease(
2844
+ relativePath: string,
2845
+ expectedDescriptor: Pick<SessionStorageStat, "dev" | "ino" | "nlink" | "size" | "mtimeNs" | "ctimeNs">,
2846
+ ): ManagedSessionReadLease {
2847
+ if (!Number.isSafeInteger(expectedDescriptor.size) || expectedDescriptor.size < 0)
2848
+ throw new RangeError("Invalid managed read identity size");
2849
+ this.#assertPathBackedReadRelative(relativePath);
2850
+ this.#assertBound();
2851
+ if (!this.#authority) {
2852
+ this.readRangeExpectedSync(relativePath, 0, 0, expectedDescriptor);
2853
+ let closed = false;
2854
+ return {
2855
+ readRange: (start, length) => {
2856
+ if (closed) throw new Error("closed");
2857
+ if (start + length > expectedDescriptor.size) throw new Error("range_not_present");
2858
+ return this.readRangeExpectedSync(relativePath, start, length, expectedDescriptor).bytes;
2859
+ },
2860
+ close: () => {
2861
+ if (closed) return;
2862
+ closed = true;
2863
+ this.readRangeExpectedSync(relativePath, 0, 0, expectedDescriptor);
2864
+ },
2865
+ };
2866
+ }
2867
+
2868
+ const relative = this.#relative(this.#resolve(relativePath));
2869
+ const authority = this.#authority;
2870
+ const matchesExpected = (identity: RecoveryFsIdentity | undefined): boolean => {
2871
+ if (!identity) return false;
2872
+ const actual = managedFileIdentityFromNative(identity);
2873
+ return (
2874
+ actual.dev === expectedDescriptor.dev &&
2875
+ actual.ino === expectedDescriptor.ino &&
2876
+ (expectedDescriptor.nlink === undefined || actual.nlink === expectedDescriptor.nlink) &&
2877
+ actual.size === expectedDescriptor.size &&
2878
+ actual.mtimeNs === expectedDescriptor.mtimeNs &&
2879
+ actual.ctimeNs === expectedDescriptor.ctimeNs
2880
+ );
2881
+ };
2882
+ const assertCurrentBinding = (): void => {
2883
+ this.#assertBound();
2884
+ const named = authority.stat(relative);
2885
+ if (!named.ok || !matchesExpected(named.identity)) throw new Error("source_changed");
2886
+ this.#assertBound();
2887
+ };
2888
+
2889
+ let file: RecoveryFsFile | undefined;
2890
+ try {
2891
+ file = authority.openFile(relative);
2892
+ const opened = file.identity();
2893
+ if (!opened.ok || !matchesExpected(opened.identity)) throw new Error("source_changed");
2894
+ assertCurrentBinding();
2895
+ } catch (error) {
2896
+ try {
2897
+ file?.close();
2898
+ } catch {
2899
+ // Preserve the identity/open failure; closing is still attempted.
2900
+ }
2901
+ throw error;
2902
+ }
2903
+
2904
+ let closed = false;
2905
+ return {
2906
+ readRange: (start, length) => {
2907
+ if (closed) throw new Error("closed");
2908
+ if (
2909
+ !Number.isSafeInteger(start) ||
2910
+ start < 0 ||
2911
+ !Number.isSafeInteger(length) ||
2912
+ length < 0 ||
2913
+ start > Number.MAX_SAFE_INTEGER - length
2914
+ )
2915
+ throw new RangeError("Invalid managed range read");
2916
+ if (length > 1024 * 1024) throw new RangeError("Managed read chunk exceeds the bounded maximum");
2917
+ if (start + length > expectedDescriptor.size) throw new Error("range_not_present");
2918
+ assertCurrentBinding();
2919
+ const before = file!.identity();
2920
+ if (!before.ok || !matchesExpected(before.identity)) throw new Error("source_changed");
2921
+ const chunk = file!.readChunk(start, length);
2922
+ if (!chunk.ok || !chunk.data || !chunk.identity) throw new Error(chunk.code ?? "managed_read_failed");
2923
+ if (!matchesExpected(chunk.identity)) throw new Error("source_changed");
2924
+ if (chunk.data.byteLength !== length) throw new Error("range_not_present");
2925
+ const after = file!.identity();
2926
+ if (!after.ok || !matchesExpected(after.identity)) throw new Error("source_changed");
2927
+ assertCurrentBinding();
2928
+ return chunk.data;
2929
+ },
2930
+ close: () => {
2931
+ if (closed) return;
2932
+ closed = true;
2933
+ let failure: unknown;
2934
+ try {
2935
+ assertCurrentBinding();
2936
+ } catch (error) {
2937
+ failure = error;
2938
+ }
2939
+ let result: RecoveryFsResult | undefined;
2940
+ try {
2941
+ result = file!.close();
2942
+ } catch (error) {
2943
+ failure ??= error;
2944
+ }
2945
+ if (failure === undefined && (!result?.ok || !matchesExpected(result.identity)))
2946
+ failure = new Error(result?.code ?? "source_changed");
2947
+ if (failure === undefined) {
2948
+ try {
2949
+ assertCurrentBinding();
2950
+ } catch (error) {
2951
+ failure = error;
2952
+ }
2953
+ }
2954
+ if (failure !== undefined) throw failure;
2955
+ },
2956
+ };
2957
+ }
2958
+
2682
2959
  /** Read an exact managed file without exposing its pathname as authority. */
2683
2960
  readExpected(relativePath: string): ManagedFileSnapshot | null {
2684
2961
  this.#assertBound();
@@ -2715,6 +2992,85 @@ export class ManagedSessionDescendantStore {
2715
2992
  };
2716
2993
  }
2717
2994
 
2995
+ /** Read one complete managed file through retained authority after descriptor-size admission. */
2996
+ readExpectedBounded(
2997
+ relativePath: string,
2998
+ maxBytes: number,
2999
+ admitSize?: (size: number, descriptor: ManagedFileIdentity) => void,
3000
+ ): ManagedFileSnapshot | null {
3001
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) throw new Error("invalid_capture_limit");
3002
+ this.#assertBound();
3003
+ const resolved = this.#resolve(relativePath);
3004
+ if (!this.#authority) {
3005
+ try {
3006
+ this.#assertPathBackedDirectoryChain(resolved);
3007
+ const captured = captureManagedFileNoFollowBounded(resolved, maxBytes, admitSize);
3008
+ this.#assertBound();
3009
+ return captured;
3010
+ } catch (error) {
3011
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
3012
+ throw error;
3013
+ }
3014
+ }
3015
+ let descriptor: SessionStorageRangeSnapshot;
3016
+ try {
3017
+ descriptor = this.readRangeExpectedSync(this.#relative(resolved), 0, 0);
3018
+ } catch (error) {
3019
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
3020
+ throw error;
3021
+ }
3022
+ const { stat } = descriptor;
3023
+ if (stat.nlink !== 1n) throw new Error("source_changed");
3024
+ if (!Number.isSafeInteger(stat.size) || stat.size < 0) throw new Error("source_changed");
3025
+ if (stat.size > maxBytes) throw new Error("artifact_capacity_exceeded");
3026
+ const lease = this.openReadLease(this.#relative(resolved), stat);
3027
+ let failed = false;
3028
+ let failure: unknown;
3029
+ let snapshot: ManagedFileSnapshot | undefined;
3030
+ try {
3031
+ admitSize?.(stat.size, {
3032
+ dev: stat.dev,
3033
+ ino: stat.ino,
3034
+ nlink: stat.nlink,
3035
+ size: stat.size,
3036
+ mtimeNs: stat.mtimeNs,
3037
+ ctimeNs: stat.ctimeNs,
3038
+ });
3039
+ const bytes = Buffer.alloc(stat.size);
3040
+ for (let offset = 0; offset < bytes.byteLength; ) {
3041
+ const length = Math.min(1024 * 1024, bytes.byteLength - offset);
3042
+ bytes.set(lease.readRange(offset, length), offset);
3043
+ offset += length;
3044
+ }
3045
+ snapshot = {
3046
+ bytes,
3047
+ identity: {
3048
+ dev: stat.dev,
3049
+ ino: stat.ino,
3050
+ nlink: stat.nlink,
3051
+ size: stat.size,
3052
+ mtimeNs: stat.mtimeNs,
3053
+ ctimeNs: stat.ctimeNs,
3054
+ sha256: createHash("sha256").update(bytes).digest("hex"),
3055
+ },
3056
+ };
3057
+ } catch (error) {
3058
+ failed = true;
3059
+ failure = error;
3060
+ }
3061
+ try {
3062
+ lease.close();
3063
+ } catch (error) {
3064
+ if (!failed) {
3065
+ failed = true;
3066
+ failure = error;
3067
+ }
3068
+ }
3069
+ if (failed) throw failure;
3070
+ if (!snapshot) throw new Error("Managed descendant snapshot is unavailable");
3071
+ return snapshot;
3072
+ }
3073
+
2718
3074
  /** Remove an exact captured file without reopening its pathname as authority. */
2719
3075
  removeExpected(relativePath: string, expected: ManagedFileSnapshot): void {
2720
3076
  this.#beforeMutation();
@@ -2950,6 +3306,7 @@ export class ManagedSessionDescendantStore {
2950
3306
  sourceStoreRelativePath: string;
2951
3307
  },
2952
3308
  ): ManagedFileSnapshot {
3309
+ this.#assertWritable();
2953
3310
  this.#assertBound();
2954
3311
  const sourceResolved = this.#resolve(sourceRelativePath);
2955
3312
  const destinationResolved = this.#resolve(destinationRelativePath);
@@ -3019,6 +3376,34 @@ export class ManagedSessionDescendantStore {
3019
3376
  if (!removed.ok) throw new Error(removed.code ?? "managed_remove_failed");
3020
3377
  this.#assertBound();
3021
3378
  }
3379
+
3380
+ /** Remove one exact managed tree through the parent-identity-bound native protocol. */
3381
+ removeTreeExpectedWithParentIdentity(
3382
+ relativePath: string,
3383
+ expected: NativeDirectoryTreeSnapshot,
3384
+ parentIdentity: { dev: bigint; ino: bigint },
3385
+ ): NativeExactUnlinkResult {
3386
+ this.#beforeMutation();
3387
+ this.#assertBound();
3388
+ const resolved = this.#resolve(relativePath);
3389
+ const parentPath = path.dirname(resolved);
3390
+ const parentRelative = path.relative(this.#baseDir, parentPath);
3391
+ if (path.isAbsolute(parentRelative) || parentRelative === ".." || parentRelative.startsWith(`..${path.sep}`))
3392
+ throw new Error("managed_remove_parent_outside_store");
3393
+ const currentParent = this.captureDirectoryIdentity(parentRelative);
3394
+ if (
3395
+ currentParent.dev !== canonicalFileId(parentIdentity.dev).toString() ||
3396
+ currentParent.ino !== canonicalFileId(parentIdentity.ino).toString()
3397
+ )
3398
+ throw new Error("managed_remove_parent_identity_mismatch");
3399
+ const removed = nativeSessionStorage().exactRemoveDirectoryTree(resolved, expected, {
3400
+ dev: BigInt(currentParent.dev),
3401
+ ino: BigInt(currentParent.ino),
3402
+ });
3403
+ this.#assertBound();
3404
+ return removed;
3405
+ }
3406
+
3022
3407
  fsyncTree(): NativeDirectoryTreeSnapshot {
3023
3408
  this.#beforeMutation();
3024
3409
  this.#assertBound();
@@ -3191,7 +3576,7 @@ function sameReplacementIdentity(
3191
3576
  );
3192
3577
  }
3193
3578
 
3194
- function parseLockBytes(bytes: Uint8Array): LockRecord | undefined {
3579
+ export function parseManagedLockRecord(bytes: Uint8Array): LockRecord | undefined {
3195
3580
  try {
3196
3581
  const value: unknown = JSON.parse(Buffer.from(bytes).toString("utf8"));
3197
3582
  if (!value || typeof value !== "object") return undefined;
@@ -3214,7 +3599,7 @@ function parseLock(pathname: string): LockRecord | undefined {
3214
3599
  try {
3215
3600
  const stat = fs.lstatSync(pathname);
3216
3601
  if (!stat.isFile() || stat.isSymbolicLink()) return undefined;
3217
- return parseLockBytes(fs.readFileSync(pathname));
3602
+ return parseManagedLockRecord(fs.readFileSync(pathname));
3218
3603
  } catch {
3219
3604
  return undefined;
3220
3605
  }
@@ -3223,7 +3608,7 @@ function parseLock(pathname: string): LockRecord | undefined {
3223
3608
  function captureLock(pathname: string): { record: LockRecord; snapshot: ManagedFileSnapshot } | undefined {
3224
3609
  try {
3225
3610
  const snapshot = captureManagedFileNoFollow(pathname);
3226
- const record = parseLockBytes(snapshot.bytes);
3611
+ const record = parseManagedLockRecord(snapshot.bytes);
3227
3612
  return record ? { record, snapshot } : undefined;
3228
3613
  } catch {
3229
3614
  return undefined;
@@ -3331,17 +3716,41 @@ export function captureManagedFilePrefixNoFollow(pathname: string, maxBytes: num
3331
3716
  return captureManagedFileNoFollowLimit(pathname, maxBytes);
3332
3717
  }
3333
3718
 
3719
+ /** Captures a complete no-follow file only after descriptor-size admission and before allocating its bytes. */
3720
+ export function captureManagedFileNoFollowBounded(
3721
+ pathname: string,
3722
+ maxBytes: number,
3723
+ admitSize?: (size: number, descriptor: ManagedFileIdentity) => void,
3724
+ ): ManagedFileSnapshot {
3725
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) throw new Error("invalid_capture_limit");
3726
+ return captureManagedFileNoFollowLimit(pathname, maxBytes, true, admitSize);
3727
+ }
3728
+
3334
3729
  /** Captures header/hash/copy input from one no-follow descriptor and rechecks the pathname before use. */
3335
3730
  export function captureManagedFileNoFollow(pathname: string): ManagedFileSnapshot {
3336
3731
  return captureManagedFileNoFollowLimit(pathname);
3337
3732
  }
3338
3733
 
3339
- function captureManagedFileNoFollowLimit(pathname: string, maxBytes?: number): ManagedFileSnapshot {
3734
+ function captureManagedFileNoFollowLimit(
3735
+ pathname: string,
3736
+ maxBytes?: number,
3737
+ rejectOversized = false,
3738
+ admitSize?: (size: number, descriptor: ManagedFileIdentity) => void,
3739
+ ): ManagedFileSnapshot {
3340
3740
  const fd = fs.openSync(pathname, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK | (fs.constants.O_NOFOLLOW ?? 0));
3741
+ let failed = false;
3742
+ let failure: unknown;
3743
+ let snapshot: ManagedFileSnapshot | undefined;
3341
3744
  try {
3342
3745
  const before = fs.fstatSync(fd, { bigint: true });
3343
- if (!before.isFile() || before.nlink > 1) throw new Error("source_changed");
3344
- const captureSize = maxBytes === undefined ? Number(before.size) : Math.min(Number(before.size), maxBytes);
3746
+ if (!before.isFile() || (rejectOversized ? before.nlink !== 1n : before.nlink > 1n))
3747
+ throw new Error("source_changed");
3748
+ const fileSize = Number(before.size);
3749
+ if (!Number.isSafeInteger(fileSize) || fileSize < 0) throw new Error("source_changed");
3750
+ if (rejectOversized && maxBytes !== undefined && fileSize > maxBytes)
3751
+ throw new Error("artifact_capacity_exceeded");
3752
+ admitSize?.(fileSize, identity(before));
3753
+ const captureSize = maxBytes === undefined ? fileSize : Math.min(fileSize, maxBytes);
3345
3754
  const bytes = Buffer.alloc(captureSize);
3346
3755
  let offset = 0;
3347
3756
  while (offset < bytes.byteLength) {
@@ -3354,10 +3763,22 @@ function captureManagedFileNoFollowLimit(pathname: string, maxBytes?: number): M
3354
3763
  const named = fs.lstatSync(pathname, { bigint: true });
3355
3764
  if (!named.isFile() || named.isSymbolicLink() || !sameIdentity(identity(before), identity(named)))
3356
3765
  throw new Error("source_changed");
3357
- return { bytes, identity: identity(before, createHash("sha256").update(bytes).digest("hex")) };
3358
- } finally {
3766
+ snapshot = { bytes, identity: identity(before, createHash("sha256").update(bytes).digest("hex")) };
3767
+ } catch (error) {
3768
+ failed = true;
3769
+ failure = error;
3770
+ }
3771
+ try {
3359
3772
  fs.closeSync(fd);
3773
+ } catch (error) {
3774
+ if (!failed) {
3775
+ failed = true;
3776
+ failure = error;
3777
+ }
3360
3778
  }
3779
+ if (failed) throw failure;
3780
+ if (!snapshot) throw new Error("Managed file capture is unavailable");
3781
+ return snapshot;
3361
3782
  }
3362
3783
 
3363
3784
  /** Streams a managed file once while retaining only a bounded header prefix and the full descriptor-bound digest. */