@sema-agent/core 5.48.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/dist/agents/agent-transcript-tool.d.ts +1 -1
  3. package/dist/agents/agent-transcript-tool.js +1 -1
  4. package/dist/agents/roster-store.js +4 -1
  5. package/dist/agents/send-message-tool.d.ts +2 -2
  6. package/dist/agents/send-message-tool.js +2 -2
  7. package/dist/agents/subagent.d.ts +6 -0
  8. package/dist/agents/subagent.js +126 -1
  9. package/dist/agents/teacher.d.ts +25 -1
  10. package/dist/agents/teacher.js +89 -13
  11. package/dist/brain/anthropic.js +11 -20
  12. package/dist/brain/open-responses.js +6 -14
  13. package/dist/brain/openai.js +6 -18
  14. package/dist/brain/reasoning.d.ts +100 -8
  15. package/dist/brain/reasoning.js +39 -15
  16. package/dist/brain/request-params.d.ts +37 -1
  17. package/dist/brain/request-params.js +40 -2
  18. package/dist/core/background-agent-store.d.ts +1 -1
  19. package/dist/core/background-agent-store.js +5 -4
  20. package/dist/core/mcp.d.ts +7 -1
  21. package/dist/core/mcp.js +64 -8
  22. package/dist/core/memory-engine/delegation-settlement.d.ts +27 -0
  23. package/dist/core/memory-engine/delegation-settlement.js +31 -4
  24. package/dist/core/memory-engine/dual-root.js +11 -0
  25. package/dist/core/memory-engine/engine.d.ts +36 -2
  26. package/dist/core/memory-engine/engine.js +354 -38
  27. package/dist/core/memory-engine/layout.d.ts +43 -0
  28. package/dist/core/memory-engine/layout.js +59 -0
  29. package/dist/core/memory-engine/memory-backend-contract.js +120 -0
  30. package/dist/core/memory-engine/origin-clearance.d.ts +19 -0
  31. package/dist/core/memory-engine/origin-clearance.js +10 -0
  32. package/dist/core/memory-engine/provenance-wording.d.ts +15 -1
  33. package/dist/core/memory-engine/provenance-wording.js +1 -0
  34. package/dist/core/memory-engine/tools.js +6 -4
  35. package/dist/core/memory-engine/types.d.ts +13 -1
  36. package/dist/core/runner/prepare-task.js +22 -8
  37. package/dist/core/runner/runtask.d.ts +26 -1
  38. package/dist/core/runner/runtask.js +18 -2
  39. package/dist/core/strategy-store.d.ts +180 -3
  40. package/dist/core/strategy-store.js +172 -23
  41. package/dist/core/task-registry-agent.js +6 -0
  42. package/dist/core/types.d.ts +24 -1
  43. package/dist/index.d.ts +2 -2
  44. package/dist/index.js +2 -2
  45. package/dist/orchestration/run-workflow-tool.d.ts +12 -0
  46. package/dist/orchestration/run-workflow-tool.js +1 -1
  47. package/dist/orchestration/workflow-governance.d.ts +27 -0
  48. package/dist/orchestration/workflow-governance.js +13 -0
  49. package/dist/orchestration/workflow-primitives.d.ts +8 -1
  50. package/dist/orchestration/workflow-primitives.js +11 -3
  51. package/dist/stores/file/file-snapshot-store.js +7 -1
  52. package/dist/stores/file/index.d.ts +8 -0
  53. package/dist/stores/file/index.js +12 -0
  54. package/dist/stores/file/session-policy-store.d.ts +0 -13
  55. package/dist/stores/file/session-policy-store.js +7 -1
  56. package/dist/stores/file/session-store.d.ts +4 -1
  57. package/dist/stores/file/session-store.js +7 -1
  58. package/dist/stores/file/strategy-store.d.ts +97 -0
  59. package/dist/stores/file/strategy-store.js +340 -0
  60. package/package.json +1 -1
  61. package/test/export-surface.snapshot.json +8 -1
@@ -9,12 +9,12 @@ import { formatMemoryAge } from "../memory-recall.js";
9
9
  import { committedOriginOf, computeEntryRev, hasOriginFormExtra, isValidEntryId, originEquals, parseEntryFile, serializeEntryFile, stripModelWrittenOrigin } from "./frontmatter.js";
10
10
  import { MEMORY_PROVENANCE_RECALL_SENTENCE, memoryExposureIndexRow, parseMemoryExposureIndexRow } from "./provenance-wording.js";
11
11
  import { isInstructionEntry } from "./header-hints.js";
12
- import { classifySessionSettlements, closeSessionAccount, disposeHold, effectiveSettlements, expireOverdueSettlements, foreignDanglingSessionAccounts, markHoldReleased, openInstructionHold, openSessionAccount, readHoldCustody, readHolds, reconcileHolds, replayExternalSettlementEffects, resolveHoldRecord, resolveSessionAccountRecord, resolveSettlementRecord, sessionSettlements, sessionUnattributedSet, } from "./delegation-settlement.js";
12
+ import { classifySessionSettlements, clearHoldResolution, closeSessionAccount, disposeHold, effectiveSettlements, expireOverdueSettlements, foreignDanglingSessionAccounts, markHoldReleased, openInstructionHold, openSessionAccount, readHoldCustody, readHolds, reconcileHolds, replayExternalSettlementEffects, resolveHoldRecord, resolveSessionAccountRecord, resolveSettlementRecord, sessionSettlements, sessionUnattributedSet, } from "./delegation-settlement.js";
13
13
  import { noReplaceRestore } from "./delegation-settlement.js";
14
- import { openOriginClearance, readOriginClearances, settleOriginClearance } from "./origin-clearance.js";
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)
@@ -1044,9 +1069,22 @@ export class MemoryEngine {
1044
1069
  this.discloseAnnounceFailure("index-gate enqueue", err);
1045
1070
  }
1046
1071
  }
1047
- const indexText = this.rebuildIndex(handle, headers, { write: writeScope !== null && !restricted, ignoreOnDisk: indexGate !== undefined || restricted });
1072
+ const materializeHealWarnings = [];
1073
+ const indexWrite = writeScope !== null && !restricted;
1074
+ const indexText = this.rebuildIndex(handle, headers, { write: indexWrite, ignoreOnDisk: indexGate !== undefined || restricted }, materializeHealWarnings);
1048
1075
  handle.indexBaselineLines = countIndexLines(indexText);
1049
1076
  handle.indexText = indexText;
1077
+ if (indexWrite && handle.indexOnDiskUntrusted !== true) {
1078
+ const dropNotes = materializeHealWarnings.filter((w) => w.includes("opaque external-origin handle"));
1079
+ if (dropNotes.length > 0) {
1080
+ try {
1081
+ enqueueMemoryAnnouncement(this.controlDir, { kind: "gate", at: this.now(), items: capItems(dropNotes.map((w) => `index note: ${inlineUntrusted(w, 200)}`)) });
1082
+ }
1083
+ catch (err) {
1084
+ this.discloseAnnounceFailure("materialize index-heal enqueue", err);
1085
+ }
1086
+ }
1087
+ }
1050
1088
  for (const [scope, dir] of scopeDirs) {
1051
1089
  if (scope === writeScope)
1052
1090
  continue;
@@ -1062,7 +1100,7 @@ export class MemoryEngine {
1062
1100
  const instruction = writeChannel ? buildMemoryInstruction(handle.writableRoot) : "";
1063
1101
  const readOnlyNotice = handle.writeScope === null ? MEMORY_READONLY_NOTICE : undefined;
1064
1102
  const indexPath = join(handle.writableRoot, MEMORY_INDEX_FILENAME);
1065
- const onDisk = handle.indexOnDiskUntrusted === true || handle.adoptionRestricted === true ? undefined : readSafe(indexPath);
1103
+ const onDisk = handle.indexOnDiskUntrusted === true || handle.adoptionRestricted === true || (handle.writeScope === null && this.provenance === "carry") ? undefined : readSafe(indexPath);
1066
1104
  const indexText = onDisk !== undefined && onDisk.trim() !== "" ? onDisk : handle.indexText;
1067
1105
  const truncated = truncateIndex(indexText);
1068
1106
  const index = composeMemoryBlock(truncated, handle.writeScope ?? handle.scopes[0] ?? "memory");
@@ -1495,9 +1533,21 @@ export class MemoryEngine {
1495
1533
  for (const [p, id] of idByBasePath)
1496
1534
  if (!handle.fileToScope.get(p) || handle.fileToScope.get(p) === handle.writeScope)
1497
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();
1498
1547
  const patches = [];
1499
1548
  const pendingProjections = [];
1500
1549
  const idsAddedThisHarvest = new Set();
1550
+ const modelIdAdds = new Set();
1501
1551
  let processed = 0;
1502
1552
  for (let i = 0; i < kept.length; i++) {
1503
1553
  const f = kept[i];
@@ -1514,8 +1564,12 @@ export class MemoryEngine {
1514
1564
  continue;
1515
1565
  }
1516
1566
  const fastBaseId = idByBasePath.get(f.canonical);
1517
- 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);
1518
1571
  continue;
1572
+ }
1519
1573
  processed++;
1520
1574
  if (pollutedReason !== undefined && !carry) {
1521
1575
  await containPollutedRecord(f);
@@ -1571,8 +1625,35 @@ export class MemoryEngine {
1571
1625
  continue;
1572
1626
  }
1573
1627
  const parsed = f.parsed;
1574
- const baseId = idByBasePath.get(f.canonical);
1628
+ let baseId = idByBasePath.get(f.canonical);
1575
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
+ }
1576
1657
  const minted = id === undefined;
1577
1658
  if (id === undefined)
1578
1659
  id = uuidv7();
@@ -1666,9 +1747,13 @@ export class MemoryEngine {
1666
1747
  continue;
1667
1748
  }
1668
1749
  if (baseId !== undefined) {
1669
- 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);
1670
1754
  continue;
1671
- 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 } : {}) });
1672
1757
  patches.push({ op: "update", id, entry, ...(revByBasePath.get(f.canonical) !== undefined ? { baseRev: revByBasePath.get(f.canonical) } : {}) });
1673
1758
  continue;
1674
1759
  }
@@ -1698,8 +1783,30 @@ export class MemoryEngine {
1698
1783
  entry.rev = computeEntryRev(entry);
1699
1784
  report.warnings.push(`${rel}: frontmatter id ${reboundId} already has a projection (${renamedFrom !== undefined ? relative(handle.memoryDir, renamedFrom) : "another new file in this harvest"}) — a copy is not a rename, so this file was committed as a NEW entry with a fresh id; the existing entry was left untouched`);
1700
1785
  }
1701
- pendingProjections.push({ path: f.canonical, entry, needed: minted || reboundId !== undefined || needsCompletion(parsed, fm) });
1786
+ let remintedId;
1787
+ if (fm.origin !== undefined && parsed.id !== undefined && reboundId === undefined) {
1788
+ const idless = serializeEntryFile({ id: "", frontmatter: fm, body: parsed.body }).replace(/^id:[ \t]*$\r?\n/m, "");
1789
+ try {
1790
+ writeFileNoFollow(f.canonical, idless);
1791
+ }
1792
+ catch (err) {
1793
+ report.rejections.push({
1794
+ path: rel,
1795
+ code: "filename",
1796
+ 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 (${err instanceof Error ? err.message : String(err)}) — remove the "id:" line so it can be committed under an engine-minted id`,
1797
+ });
1798
+ continue;
1799
+ }
1800
+ remintedId = parsed.id;
1801
+ id = uuidv7();
1802
+ entry.id = id;
1803
+ entry.rev = computeEntryRev(entry);
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)`);
1805
+ }
1806
+ pendingProjections.push({ path: f.canonical, entry, needed: minted || reboundId !== undefined || remintedId !== undefined || needsCompletion(parsed, fm), ...(minted || reboundId !== undefined || remintedId !== undefined ? { idCompletion: true } : {}) });
1702
1807
  idsAddedThisHarvest.add(entry.id);
1808
+ if (parsed.id !== undefined && entry.id === parsed.id)
1809
+ modelIdAdds.add(entry.id);
1703
1810
  patches.push({ op: "add", id: entry.id, entry });
1704
1811
  }
1705
1812
  let patchReport;
@@ -1775,6 +1882,33 @@ export class MemoryEngine {
1775
1882
  for (const pp of pendingProjections)
1776
1883
  if (pp.entry.id === p.id)
1777
1884
  pp.needed = true;
1885
+ if (p.op === "add" && modelIdAdds.has(p.id)) {
1886
+ const projection = pendingProjections.find((pp) => pp.entry.id === p.id);
1887
+ const flipRec = projection !== undefined ? records.find((r) => r.canonical === projection.path) : undefined;
1888
+ if (flipRec === undefined) {
1889
+ droppedIds.add(p.id);
1890
+ report.warnings.push(`entry ${p.id} was withdrawn from this commit: its projection record could not be located for the marked-commit id re-mint — the file stays on disk and the next harvest re-judges it`);
1891
+ continue;
1892
+ }
1893
+ const idless = serializeEntryFile({ id: "", frontmatter: p.entry.frontmatter, body: p.entry.body }).replace(/^id:[ \t]*$\r?\n/m, "");
1894
+ try {
1895
+ writeFileNoFollow(flipRec.canonical, idless);
1896
+ }
1897
+ catch (err) {
1898
+ report.rejections.push({
1899
+ path: flipRec.rel,
1900
+ code: "filename",
1901
+ 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 (${err instanceof Error ? err.message : String(err)}) — remove the "id:" line so it can be committed under an engine-minted id`,
1902
+ });
1903
+ droppedIds.add(p.id);
1904
+ continue;
1905
+ }
1906
+ p.entry.id = uuidv7();
1907
+ p.entry.rev = computeEntryRev(p.entry);
1908
+ p.id = p.entry.id;
1909
+ projection.idCompletion = true;
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)`);
1911
+ }
1778
1912
  }
1779
1913
  keptPatches.push(p);
1780
1914
  }
@@ -1807,6 +1941,68 @@ export class MemoryEngine {
1807
1941
  }
1808
1942
  }
1809
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
+ }
1810
2006
  try {
1811
2007
  patchReport = await this.backend.applyPatches(patches);
1812
2008
  }
@@ -1876,18 +2072,44 @@ export class MemoryEngine {
1876
2072
  }
1877
2073
  const appliedIds = new Set(patchReport.applied.filter((a) => a.op !== "delete").map((a) => a.id));
1878
2074
  for (const p of pendingProjections) {
2075
+ const pRel = relative(handle.memoryDir, p.path);
1879
2076
  if (appliedIds.has(p.entry.id)) {
1880
- 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
+ }
1881
2087
  continue;
1882
2088
  }
1883
- 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;
1884
2092
  if (committed !== undefined && readNoFollowSafe(p.path) !== committed) {
1885
2093
  try {
1886
2094
  writeFileNoFollow(p.path, committed);
2095
+ planeHoldsCommitted = true;
1887
2096
  }
1888
2097
  catch {
1889
2098
  }
1890
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
+ }
1891
2113
  }
1892
2114
  try {
1893
2115
  clearScanFuse(this.controlDir, pendingProjections.filter((p) => appliedIds.has(p.entry.id)).map((p) => p.path));
@@ -1942,7 +2164,7 @@ export class MemoryEngine {
1942
2164
  for (const row of readHolds(this.controlDir)) {
1943
2165
  const valveRelease = row.resolved === "release" && (row.status === "held" || (row.status === "disposed" && row.disposition?.terminal === "expired"));
1944
2166
  if (valveRelease) {
1945
- const released = await this.releaseHold(handle, report, row, { valve: true });
2167
+ const released = await this.releaseHold(handle, report, row, { valve: true, disposed: out.disposed });
1946
2168
  if (released !== undefined)
1947
2169
  out.released.push(released);
1948
2170
  continue;
@@ -1965,7 +2187,7 @@ export class MemoryEngine {
1965
2187
  }
1966
2188
  if (rows.some((r) => r.effective === "pending"))
1967
2189
  continue;
1968
- const released = await this.releaseHold(handle, report, row, { valve: false });
2190
+ const released = await this.releaseHold(handle, report, row, { valve: false, disposed: out.disposed });
1969
2191
  if (released !== undefined)
1970
2192
  out.released.push(released);
1971
2193
  }
@@ -1974,9 +2196,17 @@ export class MemoryEngine {
1974
2196
  async releaseHold(handle, report, row, opts) {
1975
2197
  const disposeOut = (terminal, why) => {
1976
2198
  const d = disposeHold(this.controlDir, { holdId: row.holdId, terminal, now: this.now });
2199
+ if (!d.ok) {
2200
+ try {
2201
+ clearHoldResolution(this.controlDir, { holdId: row.holdId, ifResolvedAt: row.resolvedAt });
2202
+ }
2203
+ catch {
2204
+ }
2205
+ report.warnings.push(`held instruction entry ${row.relPath} valve release FAILED (${terminal}): ${why} — the standing "release" verdict was consumed (the hold keeps its earlier disposition; resolveHold can re-arm it after the cause is reconciled)`);
2206
+ return undefined;
2207
+ }
1977
2208
  report.warnings.push(`held instruction entry ${row.relPath} DISPOSED (${terminal}): ${why}${d.quarantineName !== undefined ? ` — captured bytes moved to control-plane quarantine (${d.quarantineName})` : ""}`);
1978
- report.containment ??= { indexRolledBack: false, quarantinedInstruction: [], heldInstruction: [], releasedHolds: [], disposedHolds: [] };
1979
- report.containment.disposedHolds.push({ relPath: row.relPath, terminal });
2209
+ opts.disposed.push({ relPath: row.relPath, terminal });
1980
2210
  return undefined;
1981
2211
  };
1982
2212
  const content = readHoldCustody(this.controlDir, row);
@@ -2004,13 +2234,13 @@ export class MemoryEngine {
2004
2234
  if (committedOrigin !== undefined)
2005
2235
  fm.origin = committedOrigin;
2006
2236
  else if (opts.valve)
2007
- fm.origin = { taint: "external", cause: "static", at: this.now() };
2237
+ fm.origin = { taint: "external", cause: "static", at: row.capturedAt };
2008
2238
  }
2009
2239
  if (fm.name === undefined)
2010
2240
  fm.name = row.slug.split("/").pop();
2011
2241
  if (fm.description === undefined && fm.deleted !== true)
2012
2242
  fm.description = firstSentence(parsed.body) || undefined;
2013
- const id = row.entryId ?? (parsed.id !== undefined && isValidEntryId(parsed.id) ? parsed.id : `h${createHash("sha256").update(row.holdId, "utf8").digest("hex").slice(0, 31)}`);
2243
+ const id = row.entryId ?? (parsed.id !== undefined && isValidEntryId(parsed.id) && fm.origin === undefined ? parsed.id : `h${createHash("sha256").update(row.holdId, "utf8").digest("hex").slice(0, 31)}`);
2014
2244
  const entry = { id, slug: row.slug, frontmatter: fm, body: parsed.body, rev: "", scope: row.scope };
2015
2245
  entry.rev = computeEntryRev(entry);
2016
2246
  const patch = row.op === "update" && row.entryId !== undefined
@@ -2070,7 +2300,9 @@ export class MemoryEngine {
2070
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)}`);
2071
2301
  }
2072
2302
  markHoldReleased(this.controlDir, { holdId: row.holdId, now: this.now });
2073
- 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);
2074
2306
  if (existsSync(abs)) {
2075
2307
  const canonical = canonicalize(abs);
2076
2308
  const text = readSafe(canonical);
@@ -2079,7 +2311,7 @@ export class MemoryEngine {
2079
2311
  handle.baseRevs.set(canonical, computeEntryRev({ id: patch.id, frontmatter: parseEntryFile(text).frontmatter, body: parseEntryFile(text).body }));
2080
2312
  handle.fileToScope.set(canonical, row.scope);
2081
2313
  if (!handle.materialized.some((m) => m.path === canonical)) {
2082
- 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 });
2083
2315
  }
2084
2316
  }
2085
2317
  }
@@ -2201,12 +2433,24 @@ export class MemoryEngine {
2201
2433
  let landedSlug = row.slug;
2202
2434
  let detail;
2203
2435
  if (current !== undefined && committedOriginOf(current.frontmatter) !== undefined) {
2436
+ if (this.readChallengeExclusions().has(row.entryId)) {
2437
+ fail("memory.origin_clear_challenged", "the entry is challenged/latched — adjudicate the challenge first (the clear valve is not a challenge exit); the marked entry stands untouched and this row is terminal.", false);
2438
+ }
2204
2439
  if (current.rev !== row.baseRev) {
2205
2440
  fail("memory.origin_clear_conflict", `the entry moved since the clearance was judged (rev ${row.baseRev.slice(0, 12)}… → ${current.rev.slice(0, 12)}…) — call clearEntryOrigin again to re-judge (the marked entry stands untouched; this row is terminal).`, false);
2206
2441
  }
2207
2442
  if (current.scope !== row.scope || current.slug !== row.slug) {
2208
2443
  fail("memory.origin_clear_conflict", `the entry's projection moved since the clearance was judged (${JSON.stringify(row.scope)}/${JSON.stringify(row.slug.slice(0, 80))} → ${JSON.stringify(current.scope)}/${JSON.stringify(current.slug.slice(0, 80))}) — call clearEntryOrigin again to re-judge (the marked entry stands untouched; this row is terminal).`, false);
2209
2444
  }
2445
+ const projText = readNoFollowSafe(join(scopeDirFor(this.memoryDir, this.controlDir, current.scope), `${current.slug}.md`));
2446
+ let projIsPristineStub = false;
2447
+ if (projText !== undefined) {
2448
+ const { deleted: _stubDrop, ...currentStubFm } = current.frontmatter;
2449
+ projIsPristineStub = projText === serializeEntryFile({ id: current.id, frontmatter: currentStubFm, body: STUB_ARCHIVED_LINE });
2450
+ }
2451
+ if (projText !== undefined && revOfText(projText, current.id) !== current.rev && !projIsPristineStub) {
2452
+ fail("memory.origin_clear_conflict", "the entry's on-disk projection diverges from its committed state (an unadopted out-of-session edit stands on the plane) — the clear will not overwrite it; adopt or reconcile the divergence first, then call clearEntryOrigin again (the marked entry and the on-disk bytes both stand untouched; this row is terminal).", false);
2453
+ }
2210
2454
  const del = await this.backend.applyPatches([{ op: "delete", id: row.entryId, baseRev: row.baseRev }]);
2211
2455
  if (del.conflicts.length > 0) {
2212
2456
  const [reprobe] = await this.committedAuditFace().getByIds([row.entryId]);
@@ -2214,6 +2458,11 @@ export class MemoryEngine {
2214
2458
  fail("memory.origin_clear_conflict", `the tombstone leg was refused (${(del.conflicts[0]?.reason ?? "conflict").slice(0, 160)}) — call clearEntryOrigin again to re-judge (the marked entry stands untouched; this row is terminal).`, false);
2215
2459
  }
2216
2460
  if (reprobe === undefined) {
2461
+ try {
2462
+ markOriginClearanceTombstoned(this.controlDir, { clearanceId: row.clearanceId, now: this.now });
2463
+ }
2464
+ catch {
2465
+ }
2217
2466
  const add = await this.backend.applyPatches([{ op: "add", id: row.entryId, entry: cleared, guard: "absent" }]);
2218
2467
  if (add.conflicts.length > 0) {
2219
2468
  fail("memory.origin_clear_failed", `the re-record leg was refused (${(add.conflicts[0]?.reason ?? "conflict").slice(0, 160)}) — the row stays PENDING with the cleared bytes in its custody (entryText); call clearEntryOrigin again to resume.`, true);
@@ -2227,6 +2476,11 @@ export class MemoryEngine {
2227
2476
  }
2228
2477
  }
2229
2478
  else {
2479
+ try {
2480
+ markOriginClearanceTombstoned(this.controlDir, { clearanceId: row.clearanceId, now: this.now });
2481
+ }
2482
+ catch {
2483
+ }
2230
2484
  const add = await this.backend.applyPatches([{ op: "add", id: row.entryId, entry: cleared, guard: "absent" }]);
2231
2485
  if (add.conflicts.length > 0) {
2232
2486
  fail("memory.origin_clear_failed", `the re-record leg was refused (${(add.conflicts[0]?.reason ?? "conflict").slice(0, 160)}) — the row stays PENDING with the cleared bytes in its custody (entryText); call clearEntryOrigin again to resume.`, true);
@@ -2236,6 +2490,9 @@ export class MemoryEngine {
2236
2490
  }
2237
2491
  }
2238
2492
  else if (current === undefined) {
2493
+ if (row.tombstonedAt === undefined) {
2494
+ fail("memory.origin_clear_unknown", "the entry is no longer committed and this clearance never recorded a committed tombstone — the deletion is not this clearance's and stands (nothing was resurrected). The cleared bytes remain in this row's custody (entryText) for host review; this row is terminal.", false);
2495
+ }
2239
2496
  const add = await this.backend.applyPatches([{ op: "add", id: row.entryId, entry: cleared, guard: "absent" }]);
2240
2497
  if (add.conflicts.length > 0) {
2241
2498
  fail("memory.origin_clear_failed", `the resumed re-record leg was refused (${(add.conflicts[0]?.reason ?? "conflict").slice(0, 160)}) — the row stays PENDING with the cleared bytes in its custody (entryText); call clearEntryOrigin again to resume.`, true);
@@ -2361,6 +2618,28 @@ export class MemoryEngine {
2361
2618
  const handleCovered = new Set();
2362
2619
  const keptByTarget = new Map();
2363
2620
  const keptLines = [];
2621
+ const foldDots = (s) => {
2622
+ let prev = s;
2623
+ for (let i = 0; i < 32; i++) {
2624
+ const next = prev.replace(/\/(?:\.\/)+/g, "/").replace(/(?:^|[/\s(])(?:[^/\s()]+\/\.\.\/)+/g, (m) => (/^[/\s(]/.test(m) ? m[0] : ""));
2625
+ if (next === prev)
2626
+ return next;
2627
+ prev = next;
2628
+ }
2629
+ return prev;
2630
+ };
2631
+ const foldTarget = (t) => foldDots(t.toLowerCase());
2632
+ const knownTargetsFolded = new Set([...expected.keys(), ...excludedTargets, ...exposedProjectionTargets].map(foldTarget));
2633
+ const pathTokenAt = (folded, idx, len) => {
2634
+ const isPathChar = (ch) => /[a-z0-9._\-/\\]/.test(ch);
2635
+ let s = idx;
2636
+ let e = idx + len;
2637
+ while (s > 0 && isPathChar(folded[s - 1]))
2638
+ s--;
2639
+ while (e < folded.length && isPathChar(folded[e]))
2640
+ e++;
2641
+ return folded.slice(s, e).replace(/\.+$/, "");
2642
+ };
2364
2643
  for (const line of existing.split("\n")) {
2365
2644
  if (carryIndex) {
2366
2645
  const handleId = parseMemoryExposureIndexRow(line);
@@ -2373,18 +2652,16 @@ export class MemoryEngine {
2373
2652
  }
2374
2653
  }
2375
2654
  if (carryIndex && exposedProjectionTargets.size > 0) {
2376
- const foldDots = (s) => {
2377
- let prev = s;
2378
- for (let i = 0; i < 32; i++) {
2379
- const next = prev.replace(/\/(?:\.\/)+/g, "/").replace(/(?:^|[/\s(])(?:[^/\s()]+\/\.\.\/)+/g, (m) => (/^[/\s(]/.test(m) ? m[0] : ""));
2380
- if (next === prev)
2381
- return next;
2382
- prev = next;
2383
- }
2384
- return prev;
2385
- };
2386
2655
  const folded = foldDots(line.toLowerCase());
2387
- const hit = [...exposedProjectionTargets].find((t) => folded.includes(t.toLowerCase()));
2656
+ const hit = [...exposedProjectionTargets].find((t) => {
2657
+ const tf = foldTarget(t);
2658
+ for (let idx = folded.indexOf(tf); idx !== -1; idx = folded.indexOf(tf, idx + 1)) {
2659
+ const token = pathTokenAt(folded, idx, tf.length);
2660
+ if (token === tf || !knownTargetsFolded.has(token))
2661
+ return true;
2662
+ }
2663
+ return false;
2664
+ });
2388
2665
  if (hit !== undefined) {
2389
2666
  warnings?.push(`MEMORY.md index line naming ${hit} replaced by its opaque external-origin handle (the entry stays readable via memory_get)`);
2390
2667
  continue;
@@ -2461,20 +2738,28 @@ export class MemoryEngine {
2461
2738
  return text;
2462
2739
  }
2463
2740
  async committedContentFor(id) {
2741
+ return (await this.committedContentOrFault(id)).content;
2742
+ }
2743
+ async committedContentOrFault(id) {
2744
+ let fault = false;
2464
2745
  const zeroCopy = this.backendPinnedRoot !== undefined && canonicalize(this.backendPinnedRoot) === canonicalize(this.memoryDir);
2465
2746
  if (!zeroCopy) {
2466
2747
  try {
2467
2748
  const [entry] = await this.backend.getByIds([id]);
2468
2749
  if (entry !== undefined && entry.id === id)
2469
- return serializeEntryFile(entry);
2750
+ return { content: serializeEntryFile(entry), fault: false };
2470
2751
  }
2471
2752
  catch {
2753
+ fault = true;
2472
2754
  }
2473
2755
  }
2474
2756
  const viaBackend = this.backend.readCommittedShadow?.(id);
2475
2757
  if (viaBackend !== undefined)
2476
- return viaBackend;
2477
- 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 };
2478
2763
  }
2479
2764
  async committedFrontmatterFor(id) {
2480
2765
  const zeroCopy = this.backendPinnedRoot !== undefined && canonicalize(this.backendPinnedRoot) === canonicalize(this.memoryDir);
@@ -2567,14 +2852,45 @@ export class MemoryEngine {
2567
2852
  }
2568
2853
  writeBackProjection(path, entry, needed) {
2569
2854
  if (!needed)
2570
- return;
2855
+ return true;
2571
2856
  const text = serializeEntryFile(entry);
2572
2857
  if (readNoFollowSafe(path) === text)
2573
- return;
2858
+ return true;
2574
2859
  try {
2575
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" };
2576
2891
  }
2577
2892
  catch {
2893
+ return { state: "unknown" };
2578
2894
  }
2579
2895
  }
2580
2896
  siblingScopeDirNames(dir, scope) {