/** * store.ts, persistence for the trigger family, with recovery housekeeping. * * Anything persisted across restarts and crashes has to do real housekeeping at * recovery, not just load. This store does all five: * * 1. Reap on recovery, triggers whose owning session is gone are removed, * and a one-shot on-exit trigger that already fired retires itself. * 2. Bound everything, count cap AND age TTL on run history, observations * and the shared event log. An unbounded append-only store is a leak with * a nicer name. * 3. Validate by content, never by existence, the snapshot carries a * checksum written last; a torn, truncated or zero-filled file fails the * checksum and is quarantined instead of being served as good state. * 4. Reap periodically, the supervisor runs the same sweep on a timer, * because a long-lived daemon that only sweeps at boot never sweeps. * 5. Disclose what was reaped, every sweep returns a report and the startup * sweep writes `triggers-reaped.json`. Silent deletion is indistinguishable * from data loss. * * Writes go to a temp file and are renamed into place, so a crash mid-write * leaves the previous good snapshot rather than a half-written one. */ import type { TriggerActionGrant, TriggerEventLogEntry, TriggerRecord, TriggerRecoveryReport } from './types.js'; export declare const TRIGGER_STORE_VERSION = 1; export interface TriggerStoreSnapshot { readonly version: typeof TRIGGER_STORE_VERSION; readonly daemonBootId: string; readonly savedAt: number; readonly triggers: readonly TriggerRecord[]; readonly grants: readonly TriggerActionGrant[]; readonly eventLog: readonly TriggerEventLogEntry[]; /** sha256 over the canonical body, written last. Absent = torn write. */ readonly checksum: string; } export interface TriggerRetentionPolicy { readonly observationRingSize: number; readonly runHistoryLimit: number; readonly runHistoryTtlMs: number; readonly eventLogLimit: number; readonly eventLogTtlMs: number; } export declare const DEFAULT_RETENTION: TriggerRetentionPolicy; export declare function getTriggerStorePath(rootPath: string): string; export declare function getTriggerReapReportPath(storePath: string): string; interface SnapshotBody { readonly version: number; readonly daemonBootId: string; readonly savedAt: number; readonly triggers: readonly TriggerRecord[]; readonly grants: readonly TriggerActionGrant[]; readonly eventLog: readonly TriggerEventLogEntry[]; } export declare function checksumOf(body: SnapshotBody): string; /** * Content validation. `existsSync` proves nothing: a crashed run can leave a * full-size file of zeros that an existence check happily treats as complete. * We re-derive the checksum and require every record to parse into shape. */ export declare function validateSnapshot(parsed: unknown): TriggerStoreSnapshot | { readonly invalid: string; }; export declare function saveTriggerSnapshot(storePath: string, input: { readonly daemonBootId: string; readonly triggers: readonly TriggerRecord[]; readonly grants: readonly TriggerActionGrant[]; readonly eventLog: readonly TriggerEventLogEntry[]; readonly now?: number | undefined; }): void; export interface LoadedTriggerStore { readonly snapshot: TriggerStoreSnapshot | null; /** Set when the on-disk state failed content validation and was set aside. */ readonly quarantined?: string | undefined; } /** * Loads and content-validates. A file that fails validation is renamed aside * rather than deleted (so it can be inspected) and the caller starts clean. */ export declare function loadTriggerSnapshot(storePath: string): LoadedTriggerStore; export interface BoundResult { readonly record: TriggerRecord; readonly runsReaped: number; readonly observationsReaped: number; } /** Applies the count cap AND the age TTL to one record's retained state. */ export declare function boundRecord(record: TriggerRecord, policy: TriggerRetentionPolicy, now: number): BoundResult; export declare function boundEventLog(eventLog: readonly TriggerEventLogEntry[], policy: TriggerRetentionPolicy, now: number): { readonly eventLog: readonly TriggerEventLogEntry[]; readonly reaped: number; }; export interface SweepInput { readonly triggers: readonly TriggerRecord[]; readonly eventLog: readonly TriggerEventLogEntry[]; readonly policy: TriggerRetentionPolicy; readonly now: number; readonly reason: 'startup' | 'sweep'; /** Returns false when the session that created a trigger no longer exists. */ readonly sessionIsLive?: ((sessionId: string) => boolean) | undefined; /** Returns false when a tracked child process is no longer running. */ readonly processIsLive?: ((pid: number, startedAt: number) => boolean) | undefined; readonly quarantined?: string | undefined; } export interface SweepResult { readonly triggers: readonly TriggerRecord[]; readonly eventLog: readonly TriggerEventLogEntry[]; readonly report: TriggerRecoveryReport; } /** * The recovery sweep. Idempotent and safe to run concurrently from more than * one process: it only ever removes records that are already terminal or whose * owner is provably gone, and running it twice produces the same result. */ export declare function sweepTriggers(input: SweepInput): SweepResult; /** * Discloses the sweep. Mirrors the checkpoint adoption path's * `checkpoints-moved.json`: whatever was removed is written down where a person * can find it, so a reap never looks like data loss. */ export declare function writeReapReport(storePath: string, report: TriggerRecoveryReport): void; export {}; //# sourceMappingURL=store.d.ts.map