@sema-agent/core 5.46.0 → 5.48.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 +93 -0
- package/dist/agents/agent-transcript-tool.d.ts +4 -0
- package/dist/agents/agent-transcript-tool.js +10 -3
- package/dist/agents/send-message-tool.d.ts +43 -1
- package/dist/agents/send-message-tool.js +50 -11
- package/dist/agents/subagent.d.ts +18 -0
- package/dist/agents/subagent.js +231 -4
- package/dist/config/defaults.d.ts +20 -0
- package/dist/config/defaults.js +5 -0
- package/dist/core/background-agent-store.d.ts +1 -0
- package/dist/core/background-agent-store.js +13 -0
- package/dist/core/governance-codes.d.ts +13 -0
- package/dist/core/governance-codes.js +33 -0
- package/dist/core/mcp.d.ts +6 -1
- package/dist/core/mcp.js +34 -7
- package/dist/core/memory-engine/delegation-settlement.d.ts +318 -0
- package/dist/core/memory-engine/delegation-settlement.js +661 -0
- package/dist/core/memory-engine/engine.d.ts +159 -1
- package/dist/core/memory-engine/engine.js +699 -15
- package/dist/core/memory-engine/file-backend.d.ts +1 -0
- package/dist/core/memory-engine/file-backend.js +3 -1
- package/dist/core/memory-engine/frontmatter.d.ts +46 -19
- package/dist/core/memory-engine/frontmatter.js +91 -77
- 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 +14 -0
- package/dist/core/memory-engine/layout.js +2 -2
- package/dist/core/memory-engine/memory-backend-contract.js +43 -0
- package/dist/core/memory-engine/origin-clearance.d.ts +66 -0
- package/dist/core/memory-engine/origin-clearance.js +84 -0
- package/dist/core/memory-engine/provenance-wording.d.ts +50 -0
- package/dist/core/memory-engine/provenance-wording.js +15 -0
- package/dist/core/memory-engine/tools.d.ts +61 -7
- package/dist/core/memory-engine/tools.js +34 -9
- package/dist/core/memory-engine/types.d.ts +70 -2
- package/dist/core/reminder-disclosure.d.ts +90 -0
- package/dist/core/reminder-disclosure.js +64 -0
- package/dist/core/runner/prepare-acquire-reconcile.d.ts +6 -0
- package/dist/core/runner/prepare-acquire-reconcile.js +1 -1
- package/dist/core/runner/prepare-hands-readface.d.ts +4 -0
- package/dist/core/runner/prepare-hands-readface.js +1 -0
- package/dist/core/runner/prepare-memory.js +50 -15
- package/dist/core/runner/prepare-task.d.ts +39 -0
- package/dist/core/runner/prepare-task.js +128 -40
- package/dist/core/runner/runtask.js +3 -1
- package/dist/core/session-reconcile.js +3 -2
- package/dist/core/session-store.d.ts +59 -1
- package/dist/core/session-store.js +82 -14
- package/dist/core/session.d.ts +83 -1
- package/dist/core/task-registry-agent.d.ts +28 -0
- package/dist/core/task-registry-agent.js +63 -2
- package/dist/core/task-registry.d.ts +21 -0
- package/dist/core/task-registry.js +4 -1
- package/dist/core/types.d.ts +98 -3
- package/dist/core/types.js +3 -0
- package/dist/core/untrusted-text.d.ts +63 -0
- package/dist/core/untrusted-text.js +48 -0
- package/dist/core/wiring-manifest.d.ts +35 -0
- package/dist/core/wiring-manifest.js +21 -1
- package/dist/engine/harness/types.d.ts +36 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.js +6 -4
- package/dist/internal/harness-types.d.ts +1 -0
- package/dist/stores/file/index.d.ts +19 -3
- package/dist/stores/file/index.js +24 -1
- package/dist/stores/file/session-store.d.ts +18 -4
- package/dist/stores/file/session-store.js +73 -12
- package/dist/tools/fs/fs-pdf.d.ts +12 -1
- package/dist/tools/fs/fs-pdf.js +17 -3
- package/dist/tools/fs/fs-read.d.ts +2 -1
- package/dist/tools/fs/fs-read.js +33 -5
- package/dist/tools/fs/fs-shared.d.ts +6 -2
- package/dist/tools/fs/index.d.ts +7 -0
- package/dist/tools/fs/index.js +1 -1
- package/dist/tools/task-list.d.ts +5 -1
- package/dist/tools/web.js +21 -2
- package/package.json +3 -2
- package/test/export-surface.snapshot.json +24 -2
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { closeSync, constants as fsConstants, existsSync, fchmodSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { dirname, join, relative, sep } from "node:path";
|
|
3
4
|
import { uuidv7 } from "../../internal/harness.js";
|
|
4
5
|
import { MAX_MEMORY_BYTES, composeMemoryBlock, firstSentence } from "../memory.js";
|
|
5
6
|
import { inlineUntrusted } from "../untrusted-text.js";
|
|
6
7
|
import { mintSystemReminder } from "../reminder-mint.js";
|
|
7
8
|
import { formatMemoryAge } from "../memory-recall.js";
|
|
8
|
-
import { committedOriginOf, computeEntryRev, hasOriginFormExtra, originEquals, parseEntryFile, serializeEntryFile, stripModelWrittenOrigin } from "./frontmatter.js";
|
|
9
|
+
import { committedOriginOf, computeEntryRev, hasOriginFormExtra, isValidEntryId, originEquals, parseEntryFile, serializeEntryFile, stripModelWrittenOrigin } from "./frontmatter.js";
|
|
10
|
+
import { MEMORY_PROVENANCE_RECALL_SENTENCE, memoryExposureIndexRow, parseMemoryExposureIndexRow } from "./provenance-wording.js";
|
|
9
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";
|
|
13
|
+
import { noReplaceRestore } from "./delegation-settlement.js";
|
|
14
|
+
import { openOriginClearance, readOriginClearances, settleOriginClearance } from "./origin-clearance.js";
|
|
10
15
|
import { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, canonicalJsonStringify, captureErasureInput, erasureRequestInvalid, erasureSelectHash, scanEntryFiles, } from "./file-backend.js";
|
|
11
16
|
import { assembleMemoryExportBundle, computeMemoryBundleHash, memoryBundleInvalid, } from "./export-bundle.js";
|
|
12
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";
|
|
@@ -36,6 +41,25 @@ export function buildMemoryInstruction(memoryDir, instructionFileName) {
|
|
|
36
41
|
return MEMORY_INSTRUCTION_TEMPLATE.replaceAll("{{MEMORY_DIR}}", () => dir).replaceAll("{{INSTRUCTION_FILE}}", () => instructionFileName ?? "CLAUDE.md");
|
|
37
42
|
}
|
|
38
43
|
export const MEMORY_RECALL_DISCIPLINE = "Before answering questions about earlier work, decisions, dates, people, or the user's preferences, look them up: `memory_search` finds entries by keyword and `memory_get` reads a full entry — the injected memory index only lists what exists. When a lookup comes up empty, say that you checked memory and found nothing instead of guessing.";
|
|
44
|
+
export function memoryRecallDisciplineSegment(provenance) {
|
|
45
|
+
return provenance === "carry" ? `${MEMORY_RECALL_DISCIPLINE} ${MEMORY_PROVENANCE_RECALL_SENTENCE}` : MEMORY_RECALL_DISCIPLINE;
|
|
46
|
+
}
|
|
47
|
+
export function entryFileHeadCarriesOrigin(absPath, maxBytes = 64 * 1024) {
|
|
48
|
+
try {
|
|
49
|
+
const fd = openSync(absPath, "r");
|
|
50
|
+
try {
|
|
51
|
+
const buf = Buffer.alloc(maxBytes);
|
|
52
|
+
const n = readSync(fd, buf, 0, maxBytes, 0);
|
|
53
|
+
return committedOriginOf(parseEntryFile(buf.subarray(0, n).toString("utf8")).frontmatter) !== undefined;
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
closeSync(fd);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
39
63
|
export const MEMORY_PREFERENCE_DISCIPLINE = "When the user confirms a stored preference or fact still holds, refresh that entry's `last-confirmed: <YYYY-MM-DD>` frontmatter line (add it when absent). When you save a preference, add an `applies-when: <context>` frontmatter line naming when it applies. Both are plain frontmatter lines — write them yourself; nothing fills them in for you.";
|
|
40
64
|
export const MEMORY_ANNOUNCEMENT_READONLY_PLANE_CODA = "The notices immediately above concern a READ-ONLY memory store: any guidance in them to record, update, or tombstone an entry cannot be applied to that store this session — surface it to the user instead of claiming it done.";
|
|
41
65
|
export const MEMORY_ANNOUNCEMENT_READONLY_CODA = "The memory store itself is not writable this session, so any guidance above to record, update, or tombstone a memory entry cannot be applied here — surface it to the user instead of claiming it done.";
|
|
@@ -48,6 +72,7 @@ export const STUB_ARCHIVED_LINE = "[body archived — request hydration by listi
|
|
|
48
72
|
export const DEFAULT_MAX_MEMORY_FILES = 500;
|
|
49
73
|
export const DEFAULT_HARVEST_DEADLINE_MS = 5_000;
|
|
50
74
|
export const DEFAULT_HARVEST_FILE_BUDGET = 2_000;
|
|
75
|
+
export const DEFAULT_HOLD_SETTLE_TIMEOUT_MS = 72 * 60 * 60 * 1000;
|
|
51
76
|
export const MASS_DELETION_FUSE_RATIO = 0.5;
|
|
52
77
|
let indexCaptureSeq = 0;
|
|
53
78
|
export function memorySessionPollutedNotice(input) {
|
|
@@ -82,6 +107,14 @@ export function pollutionContainmentCounts(report) {
|
|
|
82
107
|
}
|
|
83
108
|
export function memoryHarvestQuarantinedNotice(input) {
|
|
84
109
|
const reason = input.reason !== undefined ? inlineUntrusted(input.reason, 200) : undefined;
|
|
110
|
+
if (input.count === 0 && input.indexRolledBack === true) {
|
|
111
|
+
return {
|
|
112
|
+
code: "memory.harvest_quarantined",
|
|
113
|
+
message: `Memory harvest rolled the derived index (MEMORY.md) back to its materialize-time baseline${reason !== undefined ? ` (${reason})` : ""}: ` +
|
|
114
|
+
`this session's index prose additions were captured to control-plane quarantine and not retained. No entry files were withheld.`,
|
|
115
|
+
detail: { count: 0, moved: 0, escalated: 0, indexRolledBack: true, ...(reason !== undefined ? { reason } : {}), ...(input.sessionId !== undefined ? { sessionId: input.sessionId } : {}) },
|
|
116
|
+
};
|
|
117
|
+
}
|
|
85
118
|
const lead = input.provenance === "carry"
|
|
86
119
|
? `Memory harvest withheld instruction-form files for this externally exposed session${reason !== undefined ? ` (${reason})` : ""} (ordinary entries committed with an external-origin marker): `
|
|
87
120
|
: `Memory harvest committed nothing for this polluted session${reason !== undefined ? ` (${reason})` : ""}: `;
|
|
@@ -103,6 +136,39 @@ export function memoryHarvestQuarantinedNotice(input) {
|
|
|
103
136
|
},
|
|
104
137
|
};
|
|
105
138
|
}
|
|
139
|
+
export function memoryHoldNotices(report, sessionId) {
|
|
140
|
+
const c = report.containment;
|
|
141
|
+
if (c === undefined)
|
|
142
|
+
return [];
|
|
143
|
+
const caps = (paths) => paths.slice(0, 20).map((p) => inlineUntrusted(p, 160));
|
|
144
|
+
const notices = [];
|
|
145
|
+
if (c.heldInstruction.length > 0) {
|
|
146
|
+
notices.push({
|
|
147
|
+
code: "memory.hold_opened",
|
|
148
|
+
message: `Memory harvest HELD ${c.heldInstruction.length} instruction-form entry file(s): this session's delegation outcome ` +
|
|
149
|
+
`is still pending, so instruction entries wait off the model-visible plane and commit automatically once every ` +
|
|
150
|
+
`delegation settles clean (a dirty or expired settlement disposes them to quarantine, announced; resolveHold is the host valve).`,
|
|
151
|
+
detail: { count: c.heldInstruction.length, paths: caps(c.heldInstruction), ...(sessionId !== undefined ? { sessionId } : {}) },
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (c.releasedHolds.length > 0) {
|
|
155
|
+
notices.push({
|
|
156
|
+
code: "memory.hold_released",
|
|
157
|
+
message: `Memory harvest RELEASED ${c.releasedHolds.length} held instruction entry file(s) — their writer sessions settled clean (or a host valve released them) and the entries re-walked the full gate set and committed.`,
|
|
158
|
+
detail: { count: c.releasedHolds.length, paths: caps(c.releasedHolds), ...(sessionId !== undefined ? { sessionId } : {}) },
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (c.disposedHolds.length > 0) {
|
|
162
|
+
notices.push({
|
|
163
|
+
code: "memory.hold_disposed",
|
|
164
|
+
message: `Memory harvest DISPOSED ${c.disposedHolds.length} held instruction entry file(s) to control-plane quarantine ` +
|
|
165
|
+
`(terminals: ${[...new Set(c.disposedHolds.map((d) => d.terminal))].join(", ")}). An "expired" terminal is a TIMEOUT, not a ` +
|
|
166
|
+
`conviction — resolveHold(holdId, "release") commits such an entry with a "static"-cause marker after host review; the bytes are never silently dropped.`,
|
|
167
|
+
detail: { count: c.disposedHolds.length, disposed: c.disposedHolds.slice(0, 20).map((d) => ({ path: inlineUntrusted(d.relPath, 160), terminal: d.terminal })), ...(sessionId !== undefined ? { sessionId } : {}) },
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return notices;
|
|
171
|
+
}
|
|
106
172
|
export class MemoryEngine {
|
|
107
173
|
backend;
|
|
108
174
|
memoryDir;
|
|
@@ -114,6 +180,7 @@ export class MemoryEngine {
|
|
|
114
180
|
maxDepth;
|
|
115
181
|
harvestDeadlineMs;
|
|
116
182
|
harvestFileBudget;
|
|
183
|
+
holdSettleTimeoutMs;
|
|
117
184
|
provenance;
|
|
118
185
|
onIncident;
|
|
119
186
|
backendPinnedRoot;
|
|
@@ -142,8 +209,27 @@ export class MemoryEngine {
|
|
|
142
209
|
this.maxDepth = opts.maxDepth ?? DEFAULT_MAX_ENTRY_DEPTH;
|
|
143
210
|
this.harvestDeadlineMs = opts.harvestDeadlineMs ?? DEFAULT_HARVEST_DEADLINE_MS;
|
|
144
211
|
this.harvestFileBudget = opts.harvestFileBudget ?? DEFAULT_HARVEST_FILE_BUDGET;
|
|
212
|
+
if (opts.holdSettleTimeoutMs !== undefined && (typeof opts.holdSettleTimeoutMs !== "number" || !Number.isFinite(opts.holdSettleTimeoutMs) || opts.holdSettleTimeoutMs <= 0)) {
|
|
213
|
+
const got = typeof opts.holdSettleTimeoutMs === "number" ? String(opts.holdSettleTimeoutMs) : opts.holdSettleTimeoutMs === null ? "null" : typeof opts.holdSettleTimeoutMs;
|
|
214
|
+
const e = new Error(`MemoryEngineOptions.holdSettleTimeoutMs must be a finite positive number of milliseconds when present (got ${got}) — an unevaluable settlement window is refused loudly, never folded to the default.`);
|
|
215
|
+
e.code = "config.memory_hold_timeout";
|
|
216
|
+
throw e;
|
|
217
|
+
}
|
|
218
|
+
this.holdSettleTimeoutMs = opts.holdSettleTimeoutMs ?? DEFAULT_HOLD_SETTLE_TIMEOUT_MS;
|
|
145
219
|
this.onIncident = opts.onIncident;
|
|
146
220
|
}
|
|
221
|
+
discloseSessionAccountIncident(what, cause) {
|
|
222
|
+
const sink = this.onIncident;
|
|
223
|
+
if (sink === undefined)
|
|
224
|
+
return;
|
|
225
|
+
try {
|
|
226
|
+
const e = new Error(`memory session account: ${what}${cause !== undefined ? `: ${cause instanceof Error ? cause.message : String(cause)}` : ""}`);
|
|
227
|
+
e.code = "memory.session_account_failed";
|
|
228
|
+
sink(e);
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
}
|
|
232
|
+
}
|
|
147
233
|
discloseAnnounceFailure(stage, cause) {
|
|
148
234
|
const sink = this.onIncident;
|
|
149
235
|
if (sink === undefined)
|
|
@@ -756,6 +842,7 @@ export class MemoryEngine {
|
|
|
756
842
|
ensureDirExists(this.memoryDir);
|
|
757
843
|
ensureDirExists(this.controlDir);
|
|
758
844
|
const restricted = opts?.adoptionRestricted === true;
|
|
845
|
+
let sessionAccountFailed = false;
|
|
759
846
|
const readBackend = restricted
|
|
760
847
|
? (this.backend.restrictedAdoptionView?.({ audit: true, writeScope }) ?? this.backend)
|
|
761
848
|
: this.backend;
|
|
@@ -768,6 +855,112 @@ export class MemoryEngine {
|
|
|
768
855
|
scopeDirs.set(writeScope, registerScope(this.memoryDir, this.controlDir, writeScope));
|
|
769
856
|
for (const [scope, dir] of scopeDirs)
|
|
770
857
|
this.chmodScopeTree(dir, 0o755, 0o644, { excludeTopDirs: this.siblingScopeDirNames(dir, scope) });
|
|
858
|
+
if (this.provenance === "carry" && opts?.sessionId !== undefined && writeScope !== null && !restricted) {
|
|
859
|
+
let accountFailed = false;
|
|
860
|
+
try {
|
|
861
|
+
const foreign = foreignDanglingSessionAccounts(this.controlDir, opts.sessionId);
|
|
862
|
+
const unattributed = [];
|
|
863
|
+
if (foreign.length > 0) {
|
|
864
|
+
const wroot = canonicalize(scopeDirFor(this.memoryDir, this.controlDir, writeScope));
|
|
865
|
+
let committedRevs;
|
|
866
|
+
const snapFace = this.backend.committedSnapshotsOfScopes;
|
|
867
|
+
if (snapFace === undefined) {
|
|
868
|
+
this.discloseSessionAccountIncident("the backend exposes no committed-snapshot face — the id-bearing crash-residue arms are degraded to the id-less arm for this materialize", undefined);
|
|
869
|
+
}
|
|
870
|
+
if (snapFace !== undefined) {
|
|
871
|
+
const snap = await snapFace.call(this.backend, [writeScope]);
|
|
872
|
+
if (snap.complete && snap.rows !== undefined) {
|
|
873
|
+
committedRevs = new Map(snap.rows.map((r) => [r.id, r.rev]));
|
|
874
|
+
}
|
|
875
|
+
else {
|
|
876
|
+
this.discloseSessionAccountIncident("the committed snapshot was contended/incomplete — the id-bearing residue arm is degraded to id-less for this materialize", undefined);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
for (const f of scanEntryFiles(wroot, { maxDepth: this.maxDepth })) {
|
|
880
|
+
const canonical = canonicalize(f.path);
|
|
881
|
+
if (canonical !== wroot && !canonical.startsWith(`${wroot}${sep}`))
|
|
882
|
+
continue;
|
|
883
|
+
const text = readSafe(canonical);
|
|
884
|
+
if (text === undefined)
|
|
885
|
+
continue;
|
|
886
|
+
const rel = relative(this.memoryDir, canonical);
|
|
887
|
+
if (rel === MEMORY_INDEX_FILENAME || rel.endsWith(`${sep}${MEMORY_INDEX_FILENAME}`))
|
|
888
|
+
continue;
|
|
889
|
+
const parsed = parseEntryFile(text);
|
|
890
|
+
const uncommitted = parsed.id === undefined || (committedRevs !== undefined && !committedRevs.has(parsed.id));
|
|
891
|
+
if (uncommitted) {
|
|
892
|
+
unattributed.push(rel);
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
if (parsed.id !== undefined && committedRevs !== undefined) {
|
|
896
|
+
const committedRev = committedRevs.get(parsed.id);
|
|
897
|
+
const diskRev = computeEntryRev({ id: parsed.id, frontmatter: parsed.frontmatter, body: parsed.body });
|
|
898
|
+
if (committedRev !== undefined && diskRev !== committedRev) {
|
|
899
|
+
const committed = await this.committedContentFor(parsed.id);
|
|
900
|
+
if (committed !== undefined && committed !== text) {
|
|
901
|
+
let captured = false;
|
|
902
|
+
const captureName = `${this.now()}-residue-${parsed.id}.md`;
|
|
903
|
+
const capturePath = join(this.controlDir, QUARANTINE_DIR, captureName);
|
|
904
|
+
try {
|
|
905
|
+
ensureDirExists(join(this.controlDir, QUARANTINE_DIR));
|
|
906
|
+
writeFileSync(capturePath, text, { encoding: "utf8", flag: "wx" });
|
|
907
|
+
captured = readFileSync(capturePath, "utf8") === text;
|
|
908
|
+
}
|
|
909
|
+
catch {
|
|
910
|
+
captured = false;
|
|
911
|
+
}
|
|
912
|
+
let restoredCommitted = false;
|
|
913
|
+
if (captured) {
|
|
914
|
+
const stagingPath = `${capturePath}.staging`;
|
|
915
|
+
try {
|
|
916
|
+
renameSync(canonical, stagingPath);
|
|
917
|
+
if (readFileSync(stagingPath, "utf8") === text) {
|
|
918
|
+
writeFileNoFollow(canonical, committed);
|
|
919
|
+
restoredCommitted = true;
|
|
920
|
+
try {
|
|
921
|
+
unlinkSync(stagingPath);
|
|
922
|
+
}
|
|
923
|
+
catch {
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
else if (noReplaceRestore(stagingPath, canonical) === "stranded") {
|
|
927
|
+
this.discloseSessionAccountIncident(`a concurrent write to ${rel} landed during residue containment and could not be restored — its bytes are preserved at quarantine/${captureName}.staging`, undefined);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
catch {
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
this.discloseSessionAccountIncident(`the crash-residue edit of ${rel} could not be captured to quarantine — the divergent bytes were left in place (the inbound gate adjudicates them)`, undefined);
|
|
935
|
+
}
|
|
936
|
+
try {
|
|
937
|
+
enqueueMemoryAnnouncement(this.controlDir, {
|
|
938
|
+
kind: "external",
|
|
939
|
+
at: this.now(),
|
|
940
|
+
items: [
|
|
941
|
+
restoredCommitted
|
|
942
|
+
? `crash residue: an edit of committed memory entry ${quoteId(rel)} pre-existed this session under an unresolved foreign session account — its writer cannot be attributed, so the edit bytes were captured to control-plane quarantine (${captureName}) and the committed content restored (host review adjudicates the edit)`
|
|
943
|
+
: `crash residue: an edit of committed memory entry ${quoteId(rel)} pre-existed this session under an unresolved foreign session account and could NOT be fully contained — the divergent/newer bytes were left in place (or preserved beside the capture) for the ordinary inbound gate`,
|
|
944
|
+
],
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
catch (annErr) {
|
|
948
|
+
this.discloseAnnounceFailure("session-account residue", annErr);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
openSessionAccount(this.controlDir, { sessionId: opts.sessionId, now: this.now, unattributed });
|
|
956
|
+
}
|
|
957
|
+
catch (err) {
|
|
958
|
+
accountFailed = true;
|
|
959
|
+
this.discloseSessionAccountIncident("crash-residue attribution is degraded for this session (the harvest boundary refuses fail-closed while this stands)", err);
|
|
960
|
+
}
|
|
961
|
+
if (accountFailed)
|
|
962
|
+
sessionAccountFailed = true;
|
|
963
|
+
}
|
|
771
964
|
const allScopes = [...scopeDirs.keys()];
|
|
772
965
|
const headers = await readBackend.listHeaders(allScopes);
|
|
773
966
|
const entries = await readBackend.getByIds(headers.map((h) => h.id));
|
|
@@ -791,6 +984,7 @@ export class MemoryEngine {
|
|
|
791
984
|
indexBaselineLines: 0,
|
|
792
985
|
indexText: "",
|
|
793
986
|
...(restricted ? { adoptionRestricted: true } : {}),
|
|
987
|
+
...(sessionAccountFailed ? { sessionAccountFailed: true } : {}),
|
|
794
988
|
};
|
|
795
989
|
if (writeScope !== null)
|
|
796
990
|
ensureDirExists(handle.writableRoot);
|
|
@@ -955,6 +1149,18 @@ export class MemoryEngine {
|
|
|
955
1149
|
const mintOrigin = () => ({ taint: "external", cause: pollutionCause ?? "observed", at: this.now() });
|
|
956
1150
|
const lineageSessionId = opts?.sessionId;
|
|
957
1151
|
const startedAt = this.now();
|
|
1152
|
+
let sessionPending = false;
|
|
1153
|
+
let pendingSettleIds = [];
|
|
1154
|
+
let unattributedSet = new Set();
|
|
1155
|
+
const containment = { indexRolledBack: false, quarantinedInstruction: [], heldInstruction: [], releasedHolds: [], disposedHolds: [] };
|
|
1156
|
+
const signalContainment = (fill) => {
|
|
1157
|
+
fill(containment);
|
|
1158
|
+
report.containment = containment;
|
|
1159
|
+
};
|
|
1160
|
+
const pushDistinct = (arr, rel) => {
|
|
1161
|
+
if (!arr.includes(rel))
|
|
1162
|
+
arr.push(rel);
|
|
1163
|
+
};
|
|
958
1164
|
const report = {
|
|
959
1165
|
ok: true,
|
|
960
1166
|
patches: { add: 0, update: 0, delete: 0 },
|
|
@@ -981,6 +1187,11 @@ export class MemoryEngine {
|
|
|
981
1187
|
report.warnings.push(opts.admitNothing.reason);
|
|
982
1188
|
return report;
|
|
983
1189
|
}
|
|
1190
|
+
if (carry && handle.sessionAccountFailed === true) {
|
|
1191
|
+
report.ok = false;
|
|
1192
|
+
report.incident = { kind: "sidecar_corrupt", detail: "memory session account could not be opened at materialize — the crash-residue classification for this session never persisted, so the harvest refuses fail-closed (repair the control plane and re-materialize)" };
|
|
1193
|
+
return report;
|
|
1194
|
+
}
|
|
984
1195
|
try {
|
|
985
1196
|
this.backend.checkControlPlane?.();
|
|
986
1197
|
}
|
|
@@ -990,6 +1201,34 @@ export class MemoryEngine {
|
|
|
990
1201
|
return report;
|
|
991
1202
|
}
|
|
992
1203
|
try {
|
|
1204
|
+
if (carry) {
|
|
1205
|
+
reconcileHolds(this.controlDir, { memoryDir: this.memoryDir, now: this.now });
|
|
1206
|
+
const newlyExpired = expireOverdueSettlements(this.controlDir, { timeoutMs: this.holdSettleTimeoutMs, now: this.now });
|
|
1207
|
+
if (newlyExpired.length > 0) {
|
|
1208
|
+
report.warnings.push(`${newlyExpired.length} delegation settlement row(s) passed the ${this.holdSettleTimeoutMs}ms window without settling and are now EXPIRED — a timeout, not a conviction: the affected sessions' unprovable windows close exposed, their held instruction entries dispose to quarantine (resolveHold can still release one with a "static"-cause marker), and resolveSettlement can overturn the expiry with an audited ruling`);
|
|
1209
|
+
}
|
|
1210
|
+
replayExternalSettlementEffects(this.controlDir, { carry, now: this.now });
|
|
1211
|
+
const settled = await this.settleDueHolds(handle, report);
|
|
1212
|
+
for (const rel of settled.released)
|
|
1213
|
+
signalContainment((c) => pushDistinct(c.releasedHolds, rel));
|
|
1214
|
+
for (const d of settled.disposed)
|
|
1215
|
+
signalContainment((c) => c.disposedHolds.push(d));
|
|
1216
|
+
if (lineageSessionId !== undefined) {
|
|
1217
|
+
const rows = sessionSettlements(this.controlDir, lineageSessionId);
|
|
1218
|
+
const cls = classifySessionSettlements(rows);
|
|
1219
|
+
if (cls.state === "exposed") {
|
|
1220
|
+
if (pollutedReason === undefined) {
|
|
1221
|
+
pollutedReason = cls.reason ?? "the session's delegation settlement account holds an external, unattestable or expired row";
|
|
1222
|
+
pollutionCause = cls.cause;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
else if (cls.state === "pending") {
|
|
1226
|
+
sessionPending = true;
|
|
1227
|
+
pendingSettleIds = rows.filter((r) => r.effective === "pending").map((r) => r.row.settleId);
|
|
1228
|
+
}
|
|
1229
|
+
unattributedSet = sessionUnattributedSet(this.controlDir, lineageSessionId);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
993
1232
|
const rec = reconcileLineage(this.controlDir, this.now);
|
|
994
1233
|
this.settlePromotions(rec.promoted);
|
|
995
1234
|
for (const u of rec.undecidable) {
|
|
@@ -1056,7 +1295,8 @@ export class MemoryEngine {
|
|
|
1056
1295
|
const withheldWhy = carry
|
|
1057
1296
|
? "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
1297
|
: "this session invoked a tool classified as an external content source, so its memory changes were not committed";
|
|
1059
|
-
const containPollutedRecord = async (f) => {
|
|
1298
|
+
const containPollutedRecord = async (f, whyOverride) => {
|
|
1299
|
+
const why = whyOverride ?? withheldWhy;
|
|
1060
1300
|
const rel = f.rel;
|
|
1061
1301
|
if (f.canonical !== handle.writableRoot && !f.canonical.startsWith(`${handle.writableRoot}${sep}`)) {
|
|
1062
1302
|
report.rejections.push({ path: rel, code: "outside_root", reason: "file resolves outside the writable memory root (containment gate, fail-closed)" });
|
|
@@ -1075,7 +1315,7 @@ export class MemoryEngine {
|
|
|
1075
1315
|
report.rejections.push({
|
|
1076
1316
|
path: rel,
|
|
1077
1317
|
code: "polluted",
|
|
1078
|
-
reason: `memory write withheld: ${
|
|
1318
|
+
reason: `memory write withheld: ${why} (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"})`,
|
|
1079
1319
|
});
|
|
1080
1320
|
if (sq.detail !== undefined || !sq.removed || !stubCaptured) {
|
|
1081
1321
|
const detail = `${sq.detail ?? (stubCaptured ? "suspect content still on the model-visible plane" : "quarantine capture failed")}${sq.removed ? "" : " — NOT contained"}`;
|
|
@@ -1106,7 +1346,7 @@ export class MemoryEngine {
|
|
|
1106
1346
|
report.rejections.push({
|
|
1107
1347
|
path: rel,
|
|
1108
1348
|
code: "polluted",
|
|
1109
|
-
reason: `memory write withheld: ${
|
|
1349
|
+
reason: `memory write withheld: ${why} (${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")})`,
|
|
1110
1350
|
});
|
|
1111
1351
|
if (q.detail !== undefined || !contained || !captured) {
|
|
1112
1352
|
const parts = [q.detail, !captured ? "quarantine capture failed" : undefined].filter((x) => x !== undefined);
|
|
@@ -1120,11 +1360,11 @@ export class MemoryEngine {
|
|
|
1120
1360
|
const canonicalIndex = canonicalize(pollutedIndexPath);
|
|
1121
1361
|
if (canonicalIndex !== pollutedIndexPath && !canonicalIndex.startsWith(`${handle.writableRoot}${sep}`)) {
|
|
1122
1362
|
report.warnings.push("memory index NOT restored: its path resolves outside the writable memory root (containment gate, fail-closed)");
|
|
1123
|
-
return;
|
|
1363
|
+
return false;
|
|
1124
1364
|
}
|
|
1125
1365
|
const indexNow = readSafe(pollutedIndexPath);
|
|
1126
1366
|
if (indexNow === undefined || indexNow === handle.indexText)
|
|
1127
|
-
return;
|
|
1367
|
+
return false;
|
|
1128
1368
|
let indexCaptureLanded = false;
|
|
1129
1369
|
try {
|
|
1130
1370
|
const dest = join(this.controlDir, QUARANTINE_DIR, `${this.now()}-polluted-${MEMORY_INDEX_FILENAME}`);
|
|
@@ -1137,15 +1377,18 @@ export class MemoryEngine {
|
|
|
1137
1377
|
try {
|
|
1138
1378
|
writeFileNoFollow(pollutedIndexPath, handle.indexText);
|
|
1139
1379
|
report.warnings.push(`memory index restored to its pre-session state — this session's index additions were not retained (session polluted; ${indexCaptureLanded ? "the removed text was captured to quarantine" : "the quarantine capture did NOT land — the removed text is gone"})`);
|
|
1380
|
+
return true;
|
|
1140
1381
|
}
|
|
1141
1382
|
catch (err) {
|
|
1142
1383
|
report.warnings.push(`memory index could NOT be restored to its pre-session state: ${err instanceof Error ? err.message : String(err)}`);
|
|
1384
|
+
return false;
|
|
1143
1385
|
}
|
|
1144
1386
|
};
|
|
1145
1387
|
const containPollutedDomain = async () => {
|
|
1146
1388
|
for (const f of records)
|
|
1147
1389
|
await containPollutedRecord(f);
|
|
1148
|
-
restorePollutedIndex()
|
|
1390
|
+
if (restorePollutedIndex())
|
|
1391
|
+
signalContainment((c) => (c.indexRolledBack = true));
|
|
1149
1392
|
report.warnings.push(`memory harvest committed nothing this session: ${inlineUntrusted(pollutedReason, 200)}`);
|
|
1150
1393
|
};
|
|
1151
1394
|
const containExposedInstructionDomain = async () => {
|
|
@@ -1158,10 +1401,13 @@ export class MemoryEngine {
|
|
|
1158
1401
|
instruction = committedFm !== undefined && isInstructionEntry(committedFm);
|
|
1159
1402
|
}
|
|
1160
1403
|
}
|
|
1161
|
-
if (instruction)
|
|
1404
|
+
if (instruction) {
|
|
1162
1405
|
await containPollutedRecord(f);
|
|
1406
|
+
signalContainment((c) => pushDistinct(c.quarantinedInstruction, f.rel));
|
|
1407
|
+
}
|
|
1163
1408
|
}
|
|
1164
|
-
restorePollutedIndex()
|
|
1409
|
+
if (restorePollutedIndex())
|
|
1410
|
+
signalContainment((c) => (c.indexRolledBack = true));
|
|
1165
1411
|
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
1412
|
};
|
|
1167
1413
|
for (const m of handle.materialized.filter((x) => x.readonly)) {
|
|
@@ -1340,8 +1586,54 @@ export class MemoryEngine {
|
|
|
1340
1586
|
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
1587
|
}
|
|
1342
1588
|
}
|
|
1343
|
-
|
|
1589
|
+
const instructionForm = carry && (isInstructionEntry(fm) || (committedFmHere !== undefined && isInstructionEntry(committedFmHere)));
|
|
1590
|
+
if (instructionForm && unattributedSet.has(rel)) {
|
|
1591
|
+
await containPollutedRecord(f, "this file pre-existed the session under an unresolved foreign session account (crash residue) and is instruction-form — its writer cannot be attributed, so it is withheld for host review");
|
|
1592
|
+
signalContainment((c) => pushDistinct(c.quarantinedInstruction, rel));
|
|
1593
|
+
continue;
|
|
1594
|
+
}
|
|
1595
|
+
if (instructionForm && pollutedReason !== undefined) {
|
|
1344
1596
|
await containPollutedRecord(f);
|
|
1597
|
+
signalContainment((c) => pushDistinct(c.quarantinedInstruction, rel));
|
|
1598
|
+
continue;
|
|
1599
|
+
}
|
|
1600
|
+
if (instructionForm && sessionPending && lineageSessionId !== undefined) {
|
|
1601
|
+
const held = openInstructionHold(this.controlDir, {
|
|
1602
|
+
sessionId: lineageSessionId,
|
|
1603
|
+
relPath: rel,
|
|
1604
|
+
absPath: f.canonical,
|
|
1605
|
+
content: f.text,
|
|
1606
|
+
...(committedIdHere !== undefined ? { entryId: committedIdHere } : {}),
|
|
1607
|
+
op: baseId !== undefined ? "update" : "add",
|
|
1608
|
+
...(baseId !== undefined && revByBasePath.get(f.canonical) !== undefined ? { baseRev: revByBasePath.get(f.canonical) } : {}),
|
|
1609
|
+
slug: f.slug,
|
|
1610
|
+
scope: writeScope,
|
|
1611
|
+
settleIds: pendingSettleIds,
|
|
1612
|
+
now: this.now,
|
|
1613
|
+
});
|
|
1614
|
+
if (held.ok) {
|
|
1615
|
+
report.warnings.push(`${rel}: instruction-form entry HELD — this session's delegation outcome is still pending (attested-only evidence standard); the file was captured off the model-visible plane and commits automatically when every delegation settles clean, disposes to quarantine on a dirty or expired settlement (resolveHold is the host valve)`);
|
|
1616
|
+
if (held.thirdWriterStranded !== undefined) {
|
|
1617
|
+
report.warnings.push(`${rel}: a concurrent writer's bytes could not be restored onto the plane while the hold was captured — they are preserved at hold/${held.thirdWriterStranded} in the memory control plane for host recovery`);
|
|
1618
|
+
}
|
|
1619
|
+
signalContainment((c) => pushDistinct(c.heldInstruction, rel));
|
|
1620
|
+
if (committedIdHere !== undefined) {
|
|
1621
|
+
const committed = await this.committedContentFor(committedIdHere);
|
|
1622
|
+
if (committed !== undefined) {
|
|
1623
|
+
try {
|
|
1624
|
+
writeFileNoFollow(f.canonical, committed);
|
|
1625
|
+
report.restored.push(rel);
|
|
1626
|
+
}
|
|
1627
|
+
catch (err) {
|
|
1628
|
+
report.warnings.push(`${rel}: the committed content could not be restored after the hold captured this session's edit — the entry is unavailable on the plane until release or the next restore: ${err instanceof Error ? err.message : String(err)}`);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
else {
|
|
1634
|
+
await containPollutedRecord(f, `this session's delegation outcome is pending and this instruction-form file could not be HELD (${held.reason}) — withheld fail-closed for host review`);
|
|
1635
|
+
signalContainment((c) => pushDistinct(c.quarantinedInstruction, rel));
|
|
1636
|
+
}
|
|
1345
1637
|
continue;
|
|
1346
1638
|
}
|
|
1347
1639
|
if (fm.deleted !== true) {
|
|
@@ -1352,6 +1644,10 @@ export class MemoryEngine {
|
|
|
1352
1644
|
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
1645
|
}
|
|
1354
1646
|
}
|
|
1647
|
+
else if (carry && unattributedSet.has(rel)) {
|
|
1648
|
+
fm.origin = { taint: "external", cause: "unattributed", at: this.now() };
|
|
1649
|
+
report.warnings.push(`${rel}: this file pre-existed the session under an unresolved foreign session account (crash residue) — committed with an external-origin marker (cause "unattributed"), not under this session's name`);
|
|
1650
|
+
}
|
|
1355
1651
|
else if (carry && pollutedReason !== undefined) {
|
|
1356
1652
|
fm.origin = mintOrigin();
|
|
1357
1653
|
}
|
|
@@ -1449,8 +1745,10 @@ export class MemoryEngine {
|
|
|
1449
1745
|
const committedFm = await this.committedFrontmatterFor(p.id);
|
|
1450
1746
|
if (committedFm !== undefined && isInstructionEntry(committedFm)) {
|
|
1451
1747
|
const rec = records.find((r) => (handle.baseIds.get(r.canonical) ?? r.parsed.id) === p.id && r.parsed.frontmatter.deleted === true);
|
|
1452
|
-
if (rec !== undefined)
|
|
1748
|
+
if (rec !== undefined) {
|
|
1453
1749
|
await containPollutedRecord(rec);
|
|
1750
|
+
signalContainment((c) => pushDistinct(c.quarantinedInstruction, rec.rel));
|
|
1751
|
+
}
|
|
1454
1752
|
continue;
|
|
1455
1753
|
}
|
|
1456
1754
|
keptPatches.push(p);
|
|
@@ -1464,8 +1762,10 @@ export class MemoryEngine {
|
|
|
1464
1762
|
if (isInstructionEntry(p.entry.frontmatter) || (committedFm !== undefined && isInstructionEntry(committedFm))) {
|
|
1465
1763
|
const projection = pendingProjections.find((pp) => pp.entry.id === p.id);
|
|
1466
1764
|
const rec = projection !== undefined ? records.find((r) => r.canonical === projection.path) : undefined;
|
|
1467
|
-
if (rec !== undefined)
|
|
1765
|
+
if (rec !== undefined) {
|
|
1468
1766
|
await containPollutedRecord(rec);
|
|
1767
|
+
signalContainment((c) => pushDistinct(c.quarantinedInstruction, rec.rel));
|
|
1768
|
+
}
|
|
1469
1769
|
droppedIds.add(p.id);
|
|
1470
1770
|
continue;
|
|
1471
1771
|
}
|
|
@@ -1601,11 +1901,18 @@ export class MemoryEngine {
|
|
|
1601
1901
|
if (drained !== undefined && drained.length > 0)
|
|
1602
1902
|
report.inboundFindings = drained;
|
|
1603
1903
|
if (pollutedReason !== undefined) {
|
|
1604
|
-
restorePollutedIndex()
|
|
1904
|
+
if (restorePollutedIndex())
|
|
1905
|
+
signalContainment((c) => (c.indexRolledBack = true));
|
|
1605
1906
|
report.warnings.push(carry
|
|
1606
1907
|
? `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
1908
|
: `memory harvest committed nothing this session: ${inlineUntrusted(pollutedReason, 200)}`);
|
|
1608
1909
|
}
|
|
1910
|
+
else if (carry && sessionPending) {
|
|
1911
|
+
if (restorePollutedIndex()) {
|
|
1912
|
+
signalContainment((c) => (c.indexRolledBack = true));
|
|
1913
|
+
report.warnings.push("this session's index prose additions were not retained: its delegation outcome is still pending (prose has no entry anchor for the settlement account to govern; committed entries and mechanical index lines are unaffected)");
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1609
1916
|
const indexGate = this.gateDerivedIndex(handle);
|
|
1610
1917
|
if (indexGate !== undefined) {
|
|
1611
1918
|
report.rejections.push(indexGate.rejection);
|
|
@@ -1615,8 +1922,344 @@ export class MemoryEngine {
|
|
|
1615
1922
|
const headers = await this.backend.listHeaders([...new Set([...handle.scopes, writeScope])]);
|
|
1616
1923
|
handle.indexText = this.rebuildIndex(handle, headers, { write: true, ignoreOnDisk: indexGate !== undefined }, report.warnings);
|
|
1617
1924
|
await this.rebaseline(handle, new Set(report.degraded?.pending ?? []));
|
|
1925
|
+
if (carry && lineageSessionId !== undefined && report.degraded === undefined) {
|
|
1926
|
+
try {
|
|
1927
|
+
closeSessionAccount(this.controlDir, { sessionId: lineageSessionId, now: this.now });
|
|
1928
|
+
}
|
|
1929
|
+
catch (err) {
|
|
1930
|
+
report.warnings.push(`memory session account could not close (the dangling row keeps future crash residue attributed — over-holding, the safe side): ${err instanceof Error ? err.message : String(err)}`);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1618
1933
|
return report;
|
|
1619
1934
|
}
|
|
1935
|
+
async settleDueHolds(handle, report) {
|
|
1936
|
+
const out = { released: [], disposed: [] };
|
|
1937
|
+
const dispose = (row, terminal, why) => {
|
|
1938
|
+
const d = disposeHold(this.controlDir, { holdId: row.holdId, terminal, now: this.now });
|
|
1939
|
+
out.disposed.push({ relPath: row.relPath, terminal });
|
|
1940
|
+
report.warnings.push(`held instruction entry ${row.relPath} DISPOSED (${terminal}): ${why}${d.quarantineName !== undefined ? ` — captured bytes moved to control-plane quarantine (${d.quarantineName})` : ""}`);
|
|
1941
|
+
};
|
|
1942
|
+
for (const row of readHolds(this.controlDir)) {
|
|
1943
|
+
const valveRelease = row.resolved === "release" && (row.status === "held" || (row.status === "disposed" && row.disposition?.terminal === "expired"));
|
|
1944
|
+
if (valveRelease) {
|
|
1945
|
+
const released = await this.releaseHold(handle, report, row, { valve: true });
|
|
1946
|
+
if (released !== undefined)
|
|
1947
|
+
out.released.push(released);
|
|
1948
|
+
continue;
|
|
1949
|
+
}
|
|
1950
|
+
if (row.status !== "held")
|
|
1951
|
+
continue;
|
|
1952
|
+
if (row.resolved === "discard") {
|
|
1953
|
+
dispose(row, "discarded", "the host valve resolved this hold as discard");
|
|
1954
|
+
continue;
|
|
1955
|
+
}
|
|
1956
|
+
const marker = this.sessionPollution(row.sessionId);
|
|
1957
|
+
const rows = sessionSettlements(this.controlDir, row.sessionId);
|
|
1958
|
+
if (marker !== undefined || rows.some((r) => r.effective === "external")) {
|
|
1959
|
+
dispose(row, "dirty", marker !== undefined ? "the writer session is marked externally exposed" : "a delegation of the writer session settled with an observed external attestation");
|
|
1960
|
+
continue;
|
|
1961
|
+
}
|
|
1962
|
+
if (rows.some((r) => r.effective === "unattestable" || r.effective === "expired")) {
|
|
1963
|
+
dispose(row, "expired", "the writer session's delegation window closed UNPROVABLE (a timeout or an unattestable delivery — not a conviction); resolveHold(holdId, \"release\") commits the entry with a \"static\"-cause marker after host review");
|
|
1964
|
+
continue;
|
|
1965
|
+
}
|
|
1966
|
+
if (rows.some((r) => r.effective === "pending"))
|
|
1967
|
+
continue;
|
|
1968
|
+
const released = await this.releaseHold(handle, report, row, { valve: false });
|
|
1969
|
+
if (released !== undefined)
|
|
1970
|
+
out.released.push(released);
|
|
1971
|
+
}
|
|
1972
|
+
return out;
|
|
1973
|
+
}
|
|
1974
|
+
async releaseHold(handle, report, row, opts) {
|
|
1975
|
+
const disposeOut = (terminal, why) => {
|
|
1976
|
+
const d = disposeHold(this.controlDir, { holdId: row.holdId, terminal, now: this.now });
|
|
1977
|
+
report.warnings.push(`held instruction entry ${row.relPath} DISPOSED (${terminal}): ${why}${d.quarantineName !== undefined ? ` — captured bytes moved to control-plane quarantine (${d.quarantineName})` : ""}`);
|
|
1978
|
+
report.containment ??= { indexRolledBack: false, quarantinedInstruction: [], heldInstruction: [], releasedHolds: [], disposedHolds: [] };
|
|
1979
|
+
report.containment.disposedHolds.push({ relPath: row.relPath, terminal });
|
|
1980
|
+
return undefined;
|
|
1981
|
+
};
|
|
1982
|
+
const content = readHoldCustody(this.controlDir, row);
|
|
1983
|
+
if (content === undefined)
|
|
1984
|
+
return disposeOut("capture_lost", "the captured bytes are missing or fail their digest — nothing verifiable to commit");
|
|
1985
|
+
const findings = [];
|
|
1986
|
+
const nameFinding = scanMemoryFileName(`${row.slug}.md`);
|
|
1987
|
+
if (nameFinding !== undefined)
|
|
1988
|
+
findings.push(nameFinding);
|
|
1989
|
+
findings.push(...scanMemoryWrite(content));
|
|
1990
|
+
if (findings.length > 0)
|
|
1991
|
+
return disposeOut("discarded", `the release re-scan refused it (${findings[0].code}: ${findings[0].reason})`);
|
|
1992
|
+
if (Buffer.byteLength(content, "utf8") > this.perFileBytes)
|
|
1993
|
+
return disposeOut("discarded", `the captured file is over the ${this.perFileBytes}-byte cap`);
|
|
1994
|
+
if (this.sessionPollution(row.sessionId) !== undefined)
|
|
1995
|
+
return disposeOut("discarded", "the writer session's pollution marker landed before the release could commit");
|
|
1996
|
+
const parsed = parseEntryFile(content);
|
|
1997
|
+
const fm = { ...parsed.frontmatter };
|
|
1998
|
+
if (stripModelWrittenOrigin(fm)) {
|
|
1999
|
+
report.warnings.push(`${row.relPath}: origin-form frontmatter in the held bytes was not adopted — the external-origin marker is engine-authored`);
|
|
2000
|
+
}
|
|
2001
|
+
const committedFm = row.entryId !== undefined ? await this.committedFrontmatterFor(row.entryId) : undefined;
|
|
2002
|
+
const committedOrigin = committedFm !== undefined ? committedOriginOf(committedFm) : undefined;
|
|
2003
|
+
if (fm.deleted !== true) {
|
|
2004
|
+
if (committedOrigin !== undefined)
|
|
2005
|
+
fm.origin = committedOrigin;
|
|
2006
|
+
else if (opts.valve)
|
|
2007
|
+
fm.origin = { taint: "external", cause: "static", at: this.now() };
|
|
2008
|
+
}
|
|
2009
|
+
if (fm.name === undefined)
|
|
2010
|
+
fm.name = row.slug.split("/").pop();
|
|
2011
|
+
if (fm.description === undefined && fm.deleted !== true)
|
|
2012
|
+
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)}`);
|
|
2014
|
+
const entry = { id, slug: row.slug, frontmatter: fm, body: parsed.body, rev: "", scope: row.scope };
|
|
2015
|
+
entry.rev = computeEntryRev(entry);
|
|
2016
|
+
const patch = row.op === "update" && row.entryId !== undefined
|
|
2017
|
+
? { op: "update", id: row.entryId, entry, ...(row.baseRev !== undefined ? { baseRev: row.baseRev } : {}) }
|
|
2018
|
+
: { op: "add", id: entry.id, entry, guard: "absent" };
|
|
2019
|
+
const txnId = uuidv7();
|
|
2020
|
+
try {
|
|
2021
|
+
stageLineagePending(this.controlDir, txnId, row.sessionId, [{ entryId: patch.id, rev: entry.rev, ...(fm.origin !== undefined ? { marked: true } : {}) }], this.now);
|
|
2022
|
+
}
|
|
2023
|
+
catch (err) {
|
|
2024
|
+
report.warnings.push(`held instruction entry ${row.relPath} could not stage its lineage row — kept HELD for the next harvest: ${err instanceof Error ? err.message : String(err)}`);
|
|
2025
|
+
return undefined;
|
|
2026
|
+
}
|
|
2027
|
+
let patchReport;
|
|
2028
|
+
try {
|
|
2029
|
+
patchReport = await this.backend.applyPatches([patch]);
|
|
2030
|
+
}
|
|
2031
|
+
catch (err) {
|
|
2032
|
+
try {
|
|
2033
|
+
discardLineagePending(this.controlDir, txnId);
|
|
2034
|
+
}
|
|
2035
|
+
catch {
|
|
2036
|
+
}
|
|
2037
|
+
report.warnings.push(`held instruction entry ${row.relPath} could not commit (backend refused) — kept HELD for the next harvest: ${err instanceof Error ? err.message : String(err)}`);
|
|
2038
|
+
return undefined;
|
|
2039
|
+
}
|
|
2040
|
+
if (patchReport.conflicts.length > 0) {
|
|
2041
|
+
try {
|
|
2042
|
+
discardLineagePending(this.controlDir, txnId);
|
|
2043
|
+
}
|
|
2044
|
+
catch {
|
|
2045
|
+
}
|
|
2046
|
+
let alreadyApplied = false;
|
|
2047
|
+
const snapFace = this.backend.committedSnapshotOf;
|
|
2048
|
+
if (snapFace !== undefined) {
|
|
2049
|
+
try {
|
|
2050
|
+
const snap = await snapFace.call(this.backend, patch.id);
|
|
2051
|
+
alreadyApplied = snap.state === "row" && snap.rev === entry.rev && snap.binding?.scope === row.scope && (snap.binding?.slug === undefined || snap.binding.slug === row.slug);
|
|
2052
|
+
}
|
|
2053
|
+
catch {
|
|
2054
|
+
alreadyApplied = false;
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
if (alreadyApplied) {
|
|
2058
|
+
markHoldReleased(this.controlDir, { holdId: row.holdId, now: this.now });
|
|
2059
|
+
report.warnings.push(`held instruction entry ${row.relPath} was already committed by an earlier release attempt (crash-idempotent retry) — marked released`);
|
|
2060
|
+
return row.relPath;
|
|
2061
|
+
}
|
|
2062
|
+
return disposeOut("conflict", `the entry moved since capture (${patchReport.conflicts[0].reason}) — never blind-written; the captured bytes stay in quarantine for the host to reconcile`);
|
|
2063
|
+
}
|
|
2064
|
+
try {
|
|
2065
|
+
recordLineageCredential(this.controlDir, txnId, patchReport.applied.filter((a) => a.op !== "delete").map((a) => a.id), this.now);
|
|
2066
|
+
const promoted = promoteLineagePending(this.controlDir, txnId, this.now);
|
|
2067
|
+
this.settlePromotions(promoted);
|
|
2068
|
+
}
|
|
2069
|
+
catch (err) {
|
|
2070
|
+
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
|
+
}
|
|
2072
|
+
markHoldReleased(this.controlDir, { holdId: row.holdId, now: this.now });
|
|
2073
|
+
const abs = join(this.memoryDir, row.relPath);
|
|
2074
|
+
if (existsSync(abs)) {
|
|
2075
|
+
const canonical = canonicalize(abs);
|
|
2076
|
+
const text = readSafe(canonical);
|
|
2077
|
+
if (text !== undefined) {
|
|
2078
|
+
handle.baseIds.set(canonical, patch.id);
|
|
2079
|
+
handle.baseRevs.set(canonical, computeEntryRev({ id: patch.id, frontmatter: parseEntryFile(text).frontmatter, body: parseEntryFile(text).body }));
|
|
2080
|
+
handle.fileToScope.set(canonical, row.scope);
|
|
2081
|
+
if (!handle.materialized.some((m) => m.path === canonical)) {
|
|
2082
|
+
handle.materialized.push({ path: canonical, relPath: row.relPath, scope: row.scope, id: patch.id, slug: row.slug, rev: handle.baseRevs.get(canonical), stub: false, readonly: false });
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
report.warnings.push(`held instruction entry ${row.relPath} RELEASED and committed${opts.valve ? ' with a "static"-cause external-origin marker (a host-valve release is not a cleanliness proof)' : " unmarked (every delegation of its writer session settled clean)"}`);
|
|
2087
|
+
return row.relPath;
|
|
2088
|
+
}
|
|
2089
|
+
resolveSettlement(settleId, to, requestId) {
|
|
2090
|
+
resolveSettlementRecord(this.controlDir, { settleId, to, requestId, now: this.now });
|
|
2091
|
+
}
|
|
2092
|
+
resolveHold(holdId, action, requestId) {
|
|
2093
|
+
resolveHoldRecord(this.controlDir, { holdId, action, requestId, now: this.now });
|
|
2094
|
+
}
|
|
2095
|
+
resolveSessionAccount(sessionId, requestId) {
|
|
2096
|
+
resolveSessionAccountRecord(this.controlDir, { sessionId, requestId, now: this.now });
|
|
2097
|
+
}
|
|
2098
|
+
get controlPlaneDir() {
|
|
2099
|
+
return this.controlDir;
|
|
2100
|
+
}
|
|
2101
|
+
readDelegationSettlements() {
|
|
2102
|
+
return effectiveSettlements(this.controlDir);
|
|
2103
|
+
}
|
|
2104
|
+
readInstructionHolds() {
|
|
2105
|
+
return readHolds(this.controlDir);
|
|
2106
|
+
}
|
|
2107
|
+
readDanglingSessionAccounts(selfSessionId) {
|
|
2108
|
+
return foreignDanglingSessionAccounts(this.controlDir, selfSessionId);
|
|
2109
|
+
}
|
|
2110
|
+
committedAuditFace() {
|
|
2111
|
+
const b = this.backend;
|
|
2112
|
+
return b.restrictedAdoptionView?.({ audit: false }) ?? b.retrievalView?.() ?? this.backend;
|
|
2113
|
+
}
|
|
2114
|
+
static originClearRefusal(code, message) {
|
|
2115
|
+
const e = new Error(message);
|
|
2116
|
+
e.code = code;
|
|
2117
|
+
throw e;
|
|
2118
|
+
}
|
|
2119
|
+
async listExternalOriginEntries(scopes) {
|
|
2120
|
+
const face = this.committedAuditFace();
|
|
2121
|
+
const headers = await face.listHeaders(scopes);
|
|
2122
|
+
const entries = await face.getByIds(headers.filter((h) => h.exposure === "external").map((h) => h.id));
|
|
2123
|
+
const requested = new Set(scopes);
|
|
2124
|
+
const out = [];
|
|
2125
|
+
for (const e of entries) {
|
|
2126
|
+
if (!requested.has(e.scope))
|
|
2127
|
+
continue;
|
|
2128
|
+
const origin = committedOriginOf(e.frontmatter);
|
|
2129
|
+
if (origin === undefined)
|
|
2130
|
+
continue;
|
|
2131
|
+
out.push({ id: e.id, slug: e.slug, scope: e.scope, origin, provenance: await this.provenanceOf(e.id) });
|
|
2132
|
+
}
|
|
2133
|
+
return out;
|
|
2134
|
+
}
|
|
2135
|
+
listOriginClearances() {
|
|
2136
|
+
return readOriginClearances(this.controlDir);
|
|
2137
|
+
}
|
|
2138
|
+
async clearEntryOrigin(entryId, input) {
|
|
2139
|
+
if (typeof input.requestId !== "string" || input.requestId.length === 0) {
|
|
2140
|
+
MemoryEngine.originClearRefusal("memory.origin_clear_unattributed", "clearEntryOrigin requires a non-empty requestId (the audit attribution of who cleared) — refused, never defaulted.");
|
|
2141
|
+
}
|
|
2142
|
+
if (typeof input.reason !== "string" || input.reason.length === 0) {
|
|
2143
|
+
MemoryEngine.originClearRefusal("memory.origin_clear_invalid", "clearEntryOrigin requires a non-empty reason (the host's stated ground rides the audit row) — refused, never defaulted.");
|
|
2144
|
+
}
|
|
2145
|
+
const pending = readOriginClearances(this.controlDir).find((r) => r.entryId === entryId && r.status === "pending");
|
|
2146
|
+
if (pending !== undefined)
|
|
2147
|
+
return await this.completeOriginClearance(pending, input.requestId);
|
|
2148
|
+
const face = this.committedAuditFace();
|
|
2149
|
+
const [committed] = await face.getByIds([entryId]);
|
|
2150
|
+
if (committed === undefined || committed.frontmatter.deleted === true) {
|
|
2151
|
+
MemoryEngine.originClearRefusal("memory.origin_clear_unknown", `clearEntryOrigin: no committed memory entry ${JSON.stringify(entryId.slice(0, 80))}.`);
|
|
2152
|
+
}
|
|
2153
|
+
const origin = committedOriginOf(committed.frontmatter);
|
|
2154
|
+
if (origin === undefined) {
|
|
2155
|
+
MemoryEngine.originClearRefusal("memory.origin_clear_not_marked", `clearEntryOrigin: entry ${JSON.stringify(entryId.slice(0, 80))} carries no external-origin marker — nothing to clear.`);
|
|
2156
|
+
}
|
|
2157
|
+
if (this.readChallengeExclusions().has(entryId)) {
|
|
2158
|
+
MemoryEngine.originClearRefusal("memory.origin_clear_challenged", `clearEntryOrigin: entry ${JSON.stringify(entryId.slice(0, 80))} is challenged/latched — adjudicate the challenge first (the clear valve is not a challenge exit).`);
|
|
2159
|
+
}
|
|
2160
|
+
const fm = { ...committed.frontmatter, ...(committed.frontmatter.extra !== undefined ? { extra: [...committed.frontmatter.extra] } : {}) };
|
|
2161
|
+
stripModelWrittenOrigin(fm);
|
|
2162
|
+
const cleared = { id: committed.id, slug: committed.slug, scope: committed.scope, frontmatter: fm, body: committed.body, rev: "" };
|
|
2163
|
+
cleared.rev = computeEntryRev(cleared);
|
|
2164
|
+
const row = {
|
|
2165
|
+
clearanceId: uuidv7(),
|
|
2166
|
+
entryId,
|
|
2167
|
+
scope: committed.scope,
|
|
2168
|
+
slug: committed.slug,
|
|
2169
|
+
baseRev: committed.rev,
|
|
2170
|
+
origin,
|
|
2171
|
+
requestId: input.requestId,
|
|
2172
|
+
reason: input.reason,
|
|
2173
|
+
at: this.now(),
|
|
2174
|
+
entryText: serializeEntryFile(cleared),
|
|
2175
|
+
};
|
|
2176
|
+
openOriginClearance(this.controlDir, row);
|
|
2177
|
+
return await this.completeOriginClearance({ ...row, status: "pending", events: [] }, input.requestId);
|
|
2178
|
+
}
|
|
2179
|
+
async completeOriginClearance(row, requestId) {
|
|
2180
|
+
const fail = (code, detail, keepPending) => {
|
|
2181
|
+
try {
|
|
2182
|
+
settleOriginClearance(this.controlDir, { clearanceId: row.clearanceId, to: "failed", requestId, eventId: uuidv7(), now: this.now, detail, ...(keepPending ? { keepPending: true } : {}) });
|
|
2183
|
+
}
|
|
2184
|
+
catch {
|
|
2185
|
+
}
|
|
2186
|
+
MemoryEngine.originClearRefusal(code, `clearEntryOrigin ${JSON.stringify(row.entryId.slice(0, 80))}: ${detail}`);
|
|
2187
|
+
};
|
|
2188
|
+
const parsed = parseEntryFile(row.entryText);
|
|
2189
|
+
const cleared = { id: row.entryId, slug: row.slug, scope: row.scope, frontmatter: parsed.frontmatter, body: parsed.body, rev: "" };
|
|
2190
|
+
cleared.rev = computeEntryRev(cleared);
|
|
2191
|
+
const [current] = await this.committedAuditFace().getByIds([row.entryId]);
|
|
2192
|
+
if (current === undefined || committedOriginOf(current.frontmatter) !== undefined) {
|
|
2193
|
+
if (parsed.id !== row.entryId || committedOriginOf(parsed.frontmatter) !== undefined) {
|
|
2194
|
+
const what = parsed.id !== row.entryId ? "the custody bytes carry a different entry id" : "an origin form survived in the custody bytes";
|
|
2195
|
+
fail("memory.origin_clear_failed", `the clearance row's custody text failed its identity/cleared-state validation (${what}) — refusing to act on it. ` +
|
|
2196
|
+
(current === undefined
|
|
2197
|
+
? "The row stays PENDING (the tombstone has committed and the row's entryText is the recovery seat — repair it, or recover the bytes by hand)."
|
|
2198
|
+
: "Re-judge with a fresh clearEntryOrigin call (this row is terminal; the marked entry stands untouched)."), current === undefined);
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
let landedSlug = row.slug;
|
|
2202
|
+
let detail;
|
|
2203
|
+
if (current !== undefined && committedOriginOf(current.frontmatter) !== undefined) {
|
|
2204
|
+
if (current.rev !== row.baseRev) {
|
|
2205
|
+
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
|
+
}
|
|
2207
|
+
if (current.scope !== row.scope || current.slug !== row.slug) {
|
|
2208
|
+
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
|
+
}
|
|
2210
|
+
const del = await this.backend.applyPatches([{ op: "delete", id: row.entryId, baseRev: row.baseRev }]);
|
|
2211
|
+
if (del.conflicts.length > 0) {
|
|
2212
|
+
const [reprobe] = await this.committedAuditFace().getByIds([row.entryId]);
|
|
2213
|
+
if (reprobe !== undefined && committedOriginOf(reprobe.frontmatter) !== undefined) {
|
|
2214
|
+
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
|
+
}
|
|
2216
|
+
if (reprobe === undefined) {
|
|
2217
|
+
const add = await this.backend.applyPatches([{ op: "add", id: row.entryId, entry: cleared, guard: "absent" }]);
|
|
2218
|
+
if (add.conflicts.length > 0) {
|
|
2219
|
+
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);
|
|
2220
|
+
}
|
|
2221
|
+
landedSlug = add.applied.find((a) => a.id === row.entryId)?.slug ?? row.slug;
|
|
2222
|
+
detail = `cleared and re-recorded at slug ${JSON.stringify(landedSlug)} (tombstone raced by a concurrent resumer)`;
|
|
2223
|
+
}
|
|
2224
|
+
else {
|
|
2225
|
+
landedSlug = reprobe.slug;
|
|
2226
|
+
detail = reprobe.rev === cleared.rev ? "already complete (raced by a concurrent resumer)" : "marker already absent (a later committed generation stands; custody retained on this row)";
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
else {
|
|
2230
|
+
const add = await this.backend.applyPatches([{ op: "add", id: row.entryId, entry: cleared, guard: "absent" }]);
|
|
2231
|
+
if (add.conflicts.length > 0) {
|
|
2232
|
+
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);
|
|
2233
|
+
}
|
|
2234
|
+
landedSlug = add.applied.find((a) => a.id === row.entryId)?.slug ?? row.slug;
|
|
2235
|
+
detail = `cleared and re-recorded at slug ${JSON.stringify(landedSlug)}`;
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
else if (current === undefined) {
|
|
2239
|
+
const add = await this.backend.applyPatches([{ op: "add", id: row.entryId, entry: cleared, guard: "absent" }]);
|
|
2240
|
+
if (add.conflicts.length > 0) {
|
|
2241
|
+
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);
|
|
2242
|
+
}
|
|
2243
|
+
landedSlug = add.applied.find((a) => a.id === row.entryId)?.slug ?? row.slug;
|
|
2244
|
+
detail = `resumed after interruption: re-recorded at slug ${JSON.stringify(landedSlug)}`;
|
|
2245
|
+
}
|
|
2246
|
+
else {
|
|
2247
|
+
landedSlug = current.slug;
|
|
2248
|
+
detail = current.rev === cleared.rev ? "already complete (terminal event replay)" : "marker already absent (a later committed generation stands; custody retained on this row)";
|
|
2249
|
+
}
|
|
2250
|
+
settleOriginClearance(this.controlDir, { clearanceId: row.clearanceId, to: "done", requestId, eventId: uuidv7(), now: this.now, detail });
|
|
2251
|
+
try {
|
|
2252
|
+
enqueueMemoryAnnouncement(this.controlDir, {
|
|
2253
|
+
kind: "gate",
|
|
2254
|
+
at: this.now(),
|
|
2255
|
+
items: [`memory entry ${inlineUntrusted(row.entryId, 80)} external-origin marker CLEARED by host request ${JSON.stringify(inlineUntrusted(requestId, 80))} — re-recorded unmarked (${inlineUntrusted(detail, 160)})`],
|
|
2256
|
+
});
|
|
2257
|
+
}
|
|
2258
|
+
catch (err) {
|
|
2259
|
+
this.discloseAnnounceFailure("origin-clearance enqueue", err);
|
|
2260
|
+
}
|
|
2261
|
+
return { entryId: row.entryId, clearanceId: row.clearanceId, origin: row.origin, landedSlug };
|
|
2262
|
+
}
|
|
1620
2263
|
async rebaseline(handle, keepBaseline = new Set()) {
|
|
1621
2264
|
const writeScope = handle.writeScope;
|
|
1622
2265
|
if (writeScope === null)
|
|
@@ -1697,6 +2340,9 @@ export class MemoryEngine {
|
|
|
1697
2340
|
const nextIndexRevs = {};
|
|
1698
2341
|
const expected = new Map();
|
|
1699
2342
|
const excludedTargets = new Set();
|
|
2343
|
+
const carryIndex = this.provenance === "carry";
|
|
2344
|
+
const exposedById = new Map();
|
|
2345
|
+
const exposedProjectionTargets = new Set();
|
|
1700
2346
|
for (const h of headers) {
|
|
1701
2347
|
const repoTarget = handle.repoIndexTargets?.get(h.id);
|
|
1702
2348
|
const target = repoTarget ?? relative(handle.writableRoot, canonicalize(join(scopeDirFor(this.memoryDir, this.controlDir, h.scope), `${h.slug}.md`)));
|
|
@@ -1704,12 +2350,46 @@ export class MemoryEngine {
|
|
|
1704
2350
|
excludedTargets.add(target);
|
|
1705
2351
|
continue;
|
|
1706
2352
|
}
|
|
2353
|
+
if (carryIndex && h.exposure === "external") {
|
|
2354
|
+
exposedById.set(h.id, h);
|
|
2355
|
+
exposedProjectionTargets.add(target);
|
|
2356
|
+
continue;
|
|
2357
|
+
}
|
|
1707
2358
|
expected.set(target, h);
|
|
1708
2359
|
}
|
|
1709
2360
|
const covered = new Set();
|
|
2361
|
+
const handleCovered = new Set();
|
|
1710
2362
|
const keptByTarget = new Map();
|
|
1711
2363
|
const keptLines = [];
|
|
1712
2364
|
for (const line of existing.split("\n")) {
|
|
2365
|
+
if (carryIndex) {
|
|
2366
|
+
const handleId = parseMemoryExposureIndexRow(line);
|
|
2367
|
+
if (handleId !== undefined) {
|
|
2368
|
+
if (exposedById.has(handleId) && !handleCovered.has(handleId) && line === memoryExposureIndexRow(handleId)) {
|
|
2369
|
+
handleCovered.add(handleId);
|
|
2370
|
+
keptLines.push(line);
|
|
2371
|
+
}
|
|
2372
|
+
continue;
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
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
|
+
const folded = foldDots(line.toLowerCase());
|
|
2387
|
+
const hit = [...exposedProjectionTargets].find((t) => folded.includes(t.toLowerCase()));
|
|
2388
|
+
if (hit !== undefined) {
|
|
2389
|
+
warnings?.push(`MEMORY.md index line naming ${hit} replaced by its opaque external-origin handle (the entry stays readable via memory_get)`);
|
|
2390
|
+
continue;
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
1713
2393
|
const target = indexLineTarget(line);
|
|
1714
2394
|
if (target === undefined) {
|
|
1715
2395
|
if (line.trim() !== "" || keptLines[keptLines.length - 1]?.trim() !== "")
|
|
@@ -1758,6 +2438,10 @@ export class MemoryEngine {
|
|
|
1758
2438
|
keptLines.push(`- [${title}](${target})${hook} (${age})`);
|
|
1759
2439
|
nextIndexRevs[target] = h.rev;
|
|
1760
2440
|
}
|
|
2441
|
+
for (const id of exposedById.keys()) {
|
|
2442
|
+
if (!handleCovered.has(id))
|
|
2443
|
+
keptLines.push(memoryExposureIndexRow(id));
|
|
2444
|
+
}
|
|
1761
2445
|
while (keptLines.length > 0 && keptLines[keptLines.length - 1].trim() === "")
|
|
1762
2446
|
keptLines.pop();
|
|
1763
2447
|
const text = keptLines.length > 0 ? `${keptLines.join("\n")}\n` : "";
|
|
@@ -2029,7 +2713,7 @@ function indexLineTarget(line) {
|
|
|
2029
2713
|
return m?.[1];
|
|
2030
2714
|
}
|
|
2031
2715
|
function countIndexLines(text) {
|
|
2032
|
-
return text.split("\n").filter((l) => indexLineTarget(l) !== undefined).length;
|
|
2716
|
+
return text.split("\n").filter((l) => indexLineTarget(l) !== undefined || parseMemoryExposureIndexRow(l) !== undefined).length;
|
|
2033
2717
|
}
|
|
2034
2718
|
function revOfText(text, id) {
|
|
2035
2719
|
const parsed = parseEntryFile(text);
|