UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

1,116 lines • 81 kB
import { type MemoryAnnouncement } from "./types.js";
import type { AssertAllKeysHandled } from "../ask-origin.js";
import { physicalPathOfExisting } from "../physical-path.js";
/** Cursor sidecar for the design/84 Seam B pair on the FileBackend (`{ [scope]: cursor }`). B3: control plane. */
export declare const CURSORS_FILE = "cursors.json";
/** The control-plane subdir name under a key dir — ONE spelling for every derivation and for the
 *  migration probes that move a control plane between key dirs. Exported for the sibling planes
 *  (dual-root's personal plane) so no second literal exists; not part of the package surface. */
export declare const CONTROL_PLANE_DIR = ".engine";
/** B3 fail-closed: a control-plane sidecar exists but cannot be trusted (unparseable / wrong shape).
 *  Read paths propagate it (materialize fails ⇒ the runner falls open to a memory-less session with
 *  onError); harvest converts it into a refused report (incident `sidecar_corrupt`). It is NEVER
 *  silently degraded to an empty ledger / unclaimed root — that would let a corrupted (or truncated)
 *  sidecar erase CAS baselines and re-open the root claim. */
export declare class ControlPlaneCorruptError extends Error {
    /** Structured discriminant for callers that fold ONE corruption family and rethrow the rest —
     *  branching on message text is banned (§B8). Only `"lock-ownership"` is minted today (the
     *  recoverable-as-incomplete family); absent = every other corruption (a definite state, throw). */
    readonly facet?: "lock-ownership";
    constructor(message: string, opts?: {
        cause?: unknown;
        facet?: "lock-ownership";
    });
}
/**
 * CC-style repo PARTITION key: the repo root path, canonicalized, then slugged. Collisions across
 * repos require pathological sibling names and only merge their memory dirs (never corrupt them).
 *
 * RB-472 (design/161, four-repo receipts on record): the key CANONICALIZES first — without
 * realpath, the same repo reached through a symlink alias and through its physical path minted two
 * different partition keys = two memory dirs, silently splitting memory. `canonicalize` degrades to
 * plain resolve semantics for a not-yet-existing path (nearest existing ancestor + remainder).
 * Its input is the HOST-declared repo root; a model-reachable directory is keyed by
 * {@link deriveDirKey} instead, and that asymmetry is deliberate (see there).
 *
 * The case axis (once a known limit here) closes with the same call: the mint answers with the ON-DISK
 * spelling, so two case-variant spellings name ONE key on a case-INSENSITIVE volume, while on a
 * case-SENSITIVE volume they are two distinct directories and keep two (FOLDING would merge that pair).
 * Registered, not fixed: the PREVIOUS key was this realpath with the CALLER's casing = `deriveDirKey`'s
 * spelling, which the arm below adopts, EXCEPT under a case-varying symlink on a non-folding library.
 */
export declare function deriveRepoKey(repoRoot: string): string;
/** Sink for the mount-time partition-key incidents (`memory.partition_split` /
 *  `memory.partition_adopt_failed`). ONE shape for every host that can receive them — the runner's
 *  onError bridge, the engine, and the file backend. */
export type MemoryPartitionIncidentSink = (err: Error & {
    code?: string;
}) => void;
/** RB-472 adoption leg: a pre-5.x deployment keyed its repo partition by the resolve-only path, so
 *  when the two mints differ its `memory/` + `.engine/` + siblings live under {@link deriveDirKey}'s
 *  dir. Move the whole partition onto the canonical key (see {@link migrateKeyedPartition}). */
export declare function adoptLegacyRepoDirs(configRoot: string, repoRoot: string, onIncident?: MemoryPartitionIncidentSink): void;
/** Every spelling an earlier release keyed this repo's partition by, minus today's: the resolve-only key and the
 *  link-resolved, caller-cased key (`/var/TMP` keyed `-private-var-TMP`; today `-private-var-tmp`). */
export declare function earlierRepoKeysOf(repoRoot: string): string[];
/**
 * The REVERSE one-shot migration (ruled 2026-08-03): for the width of ONE release the control-plane
 * key canonicalized like the repo key does, which put a directory-pinned mount's `.engine` under the
 * canonical key. {@link deriveDirKey} is the control-plane mint again (that ruling's security half),
 * so such a control plane sits at an address this build no longer reads — move it back, once.
 *
 * Scoped to the `.engine` subtree on purpose: the canonical-key dir may host unrelated siblings.
 *
 * Known limit, registered rather than fixed: when the SAME physical directory is ALSO mounted under
 * its physical spelling, that mount's own resolve-keyed plane IS this migration's source dir, and the
 * probe cannot tell the two apart (both are `<key>/.engine`). Concurrently mounting one directory
 * under two spellings is outside what this probe can discriminate; the window it recovers from is one
 * release wide.
 */
export declare function adoptCanonicalKeyedControlDir(configRoot: string, dir: string, onIncident?: MemoryPartitionIncidentSink): void;
/**
 * `<configRoot>/<repoKey>/memory` — the repo-level stable mount (§2.7: taskRoot 外, worktree-shared).
 *
 * Worktree note (验收 L5, documented behavior): the KEY must be the BASE repo root. The runner passes
 * `deps.rootPath ?? taskRootPath` — when `rootPath` is configured, every derived worktree of that repo
 * shares ONE memory dir (a worktree moves the task cwd, never the memory mount); when `rootPath` is
 * NOT configured the taskRootPath fallback keys memory to the task's own checkout path, so an
 * isolated-worktree task without `rootPath` gets a worktree-private memory dir. That fallback is the
 * accepted degraded behavior (no silent cross-repo mixing; configure `rootPath` for worktree sharing).
 */
export declare function deriveRepoMemoryDir(configRoot: string, repoRoot: string): string;
/** design/142 §1.5 — the IDENTITY-keyed mount (v2 scope contract + a resolved project marker):
 *  `<configRoot>/proj-<projectId>/memory`. Unlike the path-derived {@link deriveRepoMemoryDir},
 *  this key survives rename/move/re-clone/container remounts — identity lives in the repo's marker,
 *  the mount stops referencing the path entirely. */
export declare function deriveProjectMemoryDir(configRoot: string, projectId: string): string;
/** Control-plane sibling of {@link deriveProjectMemoryDir} (B3: always on the config-root side). */
export declare function deriveProjectControlDir(configRoot: string, projectId: string): string;
/** design/142 §1.5 — the path→projectId HINT CACHE (`<configRoot>/project-id-hints.json`). NOT an
 *  identity source (the marker is the single truth source); purely the recovery breadcrumb for a
 *  deleted/lost marker: materialize can tell "this path used to carry marker X" and prompt a
 *  restore instead of silently minting a fresh identity. Best-effort on both faces (a corrupt hint
 *  file degrades to "no hints", never blocks a session). */
export declare const PROJECT_ID_HINTS_FILE = "project-id-hints.json";
export declare function recordProjectIdHint(configRoot: string, repoRoot: string, projectId: string): void;
export declare function lookupProjectIdHint(configRoot: string, repoRoot: string): string | undefined;
/** B3 — `<configRoot>/<dirKey>/.engine`: the engine CONTROL PLANE for a DIRECTORY-pinned mount (the
 *  memory dir itself is the key). ALWAYS on the config-root side — when the memory dir is configured
 *  in-repo, the control plane still lives here (model-invisible, not injected, not in-repo) — and
 *  keyed RESOLVE-ONLY, because the key input is a directory the model's file channel may be able to
 *  replace: see {@link deriveDirKey} for why canonicalizing it is a redirect handle.
 *  The repo-partition face has its own sibling mint, {@link deriveRepoControlPlaneDir}. */
export declare function deriveControlPlaneDir(configRoot: string, key: string): string;
/** B3 — the REPO-partition control plane: the sibling of {@link deriveRepoMemoryDir} under the SAME
 *  canonical repo key. Paired on purpose: {@link adoptLegacyRepoDirs} adopts the repo partition as ONE
 *  directory (`memory/` and `.engine/` inside it), so keying its control plane differently from its
 *  memory dir would strand one of the two on every alias-reached mount. Safe to canonicalize here —
 *  the key input is the host-declared repo root, not a directory the model can replace. */
export declare function deriveRepoControlPlaneDir(configRoot: string, repoRoot: string): string;
/**
 * Resolve the memory-engine config root: explicit (`RunnerDeps.memoryEngineDir`) → `AGENT_DATA_DIR`
 * env → `~/.ai-agent` (the same default family as the file stores' `resolveDataRoot`; re-stated here
 * so core/memory-engine does not import from src/stores).
 */
export declare function resolveMemoryEngineRoot(explicit?: string): string;
/**
 * Filesystem-safe scope dir label. 评审 H5 (唯一映射): the cleaned slug alone is LOSSY
 * (`org:7` and `org/7` both fold to `org-7` — two tenants, one directory = cross-scope bleed), so any
 * scope whose cleaned form differs from the original gets a short content-hash suffix
 * (`org-7-a1b2c3`), making the mapping injective for all practical inputs; an already-safe scope maps
 * to itself (stable, human-readable). {@link registerScope} additionally FAIL-CLOSES on a registry
 * collision (two distinct scopes → one dir name), so even a hash collision cannot silently merge scopes.
 */
export declare function scopeDirName(scope: string): string;
/**
 * Whether `dir`'s volume FOLDS letter case — two names differing only in case address ONE
 * directory entry (the macOS/Windows default). Probed on the actual directory, never guessed from
 * the platform: case-sensitive APFS volumes and case-insensitive Linux mounts both exist, and this
 * answer gates a fail-closed isolation refusal, so it must describe the volume the scope dirs
 * really land on — a platform guess would fail OPEN exactly on the mismatched deployments.
 * `undefined` = the volume answered on neither leg (unwritable AND no letter-bearing entry to read
 * against); the caller must treat that as unresolved, never as either determined answer.
 * Exported for the layout pins; not part of the package surface.
 */
export declare function dirCaseFolds(dir: string): boolean | undefined;
/** The closure of every label {@link scopeDirName} can mint: a safe-form scope verbatim, or the
 *  folded form (leading `.`/`-` stripped) plus a hash suffix. One non-hidden path segment — never
 *  empty, never `.`/`..`, never a separator — so two labels are two distinct directory entries. */
export declare const SCOPE_DIR_LABEL_RE: RegExp;
/** Prefix of the case-fold probe files the layout leaves (briefly) at a memory root. One spelling
 *  for the probe writer and for the migration's residue removal. */
export declare const CASEFOLD_PROBE_PREFIX = ".casefold-probe-";
/** The four shapes a scope registry read can take. */
export declare const SCOPE_REGISTRY_SHAPES: readonly ["absent", "v1", "v2", "migrating"];
export type ScopeRegistryShape = (typeof SCOPE_REGISTRY_SHAPES)[number];
/** Which shapes the path-deriving read faces (`registerScope` / `scopeDirFor` / `registeredScopes`)
 *  may serve. `v1` (the pre-v2 file: a root-owning scope, `""` labels) and `migrating` (a v2 file
 *  carrying an unfinished move marker) are refused fail-closed: the registry then does not describe
 *  where entries physically live until {@link adoptRootScopeLayout} has run to completion. */
export declare const REGISTRY_SHAPE_DERIVES_PATHS: {
    readonly absent: true;
    readonly v1: false;
    readonly v2: true;
    readonly migrating: false;
};
export type DerivesPathsTableCoversEveryRegistryShape = AssertAllKeysHandled<Exclude<ScopeRegistryShape, keyof typeof REGISTRY_SHAPE_DERIVES_PATHS>>;
/** The unfinished-move marker: the physical root whose loose content is being moved into
 *  `<root>/<label>` for `owner`. Durable across crashes; whoever holds the registry lock next
 *  finishes it before doing anything else. */
export interface ScopeLayoutMigratingMarker {
    root: string;
    owner: string;
    label: string;
}
/** The v2 registry record. `rootOwner` is the scope that historically owned the memory root; it is
 *  consulted only to attribute loose root content during adoption and never derives a path. */
export interface ScopesRecordV2 {
    v: 2;
    scopes: Record<string, string>;
    rootOwner?: string;
    migrating?: ScopeLayoutMigratingMarker;
}
/** A parsed registry, tagged by shape. `v1` keeps the pre-v2 fields for the one-time conversion. */
export type ScopeRegistryRead = {
    shape: "absent";
    rec: ScopesRecordV2;
} | {
    shape: "v1";
    rootScope?: string;
    scopes: Record<string, string>;
} | {
    shape: "v2";
    rec: ScopesRecordV2;
} | {
    shape: "migrating";
    rec: ScopesRecordV2 & {
        migrating: ScopeLayoutMigratingMarker;
    };
};
/** Register `scope` in the durable registry (idempotent) and return its home `<memoryDir>/<label>`.
 *  The registered label is the truth for a scope that has one (a home never moves); the
 *  {@link scopeDirName} formula mints the label for a never-registered scope. Both collision arms
 *  run over the effective labels; the home is created on first registration and its physical form
 *  is proven on every call. Lock-serialized; a `v1`/`migrating` registry is refused. */
export declare function registerScope(memoryDir: string, controlDir: string, scope: string, opts?: {
    caseFoldingFs?: boolean;
}): string;
/** All registered scopes → label. A `v1`/`migrating` registry is refused. */
export declare function registeredScopes(controlDir: string): Record<string, string>;
/** The stable home directory of `scope`'s entry files: `<memoryDir>/<label>`, the REGISTERED label
 *  when there is one, the formula otherwise. Registers nothing. A `v1`/`migrating` registry is refused. */
export declare function scopeDirFor(memoryDir: string, controlDir: string, scope: string): string;
/** The scope registry's shape as it stands on disk (lock-free, journal-aware). Exported for the
 *  backend's cheap pre-check and for the layout pins; not part of the package surface. */
export declare function scopeRegistryShape(controlDir: string): ScopeRegistryShape;
/** The v1 path-key resolver (design A6): maps a memory-dir-relative persistent key written under the
 *  root-owning layout onto the ledger form `(scope, rel)`. The first segment names a registered
 *  non-owner label ⇒ that scope's home; otherwise the key belonged to the root owner. `undefined`
 *  when the row cannot be attributed (no root owner on record) — the caller keeps the row in its
 *  old form and refuses the operations that depend on it. Reads any registry shape (the conversion
 *  is independent of the physical move). */
export declare function v1MemoryPathKeyResolver(controlDir: string): (relPath: string) => {
    scope: string;
    rel: string;
} | undefined;
/** Options of {@link adoptRootScopeLayout}. `ownerCandidate` attributes loose root content when the
 *  registry records no root owner (the mount's write scope / a standalone batch's first add scope).
 *  `renew` is the caller's outer-lock heartbeat (the backend's transaction lock) — called before
 *  every filesystem action and every sidecar write, together with the registry lock's own. */
export interface AdoptRootScopeLayoutOptions {
    ownerCandidate?: string | null;
    onIncident?: MemoryPartitionIncidentSink;
    renew?: () => void;
    /** The one-time v1 sidecar key conversions, run in the adoption (before any later scope
     *  registration can change what a v1 key's first segment names). Each conversion is idempotent
     *  and takes its own sidecar lock. */
    convertSidecars?: (controlDir: string) => void;
}
/** The lock-free pre-check the backend's public faces ask before a registry-derived path: does the
 *  tree at `root` still need the adoption (a v1/migrating registry, or loose content at the root)?
 *  One journal-aware registry read plus one readdir; the adoption re-enumerates under the locks. */
export declare function layoutNeedsAdoption(root: string, controlDir: string): boolean;
/**
 * Adopt the per-scope layout for the physical tree at `root` (the engine's memory dir, or the
 * backend's directory root; both when they differ — they share one control plane, so the registry
 * conversion happens once). Runs under the registry lock; the caller holds any outer lock. Every
 * step is idempotent and the two-phase marker makes an interrupted move resumable by whoever holds
 * the lock next:
 *   0. registry shape: `absent` ⇒ an empty v2 record; `v1` ⇒ v2 with `rootOwner` = the old
 *      `rootScope` (the root owner is also given its label); `migrating` ⇒ that move is finished
 *      FIRST, whichever tree it names. The root's loose content is enumerated on every call — a v2
 *      registry proves nothing about THIS tree.
 *   1. loose content ⇒ owner = `rootOwner ?? ownerCandidate`; neither ⇒ `memory.layout_unowned_root_content`
 *      (names capped), registry converted, nothing moved.
 *   2. pre-checks, zero writes: the control plane inside the root; the owner's home a link/file, or
 *      a real non-empty directory without a marker for it; any loose name already present in the
 *      home — each refuses with {@link ControlPlaneCorruptError}.
 *   3. commit A: the v2 record with the owner registered, `rootOwner`, and the `migrating` marker.
 *   4. move: per entry, lstat the destination (absent ⇒ rename; present ⇒ refuse; source gone ⇒
 *      already moved); probe residue removed. No sidecar key and no index text is rewritten (the
 *      ledger form keys are location-free; the index heals through its rebuild law).
 *   5. `index-revs.json` cleared (fail-open snapshot; the next rebuild keeps model wording once).
 *   6. commit B: the marker removed; `memory.layout_migrated` disclosed.
 */
export declare function adoptRootScopeLayout(rootPath: string, controlDir: string, opts?: AdoptRootScopeLayoutOptions): void;
/** The memory layout's name for the tree's one physical-path mint (symlinks on the EXISTING prefix
 *  resolved, the missing tail rejoined). ONE implementation behind both names — a second canonicalizer
 *  here is what let a key minted by this layout name a directory the fence resolved differently. */
export declare const canonicalize: typeof physicalPathOfExisting;
/** True iff `child` (canonicalized) is `root` or inside it. */
export declare function isContainedIn(root: string, child: string): boolean;
/** The code an act on a store path is refused with when it would land outside the store ({@link storeTargetEscape}). */
export declare const TARGET_OUTSIDE_STORE = "memory.target_outside_store";
/** Where an act at `p` lands: the physical path of its normalized spelling (the spelling a child path
 *  joined under it carries), a MISSING tail rejoined under the deepest existing directory (what the act
 *  creates). Any other resolution fault (EACCES, ELOOP, ENOTDIR) is THROWN as itself: an act that
 *  cannot be placed is not performed — a failed resolution never implies the act would fail too
 *  (a link whose own mode refuses `realpath` is still followed by `unlink` and `rename`). */
export declare function landingPathOf(p: string): string;
/**
 * Where a write, rename or delete at `target` would physically LAND, judged against the store's
 * `physicalRoot`: `undefined` = inside, otherwise the reason. The target's directory, resolved through
 * every link on its existing prefix (a missing tail is what the act creates, under the deepest existing
 * directory), must be the root or inside it. `physicalRoot` is the root's physical form as the store
 * was opened and is not re-resolved here: a store mounted through a link is judged by what the link
 * resolved to, and a root spelling re-pointed after opening moves the acts, not the store. The final
 * component is not judged: `rename` and `unlink` act on it and never through it, and the engine's
 * writes open it no-follow — a link there is replaced, removed or refused, and what it points at is
 * untouched. A lexical spelling under the root proves nothing: a directory under it replaced by a link
 * keeps the spelling while the act follows the link out. A directory that cannot be resolved throws its
 * fault ({@link landingPathOf}).
 */
export declare function storeTargetEscape(physicalRoot: string, target: string): string | undefined;
/** mkdir -p convenience used by the engine/backend (0o700 like the file stores). */
export declare function ensureDirExists(dir: string): void;
/** The one remedy every surfaced permission refusal carries — `undefined` unless `err` is a write
 *  the file system refused on PERMISSIONS (`EACCES` / `EPERM`) at a path INSIDE `memoryDir` (a
 *  refusal elsewhere — a control plane kept outside the store — is not this store's to fix). The
 *  engine never changes permission bits on a store — a session's read-only layers are its policy,
 *  not file-system state — so bits that refuse a write were set by someone else, and nothing here
 *  clears them (an engine from 7.30.x or earlier left inherited layers read-only on disk). */
export declare function permissionRemedyOf(err: unknown, memoryDir: string): string | undefined;
/** Attach {@link permissionRemedyOf} to a permission refusal inside `memoryDir` — in place, so the
 *  error keeps its class and `code` (callers branch on both), and once (an inner seat that already
 *  named its store wins over an outer one). Any other value is returned untouched. */
export declare function withPermissionRemedy<T>(err: T, memoryDir: string): T;
/** B2/L4 — the control-plane subdir holding gate-rejected originals. ONE name for all three writers
 *  (engine harvest, engine derived-index rung, backend inbound gate): they write the same directory,
 *  so its name and its permissions must not depend on which of them ran first. */
export declare const QUARANTINE_DIR = "quarantine";
/** REF-A9 — what {@link quarantineAndTombstone} reports back; both hosts escalate `detail` into
 *  their report channel (harvest `quarantineFailures` / inbound `quarantine_failed` findings). */
export interface QuarantineOutcome {
    /** The captured copy under the quarantine dir. Absent ⇔ the capture itself failed. */
    dest?: string;
    /** True ⇔ the model-visible file no longer holds the suspect content (deleted, or tombstoned). */
    removed: boolean;
    /** WHAT this containment left at the model-visible path, which is the only thing a following restore
     *  may overwrite (see {@link restoreContainedBaseline}). ABSENT ⇔ the path was freed. Present ⇔ either
     *  this engine's own tombstone (the clear-in-place rung) or the suspect bytes themselves (neither rung
     *  worked) — in both cases replacing it takes nobody else's work, and in neither case may anything the
     *  restore did not put there be destroyed. */
    leftOnPlane?: string;
    /** Set on ANY deviation from the clean capture+delete path — never a silent best-effort downgrade. */
    detail?: string;
}
/**
 * B2/L4 + C-F9 (S2-0) — capture a gate-rejected file into control-plane quarantine FROM THE
 * ALREADY-READ `content` (never re-reading `path`: the disk bytes may have been swapped since the
 * gate scanned them — TOCTOU), then remove the suspect from the model-visible plane: delete, and if
 * that fails overwrite in place with {@link DELETED_TOMBSTONE}.
 *
 * REF-A9: ONE implementation for the engine's harvest rung and the backend's inbound-gate rung.
 * They were line-for-line twins carrying the same historical fix ("mirrors the engine-side fix",
 * opus review 1.257.2) and had already drifted twice — the backend minted its quarantine filename
 * off `Date.now()` instead of its own injected clock, and the two created the SHARED quarantine dir
 * with different permissions (whoever ran first decided how readable the captured secrets were).
 *
 * NOT to be reused for the derived-index rung (`MemoryEngine.gateDerivedIndex`): that one CLEARS the
 * file (the index is derived and is rebuilt from entry frontmatter), which is the opposite of a
 * tombstone — folding them would launder that distinction away.
 */
/**
 * Write `content` to `path`, refusing to follow a symlink at the FINAL component (`O_NOFOLLOW`).
 *
 * Every containment write in this domain is a check-then-use: the path was judged in-root when the
 * scan read it, and the write happens later. A symlink dropped in that window turns a containment
 * response — the thing whose job is removing suspect bytes — into a write through the link, i.e.
 * this process putting its own committed memory content wherever the link points. Re-checking
 * before writing cannot close a race; the kernel resolving and rejecting in one step can.
 *
 * SCOPE, stated exactly because the difference matters: `O_NOFOLLOW` constrains the LAST component
 * only. An ANCESTOR directory swapped for a symlink in the same window still redirects the write,
 * and closing that needs per-component `openat`-style traversal from a pinned root descriptor —
 * which Node's `fs` does not expose (there is no relative-open API), so it is not reachable here
 * without native bindings. What this buys is the leaf variant, which is the cheap one; the ancestor
 * variant stays open and is recorded with the rest of the family rather than papered over.
 *
 * Throws like `writeFileSync`, including ELOOP when the final component IS a symlink. A HARD link is the
 * same tamper by other means — the file's bytes live under a second name, possibly outside the store, and
 * an in-place write would change them there too — so an opened file with more than one name is refused
 * the same way, before anything is truncated (the check reads the opened descriptor: no window). The
 * write itself goes through `writeFileSync` on the open descriptor, which writes every byte or throws —
 * a bare `writeSync` may short-write, and reporting a truncated restore as contained (after the
 * truncation already destroyed the destination) is the failure this guard exists to prevent.
 */
export declare function writeFileNoFollow(path: string, content: string): void;
/** {@link writeFileNoFollow}'s NO-REPLACE twin: write `content` only if nothing is at `path`. `"written"`
 *  when the bytes landed, `"kept"` when it was occupied; every other failure throws like the twin (a
 *  write that could not be ATTEMPTED must not read as one that chose not to). `O_EXCL | O_CREAT` is the
 *  atomic form — an `existsSync`-then-write pair is a TOCTOU with exactly the window this closes — and
 *  it refuses a symlink at the final component on its own (POSIX `EEXIST` on a link, dangling or not),
 *  so the twin's no-follow property is kept without a second check. */
export declare function writeFileNoReplace(path: string, content: string): "written" | "kept";
/**
 * The ONE way a committed baseline goes back after {@link quarantineAndTombstone} emptied its seat —
 * the inverse half of that function, beside it so the removal word and its readers stay in one module.
 * Both containment arms (the capture-residue sweep and the polluted-harvest record loop) call it, and
 * the rule is ONE sentence: THE RESTORE MAY ONLY OVERWRITE WHAT THE CONTAINMENT ITSELF LEFT AT THE PATH.
 * The removal's own account of what it left (`leftOnPlane` — nothing, our tombstone, or the suspect
 * bytes) is re-read and compared immediately before the write, through a read that refuses a link, a
 * pipe and anything but a regular file: a caller that awaited the committed store cannot assume what
 * the removal saw still holds, on ANY of the three arms. Anything else — a freed path, a path somebody
 * took back, a path that could not be verified — takes the no-replace write, which is the right answer
 * for all of them at once: it lands iff nothing is there. Residual window: the read-to-open gap, with
 * nothing suspended in it.
 *
 * Declining never weakens containment. Where the suspect bytes were removed they are already in
 * quarantine; where they were NOT, the only way the comparison fails is that somebody replaced them —
 * which took them off the plane as surely as the removal would have.
 */
export declare function restoreContainedBaseline(path: string, committed: string, leftOnPlane: string | undefined): "written" | "kept" | "failed";
export declare function quarantineAndTombstone(path: string, content: string, quarantineDir: string, now: () => number): QuarantineOutcome;
/** S2-B — the queued memory-change announcements (control plane; drained at session-first inject). */
export declare const ANNOUNCEMENTS_FILE = "announcements.json";
/** S2-B — bounded queue: overflow FOLDS (oldest dropped, counted) instead of growing unbounded. */
export declare const MEMORY_ANNOUNCEMENTS_MAX = 20;
/** S2-C — per-file write-rejection counts (the beforeWrite silent-reject fuse; O-F9/C-F7: scoped
 *  state in `.engine/`, so the count survives suspend/resume and backend re-construction). */
export declare const SCAN_FUSE_FILE = "scan-fuse.json";
/** Rejections at which the write-time feedback mutes (S2-C: 拒 ≥3 次熔断为静默拒). */
export declare const SCAN_FUSE_THRESHOLD = 3;
/** Options of {@link acquireSidecarLock}. `waitCapMs` exists FOR the wait-bound pins (same hook shape
 *  as the file backend's txn-lock timings) — production callers leave it at {@link LOCK_WAIT_CAP_MS}. */
export interface SidecarLockOptions {
    onDeadline?: "proceed" | "throw";
    waitCapMs?: number;
}
/** Cross-process mutex via mkdir (atomic on POSIX). A lock older than {@link LOCK_STALE_MS} is
 *  presumed crashed and stolen; after the wait cap we proceed anyway (fail-open — these sidecars are
 *  not CAS-load-bearing, and wedging a session start on a lock would be worse).
 *
 *  opus 1.258 review (MED, steal TOCTOU): a bare rmSync+mkdir steal lets TWO stealers both win (B's
 *  force-rm deletes A's FRESH lock). The steal is made single-winner by an atomic `renameSync` of the
 *  stale lock dir to a unique tombstone first — rename is atomic, exactly one racer succeeds; the
 *  loser's rename throws ENOENT and it loops back to a normal mkdir wait. The deadline branch keeps
 *  the documented proceed-anyway semantics but steals through the same single-winner gate.
 *
 *  Wait-bound (ruled 2026-08-04): EVERY round runs the same three steps — attempt, diagnose, then
 *  the deadline check and the pause. The two "retry immediately" shortcuts this loop used to take
 *  (holder released between mkdir and stat; steal attempted) skipped both of the last two, so a
 *  condition that keeps answering the same way — an unwritable control dir (read-only mount, full
 *  volume, revoked permission) makes mkdir fail forever while the lock dir it would stat never
 *  exists — spun this synchronous loop at full speed and never reached its own cap. */
export declare function acquireSidecarLock(lockDir: string, opts?: SidecarLockOptions): string;
/** 证伪式复审 L2-analog: re-verify the owner token at the COMMIT POINT — a stolen-from holder
 *  aborts instead of writing over the stealer. A FOREIGN token always aborts (positive proof the
 *  lock is someone else's now). What an ABSENT/UNREADABLE owner file means splits by caller family
 *  (backlog #255): the fail-closed LEDGER family (`"strict"` — challenges/lineage via
 *  lockedStrictUpdate) treats unprovable ownership as lost — those sidecars promise their commit
 *  point is fenced, and "cannot disprove" is not a fence; the fail-open calibration family
 *  (`"lenient"`, the pre-#255 shape — e.g. the scope registry, whose sidecars self-heal at the next
 *  locked update) keeps the leniency, because its token writes are best-effort and an absent owner
 *  file there is the documented pre-token degraded shape, not evidence of a steal. */
export declare function assertSidecarLockOwnership(lockDir: string, token: string, what: string, mode?: "strict" | "lenient"): void;
interface AnnouncementsRecord {
    /** How many announcements were dropped by the bounded-queue fold (disclosed at render). */
    folded: number;
    queue: MemoryAnnouncement[];
}
/**
 * S2-B — enqueue one announcement (journaled + locked; bounded with overflow folding).
 *
 * `opts.supersedesPrefix` (#495) declares this announcement to be a STANDING CONDITION — one whose
 * sentence is re-derived at every entry point until the condition clears — and names the family it
 * belongs to by line prefix. Every queued line of that family is a STALE READING of the same
 * condition, so the queue is made to hold exactly one of them, always the current one:
 *   · the identical line already queued unread ⇒ no-op, and no write at all;
 *   · otherwise every queued item with that prefix is dropped and this one appended.
 * Merely SKIPPING a repeat is not enough, and that was the first form of this: a condition that goes
 * A → B → A inside one unread window would leave `A, B` in the queue and a reader looking at B as the
 * latest state — the fix for a flood turning into a wrong answer. Replacement makes the last word the
 * true one.
 *
 * All of it runs INSIDE the locked read-modify-write, which is the point: a caller that peeked first
 * and enqueued after would race a concurrent drain, and two processes could both peek-miss.
 * Deliberately keyed on the LINE, not on a structural id — two states that render the same sentence
 * are, to every consumer, the same statement. Deliberately opt-in: an ordinary event repeating IS
 * news, and only its author knows which kind it is. Callers using it pass ONE item (a standing
 * condition is one sentence), and the prefix must name the family precisely — anything else sharing
 * it would be dropped as a stale sibling.
 */
export declare function enqueueMemoryAnnouncement(controlDir: string, ann: MemoryAnnouncement, opts?: {
    supersedesPrefix?: string;
}): void;
/** S2-B — drain the queue (session-first inject, 时机①). Returns what was queued and resets it. */
export declare function drainMemoryAnnouncements(controlDir: string): AnnouncementsRecord;
/** Test/observability peek (journal-aware, non-destructive). */
export declare function peekMemoryAnnouncements(controlDir: string): AnnouncementsRecord;
/** The fuse key for a memory file: the ledger form `(scope, rel)` (`rel` relative to the scope's
 *  home, `/`-separated) — location-free, so a home move never re-keys a count. */
export declare function scanFuseKey(scope: string, rel: string): string;
/** S2-C — record one write-time scan rejection for `key` ({@link scanFuseKey}); returns the count
 *  BEFORE this bump (callers mute the feedback when it is already ≥ {@link SCAN_FUSE_THRESHOLD}). */
export declare function bumpScanFuse(controlDir: string, key: string): number;
/** S2-C — the current fuse count for `key` (0 = never rejected / cleared). */
/** The derived-index snapshot: target → the entry rev and the index line AS OF the last rebuild whose
 *  index landed. It answers one question for {@link indexLineIsStale}: did the entry change since the
 *  line was written while the line did not (the injected index would otherwise answer the OLD value
 *  to the next session)? `line` is in {@link normalizeIndexLine} form, and absent for a snapshot an
 *  older engine wrote (a bare rev string): the line is then unknown, so nothing is judged stale.
 *  Control plane, best-effort, fail-open (absent/corrupt ⇒ {}). */
export interface IndexRevSnapshot {
    rev: string;
    line?: string;
}
export declare function readIndexRevs(controlDir: string): Record<string, IndexRevSnapshot>;
/** The comparison form of an index line: trimmed, every run of whitespace one space. Case and every
 *  visible character stay — only a whitespace difference reads as "the model did not re-word it". */
export declare function normalizeIndexLine(line: string): string;
/** The one judge of a model-worded index line: stale = the entry's rev moved since the snapshot AND the
 *  line is still the text the snapshot recorded (the model has not re-worded it since), both sides
 *  compared in {@link normalizeIndexLine} form. A line the model changed is its own word and wins; a
 *  snapshot without a line cannot say, so it never judges stale. */
export declare function indexLineIsStale(snapshot: IndexRevSnapshot | undefined, entryRev: string, line: string): boolean;
export declare function writeIndexRevs(controlDir: string, revs: Record<string, Required<IndexRevSnapshot>>): void;
export declare function scanFuseCount(controlDir: string, key: string): number;
/**
 * @contract memory.session_incarnation.proven_mismatch_only — only a PROVEN mismatch (both sides
 * known and different) separates a record from the identity reading it; an absent incarnation on
 * either side — a record written before the axis existed, a deployment that wires no resolver, a
 * resolver that threw, a synthesized fail-closed record, a record imported from another plane —
 * always BINDS, which is the pre-incarnation behavior. Every leg's conservative direction is
 * therefore its old one, and the only way to lose a binding is to prove it belongs to someone else.
 */
export declare function sessionRecordBindsIncarnation(recorded: string | undefined, asking: string | undefined): boolean;
/** The incarnation this layer ACTS on: an unusable token is the unnamed occupant, never a throw. */
export declare function usableSessionIncarnation(raw: string | undefined): string | undefined;
/** How a per-session record read names the identity reading; absent ⇒ every record binds. */
export interface SessionIdentityRead {
    incarnation?: string;
    /** Called when a standing record was SET ASIDE as another occupant's, with the incarnation it carries — never silent. */
    onSeparated?: (recorded: string) => void;
}
/** Directory (under the control plane) holding one marker file per polluted session. */
export declare const SESSION_POLLUTION_DIR = "session-pollution";
export interface SessionPollutionRecord {
    at: number;
    reason: string;
    /** design/336 §2.2 — the structured mechanical cause of the exposure (the value an origin marker
     *  minted off this record carries). ADDITIVE: records written before the field existed read as
     *  honestly cause-less (the mint then falls back to `"observed"`); old readers narrow-take the
     *  two fields they know. */
    cause?: import("./types.js").MemoryOriginCause;
    /** the occupant that minted this record ({@link sessionRecordBindsIncarnation}). ADDITIVE:
     *  absent (no resolver wired, or written before the axis) ⇒ it binds every occupant. */
    incarnation?: string;
}
/**
 * Mark `sessionId` polluted (idempotent; the FIRST record wins — `wx` create refuses overwrite).
 *
 * The outcome distinguishes the two ways a durable marker can exist after the call, because the
 * `wx` create is the only ATOMIC answer to "who made this transition" available anywhere in the
 * stack (#324a, adversarial-review round 5): a check-then-act pre-read lets two concurrent markers
 * of one session both believe they were first, and a once-per-session disclosure keyed on that would
 * announce twice. `"created"` ⇔ THIS call minted the marker; `"existed"` ⇔ it was already there (a
 * repeat mark, a resumed session, or a racing peer that won the create); `"unpersisted"` ⇔ the marker
 * could not be written at all — the caller must keep its own in-memory mark and disclose (the session
 * stays polluted for this process either way).
 */
export type SessionPollutionMarkOutcome = "created" | "existed" | "unpersisted";
export declare function markSessionPolluted(controlDir: string, sessionId: string, reason: string, now: () => number, cause?: import("./types.js").MemoryOriginCause, incarnation?: string): SessionPollutionMarkOutcome;
/** Read the durable pollution record for `sessionId` (undefined = no marker).
 *  A marker that EXISTS still reads as polluted whether or not its bytes can be read or parsed
 *  (fail-closed: neither corruption nor an unreadable node may launder the state) — a synthesized
 *  record stands in. Only ABSENCE is clean, so a control plane that cannot be stat'ed at all reads
 *  clean for every session rather than disabling memory deployment-wide. */
export declare function readSessionPollution(controlDir: string, sessionId: string, identity?: SessionIdentityRead): SessionPollutionRecord | undefined;
/**
 * design/178 v2-c §3-3 — enumerate EVERY durable pollution marker (the export snapshot's pollution
 * face; `readSessionPollution` stays the per-session read). Same fail-closed record posture as the
 * per-session read: a marker file that exists but cannot be read or parsed answers a synthesized
 * record, never a silent omission (corruption must not launder the state out of an export). An
 * absent marker directory is the genuine empty state (ENOENT-only); any OTHER enumeration failure
 * THROWS — a listing that silently dropped markers would export a package that launders pollution.
 */
export declare function listSessionPollution(controlDir: string): Record<string, SessionPollutionRecord>;
/**
 * design/178 v2-c §5.3 — the STRICT durable pollution import. Deliberately NOT
 * `markSessionPolluted`: that leg is best-effort-with-disclosure and mints its own `at`, so a
 * bundle's source record (the governance fact being carried) could neither land verbatim nor prove
 * durability. This leg writes the SOURCE record verbatim (`wx`); on EEXIST it reads the standing
 * record back and COMPARES — equal = converged no-op, different = the destination's first-mark-wins
 * record stands and the divergence is REPORTED (`"divergent"`), never silently absorbed. Any other
 * write failure THROWS (the import aborts with its latch standing — §1-2⑤).
 */
export declare function importSessionPollution(controlDir: string, sessionId: string, record: SessionPollutionRecord): "written" | "already" | "divergent";
/** Directory (under the control plane) holding one record file per capture-opted-out session. */
export declare const SESSION_CAPTURE_OPTOUT_DIR = "session-capture-optout";
/** The durable capture opt-out record. `reason` is host/engine-authored (the declaring channel),
 *  never model text. */
export interface SessionCaptureOptOutRecord {
    at: number;
    reason: string;
    /** the occupant that declared this opt-out ({@link sessionRecordBindsIncarnation}).
     *  ADDITIVE: absent ⇒ binds every occupant, which is also where a carrier that drops it lands. */
    incarnation?: string;
}
/** Outcome vocabulary shared with the pollution marker (same `wx` one-way form): `"created"` ⇔ THIS
 *  call minted the record; `"existed"` ⇔ already recorded (a resumed session / an idempotent
 *  re-declaration); `"unpersisted"` ⇔ the record could not be written — the CALLER must refuse to
 *  run (design/383 §2.8: an opt-out that only holds in-process is broken by the first resume, so
 *  "could not persist" is a named refusal, never a silent in-process-only degrade — the deliberate
 *  divergence from the pollution marker's best-effort arm). */
export type SessionCaptureOptOutMarkOutcome = "created" | "existed" | "unpersisted";
/** Mark `sessionId` capture-opted-out (idempotent; the FIRST record wins — `wx` refuses overwrite).
 *  Incarnation law: first-record-wins is per IDENTITY — a new occupant whose declaration meets a stranger's
 *  standing record mints its own slot instead of answering `"existed"` ({@link sessionRecordPath}). */
export declare function markSessionCaptureOptOut(controlDir: string, sessionId: string, reason: string, now: () => number, incarnation?: string): SessionCaptureOptOutMarkOutcome;
/** Read the durable capture opt-out record for `sessionId` (undefined = capture is on). Fail-closed
 *  exactly like the pollution read: a record that EXISTS but cannot be read or parsed still answers
 *  opted-out (a synthesized record stands in) — corruption must not launder a privacy request back
 *  into capture. Only ABSENCE reads as "capture on". A store state where existence itself cannot be
 *  established (dir-level IO fault) THROWS — the engine's `sessionCaptureOptOutOrFault` catch turns
 *  it into the INDETERMINATE fault axis (same fail-closed suppression, honest attribution — see the
 *  catch below). */
export declare function readSessionCaptureOptOut(controlDir: string, sessionId: string, identity?: SessionIdentityRead): SessionCaptureOptOutRecord | undefined;
/**
 * Enumerate EVERY durable capture opt-out record (the consolidation eligibility arm's roster read —
 * design/383 §2.4). Same fail-closed posture as {@link listSessionPollution}: a record that exists
 * but cannot be read answers synthesized (never silently omitted); an absent directory is the
 * genuine empty state (ENOENT only); any OTHER enumeration failure THROWS — a listing that silently
 * dropped records would let the distiller amplify content its author asked to withhold.
 */
export declare function listSessionCaptureOptOut(controlDir: string): Record<string, SessionCaptureOptOutRecord>;
/**
 * design/383 §2.1b — the control-plane carrier a HOST may substitute for the capture opt-out
 * record's read/write/list (server F1: on a stateless multi-replica deployment the file form is
 * invisible to the resume replica — "record not found" would break exactly toward capture). The
 * core default is the file trio above, byte-identical for every deployment that supplies nothing.
 * CONTRACT (each leg mirrors the file form's law):
 *  - `mark` is create-only/first-record-wins and must answer `"unpersisted"` (never throw) when the
 *    carrier cannot durably land the record;
 *  - `read` is fail-closed: a record that exists but cannot be decoded answers a synthesized
 *    record, never `undefined`; a store that cannot establish whether a record EXISTS (an outage)
 *    may THROW — the engine treats a throwing read as the INDETERMINATE fault axis (commit
 *    boundaries suppress fail-closed), which is the honest answer there: it must not fabricate a
 *    record ("a user request that never happened") and must not answer `undefined` ("no record"
 *    breaks exactly toward capture);
 *  - `list` throws on an enumeration failure (an unreadable roster is not an empty one).
 * REGISTERED COMPANION (named, not silently absent): the session-POLLUTION marker has the same
 * cloud-form invisibility (server F1 names both). It is NOT routed through this seat in this
 * slice because the delegation-settlement lane's consumers are contractually PURE-DATA
 * (`controlDir` + `sessionId` coordinates that outlive any live object — see the settlement handle
 * doc in prepare-task.ts), and a live store object cannot ride that lane; its migration needs its
 * own seam design and is registered here rather than half-built.
 *
 * DUAL FORM (#511 件2, additive): each leg may answer `T` OR `Promise<T>` — a SQL/remote carrier is
 * async by nature and could not implement the sync-only trio at all. Every core consumption point
 * awaits (or `.then`-chains where a sync public seat must stay sync-transparent), so a sync store's
 * behavior is unchanged and an async store's Promise legs keep the exact same contract per leg:
 * a rejected `mark` promise is held to the `"unpersisted"` refusal arm, a rejected `read` promise
 * is the INDETERMINATE fault axis, a rejected `list` promise propagates as the enumeration failure.
 * The File trio below keeps its synchronous signatures byte-identical.
 */
export interface SessionCaptureRecordStore {
    mark(sessionId: string, record: SessionCaptureOptOutRecord): SessionCaptureOptOutMarkOutcome | Promise<SessionCaptureOptOutMarkOutcome>;
    /** `identity` names the occupant asking (absent ⇒ every record under the id binds, which is
     *  also what a carrier that ignores the parameter keeps — no carrier has to change). One that DOES
     *  persist the field keys its rows by (sessionId, incarnation): first record per IDENTITY wins. */
    read(sessionId: string, identity?: SessionIdentityRead): SessionCaptureOptOutRecord | undefined | Promise<SessionCaptureOptOutRecord | undefined>;
    list(): Record<string, SessionCaptureOptOutRecord> | Promise<Record<string, SessionCaptureOptOutRecord>>;
}
/** The core-default {@link SessionCaptureRecordStore}: the file trio over `controlDir` (single-
 *  process / single-host deployments keep today's carrier byte-identical). */
export declare function fileSessionCaptureRecordStore(controlDir: string): SessionCaptureRecordStore;
/** The retrieved-account sidecar (control plane): entry id → { count, lastAt }. */
export declare const USAGE_RETRIEVED_FILE = "usage-retrieved.json";
/** Bound on tracked ids — over the cap, the entries with the OLDEST `lastAt` are evicted first
 *  (a popularity account tolerates forgetting cold ids; unbounded growth does not tolerate itself). */
export declare const USAGE_RETRIEVED_MAX_IDS = 4096;
/** One retrieved-account row. */
export interface RetrievedAccountRow {
    count: number;
    lastAt: number;
}
/** Record one retrieval (a search/get hit actually RETURNED to the model) for each id. Journaled +
 *  locked like every sidecar; throws propagate to the caller, which treats them fail-open. */
export declare function recordRetrievedAccount(controlDir: string, ids: readonly string[], now: () => number): void;
/** Journal-aware read of the retrieved account (observability/tests; nothing in the engine's
 *  retention/ranking paths consumes it — see the section note). */
export declare function readRetrievedAccount(controlDir: string): Record<string, RetrievedAccountRow>;
/** S2-C — clear the fuse for files harvest just ACCEPTED (a committed write resets the strike count). */
export declare function clearScanFuse(controlDir: string, keys: Iterable<string>): void;
/** The write-ahead harvest lineage ledger (control plane, fail-closed). */
export declare const LINEAGE_FILE = "lineage.json";
/** The challenge generation-event ledger (control plane, fail-closed). */
export declare const CHALLENGES_FILE = "challenges.json";
/** Per-commit lineage AUDIT history (bookkeeping, fail-open, bounded rotation) — the load-bearing
 *  queries never read it; it exists so the deduped contribution set is not the only trace. */
export declare const LINEAGE_AUDIT_FILE = "lineage-audit.json";
/** Bound on retained audit rows (oldest rotated out first). */
export declare const LINEAGE_AUDIT_MAX_ROWS = 2048;
/** The challenged-history account (design/180 B-3: collect-only, fail-open bookkeeping —
 *  the mechanical shadow of "user saw a retrieved entry and corrected it in the same session"). */
export declare const CHALLENGED_HISTORY_FILE = "usage-challenged-history.json";
/** The locked, journaled, FAIL-CLOSED sidecar update. `coerce` must throw
 *  {@link ControlPlaneCorruptError} on any unacceptable shape; only ENOENT reads as the empty
 *  initial state (`undefined` handed to `coerce`). Exported module-level (not on the root barrel)
 *  for the strict-account family's siblings (design/336 slice 2: the delegation-settlement /
 *  session-account / instruction-hold sidecars live in their own module but MUST be this exact
 *  lock+journal discipline — a second implementation would be a drift seam). */
export declare function lockedStrictUpdate<S, T>(controlDir: string, fileName: string, what: string, coerce: (raw: unknown) => S, fn: (current: S) => {
    next?: S;
    result: T;
}): T;
/** Lock-less strict READ (journal-aware): a parseable journal wins (committed next state); an
 *  unparseable journal, or an unreadable/unparseable file, is corrupt. Exported module-level for
 *  the strict-account family's siblings (see {@link lockedStrictUpdate}). */
export declare function readStrictSidecar(controlDir: string, fileName: string, what: string): unknown;
/** One committed contribution: session → entry, latest rev/at only (the load-bearing invariant is
 *  "which sessions contributed this id", not the per-commit history — that is the audit account). */
export interface LineageContribution {
    lastRev: string;
    lastAt: number;
    /** the occupant that made this contribution, carried from the staged txn at promotion as
     *  `marked` is. Absent ⇒ unknown ⇒ the row answers every occupant of the id (the old superset). */
    incarnation?: string;
    /** design/336 — carried from the staged row at promotion: the contribution's content (at
     *  `lastRev`) carries its external-origin marker, so the pollution retroaction sweeps owe it no
     *  challenge (the account travels with the entry). Absence = unmarked content — the sweeps'
     *  domain (older vintages read as unmarked, the conservative side). */
    marked?: true;
}
/** One staged (pre-commit) row: an entry this transaction WOULD commit. `kind: "latch-only"` marks
 *  a bundle-import SYNTHETIC latch row (design/178 v2-c §1): it withholds the id on the model-visible
 *  read faces exactly like a real staged row, but it is NOT a contribution — the erasure lane's
 *  capture-and-clear strikes it without crediting its txn's sessionId, and the host adjudication
 *  refuses to touch its transaction (the import protocol's receipt-gated release is the only exit). */
export interface LineagePendingRow {
    entryId: string;
    rev: string;
    kind?: "latch-only";
    /** design/336 — set ⇔ the staged content CARRIES its external-origin marker (`frontmatter.origin`
     *  in the very rev this row names): the exposure account travels WITH the entry, so promotion
     *  settlement owes it no pollution challenge (challenging it would re-quarantine a tag-admission
     *  at the read face). Absence = the row's content is unmarked (older vintages included) — the
     *  conservative side: an unmarked row of a marked session is challenged. */
    marked?: true;
}
/** One staged transaction: rows land BEFORE `applyPatches`; the commit credential lands after it
 *  succeeds; promotion consumes both. A pending txn without a credential is the crash window —
 *  it is NEVER auto-discarded (design/180 r5: the applyPatches-success→credential-write gap is
 *  undecidable on the pinned backend contract; the host adjudicates via the explicit API). */
export interface LineagePendingTxn {
    sessionId: string;
    at: number;
    rows: LineagePendingRow[];
    /** the occupant that staged this transaction, carried onto every contribution it
     *  promotes. Absent ⇒ unknown occupant, which binds every reader of the id. */
    incarnation?: string;
    /** Present ⇔ `applyPatches` returned and its applied set was durably recorded. */
    credential?: {
        appliedIds: string[];
        at: number;
    };
    /** design/178 v2-c §1 — present ⇔ this txn is a bundle-import SYNTHETIC LATCH: it never promotes
     *  (no credential is ever written), the generic adjudication REFUSES both arms on it, and the only
     *  release channel is {@link releaseImportLatch} (durable completion-receipt precondition). An
     *  older binary reads it as an ordinary uncredentialed pending txn and reports it undecidable —
     *  the accepted mixed-version posture (the drain precondition of the ledger family applies). */
    kind?: "latch-only";
}
/** Stage this transaction's would-commit rows BEFORE `applyPatches` (write-ahead). Throws on any
 *  write/lock/corruption failure — the caller refuses the whole harvest (fail-closed). */
export declare function stageLineagePending(controlDir: string, txnId: string, sessionId: string, rows: readonly LineagePendingRow[], now: () => number, incarnation?: string): void;
/** Discard a staged transaction that never reached `applyPatches` (the pre-commit abort path —
 *  step ③ of the handshake, or a refused commit). Safe by construction: nothing was committed. */
export declare function discardLineagePending(controlDir: string, txnId: string): void;
/** Record the commit credential — `applyPatches` returned, these ids were APPLIED (non-delete ops).
 *  Written before promotion so a crash between the two converges by credential, never by reading
 *  the backend's current state back (design/180 r4: later syncs/updates rewrite that scene). */
export declare function recordLineageCredential(controlDir: string, txnId: string, appliedIds: readonly string[], now: () => number): void;
/** What one promotion moved into the committed set. */
export interface LineagePromotion {
    entryId: string;
    sessionId: string;
    rev: string;
    /** design/336 — carried through from {@link LineagePendingRow.marked}: the promoted content
     *  already carries its external-origin marker, so promotion settlement skips the pollution
     *  challenge for it. */
    marked?: true;
}
/** Promote a CREDENTIALED pending transaction: rows named by the credential's applied set join the
 *  committed contribution set (deduped by (entryId, sessionId) — a repeat pair takes the incoming
 *  lastRev and the credential's date, never grows the set); rows NOT in the applied set (conflicted/refused ops) are
 *  dropped without ever becoming ancestry. Throws when the txn is unknown or uncredentialed. */
export declare function promoteLineagePending(controlDir: string, txnId: string): LineagePromotion[];
/** The host adjudication for the UNDECIDABLE crash window (a pending txn with no credential —
 *  design/180 r5: never auto-discarded). `"promote"` = the host verified the commit landed (all
 *  staged rows join the committed set); `"discard"` = the host verified it did not. Idempotent on
 *  an unknown txn. Also accepts a credentialed txn (then `"promote"` follows the credential). */
export declare function adjudicateLineagePending(controlDir: string, txnId: string, action: "promote" | "discard", now: () => number): LineagePromotion[];
/** Reconciliation (harvest-opening / host-triggered): promote every CREDENTIALED orphan pending
 *  txn; report the uncredentialed ones (their latch stays until the host adjudicates). */
export declare function reconcileLineage(controlDir: string): {
    promoted: LineagePromotion[];
    undecidable: Array<{
        txnId: string;
        sessionId: string;
        entryIds: string[];
        kind?: "latch-only";
    }>;
};
/** design/178 v2-c §1 — the import COMPLETION-RECEIPT directory (control plane): one `wx`-minted
 *  JSON per bundle hash, written only after the import's governance legs all landed. Its presence
 *  is {@link releaseImportLatch}'s hard precondition and the idempotent re-import's short-circuit
 *  credential. */
export declare const IMPORT_RECEIPTS_DIR = "import-receipts";
/** The lineage txn id of a bundle import's synthetic latch (ONE latch per bundle — a re-import of
 *  the same bundle re-stages the same txn id, which is what makes the latch idempotent). */
export declare function importLatchTxnId(bundleHash: string): string;
/**
 * design/178 v2-c §1-1 — stage the bundle import's SYNTHETIC LATCH: one pending lineage txn
 * (txnId = {@link importLatchTxnId}) whose rows name every bundle entry id, every row and the txn
 * itself carrying the structural `kind: "latch-only"` seat. From this write until
 * {@link releaseImportLatch}, those ids are withheld on every model-visible read face (the same
 * dirty-latch consumption real staged rows get) — a crash anywhere inside the import leaves the
 * withhold standing (durable pending row), and re-staging the same bundle is an idempotent
 * overwrite with identical content. Never credentialed, never promoted.
 */
export declare function stageImportLatch(controlDir: string, bundleHash: string, rows: ReadonlyArray<{
    entryId: string;
    rev: string;
}>, now: () => number): string;
/**
 * design/178 v2-c §1-2⑥ — the ONE latch release channel. Hard precondition: the durable completion
 * receipt for the bundle is IN PLACE (`import-receipts/<bundleHash>.json`, minted after the
 * governance legs all landed) — absent receipt ⇒ loud refusal, the latch stands, and convergence is
 * re-running the same bundle import. Receipt-presence is ENOENT-only-absent (a probe failure never
 * reads as "no receipt" — fail-closed). Idempotent: an already-released (absent) txn is a no-op;
 * a NON-latch txn under the id is refused (this is never a generic discard).
 */
export declare function releaseImportLatch(controlDir: string, txnId: string): void;
/** The DIRTY-LATCH set: every entry id named by ANY pending row (credentialed or not). While an id
 *  is latched, its model-visible read faces (index mechanical rows / memory_search / memory_get)
 *  refuse — "the account is not settled, the content does not go on the table". Throws on a
 *  corrupt ledger (integrity unknown ≠ no latch). */
export declare function lineageLatchedIds(controlDir: string): Set<string>;
/** Every committed contribution of `sessionId` (the pollution-retroaction sweep input, and the
 *  erasure lane's sessionId-selector resolver). Throws on a corrupt ledger. Lookups are
 *  OWN-property (the twin law {@link lineageAccountOfEntry} states): a prototype-member sessionId
 *  such as "constructor" indexes truthy on EVERY plain object, so a bare lookup would attribute
 *  the WHOLE committed set to it — behind the erasure selector that phantom is a whole-store
 *  delete, the exact shape the selector type forbids. Belt over the null-proto parse rebuild:
 *  this guard stays honest even against a map minted elsewhere.
 *  `asking` names the occupant whose contributions are wanted: a row is kept unless it
 *  PROVABLY belongs to another ({@link sessionRecordBindsIncarnation}). An absent `asking`, and
 *  every unstamped row, keeps the whole by-id set — the superset this face has always answered, and
 *  the conservative side here (a sweep that challenges too little publishes). */
export declare function lineageContributionsOfSession(controlDir: string, sessionId: string, asking?: string): Array<{
    entryId: string;
    lastRev: string;
    lastAt: number;
    marked?: true;
}>;
/** design/178 v2-a §1.3 — the BY-ENTRY lineage account, from ONE parse of the ledger (the
 *  by-session read's transpose; the committed set is keyed by entryId, so the index is O(1) after
 *  the load — the load itself still parses and validates the whole account). BOTH halves come from
 *  the same read on purpose (adversarial r3): contributors (committed rows; pending rows are NOT
 *  contributions — the account is unsettled) and the dirty-latch fact (any pending row naming this
 *  id). Deriving them from two separate reads let a promotion land between them and assemble a
 *  scene no instant exhibited — empty ancestry AND no latch. Unknown entry ⇒ empty/false. Throws
 *  {@link ControlPlaneCorruptError} on a corrupt ledger — an audit answer is never assembled over
 *  an account whose integrity is unknown. Lookups are own-property (a prototype-member entryId
 *  such as "constructor" must answer the empty account, never a phantom). */
export declare function lineageAccountOfEntry(controlDir: string, entryId: string): {
    contributors: Array<{
        sessionId: string;
        lastRev: string;
        lastAt: number;
    }>;
    pendingLatched: boolean;
};
/** Drop an entry's lineage rows once the entry is DELETED (an applied tombstone) — space hygiene
 *  only, taken at the harvest that saw the applied delete (design/180 §10). */
export declare function clearLineageForEntries(controlDir: string, entryIds: readonly string[]): void;
/**
 * design/178 v2-b §7.2-3 — capture-and-clear an erased id set's lineage in ONE strict-lock write:
 *  - the COMMITTED contributors of each id are captured (the attestation's testimony) and the rows
 *    dropped (the organic tombstone's space hygiene, same terminal state);
 *  - every PENDING txn's staged rows naming these ids are STRUCK in the same write — a promotion
 *    landing after this lock releases iterates rows the strike already removed, so a late harvest
 *    can never write a deleted id's lineage row back (the read and the clear being two separate
 *    lock spans was exactly the interleave that both missed a landing contributor and then erased
 *    the record of having missed it).
 * Struck pending rows contribute their txn's sessionId to the returned set — EXCEPT rows carrying
 * the structural `kind: "latch-only"` seat (an import protocol's synthetic latch is not a
 * contributing session; the row is still struck). Returns id → contributing session ids
 * (committed ∪ struck). NON-durable by design: a crash between the caller's journal close and this
 * call loses the return set — the caller's replay re-runs it idempotently and its attestation
 * carries the chain's pre-captured set (honest downgrade, never a fabricated rebuild).
 */
export declare function captureAndClearLineageForEntries(controlDir: string, entryIds: readonly string[]): Map<string, string[]>;
/** One (entryId, sessionId) pair whose imported value LOST to the destination's standing row —
 *  design/178 v2-c §5.1's disclosure seat (kept is always "destination": the in-account fact is
 *  never overwritten by an equal-time divergent import, only reported). */
export interface LineageImportDivergence {
    entryId: string;
    sessionId: string;
    kept: "destination";
}
/**
 * design/178 v2-c §5.1 — the lineage FIDELITY importer: write bundle lineage rows into the
 * committed set VERBATIM (`lastRev`/`lastAt` carried byte-for-byte: the promote leg dates a contribution by its credential,
 * the import carries the exporter's date — neither reads a clock here). Deterministic same-key merge, idempotent under
 * re-import of the same bundle:
 *  - destination has no (entryId, sessionId) row ⇒ the source row lands verbatim;
 *  - both present, source `lastAt` is GREATER ⇒ source wins ("last contribution" is a time-axis
 *    fact); destination greater ⇒ destination stands (no divergence — the merge is deterministic);
 *  - equal `lastAt`, equal `lastRev` ⇒ no-op; equal `lastAt`, DIFFERENT `lastRev` ⇒ the
 *    destination row stands and the pair is reported (first-mark-wins side: an in-account fact is
 *    never rewritten by an外来 package; the disagreement must not be silent).
 * Throws on a corrupt ledger (fail-closed family).
 */
export declare function importLineageCommitted(controlDir: string, rows: ReadonlyArray<{
    entryId: string;
    sessionId: string;
    lastRev: string;
    lastAt: number;
    marked?: true;
}>): LineageImportDivergence[];
/** Full-ledger read (tests / host observability). Throws on corruption. */
export declare function readLineageRecord(controlDir: string): {
    committed: Record<string, Record<string, LineageContribution>>;
    pending: Record<string, LineagePendingTxn>;
};
/** Fail-open per-commit AUDIT history (bounded rotation) — never consulted by the load-bearing
 *  queries; a broken audit account degrades silently, exactly like the retrieved account. */
export declare function appendLineageAudit(controlDir: string, rows: ReadonlyArray<{
    entryId: string;
    sessionId: string;
    rev: string;
    at: number;
}>): void;
/** One challenge/resolve event. Append-only; the exclusion predicate is DERIVED ("any unresolved
 *  challenge generation exists"), never stored as a mutable flag. */
export interface ChallengeEvent {
    /** Stable identity for idempotent replay: the automatic arm derives it from the pollution event
     *  anchor (`pollution:<sessionId>:<markedAt>:<entryId>`); the host arm derives it from the
     *  caller's requestId (`<requestId>:<entryId>`). Same eventId replayed = same generation (a lost
     *  ack retried never opens a second generation); a DIFFERENT eventId with the same reason is a
     *  genuinely new event and opens a new generation (no immunity hole). */
    eventId: string;
    entryId: string;
    kind: "challenge" | "resolve";
    /** The generation this event opened (challenge) or closed (resolve). */
    generation: number;
    at: number;
    reason: string;
    /** Evidence grade — v2 mints only "S" (structural) events. */
    grade?: "S";
    /** Audit record only (the exclusion predicate keys the entry ID, never the rev). */
    challengedRev?: string;
}
/** One challenge assignment {@link appendChallengeEvents} returns. `replayed` = the eventId already
 *  existed (idempotent retry — the ORIGINAL generation is returned, no new one opens). */
export interface ChallengeAssignment {
    entryId: string;
    eventId: string;
    generation: number;
    at: number;
    replayed: boolean;
}
/**
 * design/180 §10 — the challenge ledger's SIZE CALIBRATION.
 *
 * The ledger is append-only and cannot be rotated: the exclusion predicate is DERIVED from the events
 * ("a challenge generation with no matching resolve"), so dropping the oldest rows would silently
 * un-withhold their entries — taint laundering, the exact failure the fail-closed family exists to
 * prevent. The bound is therefore a DISCLOSURE threshold, not a cap: crossing it changes nothing about
 * what the ledger holds, it tells the deployment the account needs attention.
 *
 * Calibrated against what actually degrades, which is not disk: every append REWRITES the whole file
 * under the strict lock, and every model-visible read path ({@link challengedEntryIds}, called on each
 * index rebuild and each memory tool call) PARSES it whole. At ~200 bytes per event, 10k events is a
 * ~2 MB parse on a read path that runs per tool call — the point where "an audit account nobody looks
 * at" becomes a latency the session feels. A deployment reaching it should resolve the settled
 * generations (they stay in the file, and that is the point) or archive the account deliberately.
 */
export declare const CHALLENGE_LEDGER_MAX_EVENTS = 10000;
/** What {@link appendChallengeEvents} returns: the per-request assignments plus the ledger's size
 *  AFTER the append — the caller owns disclosure (this module has no sink), and reading the size here
 *  costs nothing because the record is already in hand under the lock. */
export interface ChallengeAppendResult {
    assignments: ChallengeAssignment[];
    /** Total events in the ledger after this append, or `undefined` when nothing was appended — the
     *  empty-request fast path takes no lock and reads nothing, so it has no size to report and must not
     *  report a made-up `0` (a caller comparing that against the bound would read "tiny", not "unknown"). */
    eventCount: number | undefined;
}
/** Append S-grade challenge events (idempotent per eventId, generation allocation inside the strict
 *  lock). Every NEW eventId opens a NEW generation for its entry — a resolve of an earlier
 *  generation never immunizes the entry against later pollution (design/180 R-8). */
export declare function appendChallengeEvents(controlDir: string, events: ReadonlyArray<{
    eventId: string;
    entryId: string;
    reason: string;
    challengedRev?: string;
}>, now: () => number): ChallengeAppendResult;
/** The trusted resolve: closes EXACTLY the named generation (a later challenge's generation stays
 *  open — resolves never blanket-clear). Idempotent: resolving an already-resolved generation is a
 *  no-op. `resolved` is false ⇔ no such open generation existed.
 *
 *  `eventCount` rides along for the same reason it does on {@link appendChallengeEvents}: a resolve
 *  APPENDS an event too, so it crosses the size bound exactly as a challenge does (independent
 *  falsification review, verified — hanging the disclosure off the challenge writer alone leaves a ledger
 *  that crossed the bound on a resolve silent until the next challenge, which may never come).
 *  `undefined` when nothing was appended. */
export declare function resolveChallengeEvent(controlDir: string, entryId: string, generation: number, reason: string, now: () => number, requestId?: string): {
    resolved: boolean;
    eventCount: number | undefined;
};
/** The exclusion predicate's input: entry id → its LATEST unresolved challenge generation. An entry
 *  appears iff at least one challenge generation has no matching resolve. Throws on corruption —
 *  consumers refuse service (integrity unknown ≠ clean). */
export declare function challengedEntryIds(controlDir: string): Map<string, {
    generation: number;
    at: number;
}>;
/** Full event read (host audit / tests). Throws on corruption. */
export declare function readChallengeEvents(controlDir: string): ChallengeEvent[];
/** Which fail-closed control-plane ledger a rebuild targets. */
export type StrictControlPlaneLedger = "lineage" | "challenges";
/** What one rebuild did — the deployment's record of a taint-bearing account it chose to reset. */
export interface ControlPlaneRebuildReceipt {
    ledger: StrictControlPlaneLedger;
    /** The corrupt file (and its journal, when one existed) moved aside — never deleted. */
    quarantinedTo: string[];
    at: number;
}
/**
 * design/180 §10 — rebuild a CORRUPT fail-closed ledger, explicitly.
 *
 * The fail-closed family refuses service on corruption precisely because a silent "start fresh" would
 * LAUNDER taint (a challenged entry re-enters the prompt; a polluted session's contributions become
 * untraceable). That refusal is permanent by design — which left a deployment whose disk really did
 * corrupt with no way back other than deleting engine files by hand. This is that way back, and its
 * whole shape is "never automatic":
 *  · the caller names the ledger — nothing in the engine calls this on its own;
 *  · a HEALTHY ledger is REFUSED (`memory.control_plane_not_corrupt`) — a rebuild is only ever the
 *    answer to a corruption that is already blocking every read, never a way to clear an account;
 *  · the corrupt bytes are QUARANTINED beside the file, never deleted: what the ledger held is
 *    exactly the evidence someone will want after a laundering decision;
 *  · the caller (the engine's host API) announces the rebuild — a reset of this account changes what
 *    the model may see, so it is not a silent repair.
 */
/** Lock-less probe: does this fail-closed ledger currently REFUSE reads? The rebuild's own check runs
 *  again under the lock (this one is for a caller that must decide something — announce, refuse — before
 *  taking the lock). `false` for a ledger that reads cleanly, including an absent one (genuinely empty). */
export declare function isStrictControlPlaneLedgerCorrupt(controlDir: string, ledger: StrictControlPlaneLedger): boolean;
export declare function rebuildStrictControlPlaneLedger(controlDir: string, ledger: StrictControlPlaneLedger, now: () => number): ControlPlaneRebuildReceipt;
/** One challenged-history row: how often a retrieved-then-corrected event hit this entry. */
export interface ChallengedHistoryRow {
    count: number;
    lastAt: number;
    lastOp: "update" | "tombstone";
}
/** Record mechanical correction shadows (same-session retrieved → update/tombstone committed).
 *  Collect-only by contract: NOTHING in the engine reads this back into any decision (v3 candidate
 *  evidence only — design/178 ③-1 三级账 discipline). Throws propagate; callers treat fail-open. */
export declare function recordChallengedHistory(controlDir: string, rows: ReadonlyArray<{
    entryId: string;
    op: "update" | "tombstone";
}>, now: () => number): void;
/** Journal-aware read (observability/tests only — no engine consumer exists, on purpose). */
export declare function readChallengedHistory(controlDir: string): Record<string, ChallengedHistoryRow>;
export declare const PROJECTION_DEBTS_FILE = "projection-debts.json";
/** One standing debt: the plane file at `(scope, rel)` — `rel` relative to the scope's home,
 *  `/`-separated — belongs to committed entry `entryId`, whose id write-back has not landed; `rev`
 *  is the rev the projection was staged against (the CAS baseline a reconciling harvest hands its
 *  update). The key is the ledger form: location-free, so a home move never re-keys a row. */
export interface ProjectionDebtRow {
    scope: string;
    rel: string;
    entryId: string;
    rev: string;
    at: number;
}
/** WRITE-AHEAD staging. Upsert discipline: a stage lands only when the seat has NO standing row,
 *  the standing row is the stager's OWN entry (a rev refresh), or the standing row is the entry the
 *  stager itself just judged stale (`replaces` — the same-harvest stale-then-remint lane). It never
 *  blindly replaces ANOTHER writer's protection: a lagging process that validated an old row before
 *  pausing must not overwrite the row a faster sibling staged at the same seat (its own commit then
 *  CAS-conflicts and its tuple-keyed clear misses the survivor — the account converges instead of
 *  emptying). Called before the backend transaction; the caller degrades LOUDLY (report warning)
 *  on a refused stage rather than refusing the harvest — the ledger is duplicate-admission
 *  protection, not the commit's integrity. */
export declare function stageProjectionDebts(controlDir: string, rows: ReadonlyArray<{
    scope: string;
    rel: string;
    entryId: string;
    rev: string;
    replaces?: string;
}>, now: () => number): {
    refused: Array<{
        scope: string;
        rel: string;
        entryId: string;
    }>;
};
/** Settle (remove) rows by ROW IDENTITY — (scope, rel, entryId), never the bare seat: the write-back
 *  landed, the staged claim turned out stale, or a materialize re-projected the seat. Identity
 *  matters: a stale-clear judged against an OLD row must not delete the NEWER row a later staging
 *  upserted at the same seat. Missing rows are a no-op (idempotent). */
export declare function settleProjectionDebts(controlDir: string, rows: ReadonlyArray<{
    scope: string;
    rel: string;
    entryId: string;
}>): void;
/** Strict read (ENOENT ⇒ empty; corrupt ⇒ throws — the caller's fail-closed arm owns the refusal).
 *  A v1 record is converted in the ledger's lock on this first read. Rows the registry
 *  could not attribute stay on the record's `unresolved` seat and are not served. */
export declare function readProjectionDebts(controlDir: string): ProjectionDebtRow[];
/** The rows a key-form conversion could not attribute (audit read; empty on a converted ledger). */
export declare function unresolvedProjectionDebts(controlDir: string): Array<{
    relPath: string;
    entryId: string;
}>;
/**
 * REF-C6 — write EVERY byte of `data` to `fd`, looping until the OS has taken all of them.
 *
 * A bare `writeSync(fd, data)` issues ONE write(2) and may legally SHORT-WRITE (POSIX): it returns
 * how many bytes it took and silently drops the rest. `src/stores/file/fs-atomic.ts` already
 * documents and fixes this defect class twice (`writeThenLink`'s "silently SHORT-WRITES" note and
 * `AppendLog.append`'s RB-66 loop) — it was never propagated to the memory-engine control plane,
 * where a short write would fsync+rename a TRUNCATED sidecar/entry into place while every
 * fail-closed check upstream still reported success. Zero progress throws instead of spinning.
 */
export declare function writeAllSync(fd: number, data: string): void;
/** B3/H4 — TORN-WRITE-safe atomic replace (R5, ruling 2026-08-02: wording narrowed, no dirsync
 *  added): tmp file in the SAME directory (rename atomicity needs one fs), content fsync'd, then
 *  renamed over the target. A crash can leave the OLD content or the NEW content, never a torn
 *  file. What this does NOT promise: the rename itself surviving a power loss — the directory entry
 *  is not fsync'd (stores/file/fs-atomic.ts has the dir-fsync variant for stores that need it), so
 *  a crash in that window can resurface the old content. Sidecars tolerate that (they are rebuilt
 *  or re-derived); do not route data that can't through here. Throws on failure (fail-closed: a
 *  sidecar that cannot be written must not be silently skipped). */
export declare function atomicWriteFileSync(path: string, data: string): void;
/** Content-gated {@link atomicWriteFileSync} for a DERIVED/control-plane file: a byte-identical
 *  rewrite (the common case on a repeat mount over an unchanged store) is skipped entirely, so the
 *  file's mtime stays put. ONLY for files whose mtime carries no meaning of its own — a lock or a
 *  heartbeat file's mtime IS its signal and must keep calling {@link atomicWriteFileSync} directly. */
export declare function writeIfChanged(path: string, data: string): void;
export {};