@sema-agent/core 7.14.0 → 7.16.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 (136) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/core/ask-origin.d.ts +2 -2
  3. package/dist/core/checkpoint-store.d.ts +14 -15
  4. package/dist/core/governance-codes.js +2 -0
  5. package/dist/core/hooks.d.ts +4 -3
  6. package/dist/core/memory-engine/delegation-settlement.d.ts +37 -12
  7. package/dist/core/memory-engine/delegation-settlement.js +128 -39
  8. package/dist/core/memory-engine/engine.d.ts +9 -8
  9. package/dist/core/memory-engine/engine.js +115 -120
  10. package/dist/core/memory-engine/file-backend.d.ts +17 -5
  11. package/dist/core/memory-engine/file-backend.js +78 -34
  12. package/dist/core/memory-engine/index.d.ts +2 -2
  13. package/dist/core/memory-engine/index.js +2 -2
  14. package/dist/core/memory-engine/layout.d.ts +152 -54
  15. package/dist/core/memory-engine/layout.js +441 -104
  16. package/dist/core/memory-engine/types.d.ts +8 -3
  17. package/dist/core/memory-engine/types.js +5 -0
  18. package/dist/core/runner/abort-race.d.ts +3 -3
  19. package/dist/core/runner/active-skill-scope.d.ts +2 -2
  20. package/dist/core/runner/assemble-result.d.ts +29 -29
  21. package/dist/core/runner/checkpoint-scope.d.ts +4 -4
  22. package/dist/core/runner/clock-and-limits.d.ts +3 -3
  23. package/dist/core/runner/compaction-call-options.d.ts +11 -11
  24. package/dist/core/runner/compaction-knobs.d.ts +4 -4
  25. package/dist/core/runner/compaction-seams.d.ts +5 -5
  26. package/dist/core/runner/contracts.d.ts +224 -226
  27. package/dist/core/runner/decide-continuation.d.ts +6 -6
  28. package/dist/core/runner/denial-limit-arms.d.ts +8 -8
  29. package/dist/core/runner/derived-route-fallback.d.ts +2 -2
  30. package/dist/core/runner/gate-exit.d.ts +6 -6
  31. package/dist/core/runner/git-status-frame.d.ts +7 -7
  32. package/dist/core/runner/inherited-ask-grants.d.ts +6 -6
  33. package/dist/core/runner/memory-capture-optout.d.ts +1 -1
  34. package/dist/core/runner/memory-consolidation-driver.d.ts +4 -4
  35. package/dist/core/runner/memory-consolidation.d.ts +6 -6
  36. package/dist/core/runner/park-commit.d.ts +6 -6
  37. package/dist/core/runner/permission-rule-lanes.d.ts +22 -20
  38. package/dist/core/runner/permission-rule-lanes.js +1 -1
  39. package/dist/core/runner/prepare-acquire-reconcile.d.ts +4 -4
  40. package/dist/core/runner/prepare-ask-lane.d.ts +3 -3
  41. package/dist/core/runner/prepare-boundary-parks.d.ts +3 -3
  42. package/dist/core/runner/prepare-caps-and-workflow.d.ts +5 -5
  43. package/dist/core/runner/prepare-config-doors.d.ts +21 -21
  44. package/dist/core/runner/prepare-context-lane.d.ts +6 -6
  45. package/dist/core/runner/prepare-defer-classify.d.ts +3 -3
  46. package/dist/core/runner/prepare-defer-classify.js +12 -10
  47. package/dist/core/runner/prepare-delegation-surface.d.ts +1 -1
  48. package/dist/core/runner/prepare-file-history.d.ts +2 -2
  49. package/dist/core/runner/prepare-gate-stations.d.ts +8 -9
  50. package/dist/core/runner/prepare-gate-stations.js +2 -2
  51. package/dist/core/runner/prepare-hands-readface.d.ts +41 -41
  52. package/dist/core/runner/prepare-hands-readface.js +27 -29
  53. package/dist/core/runner/prepare-inherited-gate.d.ts +6 -6
  54. package/dist/core/runner/prepare-listings.d.ts +3 -3
  55. package/dist/core/runner/prepare-lsp.d.ts +3 -3
  56. package/dist/core/runner/prepare-memory-engine-session.d.ts +5 -5
  57. package/dist/core/runner/prepare-memory.js +6 -7
  58. package/dist/core/runner/prepare-offload-wrappers.d.ts +2 -2
  59. package/dist/core/runner/prepare-park-ask.d.ts +11 -12
  60. package/dist/core/runner/prepare-park-ask.js +3 -3
  61. package/dist/core/runner/prepare-policy-chain.d.ts +6 -6
  62. package/dist/core/runner/prepare-policy-chain.js +2 -2
  63. package/dist/core/runner/prepare-prompt-inputs.d.ts +5 -5
  64. package/dist/core/runner/prepare-protocol-tools.d.ts +1 -1
  65. package/dist/core/runner/prepare-question-face.d.ts +4 -4
  66. package/dist/core/runner/prepare-run-refs.d.ts +11 -11
  67. package/dist/core/runner/prepare-safety-scan.d.ts +1 -1
  68. package/dist/core/runner/prepare-suspend-saga.d.ts +4 -4
  69. package/dist/core/runner/prepare-task.d.ts +12 -12
  70. package/dist/core/runner/prepare-task.js +5 -7
  71. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +2 -3
  72. package/dist/core/runner/prepare-wiring-manifest.d.ts +4 -4
  73. package/dist/core/runner/prepare-workspace-restore.d.ts +1 -1
  74. package/dist/core/runner/remote-env-retry.d.ts +3 -3
  75. package/dist/core/runner/resume-admission.d.ts +1 -1
  76. package/dist/core/runner/resume-claim.d.ts +1 -1
  77. package/dist/core/runner/resume-internals-and-config.d.ts +1 -1
  78. package/dist/core/runner/resume-policy-outcome.d.ts +1 -1
  79. package/dist/core/runner/resume-preflight.d.ts +1 -1
  80. package/dist/core/runner/resume-review-outcome.d.ts +1 -1
  81. package/dist/core/runner/run-brain-sinks.d.ts +1 -1
  82. package/dist/core/runner/run-clock-and-content.d.ts +4 -4
  83. package/dist/core/runner/run-compaction-machinery.d.ts +3 -3
  84. package/dist/core/runner/run-git-lane.d.ts +1 -1
  85. package/dist/core/runner/run-harness-handlers.d.ts +2 -2
  86. package/dist/core/runner/run-identity-wiring.d.ts +4 -4
  87. package/dist/core/runner/run-leg.d.ts +3 -3
  88. package/dist/core/runner/run-notification-lane.d.ts +2 -2
  89. package/dist/core/runner/run-reasoning-seat.d.ts +2 -2
  90. package/dist/core/runner/run-settle-and-teardown.d.ts +4 -4
  91. package/dist/core/runner/run-telemetry-and-budget-seats.d.ts +2 -2
  92. package/dist/core/runner/run-terminal-adoption.d.ts +5 -5
  93. package/dist/core/runner/runtask.d.ts +14 -14
  94. package/dist/core/runner/session-file-state-replay.d.ts +3 -3
  95. package/dist/core/runner/session-rule-policy.d.ts +2 -2
  96. package/dist/core/runner/steer-admission.d.ts +2 -2
  97. package/dist/core/runner/stream-halt-verbs.d.ts +1 -1
  98. package/dist/core/runner/stream-reap.d.ts +1 -1
  99. package/dist/core/runner/synthetic-tools.d.ts +8 -8
  100. package/dist/core/runner/teardown-bounded.d.ts +2 -2
  101. package/dist/core/runner/tool-defer-gate.d.ts +86 -0
  102. package/dist/core/runner/tool-defer-gate.js +57 -0
  103. package/dist/core/runner/tool-disclosure.d.ts +33 -69
  104. package/dist/core/runner/tool-disclosure.js +0 -43
  105. package/dist/core/runner/tool-end-body.d.ts +3 -3
  106. package/dist/core/runner/tool-face-overlay.d.ts +2 -2
  107. package/dist/core/runner/tool-output-projection.d.ts +2 -2
  108. package/dist/core/runner/turn-attachments.d.ts +53 -53
  109. package/dist/core/runner/usage-accounting.d.ts +4 -4
  110. package/dist/core/runner/workspace-path.d.ts +1 -1
  111. package/dist/core/runner-deps.d.ts +16 -9
  112. package/dist/core/task-spec.d.ts +8 -2
  113. package/dist/core/tool-spec.d.ts +7 -4
  114. package/dist/core/types.d.ts +1 -1
  115. package/dist/engine/llm/types.d.ts +1 -1
  116. package/dist/index.d.ts +4 -4
  117. package/dist/index.js +3 -3
  118. package/dist/tools/fs/bash-readonly-classifier.d.ts +40 -40
  119. package/dist/tools/fs/encoding.d.ts +31 -40
  120. package/dist/tools/fs/fs-bash.d.ts +76 -35
  121. package/dist/tools/fs/fs-bash.js +86 -29
  122. package/dist/tools/fs/fs-pdf.d.ts +8 -8
  123. package/dist/tools/fs/fs-search-tools.d.ts +4 -4
  124. package/dist/tools/fs/fs-shared.d.ts +144 -158
  125. package/dist/tools/fs/fs-write.d.ts +8 -8
  126. package/dist/tools/fs/gh-rate-limit.d.ts +13 -14
  127. package/dist/tools/fs/index.d.ts +74 -78
  128. package/dist/tools/fs/notebook.d.ts +24 -25
  129. package/dist/tools/fs/pdf.d.ts +3 -3
  130. package/dist/tools/fs/read-deny.d.ts +22 -22
  131. package/dist/tools/fs/read-face.d.ts +21 -24
  132. package/dist/tools/fs/repo-map.d.ts +8 -8
  133. package/dist/tools/fs/safety.d.ts +116 -129
  134. package/dist/tools/fs/search.d.ts +63 -71
  135. package/package.json +5 -2
  136. package/test/export-surface.snapshot.json +9 -1
@@ -1,4 +1,4 @@
1
- import { closeSync, constants as fsConstants, copyFileSync, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
1
+ import { chmodSync, closeSync, constants as fsConstants, copyFileSync, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync, writeSync } from "node:fs";
2
2
  const { O_WRONLY, O_CREAT, O_TRUNC, O_NOFOLLOW, O_EXCL } = fsConstants;
3
3
  import { homedir } from "node:os";
4
4
  import { createHash } from "node:crypto";
@@ -201,7 +201,7 @@ function lstatOrAbsent(p) {
201
201
  }
202
202
  }
203
203
  function probeDirCaseFolds(dir) {
204
- const name = `.casefold-probe-${process.pid}-${caseFoldProbeSeq++}`;
204
+ const name = `${CASEFOLD_PROBE_PREFIX}${process.pid}-${caseFoldProbeSeq++}`;
205
205
  const writeLeg = (() => {
206
206
  let fd;
207
207
  try {
@@ -301,6 +301,18 @@ export function dirCaseFolds(dir) {
301
301
  caseFoldByDirIdentity.set(key, folds);
302
302
  return folds;
303
303
  }
304
+ export const SCOPE_DIR_LABEL_RE = /^[A-Za-z0-9_][A-Za-z0-9._-]*$/;
305
+ export const CASEFOLD_PROBE_PREFIX = ".casefold-probe-";
306
+ export const SCOPE_REGISTRY_SHAPES = ["absent", "v1", "v2", "migrating"];
307
+ export const REGISTRY_SHAPE_DERIVES_PATHS = {
308
+ absent: true,
309
+ v1: false,
310
+ v2: true,
311
+ migrating: false,
312
+ };
313
+ function ownLabel(scopes, scope) {
314
+ return scopes !== undefined && Object.prototype.hasOwnProperty.call(scopes, scope) ? scopes[scope] : undefined;
315
+ }
304
316
  function readScopesRecord(controlDir) {
305
317
  const path = join(controlDir, SCOPES_FILE);
306
318
  const journalPath = `${path}.journal`;
@@ -322,7 +334,7 @@ function readScopesRecord(controlDir) {
322
334
  }
323
335
  catch (err) {
324
336
  if (err.code === "ENOENT")
325
- return {};
337
+ return { shape: "absent", rec: { v: 2, scopes: {} } };
326
338
  throw new ControlPlaneCorruptError(`scope registry unreadable (${err.code ?? "io error"}): ${path}`, { cause: err });
327
339
  }
328
340
  }
@@ -337,95 +349,357 @@ function readScopesRecord(controlDir) {
337
349
  throw new ControlPlaneCorruptError(`scope registry has the wrong shape: ${path}`);
338
350
  }
339
351
  const rec = parsed;
340
- if (rec.rootScope !== undefined && typeof rec.rootScope !== "string") {
341
- throw new ControlPlaneCorruptError(`scope registry rootScope is not a string: ${path}`);
342
- }
343
- let scopes;
344
- if (rec.scopes !== undefined) {
345
- if (!rec.scopes || typeof rec.scopes !== "object" || Array.isArray(rec.scopes)) {
346
- throw new ControlPlaneCorruptError(`scope registry scopes map has the wrong shape: ${path}`);
347
- }
348
- for (const [k, v] of Object.entries(rec.scopes)) {
349
- if (typeof v !== "string")
350
- throw new ControlPlaneCorruptError(`scope registry entry ${JSON.stringify(k)} is not a string: ${path}`);
351
- if ((v === "") !== (k === rec.rootScope)) {
352
- throw new ControlPlaneCorruptError(v === ""
353
- ? `scope registry maps ${JSON.stringify(k)} to the root home but rootScope is ${rec.rootScope === undefined ? "unclaimed" : JSON.stringify(rec.rootScope)}: ${path}`
354
- : `scope registry maps the root scope ${JSON.stringify(k)} to subdir ${JSON.stringify(v)} instead of the root home: ${path}`);
355
- }
352
+ const scopes = coerceScopeLabels(rec.scopes, path);
353
+ if (rec.v === undefined) {
354
+ if (rec.rootScope !== undefined && typeof rec.rootScope !== "string") {
355
+ throw new ControlPlaneCorruptError(`scope registry rootScope is not a string: ${path}`);
356
+ }
357
+ return { shape: "v1", ...(rec.rootScope !== undefined ? { rootScope: rec.rootScope } : {}), scopes };
358
+ }
359
+ if (rec.v !== 2) {
360
+ throw new ControlPlaneCorruptError(`scope registry version ${JSON.stringify(rec.v)} is not one this engine reads (expected 2): ${path}`);
361
+ }
362
+ for (const [k, label] of Object.entries(scopes)) {
363
+ if (!SCOPE_DIR_LABEL_RE.test(label)) {
364
+ throw new ControlPlaneCorruptError(`scope registry maps ${JSON.stringify(k)} to the label ${JSON.stringify(label)}, which is not a single non-hidden path segment — refusing (fail-closed): ${path}`);
356
365
  }
357
- scopes = rec.scopes;
358
366
  }
359
- return { rootScope: rec.rootScope, scopes };
367
+ if (rec.rootOwner !== undefined && typeof rec.rootOwner !== "string") {
368
+ throw new ControlPlaneCorruptError(`scope registry rootOwner is not a string: ${path}`);
369
+ }
370
+ const v2 = { v: 2, scopes, ...(rec.rootOwner !== undefined ? { rootOwner: rec.rootOwner } : {}) };
371
+ if (rec.migrating === undefined)
372
+ return { shape: "v2", rec: v2 };
373
+ const m = rec.migrating;
374
+ if (!m || typeof m !== "object" || Array.isArray(m) || typeof m.root !== "string" || typeof m.owner !== "string" || typeof m.label !== "string" || !SCOPE_DIR_LABEL_RE.test(m.label)) {
375
+ throw new ControlPlaneCorruptError(`scope registry carries a malformed migrating marker: ${path}`);
376
+ }
377
+ return { shape: "migrating", rec: { ...v2, migrating: { root: m.root, owner: m.owner, label: m.label } } };
378
+ }
379
+ function coerceScopeLabels(raw, path) {
380
+ if (raw === undefined)
381
+ return {};
382
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
383
+ throw new ControlPlaneCorruptError(`scope registry scopes map has the wrong shape: ${path}`);
384
+ }
385
+ const out = Object.create(null);
386
+ for (const [k, v] of Object.entries(raw)) {
387
+ if (typeof v !== "string")
388
+ throw new ControlPlaneCorruptError(`scope registry entry ${JSON.stringify(k)} is not a string: ${path}`);
389
+ out[k] = v;
390
+ }
391
+ return out;
360
392
  }
361
- function lockedScopesUpdate(controlDir, fn) {
393
+ function requireV2Registry(read, controlDir) {
394
+ if (REGISTRY_SHAPE_DERIVES_PATHS[read.shape])
395
+ return read.rec;
396
+ throw new ControlPlaneCorruptError(read.shape === "v1"
397
+ ? `scope registry is in the v1 (pre-v2, root-owning) form at ${join(controlDir, SCOPES_FILE)} — scope homes cannot be derived until the layout adoption has run (a mount or a locked backend face runs it); refusing (fail-closed)`
398
+ : `scope registry carries an unfinished layout move (migrating marker) at ${join(controlDir, SCOPES_FILE)} — scope homes cannot be derived until the move completes (the next mount or locked backend face finishes it); refusing (fail-closed)`);
399
+ }
400
+ function commitScopesRecord(controlDir, lock, token, next) {
401
+ const file = join(controlDir, SCOPES_FILE);
402
+ const journal = `${file}.journal`;
403
+ const data = `${JSON.stringify(next, null, 2)}\n`;
404
+ const fence = () => assertSidecarLockOwnership(lock, token, "scope registry", "strict");
405
+ fence();
406
+ atomicWriteFileSync(journal, data);
407
+ fence();
408
+ atomicWriteFileSync(file, data);
409
+ fence();
410
+ rmSync(journal, { force: true });
411
+ }
412
+ function withScopesRegistry(controlDir, fn) {
362
413
  ensureDirExists(controlDir);
363
414
  const file = join(controlDir, SCOPES_FILE);
364
415
  const journal = `${file}.journal`;
365
416
  const lock = `${file}.lock`;
366
417
  const token = acquireSidecarLock(lock, { onDeadline: "throw" });
367
418
  try {
368
- rollForwardStrictSidecar(file, journal);
369
- const rec = readScopesRecord(controlDir);
370
- const { next, result } = fn(rec);
371
- if (next !== undefined) {
372
- const data = `${JSON.stringify(next, null, 2)}\n`;
373
- assertSidecarLockOwnership(lock, token, "scope registry");
374
- atomicWriteFileSync(journal, data);
375
- atomicWriteFileSync(file, data);
376
- rmSync(journal, { force: true });
377
- }
378
- return result;
419
+ rollForwardStrictSidecar(file, journal, () => assertSidecarLockOwnership(lock, token, "scope registry", "strict"));
420
+ return fn({
421
+ read: () => readScopesRecord(controlDir),
422
+ commit: (next) => commitScopesRecord(controlDir, lock, token, next),
423
+ renew: () => {
424
+ const now = new Date();
425
+ utimesSync(lock, now, now);
426
+ assertSidecarLockOwnership(lock, token, "scope registry", "strict");
427
+ },
428
+ });
379
429
  }
380
430
  finally {
381
431
  releaseSidecarLock(lock, token);
382
432
  }
383
433
  }
384
- export function rootScopeOf(controlDir) {
385
- return readScopesRecord(controlDir).rootScope;
434
+ function assertNoLabelCollision(memoryDir, scopes, scope, label, caseFoldingFs) {
435
+ for (const [other, otherLabel] of Object.entries(scopes)) {
436
+ if (other === scope)
437
+ continue;
438
+ if (otherLabel === label) {
439
+ throw new ControlPlaneCorruptError(`scope directory collision: ${JSON.stringify(scope)} and ${JSON.stringify(other)} both map to ${JSON.stringify(label)} — refusing (fail-closed)`);
440
+ }
441
+ if (otherLabel.toLowerCase() === label.toLowerCase()) {
442
+ const folds = caseFoldingFs ?? dirCaseFolds(memoryDir);
443
+ if (folds !== false) {
444
+ throw new ControlPlaneCorruptError(`scope directory collision on a case-folding filesystem: ${JSON.stringify(scope)} → ${JSON.stringify(label)} and ${JSON.stringify(other)} → ${JSON.stringify(otherLabel)} are one physical directory under ${memoryDir}` +
445
+ `${folds === undefined ? " (volume case semantics could not be probed — refusing the fold-equal pair rather than risking a silent cross-scope merge)" : ""} — refusing (fail-closed)`);
446
+ }
447
+ }
448
+ }
449
+ }
450
+ function assertHomePhysicalForm(home) {
451
+ const st = lstatOrAbsent(home);
452
+ if (st === undefined)
453
+ return "absent";
454
+ if (st.isDirectory())
455
+ return "dir";
456
+ throw new ControlPlaneCorruptError(`scope home ${home} is ${st.isSymbolicLink() ? "a symbolic link" : "not a directory"} — a scope home must be a real directory (fail-closed)`);
386
457
  }
387
- export function claimRootScope(controlDir, scope) {
388
- return lockedScopesUpdate(controlDir, (rec) => {
389
- if (rec.rootScope !== undefined)
390
- return { result: rec.rootScope };
391
- return { next: { rootScope: scope, scopes: { ...rec.scopes, [scope]: "" } }, result: scope };
392
- });
458
+ function ensureScopeHome(home) {
459
+ if (assertHomePhysicalForm(home) === "absent")
460
+ mkdirSync(home, { recursive: true });
461
+ assertHomePhysicalForm(home);
393
462
  }
394
463
  export function registerScope(memoryDir, controlDir, scope, opts) {
395
- return lockedScopesUpdate(controlDir, (rec) => {
396
- const registered = rec.scopes !== undefined && Object.prototype.hasOwnProperty.call(rec.scopes, scope) ? rec.scopes[scope] : undefined;
397
- const dirName = rec.rootScope === scope ? "" : (registered ?? scopeDirName(scope));
398
- for (const [other, otherDir] of Object.entries(rec.scopes ?? {})) {
399
- if (other === scope || otherDir === "")
400
- continue;
401
- if (otherDir === dirName) {
402
- throw new ControlPlaneCorruptError(`scope directory collision: ${JSON.stringify(scope)} and ${JSON.stringify(other)} both map to ${JSON.stringify(dirName)} — refusing (fail-closed)`);
403
- }
404
- if (dirName !== "" && otherDir.toLowerCase() === dirName.toLowerCase()) {
405
- const folds = opts?.caseFoldingFs ?? dirCaseFolds(memoryDir);
406
- if (folds !== false) {
407
- throw new ControlPlaneCorruptError(`scope directory collision on a case-folding filesystem: ${JSON.stringify(scope)} → ${JSON.stringify(dirName)} and ${JSON.stringify(other)} → ${JSON.stringify(otherDir)} are one physical directory under ${memoryDir}` +
408
- `${folds === undefined ? " (volume case semantics could not be probed — refusing the fold-equal pair rather than risking a silent cross-scope merge)" : ""} — refusing (fail-closed)`);
409
- }
410
- }
411
- }
464
+ return withScopesRegistry(controlDir, ({ read, commit }) => {
465
+ const rec = requireV2Registry(read(), controlDir);
466
+ const registered = ownLabel(rec.scopes, scope);
467
+ const label = registered ?? scopeDirName(scope);
468
+ assertNoLabelCollision(memoryDir, rec.scopes, scope, label, opts?.caseFoldingFs);
469
+ const home = join(memoryDir, label);
412
470
  if (registered === undefined) {
413
- return { next: { ...rec, scopes: { ...rec.scopes, [scope]: dirName } }, result: dirName === "" ? memoryDir : join(memoryDir, dirName) };
471
+ ensureDirExists(memoryDir);
472
+ ensureScopeHome(home);
473
+ commit({ ...rec, scopes: { ...rec.scopes, [scope]: label } });
414
474
  }
415
- return { result: dirName === "" ? memoryDir : join(memoryDir, dirName) };
475
+ else {
476
+ assertHomePhysicalForm(home);
477
+ }
478
+ return home;
416
479
  });
417
480
  }
418
481
  export function registeredScopes(controlDir) {
419
- return readScopesRecord(controlDir).scopes ?? {};
482
+ return requireV2Registry(readScopesRecord(controlDir), controlDir).scopes;
420
483
  }
421
484
  export function scopeDirFor(memoryDir, controlDir, scope) {
422
- const rec = readScopesRecord(controlDir);
423
- if (rec.rootScope === scope)
424
- return memoryDir;
425
- const registered = rec.scopes !== undefined && Object.prototype.hasOwnProperty.call(rec.scopes, scope) ? rec.scopes[scope] : undefined;
426
- if (registered !== undefined)
427
- return registered === "" ? memoryDir : join(memoryDir, registered);
428
- return join(memoryDir, scopeDirName(scope));
485
+ const rec = requireV2Registry(readScopesRecord(controlDir), controlDir);
486
+ return join(memoryDir, ownLabel(rec.scopes, scope) ?? scopeDirName(scope));
487
+ }
488
+ export function scopeRegistryShape(controlDir) {
489
+ return readScopesRecord(controlDir).shape;
490
+ }
491
+ export function v1MemoryPathKeyResolver(controlDir) {
492
+ const read = readScopesRecord(controlDir);
493
+ const owner = read.shape === "v1" ? read.rootScope : read.rec.rootOwner;
494
+ const scopes = read.shape === "v1" ? read.scopes : read.rec.scopes;
495
+ const byLabel = new Map();
496
+ for (const [scope, label] of Object.entries(scopes))
497
+ if (label !== "" && scope !== owner)
498
+ byLabel.set(label, scope);
499
+ return (relPath) => {
500
+ const norm = relPath.split(sep).join("/");
501
+ const cut = norm.indexOf("/");
502
+ if (cut > 0) {
503
+ const scope = byLabel.get(norm.slice(0, cut));
504
+ if (scope !== undefined)
505
+ return { scope, rel: norm.slice(cut + 1) };
506
+ }
507
+ return owner === undefined ? undefined : { scope: owner, rel: norm };
508
+ };
509
+ }
510
+ const UNOWNED_ROOT_CONTENT_NAMES_MAX = 8;
511
+ function discloseLayoutIncident(sink, code, message) {
512
+ if (sink === undefined)
513
+ return;
514
+ try {
515
+ const e = new Error(message);
516
+ e.code = code;
517
+ sink(e);
518
+ }
519
+ catch {
520
+ }
521
+ }
522
+ function sameEntry(a, b) {
523
+ const sa = lstatOrAbsent(a);
524
+ const sb = lstatOrAbsent(b);
525
+ return sa !== undefined && sb !== undefined && sa.dev === sb.dev && sa.ino === sb.ino;
526
+ }
527
+ function looseRootContent(root, labels, controlDir) {
528
+ let names;
529
+ try {
530
+ names = readdirSync(root);
531
+ }
532
+ catch (err) {
533
+ if (err.code === "ENOENT")
534
+ return { names: [], probes: [] };
535
+ throw new ControlPlaneCorruptError(`memory root ${root} could not be enumerated for the layout adoption (${err.code ?? "io error"}) — refusing (fail-closed)`, { cause: err });
536
+ }
537
+ const canonicalControl = canonicalize(controlDir);
538
+ const out = [];
539
+ const probes = [];
540
+ const foldedLabels = new Map();
541
+ for (const label of labels)
542
+ foldedLabels.set(label.toLowerCase(), label);
543
+ for (const name of names.sort()) {
544
+ if (labels.has(name))
545
+ continue;
546
+ if (name.startsWith(CASEFOLD_PROBE_PREFIX)) {
547
+ probes.push(name);
548
+ continue;
549
+ }
550
+ if (canonicalize(join(root, name)) === canonicalControl)
551
+ continue;
552
+ const label = foldedLabels.get(name.toLowerCase());
553
+ if (label !== undefined && sameEntry(join(root, name), join(root, label)))
554
+ continue;
555
+ out.push(name);
556
+ }
557
+ return { names: out, probes };
558
+ }
559
+ export function layoutNeedsAdoption(root, controlDir) {
560
+ const read = readScopesRecord(controlDir);
561
+ if (read.shape === "v1" || read.shape === "migrating")
562
+ return true;
563
+ const { names, probes } = looseRootContent(root, new Set(Object.values(read.rec.scopes)), controlDir);
564
+ return names.length > 0 || probes.length > 0;
565
+ }
566
+ export function adoptRootScopeLayout(rootPath, controlDir, opts = {}) {
567
+ const root = resolve(rootPath);
568
+ withScopesRegistry(controlDir, (ctx) => {
569
+ const renew = () => {
570
+ ctx.renew();
571
+ opts.renew?.();
572
+ };
573
+ const convertSidecars = () => {
574
+ renew();
575
+ opts.convertSidecars?.(controlDir);
576
+ };
577
+ const read = ctx.read();
578
+ let rec;
579
+ let converting = false;
580
+ if (read.shape === "v1") {
581
+ converting = true;
582
+ const scopes = Object.create(null);
583
+ for (const [scope, label] of Object.entries(read.scopes)) {
584
+ if (label === "")
585
+ continue;
586
+ if (!SCOPE_DIR_LABEL_RE.test(label)) {
587
+ throw new ControlPlaneCorruptError(`scope registry (v1 form) maps ${JSON.stringify(scope)} to the label ${JSON.stringify(label)}, which is not a single non-hidden path segment — refusing (fail-closed): ${join(controlDir, SCOPES_FILE)}`);
588
+ }
589
+ scopes[scope] = label;
590
+ }
591
+ rec = { v: 2, scopes, ...(read.rootScope !== undefined ? { rootOwner: read.rootScope } : {}) };
592
+ if (read.rootScope !== undefined && ownLabel(scopes, read.rootScope) === undefined) {
593
+ const label = scopeDirName(read.rootScope);
594
+ assertNoLabelCollision(root, scopes, read.rootScope, label, undefined);
595
+ const home = join(root, label);
596
+ if (assertHomePhysicalForm(home) === "dir" && readdirSync(home).length > 0) {
597
+ throw new ControlPlaneCorruptError(`memory root ${root} holds a non-empty directory ${home} named like the home of its root-owning scope ${JSON.stringify(read.rootScope)} — the layout adoption cannot tell a topic directory from a home; resolve by hand (fail-closed)`);
598
+ }
599
+ rec.scopes = { ...scopes, [read.rootScope]: label };
600
+ }
601
+ }
602
+ else if (read.shape === "migrating") {
603
+ rec = read.rec;
604
+ const m = read.rec.migrating;
605
+ const { migrating: _m, ...rest } = rec;
606
+ rec = { ...rest, scopes: { ...rec.scopes, [m.owner]: m.label }, rootOwner: m.owner };
607
+ moveLooseContent(m.root, controlDir, rec, m.owner, m.label, renew, opts.onIncident, true);
608
+ convertSidecars();
609
+ ctx.commit(rec);
610
+ if (canonicalize(m.root) === canonicalize(root))
611
+ return;
612
+ }
613
+ else {
614
+ rec = read.rec;
615
+ }
616
+ const labels = new Set(Object.values(rec.scopes));
617
+ const { names, probes } = looseRootContent(root, labels, controlDir);
618
+ if (names.length === 0) {
619
+ if (converting) {
620
+ convertSidecars();
621
+ ctx.commit(rec);
622
+ }
623
+ for (const p of probes)
624
+ rmSync(join(root, p), { force: true });
625
+ return;
626
+ }
627
+ const candidate = opts.ownerCandidate ?? undefined;
628
+ const owner = rec.rootOwner ?? candidate;
629
+ if (owner === undefined) {
630
+ if (converting) {
631
+ convertSidecars();
632
+ ctx.commit(rec);
633
+ }
634
+ discloseLayoutIncident(opts.onIncident, "memory.layout_unowned_root_content", `memory root ${root} holds ${names.length} loose entr${names.length === 1 ? "y" : "ies"} that belong to no recorded scope (${names.slice(0, UNOWNED_ROOT_CONTENT_NAMES_MAX).join(", ")}${names.length > UNOWNED_ROOT_CONTENT_NAMES_MAX ? ", …" : ""}) — left in place, not served; the next mount with a write scope adopts them into that scope's home`);
635
+ return;
636
+ }
637
+ if (isContainedIn(root, controlDir)) {
638
+ throw new ControlPlaneCorruptError(`memory control plane ${controlDir} lies inside the memory root ${root} — the layout adoption refuses to move engine metadata as scope content (fail-closed)`);
639
+ }
640
+ const label = ownLabel(rec.scopes, owner) ?? scopeDirName(owner);
641
+ assertNoLabelCollision(root, rec.scopes, owner, label, undefined);
642
+ const home = join(root, label);
643
+ renew();
644
+ const homeForm = assertHomePhysicalForm(home);
645
+ if (homeForm === "dir") {
646
+ const inHome = readdirSync(home);
647
+ if (inHome.length > 0) {
648
+ throw new ControlPlaneCorruptError(`memory root ${root} holds loose content for ${JSON.stringify(owner)} while its home ${home} is already a non-empty directory with no move in progress — the two cannot be merged automatically (a topic directory named like the home, a root re-occupied by an older engine, or a registry that lost this scope's row); resolve by hand (fail-closed)`);
649
+ }
650
+ }
651
+ for (const name of names) {
652
+ renew();
653
+ if (lstatOrAbsent(join(home, name)) !== undefined) {
654
+ throw new ControlPlaneCorruptError(`memory root ${root} and the home ${home} both hold ${JSON.stringify(name)} — the layout adoption refuses to overwrite either (fail-closed)`);
655
+ }
656
+ }
657
+ const next = { v: 2, scopes: { ...rec.scopes, [owner]: label }, rootOwner: owner, migrating: { root, owner, label } };
658
+ renew();
659
+ ctx.commit(next);
660
+ const { migrating: _marker, ...done } = next;
661
+ moveLooseContent(root, controlDir, done, owner, label, renew, opts.onIncident, false);
662
+ convertSidecars();
663
+ renew();
664
+ ctx.commit(done);
665
+ });
666
+ }
667
+ function moveLooseContent(root, controlDir, rec, owner, label, renew, sink, resumed) {
668
+ const labels = new Set(Object.values(rec.scopes));
669
+ const { names, probes } = looseRootContent(root, labels, controlDir);
670
+ const home = join(root, label);
671
+ if (names.length > 0) {
672
+ ensureScopeHome(home);
673
+ chmodSync(home, 0o755);
674
+ }
675
+ let moved = 0;
676
+ for (const name of names) {
677
+ renew();
678
+ const src = join(root, name);
679
+ const dst = join(home, name);
680
+ if (sameEntry(src, home)) {
681
+ throw new ControlPlaneCorruptError(`memory root ${root}: ${JSON.stringify(name)} is the home ${home} itself under another spelling — the layout adoption refuses to move a directory into itself (fail-closed)`);
682
+ }
683
+ if (lstatOrAbsent(dst) !== undefined) {
684
+ throw new ControlPlaneCorruptError(`memory root ${root} and the home ${home} both hold ${JSON.stringify(name)} — the layout adoption refuses to overwrite either (fail-closed)`);
685
+ }
686
+ try {
687
+ renameSync(src, dst);
688
+ moved += 1;
689
+ }
690
+ catch (err) {
691
+ if (err.code === "ENOENT" && lstatOrAbsent(src) === undefined)
692
+ continue;
693
+ throw err;
694
+ }
695
+ }
696
+ for (const p of probes) {
697
+ renew();
698
+ rmSync(join(root, p), { force: true });
699
+ }
700
+ renew();
701
+ writeIndexRevs(controlDir, {});
702
+ discloseLayoutIncident(sink, "memory.layout_migrated", `memory layout adopted at ${root}: ${moved} loose entr${moved === 1 ? "y" : "ies"} moved into the home of ${JSON.stringify(owner)} (${label})${resumed ? " — an interrupted move was finished" : ""}`);
429
703
  }
430
704
  export function canonicalize(p) {
431
705
  try {
@@ -720,11 +994,17 @@ export function drainMemoryAnnouncements(controlDir) {
720
994
  export function peekMemoryAnnouncements(controlDir) {
721
995
  return coerceAnnouncements(readSidecarJson(controlDir, ANNOUNCEMENTS_FILE));
722
996
  }
997
+ export function scanFuseKey(scope, rel) {
998
+ return JSON.stringify([scope, rel.split(sep).join("/")]);
999
+ }
723
1000
  function coerceFuse(raw) {
724
1001
  if (!raw || typeof raw !== "object" || Array.isArray(raw))
725
1002
  return {};
1003
+ const rec = raw;
1004
+ if (rec.v !== 2 || !rec.counts || typeof rec.counts !== "object" || Array.isArray(rec.counts))
1005
+ return {};
726
1006
  const out = {};
727
- for (const [k, v] of Object.entries(raw)) {
1007
+ for (const [k, v] of Object.entries(rec.counts)) {
728
1008
  if (typeof v === "number" && Number.isFinite(v) && v > 0)
729
1009
  out[k] = Math.floor(v);
730
1010
  }
@@ -736,7 +1016,7 @@ export function bumpScanFuse(controlDir, key) {
736
1016
  const counts = coerceFuse(current);
737
1017
  prior = counts[key] ?? 0;
738
1018
  counts[key] = prior + 1;
739
- return counts;
1019
+ return { v: 2, counts };
740
1020
  });
741
1021
  return prior;
742
1022
  }
@@ -992,7 +1272,7 @@ export function clearScanFuse(controlDir, keys) {
992
1272
  changed = true;
993
1273
  }
994
1274
  }
995
- return changed ? counts : undefined;
1275
+ return changed ? { v: 2, counts } : undefined;
996
1276
  });
997
1277
  }
998
1278
  export const LINEAGE_FILE = "lineage.json";
@@ -1000,7 +1280,7 @@ export const CHALLENGES_FILE = "challenges.json";
1000
1280
  export const LINEAGE_AUDIT_FILE = "lineage-audit.json";
1001
1281
  export const LINEAGE_AUDIT_MAX_ROWS = 2048;
1002
1282
  export const CHALLENGED_HISTORY_FILE = "usage-challenged-history.json";
1003
- function rollForwardStrictSidecar(file, journal) {
1283
+ function rollForwardStrictSidecar(file, journal, fence) {
1004
1284
  let raw;
1005
1285
  try {
1006
1286
  raw = readFileSync(journal, "utf8");
@@ -1014,7 +1294,9 @@ function rollForwardStrictSidecar(file, journal) {
1014
1294
  catch {
1015
1295
  throw new ControlPlaneCorruptError(`control-plane journal is unparseable (fail-closed sidecar): ${journal}`);
1016
1296
  }
1297
+ fence?.();
1017
1298
  atomicWriteFileSync(file, raw);
1299
+ fence?.();
1018
1300
  rmSync(journal, { force: true });
1019
1301
  }
1020
1302
  export function lockedStrictUpdate(controlDir, fileName, what, coerce, fn) {
@@ -1024,14 +1306,17 @@ export function lockedStrictUpdate(controlDir, fileName, what, coerce, fn) {
1024
1306
  const lock = `${file}.lock`;
1025
1307
  const token = acquireSidecarLock(lock, { onDeadline: "throw" });
1026
1308
  try {
1027
- rollForwardStrictSidecar(file, journal);
1309
+ rollForwardStrictSidecar(file, journal, () => assertSidecarLockOwnership(lock, token, what, "strict"));
1028
1310
  const current = coerce(readStrictSidecarRaw(file, what));
1029
1311
  const { next, result } = fn(current);
1030
1312
  if (next !== undefined) {
1031
1313
  const data = `${JSON.stringify(next, null, 2)}\n`;
1032
- assertSidecarLockOwnership(lock, token, what, "strict");
1314
+ const fence = () => assertSidecarLockOwnership(lock, token, what, "strict");
1315
+ fence();
1033
1316
  atomicWriteFileSync(journal, data);
1317
+ fence();
1034
1318
  atomicWriteFileSync(file, data);
1319
+ fence();
1035
1320
  rmSync(journal, { force: true });
1036
1321
  }
1037
1322
  return result;
@@ -1628,63 +1913,115 @@ export function readChallengedHistory(controlDir) {
1628
1913
  return coerceChallengedHistory(readSidecarJson(controlDir, CHALLENGED_HISTORY_FILE));
1629
1914
  }
1630
1915
  export const PROJECTION_DEBTS_FILE = "projection-debts.json";
1916
+ function safeLedgerRel(rel) {
1917
+ return typeof rel === "string" && rel.length > 0 && !rel.startsWith("/") && !rel.split(/[\\/]/).includes("..");
1918
+ }
1919
+ function malformedDebtRow(row) {
1920
+ return (typeof row !== "object" ||
1921
+ row === null ||
1922
+ typeof row.entryId !== "string" ||
1923
+ row.entryId.length === 0 ||
1924
+ typeof row.rev !== "string" ||
1925
+ row.rev.length === 0 ||
1926
+ typeof row.at !== "number" ||
1927
+ !Number.isFinite(row.at));
1928
+ }
1631
1929
  function coerceProjectionDebts(raw) {
1632
1930
  if (raw === undefined)
1633
- return { version: 1, rows: [] };
1931
+ return { version: 2, rows: [] };
1634
1932
  const rec = raw;
1635
- if (typeof rec !== "object" || rec === null || rec.version !== 1 || !Array.isArray(rec.rows)) {
1933
+ if (typeof rec !== "object" || rec === null || (rec.version !== 1 && rec.version !== 2) || !Array.isArray(rec.rows)) {
1636
1934
  throw new ControlPlaneCorruptError("memory projection-debt ledger has the wrong shape — refusing (fail-closed; an unreadable debt account must not read as 'no debts')");
1637
1935
  }
1638
- for (const r of rec.rows) {
1639
- const row = r;
1640
- if (typeof row !== "object" ||
1641
- row === null ||
1642
- typeof row.relPath !== "string" ||
1643
- row.relPath.length === 0 ||
1644
- row.relPath.startsWith("/") ||
1645
- row.relPath.split(/[\\/]/).includes("..") ||
1646
- typeof row.entryId !== "string" ||
1647
- row.entryId.length === 0 ||
1648
- typeof row.rev !== "string" ||
1649
- row.rev.length === 0 ||
1650
- typeof row.at !== "number" ||
1651
- !Number.isFinite(row.at)) {
1652
- throw new ControlPlaneCorruptError("memory projection-debt ledger: a row is malformed — refusing (fail-closed)");
1936
+ const bad = () => {
1937
+ throw new ControlPlaneCorruptError("memory projection-debt ledger: a row is malformed — refusing (fail-closed)");
1938
+ };
1939
+ const rows = rec.rows;
1940
+ if (rec.version === 1) {
1941
+ for (const r of rows) {
1942
+ const row = r;
1943
+ if (malformedDebtRow(row) || !safeLedgerRel(row.relPath))
1944
+ bad();
1653
1945
  }
1946
+ return { version: 1, rows: rows };
1654
1947
  }
1655
- return rec;
1948
+ for (const r of rows) {
1949
+ const row = r;
1950
+ if (malformedDebtRow(row) || typeof row.scope !== "string" || row.scope.length === 0 || !safeLedgerRel(row.rel))
1951
+ bad();
1952
+ }
1953
+ const unresolved = rec.unresolved === undefined ? undefined : Array.isArray(rec.unresolved) ? rec.unresolved : bad();
1954
+ for (const r of unresolved ?? []) {
1955
+ const row = r;
1956
+ if (malformedDebtRow(row) || !safeLedgerRel(row.relPath))
1957
+ bad();
1958
+ }
1959
+ return { version: 2, rows: rows, ...(unresolved !== undefined ? { unresolved: unresolved } : {}) };
1960
+ }
1961
+ function convertProjectionDebts(read, controlDir) {
1962
+ if (read.version === 2)
1963
+ return { rec: read, converted: false };
1964
+ const resolve = v1MemoryPathKeyResolver(controlDir);
1965
+ const rows = [];
1966
+ const unresolved = [];
1967
+ for (const r of read.rows) {
1968
+ const key = resolve(r.relPath);
1969
+ if (key === undefined)
1970
+ unresolved.push(r);
1971
+ else
1972
+ rows.push({ scope: key.scope, rel: key.rel, entryId: r.entryId, rev: r.rev, at: r.at });
1973
+ }
1974
+ return { rec: { version: 2, rows, ...(unresolved.length > 0 ? { unresolved } : {}) }, converted: true };
1975
+ }
1976
+ function debtSeatKey(scope, rel) {
1977
+ return JSON.stringify([scope, rel]);
1656
1978
  }
1657
1979
  export function stageProjectionDebts(controlDir, rows, now) {
1658
1980
  if (rows.length === 0)
1659
1981
  return { refused: [] };
1660
1982
  const at = now();
1661
- return lockedStrictUpdate(controlDir, PROJECTION_DEBTS_FILE, "memory projection-debt ledger", coerceProjectionDebts, (rec) => {
1662
- const byPath = new Map(rec.rows.map((r) => [r.relPath, r]));
1983
+ return lockedStrictUpdate(controlDir, PROJECTION_DEBTS_FILE, "memory projection-debt ledger", coerceProjectionDebts, (read) => {
1984
+ const { rec } = convertProjectionDebts(read, controlDir);
1985
+ const bySeat = new Map(rec.rows.map((r) => [debtSeatKey(r.scope, r.rel), r]));
1663
1986
  const refused = [];
1664
1987
  for (const r of rows) {
1665
- const existing = byPath.get(r.relPath);
1988
+ const seat = debtSeatKey(r.scope, r.rel);
1989
+ const existing = bySeat.get(seat);
1666
1990
  if (existing !== undefined && existing.entryId !== r.entryId && existing.entryId !== r.replaces) {
1667
- refused.push({ relPath: r.relPath, entryId: r.entryId });
1991
+ refused.push({ scope: r.scope, rel: r.rel, entryId: r.entryId });
1668
1992
  continue;
1669
1993
  }
1670
- byPath.set(r.relPath, { relPath: r.relPath, entryId: r.entryId, rev: r.rev, at });
1994
+ bySeat.set(seat, { scope: r.scope, rel: r.rel, entryId: r.entryId, rev: r.rev, at });
1671
1995
  }
1672
- return { next: { version: 1, rows: [...byPath.values()] }, result: { refused } };
1996
+ return { next: { ...rec, version: 2, rows: [...bySeat.values()] }, result: { refused } };
1673
1997
  });
1674
1998
  }
1675
1999
  export function settleProjectionDebts(controlDir, rows) {
1676
2000
  if (rows.length === 0)
1677
2001
  return;
1678
- const drop = new Set(rows.map((r) => JSON.stringify([r.entryId, r.relPath])));
1679
- lockedStrictUpdate(controlDir, PROJECTION_DEBTS_FILE, "memory projection-debt ledger", coerceProjectionDebts, (rec) => {
1680
- const kept = rec.rows.filter((r) => !drop.has(JSON.stringify([r.entryId, r.relPath])));
1681
- if (kept.length === rec.rows.length)
2002
+ const drop = new Set(rows.map((r) => JSON.stringify([r.entryId, r.scope, r.rel])));
2003
+ lockedStrictUpdate(controlDir, PROJECTION_DEBTS_FILE, "memory projection-debt ledger", coerceProjectionDebts, (read) => {
2004
+ const { rec, converted } = convertProjectionDebts(read, controlDir);
2005
+ const kept = rec.rows.filter((r) => !drop.has(JSON.stringify([r.entryId, r.scope, r.rel])));
2006
+ if (kept.length === rec.rows.length && !converted)
1682
2007
  return { result: undefined };
1683
- return { next: { version: 1, rows: kept }, result: undefined };
2008
+ return { next: { ...rec, version: 2, rows: kept }, result: undefined };
1684
2009
  });
1685
2010
  }
1686
2011
  export function readProjectionDebts(controlDir) {
1687
- return coerceProjectionDebts(readStrictSidecar(controlDir, PROJECTION_DEBTS_FILE, "memory projection-debt ledger")).rows;
2012
+ const read = coerceProjectionDebts(readStrictSidecar(controlDir, PROJECTION_DEBTS_FILE, "memory projection-debt ledger"));
2013
+ if (read.version === 2)
2014
+ return read.rows;
2015
+ return lockedStrictUpdate(controlDir, PROJECTION_DEBTS_FILE, "memory projection-debt ledger", coerceProjectionDebts, (again) => {
2016
+ const { rec, converted } = convertProjectionDebts(again, controlDir);
2017
+ return converted ? { next: rec, result: rec.rows } : { result: rec.rows };
2018
+ });
2019
+ }
2020
+ export function unresolvedProjectionDebts(controlDir) {
2021
+ const read = coerceProjectionDebts(readStrictSidecar(controlDir, PROJECTION_DEBTS_FILE, "memory projection-debt ledger"));
2022
+ if (read.version === 1)
2023
+ return read.rows.map((r) => ({ relPath: r.relPath, entryId: r.entryId }));
2024
+ return (read.unresolved ?? []).map((r) => ({ relPath: r.relPath, entryId: r.entryId }));
1688
2025
  }
1689
2026
  export function writeAllSync(fd, data) {
1690
2027
  const buf = Buffer.from(data, "utf8");