@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.
- package/CHANGELOG.md +49 -0
- package/dist/agents/roster-store.js +4 -1
- package/dist/agents/subagent.d.ts +6 -0
- package/dist/agents/subagent.js +126 -1
- package/dist/agents/teacher.js +4 -1
- 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/mcp.d.ts +7 -1
- package/dist/core/mcp.js +64 -8
- package/dist/core/memory-engine/engine.d.ts +30 -1
- package/dist/core/memory-engine/engine.js +219 -18
- 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 +87 -0
- package/dist/core/memory-engine/types.d.ts +13 -1
- package/dist/core/runner/prepare-task.js +15 -1
- package/dist/core/task-registry-agent.js +6 -0
- package/dist/core/types.d.ts +17 -0
- 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/package.json +1 -1
|
@@ -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(
|
|
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)
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
-
|
|
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;
|