@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.
- package/CHANGELOG.md +112 -0
- package/dist/agents/agent-transcript-tool.d.ts +1 -1
- package/dist/agents/agent-transcript-tool.js +1 -1
- package/dist/agents/roster-store.js +4 -1
- package/dist/agents/send-message-tool.d.ts +2 -2
- package/dist/agents/send-message-tool.js +2 -2
- package/dist/agents/subagent.d.ts +6 -0
- package/dist/agents/subagent.js +126 -1
- package/dist/agents/teacher.d.ts +25 -1
- package/dist/agents/teacher.js +89 -13
- package/dist/brain/anthropic.js +11 -20
- package/dist/brain/open-responses.js +6 -14
- package/dist/brain/openai.js +6 -18
- package/dist/brain/reasoning.d.ts +100 -8
- package/dist/brain/reasoning.js +39 -15
- package/dist/brain/request-params.d.ts +37 -1
- package/dist/brain/request-params.js +40 -2
- package/dist/core/background-agent-store.d.ts +1 -1
- package/dist/core/background-agent-store.js +5 -4
- package/dist/core/mcp.d.ts +7 -1
- package/dist/core/mcp.js +64 -8
- package/dist/core/memory-engine/delegation-settlement.d.ts +27 -0
- package/dist/core/memory-engine/delegation-settlement.js +31 -4
- package/dist/core/memory-engine/dual-root.js +11 -0
- package/dist/core/memory-engine/engine.d.ts +36 -2
- package/dist/core/memory-engine/engine.js +354 -38
- package/dist/core/memory-engine/layout.d.ts +43 -0
- package/dist/core/memory-engine/layout.js +59 -0
- package/dist/core/memory-engine/memory-backend-contract.js +120 -0
- package/dist/core/memory-engine/origin-clearance.d.ts +19 -0
- package/dist/core/memory-engine/origin-clearance.js +10 -0
- package/dist/core/memory-engine/provenance-wording.d.ts +15 -1
- package/dist/core/memory-engine/provenance-wording.js +1 -0
- package/dist/core/memory-engine/tools.js +6 -4
- package/dist/core/memory-engine/types.d.ts +13 -1
- package/dist/core/runner/prepare-task.js +22 -8
- package/dist/core/runner/runtask.d.ts +26 -1
- package/dist/core/runner/runtask.js +18 -2
- package/dist/core/strategy-store.d.ts +180 -3
- package/dist/core/strategy-store.js +172 -23
- package/dist/core/task-registry-agent.js +6 -0
- package/dist/core/types.d.ts +24 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/orchestration/run-workflow-tool.d.ts +12 -0
- package/dist/orchestration/run-workflow-tool.js +1 -1
- package/dist/orchestration/workflow-governance.d.ts +27 -0
- package/dist/orchestration/workflow-governance.js +13 -0
- package/dist/orchestration/workflow-primitives.d.ts +8 -1
- package/dist/orchestration/workflow-primitives.js +11 -3
- package/dist/stores/file/file-snapshot-store.js +7 -1
- package/dist/stores/file/index.d.ts +8 -0
- package/dist/stores/file/index.js +12 -0
- package/dist/stores/file/session-policy-store.d.ts +0 -13
- package/dist/stores/file/session-policy-store.js +7 -1
- package/dist/stores/file/session-store.d.ts +4 -1
- package/dist/stores/file/session-store.js +7 -1
- package/dist/stores/file/strategy-store.d.ts +97 -0
- package/dist/stores/file/strategy-store.js +340 -0
- package/package.json +1 -1
- 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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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) =>
|
|
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
|
-
|
|
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) {
|