@gajae-code/coding-agent 0.12.21 → 0.13.0

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 (222) hide show
  1. package/CHANGELOG.md +67 -2
  2. package/dist/types/cli/args.d.ts +5 -2
  3. package/dist/types/cli/root-flags.d.ts +246 -0
  4. package/dist/types/cli.d.ts +11 -19
  5. package/dist/types/commands/daemon.d.ts +0 -30
  6. package/dist/types/commands/launch.d.ts +11 -19
  7. package/dist/types/commands/quick-lane.d.ts +32 -0
  8. package/dist/types/commands/sdk.d.ts +21 -15
  9. package/dist/types/config/file-lock-gc.d.ts +2 -0
  10. package/dist/types/config/model-equivalence.d.ts +20 -0
  11. package/dist/types/config/model-profile-activation.d.ts +62 -17
  12. package/dist/types/config/model-profiles.d.ts +10 -0
  13. package/dist/types/config/model-registry.d.ts +68 -1
  14. package/dist/types/config/model-resolver.d.ts +15 -4
  15. package/dist/types/config/models-config-schema.d.ts +7 -2
  16. package/dist/types/config/provider-selection-policy.d.ts +62 -0
  17. package/dist/types/config/settings-schema.d.ts +36 -78
  18. package/dist/types/config/settings.d.ts +2 -0
  19. package/dist/types/edit/renderer.d.ts +5 -2
  20. package/dist/types/edit/streaming.d.ts +13 -0
  21. package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
  22. package/dist/types/extensibility/extensions/compact-handler.d.ts +2 -1
  23. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  24. package/dist/types/gjc-runtime/gc-runtime.d.ts +19 -5
  25. package/dist/types/internal-urls/local-protocol.d.ts +1 -0
  26. package/dist/types/modes/components/model-selector.d.ts +0 -2
  27. package/dist/types/modes/components/provider-order-context.d.ts +45 -0
  28. package/dist/types/modes/components/provider-order-editor.d.ts +24 -0
  29. package/dist/types/modes/components/settings-defs.d.ts +5 -1
  30. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  31. package/dist/types/quick-lane/classify.d.ts +40 -0
  32. package/dist/types/runtime/stt-settings-setup.d.ts +18 -0
  33. package/dist/types/sdk/broker/broker.d.ts +56 -2
  34. package/dist/types/sdk/broker/operation-receipt-ledger.d.ts +25 -0
  35. package/dist/types/sdk/broker/session-index.d.ts +83 -2
  36. package/dist/types/sdk/broker/state-version.d.ts +1 -1
  37. package/dist/types/sdk/bus/notification-service.d.ts +6 -0
  38. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +8 -1
  39. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
  40. package/dist/types/sdk/bus/telegram-daemon.d.ts +26 -0
  41. package/dist/types/sdk/cli/rows.d.ts +98 -0
  42. package/dist/types/sdk/cli/session-cli.d.ts +35 -3
  43. package/dist/types/sdk/elevation/allowlist.d.ts +20 -0
  44. package/dist/types/sdk/elevation/audit.d.ts +93 -0
  45. package/dist/types/sdk/elevation/capability.d.ts +3 -0
  46. package/dist/types/sdk/elevation/digest.d.ts +72 -0
  47. package/dist/types/sdk/elevation/dispatch-receipt.d.ts +85 -0
  48. package/dist/types/sdk/elevation/grant-ledger.d.ts +163 -0
  49. package/dist/types/sdk/elevation/owner.d.ts +48 -0
  50. package/dist/types/sdk/guides/cache.d.ts +98 -0
  51. package/dist/types/sdk/guides/catalog.d.ts +156 -0
  52. package/dist/types/sdk/guides/cli.d.ts +39 -0
  53. package/dist/types/sdk/guides/manifest.d.ts +66 -0
  54. package/dist/types/sdk/guides/verify.d.ts +66 -0
  55. package/dist/types/sdk/host/control/dispatch.d.ts +8 -0
  56. package/dist/types/sdk/host/control/index.d.ts +1 -1
  57. package/dist/types/sdk/host/control/operations.d.ts +3 -2
  58. package/dist/types/sdk/host/query/cursor.d.ts +10 -0
  59. package/dist/types/sdk/host/query/handlers.d.ts +21 -0
  60. package/dist/types/sdk/host/session-runtime.d.ts +9 -0
  61. package/dist/types/sdk/mcp/server.d.ts +2 -2
  62. package/dist/types/sdk/prompt-status.d.ts +11 -0
  63. package/dist/types/sdk/session.d.ts +4 -0
  64. package/dist/types/sdk/transport/index.d.ts +1 -0
  65. package/dist/types/sdk/transport/relay.d.ts +1 -0
  66. package/dist/types/sdk/transport/serve-cli.d.ts +20 -1
  67. package/dist/types/session/agent-session.d.ts +32 -18
  68. package/dist/types/session/blob-store.d.ts +13 -7
  69. package/dist/types/session/fallback-chain-controller.d.ts +15 -0
  70. package/dist/types/session/internal/managed-session-storage.d.ts +25 -1
  71. package/dist/types/session/internal/session-memory-sidecar.d.ts +5 -5
  72. package/dist/types/session/session-manager.d.ts +139 -10
  73. package/dist/types/session/session-storage.d.ts +48 -0
  74. package/dist/types/setup/provider-onboarding.d.ts +7 -1
  75. package/dist/types/slash-commands/builtin-registry.d.ts +14 -0
  76. package/dist/types/stt/setup.d.ts +2 -0
  77. package/dist/types/task/executor.d.ts +10 -2
  78. package/dist/types/tools/browser/runtime-diagnostics.d.ts +65 -0
  79. package/dist/types/tools/browser/tab-protocol.d.ts +4 -0
  80. package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
  81. package/dist/types/tools/browser/tab-worker.d.ts +2 -0
  82. package/dist/types/tools/browser.d.ts +2 -0
  83. package/dist/types/tools/image-gen.d.ts +14 -18
  84. package/dist/types/tools/index.d.ts +2 -0
  85. package/dist/types/tools/resource-gc.d.ts +2 -0
  86. package/dist/types/utils/pasted-image-path.d.ts +6 -0
  87. package/package.json +9 -9
  88. package/scripts/generate-sdk-operation-inventory.ts +8 -0
  89. package/scripts/run-test-manifest.ts +61 -21
  90. package/scripts/verify-gjc-sdk-canonicalization.ts +9 -4
  91. package/src/cli/args.ts +62 -9
  92. package/src/cli/completion-cli.ts +1 -0
  93. package/src/cli/fast-help.ts +1 -0
  94. package/src/cli/root-flags.ts +100 -0
  95. package/src/cli/setup-cli.ts +6 -2
  96. package/src/cli.ts +42 -65
  97. package/src/commands/acp.ts +1 -1
  98. package/src/commands/daemon.ts +2 -31
  99. package/src/commands/gc.ts +1 -1
  100. package/src/commands/launch.ts +8 -129
  101. package/src/commands/quick-lane.ts +51 -0
  102. package/src/commands/sdk.ts +200 -26
  103. package/src/commands/setup.ts +4 -2
  104. package/src/config/file-lock-gc.ts +4 -1
  105. package/src/config/model-equivalence.ts +76 -7
  106. package/src/config/model-profile-activation.ts +1115 -265
  107. package/src/config/model-profiles.ts +134 -2
  108. package/src/config/model-registry.ts +352 -50
  109. package/src/config/model-resolver.ts +289 -38
  110. package/src/config/models-config-schema.ts +11 -4
  111. package/src/config/provider-selection-policy.ts +146 -0
  112. package/src/config/settings-schema.ts +50 -77
  113. package/src/config/settings.ts +40 -0
  114. package/src/coordinator-mcp/server.ts +32 -2
  115. package/src/edit/renderer.ts +175 -223
  116. package/src/edit/streaming.ts +108 -1
  117. package/src/export/html/index.ts +57 -7
  118. package/src/extensibility/custom-tools/types.ts +2 -0
  119. package/src/extensibility/extensions/compact-handler.ts +3 -2
  120. package/src/extensibility/extensions/runner.ts +6 -0
  121. package/src/extensibility/extensions/types.ts +3 -0
  122. package/src/gjc-runtime/gc-runtime.ts +379 -20
  123. package/src/gjc-runtime/launch-worktree.ts +5 -1
  124. package/src/gjc-runtime/team-runtime.ts +59 -2
  125. package/src/gjc-runtime/tmux-owner-isolation.ts +19 -0
  126. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +16 -1
  127. package/src/internal-urls/docs-index.generated.ts +17 -13
  128. package/src/internal-urls/local-protocol.ts +1 -0
  129. package/src/main.ts +40 -3
  130. package/src/memories/index.ts +4 -2
  131. package/src/modes/acp/acp-agent.ts +235 -91
  132. package/src/modes/components/model-selector.ts +206 -42
  133. package/src/modes/components/provider-order-context.ts +197 -0
  134. package/src/modes/components/provider-order-editor.ts +441 -0
  135. package/src/modes/components/settings-defs.ts +21 -1
  136. package/src/modes/components/settings-selector.ts +45 -2
  137. package/src/modes/components/tool-execution.ts +47 -15
  138. package/src/modes/controllers/command-controller.ts +6 -3
  139. package/src/modes/controllers/extension-ui-controller.ts +2 -0
  140. package/src/modes/controllers/input-controller.ts +73 -2
  141. package/src/modes/controllers/selector-controller.ts +260 -153
  142. package/src/modes/prompt-suggestion-controller.ts +1 -1
  143. package/src/modes/runtime-init.ts +1 -0
  144. package/src/modes/utils/ui-helpers.ts +18 -0
  145. package/src/prompts/tools/browser.md +1 -0
  146. package/src/quick-lane/classify.ts +257 -0
  147. package/src/rlm/index.ts +1 -1
  148. package/src/runtime/stt-settings-setup.ts +57 -0
  149. package/src/sdk/acp/adapter.ts +2 -1
  150. package/src/sdk/broker/broker.ts +780 -12
  151. package/src/sdk/broker/lifecycle.ts +70 -29
  152. package/src/sdk/broker/operation-receipt-ledger.ts +144 -0
  153. package/src/sdk/broker/session-index.ts +463 -48
  154. package/src/sdk/broker/state-version.ts +13 -9
  155. package/src/sdk/broker/transport.ts +14 -1
  156. package/src/sdk/bus/index.ts +34 -13
  157. package/src/sdk/bus/kind-aware-reconciliation.ts +21 -2
  158. package/src/sdk/bus/notification-service.ts +47 -1
  159. package/src/sdk/bus/prompt-reconciliation.ts +22 -3
  160. package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
  161. package/src/sdk/bus/telegram-daemon.ts +54 -3
  162. package/src/sdk/cli/rows.ts +247 -0
  163. package/src/sdk/cli/session-cli.ts +930 -79
  164. package/src/sdk/elevation/allowlist.ts +54 -0
  165. package/src/sdk/elevation/audit.ts +208 -0
  166. package/src/sdk/elevation/capability.ts +41 -0
  167. package/src/sdk/elevation/digest.ts +129 -0
  168. package/src/sdk/elevation/dispatch-receipt.ts +184 -0
  169. package/src/sdk/elevation/grant-ledger.ts +1142 -0
  170. package/src/sdk/elevation/owner.ts +109 -0
  171. package/src/sdk/guides/cache.ts +363 -0
  172. package/src/sdk/guides/catalog.ts +554 -0
  173. package/src/sdk/guides/cli.ts +235 -0
  174. package/src/sdk/guides/manifest.ts +152 -0
  175. package/src/sdk/guides/verify.ts +178 -0
  176. package/src/sdk/host/control/dispatch.ts +49 -1
  177. package/src/sdk/host/control/index.ts +1 -0
  178. package/src/sdk/host/control/operations.ts +8 -1
  179. package/src/sdk/host/query/cursor.ts +47 -3
  180. package/src/sdk/host/query/handlers.ts +155 -2
  181. package/src/sdk/host/sdk-surface-parity.test.ts +1 -0
  182. package/src/sdk/host/session-runtime.test.ts +391 -1
  183. package/src/sdk/host/session-runtime.ts +308 -73
  184. package/src/sdk/host/surface-policy.ts +3 -0
  185. package/src/sdk/host/websocket-transport.ts +7 -1
  186. package/src/sdk/mcp/server.ts +88 -15
  187. package/src/sdk/prompt-status.ts +8 -0
  188. package/src/sdk/protocol/operation-inventory.generated.json +62 -0
  189. package/src/sdk/protocol/operation-registry.ts +3 -2
  190. package/src/sdk/session.ts +87 -39
  191. package/src/sdk/transport/index.ts +1 -0
  192. package/src/sdk/transport/relay.ts +3 -0
  193. package/src/sdk/transport/serve-cli.ts +111 -24
  194. package/src/secrets/obfuscator.ts +92 -12
  195. package/src/session/agent-session.ts +322 -93
  196. package/src/session/blob-store.ts +60 -8
  197. package/src/session/fallback-chain-controller.ts +35 -0
  198. package/src/session/internal/managed-session-scope.ts +25 -3
  199. package/src/session/internal/managed-session-storage.ts +574 -73
  200. package/src/session/internal/session-memory-sidecar.ts +19 -29
  201. package/src/session/session-manager.ts +2166 -743
  202. package/src/session/session-storage.ts +715 -48
  203. package/src/setup/provider-onboarding.ts +19 -3
  204. package/src/setup/provider-presets.json +3 -1
  205. package/src/slash-commands/builtin-registry.ts +85 -3
  206. package/src/stt/downloader.ts +2 -3
  207. package/src/stt/setup.ts +29 -4
  208. package/src/task/executor.ts +22 -18
  209. package/src/task/index.ts +24 -1
  210. package/src/tools/archive-reader.ts +11 -2
  211. package/src/tools/browser/runtime-diagnostics.ts +249 -0
  212. package/src/tools/browser/tab-protocol.ts +4 -0
  213. package/src/tools/browser/tab-supervisor.ts +5 -0
  214. package/src/tools/browser/tab-worker.ts +36 -1
  215. package/src/tools/browser.ts +7 -0
  216. package/src/tools/image-gen.ts +110 -166
  217. package/src/tools/index.ts +2 -0
  218. package/src/tools/read.ts +1 -1
  219. package/src/tools/resource-gc.ts +6 -1
  220. package/src/tools/tool-catalog.generated.ts +5 -1
  221. package/src/utils/pasted-image-path.ts +93 -0
  222. package/src/web/search/index.ts +3 -3
@@ -9,7 +9,7 @@ import type {
9
9
  RecoveryFsIdentity,
10
10
  RecoveryFsRoot,
11
11
  } from "@gajae-code/natives";
12
- import type { SessionStorageStat } from "../session-storage";
12
+ import type { SessionStorageRangeSnapshot, SessionStorageStat } from "../session-storage";
13
13
  import {
14
14
  classifyNativePublishOutcome,
15
15
  formatNativePublishDiagnostic,
@@ -42,6 +42,8 @@ export const MANAGED_ARTIFACT_MAX_DEPTH = 32;
42
42
  export const MANAGED_ARTIFACT_MAX_FILES = 50_000;
43
43
  export const MANAGED_ARTIFACT_MAX_FILE_BYTES = 64 * 1024 * 1024;
44
44
  export const MANAGED_ARTIFACT_MAX_TOTAL_BYTES = 512 * 1024 * 1024;
45
+ const REPLACEMENT_CLEANUP_RECEIPT_MAX_BYTES = 64 * 1024;
46
+ const REPLACEMENT_CLEANUP_RECEIPT_SCAN_LIMIT = MANAGED_ARTIFACT_MAX_FILES;
45
47
  export const MANAGED_ARTIFACT_COPY_BATCH_SIZE = 256;
46
48
  const LOCK_LEASE_MS = 60_000;
47
49
  const LOCK_HEARTBEAT_MS = 10_000;
@@ -88,6 +90,23 @@ export class ManagedReplaceError extends Error {
88
90
  }
89
91
  }
90
92
 
93
+ export class ManagedCommittedMutationError extends Error {
94
+ constructor(
95
+ readonly operation: "replace" | "append",
96
+ cause: unknown,
97
+ ) {
98
+ super(`managed_${operation}_committed_outcome_uncertain`, { cause });
99
+ this.name = "ManagedCommittedMutationError";
100
+ }
101
+ }
102
+
103
+ function managedAppendFailure(code: string | undefined): Error {
104
+ const error = new Error(code ?? "managed_append_failed");
105
+ return code === "content_too_large" || code === "too_large" || code === "header_patch_write_failed"
106
+ ? error
107
+ : new ManagedCommittedMutationError("append", error);
108
+ }
109
+
91
110
  function publishFailure(outcome: NativePublishOutcome): ManagedPublishError {
92
111
  const classification =
93
112
  outcome.reason === "destination_exists"
@@ -117,6 +136,10 @@ function exactUnlinkCompleted(result: NativeExactUnlinkResult): boolean {
117
136
  );
118
137
  }
119
138
 
139
+ function isRetryableReplacementReceiptCleanupError(error: unknown): boolean {
140
+ return error instanceof Error && error.message === "managed_replace_receipt_cleanup_pending:io_error";
141
+ }
142
+
120
143
  /** A same-filesystem rename updates the moved root's ctime but no other tree identity. */
121
144
  function sameDirectoryTreeSnapshotAfterRename(
122
145
  left: NativeDirectoryTreeSnapshot,
@@ -382,6 +405,39 @@ function replacementCleanupReceiptBinding(
382
405
  : undefined;
383
406
  }
384
407
 
408
+ function parseReplacementIdentity(value: unknown): ManagedFileSnapshot["identity"] | undefined {
409
+ const parsed = parseLegacyReplacementCleanupIdentity(value);
410
+ if (!parsed || parsed.size > BigInt(Number.MAX_SAFE_INTEGER)) return undefined;
411
+ return {
412
+ dev: parsed.dev,
413
+ ino: parsed.ino,
414
+ nlink: parsed.nlink,
415
+ size: Number(parsed.size),
416
+ mtimeNs: parsed.mtimeNs,
417
+ ctimeNs: parsed.ctimeNs,
418
+ sha256: parsed.sha256,
419
+ };
420
+ }
421
+
422
+ function parseReplacementCleanupReceipt(bytes: Uint8Array): ReplacementCleanupReceipt | undefined {
423
+ let value: unknown;
424
+ try {
425
+ value = JSON.parse(Buffer.from(bytes).toString("utf8")) as unknown;
426
+ } catch {
427
+ return undefined;
428
+ }
429
+ const receipt = asRecord(value);
430
+ if (
431
+ receipt?.version !== 3 ||
432
+ typeof receipt.staging !== "string" ||
433
+ typeof receipt.destination !== "string" ||
434
+ !parseReplacementIdentity(receipt.successor) ||
435
+ !parseReplacementIdentity(receipt.predecessor)
436
+ )
437
+ return undefined;
438
+ return receipt as ReplacementCleanupReceipt;
439
+ }
440
+
385
441
  function serializeReplacementIdentity(identity: ManagedFileSnapshot["identity"]): SerializedReplacementIdentity {
386
442
  return {
387
443
  dev: identity.dev.toString(),
@@ -420,6 +476,65 @@ function replacementReceiptPlaceholderRetirementName(
420
476
  return `.gjc-receipt-placeholder-remove-${placeholder.dev.toString(16)}-${placeholder.ino.toString(16)}-${predecessor.dev.toString(16)}-${predecessor.ino.toString(16)}-${receipt.dev.toString(16)}-${receipt.ino.toString(16)}`;
421
477
  }
422
478
 
479
+ /**
480
+ * Terminal write-protocol remnant names. The POSIX exact-unlink fallback cannot
481
+ * descriptor-unlink, so it detaches, scrubs, and retains zero-length quarantine
482
+ * entries and exchange placeholders instead of removing them (`cleanup_pending`
483
+ * with a durable payload). Linux completes deletion through the retained
484
+ * directory authority, but macOS has no retained authority, so every managed
485
+ * replacement leaks its scrubbed remnants and scope directories grow without
486
+ * bound (tens of thousands of dirents per workspace), degrading every
487
+ * per-mutation receipt scan and widening quarantine-collision windows.
488
+ */
489
+ const SCRUBBED_REMNANT_PREFIXES = [
490
+ ".gjc-exact-unlink-placeholder-",
491
+ ".gjc-exact-replace-destination-",
492
+ ".gjc-receipt-remove-",
493
+ ".gjc-receipt-placeholder-remove-",
494
+ ".gjc-replace-retry-",
495
+ ] as const;
496
+
497
+ /** In-flight protocol steps complete in milliseconds; anything older is abandoned. */
498
+ const SCRUBBED_REMNANT_MIN_AGE_MS = 15 * 60 * 1000;
499
+
500
+ /**
501
+ * Best-effort removal of scrubbed write-protocol remnants from one managed
502
+ * directory. Only zero-length, single-link, non-symlink regular files whose
503
+ * names carry a terminal remnant prefix and whose timestamps are older than
504
+ * the age gate are removed: a zero-length single-link entry is exactly what
505
+ * the scrub proof leaves behind, and non-zero entries (displaced predecessors
506
+ * or detached receipts retained as evidence) are never touched. Failures are
507
+ * swallowed — a concurrent same-owner writer may legitimately own or already
508
+ * have reconciled any candidate.
509
+ */
510
+ export function reapScrubbedProtocolRemnantsSync(
511
+ directory: string,
512
+ minAgeMs: number = SCRUBBED_REMNANT_MIN_AGE_MS,
513
+ ): number {
514
+ let names: string[];
515
+ try {
516
+ names = fs.readdirSync(directory);
517
+ } catch {
518
+ return 0;
519
+ }
520
+ const cutoff = Date.now() - minAgeMs;
521
+ let reaped = 0;
522
+ for (const name of names) {
523
+ if (!SCRUBBED_REMNANT_PREFIXES.some(prefix => name.startsWith(prefix))) continue;
524
+ const pathname = path.join(directory, name);
525
+ try {
526
+ const named = fs.lstatSync(pathname);
527
+ if (!named.isFile() || named.isSymbolicLink() || named.nlink !== 1 || named.size !== 0) continue;
528
+ if (named.mtimeMs > cutoff) continue;
529
+ fs.unlinkSync(pathname);
530
+ reaped += 1;
531
+ } catch {
532
+ // Best effort: concurrently reconciled or removed remnants are not errors.
533
+ }
534
+ }
535
+ return reaped;
536
+ }
537
+
423
538
  const ACL_FAILURE_CODES = new Set(["acl_denied", "acl_io_error", "acl_present", "acl_malformed", "acl_unknown"]);
424
539
  const ACL_CLEAR_EVIDENCE = new Set(["cleared", "already_absent", "unsupported", "not_run"]);
425
540
  const GENERAL_FAILURE_CODES = new Set([
@@ -948,7 +1063,12 @@ export class ManagedSessionDescendantStore {
948
1063
  this.#assertBound();
949
1064
  }
950
1065
  #assertBound(): void {
951
- if (!this.#authority) return;
1066
+ if (!this.#authority) {
1067
+ const named = fs.statSync(this.#baseDir, { bigint: true });
1068
+ if (!named.isDirectory() || named.dev !== this.#subtreeRoot.dev || named.ino !== this.#subtreeRoot.ino)
1069
+ throw new Error("Managed descendant root binding changed");
1070
+ return;
1071
+ }
952
1072
  const named = fs.lstatSync(this.#baseDir, { bigint: true });
953
1073
  const retained =
954
1074
  this.#authorityBaseDir === this.#baseDir
@@ -1025,7 +1145,7 @@ export class ManagedSessionDescendantStore {
1025
1145
  if (!binding) throw new Error("managed_replace_cleanup_receipt_invalid");
1026
1146
  let receipt: ManagedFileSnapshot;
1027
1147
  try {
1028
- receipt = captureManagedFileNoFollow(receiptPath);
1148
+ receipt = captureManagedFilePrefixNoFollow(receiptPath, REPLACEMENT_CLEANUP_RECEIPT_MAX_BYTES);
1029
1149
  } catch (error) {
1030
1150
  if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
1031
1151
  throw error;
@@ -1070,7 +1190,7 @@ export class ManagedSessionDescendantStore {
1070
1190
  if (!binding) throw new Error("managed_replace_cleanup_receipt_invalid");
1071
1191
  let receipt: ManagedFileSnapshot;
1072
1192
  try {
1073
- receipt = captureManagedFileNoFollow(receiptPath);
1193
+ receipt = captureManagedFilePrefixNoFollow(receiptPath, REPLACEMENT_CLEANUP_RECEIPT_MAX_BYTES);
1074
1194
  } catch (error) {
1075
1195
  if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
1076
1196
  throw error;
@@ -1105,7 +1225,7 @@ export class ManagedSessionDescendantStore {
1105
1225
 
1106
1226
  let currentReceipt: ManagedFileSnapshot;
1107
1227
  try {
1108
- currentReceipt = captureManagedFileNoFollow(receiptPath);
1228
+ currentReceipt = captureManagedFilePrefixNoFollow(receiptPath, REPLACEMENT_CLEANUP_RECEIPT_MAX_BYTES);
1109
1229
  } catch (error) {
1110
1230
  if ((error as NodeJS.ErrnoException).code === "ENOENT") {
1111
1231
  fsyncDirectory(this.#baseDir);
@@ -1134,21 +1254,82 @@ export class ManagedSessionDescendantStore {
1134
1254
  fsyncDirectory(this.#baseDir);
1135
1255
  }
1136
1256
 
1257
+ #reconcilePendingReplacementReceipt(receiptPath: string): void {
1258
+ const pending = captureManagedFilePrefixNoFollow(receiptPath, REPLACEMENT_CLEANUP_RECEIPT_MAX_BYTES);
1259
+ const parsed = parseReplacementCleanupReceipt(pending.bytes);
1260
+ const predecessor = parsed ? parseReplacementIdentity(parsed.predecessor) : undefined;
1261
+ if (
1262
+ !parsed ||
1263
+ !predecessor ||
1264
+ path.dirname(path.resolve(parsed.staging)) !== this.#baseDir ||
1265
+ path.dirname(path.resolve(parsed.destination)) !== this.#baseDir
1266
+ )
1267
+ throw new Error("managed_replace_cleanup_receipt_invalid");
1268
+ const destination = replacementReceiptPath(this.#baseDir, predecessor, pending.identity);
1269
+ const outcome = classifyNativePublishOutcome(
1270
+ nativeSessionStorage().renameNoReplacePath(receiptPath, destination),
1271
+ );
1272
+ if (outcome.ok) {
1273
+ fsyncDirectory(this.#baseDir);
1274
+ return;
1275
+ }
1276
+ if (outcome.reason === "destination_exists") {
1277
+ const existing = captureManagedFilePrefixNoFollow(destination, REPLACEMENT_CLEANUP_RECEIPT_MAX_BYTES);
1278
+ if (existing.identity.sha256 !== pending.identity.sha256)
1279
+ throw new Error("managed_replace_cleanup_receipt_invalid");
1280
+ const removed = nativeSessionStorage().exactUnlink(receiptPath, {
1281
+ dev: pending.identity.dev,
1282
+ ino: pending.identity.ino,
1283
+ nlink: pending.identity.nlink,
1284
+ parentDev: this.#subtreeRoot.dev,
1285
+ parentIno: this.#subtreeRoot.ino,
1286
+ size: BigInt(pending.identity.size),
1287
+ mtimeNs: pending.identity.mtimeNs,
1288
+ sha256: pending.identity.sha256,
1289
+ quarantineName: `.gjc-receipt-pending-remove-${pending.identity.dev.toString(16)}-${pending.identity.ino.toString(16)}`,
1290
+ });
1291
+ if (!exactUnlinkCompleted(removed) && removed.code !== "not_found")
1292
+ throw new Error(`managed_replace_receipt_cleanup_pending:${removed.code ?? "unknown"}`);
1293
+ fsyncDirectory(this.#baseDir);
1294
+ return;
1295
+ }
1296
+ throw publishFailure(outcome);
1297
+ }
1298
+
1137
1299
  #reconcileReplacementCleanupReceipts(): void {
1138
1300
  if (this.#reconcilingReplacementCleanup) return;
1139
1301
  this.#reconcilingReplacementCleanup = true;
1140
1302
  try {
1141
- for (const name of fs.readdirSync(this.#baseDir)) {
1142
- if (!name.startsWith(".gjc-replace-cleanup-")) continue;
1143
- if (replacementCleanupReceiptBinding(name)) {
1144
- this.#detachReplacementCleanupReceipt(path.join(this.#baseDir, name));
1145
- continue;
1146
- }
1147
- if (legacyReplacementCleanupReceiptBinding(name)) {
1148
- this.#reconcileLegacyReplacementCleanupReceipt(path.join(this.#baseDir, name), name);
1149
- continue;
1303
+ const directory = fs.opendirSync(this.#baseDir);
1304
+ try {
1305
+ let examined = 0;
1306
+ for (let entry = directory.readSync(); entry; entry = directory.readSync()) {
1307
+ examined++;
1308
+ if (examined > REPLACEMENT_CLEANUP_RECEIPT_SCAN_LIMIT)
1309
+ throw new Error("managed_replace_cleanup_receipt_limit_exceeded");
1310
+ const name = entry.name;
1311
+ if (!name.startsWith(".gjc-replace-receipt-pending-") && !name.startsWith(".gjc-replace-cleanup-"))
1312
+ continue;
1313
+ if (name.startsWith(".gjc-replace-receipt-pending-")) {
1314
+ this.#reconcilePendingReplacementReceipt(path.join(this.#baseDir, name));
1315
+ continue;
1316
+ }
1317
+ if (replacementCleanupReceiptBinding(name)) {
1318
+ try {
1319
+ this.#detachReplacementCleanupReceipt(path.join(this.#baseDir, name));
1320
+ } catch (error) {
1321
+ if (!isRetryableReplacementReceiptCleanupError(error)) throw error;
1322
+ }
1323
+ continue;
1324
+ }
1325
+ if (legacyReplacementCleanupReceiptBinding(name)) {
1326
+ this.#reconcileLegacyReplacementCleanupReceipt(path.join(this.#baseDir, name), name);
1327
+ continue;
1328
+ }
1329
+ throw new Error("managed_replace_cleanup_receipt_invalid");
1150
1330
  }
1151
- throw new Error("managed_replace_cleanup_receipt_invalid");
1331
+ } finally {
1332
+ directory.closeSync();
1152
1333
  }
1153
1334
  } finally {
1154
1335
  this.#reconcilingReplacementCleanup = false;
@@ -1271,18 +1452,20 @@ export class ManagedSessionDescendantStore {
1271
1452
 
1272
1453
  captureBoundedAppendExpectation(relativePath: string): ManagedBoundedAppendExpectation | undefined {
1273
1454
  this.#assertBound();
1274
- if (!this.#authority) return undefined;
1275
- const observed = this.#authority.stat(this.#relative(this.#resolve(relativePath)));
1276
- if (!observed.ok || !observed.identity) return undefined;
1277
- const sha256 = observed.identity.sha256;
1278
- if (typeof sha256 !== "string" || !/^[0-9a-f]{64}$/i.test(sha256)) return undefined;
1455
+ let identity: ManagedFileSnapshot["identity"];
1456
+ try {
1457
+ identity = captureManagedFileIdentityStreamingNoFollow(this.#resolve(relativePath));
1458
+ } catch (error) {
1459
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
1460
+ throw error;
1461
+ }
1279
1462
  return {
1280
- dev: observed.identity.dev,
1281
- ino: observed.identity.ino,
1282
- size: observed.identity.size,
1283
- mtimeNs: observed.identity.mtimeNs,
1284
- ctimeNs: observed.identity.ctimeNs,
1285
- sha256: sha256.toLowerCase(),
1463
+ dev: identity.dev.toString(),
1464
+ ino: identity.ino.toString(),
1465
+ size: identity.size.toString(),
1466
+ mtimeNs: identity.mtimeNs.toString(),
1467
+ ctimeNs: identity.ctimeNs.toString(),
1468
+ sha256: identity.sha256,
1286
1469
  };
1287
1470
  }
1288
1471
 
@@ -1293,7 +1476,26 @@ export class ManagedSessionDescendantStore {
1293
1476
  ): ManagedAppendReceipt {
1294
1477
  this.#beforeMutation();
1295
1478
  this.#assertBound();
1296
- if (!this.#authority) throw new Error("managed_bounded_append_unavailable");
1479
+ if (!this.#authority) {
1480
+ const current = captureManagedFileIdentityStreamingNoFollow(this.#resolve(relativePath));
1481
+ if (
1482
+ current.dev.toString() !== expected.dev ||
1483
+ current.ino.toString() !== expected.ino ||
1484
+ current.size.toString() !== expected.size ||
1485
+ current.mtimeNs.toString() !== expected.mtimeNs ||
1486
+ current.ctimeNs.toString() !== expected.ctimeNs ||
1487
+ current.sha256 !== expected.sha256
1488
+ )
1489
+ throw new Error("managed_append_identity_mismatch");
1490
+ const appended = appendManagedFileStreamingSync(
1491
+ this.#resolve(relativePath),
1492
+ bytes,
1493
+ this.#subtreeRoot,
1494
+ this.#policy,
1495
+ );
1496
+ this.#assertBound();
1497
+ return managedAppendReceiptFromIdentity(appended);
1498
+ }
1297
1499
  const relative = this.#relative(this.#resolve(relativePath));
1298
1500
  const appended = this.#authority.appendManaged(
1299
1501
  relative,
@@ -1305,11 +1507,11 @@ export class ManagedSessionDescendantStore {
1305
1507
  expected.ctimeNs,
1306
1508
  expected.sha256,
1307
1509
  );
1308
- if (!appended.ok) throw new Error(appended.code ?? "managed_append_failed");
1309
- const stat = this.#authority.stat(relative);
1310
- if (!stat.ok || !stat.identity) throw new Error(stat.code ?? "managed_append_identity_unavailable");
1510
+ if (!appended.ok) throw managedAppendFailure(appended.code);
1511
+ if (!appended.identity)
1512
+ throw new ManagedCommittedMutationError("append", new Error("managed_append_identity_unavailable"));
1311
1513
  this.#assertBound();
1312
- return managedAppendReceiptFromIdentity(managedFileIdentityFromNative(stat.identity));
1514
+ return managedAppendReceiptFromIdentity(managedFileIdentityFromNative(appended.identity));
1313
1515
  }
1314
1516
 
1315
1517
  appendSync(relativePath: string, bytes: Uint8Array): ManagedAppendReceipt {
@@ -1336,12 +1538,10 @@ export class ManagedSessionDescendantStore {
1336
1538
  observed.identity.ctimeNs,
1337
1539
  expectedSha256,
1338
1540
  );
1339
- if (!appended.ok) throw new Error(appended.code ?? "managed_append_failed");
1340
- // Post-fsync descriptor capture through retained authority: the native append
1341
- // is synchronous and durable, so the stat reflects the committed object.
1342
- const stat = this.#authority.stat(relative);
1343
- if (!stat.ok || !stat.identity) throw new Error(stat.code ?? "managed_append_identity_unavailable");
1344
- const receipt = managedAppendReceiptFromIdentity(managedFileIdentityFromNative(stat.identity));
1541
+ if (!appended.ok) throw managedAppendFailure(appended.code);
1542
+ if (!appended.identity)
1543
+ throw new ManagedCommittedMutationError("append", new Error("managed_append_identity_unavailable"));
1544
+ const receipt = managedAppendReceiptFromIdentity(managedFileIdentityFromNative(appended.identity));
1345
1545
  this.#assertBound();
1346
1546
  return receipt;
1347
1547
  }
@@ -1355,6 +1555,30 @@ export class ManagedSessionDescendantStore {
1355
1555
  return receipt;
1356
1556
  }
1357
1557
 
1558
+ #assertPathBackedReadRelative(relativePath: string): void {
1559
+ if (!this.#authority && relativePath.split(/[\\/]/).length > 1)
1560
+ throw new Error("managed_nested_path_unsupported");
1561
+ }
1562
+
1563
+ /**
1564
+ * Without retained root authority a nested read resolves through intermediate
1565
+ * directories that `O_NOFOLLOW` does not cover, so each component between the
1566
+ * bound base directory and the target is verified as a real same-device
1567
+ * directory before the capture opens the leaf.
1568
+ */
1569
+ #assertPathBackedDirectoryChain(resolved: string): void {
1570
+ if (this.#authority) return;
1571
+ const relative = path.relative(this.#baseDir, resolved);
1572
+ const components = relative.split(path.sep);
1573
+ let current = this.#baseDir;
1574
+ for (const component of components.slice(0, -1)) {
1575
+ current = path.join(current, component);
1576
+ const stat = fs.lstatSync(current, { bigint: true });
1577
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.dev !== this.#subtreeRoot.dev)
1578
+ throw new Error("Managed descendant path escapes retained store");
1579
+ }
1580
+ }
1581
+
1358
1582
  #relative(resolved: string): string {
1359
1583
  return path.relative(this.#authorityBaseDir, resolved).split(path.sep).join("/");
1360
1584
  }
@@ -1463,11 +1687,59 @@ export class ManagedSessionDescendantStore {
1463
1687
 
1464
1688
  /** Capture descriptor identity without copying file bytes when retained authority is available. */
1465
1689
  descriptorExpected(relativePath: string): SessionStorageStat | null {
1690
+ this.#assertPathBackedReadRelative(relativePath);
1466
1691
  this.#assertBound();
1467
1692
  const resolved = this.#resolve(relativePath);
1468
1693
  if (!this.#authority) {
1469
- const snapshot = this.readExpected(relativePath);
1470
- return snapshot ? managedAppendReceiptFromIdentity(snapshot.identity).descriptor : null;
1694
+ const rootBefore = fs.lstatSync(this.#baseDir, { bigint: true });
1695
+ if (
1696
+ !rootBefore.isDirectory() ||
1697
+ rootBefore.isSymbolicLink() ||
1698
+ rootBefore.dev !== this.#subtreeRoot.dev ||
1699
+ rootBefore.ino !== this.#subtreeRoot.ino
1700
+ )
1701
+ throw new Error("Managed descendant root binding changed");
1702
+ let fd: number | undefined;
1703
+ try {
1704
+ fd = fs.openSync(
1705
+ resolved,
1706
+ fs.constants.O_RDONLY | fs.constants.O_NONBLOCK | (fs.constants.O_NOFOLLOW ?? 0),
1707
+ );
1708
+ const opened = fs.fstatSync(fd, { bigint: true });
1709
+ const named = fs.lstatSync(resolved, { bigint: true });
1710
+ if (
1711
+ !opened.isFile() ||
1712
+ opened.nlink > 1n ||
1713
+ !named.isFile() ||
1714
+ named.isSymbolicLink() ||
1715
+ named.dev !== opened.dev ||
1716
+ named.ino !== opened.ino ||
1717
+ named.nlink !== opened.nlink
1718
+ )
1719
+ throw new Error("source_changed");
1720
+ const rootAfter = fs.lstatSync(this.#baseDir, { bigint: true });
1721
+ if (
1722
+ !rootAfter.isDirectory() ||
1723
+ rootAfter.isSymbolicLink() ||
1724
+ rootAfter.dev !== rootBefore.dev ||
1725
+ rootAfter.ino !== rootBefore.ino
1726
+ )
1727
+ throw new Error("Managed descendant root binding changed");
1728
+ this.#assertBound();
1729
+ return managedAppendReceiptFromIdentity({
1730
+ dev: opened.dev,
1731
+ ino: opened.ino,
1732
+ nlink: opened.nlink,
1733
+ size: Number(opened.size),
1734
+ mtimeNs: opened.mtimeNs,
1735
+ ctimeNs: opened.ctimeNs,
1736
+ }).descriptor;
1737
+ } catch (error) {
1738
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
1739
+ throw error;
1740
+ } finally {
1741
+ if (fd !== undefined) fs.closeSync(fd);
1742
+ }
1471
1743
  }
1472
1744
  const stat = this.#authority.stat(this.#relative(resolved));
1473
1745
  if (!stat.ok) {
@@ -1478,13 +1750,132 @@ export class ManagedSessionDescendantStore {
1478
1750
  return managedAppendReceiptFromIdentity(managedFileIdentityFromNative(stat.identity)).descriptor;
1479
1751
  }
1480
1752
 
1753
+ /**
1754
+ * Read one bounded range through a no-follow descriptor while binding the
1755
+ * opened file to retained authority (when available) and the managed root
1756
+ * before and after the read. This avoids whole-transcript capture for cold
1757
+ * managed-session indexing without weakening pathname authority.
1758
+ */
1759
+ readRangeExpectedSync(
1760
+ relativePath: string,
1761
+ start: number,
1762
+ length: number,
1763
+ expectedDescriptor?: Pick<SessionStorageStat, "dev" | "ino" | "nlink" | "size" | "mtimeNs" | "ctimeNs">,
1764
+ ): SessionStorageRangeSnapshot {
1765
+ if (!Number.isSafeInteger(start) || start < 0 || !Number.isSafeInteger(length) || length < 0)
1766
+ throw new RangeError("Invalid managed range read");
1767
+ if (start > Number.MAX_SAFE_INTEGER - length) throw new RangeError("Managed range read start overflows");
1768
+ if (length > 64 * 1024 * 1024) throw new RangeError("Managed range read exceeds the bounded maximum");
1769
+ this.#assertPathBackedReadRelative(relativePath);
1770
+ this.#assertBound();
1771
+ const rootBefore = fs.lstatSync(this.#baseDir, { bigint: true });
1772
+ if (
1773
+ !rootBefore.isDirectory() ||
1774
+ rootBefore.isSymbolicLink() ||
1775
+ rootBefore.dev !== this.#subtreeRoot.dev ||
1776
+ rootBefore.ino !== this.#subtreeRoot.ino
1777
+ )
1778
+ throw new Error("Managed descendant root binding changed");
1779
+ const resolved = this.#resolve(relativePath);
1780
+ const relative = this.#relative(resolved);
1781
+ const retained = this.#authority?.stat(relative);
1782
+ if (retained && !retained.ok) {
1783
+ if (retained.code === "not_found")
1784
+ throw Object.assign(new Error("Managed file not found"), { code: "ENOENT" });
1785
+ throw new Error(retained.code ?? "managed_stat_failed");
1786
+ }
1787
+ if (retained && !retained.identity) throw new Error("managed_stat_identity_unavailable");
1788
+ const fd = fs.openSync(
1789
+ resolved,
1790
+ fs.constants.O_RDONLY | fs.constants.O_NONBLOCK | (fs.constants.O_NOFOLLOW ?? 0),
1791
+ );
1792
+ try {
1793
+ const before = fs.fstatSync(fd, { bigint: true });
1794
+ if (!before.isFile() || before.nlink > 1n) throw new Error("source_changed");
1795
+ if (retained?.identity) {
1796
+ const expected = retained.identity;
1797
+ if (
1798
+ before.dev.toString() !== expected.dev ||
1799
+ before.ino.toString() !== expected.ino ||
1800
+ before.nlink.toString() !== expected.nlink ||
1801
+ before.size.toString() !== expected.size ||
1802
+ before.mtimeNs.toString() !== expected.mtimeNs ||
1803
+ before.ctimeNs.toString() !== expected.ctimeNs
1804
+ )
1805
+ throw new Error("source_changed");
1806
+ }
1807
+ if (
1808
+ expectedDescriptor &&
1809
+ (before.dev !== expectedDescriptor.dev ||
1810
+ before.ino !== expectedDescriptor.ino ||
1811
+ before.nlink !== (expectedDescriptor.nlink ?? before.nlink) ||
1812
+ Number(before.size) !== expectedDescriptor.size ||
1813
+ before.mtimeNs !== expectedDescriptor.mtimeNs ||
1814
+ before.ctimeNs !== expectedDescriptor.ctimeNs)
1815
+ )
1816
+ throw new Error("managed_range_generation_mismatch");
1817
+ if (Number(before.size) < start + length) throw new Error("range_not_present");
1818
+ const bytes = Buffer.alloc(length);
1819
+ let offset = 0;
1820
+ while (offset < length) {
1821
+ const count = fs.readSync(fd, bytes, offset, length - offset, start + offset);
1822
+ if (count === 0) throw new Error("range_not_present");
1823
+ offset += count;
1824
+ }
1825
+ const after = fs.fstatSync(fd, { bigint: true });
1826
+ const named = fs.lstatSync(resolved, { bigint: true });
1827
+ if (
1828
+ after.dev !== before.dev ||
1829
+ after.ino !== before.ino ||
1830
+ after.nlink !== before.nlink ||
1831
+ after.size !== before.size ||
1832
+ after.mtimeNs !== before.mtimeNs ||
1833
+ after.ctimeNs !== before.ctimeNs ||
1834
+ !named.isFile() ||
1835
+ named.isSymbolicLink() ||
1836
+ named.dev !== before.dev ||
1837
+ named.ino !== before.ino ||
1838
+ named.nlink !== before.nlink
1839
+ )
1840
+ throw new Error("source_changed");
1841
+ const rootAfter = fs.lstatSync(this.#baseDir, { bigint: true });
1842
+ if (
1843
+ !rootAfter.isDirectory() ||
1844
+ rootAfter.isSymbolicLink() ||
1845
+ rootAfter.dev !== rootBefore.dev ||
1846
+ rootAfter.ino !== rootBefore.ino
1847
+ )
1848
+ throw new Error("Managed descendant root binding changed");
1849
+ this.#assertBound();
1850
+ return {
1851
+ bytes,
1852
+ stat: {
1853
+ dev: after.dev,
1854
+ ino: after.ino,
1855
+ nlink: after.nlink,
1856
+ size: Number(after.size),
1857
+ mtimeNs: after.mtimeNs,
1858
+ ctimeNs: after.ctimeNs,
1859
+ mtimeMs: Number(after.mtimeNs) / 1_000_000,
1860
+ mtime: new Date(Number(after.mtimeNs) / 1_000_000),
1861
+ isFile: true,
1862
+ },
1863
+ };
1864
+ } finally {
1865
+ fs.closeSync(fd);
1866
+ }
1867
+ }
1481
1868
  /** Read an exact managed file without exposing its pathname as authority. */
1482
1869
  readExpected(relativePath: string): ManagedFileSnapshot | null {
1483
1870
  this.#assertBound();
1484
- const relative = this.#relative(this.#resolve(relativePath));
1871
+ const resolved = this.#resolve(relativePath);
1872
+ const relative = this.#relative(resolved);
1485
1873
  if (!this.#authority) {
1486
1874
  try {
1487
- return captureManagedFileNoFollow(this.#resolve(relativePath));
1875
+ this.#assertPathBackedDirectoryChain(resolved);
1876
+ const captured = captureManagedFileNoFollow(resolved);
1877
+ this.#assertBound();
1878
+ return captured;
1488
1879
  } catch (error) {
1489
1880
  if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
1490
1881
  throw error;
@@ -1515,12 +1906,15 @@ export class ManagedSessionDescendantStore {
1515
1906
  this.#beforeMutation();
1516
1907
  this.#assertBound();
1517
1908
  if (!this.#authority) {
1909
+ const parent = fs.statSync(path.dirname(this.#resolve(relativePath)), { bigint: true });
1518
1910
  const removed = nativeSessionStorage().exactUnlink(this.#resolve(relativePath), {
1519
1911
  dev: expected.identity.dev,
1520
1912
  ino: expected.identity.ino,
1521
1913
  size: BigInt(expected.identity.size),
1522
1914
  mtimeNs: expected.identity.mtimeNs,
1523
1915
  sha256: expected.identity.sha256,
1916
+ parentDev: parent.dev,
1917
+ parentIno: parent.ino,
1524
1918
  quarantineName: `.gjc-remove-${process.pid}-${randomUUID()}`,
1525
1919
  });
1526
1920
  if (
@@ -1550,6 +1944,31 @@ export class ManagedSessionDescendantStore {
1550
1944
  throw new Error(removed.code ?? "managed_remove_failed");
1551
1945
  this.#assertBound();
1552
1946
  }
1947
+
1948
+ /** Remove one exact file without allocating its contents. */
1949
+ removeIfExistsDescriptor(relativePath: string): boolean {
1950
+ this.#beforeMutation();
1951
+ this.#assertBound();
1952
+ const resolved = this.#resolve(relativePath);
1953
+ let named: fs.BigIntStats;
1954
+ try {
1955
+ named = fs.lstatSync(resolved, { bigint: true });
1956
+ } catch (error) {
1957
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
1958
+ throw error;
1959
+ }
1960
+ if (!named.isFile() || named.isSymbolicLink()) throw new Error("managed_remove_failed");
1961
+ let identity: ManagedFileSnapshot["identity"];
1962
+ try {
1963
+ identity = captureManagedFileIdentityStreamingNoFollow(resolved);
1964
+ } catch (error) {
1965
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
1966
+ throw error;
1967
+ }
1968
+ this.#assertBound();
1969
+ this.removeExpected(relativePath, { bytes: Buffer.alloc(0), identity });
1970
+ return true;
1971
+ }
1553
1972
  /** Read and remove one managed descendant through retained authority. */
1554
1973
  async consume(relativePath: string): Promise<Uint8Array | null> {
1555
1974
  this.#beforeMutation();
@@ -1598,14 +2017,28 @@ export class ManagedSessionDescendantStore {
1598
2017
  /** Capture a complete descendant tree through this retained root. */
1599
2018
  captureTree(relativePath: string): NativeDirectoryTreeSnapshot {
1600
2019
  this.#assertBound();
2020
+ const resolved = this.#resolve(relativePath);
2021
+ try {
2022
+ fs.lstatSync(resolved);
2023
+ } catch (error) {
2024
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") throw new Error("not_found");
2025
+ throw error;
2026
+ }
2027
+ validateManagedArtifactTree(resolved);
1601
2028
  const relative = this.#relative(this.#resolve(relativePath));
1602
2029
  if (this.#authority) {
1603
2030
  const captured = this.#authority.snapshotManagedTree(relative);
1604
- if (!captured.ok || !captured.snapshot) throw new Error(captured.code ?? "unsafe_artifacts");
2031
+ if (!captured.ok || !captured.snapshot)
2032
+ throw new Error(
2033
+ captured.code === "not_found" ? "artifact_source_changed" : (captured.code ?? "unsafe_artifacts"),
2034
+ );
1605
2035
  return captured.snapshot;
1606
2036
  }
1607
2037
  const captured = nativeSessionStorage().snapshotDirectoryTree(this.#resolve(relativePath));
1608
- if (!captured.ok || !captured.snapshot) throw new Error(captured.code ?? "unsafe_artifacts");
2038
+ if (!captured.ok || !captured.snapshot)
2039
+ throw new Error(
2040
+ captured.code === "not_found" ? "artifact_source_changed" : (captured.code ?? "unsafe_artifacts"),
2041
+ );
1609
2042
  return captured.snapshot;
1610
2043
  }
1611
2044
 
@@ -1861,7 +2294,8 @@ function sameReplacementIdentity(
1861
2294
  left.ino === right.ino &&
1862
2295
  left.nlink === right.nlink &&
1863
2296
  left.size === right.size &&
1864
- left.mtimeNs === right.mtimeNs
2297
+ left.mtimeNs === right.mtimeNs &&
2298
+ left.sha256 === right.sha256
1865
2299
  );
1866
2300
  }
1867
2301
 
@@ -2011,7 +2445,7 @@ export function captureManagedFileNoFollow(pathname: string): ManagedFileSnapsho
2011
2445
  }
2012
2446
 
2013
2447
  function captureManagedFileNoFollowLimit(pathname: string, maxBytes?: number): ManagedFileSnapshot {
2014
- const fd = fs.openSync(pathname, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
2448
+ const fd = fs.openSync(pathname, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK | (fs.constants.O_NOFOLLOW ?? 0));
2015
2449
  try {
2016
2450
  const before = fs.fstatSync(fd, { bigint: true });
2017
2451
  if (!before.isFile() || before.nlink > 1) throw new Error("source_changed");
@@ -2037,7 +2471,7 @@ function captureManagedFileNoFollowLimit(pathname: string, maxBytes?: number): M
2037
2471
  /** Streams a managed file once while retaining only a bounded header prefix and the full descriptor-bound digest. */
2038
2472
  export function inspectManagedFileNoFollow(pathname: string, prefixLimit: number): ManagedFileSnapshot {
2039
2473
  if (!Number.isSafeInteger(prefixLimit) || prefixLimit < 0) throw new Error("invalid_capture_limit");
2040
- const fd = fs.openSync(pathname, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
2474
+ const fd = fs.openSync(pathname, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK | (fs.constants.O_NOFOLLOW ?? 0));
2041
2475
  try {
2042
2476
  const before = fs.fstatSync(fd, { bigint: true });
2043
2477
  if (!before.isFile() || before.nlink > 1) throw new Error("source_changed");
@@ -2068,7 +2502,7 @@ export function inspectManagedFileNoFollow(pathname: string, prefixLimit: number
2068
2502
  }
2069
2503
 
2070
2504
  function captureManagedFileIdentityStreamingNoFollow(pathname: string): ManagedFileSnapshot["identity"] {
2071
- const fd = fs.openSync(pathname, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
2505
+ const fd = fs.openSync(pathname, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK | (fs.constants.O_NOFOLLOW ?? 0));
2072
2506
  try {
2073
2507
  const before = fs.fstatSync(fd, { bigint: true });
2074
2508
  if (!before.isFile() || before.nlink > 1) throw new Error("source_changed");
@@ -2098,6 +2532,7 @@ export async function publishManagedFileNoReplace(
2098
2532
  root?: ManagedDirectoryRoot,
2099
2533
  policy: ManagedSessionSecurityPolicy = "default",
2100
2534
  ): Promise<void> {
2535
+ if (bytes.byteLength > MANAGED_ARTIFACT_MAX_FILE_BYTES) throw new Error("content_too_large");
2101
2536
  const parent = path.dirname(destination);
2102
2537
  ensureManagedDirectory(parent, root, policy);
2103
2538
  const staging = path.join(parent, `.${path.basename(destination)}.${randomUUID()}.staging`);
@@ -2179,6 +2614,7 @@ export function publishManagedFileNoReplaceSync(
2179
2614
  root?: ManagedDirectoryRoot,
2180
2615
  policy: ManagedSessionSecurityPolicy = "default",
2181
2616
  ): ManagedFileSnapshot["identity"] {
2617
+ if (bytes.byteLength > MANAGED_ARTIFACT_MAX_FILE_BYTES) throw new Error("content_too_large");
2182
2618
  const parent = path.dirname(destination);
2183
2619
  ensureManagedDirectory(parent, root, policy);
2184
2620
  const staging = path.join(parent, `.${path.basename(destination)}.${randomUUID()}.staging`);
@@ -2250,7 +2686,9 @@ function replaceManagedFileGeneratedSync(
2250
2686
  policy: ManagedSessionSecurityPolicy = "default",
2251
2687
  assertFence?: () => void,
2252
2688
  expectedDestination?: ManagedFileSnapshot["identity"],
2253
- ): void {
2689
+ acceptCommittedCleanupFailure = false,
2690
+ operation: "replace" | "append" = "replace",
2691
+ ): ManagedFileSnapshot["identity"] {
2254
2692
  const parent = path.dirname(destination);
2255
2693
  ensureManagedDirectory(parent, root, policy);
2256
2694
  const staging = path.join(parent, `.${path.basename(destination)}.${randomUUID()}.replacement`);
@@ -2267,6 +2705,10 @@ function replaceManagedFileGeneratedSync(
2267
2705
  }
2268
2706
  | undefined;
2269
2707
  let failure: unknown;
2708
+ let publishedIdentity: ManagedFileSnapshot["identity"] | undefined;
2709
+ let publicationDurable = false;
2710
+ let publicationCommitted = false;
2711
+ let expectedSuccessor: ManagedFileSnapshot["identity"] | undefined;
2270
2712
  try {
2271
2713
  fd = fs.openSync(
2272
2714
  staging,
@@ -2274,11 +2716,14 @@ function replaceManagedFileGeneratedSync(
2274
2716
  0o600,
2275
2717
  );
2276
2718
  secureFileDescriptor(staging, fd, "apply");
2719
+ const initiallyStaged = fs.fstatSync(fd, { bigint: true });
2720
+ stagedIdentity = { dev: initiallyStaged.dev, ino: initiallyStaged.ino };
2277
2721
  const generated = writeContent(fd);
2278
2722
  fs.fsyncSync(fd);
2279
2723
  secureFileDescriptor(staging, fd, "verify");
2280
2724
  const staged = fs.fstatSync(fd, { bigint: true });
2281
2725
  stagedIdentity = { dev: staged.dev, ino: staged.ino };
2726
+ expectedSuccessor = identity(staged, generated.sha256);
2282
2727
  if (process.platform === "win32" && expectedDestination) {
2283
2728
  fs.closeSync(fd);
2284
2729
  fd = undefined;
@@ -2288,7 +2733,7 @@ function replaceManagedFileGeneratedSync(
2288
2733
  if (expectedDestination) {
2289
2734
  const parentIdentity = fs.lstatSync(parent, { bigint: true });
2290
2735
  if (Number(staged.size) !== generated.size) throw new Error("managed_replace_generated_size_changed");
2291
- const successor = identity(staged, generated.sha256);
2736
+ const successor = expectedSuccessor;
2292
2737
  const receiptStagingPath = path.join(parent, `.gjc-replace-receipt-pending-${randomUUID()}.json`);
2293
2738
  preserveStaging = true;
2294
2739
  const publishedReceiptIdentity = publishManagedFileNoReplaceSync(
@@ -2310,7 +2755,7 @@ function replaceManagedFileGeneratedSync(
2310
2755
  nativeSessionStorage().renameNoReplacePath(receiptStagingPath, receiptPath),
2311
2756
  );
2312
2757
  if (!receiptPublish.ok) throw publishFailure(receiptPublish);
2313
- const namedReceipt = captureManagedFileNoFollow(receiptPath);
2758
+ const namedReceipt = captureManagedFilePrefixNoFollow(receiptPath, REPLACEMENT_CLEANUP_RECEIPT_MAX_BYTES);
2314
2759
  if (
2315
2760
  namedReceipt.identity.dev !== publishedReceiptIdentity.dev ||
2316
2761
  namedReceipt.identity.ino !== publishedReceiptIdentity.ino
@@ -2348,23 +2793,41 @@ function replaceManagedFileGeneratedSync(
2348
2793
  sha256: expectedDestination.sha256,
2349
2794
  },
2350
2795
  );
2351
- if (!replaced.ok) throw new ManagedReplaceError(replaced, receiptCleanup.path);
2352
- const named = captureManagedFileIdentityStreamingNoFollow(destination);
2353
- if (!sameReplacementIdentity(named, successor) || named.sha256 !== successor.sha256)
2354
- throw new Error("destination_identity_changed");
2796
+ if (!replaced.ok) {
2797
+ publicationCommitted =
2798
+ replaced.code === "cleanup_pending" ||
2799
+ replaced.code === "durability_failed" ||
2800
+ Boolean(
2801
+ replaced.detachedPath ??
2802
+ replaced.retainedSuccessorPath ??
2803
+ replaced.retainedPlaceholderPath ??
2804
+ replaced.retainedUnknownPath,
2805
+ );
2806
+ throw new ManagedReplaceError(replaced, receiptCleanup.path);
2807
+ }
2808
+ publicationCommitted = true;
2355
2809
  fsyncDirectory(parent);
2356
- } else fs.renameSync(staging, destination);
2810
+ } else {
2811
+ fs.renameSync(staging, destination);
2812
+ publicationCommitted = true;
2813
+ }
2357
2814
  assertManagedDirectoryRoot(root);
2358
2815
  const named = fs.lstatSync(destination, { bigint: true });
2359
2816
  if (!named.isFile() || named.isSymbolicLink() || named.dev !== staged.dev || named.ino !== staged.ino) {
2360
2817
  throw new Error("destination_identity_changed");
2361
2818
  }
2819
+ if (!expectedSuccessor) throw new Error("managed_replace_identity_unavailable");
2362
2820
  if (fd !== undefined) {
2363
2821
  secureFileDescriptor(destination, fd, "verify");
2364
2822
  fs.closeSync(fd);
2365
2823
  fd = undefined;
2366
2824
  }
2367
2825
  fsyncDirectory(parent);
2826
+ const verifiedIdentity = captureManagedFileIdentityStreamingNoFollow(destination);
2827
+ if (!sameReplacementIdentity(verifiedIdentity, expectedSuccessor))
2828
+ throw new Error("destination_identity_changed");
2829
+ publishedIdentity = verifiedIdentity;
2830
+ publicationDurable = true;
2368
2831
  if (receiptCleanup) {
2369
2832
  try {
2370
2833
  const removed = nativeSessionStorage().exactUnlink(receiptCleanup.path, {
@@ -2394,9 +2857,27 @@ function replaceManagedFileGeneratedSync(
2394
2857
  }
2395
2858
  }
2396
2859
  } catch (error) {
2397
- failure = error;
2860
+ failure =
2861
+ publicationCommitted && !publishedIdentity ? new ManagedCommittedMutationError(operation, error) : error;
2398
2862
  } finally {
2399
- if (fd !== undefined) fs.closeSync(fd);
2863
+ if (fd !== undefined) {
2864
+ try {
2865
+ fs.closeSync(fd);
2866
+ } catch (error) {
2867
+ if (publicationCommitted && !publishedIdentity) {
2868
+ const cause =
2869
+ failure === undefined
2870
+ ? error
2871
+ : new AggregateError([failure, error], "Managed replacement and descriptor close both failed.");
2872
+ failure = new ManagedCommittedMutationError(operation, cause);
2873
+ } else {
2874
+ failure =
2875
+ failure === undefined
2876
+ ? error
2877
+ : new AggregateError([failure, error], "Managed replacement and descriptor close both failed.");
2878
+ }
2879
+ }
2880
+ }
2400
2881
  if (stagedIdentity && !preserveStaging) {
2401
2882
  try {
2402
2883
  const named = fs.lstatSync(staging, { bigint: true });
@@ -2411,7 +2892,10 @@ function replaceManagedFileGeneratedSync(
2411
2892
  }
2412
2893
  }
2413
2894
  }
2414
- if (failure !== undefined) throw failure;
2895
+ if (failure !== undefined && !(acceptCommittedCleanupFailure && publicationDurable && publishedIdentity))
2896
+ throw failure;
2897
+ if (!publishedIdentity) throw new Error("managed_replace_identity_unavailable");
2898
+ return publishedIdentity;
2415
2899
  }
2416
2900
 
2417
2901
  export function replaceManagedFileSync(
@@ -2422,6 +2906,7 @@ export function replaceManagedFileSync(
2422
2906
  assertFence?: () => void,
2423
2907
  expectedDestination?: ManagedFileSnapshot["identity"],
2424
2908
  ): void {
2909
+ if (bytes.byteLength > MANAGED_ARTIFACT_MAX_FILE_BYTES) throw new Error("content_too_large");
2425
2910
  replaceManagedFileGeneratedSync(
2426
2911
  destination,
2427
2912
  fd => {
@@ -2449,10 +2934,15 @@ function appendManagedFileStreamingSync(
2449
2934
  policy: ManagedSessionSecurityPolicy,
2450
2935
  ): ManagedFileSnapshot["identity"] {
2451
2936
  const predecessor = captureManagedFileIdentityStreamingNoFollow(destination);
2452
- replaceManagedFileGeneratedSync(
2937
+ if (predecessor.size > MANAGED_ARTIFACT_MAX_FILE_BYTES - appendedBytes.byteLength)
2938
+ throw new Error("content_too_large");
2939
+ return replaceManagedFileGeneratedSync(
2453
2940
  destination,
2454
2941
  stagingFd => {
2455
- const sourceFd = fs.openSync(destination, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
2942
+ const sourceFd = fs.openSync(
2943
+ destination,
2944
+ fs.constants.O_RDONLY | fs.constants.O_NONBLOCK | (fs.constants.O_NOFOLLOW ?? 0),
2945
+ );
2456
2946
  try {
2457
2947
  const before = fs.fstatSync(sourceFd, { bigint: true });
2458
2948
  if (!sameReplacementIdentity(identity(before, predecessor.sha256), predecessor))
@@ -2480,6 +2970,8 @@ function appendManagedFileStreamingSync(
2480
2970
  )
2481
2971
  throw new Error("managed_replace_identity_mismatch");
2482
2972
  if (hash.copy().digest("hex") !== predecessor.sha256) throw new Error("managed_replace_identity_mismatch");
2973
+ if (total > MANAGED_ARTIFACT_MAX_FILE_BYTES - appendedBytes.byteLength)
2974
+ throw new Error("content_too_large");
2483
2975
  let appended = 0;
2484
2976
  while (appended < appendedBytes.byteLength) {
2485
2977
  const amount = fs.writeSync(stagingFd, appendedBytes, appended, appendedBytes.byteLength - appended);
@@ -2496,8 +2988,9 @@ function appendManagedFileStreamingSync(
2496
2988
  policy,
2497
2989
  undefined,
2498
2990
  predecessor,
2991
+ true,
2992
+ "append",
2499
2993
  );
2500
- return captureManagedFileIdentityStreamingNoFollow(destination);
2501
2994
  }
2502
2995
 
2503
2996
  export async function replaceManagedFile(
@@ -2697,23 +3190,31 @@ export function validateManagedArtifactTree(root: string, limits: ManagedArtifac
2697
3190
  };
2698
3191
  const maxFiles = clampLimit(limits.maxFiles, MANAGED_ARTIFACT_MAX_FILES);
2699
3192
  const maxTotalBytes = clampLimit(limits.maxTotalBytes, MANAGED_ARTIFACT_MAX_TOTAL_BYTES);
3193
+ let entries = 0;
2700
3194
  let files = 0;
2701
3195
  let bytes = 0;
2702
3196
  const visit = (directory: string, depth: number): void => {
2703
3197
  if (depth > MANAGED_ARTIFACT_MAX_DEPTH) throw new Error("unsafe_artifacts");
2704
- for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
2705
- const entryPath = path.join(directory, entry.name);
2706
- const stat = fs.lstatSync(entryPath);
2707
- if (stat.isSymbolicLink()) throw new Error("unsafe_artifacts");
2708
- if (stat.isDirectory()) {
2709
- visit(entryPath, depth + 1);
2710
- continue;
3198
+ const handle = fs.opendirSync(directory);
3199
+ try {
3200
+ for (let entry = handle.readSync(); entry; entry = handle.readSync()) {
3201
+ entries++;
3202
+ if (entries > maxFiles) throw new Error("artifact_capacity_exceeded");
3203
+ const entryPath = path.join(directory, entry.name);
3204
+ const stat = fs.lstatSync(entryPath);
3205
+ if (stat.isSymbolicLink()) throw new Error("unsafe_artifacts");
3206
+ if (stat.isDirectory()) {
3207
+ visit(entryPath, depth + 1);
3208
+ continue;
3209
+ }
3210
+ if (stat.nlink > 1) throw new Error("unsafe_artifacts");
3211
+ if (!stat.isFile() || stat.size > MANAGED_ARTIFACT_MAX_FILE_BYTES) throw new Error("unsafe_artifacts");
3212
+ files++;
3213
+ bytes += stat.size;
3214
+ if (files > maxFiles || bytes > maxTotalBytes) throw new Error("artifact_capacity_exceeded");
2711
3215
  }
2712
- if (stat.nlink > 1) throw new Error("unsafe_artifacts");
2713
- if (!stat.isFile() || stat.size > MANAGED_ARTIFACT_MAX_FILE_BYTES) throw new Error("unsafe_artifacts");
2714
- files++;
2715
- bytes += stat.size;
2716
- if (files > maxFiles || bytes > maxTotalBytes) throw new Error("artifact_capacity_exceeded");
3216
+ } finally {
3217
+ handle.closeSync();
2717
3218
  }
2718
3219
  };
2719
3220
  const rootStat = fs.lstatSync(root);