import { scopeDirName } from "./layout.js"; import type { HarvestRejection, MemoryBackend, MemoryEntry, MemoryEntryHeader, NotePatch, PatchReport, ScoredMemoryEntry } from "./types.js"; /** Files/dirs the entry scan never treats as entries: the derived index, dotfiles (`.hydrate`, * transaction staging files), and anything not `.md`. */ export declare const MEMORY_INDEX_FILENAME = "MEMORY.md"; /** Default nesting depth cap under a scope dir (CC memory is flat; a small allowance for topic dirs). */ export declare const DEFAULT_MAX_ENTRY_DEPTH = 3; /** One on-disk entry file the scan found. */ export interface ScannedEntryFile { /** Absolute path. */ path: string; /** Slug = path relative to the scope dir, without `.md` (may contain `/` for nested topics). */ slug: string; } /** * Enumerate a scope dir's entry files (depth-capped, symlinks skipped, dotfiles/index/non-`.md` * ignored, `exclude` dirs — sibling scope homes under the root — skipped). Shared by the backend * and the engine's harvest scan so the two can never disagree on what counts as an entry file. */ export declare function scanEntryFiles(dir: string, opts?: { maxDepth?: number; exclude?: ReadonlySet; /** Harvest gate hook: called for every path the scan SKIPS, with why (backend scans ignore it). */ onSkip?: (path: string, kind: "symlink" | "depth" | "nonmd" | "dotfile") => void; }): ScannedEntryFile[]; /** * ⚠️ DEPLOYMENT BOUNDARY (N0 定谳,channel 给service-AI.md [412],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; /** Injected clock (opus 1.258 LOW: match the engine's `this.now` determinism discipline). */ private readonly now; /** * The COMMITTED-rev ledger (`revs.json`, id → rev). 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). The ledger * 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). * 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; /** 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; /** ⚠️ [508] service 逮的分家坑:不显式传 `controlDir` 时,控制平面按无参 `resolveMemoryEngineRoot()` * 派生(env/默认 config 根)——若你的记忆目录带自定义根,控制面会与记忆目录分家。自建 backend 的 * consumer 应显式钉 `controlDir`(engine 同参)。`now` = 注入时钟(determinism,engine 同款)。 */ constructor(dir: string, opts?: { controlDir?: string; now?: () => number; }); /** 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; /** B2/L4 + C-F9 (S2-0) — capture a gate-rejected file into control-plane quarantine FROM THE * ALREADY-READ CONTENT (never re-reading the path — TOCTOU: the disk bytes may have been swapped * since the gate scanned them), then remove the suspect from the model-visible plane (delete → * clear in place). `detail` is set on ANY deviation from the clean path; callers escalate it into * {@link inboundFindings} (report-visible) — never a silent best-effort downgrade. */ private quarantineFile; 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. */ private saveLedger; private recoverJournal; /** 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; /** The names of sibling scope subdirs to exclude when scanning the ROOT scope's own nested content. */ private excludedSubdirNames; private scopeDir; /** 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). */ private readScope; listHeaders(scopes: readonly string[]): Promise; getByIds(ids: readonly string[]): Promise; /** Lexical floor (design/138 §2.1): `1 - Jaccard` distance over name+description+body, ascending. */ search(query: string, scopes: readonly string[], opts?: { limit?: number; }): Promise; /** * 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; 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 (`/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; /** Plan one patch: decide CAS/conflicts against CURRENT state, emit journal ops (no entry-file * writes here — staging/execution happen in {@link applyPatches}). */ private planOne; private locateById; getConsolidationCursor(scope: string): Promise; setConsolidationCursor(scope: string, cursor: string): Promise; private readCursors; } export { scopeDirName }; //# sourceMappingURL=file-backend.d.ts.map