@sema-agent/core 5.49.0 → 5.50.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.
@@ -14,7 +14,7 @@ import { noReplaceRestore } from "./delegation-settlement.js";
14
14
  import { markOriginClearanceTombstoned, openOriginClearance, readOriginClearances, settleOriginClearance } from "./origin-clearance.js";
15
15
  import { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, canonicalJsonStringify, captureErasureInput, erasureRequestInvalid, erasureSelectHash, scanEntryFiles, } from "./file-backend.js";
16
16
  import { assembleMemoryExportBundle, computeMemoryBundleHash, memoryBundleInvalid, } from "./export-bundle.js";
17
- import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, readIndexRevs, writeIndexRevs, bumpScanFuse, canonicalize, claimRootScope, clearScanFuse, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, isContainedIn, markSessionPolluted, readSessionPollution, 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, resolveChallengeEvent, stageLineagePending, } from "./layout.js";
17
+ import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, readIndexRevs, writeIndexRevs, bumpScanFuse, canonicalize, claimRootScope, clearScanFuse, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, isContainedIn, markSessionPolluted, readSessionPollution, 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";
18
18
  import { scanMemoryFileName, scanMemoryWrite, scanRemediation } from "./scan.js";
19
19
  export const MEMORY_INSTRUCTION_TEMPLATE = `# Memory
20
20
 
@@ -876,6 +876,8 @@ export class MemoryEngine {
876
876
  this.discloseSessionAccountIncident("the committed snapshot was contended/incomplete — the id-bearing residue arm is degraded to id-less for this materialize", undefined);
877
877
  }
878
878
  }
879
+ const debtByRel = new Map(readProjectionDebts(this.controlDir).map((r) => [r.relPath, r.entryId]));
880
+ const canonicalMemoryDir = canonicalize(this.memoryDir);
879
881
  for (const f of scanEntryFiles(wroot, { maxDepth: this.maxDepth })) {
880
882
  const canonical = canonicalize(f.path);
881
883
  if (canonical !== wroot && !canonical.startsWith(`${wroot}${sep}`))
@@ -883,11 +885,20 @@ export class MemoryEngine {
883
885
  const text = readSafe(canonical);
884
886
  if (text === undefined)
885
887
  continue;
886
- const rel = relative(this.memoryDir, canonical);
888
+ const rel = relative(canonicalMemoryDir, canonical);
887
889
  if (rel === MEMORY_INDEX_FILENAME || rel.endsWith(`${sep}${MEMORY_INDEX_FILENAME}`))
888
890
  continue;
889
891
  const parsed = parseEntryFile(text);
890
- const uncommitted = parsed.id === undefined || (committedRevs !== undefined && !committedRevs.has(parsed.id));
892
+ const debtEntryId = parsed.id === undefined ? debtByRel.get(relative(canonicalMemoryDir, canonical)) : undefined;
893
+ let debtAttributed = false;
894
+ if (debtEntryId !== undefined) {
895
+ const proj = await this.debtCommittedProjection(debtEntryId);
896
+ if (proj.state === "unknown") {
897
+ throw new Error(`the committed state for debt-bound file ${rel} could not be read during crash-residue attribution — refusing to classify on a fault`);
898
+ }
899
+ debtAttributed = proj.state === "valid" && proj.scope === writeScope && proj.slug === f.slug;
900
+ }
901
+ const uncommitted = (parsed.id === undefined && !debtAttributed) || (parsed.id !== undefined && committedRevs !== undefined && !committedRevs.has(parsed.id));
891
902
  if (uncommitted) {
892
903
  unattributed.push(rel);
893
904
  continue;
@@ -1029,6 +1040,20 @@ export class MemoryEngine {
1029
1040
  handle.baseIds.set(path, entry.id);
1030
1041
  }
1031
1042
  }
1043
+ {
1044
+ const debts = readProjectionDebts(this.controlDir);
1045
+ if (debts.length > 0) {
1046
+ const projectedIdByRel = new Map(handle.materialized.filter((m) => !m.stub).map((m) => [m.relPath, m.id]));
1047
+ const healed = debts.filter((r) => projectedIdByRel.get(r.relPath) === r.entryId).map((r) => ({ relPath: r.relPath, entryId: r.entryId }));
1048
+ if (healed.length > 0) {
1049
+ try {
1050
+ settleProjectionDebts(this.controlDir, healed);
1051
+ }
1052
+ catch {
1053
+ }
1054
+ }
1055
+ }
1056
+ }
1032
1057
  const indexGate = this.gateDerivedIndex(handle);
1033
1058
  if (indexGate !== undefined) {
1034
1059
  if (!indexGate.contained)
@@ -1508,6 +1533,17 @@ export class MemoryEngine {
1508
1533
  for (const [p, id] of idByBasePath)
1509
1534
  if (!handle.fileToScope.get(p) || handle.fileToScope.get(p) === handle.writeScope)
1510
1535
  basePathById.set(id, p);
1536
+ let projectionDebts;
1537
+ try {
1538
+ projectionDebts = new Map(readProjectionDebts(this.controlDir).map((r) => [r.relPath, { entryId: r.entryId, rev: r.rev }]));
1539
+ }
1540
+ catch (err) {
1541
+ report.ok = false;
1542
+ report.incident = { kind: "sidecar_corrupt", detail: `memory projection-debt ledger unreadable: ${err instanceof Error ? err.message : String(err)}` };
1543
+ return report;
1544
+ }
1545
+ const debtClears = [];
1546
+ const staleReplacedDebts = new Map();
1511
1547
  const patches = [];
1512
1548
  const pendingProjections = [];
1513
1549
  const idsAddedThisHarvest = new Set();
@@ -1528,8 +1564,12 @@ export class MemoryEngine {
1528
1564
  continue;
1529
1565
  }
1530
1566
  const fastBaseId = idByBasePath.get(f.canonical);
1531
- if (fastBaseId !== undefined && !stubByPath.has(f.canonical) && revOfText(f.text, fastBaseId) === revByBasePath.get(f.canonical))
1567
+ if (fastBaseId !== undefined && !stubByPath.has(f.canonical) && revOfText(f.text, fastBaseId) === revByBasePath.get(f.canonical)) {
1568
+ const fastDebt = projectionDebts.get(rel);
1569
+ if (fastDebt !== undefined && fastDebt.entryId === fastBaseId)
1570
+ await this.repairProjectionSeat(rel, f.canonical, fastBaseId, debtClears);
1532
1571
  continue;
1572
+ }
1533
1573
  processed++;
1534
1574
  if (pollutedReason !== undefined && !carry) {
1535
1575
  await containPollutedRecord(f);
@@ -1585,8 +1625,35 @@ export class MemoryEngine {
1585
1625
  continue;
1586
1626
  }
1587
1627
  const parsed = f.parsed;
1588
- const baseId = idByBasePath.get(f.canonical);
1628
+ let baseId = idByBasePath.get(f.canonical);
1589
1629
  let id = baseId ?? parsed.id;
1630
+ let debtReconciled = false;
1631
+ if (id === undefined && projectionDebts.size > 0) {
1632
+ const debt = projectionDebts.get(rel);
1633
+ if (debt !== undefined) {
1634
+ const committed = await this.debtCommittedProjection(debt.entryId);
1635
+ if (committed.state === "unknown") {
1636
+ report.rejections.push({
1637
+ path: rel,
1638
+ code: "deferred",
1639
+ reason: "memory write deferred: this file's recorded committed id could not be validated (committed state unreadable) — retried next harvest (fail-closed: a fresh mint here could duplicate the committed entry)",
1640
+ });
1641
+ continue;
1642
+ }
1643
+ if (committed.state === "valid" && committed.scope === writeScope && committed.slug === f.slug) {
1644
+ baseId = debt.entryId;
1645
+ id = debt.entryId;
1646
+ idByBasePath.set(f.canonical, debt.entryId);
1647
+ revByBasePath.set(f.canonical, debt.rev);
1648
+ debtReconciled = true;
1649
+ report.warnings.push(`${rel}: reconciled to its committed entry — a prior harvest committed this file but the id write-back did not land; the projection is repaired through the ordinary update lane, never re-admitted as a duplicate`);
1650
+ }
1651
+ else {
1652
+ debtClears.push({ relPath: rel, entryId: debt.entryId });
1653
+ staleReplacedDebts.set(rel, debt.entryId);
1654
+ }
1655
+ }
1656
+ }
1590
1657
  const minted = id === undefined;
1591
1658
  if (id === undefined)
1592
1659
  id = uuidv7();
@@ -1680,9 +1747,13 @@ export class MemoryEngine {
1680
1747
  continue;
1681
1748
  }
1682
1749
  if (baseId !== undefined) {
1683
- if (revOfText(f.text, id) === revByBasePath.get(f.canonical))
1750
+ if (revOfText(f.text, id) === revByBasePath.get(f.canonical)) {
1751
+ const seatDebt = projectionDebts.get(rel);
1752
+ if (seatDebt !== undefined && seatDebt.entryId === id)
1753
+ await this.repairProjectionSeat(rel, f.canonical, id, debtClears);
1684
1754
  continue;
1685
- pendingProjections.push({ path: f.canonical, entry, needed: minted || parsed.id !== id || needsCompletion(parsed, fm) });
1755
+ }
1756
+ pendingProjections.push({ path: f.canonical, entry, needed: minted || parsed.id !== id || needsCompletion(parsed, fm), ...(debtReconciled ? { idCompletion: true } : {}) });
1686
1757
  patches.push({ op: "update", id, entry, ...(revByBasePath.get(f.canonical) !== undefined ? { baseRev: revByBasePath.get(f.canonical) } : {}) });
1687
1758
  continue;
1688
1759
  }
@@ -1732,7 +1803,7 @@ export class MemoryEngine {
1732
1803
  entry.rev = computeEntryRev(entry);
1733
1804
  report.warnings.push(`${rel}: the file's model-written id was not adopted for this external-origin commit — committed under an engine-minted id (a marked entry's index/search handles carry no model-authored bytes)`);
1734
1805
  }
1735
- pendingProjections.push({ path: f.canonical, entry, needed: minted || reboundId !== undefined || remintedId !== undefined || needsCompletion(parsed, fm) });
1806
+ pendingProjections.push({ path: f.canonical, entry, needed: minted || reboundId !== undefined || remintedId !== undefined || needsCompletion(parsed, fm), ...(minted || reboundId !== undefined || remintedId !== undefined ? { idCompletion: true } : {}) });
1736
1807
  idsAddedThisHarvest.add(entry.id);
1737
1808
  if (parsed.id !== undefined && entry.id === parsed.id)
1738
1809
  modelIdAdds.add(entry.id);
@@ -1835,6 +1906,7 @@ export class MemoryEngine {
1835
1906
  p.entry.id = uuidv7();
1836
1907
  p.entry.rev = computeEntryRev(p.entry);
1837
1908
  p.id = p.entry.id;
1909
+ projection.idCompletion = true;
1838
1910
  report.warnings.push(`${flipRec.rel}: the file's model-written id was not adopted for this external-origin commit — committed under an engine-minted id (a marked entry's index/search handles carry no model-authored bytes)`);
1839
1911
  }
1840
1912
  }
@@ -1869,6 +1941,68 @@ export class MemoryEngine {
1869
1941
  }
1870
1942
  }
1871
1943
  }
1944
+ const debtStage = pendingProjections
1945
+ .filter((p) => p.idCompletion === true)
1946
+ .map((p) => {
1947
+ const relPath = relative(handle.memoryDir, p.path);
1948
+ const replaces = staleReplacedDebts.get(relPath);
1949
+ return { relPath, entryId: p.entry.id, rev: p.entry.rev, ...(replaces !== undefined ? { replaces } : {}) };
1950
+ });
1951
+ if (debtStage.length > 0) {
1952
+ let refusedStage = [];
1953
+ try {
1954
+ refusedStage = stageProjectionDebts(this.controlDir, debtStage, this.now).refused;
1955
+ }
1956
+ catch (err) {
1957
+ report.warnings.push(`memory projection-debt staging failed — duplicate-admission protection for this harvest's id write-backs is degraded (a failed write-back could re-admit its file as a duplicate until the next materialize repairs the plane): ${err instanceof Error ? err.message : String(err)}`);
1958
+ }
1959
+ if (refusedStage.length > 0) {
1960
+ const refusedIds = new Set(refusedStage.map((r) => r.entryId));
1961
+ for (let i = patches.length - 1; i >= 0; i--) {
1962
+ const p = patches[i];
1963
+ if (p.op !== "delete" && refusedIds.has(p.id))
1964
+ patches.splice(i, 1);
1965
+ }
1966
+ for (let i = pendingProjections.length - 1; i >= 0; i--) {
1967
+ const proj = pendingProjections[i];
1968
+ if (refusedIds.has(proj.entry.id)) {
1969
+ idByBasePath.delete(proj.path);
1970
+ revByBasePath.delete(proj.path);
1971
+ pendingProjections.splice(i, 1);
1972
+ }
1973
+ }
1974
+ for (const r of refusedStage) {
1975
+ report.rejections.push({
1976
+ path: r.relPath,
1977
+ code: "deferred",
1978
+ reason: "memory write deferred: a concurrent writer's projection-debt claim stands at this seat — retried next harvest (committing without a protected id write-back could duplicate the concurrent entry)",
1979
+ });
1980
+ report.warnings.push(`${r.relPath}: earlier notes about this file committing under a fresh id are superseded — its commit was WITHDRAWN this harvest (deferred to the next)`);
1981
+ }
1982
+ if (lineageArmed) {
1983
+ const remaining = patches.filter((p) => p.op !== "delete" && p.entry !== undefined).map((p) => ({ entryId: p.id, rev: p.entry.rev, ...(p.entry.frontmatter.origin !== undefined ? { marked: true } : {}) }));
1984
+ if (remaining.length > 0) {
1985
+ try {
1986
+ stageLineagePending(this.controlDir, txnId, lineageSessionId, remaining, this.now);
1987
+ }
1988
+ catch (err) {
1989
+ report.ok = false;
1990
+ report.incident = { kind: "sidecar_corrupt", detail: `memory lineage restage refused after a withheld projection-debt seat: ${err instanceof Error ? err.message : String(err)}` };
1991
+ return report;
1992
+ }
1993
+ }
1994
+ else {
1995
+ try {
1996
+ discardLineagePending(this.controlDir, txnId);
1997
+ }
1998
+ catch (err) {
1999
+ report.warnings.push(`memory lineage stage for withheld entries could not be discarded — their ids stay latched until the host adjudicates pending transaction ${txnId}: ${err instanceof Error ? err.message : String(err)}`);
2000
+ }
2001
+ lineageArmed = false;
2002
+ }
2003
+ }
2004
+ }
2005
+ }
1872
2006
  try {
1873
2007
  patchReport = await this.backend.applyPatches(patches);
1874
2008
  }
@@ -1938,18 +2072,44 @@ export class MemoryEngine {
1938
2072
  }
1939
2073
  const appliedIds = new Set(patchReport.applied.filter((a) => a.op !== "delete").map((a) => a.id));
1940
2074
  for (const p of pendingProjections) {
2075
+ const pRel = relative(handle.memoryDir, p.path);
1941
2076
  if (appliedIds.has(p.entry.id)) {
1942
- this.writeBackProjection(p.path, p.entry, p.needed);
2077
+ const wrote = this.writeBackProjection(p.path, p.entry, p.needed);
2078
+ if (p.idCompletion === true) {
2079
+ if (wrote)
2080
+ debtClears.push({ relPath: pRel, entryId: p.entry.id });
2081
+ else {
2082
+ const landed = patchReport.applied.find((a) => a.id === p.entry.id)?.slug;
2083
+ 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.` : "";
2084
+ 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}`);
2085
+ }
2086
+ }
1943
2087
  continue;
1944
2088
  }
1945
- const committed = await this.committedContentFor(p.entry.id);
2089
+ const committedRead = await this.committedContentOrFault(p.entry.id);
2090
+ const committed = committedRead.content;
2091
+ let planeHoldsCommitted = false;
1946
2092
  if (committed !== undefined && readNoFollowSafe(p.path) !== committed) {
1947
2093
  try {
1948
2094
  writeFileNoFollow(p.path, committed);
2095
+ planeHoldsCommitted = true;
1949
2096
  }
1950
2097
  catch {
1951
2098
  }
1952
2099
  }
2100
+ else if (committed !== undefined) {
2101
+ planeHoldsCommitted = true;
2102
+ }
2103
+ if (p.idCompletion === true && ((committed === undefined && !committedRead.fault) || planeHoldsCommitted))
2104
+ debtClears.push({ relPath: pRel, entryId: p.entry.id });
2105
+ }
2106
+ if (debtClears.length > 0) {
2107
+ try {
2108
+ settleProjectionDebts(this.controlDir, debtClears);
2109
+ }
2110
+ catch (err) {
2111
+ report.warnings.push(`memory projection-debt settlement failed — settled rows stay on the ledger (harmless: every consumption re-validates a row against the committed state before binding): ${err instanceof Error ? err.message : String(err)}`);
2112
+ }
1953
2113
  }
1954
2114
  try {
1955
2115
  clearScanFuse(this.controlDir, pendingProjections.filter((p) => appliedIds.has(p.entry.id)).map((p) => p.path));
@@ -2074,7 +2234,7 @@ export class MemoryEngine {
2074
2234
  if (committedOrigin !== undefined)
2075
2235
  fm.origin = committedOrigin;
2076
2236
  else if (opts.valve)
2077
- fm.origin = { taint: "external", cause: "static", at: this.now() };
2237
+ fm.origin = { taint: "external", cause: "static", at: row.capturedAt };
2078
2238
  }
2079
2239
  if (fm.name === undefined)
2080
2240
  fm.name = row.slug.split("/").pop();
@@ -2140,7 +2300,9 @@ export class MemoryEngine {
2140
2300
  report.warnings.push(`released hold ${row.relPath}: lineage settlement failed after commit — the entry stays latched until a later harvest reconciles: ${err instanceof Error ? err.message : String(err)}`);
2141
2301
  }
2142
2302
  markHoldReleased(this.controlDir, { holdId: row.holdId, now: this.now });
2143
- const abs = join(this.memoryDir, row.relPath);
2303
+ const landedSlug = patchReport.applied.find((a) => a.id === patch.id)?.slug ?? row.slug;
2304
+ const landedRelPath = landedSlug === row.slug ? row.relPath : join(dirname(row.relPath), `${landedSlug.split("/").pop()}.md`);
2305
+ const abs = join(this.memoryDir, landedRelPath);
2144
2306
  if (existsSync(abs)) {
2145
2307
  const canonical = canonicalize(abs);
2146
2308
  const text = readSafe(canonical);
@@ -2149,7 +2311,7 @@ export class MemoryEngine {
2149
2311
  handle.baseRevs.set(canonical, computeEntryRev({ id: patch.id, frontmatter: parseEntryFile(text).frontmatter, body: parseEntryFile(text).body }));
2150
2312
  handle.fileToScope.set(canonical, row.scope);
2151
2313
  if (!handle.materialized.some((m) => m.path === canonical)) {
2152
- handle.materialized.push({ path: canonical, relPath: row.relPath, scope: row.scope, id: patch.id, slug: row.slug, rev: handle.baseRevs.get(canonical), stub: false, readonly: false });
2314
+ handle.materialized.push({ path: canonical, relPath: landedRelPath, scope: row.scope, id: patch.id, slug: landedSlug, rev: handle.baseRevs.get(canonical), stub: false, readonly: false });
2153
2315
  }
2154
2316
  }
2155
2317
  }
@@ -2576,20 +2738,28 @@ export class MemoryEngine {
2576
2738
  return text;
2577
2739
  }
2578
2740
  async committedContentFor(id) {
2741
+ return (await this.committedContentOrFault(id)).content;
2742
+ }
2743
+ async committedContentOrFault(id) {
2744
+ let fault = false;
2579
2745
  const zeroCopy = this.backendPinnedRoot !== undefined && canonicalize(this.backendPinnedRoot) === canonicalize(this.memoryDir);
2580
2746
  if (!zeroCopy) {
2581
2747
  try {
2582
2748
  const [entry] = await this.backend.getByIds([id]);
2583
2749
  if (entry !== undefined && entry.id === id)
2584
- return serializeEntryFile(entry);
2750
+ return { content: serializeEntryFile(entry), fault: false };
2585
2751
  }
2586
2752
  catch {
2753
+ fault = true;
2587
2754
  }
2588
2755
  }
2589
2756
  const viaBackend = this.backend.readCommittedShadow?.(id);
2590
2757
  if (viaBackend !== undefined)
2591
- return viaBackend;
2592
- return readSafe(join(this.controlDir, "shadow", `${id}.md`));
2758
+ return { content: viaBackend, fault: false };
2759
+ const viaShadow = readSafe(join(this.controlDir, "shadow", `${id}.md`));
2760
+ if (viaShadow !== undefined)
2761
+ return { content: viaShadow, fault: false };
2762
+ return { fault };
2593
2763
  }
2594
2764
  async committedFrontmatterFor(id) {
2595
2765
  const zeroCopy = this.backendPinnedRoot !== undefined && canonicalize(this.backendPinnedRoot) === canonicalize(this.memoryDir);
@@ -2682,14 +2852,45 @@ export class MemoryEngine {
2682
2852
  }
2683
2853
  writeBackProjection(path, entry, needed) {
2684
2854
  if (!needed)
2685
- return;
2855
+ return true;
2686
2856
  const text = serializeEntryFile(entry);
2687
2857
  if (readNoFollowSafe(path) === text)
2688
- return;
2858
+ return true;
2689
2859
  try {
2690
2860
  writeFileNoFollow(path, text);
2861
+ return true;
2862
+ }
2863
+ catch {
2864
+ return false;
2865
+ }
2866
+ }
2867
+ async repairProjectionSeat(rel, canonical, entryId, debtClears) {
2868
+ const committed = await this.committedContentFor(entryId);
2869
+ if (committed === undefined)
2870
+ return;
2871
+ if (readNoFollowSafe(canonical) !== committed) {
2872
+ try {
2873
+ writeFileNoFollow(canonical, committed);
2874
+ }
2875
+ catch {
2876
+ return;
2877
+ }
2878
+ }
2879
+ debtClears.push({ relPath: rel, entryId });
2880
+ }
2881
+ async debtCommittedProjection(entryId) {
2882
+ const zeroCopy = this.backendPinnedRoot !== undefined && canonicalize(this.backendPinnedRoot) === canonicalize(this.memoryDir);
2883
+ if (zeroCopy)
2884
+ return { state: "stale" };
2885
+ const face = this.backend.retrievalView?.() ?? this.backend;
2886
+ try {
2887
+ const [entry] = await face.getByIds([entryId]);
2888
+ if (entry !== undefined && entry.id === entryId)
2889
+ return { state: "valid", scope: entry.scope, slug: entry.slug };
2890
+ return { state: "stale" };
2691
2891
  }
2692
2892
  catch {
2893
+ return { state: "unknown" };
2693
2894
  }
2694
2895
  }
2695
2896
  siblingScopeDirNames(dir, scope) {
@@ -721,6 +721,49 @@ export declare function recordChallengedHistory(controlDir: string, rows: Readon
721
721
  }>, now: () => number): void;
722
722
  /** Journal-aware read (observability/tests only — no engine consumer exists, on purpose). */
723
723
  export declare function readChallengedHistory(controlDir: string): Record<string, ChallengedHistoryRow>;
724
+ export declare const PROJECTION_DEBTS_FILE = "projection-debts.json";
725
+ /** One standing debt: the plane file at `relPath` (memory-dir-relative, canonical base) belongs to
726
+ * committed entry `entryId`, whose id write-back has not landed; `rev` is the rev the projection
727
+ * was staged against (the CAS baseline a reconciling harvest hands its update). */
728
+ export interface ProjectionDebtRow {
729
+ relPath: string;
730
+ entryId: string;
731
+ rev: string;
732
+ at: number;
733
+ }
734
+ /** WRITE-AHEAD staging. Upsert discipline (adversarial round 2): a stage lands only when the seat
735
+ * has NO standing row, the standing row is the stager's OWN entry (a rev refresh), or the
736
+ * standing row is the entry the stager itself just judged stale (`replaces` — the same-harvest
737
+ * stale-then-remint lane). It never blindly replaces ANOTHER writer's protection: a lagging
738
+ * process that validated an old row before pausing must not overwrite the row a faster sibling
739
+ * staged at the same path (its own commit then CAS-conflicts and its tuple-keyed clear misses
740
+ * the survivor — the account converges instead of emptying). Called before the backend
741
+ * transaction; the caller degrades LOUDLY (report warning) on a refused stage rather than
742
+ * refusing the harvest — the ledger is duplicate-admission protection, not the commit's
743
+ * integrity. */
744
+ export declare function stageProjectionDebts(controlDir: string, rows: ReadonlyArray<{
745
+ relPath: string;
746
+ entryId: string;
747
+ rev: string;
748
+ replaces?: string;
749
+ }>, now: () => number): {
750
+ refused: Array<{
751
+ relPath: string;
752
+ entryId: string;
753
+ }>;
754
+ };
755
+ /** Settle (remove) rows by ROW IDENTITY — (relPath, entryId), never the bare path: the write-back
756
+ * landed, the staged claim turned out stale, or a materialize re-projected the seat. Identity
757
+ * matters (adversarial review F1): a stale-clear judged against an OLD row must not delete the
758
+ * NEWER row a later staging upserted at the same path (same harvest: stale X cleared while fresh
759
+ * Y's write-back failed — a path-keyed drop would erase Y and re-open the duplicate window; same
760
+ * shape across processes for a lagging sibling's clear). Missing rows are a no-op (idempotent). */
761
+ export declare function settleProjectionDebts(controlDir: string, rows: ReadonlyArray<{
762
+ relPath: string;
763
+ entryId: string;
764
+ }>): void;
765
+ /** Strict read (ENOENT ⇒ empty; corrupt ⇒ throws — the caller's fail-closed arm owns the refusal). */
766
+ export declare function readProjectionDebts(controlDir: string): ProjectionDebtRow[];
724
767
  /**
725
768
  * REF-C6 — write EVERY byte of `data` to `fd`, looping until the OS has taken all of them.
726
769
  *
@@ -1504,6 +1504,65 @@ export function recordChallengedHistory(controlDir, rows, now) {
1504
1504
  export function readChallengedHistory(controlDir) {
1505
1505
  return coerceChallengedHistory(readSidecarJson(controlDir, CHALLENGED_HISTORY_FILE));
1506
1506
  }
1507
+ export const PROJECTION_DEBTS_FILE = "projection-debts.json";
1508
+ function coerceProjectionDebts(raw) {
1509
+ if (raw === undefined)
1510
+ return { version: 1, rows: [] };
1511
+ const rec = raw;
1512
+ if (typeof rec !== "object" || rec === null || rec.version !== 1 || !Array.isArray(rec.rows)) {
1513
+ throw new ControlPlaneCorruptError("memory projection-debt ledger has the wrong shape — refusing (fail-closed; an unreadable debt account must not read as 'no debts')");
1514
+ }
1515
+ for (const r of rec.rows) {
1516
+ const row = r;
1517
+ if (typeof row !== "object" ||
1518
+ row === null ||
1519
+ typeof row.relPath !== "string" ||
1520
+ row.relPath.length === 0 ||
1521
+ row.relPath.startsWith("/") ||
1522
+ row.relPath.split(/[\\/]/).includes("..") ||
1523
+ typeof row.entryId !== "string" ||
1524
+ row.entryId.length === 0 ||
1525
+ typeof row.rev !== "string" ||
1526
+ row.rev.length === 0 ||
1527
+ typeof row.at !== "number" ||
1528
+ !Number.isFinite(row.at)) {
1529
+ throw new ControlPlaneCorruptError("memory projection-debt ledger: a row is malformed — refusing (fail-closed)");
1530
+ }
1531
+ }
1532
+ return rec;
1533
+ }
1534
+ export function stageProjectionDebts(controlDir, rows, now) {
1535
+ if (rows.length === 0)
1536
+ return { refused: [] };
1537
+ const at = now();
1538
+ return lockedStrictUpdate(controlDir, PROJECTION_DEBTS_FILE, "memory projection-debt ledger", coerceProjectionDebts, (rec) => {
1539
+ const byPath = new Map(rec.rows.map((r) => [r.relPath, r]));
1540
+ const refused = [];
1541
+ for (const r of rows) {
1542
+ const existing = byPath.get(r.relPath);
1543
+ if (existing !== undefined && existing.entryId !== r.entryId && existing.entryId !== r.replaces) {
1544
+ refused.push({ relPath: r.relPath, entryId: r.entryId });
1545
+ continue;
1546
+ }
1547
+ byPath.set(r.relPath, { relPath: r.relPath, entryId: r.entryId, rev: r.rev, at });
1548
+ }
1549
+ return { next: { version: 1, rows: [...byPath.values()] }, result: { refused } };
1550
+ });
1551
+ }
1552
+ export function settleProjectionDebts(controlDir, rows) {
1553
+ if (rows.length === 0)
1554
+ return;
1555
+ const drop = new Set(rows.map((r) => JSON.stringify([r.entryId, r.relPath])));
1556
+ lockedStrictUpdate(controlDir, PROJECTION_DEBTS_FILE, "memory projection-debt ledger", coerceProjectionDebts, (rec) => {
1557
+ const kept = rec.rows.filter((r) => !drop.has(JSON.stringify([r.entryId, r.relPath])));
1558
+ if (kept.length === rec.rows.length)
1559
+ return { result: undefined };
1560
+ return { next: { version: 1, rows: kept }, result: undefined };
1561
+ });
1562
+ }
1563
+ export function readProjectionDebts(controlDir) {
1564
+ return coerceProjectionDebts(readStrictSidecar(controlDir, PROJECTION_DEBTS_FILE, "memory projection-debt ledger")).rows;
1565
+ }
1507
1566
  export function writeAllSync(fd, data) {
1508
1567
  const buf = Buffer.from(data, "utf8");
1509
1568
  let written = 0;
@@ -426,6 +426,93 @@ export async function memoryBackendContract(hooks) {
426
426
  const flat = await b.search("same words", ["s1"], { limit: 3 });
427
427
  assert.deepStrictEqual(flat.map((h) => h.id), ["id-band-a-01", "id-band-b-01", "id-band-c-01"]);
428
428
  });
429
+ defer("design/336 §4 hold protocol (update form): mid-hold writes apply plainly (backend is hold-unaware); the release replay at the capture anchor is a reported conflict carrying currentRev; a clean release applies and the snapshot face answers its committed triple", async () => {
430
+ const b = await hooks.make();
431
+ const e1v0 = entry("id-hold-upd1", "s1", "held-clean", "committed v0");
432
+ assert.deepStrictEqual((await b.applyPatches([{ op: "add", id: e1v0.id, entry: e1v0 }])).conflicts, []);
433
+ const e1rel = entry("id-hold-upd1", "s1", "held-clean", "the released capture");
434
+ const rep1 = await b.applyPatches([{ op: "update", id: e1v0.id, entry: e1rel, baseRev: e1v0.rev }]);
435
+ assert.deepStrictEqual(rep1.conflicts, [], "a release onto an untouched anchor applies");
436
+ const snapFace = b.committedSnapshotOf;
437
+ if (typeof snapFace === "function") {
438
+ const snap = await snapFace.call(b, e1v0.id);
439
+ assert.strictEqual(snap.state, "row");
440
+ if (snap.state === "row") {
441
+ assert.strictEqual(snap.rev, e1rel.rev, "the snapshot tracks the UPDATE — not the add-time state");
442
+ assert.strictEqual(snap.binding.state, "bound");
443
+ if (snap.binding.state === "bound") {
444
+ assert.strictEqual(snap.binding.scope, "s1");
445
+ assert.strictEqual(snap.binding.slug, "held-clean");
446
+ }
447
+ }
448
+ }
449
+ const rep1b = await b.applyPatches([{ op: "update", id: e1v0.id, entry: e1rel, baseRev: e1v0.rev }]);
450
+ assert.strictEqual(rep1b.applied.length, 0);
451
+ assert.strictEqual(rep1b.conflicts.length, 1);
452
+ assert.strictEqual(rep1b.conflicts[0]?.currentRev, e1rel.rev, "currentRev is the retry's idempotency evidence");
453
+ const e2v0 = entry("id-hold-upd2", "s1", "held-raced", "committed v0");
454
+ assert.deepStrictEqual((await b.applyPatches([{ op: "add", id: e2v0.id, entry: e2v0 }])).conflicts, []);
455
+ const mid = entry("id-hold-upd2", "s1", "held-raced", "mid-hold edit by another writer");
456
+ const repMid = await b.applyPatches([{ op: "update", id: e2v0.id, entry: mid, baseRev: e2v0.rev }]);
457
+ assert.deepStrictEqual(repMid.conflicts, [], "a mid-hold write applies plainly — the backend is hold-unaware");
458
+ assert.strictEqual((await b.getByIds([e2v0.id])).length, 1, "the held id keeps serving reads during the pendency window");
459
+ const e2rel = entry("id-hold-upd2", "s1", "held-raced", "the captured bytes");
460
+ const rep2 = await b.applyPatches([{ op: "update", id: e2v0.id, entry: e2rel, baseRev: e2v0.rev }]);
461
+ assert.strictEqual(rep2.applied.length, 0, "the release never blind-writes over a mid-hold winner");
462
+ assert.strictEqual(rep2.conflicts.length, 1);
463
+ assert.strictEqual(rep2.conflicts[0]?.baseRev, e2v0.rev);
464
+ assert.strictEqual(rep2.conflicts[0]?.currentRev, mid.rev, "the conflict names the winner — the dispose('conflict') evidence");
465
+ assert.strictEqual((await b.getByIds([e2v0.id]))[0]?.body.replace(/\s+$/, ""), "mid-hold edit by another writer", "the winner's content survives");
466
+ });
467
+ defer("design/336 §4 hold protocol (add form): a guard-absent release applies onto a free id; the crash retry re-applies idempotently (one row, original slug); a concurrent claim answers the guarded conflict with currentRev and survives", async () => {
468
+ const b = await hooks.make();
469
+ const rel = entry("id-hold-add1", "s1", "held-note", "instruction body");
470
+ assert.deepStrictEqual((await b.applyPatches([{ op: "add", id: rel.id, entry: rel, guard: "absent" }])).conflicts, []);
471
+ const rep2 = await b.applyPatches([{ op: "add", id: rel.id, entry: rel, guard: "absent" }]);
472
+ assert.deepStrictEqual(rep2.conflicts, [], "the crash retry of a committed release re-applies idempotently");
473
+ assert.deepStrictEqual(rep2.applied, [{ op: "add", id: rel.id, slug: "held-note" }]);
474
+ assert.deepStrictEqual((await b.listHeaders(["s1"])).filter((h) => h.id === rel.id).map((h) => h.slug), ["held-note"], "ONE row at the ORIGINAL slug — never a suffixed duplicate");
475
+ const claimed = entry("id-hold-add2", "s1", "claimed-note", "the claimant's content");
476
+ assert.deepStrictEqual((await b.applyPatches([{ op: "add", id: claimed.id, entry: claimed }])).conflicts, []);
477
+ const rel2 = entry("id-hold-add2", "s1", "claimed-note", "the captured bytes");
478
+ const rep3 = await b.applyPatches([{ op: "add", id: rel2.id, entry: rel2, guard: "absent" }]);
479
+ assert.strictEqual(rep3.applied.length, 0);
480
+ assert.strictEqual(rep3.conflicts.length, 1);
481
+ assert.match(rep3.conflicts[0]?.reason ?? "", /add_guard_absent_conflict/);
482
+ assert.strictEqual(rep3.conflicts[0]?.currentRev, claimed.rev, "currentRev distinguishes 'claimed by another writer' from the idempotent retry");
483
+ assert.strictEqual((await b.getByIds([claimed.id]))[0]?.body.replace(/\s+$/, ""), "the claimant's content", "the claimant survives");
484
+ });
485
+ defer("design/336: the whitewash judgment precedes CAS — an origin/trust strip with a STALE baseRev still answers the malformed refusal, never the rev-mismatch conflict", async () => {
486
+ const b = await hooks.make();
487
+ const origin = { taint: "external", cause: "observed", at: 1700000000900 };
488
+ const m0 = { id: "id-prec-0001", scope: "s1", slug: "prec-marked", frontmatter: { name: "prec", origin }, body: "b0", rev: "" };
489
+ m0.rev = computeEntryRev(m0);
490
+ assert.deepStrictEqual((await b.applyPatches([{ op: "add", id: m0.id, entry: m0 }])).conflicts, []);
491
+ const m1 = { ...m0, body: "b1", rev: "" };
492
+ m1.rev = computeEntryRev(m1);
493
+ assert.deepStrictEqual((await b.applyPatches([{ op: "update", id: m0.id, entry: m1, baseRev: m0.rev }])).conflicts, []);
494
+ const stripped = { id: m0.id, scope: "s1", slug: "prec-marked", frontmatter: { name: "prec" }, body: "b2", rev: "" };
495
+ stripped.rev = computeEntryRev(stripped);
496
+ const rep = await b.applyPatches([{ op: "update", id: m0.id, entry: stripped, baseRev: m0.rev }]);
497
+ assert.strictEqual(rep.applied.length, 0);
498
+ assert.strictEqual(rep.conflicts.length, 1);
499
+ assert.match(rep.conflicts[0]?.reason ?? "", /malformed patch refused/, "the stronger refusal answers — a CAS conflict here would hide the whitewash");
500
+ const prov = { kind: "repo_file", path: "AGENTS.md", contentHash: "sha256:cccc3333", ingestedAt: 1700000001000 };
501
+ const p0 = { id: "id-prec-0002", scope: "s1", slug: "prec-repo", frontmatter: { name: "prec-repo", provenance: prov, trust: "untrusted" }, body: "p0", rev: "" };
502
+ p0.rev = computeEntryRev(p0);
503
+ assert.deepStrictEqual((await b.applyPatches([{ op: "add", id: p0.id, entry: p0 }])).conflicts, []);
504
+ const p1 = { ...p0, body: "p1", rev: "" };
505
+ p1.rev = computeEntryRev(p1);
506
+ assert.deepStrictEqual((await b.applyPatches([{ op: "update", id: p0.id, entry: p1, baseRev: p0.rev }])).conflicts, []);
507
+ const strippedTrust = { id: p0.id, scope: "s1", slug: "prec-repo", frontmatter: { name: "prec-repo", provenance: prov }, body: "p2", rev: "" };
508
+ strippedTrust.rev = computeEntryRev(strippedTrust);
509
+ const rep2 = await b.applyPatches([{ op: "update", id: p0.id, entry: strippedTrust, baseRev: p0.rev }]);
510
+ assert.strictEqual(rep2.applied.length, 0);
511
+ assert.strictEqual(rep2.conflicts.length, 1);
512
+ assert.match(rep2.conflicts[0]?.reason ?? "", /malformed patch refused/);
513
+ assert.deepStrictEqual((await b.getByIds([m0.id]))[0]?.frontmatter.origin, origin);
514
+ assert.strictEqual((await b.getByIds([p0.id]))[0]?.frontmatter.trust, "untrusted");
515
+ });
429
516
  defer("consolidation cursor round-trips per scope; unset → undefined", async () => {
430
517
  const b = await hooks.make();
431
518
  assert.strictEqual(await b.getConsolidationCursor("s1"), undefined);
@@ -254,6 +254,18 @@ export interface MemoryBackend {
254
254
  * earlier delete in the same batch does not blank the baseline). The one legal exit is the
255
255
  * COMMITTED tombstone: after a delete commits, the marker's life ends with the id (a fresh id —
256
256
  * or the same id in a LATER batch — starts an unmarked life; the engine re-judges its session);
257
+ * - PRECEDENCE (FAM-1 #5, every whitewash spelling): the malformed judgment answers BEFORE
258
+ * guard/CAS arithmetic — a strip riding a stale `baseRev` (or a guard conflict) still answers
259
+ * /malformed patch refused/, never the ordinary rev-mismatch conflict (a whitewash is illegal
260
+ * at ANY rev, and the weaker conflict would tell the caller's ladder to rebase and retry it);
261
+ * - design/336 §4 hold protocol: the backend is hold-UNAWARE — instruction holds live on the
262
+ * ENGINE's control plane, so a held id keeps serving reads and ordinary add/update patches for
263
+ * it apply plainly (no queueing, no refusal). The settlement-period write interaction resolves
264
+ * at release time through the clauses above: the release replays the capture-time anchor
265
+ * (update+`baseRev` / `guard: "absent"` add), a mid-hold winner surfaces as the REPORTED
266
+ * conflict carrying `currentRev` (the engine's conflict-disposition evidence), and the
267
+ * crash-idempotent retry reads the optional committed-snapshot face (post-update rev + bound
268
+ * projection) — all asserted by the suite's two hold-protocol cases;
257
269
  * - conflicts are per-patch and non-fatal: the rest of the batch still applies.
258
270
  */
259
271
  applyPatches(patches: readonly NotePatch[]): Promise<PatchReport>;
@@ -338,7 +350,7 @@ export interface MemorySessionHandle {
338
350
  adoptionRestricted?: boolean;
339
351
  }
340
352
  /** Stable rejection codes a harvest gate can produce (model-visible gate events — 镜头 I). */
341
- export type HarvestRejectionCode = "outside_root" | "symlink" | "secret" | "injection" | "filename" | "too_large" | "file_cap" | "readonly_layer" | "stub_modified" | "nested_too_deep" | "quarantine_failed" | "unreadable" | "polluted" | "restricted_divergence" | "invalid";
353
+ export type HarvestRejectionCode = "outside_root" | "symlink" | "secret" | "injection" | "filename" | "too_large" | "file_cap" | "readonly_layer" | "stub_modified" | "nested_too_deep" | "quarantine_failed" | "unreadable" | "deferred" | "polluted" | "restricted_divergence" | "invalid";
342
354
  /** One rejected file: path (relative to the memory dir), stable code, and a model-readable reason. */
343
355
  export interface HarvestRejection {
344
356
  path: string;