@sema-agent/core
Version:
Stateless, task-oriented AI agent core
1,249 lines • 90.3 kB
TypeScript
import { scopeDirName, type ChallengeEvent, type MemoryPartitionIncidentSink } from "./layout.js";
import type { MemoryBundleImportPlan, MemoryImportReport } from "./export-bundle.js";
import type { HarvestRejection, MemoryBackend, MemoryEntry, MemoryEntryHeader, MemoryScopeEnumeration, NotePatch, PatchReport, ScoredMemoryEntry } from "./types.js";
export { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, scanEntryFiles, type ScannedEntryFile } from "./entry-scan.js";
/** design/178 v2-b §3.3(b) — the explicit erasure selector (each form resolves to an explicit id
* list before anything is deleted; there is no whole-store selector). */
export type ErasureSelect = {
ids: readonly string[];
} | {
scope: string;
} | {
sessionId: string;
};
/** design/178 v2-b §3.3(c) — one erased row's coordinates (the snapshot's tagged binding law,
* without the account-trace extras: a wire validator refuses hybrid objects). */
export type ErasedBinding = {
state: "bound";
scope: string;
slug: string;
} | {
state: "unbound";
};
/** The input of the erasure lane (engine host API `eraseMemoryEntries` and the File capability face
* `eraseWithEvidence` share it; the File face ignores `allowUnevidenced` — with the evidence
* capability present there is nothing to degrade to). */
export interface EraseMemoryEntriesInput {
/** Idempotency identity, caller-minted (challenge-family law: the engine never mints one). */
requestId: string;
select: ErasureSelect;
/** Evidence-capability-ABSENT degradation opt-in (engine lane only; default = loud refusal). */
allowUnevidenced?: boolean;
}
/**
* design/178 v2-b §3.3(c) — the erasure attestation (the deliverable a compliance caller archives).
* Every claim is black-box re-checkable: an `erased` row is verified by `committedSnapshotOf(id)`
* answering `"absent"`, the chain containing its `evidenceEv` delete row, and `getByIds([id])`
* answering empty.
*/
export interface MemoryErasureAttestation {
/** Version envelope — a consumer reading `v > 1` must refuse, never reinterpret. */
v: 1;
requestId: string;
at: number;
/** `"complete"` ⇔ conflicts empty ∧ every pinned id ∈ erased ∪ erasedPreviously ∪ (notFound with
* `custodyState === "complete"`). Everything else is `"partial"` (replay converges it). */
status: "complete" | "partial";
/** `"journal"` = the File evidence path; `"none"` = the explicit `allowUnevidenced` degradation
* lane (always present — a silent downgrade is the forbidden shape). */
evidenceCapability: "journal" | "none";
/** The evidence chain's integrity as read at assembly (v2-a custody tri-state law). `"damaged"`
* ⇒ every notFound row carries `historyUnknown` and `erasedPreviously` is never produced ("no
* row" cannot be historically judged over a chain with a known hole). */
custodyState: "complete" | "damaged" | "capability-absent";
/** The selector, echoed verbatim. */
select: ErasureSelect;
/** sha256 hex of the selector's canonical JSON — equals the anchor row's (`"none"` mode: computed
* for this call only; there is no anchor to reconcile against). */
selectHash: string;
/** The pinned id set (= the anchor row's `ids`; a replay reads it back, never re-resolves). */
resolvedIds: string[];
erased: Array<{
id: string;
rev: string;
binding: ErasedBinding;
/** The delete evidence row's `ev` — absent in `"none"` mode. */
evidenceEv?: string;
/** Distinct census-confirmed projection files this transaction physically deleted (the sweep
* arm can answer >1: stray copies and P≥2 unbound duplicates are all removed). */
projectionsRemoved: number;
/** Contributing sessions (pre-capture ∪ the same-lock lineage capture-and-clear; `"none"` mode
* answers `[]`). NON-durable beyond the chain's pre-capture: a replay after a crash carries
* the chain's set — honest downgrade, never a fabricated rebuild. */
sessions: string[];
}>;
/** No row at judgment time and not claimable as erased under THIS request. Covers never-existed /
* organically deleted / deleted by another request — the chain can distinguish them, the
* attestation does not pretend to. `historyUnknown` ⇔ `custodyState !== "complete"`. */
notFound: Array<{
id: string;
historyUnknown?: true;
}>;
/** Chain-read rows: an origin-ABSENT delete row carrying THIS requestId (per-request judgment —
* another request's delete of the id answers notFound, never a claim). Present only when
* non-empty; never produced in `"none"` mode or over a damaged chain. */
erasedPreviously?: Array<{
id: string;
ev: string;
at: number;
/** The pre-delete binding from the most recent same-request delete row that RECORDED one —
* carried so a REPLAY can still run the index-line sweep (the ledger row is gone; the evidence
* row is the only surviving source of the projection path). `ev`/`at` come from the MOST RECENT
* same-request delete row (which may be a later, binding-less re-erase). Absent in THREE cases:
* no same-request delete row ever recorded a binding (unbound / pre-v2-form), or the recorded
* path is OCCUPIED now (a live account row binds that scope+slug, or bytes exist at the path —
* the binding is known but suppressed: the index line there belongs to the occupant, and a
* consumer must not read absence as "no residue possible"). */
from?: {
scope: string;
slug: string;
};
}>;
/** Per-id planning-stage faults (io/plan errors under the lock — there is no CAS loser inside a
* single lock span). Non-fatal; a replay converges them. */
conflicts: Array<{
id: string;
reason: string;
}>;
residuals: {
/** Quarantine files whose frontmatter id is in the pinned set (preserve-not-delete: the
* attestation enumerates, an explicit second action clears). */
quarantineHits: string[];
/** Quarantine files that could not be attributed (fragments / non-entry shapes) — the honest
* bound of the enumeration. */
quarantineOpaque: number;
/** Derived MEMORY.md index files that may STILL carry a line naming an erased entry, because
* the erase-time index sweep could not read or rewrite them (a non-file at the index path, a
* non-ENOENT read failure, or a refused write — an entry may carry a parenthesized failure
* detail). Present only when non-empty. Minted by the ENGINE lane
* (`MemoryEngine.eraseMemoryEntries` — the derived index is the engine's plane; this backend
* face answers without the seat). The next successful materialize's rebuild also clears
* orphaned lines, but a one-off compliance erasure cannot count on one — hence the disclosure
* instead of a silent clean residual set. */
indexUncleared?: string[];
/** Propagation boundary, self-declared always: deletion is a THIS-store fact; remote-peer
* convergence is the sync deployment's half to prove. */
propagation: "local-store-only";
};
}
/** Canonical JSON (recursively sorted keys, no whitespace) — the erasure protocol's ONE comparison
* and hash basis: `selectHash` complains bind to it, and evidence rows are written/compared in this
* form so a re-serialized import (key order changed by a middle layer) still compares equal. */
export declare function canonicalJsonStringify(value: unknown): string;
/** sha256 hex over the selector's canonical JSON + LF — the replay-identity judge (§4.6) and the
* anchor validator's recomputation basis (a 64-hex shape check alone would let a forged anchor swap
* the selector under a kept hash). */
export declare function erasureSelectHash(select: ErasureSelect): string;
/** Erasure input validation, shared by the engine host API and the File capability face (one rule
* set — the selector the anchor row echoes verbatim must pass the anchor's own validator, so the
* entry check and the chain check are the same predicate). Returns the violation, or undefined. */
export declare function erasureRequestInvalid(input: EraseMemoryEntriesInput): string | undefined;
/**
* Snapshot the erasure input into PLAIN data by reading every accessor exactly once (round-2 #3/#5):
* validation and use must observe the SAME value, so an accessor-backed (getter/Proxy) selector or
* requestId cannot pass validation on read #1 and yield a different (unvalidated) set on read #2.
* The returned object is inert — its `requestId` is a captured primitive and its `select` is a fresh
* plain object. Malformed shapes (a non-object select, or one with several/no keys) capture as-is so
* the validator that runs next reports them; the capture never throws.
*/
export declare function captureErasureInput(input: EraseMemoryEntriesInput): EraseMemoryEntriesInput;
/**
* design/178 v2-a §1.3 — ONE `transfers.jsonl` evidence row on the PUBLIC audit face, as an OPEN
* discriminated form: the base keys (`ev` — the row's unique identity, `channel` — the event kind,
* `at` — ms epoch) are the contract; everything else is variant payload surfaced through the index
* signature. Known channels today (each validated in full by the store's own rule set):
* - `"adopted-move"` / `"applyPatches-move"`: an ownership transfer of one id — payload
* `{ id, from: {scope, slug}, to: {scope, slug} }`;
* - `"migration-bind"`: an unbound row converging to a binding — payload `{ id, to }` (`from`
* optional: honest absence, an unbound row has no prior coordinates);
* - `"migration"`: the one-shot v1→v2 baseline summary — payload `{ boundRows, unboundRows }`
* (store management history, never per-entry custody).
* `channel` is deliberately `string`, not a closed union: later releases append new variants to the
* SAME chain (one custody chain, one recovery protocol — never a second evidence file), and a closed
* union would break every consumer at the source level on each addition. CONSUMER OBLIGATION: tolerate
* unknown channels (report/skip/display raw — never an exhaustive `switch` that throws); the per-id
* custody read passes them through so a chain written by a newer engine still answers here.
*/
export interface TransferEvidence {
/** Unique event id (the append-idempotency key of the evidence chain). */
ev: string;
/** Event kind — open on purpose; see the known-channel table above. */
channel: string;
/** ms epoch of the event. */
at: number;
/** Variant payload (known channels documented above; unknown channels' payload rides verbatim). */
[key: string]: unknown;
}
/**
* design/178 v2-a §1.3 — the committed-account BINDING half of one ledger row, shared verbatim by
* {@link CommittedEntrySnapshot} and `EntryProvenanceAccount.binding` (one tagged definition — never
* a look-alike copy: an untagged `{scope, slug}` beside `{state: "unbound"}` can be satisfied by one
* hybrid object under both readings). `"unbound"` is a LEGAL transitional state (a v1→v2 migration
* row whose projection could not be derived), never a refusal.
*/
export type CommittedBinding = {
state: "bound";
scope: string;
slug: string;
at?: number;
prev?: {
scope: string;
slug: string;
at: number;
};
} | {
state: "unbound";
};
/**
* design/178 v2-a §1.3 — the AUDIT SNAPSHOT of one entry's committed account: the account half
* (`rev` + {@link CommittedBinding}) and the content half are SEPARATE, because the account's
* authority is the ledger row while the content may be independently unavailable — an auditor asking
* "where is this entry bound, at which rev" gets the account facts even when no committed carrier
* can be produced ("cannot read it" must never collapse into "it does not exist").
* - `state: "absent"` — the account has NO row for this id (never a throw, never a guess);
* - `content.state: "present"` — the committed entry is servable: the committed bytes (control-plane
* shadow; for a pre-shadow row, the in-place projection at the bound coordinates carrying the
* committed rev) with the projection carrier in place at the bound address;
* - `content.state: "unavailable"` — the row exists (binding/rev answered) but no committed carrier
* serves it: `"shadowless-legacy"` = no shadow copy exists and the bound projection cannot vouch
* (missing/divergent/foreign id); `"carrier-missing"` = the projection carrier is not in place at
* the committed address (deleted or squatted out-of-band) or the shadow cannot carry the
* committed rev — including every UNBOUND row (no committed address exists to mint
* `MemoryEntry.scope`/`slug` from; the shadow bytes, where present, stay readable via
* `readCommittedShadow`).
*/
export type CommittedEntrySnapshot = {
state: "absent";
id: string;
} | {
state: "row";
id: string;
rev: string;
binding: CommittedBinding;
content: {
state: "present";
entry: MemoryEntry;
} | {
state: "unavailable";
reason: "shadowless-legacy" | "carrier-missing";
};
};
/**
* design/178 v2-a §1.3 — the LEDGER-DRIVEN scope enumeration's return form. `rows` carries every
* BOUND row whose `binding.scope` is in the requested set (the account is the enumeration authority
* — a shadow-backed row whose projection was deleted out-of-band is still answered; a disk scan
* would miss it). `unbound` is the STORE-LEVEL appendix of unbound row ids (they have no scope to
* belong to — the enumerating consumer disposes of them explicitly: an export lists them as
* residuals, an erasure scope-selector never resolves them). `complete: false` means a stable
* committed snapshot could not be taken (the journal-quiescence fence stayed contended past its
* bound) and the caller must FAIL CLOSED — a partial answer is never dressed as a full one; the
* union is DISCRIMINATED on `complete` so the incomplete arm cannot even carry rows to misread.
*/
export type CommittedScopeSnapshots = {
complete: true;
rows: Array<Extract<CommittedEntrySnapshot, {
state: "row";
}>>;
unbound: string[];
} | {
complete: false;
};
/**
* design/178 v2-a §1.3 — the per-id custody read's return form. `"damaged"` covers every chain whose
* integrity is impeached while a sound reading still exists to answer with: a torn tail (crash
* mid-append — the sound prefix IS the answer and the file is NOT touched: inspection and repair are
* separate duties; the recovery leg quarantines the fragment, and an audit read that "helpfully"
* rewrites the evidence it is auditing has destroyed its own subject), the evidence-loss cross-check
* (a prev-bearing ledger row beside a chain with zero events for the id), and the ev-identity law
* (one `ev` under two DIFFERENT payloads — the chain was spliced/corrupted out of band; the store's
* own write path refuses to operate on it, so custody must never read it back as `"complete"`).
* `reason` carries the impeachment detail when one is known (today: the ev-identity arm); its
* absence means the damage form speaks for itself (torn tail / evidence loss).
* `"capability-absent"` never originates here — it is the ENGINE's honest answer for a backend
* without this capability face.
*/
export interface EntryCustodyReport {
state: "complete" | "damaged";
events: TransferEvidence[];
reason?: string;
}
/**
* design/178 v2-c §3 — what the ONE-LOCK export composite ({@link FileMemoryEngineBackend.exportSnapshotOf})
* answers: every face of the store the bundle needs, read inside a single txn-mutex hold (see the
* method doc for the fence law). Entries carry committed content only; a bound row whose committed
* carrier cannot be served is enumerated under `contentUnavailable` instead (never silently
* dropped, never dressed as content). `unbound`/`pendingLatch` are STORE-LEVEL residual sets (no
* tenant to belong to); the bundle assembly decides whether their id lists travel (full-store
* export) or only their counts (subset export). `challenges`/`lineage`/`pollutedSessions` are the
* RAW governance faces — the (pure) bundle assembly slices them to the exported entry/session
* sets; `custody` is already sliced (the slicing consumes row-level scope fields this module owns).
*/
export interface MemoryExportSnapshot {
storeId: string;
/** True ⇔ the requested scopes cover every registered scope (one trust domain — the residual id
* lists and unbound-deletion custody rows may travel). */
fullStore: boolean;
entries: MemoryEntry[];
contentUnavailable: Array<{
id: string;
scope: string;
slug: string;
rev: string;
reason: string;
}>;
unbound: string[];
pendingLatch: string[];
custody: TransferEvidence[];
unsliceableCustody: number;
challenges: ChallengeEvent[];
lineage: Array<{
entryId: string;
sessionId: string;
lastRev: string;
lastAt: number;
marked?: true;
}>;
pollutedSessions: Array<{
sessionId: string;
at: number;
reason: string;
cause?: import("./types.js").MemoryOriginCause;
}>;
quarantined: string[];
quarantineOpaque: number;
}
/** Test hook (证伪式复审 L3): small constants so the crash-window leg does not really wait 15-30s. */
interface TxnLockTimings {
staleMs?: number;
waitMs?: number;
stealGraceMs?: number;
}
/** Construction options for {@link FileMemoryEngineBackend}. */
export interface FileMemoryEngineBackendOptions {
/** ⚠️ 分家坑(service 报告 2026-07-09):不显式传时,控制平面按无参 `resolveMemoryEngineRoot()`
* 派生(env/默认 config 根)——若你的记忆目录带自定义根,控制面会与记忆目录分家。自建 backend 的
* consumer 应显式钉本字段(engine 同参)。 */
controlDir?: string;
/** 注入时钟(determinism,engine 同款);默认 `Date.now`。 */
now?: () => number;
/**
* Where mount-time partition-key incidents go. The migration probes promise "never silent", and a
* self-constructed backend has no other channel to keep that promise on — the runner-assembled path
* bridges its own sink into `RunnerDeps.onError`, and without this seat the identical incident was
* unobservable here. Absent ⇒ the probe stays silent (it is advisory; a mount never fails over it).
*/
onIncident?: MemoryPartitionIncidentSink;
}
/**
* ⚠️ DEPLOYMENT BOUNDARY (ruled 2026-07-09): this backend — and the
* file memory engine's materialize/harvest — operates on the WORKER-HOST fs plane (node:fs), matching
* CC's `~/.claude` host-plane memory (the only behavior anchor). It is NOT ExecutionEnv-aware: when a
* deployment routes the model's file tools to a REMOTE sandbox (e2b/k8s/ssh/adb/local-docker), the two
* planes do not intersect — the model cannot see the memory dir and harvest cannot see sandbox writes.
* Remote-hands deployments MUST gate the engine off for non-host lanes (reference: service
* `memoryEngineBackendFor` lane gate, fail-closed + `MEMORY_ENGINE_REMOTE_LANE=allow` override for
* lanes that genuinely share the worker fs, e.g. ssh+NFS). A future "remote-memory-materialize" design
* (ExecutionEnv-seam placement/harvest with sandbox-lifecycle contracts) is demand-triggered — do not
* bolt it on here.
*/
export declare class FileMemoryEngineBackend implements MemoryBackend {
/** The repo-level memory dir this backend is the authority for (engine reads it for zero-copy). */
readonly directoryRoot: string;
/** B3 — the engine control plane home (ledger/scopes/shadow/quarantine/journal). Model-invisible:
* NEVER under {@link directoryRoot}, always on the config-root side even for an in-repo memory dir.
* The engine picks this up (like `directoryRoot`) so both sides share ONE control plane. */
readonly controlPlaneRoot: string;
/** {@link directoryRoot}'s physical form at construction — the root every act on a store path is judged against. */
private readonly physicalRoot;
/** Injected clock (opus 1.258 LOW: match the engine's `this.now` determinism discipline). */
private readonly now;
/** The construction-time incident sink (partition adoption, layout adoption on the standalone faces). */
private readonly onIncident;
/**
* The COMMITTED-rev ledger (`revs.json`, id → row; design/186 schema v2). Zero-copy makes it
* load-bearing for CAS: the entry files are the SAME files the session edits, so "read the current
* rev from the file" would make every legitimate update self-conflict (the file already holds the
* new content). Each row records the rev as of the last COMMIT (applyPatches) — a CAS compares
* against IT, so an in-session edit passes (baseRev == ledger) while a concurrently-committed
* change conflicts (ledger advanced) — PLUS the projection BINDING (owning scope + slug): bytes
* are model-plane material anyone can copy or relocate, so the account — model-invisible, outside
* every file-tool root — is what holds ownership. "rev matches" alone is no longer a serve
* credential; "rev matches AND the coordinates match the binding" is.
* Read-side (`listHeaders`/`getByIds`/`search`) SYNCS the ledger to disk THROUGH the inbound gate
* (B2 — out-of-session edits, e.g. a git pull into an in-repo memory dir, are secret/cap-scanned;
* rejects are quarantined + shadow-restored, never adopted); the applyPatches lookup does NOT sync
* (that would erase the very baseline CAS needs). B3: corrupt ⇒ {@link ControlPlaneCorruptError}
* (fail-closed), never a silent empty-ledger downgrade.
*/
private ledger;
/** Top-level unknown keys of the loaded v2 envelope — write-preserved (§1.1). */
private envelopeExtras;
/**
* §3.1 (r3-④) — the migration retry judge: the PERSISTED schema form, set at every real disk read
* of the ledger and flipped only by the locked migration. Deliberately NOT derived from the
* in-memory ledger shape: the v1 compat arm upgrades rows in memory (§3.2), so the memory shape
* reads "already v2" after the first stand-down and the retry would die. `"v1"` arms the
* per-entry opportunistic retry (construction / reads / checkControlPlane / every locked
* mutation); undefined = the disk form has not been observed yet.
*/
private persistedSchemaVersion;
/** §3.1 — the v1-compat window is loud ONCE per instance (per-row unbound noise stays suppressed;
* the OVERALL state does not). */
private announcedV1Compat;
/** §3.4 — O(1) retry judge for the READ entries' unbound-convergence attempts: recomputed at
* every disk load and every persist (both already iterate the rows), never by scanning per read.
* Slightly stale-TRUE is harmless (one try-lock + one early-return reload); stale-FALSE cannot
* happen (every seat that changes the rows passes through a maintaining seat). While a blocker
* is genuinely stuck (a standing duplicate), each read pays one census — the same bounded cost
* the v1 read retry accepts for an incomplete store, in a state the sweeps warn about loudly. */
private unboundRowsKnown;
/** Adopting-face notice dedup (copy / undecidable-probe / ambiguous / census-incomplete): one
* still-pending state announces once per instance; changed bytes mint a new key and re-announce
* (same posture as the restricted finding keys, which are per-MOUNT — this face has no mount). */
private adoptionNoticeKeys;
/** Test seam (§7.4 r4-④ arm): force a post-commit-point transfer-append failure. */
private transfersAppendFault?;
/** Test seam (#262 ③/#191 review r1 fence pins): runs inside `recoverJournal` between the redo
* replay and the commit-boundary ownership re-assertion — a test flips the lock owner (or swaps
* the journal) here to exercise the mid-replay steal arms. Production leaves it unset. */
private recoveryCommitProbe?;
/** Test seam (#262 ③/#191 review r2): runs inside the torn-tail heal between the marker work and
* the pre-truncation ownership re-assertion — a test flips the lock owner here to prove the
* chain is never truncated after an ownership loss. Production leaves it unset. */
private chainHealProbe?;
/** Test seam (v2-c §3-2 fence pins): runs INSIDE the export composite's mutex hold, after the
* data reads and before the second fingerprint — a test injects a governance-face write here to
* exercise the drift-retry and the three-round refusal. Production leaves it unset. */
private exportFaceProbe?;
/** v2-b §3-4 — the resurrection backstop's chain digest, keyed by the evidence log's stat
* fingerprint (size + mtimeMs — append-only growth and a torn-tail shrink both move `size`).
* NEVER a mount-lifetime cache: the fingerprint is re-verified under the txn mutex on every
* consultation, so a sibling process's erasure invalidates it (r3: a B-process erasure must not
* leave an A-process adopting off the old deleted-id set). The digest carries its chainState —
* a damaged read caches as damaged, never as a bare (falsely reassuring) id set. */
private deletedIdsDigest;
/** B2 — inbound-gate findings accumulated by read-side syncs; the engine drains them into
* `HarvestReport.inboundFindings` at harvest. */
private inboundFindings;
/** Batch-planning scan memo (applyPatchesLocked only): planning reads pre-batch disk exclusively,
* so one `readScope(scope, false)` per scope serves every patch of the batch. Set for the planning
* loop, cleared before EXECUTE mutates the disk. Never caches `sync:true` reads. */
private batchScan;
constructor(dir: string, opts?: FileMemoryEngineBackendOptions);
/** The adoption under a held transaction lock (before the ledger migration, whose census derives
* paths from the registry); the heartbeat refreshes the lock and re-proves it before every action. */
private adoptLayoutLocked;
/** The seat at the head of every lock-free public face: adopt under a NON-BLOCKING try-acquire. A live
* holder stands this caller down (its locked faces, or the next mount, adopt); the read that follows
* refuses fail-closed on a v1 registry rather than serving a layout it cannot describe. */
private ensureLayout;
/** The engine's mount seat: adopt under the transaction lock (`ownerCandidate` = the write scope; `onIncident` = this call's sink). */
adoptRootScopeLayout(opts?: {
ownerCandidate?: string | null;
onIncident?: MemoryPartitionIncidentSink;
}): Promise<void>;
/** B3 fail-closed preflight (engine harvest duck-types this): force-reload every load-bearing
* sidecar from disk; throws {@link ControlPlaneCorruptError} when one is corrupt. Also completes
* any interrupted transaction (H4 recovery). */
checkControlPlane(): void;
/** B2 — drain the accumulated inbound-gate findings (engine harvest reporting). */
drainInboundFindings(): HarvestRejection[];
/** B1 — read an entry's COMMITTED content copy from the control-plane shadow (undefined = none).
* The engine's missing-file recovery reads this (zero-copy: a deleted file is gone from the
* directory scan, so the shadow is the only committed copy left). */
readCommittedShadow(id: string): string | undefined;
private shadowPath;
private loadLedger;
/** B3: atomic + fail-closed — a ledger that cannot be persisted throws (the commit is incomplete
* and the caller must know), never a silent swallow. Always writes the v2 envelope; writing over
* an unmigrated v1 disk is refused loudly (mutations migrate FIRST — §3.1's "变异必先迁" is an
* invariant here, not a convention: a v2 write minus the census would strand every row unbound). */
private saveLedger;
/** v1-journal restore ONLY (§1.2 dual-read): the snapshot predates the upgrade and is persisted in
* ITS OWN form — the migration (which runs after recovery, under the lock) owns the v1→v2 swap
* together with its backup + evidence protocol. */
private persistLedgerV1;
/** Fail-open announcement convenience (S2-B queue posture — an unwritable queue never fails the
* operation that had something to say; the fail-CLOSED evidence lane is `transfers.jsonl`). */
private enqueueExternalItems;
/** One-shot dedup for adopting-face notices (see {@link adoptionNoticeKeys}). */
private announceAdoptionNotice;
/**
* §3.4 — the standing unbound-row disclosure, in ONE spelling for BOTH faces that carry it (the
* harvest preflight `checkControlPlane` and the restricted sweep `auditRestrictedDivergence` used
* to hold drifting hand-built copies of this sentence; the sweep's copy had lost the remediation
* clause, which is if anything MORE apt there — a restricted session has no write channel of its
* own to converge the row).
*
* De-duplicated against what is ALREADY QUEUED, not against a per-instance latch. The repeat this
* closes was not merely noise: the queue is bounded ({@link MEMORY_ANNOUNCEMENTS_MAX}), so a
* long-lived instance with one stuck row folded REAL notices out of it to make room for copies of
* this one. But the latch the sibling adoption notices use ({@link announceAdoptionNotice}) is the
* wrong instrument HERE, twice over, and both were found in review:
* · a backend instance is a RUNNER-level dependency reused across every session it serves, so a
* per-instance latch would announce the standing condition to the first session and then hold
* its peace forever — turning a flood into silence, against this disclosure's own contract
* (「the STATE does not go silent」). The queue is DRAINED at each session's first injection, so
* "is an identical line still pending?" is exactly the right question: no repeat within a
* session, one fresh statement in every session that has not yet been told.
* · a latch key would have to encode the id set, and ledger row ids are arbitrary external
* strings (a comma is legal) — `join(",")` makes `["a,b","c"]` and `["a","b,c"]` the same key,
* so a real change of state could be suppressed. Comparing the RENDERED LINE has no such seam:
* two states that render the same sentence are, to every consumer, the same statement.
*
* The test runs INSIDE the queue's own locked read-modify-write
* ({@link enqueueMemoryAnnouncement}'s `supersedesPrefix`), not as a peek followed by an append: a
* peek-then-enqueue would race a concurrent drain, and two processes could both peek-miss and both
* append. Fail-open like every announce site — an unwritable queue never fails an operation that
* had something to say.
*/
private announceUnboundRows;
/** Resolve a binding's absolute projection path WITHOUT registering anything (probes must stay
* side-effect-free; `scopeDir` would register an unknown scope name from a hand-edited row). */
private bindingAbsPath;
private bindingRelPath;
/**
* §3.3/§2.2 — the strict projection census: EVERY id-bearing file under EVERY registered scope,
* with `{complete}` honesty. Absence is ENOENT-only AT EVERY LEVEL — deliberately NOT
* `scanEntryFiles`, whose root arm folds an unreadable scope root into "empty scope" (`onSkip`
* fires for non-root only): copied here, one transient root EACCES would turn "only B was
* visible" into "only B exists" and mint a permanent wrong binding (bound rows are never
* re-derived). Symlinks / depth overflow / non-.md are exclusions by the entry-scan contract,
* not census gaps.
*/
private censusProjectionsLocked;
/** §3.6 — capture the pre-migration v1 bytes exactly once (`wx`: the first scene is never
* overwritten by a retry). */
private writeV1BackupOnce;
/** §3.1 — non-blocking txn-mutex acquisition for the opportunistic migration entries (the
* constructor cannot await). LIVE contention stands down — wait semantics belong to the async
* writers; every entry retries later. A STALE lock (past its deadline, owner token present) gets
* ONE synchronous single-winner steal, same judge and same token discipline as the async
* waiter's steal: without it, a crashed holder's leftover lock would stand down every sync entry
* forever, and a READ-ONLY deployment — which never runs an async writer to do the stealing —
* would sit in v1-compat indefinitely (the very suspension §3.1 bounds to seconds). Steal-safety
* is unchanged: the caller runs journal recovery INSIDE the acquired lock before touching state. */
private tryAcquireTxnLockSync;
/** §3.1 — one opportunistic migration attempt (read entries / construction / checkControlPlane):
* try-lock, recover, migrate, release. A busy lock is a clean stand-down (the mutation entries
* bootstrap the migration under their own lock regardless). */
private tryOpportunisticMigrate;
/**
* §3.3-§3.5 — the migration body. MUST hold the txn mutex. Reloads the DISK truth (the retry
* judge is {@link persistedSchemaVersion}, set by that reload — never the in-memory shape), then:
* v1 ⇒ full census-derived rebind + one-shot backup + journaled swap + one migration-summary
* evidence event; v2-with-unbound ⇒ re-derive the unbound rows only (bound rows are FINAL — only
* explicit transactions rewrite a binding), each new bind carrying a `migration-bind` evidence
* event. An INCOMPLETE census abandons the pass with ZERO writes ("only what was visible" is not
* "all that exists"; a wrong binding is worse than no binding) and retries at the next entry.
* P=0 rows stay unbound (「缺失≠删除」— dropping the row would endorse an unattributed delete);
* P≥2 rows stay unbound (the migration never adjudicates which projection is real — an existing
* dup audit keeps ringing until a harvest or a human resolves it). Idempotent: v2 + zero unbound
* is a no-op version check.
*/
private migrateLockedIfNeeded;
/** Is the durable chain-degradation marker in place? Presence is ENOENT-only-absent (a probe
* failure is never read as "not degraded" — fail-closed). */
private chainDegradedMarkerPresent;
/** §3-4 r6 — mint the chain-degradation marker DURABLY (wx + fsync data + fsync DIRECTORY +
* read-back verification). Idempotent on an existing marker. Throws when the marker cannot be
* proven on disk — the caller (the heal) must then ABORT with the chain untouched: truncating
* first would reopen the exact window this marker closes ("chain reads complete, no marker"
* after an out-of-band tear). */
private mintChainDegradedMarker;
/** backlog #270 — make a just-created control-plane directory ENTRY power-loss durable: open the
* control-plane root and fsync it. Fail-closed on failure: the one caller (the marker mint) must
* abort its heal with the chain byte-untouched — an unproven marker must never authorize a
* truncation, and the abort leaves the SAFER state either way (the standing marker or the still-
* visible tear both re-enter the heal on the next chain read). Platform notes: Node's fsync maps
* to fsync(2) — on macOS a full power-loss guarantee would additionally need F_FULLFSYNC
* (fcntl), which Node cannot reach; this is the same accepted bar as every other fsync in this
* store. On win32 a directory cannot be opened as an fd (fs.open rejects directories there), so
* the entry-durability half is structurally out of reach — an explicit documented arm, not a
* swallowed error; NTFS metadata journaling is the (weaker) cover that platform gets. */
private fsyncControlPlaneDir;
/**
* Read the WHOLE evidence chain in order (validated, collision-checked), healing a TORN TAIL (a
* crash mid-append): the fragment is quarantined for evidence, the sound prefix stands (lines are
* self-contained — damage never spreads backward). Any NON-tail corruption is fail-closed (B3
* family). Two heal forms (§3-4 r5/r6):
* - transaction journal PRESENT beside the tear = the honest crash form — the committed-pending
* protocol re-appends the lost row from the journal, so the healed chain is genuinely complete;
* - journal ABSENT = out-of-band tail damage — the lost row is unrecoverable, so the heal mints
* the durable {@link CHAIN_DEGRADED_FILE} marker BEFORE it truncates (write order is
* load-bearing: isolate fragment → marker wx + read-back → only then truncate; a marker
* failure aborts the heal with the chain byte-untouched).
* Same-`ev` collision law (§4.4, read-time arm): canonical-form-equal duplicates are tolerated
* (append-idempotency residue); DIFFERENT payloads under one ev are fail-closed — an event id is
* one identity, and a reader must never pick a side by file order.
*
* `fenceToken` (#262 ③/#191 review r1) — this "read" MUTATES on a torn tail (quarantine + marker
* + truncation), so a caller whose entry assert is far behind it (the recovery leg's validation
* reads sit after unbounded parsing work) passes its lock token and the heal re-verifies
* ownership immediately before its first mutation: a recoverer stolen from mid-validation must
* not truncate a chain the stealer is appending to. Callers whose entry assert is adjacent keep
* their documented assert→act residue and may omit it.
*/
private readTransferChain;
/** The chain's ev → canonical-payload map (§4.4 upgraded load shape — the append path compares
* PAYLOADS, never mere membership). */
private readTransferEvents;
/**
* v2-b §3-4 — the deleted-id digest behind the resurrection backstop: every `delete` evidence
* row's id (LOCAL AND IMPORTED — an origin filter here would disarm exactly the cross-store
* anti-resurrection evidence a custody import carries; the origin distinction belongs to the
* replay/anchor judgments alone), read WITHOUT healing (a backstop that rewrites the chain it
* consults has destroyed its own evidence; a torn tail answers `"damaged"` with the file
* byte-untouched). Non-tail corruption stays the fail-closed throw. Unknown channels are
* tolerated (open-form law). Must be called with the txn mutex held — the stat fingerprint is
* the cross-process staleness judge, and holding the lock is what makes "fingerprint unchanged
* ⇒ digest current" sound.
*/
private deletedIdsDigestLocked;
/**
* v2-b §3-4 — the CHAIN-LEVEL resurrection backstop's verdict for one NEW id-bearing disk file
* (an id with no committed account row) meeting the adopting read:
* - `"erased"` — the chain carries a delete evidence row for the id: the bytes are a deleted
* entry resurfacing (whatever journal shape produced the "account gone, disk copy back"
* scene), refused + quarantined — restoration is an EXPLICIT write with a new id, never a
* disk replant;
* - `"degraded"` — the chain cannot prove the deleted set (durable degradation marker, or a
* live torn tail): "the id is not on the chain" is no evidence on an incomplete chain, so
* EVERY new id-bearing file is refused adoption (left on disk, not served) until the explicit
* rebuild clears the state — the same epistemic rule a custody import's completeness
* precondition applies;
* - `"clear"` — adoption proceeds normally.
* `applyPatches` adds are NOT consulted here (host authority — an explicit add of a previously
* erased id is a legitimate new commit).
*/
private resurrectionBackstopVerdict;
/** v2-b §3-4 — the LOCK-FREE retrieval face of the deleted-id set (retrievalView / batch-plan
* locateById). Reuses the fingerprint-cached digest: a deleted id's replanted bytes never reach
* the retrieval face either. Best-effort by construction — a lock-free reader carries the same
* cross-process staleness the whole retrieval face does, and a degraded chain filters only its
* known deleted set (it never refuses-all here — retrieval must stay a plane report). */
private retrievalDeletedIds;
/** v2-b §3-4 (round-3 #1) — a PURE, fresh read of the committed ledger rows for the retrieval
* backstop's account check: bytes → parsed rows, installing no read cache and no schema memo — so
* the guard's account view is as fresh as its chain digest,
* never a stale instance cache paired with a fresh deletion read. A v1-form or absent ledger
* answers empty (a v1 store has no delete rows to police anyway). FAULT DIRECTION, stated
* truthfully (rescan corrected the original note, which claimed the opposite): a read/parse
* FAULT also answers empty, and with an empty account the consuming filter WITHHOLDS every id
* on the delete list — conservative on purpose (a host re-add cannot be distinguished from a
* replant over an unreadable account, and serving bytes that carry delete evidence during a
* fault window would reopen exactly the resurrection the backstop exists to close). No erase
* CLAIM is minted (nothing is attested); entries with no delete evidence keep serving. The
* fault is DISCLOSED on the announcement lane (坏值响亮度 — the pre-fix swallow was silent),
* once per fault code per mount. */
private freshCommittedRowsPure;
/** §1.1 commit-point honesty: any failure BEFORE the journal write must refuse the whole
* transaction cleanly — so the append target's writability (and the log's soundness, ev-collision
* law included) is probed here, never discovered after the point of no return. */
private precheckTransfersAppendable;
/** Append evidence events, idempotent by `ev` (the recovery leg re-runs this for a journal whose
* append crashed). Rows land in CANONICAL form (sorted keys) so idempotency and collision checks
* compare one representation across writers and re-serializing middle layers. Three collision
* checks (§4.4): read-time (inside {@link readTransferChain}), batch-time (one batch minting two
* identities for one ev is a minting bug — fail-closed), append-time (an ev already on the chain
* with a DIFFERENT payload is corruption, named; equal = idempotent skip). Behavior narrowed,
* disclosed: the pre-v2b reader silently skipped/overwrote same-ev rows regardless of payload.
* The ONLY failure class here is I/O — never capacity: a size threshold would freeze legitimate
* moves behind a full log, so growth is disclosed, not refused. A failure AFTER the commit point
* is COMMITTED-PENDING (the caller keeps the journal and throws — calling it "refused" would
* invite a retry of a transaction the next recovery completes anyway). */
private appendTransfers;
/**
* @param opts.unlocked RB-447 (2026-07-31, hardening review) — set by the two callers that run
* WITHOUT the txn mutex (the constructor and {@link checkControlPlane}). `applyPatches` serializes
* reload→plan→journal→execute→ledger behind a cross-process lock precisely because two writers over
* one root would otherwise interleave; these two ran the recovery and the staged sweep with no lock
* at all, and both are DESTRUCTIVE. A second process merely CONSTRUCTING a backend on the same
* memory dir — which happens at every task start — while process A sat between STAGE and EXECUTE
* removed A's `.stage-*` tmps, so A's `renameSync(op.staged, op.target)` threw ENOENT and the engine
* converted a healthy transaction into a `sidecar_corrupt` incident. Dropping A's journal has the
* worse shape: if A then crashed, its roll-forward record is gone.
*
* The gate is the txn mutex ITSELF, not a heuristic: an unlocked caller stands down only while
* someone else is holding a LIVE lock (present and not past its stale line), because that is
* precisely the window in which the journal and the `.stage-*` files belong to a transaction in
* flight. With no live holder — a genuine crash, or the ordinary cold start these two callers were
* written for — recovery and the sweep run exactly as before, so the documented "a crash is rolled
* forward/back on the next construction" behavior is unchanged. A live holder's own transaction
* finishes its journal itself; if that holder dies, the lock goes stale and the next construction or
* the next locked `applyPatches` recovers it. Nothing is deferred indefinitely.
*/
private recoverJournal;
/** RB-447 — is another writer mid-transaction right now? True while the txn lock dir exists and is
* not past its stale line (a stale lock is a crashed holder: its artifacts ARE ours to recover).
* Only consulted by the two UNLOCKED callers — inside `applyPatches` the lock is ours by
* construction. Cheap: one `stat` on the lock dir, no lock acquisition, never blocks.
* §3.1 ⓪ (r5-①): "no lock" is ENOENT-only — a stat FAILURE folded into "nobody is committing"
* let a transient EIO run the unlocked recovery (and its staged sweep) over a live transaction. */
private txnInFlight;
/**
* The act gate for store paths ({@link storeTargetEscape}) over `ops`' writes and deletes — shadow
* ops name control-plane files, not store paths. Refuses with `memory.target_outside_store` at the
* first op that would land outside the store, naming the op and where it resolves. Runs over a
* whole batch before its commit marker (no journal is written; a caller that staged files removes
* them) and per op at the act itself; past the marker a refusal keeps the journal, which replays
* once the path resolves inside the store again.
*/
private refuseEscapingTargets;
/** At the head of every face (both recovery legs, the lock-free faces' layout seat, the adoption face):
* the store root still resolves to the directory this backend opened. A root re-pointed since would
* carry every read, adoption, sweep and restore — not only the journal's acts — into another directory. */
private assertRootIntact;
/** Remove orphaned `.stage-*` files (a crash BEFORE the journal rename = clean rollback: nothing
* was committed, the staged tmps are litter). Dotfiles are invisible to entry scans regardless. */
private sweepStagedFiles;
private scopeDir;
/** B2/C-F9 — contain one inbound-gate REJECT, shared by the adopting read (`readScope` sync) and
* the adoption-restricted committed view (`readScopeCommitted`) so the two doors can never drift:
* report the finding, capture the SCANNED bytes into control-plane quarantine (never a path
* re-read — TOCTOU), restore the committed shadow over the suspect where one exists (the write
* lands by `rename`, which REPLACES a link at the final component instead of traversing it —
* pinned by test), and escalate any deviation as a report-visible `quarantine_failed`. Returns
* the shadow text that now backs the entry (undefined ⇔ never committed / shadow-less legacy —
* each caller decides what leaving the set means on its channel). */
private containInboundReject;
/** B2 inbound gate for content that did NOT come through applyPatches (git pull / hand edits /
* a hand-seeded file): secret + cap + structure. Returns the rejection, or undefined (clean). */
private inboundGate;
/** Read every COMMITTED entry of `scope` — a file must carry a valid `id:` to count. An id-less
* file is UNCOMMITTED session material: it becomes an entry only through the engine's harvest
* gates (which mint the id on commit). Reading it as an entry here would let gate-rejected files
* (secret / cap / quarantine) leak into the authoritative set through the zero-copy disk scan.
* `sync` (read-side callers) adopts a committed file's CURRENT disk rev into the committed-rev
* ledger ONLY through the inbound gate (B2: out-of-session edits, e.g. git-pull, are scanned;
* a reject is moved to control-plane quarantine and the committed shadow content is restored to
* disk). The applyPatches lookup passes false (the ledger must keep the baseline for CAS).
*
* Ledger-write discipline: the sync=true adoption path WRITES `revs.json`, and that write is
* wholesale (atomic replace) — unserialized against applyPatches' transaction it is a
* lost update (the later writer drops every row the earlier one added). sync=true is therefore
* reachable ONLY through {@link readScopeAdopting}: `probeOnly` runs the scan with NO ledger
* side effects and returns undefined at the FIRST entry that needs an ACCOUNT transaction (an
* edit adoption, or a move candidate — the caller re-runs the scan under the txn mutex against
* a freshly reloaded ledger); a completed probe pass returns the entries. A COPY or an
* undecidable binding probe needs no transaction (withheld + announced, account untouched), so
* the probe classifies those inline and keeps going — a store with a lingering copy never
* contends for the writer lock on every read. */
private readScope;
/** #1076b — the observation-table door of the adopting read: bytes a session recorded appearing
* while it held this layer read-only are never adopted. Gate-failing bytes are not taken here —
* the inbound gate's containment keeps its door. A hit is announced, the committed copy stands in
* when this path is the entry's bound projection, and the disk is left as found (the account it
* is judged against is current — the scan sees to that). The notice is deduplicated against what
* is still queued, so every session that meets the file is told once. Returns whether the file
* was settled (the caller moves on). */
private settledAsObserved;
/**
* design-A RB-5 ([A3]) — the read-side whitewash gate, shared by the bound-here divergence lane
* and the move+edit lane so the two can never drift: an out-of-session disk change (git pull /
* hand edit) of a repo-ingested entry that strips `provenance`, or strips a present `trust`
* marker, is the same whitewash applyPatches' I-2 clause refuses — adoption must not be the
* laundering back door. Baseline = the COMMITTED state (control-plane shadow; the disk file is
* the very thing under suspicion). A shadow-less legacy entry has no independent baseline, so the
* gate cannot fire for it — but every adoption/commit backfills the shadow, closing that window
* on first contact. `containment` keeps the reason honest per lane (the move lane quarantines
* WITHOUT restoring at the never-bound path).
*/
private whitewashInboundFinding;
/**
* §2.2/§2.3 — one MOVE candidate under the txn mutex: a committed entry's bytes at non-bound
* coordinates whose bound projection is CONFIRMED absent (ENOENT, probed under this same lock).
* Adoption needs all three conditions — candidate present + bound path ENOENT + store-wide
* uniqueness through a census that proves its own completeness: with B and C both planted,
* single-condition ENOENT would let READ ORDER pick the owner; with a half census, "only B was
* visible" is not "only B exists" (r4-③/r5-②). Gate-clean adoption is an EXPLICIT transaction:
* row rebind + `prev` trace in one atomic ledger write, an `adopted-move` evidence event in the
* same journaled commit, and a loud announcement — external move/rename is the writeScope-null
* plane's PRIMARY legitimate input channel (git pull), so it is upgraded to a traced transaction,
* never refused wholesale. Candid boundary (D-1, ruled): a byte-identical delete-then-plant is
* THIS SAME disk observation — the adoption is a TRACED acceptance, not a rejection; what v2
* removes is the delete's invisibility (missing now rings), the relocation's tracelessness
* (transfers row + prev), and the silence. Out-of-band writers do not take this lock, so a
* TOCTOU window between the ENOENT re-verification and the commit remains for an adversary that
* can recreate the original mid-window — that actor is the delete-then-plant actor (same candid
* boundary), not a new capability; the tri-state probe closes FAULT misjudgment, and claims no
* more. Returns the entry to serve, or undefined (refused/deferred this pass).
*/
private adoptMoveCandidateLocked;
/**
* The adopting read: {@link readScope} sync=true with its ledger write serialized behind the
* SAME cross-process txn mutex applyPatches holds (acquisition/steal/timeout semantics shared,
* never duplicated). Without it, an adoption landing beside a sibling process's commit is a
* lost update: both read the ledger, both write it wholesale, the later atomic write drops the
* earlier writer's rows (the entry file stays on disk but the ledger no longer knows it).
* - PROBE first (no lock): disk agreeing with the ledger is the common case, and that pass writes
* no LEDGER row — the read stays lock-free and never contends with a writer over the ledger.
* (Precision, merged-code scan: the non-divergent branch may still BACKFILL a missing shadow
* file for a committed entry, an unserialized control-plane write with a narrow pre-existing
* last-writer window against a sibling's commit — see the shadow-backfill site below; that
* window predates this probe and is the same one the v2 single-lock design closes.)
* - Divergence ⇒ acquire the mutex, finish any interrupted transaction (the steal-safety
* contract: a stolen stale lock is safe BECAUSE recovery runs inside the lock), RELOAD the
* ledger from disk (the probe's cached view is exactly the stale baseline that loses
* updates), then re-run the scan with adoption and save inside the lock. The save re-verifies
* lock OWNERSHIP first (a holder stalled past the stale deadline was stolen from — its
* snapshot is behind the stealer's commit and must abort, exactly like the applyPatches
* journal write).
* - A lock that cannot be acquired throws (same loud arm as the write path): an adoption that
* cannot be serialized must surface, never silently skip the gate or serve stale state as
* adopted.
* No self-deadlock: nothing under the lock re-enters acquireTxnLock — the scan's quarantine /
* shadow / announcement writes use their own per-file sidecar locks (whose holders never take
* the txn mutex), and applyPatchesLocked only issues sync=false reads. The two UNLOCKED
* recovery observers (constructor / checkControlPlane) are unaffected: they already stand down
* while a live holder exists ({@link txnInFlight}), which now covers adoption holds too.
*/
private readScopeAdopting;
/**
* design/376 slice ③ (#437③) — the File leg's scope-enumeration capability face: every scope
* this store has hosted, derived from the store root's durable scope registry (the control
* plane's `scopes.json` — the one source that can invert a scope's sanitized directory label
* back to its name; a listing of raw subdirectories could not). Side-effect-free committed
* read; a corrupt/unreadable registry THROWS its fail-closed refusal — enumeration over an
* untrustworthy registry must never answer an empty (= clean-looking) list. Honest residual,
* disclosed: the registry records scopes as they are first touched THROUGH this control plane,
* so a hand-assembled tree's never-touched scope dirs appear here only once a caller first
* names them — store-level fact, session-roster-free either way.
*/
listScopes(): MemoryScopeEnumeration;
listHeaders(scopes: readonly string[]): Promise<MemoryEntryHeader[]>;
private listHeadersFrom;
getByIds(ids: readonly string[]): Promise<MemoryEntry[]>;
private getByIdsFrom;
/**
* design/178 §3 (ruled 2026-08-08) — a projection of this backend for the session's RETRIEVAL
* face (`memory_search` / `memory_get`): NO ADOPTION, ever.
*
* The ordinary read path carries the inbound-adoption channel: a disk revision that disagrees with
* the ledger is judged an out-of-session change, gated, and — when it passes — adopted as the new
* COMMITTED baseline (ledger + shadow rewritten). That is a deliberate lifecycle synchronization
* point, and it belongs to the engine's own moments (materialize / harvest), not to every model
* lookup: a retrieval landing between them would otherwise COMMIT the session's own in-flight edits
* with no harvest gate and no pollution discipline in the way.
*
* Not running the inbound gate on this path is the point, not a concession: the view reports the
* plane, and admitting anything stays the harvest's job. Stated exactly (v2-b trued this doc up —
* the pre-v2b sentence "resolves entries from disk without touching the ledger" is no longer the
* whole truth):
* · the v2-b RESURRECTION GUARD consults the delete-evidence digest (`transfers.jsonl` stat +
* parse) and — when any delete row exists — a PURE fresh read of the committed ledger, and it
* WITHHOLDS an on-plane file whose id carries a delete row with no live account row (an erased
* id's replanted bytes are not served through any read face; a host re-add serves). So the
* view is "the plane minus proven-erased ids", and it can throw {@link ControlPlaneCorruptError}
* from a fail-closed corrupt evidence log — states a pre-v2b view could not reach;
* · the reads do no healing and no quarantine, and the COMMITTED-ROWS read is cache-free; the
* digest read installs the instance fingerprint cache (a read cache, not a store write). The
* path's write-capable hooks (rescan r3 — do not present this list as shorter): `readScope`'s
* pre-existing opportunistic v1→v2 migration retry (v1-form/unbound-row stores only, design/186
* §3.1), the guard's fault-disclosure enqueue (a ledger read/parse fault mints an announcement
* row, once per fault code per mount), and `scopeDir`'s registry mint (listing a scope name the
* registry has never seen writes its row — predates this view and rides every scope read).
*
* WRITES ARE REFUSED, loudly — a view handed to a write path is a defect, not a fallback.
*/
retrievalView(): MemoryBackend;
/** #1076b — the lock-free retrieval face serves no recorded divergence either (side-effect-free, so
* no notice): the committed copy stands in at the entry's bound projection, anything else is
* withheld. The table is read per call; the account only when it names one of these ids. */
private withoutObserved;
/** The committed rows read afresh from disk (no cache install), or undefined when the account
* cannot be read — a fault, never folded into "no rows". */
private committedRowsOrFault;
/** Dedup keys for divergence findings the restricted view already recorded: one materialize reads
* a scope several times (listHeaders, then getByIds per scope), and a checkpoint-cadence harvest
* re-audits the same still-pending state — without the key, one untouched divergence would mint
* a finding per read. Per AUDITED MOUNT (design/186 §6): the keys live and die with the mount,
* same lifecycle as the id-less scope/baseline pair — one mount discloses a still-pending
* divergence once, and the NEXT audited mount (a later restricted session in this process, or a
* cross-process resume) re-discloses once, which is the desired "still pending" notice; the
* in-process and cross-process postures agree. A divergence whose bytes CHANGE mints a new key
* and is reported again within the mount. */
private restrictedFindingKeys;
private recordRestrictedFinding;
/** Id-less audit state for an adoption-restricted session's WRITE scope. Armed only by the
* audited mount, which carries the caller's explicit session verdict together with the session's
* write scope — never inferred from plane structure (a writeScope-null layering has no write
* scope and is never given this surface: id-less files there are ordinary working files or
* external drops). The baseline is the set of id-less rel paths already under the scope at the
* first audited read after the mount (= materialize time): a pre-existing id-less file cannot be
* attributed to this session and stays unflagged, while a session under the restriction verdict
* has no sanctioned write channel into the store — so an id-less file that APPEARS after the
* baseline arrived out of band by construction and is worth naming. Per-process like the finding
* keys; when restricted sessions interleave on one instance the later mount re-baselines, which
* degrades toward silence, never toward a false alarm. */
private restrictedIdlessScope?;
private restrictedIdlessBaseline?;
/**
* The ADOPTION-RESTRICTED read of one scope: serve the COMMITTED account (ledger + control-plane
* shadow) and never advance it. This is the tamper boundary for a session with no write intent:
* the ordinary `readScope(sync=true)` ADOPTS disk divergence as the new committed baseline
* regardless of session intent, so a byte landed by a side channel (e.g. a shell write under a
* session whose persistence is declared unavailable, or into a read-only layering) would be
* committed by the very session that was told it cannot persist — its own checkpoint-resume
* materialize included. Here instead:
* - disk rev == committed rev ⇒ served as-is (it IS the committed content);
* - divergence whose bytes equal the committed SHADOW ⇒ served as-is: the shadow is written at
* the applyPatches commit point, so matching it is transactional proof that a concurrent
* writable session committed this state and only this call's ledger snapshot raced it —
* never treated as tampering;
* - other divergence of a shadow-backed entry ⇒ the SHADOW (committed content) is served; the
* divergent disk bytes stay in place UNLESS they fail the inbound gate (the existing
* quarantine door, unchanged under restriction). Scan-clean divergence is never quarantined,
* overwritten, or adopted — it stays on disk as deferred external state a later unrestricted
* session adjudicates normally;
* - a divergent entry with NO shadow (shadow-less legacy), and a never-committed id-bearing
* file, have no committed content to serve ⇒ WITHHELD with a loud finding, never served from
* disk, never adopted.
* The ledger is reloaded from disk on every call (a stale cache would misread a concurrent commit
* as divergence) and NEVER written here; the only writes this path may perform are the
* gate-failing containment above and a shadow backfill for a rev-matching entry (recording
* committed content — the account itself does not move).
*
* `audit` picks the mount: true = the engine's materialize/harvest boundary (findings recorded,
* gate-failing divergence contained); false = the retrieval-tool face (a PURE committed read —
* no findings, no containment, no writes: a model lookup must be side-effect-free).
*/
private readScopeCommitted;
/** Does `id` have a projection file anywhere in the store OTHER than `excludeAbsPath`? Returns
* that projection's rel path, or undefined. Early-exits on the first hit; only consulted on the
* rare divergent-with-shadow path (the scope-binding guard above) for UNBOUND rows — bound rows
* answer the same question in O(1) from the account binding (design/186 §2.4). RETIREMENT
* CONDITION: unbound rows are a migration transitional; when they are gone this scan form has no
* caller left and retires WITH them (delete the function, not just the call — a dead scan arm is
* the residue class the retirement registry exists for). */
private idProjectionElsewhere;
/**
* The ADOPTION-RESTRICTED projection of this backend (see {@link readScopeCommitted}) — what a
* session with no write intent materializes and retrieves through. The CALLER decides restriction
* (an explicit verdict: a read-only layering, or persistence declared unavailable) and mounts this
* view; the backend never guesses a session's intent from its own state.
* - `audit: true` (the engine's materialize/harvest mount): divergence findings are recorded for
* the harvest report + announcement queue, and gate-FAILING divergence is contained by the
* existing quarantine door; `writeScope` (when the session has one) additionally arms the
* id-less baseline for that scope — see {@link auditRestrictedDivergence};
* - `audit: false` (the retrieval-tool mount): a pure committed-view read — no findings, no
* containment, no writes, no baseline.
* WRITES ARE REFUSED loudly — same posture as {@link retrievalView}.
*/
restrictedAdoptionView(opts?: {
audit?: boolean;
writeScope?: string | null;
}): MemoryBackend;
/**
* The harvest-boundary divergence sweep for an adoption-restricted session (the engine's
* zero-admission harvest arm calls this BEFORE draining findings): re-run the committed-view audit
* over the session's scopes — bytes landed AFTER materialize (a mid-session shell write) are
* detected here, at the very boundary where an unrestricted harvest would have admitted them —
* then check the committed account for entries MISSING from disk entirely (an unattributed delete
* is divergence too). BOUND rows (design/186 §2.2) are judged AT THEIR COMMITTED PROJECTION: a
* single-copy move / delete-then-plant empties the bound address and rings here even though the
* id is still "somewhere" (the v1 any-scope presence test read exactly that as fine — the
* registered residual this schema retires); byte-carriers of the id at other addresses are named
* in the same finding. UNBOUND rows (migration transitional) keep the v1 store-wide presence
* test — no false alarms while their address is underivable. Non-destructive throughout: nothing
* is restored, forgotten, or adopted here.
*
* `opts.idlessWriteScope` is the CALLER's per-call session verdict (same posture as the mount:
* the backend never guesses): when the audited session HAS a write scope, id-less files under it
* that were not in the mount-time baseline are named too — the session has no sanctioned write
* channel into the store, so a new arrival is out of band by construction. Left out (a
* writeScope-null layering, or a caller that never mounted the audit), the sweep is exactly the
* id-bearing audit above; a scope mismatch with the armed mount likewise degrades to silence
* rather than judging against a foreign baseline.
*/
auditRestrictedDivergence(scopes: readonly string[], opts?: {
idlessWriteScope?: string;
}): void;
/** Lexical floor (design/138 §2.1): `1 - Jaccard` distance over name+description+body, ascending. */
search(query: string, scopes: readonly string[], opts?: {
limit?: number;
exposureBands?: boolean;
}): Promise<ScoredMemoryEntry[]>;
private searchFrom;
/**
* H4 — the entry transaction, staged and journaled:
* 1. RELOAD the ledger from disk (验收 M2: a concurrently-committed transaction from another
* process becomes a CAS conflict here, never a blind write over it) + finish any interrupted
* transaction (redo journal).
* 2. PLAN every patch against current state (CAS/conflicts decided now; no disk writes yet).
* 3. STAGE: write each new file content to a same-directory `.stage-*` tmp (fsync'd).
* 4. JOURNAL: atomically write the full redo log (ops with content + the post-txn ledger snapshot).
* ← the commit point: a crash after this rolls FORWARD (recovery redoes the journal); a crash
* before it rolls BACK (staged tmps are swept, nothing was committed).
* 5. EXECUTE: atomic renames / deletes / shadow updates.
* 6. Persist the ledger (atomic, fail-closed) and drop the journal.
*/
applyPatches(patches: readonly NotePatch[]): Promise<PatchReport>;
private txnLockDir;
/**
* 证伪式复审 L1/L2/L3 — the mkdir mutex, hardened (same shape as layout.ts' acquireSidecarLock, but
* fail-closed timings and an async wait):
* - L2: every acquisition writes an OWNER TOKEN (`<lock>/owner`, crypto-random — never a
* timestamp) immediately after mkdir. Release only removes the lock while the token is still
* ours; {@link assertTxnLockOwnership} re-verifies it at the commit point.
* - L1: the stale steal is SINGLE-WINNER — atomic rename of the stale lock dir to a unique
* tombstone (exactly one racer's rename succeeds; losers ENOENT back into the wait loop), then
* the winner removes the tombstone and re-acquires through a NORMAL mkdir. Never a bare
* rmSync+mkdir (two same-window stealers would force-rm each other's FRESH lock = double hold).
* - L2 (steal side): only a lock that is BOTH past its stale line AND carries an owner token is
* stealable (a tokenless dir is a mid-acquisition, not a crashed holder).
* - L3: the wait deadline is `max(waitMs, observed lock's stale line + grace)` — a holder that
* crashed with `remaining stale > waitMs` no longer strands every waiter in an all-throw window.
*/
private acquireTxnLock;
/** 证伪式复审 L2 — 提交点归属复验: called immediately before the JOURNAL write (the persistent commit
* point). A holder that stalled past the stale deadline (slow disk / GC pause / long txn) and was
* stolen from must ABORT — its staged tmps carry no committed state and the stealer's journal
* recovery is now authoritative; committing anyway would overwrite the stealer's transaction. */
private assertTxnLockOwnership;
private applyPatchesLocked;
/** E-3 ② — the journal CLOSE guard: re-read the fixed-path journal and remove it only while its
* `.txn` names THIS transaction. A holder that stalled past the stale line and was stolen from
* may reach its close after the stealer already wrote a NEW journal at the same path — a bare rm
* there deletes the stealer's LIVE redo record (its crash then loses a committed transaction).
* An absent/foreign/unparseable journal is simply not ours to remove: leave it (the stealer, or
* the recovery leg, owns its fate). The remaining read→rm window is the same assert→act residue
* the commit-point assertion accepts, stated there. */
private closeJournalIfOurs;
/** Plan one patch: decide CAS/conflicts against CURRENT state, emit journal ops (no entry-file
* writes here — staging/execution happen in {@link applyPatches}). Every row write is BOUND
* (§1.1) at the coordinates the batch ACTUALLY lands — a slug collision's `-n` suffix binds the
* suffixed slug, the same one `applied[].slug` reports (v1's ledger had no landing awareness). */
private planOne;
private locateById;
/**
* design/178 v2-b §7 — request-driven erasure with evidence (capability face; probe with
* `typeof backend.eraseWithEvidence === "function"`). Rides the SAME txn mutex, the SAME journal
* shape, and the SAME recovery leg as `applyPatches` — never a second transaction protocol.
* Everything runs INSIDE the lock (r1: resolving a selector outside it pins a set the store can
* move under before the lock lands): recover → migrate → chain replay-judgment → resolution (a
* scope selector consults the census for membership — the account alone cannot see an
* unledgered-live inhabitant of the scope) → census → plan → journal (ops + ledger snapshot +
* evidence rows in ONE commit) → EXECUTE → ledger → evidence append → guarded journal close →
* lineage capture-and-clear.
*
* Replay protocol (§4.6): the first execution pins the resolved id set on an ANCHOR row; the same
* requestId re-executes against THAT set (never re-resolves), deletes the un-deleted remainder,
* and answers the cumulative view. A same-requestId call with a DIFFERENT selector is refused
* (`memory.erasure_selector_mismatch`); two same-req anchors on the chain are a spliced chain —
* refused for a human, never adjudicated by file order.
*
* This lane deletes by the ACCOUNT (ledger row), not by a physical locate: shadow-only rows
* (projection deleted out of band), carrier-missing rows, and unbound rows (P=0 / P≥2) are all
* erasable — the census sweep removes EVERY projection carrying the id (erasure needs no
* adjudication of which copy was real: removing all of them IS the requested outcome). A ledger
* key failing the entry-id contract takes the row-only arm (`legacyId` evidence; no ops — a
* shadow op on it would fail this store's own journal validator). The NotePatch delete lane's
* semantics are untouched (a shadow-only row still answers unknown-id there — pinned).
*
* Failure postures: an unreadable census refuses the WHOLE request pre-commit
* (`memory.erasure_census_incomplete` — "deleted clean" must not be attested over a store that
* cannot be fully read); per-id planning faults are non-fatal `conflicts` rows (a replay
* converges them); everything before the journal write refuses cleanly (F-1); every crash window
* after it converges through the shared recovery leg (F-2..F-5, F-13).
*/
eraseWithEvidence(input: EraseMemoryEntriesInput): Promise<MemoryErasureAttestation>;
/** Bounded journal-quiescence fence (attempt count × retry pause). Constants, not knobs: the
* contended window is the commit tail of one transaction (milliseconds); a store where it stays
* contended for the full bound has a crashed/wedged writer, and the honest answer is refusal,
* not a longer wait. */
private static readonly AUDIT_FENCE_ATTEMPTS;
private static readonly AUDIT_FENCE_RETRY_MS;
/** Is a transaction journal present right now? ENOENT-only absence (§3.1 ⓪ — a probe FAILURE is
* not "no journal"; it throws the fail-closed corrupt error, never folds into quiescence). */
private auditJournalPending;
/** PURE ledger read for the audit faces: raw bytes → parsed rows, touching NOTHING on the
* instance (no cache install, no schema-version memo, no v1-compat announcement) and never the
* disk. A v1-form ledger is refused here — the migration belongs to the store's own entries
* (construction / adopting reads / locked mutations), never to an audit read. */
private auditReadLedger;
/** Run `read` over a STABLE committed account: journal absent before and after, ledger bytes
* identical across the read (closes both tear channels — the journal-first commit window and any
* ledger-advancing path, the journal-free no-move adoption save included), retried up to the
* bound. `read` may throw {@link AuditObservationUnstableError} on a mid-transition scene — the
* loop re-observes, and the FINAL attempt receives `final: true` to answer the steady-state
* reading instead. Throws {@link AuditSnapshotContendedError} when the bound exhausts.
*
* CONSISTENCY CLAIM, stated precisely (adversarial-review wording pass): the answer is a
* CONSISTENT committed account as observed at the fenced read — never a torn mixture of two
* transactions' halves. FRESHNESS-AT-RETURN is deliberately not claimed: a lock-free reader
* cannot exclude a writer whose entire commit lands in the sub-window between the last two
* probes, and such an answer is simply the immediately-preceding committed state (the same
* answer the call would have produced had it returned a moment earlier). Holding the store's
* txn mutex would buy freshness at the cost of the audit read acquiring writer machinery (a
* write, and a contention seat) — refused: an audit must not perturb the store it audits. */
private auditStable;
/** The audit faces' committed-CONTENT resolution for one row (see {@link CommittedEntrySnapshot}
* for the state law). Reads only; the shadow read is fail-closed (a control-plane read FAILURE
* is not absence), the projection probe is the shared side-effect-free tri-state probe. */
private resolveCommittedContent;
/**
* design/178 v2-a §1.3 — the per-id AUDIT SNAPSHOT (capability face; probe with
* `typeof backend.committedSnapshotOf === "function"`). Pure committed read under the double
* fence; unknown id ⇒ `{ state: "absent" }` (never a throw, never an empty guess); unbound row ⇒
* the account half answers with `binding: { state: "unbound" }` (legal transitional state);
* v1-compat store ⇒ loud refusal (migrate first); fence contention past the bound ⇒ loud refusal.
*/
committedSnapshotOf(id: string): Promise<CommittedEntrySnapshot>;
/** One row's public {@link CommittedBinding} (shared tagged form — see the type's own doc). */
private publicBinding;
/**
* design/178 v2-a §1.3 — the LEDGER-DRIVEN scope enumeration (capability face). The account is
* the enumeration authority: every bound row whose scope is requested is answered — including
* shadow-only rows a disk scan would miss (`listHeaders` is an ADOPTING read and enumerates the
* plane; it is not an audit face). Unbound rows land in the store-level `unbound` appendix.
* Fence contention past the bound ⇒ `{ complete: false }` (NO data members — nothing to misread
* as a truncated package): the caller fails closed. Corrupt control-plane bytes and the
* v1-compat refusal still THROW (they are definite states, not contention).
*/
committedSnapshotsOfScopes(scopes: readonly string[]): Promise<CommittedScopeSnapshots>;
/**
* design/178 v2-a §1.3 — the per-id CUSTODY read (capability face): every evidence row of
* `transfers.jsonl` naming this id, in file (= time) order, as OPEN {@link TransferEvidence}.
* PURE INSPECTION, never repair: a torn tail (crash mid-append) answers the sound prefix with
* `state: "damaged"` and the file is NOT touched — quarantining the fragment is the recovery
* leg's duty (the adopting read path), and an audit that rewrites its subject has destroyed the
* evidence it was asked about. Non-tail corruption stays the fail-closed throw. Unknown channels
* are TOLERATED and passed through (open form: a chain written by a newer engine still answers);
* known channels are validated by the store's own rule set. Runs under the same journal fence as
* the snapshot faces — a pending journal may hold committed-but-unappended evidence, and a chain
* read across it would be completed differently by the very next recovery.
*/
custodyOf(id: string): Promise<EntryCustodyReport>;
/** The pure open-form evidence read behind {@link custodyOf} (no healing, no writes). `raw` is
* the caller's already-fenced read of the log (undefined = absent). */
private custodyReadPure;
/** design/178 v2-c §4 — the export/import refusal code (engine coded-error family, deliberately
* outside the governance registry: a store-state refusal, not a per-principal verdict). */
private static readonly EXPORT_INCOMPLETE;
/** The durable store identity (§6.2): read it, or mint it (`wx`) on first need — the txn mutex is
* held by every caller, so the exclusive create cannot race in-process; a cross-process EEXIST
* loser adopts the winner's identity. Corrupt/unreadable identity is fail-closed. */
private storeIdentityLocked;
/** One consistency fingerprint over the three ENGINE governance faces (challenge ledger, lineage
* ledger, pollution markers) — raw bytes, journal included. The two strict sidecars' JOURNAL
* reads are wrapped FAIL-CLOSED here (§4-6): the shared strict reader folds a journal read
* error into absence, and an export that read a stale main file over an unreadable journal
* would seal stale governance under an honest hash. Marker files that exist but cannot be read
* fingerprint their error code (a stable fault stays stable; a flapping one drifts the fence). */
private exportGovernanceFingerprint;
/**
* The export chain read: PURE INSPECTION (never heals, never quarantines, never truncates — C-8
* pins the file byte-identical across a refused export). A torn tail refuses the export
* (`memory.export_incomplete`; the deployment heals it by running any locked mutation entry,
* then re-exports); an unknown channel refuses too (T-14: a row whose slicing rules this build
* does not know can neither be carried — possible cross-tenant leak — nor silently dropped —
* lost mandatory governance); one ev under two different payloads refuses (spliced chain);
* non-tail corruption stays the fail-closed {@link ControlPlaneCorruptError}. Byte-equal
* duplicates collapse (append-idempotency residue).
*/
private readExportChainRowsPure;
/**
* v2-c §2 — the tenant-slicing table over validated chain rows (T-6..T-11; T-9/T-10 refusals,
* T-14 already refused by the reader). `storeId` is a thunk so the identity mints only when a
* redaction actually needs the provenance stamp.
*/
private sliceCustodyForScopes;
/** T-12 — quarantine residual enumeration for an id set (names for attributable hits, an opaque
* count for the rest — same honest bound the erasure attestation uses). */
private enumerateQuarantineFor;
/**
* design/178 v2-c §3 — the ONE-LOCK export composite (capability face; probe with
* `typeof backend.exportSnapshotOf === "function"`). All five reads — entries (ledger-driven,
* committed content), custody chain, challenge ledger, lineage ledger, pollution markers — happen
* inside a single txn-mutex hold: the two backend faces are writer-serialized by the mutex
* itself; the three engine faces (written under their own sidecar locks, not the mutex) are
* fenced by a byte-level double fingerprint around the reads (≤3 rounds, then a loud refusal —
* a store too hot to snapshot answers `memory.export_incomplete`, never a mixed-epoch bundle).
* The terminal ownership assertion turns a stolen lock (stale-steal past the 30s line during a
* long assembly) into a refusal: an unstolen lock means no writer entered, which IS the
* consistency proof — no lease machinery needed. The chain read is pure inspection (a torn tail
* refuses with the file byte-untouched); `complete:false`-class scenes all refuse loudly — there
* is no degraded bundle shape, by design (§4).
*/
exportSnapshotOf(scopes: readonly string[], timings?: TxnLockTimings): Promise<MemoryExportSnapshot>;
/**
* design/178 v2 §4.3 — the single-item governance slice face (parent-design form, kept for
* NON-export consumers: erasure resolution, per-id inspection tooling). Answers
* `complete: false` + reason instead of a bundle-shaped refusal (its callers fail closed on the
* flag); definite corruption still throws. The export path deliberately does NOT use this face —
* two independently-locked single faces are exactly the cross-face tear the composite closes.
*/
governanceExport(scopes: readonly string[], timings?: TxnLockTimings): Promise<{
custody: TransferEvidence[];
unbound: string[];
complete: boolean;
unsliceableCustody: number;
reason?: string;
}>;
/**
* design/178 v2-c §5.4 — custody carriage onto THIS chain (capability face). Rows land under the
* import namespace — `ev = imp:<bundleHash>:<source ev>` (full-hash salt: deterministic per
* bundle, chain-deterministic across hops — a re-exported imported row salts its CURRENT ev
* next hop), `origin` kept when present (multi-hop provenance) else stamped with the source
* store's identity, `srcEv` kept else stamped with the source ev — so an imported row is ALWAYS
* origin-bearing and the local replay/anchor judgments structurally never read it. Unknown
* channels and rows failing this store's own validation are WITHHELD and reported (never
* appended verbatim — an unvalidatable row would poison the fail-closed chain — and never a
* whole-package refusal — a newer exporter must not brick an older importer). Appends are
* idempotent by ev with canonical-payload comparison (same-bundle re-import converges; a
* different payload under an existing ev is fail-closed corruption). Direct chain append, no
* journal: custody carriage is not a ledger mutation, and origin-bearing rows are forbidden in
* journals by the recovery leg's own validation.
*/
custodyImport(rows: ReadonlyArray<Record<string, unknown>>, opts: {
bundleHash: string;
sourceStoreId: string;
}, timings?: TxnLockTimings): Promise<{
appended: number;
withheld: Array<{
srcEv: string;
channel: string;
}>;
}>;
/** The in-lock custody carriage body ({@link custodyImport}'s and the import composite's shared
* leg — the composite already holds the mutex and must not re-enter the acquisition). */
private custodyImportLocked;
/** Append import-namespace challenge events and VERIFY every key-idempotent replay by reading
* the full payload back (§5.2 defense ③): `appendChallengeEvents` is key-idempotent, not
* payload-idempotent — a pre-planted same-key event (a resolve, or a same-entry different-
* reason challenge) would silently swallow the imported challenge while the return echoes a
* plausible assignment. A mismatch ABORTS the import (the latch stays; the account is not
* silently short). */
private appendImportChallengesVerified;
/**
* design/178 v2-c §1 — the IMPORT COMPOSITE (capability face; the engine's `importMemoryBundle`
* is its validating membrane). One txn-mutex hold end to end:
* ①b destination-chain completeness precheck (torn/degraded ⇒ whole package refused with ZERO
* side effects — before the latch, "reject before landing" made literal);
* ② the synthetic latch (durable pending lineage txn over every bundle entry id — from here
* to release, those ids are withheld on every model-visible read face; crash-safe);
* ③ authoritative in-lock judgment (the five import rules — a pre-lock classification would
* only be advisory, so there is none: the lock-held pass IS the classification);
* ④ entry landing through {@link applyPatchesLocked} (same skeleton, same lock, same recovery
* leg — never a second transaction protocol; `guard:"absent"` adjudicates the residual
* same-id arms);
* ⑤ governance legs, custody FIRST unconditionally (chain facts are exempt from the
* entry-eligibility rule — an absent-id delete row is exactly the anti-resurrection
* payload), then entry-attached lineage/challenges for landed ∪ already-present ids only,
* then pollution (retroactive sweep DURABLE BEFORE the marker — the crash between them
* leaves contributions challenged and the marker converging on re-import, never the
* reverse);
* ⑥ the durable completion receipt (`wx`, carrying the report) and the latch release —
* receipt-gated, the ONLY release channel.
* Any failure past ② leaves the latch standing: re-importing the SAME bundle converges every
* crash window (idempotent latch, idempotent judgments, ev/key-idempotent governance legs,
* receipt short-circuit).
*/
importBundleCommit(plan: MemoryBundleImportPlan, timings?: TxnLockTimings): Promise<MemoryImportReport>;
getConsolidationCursor(scope: string): Promise<string | undefined>;
setConsolidationCursor(scope: string, cursor: string): Promise<void>;
private readCursors;
}
export { scopeDirName };