@sema-agent/core
Version:
Stateless, task-oriented AI agent core
67 lines (66 loc) • 4.11 kB
TypeScript
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;
}