import { type Conversation } from "@threadbase-sh/scanner"; import type { IncomingMessage, ServerResponse } from "http"; import { ConversationCache } from "../../conversation-cache"; import type { LiveSessionManager } from "../../live-session-manager"; import type { Logger } from "../../logger"; import type { ScannerManager, ScanProfile } from "../../scanner-manager"; import type { SessionStore } from "../../session-store"; import type { ServerWarmupState } from "../../types"; import type { WSHub } from "../../ws-hub"; /** * Everything ConversationHandlers reads from the server. Collaborators * constructed once in the server constructor are passed by reference; anything * bound later (the cache opens during listen() and is rebound by the integrity * monitor's reset-and-rescan) or swapped by tests (`log`) is a thunk, for the * same reason ApiDeps passes `cache: () => ConversationCache | null`. * * The warm-up gate, the cache-write tracker and the three conversation-id * resolvers stay late-bound calls back into the server rather than moved code: * they read state (`activeWarmups`, `inFlightCacheWrites`, `sessionFileMap`) * that spans well beyond conversations. */ export type ConversationHandlersDeps = { includeSubagentSessions?: () => boolean; scannerManager: ScannerManager; sessionStore: SessionStore; ptyManager: LiveSessionManager; wsHub: WSHub; scanProfiles: ScanProfile[] | undefined; cache: () => ConversationCache | null; log: () => Logger; rejectIfWarmingUp: (res: ServerResponse) => boolean; withWarmup: (state: ServerWarmupState, operation: () => Promise) => Promise; trackCacheWrite: (task: Promise) => void; resolveConversationLookupId: (uuid: string) => string; findLiveSessionFilePath: (uuid: string) => string | null; isBoundConversationLive: (boundId: string) => boolean; }; /** * The conversation read surface: listing, counting, project summaries, the * single-conversation detail fetch (pagination + ETag + stale-while-revalidate) * and search — plus the JSONL/cwd resolvers the resume and adopt paths share. * * Extracted from StreamerServer so conversation work stops editing the server * file (see docs/plans/2026-07-12-server-ts-split.md, PR 4). State stays on the * server: this class only reads it through `deps`. */ export declare class ConversationHandlers { private deps; private publicScannerMeta; isExcludedSubagent(id: string): Promise; constructor(deps: ConversationHandlersDeps); private get scannerManager(); private get sessionStore(); private get ptyManager(); private get wsHub(); private get scanProfiles(); private get cache(); private get log(); private listFilters; private importFlags; private matchesImportFilters; handleListConversations(url: URL, res: ServerResponse): Promise; handleConversationsCount(url: URL, res: ServerResponse): Promise; private refreshCountInBackground; handleGetRecentSessions(url: URL, res: ServerResponse): void; handleGetPopularProjects(url: URL, res: ServerResponse): void; handleGetProjectSummaries(url: URL, res: ServerResponse): void; findJsonlPath(uuid: string): string | null; /** * Resolve a conversation id to a JSONL path, in order of authority. * * `findJsonlPath` alone answers 64.0% of this machine's 961 conversations and * 0 of 343 Codex ones — it reconstructs `//.jsonl`, * which is Claude Code's layout, and a Codex rollout is * `rollout--.jsonl` under a date path, so that walk cannot match one * by construction. * * That made the cache row the ONLY rung a Codex conversation could use, which * is why the scanner-index rung exists: measured 2026-09-04, 3 of the 50 ids * `GET /api/conversations` was serving 404'd here — every one a Codex rollout * present on disk, listed from the scanner index, with no cache row left. The * ladder's old "99.7%, only the file-is-gone case remains" held only while * every Codex row still had its cache entry. */ locateJsonlPath(uuid: string, lookupId: string): Promise; /** * Does `filePath` actually hold the conversation `requestedId` names? * * The filename settles it for every JSONL provider: Claude writes * `.jsonl` (or `agent-.jsonl`), Codex writes * `rollout--.jsonl`, Cursor writes `.jsonl` under * `agent-transcripts/`. * Only when the name says nothing do we open the file — and there the naive * rule is wrong, because **a Claude subagent transcript carries the PARENT's * `sessionId`**. Matching on `sessionId` alone therefore verifies exactly the * file this check exists to reject, so a sidechain is refused outright unless * it was asked for by its own `agent-` name, which the filename * branch above already covers. */ isJsonlPathFor(filePath: string, requestedId: string): Promise; /** First parseable JSONL line, for identity checks. Null on an empty or unreadable file. */ readFirstJsonlEntry(filePath: string): Promise<{ sessionId?: string; isSidechain?: boolean; } | null>; readCwdFromJsonl(filePath: string): Promise; findConversationByUuid(uuid: string): Promise; /** * The 200 body for a session that exists but has written no transcript yet. * * Same shape as the cache-tail fallback in handleGetConversation, with an * empty message list — a client cannot tell "no turns yet" from "a * conversation that happens to be empty", which is the point: both are a * working session with nothing to show, and neither is an error. There is no * `file_path` on purpose; the file does not exist yet. */ private emptyConversationPayload; /** * Is this conversation a Codex rollout? * * Two callers, both needing the same fact for different reasons: only Codex * writes a fork link, and only Codex numbers its offset index in a different * space than this handler serves (see the gate in handleGetConversation). A * conversation GET is a hot path, so answering from the cache row keeps every * Claude request from paying an open+read of a first line that can never * contain a link. */ private isCodexConversation; /** * The stand-in for a conversation whose own file holds no messages yet but * which inherits a history (a fresh fork). Carries the identity fields the * response meta needs; `messages` stays empty because the prefix is merged in * at the filter step, where the served index space is decided. */ private shellConversationForInherited; handleGetConversation(id: string, url: URL, res: ServerResponse, ifNoneMatch?: string): Promise; handleSearchTarget(id: string, req: IncomingMessage, res: ServerResponse): Promise; handleSearch(url: URL, res: ServerResponse): Promise; } //# sourceMappingURL=conversations.handlers.d.ts.map