@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
111 lines • 5.19 kB
TypeScript
import type { FileCacheMixin } from '../../cache/file/mixin.ts';
import { type OpRecord } from '../../observe/record.ts';
import { DriftPolicy } from '../../types.ts';
import type { MountEntry } from '../mount/mount.ts';
import type { FingerprintEntrySnapshot } from './types.ts';
/**
* Raised at load time when a remote VFS's live fingerprint differs
* from what was recorded in the snapshot.
*
* Indicates the underlying source has been modified since the snapshot
* was taken, so reading current bytes would silently diverge from what
* the original agent saw. Surface to the caller rather than mask.
*/
export declare class ContentDriftError extends Error {
readonly path: string;
readonly snapshotFingerprint: string;
readonly liveFingerprint: string | null;
constructor(path: string, snapshotFingerprint: string, liveFingerprint: string | null);
}
interface RegistryLike {
tryMountFor(path: string): MountEntry | null;
allMounts(): readonly MountEntry[];
}
export interface MountLookup {
tryMountFor(path: string): MountEntry | null;
}
/**
* Fingerprint checks a load queued, drained on the first async op.
*
* `Workspace.load` records one entry per read whose snapshot manifest
* carried a fingerprint but no stable revision (a pinned read needs no
* check: the pin guarantees the bytes). The first `dispatch` or
* `shell` drains them, so downstream code can rely on consistent
* state. Mirrors the Python `DriftQueue` in `snapshot/drift.py`.
*/
export declare class DriftQueue {
private entries;
private isPending;
get pending(): boolean;
/** Paths still queued for a check (audit surface). */
get paths(): string[];
/** Drop any queued state (a re-install starts fresh). */
clear(): void;
queue(path: string, fingerprint: string, mountId?: string | null): void;
/**
* Stat every queued path in parallel; throw on the first drift.
* Subsequent calls are no-ops. Stats run concurrently so first-op
* latency does not scale linearly with the number of recorded reads.
*/
drain(registry: MountLookup, statFn: (path: string) => Promise<unknown>): Promise<void>;
}
/**
* Walk a loaded snapshot's fingerprint manifest. For entries with a
* revision, install the pin on the owning mount so replay reads pin to
* that revision. For fingerprint-only entries, queue the path on the
* drift queue. OFF skips the checks and evicts the snapshot cache for
* fingerprinted paths so reads serve current state.
*
* Idempotent: clears queued state before installing. Called from
* `Workspace.fromState`.
*/
export declare function installFingerprints(registry: RegistryLike, cache: Pick<FileCacheMixin, 'remove'>, drift: DriftQueue, state: {
fingerprints?: FingerprintEntrySnapshot[];
live_only_mounts?: string[];
}, policy: DriftPolicy): Promise<void>;
/**
* Walk recorded ops and emit one pin per path still worth checking.
*
* A single forward pass over the time-ordered records, so the last word on
* a path wins. Three things can be that last word:
*
* - an op that removed or replaced the object (`RETRACT_FINGERPRINT_OPS`)
* drops the pin, and every pin beneath it — `rm -r` and a prefix rename
* take a subtree with them;
* - an op that changed the bytes without describing them — a write whose
* backend returned no token, or any `append` — drops the pin too,
* because the token on file no longer names what is there;
* - an op carrying a token (`STAMP_FINGERPRINT_OPS`) replaces the pin
* whole, never field-merging, so a read's revision cannot survive onto a
* later write's fingerprint and pin replay to pre-write bytes.
*
* A read that reported no token changes nothing and leaves the pin alone.
* Each token is what the backend returned at the moment the agent moved
* the bytes, not a fresh stat at snapshot time.
*
* Paths on a mount that opts out of snapshot replay are never pinned; a
* retraction still applies to them, because dropping a pin is the safe
* direction and the mount a retraction names may no longer be the one that
* set the pin.
*/
export declare function captureFingerprints(records: readonly OpRecord[], registry: RegistryLike): FingerprintEntrySnapshot[];
/**
* Return mount prefixes whose VFS opts out of snapshot replay.
*
* These mounts will serve current state at load time with no drift
* detection. Surfaced in the snapshot manifest so the load layer can
* log them and so users can audit which paths are non-replayable.
*/
export declare function liveOnlyMountPrefixes(registry: RegistryLike): string[];
/**
* Stat `path` and throw {@link ContentDriftError} if the live fingerprint
* does not match `recorded`. No-op if the mount cannot be resolved or the
* VFS cannot fingerprint.
*
* The caller provides `statFn` (typically a thin wrapper over
* {@link Workspace.dispatch}) so that drift.ts stays decoupled from the
* workspace's op-resolution machinery.
*/
export declare function checkDrift(registry: MountLookup, statFn: (path: string) => Promise<unknown>, path: string, recorded: string, mountId?: string | null): Promise<void>;
export {};
//# sourceMappingURL=drift.d.ts.map