UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

97 lines (96 loc) • 6.66 kB
import type { Session, SessionCreateOptions, SessionForkOptions, SessionMetadata, SessionPlacementRecord, SessionRepo, SessionTreeEntry } from "../../internal/harness.js"; import type { PlacedSessionRow } from "../../core/session.js"; import { type CorruptReadInfo } from "./corrupt-read-sink.js"; export interface FileSessionRepoOptions { /** * Disclosure sink for a durable read this repo treats as ABSENT (ruled 2026-08-03). Same name and * same `{path, reason}` shape as the sibling file stores' seat — "unreadable read as missing" and * "corrupt read as missing" are one failure grammar, so they get one seat name. * * Fires for: an unreadable sessions DIRECTORY (the listing degrades to "no sessions"), a session log * that could not be replayed during a listing (skipped from the result), and each corrupt interior JSONL * record dropped on replay plus that replay's tally (turns missing from an otherwise healthy session). * * Never fires on plain ENOENT / `not_found` — that is honest absence, and the torn TAIL stays silent * too (dying mid-write is the append log's documented crash-recovery shape, not corruption). * Swallow-guarded: a throwing sink cannot re-introduce the fault the fail-open avoids. * * `sessionId` rides every disclosure about ONE session — its log's skipped records and tally, and its log * skipped from a listing — naming the session the repo was reading (on a listing, the id its file name * spells); a disclosure about the sessions directory itself carries none. */ onCorruptRead?: (info: CorruptReadInfo & { sessionId?: string; }) => void; } export declare class FileSessionRepo implements SessionRepo { private readonly dir; private readonly tmpDir; private readonly onCorruptRead; /** RB-437 — the shared authorities THIS repo has joined, weakly (canonical key → the joined storage). * `dispose()` walks it and releases each holder share; the weak value is what makes the identity * check on the way out possible without pinning a storage the table itself only holds weakly. */ private readonly joined; constructor(root: string, opts?: FileSessionRepoOptions); /** The one delivery point for {@link FileSessionRepoOptions.onCorruptRead}'s whole-file level; `sessionId` when the file is one session's log. */ private disclose; private pathFor; /** * Read + torn-tail-recover a session file into (meta, entries). Three answers: no file → `not_found`; a * file with no meta header → `corrupt_session`, even when every other record is intact (every write path * here lays the header down first and the header alone carries `createdAt` / fork / placement, so the file * is not a session this store wrote, or its header line is damaged); otherwise the session, each skipped * record disclosed. */ private read; private storage; /** * RB-437 — release every shared authority this repo joined (LAST holder ⇒ evict + close the append fd). * * This is the lifecycle-tied half of the eviction rule the four file-REPLACING operations already * cover: after it, a NEW repo/backend over the same root replays each session from disk instead of * serving this process's pre-dispose in-memory leaf, and a Session handle held across the teardown * is refused with `code=log_closed` rather than writing onto a data root the process has released. * * Idempotent; safe to call on a repo that never opened anything. A repo whose authority was replaced * meanwhile (delete/fork/import/create-over-absent) releases NOTHING for that key — the identity check * means a stale holder can never revoke the rebuilt entry. */ dispose(): Promise<void>; create(options?: SessionCreateOptions): Promise<Session>; open(metadata: SessionMetadata): Promise<Session>; list(): Promise<SessionMetadata[]>; /** * Subagent transcript persistence — the PLACED partition's own enumeration (the joint reap's * partition-leg input; `TtlSessionStore` re-exposes it verbatim). Age (`olderThanMs`) is judged on * the session FILE's mtime — the transcript's last append — falling back to `placedAt` when the * stat fails (mtime is also the CC orphan-adoption window's basis, so the two age reads agree). * Rows missing either `(scope, handle)` join half return the honest `tupleIncomplete` variant * (consumers fail closed on it — never age-reaped, see the SessionStore contract). */ listPlaced(kind: "subagent", opts?: { olderThanMs?: number; scope?: string; }): Promise<PlacedSessionRow[]>; /** The row as it stands before a file-replacing write: `undefined` ⇔ there is no row (no file); a row * whose header cannot be read answers an empty header (it exists, its cells are unknown). */ private priorHeader; /** Cheap placement probe for one id (the TtlSessionStore's cold-cache release consults it so the * placed real-deletion obligation holds after a restart). `undefined` = missing file OR unplaced. */ placementOf(sessionId: string): Promise<SessionPlacementRecord | undefined>; delete(metadata: SessionMetadata): Promise<void>; fork(sourceMetadata: SessionMetadata, options?: SessionForkOptions): Promise<Session>; /** 2c session-sync (the FILE half): the FULL oldest-first log for export. The file backend * has NO F3 floor — `read` returns EVERY persisted entry (`id`/`parentId` verbatim) — so this is the complete * log, BYPASSing any bounded tail (matches `InMemorySessionRepo.exportEntries`). Missing file → not_found. */ exportEntries(sessionId: string): Promise<SessionTreeEntry[]>; /** 2c session-sync: import a verbatim log into `sessionId`. The single fail-closed invariant gate runs * FIRST ({@link validateEntriesForImport}); `owner` is a durable multi-tenant concept the local file backend * has no field for (accepted, unused — same as `InMemorySessionRepo`). Writes the WHOLE file (meta + entries) * atomically via tmp→fsync→rename — the rename is the commit point, so a crash leaves either nothing or an * orphan temp, never a partially-imported session (mirrors `fork`). An existing session is REPLACED (the * orchestrator's importSession owns the already-exists policy, same as InMemory's overwrite). */ importEntries(sessionId: string, owner: string | undefined, entries: SessionTreeEntry[], options?: { preserveActorAssertions?: boolean; }): Promise<void>; }