@sema-agent/core 5.36.0 → 5.37.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.
@@ -23,6 +23,271 @@ export declare function scanEntryFiles(dir: string, opts?: {
23
23
  /** Harvest gate hook: called for every path the scan SKIPS, with why (backend scans ignore it). */
24
24
  onSkip?: (path: string, kind: "symlink" | "depth" | "nonmd" | "dotfile" | "unreadable") => void;
25
25
  }): ScannedEntryFile[];
26
+ /** design/178 v2-b §3.3(b) — the explicit erasure selector (each form resolves to an explicit id
27
+ * list before anything is deleted; there is no whole-store selector). */
28
+ export type ErasureSelect = {
29
+ ids: readonly string[];
30
+ } | {
31
+ scope: string;
32
+ } | {
33
+ sessionId: string;
34
+ };
35
+ /** design/178 v2-b §3.3(c) — one erased row's coordinates (the snapshot's tagged binding law,
36
+ * without the account-trace extras: a wire validator refuses hybrid objects). */
37
+ export type ErasedBinding = {
38
+ state: "bound";
39
+ scope: string;
40
+ slug: string;
41
+ } | {
42
+ state: "unbound";
43
+ };
44
+ /** The input of the erasure lane (engine host API `eraseMemoryEntries` and the File capability face
45
+ * `eraseWithEvidence` share it; the File face ignores `allowUnevidenced` — with the evidence
46
+ * capability present there is nothing to degrade to). */
47
+ export interface EraseMemoryEntriesInput {
48
+ /** Idempotency identity, caller-minted (challenge-family law: the engine never mints one). */
49
+ requestId: string;
50
+ select: ErasureSelect;
51
+ /** Evidence-capability-ABSENT degradation opt-in (engine lane only; default = loud refusal). */
52
+ allowUnevidenced?: boolean;
53
+ }
54
+ /**
55
+ * design/178 v2-b §3.3(c) — the erasure attestation (the deliverable a compliance caller archives).
56
+ * Every claim is black-box re-checkable: an `erased` row is verified by `committedSnapshotOf(id)`
57
+ * answering `"absent"`, the chain containing its `evidenceEv` delete row, and `getByIds([id])`
58
+ * answering empty.
59
+ */
60
+ export interface MemoryErasureAttestation {
61
+ /** Version envelope — a consumer reading `v > 1` must refuse, never reinterpret. */
62
+ v: 1;
63
+ requestId: string;
64
+ at: number;
65
+ /** `"complete"` ⇔ conflicts empty ∧ every pinned id ∈ erased ∪ erasedPreviously ∪ (notFound with
66
+ * `custodyState === "complete"`). Everything else is `"partial"` (replay converges it). */
67
+ status: "complete" | "partial";
68
+ /** `"journal"` = the File evidence path; `"none"` = the explicit `allowUnevidenced` degradation
69
+ * lane (always present — a silent downgrade is the forbidden shape). */
70
+ evidenceCapability: "journal" | "none";
71
+ /** The evidence chain's integrity as read at assembly (v2-a custody tri-state law). `"damaged"`
72
+ * ⇒ every notFound row carries `historyUnknown` and `erasedPreviously` is never produced ("no
73
+ * row" cannot be historically judged over a chain with a known hole). */
74
+ custodyState: "complete" | "damaged" | "capability-absent";
75
+ /** The selector, echoed verbatim. */
76
+ select: ErasureSelect;
77
+ /** sha256 hex of the selector's canonical JSON — equals the anchor row's (`"none"` mode: computed
78
+ * for this call only; there is no anchor to reconcile against). */
79
+ selectHash: string;
80
+ /** The pinned id set (= the anchor row's `ids`; a replay reads it back, never re-resolves). */
81
+ resolvedIds: string[];
82
+ erased: Array<{
83
+ id: string;
84
+ rev: string;
85
+ binding: ErasedBinding;
86
+ /** The delete evidence row's `ev` — absent in `"none"` mode. */
87
+ evidenceEv?: string;
88
+ /** Distinct census-confirmed projection files this transaction physically deleted (the sweep
89
+ * arm can answer >1: stray copies and P≥2 unbound duplicates are all removed). */
90
+ projectionsRemoved: number;
91
+ /** Contributing sessions (pre-capture ∪ the same-lock lineage capture-and-clear; `"none"` mode
92
+ * answers `[]`). NON-durable beyond the chain's pre-capture: a replay after a crash carries
93
+ * the chain's set — honest downgrade, never a fabricated rebuild. */
94
+ sessions: string[];
95
+ }>;
96
+ /** No row at judgment time and not claimable as erased under THIS request. Covers never-existed /
97
+ * organically deleted / deleted by another request — the chain can distinguish them, the
98
+ * attestation does not pretend to. `historyUnknown` ⇔ `custodyState !== "complete"`. */
99
+ notFound: Array<{
100
+ id: string;
101
+ historyUnknown?: true;
102
+ }>;
103
+ /** Chain-read rows: an origin-ABSENT delete row carrying THIS requestId (per-request judgment —
104
+ * another request's delete of the id answers notFound, never a claim). Present only when
105
+ * non-empty; never produced in `"none"` mode or over a damaged chain. */
106
+ erasedPreviously?: Array<{
107
+ id: string;
108
+ ev: string;
109
+ at: number;
110
+ /** The pre-delete binding from the most recent same-request delete row that RECORDED one —
111
+ * carried so a REPLAY can still run the index-line sweep (the ledger row is gone; the evidence
112
+ * row is the only surviving source of the projection path). `ev`/`at` come from the MOST RECENT
113
+ * same-request delete row (which may be a later, binding-less re-erase). Absent in THREE cases:
114
+ * no same-request delete row ever recorded a binding (unbound / pre-v2-form), or the recorded
115
+ * path is OCCUPIED now (a live account row binds that scope+slug, or bytes exist at the path —
116
+ * the binding is known but suppressed: the index line there belongs to the occupant, and a
117
+ * consumer must not read absence as "no residue possible"). */
118
+ from?: {
119
+ scope: string;
120
+ slug: string;
121
+ };
122
+ }>;
123
+ /** Per-id planning-stage faults (io/plan errors under the lock — there is no CAS loser inside a
124
+ * single lock span). Non-fatal; a replay converges them. */
125
+ conflicts: Array<{
126
+ id: string;
127
+ reason: string;
128
+ }>;
129
+ residuals: {
130
+ /** Quarantine files whose frontmatter id is in the pinned set (preserve-not-delete: the
131
+ * attestation enumerates, an explicit second action clears). */
132
+ quarantineHits: string[];
133
+ /** Quarantine files that could not be attributed (fragments / non-entry shapes) — the honest
134
+ * bound of the enumeration. */
135
+ quarantineOpaque: number;
136
+ /** Derived MEMORY.md index files that may STILL carry a line naming an erased entry, because
137
+ * the erase-time index sweep could not read or rewrite them (a non-file at the index path, a
138
+ * non-ENOENT read failure, or a refused write — an entry may carry a parenthesized failure
139
+ * detail). Present only when non-empty. Minted by the ENGINE lane
140
+ * (`MemoryEngine.eraseMemoryEntries` — the derived index is the engine's plane; this backend
141
+ * face answers without the seat). The next successful materialize's rebuild also clears
142
+ * orphaned lines, but a one-off compliance erasure cannot count on one — hence the disclosure
143
+ * instead of a silent clean residual set. */
144
+ indexUncleared?: string[];
145
+ /** Propagation boundary, self-declared always: deletion is a THIS-store fact; remote-peer
146
+ * convergence is the sync deployment's half to prove. */
147
+ propagation: "local-store-only";
148
+ };
149
+ }
150
+ /** Canonical JSON (recursively sorted keys, no whitespace) — the erasure protocol's ONE comparison
151
+ * and hash basis: `selectHash` complains bind to it, and evidence rows are written/compared in this
152
+ * form so a re-serialized import (key order changed by a middle layer) still compares equal. */
153
+ export declare function canonicalJsonStringify(value: unknown): string;
154
+ /** sha256 hex over the selector's canonical JSON + LF — the replay-identity judge (§4.6) and the
155
+ * anchor validator's recomputation basis (a 64-hex shape check alone would let a forged anchor swap
156
+ * the selector under a kept hash). */
157
+ export declare function erasureSelectHash(select: ErasureSelect): string;
158
+ /** Erasure input validation, shared by the engine host API and the File capability face (one rule
159
+ * set — the selector the anchor row echoes verbatim must pass the anchor's own validator, so the
160
+ * entry check and the chain check are the same predicate). Returns the violation, or undefined. */
161
+ export declare function erasureRequestInvalid(input: EraseMemoryEntriesInput): string | undefined;
162
+ /**
163
+ * Snapshot the erasure input into PLAIN data by reading every accessor exactly once (round-2 #3/#5):
164
+ * validation and use must observe the SAME value, so an accessor-backed (getter/Proxy) selector or
165
+ * requestId cannot pass validation on read #1 and yield a different (unvalidated) set on read #2.
166
+ * The returned object is inert — its `requestId` is a captured primitive and its `select` is a fresh
167
+ * plain object. Malformed shapes (a non-object select, or one with several/no keys) capture as-is so
168
+ * the validator that runs next reports them; the capture never throws.
169
+ */
170
+ export declare function captureErasureInput(input: EraseMemoryEntriesInput): EraseMemoryEntriesInput;
171
+ /**
172
+ * design/178 v2-a §1.3 — ONE `transfers.jsonl` evidence row on the PUBLIC audit face, as an OPEN
173
+ * discriminated form: the base keys (`ev` — the row's unique identity, `channel` — the event kind,
174
+ * `at` — ms epoch) are the contract; everything else is variant payload surfaced through the index
175
+ * signature. Known channels today (each validated in full by the store's own rule set):
176
+ * - `"adopted-move"` / `"applyPatches-move"`: an ownership transfer of one id — payload
177
+ * `{ id, from: {scope, slug}, to: {scope, slug} }`;
178
+ * - `"migration-bind"`: an unbound row converging to a binding — payload `{ id, to }` (`from`
179
+ * optional: honest absence, an unbound row has no prior coordinates);
180
+ * - `"migration"`: the one-shot v1→v2 baseline summary — payload `{ boundRows, unboundRows }`
181
+ * (store management history, never per-entry custody).
182
+ * `channel` is deliberately `string`, not a closed union: later releases append new variants to the
183
+ * SAME chain (one custody chain, one recovery protocol — never a second evidence file), and a closed
184
+ * union would break every consumer at the source level on each addition. CONSUMER OBLIGATION: tolerate
185
+ * unknown channels (report/skip/display raw — never an exhaustive `switch` that throws); the per-id
186
+ * custody read passes them through so a chain written by a newer engine still answers here.
187
+ */
188
+ export interface TransferEvidence {
189
+ /** Unique event id (the append-idempotency key of the evidence chain). */
190
+ ev: string;
191
+ /** Event kind — open on purpose; see the known-channel table above. */
192
+ channel: string;
193
+ /** ms epoch of the event. */
194
+ at: number;
195
+ /** Variant payload (known channels documented above; unknown channels' payload rides verbatim). */
196
+ [key: string]: unknown;
197
+ }
198
+ /**
199
+ * design/178 v2-a §1.3 — the committed-account BINDING half of one ledger row, shared verbatim by
200
+ * {@link CommittedEntrySnapshot} and `EntryProvenanceAccount.binding` (one tagged definition — never
201
+ * a look-alike copy: an untagged `{scope, slug}` beside `{state: "unbound"}` can be satisfied by one
202
+ * hybrid object under both readings). `"unbound"` is a LEGAL transitional state (a v1→v2 migration
203
+ * row whose projection could not be derived), never a refusal.
204
+ */
205
+ export type CommittedBinding = {
206
+ state: "bound";
207
+ scope: string;
208
+ slug: string;
209
+ at?: number;
210
+ prev?: {
211
+ scope: string;
212
+ slug: string;
213
+ at: number;
214
+ };
215
+ } | {
216
+ state: "unbound";
217
+ };
218
+ /**
219
+ * design/178 v2-a §1.3 — the AUDIT SNAPSHOT of one entry's committed account: the account half
220
+ * (`rev` + {@link CommittedBinding}) and the content half are SEPARATE, because the account's
221
+ * authority is the ledger row while the content may be independently unavailable — an auditor asking
222
+ * "where is this entry bound, at which rev" gets the account facts even when no committed carrier
223
+ * can be produced ("cannot read it" must never collapse into "it does not exist").
224
+ * - `state: "absent"` — the account has NO row for this id (never a throw, never a guess);
225
+ * - `content.state: "present"` — the committed entry is servable: the committed bytes (control-plane
226
+ * shadow; for a pre-shadow row, the in-place projection at the bound coordinates carrying the
227
+ * committed rev) with the projection carrier in place at the bound address;
228
+ * - `content.state: "unavailable"` — the row exists (binding/rev answered) but no committed carrier
229
+ * serves it: `"shadowless-legacy"` = no shadow copy exists and the bound projection cannot vouch
230
+ * (missing/divergent/foreign id); `"carrier-missing"` = the projection carrier is not in place at
231
+ * the committed address (deleted or squatted out-of-band) or the shadow cannot carry the
232
+ * committed rev — including every UNBOUND row (no committed address exists to mint
233
+ * `MemoryEntry.scope`/`slug` from; the shadow bytes, where present, stay readable via
234
+ * `readCommittedShadow`).
235
+ */
236
+ export type CommittedEntrySnapshot = {
237
+ state: "absent";
238
+ id: string;
239
+ } | {
240
+ state: "row";
241
+ id: string;
242
+ rev: string;
243
+ binding: CommittedBinding;
244
+ content: {
245
+ state: "present";
246
+ entry: MemoryEntry;
247
+ } | {
248
+ state: "unavailable";
249
+ reason: "shadowless-legacy" | "carrier-missing";
250
+ };
251
+ };
252
+ /**
253
+ * design/178 v2-a §1.3 — the LEDGER-DRIVEN scope enumeration's return form. `rows` carries every
254
+ * BOUND row whose `binding.scope` is in the requested set (the account is the enumeration authority
255
+ * — a shadow-backed row whose projection was deleted out-of-band is still answered; a disk scan
256
+ * would miss it). `unbound` is the STORE-LEVEL appendix of unbound row ids (they have no scope to
257
+ * belong to — the enumerating consumer disposes of them explicitly: an export lists them as
258
+ * residuals, an erasure scope-selector never resolves them). `complete: false` means a stable
259
+ * committed snapshot could not be taken (the journal-quiescence fence stayed contended past its
260
+ * bound) and the caller must FAIL CLOSED — a partial answer is never dressed as a full one; the
261
+ * union is DISCRIMINATED on `complete` so the incomplete arm cannot even carry rows to misread.
262
+ */
263
+ export type CommittedScopeSnapshots = {
264
+ complete: true;
265
+ rows: Array<Extract<CommittedEntrySnapshot, {
266
+ state: "row";
267
+ }>>;
268
+ unbound: string[];
269
+ } | {
270
+ complete: false;
271
+ };
272
+ /**
273
+ * design/178 v2-a §1.3 — the per-id custody read's return form. `"damaged"` covers every chain whose
274
+ * integrity is impeached while a sound reading still exists to answer with: a torn tail (crash
275
+ * mid-append — the sound prefix IS the answer and the file is NOT touched: inspection and repair are
276
+ * separate duties; the recovery leg quarantines the fragment, and an audit read that "helpfully"
277
+ * rewrites the evidence it is auditing has destroyed its own subject), the evidence-loss cross-check
278
+ * (a prev-bearing ledger row beside a chain with zero events for the id), and the ev-identity law
279
+ * (one `ev` under two DIFFERENT payloads — the chain was spliced/corrupted out of band; the store's
280
+ * own write path refuses to operate on it, so custody must never read it back as `"complete"`).
281
+ * `reason` carries the impeachment detail when one is known (today: the ev-identity arm); its
282
+ * absence means the damage form speaks for itself (torn tail / evidence loss).
283
+ * `"capability-absent"` never originates here — it is the ENGINE's honest answer for a backend
284
+ * without this capability face.
285
+ */
286
+ export interface EntryCustodyReport {
287
+ state: "complete" | "damaged";
288
+ events: TransferEvidence[];
289
+ reason?: string;
290
+ }
26
291
  /** Construction options for {@link FileMemoryEngineBackend}. */
27
292
  export interface FileMemoryEngineBackendOptions {
28
293
  /** ⚠️ 分家坑(service 报告 2026-07-09):不显式传时,控制平面按无参 `resolveMemoryEngineRoot()`
@@ -104,6 +369,13 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
104
369
  private adoptionNoticeKeys;
105
370
  /** Test seam (§7.4 r4-④ arm): force a post-commit-point transfer-append failure. */
106
371
  private transfersAppendFault?;
372
+ /** v2-b §3-4 — the resurrection backstop's chain digest, keyed by the evidence log's stat
373
+ * fingerprint (size + mtimeMs — append-only growth and a torn-tail shrink both move `size`).
374
+ * NEVER a mount-lifetime cache: the fingerprint is re-verified under the txn mutex on every
375
+ * consultation, so a sibling process's erasure invalidates it (r3: a B-process erasure must not
376
+ * leave an A-process adopting off the old deleted-id set). The digest carries its chainState —
377
+ * a damaged read caches as damaged, never as a bare (falsely reassuring) id set. */
378
+ private deletedIdsDigest;
107
379
  /** B2 — inbound-gate findings accumulated by read-side syncs; the engine drains them into
108
380
  * `HarvestReport.inboundFindings` at harvest. */
109
381
  private inboundFindings;
@@ -182,19 +454,97 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
182
454
  * is a no-op version check.
183
455
  */
184
456
  private migrateLockedIfNeeded;
185
- /** Read the evidence log's event ids, healing a TORN TAIL (a crash mid-append): the fragment is
186
- * quarantined for evidence, the sound prefix stands (lines are self-contained — damage never
187
- * spreads backward). Any NON-tail corruption is fail-closed (B3 family). */
188
- private readTransferEvs;
457
+ /** Is the durable chain-degradation marker in place? Presence is ENOENT-only-absent (a probe
458
+ * failure is never read as "not degraded" fail-closed). */
459
+ private chainDegradedMarkerPresent;
460
+ /** §3-4 r6 — mint the chain-degradation marker DURABLY (wx + fsync + read-back verification).
461
+ * Idempotent on an existing marker. Throws when the marker cannot be proven on disk — the caller
462
+ * (the heal) must then ABORT with the chain untouched: truncating first would reopen the exact
463
+ * window this marker closes ("chain reads complete, no marker" after an out-of-band tear). */
464
+ private mintChainDegradedMarker;
465
+ /**
466
+ * Read the WHOLE evidence chain in order (validated, collision-checked), healing a TORN TAIL (a
467
+ * crash mid-append): the fragment is quarantined for evidence, the sound prefix stands (lines are
468
+ * self-contained — damage never spreads backward). Any NON-tail corruption is fail-closed (B3
469
+ * family). Two heal forms (§3-4 r5/r6):
470
+ * - transaction journal PRESENT beside the tear = the honest crash form — the committed-pending
471
+ * protocol re-appends the lost row from the journal, so the healed chain is genuinely complete;
472
+ * - journal ABSENT = out-of-band tail damage — the lost row is unrecoverable, so the heal mints
473
+ * the durable {@link CHAIN_DEGRADED_FILE} marker BEFORE it truncates (write order is
474
+ * load-bearing: isolate fragment → marker wx + read-back → only then truncate; a marker
475
+ * failure aborts the heal with the chain byte-untouched).
476
+ * Same-`ev` collision law (§4.4, read-time arm): canonical-form-equal duplicates are tolerated
477
+ * (append-idempotency residue); DIFFERENT payloads under one ev are fail-closed — an event id is
478
+ * one identity, and a reader must never pick a side by file order.
479
+ */
480
+ private readTransferChain;
481
+ /** The chain's ev → canonical-payload map (§4.4 upgraded load shape — the append path compares
482
+ * PAYLOADS, never mere membership). */
483
+ private readTransferEvents;
484
+ /**
485
+ * v2-b §3-4 — the deleted-id digest behind the resurrection backstop: every `delete` evidence
486
+ * row's id (LOCAL AND IMPORTED — an origin filter here would disarm exactly the cross-store
487
+ * anti-resurrection evidence a custody import carries; the origin distinction belongs to the
488
+ * replay/anchor judgments alone), read WITHOUT healing (a backstop that rewrites the chain it
489
+ * consults has destroyed its own evidence; a torn tail answers `"damaged"` with the file
490
+ * byte-untouched). Non-tail corruption stays the fail-closed throw. Unknown channels are
491
+ * tolerated (open-form law). Must be called with the txn mutex held — the stat fingerprint is
492
+ * the cross-process staleness judge, and holding the lock is what makes "fingerprint unchanged
493
+ * ⇒ digest current" sound.
494
+ */
495
+ private deletedIdsDigestLocked;
496
+ /**
497
+ * v2-b §3-4 — the CHAIN-LEVEL resurrection backstop's verdict for one NEW id-bearing disk file
498
+ * (an id with no committed account row) meeting the adopting read:
499
+ * - `"erased"` — the chain carries a delete evidence row for the id: the bytes are a deleted
500
+ * entry resurfacing (whatever journal shape produced the "account gone, disk copy back"
501
+ * scene), refused + quarantined — restoration is an EXPLICIT write with a new id, never a
502
+ * disk replant;
503
+ * - `"degraded"` — the chain cannot prove the deleted set (durable degradation marker, or a
504
+ * live torn tail): "the id is not on the chain" is no evidence on an incomplete chain, so
505
+ * EVERY new id-bearing file is refused adoption (left on disk, not served) until the explicit
506
+ * rebuild clears the state — the same epistemic rule a custody import's completeness
507
+ * precondition applies;
508
+ * - `"clear"` — adoption proceeds normally.
509
+ * `applyPatches` adds are NOT consulted here (host authority — an explicit add of a previously
510
+ * erased id is a legitimate new commit).
511
+ */
512
+ private resurrectionBackstopVerdict;
513
+ /** v2-b §3-4 — the LOCK-FREE retrieval face of the deleted-id set (retrievalView / batch-plan
514
+ * locateById). Reuses the fingerprint-cached digest: a deleted id's replanted bytes never reach
515
+ * the retrieval face either. Best-effort by construction — a lock-free reader carries the same
516
+ * cross-process staleness the whole retrieval face does, and a degraded chain filters only its
517
+ * known deleted set (it never refuses-all here — retrieval must stay a plane report). */
518
+ private retrievalDeletedIds;
519
+ /** v2-b §3-4 (round-3 #1) — a PURE, fresh read of the committed ledger rows for the retrieval
520
+ * backstop's account check: bytes → parsed rows, installing no read cache and no schema memo — so
521
+ * the guard's account view is as fresh as its chain digest,
522
+ * never a stale instance cache paired with a fresh deletion read. A v1-form or absent ledger
523
+ * answers empty (a v1 store has no delete rows to police anyway). FAULT DIRECTION, stated
524
+ * truthfully (rescan corrected the original note, which claimed the opposite): a read/parse
525
+ * FAULT also answers empty, and with an empty account the consuming filter WITHHOLDS every id
526
+ * on the delete list — conservative on purpose (a host re-add cannot be distinguished from a
527
+ * replant over an unreadable account, and serving bytes that carry delete evidence during a
528
+ * fault window would reopen exactly the resurrection the backstop exists to close). No erase
529
+ * CLAIM is minted (nothing is attested); entries with no delete evidence keep serving. The
530
+ * fault is DISCLOSED on the announcement lane (坏值响亮度 — the pre-fix swallow was silent),
531
+ * once per fault code per mount. */
532
+ private freshCommittedRowsPure;
189
533
  /** §1.1 commit-point honesty: any failure BEFORE the journal write must refuse the whole
190
- * transaction cleanly — so the append target's writability (and the log's soundness) is probed
191
- * here, never discovered after the point of no return. */
534
+ * transaction cleanly — so the append target's writability (and the log's soundness, ev-collision
535
+ * law included) is probed here, never discovered after the point of no return. */
192
536
  private precheckTransfersAppendable;
193
537
  /** Append evidence events, idempotent by `ev` (the recovery leg re-runs this for a journal whose
194
- * append crashed). The ONLY failure class here is I/O never capacity: a size threshold would
195
- * freeze legitimate moves behind a full log, so growth is disclosed, not refused. A failure
196
- * AFTER the commit point is COMMITTED-PENDING (the caller keeps the journal and throws — calling
197
- * it "refused" would invite a retry of a transaction the next recovery completes anyway). */
538
+ * append crashed). Rows land in CANONICAL form (sorted keys) so idempotency and collision checks
539
+ * compare one representation across writers and re-serializing middle layers. Three collision
540
+ * checks (§4.4): read-time (inside {@link readTransferChain}), batch-time (one batch minting two
541
+ * identities for one ev is a minting bug fail-closed), append-time (an ev already on the chain
542
+ * with a DIFFERENT payload is corruption, named; equal = idempotent skip). Behavior narrowed,
543
+ * disclosed: the pre-v2b reader silently skipped/overwrote same-ev rows regardless of payload.
544
+ * The ONLY failure class here is I/O — never capacity: a size threshold would freeze legitimate
545
+ * moves behind a full log, so growth is disclosed, not refused. A failure AFTER the commit point
546
+ * is COMMITTED-PENDING (the caller keeps the journal and throws — calling it "refused" would
547
+ * invite a retry of a transaction the next recovery completes anyway). */
198
548
  private appendTransfers;
199
549
  /**
200
550
  * @param opts.unlocked RB-447 (2026-07-31, hardening review) — set by the two callers that run
@@ -328,19 +678,33 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
328
678
  getByIds(ids: readonly string[]): Promise<MemoryEntry[]>;
329
679
  private getByIdsFrom;
330
680
  /**
331
- * design/178 §3 (ruled 2026-08-08) — a SIDE-EFFECT-FREE projection of this backend for the session's
332
- * RETRIEVAL face (`memory_search` / `memory_get`).
681
+ * design/178 §3 (ruled 2026-08-08) — a projection of this backend for the session's RETRIEVAL
682
+ * face (`memory_search` / `memory_get`): NO ADOPTION, ever.
333
683
  *
334
684
  * The ordinary read path carries the inbound-adoption channel: a disk revision that disagrees with
335
685
  * the ledger is judged an out-of-session change, gated, and — when it passes — adopted as the new
336
686
  * COMMITTED baseline (ledger + shadow rewritten). That is a deliberate lifecycle synchronization
337
687
  * point, and it belongs to the engine's own moments (materialize / harvest), not to every model
338
688
  * lookup: a retrieval landing between them would otherwise COMMIT the session's own in-flight edits
339
- * with no harvest gate and no pollution discipline in the way. Reads through this view resolve
340
- * entries from disk without touching the ledger, the shadow, or the quarantine.
689
+ * with no harvest gate and no pollution discipline in the way.
341
690
  *
342
- * Not running the inbound gate on this path is the point, not a concession: the view reports what
343
- * is on the plane, and admitting anything stays the harvest's job.
691
+ * Not running the inbound gate on this path is the point, not a concession: the view reports the
692
+ * plane, and admitting anything stays the harvest's job. Stated exactly (v2-b trued this doc up —
693
+ * the pre-v2b sentence "resolves entries from disk without touching the ledger" is no longer the
694
+ * whole truth):
695
+ * · the v2-b RESURRECTION GUARD consults the delete-evidence digest (`transfers.jsonl` stat +
696
+ * parse) and — when any delete row exists — a PURE fresh read of the committed ledger, and it
697
+ * WITHHOLDS an on-plane file whose id carries a delete row with no live account row (an erased
698
+ * id's replanted bytes are not served through any read face; a host re-add serves). So the
699
+ * view is "the plane minus proven-erased ids", and it can throw {@link ControlPlaneCorruptError}
700
+ * from a fail-closed corrupt evidence log — states a pre-v2b view could not reach;
701
+ * · the reads do no healing and no quarantine, and the COMMITTED-ROWS read is cache-free; the
702
+ * digest read installs the instance fingerprint cache (a read cache, not a store write). The
703
+ * path's write-capable hooks (rescan r3 — do not present this list as shorter): `readScope`'s
704
+ * pre-existing opportunistic v1→v2 migration retry (v1-form/unbound-row stores only, design/186
705
+ * §3.1), the guard's fault-disclosure enqueue (a ledger read/parse fault mints an announcement
706
+ * row, once per fault code per mount), and `scopeDir`'s registry mint (listing a scope name the
707
+ * registry has never seen writes its row — predates this view and rides every scope read).
344
708
  *
345
709
  * WRITES ARE REFUSED, loudly — a view handed to a write path is a defect, not a fallback.
346
710
  */
@@ -492,12 +856,122 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
492
856
  * recovery is now authoritative; committing anyway would overwrite the stealer's transaction. */
493
857
  private assertTxnLockOwnership;
494
858
  private applyPatchesLocked;
859
+ /** E-3 ② — the journal CLOSE guard: re-read the fixed-path journal and remove it only while its
860
+ * `.txn` names THIS transaction. A holder that stalled past the stale line and was stolen from
861
+ * may reach its close after the stealer already wrote a NEW journal at the same path — a bare rm
862
+ * there deletes the stealer's LIVE redo record (its crash then loses a committed transaction).
863
+ * An absent/foreign/unparseable journal is simply not ours to remove: leave it (the stealer, or
864
+ * the recovery leg, owns its fate). The remaining read→rm window is the same assert→act residue
865
+ * the commit-point assertion accepts, stated there. */
866
+ private closeJournalIfOurs;
495
867
  /** Plan one patch: decide CAS/conflicts against CURRENT state, emit journal ops (no entry-file
496
868
  * writes here — staging/execution happen in {@link applyPatches}). Every row write is BOUND
497
869
  * (§1.1) at the coordinates the batch ACTUALLY lands — a slug collision's `-n` suffix binds the
498
870
  * suffixed slug, the same one `applied[].slug` reports (v1's ledger had no landing awareness). */
499
871
  private planOne;
500
872
  private locateById;
873
+ /**
874
+ * design/178 v2-b §7 — request-driven erasure with evidence (capability face; probe with
875
+ * `typeof backend.eraseWithEvidence === "function"`). Rides the SAME txn mutex, the SAME journal
876
+ * shape, and the SAME recovery leg as `applyPatches` — never a second transaction protocol.
877
+ * Everything runs INSIDE the lock (r1: resolving a selector outside it pins a set the store can
878
+ * move under before the lock lands): recover → migrate → chain replay-judgment → resolution (a
879
+ * scope selector consults the census for membership — the account alone cannot see an
880
+ * unledgered-live inhabitant of the scope) → census → plan → journal (ops + ledger snapshot +
881
+ * evidence rows in ONE commit) → EXECUTE → ledger → evidence append → guarded journal close →
882
+ * lineage capture-and-clear.
883
+ *
884
+ * Replay protocol (§4.6): the first execution pins the resolved id set on an ANCHOR row; the same
885
+ * requestId re-executes against THAT set (never re-resolves), deletes the un-deleted remainder,
886
+ * and answers the cumulative view. A same-requestId call with a DIFFERENT selector is refused
887
+ * (`memory.erasure_selector_mismatch`); two same-req anchors on the chain are a spliced chain —
888
+ * refused for a human, never adjudicated by file order.
889
+ *
890
+ * This lane deletes by the ACCOUNT (ledger row), not by a physical locate: shadow-only rows
891
+ * (projection deleted out of band), carrier-missing rows, and unbound rows (P=0 / P≥2) are all
892
+ * erasable — the census sweep removes EVERY projection carrying the id (erasure needs no
893
+ * adjudication of which copy was real: removing all of them IS the requested outcome). A ledger
894
+ * key failing the entry-id contract takes the row-only arm (`legacyId` evidence; no ops — a
895
+ * shadow op on it would fail this store's own journal validator). The NotePatch delete lane's
896
+ * semantics are untouched (a shadow-only row still answers unknown-id there — pinned).
897
+ *
898
+ * Failure postures: an unreadable census refuses the WHOLE request pre-commit
899
+ * (`memory.erasure_census_incomplete` — "deleted clean" must not be attested over a store that
900
+ * cannot be fully read); per-id planning faults are non-fatal `conflicts` rows (a replay
901
+ * converges them); everything before the journal write refuses cleanly (F-1); every crash window
902
+ * after it converges through the shared recovery leg (F-2..F-5, F-13).
903
+ */
904
+ eraseWithEvidence(input: EraseMemoryEntriesInput): Promise<MemoryErasureAttestation>;
905
+ /** Bounded journal-quiescence fence (attempt count × retry pause). Constants, not knobs: the
906
+ * contended window is the commit tail of one transaction (milliseconds); a store where it stays
907
+ * contended for the full bound has a crashed/wedged writer, and the honest answer is refusal,
908
+ * not a longer wait. */
909
+ private static readonly AUDIT_FENCE_ATTEMPTS;
910
+ private static readonly AUDIT_FENCE_RETRY_MS;
911
+ /** Is a transaction journal present right now? ENOENT-only absence (§3.1 ⓪ — a probe FAILURE is
912
+ * not "no journal"; it throws the fail-closed corrupt error, never folds into quiescence). */
913
+ private auditJournalPending;
914
+ /** PURE ledger read for the audit faces: raw bytes → parsed rows, touching NOTHING on the
915
+ * instance (no cache install, no schema-version memo, no v1-compat announcement) and never the
916
+ * disk. A v1-form ledger is refused here — the migration belongs to the store's own entries
917
+ * (construction / adopting reads / locked mutations), never to an audit read. */
918
+ private auditReadLedger;
919
+ /** Run `read` over a STABLE committed account: journal absent before and after, ledger bytes
920
+ * identical across the read (closes both tear channels — the journal-first commit window and any
921
+ * ledger-advancing path, the journal-free no-move adoption save included), retried up to the
922
+ * bound. `read` may throw {@link AuditObservationUnstableError} on a mid-transition scene — the
923
+ * loop re-observes, and the FINAL attempt receives `final: true` to answer the steady-state
924
+ * reading instead. Throws {@link AuditSnapshotContendedError} when the bound exhausts.
925
+ *
926
+ * CONSISTENCY CLAIM, stated precisely (adversarial-review wording pass): the answer is a
927
+ * CONSISTENT committed account as observed at the fenced read — never a torn mixture of two
928
+ * transactions' halves. FRESHNESS-AT-RETURN is deliberately not claimed: a lock-free reader
929
+ * cannot exclude a writer whose entire commit lands in the sub-window between the last two
930
+ * probes, and such an answer is simply the immediately-preceding committed state (the same
931
+ * answer the call would have produced had it returned a moment earlier). Holding the store's
932
+ * txn mutex would buy freshness at the cost of the audit read acquiring writer machinery (a
933
+ * write, and a contention seat) — refused: an audit must not perturb the store it audits. */
934
+ private auditStable;
935
+ /** The audit faces' committed-CONTENT resolution for one row (see {@link CommittedEntrySnapshot}
936
+ * for the state law). Reads only; the shadow read is fail-closed (a control-plane read FAILURE
937
+ * is not absence), the projection probe is the shared side-effect-free tri-state probe. */
938
+ private resolveCommittedContent;
939
+ /**
940
+ * design/178 v2-a §1.3 — the per-id AUDIT SNAPSHOT (capability face; probe with
941
+ * `typeof backend.committedSnapshotOf === "function"`). Pure committed read under the double
942
+ * fence; unknown id ⇒ `{ state: "absent" }` (never a throw, never an empty guess); unbound row ⇒
943
+ * the account half answers with `binding: { state: "unbound" }` (legal transitional state);
944
+ * v1-compat store ⇒ loud refusal (migrate first); fence contention past the bound ⇒ loud refusal.
945
+ */
946
+ committedSnapshotOf(id: string): Promise<CommittedEntrySnapshot>;
947
+ /** One row's public {@link CommittedBinding} (shared tagged form — see the type's own doc). */
948
+ private publicBinding;
949
+ /**
950
+ * design/178 v2-a §1.3 — the LEDGER-DRIVEN scope enumeration (capability face). The account is
951
+ * the enumeration authority: every bound row whose scope is requested is answered — including
952
+ * shadow-only rows a disk scan would miss (`listHeaders` is an ADOPTING read and enumerates the
953
+ * plane; it is not an audit face). Unbound rows land in the store-level `unbound` appendix.
954
+ * Fence contention past the bound ⇒ `{ complete: false }` (NO data members — nothing to misread
955
+ * as a truncated package): the caller fails closed. Corrupt control-plane bytes and the
956
+ * v1-compat refusal still THROW (they are definite states, not contention).
957
+ */
958
+ committedSnapshotsOfScopes(scopes: readonly string[]): Promise<CommittedScopeSnapshots>;
959
+ /**
960
+ * design/178 v2-a §1.3 — the per-id CUSTODY read (capability face): every evidence row of
961
+ * `transfers.jsonl` naming this id, in file (= time) order, as OPEN {@link TransferEvidence}.
962
+ * PURE INSPECTION, never repair: a torn tail (crash mid-append) answers the sound prefix with
963
+ * `state: "damaged"` and the file is NOT touched — quarantining the fragment is the recovery
964
+ * leg's duty (the adopting read path), and an audit that rewrites its subject has destroyed the
965
+ * evidence it was asked about. Non-tail corruption stays the fail-closed throw. Unknown channels
966
+ * are TOLERATED and passed through (open form: a chain written by a newer engine still answers);
967
+ * known channels are validated by the store's own rule set. Runs under the same journal fence as
968
+ * the snapshot faces — a pending journal may hold committed-but-unappended evidence, and a chain
969
+ * read across it would be completed differently by the very next recovery.
970
+ */
971
+ custodyOf(id: string): Promise<EntryCustodyReport>;
972
+ /** The pure open-form evidence read behind {@link custodyOf} (no healing, no writes). `raw` is
973
+ * the caller's already-fenced read of the log (undefined = absent). */
974
+ private custodyReadPure;
501
975
  getConsolidationCursor(scope: string): Promise<string | undefined>;
502
976
  setConsolidationCursor(scope: string, cursor: string): Promise<void>;
503
977
  private readCursors;