@sema-agent/core 5.44.0 → 5.46.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 +109 -0
- package/dist/agents/subagent.d.ts +3 -1
- package/dist/agents/subagent.js +1 -1
- package/dist/brain/reasoning.d.ts +50 -4
- package/dist/brain/reasoning.js +28 -7
- package/dist/brain/request-params.d.ts +0 -12
- package/dist/brain/request-params.js +1 -1
- package/dist/core/checkpoint-store.d.ts +12 -0
- package/dist/core/governance-codes.js +1 -0
- package/dist/core/hooks.d.ts +9 -2
- package/dist/core/hooks.js +6 -5
- package/dist/core/memory-engine/content-origin.d.ts +9 -4
- package/dist/core/memory-engine/delegation-provenance.d.ts +12 -7
- package/dist/core/memory-engine/engine.d.ts +50 -3
- package/dist/core/memory-engine/engine.js +194 -32
- package/dist/core/memory-engine/export-bundle.d.ts +10 -1
- package/dist/core/memory-engine/export-bundle.js +21 -0
- package/dist/core/memory-engine/file-backend.d.ts +33 -4
- package/dist/core/memory-engine/file-backend.js +165 -39
- package/dist/core/memory-engine/frontmatter.d.ts +42 -1
- package/dist/core/memory-engine/frontmatter.js +141 -1
- package/dist/core/memory-engine/header-hints.d.ts +17 -0
- package/dist/core/memory-engine/header-hints.js +6 -0
- package/dist/core/memory-engine/index.d.ts +4 -3
- package/dist/core/memory-engine/index.js +3 -2
- package/dist/core/memory-engine/layout.d.ts +25 -2
- package/dist/core/memory-engine/layout.js +25 -12
- package/dist/core/memory-engine/memory-backend-contract.js +65 -0
- package/dist/core/memory-engine/sync-client.d.ts +1 -1
- package/dist/core/memory-engine/sync-client.js +33 -1
- package/dist/core/memory-engine/tools.d.ts +7 -0
- package/dist/core/memory-engine/tools.js +3 -0
- package/dist/core/memory-engine/types.d.ts +75 -1
- package/dist/core/memory-engine/types.js +1 -1
- package/dist/core/reminder-mint.d.ts +70 -0
- package/dist/core/reminder-mint.js +25 -0
- package/dist/core/runner/assemble-result.d.ts +5 -0
- package/dist/core/runner/assemble-result.js +1 -1
- package/dist/core/runner/git-status-frame.d.ts +3 -14
- package/dist/core/runner/git-status-frame.js +39 -14
- package/dist/core/runner/prepare-config-doors.d.ts +10 -0
- package/dist/core/runner/prepare-config-doors.js +32 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +5 -11
- package/dist/core/runner/prepare-hands-readface.js +26 -0
- package/dist/core/runner/prepare-memory.d.ts +11 -0
- package/dist/core/runner/prepare-memory.js +12 -10
- package/dist/core/runner/prepare-task.d.ts +25 -2
- package/dist/core/runner/prepare-task.js +71 -14
- package/dist/core/runner/runtask.js +82 -72
- package/dist/core/side-query.d.ts +11 -1
- package/dist/core/side-query.js +3 -0
- package/dist/core/trace.d.ts +17 -2
- package/dist/core/types.d.ts +119 -8
- package/dist/engine/harness/types.d.ts +46 -1
- package/dist/engine/harness/types.js +11 -0
- package/dist/engine/session/import-validate.js +6 -1
- package/dist/engine/session/session.d.ts +20 -0
- package/dist/engine/session/session.js +26 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/orchestration/run-workflow-tool.d.ts +16 -0
- package/dist/orchestration/run-workflow-tool.js +23 -3
- package/dist/orchestration/workflow-governance.d.ts +8 -1
- package/dist/prompt-assembly/epoch.js +2 -0
- package/dist/prompt-assembly/types.d.ts +6 -0
- package/dist/prompts/default.d.ts +13 -1
- package/dist/prompts/default.js +5 -1
- package/dist/tools/fs/fs-bash.d.ts +4 -0
- package/dist/tools/fs/fs-bash.js +1 -1
- package/dist/tools/fs/fs-read.d.ts +1 -1
- package/dist/tools/fs/fs-read.js +8 -7
- package/dist/tools/fs/fs-shared.d.ts +10 -4
- package/dist/tools/fs/fs-shared.js +6 -3
- package/dist/tools/fs/gh-rate-limit.d.ts +4 -1
- package/dist/tools/fs/gh-rate-limit.js +3 -2
- package/dist/tools/fs/index.d.ts +10 -2
- package/dist/tools/fs/index.js +2 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +12 -1
|
@@ -3,8 +3,10 @@ import { dirname, join, relative, sep } from "node:path";
|
|
|
3
3
|
import { uuidv7 } from "../../internal/harness.js";
|
|
4
4
|
import { MAX_MEMORY_BYTES, composeMemoryBlock, firstSentence } from "../memory.js";
|
|
5
5
|
import { inlineUntrusted } from "../untrusted-text.js";
|
|
6
|
+
import { mintSystemReminder } from "../reminder-mint.js";
|
|
6
7
|
import { formatMemoryAge } from "../memory-recall.js";
|
|
7
|
-
import { computeEntryRev, parseEntryFile, serializeEntryFile } from "./frontmatter.js";
|
|
8
|
+
import { committedOriginOf, computeEntryRev, hasOriginFormExtra, originEquals, parseEntryFile, serializeEntryFile, stripModelWrittenOrigin } from "./frontmatter.js";
|
|
9
|
+
import { isInstructionEntry } from "./header-hints.js";
|
|
8
10
|
import { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, canonicalJsonStringify, captureErasureInput, erasureRequestInvalid, erasureSelectHash, scanEntryFiles, } from "./file-backend.js";
|
|
9
11
|
import { assembleMemoryExportBundle, computeMemoryBundleHash, memoryBundleInvalid, } from "./export-bundle.js";
|
|
10
12
|
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";
|
|
@@ -50,13 +52,22 @@ export const MASS_DELETION_FUSE_RATIO = 0.5;
|
|
|
50
52
|
let indexCaptureSeq = 0;
|
|
51
53
|
export function memorySessionPollutedNotice(input) {
|
|
52
54
|
const reason = inlineUntrusted(input.reason, 200);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
const message = input.provenance === "carry"
|
|
56
|
+
? `Memory for this session is marked externally exposed: ${reason}. Ordinary memory entries it writes are ` +
|
|
57
|
+
`still committed to the long-term library, each carrying an engine-minted external-origin marker (the ` +
|
|
58
|
+
`content scan gates run unchanged); instruction-form files (type: feedback, or pinned/triggers/applies-when ` +
|
|
59
|
+
`header lines) are withheld and quarantined for host review, and this session's index prose additions are ` +
|
|
60
|
+
`not retained. Contributions this session already committed are challenged (a sweep that cannot run ` +
|
|
61
|
+
`announces memory.challenge_sweep_failed). What a given harvest actually withheld is announced as ` +
|
|
62
|
+
`memory.harvest_quarantined.`
|
|
63
|
+
: `Memory for this session is marked polluted: ${reason}. Its memory writes are no longer eligible for the ` +
|
|
56
64
|
`long-term library — a harvest withholds what it collects (quarantined for host review, judged against the ` +
|
|
57
65
|
`session's opening baseline, so changes made before this point are withheld too), and contributions this ` +
|
|
58
66
|
`session already committed are challenged (a sweep that cannot run announces memory.challenge_sweep_failed). ` +
|
|
59
|
-
`What a given harvest actually contained is announced as memory.harvest_quarantined
|
|
67
|
+
`What a given harvest actually contained is announced as memory.harvest_quarantined.`;
|
|
68
|
+
return {
|
|
69
|
+
code: "memory.session_polluted",
|
|
70
|
+
message,
|
|
60
71
|
detail: { reason, ...(input.sessionId !== undefined ? { sessionId: input.sessionId } : {}) },
|
|
61
72
|
};
|
|
62
73
|
}
|
|
@@ -71,9 +82,12 @@ export function pollutionContainmentCounts(report) {
|
|
|
71
82
|
}
|
|
72
83
|
export function memoryHarvestQuarantinedNotice(input) {
|
|
73
84
|
const reason = input.reason !== undefined ? inlineUntrusted(input.reason, 200) : undefined;
|
|
85
|
+
const lead = input.provenance === "carry"
|
|
86
|
+
? `Memory harvest withheld instruction-form files for this externally exposed session${reason !== undefined ? ` (${reason})` : ""} (ordinary entries committed with an external-origin marker): `
|
|
87
|
+
: `Memory harvest committed nothing for this polluted session${reason !== undefined ? ` (${reason})` : ""}: `;
|
|
74
88
|
return {
|
|
75
89
|
code: "memory.harvest_quarantined",
|
|
76
|
-
message:
|
|
90
|
+
message: lead +
|
|
77
91
|
`${input.count} memory entry file(s) written or changed in it were withheld, ${input.moved} of them moved into ` +
|
|
78
92
|
`the control-plane quarantine directory for host review` +
|
|
79
93
|
(input.escalated > 0
|
|
@@ -100,9 +114,17 @@ export class MemoryEngine {
|
|
|
100
114
|
maxDepth;
|
|
101
115
|
harvestDeadlineMs;
|
|
102
116
|
harvestFileBudget;
|
|
117
|
+
provenance;
|
|
103
118
|
onIncident;
|
|
104
119
|
backendPinnedRoot;
|
|
105
120
|
constructor(opts) {
|
|
121
|
+
if (opts.provenance !== undefined && opts.provenance !== "off" && opts.provenance !== "carry") {
|
|
122
|
+
const got = typeof opts.provenance === "string" ? JSON.stringify(opts.provenance) : opts.provenance === null ? "null" : typeof opts.provenance;
|
|
123
|
+
const e = new Error(`MemoryEngineOptions.provenance must be "off" or "carry" when present (got ${got}) — an unevaluable provenance mode is refused loudly, never folded to either mode.`);
|
|
124
|
+
e.code = "config.memory_provenance";
|
|
125
|
+
throw e;
|
|
126
|
+
}
|
|
127
|
+
this.provenance = opts.provenance ?? "carry";
|
|
106
128
|
this.backend = opts.backend;
|
|
107
129
|
this.memoryDir = opts.memoryDir;
|
|
108
130
|
const pinned = opts.backend.directoryRoot;
|
|
@@ -153,12 +175,12 @@ export class MemoryEngine {
|
|
|
153
175
|
}
|
|
154
176
|
}
|
|
155
177
|
pollutedSessions = new Map();
|
|
156
|
-
markSessionPolluted(sessionId, reason) {
|
|
178
|
+
markSessionPolluted(sessionId, reason, cause) {
|
|
157
179
|
if (!this.pollutedSessions.has(sessionId))
|
|
158
|
-
this.pollutedSessions.set(sessionId, { at: this.now(), reason });
|
|
180
|
+
this.pollutedSessions.set(sessionId, { at: this.now(), reason, ...(cause !== undefined ? { cause } : {}) });
|
|
159
181
|
let outcome;
|
|
160
182
|
try {
|
|
161
|
-
outcome = markSessionPolluted(this.controlDir, sessionId, reason, this.now);
|
|
183
|
+
outcome = markSessionPolluted(this.controlDir, sessionId, reason, this.now, cause);
|
|
162
184
|
}
|
|
163
185
|
catch {
|
|
164
186
|
outcome = "unpersisted";
|
|
@@ -177,7 +199,8 @@ export class MemoryEngine {
|
|
|
177
199
|
}
|
|
178
200
|
try {
|
|
179
201
|
const rec = this.sessionPollution(sessionId) ?? { at: this.now(), reason };
|
|
180
|
-
const
|
|
202
|
+
const swept = lineageContributionsOfSession(this.controlDir, sessionId);
|
|
203
|
+
const contributions = this.provenance === "carry" ? swept.filter((c) => c.marked !== true) : swept;
|
|
181
204
|
this.challengeAndAnnounce(contributions.map((c) => ({
|
|
182
205
|
eventId: `pollution:${sessionId}:${rec.at}:${c.entryId}`,
|
|
183
206
|
entryId: c.entryId,
|
|
@@ -702,6 +725,8 @@ export class MemoryEngine {
|
|
|
702
725
|
return;
|
|
703
726
|
const events = [];
|
|
704
727
|
for (const p of promoted) {
|
|
728
|
+
if (this.provenance === "carry" && p.marked === true)
|
|
729
|
+
continue;
|
|
705
730
|
let rec;
|
|
706
731
|
try {
|
|
707
732
|
rec = readSessionPollution(this.controlDir, p.sessionId);
|
|
@@ -856,7 +881,7 @@ export class MemoryEngine {
|
|
|
856
881
|
const drained = drainMemoryAnnouncements(this.controlDir);
|
|
857
882
|
if (drained.queue.length > 0 || drained.folded > 0) {
|
|
858
883
|
announcements = drained.queue;
|
|
859
|
-
announceBlock = renderAnnouncements(drained.queue, drained.folded);
|
|
884
|
+
announceBlock = renderAnnouncements(drained.queue, drained.folded, opts?.reminderMark);
|
|
860
885
|
if (handle.writeScope === null)
|
|
861
886
|
announceBlock = `${announceBlock}\n\n${MEMORY_ANNOUNCEMENT_READONLY_PLANE_CODA}`;
|
|
862
887
|
}
|
|
@@ -923,7 +948,11 @@ export class MemoryEngine {
|
|
|
923
948
|
return report;
|
|
924
949
|
}
|
|
925
950
|
async harvestCore(handle, opts) {
|
|
926
|
-
|
|
951
|
+
const openingRecord = opts?.sessionId !== undefined ? this.sessionPollution(opts.sessionId) : undefined;
|
|
952
|
+
let pollutedReason = opts?.polluted?.reason ?? openingRecord?.reason;
|
|
953
|
+
let pollutionCause = opts?.polluted?.cause ?? openingRecord?.cause;
|
|
954
|
+
const carry = this.provenance === "carry";
|
|
955
|
+
const mintOrigin = () => ({ taint: "external", cause: pollutionCause ?? "observed", at: this.now() });
|
|
927
956
|
const lineageSessionId = opts?.sessionId;
|
|
928
957
|
const startedAt = this.now();
|
|
929
958
|
const report = {
|
|
@@ -971,7 +1000,8 @@ export class MemoryEngine {
|
|
|
971
1000
|
if (pollutedReason !== undefined && lineageSessionId !== undefined) {
|
|
972
1001
|
const rec2 = this.sessionPollution(lineageSessionId);
|
|
973
1002
|
if (rec2 !== undefined) {
|
|
974
|
-
const
|
|
1003
|
+
const replaySwept = lineageContributionsOfSession(this.controlDir, lineageSessionId);
|
|
1004
|
+
const contributions = carry ? replaySwept.filter((c) => c.marked !== true) : replaySwept;
|
|
975
1005
|
this.challengeAndAnnounce(contributions.map((c) => ({
|
|
976
1006
|
eventId: `pollution:${lineageSessionId}:${rec2.at}:${c.entryId}`,
|
|
977
1007
|
entryId: c.entryId,
|
|
@@ -1023,6 +1053,9 @@ export class MemoryEngine {
|
|
|
1023
1053
|
presentIds.add(r.parsed.id);
|
|
1024
1054
|
const recordByPath = new Map(records.map((r) => [r.canonical, r]));
|
|
1025
1055
|
const pollutedStubs = new Map(handle.materialized.filter((m) => !m.readonly && m.stub).map((m) => [m.path, m]));
|
|
1056
|
+
const withheldWhy = carry
|
|
1057
|
+
? "this session is marked externally exposed and this file is instruction-form (type: feedback, or pinned/triggers/applies-when header lines) — instruction-form entries from exposed sessions are withheld for host review"
|
|
1058
|
+
: "this session invoked a tool classified as an external content source, so its memory changes were not committed";
|
|
1026
1059
|
const containPollutedRecord = async (f) => {
|
|
1027
1060
|
const rel = f.rel;
|
|
1028
1061
|
if (f.canonical !== handle.writableRoot && !f.canonical.startsWith(`${handle.writableRoot}${sep}`)) {
|
|
@@ -1042,7 +1075,7 @@ export class MemoryEngine {
|
|
|
1042
1075
|
report.rejections.push({
|
|
1043
1076
|
path: rel,
|
|
1044
1077
|
code: "polluted",
|
|
1045
|
-
reason: `memory write withheld:
|
|
1078
|
+
reason: `memory write withheld: ${withheldWhy} (archived-body stub; ${stubCaptured && sq.removed ? "file moved to quarantine for host review, and the next session re-projects the stub" : "containment incomplete — the edited stub may still be on the model-visible plane"})`,
|
|
1046
1079
|
});
|
|
1047
1080
|
if (sq.detail !== undefined || !sq.removed || !stubCaptured) {
|
|
1048
1081
|
const detail = `${sq.detail ?? (stubCaptured ? "suspect content still on the model-visible plane" : "quarantine capture failed")}${sq.removed ? "" : " — NOT contained"}`;
|
|
@@ -1073,7 +1106,7 @@ export class MemoryEngine {
|
|
|
1073
1106
|
report.rejections.push({
|
|
1074
1107
|
path: rel,
|
|
1075
1108
|
code: "polluted",
|
|
1076
|
-
reason: `memory write withheld:
|
|
1109
|
+
reason: `memory write withheld: ${withheldWhy} (${captured && contained ? "file moved to quarantine for host review" : captured ? "quarantine copy captured; removal from the model-visible plane incomplete" : "quarantine copy FAILED; file removal " + (contained ? "succeeded" : "incomplete")})`,
|
|
1077
1110
|
});
|
|
1078
1111
|
if (q.detail !== undefined || !contained || !captured) {
|
|
1079
1112
|
const parts = [q.detail, !captured ? "quarantine capture failed" : undefined].filter((x) => x !== undefined);
|
|
@@ -1115,6 +1148,22 @@ export class MemoryEngine {
|
|
|
1115
1148
|
restorePollutedIndex();
|
|
1116
1149
|
report.warnings.push(`memory harvest committed nothing this session: ${inlineUntrusted(pollutedReason, 200)}`);
|
|
1117
1150
|
};
|
|
1151
|
+
const containExposedInstructionDomain = async () => {
|
|
1152
|
+
for (const f of records) {
|
|
1153
|
+
let instruction = isInstructionEntry(f.parsed.frontmatter);
|
|
1154
|
+
if (!instruction) {
|
|
1155
|
+
const knownId = handle.baseIds.get(f.canonical);
|
|
1156
|
+
if (knownId !== undefined) {
|
|
1157
|
+
const committedFm = await this.committedFrontmatterFor(knownId);
|
|
1158
|
+
instruction = committedFm !== undefined && isInstructionEntry(committedFm);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
if (instruction)
|
|
1162
|
+
await containPollutedRecord(f);
|
|
1163
|
+
}
|
|
1164
|
+
restorePollutedIndex();
|
|
1165
|
+
report.warnings.push(`memory harvest refused by an incident while this session is marked externally exposed — no entries were committed; instruction-form files were withheld and the derived index rolled back: ${inlineUntrusted(pollutedReason, 200)}`);
|
|
1166
|
+
};
|
|
1118
1167
|
for (const m of handle.materialized.filter((x) => x.readonly)) {
|
|
1119
1168
|
const text = readSafe(m.path);
|
|
1120
1169
|
if (text === undefined) {
|
|
@@ -1157,7 +1206,7 @@ export class MemoryEngine {
|
|
|
1157
1206
|
detail: `${missingWritable.length}/${writable.length} materialized memory files are missing — judged an accident, harvest refused (no delete patches produced; explicit frontmatter tombstones are the only deletion channel)`,
|
|
1158
1207
|
};
|
|
1159
1208
|
if (pollutedReason !== undefined)
|
|
1160
|
-
await containPollutedDomain();
|
|
1209
|
+
await (carry ? containExposedInstructionDomain() : containPollutedDomain());
|
|
1161
1210
|
return report;
|
|
1162
1211
|
}
|
|
1163
1212
|
const indexPath = join(handle.writableRoot, MEMORY_INDEX_FILENAME);
|
|
@@ -1166,7 +1215,7 @@ export class MemoryEngine {
|
|
|
1166
1215
|
report.ok = false;
|
|
1167
1216
|
report.incident = { kind: "index_cleared", detail: "MEMORY.md was emptied alongside missing memory files — judged an accident, harvest refused" };
|
|
1168
1217
|
if (pollutedReason !== undefined)
|
|
1169
|
-
await containPollutedDomain();
|
|
1218
|
+
await (carry ? containExposedInstructionDomain() : containPollutedDomain());
|
|
1170
1219
|
return report;
|
|
1171
1220
|
}
|
|
1172
1221
|
for (const m of missingWritable) {
|
|
@@ -1187,7 +1236,7 @@ export class MemoryEngine {
|
|
|
1187
1236
|
.sort((a, b) => Number(baselinePaths.has(b.canonical)) - Number(baselinePaths.has(a.canonical)) || a.slug.localeCompare(b.slug));
|
|
1188
1237
|
const kept = [];
|
|
1189
1238
|
for (const f of inDomain) {
|
|
1190
|
-
if (pollutedReason === undefined && kept.length >= this.maxFiles) {
|
|
1239
|
+
if ((pollutedReason === undefined || carry) && kept.length >= this.maxFiles) {
|
|
1191
1240
|
report.rejections.push({ path: f.rel, code: "file_cap", reason: `memory file count exceeds the cap (${this.maxFiles}); consolidate before adding more` });
|
|
1192
1241
|
continue;
|
|
1193
1242
|
}
|
|
@@ -1206,7 +1255,7 @@ export class MemoryEngine {
|
|
|
1206
1255
|
let processed = 0;
|
|
1207
1256
|
for (let i = 0; i < kept.length; i++) {
|
|
1208
1257
|
const f = kept[i];
|
|
1209
|
-
if (pollutedReason === undefined && (processed >= this.harvestFileBudget || this.now() - startedAt > this.harvestDeadlineMs)) {
|
|
1258
|
+
if ((pollutedReason === undefined || carry) && (processed >= this.harvestFileBudget || this.now() - startedAt > this.harvestDeadlineMs)) {
|
|
1210
1259
|
report.degraded = {
|
|
1211
1260
|
reason: processed >= this.harvestFileBudget ? "file_budget" : "deadline",
|
|
1212
1261
|
pending: kept.slice(i).map((r) => r.rel),
|
|
@@ -1222,7 +1271,7 @@ export class MemoryEngine {
|
|
|
1222
1271
|
if (fastBaseId !== undefined && !stubByPath.has(f.canonical) && revOfText(f.text, fastBaseId) === revByBasePath.get(f.canonical))
|
|
1223
1272
|
continue;
|
|
1224
1273
|
processed++;
|
|
1225
|
-
if (pollutedReason !== undefined) {
|
|
1274
|
+
if (pollutedReason !== undefined && !carry) {
|
|
1226
1275
|
await containPollutedRecord(f);
|
|
1227
1276
|
continue;
|
|
1228
1277
|
}
|
|
@@ -1282,6 +1331,31 @@ export class MemoryEngine {
|
|
|
1282
1331
|
if (id === undefined)
|
|
1283
1332
|
id = uuidv7();
|
|
1284
1333
|
const fm = { ...parsed.frontmatter };
|
|
1334
|
+
const committedIdHere = baseId ?? (parsed.id !== undefined && basePathById.has(parsed.id) ? parsed.id : undefined);
|
|
1335
|
+
const committedFmHere = committedIdHere !== undefined ? await this.committedFrontmatterFor(committedIdHere) : undefined;
|
|
1336
|
+
if (stripModelWrittenOrigin(fm)) {
|
|
1337
|
+
const committedEcho = committedFmHere !== undefined ? committedOriginOf(committedFmHere) : undefined;
|
|
1338
|
+
const benignEcho = committedEcho !== undefined && originEquals(parsed.frontmatter.origin, committedEcho) && !hasOriginFormExtra(parsed.frontmatter);
|
|
1339
|
+
if (!benignEcho) {
|
|
1340
|
+
report.warnings.push(`${rel}: origin-form frontmatter written on the model-visible plane was not adopted — the external-origin marker is engine-authored (the engine's own record of this session decides it)`);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
if (carry && pollutedReason !== undefined && (isInstructionEntry(fm) || (committedFmHere !== undefined && isInstructionEntry(committedFmHere)))) {
|
|
1344
|
+
await containPollutedRecord(f);
|
|
1345
|
+
continue;
|
|
1346
|
+
}
|
|
1347
|
+
if (fm.deleted !== true) {
|
|
1348
|
+
const committedOrigin = committedFmHere !== undefined ? committedOriginOf(committedFmHere) : undefined;
|
|
1349
|
+
if (committedOrigin !== undefined) {
|
|
1350
|
+
fm.origin = committedOrigin;
|
|
1351
|
+
if (committedFmHere.origin === undefined) {
|
|
1352
|
+
report.warnings.push(`${rel}: the entry's committed external-origin marker was carried in a legacy byte form — normalized into the typed origin field (the marker follows the content)`);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
else if (carry && pollutedReason !== undefined) {
|
|
1356
|
+
fm.origin = mintOrigin();
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1285
1359
|
if (fm.name === undefined)
|
|
1286
1360
|
fm.name = f.slug.split("/").pop();
|
|
1287
1361
|
if (fm.description === undefined && !fm.deleted)
|
|
@@ -1333,13 +1407,13 @@ export class MemoryEngine {
|
|
|
1333
1407
|
patches.push({ op: "add", id: entry.id, entry });
|
|
1334
1408
|
}
|
|
1335
1409
|
let patchReport;
|
|
1336
|
-
if (pollutedReason !== undefined) {
|
|
1410
|
+
if (pollutedReason !== undefined && !carry) {
|
|
1337
1411
|
patchReport = { applied: [], conflicts: [] };
|
|
1338
1412
|
}
|
|
1339
1413
|
else {
|
|
1340
1414
|
const txnId = uuidv7();
|
|
1341
|
-
const pendingRows = patches.filter((p) => p.op !== "delete" && p.entry !== undefined).map((p) => ({ entryId: p.id, rev: p.entry.rev }));
|
|
1342
|
-
|
|
1415
|
+
const pendingRows = 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 } : {}) }));
|
|
1416
|
+
let lineageArmed = lineageSessionId !== undefined && pendingRows.length > 0;
|
|
1343
1417
|
if (lineageArmed) {
|
|
1344
1418
|
try {
|
|
1345
1419
|
stageLineagePending(this.controlDir, txnId, lineageSessionId, pendingRows, this.now);
|
|
@@ -1351,7 +1425,7 @@ export class MemoryEngine {
|
|
|
1351
1425
|
}
|
|
1352
1426
|
}
|
|
1353
1427
|
const preCommit = lineageSessionId !== undefined ? this.sessionPollution(lineageSessionId) : undefined;
|
|
1354
|
-
if (preCommit !== undefined) {
|
|
1428
|
+
if (preCommit !== undefined && !carry) {
|
|
1355
1429
|
if (lineageArmed) {
|
|
1356
1430
|
try {
|
|
1357
1431
|
discardLineagePending(this.controlDir, txnId);
|
|
@@ -1365,6 +1439,74 @@ export class MemoryEngine {
|
|
|
1365
1439
|
patchReport = { applied: [], conflicts: [] };
|
|
1366
1440
|
}
|
|
1367
1441
|
else {
|
|
1442
|
+
if (carry && preCommit !== undefined && pollutedReason === undefined) {
|
|
1443
|
+
pollutedReason = preCommit.reason;
|
|
1444
|
+
pollutionCause = preCommit.cause;
|
|
1445
|
+
const keptPatches = [];
|
|
1446
|
+
const droppedIds = new Set();
|
|
1447
|
+
for (const p of patches) {
|
|
1448
|
+
if (p.op === "delete") {
|
|
1449
|
+
const committedFm = await this.committedFrontmatterFor(p.id);
|
|
1450
|
+
if (committedFm !== undefined && isInstructionEntry(committedFm)) {
|
|
1451
|
+
const rec = records.find((r) => (handle.baseIds.get(r.canonical) ?? r.parsed.id) === p.id && r.parsed.frontmatter.deleted === true);
|
|
1452
|
+
if (rec !== undefined)
|
|
1453
|
+
await containPollutedRecord(rec);
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
keptPatches.push(p);
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
if (p.entry === undefined) {
|
|
1460
|
+
keptPatches.push(p);
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
const committedFm = await this.committedFrontmatterFor(p.id);
|
|
1464
|
+
if (isInstructionEntry(p.entry.frontmatter) || (committedFm !== undefined && isInstructionEntry(committedFm))) {
|
|
1465
|
+
const projection = pendingProjections.find((pp) => pp.entry.id === p.id);
|
|
1466
|
+
const rec = projection !== undefined ? records.find((r) => r.canonical === projection.path) : undefined;
|
|
1467
|
+
if (rec !== undefined)
|
|
1468
|
+
await containPollutedRecord(rec);
|
|
1469
|
+
droppedIds.add(p.id);
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
if (p.entry.frontmatter.origin === undefined) {
|
|
1473
|
+
p.entry.frontmatter.origin = mintOrigin();
|
|
1474
|
+
p.entry.rev = computeEntryRev(p.entry);
|
|
1475
|
+
for (const pp of pendingProjections)
|
|
1476
|
+
if (pp.entry.id === p.id)
|
|
1477
|
+
pp.needed = true;
|
|
1478
|
+
}
|
|
1479
|
+
keptPatches.push(p);
|
|
1480
|
+
}
|
|
1481
|
+
patches.length = 0;
|
|
1482
|
+
patches.push(...keptPatches);
|
|
1483
|
+
for (let pi = pendingProjections.length - 1; pi >= 0; pi--) {
|
|
1484
|
+
if (droppedIds.has(pendingProjections[pi].entry.id))
|
|
1485
|
+
pendingProjections.splice(pi, 1);
|
|
1486
|
+
}
|
|
1487
|
+
const restagedRows = 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 } : {}) }));
|
|
1488
|
+
if (lineageArmed) {
|
|
1489
|
+
if (restagedRows.length > 0) {
|
|
1490
|
+
try {
|
|
1491
|
+
stageLineagePending(this.controlDir, txnId, lineageSessionId, restagedRows, this.now);
|
|
1492
|
+
}
|
|
1493
|
+
catch (err) {
|
|
1494
|
+
report.ok = false;
|
|
1495
|
+
report.incident = { kind: "sidecar_corrupt", detail: `memory lineage restage refused: ${err instanceof Error ? err.message : String(err)}` };
|
|
1496
|
+
return report;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
else {
|
|
1500
|
+
try {
|
|
1501
|
+
discardLineagePending(this.controlDir, txnId);
|
|
1502
|
+
}
|
|
1503
|
+
catch (err) {
|
|
1504
|
+
report.warnings.push(`memory lineage stage for withdrawn instruction entries could not be discarded — their ids stay latched (memory reads refuse them) until the host adjudicates pending transaction ${txnId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1505
|
+
}
|
|
1506
|
+
lineageArmed = false;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1368
1510
|
try {
|
|
1369
1511
|
patchReport = await this.backend.applyPatches(patches);
|
|
1370
1512
|
}
|
|
@@ -1416,7 +1558,7 @@ export class MemoryEngine {
|
|
|
1416
1558
|
}
|
|
1417
1559
|
catch {
|
|
1418
1560
|
}
|
|
1419
|
-
const postCommit = lineageSessionId !== undefined ? this.sessionPollution(lineageSessionId) : undefined;
|
|
1561
|
+
const postCommit = lineageSessionId !== undefined && pollutedReason === undefined ? this.sessionPollution(lineageSessionId) : undefined;
|
|
1420
1562
|
if (postCommit !== undefined) {
|
|
1421
1563
|
const appliedNonDelete = patchReport.applied.filter((a) => a.op !== "delete").map((a) => a.id);
|
|
1422
1564
|
try {
|
|
@@ -1460,7 +1602,9 @@ export class MemoryEngine {
|
|
|
1460
1602
|
report.inboundFindings = drained;
|
|
1461
1603
|
if (pollutedReason !== undefined) {
|
|
1462
1604
|
restorePollutedIndex();
|
|
1463
|
-
report.warnings.push(
|
|
1605
|
+
report.warnings.push(carry
|
|
1606
|
+
? `memory entries from this externally exposed session were committed with an external-origin marker; its index prose additions were not retained: ${inlineUntrusted(pollutedReason, 200)}`
|
|
1607
|
+
: `memory harvest committed nothing this session: ${inlineUntrusted(pollutedReason, 200)}`);
|
|
1464
1608
|
}
|
|
1465
1609
|
const indexGate = this.gateDerivedIndex(handle);
|
|
1466
1610
|
if (indexGate !== undefined) {
|
|
@@ -1648,6 +1792,22 @@ export class MemoryEngine {
|
|
|
1648
1792
|
return viaBackend;
|
|
1649
1793
|
return readSafe(join(this.controlDir, "shadow", `${id}.md`));
|
|
1650
1794
|
}
|
|
1795
|
+
async committedFrontmatterFor(id) {
|
|
1796
|
+
const zeroCopy = this.backendPinnedRoot !== undefined && canonicalize(this.backendPinnedRoot) === canonicalize(this.memoryDir);
|
|
1797
|
+
if (!zeroCopy) {
|
|
1798
|
+
const face = this.backend.retrievalView?.() ?? this.backend;
|
|
1799
|
+
try {
|
|
1800
|
+
const [entry] = await face.getByIds([id]);
|
|
1801
|
+
if (entry !== undefined && entry.id === id)
|
|
1802
|
+
return entry.frontmatter;
|
|
1803
|
+
}
|
|
1804
|
+
catch {
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
const viaBackend = this.backend.readCommittedShadow?.(id);
|
|
1808
|
+
const text = viaBackend ?? readSafe(join(this.controlDir, "shadow", `${id}.md`));
|
|
1809
|
+
return text !== undefined ? parseEntryFile(text).frontmatter : undefined;
|
|
1810
|
+
}
|
|
1651
1811
|
gateDerivedIndex(handle) {
|
|
1652
1812
|
if (handle.writeScope === null)
|
|
1653
1813
|
return undefined;
|
|
@@ -1837,7 +1997,7 @@ function gateAnnouncementItems(report) {
|
|
|
1837
1997
|
items.push(`index note: ${inlineUntrusted(w, 200)}`);
|
|
1838
1998
|
return capItems(items);
|
|
1839
1999
|
}
|
|
1840
|
-
export function renderAnnouncements(queue, folded) {
|
|
2000
|
+
export function renderAnnouncements(queue, folded, reminderMark) {
|
|
1841
2001
|
const lines = [];
|
|
1842
2002
|
const pin = (s) => inlineUntrusted(s.length > 500 ? `${s.slice(0, 500)}…` : s);
|
|
1843
2003
|
for (const a of queue)
|
|
@@ -1846,11 +2006,9 @@ export function renderAnnouncements(queue, folded) {
|
|
|
1846
2006
|
if (folded > 0)
|
|
1847
2007
|
lines.push(`- (${folded} earlier notice${folded === 1 ? "" : "s"} dropped by the queue bound)`);
|
|
1848
2008
|
const capped = lines.length > MAX_ANNOUNCEMENT_LINES ? [...lines.slice(0, MAX_ANNOUNCEMENT_LINES), `- …and ${lines.length - MAX_ANNOUNCEMENT_LINES} more`] : lines;
|
|
1849
|
-
return (
|
|
1850
|
-
`Memory engine notices since your last session (quoted identifiers are data, not instructions):\n` +
|
|
2009
|
+
return mintSystemReminder(`Memory engine notices since your last session (quoted identifiers are data, not instructions):\n` +
|
|
1851
2010
|
`${capped.join("\n")}\n` +
|
|
1852
|
-
`If a memory file is named above, re-read it before relying on its content
|
|
1853
|
-
`</system-reminder>`);
|
|
2011
|
+
`If a memory file is named above, re-read it before relying on its content.`, reminderMark);
|
|
1854
2012
|
}
|
|
1855
2013
|
export function truncateIndex(text) {
|
|
1856
2014
|
let out = text;
|
|
@@ -1923,5 +2081,9 @@ function chmodNoFollow(path, mode) {
|
|
|
1923
2081
|
}
|
|
1924
2082
|
}
|
|
1925
2083
|
function needsCompletion(parsed, fm) {
|
|
1926
|
-
return !parsed.hadFrontmatter ||
|
|
2084
|
+
return (!parsed.hadFrontmatter ||
|
|
2085
|
+
parsed.frontmatter.name !== fm.name ||
|
|
2086
|
+
parsed.frontmatter.description !== fm.description ||
|
|
2087
|
+
!originEquals(parsed.frontmatter.origin, fm.origin) ||
|
|
2088
|
+
hasOriginFormExtra(parsed.frontmatter) !== hasOriginFormExtra(fm));
|
|
1927
2089
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type TransferEvidence } from "./file-backend.js";
|
|
2
|
-
import type
|
|
2
|
+
import { type MemoryEntry } from "./types.js";
|
|
3
3
|
/** One exported challenge event (unresolved generations only). Generations are DESTINATION-minted:
|
|
4
4
|
* the bundle deliberately carries no generation number — the importing store re-mints events under
|
|
5
5
|
* its own idempotent namespace and allocates generations there. */
|
|
@@ -15,6 +15,10 @@ export interface BundlePollutedSession {
|
|
|
15
15
|
sessionId: string;
|
|
16
16
|
at: number;
|
|
17
17
|
reason: string;
|
|
18
|
+
/** design/336 — the record's structured mechanical cause, carried verbatim (absent = the source
|
|
19
|
+
* record predates the vocabulary or genuinely recorded none; the destination's mints then fall
|
|
20
|
+
* back to "observed"). */
|
|
21
|
+
cause?: import("./types.js").MemoryOriginCause;
|
|
18
22
|
}
|
|
19
23
|
/** One exported committed lineage contribution (verbatim `lastRev`/`lastAt`). */
|
|
20
24
|
export interface BundleLineageRow {
|
|
@@ -22,6 +26,11 @@ export interface BundleLineageRow {
|
|
|
22
26
|
sessionId: string;
|
|
23
27
|
lastRev: string;
|
|
24
28
|
lastAt: number;
|
|
29
|
+
/** design/336 — the contribution's content carries its external-origin marker (see
|
|
30
|
+
* {@link import("./layout.js").LineageContribution.marked}). MUST travel with the row: dropping
|
|
31
|
+
* it re-quarantines the source's tag-admissions at the destination (the import pollution sweep
|
|
32
|
+
* challenges unmarked contributions of imported polluted sessions). */
|
|
33
|
+
marked?: true;
|
|
25
34
|
}
|
|
26
35
|
/**
|
|
27
36
|
* design/178 v2 §4.3 + v2-c §6 — the governance-complete export bundle. The governance section is
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { canonicalJsonStringify } from "./file-backend.js";
|
|
3
3
|
import { computeEntryRev } from "./frontmatter.js";
|
|
4
|
+
import { MEMORY_ORIGIN_CAUSES } from "./types.js";
|
|
5
|
+
import { ambiguousOriginRepresentation, committedOriginOf } from "./frontmatter.js";
|
|
4
6
|
export const BUNDLE_SECTION_KEYS = ["meta", "entries", "governance", "residuals"];
|
|
5
7
|
function sha256Line(text) {
|
|
6
8
|
return createHash("sha256").update(`${text}\n`, "utf8").digest("hex");
|
|
@@ -69,6 +71,15 @@ function entryInvalid(raw, scopes) {
|
|
|
69
71
|
return `entry ${JSON.stringify(e.id)} frontmatter.provenance is malformed`;
|
|
70
72
|
}
|
|
71
73
|
}
|
|
74
|
+
if (fm.origin !== undefined) {
|
|
75
|
+
const o = fm.origin;
|
|
76
|
+
if (!isRecord(o) || o.taint !== "external" || typeof o.at !== "number" || !Number.isFinite(o.at) || (o.cause !== undefined && !(typeof o.cause === "string" && MEMORY_ORIGIN_CAUSES.includes(o.cause)))) {
|
|
77
|
+
return `entry ${JSON.stringify(e.id)} frontmatter.origin is malformed`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (ambiguousOriginRepresentation(fm)) {
|
|
81
|
+
return `entry ${JSON.stringify(e.id)} carries conflicting or duplicated origin representations (ambiguous marker representation)`;
|
|
82
|
+
}
|
|
72
83
|
const recomputed = computeEntryRev({ id: e.id, frontmatter: fm, body: e.body });
|
|
73
84
|
if (recomputed !== e.rev)
|
|
74
85
|
return `entry ${JSON.stringify(e.id)} rev ${JSON.stringify(e.rev)} does not equal its recomputed content rev ${JSON.stringify(recomputed)} (forged or corrupted package)`;
|
|
@@ -156,6 +167,8 @@ export function memoryBundleInvalid(raw, opts = {}) {
|
|
|
156
167
|
return "polluted-session row has no at";
|
|
157
168
|
if (typeof raw2.reason !== "string")
|
|
158
169
|
return "polluted-session row has no reason";
|
|
170
|
+
if (raw2.cause !== undefined && !(typeof raw2.cause === "string" && MEMORY_ORIGIN_CAUSES.includes(raw2.cause)))
|
|
171
|
+
return "polluted-session row cause is not a known spelling";
|
|
159
172
|
if (pollutedIds.has(raw2.sessionId))
|
|
160
173
|
return `bundle.governance.pollutedSessions carries sessionId ${JSON.stringify(raw2.sessionId)} twice (bad package)`;
|
|
161
174
|
pollutedIds.add(raw2.sessionId);
|
|
@@ -175,6 +188,14 @@ export function memoryBundleInvalid(raw, opts = {}) {
|
|
|
175
188
|
return "lineage row has no lastRev";
|
|
176
189
|
if (typeof raw2.lastAt !== "number")
|
|
177
190
|
return "lineage row has no lastAt";
|
|
191
|
+
if (raw2.marked !== undefined && raw2.marked !== true)
|
|
192
|
+
return "lineage row marked is not the literal true";
|
|
193
|
+
if (raw2.marked === true) {
|
|
194
|
+
const carried = entriesArr.find((e) => e.id === raw2.entryId);
|
|
195
|
+
if (carried !== undefined && committedOriginOf(carried.frontmatter) === undefined) {
|
|
196
|
+
return `bundle.governance.lineage row (${String(raw2.entryId)}, ${String(raw2.sessionId)}) claims marked:true but the carried entry has no origin marker (dishonest package)`;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
178
199
|
const key = `${raw2.entryId}\u0000${raw2.sessionId}`;
|
|
179
200
|
if (lineagePairs.has(key))
|
|
180
201
|
return `bundle.governance.lineage carries (${raw2.entryId}, ${raw2.sessionId}) twice (bad package)`;
|
|
@@ -323,11 +323,13 @@ export interface MemoryExportSnapshot {
|
|
|
323
323
|
sessionId: string;
|
|
324
324
|
lastRev: string;
|
|
325
325
|
lastAt: number;
|
|
326
|
+
marked?: true;
|
|
326
327
|
}>;
|
|
327
328
|
pollutedSessions: Array<{
|
|
328
329
|
sessionId: string;
|
|
329
330
|
at: number;
|
|
330
331
|
reason: string;
|
|
332
|
+
cause?: import("./types.js").MemoryOriginCause;
|
|
331
333
|
}>;
|
|
332
334
|
quarantined: string[];
|
|
333
335
|
quarantineOpaque: number;
|
|
@@ -419,6 +421,14 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
|
|
|
419
421
|
private adoptionNoticeKeys;
|
|
420
422
|
/** Test seam (§7.4 r4-④ arm): force a post-commit-point transfer-append failure. */
|
|
421
423
|
private transfersAppendFault?;
|
|
424
|
+
/** Test seam (#262 ③/#191 review r1 fence pins): runs inside `recoverJournal` between the redo
|
|
425
|
+
* replay and the commit-boundary ownership re-assertion — a test flips the lock owner (or swaps
|
|
426
|
+
* the journal) here to exercise the mid-replay steal arms. Production leaves it unset. */
|
|
427
|
+
private recoveryCommitProbe?;
|
|
428
|
+
/** Test seam (#262 ③/#191 review r2): runs inside the torn-tail heal between the marker work and
|
|
429
|
+
* the pre-truncation ownership re-assertion — a test flips the lock owner here to prove the
|
|
430
|
+
* chain is never truncated after an ownership loss. Production leaves it unset. */
|
|
431
|
+
private chainHealProbe?;
|
|
422
432
|
/** Test seam (v2-c §3-2 fence pins): runs INSIDE the export composite's mutex hold, after the
|
|
423
433
|
* data reads and before the second fingerprint — a test injects a governance-face write here to
|
|
424
434
|
* exercise the drift-retry and the three-round refusal. Production leaves it unset. */
|
|
@@ -511,11 +521,23 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
|
|
|
511
521
|
/** Is the durable chain-degradation marker in place? Presence is ENOENT-only-absent (a probe
|
|
512
522
|
* failure is never read as "not degraded" — fail-closed). */
|
|
513
523
|
private chainDegradedMarkerPresent;
|
|
514
|
-
/** §3-4 r6 — mint the chain-degradation marker DURABLY (wx + fsync +
|
|
515
|
-
* Idempotent on an existing marker. Throws when the marker cannot be
|
|
516
|
-
* (the heal) must then ABORT with the chain untouched: truncating
|
|
517
|
-
* window this marker closes ("chain reads complete, no marker"
|
|
524
|
+
/** §3-4 r6 — mint the chain-degradation marker DURABLY (wx + fsync data + fsync DIRECTORY +
|
|
525
|
+
* read-back verification). Idempotent on an existing marker. Throws when the marker cannot be
|
|
526
|
+
* proven on disk — the caller (the heal) must then ABORT with the chain untouched: truncating
|
|
527
|
+
* first would reopen the exact window this marker closes ("chain reads complete, no marker"
|
|
528
|
+
* after an out-of-band tear). */
|
|
518
529
|
private mintChainDegradedMarker;
|
|
530
|
+
/** backlog #270 — make a just-created control-plane directory ENTRY power-loss durable: open the
|
|
531
|
+
* control-plane root and fsync it. Fail-closed on failure: the one caller (the marker mint) must
|
|
532
|
+
* abort its heal with the chain byte-untouched — an unproven marker must never authorize a
|
|
533
|
+
* truncation, and the abort leaves the SAFER state either way (the standing marker or the still-
|
|
534
|
+
* visible tear both re-enter the heal on the next chain read). Platform notes: Node's fsync maps
|
|
535
|
+
* to fsync(2) — on macOS a full power-loss guarantee would additionally need F_FULLFSYNC
|
|
536
|
+
* (fcntl), which Node cannot reach; this is the same accepted bar as every other fsync in this
|
|
537
|
+
* store. On win32 a directory cannot be opened as an fd (fs.open rejects directories there), so
|
|
538
|
+
* the entry-durability half is structurally out of reach — an explicit documented arm, not a
|
|
539
|
+
* swallowed error; NTFS metadata journaling is the (weaker) cover that platform gets. */
|
|
540
|
+
private fsyncControlPlaneDir;
|
|
519
541
|
/**
|
|
520
542
|
* Read the WHOLE evidence chain in order (validated, collision-checked), healing a TORN TAIL (a
|
|
521
543
|
* crash mid-append): the fragment is quarantined for evidence, the sound prefix stands (lines are
|
|
@@ -530,6 +552,13 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
|
|
|
530
552
|
* Same-`ev` collision law (§4.4, read-time arm): canonical-form-equal duplicates are tolerated
|
|
531
553
|
* (append-idempotency residue); DIFFERENT payloads under one ev are fail-closed — an event id is
|
|
532
554
|
* one identity, and a reader must never pick a side by file order.
|
|
555
|
+
*
|
|
556
|
+
* `fenceToken` (#262 ③/#191 review r1) — this "read" MUTATES on a torn tail (quarantine + marker
|
|
557
|
+
* + truncation), so a caller whose entry assert is far behind it (the recovery leg's validation
|
|
558
|
+
* reads sit after unbounded parsing work) passes its lock token and the heal re-verifies
|
|
559
|
+
* ownership immediately before its first mutation: a recoverer stolen from mid-validation must
|
|
560
|
+
* not truncate a chain the stealer is appending to. Callers whose entry assert is adjacent keep
|
|
561
|
+
* their documented assert→act residue and may omit it.
|
|
533
562
|
*/
|
|
534
563
|
private readTransferChain;
|
|
535
564
|
/** The chain's ev → canonical-payload map (§4.4 upgraded load shape — the append path compares
|