UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

67 lines (66 loc) • 4.11 kB
import { type BackgroundAgentReapOptions, type BackgroundAgentRecord, type BackgroundAgentRowSummary, type BackgroundAgentStore, type BackgroundAgentUpdateIfExpect } from "../../core/background-agent-store.js"; import { type CorruptReadInfo } from "./corrupt-read-sink.js"; export interface FileBackgroundAgentStoreOptions { /** This store's half of {@link import("./shared-ledger.js").LedgerPaths.onCorruptRead} — the same seat, * over the agent row model. */ onCorruptRead?: (info: CorruptReadInfo) => void; /** When false, a ledger append is NOT fsync'd. 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 FileBackgroundAgentStore implements BackgroundAgentStore { private readonly fsyncEnabled; private readonly compactEvery; /** RB-55: all mutable state is SHARED per canonical directory (see the note above the class). */ private readonly ledger; /** RB-145: this instance's own close latch — the refcount belongs to the DIRECTORY, not to callers. */ private closed; private get rows(); constructor(root: string, opts?: FileBackgroundAgentStoreOptions); /** * RB-60 (2026-07-25, red probe): the composite key must be INJECTIVE — `get`/`update` no longer re-check * scope, so tenant isolation rests entirely on this function. A SPACE separator is not injective * (`scope="a", handle="b c"` and `scope="a b", handle="c"` produce the same key), which let one tenant's * row collide with another's. The in-memory reference implementation (`core/background-agent-store.ts`) * uses `\u0000` for exactly this reason ("the composite key keeps the info-hiding property structural") — * a NUL can never occur in a scope or handle, so the mapping is injective. Matching it also restores this * backend's stated "byte-for-byte the InMemory reference implementation's" claim. * * MIGRATION: the key is IN-MEMORY only (the ledger stores whole records with their own `scope`/`handle` * fields, and replay rebuilds keys through this function), so changing the separator needs no on-disk * migration — an existing directory replays into the new key space on next open. */ static key(handle: string, scope: string): string; private withLock; /** Durable append FIRST, in-memory flip AFTER, then best-effort compaction — all in the base * (RB-150/RB-167: a failing housekeeping compaction must never be reported as this CAS failing). */ private commit; put(record: BackgroundAgentRecord): Promise<void>; get(handle: string, scope: string): Promise<BackgroundAgentRecord | null>; update(handle: string, scope: string, record: BackgroundAgentRecord, expect: { rev: number; }): Promise<boolean>; updateIf(handle: string, scope: string, record: BackgroundAgentRecord, expect: BackgroundAgentUpdateIfExpect): Promise<boolean>; listBySession(scope: string, sessionId: string, opts?: { status?: BackgroundAgentRecord["status"]; limit?: number; }): Promise<BackgroundAgentRowSummary[]>; listByScope(scope: string, opts?: { status?: BackgroundAgentRecord["status"]; limit?: number; }): Promise<BackgroundAgentRowSummary[]>; listScopes(): Promise<string[]>; delete(handle: string, scope: string, expect?: { rev: number; }): Promise<boolean>; reap(scope: string, now: number, opts?: BackgroundAgentReapOptions): Promise<number>; /** Force a compaction now (test/inspection). RB-167's closeForSwap recipe lives in the base. */ compactNow(): void; /** Test/inspection helper: number of stored rows. */ get size(): number; /** Release the append handle (best-effort). RB-55: refcounted — the directory's shared authority * (rows/locks/log) is torn down only when the LAST instance over it closes, so a sibling instance * is never left holding a closed fd or a half-dropped view. */ close(): void; }