@sema-agent/core 5.45.0 → 5.47.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 +97 -0
- package/dist/agents/subagent.js +130 -3
- package/dist/core/checkpoint-store.d.ts +12 -0
- package/dist/core/governance-codes.d.ts +13 -0
- package/dist/core/governance-codes.js +33 -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 +3 -1
- 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 +209 -4
- package/dist/core/memory-engine/engine.js +885 -39
- 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 +34 -4
- package/dist/core/memory-engine/file-backend.js +168 -40
- package/dist/core/memory-engine/frontmatter.d.ts +69 -1
- package/dist/core/memory-engine/frontmatter.js +156 -2
- 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 +7 -5
- package/dist/core/memory-engine/index.js +5 -3
- package/dist/core/memory-engine/layout.d.ts +39 -2
- package/dist/core/memory-engine/layout.js +27 -14
- package/dist/core/memory-engine/memory-backend-contract.js +108 -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/sync-client.d.ts +1 -1
- package/dist/core/memory-engine/sync-client.js +33 -1
- package/dist/core/memory-engine/tools.d.ts +64 -3
- package/dist/core/memory-engine/tools.js +37 -9
- package/dist/core/memory-engine/types.d.ts +145 -3
- 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/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 +4 -0
- package/dist/core/runner/prepare-config-doors.js +15 -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 +61 -24
- package/dist/core/runner/prepare-task.d.ts +46 -1
- package/dist/core/runner/prepare-task.js +128 -23
- package/dist/core/runner/runtask.js +62 -55
- package/dist/core/session-reconcile.js +3 -2
- package/dist/core/side-query.d.ts +11 -1
- package/dist/core/side-query.js +3 -0
- package/dist/core/types.d.ts +85 -10
- package/dist/core/types.js +3 -0
- 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 +3 -1
- package/dist/index.js +3 -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/dist/tools/task-list.d.ts +5 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +21 -2
|
@@ -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
|
|
@@ -871,6 +900,7 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
|
|
|
871
900
|
/** Lexical floor (design/138 §2.1): `1 - Jaccard` distance over name+description+body, ascending. */
|
|
872
901
|
search(query: string, scopes: readonly string[], opts?: {
|
|
873
902
|
limit?: number;
|
|
903
|
+
exposureBands?: boolean;
|
|
874
904
|
}): Promise<ScoredMemoryEntry[]>;
|
|
875
905
|
private searchFrom;
|
|
876
906
|
/**
|
|
@@ -4,7 +4,7 @@ import { dirname, join, relative } from "node:path";
|
|
|
4
4
|
import { jaccardDistance, termSet } from "../memory-vector.js";
|
|
5
5
|
import { MAX_MEMORY_BYTES } from "../memory.js";
|
|
6
6
|
import { inlineUntrusted } from "../untrusted-text.js";
|
|
7
|
-
import { computeEntryRev, entryFromFile, isValidEntryId, parseEntryFile, serializeEntryFile } from "./frontmatter.js";
|
|
7
|
+
import { ambiguousOriginRepresentation, committedOriginOf, computeEntryRev, entryFromFile, isValidEntryId, originEquals, parseEntryFile, serializeEntryFile } from "./frontmatter.js";
|
|
8
8
|
import { ControlPlaneCorruptError, CURSORS_FILE, QUARANTINE_DIR, CHALLENGES_FILE, IMPORT_RECEIPTS_DIR, LINEAGE_FILE, SESSION_POLLUTION_DIR, appendChallengeEvents, atomicWriteFileSync, quarantineAndTombstone, captureAndClearLineageForEntries, claimRootScope, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, ensureDirExists, enqueueMemoryAnnouncement, importLatchTxnId, importLineageCommitted, importSessionPollution, isContainedIn, lineageContributionsOfSession, listSessionPollution, readChallengeEvents, readLineageRecord, registerScope, registeredScopes, releaseImportLatch, resolveMemoryEngineRoot, scopeDirFor, scopeDirName, stageImportLatch, writeAllSync, } from "./layout.js";
|
|
9
9
|
import { screenInboundEntries } from "./data-plane.js";
|
|
10
10
|
import { scanMemoryFileName, scanMemoryWrite } from "./scan.js";
|
|
@@ -540,6 +540,8 @@ export class FileMemoryEngineBackend {
|
|
|
540
540
|
unboundRowsKnown = false;
|
|
541
541
|
adoptionNoticeKeys = new Set();
|
|
542
542
|
transfersAppendFault;
|
|
543
|
+
recoveryCommitProbe;
|
|
544
|
+
chainHealProbe;
|
|
543
545
|
exportFaceProbe;
|
|
544
546
|
deletedIdsDigest;
|
|
545
547
|
inboundFindings = [];
|
|
@@ -735,11 +737,18 @@ export class FileMemoryEngineBackend {
|
|
|
735
737
|
const tryMkdir = () => {
|
|
736
738
|
try {
|
|
737
739
|
mkdirSync(this.controlPlaneRoot, { recursive: true });
|
|
740
|
+
}
|
|
741
|
+
catch (err) {
|
|
742
|
+
throw new ControlPlaneCorruptError(`memory control-plane root could not be created (${err.code ?? "io error"}) at ${this.controlPlaneRoot} — a lock-acquisition fault is not contention; deferring recovery/migration on it would be silent (fail-closed)`, { cause: err });
|
|
743
|
+
}
|
|
744
|
+
try {
|
|
738
745
|
mkdirSync(lockDir);
|
|
739
746
|
return true;
|
|
740
747
|
}
|
|
741
|
-
catch {
|
|
742
|
-
|
|
748
|
+
catch (err) {
|
|
749
|
+
if (err.code === "EEXIST")
|
|
750
|
+
return false;
|
|
751
|
+
throw new ControlPlaneCorruptError(`memory txn lock could not be created (${err.code ?? "io error"}) at ${lockDir} — a lock-acquisition fault is not contention; deferring recovery/migration on it would be silent (fail-closed)`, { cause: err });
|
|
743
752
|
}
|
|
744
753
|
};
|
|
745
754
|
let acquired = tryMkdir();
|
|
@@ -748,7 +757,22 @@ export class FileMemoryEngineBackend {
|
|
|
748
757
|
try {
|
|
749
758
|
mtimeMs = statSync(lockDir).mtimeMs;
|
|
750
759
|
}
|
|
751
|
-
catch {
|
|
760
|
+
catch (err) {
|
|
761
|
+
if (err.code !== "ENOENT") {
|
|
762
|
+
throw new ControlPlaneCorruptError(`memory txn lock state could not be probed (${err.code ?? "io error"}) at ${lockDir} — a probe failure is not contention (fail-closed)`, { cause: err });
|
|
763
|
+
}
|
|
764
|
+
let occupant;
|
|
765
|
+
try {
|
|
766
|
+
occupant = lstatSync(lockDir);
|
|
767
|
+
}
|
|
768
|
+
catch (lerr) {
|
|
769
|
+
if (lerr.code !== "ENOENT") {
|
|
770
|
+
throw new ControlPlaneCorruptError(`memory txn lock re-probe failed (${lerr.code ?? "io error"}) at ${lockDir} — a probe failure is not contention (fail-closed)`, { cause: lerr });
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
if (occupant !== undefined && !occupant.isDirectory()) {
|
|
774
|
+
throw new ControlPlaneCorruptError(`memory txn lock path is occupied by an invalid node (a dangling symlink?) at ${lockDir} — not contention and not a release race; remove the occupant (fail-closed)`, { cause: err });
|
|
775
|
+
}
|
|
752
776
|
mtimeMs = undefined;
|
|
753
777
|
}
|
|
754
778
|
if (mtimeMs !== undefined && this.now() - mtimeMs > TXN_LOCK_STALE_MS) {
|
|
@@ -757,7 +781,10 @@ export class FileMemoryEngineBackend {
|
|
|
757
781
|
statSync(ownerPath);
|
|
758
782
|
tokenPresent = true;
|
|
759
783
|
}
|
|
760
|
-
catch {
|
|
784
|
+
catch (err) {
|
|
785
|
+
if (err.code !== "ENOENT") {
|
|
786
|
+
throw new ControlPlaneCorruptError(`memory txn lock owner token could not be probed (${err.code ?? "io error"}) at ${ownerPath} — a probe failure is not a mid-acquisition (fail-closed)`, { cause: err });
|
|
787
|
+
}
|
|
761
788
|
}
|
|
762
789
|
if (tokenPresent) {
|
|
763
790
|
const tomb = join(this.controlPlaneRoot, `txn.lock.stale-${randomUUID()}`);
|
|
@@ -766,7 +793,10 @@ export class FileMemoryEngineBackend {
|
|
|
766
793
|
renameSync(lockDir, tomb);
|
|
767
794
|
stolen = true;
|
|
768
795
|
}
|
|
769
|
-
catch {
|
|
796
|
+
catch (err) {
|
|
797
|
+
if (err.code !== "ENOENT") {
|
|
798
|
+
throw new ControlPlaneCorruptError(`memory txn lock stale-steal rename failed (${err.code ?? "io error"}) at ${lockDir} — a rename fault is not a lost race (fail-closed)`, { cause: err });
|
|
799
|
+
}
|
|
770
800
|
}
|
|
771
801
|
if (stolen) {
|
|
772
802
|
rmSync(tomb, { recursive: true, force: true });
|
|
@@ -819,7 +849,7 @@ export class FileMemoryEngineBackend {
|
|
|
819
849
|
if (lock === undefined)
|
|
820
850
|
return;
|
|
821
851
|
try {
|
|
822
|
-
this.recoverJournal();
|
|
852
|
+
this.recoverJournal({ token: lock.token });
|
|
823
853
|
this.migrateLockedIfNeeded(lock.token);
|
|
824
854
|
}
|
|
825
855
|
finally {
|
|
@@ -892,8 +922,10 @@ export class FileMemoryEngineBackend {
|
|
|
892
922
|
fd = openSync(path, "wx", 0o600);
|
|
893
923
|
}
|
|
894
924
|
catch (err) {
|
|
895
|
-
if (err.code === "EEXIST")
|
|
925
|
+
if (err.code === "EEXIST") {
|
|
926
|
+
this.fsyncControlPlaneDir();
|
|
896
927
|
return;
|
|
928
|
+
}
|
|
897
929
|
throw new ControlPlaneCorruptError(`evidence-chain degradation marker could not be created at ${path} (${err.code ?? "io error"}) — heal aborted, the chain is unchanged (fail-closed)`);
|
|
898
930
|
}
|
|
899
931
|
try {
|
|
@@ -903,11 +935,32 @@ export class FileMemoryEngineBackend {
|
|
|
903
935
|
finally {
|
|
904
936
|
closeSync(fd);
|
|
905
937
|
}
|
|
938
|
+
this.fsyncControlPlaneDir();
|
|
906
939
|
if (readControlFileOrAbsent(path, "evidence-chain degradation marker") === undefined) {
|
|
907
940
|
throw new ControlPlaneCorruptError(`evidence-chain degradation marker vanished after its write at ${path} — heal aborted, the chain is unchanged (fail-closed)`);
|
|
908
941
|
}
|
|
909
942
|
}
|
|
910
|
-
|
|
943
|
+
fsyncControlPlaneDir() {
|
|
944
|
+
if (process.platform === "win32")
|
|
945
|
+
return;
|
|
946
|
+
let dfd;
|
|
947
|
+
try {
|
|
948
|
+
dfd = openSync(this.controlPlaneRoot, "r");
|
|
949
|
+
}
|
|
950
|
+
catch (err) {
|
|
951
|
+
throw new ControlPlaneCorruptError(`evidence-chain degradation marker's directory could not be opened for fsync (${err.code ?? "io error"}) at ${this.controlPlaneRoot} — heal aborted, the chain is unchanged (fail-closed: an unproven marker must not authorize a truncation)`, { cause: err });
|
|
952
|
+
}
|
|
953
|
+
try {
|
|
954
|
+
fsyncSync(dfd);
|
|
955
|
+
}
|
|
956
|
+
catch (err) {
|
|
957
|
+
throw new ControlPlaneCorruptError(`evidence-chain degradation marker's directory entry could not be fsynced (${err.code ?? "io error"}) at ${this.controlPlaneRoot} — heal aborted, the chain is unchanged (fail-closed: an unproven marker must not authorize a truncation)`, { cause: err });
|
|
958
|
+
}
|
|
959
|
+
finally {
|
|
960
|
+
closeSync(dfd);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
readTransferChain(fenceToken) {
|
|
911
964
|
const path = join(this.controlPlaneRoot, TRANSFERS_FILE);
|
|
912
965
|
const raw = readControlFileOrAbsent(path, "transfer evidence log");
|
|
913
966
|
const out = [];
|
|
@@ -940,6 +993,8 @@ export class FileMemoryEngineBackend {
|
|
|
940
993
|
}
|
|
941
994
|
}
|
|
942
995
|
const outOfBand = !journalHasPendingEvidence;
|
|
996
|
+
if (fenceToken !== undefined)
|
|
997
|
+
this.assertTxnLockOwnership(fenceToken);
|
|
943
998
|
let fragmentPath;
|
|
944
999
|
try {
|
|
945
1000
|
ensureDirExists(join(this.controlPlaneRoot, QUARANTINE_DIR));
|
|
@@ -951,6 +1006,9 @@ export class FileMemoryEngineBackend {
|
|
|
951
1006
|
}
|
|
952
1007
|
if (outOfBand)
|
|
953
1008
|
this.mintChainDegradedMarker(fragmentPath);
|
|
1009
|
+
this.chainHealProbe?.();
|
|
1010
|
+
if (fenceToken !== undefined)
|
|
1011
|
+
this.assertTxnLockOwnership(fenceToken);
|
|
954
1012
|
try {
|
|
955
1013
|
atomicWriteFileSync(path, lines.slice(0, i).join("\n") + (i > 0 ? "\n" : ""));
|
|
956
1014
|
}
|
|
@@ -1110,21 +1168,22 @@ export class FileMemoryEngineBackend {
|
|
|
1110
1168
|
closeSync(fd);
|
|
1111
1169
|
}
|
|
1112
1170
|
}
|
|
1113
|
-
recoverJournal(
|
|
1114
|
-
if (
|
|
1171
|
+
recoverJournal(fence) {
|
|
1172
|
+
if (fence.token === undefined) {
|
|
1115
1173
|
if (this.txnInFlight())
|
|
1116
1174
|
return;
|
|
1117
1175
|
const lock = this.tryAcquireTxnLockSync();
|
|
1118
1176
|
if (lock === undefined)
|
|
1119
1177
|
return;
|
|
1120
1178
|
try {
|
|
1121
|
-
this.recoverJournal();
|
|
1179
|
+
this.recoverJournal({ token: lock.token });
|
|
1122
1180
|
}
|
|
1123
1181
|
finally {
|
|
1124
1182
|
lock.release();
|
|
1125
1183
|
}
|
|
1126
1184
|
return;
|
|
1127
1185
|
}
|
|
1186
|
+
const fenceToken = fence.token;
|
|
1128
1187
|
const jp = join(this.controlPlaneRoot, JOURNAL_FILE);
|
|
1129
1188
|
const raw = readControlFileOrAbsent(jp, "transaction journal");
|
|
1130
1189
|
if (raw !== undefined) {
|
|
@@ -1205,7 +1264,7 @@ export class FileMemoryEngineBackend {
|
|
|
1205
1264
|
if (hasV1)
|
|
1206
1265
|
throw new ControlPlaneCorruptError(`transaction journal carries transfers with a v1 snapshot (impossible form): ${jp}`);
|
|
1207
1266
|
if (journalEvs.size > 0) {
|
|
1208
|
-
for (const row of this.readTransferChain()) {
|
|
1267
|
+
for (const row of this.readTransferChain(fenceToken)) {
|
|
1209
1268
|
const j = journalEvs.get(row.ev);
|
|
1210
1269
|
if (j !== undefined && j !== row.canonical) {
|
|
1211
1270
|
throw new ControlPlaneCorruptError(`transaction journal event id ${JSON.stringify(row.ev)} collides with a DIFFERENT payload already on the evidence chain (fail-closed before redo): ${jp}`);
|
|
@@ -1242,7 +1301,7 @@ export class FileMemoryEngineBackend {
|
|
|
1242
1301
|
}
|
|
1243
1302
|
}
|
|
1244
1303
|
if (e.req !== undefined && !anchorRows.some((a) => a.req === e.req)) {
|
|
1245
|
-
chainAnchors ??= this.readTransferChain()
|
|
1304
|
+
chainAnchors ??= this.readTransferChain(fenceToken)
|
|
1246
1305
|
.map((r) => r.parsed)
|
|
1247
1306
|
.filter((p) => p.channel === "erasure-anchor" && p.origin === undefined);
|
|
1248
1307
|
const matching = chainAnchors.filter((p) => p.req === e.req);
|
|
@@ -1254,7 +1313,7 @@ export class FileMemoryEngineBackend {
|
|
|
1254
1313
|
}
|
|
1255
1314
|
const chainAnchorEvsByReq = new Map();
|
|
1256
1315
|
if (anchorRows.length > 0) {
|
|
1257
|
-
for (const p of (chainAnchors ??= this.readTransferChain()
|
|
1316
|
+
for (const p of (chainAnchors ??= this.readTransferChain(fenceToken)
|
|
1258
1317
|
.map((r) => r.parsed)
|
|
1259
1318
|
.filter((p) => p.channel === "erasure-anchor" && p.origin === undefined))) {
|
|
1260
1319
|
const set = chainAnchorEvsByReq.get(p.req) ?? new Set();
|
|
@@ -1332,6 +1391,7 @@ export class FileMemoryEngineBackend {
|
|
|
1332
1391
|
}
|
|
1333
1392
|
}
|
|
1334
1393
|
}
|
|
1394
|
+
this.assertTxnLockOwnership(fenceToken);
|
|
1335
1395
|
for (const op of journal.ops) {
|
|
1336
1396
|
try {
|
|
1337
1397
|
if (op.kind === "write" && op.content !== undefined) {
|
|
@@ -1354,6 +1414,8 @@ export class FileMemoryEngineBackend {
|
|
|
1354
1414
|
throw new ControlPlaneCorruptError(`transaction journal replay failed on op kind=${op.kind} target=${JSON.stringify(op.target)} (${code ?? (err instanceof Error ? err.message : String(err))}) — the journal is kept at ${jp}; an I/O failure retries at the next construction, a persistent fault needs the explicit control-plane rebuild path`);
|
|
1355
1415
|
}
|
|
1356
1416
|
}
|
|
1417
|
+
this.recoveryCommitProbe?.();
|
|
1418
|
+
this.assertTxnLockOwnership(fenceToken);
|
|
1357
1419
|
if (hasV2) {
|
|
1358
1420
|
this.ledger = snapshotRows;
|
|
1359
1421
|
this.envelopeExtras = snapshotExtras;
|
|
@@ -1366,8 +1428,15 @@ export class FileMemoryEngineBackend {
|
|
|
1366
1428
|
this.persistedSchemaVersion = "v1";
|
|
1367
1429
|
this.persistLedgerV1(snapshotRows);
|
|
1368
1430
|
}
|
|
1369
|
-
|
|
1431
|
+
if (typeof journal.txn === "string") {
|
|
1432
|
+
this.closeJournalIfOurs(journal.txn);
|
|
1433
|
+
}
|
|
1434
|
+
else {
|
|
1435
|
+
this.assertTxnLockOwnership(fenceToken);
|
|
1436
|
+
rmSync(jp, { force: true });
|
|
1437
|
+
}
|
|
1370
1438
|
}
|
|
1439
|
+
this.assertTxnLockOwnership(fenceToken);
|
|
1371
1440
|
this.sweepStagedFiles();
|
|
1372
1441
|
}
|
|
1373
1442
|
txnInFlight() {
|
|
@@ -1452,6 +1521,9 @@ export class FileMemoryEngineBackend {
|
|
|
1452
1521
|
if (first !== undefined) {
|
|
1453
1522
|
return { path: relPath, code: first.code, reason: `inbound memory change blocked: ${first.reason} (quarantined)` };
|
|
1454
1523
|
}
|
|
1524
|
+
if (ambiguousOriginRepresentation(parseEntryFile(text).frontmatter)) {
|
|
1525
|
+
return { path: relPath, code: "invalid", reason: `inbound memory change blocked: conflicting or duplicated origin representations (ambiguous external-origin marker; quarantined)` };
|
|
1526
|
+
}
|
|
1455
1527
|
return undefined;
|
|
1456
1528
|
}
|
|
1457
1529
|
readScope(scope, sync, probeOnly, lockToken) {
|
|
@@ -1628,17 +1700,28 @@ export class FileMemoryEngineBackend {
|
|
|
1628
1700
|
whitewashInboundFinding(relPath, entry, containment) {
|
|
1629
1701
|
const committedShadow = this.readCommittedShadow(entry.id);
|
|
1630
1702
|
const committedFm = committedShadow !== undefined ? parseEntryFile(committedShadow).frontmatter : undefined;
|
|
1631
|
-
if (committedFm
|
|
1703
|
+
if (committedFm === undefined)
|
|
1632
1704
|
return undefined;
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1705
|
+
if (committedFm.provenance?.kind === "repo_file") {
|
|
1706
|
+
const stripsProvenance = entry.frontmatter.provenance === undefined;
|
|
1707
|
+
const stripsTrust = committedFm.trust !== undefined && entry.frontmatter.trust === undefined;
|
|
1708
|
+
if (stripsProvenance || stripsTrust) {
|
|
1709
|
+
return {
|
|
1710
|
+
path: relPath,
|
|
1711
|
+
code: "invalid",
|
|
1712
|
+
reason: `inbound memory change blocked: repo_file provenance whitewash refused — the on-disk change drops ${stripsProvenance ? "provenance" : "trust"} from a repo-ingested entry ${containment === "restored" ? "(quarantined; committed content restored)" : "(quarantined)"}`,
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
const committedOrigin = committedOriginOf(committedFm);
|
|
1717
|
+
if (committedOrigin !== undefined && !originEquals(committedOrigin, committedOriginOf(entry.frontmatter))) {
|
|
1718
|
+
return {
|
|
1719
|
+
path: relPath,
|
|
1720
|
+
code: "invalid",
|
|
1721
|
+
reason: `inbound memory change blocked: external-origin marker whitewash refused — the on-disk change strips or rewrites the origin marker of a marked entry ${containment === "restored" ? "(quarantined; committed content restored)" : "(quarantined)"}`,
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
return undefined;
|
|
1642
1725
|
}
|
|
1643
1726
|
adoptMoveCandidateLocked(rows, entry, absPath, slug, scope, relPath, text, committedRev, from, moveEvents, moveAnnouncements, adoptedExternal) {
|
|
1644
1727
|
const census = this.censusProjectionsLocked();
|
|
@@ -1685,7 +1768,7 @@ export class FileMemoryEngineBackend {
|
|
|
1685
1768
|
try {
|
|
1686
1769
|
this.assertTxnLockOwnership(lock.token);
|
|
1687
1770
|
this.ledger = undefined;
|
|
1688
|
-
this.recoverJournal();
|
|
1771
|
+
this.recoverJournal({ token: lock.token });
|
|
1689
1772
|
this.migrateLockedIfNeeded(lock.token);
|
|
1690
1773
|
return this.readScope(scope, true, false, lock.token);
|
|
1691
1774
|
}
|
|
@@ -2052,7 +2135,8 @@ export class FileMemoryEngineBackend {
|
|
|
2052
2135
|
scored.push({ ...headerOf(e, join(dir, `${e.slug}.md`)), score: d });
|
|
2053
2136
|
}
|
|
2054
2137
|
}
|
|
2055
|
-
|
|
2138
|
+
const band = opts?.exposureBands === true ? (h) => (h.exposure === "external" ? 1 : 0) : () => 0;
|
|
2139
|
+
scored.sort((a, b) => band(a) - band(b) || a.score - b.score || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
2056
2140
|
return scored.slice(0, limit);
|
|
2057
2141
|
}
|
|
2058
2142
|
async applyPatches(patches) {
|
|
@@ -2177,7 +2261,7 @@ export class FileMemoryEngineBackend {
|
|
|
2177
2261
|
async applyPatchesLocked(patches, lockToken) {
|
|
2178
2262
|
this.assertTxnLockOwnership(lockToken);
|
|
2179
2263
|
this.ledger = undefined;
|
|
2180
|
-
this.recoverJournal();
|
|
2264
|
+
this.recoverJournal({ token: lockToken });
|
|
2181
2265
|
this.migrateLockedIfNeeded(lockToken);
|
|
2182
2266
|
const rows = cloneRows(this.loadLedger());
|
|
2183
2267
|
const report = { applied: [], conflicts: [] };
|
|
@@ -2291,6 +2375,14 @@ export class FileMemoryEngineBackend {
|
|
|
2291
2375
|
report.conflicts.push({ op: patch.op, id: patch.id, reason: `invalid_slug_shape (slug ${JSON.stringify(patch.entry.slug)} is empty, absolute, or path-escaping — a projection must stay inside its scope dir)` });
|
|
2292
2376
|
return;
|
|
2293
2377
|
}
|
|
2378
|
+
if (patch.op !== "delete" && patch.entry !== undefined && ambiguousOriginRepresentation(patch.entry.frontmatter)) {
|
|
2379
|
+
report.conflicts.push({
|
|
2380
|
+
op: patch.op,
|
|
2381
|
+
id: patch.id,
|
|
2382
|
+
reason: `external-origin marker representation conflict: the entry carries conflicting or duplicated origin representations (malformed patch refused)`,
|
|
2383
|
+
});
|
|
2384
|
+
return;
|
|
2385
|
+
}
|
|
2294
2386
|
if (patch.op === "add") {
|
|
2295
2387
|
const entry = patch.entry;
|
|
2296
2388
|
if (!entry) {
|
|
@@ -2299,6 +2391,19 @@ export class FileMemoryEngineBackend {
|
|
|
2299
2391
|
}
|
|
2300
2392
|
const locatedAnywhere = this.locateById(patch.id);
|
|
2301
2393
|
const existingAnywhere = locatedAnywhere !== undefined && plannedDeletes.has(patch.id) ? undefined : locatedAnywhere;
|
|
2394
|
+
if (locatedAnywhere !== undefined) {
|
|
2395
|
+
const shadowText = this.readCommittedShadow(patch.id);
|
|
2396
|
+
const committedFm = shadowText !== undefined ? parseEntryFile(shadowText).frontmatter : locatedAnywhere.entry.frontmatter;
|
|
2397
|
+
const committedOrigin = committedOriginOf(committedFm);
|
|
2398
|
+
if (committedOrigin !== undefined && !originEquals(committedOrigin, committedOriginOf(entry.frontmatter))) {
|
|
2399
|
+
report.conflicts.push({
|
|
2400
|
+
op: "add",
|
|
2401
|
+
id: patch.id,
|
|
2402
|
+
reason: `external-origin marker whitewash refused: the add strips or rewrites the origin marker of a marked entry (malformed patch refused)`,
|
|
2403
|
+
});
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2302
2407
|
if (patch.guard === "absent") {
|
|
2303
2408
|
if (existingAnywhere) {
|
|
2304
2409
|
const currentRev = rowsRev(rows, patch.id) ?? existingAnywhere.entry.rev;
|
|
@@ -2376,6 +2481,15 @@ export class FileMemoryEngineBackend {
|
|
|
2376
2481
|
return;
|
|
2377
2482
|
}
|
|
2378
2483
|
}
|
|
2484
|
+
const committedOrigin = committedOriginOf(committedFm);
|
|
2485
|
+
if (committedOrigin !== undefined && !originEquals(committedOrigin, committedOriginOf(patch.entry.frontmatter))) {
|
|
2486
|
+
report.conflicts.push({
|
|
2487
|
+
op: "update",
|
|
2488
|
+
id: patch.id,
|
|
2489
|
+
reason: `external-origin marker whitewash refused: the update strips or rewrites the origin marker of a marked entry (malformed patch refused)`,
|
|
2490
|
+
});
|
|
2491
|
+
return;
|
|
2492
|
+
}
|
|
2379
2493
|
}
|
|
2380
2494
|
const currentRev = rowsRev(rows, patch.id) ?? found.entry.rev;
|
|
2381
2495
|
if (patch.baseRev !== undefined && patch.baseRev !== currentRev) {
|
|
@@ -2447,10 +2561,10 @@ export class FileMemoryEngineBackend {
|
|
|
2447
2561
|
try {
|
|
2448
2562
|
this.assertTxnLockOwnership(lock.token);
|
|
2449
2563
|
this.ledger = undefined;
|
|
2450
|
-
this.recoverJournal();
|
|
2564
|
+
this.recoverJournal({ token: lock.token });
|
|
2451
2565
|
this.migrateLockedIfNeeded(lock.token);
|
|
2452
2566
|
const rows = cloneRows(this.loadLedger());
|
|
2453
|
-
const chain = this.readTransferChain();
|
|
2567
|
+
const chain = this.readTransferChain(lock.token);
|
|
2454
2568
|
const degraded = this.chainDegradedMarkerPresent();
|
|
2455
2569
|
const custodyState = degraded ? "damaged" : "complete";
|
|
2456
2570
|
const selectHash = erasureSelectHash(select);
|
|
@@ -3141,7 +3255,7 @@ export class FileMemoryEngineBackend {
|
|
|
3141
3255
|
try {
|
|
3142
3256
|
this.assertTxnLockOwnership(lock.token);
|
|
3143
3257
|
this.ledger = undefined;
|
|
3144
|
-
this.recoverJournal();
|
|
3258
|
+
this.recoverJournal({ token: lock.token });
|
|
3145
3259
|
const rows = this.loadLedger();
|
|
3146
3260
|
if (this.persistedSchemaVersion !== "v2") {
|
|
3147
3261
|
throw erasureCodedError("memory export refused: the committed-rev ledger is still schema v1 (pre-binding) — the store migrates itself at its next write/read entry or checkControlPlane; an export never triggers the migration", FileMemoryEngineBackend.EXPORT_INCOMPLETE);
|
|
@@ -3187,9 +3301,9 @@ export class FileMemoryEngineBackend {
|
|
|
3187
3301
|
const lineage = [];
|
|
3188
3302
|
for (const [entryId, sessions] of Object.entries(lineageRec.committed)) {
|
|
3189
3303
|
for (const [sessionId, c] of Object.entries(sessions))
|
|
3190
|
-
lineage.push({ entryId, sessionId, lastRev: c.lastRev, lastAt: c.lastAt });
|
|
3304
|
+
lineage.push({ entryId, sessionId, lastRev: c.lastRev, lastAt: c.lastAt, ...(c.marked === true ? { marked: true } : {}) });
|
|
3191
3305
|
}
|
|
3192
|
-
const pollutedSessions = Object.entries(polluted).map(([sessionId, r]) => ({ sessionId, at: r.at, reason: r.reason }));
|
|
3306
|
+
const pollutedSessions = Object.entries(polluted).map(([sessionId, r]) => ({ sessionId, at: r.at, reason: r.reason, ...(r.cause !== undefined ? { cause: r.cause } : {}) }));
|
|
3193
3307
|
try {
|
|
3194
3308
|
this.assertTxnLockOwnership(lock.token);
|
|
3195
3309
|
}
|
|
@@ -3224,7 +3338,7 @@ export class FileMemoryEngineBackend {
|
|
|
3224
3338
|
try {
|
|
3225
3339
|
this.assertTxnLockOwnership(lock.token);
|
|
3226
3340
|
this.ledger = undefined;
|
|
3227
|
-
this.recoverJournal();
|
|
3341
|
+
this.recoverJournal({ token: lock.token });
|
|
3228
3342
|
const rows = this.loadLedger();
|
|
3229
3343
|
if (this.persistedSchemaVersion !== "v2")
|
|
3230
3344
|
return { custody: [], unbound: [], complete: false, unsliceableCustody: 0, reason: "the committed-rev ledger is still schema v1 (migrate first)" };
|
|
@@ -3259,7 +3373,7 @@ export class FileMemoryEngineBackend {
|
|
|
3259
3373
|
const lock = await this.acquireTxnLock(timings);
|
|
3260
3374
|
try {
|
|
3261
3375
|
this.assertTxnLockOwnership(lock.token);
|
|
3262
|
-
this.recoverJournal();
|
|
3376
|
+
this.recoverJournal({ token: lock.token });
|
|
3263
3377
|
return this.custodyImportLocked(rows, opts);
|
|
3264
3378
|
}
|
|
3265
3379
|
finally {
|
|
@@ -3328,7 +3442,7 @@ export class FileMemoryEngineBackend {
|
|
|
3328
3442
|
try {
|
|
3329
3443
|
this.assertTxnLockOwnership(lock.token);
|
|
3330
3444
|
this.ledger = undefined;
|
|
3331
|
-
this.recoverJournal();
|
|
3445
|
+
this.recoverJournal({ token: lock.token });
|
|
3332
3446
|
this.migrateLockedIfNeeded(lock.token);
|
|
3333
3447
|
const ctl = this.controlPlaneRoot;
|
|
3334
3448
|
const txnId = importLatchTxnId(bundleHash);
|
|
@@ -3450,8 +3564,21 @@ export class FileMemoryEngineBackend {
|
|
|
3450
3564
|
const orphanIds = new Set();
|
|
3451
3565
|
const lineageToApply = [];
|
|
3452
3566
|
for (const row of plan.lineage) {
|
|
3453
|
-
if (eligible.has(row.entryId))
|
|
3567
|
+
if (eligible.has(row.entryId)) {
|
|
3568
|
+
if (row.marked === true) {
|
|
3569
|
+
const shadow = this.readCommittedShadow(row.entryId);
|
|
3570
|
+
let committedFm = shadow !== undefined ? parseEntryFile(shadow).frontmatter : undefined;
|
|
3571
|
+
if (committedFm === undefined)
|
|
3572
|
+
committedFm = this.locateById(row.entryId)?.entry.frontmatter;
|
|
3573
|
+
if (committedFm !== undefined && committedOriginOf(committedFm) === undefined) {
|
|
3574
|
+
const { marked: _dropped, ...unmarked } = row;
|
|
3575
|
+
lineageToApply.push(unmarked);
|
|
3576
|
+
report.conflicts.push({ id: row.entryId, reason: `lineage row claims marked:true but the committed content carries no origin marker — the bit was dropped (the pollution sweep judges the contribution)` });
|
|
3577
|
+
continue;
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3454
3580
|
lineageToApply.push(row);
|
|
3581
|
+
}
|
|
3455
3582
|
else if (destKnown(row.entryId) || entryById.has(row.entryId))
|
|
3456
3583
|
withheldIds.add(row.entryId);
|
|
3457
3584
|
else
|
|
@@ -3476,14 +3603,14 @@ export class FileMemoryEngineBackend {
|
|
|
3476
3603
|
...(c.challengedRev !== undefined ? { challengedRev: c.challengedRev } : {}),
|
|
3477
3604
|
})));
|
|
3478
3605
|
for (const p of [...plan.pollutedSessions].sort((a, b) => (a.sessionId < b.sessionId ? -1 : 1))) {
|
|
3479
|
-
const contributions = lineageContributionsOfSession(ctl, p.sessionId);
|
|
3606
|
+
const contributions = lineageContributionsOfSession(ctl, p.sessionId).filter((c) => c.marked !== true);
|
|
3480
3607
|
this.appendImportChallengesVerified(contributions.map((c) => ({
|
|
3481
3608
|
eventId: `imp-sweep:${bundleHash}:pollution:${p.sessionId}:${p.at}:${c.entryId}`,
|
|
3482
3609
|
entryId: c.entryId,
|
|
3483
3610
|
reason: `challenged: the contributing session was marked polluted`,
|
|
3484
3611
|
challengedRev: c.lastRev,
|
|
3485
3612
|
})));
|
|
3486
|
-
const outcome = importSessionPollution(ctl, p.sessionId, { at: p.at, reason: p.reason });
|
|
3613
|
+
const outcome = importSessionPollution(ctl, p.sessionId, { at: p.at, reason: p.reason, ...(p.cause !== undefined ? { cause: p.cause } : {}) });
|
|
3487
3614
|
if (outcome === "divergent")
|
|
3488
3615
|
report.pollutionDivergence.push({ sessionId: p.sessionId, kept: "destination" });
|
|
3489
3616
|
}
|
|
@@ -3568,6 +3695,7 @@ function headerOf(e, path) {
|
|
|
3568
3695
|
mtimeMs,
|
|
3569
3696
|
rev: e.rev,
|
|
3570
3697
|
sizeBytes: Buffer.byteLength(serializeEntryFile(e), "utf8"),
|
|
3698
|
+
...(committedOriginOf(e.frontmatter) !== undefined ? { exposure: "external" } : {}),
|
|
3571
3699
|
};
|
|
3572
3700
|
}
|
|
3573
3701
|
function readSafe(path) {
|