/** * How stale a thread's newest checkpoint must be before automatic reclamation will touch it. * * Not a tidy-up margin: after a `/clear` a live session writes every remaining checkpoint under a * thread no conversation row names, and another process cannot be asked whether it is still there. * A day is far longer than the gap between two super-steps of a session anyone is still using, * while still reclaiming the state within a day of the session ending. */ export declare const RECLAIM_GRACE_MS: number; /** How many threads the readout names individually. */ export declare const DEFAULT_TOP_THREADS = 5; /** One thread's footprint in the store. */ export interface ThreadUsage { threadId: string; /** The conversation that names this thread, when one does — absent means unaddressable. */ conversationId?: number; command?: string; checkpointCount: number; /** Bytes of checkpoint / metadata / pending-write blobs stored under the thread. */ bytes: number; /** The `ts` of the newest checkpoint, when it could be read. */ newestTs?: string; } /** What the store's checkpoint state holds, for the readout. */ export interface CheckpointStoreStats { /** The store directory. */ dbPath: string; /** * Bytes the whole store occupies on disk — the index (transcripts and the search index) and every * thread file — so it is reported beside {@link checkpointBytes} rather than instead of it. Zero * when nothing is there. */ fileBytes: number; /** Bytes of checkpoint, metadata and pending-write blobs — the checkpoint state's own share. */ checkpointBytes: number; checkpointCount: number; writeCount: number; threadCount: number; /** The biggest threads by stored bytes, largest first. */ largestThreads: ThreadUsage[]; /** Threads no conversation row names, and what they cost — what reclamation will take. */ unresumableThreadCount: number; unresumableBytes: number; /** * Threads holding pending writes and no checkpoint, and what they cost. Counted inside * {@link checkpointBytes}; reported apart because only `gth history prune` removes them. */ writeOnlyThreadCount: number; writeOnlyBytes: number; } /** What one {@link reclaimUnresumableThreads} / {@link removeThreadState} pass removed. */ export interface ReclaimSummary { threadCount: number; checkpointCount: number; writeCount: number; bytes: number; } /** A conversation `gth history prune` would remove the stored state of. */ export interface PrunableConversation { conversationId: number; threadId: string; command?: string; /** Last recorded activity: the newest turn's timestamp, or the conversation's start. */ lastActivityTs: string; turnCount: number; checkpointCount: number; bytes: number; /** * True when the last recorded turn is newer than {@link RECLAIM_GRACE_MS} — so this conversation * may be **open in another window right now**, and pruning it would take a live session's memory * rather than an old one's. * * A flag and not an exclusion, deliberately. `gth history prune` is given an explicit bound by the * person typing it, and silently keeping back rows inside that bound would make the bound a lie; * liveness across processes is also not knowable from here, so a refusal would be a guess wearing * a guarantee. The automatic pass can be conservative because nobody asked for it. This one says * what it is about to do and lets the person answer. */ recentlyActive: boolean; } /** Bounds for {@link selectPrunableConversations}. At least one is required by the command. */ export interface PruneBounds { /** Prune conversations whose last activity is older than this many days. */ olderThanDays?: number; /** * Keep the N most recently active resumable conversations **whole** and prune the rest. * * Deliberately NOT "keep the last N super-steps of each thread": that is the prefix policy this * module forbids, and a flag whose name suggested it would be a documentation hazard. */ keepLast?: number; /** Injectable clock for the age bound. */ now?: number; } /** * Every thread a conversation row names — the half of the link the predicate reads. Named as one * constant so a spec can hold the query this runs. */ export declare const NAMED_THREADS_SQL = "SELECT DISTINCT thread_id FROM conversations\n WHERE thread_id IS NOT NULL AND thread_id <> ''"; /** * Every thread with checkpoints that **no conversation row names** — see the module note for why * that one predicate covers both unresumable classes. * * `includeWithinGrace: true` answers "what is unaddressable", which is what the readout reports; * the default answers "what may be removed now", which additionally requires the thread's newest * checkpoint to be older than {@link RECLAIM_GRACE_MS}. Never throws; `[]` when the store has no * index to read the conversation rows from. */ export declare function findUnaddressableThreads(storePath: string, options?: { now?: number; graceMs?: number; includeWithinGrace?: boolean; excludeThreadIds?: readonly string[]; }): string[]; /** * GS2-108 — the second predicate: every thread file holding pending writes and no checkpoint — see * the module note for why the conversation-link predicate cannot see one. Never throws. * * The residual, stated rather than engineered around: another process can hold a thread that is * momentarily write-only, between the checkpoint write for a super-step and the writes of its * tasks. Taking those rows costs that conversation a re-run of one super-step's tasks on a later * resume. `excludeThreadIds` is here so an in-process caller could supply protection; no production * caller does, because the only ones are the readout and `gth history prune`, which write no * checkpoints of their own. */ export declare function findWriteOnlyThreads(storePath: string, options?: { excludeThreadIds?: readonly string[]; }): string[]; /** * Remove every checkpoint and pending write of the named threads, and report what went. Each * thread's file is deleted whole; a file that also holds a conversation record is stripped of its * checkpoint state inside one transaction (both tables or neither) and vacuumed instead. See the * module note. * * `beforeRemove` is called with each thread id before its file is touched, so a caller holding a * connection to it can close it first: on win32 an open file cannot be deleted. A file that still * cannot be deleted is stripped instead, so its bytes are released either way. * * Never throws. A thread whose removal failed is not counted. */ export declare function removeThreadState(storePath: string, threadIds: readonly string[], options?: { beforeRemove?: (threadId: string) => void; }): ReclaimSummary; /** * The automatic half of the policy: remove the state of every thread no conversation row names and * whose newest checkpoint is past the grace window. Never throws, and never touches a thread a * resume could reach. */ export declare function reclaimUnresumableThreads(storePath: string, options?: { now?: number; graceMs?: number; excludeThreadIds?: readonly string[]; beforeRemove?: (threadId: string) => void; }): ReclaimSummary; /** * The conversations `gth history prune` would remove the stored state of, newest activity first. * * A conversation qualifies when it still names a thread that has checkpoints (there is something to * remove) and it satisfies **every** bound given. `olderThanDays` and `keepLast` therefore compose * as a conjunction: with both, a conversation is pruned only when it is older than the age AND * outside the newest N. Passing neither selects nothing — the command requires an explicit bound, * so that there is no silent default for an operation that can cost a resume. * * **Neither of the automatic pass's two guards applies here, and that is the design.** The bounds * the person typed are the whole selection; what this does instead is *say so* — * {@link PrunableConversation.recentlyActive} marks every candidate whose last turn is inside * {@link RECLAIM_GRACE_MS}, the plan prints that marker, and the command asks before removing * anything. */ export declare function selectPrunableConversations(storePath: string, bounds: PruneBounds): PrunableConversation[]; /** * The readout: what the store's checkpoint state holds, and what of it is unaddressable. * * `fileBytes` and `checkpointBytes` are reported separately on purpose — the store also holds the * session transcripts (twice: in the index, and in each record file) and the search index, so one * number labelled "checkpoints" that is really the whole store would be a false statement on the * very screen this exists to make honest. */ export declare function collectCheckpointStoreStats(storePath: string, topN?: number): CheckpointStoreStats; /** * The maintenance commands' handle on one store, or `null` when there is nothing there. Fail-soft * in the same shape as `openHistoryStore` / `openCheckpointSaver`, and it never CREATES a store: * `gth history prune` on a machine with no history should say there is none, not leave an empty * store behind. It does split a single-file store, like every other open. */ export declare class CheckpointMaintenance { private readonly storePath; private constructor(); static open(storePath: string): CheckpointMaintenance | null; stats(topN?: number): CheckpointStoreStats; prunable(bounds: PruneBounds): PrunableConversation[]; unaddressable(options?: { now?: number; graceMs?: number; }): string[]; /** GS2-108 — threads holding pending writes and no checkpoint, which only a prune reclaims. */ writeOnly(options?: { excludeThreadIds?: readonly string[]; }): string[]; remove(threadIds: readonly string[]): ReclaimSummary; /** Bytes stored under exactly these threads — what removing them would reclaim. */ bytesOf(threadIds: readonly string[]): number; /** The bytes the store occupies on disk, for the before/after of a prune. */ diskBytes(): number; close(): void; } /** Fail-soft opener for {@link CheckpointMaintenance}; `null` when there is no store to maintain. */ export declare function openCheckpointMaintenance(storePath: string): CheckpointMaintenance | null;