import type { ManagedSessionRow } from "../../db/repositories/managed-sessions.repository"; import type { ManagedSession } from "../../types"; /** * Boot rehydration (persistence plan Phase 1, gaps G1/G2/G8). * See docs/plans/live-sessions-persistence-plan.md §4. * * `recordShutdownState()` stamps `completed_at` on every live session as the * streamer stops, which takes the row out of the reconciler's probe set. So a * *cleanly* restarted session is not classified as anything — it simply * disappears from `GET /api/sessions`, because `SessionStore` starts empty and * nothing ever seeds it from the registry. * * This module is the decision half of the fix: which rows come back, and what * they look like when they do. Pure functions with no database import, mirroring * how `classifySession` is split out of `reconcileSessions` — the decision table * is the part worth testing, and it should not need a SQLite file to do it. * * The stub these produce holds no PTY and never reaches `LiveSessionManager`; * it lives in `SessionStore` alone, so the idle reaper and the grace timer * (both of which iterate `ptyManager.listSessions()`) cannot see it. */ /** Most recovered sessions to seed in one boot. */ export declare const REHYDRATE_MAX = 25; /** How far back a registry row may reach and still be offered as recoverable. */ export declare const REHYDRATE_WINDOW_MS: number; export interface ShouldRehydrateOptions { now: number; /** Whether the row's project directory still exists on disk. */ projectExists: (projectPath: string) => boolean; /** * Whether the conversation cache holds a row for this id. Omitted when there * is no cache, which leaves an unprompted row unprovable rather than empty. */ hasConversation?: (id: string) => boolean; } /** * Why a registry row was not brought back, or `null` when it was. * * A stable code rather than a sentence: it is logged per row and surfaced by * `GET /api/diagnostics/sessions`, where "my session did not come back" has to * be answerable without reading the source. */ export type RehydrateSkipReason = "never_prompted" | "codex_unbound" | "project_missing" | "too_old" | "agent_exited"; /** * Should this registry row come back as a recovered session? `null` means yes. * * Three ways to answer no, all of them "resuming this would waste the user's * tap": * * - the project directory is gone — `handleResume` would fail to spawn, and * `classifyResumability` already encodes the same rule for conversations; * - the row is older than the window — a week-old session belongs in the * conversations list, not the live-session list; * - the agent exited on its own without recording a failure — it finished, so * there is nothing to recover. A row that recorded a `failureReason` is * deliberately still offered: that one was cut short. * * Plus a fourth that is not a heuristic but an impossibility: a Codex row that * never bound its rollout id has no id that can resume it at all (G6). * * And a fifth, checked first because the caller deletes its row rather than * skipping it: a session that was never prompted and has no cached * conversation under any of its ids. Neither provider writes a transcript * before the first turn, so the stub would open on "No messages" and resume * into nothing. Same evidence the stop path's `shouldForgetEmptySession` uses. */ export declare function rehydrateSkipReason(row: ManagedSessionRow, opts: ShouldRehydrateOptions): RehydrateSkipReason | null; /** * Registry row → an idle, non-attached `ManagedSession` stub. * * `status` is `idle` rather than anything new: a fresh `SessionStatus` value * would be rejected by `VALID_STATUSES` on `?status=` and dropped by * `SessionStore.paginate`'s filter, making recovered sessions *vanish* from * already-shipped mobile clients. The recovered-ness travels on `rehydrated` * instead, which `managedToResponse` turns into the existing * `ownership: "historical"` / `lifecycle: "resumable"` pair. */ export declare function rowToStubSession(row: ManagedSessionRow): ManagedSession; //# sourceMappingURL=rehydrateSessions.d.ts.map