@sema-agent/core 7.30.1 → 7.31.1

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 (80) hide show
  1. package/CHANGELOG.md +284 -0
  2. package/dist/agents/cross-session-judge.d.ts +4 -12
  3. package/dist/agents/cross-session-judge.js +4 -11
  4. package/dist/agents/peer-session-drain.js +0 -5
  5. package/dist/agents/send-message-tool.js +1 -0
  6. package/dist/agents/subagent.d.ts +1 -0
  7. package/dist/agents/subagent.js +1 -0
  8. package/dist/core/ask-origin.d.ts +6 -0
  9. package/dist/core/ask-origin.js +10 -0
  10. package/dist/core/checkpoint-store.d.ts +14 -0
  11. package/dist/core/checkpoint-store.js +1 -0
  12. package/dist/core/engine-notice.d.ts +4 -0
  13. package/dist/core/governance-codes.d.ts +1 -1
  14. package/dist/core/governance-codes.js +4 -0
  15. package/dist/core/memory-engine/data-plane.d.ts +3 -1
  16. package/dist/core/memory-engine/data-plane.js +1 -1
  17. package/dist/core/memory-engine/delegation-settlement.d.ts +44 -3
  18. package/dist/core/memory-engine/delegation-settlement.js +38 -3
  19. package/dist/core/memory-engine/engine.d.ts +133 -27
  20. package/dist/core/memory-engine/engine.js +452 -151
  21. package/dist/core/memory-engine/entry-scan.d.ts +23 -0
  22. package/dist/core/memory-engine/entry-scan.js +55 -0
  23. package/dist/core/memory-engine/file-backend.d.ts +31 -23
  24. package/dist/core/memory-engine/file-backend.js +174 -100
  25. package/dist/core/memory-engine/layer-observations.d.ts +109 -0
  26. package/dist/core/memory-engine/layer-observations.js +148 -0
  27. package/dist/core/memory-engine/layout.d.ts +40 -4
  28. package/dist/core/memory-engine/layout.js +43 -7
  29. package/dist/core/memory-engine/types.d.ts +26 -2
  30. package/dist/core/parent-spec-seat.d.ts +4 -1
  31. package/dist/core/parent-spec-seat.js +2 -0
  32. package/dist/core/park-selfcheck.js +1 -0
  33. package/dist/core/runner/announce-once-ledger.d.ts +26 -0
  34. package/dist/core/runner/announce-once-ledger.js +37 -0
  35. package/dist/core/runner/contracts.d.ts +22 -5
  36. package/dist/core/runner/denial-limit-arms.d.ts +2 -2
  37. package/dist/core/runner/prepare-acquire-reconcile.d.ts +0 -4
  38. package/dist/core/runner/prepare-acquire-reconcile.js +1 -3
  39. package/dist/core/runner/prepare-file-history.d.ts +13 -15
  40. package/dist/core/runner/prepare-file-history.js +57 -43
  41. package/dist/core/runner/prepare-hands-readface.d.ts +2 -2
  42. package/dist/core/runner/prepare-listings.d.ts +3 -1
  43. package/dist/core/runner/prepare-listings.js +2 -2
  44. package/dist/core/runner/prepare-memory.js +16 -5
  45. package/dist/core/runner/prepare-park-ask.js +27 -16
  46. package/dist/core/runner/prepare-prompt-inputs.d.ts +8 -1
  47. package/dist/core/runner/prepare-prompt-inputs.js +5 -3
  48. package/dist/core/runner/prepare-task.js +7 -5
  49. package/dist/core/runner/prepare-wiring-manifest.d.ts +1 -0
  50. package/dist/core/runner/prepare-wiring-manifest.js +2 -1
  51. package/dist/core/runner/run-attachment-seats.js +1 -1
  52. package/dist/core/runner/run-leg.js +61 -52
  53. package/dist/core/runner/run-settle-and-teardown.js +4 -0
  54. package/dist/core/runner/run-turn-boundary.js +6 -6
  55. package/dist/core/runner/turn-attachments.d.ts +6 -6
  56. package/dist/core/runner-deps.d.ts +3 -1
  57. package/dist/core/secret-env.d.ts +12 -1
  58. package/dist/core/secret-env.js +27 -2
  59. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +2 -0
  60. package/dist/core/store-contracts/checkpoint-store-contract.js +22 -0
  61. package/dist/core/task-result.d.ts +1 -1
  62. package/dist/core/task-spec.d.ts +17 -8
  63. package/dist/core/tool-spec.d.ts +1 -1
  64. package/dist/core/wiring-manifest.d.ts +10 -0
  65. package/dist/core/wiring-manifest.js +7 -1
  66. package/dist/engine/execution-env/node-execution-env.d.ts +6 -0
  67. package/dist/engine/execution-env/node-execution-env.js +15 -17
  68. package/dist/engine/harness/agent-harness.js +7 -5
  69. package/dist/engine/lsp/node-lsp-manager.d.ts +4 -0
  70. package/dist/engine/lsp/node-lsp-manager.js +10 -4
  71. package/dist/index.d.ts +1 -1
  72. package/dist/index.js +1 -1
  73. package/dist/tools/fs/shell-read-plan.d.ts +2 -2
  74. package/dist/tools/fs/shell-read-plan.js +15 -10
  75. package/dist/tools/fs/shell-read-word.d.ts +5 -4
  76. package/dist/tools/fs/shell-read-word.js +3 -2
  77. package/package.json +1 -1
  78. package/test/export-surface.snapshot.json +7 -1
  79. package/wire-changes/core@7.31.0.json +705 -0
  80. package/wire-changes/core@7.31.1.json +189 -0
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { closeSync, constants as fsConstants, existsSync, fchmodSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { accessSync, closeSync, constants as fsConstants, existsSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join, relative, sep } from "node:path";
4
4
  import { uuidv7 } from "../../internal/harness.js";
5
5
  import { MAX_MEMORY_BYTES, composeMemoryBlock, firstSentence } from "../memory.js";
@@ -11,12 +11,13 @@ import { MEMORY_PROVENANCE_RECALL_SENTENCE, memoryExposureIndexRow, parseMemoryE
11
11
  import { isInstructionEntry } from "./header-hints.js";
12
12
  import { ConsolidationRefusedError, CONSOLIDATION_FORCE_THROTTLE_FLOOR_MS, CONSOLIDATION_SEAT_FREEZE_GRACE_MS, consolidationTypeEligible, deriveProductSlug, deriveSupersededSet, isValidConsolidationPlanId, listConsolidationPlanIds, productAddPatch, quarantineCorruptPlan, readConsolidationGateFile, readConsolidationPlan, readIntentCredentials, recordActiveSetBaseline, recordConsolidationSession, sessionsSinceExcludingCurrent, recordIntentCredentials, scanRefusalCeiling, screenConsolidationOptions, supersessionFuseCeiling, updateConsolidationGate, writeConsolidationPlan, } from "./consolidation.js";
13
13
  import { deriveShadowedSet } from "./shadowing.js";
14
- import { classifySessionSettlements, clearHoldResolution, closeSessionAccount, disposeHold, effectiveSettlements, expireOverdueSettlements, foreignDanglingSessionAccounts, markHoldReleased, openInstructionHold, openSessionAccount, readHoldCustody, readHolds, reconcileHolds, replayExternalSettlementEffects, resolveHoldRecord, resolveSessionAccountRecord, resolveSettlementRecord, sessionSettlements, sessionUnattributedSet, emptyUnattributedSet, convertV1SidecarKeys, } from "./delegation-settlement.js";
14
+ import { classifySessionSettlements, clearHoldResolution, closeSessionAccount, disposeHold, effectiveSettlements, expireOverdueSettlements, foreignDanglingSessionAccounts, openLayerWriters, terminateSessionAccount, sessionAccountLeg, markHoldReleased, openInstructionHold, openSessionAccount, readHoldCustody, readHolds, reconcileHolds, replayExternalSettlementEffects, resolveHoldRecord, resolveSessionAccountRecord, resolveSettlementRecord, sessionSettlements, sessionUnattributedSet, emptyUnattributedSet, convertV1SidecarKeys, } from "./delegation-settlement.js";
15
15
  import { noReplaceRestore, retireStagingToCustody, stagingPathBeside } from "./delegation-settlement.js";
16
+ import { landedUndecided, layerDigest, layerObservationLookup, readOnlyLayerCandidates, readOnlyLayerCensus, recordLayerObservations, settleUndecidedLayerObservations, observationBacked, sameObservation, undecidedLayerObservations } from "./layer-observations.js";
16
17
  import { markOriginClearanceTombstoned, openOriginClearance, readOriginClearances, settleOriginClearance } from "./origin-clearance.js";
17
18
  import { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, canonicalJsonStringify, captureErasureInput, erasureRequestInvalid, erasureSelectHash, scanEntryFiles, } from "./file-backend.js";
18
19
  import { assembleMemoryExportBundle, computeMemoryBundleHash, memoryBundleInvalid, } from "./export-bundle.js";
19
- import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, restoreContainedBaseline, readIndexRevs, writeIndexRevs, bumpScanFuse, scanFuseKey, canonicalize, adoptRootScopeLayout, clearScanFuse, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, markSessionPolluted, readSessionPollution, fileSessionCaptureRecordStore, recordRetrievedAccount, writeFileNoFollow, readRetrievedAccount, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirFor, appendChallengeEvents, appendLineageAudit, rebuildStrictControlPlaneLedger, isStrictControlPlaneLedgerCorrupt, CHALLENGE_LEDGER_MAX_EVENTS, adjudicateLineagePending, challengedEntryIds, clearLineageForEntries, discardLineagePending, lineageAccountOfEntry, lineageContributionsOfSession, lineageLatchedIds, promoteLineagePending, readChallengeEvents, readChallengedHistory, readLineageRecord, recordChallengedHistory, recordLineageCredential, reconcileLineage, readProjectionDebts, resolveChallengeEvent, settleProjectionDebts, stageLineagePending, stageProjectionDebts, } from "./layout.js";
20
+ import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, restoreContainedBaseline, readIndexRevs, writeIndexRevs, bumpScanFuse, scanFuseKey, canonicalize, landingPathOf, storeTargetEscape, TARGET_OUTSIDE_STORE, adoptRootScopeLayout, clearScanFuse, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, markSessionPolluted, readSessionPollution, fileSessionCaptureRecordStore, recordRetrievedAccount, writeFileNoFollow, withPermissionRemedy, permissionRemedyOf, readRetrievedAccount, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirFor, appendChallengeEvents, appendLineageAudit, rebuildStrictControlPlaneLedger, isStrictControlPlaneLedgerCorrupt, CHALLENGE_LEDGER_MAX_EVENTS, adjudicateLineagePending, challengedEntryIds, clearLineageForEntries, discardLineagePending, lineageAccountOfEntry, lineageContributionsOfSession, lineageLatchedIds, promoteLineagePending, readChallengeEvents, readChallengedHistory, readLineageRecord, recordChallengedHistory, recordLineageCredential, reconcileLineage, readProjectionDebts, resolveChallengeEvent, settleProjectionDebts, stageLineagePending, stageProjectionDebts, } from "./layout.js";
20
21
  import { createSessionIncarnationSeat, recalledForAnyIdentity, recalledForIdentity, rememberForIdentity } from "./session-incarnation.js";
21
22
  import { scanMemoryFileName, scanMemoryWrite, scanRemediation } from "./scan.js";
22
23
  import { writableRootOf } from "./types.js";
@@ -301,9 +302,15 @@ export function memoryConsolidationRefusedNotice(input) {
301
302
  },
302
303
  };
303
304
  }
305
+ const INDEX_WRITE_REFUSED = "the derived memory index could NOT be written";
306
+ function unexpectedLayerFile(handle, c) {
307
+ return { path: relative(handle.memoryDir, c.path), code: "readonly_layer", reason: "a file appeared in a read-only inherited memory layer during this session — it is recorded as an unexpected write and never adopted into memory" };
308
+ }
309
+ const STALE_LEG_NOTE = "memory session account: this harvest belongs to an earlier leg of the session (a later leg re-opened the account while it ran) — its account close and leg end were not written over the later leg";
304
310
  export class MemoryEngine {
305
311
  backend;
306
312
  memoryDir;
313
+ mountedRoot;
307
314
  controlDir;
308
315
  now;
309
316
  perScopeBudgetBytes;
@@ -315,6 +322,7 @@ export class MemoryEngine {
315
322
  holdSettleTimeoutMs;
316
323
  provenance;
317
324
  onIncident;
325
+ unsettleableDisclosed = false;
318
326
  backendPinnedRoot;
319
327
  constructor(opts) {
320
328
  if (opts.provenance !== undefined && opts.provenance !== "off" && opts.provenance !== "carry") {
@@ -360,6 +368,18 @@ export class MemoryEngine {
360
368
  return this.provenance;
361
369
  }
362
370
  consolidation;
371
+ endSessionLeg(handle, sessionId) {
372
+ return handle.accountLeg !== undefined ? this.endLeg(sessionId, handle.accountLeg) : undefined;
373
+ }
374
+ endLeg(sessionId, leg) {
375
+ try {
376
+ return terminateSessionAccount(this.controlDir, { sessionId, now: this.now, leg });
377
+ }
378
+ catch (err) {
379
+ this.discloseSessionAccountIncident("the end of this session's leg could not be recorded — it keeps reading as a live writer of its layer (read-only restores there are withheld)", err);
380
+ return undefined;
381
+ }
382
+ }
363
383
  discloseSessionAccountIncident(what, cause) {
364
384
  const sink = this.onIncident;
365
385
  if (sink === undefined)
@@ -372,6 +392,209 @@ export class MemoryEngine {
372
392
  catch {
373
393
  }
374
394
  }
395
+ async detectReadOnlyLayerChanges(handle, report, admitsNothing) {
396
+ const candidates = [];
397
+ for (const m of handle.materialized.filter((x) => x.readonly)) {
398
+ const text = readSafe(m.path);
399
+ if (text === undefined) {
400
+ report.missing.push(m.relPath);
401
+ continue;
402
+ }
403
+ if (revOfText(text, m.id) === m.rev)
404
+ continue;
405
+ if ((await this.committedContentUnadopted(m.id)) === text)
406
+ continue;
407
+ const stood = handle.layerBaseline?.get(m.path) === layerDigest(text);
408
+ const rejection = admitsNothing
409
+ ? { path: m.relPath, code: "readonly_layer", reason: `a read-only inherited memory layer's file ${stood ? "differed from its committed content when this session started (not this session's write)" : "differs from what this session was served"}; this session admits nothing, so it was not restored — the bytes stay on disk for a session that may write to adjudicate` }
410
+ : { path: m.relPath, code: "readonly_layer", reason: stood ? "a read-only inherited memory layer's file differed from its committed content when this session started (not this session's write)" : "a read-only inherited memory layer was modified; changes to inherited layers are not writable from this session" };
411
+ report.rejections.push(rejection);
412
+ if (!admitsNothing)
413
+ candidates.push({ path: m.path, relPath: m.relPath, scope: m.scope, id: m.id, stood, rejection });
414
+ }
415
+ return candidates;
416
+ }
417
+ async committedContentUnadopted(id) {
418
+ const zeroCopy = this.backendPinnedRoot !== undefined && canonicalize(this.backendPinnedRoot) === canonicalize(this.memoryDir);
419
+ const view = zeroCopy ? undefined : this.backend.restrictedAdoptionView?.({ audit: false });
420
+ if (view === undefined)
421
+ return this.committedContentFor(id);
422
+ try {
423
+ const [entry] = await view.getByIds([id]);
424
+ if (entry !== undefined && entry.id === id)
425
+ return serializeEntryFile(entry);
426
+ }
427
+ catch {
428
+ }
429
+ return this.backend.readCommittedShadow?.(id) ?? readSafe(join(this.controlDir, "shadow", `${id}.md`));
430
+ }
431
+ async restoreReadOnlyLayers(candidates, report, selfLeg, root) {
432
+ let writers;
433
+ for (const c of candidates) {
434
+ const text = readSafe(c.path);
435
+ const committed = await this.committedContentUnadopted(c.id);
436
+ if (text !== undefined && text === committed) {
437
+ report.rejections = report.rejections.filter((r) => r !== c.rejection);
438
+ continue;
439
+ }
440
+ if (committed === undefined)
441
+ continue;
442
+ try {
443
+ writers ??= openLayerWriters(this.controlDir, selfLeg);
444
+ }
445
+ catch {
446
+ writers = { scopes: new Set(), unscoped: 1 };
447
+ }
448
+ if (writers.unscoped > 0 || writers.scopes.has(c.scope)) {
449
+ c.rejection.reason = `a read-only inherited memory layer's file ${c.stood ? "differed from its committed content when this session started (not this session's write)" : "differs from what this session was served"}; a session that writes this layer is open (the bytes may be its uncommitted edit), so it was not restored`;
450
+ report.warnings.push(`read-only layer file ${quoteId(c.relPath)} was not restored: a session that writes this layer is open, and the bytes may be its uncommitted edit`);
451
+ continue;
452
+ }
453
+ try {
454
+ this.refuseOutsideStore(root, c.path, "restoring the read-only layer file");
455
+ writeFileNoFollow(c.path, committed);
456
+ report.restored.push(c.relPath);
457
+ }
458
+ catch (err) {
459
+ report.warnings.push(`read-only layer file ${quoteId(c.relPath)} could NOT be restored to its committed content — the modified bytes are still on disk: ${this.writeFailureText(err)}`);
460
+ }
461
+ }
462
+ }
463
+ async observeReadOnlyLayers(handle, report, sessionId) {
464
+ const served = new Set(handle.materialized.filter((m) => m.readonly).map((m) => m.path));
465
+ const batch = { rows: [], rejections: [], undecided: [], landed: false };
466
+ for (const c of readOnlyLayerCandidates(this.memoryDir, this.controlDir, handle)) {
467
+ const by = { scope: c.scope, rel: c.rel, ...(sessionId !== undefined ? { sessionId } : {}), observedAt: this.now() };
468
+ if (c.id === undefined) {
469
+ batch.rows.push({ ...by, observedRev: c.digest });
470
+ }
471
+ else {
472
+ const committed = await this.committedRowOf(c.id);
473
+ if (committed === "undecidable") {
474
+ const row = { ...by, entryId: c.id, observedRev: c.observedRev, undecided: true, observationId: uuidv7() };
475
+ batch.rows.push(row);
476
+ batch.undecided.push({ candidate: c, row });
477
+ continue;
478
+ }
479
+ if (committed !== undefined && committed.scope === c.scope && committed.slug === c.slug && committed.rev === c.observedRev)
480
+ continue;
481
+ batch.rows.push({ ...by, entryId: c.id, ...(committed !== undefined ? { rev: committed.rev } : {}), observedRev: c.observedRev });
482
+ }
483
+ if (!served.has(c.path)) {
484
+ batch.rejections.push(unexpectedLayerFile(handle, c));
485
+ }
486
+ }
487
+ this.landObservations(batch, report);
488
+ return batch;
489
+ }
490
+ landObservations(batch, report) {
491
+ try {
492
+ recordLayerObservations(this.controlDir, batch.rows);
493
+ }
494
+ catch (err) {
495
+ batch.failure = this.writeFailureText(err);
496
+ return;
497
+ }
498
+ batch.landed = true;
499
+ delete batch.failure;
500
+ report.rejections.push(...batch.rejections);
501
+ }
502
+ concludeObservations(batch, report) {
503
+ if (batch !== undefined && !batch.landed) {
504
+ report.warnings.push(`${batch.rows.length} unexpected write(s) into read-only memory layers could NOT be recorded — a later session's read may adopt them: ${batch.failure ?? "unknown failure"}`);
505
+ }
506
+ this.discloseUndecidedObservations(landedUndecided(batch).map((u) => u.candidate), report);
507
+ }
508
+ discloseUndecidedObservations(undecided, report) {
509
+ for (const c of undecided) {
510
+ report.warnings.push(`${quoteId(relative(this.memoryDir, c.path))}: changed in a read-only memory layer during this session, and whether a commit backs it could not be established (a memory transaction was pending, or the committed account could not be read) — recorded as undecided (not adopted), settled once a writing session can read the committed account`);
511
+ }
512
+ }
513
+ async settleUndecidedObservations(handle, report, own) {
514
+ let rows;
515
+ try {
516
+ rows = undecidedLayerObservations(this.controlDir);
517
+ }
518
+ catch (err) {
519
+ report.warnings.push(`undecided read-only-layer observations could not be read for settlement — they stay undecided (not adopted): ${this.writeFailureText(err)}`);
520
+ return;
521
+ }
522
+ const verdicts = new Map();
523
+ for (const row of rows) {
524
+ const committed = row.entryId !== undefined ? await this.committedRowOf(row.entryId) : "undecidable";
525
+ if (committed !== "undecidable")
526
+ verdicts.set(row, observationBacked(row, committed) ? "backed" : committed !== undefined ? { rev: committed.rev } : {});
527
+ }
528
+ try {
529
+ settleUndecidedLayerObservations(this.controlDir, verdicts);
530
+ }
531
+ catch (err) {
532
+ report.warnings.push(`undecided read-only-layer observations could not be settled — they stay undecided (not adopted): ${this.writeFailureText(err)}`);
533
+ return;
534
+ }
535
+ const served = new Set(handle.materialized.filter((m) => m.readonly).map((m) => m.path));
536
+ const stillUndecided = [];
537
+ for (const { candidate: c, row: mine } of own) {
538
+ const row = rows.find((r) => sameObservation(r, mine));
539
+ if (row === undefined) {
540
+ const refused = served.has(c.path) ? false : await this.refusedNow(mine.entryId, c.observedRev);
541
+ if (refused === true)
542
+ report.rejections.push(unexpectedLayerFile(handle, c));
543
+ else if (refused === undefined)
544
+ report.warnings.push(`${quoteId(relative(this.memoryDir, c.path))}: changed in a read-only memory layer during this session; another writing session settled its record first, and whether memory refuses these bytes now could not be read`);
545
+ continue;
546
+ }
547
+ const verdict = verdicts.get(row);
548
+ if (verdict === undefined)
549
+ stillUndecided.push(c);
550
+ else if (verdict !== "backed" && !served.has(c.path))
551
+ report.rejections.push(unexpectedLayerFile(handle, c));
552
+ }
553
+ this.discloseUndecidedObservations(stillUndecided, report);
554
+ }
555
+ async refusedNow(entryId, observedRev) {
556
+ const committed = entryId !== undefined ? await this.committedRowOf(entryId) : "undecidable";
557
+ if (entryId === undefined || committed === "undecidable")
558
+ return undefined;
559
+ try {
560
+ return layerObservationLookup(this.controlDir).find(entryId, committed?.rev, observedRev) !== undefined;
561
+ }
562
+ catch {
563
+ return undefined;
564
+ }
565
+ }
566
+ discloseUnsettleableObservations() {
567
+ if (this.unsettleableDisclosed || typeof this.backend.committedSnapshotOf === "function")
568
+ return;
569
+ this.unsettleableDisclosed = true;
570
+ const sink = this.onIncident;
571
+ if (sink === undefined)
572
+ return;
573
+ try {
574
+ const e = new Error("memory read-only layers: this backend has no committed-snapshot face (committedSnapshotOf), so whether a commit backs a change observed in a read-only layer cannot be established — every such change is recorded as undecided (not adopted), and undecided observations cannot settle on this backend");
575
+ e.code = "memory.observation_settlement_unavailable";
576
+ sink(e);
577
+ }
578
+ catch {
579
+ }
580
+ }
581
+ async committedRowOf(id) {
582
+ const face = this.backend.committedSnapshotOf;
583
+ if (face === undefined)
584
+ return "undecidable";
585
+ try {
586
+ const snap = await face.call(this.backend, id);
587
+ if (snap.state !== "row")
588
+ return undefined;
589
+ return { rev: snap.rev, ...(snap.binding.state === "bound" ? { scope: snap.binding.scope, slug: snap.binding.slug } : {}) };
590
+ }
591
+ catch {
592
+ return "undecidable";
593
+ }
594
+ }
595
+ writeFailureText(err) {
596
+ return thrownText(withPermissionRemedy(err, this.memoryDir));
597
+ }
375
598
  discloseAnnounceFailure(stage, cause) {
376
599
  const sink = this.onIncident;
377
600
  if (sink === undefined)
@@ -534,6 +757,7 @@ export class MemoryEngine {
534
757
  const indexPath = join(writableRoot, MEMORY_INDEX_FILENAME);
535
758
  const baseline = handle.planeBaseline !== undefined ? handle.planeBaseline.index : handle.indexText;
536
759
  try {
760
+ this.refuseOutsideStore(handle.memoryDir, indexPath, "sweeping the index");
537
761
  const first = existsSync(indexPath) ? readSafe(indexPath) : undefined;
538
762
  if (first !== undefined && first !== baseline) {
539
763
  const onDisk = readSafe(indexPath);
@@ -557,7 +781,7 @@ export class MemoryEngine {
557
781
  }
558
782
  }
559
783
  catch (err) {
560
- out.failures.push({ path: MEMORY_INDEX_FILENAME, detail: `index sweep failed: ${thrownText(err)}` });
784
+ out.failures.push({ path: MEMORY_INDEX_FILENAME, detail: `index sweep failed: ${this.writeFailureText(err)}` });
561
785
  }
562
786
  }
563
787
  const scanned = scanEntryFiles(writableRoot, {
@@ -726,6 +950,14 @@ export class MemoryEngine {
726
950
  return this.challengeAndAnnounce(ids.map((entryId) => ({ eventId: `${requestId}:${entryId}`, entryId, reason })));
727
951
  }
728
952
  async eraseMemoryEntries(input) {
953
+ try {
954
+ return await this.eraseMemoryEntriesCore(input);
955
+ }
956
+ catch (err) {
957
+ throw withPermissionRemedy(err, this.memoryDir);
958
+ }
959
+ }
960
+ async eraseMemoryEntriesCore(input) {
729
961
  const captured = captureErasureInput(input);
730
962
  const bad = erasureRequestInvalid(captured);
731
963
  if (bad !== undefined) {
@@ -859,6 +1091,7 @@ export class MemoryEngine {
859
1091
  if (kept.length === lines.length)
860
1092
  continue;
861
1093
  try {
1094
+ this.refuseOutsideStore(landingPathOf(this.memoryDir), indexPath, "rewriting the index");
862
1095
  writeFileNoFollow(indexPath, kept.join("\n"));
863
1096
  }
864
1097
  catch {
@@ -867,7 +1100,7 @@ export class MemoryEngine {
867
1100
  }
868
1101
  }
869
1102
  catch (err) {
870
- uncleared.push(`${join(this.memoryDir, "<scope homes>", MEMORY_INDEX_FILENAME)} (index sweep failed: ${thrownText(err)})`);
1103
+ uncleared.push(`${join(this.memoryDir, "<scope homes>", MEMORY_INDEX_FILENAME)} (index sweep failed: ${this.writeFailureText(err)})`);
871
1104
  }
872
1105
  if (uncleared.length > 0) {
873
1106
  try {
@@ -1168,12 +1401,30 @@ export class MemoryEngine {
1168
1401
  adoptRootScopeLayout(this.memoryDir, this.controlDir, { ownerCandidate: writeScope, onIncident: this.onIncident, convertSidecars: convertV1SidecarKeys });
1169
1402
  }
1170
1403
  async materialize(scopes, writeScope, opts) {
1404
+ const legSeat = {};
1405
+ try {
1406
+ return await this.materializeCore(scopes, writeScope, opts, legSeat);
1407
+ }
1408
+ catch (err) {
1409
+ if (legSeat.leg !== undefined && opts?.sessionId !== undefined)
1410
+ this.endLeg(opts.sessionId, legSeat.leg);
1411
+ throw withPermissionRemedy(err, this.memoryDir);
1412
+ }
1413
+ }
1414
+ async materializeCore(scopes, writeScope, opts, legSeat) {
1171
1415
  ensureDirExists(this.memoryDir);
1416
+ const physicalRoot = landingPathOf(this.memoryDir);
1417
+ this.mountedRoot ??= physicalRoot;
1418
+ if (physicalRoot !== this.mountedRoot) {
1419
+ const e = new Error(`${TARGET_OUTSIDE_STORE}: the memory dir ${this.memoryDir} now resolves to ${physicalRoot}, not ${this.mountedRoot} where this engine first mounted it — refused; open a new engine on the directory you mean`);
1420
+ e.code = TARGET_OUTSIDE_STORE;
1421
+ throw e;
1422
+ }
1172
1423
  ensureDirExists(this.controlDir);
1173
1424
  await this.adoptLayoutBeforeMount(writeScope);
1174
- chmodNoFollow(this.memoryDir, 0o755);
1175
1425
  const restricted = opts?.adoptionRestricted === true;
1176
1426
  let sessionAccountFailed = false;
1427
+ let accountLeg;
1177
1428
  const readBackend = restricted
1178
1429
  ? (this.backend.restrictedAdoptionView?.({ audit: true, writeScope }) ?? this.backend)
1179
1430
  : this.backend;
@@ -1182,9 +1433,13 @@ export class MemoryEngine {
1182
1433
  scopeDirs.set(scope, registerScope(this.memoryDir, this.controlDir, scope));
1183
1434
  if (writeScope !== null && !scopeDirs.has(writeScope))
1184
1435
  scopeDirs.set(writeScope, registerScope(this.memoryDir, this.controlDir, writeScope));
1185
- const sweepAnnounced = new Set();
1186
1436
  for (const dir of scopeDirs.values())
1187
- this.chmodScopeTree(dir, 0o755, 0o644, { announced: sweepAnnounced });
1437
+ this.censusScopeTree(dir);
1438
+ const layerBaseline = this.backendPinnedRoot !== undefined && canonicalize(this.backendPinnedRoot) === canonicalize(this.memoryDir)
1439
+ ? readOnlyLayerCensus([...scopeDirs].filter(([scope]) => scope !== writeScope).map(([, dir]) => dir))
1440
+ : undefined;
1441
+ if (layerBaseline !== undefined)
1442
+ this.discloseUnsettleableObservations();
1188
1443
  if (this.provenance === "carry" && opts?.sessionId !== undefined && writeScope !== null && !restricted) {
1189
1444
  let accountFailed = false;
1190
1445
  try {
@@ -1300,10 +1555,19 @@ export class MemoryEngine {
1300
1555
  }
1301
1556
  }
1302
1557
  }
1303
- openSessionAccount(this.controlDir, { sessionId: opts.sessionId, now: this.now, unattributed });
1558
+ accountLeg = legSeat.leg = uuidv7();
1559
+ openSessionAccount(this.controlDir, { sessionId: opts.sessionId, now: this.now, unattributed, writeScope, leg: accountLeg });
1304
1560
  }
1305
1561
  catch (err) {
1306
1562
  accountFailed = true;
1563
+ if (accountLeg !== undefined) {
1564
+ try {
1565
+ if (sessionAccountLeg(this.controlDir, opts.sessionId) !== accountLeg)
1566
+ accountLeg = legSeat.leg = undefined;
1567
+ }
1568
+ catch {
1569
+ }
1570
+ }
1307
1571
  this.discloseSessionAccountIncident("crash-residue attribution is degraded for this session (the harvest boundary refuses fail-closed while this stands)", err);
1308
1572
  }
1309
1573
  if (accountFailed)
@@ -1334,10 +1598,13 @@ export class MemoryEngine {
1334
1598
  ...(opts?.shadowNames !== undefined ? { shadowNames: opts.shadowNames } : {}),
1335
1599
  ...(restricted ? { adoptionRestricted: true } : {}),
1336
1600
  ...(sessionAccountFailed ? { sessionAccountFailed: true } : {}),
1601
+ ...(layerBaseline !== undefined ? { layerBaseline } : {}),
1602
+ ...(accountLeg !== undefined ? { accountLeg } : {}),
1337
1603
  };
1338
1604
  if (handle.writableRoot !== null)
1339
1605
  ensureDirExists(handle.writableRoot);
1340
1606
  const zeroCopy = this.backendPinnedRoot !== undefined && canonicalize(this.backendPinnedRoot) === canonicalize(this.memoryDir);
1607
+ const projectionRoot = zeroCopy ? undefined : this.mountedRoot;
1341
1608
  for (const [scope, dir] of scopeDirs) {
1342
1609
  const scopeEntries = (byScope.get(scope) ?? []).slice();
1343
1610
  scopeEntries.sort((a, b) => (mtimeById.get(b.id) ?? 0) - (mtimeById.get(a.id) ?? 0));
@@ -1359,8 +1626,8 @@ export class MemoryEngine {
1359
1626
  const { deleted: _stubDrop, ...stubFm } = entry.frontmatter;
1360
1627
  const text = stub ? serializeEntryFile({ id: entry.id, frontmatter: stubFm, body: STUB_ARCHIVED_LINE }) : fullText;
1361
1628
  const target = join(dir, `${entry.slug}.md`);
1362
- if (!(zeroCopy && restricted))
1363
- this.writeIfChanged(target, text);
1629
+ if (projectionRoot !== undefined)
1630
+ this.writeIfChanged(target, text, projectionRoot);
1364
1631
  const onDiskRev = stub ? revOfText(text, entry.id) : entry.rev;
1365
1632
  const path = canonicalize(target);
1366
1633
  handle.materialized.push({
@@ -1444,6 +1711,15 @@ export class MemoryEngine {
1444
1711
  catch {
1445
1712
  }
1446
1713
  }
1714
+ const indexRefusals = materializeHealWarnings.filter((w) => w.startsWith(INDEX_WRITE_REFUSED));
1715
+ if (indexRefusals.length > 0) {
1716
+ try {
1717
+ enqueueMemoryAnnouncement(this.controlDir, { kind: "gate", at: this.now(), items: capItems(indexRefusals.map((w) => inlineUntrusted(w, 500))) });
1718
+ }
1719
+ catch (err) {
1720
+ this.discloseAnnounceFailure("materialize index-write enqueue", err);
1721
+ }
1722
+ }
1447
1723
  if (indexWrite && handle.indexOnDiskUntrusted !== true) {
1448
1724
  const dropNotes = materializeHealWarnings.filter((w) => w.includes("opaque external-origin handle"));
1449
1725
  if (dropNotes.length > 0) {
@@ -1455,16 +1731,20 @@ export class MemoryEngine {
1455
1731
  }
1456
1732
  }
1457
1733
  }
1458
- for (const [scope, dir] of scopeDirs) {
1459
- if (scope === writeScope)
1460
- continue;
1461
- this.chmodScopeTree(dir, 0o555, 0o444, {
1462
- announced: sweepAnnounced,
1463
- });
1464
- }
1465
1734
  if (handle.writableRoot !== null) {
1466
1735
  const writableRoot = handle.writableRoot;
1467
1736
  const files = new Map();
1737
+ const refused = new Map();
1738
+ const probeWritable = (p) => {
1739
+ try {
1740
+ accessSync(p, fsConstants.W_OK);
1741
+ }
1742
+ catch (err) {
1743
+ if (err.code !== "ENOENT" && !refused.has(p))
1744
+ refused.set(p, err);
1745
+ }
1746
+ };
1747
+ probeWritable(writableRoot);
1468
1748
  for (const f of scanEntryFiles(writableRoot, { maxDepth: this.maxDepth })) {
1469
1749
  const canonical = canonicalize(f.path);
1470
1750
  if (canonical !== writableRoot && !canonical.startsWith(`${writableRoot}${sep}`))
@@ -1472,6 +1752,19 @@ export class MemoryEngine {
1472
1752
  const text = readSafe(canonical);
1473
1753
  if (text !== undefined)
1474
1754
  files.set(canonical, planeDigest(text));
1755
+ probeWritable(dirname(canonical));
1756
+ probeWritable(canonical);
1757
+ }
1758
+ const [firstRefused] = refused;
1759
+ if (firstRefused !== undefined) {
1760
+ const [path, err] = firstRefused;
1761
+ const remedy = permissionRemedyOf(Object.assign(new Error("write refused"), { code: err?.code, path }), this.memoryDir);
1762
+ try {
1763
+ enqueueMemoryAnnouncement(this.controlDir, { kind: "gate", at: this.now(), items: [inlineUntrusted(`this session's memory write home cannot be written at ${refused.size} path(s) (${path}${refused.size > 1 ? ", …" : ""}): ${remedy ?? thrownText(err)}`, 700)] });
1764
+ }
1765
+ catch (err) {
1766
+ this.discloseAnnounceFailure("write-home permission enqueue", err);
1767
+ }
1475
1768
  }
1476
1769
  const indexOnDisk = readSafe(join(writableRoot, MEMORY_INDEX_FILENAME));
1477
1770
  handle.planeBaseline = { files, ...(indexOnDisk !== undefined ? { index: indexOnDisk } : {}) };
@@ -1524,14 +1817,17 @@ export class MemoryEngine {
1524
1817
  };
1525
1818
  }
1526
1819
  gateWrite(handle, canonicalPath, content) {
1527
- const root = handle.writableRoot ?? handle.memoryDir;
1528
- if (canonicalPath !== root && !canonicalPath.startsWith(`${root}${sep}`))
1820
+ const inside = (root) => canonicalPath === root || canonicalPath.startsWith(`${root}${sep}`);
1821
+ if (!inside(handle.memoryDir))
1529
1822
  return { ok: true };
1530
- if (handle.writeScope === null) {
1823
+ const root = handle.writableRoot;
1824
+ if (handle.writeScope === null || root === null || !inside(root)) {
1531
1825
  return {
1532
1826
  ok: false,
1533
1827
  code: "read_only_layering",
1534
- reason: "this session's memory is read-only (no write scope) — the engine does not accept writes into the memory domain. Nothing was written.",
1828
+ reason: handle.writeScope === null || root === null
1829
+ ? "this session's memory is read-only (no write scope) — the engine does not accept writes into the memory domain. Nothing was written."
1830
+ : `this path is in a read-only inherited memory layer — this session writes memory only under ${root}. Nothing was written.`,
1535
1831
  muted: false,
1536
1832
  };
1537
1833
  }
@@ -1555,7 +1851,18 @@ export class MemoryEngine {
1555
1851
  return { ok: false, code: f.code, reason, muted };
1556
1852
  }
1557
1853
  async harvest(handle, opts) {
1558
- const report = await this.harvestCore(handle, opts);
1854
+ const out = {};
1855
+ try {
1856
+ out.report = await this.harvestCore(handle, opts);
1857
+ }
1858
+ catch (err) {
1859
+ throw withPermissionRemedy(err, this.memoryDir);
1860
+ }
1861
+ finally {
1862
+ if (opts?.phase === "terminal" && opts.sessionId !== undefined && this.endSessionLeg(handle, opts.sessionId) === "stale" && out.report !== undefined && !out.report.warnings.includes(STALE_LEG_NOTE))
1863
+ out.report.warnings.push(STALE_LEG_NOTE);
1864
+ }
1865
+ const report = out.report;
1559
1866
  try {
1560
1867
  const named = report.reportId !== undefined ? { reportId: report.reportId } : {};
1561
1868
  const gateItems = gateAnnouncementItems(report);
@@ -1606,7 +1913,16 @@ export class MemoryEngine {
1606
1913
  const writeScope = handle.writeScope;
1607
1914
  const captureRead = opts?.sessionId !== undefined ? await this.sessionCaptureOptOutOrFault(opts.sessionId) : { fault: false };
1608
1915
  const captureOptOut = captureRead.record;
1609
- if (writeScope === null || opts?.admitNothing !== undefined || captureOptOut !== undefined || captureRead.fault) {
1916
+ const admitsNothing = writeScope === null || opts?.admitNothing !== undefined || captureOptOut !== undefined || captureRead.fault;
1917
+ const readOnlyRestorable = await this.detectReadOnlyLayerChanges(handle, report, admitsNothing);
1918
+ let observed;
1919
+ try {
1920
+ observed = await this.observeReadOnlyLayers(handle, report, opts?.sessionId);
1921
+ }
1922
+ catch (err) {
1923
+ report.warnings.push(`read-only memory layers could not be examined for unexpected writes (nothing was recorded): ${this.writeFailureText(err)}`);
1924
+ }
1925
+ if (admitsNothing) {
1610
1926
  try {
1611
1927
  this.backend.auditRestrictedDivergence?.([...new Set([...handle.scopes, ...(writeScope !== null ? [writeScope] : [])])], (handle.adoptionRestricted === true || captureOptOut !== undefined) && writeScope !== null ? { idlessWriteScope: writeScope } : undefined);
1612
1928
  }
@@ -1627,9 +1943,10 @@ export class MemoryEngine {
1627
1943
  }
1628
1944
  }
1629
1945
  catch (err) {
1630
- report.warnings.push(`capture opt-out boundary sweep failed: ${thrownText(err)} — residue on the writable memory root may await a later session's adoption`);
1946
+ report.warnings.push(`capture opt-out boundary sweep failed: ${this.writeFailureText(err)} — residue on the writable memory root may await a later session's adoption`);
1631
1947
  }
1632
1948
  }
1949
+ this.concludeObservations(observed, report);
1633
1950
  if (opts?.admitNothing !== undefined)
1634
1951
  report.warnings.push(opts.admitNothing.reason);
1635
1952
  else if (captureOptOut !== undefined) {
@@ -1643,6 +1960,7 @@ export class MemoryEngine {
1643
1960
  if (carry && handle.sessionAccountFailed === true) {
1644
1961
  report.ok = false;
1645
1962
  report.incident = { kind: "sidecar_corrupt", detail: "memory session account could not be opened at materialize — the crash-residue classification for this session never persisted, so the harvest refuses fail-closed (repair the control plane and re-materialize)" };
1963
+ this.concludeObservations(observed, report);
1646
1964
  return report;
1647
1965
  }
1648
1966
  try {
@@ -1650,9 +1968,15 @@ export class MemoryEngine {
1650
1968
  }
1651
1969
  catch (err) {
1652
1970
  report.ok = false;
1653
- report.incident = { kind: "sidecar_corrupt", detail: `memory control plane refused: ${thrownText(err)}` };
1971
+ report.incident = { kind: "sidecar_corrupt", detail: `memory control plane refused: ${this.writeFailureText(err)}` };
1972
+ this.concludeObservations(observed, report);
1654
1973
  return report;
1655
1974
  }
1975
+ if (observed !== undefined && !observed.landed)
1976
+ this.landObservations(observed, report);
1977
+ if (observed !== undefined && !observed.landed)
1978
+ this.concludeObservations(observed, report);
1979
+ await this.settleUndecidedObservations(handle, report, landedUndecided(observed));
1656
1980
  try {
1657
1981
  if (carry) {
1658
1982
  reconcileHolds(this.controlDir, { homeOf: (scope) => scopeDirFor(this.memoryDir, this.controlDir, scope), now: this.now });
@@ -1834,7 +2158,7 @@ export class MemoryEngine {
1834
2158
  return true;
1835
2159
  }
1836
2160
  catch (err) {
1837
- report.warnings.push(`memory index could NOT be restored to its pre-session state: ${thrownText(err)}`);
2161
+ report.warnings.push(`memory index could NOT be restored to its pre-session state: ${this.writeFailureText(err)}`);
1838
2162
  return false;
1839
2163
  }
1840
2164
  };
@@ -1864,39 +2188,7 @@ export class MemoryEngine {
1864
2188
  signalContainment((c) => (c.indexRolledBack = true));
1865
2189
  report.warnings.push(`memory harvest refused by an incident while this session is marked externally exposed — no entries were committed; instruction-form files were withheld and the derived index rolled back: ${inlineUntrusted(pollutedReason, 200)}`);
1866
2190
  };
1867
- for (const m of handle.materialized.filter((x) => x.readonly)) {
1868
- const text = readSafe(m.path);
1869
- if (text === undefined) {
1870
- report.missing.push(m.relPath);
1871
- continue;
1872
- }
1873
- if (revOfText(text, m.id) !== m.rev) {
1874
- report.rejections.push({ path: m.relPath, code: "readonly_layer", reason: "a read-only inherited memory layer was modified; changes to inherited layers are not writable from this session" });
1875
- const committed = await this.committedContentFor(m.id);
1876
- if (committed !== undefined) {
1877
- let fd;
1878
- try {
1879
- fd = openNoFollowForMode(m.path);
1880
- if (fd !== undefined)
1881
- fchmodSync(fd, 0o644);
1882
- writeFileNoFollow(m.path, committed);
1883
- report.restored.push(m.relPath);
1884
- }
1885
- catch {
1886
- }
1887
- finally {
1888
- if (fd !== undefined) {
1889
- try {
1890
- fchmodSync(fd, 0o444);
1891
- }
1892
- catch {
1893
- }
1894
- closeSync(fd);
1895
- }
1896
- }
1897
- }
1898
- }
1899
- }
2191
+ await this.restoreReadOnlyLayers(readOnlyRestorable, report, handle.accountLeg, handle.memoryDir);
1900
2192
  const missingWritable = writable.filter((m) => !recordByPath.has(m.path) && !existsSync(m.path) && !presentIds.has(m.id));
1901
2193
  report.missing.push(...missingWritable.map((m) => m.relPath));
1902
2194
  if (writable.length > 0 && missingWritable.length / writable.length > MASS_DELETION_FUSE_RATIO) {
@@ -1923,11 +2215,13 @@ export class MemoryEngine {
1923
2215
  if (content === undefined)
1924
2216
  continue;
1925
2217
  try {
2218
+ this.refuseOutsideStore(handle.memoryDir, m.path, "restoring");
1926
2219
  mkdirSync(dirname(m.path), { recursive: true });
1927
2220
  writeFileNoFollow(m.path, content);
1928
2221
  report.restored.push(m.relPath);
1929
2222
  }
1930
- catch {
2223
+ catch (err) {
2224
+ report.warnings.push(`${m.relPath}: the committed copy of this missing memory file could NOT be restored: ${this.writeFailureText(err)}`);
1931
2225
  }
1932
2226
  }
1933
2227
  const baselinePaths = new Set(writable.map((m) => m.path));
@@ -1964,6 +2258,8 @@ export class MemoryEngine {
1964
2258
  const pendingProjections = [];
1965
2259
  const idsAddedThisHarvest = new Set();
1966
2260
  const modelIdAdds = new Set();
2261
+ const stoodAsFound = new Set();
2262
+ let observedTable;
1967
2263
  let processed = 0;
1968
2264
  for (let i = 0; i < kept.length; i++) {
1969
2265
  const f = kept[i];
@@ -1980,10 +2276,14 @@ export class MemoryEngine {
1980
2276
  continue;
1981
2277
  }
1982
2278
  const fastBaseId = idByBasePath.get(f.canonical);
2279
+ if (fastBaseId !== undefined && !stubByPath.has(f.canonical) && revOfText(f.text, fastBaseId) !== revByBasePath.get(f.canonical) && handle.planeBaseline?.files.get(f.canonical) === planeDigest(f.text)) {
2280
+ stoodAsFound.add(rel);
2281
+ continue;
2282
+ }
1983
2283
  if (fastBaseId !== undefined && !stubByPath.has(f.canonical) && revOfText(f.text, fastBaseId) === revByBasePath.get(f.canonical)) {
1984
2284
  const fastDebt = projectionDebts.get(debtSeat(writeScope, homeRel(writableRoot, f.canonical)));
1985
2285
  if (fastDebt !== undefined && fastDebt.entryId === fastBaseId)
1986
- await this.repairProjectionSeat(seatOf(f.canonical), f.canonical, fastBaseId, debtClears);
2286
+ await this.repairProjectionSeat(seatOf(f.canonical), f.canonical, fastBaseId, debtClears, report.warnings);
1987
2287
  continue;
1988
2288
  }
1989
2289
  processed++;
@@ -2043,6 +2343,21 @@ export class MemoryEngine {
2043
2343
  const parsed = f.parsed;
2044
2344
  let baseId = idByBasePath.get(f.canonical);
2045
2345
  let id = baseId ?? parsed.id;
2346
+ if (baseId === undefined && handle.planeBaseline?.files.get(f.canonical) === planeDigest(f.text)) {
2347
+ const observed = (observedTable ??= layerObservationLookup(this.controlDir));
2348
+ const committedRow = parsed.id !== undefined && observed.covers(parsed.id) ? await this.committedRowOf(parsed.id) : undefined;
2349
+ if (committedRow === "undecidable") {
2350
+ report.rejections.push({ path: rel, code: "deferred", reason: "memory write deferred: this file's id is named by the read-only-layer observation table and the committed account could not be read whole — retried next harvest" });
2351
+ continue;
2352
+ }
2353
+ const recorded = parsed.id === undefined
2354
+ ? observed.findNote(writeScope, homeRel(writableRoot, f.canonical), planeDigest(f.text)) !== undefined
2355
+ : observed.covers(parsed.id) && observed.find(parsed.id, committedRow?.rev, revOfText(f.text, parsed.id)) !== undefined;
2356
+ if (recorded) {
2357
+ report.rejections.push({ path: rel, code: "readonly_layer", reason: "this file appeared while a session held this memory layer read-only (recorded as an unexpected write) — it is not committed as this session's; remove it, or edit it to commit this session's own version" });
2358
+ continue;
2359
+ }
2360
+ }
2046
2361
  let debtReconciled = false;
2047
2362
  if (id === undefined && projectionDebts.size > 0) {
2048
2363
  const debt = projectionDebts.get(debtSeat(writeScope, homeRel(writableRoot, f.canonical)));
@@ -2129,7 +2444,7 @@ export class MemoryEngine {
2129
2444
  report.restored.push(rel);
2130
2445
  }
2131
2446
  catch (err) {
2132
- report.warnings.push(`${rel}: the committed content could not be restored after the hold captured this session's edit — the entry is unavailable on the plane until release or the next restore: ${thrownText(err)}`);
2447
+ report.warnings.push(`${rel}: the committed content could not be restored after the hold captured this session's edit — the entry is unavailable on the plane until release or the next restore: ${this.writeFailureText(err)}`);
2133
2448
  }
2134
2449
  }
2135
2450
  }
@@ -2176,7 +2491,7 @@ export class MemoryEngine {
2176
2491
  if (revOfText(f.text, id) === revByBasePath.get(f.canonical)) {
2177
2492
  const seatDebt = projectionDebts.get(debtSeat(writeScope, homeRel(writableRoot, f.canonical)));
2178
2493
  if (seatDebt !== undefined && seatDebt.entryId === id)
2179
- await this.repairProjectionSeat(seatOf(f.canonical), f.canonical, id, debtClears);
2494
+ await this.repairProjectionSeat(seatOf(f.canonical), f.canonical, id, debtClears, report.warnings);
2180
2495
  continue;
2181
2496
  }
2182
2497
  pendingProjections.push({ path: f.canonical, entry, needed: minted || parsed.id !== id || needsCompletion(parsed, fm), ...(debtReconciled ? { idCompletion: true } : {}) });
@@ -2200,7 +2515,7 @@ export class MemoryEngine {
2200
2515
  report.rejections.push({
2201
2516
  path: rel,
2202
2517
  code: "filename",
2203
- reason: `memory write blocked: this file carries the entry id of another memory file (a copy is not a rename) and its frontmatter could not be rewritten (${thrownText(err)}) — remove the "id:" line so it can be committed as a new entry`,
2518
+ reason: `memory write blocked: this file carries the entry id of another memory file (a copy is not a rename) and its frontmatter could not be rewritten (${this.writeFailureText(err)}) — remove the "id:" line so it can be committed as a new entry`,
2204
2519
  });
2205
2520
  continue;
2206
2521
  }
@@ -2219,7 +2534,7 @@ export class MemoryEngine {
2219
2534
  report.rejections.push({
2220
2535
  path: rel,
2221
2536
  code: "filename",
2222
- reason: `memory write blocked: an external-origin commit does not adopt the file's model-written entry id and its frontmatter could not be rewritten (${thrownText(err)}) — remove the "id:" line so it can be committed under an engine-minted id`,
2537
+ reason: `memory write blocked: an external-origin commit does not adopt the file's model-written entry id and its frontmatter could not be rewritten (${this.writeFailureText(err)}) — remove the "id:" line so it can be committed under an engine-minted id`,
2223
2538
  });
2224
2539
  continue;
2225
2540
  }
@@ -2341,7 +2656,7 @@ export class MemoryEngine {
2341
2656
  report.rejections.push({
2342
2657
  path: flipRec.rel,
2343
2658
  code: "filename",
2344
- reason: `memory write blocked: an external-origin commit does not adopt the file's model-written entry id and its frontmatter could not be rewritten (${thrownText(err)}) — remove the "id:" line so it can be committed under an engine-minted id`,
2659
+ reason: `memory write blocked: an external-origin commit does not adopt the file's model-written entry id and its frontmatter could not be rewritten (${this.writeFailureText(err)}) — remove the "id:" line so it can be committed under an engine-minted id`,
2345
2660
  });
2346
2661
  droppedIds.add(p.id);
2347
2662
  continue;
@@ -2458,7 +2773,7 @@ export class MemoryEngine {
2458
2773
  }
2459
2774
  }
2460
2775
  report.ok = false;
2461
- report.incident = { kind: "sidecar_corrupt", detail: `memory commit refused: ${thrownText(err)}` };
2776
+ report.incident = { kind: "sidecar_corrupt", detail: `memory commit refused: ${this.writeFailureText(err)}` };
2462
2777
  return report;
2463
2778
  }
2464
2779
  if (lineageArmed) {
@@ -2514,18 +2829,25 @@ export class MemoryEngine {
2514
2829
  }
2515
2830
  }
2516
2831
  const appliedIds = new Set(patchReport.applied.filter((a) => a.op !== "delete").map((a) => a.id));
2832
+ const planeAfterFailedWriteBack = this.backendPinnedRoot !== undefined && canonicalize(this.backendPinnedRoot) === canonicalize(this.memoryDir)
2833
+ ? "this plane is the store itself: the next read adopts them as an out-of-session edit unless the committed copy is put back first"
2834
+ : "the next materialize re-projects the committed copy over them";
2517
2835
  for (const p of pendingProjections) {
2518
2836
  const pRel = relative(handle.memoryDir, p.path);
2519
2837
  const pSeat = seatOf(p.path);
2520
2838
  if (appliedIds.has(p.entry.id)) {
2521
- const wrote = this.writeBackProjection(p.path, p.entry, p.needed);
2522
- if (p.idCompletion === true) {
2523
- if (wrote)
2839
+ const writeBackFailure = this.writeBackProjection(p.path, p.entry, p.needed);
2840
+ if (p.idCompletion !== true) {
2841
+ if (writeBackFailure !== undefined)
2842
+ report.warnings.push(`${pRel}: the entry committed but its projection write-back FAILED — the plane keeps this session's bytes (${planeAfterFailedWriteBack}): ${writeBackFailure}`);
2843
+ }
2844
+ else {
2845
+ if (writeBackFailure === undefined)
2524
2846
  debtClears.push({ ...pSeat, entryId: p.entry.id });
2525
2847
  else {
2526
2848
  const landed = patchReport.applied.find((a) => a.id === p.entry.id)?.slug;
2527
2849
  const suffixNote = landed !== undefined && landed !== p.entry.slug ? ` NOTE: the entry landed at slug ${JSON.stringify(landed)} (collision suffix) — the seat claim will not re-bind; host reconciliation may be needed.` : "";
2528
- report.warnings.push(`${pRel}: the entry committed but its id write-back FAILED — the projection-debt row stands, so the file re-binds to its committed id at the next harvest (and the next materialize repairs the plane); it is never re-admitted as a duplicate.${suffixNote}`);
2850
+ report.warnings.push(`${pRel}: the entry committed but its id write-back FAILED — the projection-debt row stands, so the file re-binds to its committed id at the next harvest (and the next materialize repairs the plane); it is never re-admitted as a duplicate.${suffixNote} Cause: ${writeBackFailure}`);
2529
2851
  }
2530
2852
  }
2531
2853
  continue;
@@ -2538,7 +2860,8 @@ export class MemoryEngine {
2538
2860
  writeFileNoFollow(p.path, committed);
2539
2861
  planeHoldsCommitted = true;
2540
2862
  }
2541
- catch {
2863
+ catch (err) {
2864
+ report.warnings.push(`${pRel}: this session's version lost to a concurrent commit and the committed copy could NOT be written back over it — the plane keeps this session's bytes (${planeAfterFailedWriteBack}): ${this.writeFailureText(err)}`);
2542
2865
  }
2543
2866
  }
2544
2867
  else if (committed !== undefined) {
@@ -2587,15 +2910,17 @@ export class MemoryEngine {
2587
2910
  }
2588
2911
  const headers = await this.backend.listHeaders([...new Set([...handle.scopes, writeScope])]);
2589
2912
  handle.indexText = this.rebuildIndex(handle, headers, { write: true, ignoreOnDisk: indexGate !== undefined }, report.warnings);
2590
- await this.rebaseline(handle, new Set(report.degraded?.pending ?? []), new Set(report.rejections.map((r) => r.path)));
2913
+ await this.rebaseline(handle, new Set([...(report.degraded?.pending ?? []), ...stoodAsFound]), new Set(report.rejections.map((r) => r.path)));
2591
2914
  if (carry && lineageSessionId !== undefined && report.degraded === undefined) {
2592
2915
  const deferredSeats = [...new Set(report.rejections.filter((r) => r.code === "deferred").map((r) => r.path))];
2593
2916
  const shown = deferredSeats.slice(0, MAX_DISCLOSED_DEFERRED_SEATS);
2594
2917
  const rest = deferredSeats.length - shown.length;
2595
2918
  const seatList = `${shown.join(", ")}${rest > 0 ? ` (+${rest} more — see the rejections)` : ""}`;
2596
2919
  try {
2597
- const closed = closeSessionAccount(this.controlDir, { sessionId: lineageSessionId, now: this.now });
2598
- if (closed && deferredSeats.length > 0) {
2920
+ const closed = handle.accountLeg !== undefined ? closeSessionAccount(this.controlDir, { sessionId: lineageSessionId, now: this.now, leg: handle.accountLeg }) : "absent";
2921
+ if (closed === "stale" && !report.warnings.includes(STALE_LEG_NOTE))
2922
+ report.warnings.push(STALE_LEG_NOTE);
2923
+ if (closed === "written" && deferredSeats.length > 0) {
2599
2924
  report.warnings.push(`memory session account closed with ${deferredSeats.length} file(s) still DEFERRED on the model-visible plane — their projection-debt rows STAND and are re-judged by the next harvest's consult, not by this close: ${seatList}`);
2600
2925
  }
2601
2926
  }
@@ -4288,6 +4613,8 @@ export class MemoryEngine {
4288
4613
  const id = parsed.id ?? handle.baseIds.get(path);
4289
4614
  if (id === undefined)
4290
4615
  continue;
4616
+ if (uncommitted && handle.baseIds.get(path) === undefined)
4617
+ continue;
4291
4618
  const stub = stubs.get(path);
4292
4619
  const priorRev = deferred ? handle.baseRevs.get(path) : undefined;
4293
4620
  const priorId = deferred ? handle.baseIds.get(path) : undefined;
@@ -4504,13 +4831,15 @@ export class MemoryEngine {
4504
4831
  if (opts.write) {
4505
4832
  let wrote = false;
4506
4833
  try {
4834
+ this.refuseOutsideStore(handle.memoryDir, indexPath, "writing the index");
4507
4835
  mkdirSync(dirname(indexPath), { recursive: true });
4508
4836
  if (readNoFollowSafe(indexPath) !== text)
4509
4837
  writeFileNoFollow(indexPath, text);
4510
4838
  wrote = true;
4511
4839
  }
4512
- catch {
4840
+ catch (err) {
4513
4841
  handle.indexOnDiskUntrusted = true;
4842
+ warnings?.push(`${INDEX_WRITE_REFUSED}: ${this.writeFailureText(err)}`);
4514
4843
  }
4515
4844
  if (wrote)
4516
4845
  writeIndexRevs(this.controlDir, nextIndexRevs);
@@ -4593,11 +4922,12 @@ export class MemoryEngine {
4593
4922
  }
4594
4923
  let cleared = false;
4595
4924
  try {
4925
+ this.refuseOutsideStore(handle.memoryDir, indexPath, "clearing the index");
4596
4926
  writeFileNoFollow(indexPath, "");
4597
4927
  cleared = true;
4598
4928
  }
4599
4929
  catch (err) {
4600
- detail = `${detail}; the derived index could NOT be cleared: ${thrownText(err)}`;
4930
+ detail = `${detail}; the derived index could NOT be cleared: ${this.writeFailureText(err)}`;
4601
4931
  }
4602
4932
  const outcome = [
4603
4933
  captured ? "the offending prose was captured to quarantine" : "the quarantine capture FAILED — the prose was not preserved",
@@ -4612,7 +4942,16 @@ export class MemoryEngine {
4612
4942
  contained: cleared,
4613
4943
  };
4614
4944
  }
4615
- writeIfChanged(target, text) {
4945
+ refuseOutsideStore(root, path, act) {
4946
+ const escape = storeTargetEscape(root, path);
4947
+ if (escape === undefined)
4948
+ return;
4949
+ const e = new Error(`${TARGET_OUTSIDE_STORE}: ${act} ${path} was refused — ${escape}; not performed`);
4950
+ e.code = TARGET_OUTSIDE_STORE;
4951
+ throw e;
4952
+ }
4953
+ writeIfChanged(target, text, root) {
4954
+ this.refuseOutsideStore(root, target, "the projection write of");
4616
4955
  const current = readNoFollowSafe(target);
4617
4956
  if (current === text)
4618
4957
  return;
@@ -4621,19 +4960,19 @@ export class MemoryEngine {
4621
4960
  }
4622
4961
  writeBackProjection(path, entry, needed) {
4623
4962
  if (!needed)
4624
- return true;
4963
+ return undefined;
4625
4964
  const text = serializeEntryFile(entry);
4626
4965
  if (readNoFollowSafe(path) === text)
4627
- return true;
4966
+ return undefined;
4628
4967
  try {
4629
4968
  writeFileNoFollow(path, text);
4630
- return true;
4969
+ return undefined;
4631
4970
  }
4632
- catch {
4633
- return false;
4971
+ catch (err) {
4972
+ return this.writeFailureText(err);
4634
4973
  }
4635
4974
  }
4636
- async repairProjectionSeat(seat, canonical, entryId, debtClears) {
4975
+ async repairProjectionSeat(seat, canonical, entryId, debtClears, warnings) {
4637
4976
  const committed = await this.committedContentFor(entryId);
4638
4977
  if (committed === undefined)
4639
4978
  return;
@@ -4641,7 +4980,8 @@ export class MemoryEngine {
4641
4980
  try {
4642
4981
  writeFileNoFollow(canonical, committed);
4643
4982
  }
4644
- catch {
4983
+ catch (err) {
4984
+ warnings.push(`${relative(this.memoryDir, canonical)}: the committed copy could NOT be written over this id-less seat — the projection-debt row stands and the repair retries next pass: ${this.writeFailureText(err)}`);
4645
4985
  return;
4646
4986
  }
4647
4987
  }
@@ -4662,18 +5002,18 @@ export class MemoryEngine {
4662
5002
  return { state: "unknown" };
4663
5003
  }
4664
5004
  }
4665
- chmodScopeTree(dir, dirMode, fileMode, opts = {}) {
5005
+ censusScopeTree(dir) {
4666
5006
  if (!existsSync(dir))
4667
5007
  return;
4668
- const sweepSkipped = [];
4669
- const sweptLinks = [];
5008
+ const unreadable = [];
5009
+ const links = [];
4670
5010
  const walk = (d) => {
4671
5011
  let names = [];
4672
5012
  try {
4673
5013
  names = readdirSync(d);
4674
5014
  }
4675
5015
  catch {
4676
- sweepSkipped.push(d);
5016
+ unreadable.push(d);
4677
5017
  return;
4678
5018
  }
4679
5019
  for (const name of names) {
@@ -4683,50 +5023,34 @@ export class MemoryEngine {
4683
5023
  st = lstatSync(p);
4684
5024
  }
4685
5025
  catch {
4686
- sweepSkipped.push(p);
5026
+ unreadable.push(p);
4687
5027
  continue;
4688
5028
  }
4689
- if (st.isSymbolicLink()) {
4690
- sweptLinks.push(p);
4691
- continue;
4692
- }
4693
- if (st.isDirectory())
5029
+ if (st.isSymbolicLink())
5030
+ links.push(p);
5031
+ else if (st.isDirectory())
4694
5032
  walk(p);
4695
- else if (st.isFile() && name.endsWith(".md") && name !== MEMORY_INDEX_FILENAME)
4696
- chmodNoFollow(p, fileMode);
4697
5033
  }
4698
- chmodNoFollow(d, dirMode);
4699
5034
  };
4700
5035
  walk(dir);
4701
- const said = opts.announced;
4702
- const anomalyKey = (kind, path) => JSON.stringify([kind, path]);
4703
- const unsaid = (paths, kind) => (said === undefined ? paths : paths.filter((p) => !said.has(anomalyKey(kind, p))));
4704
- const skipped = unsaid(sweepSkipped, "unreadable");
4705
- const links = unsaid(sweptLinks, "symlink");
4706
- if (skipped.length + links.length > 0) {
4707
- try {
4708
- enqueueMemoryAnnouncement(this.controlDir, {
4709
- kind: "gate",
4710
- at: this.now(),
4711
- items: skipped
4712
- .slice(0, 12)
4713
- .map((p) => `mode sweep could not read ${JSON.stringify(inlineUntrusted(relative(dir, p) || "."))} — files under it keep their previous mode`)
4714
- .concat(skipped.length > 12 ? [`…and ${skipped.length - 12} more sweepSkipped path(s)`] : [])
4715
- .concat(links
4716
- .slice(0, 12)
4717
- .map((p) => `mode sweep skipped the symlink ${JSON.stringify(inlineUntrusted(relative(dir, p) || "."))} — links are not part of a memory scope tree and are never followed`))
4718
- .concat(links.length > 12 ? [`…and ${links.length - 12} more skipped symlink(s)`] : []),
4719
- });
4720
- if (said !== undefined) {
4721
- for (const p of skipped)
4722
- said.add(anomalyKey("unreadable", p));
4723
- for (const p of links)
4724
- said.add(anomalyKey("symlink", p));
4725
- }
4726
- }
4727
- catch (err) {
4728
- this.discloseAnnounceFailure("mode-sweep enqueue", err);
4729
- }
5036
+ if (unreadable.length + links.length === 0)
5037
+ return;
5038
+ try {
5039
+ enqueueMemoryAnnouncement(this.controlDir, {
5040
+ kind: "gate",
5041
+ at: this.now(),
5042
+ items: unreadable
5043
+ .slice(0, 12)
5044
+ .map((p) => `layer census could not read ${JSON.stringify(inlineUntrusted(relative(dir, p) || "."))} — nothing under it was examined`)
5045
+ .concat(unreadable.length > 12 ? [`…and ${unreadable.length - 12} more unreadable path(s)`] : [])
5046
+ .concat(links
5047
+ .slice(0, 12)
5048
+ .map((p) => `layer census skipped the symlink ${JSON.stringify(inlineUntrusted(relative(dir, p) || "."))} — links are not part of a memory scope tree and are never followed`))
5049
+ .concat(links.length > 12 ? [`…and ${links.length - 12} more skipped symlink(s)`] : []),
5050
+ });
5051
+ }
5052
+ catch (err) {
5053
+ this.discloseAnnounceFailure("layer-census enqueue", err);
4730
5054
  }
4731
5055
  }
4732
5056
  }
@@ -4813,16 +5137,6 @@ function readSafe(path) {
4813
5137
  return undefined;
4814
5138
  }
4815
5139
  }
4816
- function openNoFollowForMode(path) {
4817
- for (const flags of [fsConstants.O_RDONLY, fsConstants.O_WRONLY]) {
4818
- try {
4819
- return openSync(path, flags | fsConstants.O_NOFOLLOW);
4820
- }
4821
- catch {
4822
- }
4823
- }
4824
- return undefined;
4825
- }
4826
5140
  function readNoFollowSafe(path) {
4827
5141
  let fd;
4828
5142
  try {
@@ -4837,19 +5151,6 @@ function readNoFollowSafe(path) {
4837
5151
  closeSync(fd);
4838
5152
  }
4839
5153
  }
4840
- function chmodNoFollow(path, mode) {
4841
- const fd = openNoFollowForMode(path);
4842
- if (fd === undefined)
4843
- return;
4844
- try {
4845
- fchmodSync(fd, mode);
4846
- }
4847
- catch {
4848
- }
4849
- finally {
4850
- closeSync(fd);
4851
- }
4852
- }
4853
5154
  function needsCompletion(parsed, fm) {
4854
5155
  return (!parsed.hadFrontmatter ||
4855
5156
  parsed.frontmatter.name !== fm.name ||