/** * store.ts, durable state for hosted sessions. * * A hosted session that is allowed to survive a client's departure must also * survive the daemon restarting, or "survive-detach" means "survives until the * next update swaps the binary". So the record and its conversation are written * to disk, and the platform's standing treatment for anything persisted applies * in full: * * - BOUNDS. At most `maxSessions` session files, and at most * `maxMessagesPerSession` messages kept per file (the TAIL is kept: a * conversation's recent turns are what a reattach needs). A session's file * cannot grow without limit, and the directory cannot either. * - CONTENT VALIDATION. Every file is validated on load against the shape the * engine can actually rebuild from. A file that fails is not silently * dropped and not half-restored: it is counted, named in the restore report, * and moved aside with a `.rejected` suffix so it can be inspected rather * than lost. * - SWEEP. Terminated sessions are retired once they are older than * `terminatedRetentionMs`; the sweep runs at init and on a timer. * - DISCLOSURE. `load()` returns exactly what happened, restored, rejected, * swept, and truncated counts, so the manager can state it rather than the * numbers living only in a log line nobody reads. * * Writes are atomic (tmp file + rename), so a crash mid-write leaves the * previous good file rather than a truncated one. */ import type { HostedSessionRecord } from './types.js'; /** The on-disk envelope. `version` is checked, not assumed. */ export interface PersistedHostedSession { readonly version: 1; readonly record: HostedSessionRecord; /** `ConversationManager.toJSON()`, replayed through `fromJSON` on restore. */ readonly conversation: unknown; } /** Bounds and retention, all configurable. */ export interface HostedSessionStoreLimits { /** Hard cap on persisted session files. Oldest terminated go first, then oldest idle. */ readonly maxSessions: number; /** Hard cap on messages persisted per session; the tail is kept. */ readonly maxMessagesPerSession: number; /** How long a terminated session's record is kept before it is retired. */ readonly terminatedRetentionMs: number; } /** What a load pass actually did, the disclosure half of the doctrine. */ export interface HostedSessionLoadReport { readonly restored: readonly PersistedHostedSession[]; /** Files that failed validation, by filename, with the reason. */ readonly rejected: readonly { readonly file: string; readonly reason: string; }[]; /** Sessions retired by the retention sweep, by id. */ readonly swept: readonly string[]; /** Sessions dropped because the directory was over `maxSessions`, by id. */ readonly evicted: readonly string[]; } /** * Validate one loaded file against the shape the engine can rebuild from. * Returns the reason it is unusable, or null when it is usable. */ export declare function describeInvalidPersistedHostedSession(value: unknown): string | null; /** Keep the last `max` entries of a message array. Returns the kept slice. */ export declare function boundMessages(messages: readonly unknown[], max: number): readonly unknown[]; /** * The disk store. One directory, one file per session, atomic writes. */ export declare class HostedSessionStore { private readonly directory; private readonly limits; constructor(directory: string, limits: HostedSessionStoreLimits); /** The directory this store owns, for status reporting. */ path(): string; private ensureDir; private fileFor; /** * Load every persisted session, sweeping retired ones and rejecting * unusable files. Never throws: an unreadable directory is an empty load * with the reason logged. */ load(now?: number): Promise; /** Whether a terminated record has outlived its retention. */ private isRetired; /** * Move an unusable file aside instead of deleting it. A rejected file the * operator can still read is the difference between "the engine told me it * dropped one and where it is" and "a session disappeared". */ private setAside; /** Atomically write one session. Bounds the conversation before writing. */ save(record: HostedSessionRecord, conversation: unknown): Promise; /** * Apply the per-session message bound to a `ConversationManager.toJSON()` * payload. Unknown shapes pass through untouched, the bound is a cap on a * known field, not a rewrite of a payload this store does not understand. */ boundConversation(conversation: unknown): unknown; /** Remove one session's file. Absent is success. */ delete(sessionId: string): Promise; /** * Retire terminated sessions past their retention. Returns the ids retired, * so the caller can drop them from its own map and say so. */ sweep(records: Iterable, now?: number): Promise; } //# sourceMappingURL=store.d.ts.map