/** * Per-session provider/model memory. * * The parent route and the subagent route used to be one global choice: `/model` * and `/submodel` wrote a default that every later session inherited. Once a * child could run on a *different* supplier that stopped being enough — resuming * an old conversation silently re-ran it on whatever the last conversation used, * and there was no way to see which supplier had paid for the earlier one. * * Each session's effective route is recorded here, next to the TUI's other * per-session state under `$DSH_HOME`, and applied when that session starts * again. A new session still starts from the global default; the record only * ever overrides it for the session it belongs to, and never for another one. * * The record is a cache of a decision, not a source of truth: every field is * parsed defensively, and a route whose provider no longer exists is still * applied (the launcher reports the failure the same way it would for a CLI * flag). * * @module dsh-ssh-tui/session-route */ import { type SubagentSelectionRef } from './subagent-model.js'; /** Subagent route as this session ran it; no provider means "follows parent". */ export interface SessionSubagentRoute { provider?: string; model: string; reasoningEffort?: string; } /** One session's route: what `/model`, `/provider`, `/submodel` settled on. */ export interface SessionRoute { provider: string; model: string; reasoningEffort?: string; subagent?: SessionSubagentRoute; /** Epoch ms of the last write; the oldest records are pruned first. */ updatedAt: number; } /** * How many sessions keep a record. The file is a convenience cache, and a * hundred-odd conversations is more history than a resume picker offers. */ export declare const SESSION_ROUTE_LIMIT = 200; export declare function sessionRoutePath(dshHome?: string): string; /** Parse one record; anything that cannot name a route is dropped. */ export declare function parseSessionRoute(raw: unknown): SessionRoute | undefined; /** Parse the whole file; a file from another version reads as empty. */ export declare function parseSessionRoutes(raw: unknown): Map; export declare function loadSessionRoutes(path?: string): Promise>; /** The record for one session, or undefined when it never had one. */ export declare function loadSessionRoute(sessionId: string, path?: string): Promise; /** Keep the newest `limit` records, so the file cannot grow without bound. */ export declare function pruneSessionRoutes(entries: ReadonlyMap, limit?: number): Map; export declare function saveSessionRoutes(path: string, entries: ReadonlyMap): Promise; /** * Record one session's route, leaving every other session alone. * * Read-modify-write on purpose: two Hosts (two SSH windows) can be open at * once, and neither may drop the other's entry. */ export declare function saveSessionRoute(sessionId: string, route: Omit, path?: string): Promise; /** Whether two records describe the same route (the timestamp is ignored). */ export declare function sameSessionRoute(left: SessionRoute | undefined, right: Omit | undefined): boolean; /** * Shape a live parent route plus the subagent selection into a record. * * Shared by the TUI (which reports what its refs hold) and the launcher (which * writes it), so the two cannot disagree about what a session was running on. * An empty provider or model yields nothing: a record that cannot name a route * would come back as a lie. */ export declare function sessionRouteInput(input: { provider: string; model: string; reasoningEffort?: string; subagent?: { provider?: string; model: string; reasoningEffort?: string; }; }): Omit | undefined; /** * Providers the harness ships with. They are routable in any install even when * the adapter list is still warming up, so a record naming one is never treated * as stale on that basis alone. */ export declare const BUILTIN_ROUTABLE_PROVIDERS: readonly ["deepseek-official", "xai", "opencode-go", "opencode"]; /** * Whether this install can still route to a provider. * * Deliberately permissive: the cost of wrongly *keeping* a record is an error the * harness already reports clearly (an unknown route, the same one a launch flag * would hit), while wrongly *dropping* one silently loses the session's route — * which is the whole feature. So a provider counts as routable when it is a * built-in, appears in the adapter list, has a configured `llm-pi-ai` profile, or * is listed by the adapter's own model catalog. */ export declare function providerIsRoutable(input: { provider: string; /** Ids from `llm.listProviders()`. */ listed?: readonly string[]; /** Ids with an `llm-pi-ai.providers.` section. */ configured?: readonly string[]; }): boolean; /** What a resumed session's record is worth in this install. */ export interface RouteAvailabilityPlan { /** The parent route, when the provider it names can still be routed to. */ route?: SessionRoute; /** The subagent route, when its own pin can be routed to as well. */ subagent?: SessionSubagentRoute; /** Recorded parent provider that is gone; the caller says so on screen. */ droppedProvider?: string; /** Recorded subagent pin that is gone; the app-level setting takes over. */ droppedSubagentProvider?: string; } /** * Decide what of a recorded route still applies. * * A provider that no longer exists takes its whole record with it: the session * was configured around that supplier (its model, its effort, and the children * it pinned), so the honest answer is the app-level default for all of it rather * than a half-restored route. A pinned *child* provider that is gone is narrower: * the parent route is kept and the children fall back to the app-level subagent * setting. An inherited child route has no pin to check — it follows the parent, * which was just found routable. */ export declare function routeAvailabilityPlan(input: { recorded?: SessionRoute; routable: (provider: string) => boolean; }): RouteAvailabilityPlan; /** * Apply a resumed session's record to the live selection. * * Returns what the record was worth (see `routeAvailabilityPlan`) so the * launcher can say which route the session came back on — and, when a supplier * has been removed since, which one it fell back from. A new session is left * alone, and so is a resumed one that never had a record: both start from the * app-level defaults, which is the behaviour before any of this existed. * * The subagent route is applied in memory only: it belongs to the conversation * being resumed, and writing it back to the settings would make one session's * pin the default for every later session. */ export declare function restoreSessionRoute(input: { sessionId: string; resume: boolean; subagentSelection: SubagentSelectionRef; path?: string; /** Availability test; omitted means "everything is routable". */ routable?: (provider: string) => boolean; }): Promise; /** One remembered route as the launch waterfall sees it. */ export interface LaunchRouteSource { provider: string; model: string; reasoningEffort?: string; } export interface LaunchRouteInput { /** In-process change from `/model` or `/setup` earlier in this process. */ live?: LaunchRouteSource; /** Launch flags (`--provider` / `--model`). */ cli?: { provider?: string; model?: string; }; /** This session's own record, when the launch resumes it. */ session?: SessionRoute; /** Global default (the settings file, or `agent-default-model`). */ saved?: LaunchRouteSource; /** Last model per provider, used only when no user default section exists. */ remembered?: LaunchRouteSource; } export interface LaunchRoute { provider: string; model: string; reasoningEffort?: string; } /** * The route a launch should run on, in one place. * * Precedence: an in-process change, then launch flags, then the session's own * record, then the global default, then the last remembered route. A resumed * conversation therefore comes back on the supplier it was actually using, while * a new one still starts from the default — and an explicit flag still wins, * because that is this launch's ask. * * The reasoning effort travels with the route it was chosen for: it is taken * from whichever source supplied the provider/model, and only when the flags did * not move the route away from it. A CLI that changes either half must not * inherit the saved effort of a different model. */ export declare function resolveLaunchRoute(input: LaunchRouteInput): LaunchRoute;