export interface KVStateOptions { readonly sessionId?: string | undefined; readonly stateDir: string; /** * A legacy, unscoped stateDir to fall back to for reads when the scoped * `stateDir` has no file yet for this session id (dual-read, one release * only, see the session-surface migration, runtime/session-migration.ts). * Consulted ONLY when the scoped file is absent; a hit is copied forward * into the scoped location on the next persist, so subsequent reads never * need the fallback again. Copy-forward never moves or deletes its source, so * this directory's `session_*.json` files would otherwise strand there * permanently, nothing else in the SDK reclaims them. The housekeeping sweep * therefore applies the AGE bound (and only the age bound) here as well; see * `KVState.sweep` for why a count bound would be unsafe in a directory that is * shared with other products. * * A legacy file that cannot be read or parsed, or that parses to something * other than a state object, is treated as ABSENT (logged, then ignored): the * fallback may only ever recover data, never turn junk in the old unscoped * directory into a failure for a session that would otherwise have started * clean. */ readonly legacyStateDir?: string | undefined; } /** * KVState, Session-scoped persistent key-value store. * * Storage: /session_{id}.json * Session ID: 8-char hex string, auto-generated if not provided. * * Features: * - Lazy load: defers disk read until first operation. * - Atomic persistence: write to temp file then rename. * - Debounced auto-persist: 5-second timer after each set(). * - Bounded store: every session file older than SESSION_MAX_AGE_MS, and every * file past the SESSION_KEEP_COUNT most recent, is reclaimed at recovery and * then on a SWEEP_INTERVAL_MS timer. The instance's own file is exempt. */ export declare class KVState { private sessionId; private stateDir; private filePath; /** Basename of this instance's own file, the one name housekeeping must never touch. */ private readonly fileName; private data; private persistTimer; private loadPromise; private readonly store; /** Legacy unscoped store to fall back to for reads only; undefined when no legacyStateDir was given or it is identical to the scoped stateDir. */ private readonly legacyStore; /** The legacy unscoped state DIRECTORY, kept for the age-bounded sweep; undefined whenever legacyStore is. */ private readonly legacyStateDir; private sweepTimer; private housekeepingStarted; /** Whole-file writes run one at a time, in call order. See StoreWriteQueue. */ private readonly writes; constructor(options: KVStateOptions); get(keys: string[]): Promise>; set(values: Record): Promise; list(prefix?: string): Promise>; clear(keys: string[]): Promise; load(): Promise; /** * Write the session file, after every write already queued has finished. * * There are two writers and nothing ordered them: the 5-second debounce armed * by `set`/`clear`, and `dispose()`, which clears the timer and writes * directly. Clearing a timer that has ALREADY fired stops nothing, so an * agent that finishes just after a debounced write started has two writes in * flight, and `JsonFileStore.save` renames atomically without saying which * rename lands last. Unordered, the earlier write's older view can land second * and put back a key `clear` removed, which a resumed session then reads as * still set. * * `this.data` is passed by reference, as it always was: `save` serialises it * when the write RUNS, so each queued write emits a view at least as new as * the one before it. Only the order was missing. */ persist(): Promise; getSessionId(): string; static listSessions(options: Pick): string[]; /** * Count-bounded reap of a state directory, on demand. * * This is NOT what keeps the store bounded, for a long time nothing called * it, and every session file the SDK had ever written accumulated forever. * The bounds are now enforced by each instance's own housekeeping (see * {@link KVState.load}), which runs both this count bound and an age bound at * recovery and then on a timer. This static form survives for callers that * want an explicit count-only pass, and now shares the same idempotent, * concurrency-tolerant, disclosed implementation instead of its own * unguarded `unlinkSync`. * * No file is exempt here: the caller names the directory and the keep count, * and nothing in a bare static call identifies a "current" session. */ static cleanupOldSessions(keepCount: number, options: Pick): void; dispose(): Promise; /** * Validate a loaded session file by its parsed SHAPE, not by the file having * existed and parsed. * * JsonFileStore already rejects bytes that are not JSON at all, which covers a * zero-byte file, a truncated object and a page of NULs. It does not cover * bytes that parse to something that is not a state record: `null`, `[]`, * `123`, `"…"`. A crash can leave any of those, and every one of them would * otherwise be installed as `this.data` and served to callers as if it were * their session state, `list()` would spread an array, `get()` would read * properties off a number. * * `onInvalid: 'throw'` is used for the SCOPED file, matching this class's * existing contract that a corrupt current-session file is a hard failure * rather than a silently substituted blank session. `'absent'` is used for the * LEGACY fallback, matching its documented rule that the fallback may only * ever recover data, never turn junk in the old unscoped directory into a * failure for a session that would otherwise have started clean. */ private validateLoaded; /** * Start this instance's housekeeping: one pass now, then a pass every * {@link SWEEP_INTERVAL_MS}. * * The timer exists because startup-only housekeeping in a process that stays * up for days reclaims nothing after its first minute, a long-lived surface * spawning agents all week would cross both bounds without ever restarting. * It is unref'd, so it can never be the reason a process refuses to exit, and * `dispose()` clears it. * * Runs at most once per instance. Several KVState instances in one process may * point at the same directory (the surface's own, plus one per agent); every * pass is idempotent and race-tolerant, so the overlap costs a directory * listing and nothing else. */ private startHousekeeping; /** * One housekeeping pass over the scoped directory and, when configured, the * legacy unscoped one. * * The legacy directory gets the AGE bound only, never the count bound. That * directory is unscoped and therefore SHARED: a second product working in the * same working directory dual-reads its own `session_.json` out of it and * has not necessarily copied it forward yet. A count bound orders files by * recency across all of those products at once, so it could delete a * two-day-old file belonging to a session another surface resumes tomorrow. * The age bound cannot: a file untouched for the full TTL belongs to a session * no surface has resumed in that whole window, and the dual-read only ever * fires for the exact session id being resumed. Leaving the legacy directory * entirely unswept was the other option and is not acceptable, copy-forward * never deletes the source, so those files strand there permanently and * nothing else in the SDK reclaims them. * * Never throws: a housekeeping problem must not become a failure of the * session that triggered it. */ private sweep; private ensureLoaded; private schedulePersist; private static generateId; } //# sourceMappingURL=kv-state.d.ts.map