import type { ExecutionEnv } from "../internal/harness.js"; export interface FileSnapshotError { /** `too_large` = the tree exceeded a bound (fail-CLOSED, NOT a silent partial snapshot); `not_found` = no * snapshot for that key; `aborted` = the signal fired; the rest = an underlying fs op failed. */ code: "too_large" | "enumerate_failed" | "read_failed" | "restore_failed" | "not_found" | "aborted"; message: string; } export type FileSnapshotResult = { ok: true; } | { ok: false; error: FileSnapshotError; }; /** * Capture/restore a working-tree file set keyed by `(scope, key)` — `scope` = sessionId, `key` = * `SessionTreeEntry.id` (same anchor as E18 resume-at). All methods are best-effort and NEVER throw — failures * are encoded in the returned {@link FileSnapshotResult} (mirrors the ExecutionEnv FileSystem contract). * * **v1 fidelity bounds (design/101 §defer):** files only — symlinks are NOT captured and are REMOVED by * `restore` (they are a path-escape vector); empty directories are not tracked; `restore` re-enumerates under * the SAME bounds, so a tree that grew past a bound since the snapshot cannot be rewound (fail-closed). */ export interface FileSnapshotStore { /** Capture the tree under `root` for `(scope, key)`. CREATE-ONCE: a second call for an existing key is a * no-op `{ok:true}` (a session entry id is immutable, so its tree state is too). */ snapshot(scope: string, key: string, env: ExecutionEnv, root: string, signal?: AbortSignal): Promise; /** Converge the tree under `root` back to the snapshot for `(scope, key)`: remove obstructions/strays * (symlinks + files created since), then write every captured file — so the tree MATCHES the snapshot. */ restore(scope: string, key: string, env: ExecutionEnv, root: string, signal?: AbortSignal): Promise; /** Whether a snapshot exists for `(scope, key)`. */ has(scope: string, key: string): Promise; /** GC: drop every snapshot in `scope` whose key is NOT in `keepKeys` (e.g. unreachable branches). Returns the * number of snapshots removed. */ reap(scope: string, keepKeys: string[]): Promise; /** Every snapshot key in `scope` (order unspecified). The enumeration counterpart of {@link reap} (which * already iterates the same set internally) — exposed for a cross-backend session EXPORT (service 2c * session-sync, [266]): list a session's snapshot keys to move its {entries + file snapshots} to another * backend. A durable backend implements it as a `SELECT key WHERE scope = ?`. */ listKeys(scope: string): Promise; /** * 2c session-sync ([271]): the manifest (`relPath → blobHash`) for `(scope, key)`, or `null` if absent — to * EXPORT a snapshot's content to another backend. Pairs with {@link getBlob} for CONTENT-ADDRESSED (deduped) * blob transfer: the caller fetches each distinct hash ONCE across all of a session's snapshots. Optional — a * backend that supports cross-backend snapshot export implements it. */ exportManifest?(scope: string, key: string): Promise | null>; /** 2c session-sync ([271]): the content-addressed bytes for `hash` (undefined if absent). */ getBlob?(hash: string): Promise; /** * 2c session-sync ([273]): STORE a snapshot INTO this store from another store's {@link exportManifest} + * {@link getBlob} output — the import-side mirror that closes the cross-backend transfer loop. For each DISTINCT * blob hash in `manifest`, fetch its bytes via `srcGetBlob`, VERIFY content-address integrity * (`sha256(bytes) === hash`), and store it (content-addressed dedup); THEN record `(scope, key) → manifest`. * * - CREATE-ONCE: a second import for an existing key is a no-op `{ok:true}` (an entry id is immutable). * - FAIL-CLOSED: a missing OR hash-mismatched source blob → `read_failed` and NO manifest is committed (never a * partial snapshot that a later {@link restore} would silently truncate). The manifest's relPaths are NOT * re-validated here — `restore` remains the path-escape gate (it already neutralizes unsafe relPaths). * - Pure store→store: does NOT touch an `ExecutionEnv` (unlike `applyManifest`, which converges ONE snapshot to a * working tree; import preserves ALL historical snapshots in the dst store so a later rewind-to-past finds them). * * Optional — a backend that supports cross-backend snapshot import implements it. */ importManifest?(scope: string, key: string, manifest: Map, srcGetBlob: (hash: string) => Promise): Promise; /** * 2c session-sync ([277]): STORE a single content-addressed blob — the symmetric WRITE side of {@link getBlob}, * for a two-phase PUSH (upload blobs, THEN import the manifest that references them). VERIFIES content-address * integrity (`sha256(bytes) === hash`) so a corrupt/mismatched upload can never poison the store (a later * getBlob/restore would otherwise return wrong content); a mismatch → `read_failed`, nothing stored. IMMUTABLE + * content-addressed: a repeat putBlob for the same hash is a no-op (the bytes are identical by definition). * Returns a {@link FileSnapshotResult} (NOT void) so the integrity failure is encodable under the never-throw * contract. Optional — a backend that supports a two-phase push implements it. * * ⚠️ ORCHESTRATION CAVEAT: a pushed blob is NOT yet referenced by any manifest (the matching * {@link importManifest} runs LATER), so — unlike `snapshot`/`importManifest`, which hold their blobs in an * `inFlight` live-set across the whole store-then-commit — a `putBlob`'d blob is reap-ELIGIBLE in the window * before its manifest is imported. The two-phase pusher MUST NOT `reap` the scope between PUT and import (a * grace-window). This is fail-closed, not corrupting: blobs are content-addressed (re-push is always safe) and * `importManifest` re-verifies+stores every blob before committing, so a reaped blob → `read_failed` with NO * dangling-reference manifest, never wrong content. */ putBlob?(hash: string, bytes: Uint8Array): Promise; } /** Bounds for the reference enumerator — a deliberate, fail-CLOSED cost policy (NOT the grep walk's silent caps). */ export interface FileSnapshotBounds { /** Hard cap on file count; exceeding it REFUSES the snapshot (`too_large`) rather than silently truncating. */ maxFiles: number; /** Hard cap on total bytes; exceeding it REFUSES the snapshot. */ maxBytes: number; /** Directory BASENAMES skipped anywhere in the tree (cost bound). Default `.git` + `node_modules`. */ ignoreDirs: Set; } export declare const DEFAULT_SNAPSHOT_BOUNDS: FileSnapshotBounds; /** * Capture the working tree under `root` into a manifest `relPath → sha256`, handing each file's bytes to * `putBlob` (the backend stores them content-addressed). **The env-operating + security-critical half a durable * backend MUST reuse** (not re-implement — duplication risks re-introducing the symlink-escape / fail-closed * bugs the dual-review fixed). Fail-CLOSED: an enumerate/read/bound failure returns an error and the caller must * NOT persist a partial manifest. Never throws. */ export declare function captureManifest(env: ExecutionEnv, root: string, bounds: FileSnapshotBounds, signal: AbortSignal | undefined, putBlob: (hash: string, bytes: Uint8Array) => void | Promise): Promise<{ ok: true; value: Map; } | { ok: false; error: FileSnapshotError; }>; /** * CONVERGE the tree under `root` to `manifest`, loading each captured file's bytes via `getBlob`. **The * env-operating + security-critical half a durable backend MUST reuse.** Order is load-bearing (codex BLOCKER + * Opus M1): PHASE 1 removes every current symlink (neutralizes a `dir -> /outside` ancestor that writeFile would * otherwise follow OUT of root) + every file created since; PHASE 2 writes each captured file, clearing a * directory that now occupies a file's path first. Never throws. */ export declare function applyManifest(env: ExecutionEnv, root: string, bounds: FileSnapshotBounds, signal: AbortSignal | undefined, manifest: Map, getBlob: (hash: string) => Uint8Array | undefined | Promise): Promise; /** * In-memory, content-addressed reference {@link FileSnapshotStore}. Blobs are deduplicated by sha256 ACROSS all * snapshots (a file unchanged between turns is stored once); each snapshot keeps a manifest `relPath → hash`. * Single-process (the default-deps reference; a durable/file-backed impl mirrors `src/stores/file/`). */ export declare class InMemoryFileSnapshotStore implements FileSnapshotStore { private readonly blobs; private readonly manifests; private readonly inFlight; private readonly bounds; constructor(bounds?: Partial); snapshot(scope: string, key: string, env: ExecutionEnv, root: string, signal?: AbortSignal): Promise; restore(scope: string, key: string, env: ExecutionEnv, root: string, signal?: AbortSignal): Promise; has(scope: string, key: string): Promise; listKeys(scope: string): Promise; exportManifest(scope: string, key: string): Promise | null>; getBlob(hash: string): Promise; putBlob(hash: string, bytes: Uint8Array): Promise; importManifest(scope: string, key: string, manifest: Map, srcGetBlob: (hash: string) => Promise): Promise; reap(scope: string, keepKeys: string[]): Promise; } //# sourceMappingURL=file-snapshot-store.d.ts.map