@sema-agent/core
Version:
Stateless, task-oriented AI agent core
89 lines (88 loc) • 6.23 kB
TypeScript
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 this backend holds is the JSON
* PROJECTION of the row — a `Date` reads as its ISO string, a `Map`/`Set` as `{}`, `NaN` as `null`, an
* `undefined`-valued key not at all — before a restart and after it alike (the live map is filled from the
* same JSON reading as the ledger line); the declaration is what makes the loss 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;
}