UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

89 lines (88 loc) • 6.21 kB
import { type ExecutionOutcomeRecordWord } from "../../core/checkpoint-execution-record.js"; import { type Checkpoint, type PendingSteerInput, type CheckpointFaultMode, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type ReopenReason, type ResolveExpectation, type ResumeOutcome } from "../../core/checkpoint-store.js"; import type { GateOutcome } from "../../core/gate-outcome.js"; import { type CorruptReadInfo } from "./corrupt-read-sink.js"; export interface FileCheckpointStoreOptions { /** This store's half of {@link import("./shared-ledger.js").LedgerPaths.onCorruptRead}. */ onCorruptRead?: (info: CorruptReadInfo) => void; /** When false, an `appendLine` for a state transition is NOT fsync'd. The checkpoint COMMIT POINT always * fsyncs regardless (its crash-safety depends on it); this governs the `put` and `setPendingSteer` * (steer-append) commits only — `resolve`/`reopen`/`expire` fsync unconditionally. Default true. */ fsync?: boolean; /** Compact the ledger into a snapshot once it exceeds this many events (then truncate). Default 1000. */ compactEvery?: number; } export declare class FileCheckpointStore implements CheckpointStore { /** design/173 §2.3 — honest declaration on the restart-survival axis the vocabulary claims: rows * live on disk (fsync'd append log) and survive a process restart. Multi-replica coordination is * NOT claimed by this axis (see {@link StoreDurability}) — this backend is deliberately * single-instance-per-data-dir, and now SELF-ENFORCING about it: the constructor takes the ledger * directory's writer fence and refuses a second OS process by name (see the class header). Serving * many writers at once remains Pg/TiDB's job — this refuses the second one, it does not coordinate it. */ readonly durability: "durable"; /** Honest declaration on the fidelity axis: the ledger is JSONL, so what survives the restart this * backend promises is the JSON PROJECTION of the row — a `Date` replays as its ISO string, a * `Map`/`Set` as `{}`, `NaN` as `null`. The in-process map holds the structured clone, which is why * the loss is invisible until something reads the disk; the declaration is what makes it visible to * the mint BEFORE the row is filed. */ readonly fidelity: "json"; /** F-012 L2: `reopen` here is a real resolved→pending CAS journaled to the ledger — declared, not sniffed. */ readonly redecision: { readonly reopen: true; }; /** The execution-outcome record verb below is the contract's CAS (journaled), and `reopen` honours the record. */ readonly execution: { readonly outcome: true; }; private readonly fsyncEnabled; private readonly compactEvery; /** RB-134: the directory's ONE authority (map + token mutex + append log), joined not rebuilt. */ private readonly ledger; private closed; private get cps(); /** One-shot crash fault (mirrors `InMemoryCheckpointStore.testInjectFault`). */ private fault; constructor(root: string, opts?: FileCheckpointStoreOptions); /** Serialize an op behind any in-flight op on the same token (the in-process per-token async mutex). */ private withLock; /** * Commit one ledger event (append + fsync) THEN flip the in-memory map (crash-safe ordering, §2.4). * * RB-150 (2026-07-25, cross-backend differential): the compaction that may follow is HOUSEKEEPING and * runs AFTER the operation is durable and the memory flip has happened — so its failure must never be * reported as the operation's failure. It was: a snapshot write hitting EACCES made `resolve()` REJECT * while the row was already `resolved` and the winner recorded, i.e. a consumed approval presented to * the caller as "this did not happen" — the worst possible direction for a once-only gate, and something * the in-memory reference cannot do because it has no I/O here. Both halves are now in the base. */ private commit; put(token: CheckpointToken, cp: Checkpoint): Promise<void>; get(token: CheckpointToken): Promise<Checkpoint | null>; resolve(token: CheckpointToken, scope: string, outcome: ResumeOutcome, expect?: ResolveExpectation): Promise<boolean>; recordExecutionOutcome(token: CheckpointToken, scope: string, gate: GateOutcome): Promise<ExecutionOutcomeRecordWord>; reopen(token: CheckpointToken, scope: string, reason: ReopenReason): Promise<boolean>; setPendingSteer(token: CheckpointToken, scope: string, steer: PendingSteerInput): Promise<boolean>; expire(token: CheckpointToken, scope: string): Promise<boolean>; reap(scope: string, cutoff: number): Promise<number>; listByScope(scope: string): Promise<CheckpointSummary[]>; listScopes(): Promise<string[]>; /** Force a compaction now (test/inspection). The snapshot rewrite + RB-167 closeForSwap swap that * makes `replay` read the full state from the snapshot alone is the base's {@link LedgerCore.compact}. */ compactNow(): void; /** Arm a one-shot fault on the next `resolve` (mirrors InMemory `testInjectFault`, council #5). */ testInjectFault(mode: CheckpointFaultMode | null): void; /** Test/inspection helper: number of stored checkpoints. */ get size(): number; /** * Release the append handle (best-effort). The LAST holder over the directory also drops its * cross-process writer fence, so a successor process can open the same data root; the data-dir-wide * `root/LOCK` (a different fence) stays the backend factory's to release. * * RB-134: refcounted, and the LAST holder REVOKES the directory's authority. Adding the shared table * without this would have repeated RB-73's mistake exactly — a cache with no invalidation: after a * `close()` the next `new FileCheckpointStore(sameRoot)` joined a table whose log was already closed and * every write threw `log_closed`, instead of replaying the on-disk truth into a fresh authority. Idempotent: * a second `close()` on the same instance must not decrement someone else's share. */ close(): void; }