@threadbase-sh/streamer 1.31.0 → 1.31.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/agent/agent-client.ts","../src/agent/agent-config.ts","../src/agent/conversation-writer.ts","../src/agent/dedupe.ts","../src/api/routes/progress.routes.ts","../src/auth.ts","../src/db/config.ts","../src/db/migrations.ts","../src/db/pool.ts","../src/live-session-manager.ts","../src/codex-pty-runner.ts","../src/logger.ts","../src/platform.ts","../src/providers.ts","../src/services/questions/codexGateAnswers.ts","../src/utils/debounce.ts","../src/pty-manager.ts","../src/services/questions/detectPermissionGate.ts","../src/services/questions/detectQuestionFromScreen.ts","../src/services/questions/detectShellPrompt.ts","../src/process-discovery.ts","../src/server.ts","../node_modules/nanoid/index.js","../node_modules/nanoid/url-alphabet/index.js","../src/agent/errors.ts","../src/agent/history-mapper.ts","../src/agent/payload-guard.ts","../src/agent/handle-send-agent-input.ts","../src/agent/handle-start-agent-session.ts","../src/api/app.ts","../src/api/middleware/auth.middleware.ts","../src/api/middleware/cors.middleware.ts","../src/api/middleware/error.middleware.ts","../src/api/routes/browse.routes.ts","../src/api/routes/conversations.routes.ts","../src/api/routes/health.routes.ts","../src/version.ts","../src/api/routes/logs.routes.ts","../src/lifecycle/constants.ts","../src/api/routes/misc.routes.ts","../src/config/update-config.ts","../src/schemas/updateConfig.schema.ts","../src/api/routes/pair.routes.ts","../src/api/routes/projects.routes.ts","../src/api/routes/scanner.routes.ts","../src/api/routes/sessions.routes.ts","../src/api/routes/ws.routes.ts","../src/browse.ts","../src/conversation-cache.ts","../src/db/sqlite-migrate.ts","../src/services/conversations/isAgentConversation.ts","../src/utils/fileIdentity.ts","../src/db/repositories/cacheMetadata.repository.ts","../src/db/repositories/conversations.repository.ts","../src/db/repositories/projects.repository.ts","../src/utils/canonicalizeProjectPath.ts","../src/db/repositories/sessions.repository.ts","../src/db/upload-records.ts","../src/handlers/handleListProjects.ts","../src/pair-store.ts","../src/seal.ts","../src/services/conversations/conversationWatcher.ts","../src/services/conversations/findSearchTarget.ts","../src/services/conversations/pruneAgentConversations.ts","../src/services/projectChats/deriveProjectChatTitle.ts","../src/services/questions/detectAskUserQuestion.ts","../src/services/questions/questionBroadcast.ts","../src/services/questions/answersToKeystrokes.ts","../src/services/questions/resolveAnswer.ts","../src/session-store.ts","../src/uploads.ts","../src/utils/codexConversationLine.ts","../src/utils/conversationEtag.ts","../src/utils/dates.ts","../src/utils/isScannedSnapshotStale.ts","../src/utils/scanProgressThrottle.ts","../src/ws-hub.ts"],"sourcesContent":["// src/agent/agent-client.ts\n//\n// Thin Temporal client wrapper used by tb-streamer in multi-agent mode.\n// Does NOT import workflow code from tb-multi-agent — we identify the\n// workflow and its signals/queries by name. The workflow's wire contract\n// lives in @threadbase-sh/agent-types.\n\nimport type { Client } from \"@temporalio/client\";\nimport type { UserInputSignal } from \"@threadbase-sh/agent-types\";\n\n// `defineSignal` / `defineQuery` only ship in `@temporalio/workflow` (a worker-\n// side package we deliberately do NOT pull into the streamer). The handle\n// methods `.signal(...)` and `.query(...)` accept either a plain string OR a\n// definition object with `{ type, name }`. We construct minimal definition\n// objects here — Temporal matches by name, the rest of the shape is virtual\n// branding.\n//\n// Same identifiers as tb-multi-agent's src/workflows/signals.ts.\nconst userInputSignal = { type: \"signal\", name: \"userInput\" } as unknown as {\n type: \"signal\";\n name: \"userInput\";\n};\nconst stageQuery = { type: \"query\", name: \"stage\" } as unknown as {\n type: \"query\";\n name: \"stage\";\n};\n\nconst ORCHESTRATOR_WORKFLOW_TYPE = \"orchestratorWorkflow\";\n\nexport interface AgentClient {\n startSession(sessionId: string): Promise<string>;\n sendUserInput(sessionId: string, payload: UserInputSignal): Promise<void>;\n endSession(sessionId: string): Promise<void>;\n getSessionStage(sessionId: string): Promise<string>;\n}\n\nexport interface AgentClientOpts {\n temporalClient: Client;\n taskQueue: string;\n}\n\nconst sessionWorkflowId = (sessionId: string): string => `session-${sessionId}`;\n\nexport function createAgentClient({ temporalClient, taskQueue }: AgentClientOpts): AgentClient {\n return {\n async startSession(sessionId: string): Promise<string> {\n const handle = await temporalClient.workflow.start(ORCHESTRATOR_WORKFLOW_TYPE, {\n taskQueue,\n workflowId: sessionWorkflowId(sessionId),\n args: [sessionId],\n workflowIdReusePolicy: \"REJECT_DUPLICATE\",\n } as any);\n return handle.workflowId;\n },\n async sendUserInput(sessionId: string, payload: UserInputSignal): Promise<void> {\n await temporalClient.workflow\n .getHandle(sessionWorkflowId(sessionId))\n .signal(userInputSignal as any, payload);\n },\n async endSession(sessionId: string): Promise<void> {\n await temporalClient.workflow.getHandle(sessionWorkflowId(sessionId)).cancel();\n },\n async getSessionStage(sessionId: string): Promise<string> {\n return temporalClient.workflow\n .getHandle(sessionWorkflowId(sessionId))\n .query(stageQuery as any);\n },\n };\n}\n","// src/agent/agent-config.ts\n//\n// Runtime config for multi-agent mode. Read once at server startup so we don't\n// thread env-var lookups through the rest of the codebase.\n\nexport interface AgentConfig {\n enabled: boolean;\n temporal: {\n address: string;\n namespace: string;\n taskQueue: string;\n };\n webhook: {\n hmacSecret: string;\n timestampSkewSeconds: number;\n };\n dedupe: {\n perSessionCapacity: number;\n };\n payload: {\n limitBytes: number;\n trajectoryLogBytes: number;\n trajectoryLogTurns: number;\n };\n sessionBusyRetryMs: number;\n conversationsDir: string;\n}\n\nconst DEFAULTS = {\n TEMPORAL_ADDRESS: \"localhost:7233\",\n TEMPORAL_NAMESPACE: \"default\",\n TEMPORAL_TASK_QUEUE: \"agent-tasks\",\n PROGRESS_HMAC_SECRET: \"dev-secret-change-me\",\n PROGRESS_WEBHOOK_TIMESTAMP_SKEW_SECONDS: \"300\",\n PROGRESS_DEDUPE_CAPACITY: \"1024\",\n AGENT_PAYLOAD_LIMIT_BYTES: \"1572864\", // 1.5 MB — 75% of Temporal's 2 MB ceiling\n AGENT_TRAJECTORY_LOG_BYTES: \"512000\", // 500 KB\n AGENT_TRAJECTORY_LOG_TURNS: \"20\", // First turn count for trajectory WARN\n AGENT_SESSION_BUSY_RETRY_MS: \"1000\",\n};\n\nfunction isTruthy(v: string | undefined): boolean {\n if (v === undefined) return false;\n return v === \"true\" || v === \"1\" || v === \"yes\" || v === \"on\";\n}\n\nexport function readAgentConfig(env: NodeJS.ProcessEnv = process.env): AgentConfig {\n const enabled = isTruthy(env.MULTI_AGENT_FLOW);\n return {\n enabled,\n temporal: {\n address: env.TEMPORAL_ADDRESS ?? DEFAULTS.TEMPORAL_ADDRESS,\n namespace: env.TEMPORAL_NAMESPACE ?? DEFAULTS.TEMPORAL_NAMESPACE,\n taskQueue: env.TEMPORAL_TASK_QUEUE ?? DEFAULTS.TEMPORAL_TASK_QUEUE,\n },\n webhook: {\n hmacSecret: env.PROGRESS_HMAC_SECRET ?? DEFAULTS.PROGRESS_HMAC_SECRET,\n timestampSkewSeconds: Number(\n env.PROGRESS_WEBHOOK_TIMESTAMP_SKEW_SECONDS ??\n DEFAULTS.PROGRESS_WEBHOOK_TIMESTAMP_SKEW_SECONDS,\n ),\n },\n dedupe: {\n perSessionCapacity: Number(env.PROGRESS_DEDUPE_CAPACITY ?? DEFAULTS.PROGRESS_DEDUPE_CAPACITY),\n },\n payload: {\n limitBytes: Number(env.AGENT_PAYLOAD_LIMIT_BYTES ?? DEFAULTS.AGENT_PAYLOAD_LIMIT_BYTES),\n trajectoryLogBytes: Number(\n env.AGENT_TRAJECTORY_LOG_BYTES ?? DEFAULTS.AGENT_TRAJECTORY_LOG_BYTES,\n ),\n trajectoryLogTurns: Number(\n env.AGENT_TRAJECTORY_LOG_TURNS ?? DEFAULTS.AGENT_TRAJECTORY_LOG_TURNS,\n ),\n },\n sessionBusyRetryMs: Number(\n env.AGENT_SESSION_BUSY_RETRY_MS ?? DEFAULTS.AGENT_SESSION_BUSY_RETRY_MS,\n ),\n // Mirrors ServerConfig.cacheDir's parent — the actual JSONL directory.\n // We read it from env here; the conversation writer takes the resolved\n // value from ServerConfig in Task 9.\n conversationsDir: env.THREADBASE_CONVERSATIONS_DIR ?? \"\",\n };\n}\n","// src/agent/conversation-writer.ts\n//\n// Persists assistant turns to JSONL when the worker's final agent_output for a\n// turn arrives. The existing ConversationCache + ConversationWatcher then\n// ingest the line via the existing watcher pipeline — see spec §6.3.\n\nimport { appendFile, mkdir } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\n\nexport interface AppendArgs {\n sessionId: string;\n turnId: string;\n content: string;\n reviewerOverruled?: boolean;\n}\n\nexport interface ConversationWriter {\n appendAssistantTurn(args: AppendArgs): Promise<void>;\n}\n\nexport function createConversationWriter(opts: { baseDir: string }): ConversationWriter {\n const { baseDir } = opts;\n\n return {\n async appendAssistantTurn(args: AppendArgs): Promise<void> {\n if (!args.content || args.content.length === 0) {\n throw new Error(\"ConversationWriter: refusing to write empty assistant turn\");\n }\n const file = join(baseDir, `${args.sessionId}.jsonl`);\n await mkdir(dirname(file), { recursive: true });\n\n const record = {\n role: \"assistant\" as const,\n turnId: args.turnId,\n content: args.content,\n timestamp: Date.now(),\n ...(args.reviewerOverruled ? { reviewerOverruled: true } : {}),\n };\n\n const line = `${JSON.stringify(record)}\\n`;\n await appendFile(file, line, { encoding: \"utf8\" });\n },\n };\n}\n","// src/agent/dedupe.ts\n//\n// Bounded LRU for per-session progress-event dedupe. Implementation uses the\n// fact that Map iterates in insertion order — re-inserting a key moves it to\n// the end, which is exactly LRU semantics with no extra bookkeeping.\n//\n// Spec §7.1: this is the milestone-B dedupe. The map lives on the session\n// record and dies with the session. Postgres-backed durability is option D,\n// deferred — see tb-multi-agent docs/plans/postgres-dedupe.md.\n\nexport interface ProgressDedupeLRU {\n hasSeen(eventId: string): boolean;\n readonly size: number;\n}\n\nexport function createProgressDedupeLRU(capacity: number): ProgressDedupeLRU {\n if (!Number.isFinite(capacity) || capacity < 1) {\n throw new Error(`dedupe LRU capacity must be >= 1, got ${capacity}`);\n }\n const map = new Map<string, true>();\n\n return {\n hasSeen(eventId: string): boolean {\n if (map.has(eventId)) {\n // Refresh recency: remove + reinsert moves to most-recent position.\n map.delete(eventId);\n map.set(eventId, true);\n return true;\n }\n map.set(eventId, true);\n if (map.size > capacity) {\n // Evict the oldest entry (the first key in insertion order).\n const oldest = map.keys().next().value;\n if (oldest !== undefined) map.delete(oldest);\n }\n return false;\n },\n get size(): number {\n return map.size;\n },\n };\n}\n","// src/api/routes/progress.routes.ts\n//\n// Webhook receiver for worker → tb-streamer progress events.\n//\n// Auth: HMAC over the raw request body, header X-Progress-Signature.\n// Auth bypass: the auth middleware skips this prefix because validation\n// happens inside the handler (mirrors /api/__update).\n//\n// Idempotency: per-session LRU on the ManagedSession record. Duplicates\n// return 200 with deduped:true and do not broadcast.\n\nimport crypto from \"node:crypto\";\nimport type { IncomingMessage } from \"node:http\";\nimport type { AgentOutputPayload, ProgressEvent, Stage } from \"@threadbase-sh/agent-types\";\nimport { Hono } from \"hono\";\nimport type { WSMessage } from \"../../types\";\nimport type { AppEnv } from \"../app\";\nimport type { ApiDeps } from \"../types/api-deps\";\n\nfunction readRawBody(req: IncomingMessage): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk) => chunks.push(chunk));\n req.on(\"end\", () => resolve(Buffer.concat(chunks)));\n req.on(\"error\", reject);\n });\n}\n\ninterface AgentDeps {\n sessionStore: {\n getManaged: (sessionId: string) => {\n id: string;\n progressDedupeIds?: { hasSeen: (id: string) => boolean };\n currentTurnId?: string | null;\n } | null;\n };\n wsHub: { broadcast: (m: WSMessage) => void };\n conversationWriter: {\n appendAssistantTurn: (a: {\n sessionId: string;\n turnId: string;\n content: string;\n reviewerOverruled?: boolean;\n }) => Promise<void>;\n } | null;\n agentConfig: {\n enabled: boolean;\n webhook: { hmacSecret: string; timestampSkewSeconds: number };\n dedupe: { perSessionCapacity: number };\n };\n}\n\nfunction verifySignature(rawBody: Buffer, signature: string, secret: string): boolean {\n if (!signature || signature.length === 0) return false;\n const expected = crypto.createHmac(\"sha256\", secret).update(rawBody).digest(\"hex\");\n if (expected.length !== signature.length) return false;\n try {\n return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));\n } catch {\n return false;\n }\n}\n\nfunction isWithinSkew(timestampHeader: string | undefined, skewSeconds: number): boolean {\n if (!timestampHeader) return true; // header optional in milestone B\n const t = Number(timestampHeader);\n if (!Number.isFinite(t)) return false;\n const now = Math.floor(Date.now() / 1000);\n return Math.abs(now - t) <= skewSeconds;\n}\n\nfunction stageToRole(stage: Stage | string | undefined): \"worker\" | \"reviewer\" | \"signoff\" {\n if (stage === \"review\") return \"reviewer\";\n if (stage === \"sign-off\") return \"signoff\";\n return \"worker\";\n}\n\nexport const createProgressRoutes = (deps: ApiDeps & AgentDeps) => {\n const app = new Hono<AppEnv>();\n\n app.post(\"/sessions/:sessionId/progress\", async (c) => {\n if (!deps.agentConfig.enabled) {\n return c.json({ error: \"multi-agent mode not enabled\" }, 404);\n }\n const sessionId = c.req.param(\"sessionId\");\n const session = deps.sessionStore.getManaged(sessionId);\n if (!session) {\n return c.json({ error: \"unknown session\" }, 404);\n }\n\n // Read raw body from the underlying Node IncomingMessage stream — mirrors\n // /api/__update. Hono's c.req.arrayBuffer() returns empty when the request\n // arrives via @hono/node-server's bindings, leaving HMAC verification with\n // the wrong byte buffer. In tests (app.request), c.env.incoming is absent\n // and arrayBuffer() works fine — fall back to it.\n let rawBuf: Buffer;\n try {\n const incoming = c.env?.incoming;\n rawBuf = incoming ? await readRawBody(incoming) : Buffer.from(await c.req.arrayBuffer());\n } catch {\n return c.json({ error: \"could not read body\" }, 400);\n }\n const sigHeader = c.req.header(\"x-progress-signature\") ?? \"\";\n if (!verifySignature(rawBuf, sigHeader, deps.agentConfig.webhook.hmacSecret)) {\n return c.json({ error: \"unauthorized\" }, 401);\n }\n if (\n !isWithinSkew(\n c.req.header(\"x-progress-timestamp\"),\n deps.agentConfig.webhook.timestampSkewSeconds,\n )\n ) {\n return c.json({ error: \"stale timestamp\" }, 401);\n }\n\n let event: ProgressEvent;\n try {\n event = JSON.parse(rawBuf.toString(\"utf8\")) as ProgressEvent;\n } catch {\n return c.json({ error: \"bad json\" }, 400);\n }\n if (!event.eventId || !event.sessionId || !event.turnId) {\n return c.json({ error: \"missing required fields\" }, 400);\n }\n\n // Dedupe (per spec §7.1). If the session lacks a dedupe map (e.g., it was\n // created in PTY mode and re-used), the receiver still works — every event\n // is treated as new.\n if (session.progressDedupeIds?.hasSeen(event.eventId)) {\n return c.json({ ok: true, deduped: true }, 200);\n }\n\n // ─── Translate to WSMessage and broadcast ───────────────────────────\n if (event.type === \"stage_transition\") {\n const msg: WSMessage = {\n type: \"session_update\",\n sessionId: event.sessionId,\n // turnId disambiguates which turn this stage applies to. Critical\n // for `queued` (identifies the waiting turn) and `rework` (with\n // reworkAttempt). Existing single-turn consumers can ignore it.\n turnId: event.turnId,\n // Existing session_update consumers expect status; we leave it\n // undefined here (stage is the new-only field).\n stage: event.stage,\n reworkAttempt: event.reworkAttempt,\n stalledSinceMs: 0,\n } as WSMessage;\n deps.wsHub.broadcast(msg);\n } else if (event.type === \"agent_output\") {\n const payload = (event.payload ?? {}) as unknown as AgentOutputPayload;\n const msg: WSMessage = {\n type: \"agent_output\",\n sessionId: event.sessionId,\n turnId: event.turnId,\n role: stageToRole(event.stage),\n content: payload.content ?? \"\",\n partial: payload.partial,\n reviewerOverruled: payload.reviewerOverruled,\n stage: event.stage,\n reworkAttempt: event.reworkAttempt,\n } as WSMessage;\n deps.wsHub.broadcast(msg);\n\n // Persist final answer to JSONL.\n if (event.stage === \"done\" && deps.conversationWriter && payload.content) {\n await deps.conversationWriter.appendAssistantTurn({\n sessionId: event.sessionId,\n turnId: event.turnId,\n content: payload.content,\n reviewerOverruled: payload.reviewerOverruled,\n });\n }\n\n // Release the session lock when the turn completes (spec §6).\n if (event.stage === \"done\" && session.currentTurnId === event.turnId) {\n (session as { currentTurnId: string | null }).currentTurnId = null;\n }\n } else if (event.type === \"terminal_failure\") {\n const reason = (event.payload as { reason?: string } | undefined)?.reason ?? \"unknown\";\n const msg: WSMessage = {\n type: \"turn_failure\",\n sessionId: event.sessionId,\n turnId: event.turnId,\n reason,\n } as WSMessage;\n deps.wsHub.broadcast(msg);\n\n // Release the session lock on failure (spec §6).\n if (session.currentTurnId === event.turnId) {\n (session as { currentTurnId: string | null }).currentTurnId = null;\n }\n }\n\n return c.json({ ok: true }, 200);\n });\n\n return app;\n};\n","import { randomBytes, timingSafeEqual } from \"crypto\";\nimport { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\n\n// Resolved per call (not frozen at module load) so tests can redirect writes\n// away from the real config via THREADBASE_CONFIG_DIR. Without this, importing\n// this module freezes the path to the real home before any test can sandbox it,\n// and a rotate/set-key test would clobber the live ~/.threadbase/server.yaml.\nfunction configDir(): string {\n return process.env.THREADBASE_CONFIG_DIR ?? join(homedir(), \".threadbase\");\n}\nfunction configFile(): string {\n return join(configDir(), \"server.yaml\");\n}\n\nexport function generateApiKey(): string {\n return `tb_${randomBytes(16).toString(\"hex\")}`;\n}\n\nexport function validateApiKey(provided: string, expected: string): boolean {\n const a = Buffer.from(provided);\n const b = Buffer.from(expected);\n if (a.length !== b.length) return false;\n return timingSafeEqual(a, b);\n}\n\nexport function loadOrCreateApiKey(): string {\n try {\n const content = readFileSync(configFile(), \"utf-8\");\n const match = content.match(/api_key:\\s*(.+)/);\n if (match?.[1]) return match[1].trim();\n } catch {\n // File doesn't exist, create one\n }\n\n const key = generateApiKey();\n mkdirSync(configDir(), { recursive: true });\n writeFileSync(configFile(), `api_key: ${key}\\n`, \"utf-8\");\n return key;\n}\n\nexport function loadBrowseRoot(): string | undefined {\n try {\n const content = readFileSync(configFile(), \"utf-8\");\n const match = content.match(/browse_root:\\s*(.+)/);\n if (match?.[1]) return match[1].trim();\n } catch {\n // File doesn't exist or not readable\n }\n return undefined;\n}\n\nexport function loadPublicUrl(): string | undefined {\n try {\n const content = readFileSync(configFile(), \"utf-8\");\n const match = content.match(/public_url:\\s*(.+)/);\n if (match?.[1]) return match[1].trim();\n } catch {\n // File doesn't exist or not readable\n }\n return undefined;\n}\n\nexport function loadBrowserCors(): string | undefined {\n try {\n const content = readFileSync(configFile(), \"utf-8\");\n const match = content.match(/browser_cors:\\s*(.+)/);\n if (match?.[1]) return match[1].trim();\n } catch {\n // File doesn't exist or not readable\n }\n return undefined;\n}\n\nexport function loadCacheDir(): string | undefined {\n try {\n const content = readFileSync(configFile(), \"utf-8\");\n const match = content.match(/cache_dir:\\s*(.+)/);\n if (match?.[1]) return match[1].trim();\n } catch {\n // File doesn't exist or not readable\n }\n return undefined;\n}\n\nexport function loadTailSize(): number | undefined {\n try {\n const content = readFileSync(configFile(), \"utf-8\");\n const match = content.match(/tail_size:\\s*(\\d+)/);\n if (match?.[1]) return Number.parseInt(match[1], 10);\n } catch {\n // File doesn't exist or not readable\n }\n return undefined;\n}\n\n// Auto-kill grace period in ms. 0 disables the automatic hold-on-disconnect\n// timer entirely (explicit hold_session still works); a positive value sets the\n// delay; unset falls through to DEFAULT_PTY_GRACE_PERIOD_MS.\nexport function loadPtyGracePeriodMs(): number | undefined {\n try {\n const content = readFileSync(configFile(), \"utf-8\");\n const match = content.match(/pty_grace_period_ms:\\s*(\\d+)/);\n if (match?.[1]) return Number.parseInt(match[1], 10);\n } catch {\n // File doesn't exist or not readable\n }\n return undefined;\n}\n\nexport function loadDefaultPermissionMode(): \"acceptEdits\" | \"manual\" | undefined {\n try {\n const content = readFileSync(configFile(), \"utf-8\");\n const match = content.match(/default_permission_mode:\\s*(\\S+)/);\n const value = match?.[1]?.trim();\n if (value === \"acceptEdits\" || value === \"manual\") return value;\n } catch {\n // File doesn't exist or not readable\n }\n return undefined;\n}\n\n// Persists the first-run prompt's answer so subsequent `serve` invocations\n// don't ask again (loadDefaultPermissionMode() returning a value is the\n// \"already configured\" signal cli/index.ts checks before prompting).\nexport function setDefaultPermissionMode(mode: \"acceptEdits\" | \"manual\"): void {\n const file = configFile();\n mkdirSync(configDir(), { recursive: true });\n\n let content = \"\";\n try {\n content = readFileSync(file, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") throw err;\n // file does not exist; we'll create it\n }\n\n const line = `default_permission_mode: ${mode}`;\n let updated: string;\n if (/^default_permission_mode:\\s*.+$/m.test(content)) {\n updated = content.replace(/^default_permission_mode:\\s*.+$/m, line);\n } else if (content.length === 0 || content.endsWith(\"\\n\")) {\n updated = `${content}${line}\\n`;\n } else {\n updated = `${content}\\n${line}\\n`;\n }\n\n const tmpFile = `${file}.tmp`;\n writeFileSync(tmpFile, updated, { encoding: \"utf-8\", mode: 0o600 });\n chmodSync(tmpFile, 0o600);\n renameSync(tmpFile, file);\n}\n\nexport type PublicUrlValidation = { ok: true; normalized: string } | { ok: false; error: string };\n\nexport function validatePublicUrl(raw: string): PublicUrlValidation {\n let parsed: URL;\n try {\n parsed = new URL(raw);\n } catch {\n return { ok: false, error: `Invalid URL: ${raw}` };\n }\n const localHosts = new Set([\"localhost\", \"127.0.0.1\", \"::1\"]);\n if (parsed.protocol === \"https:\") {\n return { ok: true, normalized: stripTrailingSlash(parsed.toString()) };\n }\n if (parsed.protocol === \"http:\" && localHosts.has(parsed.hostname)) {\n return { ok: true, normalized: stripTrailingSlash(parsed.toString()) };\n }\n return {\n ok: false,\n error: `publicUrl must be https:// (got ${parsed.protocol}//). Plain http is only allowed for localhost.`,\n };\n}\n\nfunction stripTrailingSlash(url: string): string {\n return url.endsWith(\"/\") ? url.slice(0, -1) : url;\n}\n\nexport function setApiKey(key: string): void {\n const file = configFile();\n mkdirSync(configDir(), { recursive: true });\n\n let content = \"\";\n try {\n content = readFileSync(file, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") throw err;\n // file does not exist; we'll create it\n }\n\n const apiKeyLine = `api_key: ${key}`;\n let updated: string;\n if (/^api_key:\\s*.+$/m.test(content)) {\n updated = content.replace(/^api_key:\\s*.+$/m, apiKeyLine);\n } else if (content.length === 0 || content.endsWith(\"\\n\")) {\n updated = `${content}${apiKeyLine}\\n`;\n } else {\n updated = `${content}\\n${apiKeyLine}\\n`;\n }\n\n const tmpFile = `${file}.tmp`;\n writeFileSync(tmpFile, updated, { encoding: \"utf-8\", mode: 0o600 });\n chmodSync(tmpFile, 0o600);\n renameSync(tmpFile, file);\n}\n","export interface DbConfig {\n connectionString: string;\n max: number;\n ssl?: string;\n statementTimeout?: number;\n instanceId: string;\n}\n\nexport function isDbEnabled(): boolean {\n const url = process.env.THREADBASE_DATABASE_URL;\n return typeof url === \"string\" && url.length > 0;\n}\n\nexport function getInstanceId(): string {\n return process.env.THREADBASE_INSTANCE_ID || require(\"os\").hostname();\n}\n\nexport function getDbConfig(): DbConfig | null {\n if (!isDbEnabled()) return null;\n\n const connectionString = process.env.THREADBASE_DATABASE_URL ?? \"\";\n const poolMax = Number.parseInt(process.env.THREADBASE_DATABASE_POOL_MAX ?? \"\", 10);\n const stmtTimeout = Number.parseInt(\n process.env.THREADBASE_DATABASE_STATEMENT_TIMEOUT_MS ?? \"\",\n 10,\n );\n const ssl = process.env.THREADBASE_DATABASE_SSL || undefined;\n\n return {\n connectionString,\n max: Number.isNaN(poolMax) ? 10 : poolMax,\n ssl,\n statementTimeout: Number.isNaN(stmtTimeout) ? undefined : stmtTimeout,\n instanceId: getInstanceId(),\n };\n}\n","import { readdirSync, readFileSync } from \"fs\";\nimport { dirname, join } from \"path\";\nimport type pg from \"pg\";\nimport { fileURLToPath } from \"url\";\n\nfunction getMigrationsDir(): string {\n // ESM: import.meta.url is available\n if (import.meta.url) {\n return dirname(fileURLToPath(import.meta.url));\n }\n // CJS: __dirname is available (injected by tsup)\n return __dirname;\n}\n\nexport async function runMigrations(pool: pg.Pool, migrationsDir?: string): Promise<void> {\n await pool.query(`\n CREATE TABLE IF NOT EXISTS _migrations (\n id SERIAL PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n )\n `);\n\n const { rows: applied } = await pool.query<{ name: string }>(\n \"SELECT name FROM _migrations ORDER BY name\",\n );\n const appliedSet = new Set(applied.map((r) => r.name));\n\n const dir = migrationsDir ?? join(getMigrationsDir(), \"pg-migrations\");\n const files = readdirSync(dir)\n .filter((f) => f.endsWith(\".sql\"))\n .sort();\n\n for (const file of files) {\n if (appliedSet.has(file)) continue;\n\n const sql = readFileSync(join(dir, file), \"utf-8\");\n await pool.query(sql);\n await pool.query(\"INSERT INTO _migrations (name) VALUES ($1)\", [file]);\n }\n}\n","import type { Pool as PoolType } from \"pg\";\nimport type { DbConfig } from \"./config\";\n\nexport function maskConnectionString(url: string): string {\n try {\n const parsed = new URL(url);\n // Check if password field is present (including empty password like \"user:@host\")\n const hasPasswordField =\n parsed.username.length > 0 && url.includes(`${parsed.username}:`) && url.includes(\"@\");\n if (hasPasswordField) {\n parsed.password = \"***\";\n }\n return parsed.toString();\n } catch {\n return \"***masked***\";\n }\n}\n\nexport async function createPool(config: DbConfig): Promise<PoolType> {\n const pg = await import(\"pg\");\n const { Pool } = pg.default ?? pg;\n\n const poolConfig: ConstructorParameters<typeof Pool>[0] = {\n connectionString: config.connectionString,\n max: config.max,\n };\n\n if (config.ssl === \"require\") {\n poolConfig.ssl = { rejectUnauthorized: false };\n } else if (config.ssl === \"disable\") {\n poolConfig.ssl = false;\n }\n\n if (config.statementTimeout) {\n poolConfig.statement_timeout = config.statementTimeout;\n }\n\n return new Pool(poolConfig);\n}\n","import { basename } from \"path\";\nimport { CodexPtyRunner } from \"./codex-pty-runner\";\nimport { CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER, type ProviderName } from \"./providers\";\nimport { PTYManager } from \"./pty-manager\";\nimport type {\n ManagedSession,\n PTYManagerOptions,\n SessionRunner,\n StartFreshSessionOptions,\n StartSessionOptions,\n UserMessage,\n} from \"./types\";\n\nexport class LiveSessionManager {\n private runners: Map<ProviderName, SessionRunner>;\n\n constructor(options: PTYManagerOptions = {}) {\n this.runners = new Map<ProviderName, SessionRunner>([\n [CLAUDE_CODE_PROVIDER, new PTYManager(options)],\n [CODEX_CLI_PROVIDER, new CodexPtyRunner(options)],\n ]);\n }\n\n async start(\n sessionId: string,\n options: StartSessionOptions & { provider?: ProviderName },\n ): Promise<ManagedSession> {\n const provider = options.provider ?? CLAUDE_CODE_PROVIDER;\n const runner = this.assertSupportedProvider(provider, options.projectPath);\n return runner.start(sessionId, options);\n }\n\n async startFresh(\n options: StartFreshSessionOptions & { provider?: ProviderName },\n ): Promise<ManagedSession> {\n const provider = options.provider ?? CLAUDE_CODE_PROVIDER;\n const runner = this.assertSupportedProvider(provider, options.projectPath);\n return runner.startFresh(options);\n }\n\n sendInput(sessionId: string, input: string): number {\n return this.runnerFor(sessionId).sendInput(sessionId, input);\n }\n\n sendKeys(sessionId: string, keys: string): void {\n this.runnerFor(sessionId).sendKeys(sessionId, keys);\n }\n\n cancel(sessionId: string): void {\n this.runnerFor(sessionId).cancel(sessionId);\n }\n\n killPid(pid: number): void {\n for (const runner of this.runners.values()) {\n runner.killPid(pid);\n }\n }\n\n // putOnHold tolerates an unknown sessionId (PTYManager.putOnHold is a no-op\n // when the session isn't in its map), so — unlike the other session-keyed\n // methods — route to the owning runner when found, otherwise broadcast to\n // every runner rather than throwing; this matches the pre-extraction\n // behavior of delegating straight through with no existence check.\n putOnHold(sessionId: string): void {\n for (const runner of this.runners.values()) {\n if (runner.hasSession(sessionId) || runner.getSession(sessionId)) {\n runner.putOnHold(sessionId);\n return;\n }\n }\n for (const runner of this.runners.values()) {\n runner.putOnHold(sessionId);\n }\n }\n\n getOutput(sessionId: string): string {\n return this.runnerFor(sessionId).getOutput(sessionId);\n }\n\n getOutputLines(sessionId: string, maxLines: number): Promise<string[]> {\n return this.runnerFor(sessionId).getOutputLines(sessionId, maxLines);\n }\n\n getInputHistory(sessionId: string): UserMessage[] {\n return this.runnerFor(sessionId).getInputHistory(sessionId);\n }\n\n getSession(sessionId: string): ManagedSession | null {\n for (const runner of this.runners.values()) {\n const session = runner.getSession(sessionId);\n if (session) return session;\n }\n return null;\n }\n\n hasSession(sessionId: string): boolean {\n for (const runner of this.runners.values()) {\n if (runner.hasSession(sessionId)) return true;\n }\n return false;\n }\n\n listSessions(): ManagedSession[] {\n return Array.from(this.runners.values()).flatMap((runner) => runner.listSessions());\n }\n\n dispose(): void {\n for (const runner of this.runners.values()) {\n runner.dispose();\n }\n }\n\n // Look up which runner owns a session. Only one runner exists today, so\n // this is a linear scan across hasSession()/getSession() rather than a\n // separate session→provider index — see task-1-brief.md.\n private runnerFor(sessionId: string): SessionRunner {\n for (const runner of this.runners.values()) {\n if (runner.hasSession(sessionId) || runner.getSession(sessionId)) return runner;\n }\n throw new Error(`Session not found: ${sessionId}`);\n }\n\n private assertSupportedProvider(provider: ProviderName, projectPath: string): SessionRunner {\n const runner = this.runners.get(provider);\n if (runner) return runner;\n const err = new Error(\n `Live ${provider} sessions are not implemented yet for ${basename(projectPath)}`,\n );\n (err as Error & { statusCode?: number }).statusCode = 501;\n throw err;\n }\n}\n","import { Terminal } from \"@xterm/headless\";\nimport { randomUUID } from \"crypto\";\nimport { existsSync } from \"fs\";\nimport { basename } from \"path\";\nimport { getLogger, type Logger } from \"./logger\";\nimport { resolveCodexExe } from \"./platform\";\nimport { CODEX_CLI_PROVIDER } from \"./providers\";\nimport {\n type CodexGateType,\n rememberedGateDigit,\n saveGateAnswer,\n} from \"./services/questions/codexGateAnswers\";\nimport type {\n ManagedSession,\n PermissionOption,\n PTYManagerOptions,\n SessionRunner,\n StartFreshSessionOptions,\n StartSessionOptions,\n UserMessage,\n} from \"./types\";\nimport { debounce } from \"./utils/debounce\";\n\nconst OUTPUT_BUFFER_MAX = 65536;\n\n// Cap on recorded user messages per session (drop oldest); mirrors\n// pty-manager.ts INPUT_HISTORY_MAX.\nconst INPUT_HISTORY_MAX = 50;\n\n// PTY geometry — same as pty-manager.ts. The headless render terminal\n// (session.screen) MUST match these so Codex's absolute cursor moves\n// (ESC[<row>;<col>H) resolve to the same screen coordinates the real TUI is\n// painting against.\nconst PTY_COLS = 120;\nconst PTY_ROWS = 40;\nconst SCREEN_SCROLLBACK = 1000;\n\n// Phase 0 findings (live PTY probe, not spec): Codex's status bar renders\n// \"Ready\" (case-sensitive) once the session is actually usable, e.g.\n// \"gpt-5.5 medium · /path · gpt-5.5 · medium · Ready · Wo…\". Two other\n// observed states — \"Starting\" (MCP servers loading; the compose box `›`\n// prefix is ALREADY visible here, so `›` alone is not a valid readiness\n// signal) and \"Working\" (mid-turn) — must NOT be treated as ready.\nexport const CODEX_PROMPT_READY_TEXT = \"Ready\";\n\n// Phase 0: a brand-new `--cd <dir>` shows a blocking directory-trust gate on\n// first-ever launch — a rendered screen containing this text, with options\n// \"1. Yes, continue\" / \"2. No, quit\" and a \"Press enter to continue\" footer.\n// Does not appear on `codex resume` or on later launches in an\n// already-trusted directory (Codex persists trust in ~/.codex/config.toml).\nexport const CODEX_TRUST_GATE_REGEX = /trust the contents/i;\n\n// Codex's hooks-review gate: shown pre-boot (fresh start AND resume) whenever\n// a configured hook is new/changed vs the trusted hashes in\n// ~/.codex/config.toml [hooks.state]. Options \"1. Review hooks\" (highlighted\n// default) / \"2. Trust all and continue\" / \"3. Continue without trusting\",\n// with a \"Press enter to confirm or esc to go back\" footer. Live-probe\n// verified: a digit keypress selects AND confirms instantly (no Enter).\nexport const CODEX_HOOKS_GATE_REGEX = /hooks need review/i;\n\n// Re-run screen detection this long after the PTY goes quiet — a session\n// blocked on a gate (or a status bar whose \"Ready\" got truncated) may never\n// produce another chunk to trigger detection. Same value/rationale as\n// pty-manager.ts QUIET_DETECT_MS. On quiet with pendingReady still set we mark\n// ready anyway: the mobile client is better off inside the session watching\n// live boot output than stuck on a spinner.\nconst QUIET_DETECT_MS = 500;\n\n// Flat backstop from spawn: \"Ready\" lives at the END of a single status line\n// whose prefix (dir · repo · branch · diffstats) can exceed PTY_COLS, in which\n// case the marker is truncated off-screen and can NEVER match (live-probe\n// verified). Must stay below server.ts START_READY_TIMEOUT_MS (10s) so the\n// start request resolves 200-with-session rather than 202-pending.\nconst CODEX_READY_FALLBACK_MS = 8_000;\n\nconst SUBMIT_BYTES = \"\\r\";\n\n// Delay between the input write and the submit \\r. Same value as Claude's\n// SUBMIT_DELAY_MS (pty-manager.ts) — no bracketed-paste wrap needed for\n// Codex (Phase 0: plain keystrokes are accepted directly into the compose\n// box), but we still yield an event-loop tick before Enter for consistency\n// with the observed-stable Claude pattern.\nconst CODEX_SUBMIT_DELAY_MS = 16;\n\nfunction digestBytes(s: string): string {\n const escaped = s\n .replace(new RegExp(String.fromCharCode(0x1b), \"g\"), \"\\\\x1b\")\n .replace(/\\r/g, \"\\\\r\")\n .replace(/\\n/g, \"\\\\n\")\n .replace(/\\t/g, \"\\\\t\");\n if (escaped.length <= 200) return escaped;\n return `${escaped.slice(0, 100)}…[${escaped.length - 200}B omitted]…${escaped.slice(-100)}`;\n}\n\n// Build the question card for a gate, broadcast over the existing `permission`\n// WS transport (mobile already renders these as tappable cards). Real options\n// keep their literal on-screen digits; the synthetic \"remember\" variants\n// continue the numbering and are intercepted in sendKeys() — they never reach\n// the PTY as-is. answerKeys mirrors `${index}\\r` so old clients (index\n// fallback) and new clients (answerKeys) send identical bytes.\n// \"1. Review hooks\" is deliberately omitted: a per-hook review screen is a\n// desktop affordance with no workable mobile rendering.\nfunction gateCard(\n gate: CodexGateType,\n lines: string[],\n): { prompt: string; options: PermissionOption[] } {\n if (gate === \"hooks\") {\n const countLine = lines.find((l) => /new or changed/i.test(l))?.trim();\n return {\n prompt: [\n \"Hooks need review\",\n countLine,\n \"Hooks can run outside the sandbox after you trust them.\",\n ]\n .filter(Boolean)\n .join(\" — \"),\n options: [\n { index: 2, label: \"Trust all and continue\", answerKeys: \"2\\r\" },\n { index: 3, label: \"Continue without trusting (hooks won't run)\", answerKeys: \"3\\r\" },\n {\n index: 4,\n label: \"Trust all and continue (remember for all projects)\",\n answerKeys: \"4\\r\",\n },\n {\n index: 5,\n label: \"Continue without trusting (remember for all projects)\",\n answerKeys: \"5\\r\",\n },\n ],\n };\n }\n return {\n prompt:\n lines.find((l) => CODEX_TRUST_GATE_REGEX.test(l))?.trim() ??\n \"Do you trust the contents of this directory?\",\n options: [\n { index: 1, label: \"Yes, continue\", answerKeys: \"1\\r\" },\n { index: 2, label: \"No, quit\", answerKeys: \"2\\r\" },\n { index: 3, label: \"Yes, continue (remember for all projects)\", answerKeys: \"3\\r\" },\n ],\n };\n}\n\n// node-pty is a native addon — import dynamically to allow graceful failure\nlet pty: typeof import(\"node-pty\") | null = null;\n\nasync function loadPty(): Promise<typeof import(\"node-pty\")> {\n if (pty) return pty;\n try {\n pty = await import(\"node-pty\");\n return pty;\n } catch (err) {\n throw new Error(\n \"node-pty is required for PTY management but failed to load. \" +\n \"Ensure it is installed: npm install node-pty\\n\" +\n `Original error: ${err}`,\n );\n }\n}\n\ninterface InternalSession extends ManagedSession {\n process: any; // node-pty IPty\n outputBuffer: Buffer;\n // Headless terminal rendering the raw PTY stream into a real screen grid —\n // same rationale as pty-manager.ts: Codex's absolute-cursor repaints\n // scramble raw byte order, so readiness/trust-gate detection reads the\n // rendered screen, not the raw chunk.\n screen: Terminal;\n // Ground-truth user messages submitted to this PTY, oldest-first, capped at\n // INPUT_HISTORY_MAX. Recorded in writeSubmit(); replayed via getInputHistory().\n inputHistory: UserMessage[];\n}\n\nfunction createScreen(): Terminal {\n return new Terminal({\n cols: PTY_COLS,\n rows: PTY_ROWS,\n scrollback: SCREEN_SCROLLBACK,\n allowProposedApi: true,\n });\n}\n\nexport class CodexPtyRunner implements SessionRunner {\n private sessions = new Map<string, InternalSession>();\n private onOutput: PTYManagerOptions[\"onOutput\"];\n private onStatusChange: PTYManagerOptions[\"onStatusChange\"];\n private onReady: PTYManagerOptions[\"onReady\"];\n // Broadcasts Codex's blocking startup gates (directory trust, hooks review)\n // as question cards; null dismisses the card once the gate leaves the screen.\n private onPermissionChange: PTYManagerOptions[\"onPermissionChange\"];\n private onLiveQuestion: PTYManagerOptions[\"onLiveQuestion\"];\n private onLiveQuestionGone: PTYManagerOptions[\"onLiveQuestionGone\"];\n private onUserMessage: PTYManagerOptions[\"onUserMessage\"];\n private log: Logger;\n // Tracks sessions whose PTY has spawned but Codex hasn't yet reached its\n // \"Ready\" status bar — i.e. onReady hasn't fired.\n private pendingReady = new Set<string>();\n // Inputs received via sendInput() while the session was still pendingReady.\n // Flushed in arrival order once Codex reaches Ready.\n private queuedInputs = new Map<string, string[]>();\n // Gate currently on a session's screen (card broadcast, unanswered). While\n // set, queued-input flushes are held — a flushed digit would CONFIRM a\n // dialog option — and sendKeys() intercepts remember-variant digits.\n private openGate = new Map<string, CodexGateType>();\n // `${sessionId}:${gate}` once a gate has been actioned (auto-answered or\n // card broadcast) — dedupes repaints of the same dialog.\n private gateActioned = new Set<string>();\n // Per-session trailing debounce re-armed on every chunk; on quiet, re-runs\n // screen detection so a blocked/truncated boot still reaches ready.\n private quietCheckers = new Map<string, ReturnType<typeof debounce<[]>>>();\n // Per-session flat backstop from spawn (CODEX_READY_FALLBACK_MS).\n private readyFallbackTimers = new Map<string, NodeJS.Timeout>();\n // In-flight start()/startFresh() calls keyed by sessionId. A second\n // concurrent resume for the same session (double-tap, client retry) awaits\n // the first call's promise instead of spawning a duplicate PTY (CRITICAL #3).\n private startPromises = new Map<string, Promise<ManagedSession>>();\n\n constructor(options: PTYManagerOptions = {}) {\n this.onOutput = options.onOutput;\n this.onStatusChange = options.onStatusChange;\n this.onReady = options.onReady;\n this.onPermissionChange = options.onPermissionChange;\n this.onLiveQuestion = options.onLiveQuestion;\n this.onLiveQuestionGone = options.onLiveQuestionGone;\n this.onUserMessage = options.onUserMessage;\n this.log = options.logger ?? getLogger(\"codex-pty\");\n }\n\n // Resume an existing Codex session. sessionId is the Codex-persisted\n // session_meta.payload.id (Phase 0, Section 8) — Codex has no fresh-session\n // equivalent of --session-id, so start() always means \"resume\".\n async start(sessionId: string, options: StartSessionOptions): Promise<ManagedSession> {\n // Guard the check-then-spawn: a second concurrent resume for the same\n // sessionId must not race past both checks and spawn a second PTY. See\n // PTYManager.start() for the identical pattern (CRITICAL #3).\n const existing = this.sessions.get(sessionId);\n if (existing) return toPublicSession(existing);\n\n const inFlight = this.startPromises.get(sessionId);\n if (inFlight) return inFlight;\n\n const promise = this.doStart(sessionId, options).finally(() => {\n this.startPromises.delete(sessionId);\n });\n this.startPromises.set(sessionId, promise);\n return promise;\n }\n\n private async doStart(sessionId: string, options: StartSessionOptions): Promise<ManagedSession> {\n const nodePty = await loadPty();\n const projectName = options.projectName ?? basename(options.projectPath);\n\n const proc = nodePty.spawn(\n resolveCodexExe(),\n [\"resume\", sessionId, \"--cd\", options.projectPath, \"--no-alt-screen\"],\n {\n name: \"xterm-256color\",\n cols: PTY_COLS,\n rows: PTY_ROWS,\n cwd: options.projectPath,\n env: process.env as Record<string, string>,\n },\n );\n\n const session: InternalSession = {\n id: sessionId,\n provider: CODEX_CLI_PROVIDER,\n projectPath: options.projectPath,\n projectName,\n branch: options.branch ?? \"\",\n status: \"running\",\n startedAt: new Date(),\n completedAt: null,\n promptCount: 0,\n lastOutput: \"\",\n process: proc,\n outputBuffer: Buffer.alloc(0),\n screen: createScreen(),\n inputHistory: [],\n };\n\n this.sessions.set(sessionId, session);\n this.pendingReady.add(sessionId);\n this.armReadyFallback(sessionId);\n\n proc.onData((data: string) => {\n this.handleOutput(sessionId, data);\n });\n\n proc.onExit(({ exitCode }: { exitCode: number }) => {\n this.pendingReady.delete(sessionId);\n this.handleExit(sessionId, exitCode);\n });\n\n return toPublicSession(session);\n }\n\n // Start a brand-new Codex session. Codex has no --session-id equivalent for\n // a fresh launch — it assigns its own id, discovered later (Task 3's\n // binding logic). This runner generates a local placeholder id for the\n // ManagedSession handle only.\n async startFresh(options: StartFreshSessionOptions): Promise<ManagedSession> {\n const nodePty = await loadPty();\n const sessionId = randomUUID();\n const projectName = options.projectName ?? basename(options.projectPath);\n\n // Codex CLI has no `--system-prompt` flag (unlike Claude). Its only\n // launch-time injection point is the positional `[PROMPT]` argument, which\n // Codex processes as the opening turn. Pass the server-built prompt\n // (default + browse-root boundary + client prompt) there so the safety\n // boundary and client instructions aren't silently dropped for Codex\n // sessions. Positional arg goes last, after all `[OPTIONS]`.\n const args = [\"--cd\", options.projectPath, \"--no-alt-screen\"];\n if (options.systemPrompt) {\n args.push(options.systemPrompt);\n }\n\n const proc = nodePty.spawn(resolveCodexExe(), args, {\n name: \"xterm-256color\",\n cols: PTY_COLS,\n rows: PTY_ROWS,\n cwd: options.projectPath,\n env: process.env as Record<string, string>,\n });\n\n const session: InternalSession = {\n id: sessionId,\n provider: CODEX_CLI_PROVIDER,\n projectPath: options.projectPath,\n projectName,\n branch: \"\",\n status: \"running\",\n startedAt: new Date(),\n completedAt: null,\n promptCount: 0,\n lastOutput: \"\",\n process: proc,\n outputBuffer: Buffer.alloc(0),\n screen: createScreen(),\n inputHistory: [],\n };\n\n this.sessions.set(sessionId, session);\n this.pendingReady.add(sessionId);\n this.armReadyFallback(sessionId);\n\n proc.onData((data: string) => {\n this.handleOutput(sessionId, data);\n });\n\n proc.onExit(({ exitCode }: { exitCode: number }) => {\n this.pendingReady.delete(sessionId);\n this.handleExit(sessionId, exitCode);\n });\n\n return toPublicSession(session);\n }\n\n // Flat backstop: if neither the \"Ready\" marker nor the quiet-checker settled\n // the session within CODEX_READY_FALLBACK_MS of spawn, mark it ready anyway\n // so start requests resolve and mobile can watch the boot live. unref() so a\n // pending timer never holds the process open.\n private armReadyFallback(sessionId: string): void {\n const timer = setTimeout(() => {\n this.readyFallbackTimers.delete(sessionId);\n const session = this.sessions.get(sessionId);\n if (session?.status === \"running\" && this.pendingReady.has(sessionId)) {\n this.markReady(sessionId, session, \"fallback:timeout\");\n }\n }, CODEX_READY_FALLBACK_MS);\n timer.unref?.();\n this.readyFallbackTimers.set(sessionId, timer);\n }\n\n // Write raw key bytes directly to the PTY, same as PTYManager.sendKeys.\n sendKeys(sessionId: string, keys: string): void {\n const session = this.sessions.get(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n if (session.status === \"idle\") {\n throw new Error(`Session is idle (no active PTY): ${sessionId}`);\n }\n if (session.status === \"waiting_input\") {\n session.status = \"running\";\n this.onStatusChange?.(toPublicSession(session));\n }\n const gate = this.openGate.get(sessionId);\n const digit = gate ? /^([0-9])\\r?$/.exec(keys)?.[1] : undefined;\n const out = gate && digit ? this.resolveGateAnswer(sessionId, gate, digit) : keys;\n this.log.info(\n `[codex.keys.write] ${sessionId.slice(0, 8)} bytes=${out.length} digest=${digestBytes(out)}`,\n { event: \"codex.keys_write\", sessionId, byteLen: out.length },\n );\n session.process.write(out);\n session.lastActivityAt = new Date();\n }\n\n // Map a gate-card digit to the PTY bytes that answer the real dialog,\n // persisting the choice when the digit was a synthetic \"remember for all\n // projects\" option (those numbers don't exist on the actual dialog and must\n // never reach codex). The trailing \\r mobile sends is dropped: a digit alone\n // selects AND confirms (live-probe verified), and a stray Enter would land\n // on whatever screen follows.\n private resolveGateAnswer(sessionId: string, gate: CodexGateType, digit: string): string {\n let real = digit;\n let remembered = false;\n if (gate === \"hooks\" && digit === \"4\") {\n saveGateAnswer(\"codexHooksGate\", \"trust_all\");\n real = \"2\";\n remembered = true;\n } else if (gate === \"hooks\" && digit === \"5\") {\n saveGateAnswer(\"codexHooksGate\", \"continue_untrusted\");\n real = \"3\";\n remembered = true;\n } else if (gate === \"trust\" && digit === \"3\") {\n saveGateAnswer(\"codexTrustGate\", \"yes\");\n real = \"1\";\n remembered = true;\n }\n this.log.info(`[codex.gate_answer] ${sessionId.slice(0, 8)} ${gate} digit=${real}`, {\n event: \"codex.gate_answer\",\n sessionId,\n gate,\n digit: real,\n remembered,\n });\n return real;\n }\n\n sendInput(sessionId: string, input: string): number {\n const session = this.sessions.get(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n if (session.status === \"idle\") {\n throw new Error(`Session is idle (no active PTY): ${sessionId}`);\n }\n // Codex hasn't reached Ready yet — queue and flush once it does. Same\n // rationale as PTYManager: writing into the raw PTY mid-boot risks the\n // startup/trust-gate UI swallowing the keystrokes.\n if (this.pendingReady.has(sessionId)) {\n const queue = this.queuedInputs.get(sessionId) ?? [];\n queue.push(input);\n this.queuedInputs.set(sessionId, queue);\n session.lastActivityAt = new Date();\n session.promptCount++;\n this.log.warn(\n `[codex.input.queued] ${sessionId.slice(0, 8)} promptCount=${session.promptCount} queueLen=${queue.length}`,\n {\n event: \"codex.input_queued\",\n sessionId,\n promptCount: session.promptCount,\n queueLen: queue.length,\n inputLen: input.length,\n },\n );\n return session.promptCount;\n }\n if (session.status === \"waiting_input\") {\n session.status = \"running\";\n this.onStatusChange?.(toPublicSession(session));\n }\n this.writeSubmit(sessionId, session, input, \"direct\", session.promptCount + 1);\n session.lastActivityAt = new Date();\n session.promptCount++;\n return session.promptCount;\n }\n\n // Write the input as plain bytes (no bracketed-paste wrap — Phase 0\n // confirmed Codex accepts plain keystrokes), then submit \\r after a short\n // delay so Codex's TUI gets an event-loop tick to process the input first.\n private writeSubmit(\n sessionId: string,\n session: InternalSession,\n input: string,\n path: \"direct\" | \"flush\",\n promptCount: number,\n ): void {\n this.recordUserMessage(session, input);\n this.log.info(\n `[codex.input.write] ${sessionId.slice(0, 8)} promptCount=${promptCount} bytes=${input.length} digest=${digestBytes(input)}`,\n {\n event: \"codex.input_write\",\n sessionId,\n promptCount,\n byteLen: input.length,\n digest: digestBytes(input),\n path,\n phase: \"input\",\n },\n );\n session.process.write(input);\n setTimeout(() => {\n const current = this.sessions.get(sessionId);\n if (!current || current !== session) return;\n this.log.info(\n `[codex.input.submit] ${sessionId.slice(0, 8)} promptCount=${promptCount} digest=\\\\r`,\n {\n event: \"codex.input_write\",\n sessionId,\n promptCount,\n byteLen: SUBMIT_BYTES.length,\n digest: \"\\\\r\",\n path,\n phase: \"submit\",\n },\n );\n current.process.write(SUBMIT_BYTES);\n }, CODEX_SUBMIT_DELAY_MS);\n }\n\n // Drain any inputs sent while the session was still pendingReady, writing\n // them in arrival order now that Codex is Ready. No-op while a gate dialog\n // is open (a flushed digit would confirm a dialog option) or while still\n // pendingReady (markReady drains it) — the gate-close path re-drives it for\n // the ready-with-gate-open case.\n private flushQueuedInputs(sessionId: string): void {\n if (this.openGate.has(sessionId) || this.pendingReady.has(sessionId)) return;\n const queue = this.queuedInputs.get(sessionId);\n if (!queue || queue.length === 0) return;\n this.queuedInputs.delete(sessionId);\n const session = this.sessions.get(sessionId);\n if (!session) return;\n this.log.info(\n `[codex.flush] ${sessionId.slice(0, 8)} flushing ${queue.length} queued input(s)`,\n {\n event: \"codex.flush_queued\",\n sessionId,\n queueLen: queue.length,\n },\n );\n queue.forEach((input, i) => {\n const writeAt = i * CODEX_SUBMIT_DELAY_MS * 2;\n if (writeAt === 0) {\n this.writeSubmit(sessionId, session, input, \"flush\", session.promptCount);\n } else {\n setTimeout(() => {\n const current = this.sessions.get(sessionId);\n if (!current || current !== session) return;\n this.writeSubmit(sessionId, session, input, \"flush\", session.promptCount);\n }, writeAt);\n }\n });\n }\n\n // SIGINT produces a clean exitCode=0 exit (Phase 0 — confirmed).\n cancel(sessionId: string): void {\n const session = this.sessions.get(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n session.process.kill(\"SIGINT\");\n }\n\n killPid(pid: number): void {\n try {\n process.kill(pid, \"SIGTERM\");\n } catch {\n // Process may already be gone\n }\n }\n\n // Kill the PTY and mark the session idle. Mirrors PTYManager.putOnHold.\n putOnHold(sessionId: string): void {\n const session = this.sessions.get(sessionId);\n if (!session) return;\n this.pendingReady.delete(sessionId);\n this.queuedInputs.delete(sessionId);\n this.clearSessionDetectors(sessionId);\n try {\n session.process.kill(\"SIGINT\");\n } catch {\n // already dead\n }\n session.status = \"idle\";\n session.completedAt = new Date();\n session.screen.dispose();\n this.sessions.delete(sessionId);\n this.onStatusChange?.(toPublicSession(session));\n }\n\n // Drop a session's detection state: quiet-checker, ready-fallback timer,\n // gate bookkeeping — and dismiss a still-open gate card so mobile doesn't\n // keep rendering a question for a dead PTY.\n private clearSessionDetectors(sessionId: string): void {\n this.quietCheckers.get(sessionId)?.cancel();\n this.quietCheckers.delete(sessionId);\n const timer = this.readyFallbackTimers.get(sessionId);\n if (timer) clearTimeout(timer);\n this.readyFallbackTimers.delete(sessionId);\n if (this.openGate.delete(sessionId)) {\n this.onPermissionChange?.(sessionId, null);\n }\n this.gateActioned.delete(`${sessionId}:hooks`);\n this.gateActioned.delete(`${sessionId}:trust`);\n }\n\n getOutput(sessionId: string): string {\n const session = this.sessions.get(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n return session.outputBuffer.toString(\"utf-8\");\n }\n\n // Render the last `maxLines` rows of the session's screen in true on-screen\n // order — same flush-then-read technique as PTYManager.getOutputLines.\n async getOutputLines(sessionId: string, maxLines: number): Promise<string[]> {\n const session = this.sessions.get(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n await new Promise<void>((resolve) => session.screen.write(\"\", () => resolve()));\n\n const buf = session.screen.buffer.active;\n const lines: string[] = [];\n for (let y = 0; y < buf.length; y++) {\n lines.push(buf.getLine(y)?.translateToString(true) ?? \"\");\n }\n while (lines.length > 0 && lines[lines.length - 1] === \"\") {\n lines.pop();\n }\n return lines.slice(-maxLines);\n }\n\n getInputHistory(sessionId: string): UserMessage[] {\n return this.sessions.get(sessionId)?.inputHistory ?? [];\n }\n\n // Record a submitted user message as ground truth and fire onUserMessage.\n // Called from writeSubmit (direct and flush paths) — never from sendKeys.\n private recordUserMessage(session: InternalSession, text: string): void {\n const ts = Date.now();\n session.inputHistory.push({ text, ts });\n if (session.inputHistory.length > INPUT_HISTORY_MAX) {\n session.inputHistory.shift();\n }\n this.onUserMessage?.(session.id, text, ts);\n }\n\n getSession(sessionId: string): ManagedSession | null {\n const session = this.sessions.get(sessionId);\n return session ? toPublicSession(session) : null;\n }\n\n hasSession(sessionId: string): boolean {\n return this.sessions.has(sessionId);\n }\n\n listSessions(): ManagedSession[] {\n return Array.from(this.sessions.values()).map(toPublicSession);\n }\n\n dispose(): void {\n for (const session of this.sessions.values()) {\n try {\n session.process.kill();\n } catch {\n // Process may already be dead\n }\n session.screen.dispose();\n }\n for (const sessionId of Array.from(this.quietCheckers.keys())) {\n this.clearSessionDetectors(sessionId);\n }\n for (const timer of this.readyFallbackTimers.values()) {\n clearTimeout(timer);\n }\n this.sessions.clear();\n this.pendingReady.clear();\n this.queuedInputs.clear();\n this.openGate.clear();\n this.gateActioned.clear();\n this.quietCheckers.clear();\n this.readyFallbackTimers.clear();\n }\n\n private handleOutput(sessionId: string, data: string): void {\n const session = this.sessions.get(sessionId);\n if (!session) return;\n\n const chunk = Buffer.from(data, \"utf-8\");\n session.outputBuffer = Buffer.concat([session.outputBuffer, chunk]);\n if (session.outputBuffer.length > OUTPUT_BUFFER_MAX) {\n session.outputBuffer = session.outputBuffer.subarray(\n session.outputBuffer.length - OUTPUT_BUFFER_MAX,\n );\n }\n\n // Render into the headless screen so detectReady()/getOutputLines() read\n // true on-screen order — Codex paints via absolute cursor-position\n // escapes that split words across non-contiguous PTY writes (Phase 0),\n // so raw substring matching on `data` would miss things.\n session.screen.write(data);\n session.lastOutput = stripAnsi(data);\n\n this.onOutput?.(sessionId, data);\n\n this.detectScreenState(sessionId, \"chunk\").catch((err) => {\n this.log.warn(\"[codex.ready_detect] failed\", {\n event: \"codex.ready_detect_failed\",\n sessionId,\n err,\n });\n });\n\n // Re-arm the quiet-checker on every chunk. A session blocked on a gate\n // dialog (or whose status-bar \"Ready\" is truncated off-screen) may never\n // produce the chunk that would trigger detection — re-run after\n // QUIET_DETECT_MS of silence instead.\n let quiet = this.quietCheckers.get(sessionId);\n if (!quiet) {\n quiet = debounce(() => {\n this.detectScreenState(sessionId, \"quiet\").catch((err) => {\n this.log.warn(\"[codex.ready_detect] failed\", {\n event: \"codex.ready_detect_failed\",\n sessionId,\n err,\n });\n });\n }, QUIET_DETECT_MS);\n this.quietCheckers.set(sessionId, quiet);\n }\n quiet();\n }\n\n // Renders the session's headless screen and drives both detections:\n // - Gates (directory trust, hooks review) — checked on EVERY pass,\n // independent of pendingReady, so a gate appearing after ready is still\n // surfaced and a gate leaving the screen closes its card.\n // - Readiness — the \"Ready\" status-bar marker while pendingReady, plus the\n // quiet path: after QUIET_DETECT_MS of PTY silence a still-pending\n // session is marked ready anyway (`›` alone is NOT a marker — Phase 0 —\n // but a quiet boot screen is more useful to the user live than a\n // spinner, and \"Ready\" may be truncated off the 120-col status bar).\n private async detectScreenState(sessionId: string, trigger: \"chunk\" | \"quiet\"): Promise<void> {\n const session = this.sessions.get(sessionId);\n if (!session || session.status === \"idle\") return;\n\n const lines = await this.getOutputLines(sessionId, PTY_ROWS);\n const screenText = lines.join(\"\\n\");\n\n // ── Gates ──────────────────────────────────────────────────────\n const gate: CodexGateType | null = CODEX_HOOKS_GATE_REGEX.test(screenText)\n ? \"hooks\"\n : CODEX_TRUST_GATE_REGEX.test(screenText)\n ? \"trust\"\n : null;\n\n if (gate) {\n this.handleGate(sessionId, session, gate, lines);\n } else if (this.openGate.delete(sessionId)) {\n // The dialog left the screen (answered via card, keys, or desktop) —\n // dismiss the card and release inputs held while it was open.\n this.onPermissionChange?.(sessionId, null);\n this.flushQueuedInputs(sessionId);\n }\n\n // ── Readiness ──────────────────────────────────────────────────\n if (session.status !== \"running\" || !this.pendingReady.has(sessionId)) return;\n const lastNonBlank = [...lines].reverse().find((l) => l.trim() !== \"\") ?? \"\";\n if (lastNonBlank.includes(CODEX_PROMPT_READY_TEXT)) {\n this.markReady(sessionId, session, `marker:${CODEX_PROMPT_READY_TEXT}`);\n } else if (trigger === \"quiet\") {\n this.markReady(sessionId, session, \"quiet:timeout\");\n }\n }\n\n // Answer a gate from the persisted remember-store, or surface it as a\n // question card over the permission transport. Actioned once per session and\n // gate type — repaints of the same dialog neither re-write nor re-broadcast.\n private handleGate(\n sessionId: string,\n session: InternalSession,\n gate: CodexGateType,\n lines: string[],\n ): void {\n const key = `${sessionId}:${gate}`;\n if (this.gateActioned.has(key)) return;\n this.gateActioned.add(key);\n\n const remembered = rememberedGateDigit(gate);\n if (remembered) {\n this.log.info(`[codex.gate_auto_answer] ${sessionId.slice(0, 8)} ${gate} → ${remembered}`, {\n event: \"codex.gate_auto_answer\",\n sessionId,\n gate,\n digit: remembered,\n });\n session.process.write(remembered);\n return;\n }\n\n this.openGate.set(sessionId, gate);\n const card = gateCard(gate, lines);\n this.log.info(`[codex.gate_prompt] ${sessionId.slice(0, 8)} ${gate}`, {\n event: \"codex.gate_prompt\",\n sessionId,\n gate,\n prompt: card.prompt,\n });\n this.onPermissionChange?.(sessionId, card);\n }\n\n private markReady(sessionId: string, session: InternalSession, reason: string): void {\n session.lastActivityAt = new Date();\n session.status = \"waiting_input\";\n // `reason=quiet:timeout`/`fallback:timeout` in volume would mean the\n // status-bar marker regressed (e.g. a Codex TUI redesign) — keep logged.\n this.log.info(`[codex.ready] ${sessionId.slice(0, 8)} ${reason}`, {\n event: \"codex.ready\",\n sessionId,\n reason,\n });\n this.onStatusChange?.(toPublicSession(session));\n if (this.pendingReady.has(sessionId)) {\n this.pendingReady.delete(sessionId);\n this.flushQueuedInputs(sessionId);\n this.onReady?.(toPublicSession(session));\n }\n }\n\n private handleExit(sessionId: string, exitCode: number): void {\n const session = this.sessions.get(sessionId);\n if (!session) return;\n\n session.completedAt = new Date();\n session.status = \"idle\";\n\n // Instant exit with no output — diagnose the most likely cause.\n const elapsedMs = session.completedAt.getTime() - session.startedAt.getTime();\n if (exitCode !== 0 && elapsedMs < 2000 && session.lastOutput === \"\") {\n if (!existsSync(session.projectPath)) {\n session.failureReason = `Project directory not found: ${session.projectPath}`;\n } else {\n session.failureReason = `Codex process exited immediately (code ${exitCode}).`;\n }\n }\n\n this.onStatusChange?.(toPublicSession(session));\n session.screen.dispose();\n this.sessions.delete(sessionId);\n this.queuedInputs.delete(sessionId);\n this.clearSessionDetectors(sessionId);\n }\n}\n\nfunction toPublicSession(s: InternalSession): ManagedSession {\n return {\n id: s.id,\n provider: s.provider ?? CODEX_CLI_PROVIDER,\n projectPath: s.projectPath,\n projectName: s.projectName,\n branch: s.branch,\n status: s.status,\n startedAt: s.startedAt,\n completedAt: s.completedAt,\n promptCount: s.promptCount,\n lastOutput: s.lastOutput,\n ...(s.failureReason != null && { failureReason: s.failureReason }),\n ...(s.lastActivityAt != null && { lastActivityAt: s.lastActivityAt }),\n ...(s.filePath != null && { filePath: s.filePath }),\n };\n}\n\n// Strip ANSI escape sequences for clean text preview — identical to\n// pty-manager.ts's stripAnsi.\nfunction stripAnsi(str: string): string {\n // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional ANSI stripping\n return str.replace(/\\x1b\\[[0-9;]*[a-zA-Z]/g, \"\").replace(/\\x1b\\][^\\x07]*\\x07/g, \"\");\n}\n","import pino, { type Logger as PinoLogger } from \"pino\";\n\nexport type LogDest = \"console\" | \"pino\" | \"both\";\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nconst baseLogger: PinoLogger = pino({\n level: process.env.LOG_LEVEL ?? \"info\",\n base: { service: \"tb-streamer\" },\n timestamp: pino.stdTimeFunctions.isoTime,\n redact: {\n paths: [\"req.headers.authorization\", \"req.headers.cookie\", 'req.headers[\"x-api-key\"]'],\n censor: \"[redacted]\",\n },\n});\n\nexport interface Logger {\n debug(msg: string, fields?: Record<string, unknown>, dest?: LogDest): void;\n info(msg: string, fields?: Record<string, unknown>, dest?: LogDest): void;\n warn(msg: string, fields?: Record<string, unknown>, dest?: LogDest): void;\n error(msg: string, fields?: Record<string, unknown>, dest?: LogDest): void;\n log(level: LogLevel, msg: string, fields?: Record<string, unknown>, dest?: LogDest): void;\n pino: PinoLogger;\n}\n\nfunction emit(\n pinoChild: PinoLogger,\n level: LogLevel,\n msg: string,\n fields: Record<string, unknown> | undefined,\n dest: LogDest,\n): void {\n if (dest === \"pino\" || dest === \"both\") {\n if (fields && Object.keys(fields).length > 0) pinoChild[level](fields, msg);\n else pinoChild[level](msg);\n }\n if (dest === \"console\" || dest === \"both\") {\n const consoleMethod: \"log\" | \"warn\" | \"error\" =\n level === \"error\" ? \"error\" : level === \"warn\" ? \"warn\" : \"log\";\n console[consoleMethod](msg);\n }\n}\n\nfunction build(pinoChild: PinoLogger): Logger {\n return {\n debug: (m, f, d = \"both\") => emit(pinoChild, \"debug\", m, f, d),\n info: (m, f, d = \"both\") => emit(pinoChild, \"info\", m, f, d),\n warn: (m, f, d = \"both\") => emit(pinoChild, \"warn\", m, f, d),\n error: (m, f, d = \"both\") => emit(pinoChild, \"error\", m, f, d),\n log: (lvl, m, f, d = \"both\") => emit(pinoChild, lvl, m, f, d),\n pino: pinoChild,\n };\n}\n\nexport function getLogger(component?: string): Logger {\n return build(component ? baseLogger.child({ component }) : baseLogger);\n}\n\nexport const logger: Logger = build(baseLogger);\n","import { execFileSync } from \"child_process\";\nimport { existsSync } from \"fs\";\nimport { homedir, platform } from \"os\";\nimport { join } from \"path\";\n\nexport const isWindows = platform() === \"win32\";\n\n// ─── Claude executable resolution ─────────────────────────────────────────────\n// On Windows, Task Scheduler strips PATH to bare system directories, so\n// `claude` alone will not resolve. We try where.exe first, then fall back to\n// well-known install locations before giving up and returning the bare name.\n//\n// On macOS, launchd inherits PATH=/usr/bin:/bin:/usr/sbin:/sbin by default,\n// which excludes both Homebrew prefixes. Without an explicit fallback,\n// node-pty's execvp(\"claude\", …) fails with ENOENT and every session\n// dies in milliseconds — see docs/troubleshooting.md. The plist's\n// EnvironmentVariables block is the primary fix; this is defense in depth.\n\nlet _claudeExe: string | undefined;\n\nexport function resolveClaudeExe(): string {\n if (_claudeExe !== undefined) return _claudeExe;\n\n if (isWindows) {\n try {\n const found = execFileSync(\"where.exe\", [\"claude\"], {\n encoding: \"utf-8\",\n windowsHide: true,\n timeout: 3000,\n })\n .trim()\n .split(\"\\n\")[0]\n .trim();\n if (found) {\n _claudeExe = found;\n return _claudeExe;\n }\n } catch {}\n\n const candidates = [\n join(homedir(), \".local\", \"bin\", \"claude.exe\"),\n join(\n process.env.LOCALAPPDATA ?? join(homedir(), \"AppData\", \"Local\"),\n \"Microsoft\",\n \"WindowsApps\",\n \"claude.exe\",\n ),\n ];\n for (const p of candidates) {\n if (existsSync(p)) {\n _claudeExe = p;\n return _claudeExe;\n }\n }\n } else {\n try {\n const found = execFileSync(\"/usr/bin/which\", [\"claude\"], {\n encoding: \"utf-8\",\n timeout: 3000,\n })\n .trim()\n .split(\"\\n\")[0]\n .trim();\n if (found && existsSync(found)) {\n _claudeExe = found;\n return _claudeExe;\n }\n } catch {}\n\n const candidates = [\n \"/opt/homebrew/bin/claude\",\n \"/usr/local/bin/claude\",\n join(homedir(), \".local\", \"bin\", \"claude\"),\n ];\n for (const p of candidates) {\n if (existsSync(p)) {\n _claudeExe = p;\n return _claudeExe;\n }\n }\n }\n\n _claudeExe = \"claude\";\n return _claudeExe;\n}\n\n// ─── Codex executable resolution ──────────────────────────────────────────────\n// Mirrors resolveClaudeExe() exactly, swapped for the `codex` binary. Same\n// rationale: launchd/Task Scheduler strip PATH down to system directories, so\n// an explicit which/where.exe + well-known-path fallback is needed.\n\nlet _codexExe: string | undefined;\n\nexport function resolveCodexExe(): string {\n if (_codexExe !== undefined) return _codexExe;\n\n if (isWindows) {\n try {\n const found = execFileSync(\"where.exe\", [\"codex\"], {\n encoding: \"utf-8\",\n windowsHide: true,\n timeout: 3000,\n })\n .trim()\n .split(\"\\n\")[0]\n .trim();\n if (found) {\n _codexExe = found;\n return _codexExe;\n }\n } catch {}\n\n const candidates = [\n join(homedir(), \".local\", \"bin\", \"codex.exe\"),\n join(\n process.env.LOCALAPPDATA ?? join(homedir(), \"AppData\", \"Local\"),\n \"Microsoft\",\n \"WindowsApps\",\n \"codex.exe\",\n ),\n ];\n for (const p of candidates) {\n if (existsSync(p)) {\n _codexExe = p;\n return _codexExe;\n }\n }\n } else {\n try {\n const found = execFileSync(\"/usr/bin/which\", [\"codex\"], {\n encoding: \"utf-8\",\n timeout: 3000,\n })\n .trim()\n .split(\"\\n\")[0]\n .trim();\n if (found && existsSync(found)) {\n _codexExe = found;\n return _codexExe;\n }\n } catch {}\n\n const candidates = [\n \"/opt/homebrew/bin/codex\",\n \"/usr/local/bin/codex\",\n join(homedir(), \".local\", \"bin\", \"codex\"),\n ];\n for (const p of candidates) {\n if (existsSync(p)) {\n _codexExe = p;\n return _codexExe;\n }\n }\n }\n\n _codexExe = \"codex\";\n return _codexExe;\n}\n\n// ─── execHidden ────────────────────────────────────────────────────────────────\n// Thin wrapper around execFileSync that adds windowsHide: true on Windows so\n// spawned child processes (where.exe, tasklist, wmic, pgrep, git …) don't\n// flash a console window.\n\ntype SyncOptions = Parameters<typeof execFileSync>[2];\n\nexport function execHidden(\n file: string,\n args: string[],\n opts?: SyncOptions & { encoding: \"utf-8\" },\n): string {\n return execFileSync(file, args, {\n windowsHide: isWindows,\n ...opts,\n }) as string;\n}\n","export const CLAUDE_CODE_PROVIDER = \"claude-code\" as const;\nexport const CODEX_CLI_PROVIDER = \"codex-cli\" as const;\nexport type ProviderName = typeof CLAUDE_CODE_PROVIDER | typeof CODEX_CLI_PROVIDER;\n\nexport function isProviderName(value: unknown): value is ProviderName {\n return value === CLAUDE_CODE_PROVIDER || value === CODEX_CLI_PROVIDER;\n}\n\n// Resolve a provider for a runner lookup. A `??` chain only defends against\n// null/undefined, so a present-but-unknown value (e.g. the legacy 'threadbase'\n// default from an old scanner-era cache) sails through and 501s at\n// assertSupportedProvider. Coerce anything that isn't a real runner to Claude Code.\nexport function coerceProviderForRunner(value: unknown): ProviderName {\n return isProviderName(value) ? value : CLAUDE_CODE_PROVIDER;\n}\n\n// Codex resume is implemented and verified (Phase 0: `codex resume <id>\n// --cd <dir>` replays the prior transcript end-to-end) — codex-cli now\n// defers to the same availability check as claude-code (project path\n// present, etc.) instead of forcing resumable=false unconditionally.\n// `provider` is kept in the signature (unused) so call sites don't need to\n// change if resumability ever needs to differentiate by provider again.\nexport function isProviderResumable(\n _provider: string | null | undefined,\n availabilityResumable: boolean,\n): boolean {\n return availabilityResumable;\n}\n","// Persisted answers for Codex's blocking startup gates (directory trust,\n// hooks review). When mobile answers a gate card with a \"remember for all\n// projects\" option, the choice lands here and future gates are auto-answered\n// without a card. One tiny JSON file; read at gate time so edits (or tests\n// via THREADBASE_CONFIG_DIR) apply without a restart.\n\nimport { mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { dirname, join } from \"path\";\n\nexport type CodexGateType = \"hooks\" | \"trust\";\n\nexport interface GateAnswers {\n codexHooksGate?: \"trust_all\" | \"continue_untrusted\";\n codexTrustGate?: \"yes\";\n}\n\nfunction gateAnswersPath(): string {\n const dir = process.env.THREADBASE_CONFIG_DIR ?? join(homedir(), \".threadbase\");\n return join(dir, \"gate-answers.json\");\n}\n\nexport function loadGateAnswers(): GateAnswers {\n try {\n const parsed = JSON.parse(readFileSync(gateAnswersPath(), \"utf-8\"));\n return parsed && typeof parsed === \"object\" ? (parsed as GateAnswers) : {};\n } catch {\n return {};\n }\n}\n\nexport function saveGateAnswer<K extends keyof GateAnswers>(key: K, value: GateAnswers[K]): void {\n const path = gateAnswersPath();\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, `${JSON.stringify({ ...loadGateAnswers(), [key]: value }, null, 2)}\\n`);\n}\n\n/**\n * The digit that answers a gate per the remembered choice, or null when the\n * user must be asked. Digits are the dialogs' literal on-screen numbers\n * (probe-verified: a digit keypress selects AND confirms, no Enter needed).\n */\nexport function rememberedGateDigit(gate: CodexGateType): string | null {\n const answers = loadGateAnswers();\n if (gate === \"hooks\") {\n if (answers.codexHooksGate === \"trust_all\") return \"2\";\n if (answers.codexHooksGate === \"continue_untrusted\") return \"3\";\n return null;\n }\n return answers.codexTrustGate === \"yes\" ? \"1\" : null;\n}\n","/**\n * Trailing-edge debounce: collapses calls that arrive within `waitMs` of each\n * other into a single invocation, scheduled `waitMs` after the most recent\n * call. The last call's arguments win.\n *\n * The returned function carries two extra methods:\n * - cancel(): drop any pending invocation (use on shutdown so a timer\n * doesn't keep the process alive).\n * - flush(): run any pending invocation immediately (useful in tests).\n */\nexport function debounce<A extends unknown[]>(\n fn: (...args: A) => void,\n waitMs: number,\n): ((...args: A) => void) & { flush: () => void; cancel: () => void } {\n let timer: ReturnType<typeof setTimeout> | null = null;\n let lastArgs: A | null = null;\n\n const run = () => {\n timer = null;\n if (lastArgs) {\n const args = lastArgs;\n lastArgs = null;\n fn(...args);\n }\n };\n\n const debounced = (...args: A): void => {\n lastArgs = args;\n if (timer) clearTimeout(timer);\n timer = setTimeout(run, waitMs);\n };\n\n debounced.cancel = (): void => {\n if (timer) clearTimeout(timer);\n timer = null;\n lastArgs = null;\n };\n\n debounced.flush = (): void => {\n if (timer) {\n clearTimeout(timer);\n run();\n }\n };\n\n return debounced;\n}\n","import { Terminal } from \"@xterm/headless\";\nimport { randomUUID } from \"crypto\";\nimport { existsSync } from \"fs\";\nimport { basename } from \"path\";\nimport { getLogger, type Logger } from \"./logger\";\nimport { resolveClaudeExe } from \"./platform\";\nimport { CLAUDE_CODE_PROVIDER } from \"./providers\";\nimport { hasPermissionOsc, scrapePermissionGate } from \"./services/questions/detectPermissionGate\";\nimport {\n detectQuestionFromScreen,\n questionContentKey,\n} from \"./services/questions/detectQuestionFromScreen\";\nimport { detectShellPrompt } from \"./services/questions/detectShellPrompt\";\nimport type {\n ManagedSession,\n PTYManagerOptions,\n SessionRunner,\n StartFreshSessionOptions,\n StartSessionOptions,\n UserMessage,\n} from \"./types\";\nimport { debounce } from \"./utils/debounce\";\n\nconst OUTPUT_BUFFER_MAX = 65536;\n\n// Cap on recorded user messages per session (drop oldest). Bounds memory on a\n// long-lived session; replay only needs the recent tail for ownership.\nconst INPUT_HISTORY_MAX = 50;\n\n// PTY geometry. The headless render terminal (session.screen) MUST match these\n// so Claude's absolute cursor moves (ESC[<row>;<col>H) resolve to the same\n// screen coordinates the real TUI is painting against.\nconst PTY_COLS = 120;\nconst PTY_ROWS = 40;\n// Scrollback depth for the render terminal. Replay reads up to maxLines (200)\n// from the rendered buffer, so keep enough history above the viewport.\nconst SCREEN_SCROLLBACK = 1000;\n\n// Markers that indicate Claude has reached an interactive prompt and is ready\n// for user input. The TUI has at least two startup variants:\n// - \"Tips\" banner: renders a box with corner ╭ characters\n// - Connector/MCP-status splash: no box at all; shows ❯ as the prompt arrow\n// We treat either marker in a chunk as \"ready\". False-positives are harmless —\n// the worst case is flushing queued input slightly early, which Claude buffers.\nconst CLAUDE_PROMPT_MARKERS = [\"╭\", \"❯\"] as const;\n\n// If a fresh session has produced any output but neither prompt marker has\n// fired within this window, flush queued input anyway. Defense against future\n// TUI changes that drop both markers from the boot screen.\nconst PROMPT_MARKER_FALLBACK_MS = 10_000;\n\n// Re-run ready/prompt detection this long after the PTY goes quiet, instead of\n// waiting for another chunk (which may never come if Claude is blocked on a\n// prompt) or the full PROMPT_MARKER_FALLBACK_MS. 500ms was picked from real\n// [pty.chunk] gap logs: p50/p75 inter-chunk gaps while status=running are\n// 31ms/467ms, so 500ms clears normal streaming pauses without adding\n// meaningful perceived latency (see docs/postmortems or session notes for the\n// gapMs sample this was based on).\nconst QUIET_DETECT_MS = 500;\n\n// Build the two byte sequences for a paste-then-submit. We deliberately split\n// the paste body and the trailing \\r into two separate PTY writes (see\n// writeSubmit() below) so Claude's TUI gets one event-loop tick to process the\n// paste (clear input buffer, render `Pasting…`) before Enter arrives.\n//\n// Why bracketed paste at all: Claude's TUI enables bracketed paste mode\n// (\\x1b[?2004h) at startup. Content between \\x1b[200~ and \\x1b[201~ is\n// committed as a single insertion without triggering autocomplete or key\n// bindings. Without this wrap an input like \"@<path>\" opens the mention\n// picker and the trailing \\r gets consumed as \"accept completion\" rather\n// than \"submit\" — see docs/postmortems/2026-05-20-pty-bracketed-paste-fix.md.\n//\n// Why split paste and \\r: on 2026-05-27 a follow-up stuck session\n// (39118d3e) showed the bracketed-paste wrap was being written but the\n// trailing \\r still didn't submit — Claude's TUI was mid-render of a\n// startup status banner (\"Update available\", \"192 skill descriptions\n// dropped\", etc.) when the bytes arrived, and the whole chunk landed in\n// the wrong handler context. Splitting the write lets the TUI ingest the\n// paste in one tick (the data event runs after current render finishes)\n// before the next tick delivers the Enter.\nfunction buildPasteBytes(input: string): string {\n return `\\x1b[200~${input}\\x1b[201~`;\n}\n\nconst SUBMIT_BYTES = \"\\r\";\n\n// Delay between the paste write and the submit \\r. We need to yield the event\n// loop at least once so Claude's TUI processes the paste before Enter lands;\n// a small real-time delay is more robust against the TUI batching renders\n// across multiple data events. Kept tiny so user-perceived latency is nil.\nconst SUBMIT_DELAY_MS = 16;\n\n// writeSubmit() polls in SUBMIT_DELAY_MS steps and only writes \\r once the PTY\n// has been quiet (no chunk) for a full SUBMIT_DELAY_MS — see writeSubmit() for\n// the redraw race this closes. Cap the wait so a genuinely wedged/silent PTY\n// still gets its \\r rather than hanging forever.\nconst SUBMIT_MAX_WAIT_MS = 500;\n\nfunction digestBytes(s: string): string {\n // Replace control chars with their hex form so logs are grep-able.\n // Building the regex via RegExp() sidesteps a Biome lint rule that flags\n // literal control characters in regex literals.\n const escaped = s\n .replace(new RegExp(String.fromCharCode(0x1b), \"g\"), \"\\\\x1b\")\n .replace(/\\r/g, \"\\\\r\")\n .replace(/\\n/g, \"\\\\n\")\n .replace(/\\t/g, \"\\\\t\");\n if (escaped.length <= 200) return escaped;\n return `${escaped.slice(0, 100)}…[${escaped.length - 200}B omitted]…${escaped.slice(-100)}`;\n}\n\n// node-pty is a native addon — import dynamically to allow graceful failure\nlet pty: typeof import(\"node-pty\") | null = null;\n\nasync function loadPty(): Promise<typeof import(\"node-pty\")> {\n if (pty) return pty;\n try {\n pty = await import(\"node-pty\");\n return pty;\n } catch (err) {\n throw new Error(\n \"node-pty is required for PTY management but failed to load. \" +\n \"Ensure it is installed: npm install node-pty\\n\" +\n `Original error: ${err}`,\n );\n }\n}\n\ninterface InternalSession extends ManagedSession {\n process: any; // node-pty IPty\n outputBuffer: Buffer;\n // Headless terminal that renders the raw PTY stream into a real screen grid.\n // getOutputLines() reads its rendered buffer so replay reflects true screen\n // order rather than raw byte order (which Claude's absolute-cursor repaints\n // scramble — see getOutputLines for the desync this fixes).\n screen: Terminal;\n // Ground-truth user messages submitted to this PTY, oldest-first, capped at\n // INPUT_HISTORY_MAX. Recorded in writeSubmit(); replayed via getInputHistory().\n inputHistory: UserMessage[];\n}\n\nfunction createScreen(): Terminal {\n return new Terminal({\n cols: PTY_COLS,\n rows: PTY_ROWS,\n scrollback: SCREEN_SCROLLBACK,\n allowProposedApi: true,\n });\n}\n\n// Build the environment for a spawned `claude` process. The Anthropic API key\n// is injected only here — never exported into the streamer's global process\n// env — so it does not leak into unrelated child processes. CLAUDE_API_KEY\n// (the Fly secret) is mapped to ANTHROPIC_MODEL's sibling, ANTHROPIC_API_KEY,\n// which the CLI reads. If CLAUDE_API_KEY is unset, nothing is added and the CLI\n// falls back to its own auth (e.g. an interactive login).\nfunction buildSpawnEnv(): Record<string, string> {\n const env = { ...process.env } as Record<string, string>;\n if (env.CLAUDE_API_KEY) {\n env.ANTHROPIC_API_KEY = env.CLAUDE_API_KEY;\n }\n // If the streamer itself was launched from inside a Claude Code session,\n // it inherits that session's markers (CLAUDECODE, CLAUDE_CODE_SESSION_ID,\n // CLAUDE_CODE_CHILD_SESSION, ...). A spawned `claude` seeing them treats\n // itself as a nested child session and never persists a project JSONL —\n // the session \"works\" in the terminal but has no conversation to stream.\n for (const key of Object.keys(env)) {\n if (key === \"CLAUDECODE\" || key.startsWith(\"CLAUDE_CODE_\")) {\n delete env[key];\n }\n }\n return env;\n}\n\nexport class PTYManager implements SessionRunner {\n private sessions = new Map<string, InternalSession>();\n private onOutput: PTYManagerOptions[\"onOutput\"];\n private onStatusChange: PTYManagerOptions[\"onStatusChange\"];\n private onReady: PTYManagerOptions[\"onReady\"];\n private onPermissionChange: PTYManagerOptions[\"onPermissionChange\"];\n private onLiveQuestion: PTYManagerOptions[\"onLiveQuestion\"];\n private onLiveQuestionGone: PTYManagerOptions[\"onLiveQuestionGone\"];\n private onUserMessage: PTYManagerOptions[\"onUserMessage\"];\n // Per-session permission-gate state. True between an OSC 777 (gate open) and\n // the next prompt-ready without a fresh 777 (gate closed). Prevents\n // re-broadcasting open/close on every chunk.\n private permissionOpen = new Set<string>();\n // Content key of the last AskUserQuestion broadcast from the rendered screen,\n // per session — de-dupes the same menu firing on consecutive repaints.\n private lastScreenQuestionKey = new Map<string, string>();\n // Content key of the last unstructured shell prompt (detectShellPrompt)\n // broadcast per session — present between open and resolve so we can clear it\n // on a prompt-ready/marker return and de-dupe consecutive repaints. Modelled\n // on permissionOpen but keyed by content (a shell prompt has no OSC trigger).\n private shellPromptOpen = new Map<string, string>();\n // Tracks sessions (both fresh and resume) whose PTY has spawned but Claude\n // hasn't yet reached an interactive prompt — i.e. onReady hasn't fired.\n private pendingReady = new Set<string>();\n // Inputs received via sendInput() while the session was still in pendingReady.\n // Flushed in arrival order once Claude reaches its first prompt. Without this,\n // input written into the raw PTY mid-boot is consumed by Claude's startup TUI\n // (welcome banner / first-run modals on fresh, JSONL restore on resume) and\n // silently lost — the \"dot bug\".\n private queuedInputs = new Map<string, string[]>();\n private log: Logger;\n // Timestamp of first PTY chunk per session; drives the prompt-marker fallback\n // when neither ╭ nor ❯ shows up within PROMPT_MARKER_FALLBACK_MS.\n private firstChunkAt = new Map<string, number>();\n // Per-session chunk counter and last-chunk timestamp. Diagnostic-only,\n // feeds the [pty.chunk] log lines so we can trace whether Claude responded\n // to a given input or fell silent. Reset on dispose().\n private chunkIndex = new Map<string, number>();\n private lastChunkAt = new Map<string, number>();\n // Per-session debounced \"went quiet\" checker, re-armed on every chunk. Fires\n // QUIET_DETECT_MS after the last chunk so ready/prompt detection doesn't\n // wait for another chunk that may never arrive (Claude blocked on input).\n private quietCheckers = new Map<string, ReturnType<typeof debounce>>();\n // In-flight start()/startFresh() calls keyed by sessionId. A second\n // concurrent resume for the same session (double-tap, client retry) awaits\n // the first call's promise instead of spawning a duplicate PTY (CRITICAL #3).\n private startPromises = new Map<string, Promise<ManagedSession>>();\n\n constructor(options: PTYManagerOptions = {}) {\n this.onOutput = options.onOutput;\n this.onStatusChange = options.onStatusChange;\n this.onReady = options.onReady;\n this.onPermissionChange = options.onPermissionChange;\n this.onLiveQuestion = options.onLiveQuestion;\n this.onLiveQuestionGone = options.onLiveQuestionGone;\n this.onUserMessage = options.onUserMessage;\n this.log = options.logger ?? getLogger(\"pty\");\n }\n\n // Resume an existing Claude conversation. sessionId is the JSONL UUID.\n //\n // options.permissionMode defaults to `acceptEdits` rather than\n // `--dangerously-skip-permissions`/`bypassPermissions`. Both suppress\n // file-edit prompts, but in an interactive (TUI) launch the skip-permissions\n // flag renders a blocking \"Bypass Permissions mode\" warning menu on every\n // boot that no known ~/.claude.json flag suppressed (as of Claude CLI\n // v2.1.x) — the session never reaches a usable prompt, so the mobile app\n // shows an empty/stuck screen. `acceptEdits` auto-approves file edits\n // without that warning gate, while still prompting for shell commands.\n // `manual` (prompt for everything) is the only other mode callers may pass;\n // `bypassPermissions`/`plan`/`dontAsk`/`auto` are not supported here.\n // (The other first-run gates — onboarding/theme, workspace trust,\n // custom-API-key — are cleared by the seeded ~/.claude.json in\n // docker/entrypoint.sh.) startFresh() uses the same default for the same reason.\n async start(sessionId: string, options: StartSessionOptions): Promise<ManagedSession> {\n // Guard the check-then-spawn: a second concurrent resume for the same\n // sessionId (double-tap, client retry — server.ts's own hasSession check\n // has an await gap before it calls start()) must not race past both\n // checks and spawn a second PTY. Returning the first call's in-flight\n // promise serializes the spawn; an already-running session short-circuits\n // without touching doStart at all.\n const existing = this.sessions.get(sessionId);\n if (existing) return toPublicSession(existing);\n\n const inFlight = this.startPromises.get(sessionId);\n if (inFlight) return inFlight;\n\n const promise = this.doStart(sessionId, options).finally(() => {\n this.startPromises.delete(sessionId);\n });\n this.startPromises.set(sessionId, promise);\n return promise;\n }\n\n private async doStart(sessionId: string, options: StartSessionOptions): Promise<ManagedSession> {\n const nodePty = await loadPty();\n const projectName = options.projectName ?? basename(options.projectPath);\n\n const proc = nodePty.spawn(\n resolveClaudeExe(),\n [\n \"--permission-mode\",\n options.permissionMode ?? \"acceptEdits\",\n \"--settings\",\n '{\"spinnerTipsEnabled\":false}',\n \"--resume\",\n sessionId,\n ],\n {\n name: \"xterm-256color\",\n cols: 120,\n rows: 40,\n cwd: options.projectPath,\n env: buildSpawnEnv(),\n },\n );\n\n const session: InternalSession = {\n id: sessionId,\n provider: CLAUDE_CODE_PROVIDER,\n projectPath: options.projectPath,\n projectName,\n branch: options.branch ?? \"\",\n status: \"running\",\n startedAt: new Date(),\n completedAt: null,\n promptCount: 0,\n lastOutput: \"\",\n process: proc,\n outputBuffer: Buffer.alloc(0),\n screen: createScreen(),\n inputHistory: [],\n };\n\n this.sessions.set(sessionId, session);\n // Resume re-uses the same boot path as a fresh launch: --resume replays the\n // JSONL into Claude's TUI, which can take several seconds before the prompt\n // is reachable. Until then, raw pty.write() bytes land in the boot UI and\n // are swallowed (the \"dot bug\" — first message vanishes, second message\n // appears to trigger both). Same pendingReady + flush gating as startFresh.\n this.pendingReady.add(sessionId);\n\n proc.onData((data: string) => {\n this.handleOutput(sessionId, data);\n });\n\n proc.onExit(({ exitCode }: { exitCode: number }) => {\n this.pendingReady.delete(sessionId);\n this.handleExit(sessionId, exitCode);\n });\n\n return toPublicSession(session);\n }\n\n // Start a brand-new Claude session. A stable UUID is generated here and passed\n // to Claude via --session-id so the JSONL filename matches from the start.\n // onReady fires once Claude reaches its first prompt (waiting_input).\n async startFresh(options: StartFreshSessionOptions): Promise<ManagedSession> {\n const nodePty = await loadPty();\n const sessionId = randomUUID();\n const projectName = options.projectName ?? basename(options.projectPath);\n\n // `--permission-mode` defaults to acceptEdits for the same reason as start()\n // above — do not allow bypassPermissions (TUI warning gate). Guarded by\n // __tests__/pty-ready-detection.test.ts.\n const args = [\n \"--permission-mode\",\n options.permissionMode ?? \"acceptEdits\",\n \"--settings\",\n '{\"spinnerTipsEnabled\":false}',\n \"--session-id\",\n sessionId,\n ];\n if (options.systemPrompt) {\n args.push(\"--system-prompt\", options.systemPrompt);\n }\n\n const proc = nodePty.spawn(resolveClaudeExe(), args, {\n name: \"xterm-256color\",\n cols: 120,\n rows: 40,\n cwd: options.projectPath,\n env: buildSpawnEnv(),\n });\n\n const session: InternalSession = {\n id: sessionId,\n provider: CLAUDE_CODE_PROVIDER,\n projectPath: options.projectPath,\n projectName,\n branch: \"\",\n status: \"running\",\n startedAt: new Date(),\n completedAt: null,\n promptCount: 0,\n lastOutput: \"\",\n process: proc,\n outputBuffer: Buffer.alloc(0),\n screen: createScreen(),\n inputHistory: [],\n };\n\n this.sessions.set(sessionId, session);\n this.pendingReady.add(sessionId);\n\n proc.onData((data: string) => {\n this.handleOutput(sessionId, data);\n });\n\n proc.onExit(({ exitCode }: { exitCode: number }) => {\n this.pendingReady.delete(sessionId);\n this.handleExit(sessionId, exitCode);\n });\n\n return toPublicSession(session);\n }\n\n // Write raw key bytes directly to the PTY without bracketed-paste wrapping.\n // Use for control sequences (arrow keys, Enter) that must not be quoted.\n sendKeys(sessionId: string, keys: string): void {\n const session = this.sessions.get(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n if (session.status === \"idle\") {\n throw new Error(`Session is idle (no active PTY): ${sessionId}`);\n }\n if (session.status === \"waiting_input\") {\n session.status = \"running\";\n this.onStatusChange?.(toPublicSession(session));\n }\n this.log.info(\n `[pty.keys.write] ${sessionId.slice(0, 8)} bytes=${keys.length} digest=${digestBytes(keys)}`,\n { event: \"pty.keys_write\", sessionId, byteLen: keys.length },\n );\n session.process.write(keys);\n session.lastActivityAt = new Date();\n }\n\n sendInput(sessionId: string, input: string): number {\n const session = this.sessions.get(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n if (session.status === \"idle\") {\n throw new Error(`Session is idle (no active PTY): ${sessionId}`);\n }\n // Claude is still booting (TUI not yet at first prompt). Writing into the\n // raw PTY now would let the startup UI swallow the keystrokes. Queue and\n // flush in flushQueuedInputs() once the prompt marker fires.\n if (this.pendingReady.has(sessionId)) {\n const queue = this.queuedInputs.get(sessionId) ?? [];\n queue.push(input);\n this.queuedInputs.set(sessionId, queue);\n session.lastActivityAt = new Date();\n session.promptCount++;\n // Surface that input is being held because Claude hasn't yet emitted a\n // prompt marker. If you see this without a corresponding pty.ready\n // follow-up, the marker detection has regressed.\n this.log.warn(\n `[pty.input.queued] ${sessionId.slice(0, 8)} promptCount=${session.promptCount} queueLen=${queue.length}`,\n {\n event: \"pty.input_queued\",\n sessionId,\n promptCount: session.promptCount,\n queueLen: queue.length,\n inputLen: input.length,\n },\n );\n return session.promptCount;\n }\n if (session.status === \"waiting_input\") {\n session.status = \"running\";\n this.onStatusChange?.(toPublicSession(session));\n }\n this.writeSubmit(sessionId, session, input, \"direct\", session.promptCount + 1);\n session.lastActivityAt = new Date();\n session.promptCount++;\n return session.promptCount;\n }\n\n // Two-step paste-then-submit. Writes the bracketed-paste body, then waits\n // for the PTY to go quiet before writing \\r. See buildPasteBytes() for why\n // the split matters.\n //\n // The wait is quiescence-based, not a flat delay: a fixed SUBMIT_DELAY_MS\n // timer (the original fix) still races a TUI that's mid-redraw of its own\n // output (e.g. re-painting right after posting a question) when the paste\n // lands — the timer can elapse and fire \\r while the TUI is still busy,\n // and that \\r gets absorbed by the redraw instead of submitting (see\n // 2026-07 session 14dda340: a \"Yes\" reply was accepted into the input line\n // but never landed as a submitted JSONL turn). Polling in SUBMIT_DELAY_MS\n // steps and only submitting once lastChunkAt hasn't advanced for a full\n // step gives the TUI as many extra ticks as it needs, capped at\n // SUBMIT_MAX_WAIT_MS so a silent/wedged PTY still gets its \\r eventually.\n private writeSubmit(\n sessionId: string,\n session: InternalSession,\n input: string,\n path: \"direct\" | \"flush\",\n promptCount: number,\n ): void {\n this.recordUserMessage(session, input);\n const pasteBytes = buildPasteBytes(input);\n this.log.info(\n `[pty.input.write] ${sessionId.slice(0, 8)} promptCount=${promptCount} bytes=${pasteBytes.length} digest=${digestBytes(pasteBytes)}`,\n {\n event: \"pty.input_write\",\n sessionId,\n promptCount,\n byteLen: pasteBytes.length,\n digest: digestBytes(pasteBytes),\n path,\n phase: \"paste\",\n },\n );\n const pasteAt = Date.now();\n session.process.write(pasteBytes);\n\n const trySubmit = () => {\n const current = this.sessions.get(sessionId);\n if (!current || current !== session) return;\n const now = Date.now();\n const lastChunk = this.lastChunkAt.get(sessionId) ?? pasteAt;\n const quiet = now - lastChunk >= SUBMIT_DELAY_MS;\n const timedOut = now - pasteAt >= SUBMIT_MAX_WAIT_MS;\n if (!quiet && !timedOut) {\n setTimeout(trySubmit, SUBMIT_DELAY_MS);\n return;\n }\n this.log.info(\n `[pty.input.submit] ${sessionId.slice(0, 8)} promptCount=${promptCount} digest=\\\\r waitedMs=${now - pasteAt} timedOut=${timedOut}`,\n {\n event: \"pty.input_write\",\n sessionId,\n promptCount,\n byteLen: SUBMIT_BYTES.length,\n digest: \"\\\\r\",\n path,\n phase: \"submit\",\n waitedMs: now - pasteAt,\n timedOut,\n },\n );\n current.process.write(SUBMIT_BYTES);\n };\n setTimeout(trySubmit, SUBMIT_DELAY_MS);\n }\n\n // Drain any inputs that were sent while the session was still pendingReady,\n // writing them in arrival order now that Claude is at its prompt.\n private flushQueuedInputs(sessionId: string): void {\n const queue = this.queuedInputs.get(sessionId);\n if (!queue || queue.length === 0) return;\n this.queuedInputs.delete(sessionId);\n const session = this.sessions.get(sessionId);\n if (!session) return;\n this.log.info(`[pty.flush] ${sessionId.slice(0, 8)} flushing ${queue.length} queued input(s)`, {\n event: \"pty.flush_queued\",\n sessionId,\n queueLen: queue.length,\n });\n // Chain queued inputs serially: each writeSubmit() defers its \\r by\n // SUBMIT_DELAY_MS, and we further stagger subsequent inputs by 2x the\n // delay so paste/submit pairs don't interleave on the wire. Two queued\n // inputs is rare in practice (user tapped Send twice during the brief\n // boot window), but ordering must still produce two distinct submits.\n queue.forEach((input, i) => {\n const writeAt = i * SUBMIT_DELAY_MS * 2;\n if (writeAt === 0) {\n this.writeSubmit(sessionId, session, input, \"flush\", session.promptCount);\n } else {\n setTimeout(() => {\n const current = this.sessions.get(sessionId);\n if (!current || current !== session) return;\n this.writeSubmit(sessionId, session, input, \"flush\", session.promptCount);\n }, writeAt);\n }\n });\n }\n\n cancel(sessionId: string): void {\n const session = this.sessions.get(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n session.process.kill(\"SIGINT\");\n }\n\n killPid(pid: number): void {\n try {\n process.kill(pid, \"SIGTERM\");\n } catch {\n // Process may already be gone\n }\n }\n\n // Kill the PTY and mark the session idle. Called by the WS grace timer.\n putOnHold(sessionId: string): void {\n const session = this.sessions.get(sessionId);\n if (!session) return;\n this.pendingReady.delete(sessionId);\n this.queuedInputs.delete(sessionId);\n this.firstChunkAt.delete(sessionId);\n this.permissionOpen.delete(sessionId);\n this.lastScreenQuestionKey.delete(sessionId);\n this.shellPromptOpen.delete(sessionId);\n this.quietCheckers.get(sessionId)?.cancel();\n this.quietCheckers.delete(sessionId);\n try {\n session.process.kill(\"SIGINT\");\n } catch {\n // already dead\n }\n session.status = \"idle\";\n session.completedAt = new Date();\n session.screen.dispose();\n this.sessions.delete(sessionId);\n this.onStatusChange?.(toPublicSession(session));\n }\n\n getOutput(sessionId: string): string {\n const session = this.sessions.get(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n return session.outputBuffer.toString(\"utf-8\");\n }\n\n // Render the last `maxLines` rows of the session's screen in true on-screen\n // order. Reads the headless terminal (fed raw PTY bytes in handleOutput) so\n // Claude's absolute-cursor repaints resolve to where they actually paint —\n // unlike the old raw-byte slice, which scrambled order after a TUI repaint\n // and made replayed conversations appear out of order on resume.\n //\n // Async because xterm parses writes on a deferred tick; we flush pending\n // writes (empty write + callback) before reading so the buffer is current.\n async getOutputLines(sessionId: string, maxLines: number): Promise<string[]> {\n const session = this.sessions.get(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n await new Promise<void>((resolve) => session.screen.write(\"\", () => resolve()));\n\n const buf = session.screen.buffer.active;\n const lines: string[] = [];\n // buf.length spans scrollback + viewport; iterate the whole thing top-down\n // so the rendered output preserves screen order, then keep the last N.\n for (let y = 0; y < buf.length; y++) {\n lines.push(buf.getLine(y)?.translateToString(true) ?? \"\");\n }\n // Drop trailing blank rows (the unused bottom of the viewport) before\n // trimming to maxLines, so replay isn't padded with empty lines.\n while (lines.length > 0 && lines[lines.length - 1] === \"\") {\n lines.pop();\n }\n return lines.slice(-maxLines);\n }\n\n getInputHistory(sessionId: string): UserMessage[] {\n return this.sessions.get(sessionId)?.inputHistory ?? [];\n }\n\n // Record a submitted user message as ground truth and fire onUserMessage.\n // Called from writeSubmit (both direct and flush paths) — never from\n // sendKeys, so raw keystrokes aren't logged as messages.\n private recordUserMessage(session: InternalSession, text: string): void {\n const ts = Date.now();\n session.inputHistory.push({ text, ts });\n if (session.inputHistory.length > INPUT_HISTORY_MAX) {\n session.inputHistory.shift();\n }\n this.onUserMessage?.(session.id, text, ts);\n }\n\n getSession(sessionId: string): ManagedSession | null {\n const session = this.sessions.get(sessionId);\n return session ? toPublicSession(session) : null;\n }\n\n hasSession(sessionId: string): boolean {\n return this.sessions.has(sessionId);\n }\n\n listSessions(): ManagedSession[] {\n return Array.from(this.sessions.values()).map(toPublicSession);\n }\n\n dispose(): void {\n for (const session of this.sessions.values()) {\n try {\n session.process.kill();\n } catch {\n // Process may already be dead\n }\n session.screen.dispose();\n }\n this.sessions.clear();\n this.firstChunkAt.clear();\n this.chunkIndex.clear();\n this.lastChunkAt.clear();\n for (const quiet of this.quietCheckers.values()) quiet.cancel();\n this.quietCheckers.clear();\n this.permissionOpen.clear();\n this.lastScreenQuestionKey.clear();\n this.shellPromptOpen.clear();\n }\n\n private handleOutput(sessionId: string, data: string): void {\n const session = this.sessions.get(sessionId);\n if (!session) return;\n\n const chunk = Buffer.from(data, \"utf-8\");\n const now = Date.now();\n if (!this.firstChunkAt.has(sessionId)) {\n this.firstChunkAt.set(sessionId, now);\n }\n\n // Per-chunk diagnostic log. Keep until the @<path> submit bug is solved.\n const idx = (this.chunkIndex.get(sessionId) ?? 0) + 1;\n this.chunkIndex.set(sessionId, idx);\n const last = this.lastChunkAt.get(sessionId);\n this.lastChunkAt.set(sessionId, now);\n const gapMs = last == null ? 0 : now - last;\n this.log.info(\n `[pty.chunk] ${sessionId.slice(0, 8)} #${idx} +${chunk.length}B gap=${gapMs}ms status=${session.status} digest=${digestBytes(data)}`,\n {\n event: \"pty.chunk\",\n sessionId,\n chunkIndex: idx,\n chunkBytes: chunk.length,\n gapMs,\n status: session.status,\n pendingReady: this.pendingReady.has(sessionId),\n digest: digestBytes(data),\n },\n );\n\n session.outputBuffer = Buffer.concat([session.outputBuffer, chunk]);\n\n if (session.outputBuffer.length > OUTPUT_BUFFER_MAX) {\n session.outputBuffer = session.outputBuffer.subarray(\n session.outputBuffer.length - OUTPUT_BUFFER_MAX,\n );\n }\n\n // Render into the headless screen so getOutputLines() can reproduce true\n // on-screen order. write() is async (parsed on a later tick) but we never\n // read the screen synchronously after a single chunk — replay only happens\n // on subscribe, long after these writes have drained.\n session.screen.write(data);\n\n const stripped = stripAnsi(data);\n session.lastOutput = stripped;\n const matchedMarker = CLAUDE_PROMPT_MARKERS.find((m) => stripped.includes(m));\n\n if (session.status === \"running\" && matchedMarker) {\n this.markReady(sessionId, session, `marker:${matchedMarker}`);\n } else if (\n session.status === \"running\" &&\n this.pendingReady.has(sessionId) &&\n now - (this.firstChunkAt.get(sessionId) ?? now) >= PROMPT_MARKER_FALLBACK_MS\n ) {\n // Fallback: PTY has produced output for >=10s but neither marker fired.\n // Treat the session as ready so queued input doesn't sit forever.\n this.markReady(sessionId, session, \"fallback:timeout\");\n }\n\n this.onOutput?.(sessionId, data);\n\n // Live interactive-prompt detection from the PTY stream — fires the moment\n // a prompt is on screen, independent of (and ahead of) the JSONL flush.\n // Trigger-gated so we don't scrape the rendered buffer on every chunk:\n // - OSC 777 (raw byte signal) → permission gate opened.\n // - \"Enter to select\" footer (AskUserQuestion menu) → structured question.\n // - prompt-ready marker without a fresh 777 → gate may have closed.\n this.detectLivePrompts(sessionId, data, stripped).catch((err) => {\n this.log.warn(\"[pty.prompt_detect] failed\", {\n event: \"pty.prompt_detect_failed\",\n sessionId,\n err,\n });\n });\n\n // Re-arm the quiet-checker on every chunk. If no further chunk arrives for\n // QUIET_DETECT_MS, re-run the same ready/prompt detection without waiting\n // for a chunk that may never come (Claude blocked on a prompt).\n let quiet = this.quietCheckers.get(sessionId);\n if (!quiet) {\n quiet = debounce(() => this.handleQuiet(sessionId), QUIET_DETECT_MS);\n this.quietCheckers.set(sessionId, quiet);\n }\n quiet();\n }\n\n // Detect permission gates (OSC 777 + scraped options) and AskUserQuestion\n // menus from the rendered screen, firing the additive callbacks. Async because\n // reading the rendered buffer needs the xterm write queue flushed. Pure\n // detection lives in services/questions/*; this only orchestrates triggers,\n // per-session debounce, and the callbacks.\n private async detectLivePrompts(\n sessionId: string,\n rawData: string,\n stripped: string,\n ): Promise<void> {\n const session = this.sessions.get(sessionId);\n if (!session) return;\n\n const oscPermission = hasPermissionOsc(rawData);\n // Footer test on the CURRENT chunk — a cheap trigger only. The authoritative\n // test runs on the full rendered screen below (askFooterOnScreen), because\n // the OSC-777 notify and the \"Enter to select\" footer often arrive in\n // SEPARATE chunks: the OSC lands first (no footer yet), so a chunk-only test\n // misclassifies an AskUserQuestion menu as a permission gate.\n const hasAskFooter = /Enter to select/i.test(stripped);\n const hasPromptMarker = CLAUDE_PROMPT_MARKERS.some((m) => stripped.includes(m));\n // Cheap raw-chunk hint that an UNSTRUCTURED shell prompt may be on screen:\n // a y/N hint, a \"press enter\"/\"continue?\" tail, or a numbered menu row. Just\n // a trigger gate (mirrors hasAskFooter) — the real conservative matching is\n // in detectShellPrompt against the rendered tail. ponytail: substring gate.\n const hasShellPromptHint =\n /[[(]\\s*y\\s*\\/\\s*n\\s*[\\])]|press\\s+(enter|return|any key)|\\bcontinue\\b\\s*\\?|^\\s*(?:❯|>)?\\s*\\d+[.)]\\s+\\S/im.test(\n stripped,\n );\n\n // Nothing to do unless a trigger fired or a prompt we already broadcast is\n // open (so we can detect its close on the next prompt-ready).\n if (\n !oscPermission &&\n !hasAskFooter &&\n !hasShellPromptHint &&\n !this.permissionOpen.has(sessionId) &&\n !this.shellPromptOpen.has(sessionId) &&\n !this.lastScreenQuestionKey.has(sessionId)\n ) {\n return;\n }\n\n const lines = await this.getOutputLines(sessionId, 60);\n\n // Authoritative footer test on the FULL rendered screen (not just the\n // trigger chunk). The \"Enter to select · Tab/Arrow keys to navigate\" footer\n // is unique to an AskUserQuestion menu; a permission gate uses \"Tab to amend\n // · ctrl+e to explain\". When this is on screen the prompt IS an\n // AskUserQuestion — even if an OSC-777 notify also fired — so it must take\n // priority over the permission path (Claude emits OSC 777 for BOTH).\n const askFooterOnScreen = lines.some((l) => /Enter to select/i.test(l));\n\n // Diagnostic: dump the rendered window + detector verdicts when a trigger\n // fires — the on-device source of truth for prompt detection (e.g. the\n // multi-question carousel work). At debug level so it's silent under normal\n // --verbose; enable with LOG_LEVEL=debug.\n if (oscPermission || hasAskFooter || askFooterOnScreen) {\n this.log.debug?.(`[pty.prompt_detect] ${sessionId.slice(0, 8)} trigger`, {\n event: \"pty.prompt_detect\",\n sessionId,\n oscPermission,\n hasAskFooter,\n askFooterOnScreen,\n permGate: oscPermission && !askFooterOnScreen ? scrapePermissionGate(lines) : undefined,\n askQuestion: askFooterOnScreen ? detectQuestionFromScreen(lines) : undefined,\n renderedTail: lines.slice(-25),\n });\n }\n\n // ── Permission gate ──────────────────────────────────────────────\n // Skip entirely when the screen is an AskUserQuestion menu — its OSC-777\n // notify would otherwise be misread as a permission gate.\n if (oscPermission && !askFooterOnScreen) {\n const gate = scrapePermissionGate(lines);\n this.permissionOpen.add(sessionId);\n // Broadcast even if options aren't painted yet (gate: empty options) so\n // the client can show \"Claude needs permission\" immediately; a later\n // repaint with the footer/options re-broadcasts the populated gate.\n this.onPermissionChange?.(sessionId, gate ?? { options: [] });\n } else if (this.permissionOpen.has(sessionId) && !askFooterOnScreen) {\n // Gate was open. If options are still on screen, refresh (cursor moved);\n // if the prompt is ready again and the options are gone, the gate closed.\n const gate = scrapePermissionGate(lines);\n if (gate) {\n this.onPermissionChange?.(sessionId, gate);\n } else if (hasPromptMarker) {\n this.permissionOpen.delete(sessionId);\n this.onPermissionChange?.(sessionId, null);\n }\n }\n\n // ── AskUserQuestion menu (rendered, ahead of JSONL) ──────────────\n // The multi-question TUI's final \"Ready to submit your answers?\" screen is\n // detected and broadcast as a normal card (Submit answers / Cancel) so the\n // user can tap to confirm — the carousel doesn't reliably auto-submit.\n // (Roadmap: robust carousel auto-submit.)\n if (askFooterOnScreen) {\n const detected = detectQuestionFromScreen(lines);\n if (detected) {\n const key = questionContentKey(detected.questions);\n if (this.lastScreenQuestionKey.get(sessionId) !== key) {\n this.lastScreenQuestionKey.set(sessionId, key);\n this.onLiveQuestion?.(sessionId, detected.questions);\n }\n }\n } else if (this.lastScreenQuestionKey.has(sessionId) && hasPromptMarker) {\n // The menu was open but its footer is gone and Claude's prompt marker is\n // back — the question was answered (or dismissed). Clear the screen key\n // and tell the server so the pending question is cancelled; without this\n // the answered menu lingers and a later repaint can re-broadcast it.\n this.lastScreenQuestionKey.delete(sessionId);\n this.onLiveQuestionGone?.(sessionId);\n }\n\n // ── Unstructured shell prompt (read -p \"[y/N]\", CLI picker, … ) ───\n // Last fallback: only when this isn't already a structured permission gate\n // or AskUserQuestion menu. Reuses the `permission` transport so mobile\n // renders a QuestionCard with zero new event handling; each option carries\n // its literal answerKeys (y\\r / n\\r / N\\r) so the client stays dumb.\n if (!oscPermission && !askFooterOnScreen && !this.permissionOpen.has(sessionId)) {\n const shell = detectShellPrompt(lines);\n if (shell) {\n const key = `${shell.prompt}\u0000${shell.options.map((o) => o.label).join(\"\u0000\")}`;\n if (this.shellPromptOpen.get(sessionId) !== key) {\n this.shellPromptOpen.set(sessionId, key);\n this.onPermissionChange?.(sessionId, {\n prompt: shell.prompt,\n options: shell.options,\n });\n }\n } else if (this.shellPromptOpen.has(sessionId) && hasPromptMarker) {\n // Prompt is gone and Claude's marker is back (input was answered) —\n // close the card.\n this.shellPromptOpen.delete(sessionId);\n this.onPermissionChange?.(sessionId, null);\n }\n }\n }\n\n // Fired QUIET_DETECT_MS after the last PTY chunk. Re-runs the same\n // ready/prompt detection handleOutput() runs per-chunk, using the last\n // rendered output — a session blocked on a prompt (or an unmarked boot\n // screen) may never produce another chunk to trigger detection otherwise.\n private handleQuiet(sessionId: string): void {\n const session = this.sessions.get(sessionId);\n if (session?.status !== \"running\") return;\n\n if (this.pendingReady.has(sessionId)) {\n this.markReady(sessionId, session, \"quiet:timeout\");\n }\n\n this.detectLivePrompts(sessionId, \"\", session.lastOutput).catch((err) => {\n this.log.warn(\"[pty.prompt_detect] failed\", {\n event: \"pty.prompt_detect_failed\",\n sessionId,\n err,\n });\n });\n }\n\n // Transition a session from \"running\" to \"waiting_input\", clear pendingReady,\n // and flush any queued input. Idempotent: callers can invoke at any chunk.\n private markReady(sessionId: string, session: InternalSession, reason: string): void {\n session.lastActivityAt = new Date();\n session.status = \"waiting_input\";\n // Log retained on purpose: `reason=fallback:timeout` would be the only\n // signal that Claude's TUI introduced a new boot variant our markers miss.\n const elapsedMs = Date.now() - (this.firstChunkAt.get(sessionId) ?? Date.now());\n this.log.info(`[pty.ready] ${sessionId.slice(0, 8)} ${reason} (elapsed=${elapsedMs}ms)`, {\n event: \"pty.ready\",\n sessionId,\n reason,\n elapsedMs,\n });\n this.onStatusChange?.(toPublicSession(session));\n if (this.pendingReady.has(sessionId)) {\n this.pendingReady.delete(sessionId);\n this.flushQueuedInputs(sessionId);\n this.onReady?.(toPublicSession(session));\n }\n }\n\n private handleExit(sessionId: string, exitCode: number): void {\n const session = this.sessions.get(sessionId);\n if (!session) return;\n\n session.completedAt = new Date();\n session.status = \"idle\";\n\n // Instant exit with no output — diagnose the most likely cause.\n const elapsedMs = session.completedAt.getTime() - session.startedAt.getTime();\n if (exitCode !== 0 && elapsedMs < 2000 && session.lastOutput === \"\") {\n if (!existsSync(session.projectPath)) {\n session.failureReason = `Project directory not found: ${session.projectPath}`;\n } else {\n session.failureReason =\n `Process exited immediately (code ${exitCode}). ` +\n `Check that the Claude binary is installed and accessible.`;\n }\n }\n\n this.onStatusChange?.(toPublicSession(session));\n session.screen.dispose();\n this.sessions.delete(sessionId);\n this.queuedInputs.delete(sessionId);\n this.firstChunkAt.delete(sessionId);\n this.permissionOpen.delete(sessionId);\n this.lastScreenQuestionKey.delete(sessionId);\n this.shellPromptOpen.delete(sessionId);\n this.quietCheckers.get(sessionId)?.cancel();\n this.quietCheckers.delete(sessionId);\n }\n}\n\nfunction toPublicSession(s: InternalSession): ManagedSession {\n return {\n id: s.id,\n provider: s.provider ?? CLAUDE_CODE_PROVIDER,\n projectPath: s.projectPath,\n projectName: s.projectName,\n branch: s.branch,\n status: s.status,\n startedAt: s.startedAt,\n completedAt: s.completedAt,\n promptCount: s.promptCount,\n lastOutput: s.lastOutput,\n ...(s.failureReason != null && { failureReason: s.failureReason }),\n ...(s.lastActivityAt != null && { lastActivityAt: s.lastActivityAt }),\n ...(s.filePath != null && { filePath: s.filePath }),\n };\n}\n\n// Strip ANSI escape sequences for clean text preview\nfunction stripAnsi(str: string): string {\n // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional ANSI stripping\n return str.replace(/\\x1b\\[[0-9;]*[a-zA-Z]/g, \"\").replace(/\\x1b\\][^\\x07]*\\x07/g, \"\");\n}\n","// Permission-gate detection. Two independent signals:\n//\n// 1. OSC 777 escape — the deterministic TRIGGER. The PTY emits\n// `\\x1b]777;notify;Claude Code;Claude needs your permission\\x07`\n// (often tmux-wrapped: `\\x1bPtmux;\\x1b\\x1b]777;notify;…\\x1b\\`) the instant\n// a gate opens. Confirmed in live logs (pty.chunk #53/#193). This tells us\n// a gate is open; it carries no option text.\n//\n// 2. Rendered option scrape — the gate's numbered options are painted into the\n// screen grid via absolute-cursor moves, so they live in the rendered\n// headless buffer (getOutputLines), not the raw byte stream. We read the\n// ACTUAL leading numbers and the `❯` cursor — the numbers are NOT a stable\n// 1-based index (a gate can show \"2. Yes / 3. No\"), which is exactly the\n// \"2 didn't take\" bug this avoids.\n\nexport interface PermissionOption {\n /** The real leading number shown on screen (NOT a 1-based array index). */\n index: number;\n label: string;\n /**\n * Literal keystroke bytes that answer this option, when the number alone\n * isn't the answer (e.g. a y/N shell prompt answers \"y\\r\"/\"n\\r\", not \"1\\r\").\n * Additive: OSC-777 gates omit it and the client answers via `index`; the\n * unstructured shell-prompt path (detectShellPrompt) populates it.\n */\n answerKeys?: string;\n}\n\nexport interface PermissionGate {\n /** Prompt text above the options, e.g. \"Claude needs your permission to use Bash\". */\n prompt?: string;\n /**\n * The descriptive block above the prompt — the tool title, the command, and any\n * action description Claude paints inside the gate box (newline-joined). Lets the\n * client show WHAT is being permitted, not just \"Do you want to proceed?\".\n */\n detail?: string;\n options: PermissionOption[];\n /** Index value (the on-screen number) of the `❯`-highlighted option, if any. */\n cursor?: number;\n}\n\n// OSC 777 with the notify command from Claude Code. We match the stable core\n// (`]777;notify;Claude Code`) rather than the whole tmux-passthrough wrapper so\n// detection survives both the wrapped and unwrapped forms.\n// biome-ignore lint/suspicious/noControlCharactersInRegex: matching the literal OSC 777 escape\nconst OSC_777_RE = /\\x1b\\]777;notify;Claude Code;[^\\x07\\x1b]*/;\n\n/** True if a raw PTY chunk contains the Claude Code OSC 777 permission notify. */\nexport function hasPermissionOsc(rawData: string): boolean {\n return OSC_777_RE.test(rawData);\n}\n\n// A rendered option row: optional `❯` cursor, then \"N. label\". Leading spaces\n// from the box/indent are tolerated (the box gutter is stripped first). We\n// capture N and the label separately.\nconst OPTION_RE = /^\\s*(❯)?\\s*(\\d+)\\.\\s+(.+?)\\s*$/;\n\n// Chrome lines that are never the prompt: footers, box-drawing, prompt arrows.\nconst FOOTER_RE = /Enter to select|Esc to cancel|↑|↓|to navigate|to cancel/i;\nconst BOX_ONLY_RE = /^[\\s│─┌┐└┘├┤┬┴┼╭╮╰╯╱╲=_-]+$/;\nconst PROMPT_ARROW_RE = /^[\\s]*[❯›>]\\s*$/;\n\n// Strip leading and trailing box-drawing gutters (\"│ … │\") so a boxed gate\n// frame parses the same as an unboxed one.\nfunction stripGutter(line: string): string {\n return line.replace(/^\\s*[│|]\\s?/, \"\").replace(/\\s*[│|]\\s*$/, \"\");\n}\n\n/**\n * Scrape the permission gate's options + prompt from rendered screen lines.\n * Returns null when no numbered options are present (not a gate, or not painted\n * yet). Pure — no I/O.\n */\nexport function scrapePermissionGate(lines: string[]): PermissionGate | null {\n const options: PermissionOption[] = [];\n let cursor: number | undefined;\n let firstOptionLine = -1;\n\n for (let i = 0; i < lines.length; i++) {\n const m = OPTION_RE.exec(stripGutter(lines[i]));\n if (!m) continue;\n const index = Number.parseInt(m[2], 10);\n if (!Number.isFinite(index)) continue;\n if (firstOptionLine === -1) firstOptionLine = i;\n if (m[1]) cursor = index; // `❯` marks the highlighted option\n options.push({ index, label: m[3] });\n }\n\n if (options.length === 0) return null;\n\n // Prompt = nearest non-empty, non-chrome line above the first option row.\n let prompt: string | undefined;\n let promptLine = -1;\n for (let i = firstOptionLine - 1; i >= 0; i--) {\n const t = stripGutter(lines[i]).trim();\n if (t.length === 0) continue;\n if (BOX_ONLY_RE.test(lines[i].trim()) || FOOTER_RE.test(t) || PROMPT_ARROW_RE.test(t)) continue;\n prompt = t || undefined;\n promptLine = i;\n break;\n }\n\n // Detail = the descriptive block ABOVE the prompt (tool title + command +\n // action description), newline-joined. The box top, 2+ consecutive blanks (the\n // gap to prior scrollback), or a ~6-line cap ends the block — so a\n // partial-window scrape can't vacuum unrelated terminal output.\n const detail = promptLine > 0 ? scrapeDetail(lines, promptLine) : undefined;\n\n return {\n ...(prompt ? { prompt } : {}),\n ...(detail ? { detail } : {}),\n options,\n ...(cursor !== undefined ? { cursor } : {}),\n };\n}\n\nconst MAX_DETAIL_LINES = 6;\n\nfunction scrapeDetail(lines: string[], promptLine: number): string | undefined {\n const collected: string[] = []; // bottom-up, content lines only\n let blankRun = 0;\n for (let i = promptLine - 1; i >= 0; i--) {\n // Strip the gutter first: a bare \"│\" is an INTERNAL blank line, not a frame\n // edge — only the true border rows (\"╭──╮\" / \"╰──╯\") are box-only once the\n // gutter is removed.\n const t = stripGutter(lines[i]).trim();\n if (BOX_ONLY_RE.test(t)) break; // box top / frame edge\n if (t.length === 0) {\n blankRun++;\n if (blankRun >= 2) break; // gap to prior scrollback\n continue; // single blank: skip (join already separates lines)\n }\n blankRun = 0;\n if (FOOTER_RE.test(t) || PROMPT_ARROW_RE.test(t)) continue; // skip chrome, keep walking\n collected.push(t);\n if (collected.length >= MAX_DETAIL_LINES) break;\n }\n\n return collected.length > 0 ? collected.reverse().join(\"\\n\") : undefined;\n}\n","import type { AskOption, AskQuestion } from \"../../types\";\n\n// Detect a structured AskUserQuestion menu from the RENDERED screen lines\n// (getOutputLines), not JSONL. The JSONL path (detectAskUserQuestion) is\n// structurally late — the question is painted in the TUI before the tool_use\n// block flushes to disk, and a backgrounded sub-agent may write to a different\n// JSONL than the watched session. Scraping the rendered screen fires the moment\n// the menu is on screen.\n//\n// The on-screen AskUserQuestion menu looks like:\n//\n// Which area are you focused on?\n// ❯ 1. macOS / Chrome\n// 2. iOS / Safari\n// …\n// 6. Chat about this\n// Enter to select · Tab/Arrow keys to navigate · Esc to cancel\n//\n// Differences from the JSONL form: no header/description/preview (the screen\n// only shows labels), and the leading number is the on-screen index. We map it\n// to the AskQuestion shape (header/description default to \"\") so the existing\n// `question` WS event and QuestionCard render path are reused unchanged.\n\n// The footer that terminates an AskUserQuestion menu — its presence is the\n// positive signal that this block IS a structured picker (vs a permission gate,\n// which uses a different footer and y/n phrasing).\nconst ASK_FOOTER_RE = /Enter to select/i;\nconst ESC_FOOTER_RE = /Esc to cancel|to navigate/i;\n\n// An option row: optional `❯` cursor + \"N. label\". Tolerates the box indent.\nconst OPTION_RE = /^\\s*(?:❯)?\\s*(\\d+)\\.\\s+(.+?)\\s*$/;\n\n// A question header ends with \"?\" (AskUserQuestion questions are interrogative).\nconst QUESTION_RE = /\\?\\s*$/;\n\nconst BOX_ONLY_RE = /^[\\s│─┌┐└┘├┤┬┴┼╭╮╰╯╱╲=_-]+$/;\n\n// Reject permission-gate option labels so a gate (caught separately via OSC 777)\n// never doubles as a structured question.\nconst PERMISSION_LABEL_RE = /^(Yes|No)\\b/i;\n\n// The final confirmation screen of a multi-question AskUserQuestion TUI:\n// Ready to submit your answers?\n// ❯ 1. Submit answers\n// 2. Cancel\n// Enter to select · …\n// The user already answered each question via cards, so this internal step is\n// auto-confirmed by the PTY layer and must never render as its own card.\nconst SUBMIT_QUESTION_RE = /ready to submit your answers\\?/i;\nconst SUBMIT_OPTION_RE = /^submit answers?$/i;\n\n// Strip leading AND trailing box-drawing gutters (\"│ … │\") so a menu drawn\n// inside a box parses the same as an unboxed one (the trailing gutter matters\n// for the \"?\"-suffix question test).\nfunction stripBoxGutter(line: string): string {\n return line.replace(/^\\s*[│|]\\s?/, \"\").replace(/\\s*[│|]\\s*$/, \"\");\n}\n\n/**\n * Detect an AskUserQuestion menu in rendered screen lines. Returns the question\n * set (single-question; the TUI shows one picker at a time) or null.\n * Pure — no I/O.\n */\nexport function detectQuestionFromScreen(lines: string[]): { questions: AskQuestion[] } | null {\n // Find the footer that closes the menu; scan upward from it.\n let footerIdx = -1;\n for (let i = lines.length - 1; i >= 0; i--) {\n if (ASK_FOOTER_RE.test(lines[i])) {\n footerIdx = i;\n break;\n }\n }\n if (footerIdx === -1) return null;\n\n // Collect option rows above the footer. Real menus interleave each \"N. label\"\n // row with wrapped DESCRIPTION lines and may insert a box border between\n // options, so a non-option line is NOT a hard stop — it's skipped as long as\n // we haven't reached the question line yet. We stop at: the question line (ends\n // with \"?\"), a blank gap once options have started, or the top of the window.\n const options: AskOption[] = [];\n let firstOptionIdx = -1;\n for (let i = footerIdx - 1; i >= 0; i--) {\n const line = lines[i];\n const inner = stripBoxGutter(line);\n const trimmed = inner.trim();\n if (trimmed.length === 0) {\n if (options.length === 0) continue; // gap between footer and options\n break; // blank line above the option block ends it\n }\n if (BOX_ONLY_RE.test(line.trim())) continue; // box border between/around options\n if (ESC_FOOTER_RE.test(line) && options.length === 0) continue;\n // The question line terminates the option block (it's the row right above\n // the first option, after any descriptions). Stop before consuming it.\n if (options.length > 0 && QUESTION_RE.test(trimmed) && !OPTION_RE.test(inner)) break;\n const m = OPTION_RE.exec(inner);\n if (m) {\n const label = m[2].trim();\n if (PERMISSION_LABEL_RE.test(label)) return null; // it's a permission gate\n options.unshift({ label, description: \"\" });\n firstOptionIdx = i;\n }\n // else: a wrapped description / continuation line for the option below it —\n // skip it and keep scanning upward for more options.\n }\n\n if (options.length < 2 || firstOptionIdx === -1) return null;\n\n // Question = nearest non-empty, non-box line above the first option row that\n // ends with \"?\".\n let question: string | undefined;\n for (let i = firstOptionIdx - 1; i >= 0; i--) {\n const raw = stripBoxGutter(lines[i]);\n const trimmed = raw.trim();\n if (trimmed.length === 0) continue;\n if (BOX_ONLY_RE.test(lines[i].trim())) continue;\n if (QUESTION_RE.test(trimmed)) {\n question = trimmed;\n }\n break;\n }\n\n if (!question) return null;\n\n // The multi-question \"Ready to submit your answers?\" confirmation IS surfaced\n // as a real card (Submit answers / Cancel) so the user can tap to confirm —\n // the multi-question carousel doesn't reliably auto-submit. (Roadmap: robust\n // auto-submit.) So we no longer reject it here.\n\n return {\n questions: [{ question, header: \"\", multiSelect: false, options }],\n };\n}\n\n/**\n * True when the rendered screen is the AskUserQuestion \"Ready to submit your\n * answers?\" confirmation (a question line + a \"Submit answers\" option under the\n * Ask footer). Pure — no I/O. Retained as a classifier (e.g. for analytics);\n * the submit screen now renders as a normal card.\n */\nexport function isSubmitConfirmationScreen(lines: string[]): boolean {\n let hasFooter = false;\n let hasSubmitQuestion = false;\n let hasSubmitOption = false;\n for (const line of lines) {\n const inner = stripBoxGutter(line).trim();\n if (ASK_FOOTER_RE.test(line)) hasFooter = true;\n if (SUBMIT_QUESTION_RE.test(inner)) hasSubmitQuestion = true;\n const m = OPTION_RE.exec(inner);\n if (m && SUBMIT_OPTION_RE.test(m[2].trim())) hasSubmitOption = true;\n }\n return hasFooter && hasSubmitQuestion && hasSubmitOption;\n}\n\n/**\n * Stable content key for de-duping a screen-scraped question against the JSONL\n * detection of the same question (they share no toolUseId). Keyed on the\n * question text + option labels.\n */\nexport function questionContentKey(questions: AskQuestion[]): string {\n return questions\n .map((q) => `${q.question} ${q.options.map((o) => o.label).join(\",\")}`)\n .join(\"::\");\n}\n","// Detect UNSTRUCTURED blocking prompts in the rendered tail — the ones that are\n// neither an AskUserQuestion menu (detectQuestionFromScreen) nor an OSC-777\n// permission gate (scrapePermissionGate): a shell `read -p \"… [y/N]\"`, a CLI\n// picker's `(y/n)`, a numbered menu printed by a plain script, etc.\n//\n// Scraped into the SAME structured shape the permission gate emits, so mobile\n// renders it as an ordinary QuestionCard with tappable options. Each option\n// carries the LITERAL keystroke that answers it (`answerKeys`) so the client\n// stays dumb — it just sends those bytes. This is the ONE net-new detector;\n// everything downstream (event shape, mobile rendering, answer-by-keystroke) is\n// reused.\n//\n// Philosophy mirrors the existing detectors: conservative but false-positive\n// tolerant. A spurious card is recoverable (the user ignores it or taps and the\n// keys are harmless); a MISSED prompt strands a blocked PTY. We only fire on a\n// tight set of patterns anchored to the LAST non-blank rendered line.\n\nconst ENTER = \"\\r\";\n\nexport interface ShellPromptOption {\n /** Positional fallback number (1-based). Authoritative answer is `answerKeys`. */\n index: number;\n label: string;\n /** The exact bytes to send to answer this option, e.g. \"y\\r\" or \"2\\r\". */\n answerKeys: string;\n}\n\nexport interface ShellPrompt {\n prompt: string;\n options: ShellPromptOption[];\n}\n\n// A y/N family prompt: the last line ends in a bracketed/parenthesised yes-no\n// hint. Capture which letter is the default (upper-case) only to keep the label\n// order natural — both options are always offered.\n// \"Continue? [y/N]\" \"Overwrite (y/n)?\" \"Proceed [Y/n] \"\nconst YN_RE = /[[(]\\s*y\\s*\\/\\s*n\\s*[\\])]/i;\n\n// A bare confirmation with no explicit options — Enter is the only sensible\n// answer. \"Press enter to continue\", \"Press any key…\", a trailing \"Continue?\".\nconst PRESS_ENTER_RE = /press\\s+(enter|return|any key)/i;\nconst CONTINUE_RE = /\\bcontinue\\b\\s*\\??\\s*$/i;\n\n// A numbered menu row: optional cursor, \"N. label\" or \"N) label\". Same grammar\n// scrapePermissionGate uses, but here the menu is a plain script's output with\n// no OSC-777 and no Claude box.\nconst NUMBERED_RE = /^\\s*(?:❯|>)?\\s*(\\d+)[.)]\\s+(.+?)\\s*$/;\n\n// Lines that are never a prompt: Claude's own box-drawing / menu chrome. If any\n// of these appear in the tail we defer to the structured detectors and bail —\n// this detector is strictly the fallback for plain shell output.\nconst CLAUDE_CHROME_RE = /Enter to select|Esc to cancel|╭|╰|│.*│/;\nconst BOX_ONLY_RE = /^[\\s│─┌┐└┘├┤┬┴┼╭╮╰╯╱╲=_-]+$/;\n\nfunction lastNonBlank(lines: string[]): { text: string; idx: number } | null {\n for (let i = lines.length - 1; i >= 0; i--) {\n const t = lines[i].trim();\n if (t.length > 0) return { text: t, idx: i };\n }\n return null;\n}\n\n/**\n * Scrape an unstructured shell prompt from rendered screen lines. Returns null\n * when the tail isn't a recognised prompt (prose, Claude's own UI, empty).\n * Pure — no I/O.\n */\nexport function detectShellPrompt(lines: string[]): ShellPrompt | null {\n // Defer to the structured detectors if Claude's TUI is on screen. Those paths\n // own AskUserQuestion / permission gates; this is plain-shell only.\n if (lines.some((l) => CLAUDE_CHROME_RE.test(l))) return null;\n\n const last = lastNonBlank(lines);\n if (!last) return null;\n\n // ── y/N family ───────────────────────────────────────────────────\n if (YN_RE.test(last.text)) {\n return {\n prompt: last.text,\n options: [\n { index: 1, label: \"Yes\", answerKeys: `y${ENTER}` },\n { index: 2, label: \"No\", answerKeys: `n${ENTER}` },\n ],\n };\n }\n\n // ── numbered menu ────────────────────────────────────────────────\n // Fire when the last line is itself a numbered row (menu just finished\n // printing) OR when a bare \"Press enter to confirm\"-style footer trails a\n // numbered block (Codex's \"Hooks need review\" trust dialog paints its options\n // above a \"Press enter to confirm or esc to go back\" line). Collect the\n // contiguous numbered block regardless — its options must not collapse into a\n // single generic Continue button (the bare-confirmation branch below).\n const lastNumberedIdx = (() => {\n for (let i = last.idx; i >= 0; i--) {\n if (NUMBERED_RE.test(lines[i])) return i;\n }\n return -1;\n })();\n if (lastNumberedIdx >= 0) {\n const options: ShellPromptOption[] = [];\n for (let i = 0; i <= lastNumberedIdx; i++) {\n const m = NUMBERED_RE.exec(lines[i]);\n if (!m) continue;\n const num = Number.parseInt(m[1], 10);\n if (!Number.isFinite(num)) continue;\n options.push({ index: num, label: m[2].trim(), answerKeys: `${num}${ENTER}` });\n }\n if (options.length >= 2) {\n // Prompt = nearest non-chrome line above the first numbered row.\n const firstRow = lines.findIndex((l) => NUMBERED_RE.test(l));\n let prompt = \"\";\n for (let i = firstRow - 1; i >= 0; i--) {\n const t = lines[i].trim();\n if (t.length === 0 || BOX_ONLY_RE.test(t)) continue;\n prompt = t;\n break;\n }\n return { prompt: prompt || \"Select an option\", options };\n }\n }\n\n // ── bare confirmation (Enter only) ───────────────────────────────\n if (PRESS_ENTER_RE.test(last.text) || CONTINUE_RE.test(last.text)) {\n return {\n prompt: last.text,\n options: [{ index: 1, label: \"Continue\", answerKeys: ENTER }],\n };\n }\n\n return null;\n}\n","import { execFile } from \"child_process\";\nimport { platform } from \"os\";\nimport { basename, dirname } from \"path\";\nimport { isWindows } from \"./platform\";\nimport type { DiscoveredProcess } from \"./types\";\n\nexport async function discoverClaudeProcesses(): Promise<DiscoveredProcess[]> {\n if (platform() === \"win32\") return discoverWindows();\n return discoverUnix();\n}\n\nasync function discoverUnix(): Promise<DiscoveredProcess[]> {\n const pids = await getPidsUnix();\n\n const results = await Promise.all(\n pids.map(async (pid) => {\n try {\n const [cwd, args, startedAt] = await Promise.all([\n getProcessCwdUnix(pid),\n getProcessArgsUnix(pid),\n getProcessStartTimeUnix(pid),\n ]);\n const conversationId = extractResumeId(args);\n\n return {\n pid,\n projectPath: cwd,\n projectName: basename(cwd),\n branch: await readGitBranch(cwd),\n conversationId,\n startedAt,\n } satisfies DiscoveredProcess;\n } catch {\n return null;\n }\n }),\n );\n\n return results.filter((r): r is DiscoveredProcess => r !== null);\n}\n\nasync function discoverWindows(): Promise<DiscoveredProcess[]> {\n const pids = await getPidsWindows();\n\n const results = await Promise.all(\n pids.map(async (pid) => {\n try {\n const info = await getProcessInfoWindows(pid);\n if (!info) return null;\n\n return {\n pid,\n projectPath: info.cwd,\n projectName: basename(info.cwd),\n branch: await readGitBranch(info.cwd),\n conversationId: extractResumeId(info.args),\n startedAt: info.startedAt,\n } satisfies DiscoveredProcess;\n } catch {\n return null;\n }\n }),\n );\n\n return results.filter((r): r is DiscoveredProcess => r !== null);\n}\n\n// ─── Unix Helpers ──────────────────────────────────────────────────\n\nfunction run(\n cmd: string,\n args: string[],\n opts: { cwd?: string; timeout?: number } = {},\n): Promise<string> {\n return new Promise((resolve, reject) => {\n execFile(\n cmd,\n args,\n { windowsHide: isWindows, encoding: \"utf-8\", timeout: opts.timeout ?? 5000, cwd: opts.cwd },\n (err, stdout) => {\n if (err) reject(err);\n else resolve(stdout as string);\n },\n );\n });\n}\n\nasync function getPidsUnix(): Promise<number[]> {\n try {\n const output = await run(\"pgrep\", [\"-x\", \"claude\"]);\n return output\n .trim()\n .split(\"\\n\")\n .filter(Boolean)\n .map((s) => Number.parseInt(s, 10));\n } catch {\n return [];\n }\n}\n\nasync function getProcessCwdUnix(pid: number): Promise<string> {\n const output = await run(\"lsof\", [\"-p\", String(pid), \"-a\", \"-d\", \"cwd\", \"-Fn\"]);\n const match = output.match(/n(.+)/);\n return match?.[1] ?? \"\";\n}\n\nasync function getProcessArgsUnix(pid: number): Promise<string> {\n return (await run(\"ps\", [\"-p\", String(pid), \"-o\", \"args=\"])).trim();\n}\n\nasync function getProcessStartTimeUnix(pid: number): Promise<Date> {\n const raw = (await run(\"ps\", [\"-p\", String(pid), \"-o\", \"lstart=\"])).trim();\n const d = new Date(raw);\n return Number.isNaN(d.getTime()) ? new Date() : d;\n}\n\n// ─── Windows Helpers ───────────────────────────────────────────────\n\nasync function getPidsWindows(): Promise<number[]> {\n try {\n const output = await run(\"tasklist\", [\"/FI\", \"IMAGENAME eq claude.exe\", \"/FO\", \"CSV\", \"/NH\"]);\n return output\n .trim()\n .split(\"\\n\")\n .filter(Boolean)\n .map((line) => {\n const parts = line.split(\",\");\n return Number.parseInt(parts[1]?.replace(/\"/g, \"\") ?? \"0\", 10);\n })\n .filter((pid) => pid > 0);\n } catch {\n return [];\n }\n}\n\nasync function getProcessInfoWindows(\n pid: number,\n): Promise<{ cwd: string; args: string; startedAt: Date } | null> {\n try {\n const output = await run(\"wmic\", [\n \"process\",\n \"where\",\n `ProcessId=${pid}`,\n \"get\",\n \"CommandLine,CreationDate,ExecutablePath\",\n \"/FORMAT:CSV\",\n ]);\n // wmic uses CRLF; split on \\r?\\n so the blank separator line becomes \"\" and is filtered out.\n const lines = output\n .trim()\n .split(/\\r?\\n/)\n .filter((l) => l.trim().length > 0);\n if (lines.length < 2) return null;\n\n const parts = lines[1].split(\",\");\n const args = parts[1] ?? \"\";\n const creationDate = parts[2] ?? \"\";\n\n // WMIC CreationDate format: 20260418153000.000000+000\n const year = creationDate.slice(0, 4);\n const month = creationDate.slice(4, 6);\n const day = creationDate.slice(6, 8);\n const hour = creationDate.slice(8, 10);\n const min = creationDate.slice(10, 12);\n const sec = creationDate.slice(12, 14);\n const startedAt = new Date(`${year}-${month}-${day}T${hour}:${min}:${sec}`);\n if (Number.isNaN(startedAt.getTime())) return null;\n\n // CWD is not directly available via wmic; use the executable path's parent directory as fallback\n const exePath = parts[3] ?? \"\";\n const cwd = exePath ? dirname(exePath) : \"\";\n\n return { cwd, args, startedAt };\n } catch {\n return null;\n }\n}\n\n// ─── Shared Helpers ────────────────────────────────────────────────\n\nfunction extractResumeId(args: string): string | null {\n const match = args.match(/--resume\\s+(\\S+)/);\n return match?.[1] ?? null;\n}\n\nasync function readGitBranch(dir: string): Promise<string> {\n try {\n return (\n await run(\"git\", [\"rev-parse\", \"--abbrev-ref\", \"HEAD\"], { cwd: dir, timeout: 3000 })\n ).trim();\n } catch {\n return \"\";\n }\n}\n","import { createNodeWebSocket } from \"@hono/node-ws\";\nimport { Connection, Client as TemporalClient } from \"@temporalio/client\";\nimport {\n applyIncludeFilter,\n applyPagination,\n applyProjectFilter,\n applySort,\n type Conversation,\n type ConversationMeta,\n ConversationScanner,\n type FileStatEntry,\n type SortOrder,\n search,\n} from \"@threadbase-sh/scanner\";\nimport { EventEmitter } from \"events\";\nimport {\n createReadStream,\n existsSync,\n watch as fsWatch,\n readdirSync,\n readFileSync,\n statSync,\n} from \"fs\";\nimport { realpath } from \"fs/promises\";\nimport type { Hono } from \"hono\";\nimport { createServer, type IncomingMessage, type ServerResponse } from \"http\";\nimport { homedir } from \"os\";\nimport { dirname, join } from \"path\";\nimport { createInterface } from \"readline\";\nimport type { WebSocket } from \"ws\";\nimport { type AgentClient, createAgentClient } from \"./agent/agent-client\";\nimport { type AgentConfig, readAgentConfig } from \"./agent/agent-config\";\nimport { type ConversationWriter, createConversationWriter } from \"./agent/conversation-writer\";\nimport { handleSendAgentInput } from \"./agent/handle-send-agent-input\";\nimport { handleStartAgentSession } from \"./agent/handle-start-agent-session\";\nimport { type AppEnv, createHonoApp } from \"./api/app\";\nimport { ALREADY_HANDLED } from \"./api/routes/sessions.routes\";\nimport { createWsRoutes } from \"./api/routes/ws.routes\";\nimport type { ApiDeps } from \"./api/types/api-deps\";\nimport {\n generateApiKey,\n loadBrowseRoot,\n loadBrowserCors,\n loadCacheDir,\n loadDefaultPermissionMode,\n loadPublicUrl,\n loadTailSize,\n setApiKey,\n validatePublicUrl,\n} from \"./auth\";\nimport {\n BrowsePathNotFoundError,\n createDirectory,\n listDirectories,\n resolveBrowsePath,\n} from \"./browse\";\nimport { ConversationCache, type ConversationListItem } from \"./conversation-cache\";\nimport { createPool, getDbConfig, maskConnectionString, runMigrations } from \"./db\";\nimport { CacheMetadataRepository } from \"./db/repositories/cacheMetadata.repository\";\nimport { ConversationsRepository } from \"./db/repositories/conversations.repository\";\nimport { ProjectsRepository } from \"./db/repositories/projects.repository\";\nimport { SessionsRepository } from \"./db/repositories/sessions.repository\";\nimport { recordUpload } from \"./db/upload-records\";\nimport { handleListProjects } from \"./handlers/handleListProjects\";\nimport { LiveSessionManager } from \"./live-session-manager\";\nimport { getLogger } from \"./logger\";\nimport { PairTokenStore } from \"./pair-store\";\nimport { discoverClaudeProcesses } from \"./process-discovery\";\nimport {\n CLAUDE_CODE_PROVIDER,\n CODEX_CLI_PROVIDER,\n coerceProviderForRunner,\n isProviderName,\n isProviderResumable,\n} from \"./providers\";\nimport { seal } from \"./seal\";\nimport { ConversationWatcher } from \"./services/conversations/conversationWatcher\";\nimport {\n findSearchTarget,\n type SearchableMessage,\n} from \"./services/conversations/findSearchTarget\";\nimport { parseAgentEntrypointsEnv } from \"./services/conversations/isAgentConversation\";\nimport { pruneAgentConversations } from \"./services/conversations/pruneAgentConversations\";\nimport { deriveProjectChatTitle } from \"./services/projectChats/deriveProjectChatTitle\";\nimport { questionContentKey } from \"./services/questions/detectQuestionFromScreen\";\nimport {\n questionsFromLines,\n shouldBroadcastQuestion,\n} from \"./services/questions/questionBroadcast\";\nimport { resolveAnswer } from \"./services/questions/resolveAnswer\";\nimport { SessionStore } from \"./session-store\";\nimport type {\n AskQuestion,\n DiscoveredProcess,\n PermissionOption,\n ServerConfig,\n SessionSortKey,\n SortOrder as SessionSortOrder,\n SessionStatus,\n} from \"./types\";\n\nimport { saveUploadFile } from \"./uploads\";\nimport { isCodexInjectedContext, toClientConversationLines } from \"./utils/codexConversationLine\";\nimport { computeConversationEtag } from \"./utils/conversationEtag\";\nimport { debounce } from \"./utils/debounce\";\nimport { isScannedSnapshotStale } from \"./utils/isScannedSnapshotStale\";\nimport { createScanProgressThrottle } from \"./utils/scanProgressThrottle\";\nimport { WSHub } from \"./ws-hub\";\n\nconst BROWSE_SYSTEM_PROMPT = (browseRoot: string) =>\n `You are working within the project boundary: ${browseRoot}. ` +\n `Do not read, write, or execute commands that access files or directories outside this boundary.`;\n\nconst DEFAULT_SYSTEM_PROMPT =\n \"When presenting options or choices to the user, limit the options to at most 3.\";\n\nconst DEFAULT_PTY_GRACE_PERIOD_MS = 270_000; // 4.5 minutes\n\n// A completed refreshFile within this window is treated as fresh — a retry\n// storm on a live conversation collapses to one parse per window instead of\n// one per request.\nconst REFRESH_TTL_MS = 2000;\n\n// Session start blocks for the PTY to reach waiting_input/idle before\n// responding; past this we fall back to the async 202 shape. Must stay BELOW\n// the mobile client's start-request fetch timeout (15s) — at the old 15s value\n// the client aborted first (\"fetch canceled\") and its retry double-spawned\n// sessions. Ready normally lands well under this: Claude's quiet-checker and\n// Codex's CODEX_READY_FALLBACK_MS (8s) both settle pendingReady first.\nconst START_READY_TIMEOUT_MS = 10_000;\n\n// Default OFF. Set to \"1\" or \"true\" to show Claude Agent SDK / claude-mem\n// runs in /api/conversations and /project-chats.\nexport function parseIncludeAgentsEnv(raw: string | undefined): boolean {\n if (raw === undefined) return false;\n const v = raw.trim().toLowerCase();\n return !(v === \"0\" || v === \"false\" || v === \"no\" || v === \"off\" || v === \"\");\n}\n\nexport class StreamerServer {\n private httpServer: ReturnType<typeof createServer>;\n private ptyManager: LiveSessionManager;\n private sessionStore: SessionStore;\n private wsHub: WSHub;\n private fileWatcher: ConversationWatcher;\n private sessionFileMap = new Map<string, string>(); // sessionId → JSONL filePath\n // Per-file seq assignments from the most recent onNewLineSpans (offset index),\n // handed to the immediately-following onNewLines so it can stamp WS `seq` on\n // the matching conversation_events entries. Same read → same lines order.\n private pendingLineSeqs = new Map<string, (number | null)[]>();\n private pendingQuestions = new Map<string, { toolUseId: string; questions: AskQuestion[] }>();\n // Content key of the AskUserQuestion currently broadcast for a session (from\n // either the rendered screen or JSONL), used to de-dupe the two paths: when\n // the screen detection fires first, the later JSONL flush of the same question\n // is suppressed. Cleared alongside pendingQuestions.\n private pendingQuestionKey = new Map<string, string>();\n // Per-session permission gate currently open (scraped via OSC 777). Parallel\n // to pendingQuestions; mobile answers it by sending the option index via\n // /input { keys }. Cleared when the gate closes.\n private pendingPermission = new Map<\n string,\n { prompt?: string; detail?: string; options: PermissionOption[]; cursor?: number }\n >();\n private scanner: ConversationScanner | null = null;\n // Set when better-sqlite3 is unusable (e.g. node ABI mismatch made\n // ConversationCache.open throw), or when config.scannerPersistent is false\n // (test isolation — the scanner's default SQLite index is a single shared\n // file unscoped by scanProfiles). All scanners are then built with\n // persistent: false so requests serve from disk instead of 500ing on\n // every touch of the scanner's own SQLite index.\n private scannerPersistenceDisabled = false;\n // Tracks every ConversationScanner ever created so close() can shut them all\n // down and release SQLite handles (open handles block temp-dir deletion on Windows).\n private allScanners = new Set<ConversationScanner>();\n private scannerReady: Promise<unknown> | null = null;\n // Set by onConversationChanged while a scan is in-flight; getScanner() does\n // a single rescan after the current one completes instead of restarting it.\n private scannerStale = false;\n // Single-flight + TTL guard around scanner.refreshFile (see refreshFileGuarded).\n // A live file's mtime is always newer than the snapshot, so an unguarded\n // refresh fires on every request and re-parses the whole file from byte 0.\n // Keyed by filePath; entries drop on settle + TTL expiry, so the map stays\n // bounded by the active-file set.\n private refreshInFlight = new Map<string, { promise: Promise<unknown>; completedAt: number }>();\n // True only while bindWithRetry is actively retrying. The persistent\n // listener-level 'error' handler demotes EADDRINUSE to debug during this\n // window so the self-healing kickstart-relaunch race doesn't spam warn.\n private binding = false;\n private cacheReady = false;\n // Every fire-and-forget task that runs a scan and then writes to this.cache\n // in an async continuation (startup warm-up, background count refresh, …).\n // close() awaits all of them before closing this.cache, so a scan's post-scan\n // cache writes (upsertFromScannerMeta / populateTailFromFile / pruneGhostFiles\n // / reconcileDeletions) can never hit a cache.db that was already closed\n // (\"database connection is not open\"), which would otherwise leave the cache\n // empty. Register via trackCacheWrite(); each entry removes itself on settle.\n private inFlightCacheWrites = new Set<Promise<unknown>>();\n private apiKey: string;\n private apiKeySource: \"config\" | \"cli\";\n private localNoAuth: boolean;\n private logMenubarRequests: boolean;\n private verbose: boolean;\n private scanProfiles:\n | Array<{ id: string; label: string; configDir: string; enabled: boolean; emoji: string }>\n | undefined;\n private dbPool: Awaited<ReturnType<typeof createPool>> | null = null;\n private dbInstanceId: string | null = null;\n private disableDb = false;\n private browseRoot: string | null = null;\n private publicUrl: string | null = null;\n private browserCors: string | undefined;\n private pairTokens = new PairTokenStore();\n private exchangeAttempts = new Map<string, number[]>();\n private sessionStartAttempts = new Map<string, number[]>();\n private sessionInputAttempts = new Map<string, number[]>();\n private ptyGracePeriodMs: number;\n private defaultSystemPrompt: string;\n private defaultPermissionMode: \"acceptEdits\" | \"manual\";\n // Map of sessionId → grace timer; fires to kill PTY after WS disconnect\n private ptyGraceTimers = new Map<string, ReturnType<typeof setTimeout>>();\n // Map of sessionId → set of subscribed WS clients\n private sessionSubscribers = new Map<string, Set<WebSocket>>();\n // Map of clientId → WS socket (populated by the \"register\" WS handshake)\n private clientIdToWs = new Map<string, WebSocket>();\n // Reverse map for cleanup on close\n private wsToClientId = new Map<WebSocket, string>();\n private cache: ConversationCache | null = null;\n private projectsRepo: ProjectsRepository | null = null;\n private conversationsRepo: ConversationsRepository | null = null;\n private sessionsRepo: SessionsRepository | null = null;\n private cacheMetadataRepo: CacheMetadataRepository | null = null;\n private discoveryCache: {\n entries: DiscoveredProcess[];\n fetchedAt: number;\n } | null = null;\n private cacheDir: string;\n private tailSize: number;\n private directoryDebounceMs: number;\n // Trailing-debounced trigger that flags the scanner stale after a quiet\n // period, collapsing a burst of directory events into one rescan. Assigned\n // in the constructor body (NOT a field initializer) so directoryDebounceMs\n // is already set when debounce() captures the wait.\n private markScannerStaleDebounced!: ReturnType<typeof debounce>;\n private codexRoots: string[];\n private includeAgents: boolean;\n private agentEntrypoints: ReadonlySet<string>;\n private honoApp: Hono<AppEnv>;\n private log = getLogger(\"server\");\n private agentConfig: AgentConfig;\n private agentClient: AgentClient | null = null;\n private sessionStatusBus = new EventEmitter();\n\n constructor(config: ServerConfig & { apiKey: string }) {\n this.sessionStatusBus.setMaxListeners(0);\n this.apiKey = config.apiKey;\n this.apiKeySource = config.apiKeySource ?? \"config\";\n this.localNoAuth = config.localNoAuth ?? false;\n this.logMenubarRequests = config.logMenubarRequests ?? false;\n if (this.localNoAuth) {\n console.warn(\n \"[WARN] localNoAuth is ENABLED — all requests from localhost bypass authentication. \" +\n \"Do not run with --local-no-auth in shared or production environments.\",\n );\n }\n this.verbose = config.verbose ?? false;\n this.disableDb = config.disableDb ?? false;\n this.scannerPersistenceDisabled = config.scannerPersistent === false;\n this.scanProfiles = config.scanProfiles;\n this.codexRoots = config.codexRoots ?? [join(homedir(), \".codex\", \"sessions\")];\n this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;\n this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;\n this.defaultPermissionMode =\n config.defaultPermissionMode ?? loadDefaultPermissionMode() ?? \"acceptEdits\";\n this.cacheDir = config.cacheDir ?? loadCacheDir() ?? join(homedir(), \".threadbase\", \"cache\");\n this.tailSize = config.tailSize ?? loadTailSize() ?? 10;\n this.directoryDebounceMs =\n parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ??\n config.directoryScanDebounceMs ??\n 1000;\n this.markScannerStaleDebounced = debounce(() => {\n if (this.scannerReady) this.scannerStale = true;\n else this.scanner = null;\n }, this.directoryDebounceMs);\n this.includeAgents = parseIncludeAgentsEnv(process.env.THREADBASE_INCLUDE_AGENTS);\n this.agentEntrypoints = parseAgentEntrypointsEnv(process.env.THREADBASE_AGENT_ENTRYPOINTS);\n\n const rawRoot = process.env.THREADBASE_BROWSE_ROOT ?? loadBrowseRoot() ?? config.browseRoot;\n if (rawRoot) {\n realpath(rawRoot)\n .then((resolved) => {\n this.browseRoot = resolved;\n if (this.verbose) this.log.info(`Browse root: ${resolved}`, { browseRoot: resolved });\n })\n .catch(() => {\n this.log.warn(`Warning: browse root does not exist: ${rawRoot}`, { browseRoot: rawRoot });\n });\n }\n\n const rawPublicUrl = process.env.THREADBASE_PUBLIC_URL ?? config.publicUrl ?? loadPublicUrl();\n if (rawPublicUrl) {\n const result = validatePublicUrl(rawPublicUrl);\n if (result.ok) {\n this.publicUrl = result.normalized;\n if (this.verbose)\n this.log.info(`Public URL: ${this.publicUrl}`, { publicUrl: this.publicUrl });\n } else {\n this.log.warn(`Warning: ${result.error}`, { error: result.error });\n }\n }\n\n this.browserCors = config.browserCors ?? loadBrowserCors();\n\n this.sessionStore = new SessionStore();\n this.wsHub = new WSHub();\n\n this.fileWatcher = new ConversationWatcher({\n onNewLineSpans: (filePath, spans, readFrom, endOffset) => {\n // Offset index: extend the per-message byte-span index with this read's\n // lines. Fires alongside onNewLines (which writes the tail); both\n // consume the same read. Best-effort — a failure here must never break\n // the tail write or WS broadcast.\n if (!this.cache) return;\n const cache = this.cache;\n // No stale seqs from a prior read may leak into this one's WS stamping.\n this.pendingLineSeqs.delete(filePath);\n try {\n const seqs = cache.extendMessageIndex(\n filePath,\n spans,\n statSync(filePath),\n readFrom,\n endOffset,\n );\n if (seqs === null) {\n // Non-contiguous read (watcher attached at EOF after downtime, or an\n // append raced a backfill): drop the file's index and rebuild from\n // scratch. Single-flighted, tracked so close() awaits it. No seqs\n // are stamped for this read — the client refetches on reconcile.\n cache.deleteFileIndex(filePath, ConversationCache.conversationIdForFile(filePath));\n cache.clearIndexParseState(filePath);\n this.trackCacheWrite(\n cache.backfillIndex(filePath).catch((err) => {\n this.log.warn(\"offset-index.backfill_failed\", {\n event: \"offset_index.backfill_failed\",\n filePath,\n trigger: \"noncontiguous-append\",\n err,\n });\n }),\n );\n return;\n }\n // Stash for the onNewLines handler (fires next for the same read) to\n // stamp WS `seq`. spans and lines are the same set in the same order.\n this.pendingLineSeqs.set(filePath, seqs);\n } catch (err) {\n this.log.warn(\"offset-index.extend_failed\", {\n event: \"offset_index.extend_failed\",\n filePath,\n err,\n });\n }\n },\n onNewLines: (filePath, lines) => {\n // One transactional cache write for the whole batch instead of per line.\n this.cache?.updateFromLines(filePath, lines);\n for (const [sessionId, watchedPath] of this.sessionFileMap) {\n if (watchedPath === filePath) {\n // toolUseId the client currently holds for this session (set by the\n // live-screen path as `screen:…`, or a prior JSONL flush). Captured\n // BEFORE the overwrite below so we can detect an id change.\n const priorToolUseId = this.pendingQuestions.get(sessionId)?.toolUseId;\n const { messages, pending } = questionsFromLines(sessionId, lines);\n for (const p of pending) {\n this.pendingQuestions.set(sessionId, p); // last pending wins\n const t = setTimeout(() => {\n if (this.pendingQuestions.get(sessionId)?.toolUseId === p.toolUseId) {\n this.cancelPendingQuestion(sessionId);\n }\n }, 60_000);\n t.unref();\n }\n // De-dupe vs the live-screen path: if the rendered detection already\n // broadcast this exact question (same content key), don't re-render —\n // EXCEPT when the real JSONL toolUseId differs from the synthetic\n // `screen:` id the client holds. The client answers with the id it\n // was given; if it still has the screen id, resolveAnswer rejects the\n // POST as tool_use_mismatch. Re-broadcasting the real id re-syncs the\n // client (mapAskQuestionToBlock just replaces activeQuestion — the\n // card re-renders identically) so answering works.\n for (const m of messages) {\n const key = questionContentKey(m.questions);\n const broadcast = shouldBroadcastQuestion({\n newContentKey: key,\n lastContentKey: this.pendingQuestionKey.get(sessionId),\n newToolUseId: m.toolUseId,\n priorToolUseId,\n });\n this.pendingQuestionKey.set(sessionId, key);\n if (broadcast) this.wsHub.broadcast(m);\n }\n // Additive batched event (one socket write) for newer clients. When\n // the offset index assigned seqs for this read, carry them parallel\n // to lines so a client can map each event to its message_index.\n // Codex rollout lines are normalized to Claude shape here — mobile\n // parseLineToMessage only understands type:user|assistant.\n const seqs = this.pendingLineSeqs.get(filePath);\n this.broadcastConversationLines(sessionId, lines, seqs);\n break;\n }\n }\n // Seqs are consumed for this read; drop them so a later read for a file\n // with no watched session can't reuse a stale mapping.\n this.pendingLineSeqs.delete(filePath);\n },\n onConversationChanged: (filePath) => {\n // A new JSONL appeared (or changed) in a watched project directory.\n // If we hold a per-file tail for it, re-drive the tail read from here\n // too: per-file fs.watch handles can die silently (2026-07-01 incident\n // — tails went permanently quiet while directory events kept flowing),\n // and the directory watcher is the survivor that can heal them.\n this.fileWatcher.poke(filePath);\n // Invalidate only the affected file's cache row immediately (cheap\n // single-row delete; wiping the whole cache on every event would\n // prevent the warm-up from persisting while active sessions write).\n // skipIfTailed: this same append also drives the live-tail watcher's\n // updateFromLines upsert; the two fire with no ordering guarantee, so\n // never delete a row a live tail just wrote (CRITICAL #2).\n this.cache?.invalidateByFilePath(filePath, { skipIfTailed: true });\n // Debounce the global scanner-staleness flip so a burst of directory\n // events during active sessions collapses into one rescan trigger\n // after a quiet period. The debounced callback still checks\n // scannerReady at fire time, preserving the anti-infinite-loop rule\n // (never null scannerReady mid-scan).\n this.markScannerStaleDebounced();\n this.log.debug?.(`Scanner invalidated by directory event: ${filePath}`, {\n filePath,\n event: \"cache.directory_change\",\n });\n },\n onFileDeleted: (filePath) => {\n const id = this.cache?.invalidateByFilePath(filePath);\n if (id)\n this.log.info(`Cache row invalidated after JSONL delete: ${id}`, {\n id,\n filePath,\n event: \"cache.invalidate_on_unlink\",\n });\n },\n });\n\n this.ptyManager = new LiveSessionManager({\n logger: getLogger(\"pty\"),\n onOutput: (sessionId, data) => {\n this.wsHub.broadcast({ type: \"terminal_output\", sessionId, data });\n },\n onUserMessage: (sessionId, text, ts) => {\n this.wsHub.broadcast({ type: \"user_message\", sessionId, text, ts });\n },\n onPermissionChange: (sessionId, gate) => {\n this.handlePermissionChange(sessionId, gate);\n },\n onLiveQuestion: (sessionId, questions) => {\n this.handleLiveQuestion(sessionId, questions);\n },\n onLiveQuestionGone: (sessionId) => {\n // The rendered AskUserQuestion menu closed. Clear the screen-dedupe key\n // and cancel the pending question so the answered card is dismissed and\n // a later repaint can't re-broadcast it. Only acts on a screen-scoped\n // question — once the JSONL flush recorded the real toolUseId, the\n // answer path (handleSendAnswer) already cleared it.\n this.pendingQuestionKey.delete(sessionId);\n const pq = this.pendingQuestions.get(sessionId);\n if (pq?.toolUseId.startsWith(\"screen:\")) {\n this.cancelPendingQuestion(sessionId);\n }\n },\n onReady: (session) => {\n const resp = this.sessionStore.get(session.id, this.ptyAttachedIds());\n if (resp) this.wsHub.broadcast({ type: \"session_ready\", session: resp });\n },\n onStatusChange: (session) => {\n this.sessionStore.updateManaged(session.id, {\n status: session.status,\n completedAt: session.completedAt,\n ...(session.lastActivityAt != null && { lastActivityAt: session.lastActivityAt }),\n });\n // Refresh the scanner index at the end of each Claude turn so the\n // conversation is searchable with up-to-date content immediately.\n if (session.status === \"waiting_input\" || session.status === \"idle\") {\n const filePath = this.sessionFileMap.get(session.id);\n if (filePath) {\n this.getScanner()\n .then((scanner) => this.refreshFileGuarded(scanner, filePath))\n .then((meta) => {\n // meta === null means the guard coalesced/skipped this refresh\n // (already in flight or within the TTL) — not a real result.\n this.log.info(\"scanner.refreshFile: ok\", {\n event: \"scanner.refresh\",\n sessionId: session.id,\n filePath,\n trigger: session.status,\n messageCount: meta?.messageCount,\n });\n })\n .catch((err) => {\n this.log.warn(\"scanner.refreshFile: failed\", {\n event: \"scanner.refresh_failed\",\n sessionId: session.id,\n filePath,\n trigger: session.status,\n err,\n });\n });\n }\n }\n // Stop watching JSONL when PTY exits (session goes idle)\n if (session.status === \"idle\") {\n const filePath = this.sessionFileMap.get(session.id);\n if (filePath) {\n this.fileWatcher.unwatch(filePath);\n this.sessionFileMap.delete(session.id);\n this.cancelPendingQuestion(session.id);\n }\n // A gone PTY can never have an open gate; clear silently.\n this.pendingPermission.delete(session.id);\n }\n const resp = this.sessionStore.get(session.id, this.ptyAttachedIds());\n if (resp) {\n this.wsHub.broadcast({ type: \"session_update\", session: resp });\n }\n this.sessionStatusBus.emit(`status:${session.id}`, session.status);\n },\n });\n\n // ─── Multi-agent mode bootstrap ──────────────────────────────────\n // When MULTI_AGENT_FLOW is on, construct the Temporal client + JSONL\n // writer. We use Connection.lazy() so the constructor stays sync —\n // the actual gRPC connection happens on first RPC.\n this.agentConfig = readAgentConfig();\n const agentConfig = this.agentConfig;\n let conversationWriter: ConversationWriter | null = null;\n if (agentConfig.enabled) {\n const connection = Connection.lazy({\n address: agentConfig.temporal.address,\n });\n const temporalClient = new TemporalClient({\n connection,\n namespace: agentConfig.temporal.namespace,\n });\n this.agentClient = createAgentClient({\n temporalClient,\n taskQueue: agentConfig.temporal.taskQueue,\n });\n // JSONL goes next to (not inside) the SQLite cacheDir, mirroring the\n // existing convention: ~/.threadbase/conversations/.\n const conversationsBaseDir =\n agentConfig.conversationsDir || join(dirname(this.cacheDir), \"conversations\");\n conversationWriter = createConversationWriter({\n baseDir: conversationsBaseDir,\n });\n }\n const agentClient = this.agentClient;\n\n const self = this;\n const apiDeps: ApiDeps = {\n // ponytail: getter so rotateApiKey() takes effect without restarting the server\n get apiKey() {\n return self.apiKey;\n },\n localNoAuth: this.localNoAuth,\n logMenubarRequests: this.logMenubarRequests,\n rotateApiKey: () => this.rotateApiKey(),\n publicUrl: this.publicUrl,\n browseRoot: this.browseRoot,\n browserCors: this.browserCors,\n ptyManager: this.ptyManager,\n sessionStore: this.sessionStore,\n wsHub: this.wsHub,\n cache: () => this.cache,\n projectsRepo: () => this.projectsRepo,\n conversationsRepo: () => this.conversationsRepo,\n sessionsRepo: () => this.sessionsRepo,\n cacheMetadataRepo: () => this.cacheMetadataRepo,\n ptyAttachedIds: () => this.ptyAttachedIds(),\n handleListSessions: (url, res) => this.handleListSessions(url, res),\n handleSessionsCount: (res) => this.handleSessionsCount(res),\n handleGetRecentSessions: (url, res) => this.handleGetRecentSessions(url, res),\n handleGetSessionNames: (res) => this.handleGetSessionNames(res),\n handleGetSession: (id, res) => this.handleGetSession(id, res),\n handleGetOutput: (id, res) => this.handleGetOutput(id, res),\n handleSendInput: (id, req, res) => this.handleSendInput(id, req, res),\n handleSendAnswer: (id, req, res) => this.handleSendAnswer(id, req, res),\n handleCancel: (id, res) => this.handleCancel(id, res),\n handleStopSession: (id, res) => this.handleStopSession(id, res),\n handleSetSessionName: (id, req, res) => this.handleSetSessionName(id, req, res),\n handleUploadFile: (id, req, res) => this.handleUploadFile(id, req, res),\n handleAdopt: (id, res) => this.handleAdopt(id, res),\n handleResume: (req, res) => this.handleResume(req, res),\n handleStartSession: (req, res) => this.handleStartSession(req, res),\n handleListConversations: (url, res) => this.handleListConversations(url, res),\n handleConversationsCount: (url, res) => this.handleConversationsCount(url, res),\n handleGetConversation: (id, url, res, ifNoneMatch) =>\n this.handleGetConversation(id, url, res, ifNoneMatch),\n handleSearch: (url, res) => this.handleSearch(url, res),\n handleSearchTarget: (id, req, res) => this.handleSearchTarget(id, req, res),\n handleListProjects: (url, res) => handleListProjects(url, res),\n handleGetPopularProjects: (url, res) => this.handleGetPopularProjects(url, res),\n handlePairStart: (res) => this.handlePairStart(res),\n handlePairExchange: (req, res) => this.handlePairExchange(req, res),\n handleBrowse: (url, res) => this.handleBrowse(url, res),\n handleMkdir: (req, res) => this.handleMkdir(req, res),\n handleWsOpen: (ws) => {\n this.wsHub.addClient(ws);\n const sessions = this.sessionStore.list(this.ptyAttachedIds());\n ws.send(JSON.stringify({ type: \"session_list\", sessions }));\n if (this.cacheReady) {\n ws.send(JSON.stringify({ type: \"cache_ready\" }));\n }\n },\n handleWsMessage: async (ws, raw) => {\n try {\n const msg = JSON.parse(String(raw));\n if (msg.type === \"register\" && typeof msg.clientId === \"string\") {\n const oldClientId = this.wsToClientId.get(ws);\n if (oldClientId) this.clientIdToWs.delete(oldClientId);\n this.clientIdToWs.set(msg.clientId, ws);\n this.wsToClientId.set(ws, msg.clientId);\n }\n if (msg.type === \"subscribe_session\" && typeof msg.sessionId === \"string\") {\n this.addSessionSubscriber(msg.sessionId, ws);\n if (this.ptyManager.hasSession(msg.sessionId)) {\n const lines = await this.ptyManager.getOutputLines(msg.sessionId, 200);\n const userMessages = this.ptyManager.getInputHistory(msg.sessionId);\n ws.send(\n JSON.stringify({\n type: \"terminal_replay\",\n sessionId: msg.sessionId,\n lines,\n userMessages,\n }),\n );\n }\n // A gate/question can open before the client finishes subscribing\n // (Codex's startup gates fire within ~500ms of spawn) — broadcast()\n // only reaches already-subscribed sockets, so a card that opened in\n // that window is otherwise lost forever. Replay pending state the\n // same way terminal_replay does above.\n const pendingGate = this.pendingPermission.get(msg.sessionId);\n if (pendingGate) {\n this.log.info(`[ws.replay_permission] ${msg.sessionId.slice(0, 8)}`, {\n event: \"ws.replay_permission\",\n sessionId: msg.sessionId,\n });\n ws.send(\n JSON.stringify({\n type: \"permission\",\n sessionId: msg.sessionId,\n ...(pendingGate.prompt ? { prompt: pendingGate.prompt } : {}),\n ...(pendingGate.detail ? { detail: pendingGate.detail } : {}),\n options: pendingGate.options,\n ...(pendingGate.cursor !== undefined ? { cursor: pendingGate.cursor } : {}),\n }),\n );\n }\n const pendingQuestion = this.pendingQuestions.get(msg.sessionId);\n if (pendingQuestion) {\n this.log.info(`[ws.replay_question] ${msg.sessionId.slice(0, 8)}`, {\n event: \"ws.replay_question\",\n sessionId: msg.sessionId,\n });\n ws.send(\n JSON.stringify({\n type: \"question\",\n sessionId: msg.sessionId,\n toolUseId: pendingQuestion.toolUseId,\n questions: pendingQuestion.questions,\n }),\n );\n }\n }\n if (msg.type === \"hold_session\" && typeof msg.sessionId === \"string\") {\n this.startGraceTimer(msg.sessionId, 0);\n }\n } catch {\n // malformed JSON, ignore\n }\n },\n handleWsClose: (ws) => {\n const clientId = this.wsToClientId.get(ws);\n if (clientId) {\n this.clientIdToWs.delete(clientId);\n this.wsToClientId.delete(ws);\n }\n for (const [sessionId, subscribers] of this.sessionSubscribers) {\n subscribers.delete(ws);\n // ptyGracePeriodMs === 0 disables the automatic hold-on-disconnect\n // timer; an explicit hold_session message still holds immediately.\n if (subscribers.size === 0 && this.ptyGracePeriodMs > 0) {\n this.startGraceTimer(sessionId, this.ptyGracePeriodMs);\n }\n }\n },\n agentClient,\n conversationWriter,\n agentConfig,\n };\n\n this.httpServer = createServer((req, res) => this.handleRequest(req, res));\n\n // Defense-in-depth against unhandled socket errors that would otherwise\n // crash the process with \"Unhandled 'error' event\":\n //\n // 1. 'clientError' fires when the http parser rejects a request (bad\n // headers, etc.). Default behavior destroys the socket, but a stale\n // handler could leak. We respond 400 (or destroy on any I/O error)\n // and never throw.\n this.httpServer.on(\"clientError\", (_err, socket) => {\n try {\n socket.end(\"HTTP/1.1 400 Bad Request\\r\\nConnection: close\\r\\n\\r\\n\");\n } catch {\n socket.destroy();\n }\n });\n // 2. Listener-level 'error' (port in use, etc.) — log instead of crashing.\n this.httpServer.on(\"error\", (err) => {\n const e = err as NodeJS.ErrnoException;\n // While bindWithRetry is retrying, each failed listen() attempt also\n // reaches this persistent handler. That EADDRINUSE is the expected,\n // self-healing kickstart race — log it at debug, not warn, so boots stay\n // quiet. Genuine runtime errors (and the final give-up) still warn.\n if (this.binding && e.code === \"EADDRINUSE\") {\n this.log.debug?.(`httpServer error during bind: ${err.message}`, {\n error: err.message,\n event: \"http.server_error\",\n });\n return;\n }\n this.log.warn(`httpServer error: ${err.message}`, {\n error: err.message,\n event: \"http.server_error\",\n });\n });\n // 3. The WebSocket upgrade race that caused real prod crashes:\n // @hono/node-ws registers an 'upgrade' listener that does `await\n // app.request(...)` before promoting the socket. If the peer RSTs\n // during the await, the raw net.Socket emits 'error' with no listener,\n // crashing the process. Registering our own 'upgrade' listener FIRST\n // attaches a noop 'error' handler to the raw socket so the upgrade\n // abort becomes a harmless event. Node fires upgrade listeners in\n // registration order, so this must be wired before injectWebSocket().\n this.httpServer.on(\"upgrade\", (_req, socket) => {\n socket.on(\"error\", () => {\n // Intentional: a RST during the WS handshake is normal client\n // behavior (network blip, peer kill). The socket is already torn\n // down; we just need to absorb the event so Node doesn't crash.\n });\n });\n\n // createNodeWebSocket needs the real Hono app (it calls app.request() on\n // upgrade). Resolve the chicken-and-egg by creating the app without WS\n // routes first, handing it to createNodeWebSocket, then mounting the WS\n // route onto the same app instance.\n this.honoApp = createHonoApp(apiDeps);\n const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app: this.honoApp });\n this.honoApp.route(\"/\", createWsRoutes(apiDeps, upgradeWebSocket));\n injectWebSocket(this.httpServer);\n }\n\n // ─── PTY Grace Timer ────────────────────────────────────────────\n\n private ptyAttachedIds(): Set<string> {\n return new Set(this.ptyManager.listSessions().map((s) => s.id));\n }\n\n /**\n * Send a session_list to only the client that triggered this HTTP request\n * (identified by X-Client-Id header → registered WS socket). Falls back to\n * a full broadcast if no match exists (old clients, or no WS registered yet).\n */\n private broadcastOrUnicastSessionList(req: IncomingMessage): void {\n const clientId = req.headers[\"x-client-id\"];\n const ws = typeof clientId === \"string\" ? this.clientIdToWs.get(clientId) : undefined;\n const payload = {\n type: \"session_list\" as const,\n sessions: this.sessionStore.list(this.ptyAttachedIds()),\n };\n if (ws) {\n this.wsHub.unicast(ws, payload);\n } else {\n this.wsHub.broadcast(payload);\n }\n }\n\n private addSessionSubscriber(sessionId: string, ws: WebSocket): void {\n let subs = this.sessionSubscribers.get(sessionId);\n if (!subs) {\n subs = new Set();\n this.sessionSubscribers.set(sessionId, subs);\n }\n subs.add(ws);\n // Cancel any pending grace timer since someone is now watching\n const existing = this.ptyGraceTimers.get(sessionId);\n if (existing) {\n clearTimeout(existing);\n this.ptyGraceTimers.delete(sessionId);\n }\n }\n\n private startGraceTimer(sessionId: string, delayMs: number): void {\n const existing = this.ptyGraceTimers.get(sessionId);\n if (existing) clearTimeout(existing);\n\n const timer = setTimeout(() => {\n this.ptyGraceTimers.delete(sessionId);\n if (this.ptyManager.hasSession(sessionId)) {\n // Never interrupt a session mid-response. A `running` PTY is actively\n // streaming a Claude turn that hasn't flushed to the JSONL yet; killing\n // it (SIGINT) would lose the in-flight answer. Re-arm the grace timer\n // and re-check after another grace period — it only becomes eligible\n // for hold once it settles back to waiting_input/idle.\n const resp = this.sessionStore.get(sessionId, this.ptyAttachedIds());\n if (resp?.status === \"running\") {\n this.log.info(\n `[grace] session ${sessionId} still running, deferring hold`,\n { sessionId, event: \"pty.grace_defer\" },\n \"pino\",\n );\n this.startGraceTimer(sessionId, delayMs);\n return;\n }\n this.sessionSubscribers.delete(sessionId);\n this.log.info(\n `[grace] killing idle PTY for ${sessionId}`,\n { sessionId, event: \"pty.grace_kill\" },\n \"pino\",\n );\n this.ptyManager.putOnHold(sessionId);\n const held = this.sessionStore.get(sessionId, this.ptyAttachedIds());\n if (held) this.wsHub.broadcast({ type: \"session_update\", session: held });\n } else {\n this.sessionSubscribers.delete(sessionId);\n }\n }, delayMs);\n\n this.ptyGraceTimers.set(sessionId, timer);\n }\n\n get port(): number {\n const addr = this.httpServer.address();\n return typeof addr === \"object\" && addr ? addr.port : 0;\n }\n\n async listen(port: number, opts?: { awaitReady?: boolean }): Promise<void> {\n // DB is still used for upload records and other non-session purposes.\n // Session state is no longer persisted to DB.\n const dbConfig = this.disableDb ? null : getDbConfig();\n if (dbConfig) {\n this.dbPool = await createPool(dbConfig);\n this.dbInstanceId = dbConfig.instanceId;\n const masked = maskConnectionString(dbConfig.connectionString);\n this.log.info(`Database enabled: ${masked}`, {\n connectionString: masked,\n instanceId: dbConfig.instanceId,\n });\n this.log.info(`Instance ID: ${dbConfig.instanceId}`, { instanceId: dbConfig.instanceId });\n await runMigrations(this.dbPool);\n this.log.info(\"Database migrations applied\", { event: \"db.migrations_applied\" });\n }\n\n // Bind with bounded retry. `launchctl kickstart -k` kills the old prod\n // instance and relaunches immediately; even after the old process has\n // exited cleanly, the kernel can hold :PORT in a transient teardown state\n // for a beat, so the fresh instance's first bind can race into EADDRINUSE.\n // Retrying with a short backoff absorbs that window instead of leaving the\n // process listener-less (the old behavior: the listener-level 'error'\n // handler logged EADDRINUSE once and gave up, failing the deploy\n // healthcheck). On the final attempt we let the error propagate so a\n // genuinely occupied port still surfaces loudly.\n await this.bindWithRetry(port);\n\n const warmUp = new Promise<void>((resolveWarm) => {\n {\n this.log.info(`Streamer server listening on port ${port}`, {\n port,\n event: \"server.listening\",\n });\n try {\n this.cache = ConversationCache.open(\n join(this.cacheDir, \"cache.db\"),\n this.tailSize,\n undefined,\n {\n filterAgentConversations: !this.includeAgents,\n agentEntrypoints: this.agentEntrypoints,\n onAgentFileDetected: (fp) => this.fileWatcher.unwatch(fp),\n },\n );\n if (!this.includeAgents) {\n const result = pruneAgentConversations(this.cache);\n if (result.pruned > 0 || result.missing > 0) {\n this.log.info(\n `Agent conversation prune: scanned=${result.scanned} pruned=${result.pruned} missing=${result.missing}`,\n { ...result, event: \"cache.prune_agents\" },\n );\n }\n }\n const db = this.cache.getDatabase();\n this.projectsRepo = new ProjectsRepository(db);\n this.conversationsRepo = new ConversationsRepository(this.cache);\n this.sessionsRepo = new SessionsRepository(this.sessionStore);\n this.cacheMetadataRepo = new CacheMetadataRepository(db);\n // Watch the active profile dirs (or ~/.claude/projects as fallback) so\n // new JSONL files created after startup are discovered and the scanner\n // and cache are invalidated without a restart.\n if (this.scanProfiles && this.scanProfiles.length > 0) {\n for (const profile of this.scanProfiles) {\n if (profile.enabled) {\n this.fileWatcher.watchDirectory(join(profile.configDir, \"projects\"));\n }\n }\n } else {\n this.fileWatcher.watchDirectory(join(homedir(), \".claude\", \"projects\"));\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n // Loud, not swallowed: without the cache every /api/conversations*\n // request falls back to slower disk-only scans. The most common\n // cause is a better-sqlite3 ABI mismatch (node_modules built against\n // a different Node) — name the fix so it isn't rediscovered from a\n // bare failure. The serve preflight (check-sqlite-abi.ts) catches\n // the ABI case before we ever get here; this covers a cache that\n // dies for any other reason mid-run.\n const abiMismatch =\n message.includes(\"NODE_MODULE_VERSION\") ||\n message.includes(\"was compiled against a different Node.js version\");\n this.log.error(\n `ConversationCache failed to open — running WITHOUT cache; ` +\n `/api/conversations, /api/conversations/count and /project-chats serve from disk (degraded).` +\n (abiMismatch ? ` Fix: npm rebuild better-sqlite3` : \"\") +\n ` (${message})`,\n { error: message, abiMismatch, event: \"cache.open_failed\" },\n );\n // The scanner's persistent index uses the same better-sqlite3 module;\n // fall back to in-memory scans so requests keep working from disk.\n this.scannerPersistenceDisabled = true;\n }\n // Use a dedicated scanner for warm-up, independent of this.scanner, so\n // that onConversationChanged invalidations during the scan cannot cause\n // getScanner() to restart indefinitely and leave the warm-up stuck.\n const warmupStatCache = this.buildStatCache(null);\n // Scanner 0.9.4 reads statCache only in non-persistent scans.\n const warmupScanner = this.newScanner(warmupStatCache ? { persistent: false } : undefined);\n this.allScanners.add(warmupScanner);\n // Throttle the per-file onProgress firings to ~one frame per whole\n // percent (plus the final tick) so a large scan doesn't flood every\n // WebSocket client with thousands of scan_progress messages.\n const shouldEmitProgress = createScanProgressThrottle();\n const scanOpts = {\n ...(this.scanProfiles ? { profiles: this.scanProfiles } : {}),\n ...this.codexScanOpts(),\n ...(warmupStatCache ? { statCache: warmupStatCache } : {}),\n };\n warmupScanner\n .scan({\n ...scanOpts,\n onProgress: (scanned, total) => {\n if (shouldEmitProgress(scanned, total)) {\n this.wsHub.broadcast({ type: \"scan_progress\", scanned, total });\n }\n },\n })\n .then(async () => {\n // Adopt the warm-up scan as the live scanner so the first real\n // request reuses it instead of paying for a second full scan.\n // Success path only — adopting a scanner whose scan rejected would\n // pair a broken engine with a resolved scannerReady, making every\n // later request throw instantly. Guard: only adopt if nothing else\n // already owns the slot.\n if (!this.scannerReady && !this.scanner) {\n this.scanner = warmupScanner;\n this.scannerReady = Promise.resolve();\n }\n if (!this.cache) return;\n const metas = [...warmupScanner.getMetadataCache().values()] as any[];\n // upsertFromScannerMeta returns IDs of rows actually upserted\n // (excluding agent JSONLs skipped when includeAgents=false).\n // Warming tails for filtered-out IDs would hit the\n // conversation_tail.conversation_id → conversation_meta(id) FK\n // and abort the whole warm-up before pruneGhostFiles can run.\n const upsertedIds = new Set(this.cache.upsertFromScannerMeta(metas));\n const tailTargets: Array<{ id: string; filePath: string }> = [];\n for (const m of metas) {\n if (!m.filePath) continue;\n const id =\n m.sessionId ||\n m.id\n ?.split(\"/\")\n .pop()\n ?.replace(/\\.jsonl$/, \"\") ||\n m.id;\n if (upsertedIds.has(id)) tailTargets.push({ id, filePath: m.filePath });\n }\n const BATCH = 50;\n let tailFailures = 0;\n for (let i = 0; i < tailTargets.length; i += BATCH) {\n const batch = tailTargets.slice(i, i + BATCH);\n for (const t of batch) {\n try {\n this.cache.populateTailFromFile(t.id, t.filePath);\n } catch (err) {\n // Benign race: the live ConversationWatcher runs during\n // warm-up, so an active session writing/deleting its JSONL\n // fires invalidateByFilePath() → invalidate(id), which deletes\n // the conversation_meta row we just upserted. The follow-up\n // tail insert then trips the conversation_tail → conversation_meta\n // FK. Skipping is correct — the row was invalidated and gets\n // re-upserted on the next scan, and pruneGhostFiles (below)\n // reconciles any file that was genuinely deleted. We must not\n // throw here or pruneGhostFiles never runs. Logged at info (not\n // debug) so the failing id+reason is visible by default.\n tailFailures += 1;\n this.log.info(\n `populateTailFromFile skipped for ${t.id}: ${\n err instanceof Error ? err.message : String(err)\n }`,\n { id: t.id, event: \"cache.warmup_tail_failed\" },\n );\n }\n }\n await new Promise<void>((r) => setImmediate(r));\n }\n if (tailFailures > 0) {\n this.log.warn(\n `Warm-up: ${tailFailures}/${tailTargets.length} tail populates skipped (see info logs for ids)`,\n {\n failures: tailFailures,\n total: tailTargets.length,\n event: \"cache.warmup_tail_failures\",\n },\n );\n }\n const pruned = this.cache.pruneGhostFiles();\n this.log.info(`Startup ghost prune: removed ${pruned.length} stale cache rows`, {\n count: pruned.length,\n event: \"cache.prune_ghosts\",\n });\n })\n .catch((err) => {\n const message = err instanceof Error ? err.message : String(err);\n this.log.warn(`Startup cache warm-up failed: ${message}`, {\n error: message,\n event: \"cache.warmup_failed\",\n });\n })\n .finally(() => {\n this.cacheReady = true;\n this.wsHub.broadcast({ type: \"cache_ready\" });\n resolveWarm();\n });\n }\n });\n // Track the warm-up's scan→cache-write chain so close() can await it.\n this.trackCacheWrite(warmUp);\n if (opts?.awaitReady) await warmUp;\n }\n\n // Bind the HTTP listener, retrying on a transient EADDRINUSE. See the call\n // site in listen() for why the race exists (kickstart -k relaunch). Total\n // worst case ≈ 6 × 500 ms = 3 s before the final attempt rethrows.\n private async bindWithRetry(port: number, attempts = 6, delayMs = 500): Promise<void> {\n this.binding = true;\n try {\n await this.bindWithRetryLoop(port, attempts, delayMs);\n } finally {\n this.binding = false;\n }\n }\n\n private async bindWithRetryLoop(port: number, attempts: number, delayMs: number): Promise<void> {\n for (let attempt = 1; attempt <= attempts; attempt++) {\n try {\n await new Promise<void>((resolve, reject) => {\n const onError = (err: NodeJS.ErrnoException) => {\n this.httpServer.removeListener(\"listening\", onListening);\n reject(err);\n };\n const onListening = () => {\n this.httpServer.removeListener(\"error\", onError);\n resolve();\n };\n this.httpServer.once(\"error\", onError);\n this.httpServer.once(\"listening\", onListening);\n this.httpServer.listen(port);\n });\n return;\n } catch (err) {\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"EADDRINUSE\" && attempt === attempts) {\n // Final attempt exhausted on a still-busy port: this is a genuine\n // failure (not the self-healing kickstart race), so surface it once\n // before rethrowing.\n this.log.error(\n `port ${port} still busy (EADDRINUSE) after ${attempts} attempts; giving up`,\n { port, attempts, event: \"server.bind_failed\" },\n );\n }\n if (e.code !== \"EADDRINUSE\" || attempt === attempts) throw err;\n // Routine kickstart-relaunch race: log at debug (invisible by default)\n // since bindWithRetry recovers on its own within the attempt budget.\n this.log.debug?.(\n `port ${port} busy (EADDRINUSE), retry ${attempt}/${attempts - 1} in ${delayMs}ms`,\n { port, attempt, event: \"server.bind_retry\" },\n );\n await new Promise<void>((r) => setTimeout(r, delayMs));\n }\n }\n }\n\n // Register a fire-and-forget task that writes to this.cache after a scan, so\n // close() can await it before closing cache.db. Removes itself on settle. The\n // caller keeps its own error handling; this wrapper swallows rejections so a\n // failed task never rejects close()'s Promise.all.\n private trackCacheWrite(task: Promise<unknown>): void {\n const guarded = task.catch(() => undefined);\n this.inFlightCacheWrites.add(guarded);\n void guarded.finally(() => {\n this.inFlightCacheWrites.delete(guarded);\n });\n }\n\n // Single-flight + TTL wrapper for scanner.refreshFile. Both call sites (the\n // detail-stale branch and the per-turn refresh) route through here so that\n // N stacked retries on a live, actively-appended file cost one parse, not N:\n // - a refresh already in flight for the path → await the same promise;\n // - a refresh that settled within REFRESH_TTL_MS → skip, return null (the\n // caller keeps serving the current snapshot);\n // - otherwise start one, cache the promise, and stamp completedAt on settle.\n // The map is bounded by the active-file set (entries only live while a\n // refresh is in flight or within its TTL and are overwritten on the next\n // refresh of the same path).\n private refreshFileGuarded(\n scanner: ConversationScanner,\n filePath: string,\n ): Promise<Awaited<ReturnType<ConversationScanner[\"refreshFile\"]>> | null> {\n const existing = this.refreshInFlight.get(filePath);\n if (existing) {\n const settled = existing.completedAt > 0;\n if (!settled) {\n // In flight: coalesce onto the same parse.\n return existing.promise as Promise<Awaited<\n ReturnType<ConversationScanner[\"refreshFile\"]>\n > | null>;\n }\n if (Date.now() - existing.completedAt < REFRESH_TTL_MS) {\n // Completed recently enough: serve the snapshot, skip the parse.\n return Promise.resolve(null);\n }\n }\n const entry = { promise: Promise.resolve<unknown>(null), completedAt: 0 };\n entry.promise = scanner.refreshFile(filePath).finally(() => {\n entry.completedAt = Date.now();\n });\n this.refreshInFlight.set(filePath, entry);\n return entry.promise as Promise<Awaited<ReturnType<ConversationScanner[\"refreshFile\"]>> | null>;\n }\n\n async close(): Promise<void> {\n for (const timer of this.ptyGraceTimers.values()) clearTimeout(timer);\n this.ptyGraceTimers.clear();\n this.markScannerStaleDebounced.cancel();\n // Wait for every fire-and-forget scan→cache-write task to finish before\n // tearing anything down. Their post-scan steps write to this.cache\n // (upsert / populateTail / pruneGhostFiles); closing cache.db under them\n // throws \"database connection is not open\" and leaves the cache empty\n // (deterministic once Stage 4's dir-mtime gate widened the scan window).\n // Snapshot the set — entries remove themselves as they settle.\n await Promise.all([...this.inFlightCacheWrites]);\n // Close all scanner SQLite connections before the cache so file handles are\n // released on Windows (open handles block temp-dir deletion in tests).\n // scanner.close() is async (scanner >=0.9.2): it awaits any in-flight scan\n // before releasing the DB handle, so a fire-and-forget refresh scan can't be\n // shut mid-indexAll(). Await all so handles are torn down only after scans\n // settle.\n await Promise.all([...this.allScanners].map((s) => s.close()));\n this.allScanners.clear();\n this.scanner = null;\n this.cache?.close();\n this.ptyManager.dispose();\n this.fileWatcher.dispose();\n this.wsHub.dispose();\n this.pairTokens.dispose();\n if (this.dbPool) {\n await this.dbPool.end();\n }\n // Force any sockets that survived wsHub.dispose() (e.g. a half-open\n // connection mid-upgrade) to close, so httpServer.close()'s callback —\n // which only fires once every connection drains — can't hang. Without\n // this the old process keeps :PORT bound until launchd's SIGKILL, and the\n // freshly-started instance hits EADDRINUSE. Guarded for Node < 18.2.\n this.httpServer.closeAllConnections?.();\n return new Promise((resolve) => {\n // Belt-and-suspenders: never let process exit block forever on the\n // listener close. The port is released the moment closeAllConnections()\n // runs; the timeout only guards against an unforeseen lingering socket.\n const timer = setTimeout(resolve, 2000);\n this.httpServer.close(() => {\n clearTimeout(timer);\n resolve();\n });\n });\n }\n\n // ─── Request Router ────────────────────────────────────────────\n\n private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const host = req.headers.host ?? \"localhost\";\n const webReq = new Request(`http://${host}${req.url ?? \"/\"}`, {\n method: req.method ?? \"GET\",\n headers: req.headers as Record<string, string>,\n });\n const honoRes = await this.honoApp.fetch(webReq, { incoming: req, outgoing: res });\n if (honoRes.status !== ALREADY_HANDLED) {\n await writeHonoResponse(honoRes, res);\n }\n }\n\n // ─── Handlers ──────────────────────────────────────────────────\n\n private handlePairStart(res: ServerResponse): void {\n const minted = this.pairTokens.mint();\n json(res, 200, {\n token: minted.token,\n expiresAt: minted.expiresAt,\n expiresInSeconds: minted.expiresInSeconds,\n publicUrl: this.publicUrl,\n });\n }\n\n private async handlePairExchange(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const ct = req.headers[\"content-type\"] ?? \"\";\n if (!String(ct).toLowerCase().includes(\"application/json\")) {\n json(res, 415, { error: \"Content-Type: application/json required\" });\n return;\n }\n\n const ip = req.socket.remoteAddress ?? \"unknown\";\n if (!this.checkExchangeRateLimit(ip)) {\n json(res, 429, { error: \"Too many pair exchange attempts; try again in a minute\" });\n return;\n }\n\n let body: any;\n try {\n body = await readBody(req);\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Invalid body\";\n json(res, 400, { error: message });\n return;\n }\n\n const { token, clientPublicKey } = body ?? {};\n if (typeof token !== \"string\" || typeof clientPublicKey !== \"string\") {\n json(res, 400, { error: \"Missing token or clientPublicKey\" });\n return;\n }\n\n const result = this.pairTokens.consume(token);\n if (!result.ok) {\n json(res, 401, { error: `Pair token ${result.reason}` });\n return;\n }\n\n let sealed: ReturnType<typeof seal>;\n try {\n sealed = seal(this.apiKey, clientPublicKey);\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Invalid clientPublicKey\";\n json(res, 400, { error: message });\n return;\n }\n\n const { hostname } = require(\"os\");\n const ts = new Date().toISOString();\n this.log.info(`[pair] token exchanged from ${ip} at ${ts}`, {\n event: \"pair.token_exchanged\",\n ip,\n ts,\n });\n\n json(res, 200, {\n ciphertext: sealed.ciphertext,\n nonce: sealed.nonce,\n ephemeralPublicKey: sealed.ephemeralPublicKey,\n publicUrl: this.publicUrl,\n machineName: hostname(),\n });\n }\n\n private rotateApiKey(): { newKey: string; persisted: boolean } {\n const oldKey = this.apiKey;\n const newKey = generateApiKey();\n // Only persist to server.yaml when the key came from there.\n // If --api-key was passed on the CLI, the flag wins on restart and\n // would silently revert to the old key — so skip the write and let\n // the caller know via the response.\n const persisted = this.apiKeySource === \"config\";\n if (persisted) setApiKey(newKey);\n this.apiKey = newKey;\n this.log.info(\"API key rotated\", {\n event: \"auth.api_key_rotated\",\n oldKeyMasked: `${oldKey.slice(0, 6)}…`,\n newKeyMasked: `${newKey.slice(0, 6)}…`,\n persisted,\n });\n return { newKey, persisted };\n }\n\n private checkRateLimit(\n map: Map<string, number[]>,\n key: string,\n limit: number,\n windowMs: number,\n ): boolean {\n const now = Date.now();\n const arr = (map.get(key) ?? []).filter((t) => now - t < windowMs);\n if (arr.length >= limit) {\n map.set(key, arr);\n return false;\n }\n arr.push(now);\n map.set(key, arr);\n // TTL-evict the entry once the window expires so the map doesn't grow unbounded.\n setTimeout(() => {\n const remaining = (map.get(key) ?? []).filter((t) => Date.now() - t < windowMs);\n if (remaining.length === 0) map.delete(key);\n else map.set(key, remaining);\n }, windowMs);\n return true;\n }\n\n private checkExchangeRateLimit(ip: string): boolean {\n return this.checkRateLimit(this.exchangeAttempts, ip, 5, 60_000);\n }\n\n private checkSessionStartRateLimit(ip: string): boolean {\n // 10 new sessions per minute per client IP\n return this.checkRateLimit(this.sessionStartAttempts, ip, 10, 60_000);\n }\n\n private checkSessionInputRateLimit(sessionId: string): boolean {\n // 500 keystrokes per minute per session\n return this.checkRateLimit(this.sessionInputAttempts, sessionId, 500, 60_000);\n }\n\n private async handleListConversations(url: URL, res: ServerResponse): Promise<void> {\n const limit = intParam(url, \"limit\", 50);\n const offset = intParam(url, \"offset\", 0);\n const sort = (url.searchParams.get(\"sort\") ?? \"recent\") as SortOrder;\n const project = url.searchParams.get(\"project\") ?? undefined;\n const providerFilter = url.searchParams.get(\"provider\") ?? undefined;\n const bustCache = url.searchParams.get(\"refresh\") === \"1\";\n\n // refresh=1 is a RECONCILE, not a wipe. The old path did\n // cache.invalidate() (deletes every row, including live-tailed ones) +\n // scanner=null (discards the warm scanner and forces a cold cache\n // rebuild). Instead: run one fresh full-glob scan (fullRescan bypasses the\n // scanner's dir-mtime gate — an explicit user refresh is the \"check for\n // real\" signal), then reconcile the cache from disk truth: upsert what\n // exists (newest-wins, so a concurrent live line still takes precedence),\n // and drop only the rows whose files no longer exist. Live-tailed rows are\n // never blanket-deleted, so an active conversation can't flicker out.\n if (bustCache && this.cache) {\n const scanner = await this.rescanForRefresh();\n const metas = [...scanner.getMetadataCache().values()];\n try {\n this.cache.upsertFromScannerMeta(metas as any[]);\n const livePaths = new Set(\n metas.map((m) => m.filePath).filter((p): p is string => Boolean(p)),\n );\n this.cache.reconcileDeletions(livePaths);\n } catch (err) {\n this.log.warn(\n `refresh reconcile failed: ${err instanceof Error ? err.message : String(err)}`,\n { event: \"conversations.reconcile_failed\" },\n );\n }\n }\n\n if (this.cache) {\n const { conversations, total } = this.cache.listConversations({\n project,\n provider: providerFilter,\n limit,\n offset,\n });\n const adapted = conversations.map((c) => ({\n id: c.id,\n title: deriveProjectChatTitle({\n title: c.title,\n projectName: c.projectName,\n projectPath: c.projectPath,\n id: c.id,\n }),\n sessionName: undefined as string | undefined,\n filePath: c.filePath,\n projectPath: c.projectPath,\n branch: c.branch ?? undefined,\n account: c.account ?? undefined,\n preview: c.preview ?? undefined,\n messageCount: c.messageCount,\n lastActivity: c.lastActivity,\n firstMessage: c.firstMessage ? (JSON.parse(c.firstMessage) as unknown) : undefined,\n lastMessage: c.lastMessage ? (JSON.parse(c.lastMessage) as unknown) : undefined,\n model: c.model ?? undefined,\n provider: c.provider ?? CLAUDE_CODE_PROVIDER,\n }));\n json(res, 200, { conversations: adapted, hasMore: offset + limit < total, offset, total });\n return;\n }\n\n const scanner = await this.getScanner();\n let metas = [...scanner.getMetadataCache().values()];\n metas = applyIncludeFilter(metas, \"conversations\");\n if (project) metas = applyProjectFilter(metas, project);\n if (providerFilter)\n metas = metas.filter((m) => (m.provider ?? CLAUDE_CODE_PROVIDER) === providerFilter);\n metas = applySort(metas, sort);\n const total = metas.length;\n const page = applyPagination(metas, limit, offset);\n\n const adapted = (page.items as ConversationMeta[]).map((c) => {\n const id =\n c.sessionId ||\n c.id\n .split(\"/\")\n .pop()\n ?.replace(/\\.jsonl$/, \"\") ||\n c.id;\n return {\n id,\n title: deriveProjectChatTitle({\n title: c.sessionName,\n projectName: c.projectName,\n projectPath: c.projectPath,\n id,\n }),\n sessionName: c.sessionName || undefined,\n filePath: c.filePath,\n projectPath: c.projectPath,\n branch: c.gitBranch ?? undefined,\n account: c.account,\n preview: c.preview || undefined,\n messageCount: c.messageCount,\n lastActivity: c.timestamp,\n firstMessage: c.firstMessage ?? undefined,\n lastMessage: c.lastMessage ?? undefined,\n model: c.model ?? undefined,\n provider: (c as any).provider ?? CLAUDE_CODE_PROVIDER,\n };\n });\n json(res, 200, { conversations: adapted, hasMore: offset + limit < total, offset, total });\n }\n\n private async handleConversationsCount(url: URL, res: ServerResponse): Promise<void> {\n const project = url.searchParams.get(\"project\") ?? undefined;\n const providerFilter = url.searchParams.get(\"provider\") ?? undefined;\n const bustCache = url.searchParams.get(\"refresh\") === \"1\";\n\n // refresh=1 historically forced a full synchronous scan() to recount from\n // disk. On a cold/empty index that scan walks every JSONL and blocks ~16s,\n // tripping mobile's request timeout into a false \"unreachable\". Mirror the\n // detail path's skipStaleRescan stance: serve the indexed/cached total\n // immediately and reconcile from disk in the BACKGROUND so the count stays\n // fast regardless of refresh.\n if (this.cache) {\n const { total } = this.cache.listConversations({\n project,\n provider: providerFilter,\n limit: 0,\n offset: 0,\n });\n json(res, 200, { total });\n if (bustCache) this.refreshCountInBackground();\n return;\n }\n\n const scanner = await this.getScanner(true);\n let metas = [...scanner.getMetadataCache().values()];\n metas = applyIncludeFilter(metas, \"conversations\");\n if (project) metas = applyProjectFilter(metas, project);\n if (providerFilter)\n metas = metas.filter((m) => (m.provider ?? CLAUDE_CODE_PROVIDER) === providerFilter);\n json(res, 200, { total: metas.length });\n }\n\n // Fire-and-forget full rescan that reconciles the SQLite cache from disk so a\n // later count reflects new/removed conversations. Never awaited by the request\n // path — refresh=1 returns the cached total synchronously and this catches up.\n private refreshCountInBackground(): void {\n // Tracked so close() awaits this scan→cache-write before closing cache.db.\n this.trackCacheWrite(\n (async () => {\n try {\n const scanner = await this.getFreshScanner();\n if (this.cache) {\n this.cache.upsertFromScannerMeta([...scanner.getMetadataCache().values()] as any[]);\n }\n } catch (err) {\n this.log.warn(\n `Background count refresh failed: ${err instanceof Error ? err.message : String(err)}`,\n { event: \"count.refresh_failed\" },\n );\n }\n })(),\n );\n }\n\n private handleSessionsCount(res: ServerResponse): void {\n json(res, 200, { total: this.sessionStore.list(this.ptyAttachedIds()).length });\n }\n\n private handleGetRecentSessions(url: URL, res: ServerResponse): void {\n const limit = intParam(url, \"limit\", 20);\n if (!this.cache) {\n json(res, 200, { sessions: [], total: 0 });\n return;\n }\n const { conversations } = this.cache.listConversations({ limit, offset: 0 });\n // Items here are conversation cache rows, not live sessions in SessionStore.\n // The `type` discriminator lets mobile route taps through /api/sessions/resume\n // (which spawns a fresh PTY) instead of GET /api/sessions/:id (which 404s).\n const sessions = conversations.map((c) => ({\n type: \"conversation\" as const,\n id: c.id,\n status: \"idle\" as const,\n ptyAttached: false,\n projectId: c.projectId ?? undefined,\n projectPath: c.projectPath ?? \"\",\n projectName: c.projectName ?? \"\",\n branch: c.branch ?? undefined,\n lastOutput: \"\",\n elapsedMs: 0,\n promptCount: c.messageCount,\n startedAt: c.lastActivity,\n lastActivityAt: c.lastActivity,\n }));\n json(res, 200, { sessions, total: sessions.length });\n }\n\n private handleGetPopularProjects(url: URL, res: ServerResponse): void {\n const limit = intParam(url, \"limit\", 20);\n if (!this.cache) {\n json(res, 200, { projects: [], total: 0 });\n return;\n }\n const projects = this.cache.getPopularProjects(limit);\n json(res, 200, { projects, total: projects.length });\n }\n\n private buildStatCache(\n previousScanner: ConversationScanner | null,\n ): Map<string, { stat: FileStatEntry; meta: ConversationMeta }> | undefined {\n if (!this.cache) return undefined;\n if (!previousScanner) {\n const persisted = this.cache.getScannerStatCache();\n return persisted.size > 0 ? persisted : undefined;\n }\n const dbStats = this.cache.getFileStats();\n if (dbStats.size === 0) return undefined;\n const metaByPath = new Map<string, ConversationMeta>();\n if (previousScanner) {\n for (const meta of previousScanner.getMetadataCache().values()) {\n if (meta.filePath) metaByPath.set(meta.filePath, meta);\n }\n }\n const statCache = new Map<string, { stat: FileStatEntry; meta: ConversationMeta }>();\n for (const [filePath, stat] of dbStats) {\n const meta = metaByPath.get(filePath);\n if (meta) statCache.set(filePath, { stat, meta });\n }\n return statCache.size > 0 ? statCache : undefined;\n }\n\n // Returns the provider + codexRoots fragment to spread into every scan()/search() call.\n // codexRoots=[] disables codex scanning (safe no-op per scanner contract).\n private codexScanOpts() {\n return {\n providers: [CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER],\n codexRoots: this.codexRoots,\n };\n }\n\n // skipStaleRescan: when an indexed scanner already exists, return it directly\n // even if scannerStale is set, leaving the flag untouched so the next\n // list-level call still rescans. The single-conversation detail path passes\n // this — its per-file refreshFile (in findConversationByUuid) already\n // reconciles the one conversation being requested, so paying a full-tree\n // rescan just because some OTHER file changed is the stall this avoids.\n private newScanner(\n options?: ConstructorParameters<typeof ConversationScanner>[0],\n ): ConversationScanner {\n return new ConversationScanner(\n options ?? (this.scannerPersistenceDisabled ? { persistent: false } : undefined),\n );\n }\n\n private async getScanner(skipStaleRescan = false): Promise<ConversationScanner> {\n if (this.scannerReady) {\n await this.scannerReady;\n // onConversationChanged may have nulled this.scanner while we awaited —\n // if so, fall through and create a fresh one.\n if (this.scanner) {\n if (skipStaleRescan) return this.scanner;\n // If file events arrived while the scan was running, do one rescan now\n // rather than serving a stale result. The stale flag is cleared first so\n // any events during the rescan trigger another pass on the next call.\n if (this.scannerStale) {\n this.scannerStale = false;\n this.scanner = null;\n this.scannerReady = null;\n return this.getScanner();\n }\n return this.scanner;\n }\n }\n this.scannerStale = false;\n const statCache = this.buildStatCache(this.scanner);\n // Scanner 0.9.4 reads statCache only in non-persistent scans.\n this.scanner = this.newScanner(statCache ? { persistent: false } : undefined);\n this.allScanners.add(this.scanner);\n this.scannerReady = this.scanner.scan({\n ...(this.scanProfiles ? { profiles: this.scanProfiles } : {}),\n ...this.codexScanOpts(),\n ...(statCache ? { statCache } : {}),\n });\n try {\n await this.scannerReady;\n } catch (err) {\n // Don't memoize the rejection — a stored rejected promise would make\n // every future request replay this error instantly instead of retrying.\n this.scanner = null;\n this.scannerReady = null;\n throw err;\n }\n // Capture before returning — onConversationChanged could null this.scanner\n // in the microtask between the await and the return.\n const scanner = this.scanner;\n if (!scanner) return this.getScanner();\n return scanner;\n }\n\n private async getFreshScanner(): Promise<ConversationScanner> {\n this.scanner = null;\n this.scannerReady = null;\n return this.getScanner();\n }\n\n // refresh=1's scan: reuse the WARM scanner and re-run its scan with\n // fullRescan:true — the escape hatch that bypasses the scanner's\n // dir-mtime discovery gate, since an explicit user pull-to-refresh is exactly\n // the \"don't trust the gate, check disk for real\" signal. Unlike\n // getFreshScanner() this does NOT discard the warm scanner. scannerReady is\n // only ever reassigned to a live scan promise (never nulled mid-scan), so the\n // getScanner() anti-infinite-loop guard is preserved.\n private async rescanForRefresh(): Promise<ConversationScanner> {\n // Let any in-flight scan finish first so we don't run two scans on the same\n // index concurrently.\n if (this.scannerReady) await this.scannerReady;\n // A full rescan supersedes any pending staleness.\n this.scannerStale = false;\n if (!this.scanner) {\n this.scanner = new ConversationScanner();\n this.allScanners.add(this.scanner);\n }\n const scanner = this.scanner;\n this.scannerReady = scanner.scan({\n ...(this.scanProfiles ? { profiles: this.scanProfiles } : {}),\n ...this.codexScanOpts(),\n fullRescan: true,\n });\n await this.scannerReady;\n return scanner;\n }\n\n private findJsonlPath(uuid: string): string | null {\n const projectsDir = join(homedir(), \".claude\", \"projects\");\n if (!existsSync(projectsDir)) return null;\n const filename = `${uuid}.jsonl`;\n for (const dir of readdirSync(projectsDir)) {\n const fp = join(projectsDir, dir, filename);\n if (existsSync(fp)) return fp;\n const projectDir = join(projectsDir, dir);\n try {\n for (const sub of readdirSync(projectDir)) {\n const subagentPath = join(projectDir, sub, \"subagents\", filename);\n if (existsSync(subagentPath)) return subagentPath;\n }\n } catch {\n // Not a directory or no access\n }\n }\n return null;\n }\n\n private async readCwdFromJsonl(filePath: string): Promise<string | null> {\n return new Promise((resolve) => {\n const rl = createInterface({ input: createReadStream(filePath), crlfDelay: Infinity });\n let found = false;\n rl.on(\"line\", (line) => {\n if (found) return;\n try {\n const entry = JSON.parse(line);\n if (entry.cwd) {\n found = true;\n rl.close();\n resolve(entry.cwd as string);\n }\n } catch {\n // skip malformed lines\n }\n });\n rl.on(\"close\", () => {\n if (!found) resolve(null);\n });\n rl.on(\"error\", () => resolve(null));\n });\n }\n\n /**\n * Live Codex sessions keep `SessionResponse.conversationId === managed.id`\n * (stable deep-link / PTY key) and store the rollout UUID separately as\n * `boundConversationId`. REST history is indexed under the rollout UUID, so\n * resolve the placeholder → bound id before looking up the scanner.\n */\n private resolveConversationLookupId(uuid: string): string {\n const managed = this.sessionStore.getManaged(uuid);\n if (managed?.boundConversationId) return managed.boundConversationId;\n return uuid;\n }\n\n /** File path for a live managed session (placeholder id or bound Codex id). */\n private findLiveSessionFilePath(uuid: string): string | null {\n const direct = this.sessionFileMap.get(uuid);\n if (direct) return direct;\n for (const s of this.sessionStore.listManaged()) {\n if (s.boundConversationId === uuid) {\n return this.sessionFileMap.get(s.id) ?? null;\n }\n }\n return null;\n }\n\n /** True when a conversation UUID is the bound rollout of a live PTY session. */\n private isBoundConversationLive(boundId: string): boolean {\n for (const s of this.sessionStore.listManaged()) {\n if (s.boundConversationId === boundId && this.ptyManager.hasSession(s.id)) {\n return true;\n }\n }\n return false;\n }\n\n /**\n * Broadcast conversation JSONL lines to WS clients. Codex rollout lines are\n * normalized to the Claude `type:user|assistant` shape mobile understands;\n * Claude lines pass through unchanged so seq alignment stays intact.\n */\n private broadcastConversationLines(\n sessionId: string,\n lines: string[],\n seqs?: (number | null)[] | null,\n ): void {\n const clientLines = toClientConversationLines(lines);\n if (clientLines.length === 0) return;\n const seqsOk = !!seqs && seqs.length === lines.length && clientLines.length === lines.length;\n this.wsHub.broadcast({\n type: \"conversation_events\",\n sessionId,\n lines: clientLines,\n ...(seqsOk ? { seqs } : {}),\n });\n // ...plus per-line conversation_event so older mobile clients,\n // which only know that shape, keep working byte-for-byte.\n for (const line of clientLines) {\n this.wsHub.broadcast({ type: \"conversation_event\", sessionId, line });\n }\n }\n\n private async findConversationByUuid(uuid: string): Promise<Conversation | null> {\n const lookupId = this.resolveConversationLookupId(uuid);\n\n // Cold-start fast path: until the warm-up scan has populated this.scanner\n // (this.scannerReady is null), do NOT trigger a full scan to answer a\n // single-conversation request — that scan walks every JSONL on disk and is\n // the 20s+ stall that makes mobile abort. Resolve the file directly\n // (findJsonlPath is an O(project-dirs) walk) and parse just that one file.\n // The warm-up scan keeps running in the background; once it adopts the\n // scanner, subsequent requests use the indexed hot path below.\n if (!this.scannerReady && !this.scanProfiles) {\n const filePath =\n this.findJsonlPath(lookupId) ??\n this.findLiveSessionFilePath(uuid) ??\n this.findLiveSessionFilePath(lookupId);\n if (filePath) {\n const account = this.cache?.getMetaById(lookupId)?.account ?? undefined;\n const coldScanner = this.scanner ?? this.newScanner();\n const page = await coldScanner.parseSingleFilePage(filePath, account, {\n limit: Number.MAX_SAFE_INTEGER,\n });\n if (page) return page.conversation;\n }\n // No JSONL on disk (or unparseable). Return null WITHOUT triggering a\n // full scan — confirming not-found is not worth the 20s stall. The\n // caller's cache-tail fallback / 404 self-heal handles it.\n return null;\n }\n\n // Use the existing indexed scanner without honoring the global scannerStale\n // full-rescan: the per-file refreshFile below reconciles the one\n // conversation we care about, so a sibling file changing must not stall this\n // single-conversation request behind a full-tree rescan.\n const scanner = await this.getScanner(true);\n const fromIndex = await scanner.getConversation(lookupId);\n if (fromIndex) {\n // Live-session bypass: a conversation with a live PTY is exactly the case\n // that stalls — its mtime is always newer than the snapshot, so the stale\n // check below would refresh on every request. Serve the current snapshot\n // with no stale-check and no refresh. The live client is on WS receiving\n // conversation_event lines; the (TTL-throttled) turn-end refresh advances\n // the snapshot server-side; mobile refetches once on the running →\n // not-running transition, which is the reconcile point.\n // Codex: PTY is keyed by the placeholder session id, while the scanner\n // indexes the bound rollout UUID — check both.\n if (\n this.ptyManager.hasSession(uuid) ||\n this.ptyManager.hasSession(lookupId) ||\n this.isBoundConversationLive(lookupId)\n ) {\n // Codex live sessions: the scanner LRU snapshot is often frozen at bind\n // time (mtime always looks \"live\", so SWR never refreshes). Re-parse the\n // watched rollout so REST history includes turns written after bind.\n // Claude keeps the cheap bypass — its offset index + WS seq path stay fresh.\n const livePath =\n this.findLiveSessionFilePath(uuid) ??\n this.findLiveSessionFilePath(lookupId) ??\n fromIndex.filePath ??\n null;\n const isCodexLive =\n this.isBoundConversationLive(lookupId) ||\n this.sessionStore.getManaged(uuid)?.provider === CODEX_CLI_PROVIDER;\n if (isCodexLive && livePath) {\n try {\n const account =\n this.cache?.getMetaById(lookupId)?.account ??\n (fromIndex as { account?: string }).account ??\n undefined;\n const page = await scanner.parseSingleFilePage(livePath, account, {\n limit: Number.MAX_SAFE_INTEGER,\n });\n if (page?.conversation) return page.conversation;\n } catch (err) {\n this.log.warn(\"codex.live_reparse_failed\", {\n event: \"codex.live_reparse_failed\",\n conversationId: lookupId,\n filePath: livePath,\n err,\n });\n }\n }\n return fromIndex;\n }\n // The scanner memoizes both its metadata index and parsed conversations\n // for the server's lifetime. A conversation that grows after the initial\n // scan (the chokidar watcher keeps the SQLite cache fresh, but never the\n // scanner) keeps serving the startup snapshot here — so the detail/info\n // view shows a stale message count + last activity that disagrees with\n // the list view and with what --resume actually replays.\n //\n // Stale-while-revalidate: since a snapshot already exists, respond from it\n // immediately and refresh the one file's indexes in the background\n // (single-flighted + TTL-throttled via refreshFileGuarded, tracked so\n // close() awaits it). The next request after the refresh settles sees the\n // fresh data. Only a conversation with NO snapshot pays the parse\n // synchronously (the getConversation-null fallthrough below), so a cold\n // thundering herd costs one parse, not N.\n if (fromIndex.filePath && this.isConversationSnapshotStale(fromIndex)) {\n const filePath = fromIndex.filePath;\n this.trackCacheWrite(\n this.refreshFileGuarded(scanner, filePath).catch((err) => {\n this.log.warn(\"scanner.refreshFile: failed\", {\n event: \"scanner.refresh_failed\",\n conversationId: uuid,\n filePath,\n trigger: \"detail-swr\",\n err,\n });\n }),\n );\n }\n return fromIndex;\n }\n\n if (this.scanProfiles) return null;\n\n const filePath =\n this.findJsonlPath(lookupId) ??\n this.findLiveSessionFilePath(uuid) ??\n this.findLiveSessionFilePath(lookupId);\n if (!filePath) return null;\n this.scanner = null;\n this.scannerReady = null;\n const freshScanner = await this.getScanner();\n return freshScanner.getConversation(lookupId);\n }\n\n // True when the JSONL on disk is meaningfully newer than the scanned\n // snapshot's last-activity timestamp — i.e. the file grew after the scan.\n private isConversationSnapshotStale(conv: Conversation): boolean {\n if (!conv.filePath) return false;\n let mtimeMs: number | null = null;\n try {\n mtimeMs = statSync(conv.filePath).mtimeMs;\n } catch {\n // Stat failed (file moved/deleted mid-flight) — don't force a re-scan.\n return false;\n }\n return isScannedSnapshotStale(conv.timestamp, mtimeMs);\n }\n\n private async handleGetConversation(\n id: string,\n url: URL,\n res: ServerResponse,\n ifNoneMatch?: string,\n ): Promise<void> {\n // Try the scanner first (has full content including tool_use blocks).\n // Fall back to the cache tail only when the scanner can't find the file —\n // e.g. a conversation that existed in a previous run but whose JSONL was deleted.\n const conversation = await this.findConversationByUuid(id);\n\n if (!conversation && this.cache) {\n // Only `before_index` indicates the client is paginating backward (asking\n // for messages older than a cursor) — `msg_limit` is just page size and is\n // sent on the first page too. The tail fallback should serve any first-page\n // request when the JSONL is missing, regardless of msg_limit.\n const isFirstLoad = !url.searchParams.has(\"before_index\");\n if (isFirstLoad) {\n const tail = this.cache.getConversationTail(id);\n if (tail && tail.messages.length > 0) {\n const cachedMeta = this.cache.getMetaById(id);\n const cachedProvider = cachedMeta?.provider ?? CLAUDE_CODE_PROVIDER;\n const availability = classifyResumability(cachedMeta?.projectPath);\n const messagesPayload = tail.messages.map((m, idx) => ({\n message_index: idx,\n role: m.role,\n timestamp: m.timestamp,\n text: m.text,\n tool_calls: [] as unknown[],\n content: (m.content ?? []).filter((b: any) => b.type !== \"text\"),\n }));\n json(res, 200, {\n meta: {\n id,\n profile_id: cachedMeta?.account ?? undefined,\n project_name: cachedMeta?.projectName ?? undefined,\n project_path: cachedMeta?.projectPath ?? undefined,\n file_path: cachedMeta?.filePath ?? undefined,\n last_updated_at: cachedMeta?.lastActivity ?? undefined,\n message_count: cachedMeta?.messageCount ?? undefined,\n provider: cachedProvider,\n resumable: isProviderResumable(cachedProvider, availability.resumable),\n ...(availability.unavailable_reason && {\n unavailable_reason: availability.unavailable_reason,\n }),\n },\n messages: messagesPayload,\n message_pagination: {\n total: tail.tailSize,\n before_index: tail.tailSize,\n from_index: 0,\n has_more_older: false,\n next_before_index: null,\n },\n });\n return;\n }\n }\n }\n\n if (!conversation) {\n // Self-heal: the row is a ghost (JSONL gone, no usable tail). Drop it so\n // the next list refresh doesn't keep offering this id to clients.\n this.cache?.invalidate(id);\n json(res, 404, { error: \"Conversation not found\", code: \"not_found\" });\n return;\n }\n\n // Compute the conditional-fetch validator from the RESOLVED conversation —\n // findConversationByUuid has already done its staleness refresh above, so\n // these fields reflect the same state the body would. Computing it from a\n // pre-refresh snapshot would let us hand out a 304 against stale data.\n const etagSource = conversation as unknown as {\n filePath: string;\n messageCount: number;\n timestamp: string;\n };\n // Fold the offset index's count into the validator: when the index is\n // fresher than the scanner snapshot (a live/appended file), the ETag must\n // change so a client holding the old tail doesn't get a 304 against grown\n // content. Cheap count lookup; the window read below reuses the same number.\n const indexedCount =\n etagSource.filePath && this.cache\n ? this.cache.getIndexedMessageCount(\n ConversationCache.conversationIdForFile(etagSource.filePath),\n )\n : 0;\n const etagMessageCount = Math.max(etagSource.messageCount, indexedCount);\n const etag = computeConversationEtag({\n filePath: etagSource.filePath,\n messageCount: etagMessageCount,\n timestamp: etagSource.timestamp,\n });\n\n // Only the first page (\"is the conversation as a whole still current?\")\n // participates in the freshness check. Older pages are immutable history —\n // a back-page request (before_index set) always returns its 200 body, never\n // a 304, even when the client echoes a matching If-None-Match. Anchored and\n // after-windows also always return 200: their ETag inputs are identical to\n // the tail page's, so honoring If-None-Match here would 304 a client that\n // holds the tail page but is asking for a different window.\n const isFirstPage =\n !url.searchParams.has(\"before_index\") &&\n !url.searchParams.has(\"anchor_index\") &&\n !url.searchParams.has(\"after_index\");\n if (isFirstPage && ifNoneMatch && ifNoneMatch === etag) {\n // This is a direct-`ServerResponse` write, so the Hono CORS middleware's\n // headers don't reach it — set the expose header here so a cross-origin\n // client can read the validator off the 304 too.\n res.writeHead(304, { ETag: etag, \"Access-Control-Expose-Headers\": \"ETag\" });\n res.end();\n return;\n }\n\n // Codex writes AGENTS.md / permissions dumps as role:user before any real\n // turn. Drop them from the REST payload so the chat opens as user→agent\n // rather than fake-user→fake-user→agent. Heuristic is Codex-specific text;\n // Claude messages never match.\n const filtered = conversation.messages.filter(\n (m) => !(m.role === \"user\" && typeof m.text === \"string\" && isCodexInjectedContext(m.text)),\n );\n const total = filtered.length;\n\n const hasAnchor = url.searchParams.has(\"anchor_index\");\n const hasAfter = url.searchParams.has(\"after_index\");\n const usePaging =\n url.searchParams.has(\"msg_limit\") ||\n url.searchParams.has(\"before_index\") ||\n hasAnchor ||\n hasAfter;\n\n let slice = filtered;\n let fromIdx = 0;\n let messagePagination: Record<string, unknown> | undefined;\n // Set to the offset index's total when it served this response, so the meta\n // block can reflect the freshly-indexed count/timestamp instead of the\n // (possibly stale) scanner snapshot.\n let indexTotal: number | null = null;\n\n if (usePaging) {\n const limit = Math.min(Math.max(intParam(url, \"msg_limit\", 80), 1), 500);\n let beforeIndex = total;\n let scanLimit = limit;\n let anchorIndex: number | null = null;\n let newerPaging = false;\n // True only when the after_index branch actually ran (before_index takes\n // precedence over after_index, so `hasAfter` alone isn't enough).\n let usedAfterIndex = false;\n if (url.searchParams.has(\"before_index\")) {\n beforeIndex = intParam(url, \"before_index\", total);\n beforeIndex = Math.min(Math.max(beforeIndex, 0), total);\n } else if (hasAfter) {\n // Newer-direction page: [after_index, after_index + limit). The paged\n // reader is end-anchored, so cap its limit at the window width — a full\n // `limit` near the tail would widen the window backward over rows the\n // client already has (duplicate message_index rows on mobile).\n const from = Math.min(Math.max(intParam(url, \"after_index\", 0), 0), total);\n beforeIndex = Math.min(total, from + limit);\n scanLimit = beforeIndex - from;\n newerPaging = true;\n usedAfterIndex = true;\n } else if (hasAnchor) {\n // Centered window around the anchor, clamped into [0, total-1] — a\n // stale index from search must still open the conversation, never 400.\n // Near the tail the window widens backward so it stays full-size.\n anchorIndex = Math.min(\n Math.max(intParam(url, \"anchor_index\", 0), 0),\n Math.max(0, total - 1),\n );\n const from = Math.max(0, anchorIndex - Math.floor(limit / 2));\n beforeIndex = Math.min(total, from + limit);\n newerPaging = true;\n }\n // A plain tail request (no explicit cursor) means \"the newest `limit`\n // messages\". Its window was derived from the scanner's snapshot `total`,\n // which lags a live, actively-appended file — the exact case the offset\n // index exists to serve. When the index is fresher than the snapshot,\n // anchor the tail on the INDEX's total so newly-appended messages aren't\n // dropped by a stale upper bound.\n const isTailRequest = !url.searchParams.has(\"before_index\") && !hasAfter && !hasAnchor;\n const indexFilePath = (conversation as { filePath?: string }).filePath;\n if (isTailRequest && indexFilePath && this.cache) {\n const indexed = this.cache.getIndexedMessageCount(\n ConversationCache.conversationIdForFile(indexFilePath),\n );\n if (indexed > beforeIndex) {\n beforeIndex = indexed;\n }\n }\n const windowStart = Math.max(0, beforeIndex - scanLimit);\n\n // Offset-index fast path: when the index is warm and matches the file on\n // disk, serve the window straight from SQLite + pread of the exact byte\n // ranges — no scanner, no re-parse. Falls through to the scanner (and\n // enqueues a backfill) on any miss/mismatch so the response is never\n // wrong. Only for the linear paging windows (before/after/tail); the\n // anchored-search window keeps using the scanner's reader.\n const indexWindow =\n scanLimit > 0 && !hasAnchor && indexFilePath && this.cache\n ? this.cache.readMessageWindow(indexFilePath, windowStart, beforeIndex)\n : null;\n if (!indexWindow && indexFilePath && this.cache && !hasAnchor) {\n // Cold/stale index for a file we page linearly → backfill in the\n // background (tracked so close() awaits it) for next time. The current\n // request is served by the scanner path below.\n this.trackCacheWrite(\n this.cache.backfillIndex(indexFilePath).catch((err) => {\n this.log.warn(\"offset-index.backfill_failed\", {\n event: \"offset_index.backfill_failed\",\n conversationId: id,\n filePath: indexFilePath,\n err,\n });\n }),\n );\n }\n\n // Only consult the scanner's paged reader when it's already warm. On the\n // cold path `conversation` came from the single-file fast path and holds\n // every message in memory, so slice it locally — calling getScanner()\n // here would trigger the full scan the fast path exists to avoid. Pass\n // skipStaleRescan: this is the same single-conversation detail path, whose\n // refreshFile already reconciled the one file we page here, so a sibling\n // file's stale flag must not stall this read behind a full-tree rescan.\n // Prefer the offset-index window when warm (Claude). Otherwise slice the\n // in-memory `filtered` list — do NOT call getConversationPage here.\n // That helper re-reads the scanner LRU (unfiltered, often stale for live\n // Codex) and would bypass isCodexInjectedContext, which is exactly how\n // mobile's ?msg_limit=80 path lost real user turns / showed PTY-only UI.\n const page = indexWindow;\n if (indexWindow) indexTotal = indexWindow.total;\n const start = page?.fromIndex ?? windowStart;\n slice = page?.messages ?? filtered.slice(start, beforeIndex);\n fromIdx = start;\n const effectiveTotal = page?.total ?? total;\n messagePagination = {\n total: effectiveTotal,\n before_index: beforeIndex,\n from_index: start,\n has_more_older: start > 0,\n next_before_index: start > 0 ? start : null,\n };\n if (anchorIndex != null) messagePagination.anchor_index = anchorIndex;\n if (newerPaging) {\n messagePagination.has_more_newer = beforeIndex < effectiveTotal;\n messagePagination.next_after_index = beforeIndex < effectiveTotal ? beforeIndex : null;\n }\n // Delta-validity token: an after_index delta carries the conversation's\n // current etag so a client can detect that its stored cursor is stale\n // (etag mismatch → discard the cursor, refetch the tail). Only on the\n // forward-delta path (before_index takes precedence, so gate on the flag\n // not merely hasAfter); additive, so old clients ignore it.\n if (usedAfterIndex) {\n messagePagination.etag = etag;\n }\n }\n\n const messagesPayload = slice.map((m: any, localIdx: number) => {\n const content: unknown[] = [];\n if (m.isThinking) {\n content.push({\n type: \"thinking\",\n thinking: m.thinkingContent ?? \"\",\n signature: m.thinkingSignature,\n });\n }\n for (const b of m.metadata?.toolUseBlocks ?? []) {\n content.push({ type: \"tool_use\", id: b.id, name: b.name, input: b.input });\n }\n for (const r of m.metadata?.toolResults ?? []) {\n content.push({\n type: \"tool_result\",\n tool_use_id: r.toolUseId,\n content: JSON.stringify(r.content),\n is_error: r.isError ?? false,\n });\n }\n return {\n uuid: m.uuid ?? null,\n message_index: fromIdx + localIdx,\n role: m.role,\n timestamp: m.timestamp,\n text: m.text,\n tool_calls: m.metadata?.toolUses ?? [],\n has_images: m.hasImages ?? false,\n parent_uuid: m.parentUuid ?? null,\n permission_mode: m.permissionMode ?? null,\n is_sidechain: m.isSidechain ?? false,\n is_tool_result: m.isToolResult ?? false,\n attachment: m.attachment ?? null,\n content,\n };\n });\n\n const conv = conversation as any;\n const cachedConvMeta = this.cache?.getMetaById(id);\n const convProvider = coerceProviderForRunner(conv.provider ?? cachedConvMeta?.provider);\n const availability = classifyResumability(conv.projectPath);\n // When the offset index served a fresher view than the scanner snapshot,\n // the meta (message_count / last_updated_at) must reflect what was actually\n // served — otherwise meta disagrees with the messages array. Prefer the\n // index total and the newest served message's timestamp.\n const metaMessageCount =\n indexTotal != null && indexTotal > conv.messageCount ? indexTotal : conv.messageCount;\n const metaLastUpdatedAt =\n indexTotal != null && indexTotal > conv.messageCount\n ? (slice.at(-1)?.timestamp ?? conv.timestamp)\n : conv.timestamp;\n const body: Record<string, unknown> = {\n meta: {\n id,\n profile_id: conv.account,\n project_name: conv.projectName,\n project_path: conv.projectPath,\n file_path: conv.filePath,\n last_updated_at: metaLastUpdatedAt,\n message_count: metaMessageCount,\n last_prompt: conv.lastPrompt ?? undefined,\n provider: convProvider,\n resumable: isProviderResumable(convProvider, availability.resumable),\n ...(availability.unavailable_reason && {\n unavailable_reason: availability.unavailable_reason,\n }),\n },\n messages: messagesPayload,\n };\n if (messagePagination) body.message_pagination = messagePagination;\n if (conv.turnDurations?.length) {\n body.turn_durations = conv.turnDurations.map((d: any) => ({\n duration_ms: d.durationMs,\n message_count: d.messageCount,\n uuid: d.uuid,\n }));\n }\n // Always expose the ETag on the 200 so the client can store it and send it\n // back as If-None-Match next time. Old clients ignore the header. This is a\n // direct-`ServerResponse` write that bypasses the Hono CORS middleware, so\n // the expose header is set here too — without it a cross-origin client\n // can't read ETag.\n res.writeHead(200, {\n \"Content-Type\": \"application/json\",\n ETag: etag,\n \"Access-Control-Expose-Headers\": \"ETag\",\n });\n res.end(JSON.stringify(body));\n }\n\n // Resolves an active search query to the message a client should anchor to\n // inside one conversation. Matching is body-only (text first, then\n // thinking/tool payloads) — a metadata-only search hit (project path, title)\n // has no scroll target and returns 404 search_target_not_found.\n //\n // Implements HTTP QUERY (RFC 10008): the search query travels in a JSON\n // request body instead of a URL query param — QUERY is safe + idempotent +\n // cacheable like GET, but (like POST) can carry a body, which fits this\n // endpoint's single-string input exactly. `Accept-Query` advertises the\n // supported request media type per the spec.\n private async handleSearchTarget(\n id: string,\n req: IncomingMessage,\n res: ServerResponse,\n ): Promise<void> {\n const contentType = (req.headers[\"content-type\"] ?? \"\").split(\";\")[0].trim();\n if (contentType && contentType !== \"application/json\") {\n res.setHeader(\"Accept-Query\", \"application/json\");\n json(res, 415, {\n error: \"Unsupported Content-Type; expected application/json\",\n code: \"unsupported_media_type\",\n });\n return;\n }\n\n let body: unknown;\n try {\n body = await readBody(req);\n } catch {\n res.setHeader(\"Accept-Query\", \"application/json\");\n json(res, 422, { error: \"Malformed JSON body\", code: \"invalid_query\" });\n return;\n }\n\n const q =\n typeof (body as { q?: unknown })?.q === \"string\" ? (body as { q: string }).q.trim() : \"\";\n if (!q) {\n res.setHeader(\"Accept-Query\", \"application/json\");\n json(res, 422, { error: \"Missing or empty query field: q\", code: \"invalid_query\" });\n return;\n }\n if (q.length > 256) {\n res.setHeader(\"Accept-Query\", \"application/json\");\n json(res, 422, { error: \"Query too long (max 256 characters)\", code: \"invalid_query\" });\n return;\n }\n\n const conversation = await this.findConversationByUuid(id);\n if (!conversation) {\n json(res, 404, { error: \"Conversation not found\", code: \"not_found\" });\n return;\n }\n\n const target = findSearchTarget(conversation.messages as unknown as SearchableMessage[], q);\n if (!target) {\n json(res, 404, { error: \"No message body matches query\", code: \"search_target_not_found\" });\n return;\n }\n\n res.setHeader(\"Accept-Query\", \"application/json\");\n json(res, 200, {\n query: q,\n message_index: target.messageIndex,\n uuid: target.uuid,\n snippet: target.snippet,\n match_indexes: target.matchIndexes,\n total_matches: target.totalMatches,\n });\n }\n\n private async handleSearch(url: URL, res: ServerResponse): Promise<void> {\n const q = url.searchParams.get(\"q\") ?? \"\";\n if (!q) {\n json(res, 400, { error: \"Missing query parameter: q\" });\n return;\n }\n\n const limit = intParam(url, \"limit\", 50);\n const scanner = await this.getScanner();\n const results = await search(\n q,\n {\n limit,\n include: \"conversations\",\n ...(this.scanProfiles ? { profiles: this.scanProfiles } : {}),\n ...this.codexScanOpts(),\n },\n scanner,\n );\n const adapted = results.map((r: any) => ({\n // Use sessionId so the id matches /api/conversations and resolves via\n // findConversationByUuid — a client can round-trip a search result into\n // GET /api/conversations/:id or the search-target QUERY. The old\n // filename-stem derivation produced an id no other endpoint recognized.\n id: r.meta.sessionId || r.meta.id,\n title: r.meta.projectName,\n sessionName: r.meta.sessionName || undefined,\n filePath: r.meta.filePath,\n projectPath: r.meta.projectPath,\n branch: r.meta.gitBranch ?? undefined,\n account: r.meta.account,\n preview: r.meta.preview || undefined,\n messageCount: r.meta.messageCount,\n lastActivity: r.meta.timestamp,\n firstMessage: r.meta.firstMessage ?? undefined,\n lastMessage: r.meta.lastMessage ?? undefined,\n provider: r.meta.provider ?? CLAUDE_CODE_PROVIDER,\n }));\n json(res, 200, {\n conversations: adapted,\n hasMore: false,\n offset: 0,\n total: adapted.length,\n });\n }\n\n private async handleListSessions(url: URL, res: ServerResponse): Promise<void> {\n const DISCOVERY_TTL_MS = 15_000;\n const now = Date.now();\n\n if (!this.discoveryCache || now - this.discoveryCache.fetchedAt >= DISCOVERY_TTL_MS) {\n try {\n const discovered = await discoverClaudeProcesses();\n this.sessionStore.setDiscovered(discovered);\n this.discoveryCache = { entries: discovered, fetchedAt: now };\n } catch {\n // Discovery is best-effort\n }\n }\n\n // Backwards compat: a bare GET /api/sessions returns the legacy plain\n // array. Any pagination param switches to the new envelope.\n const hasPaginationParams =\n url.searchParams.has(\"limit\") ||\n url.searchParams.has(\"cursor\") ||\n url.searchParams.has(\"sortBy\") ||\n url.searchParams.has(\"order\") ||\n url.searchParams.has(\"status\");\n\n if (!hasPaginationParams) {\n json(res, 200, this.sessionStore.list(this.ptyAttachedIds()));\n return;\n }\n\n const parsed = parseSessionListQuery(url);\n if (\"error\" in parsed) {\n json(res, 400, { error: parsed.error });\n return;\n }\n\n try {\n const page = this.sessionStore.paginate(this.ptyAttachedIds(), parsed.query);\n json(res, 200, page);\n } catch (err) {\n if (err instanceof Error && err.message === \"INVALID_CURSOR\") {\n json(res, 400, { error: \"Invalid cursor\" });\n return;\n }\n throw err;\n }\n }\n\n private handleGetSession(sessionId: string, res: ServerResponse): void {\n const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());\n if (session) {\n if (!existsSync(session.projectPath)) {\n session.failureReason = `Project directory not found: ${session.projectPath}`;\n }\n json(res, 200, session);\n return;\n }\n // Fall back to the conversation cache: older mobile builds tap recents\n // entries via GET /api/sessions/:id even though those IDs are conversation\n // UUIDs, not live sessions. Returning a resumable shape (status=on_hold)\n // lets the mobile open flow proceed to /api/sessions/resume.\n const conversation = this.cache?.getMetaById(sessionId);\n if (conversation) {\n json(res, 200, conversationToResumableSession(conversation));\n return;\n }\n json(res, 404, { error: \"Session not found\" });\n }\n\n private async handleResume(req: IncomingMessage, res: ServerResponse): Promise<void> {\n this.discoveryCache = null;\n const body = await readBody(req);\n // Accept both sessionId (new) and conversationId (legacy alias)\n const sessionId: string | undefined = body.sessionId ?? body.conversationId;\n\n if (!sessionId) {\n json(res, 400, { error: \"Missing sessionId\" });\n return;\n }\n\n // If a PTY is already running for this session, return it immediately\n if (this.ptyManager.hasSession(sessionId)) {\n const resp = this.sessionStore.get(sessionId, this.ptyAttachedIds());\n if (resp) {\n json(res, 200, resp);\n return;\n }\n }\n\n // Authoritative cwd comes from the JSONL itself — the file Claude looks\n // up by filename when processing --resume. The scanner index can return a\n // stale or wrong path (e.g. …/tb-mobile/android vs …/tb-mobile), so we\n // read the first cwd field directly, mirroring tb-scanner/src/parser.ts.\n const jsonlPath = this.findJsonlPath(sessionId);\n const jsonlCwd = jsonlPath ? await this.readCwdFromJsonl(jsonlPath) : null;\n\n const conv = await this.findConversationByUuid(sessionId);\n const projectPath: string = jsonlCwd ?? (conv as any)?.projectPath;\n if (!projectPath) {\n if (!conv && !jsonlPath) {\n json(res, 404, { error: \"Conversation not found\" });\n return;\n }\n json(res, 400, { error: \"Could not determine project path\" });\n return;\n }\n\n // Same provider-resolution fallback as the conversation-detail path\n // (server.ts ~1685): `conv` (the full Conversation shape) doesn't carry\n // provider, so fall back to the cached metadata, then default to Claude.\n const cachedConvMeta = this.cache?.getMetaById(sessionId);\n const provider = coerceProviderForRunner((conv as any)?.provider ?? cachedConvMeta?.provider);\n\n const session = await this.ptyManager.start(sessionId, {\n provider,\n projectPath,\n projectName: body.projectName,\n branch: body.branch,\n permissionMode: this.defaultPermissionMode,\n });\n\n this.sessionStore.addManaged(session);\n\n // Watch the conversation's JSONL file for structured events\n void this.watchConversationFile(sessionId);\n\n const resp = this.sessionStore.get(session.id, this.ptyAttachedIds());\n this.broadcastOrUnicastSessionList(req);\n\n json(res, 201, resp ?? session);\n\n // Enrich session metadata and update DB in background (fire-and-forget).\n // The conversation history is already in the JSONL; DB writes are\n // bookkeeping that can happen asynchronously without blocking the response.\n this.enrichResumedSessionAsync(sessionId, projectPath, conv);\n }\n\n private enrichResumedSessionAsync(sessionId: string, projectPath: string, conv: any): void {\n try {\n const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());\n if (!session) return;\n\n if (conv) {\n session.sessionName = conv.sessionName ?? undefined;\n session.messageCount = conv.messageCount ?? 0;\n session.account = conv.account ?? undefined;\n session.filePath = conv.filePath ?? undefined;\n }\n\n if (!this.cache || !this.projectsRepo || !this.conversationsRepo) return;\n\n // Single SQLite read covers model, preview, timestamps, and projectId —\n // no scanner round-trip needed; these fields are already cached.\n const cached = this.cache.getMetaById(sessionId);\n if (cached) {\n session.model = cached.model ?? undefined;\n session.preview = cached.preview ?? undefined;\n const first = cached.firstMessage ? JSON.parse(cached.firstMessage as string) : null;\n const last = cached.lastMessage ? JSON.parse(cached.lastMessage as string) : null;\n session.firstMessageText = first?.text ?? undefined;\n session.firstMessageAt = first?.timestamp\n ? new Date(first.timestamp).toISOString()\n : undefined;\n session.lastMessageText = last?.text ?? undefined;\n session.lastMessageAt = last?.timestamp\n ? new Date(last.timestamp).toISOString()\n : undefined;\n }\n\n let resolvedProjectId: string | null = cached?.projectId ?? null;\n if (!resolvedProjectId) {\n const project = this.projectsRepo.upsertProjectByPath(projectPath);\n resolvedProjectId = project.id;\n this.conversationsRepo.updateConversationProjectId({\n conversationId: sessionId,\n projectId: project.id,\n });\n }\n if (resolvedProjectId) {\n session.projectId = resolvedProjectId;\n session.resumedFromConversationId = sessionId;\n }\n } catch (err) {\n // ponytail: log but don't crash; session is already live and usable\n console.error(`[enrichResumedSessionAsync] ${sessionId}:`, err);\n }\n }\n\n private async handleSendInput(\n sessionId: string,\n req: IncomingMessage,\n res: ServerResponse,\n ): Promise<void> {\n if (!this.checkSessionInputRateLimit(sessionId)) {\n json(res, 429, { error: \"Too many input requests for this session. Please slow down.\" });\n return;\n }\n if (this.agentConfig.enabled) {\n const body = await readBody(req);\n const cache = this.cache;\n if (!cache) {\n json(res, 503, {\n error: \"Conversation cache is not available\",\n code: \"INTERNAL_ERROR\",\n });\n return;\n }\n const result = await handleSendAgentInput(sessionId, body, {\n sessionStore: this.sessionStore,\n cache,\n // biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true\n agentClient: this.agentClient!,\n agentConfig: this.agentConfig,\n });\n json(res, result.status, result.body);\n return;\n }\n const body = await readBody(req);\n const { input, keys } = body;\n\n if (typeof keys === \"string\") {\n // Raw key bytes (e.g. arrow navigation for interactive prompts).\n // These bypass bracketed-paste wrapping — caller is responsible for\n // sending well-formed escape sequences.\n try {\n this.ptyManager.sendKeys(sessionId, keys);\n const updated = this.sessionStore.get(sessionId, this.ptyAttachedIds());\n if (updated) {\n this.wsHub.broadcast({ type: \"session_update\", session: updated });\n }\n json(res, 200, { ok: true });\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Failed to send keys\";\n json(res, 400, { error: message });\n }\n return;\n }\n\n if (typeof input !== \"string\") {\n json(res, 400, { error: \"Missing input field\" });\n return;\n }\n\n try {\n const promptCount = this.ptyManager.sendInput(sessionId, input);\n this.sessionStore.updateManaged(sessionId, { promptCount });\n const updated = this.sessionStore.get(sessionId, this.ptyAttachedIds());\n if (updated) {\n this.wsHub.broadcast({ type: \"session_update\", session: updated });\n }\n // Index the user's new message immediately so it's searchable right away.\n const filePath = this.sessionFileMap.get(sessionId);\n if (filePath) {\n this.getScanner()\n .then((scanner) => scanner.refreshFile(filePath))\n .then((meta) => {\n this.log.info(\"scanner.refreshFile: ok\", {\n event: \"scanner.refresh\",\n sessionId,\n filePath,\n trigger: \"sendInput\",\n messageCount: meta?.messageCount,\n });\n })\n .catch((err) => {\n this.log.warn(\"scanner.refreshFile: failed\", {\n event: \"scanner.refresh_failed\",\n sessionId,\n filePath,\n trigger: \"sendInput\",\n err,\n });\n });\n }\n json(res, 200, { ok: true });\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Failed to send input\";\n json(res, 400, { error: message });\n }\n }\n\n private cancelPendingQuestion(sessionId: string): void {\n const pq = this.pendingQuestions.get(sessionId);\n if (!pq) return;\n this.pendingQuestions.delete(sessionId);\n this.pendingQuestionKey.delete(sessionId);\n this.wsHub.broadcast({ type: \"question_cancelled\", sessionId, toolUseId: pq.toolUseId });\n }\n\n // Live AskUserQuestion detected from the rendered screen (ahead of JSONL).\n // Broadcasts the `question` event immediately and records the content key so\n // the later JSONL flush of the same question is de-duped. We synthesize a\n // screen-scoped toolUseId; the JSONL path overwrites pendingQuestions with the\n // real toolUseId when it lands, so answering works once JSONL catches up.\n private handleLiveQuestion(sessionId: string, questions: AskQuestion[]): void {\n const key = questionContentKey(questions);\n if (this.pendingQuestionKey.get(sessionId) === key) return; // already shown\n const toolUseId = `screen:${sessionId}:${key.length}`;\n this.pendingQuestions.set(sessionId, { toolUseId, questions });\n this.pendingQuestionKey.set(sessionId, key);\n this.wsHub.broadcast({ type: \"question\", sessionId, toolUseId, questions });\n }\n\n // Permission gate opened/closed (OSC 777 + scraped options). Broadcasts the\n // additive `permission` / `permission_cancelled` events. Mobile answers by\n // sending the chosen option index via /input { keys } (e.g. \"2\\r\").\n private handlePermissionChange(\n sessionId: string,\n gate: {\n prompt?: string;\n detail?: string;\n options: PermissionOption[];\n cursor?: number;\n } | null,\n ): void {\n if (gate === null) {\n if (!this.pendingPermission.has(sessionId)) return;\n this.pendingPermission.delete(sessionId);\n this.wsHub.broadcast({ type: \"permission_cancelled\", sessionId });\n return;\n }\n this.pendingPermission.set(sessionId, gate);\n const subscriberCount = this.sessionSubscribers.get(sessionId)?.size ?? 0;\n this.log.info(\n `[ws.broadcast_permission] ${sessionId.slice(0, 8)} subscribers=${subscriberCount}`,\n { event: \"ws.broadcast_permission\", sessionId, subscriberCount },\n );\n this.wsHub.broadcast({\n type: \"permission\",\n sessionId,\n ...(gate.prompt ? { prompt: gate.prompt } : {}),\n ...(gate.detail ? { detail: gate.detail } : {}),\n options: gate.options,\n ...(gate.cursor !== undefined ? { cursor: gate.cursor } : {}),\n });\n }\n\n private async handleSendAnswer(\n sessionId: string,\n req: IncomingMessage,\n res: ServerResponse,\n ): Promise<void> {\n const body = await readBody(req);\n const pending = this.pendingQuestions.get(sessionId);\n const resolution = resolveAnswer(pending, body);\n if (!resolution.ok) {\n json(res, 400, { ok: false, reason: resolution.reason });\n return;\n }\n // pending is guaranteed defined when resolution.ok is true (resolveAnswer guards it)\n const toolUseId = pending?.toolUseId ?? \"\";\n try {\n this.ptyManager.sendKeys(sessionId, resolution.keys);\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Failed to send answer\";\n json(res, 400, { ok: false, reason: message });\n return;\n }\n this.pendingQuestions.delete(sessionId);\n this.wsHub.broadcast({ type: \"question_cancelled\", sessionId, toolUseId });\n json(res, 200, { ok: true });\n }\n\n private async handleUploadFile(\n sessionId: string,\n req: IncomingMessage,\n res: ServerResponse,\n ): Promise<void> {\n const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());\n if (!session) {\n json(res, 404, { error: \"Session not found\" });\n return;\n }\n if (!session.projectPath) {\n json(res, 400, { error: \"Session has no project path\" });\n return;\n }\n\n const body = await readBody(req);\n const { filename, mimeType, dataBase64 } = body ?? {};\n if (\n typeof filename !== \"string\" ||\n typeof mimeType !== \"string\" ||\n typeof dataBase64 !== \"string\"\n ) {\n json(res, 400, { error: \"Missing filename, mimeType, or dataBase64\" });\n return;\n }\n\n try {\n const saved = await saveUploadFile({\n sessionId,\n projectPath: session.projectPath,\n originalName: filename,\n mimeType,\n dataBase64,\n });\n\n try {\n await recordUpload(this.dbPool, this.dbInstanceId, {\n id: saved.id,\n sessionId,\n filePath: saved.filePath,\n originalName: saved.originalName,\n mimeType: saved.mimeType,\n sizeBytes: saved.sizeBytes,\n });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n this.log.warn(\n `[uploads] DB record failed: ${message}`,\n { event: \"uploads.db_record_failed\", error: message },\n \"pino\",\n );\n }\n\n json(res, 201, {\n id: saved.id,\n path: saved.filePath,\n originalName: saved.originalName,\n mimeType: saved.mimeType,\n sizeBytes: saved.sizeBytes,\n });\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Upload failed\";\n json(res, 400, { error: message });\n }\n }\n\n private handleGetOutput(sessionId: string, res: ServerResponse): void {\n // Return PTY ring buffer if a PTY is attached; otherwise return empty\n // so clients render \"no buffered output\" instead of an error.\n try {\n const output = this.ptyManager.getOutput(sessionId);\n json(res, 200, { output });\n } catch {\n json(res, 200, { output: \"\" });\n }\n }\n\n private handleCancel(sessionId: string, res: ServerResponse): void {\n this.discoveryCache = null;\n try {\n this.ptyManager.cancel(sessionId);\n json(res, 200, { ok: true });\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Failed to cancel\";\n json(res, 400, { error: message });\n }\n }\n\n private async handleStopSession(sessionId: string, res: ServerResponse): Promise<void> {\n const STOP_TIMEOUT_MS = 5000;\n\n const session = this.ptyManager.getSession(sessionId);\n if (!session) {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Session not found\" }));\n return;\n }\n\n if (session.status === \"idle\") {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"already_idle\", sessionId }));\n return;\n }\n\n res.writeHead(200, {\n \"Content-Type\": \"application/x-ndjson\",\n \"Transfer-Encoding\": \"chunked\",\n \"Cache-Control\": \"no-cache\",\n \"X-Accel-Buffering\": \"no\",\n });\n res.write(`${JSON.stringify({ event: \"stopping\", sessionId })}\\n`);\n\n const idlePromise = new Promise<\"idle\">((resolve) => {\n const handler = (status: string) => {\n if (status === \"idle\") {\n this.sessionStatusBus.off(`status:${sessionId}`, handler);\n resolve(\"idle\");\n }\n };\n this.sessionStatusBus.on(`status:${sessionId}`, handler);\n });\n\n const timeoutPromise = new Promise<\"timeout\">((resolve) =>\n setTimeout(() => resolve(\"timeout\"), STOP_TIMEOUT_MS),\n );\n\n this.ptyManager.putOnHold(sessionId);\n this.discoveryCache = null;\n\n const outcome = await Promise.race([idlePromise, timeoutPromise]);\n\n if (outcome === \"idle\") {\n res.write(`${JSON.stringify({ event: \"stopped\", sessionId })}\\n`);\n } else {\n res.write(`${JSON.stringify({ event: \"timeout\", sessionId })}\\n`);\n this.log.warn(\n `[stop] session ${sessionId.slice(0, 8)} did not idle within ${STOP_TIMEOUT_MS}ms`,\n );\n }\n\n res.end();\n }\n\n private async handleAdopt(sessionId: string, res: ServerResponse): Promise<void> {\n // Refresh discovery so we have the latest metadata\n const discovered = await discoverClaudeProcesses();\n this.sessionStore.setDiscovered(discovered);\n this.discoveryCache = null;\n\n const discSession = this.sessionStore.get(sessionId, this.ptyAttachedIds());\n if (!discSession || discSession.ptyAttached) {\n json(res, 404, { error: \"Discovered session not found\" });\n return;\n }\n\n const { projectPath, projectName, branch } = discSession;\n const convId = discSession.id;\n\n if (discSession.pid == null) {\n json(res, 400, { error: \"Session has no known PID\" });\n return;\n }\n\n // Kill the external process\n this.ptyManager.killPid(discSession.pid);\n\n // Start a new managed session, resuming the conversation\n const session = await this.ptyManager.start(convId, {\n projectPath,\n projectName,\n branch,\n permissionMode: this.defaultPermissionMode,\n });\n\n this.sessionStore.addManaged(session);\n void this.watchConversationFile(session.id);\n\n this.wsHub.broadcast({\n type: \"session_list\",\n sessions: this.sessionStore.list(this.ptyAttachedIds()),\n });\n\n json(res, 201, { sessionId: session.id });\n }\n\n private async handleStartSession(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const ip = req.socket?.remoteAddress ?? \"unknown\";\n if (!this.checkSessionStartRateLimit(ip)) {\n json(res, 429, {\n error: \"Too many session start requests. Please wait before trying again.\",\n });\n return;\n }\n if (this.agentConfig.enabled) {\n const body = await readBody(req);\n const result = await handleStartAgentSession(body, {\n sessionStore: this.sessionStore,\n // biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true\n agentClient: this.agentClient!,\n conversationsDir: this.cacheDir ? join(dirname(this.cacheDir), \"conversations\") : \"\",\n agentConfig: this.agentConfig,\n });\n json(res, result.status, result.body);\n if (result.status === 200) {\n this.broadcastOrUnicastSessionList(req);\n }\n return;\n }\n const body = await readBody(req);\n const { path: relativePath, provider: requestedProvider, systemPrompt: clientPrompt } = body;\n\n if (requestedProvider !== undefined && !isProviderName(requestedProvider)) {\n json(res, 400, { error: \"Invalid provider\" });\n return;\n }\n const provider = requestedProvider ?? CLAUDE_CODE_PROVIDER;\n\n if (!this.browseRoot) {\n json(res, 403, {\n error: \"File browsing not configured. Set browseRoot on the server.\",\n code: \"BROWSE_ROOT_NOT_SET\",\n });\n return;\n }\n\n if (typeof relativePath !== \"string\") {\n json(res, 400, { error: \"Missing path field\" });\n return;\n }\n\n let resolvedPath: string;\n try {\n resolvedPath = await resolveBrowsePath(this.browseRoot, relativePath);\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Invalid path\";\n json(res, 400, { error: message });\n return;\n }\n\n this.discoveryCache = null;\n\n const systemPromptParts = [\n this.defaultSystemPrompt,\n BROWSE_SYSTEM_PROMPT(this.browseRoot),\n typeof clientPrompt === \"string\" ? clientPrompt : null,\n ].filter(Boolean);\n\n try {\n const session = await this.ptyManager.startFresh({\n provider,\n projectPath: resolvedPath,\n projectName: body.projectName,\n systemPrompt: systemPromptParts.join(\"\\n\"),\n permissionMode: this.defaultPermissionMode,\n });\n\n this.sessionStore.addManaged(session);\n\n // Block for the PTY to actually reach waiting_input (or fail) so the\n // caller gets a trustworthy status instead of navigating on a guess.\n // Races against the same fallback window pty-manager itself uses for\n // prompt-marker detection, plus margin — if neither settles in time we\n // fall back to the old fire-and-forget shape rather than hang the request.\n const readyOrFailed = new Promise<\"ready\" | \"failed\">((resolve) => {\n const handler = (status: string) => {\n if (status === \"waiting_input\" || status === \"idle\") {\n this.sessionStatusBus.off(`status:${session.id}`, handler);\n resolve(status === \"waiting_input\" ? \"ready\" : \"failed\");\n }\n };\n this.sessionStatusBus.on(`status:${session.id}`, handler);\n });\n const timeoutPromise = new Promise<\"timeout\">((resolve) =>\n setTimeout(() => resolve(\"timeout\"), START_READY_TIMEOUT_MS),\n );\n\n const outcome = await Promise.race([readyOrFailed, timeoutPromise]);\n const current = this.sessionStore.get(session.id, this.ptyAttachedIds());\n\n if (outcome === \"ready\" && current) {\n json(res, 200, { session: current });\n } else if (outcome === \"failed\" && current) {\n json(res, 502, {\n id: session.id,\n status: \"idle\",\n error: current.failureReason ?? \"Session exited before becoming ready\",\n });\n } else {\n // Timeout, or session vanished from the store — old async contract.\n json(res, 202, { id: session.id, status: \"pending\" });\n }\n\n if (provider === CODEX_CLI_PROVIDER) {\n // Wire up rollout-file binding once Codex creates its persisted session.\n this.watchForCodexRollout(session.id, resolvedPath);\n } else {\n // Wire up JSONL watching once Claude creates the conversation file.\n this.watchForJsonl(session.id, resolvedPath);\n }\n\n this.broadcastOrUnicastSessionList(req);\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Failed to start session\";\n const statusCode =\n typeof (err as Error & { statusCode?: unknown }).statusCode === \"number\"\n ? (err as Error & { statusCode: number }).statusCode\n : 500;\n this.log.error(`[start] failed to start session: ${message}`, {\n event: \"session.start_failed\",\n error: message,\n });\n json(res, statusCode, { error: message });\n }\n }\n\n // ─── Project linking ─────────────────────────────────────────────\n\n private linkSessionToProject(sessionId: string, projectPath: string, filePath: string): void {\n if (!this.projectsRepo || !this.conversationsRepo || !this.sessionsRepo || !this.cache) {\n return;\n }\n try {\n const project = this.projectsRepo.upsertProjectByPath(projectPath, {\n lastConversationId: sessionId,\n lastConversationCreatedAt: new Date().toISOString(),\n });\n // The conversation row may not exist yet (Claude is still writing the\n // JSONL). Best-effort: only link if the row is present.\n if (this.cache.hasConversation(sessionId)) {\n this.conversationsRepo.updateConversationProjectId({\n conversationId: sessionId,\n projectId: project.id,\n });\n }\n this.sessionsRepo.updateSessionProjectId({\n sessionId,\n projectId: project.id,\n });\n if (this.cacheMetadataRepo) {\n this.cacheMetadataRepo.setCacheMetadata(\"last_conversation_id\", sessionId);\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n this.log.warn(`[projects] failed to link session to project: ${message}`, {\n event: \"session.project_link_failed\",\n sessionId,\n projectPath,\n filePath,\n error: message,\n });\n }\n }\n\n // ─── File Watcher Wiring ─────────────────────────────────────────\n\n private async watchConversationFile(sessionId: string): Promise<void> {\n try {\n const conversation = await this.findConversationByUuid(sessionId);\n if (conversation?.filePath) {\n this.sessionFileMap.set(sessionId, conversation.filePath);\n this.fileWatcher.watch(conversation.filePath);\n }\n } catch {\n // Best-effort: if we can't find the JSONL file, raw terminal output still works\n }\n }\n\n // Watch the project directory for the JSONL file Claude creates for sessionId.\n // Once found, wire up structured event streaming. No rekeying needed — the UUID\n // was passed to Claude via --session-id so the filename matches from the start.\n private watchForJsonl(sessionId: string, projectPath: string): void {\n const encoded = projectPath.replace(/[/\\\\:.]/g, \"-\");\n const projectsDir = join(homedir(), \".claude\", \"projects\", encoded);\n const expectedFile = `${sessionId}.jsonl`;\n const filePath = join(projectsDir, expectedFile);\n const deadline = Date.now() + 120_000;\n\n let watcher: ReturnType<typeof fsWatch> | null = null;\n const cleanup = () => {\n try {\n watcher?.close();\n } catch {\n /* ignore */\n }\n };\n\n const tryWire = () => {\n if (!this.ptyManager.hasSession(sessionId)) {\n cleanup();\n return;\n }\n if (Date.now() > deadline) {\n cleanup();\n return;\n }\n\n // Primary: Claude named the file after the session UUID\n let resolvedFilePath = existsSync(filePath) ? filePath : null;\n\n // Fallback: Claude resumed an existing conversation — the JSONL it writes\n // to will be a different UUID. Pick the most recently modified JSONL in\n // the directory that was touched within the last 5 seconds (session just started).\n if (!resolvedFilePath && existsSync(projectsDir)) {\n try {\n const now = Date.now();\n const recent = readdirSync(projectsDir)\n .filter((f) => f.endsWith(\".jsonl\"))\n .map((f) => ({ f, mtime: statSync(join(projectsDir, f)).mtimeMs }))\n .filter(({ mtime }) => now - mtime < 5_000)\n .sort((a, b) => b.mtime - a.mtime)[0];\n if (recent) resolvedFilePath = join(projectsDir, recent.f);\n } catch {\n /* ignore */\n }\n }\n\n if (!resolvedFilePath) return;\n\n cleanup();\n this.sessionFileMap.set(sessionId, resolvedFilePath);\n this.fileWatcher.watch(resolvedFilePath);\n\n // Broadcast any lines already written before the watcher started — Claude\n // can finish writing the JSONL in the same tick as the watcher wires up,\n // so chokidar won't emit a change event for those lines.\n try {\n const existing = readFileSync(resolvedFilePath, \"utf8\").split(\"\\n\").filter(Boolean);\n if (existing.length > 0) {\n this.broadcastConversationLines(sessionId, existing);\n }\n } catch {\n /* ignore — file may not be readable yet; watcher will catch future writes */\n }\n\n if (this.scannerReady) {\n this.scannerStale = true;\n } else {\n this.scanner = null;\n }\n this.linkSessionToProject(sessionId, projectPath, resolvedFilePath);\n this.cache?.markAsStreamer(sessionId);\n this.log.info(\n `[startFresh] wired JSONL for ${sessionId}`,\n { event: \"session.jsonl_wired\", sessionId, filePath: resolvedFilePath },\n \"pino\",\n );\n };\n\n tryWire();\n if (this.sessionFileMap.has(sessionId)) return; // already found\n\n try {\n require(\"fs\").mkdirSync(projectsDir, { recursive: true });\n watcher = fsWatch(projectsDir, tryWire);\n watcher.on(\"error\", cleanup);\n } catch {\n // fs.watch not available (e.g. in tests), ignore\n }\n }\n\n // Codex-equivalent of watchForJsonl(). Differs because Codex has no\n // filename-encoded session id (it assigns its own persisted id) and its\n // rollout files live under a date-nested directory\n // (~/.codex/sessions/<YYYY>/<MM>/<DD>/rollout-*.jsonl) that Codex creates\n // itself — it may not exist yet when this function is first called, so we\n // poll rather than fs.watch a not-yet-existent directory. Per Phase 0\n // findings, the rollout file appears within ~1s of process spawn (after\n // any directory-trust gate is cleared), well before any user input.\n private watchForCodexRollout(sessionId: string, projectPath: string): void {\n const deadline = Date.now() + 120_000;\n const now = new Date();\n const dateDir = join(\n String(now.getFullYear()),\n String(now.getMonth() + 1).padStart(2, \"0\"),\n String(now.getDate()).padStart(2, \"0\"),\n );\n\n // When this placeholder session started. Used to reject a stale same-cwd\n // rollout that Codex wrote before this session launched — the cwd match\n // alone can't tell a fresh rollout from a seconds-old one. 5s of slack\n // absorbs clock skew between our clock and Codex's session_meta timestamp.\n const sessionStartedAtMs =\n (this.sessionStore.getManaged(sessionId)?.startedAt?.getTime() ?? Date.now()) - 5_000;\n\n let intervalHandle: ReturnType<typeof setInterval> | null = null;\n const cleanup = () => {\n if (intervalHandle) clearInterval(intervalHandle);\n intervalHandle = null;\n };\n\n // Read a candidate file's session_meta first line; accept only if its cwd\n // matches this session's projectPath and it was created at/after this\n // session started. Guards against picking up an unrelated concurrent Codex\n // session's rollout, or a stale same-cwd rollout from an earlier run, in\n // the same date-nested directory. Returns { id, createdAtMs } or null.\n const matchesProjectPath = (\n candidatePath: string,\n ): { id: string; createdAtMs: number } | null => {\n try {\n const firstLine = readFileSync(candidatePath, \"utf8\").split(\"\\n\", 1)[0];\n if (!firstLine) return null;\n const parsed = JSON.parse(firstLine);\n if (parsed?.type !== \"session_meta\") return null;\n const payload = parsed.payload ?? {};\n if (payload.cwd !== projectPath) return null;\n if (typeof payload.id !== \"string\") return null;\n // payload.timestamp is Codex's session-creation time; fall back to the\n // outer envelope timestamp if absent.\n const createdIso = payload.timestamp ?? parsed.timestamp;\n const createdAtMs = typeof createdIso === \"string\" ? Date.parse(createdIso) : Number.NaN;\n if (Number.isNaN(createdAtMs) || createdAtMs < sessionStartedAtMs) return null;\n return { id: payload.id, createdAtMs };\n } catch {\n return null;\n }\n };\n\n const tryWire = () => {\n if (!this.ptyManager.hasSession(sessionId)) {\n cleanup();\n return;\n }\n if (Date.now() > deadline) {\n cleanup();\n return;\n }\n\n // Codex ids already bound to another live placeholder — never bind two\n // placeholders to the same rollout (e.g. two Codex sessions started in\n // the same project inside the mtime window).\n const boundElsewhere = new Set(\n this.sessionStore\n .listManaged()\n .filter((s) => s.id !== sessionId && s.boundConversationId != null)\n .map((s) => s.boundConversationId as string),\n );\n\n for (const root of this.codexRoots) {\n const sessionsDir = join(root, dateDir);\n if (!existsSync(sessionsDir)) continue;\n\n let candidateFiles: string[];\n try {\n candidateFiles = readdirSync(sessionsDir).filter((f) => f.endsWith(\".jsonl\"));\n } catch {\n continue;\n }\n\n const nowMs = Date.now();\n const recentCandidates = candidateFiles\n .map((f) => ({ f, mtime: statSync(join(sessionsDir, f)).mtimeMs }))\n .filter(({ mtime }) => nowMs - mtime < 10_000)\n .sort((a, b) => b.mtime - a.mtime);\n\n for (const { f } of recentCandidates) {\n const candidatePath = join(sessionsDir, f);\n const match = matchesProjectPath(candidatePath);\n if (!match) continue;\n if (boundElsewhere.has(match.id)) continue;\n const codexSessionId = match.id;\n\n cleanup();\n this.sessionStore.updateManaged(sessionId, { boundConversationId: codexSessionId });\n\n // Wire the bound rollout into the live update path: tail it for\n // structured events and replay anything already written before the\n // watcher attached (mirrors watchForJsonl()). Without this the bound\n // Codex JSONL is never live-streamed to clients.\n this.sessionFileMap.set(sessionId, candidatePath);\n this.fileWatcher.watch(candidatePath);\n try {\n const existing = readFileSync(candidatePath, \"utf8\").split(\"\\n\").filter(Boolean);\n if (existing.length > 0) {\n this.broadcastConversationLines(sessionId, existing);\n }\n } catch {\n /* ignore — file may not be readable yet; watcher will catch future writes */\n }\n\n if (this.scannerReady) {\n this.scannerStale = true;\n } else {\n this.scanner = null;\n }\n this.linkSessionToProject(sessionId, projectPath, candidatePath);\n this.cache?.markAsStreamer(sessionId);\n\n // Push the binding to subscribers now — the async discovery means the\n // session_update at start time carried no boundConversationId.\n const resp = this.sessionStore.get(sessionId, this.ptyAttachedIds());\n if (resp) {\n this.wsHub.broadcast({ type: \"session_update\", session: resp });\n }\n\n this.log.info(\n `[startFresh] bound Codex rollout for ${sessionId}`,\n {\n event: \"session.codex_rollout_bound\",\n sessionId,\n boundConversationId: codexSessionId,\n filePath: candidatePath,\n },\n \"pino\",\n );\n return;\n }\n }\n };\n\n tryWire();\n if (!intervalHandle && Date.now() <= deadline) {\n // Only keep polling if tryWire() didn't already find + cleanup() the match.\n const alreadyBound = this.sessionStore.getManaged(sessionId)?.boundConversationId != null;\n if (!alreadyBound) {\n intervalHandle = setInterval(tryWire, 250);\n }\n }\n }\n\n private async handleBrowse(url: URL, res: ServerResponse): Promise<void> {\n if (!this.browseRoot) {\n json(res, 403, {\n error: \"File browsing not configured. Set browseRoot on the server.\",\n code: \"BROWSE_ROOT_NOT_SET\",\n });\n return;\n }\n const relativePath = url.searchParams.get(\"path\") ?? \"\";\n try {\n const resolved = await resolveBrowsePath(this.browseRoot, relativePath);\n const directories = await listDirectories(resolved);\n json(res, 200, { path: relativePath, directories });\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Browse failed\";\n if (err instanceof BrowsePathNotFoundError) {\n json(res, 404, { error: message, code: \"PATH_NOT_FOUND\" });\n return;\n }\n json(res, 400, { error: message });\n }\n }\n\n private async handleMkdir(req: IncomingMessage, res: ServerResponse): Promise<void> {\n if (!this.browseRoot) {\n json(res, 403, {\n error: \"File browsing not configured. Set browseRoot on the server.\",\n code: \"BROWSE_ROOT_NOT_SET\",\n });\n return;\n }\n const body = await readBody(req);\n const { path: relativePath, name } = body;\n if (!name || typeof name !== \"string\") {\n json(res, 400, { error: \"Missing name field\" });\n return;\n }\n try {\n const parentPath = await resolveBrowsePath(this.browseRoot, relativePath ?? \"\");\n await createDirectory(parentPath, name);\n const parentRelative = relativePath ?? \"\";\n const created = parentRelative ? `${parentRelative}/${name}` : name;\n json(res, 201, { created });\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Failed to create directory\";\n if (message.includes(\"already exists\")) {\n json(res, 409, { error: message });\n } else if (message.includes(\"Invalid directory name\")) {\n json(res, 400, { error: message });\n } else {\n json(res, 400, { error: message });\n }\n }\n }\n\n private async handleSetSessionName(\n sessionId: string,\n req: IncomingMessage,\n res: ServerResponse,\n ): Promise<void> {\n if (!this.cache) {\n json(res, 503, { error: \"Cache not available\" });\n return;\n }\n let parsed: { name?: string };\n try {\n parsed = await readBody(req);\n } catch {\n json(res, 400, { error: \"Invalid JSON\" });\n return;\n }\n const name = parsed.name?.trim();\n if (!name) {\n json(res, 400, { error: \"name is required\" });\n return;\n }\n this.cache.upsertSessionName(sessionId, name);\n json(res, 200, { ok: true });\n }\n\n private handleGetSessionNames(res: ServerResponse): void {\n if (!this.cache) {\n json(res, 200, {});\n return;\n }\n json(res, 200, this.cache.listSessionNames());\n }\n}\n\n// ─── Utilities ─────────────────────────────────────────────────────\n\n// Classify whether a conversation can be resumed from the project directory\n// (cwd) the session ran in. Shared by the detail handler and the\n// resumable-session shape. A conversation's JSONL parses fine even when its\n// cwd is gone, so callers still serve the full history — this only flags that\n// resume would fail and why. Returns optional meta fields older clients\n// ignore: cwd exists → resumable; gone → not resumable, with a\n// worktree-specific reason when the path was a git worktree (now removed).\nfunction classifyResumability(cwd: string | null | undefined): {\n resumable: boolean;\n unavailable_reason?: \"path_missing\" | \"worktree_removed\";\n} {\n if (!cwd) return { resumable: true };\n if (existsSync(cwd)) return { resumable: true };\n const ranInWorktree = /\\/\\.worktrees\\//.test(cwd) || /\\/\\.claude\\/worktrees\\//.test(cwd);\n return {\n resumable: false,\n unavailable_reason: ranInWorktree ? \"worktree_removed\" : \"path_missing\",\n };\n}\n\nfunction conversationToResumableSession(c: ConversationListItem) {\n const availability = classifyResumability(c.projectPath);\n const provider = c.provider ?? CLAUDE_CODE_PROVIDER;\n return {\n type: \"conversation\" as const,\n id: c.id,\n conversationId: c.id,\n status: \"on_hold\" as const,\n ptyAttached: false,\n projectId: c.projectId ?? undefined,\n projectPath: c.projectPath ?? \"\",\n projectName: c.projectName ?? \"\",\n branch: c.branch ?? undefined,\n lastOutput: \"\",\n elapsedMs: 0,\n promptCount: c.messageCount,\n startedAt: c.lastActivity,\n completedAt: null,\n lastActivityAt: c.lastActivity,\n ...(c.title != null && { sessionName: c.title }),\n ...(c.model != null && { model: c.model }),\n ...(c.account != null && { account: c.account }),\n messageCount: c.messageCount,\n ...(c.preview != null && { preview: c.preview }),\n ...(c.firstMessage != null && { firstMessageText: c.firstMessage }),\n ...(c.lastMessage != null && { lastMessageText: c.lastMessage }),\n filePath: c.filePath,\n provider,\n resumable: isProviderResumable(provider, availability.resumable),\n ...(availability.unavailable_reason && {\n unavailable_reason: availability.unavailable_reason,\n }),\n };\n}\n\nfunction json(res: ServerResponse, status: number, data: unknown): void {\n res.writeHead(status, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(data));\n}\n\nasync function writeHonoResponse(honoRes: Response, res: ServerResponse): Promise<void> {\n const headers: Record<string, string> = {};\n honoRes.headers.forEach((value, key) => {\n headers[key] = value;\n });\n res.writeHead(honoRes.status, headers);\n if (honoRes.body) {\n const reader = honoRes.body.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n res.write(value);\n }\n } finally {\n reader.releaseLock();\n }\n }\n res.end();\n}\n\n// Parse THREADBASE_DIR_SCAN_DEBOUNCE_MS → a non-negative integer, or undefined\n// when unset/invalid so the caller can fall through to config/default.\nfunction parseDirScanDebounceEnv(raw: string | undefined): number | undefined {\n if (raw == null || raw === \"\") return undefined;\n const parsed = Number.parseInt(raw, 10);\n return Number.isNaN(parsed) || parsed < 0 ? undefined : parsed;\n}\n\nfunction intParam(url: URL, name: string, defaultValue: number): number {\n const val = url.searchParams.get(name);\n if (!val) return defaultValue;\n const parsed = Number.parseInt(val, 10);\n return Number.isNaN(parsed) ? defaultValue : parsed;\n}\n\nconst VALID_SORT_KEYS: SessionSortKey[] = [\"startedAt\", \"lastActivityAt\", \"projectName\", \"status\"];\nconst VALID_ORDERS: SessionSortOrder[] = [\"asc\", \"desc\"];\nconst VALID_STATUSES: SessionStatus[] = [\"running\", \"waiting_input\", \"idle\"];\n\nconst SESSIONS_DEFAULT_LIMIT = 200;\nconst SESSIONS_MAX_LIMIT = 500;\n\ntype ParsedSessionListQuery = { query: import(\"./types\").SessionListQuery } | { error: string };\n\nfunction parseSessionListQuery(url: URL): ParsedSessionListQuery {\n const limitRaw = url.searchParams.get(\"limit\");\n let limit = SESSIONS_DEFAULT_LIMIT;\n if (limitRaw !== null) {\n const n = Number.parseInt(limitRaw, 10);\n if (!Number.isFinite(n) || n < 1 || n > SESSIONS_MAX_LIMIT) {\n return { error: `limit must be 1..${SESSIONS_MAX_LIMIT}` };\n }\n limit = n;\n }\n\n const sortByRaw = url.searchParams.get(\"sortBy\") ?? \"startedAt\";\n if (!VALID_SORT_KEYS.includes(sortByRaw as SessionSortKey)) {\n return { error: `sortBy must be one of ${VALID_SORT_KEYS.join(\",\")}` };\n }\n const sortBy = sortByRaw as SessionSortKey;\n\n const orderRaw = url.searchParams.get(\"order\") ?? \"desc\";\n if (!VALID_ORDERS.includes(orderRaw as SessionSortOrder)) {\n return { error: `order must be asc or desc` };\n }\n const order = orderRaw as SessionSortOrder;\n\n const statusRaw = url.searchParams.get(\"status\");\n let status: SessionStatus[] | undefined;\n if (statusRaw) {\n const parts = statusRaw\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n for (const p of parts) {\n if (!VALID_STATUSES.includes(p as SessionStatus)) {\n return { error: `status entry \"${p}\" is invalid` };\n }\n }\n status = parts as SessionStatus[];\n }\n\n const cursor = url.searchParams.get(\"cursor\") ?? undefined;\n\n return { query: { limit, sortBy, order, status, cursor } };\n}\n\nfunction readBody(req: IncomingMessage): Promise<any> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk) => chunks.push(chunk));\n req.on(\"end\", () => {\n try {\n const raw = Buffer.concat(chunks).toString(\"utf-8\");\n resolve(raw ? JSON.parse(raw) : {});\n } catch {\n reject(new Error(\"Invalid JSON body\"));\n }\n });\n req.on(\"error\", reject);\n });\n}\n","import crypto from 'crypto'\n\nimport { urlAlphabet } from './url-alphabet/index.js'\n\nconst POOL_SIZE_MULTIPLIER = 128\nlet pool, poolOffset\n\nlet fillPool = bytes => {\n if (bytes < 0) throw new RangeError('Wrong ID size')\n try {\n if (!pool || pool.length < bytes) {\n pool = Buffer.allocUnsafe(bytes * POOL_SIZE_MULTIPLIER)\n crypto.randomFillSync(pool)\n poolOffset = 0\n } else if (poolOffset + bytes > pool.length) {\n crypto.randomFillSync(pool)\n poolOffset = 0\n }\n } catch (e) {\n pool = undefined\n throw e\n }\n poolOffset += bytes\n}\n\nlet random = bytes => {\n fillPool((bytes |= 0))\n return pool.subarray(poolOffset - bytes, poolOffset)\n}\n\nlet customRandom = (alphabet, defaultSize, getRandom) => {\n let mask = (2 << (31 - Math.clz32((alphabet.length - 1) | 1))) - 1\n\n\n let step = Math.ceil((1.6 * mask * defaultSize) / alphabet.length)\n\n return (size = defaultSize) => {\n let id = ''\n while (true) {\n let bytes = getRandom(step)\n let i = step\n while (i--) {\n id += alphabet[bytes[i] & mask] || ''\n if (id.length === size) return id\n }\n }\n }\n}\n\nlet customAlphabet = (alphabet, size = 21) =>\n customRandom(alphabet, size, random)\n\nlet nanoid = (size = 21) => {\n fillPool((size |= 0))\n let id = ''\n for (let i = poolOffset - size; i < poolOffset; i++) {\n id += urlAlphabet[pool[i] & 63]\n }\n return id\n}\n\nexport { nanoid, customAlphabet, customRandom, urlAlphabet, random }\n","let urlAlphabet =\n 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict'\n\nexport { urlAlphabet }\n","// src/agent/errors.ts\n//\n// Structured error codes for multi-agent HTTP endpoints.\n// Existing PTY-mode endpoints keep their unstructured {error: \"msg\"} shape;\n// the retrofit is captured in tb-multi-agent/docs/plans/structured-error-codes-retrofit.md.\n\nexport const AgentErrorCode = {\n SESSION_NOT_FOUND: \"SESSION_NOT_FOUND\",\n SESSION_HISTORY_FULL: \"SESSION_HISTORY_FULL\",\n SESSION_BUSY: \"SESSION_BUSY\",\n INVALID_SESSION_STATE: \"INVALID_SESSION_STATE\",\n CONVERSATION_NOT_FOUND: \"CONVERSATION_NOT_FOUND\",\n INPUT_REQUIRED: \"INPUT_REQUIRED\",\n INVALID_BODY: \"INVALID_BODY\",\n TEMPORAL_UNAVAILABLE: \"TEMPORAL_UNAVAILABLE\",\n NOT_APPLICABLE_IN_MULTI_AGENT_MODE: \"NOT_APPLICABLE_IN_MULTI_AGENT_MODE\",\n INTERNAL_ERROR: \"INTERNAL_ERROR\",\n} as const;\n\nexport type AgentErrorCode = (typeof AgentErrorCode)[keyof typeof AgentErrorCode];\n\n/**\n * Build a structured error response. `error` and `code` are canonical;\n * `extra` may carry hint fields like `retryAfterMs` or `limitBytes` but\n * cannot override the canonical fields.\n */\nexport function agentErrorResponse(\n code: AgentErrorCode,\n message: string,\n extra: Record<string, unknown> = {},\n): { error: string; code: AgentErrorCode } & Record<string, unknown> {\n return { ...extra, error: message, code };\n}\n","// src/agent/history-mapper.ts\n//\n// Converts a CachedTail (the shape ConversationCache returns) into a\n// ConversationTurn[] (the shape the worker's UserInputSignal expects).\n//\n// Rules per spec §4:\n// - Text blocks: keep, concatenate with \"\\n\".\n// - tool_use, tool_result, thinking blocks: drop.\n// - Messages with empty content after stripping: drop.\n// - Unknown roles: drop with WARN.\n\nimport type { ConversationTurn } from \"@threadbase-sh/agent-types\";\nimport type { CachedTail } from \"../conversation-cache\";\nimport { getLogger } from \"../logger\";\n\nconst log = getLogger(\"agent.history-mapper\");\n\ntype ContentBlock =\n | { type: \"text\"; text: string }\n | { type: \"tool_use\"; id: string; name: string; input: unknown }\n | { type: \"tool_result\"; tool_use_id: string; content: unknown }\n | { type: \"thinking\"; thinking: string }\n | { type: string; [key: string]: unknown };\n\nfunction extractText(message: { text?: string; content?: unknown[] | null }): string {\n // Prefer structured content[] over flat text if both are present.\n if (Array.isArray(message.content)) {\n const blocks = message.content as ContentBlock[];\n const textParts: string[] = [];\n for (const block of blocks) {\n if (\n block &&\n typeof block === \"object\" &&\n block.type === \"text\" &&\n typeof block.text === \"string\"\n ) {\n textParts.push(block.text);\n }\n // tool_use / tool_result / thinking: ignored by design.\n }\n return textParts.join(\"\\n\");\n }\n return typeof message.text === \"string\" ? message.text : \"\";\n}\n\nexport function mapTailToConversationTurns(tail: CachedTail | null): ConversationTurn[] {\n if (!tail || !Array.isArray(tail.messages) || tail.messages.length === 0) {\n return [];\n }\n\n const turns: ConversationTurn[] = [];\n for (const message of tail.messages) {\n const role = message.role;\n if (role !== \"user\" && role !== \"assistant\") {\n log.warn(\"unknown role in tail; skipping\", {\n role,\n conversationId: tail.conversationId,\n });\n continue;\n }\n const content = extractText(message);\n if (!content || content.length === 0) {\n continue;\n }\n turns.push({ role, content });\n }\n return turns;\n}\n","// src/agent/payload-guard.ts\n//\n// Per spec §5: enforce a 1.5 MB ceiling on UserInputSignal payloads (75% of\n// Temporal's 2 MB hard limit) and emit trajectory WARN logs as a session\n// approaches the wall.\n\nimport type { UserInputSignal } from \"@threadbase-sh/agent-types\";\n\nexport interface PayloadMeasurement {\n bytes: number;\n exceedsLimit: boolean;\n}\n\n/**\n * Serialize the signal and measure its byte size. Returns both the count and\n * whether it exceeds the supplied limit.\n *\n * Callers should refuse the input and return 413 SESSION_HISTORY_FULL when\n * `exceedsLimit` is true.\n */\nexport function measureSignalPayload(\n signal: UserInputSignal,\n limitBytes: number,\n): PayloadMeasurement {\n const bytes = Buffer.byteLength(JSON.stringify(signal), \"utf8\");\n return { bytes, exceedsLimit: bytes > limitBytes };\n}\n\nexport interface TrajectoryConfig {\n trajectoryLogBytes: number;\n trajectoryLogTurns: number;\n}\n\n/**\n * Trajectory log trigger. Fires when EITHER:\n * - The session has reached a turn count that's a multiple of 5, starting at\n * `trajectoryLogTurns` (default 20), OR\n * - The composed signal is >= `trajectoryLogBytes` (default 500 KB) regardless\n * of turn count.\n *\n * Returning true means the caller should emit a WARN log line with current\n * size + turn count + percentage-of-limit info.\n */\nexport function shouldLogTrajectory(\n turnCount: number,\n bytes: number,\n cfg: TrajectoryConfig,\n): boolean {\n if (bytes >= cfg.trajectoryLogBytes) return true;\n if (turnCount < cfg.trajectoryLogTurns) return false;\n return (turnCount - cfg.trajectoryLogTurns) % 5 === 0;\n}\n","// src/agent/handle-send-agent-input.ts\n//\n// Pure function for multi-agent user-input. Server.ts wraps this in HTTP\n// plumbing. Implements spec §3.2 + §5 (payload guard) + §6 (lock check).\n\nimport type { UserInputSignal } from \"@threadbase-sh/agent-types\";\nimport { nanoid } from \"nanoid\";\nimport type { ConversationCache } from \"../conversation-cache\";\nimport { getLogger } from \"../logger\";\nimport type { ManagedSession } from \"../types\";\nimport type { AgentClient } from \"./agent-client\";\nimport type { AgentConfig } from \"./agent-config\";\nimport { AgentErrorCode, agentErrorResponse } from \"./errors\";\nimport { mapTailToConversationTurns } from \"./history-mapper\";\nimport { measureSignalPayload, shouldLogTrajectory } from \"./payload-guard\";\n\nconst log = getLogger(\"agent.send-input\");\n\nexport interface SendInputBody {\n text?: string;\n}\n\nexport interface SendAgentInputDeps {\n sessionStore: {\n getManaged: (sessionId: string) => ManagedSession | null;\n };\n cache: ConversationCache;\n agentClient: AgentClient;\n agentConfig: AgentConfig;\n}\n\nexport interface SendAgentInputResult {\n status: number;\n body: Record<string, unknown>;\n}\n\nexport async function handleSendAgentInput(\n sessionId: string,\n body: SendInputBody,\n deps: SendAgentInputDeps,\n): Promise<SendAgentInputResult> {\n // 1. Validate body\n if (typeof body.text !== \"string\" || body.text.length === 0) {\n return {\n status: 400,\n body: agentErrorResponse(\n AgentErrorCode.INPUT_REQUIRED,\n \"Body must contain a non-empty `text` field\",\n ),\n };\n }\n\n // 2. Look up session\n const session = deps.sessionStore.getManaged(sessionId);\n if (!session) {\n return {\n status: 404,\n body: agentErrorResponse(AgentErrorCode.SESSION_NOT_FOUND, `Session ${sessionId} not found`),\n };\n }\n\n // 3. Session-busy check\n if (session.currentTurnId) {\n return {\n status: 429,\n body: agentErrorResponse(\n AgentErrorCode.SESSION_BUSY,\n \"A turn is already in flight; retry shortly\",\n { retryAfterMs: deps.agentConfig.sessionBusyRetryMs },\n ),\n };\n }\n\n const turnId = nanoid();\n // 4. Acquire the lock by setting currentTurnId before any I/O.\n session.currentTurnId = turnId;\n\n // 5. Build conversation history from cache\n const conversationId = session.conversationId ?? session.id;\n const tail = deps.cache.getConversationTail(conversationId);\n const conversationHistory = mapTailToConversationTurns(tail);\n\n // 6. Compose signal\n const signal: UserInputSignal = {\n turnId,\n prompt: body.text,\n conversationHistory,\n };\n\n // 7. Payload-size guard\n const measurement = measureSignalPayload(signal, deps.agentConfig.payload.limitBytes);\n const turnCount = conversationHistory.length;\n if (shouldLogTrajectory(turnCount, measurement.bytes, deps.agentConfig.payload)) {\n log.warn(`session payload trajectory`, {\n sessionId,\n turnCount,\n observedBytes: measurement.bytes,\n limitBytes: deps.agentConfig.payload.limitBytes,\n pctOfLimit: Math.round((measurement.bytes / deps.agentConfig.payload.limitBytes) * 100),\n });\n }\n if (measurement.exceedsLimit) {\n session.currentTurnId = null; // release lock — no signal will be sent\n return {\n status: 413,\n body: agentErrorResponse(\n AgentErrorCode.SESSION_HISTORY_FULL,\n \"Conversation history exceeds payload limit\",\n {\n limitBytes: deps.agentConfig.payload.limitBytes,\n observedBytes: measurement.bytes,\n },\n ),\n };\n }\n\n // 8. Send signal\n try {\n await deps.agentClient.sendUserInput(sessionId, signal);\n } catch (err) {\n session.currentTurnId = null; // release lock on failure\n const message = err instanceof Error ? err.message : \"Temporal unavailable\";\n return {\n status: 503,\n body: agentErrorResponse(AgentErrorCode.TEMPORAL_UNAVAILABLE, message),\n };\n }\n\n return { status: 202, body: { turnId, status: \"queued\" } };\n}\n","// src/agent/handle-start-agent-session.ts\n//\n// Pure function (deps + body in, response out) for multi-agent session\n// creation. Server.ts wraps this in HTTP plumbing.\n\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { nanoid } from \"nanoid\"; // already a transitive dep of tb-streamer\nimport type { ManagedSession } from \"../types\";\nimport type { AgentClient } from \"./agent-client\";\nimport type { AgentConfig } from \"./agent-config\";\nimport { AgentErrorCode, agentErrorResponse } from \"./errors\";\n\nexport interface StartSessionBody {\n conversationId?: string;\n}\n\nexport interface StartAgentSessionDeps {\n sessionStore: {\n addManaged: (session: ManagedSession) => void;\n initAgentSession: (sessionId: string, dedupeCapacity: number) => void;\n };\n agentClient: AgentClient;\n conversationsDir: string;\n agentConfig: AgentConfig;\n}\n\nexport interface StartAgentSessionResult {\n status: number;\n body: Record<string, unknown>;\n}\n\n/**\n * Validate body shape. Accept only `{}` or `{conversationId: string}`.\n * Anything else is `INVALID_BODY`.\n */\nfunction validateBody(body: unknown): { ok: true; conversationId: string | null } | { ok: false } {\n if (body === null || body === undefined || typeof body !== \"object\") {\n return { ok: false };\n }\n const keys = Object.keys(body);\n if (keys.length === 0) {\n return { ok: true, conversationId: null };\n }\n if (keys.length === 1 && keys[0] === \"conversationId\") {\n const v = (body as { conversationId: unknown }).conversationId;\n if (typeof v === \"string\" && v.length > 0) {\n return { ok: true, conversationId: v };\n }\n }\n return { ok: false };\n}\n\nexport async function handleStartAgentSession(\n body: unknown,\n deps: StartAgentSessionDeps,\n): Promise<StartAgentSessionResult> {\n const parsed = validateBody(body);\n if (!parsed.ok) {\n return {\n status: 400,\n body: agentErrorResponse(\n AgentErrorCode.INVALID_BODY,\n \"Body must be {} or {conversationId: string}\",\n ),\n };\n }\n\n let conversationId = parsed.conversationId;\n if (conversationId) {\n // Resume — JSONL must exist\n const jsonlPath = join(deps.conversationsDir, `${conversationId}.jsonl`);\n if (!existsSync(jsonlPath)) {\n return {\n status: 404,\n body: agentErrorResponse(\n AgentErrorCode.CONVERSATION_NOT_FOUND,\n `No conversation found for id ${conversationId}`,\n ),\n };\n }\n }\n\n const sessionId = nanoid();\n if (!conversationId) conversationId = sessionId;\n\n // Build a minimal ManagedSession. PTY-specific fields stay undefined/null;\n // the spec (§3.3) says they're returned as null in multi-agent mode.\n const now = new Date();\n const session: ManagedSession = {\n id: sessionId,\n conversationId,\n projectPath: \"\",\n projectName: \"\",\n branch: \"\",\n status: \"running\",\n startedAt: now,\n completedAt: null,\n promptCount: 0,\n lastOutput: \"\",\n currentTurnId: null,\n };\n\n try {\n deps.sessionStore.addManaged(session);\n deps.sessionStore.initAgentSession(sessionId, deps.agentConfig.dedupe.perSessionCapacity);\n await deps.agentClient.startSession(sessionId);\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Temporal unavailable\";\n return {\n status: 503,\n body: agentErrorResponse(AgentErrorCode.TEMPORAL_UNAVAILABLE, message),\n };\n }\n\n return {\n status: 200,\n body: { sessionId, conversationId, status: \"running\" },\n };\n}\n","import type { HttpBindings } from \"@hono/node-server\";\nimport { Hono } from \"hono\";\nimport type { UpgradeWebSocket } from \"hono/ws\";\nimport type { WebSocket } from \"ws\";\nimport { getLogger } from \"../logger\";\nimport { authMiddleware } from \"./middleware/auth.middleware\";\nimport { corsMiddleware } from \"./middleware/cors.middleware\";\nimport { errorMiddleware } from \"./middleware/error.middleware\";\nimport { createBrowseRoutes } from \"./routes/browse.routes\";\nimport { createConversationRoutes } from \"./routes/conversations.routes\";\nimport { createHealthRoutes } from \"./routes/health.routes\";\nimport { createLogsRoutes } from \"./routes/logs.routes\";\nimport { createMiscRoutes } from \"./routes/misc.routes\";\nimport { createPairRoutes } from \"./routes/pair.routes\";\nimport { createProgressRoutes } from \"./routes/progress.routes\";\nimport { createProjectRoutes } from \"./routes/projects.routes\";\nimport { createScannerRoutes } from \"./routes/scanner.routes\";\nimport { createSessionRoutes } from \"./routes/sessions.routes\";\nimport { createWsRoutes } from \"./routes/ws.routes\";\nimport type { ApiDeps } from \"./types/api-deps\";\n\nexport type AppEnv = {\n Bindings: HttpBindings;\n Variables: {\n requestId?: string;\n validatedBody?: unknown;\n validatedQuery?: unknown;\n };\n};\n\nexport const createHonoApp = (deps: ApiDeps, upgradeWebSocket?: UpgradeWebSocket<WebSocket>) => {\n const app = new Hono<AppEnv>();\n const httpLog = getLogger(\"http\");\n\n app.use(\"*\", async (c, next) => {\n const start = Date.now();\n const ua = c.req.header(\"user-agent\") ?? \"\";\n await next();\n if (!deps.logMenubarRequests && c.req.header(\"x-client\") === \"menubar\") return;\n const ms = Date.now() - start;\n httpLog.info(`[req] ${c.req.method} ${c.req.path} → ${c.res.status} ${ms}ms`, {\n method: c.req.method,\n path: c.req.path,\n status: c.res.status,\n ms,\n ua,\n event: \"http.request\",\n });\n });\n app.use(\"*\", corsMiddleware(deps.browserCors));\n app.use(\"*\", authMiddleware(deps));\n app.onError(errorMiddleware);\n\n app.route(\"/healthz\", createHealthRoutes());\n app.route(\"/\", createMiscRoutes(deps));\n app.route(\"/api/sessions\", createSessionRoutes(deps));\n app.route(\"/api/conversations\", createConversationRoutes(deps));\n app.route(\"/api/projects\", createProjectRoutes(deps));\n app.route(\"/api/pair\", createPairRoutes(deps));\n app.route(\"/api\", createBrowseRoutes(deps));\n app.route(\"/\", createScannerRoutes(deps));\n app.route(\"/internal\", createProgressRoutes(deps));\n app.route(\"/api/logs\", createLogsRoutes());\n\n if (upgradeWebSocket) {\n app.route(\"/\", createWsRoutes(deps, upgradeWebSocket));\n }\n\n return app;\n};\n","import type { MiddlewareHandler } from \"hono\";\nimport { validateApiKey } from \"../../auth\";\nimport type { AppEnv } from \"../app\";\nimport type { ApiDeps } from \"../types/api-deps\";\n\nfunction isLocalRequest(remoteAddr: string | undefined): boolean {\n const addr = remoteAddr ?? \"\";\n return addr === \"127.0.0.1\" || addr === \"::1\" || addr === \"::ffff:127.0.0.1\";\n}\n\nconst PUBLIC_PATHS = new Set([\"/healthz\"]);\n// Localhost-only unauthenticated paths (menubar logs viewer).\nconst LOCAL_ONLY_PATHS = new Set([\"/api/logs\", \"/api/logs/meta\"]);\n// /api/__update uses HMAC signature auth instead of Bearer; skip the\n// Bearer-token middleware so the route handler can validate the signature.\nconst PUBLIC_POST_PATHS = new Set([\"/api/pair/exchange\", \"/api/__update\"]);\n// /internal/sessions/:sessionId/progress also uses HMAC (Progress webhook),\n// and the sessionId is dynamic so we match by prefix.\nconst PUBLIC_POST_PREFIXES = [\"/internal/sessions/\"];\n\nexport const authMiddleware =\n (deps: Pick<ApiDeps, \"apiKey\" | \"localNoAuth\">): MiddlewareHandler<AppEnv> =>\n async (c, next) => {\n const path = new URL(c.req.url).pathname;\n const method = c.req.method;\n const isPublicPostPath =\n method === \"POST\" &&\n (PUBLIC_POST_PATHS.has(path) || PUBLIC_POST_PREFIXES.some((p) => path.startsWith(p)));\n if (PUBLIC_PATHS.has(path) || isPublicPostPath) {\n await next();\n return;\n }\n\n const remoteAddr = c.env.incoming?.socket?.remoteAddress;\n if (LOCAL_ONLY_PATHS.has(path) && isLocalRequest(remoteAddr)) {\n await next();\n return;\n }\n\n if (deps.localNoAuth) {\n if (isLocalRequest(remoteAddr)) {\n await next();\n return;\n }\n }\n\n const authorization = c.req.header(\"authorization\");\n if (authorization?.startsWith(\"Bearer \")) {\n const token = authorization.slice(7);\n if (validateApiKey(token, deps.apiKey)) {\n await next();\n return;\n }\n }\n\n const key = c.req.query(\"key\");\n if (key && validateApiKey(key, deps.apiKey)) {\n await next();\n return;\n }\n\n return c.json({ error: \"Unauthorized\" }, 401);\n };\n","import type { MiddlewareHandler } from \"hono\";\nimport type { AppEnv } from \"../app\";\n\n// Local dev origins allowed when browser CORS is enabled.\nconst DEFAULT_DEV_ORIGINS = [\n \"http://localhost:8081\",\n \"http://localhost:19006\",\n \"http://localhost:3000\",\n];\n\n// Browser CORS is OFF by default: without an Origin allow-list, no web page can\n// make authenticated requests even if it obtains the API key out-of-band. Set\n// THREADBASE_ALLOW_BROWSER_CORS to enable (any of: 1, true, yes, on), and\n// optionally to a comma-separated origin list to allow origins beyond the\n// localhost dev defaults, e.g. THREADBASE_ALLOW_BROWSER_CORS=https://app.example.com\n// Mobile requests aren't browser-originated (no Origin header) and are unaffected.\nexport function resolveAllowedOrigins(raw: string | undefined): Set<string> | null {\n if (!raw) return null;\n const trimmed = raw.trim();\n const lower = trimmed.toLowerCase();\n if (lower === \"0\" || lower === \"false\" || lower === \"no\" || lower === \"off\" || trimmed === \"\") {\n return null;\n }\n\n const origins = new Set(DEFAULT_DEV_ORIGINS);\n // Treat plain on/off tokens as \"just enable the defaults\"; anything else is an\n // explicit origin list to add on top.\n if (![\"1\", \"true\", \"yes\", \"on\"].includes(lower)) {\n for (const o of trimmed.split(\",\")) {\n const origin = o.trim();\n if (origin) origins.add(origin);\n }\n }\n return origins;\n}\n\n// `configValue` is the resolved server.yaml `browser_cors:` setting (if any);\n// THREADBASE_ALLOW_BROWSER_CORS always takes precedence, matching the\n// env-over-yaml precedence used for browseRoot/publicUrl/etc. in server.ts.\nexport const corsMiddleware = (configValue?: string): MiddlewareHandler<AppEnv> => {\n const allowedOrigins = resolveAllowedOrigins(\n process.env.THREADBASE_ALLOW_BROWSER_CORS ?? configValue,\n );\n\n return async (c, next) => {\n const origin = c.req.header(\"origin\");\n const allowedOrigin = allowedOrigins && origin && allowedOrigins.has(origin) ? origin : null;\n\n if (allowedOrigin) {\n // Set on the raw ServerResponse too: many handlers write directly to\n // c.env.outgoing and return the ALREADY_HANDLED sentinel, so Hono never\n // pipes c.res.headers onto the actual response. writeHead() merges (does\n // not clear) setHeader()-set headers, so these survive the direct write.\n const raw = c.env.outgoing;\n raw.setHeader(\"Access-Control-Allow-Origin\", allowedOrigin);\n raw.setHeader(\"Vary\", \"Origin\");\n raw.setHeader(\"Access-Control-Allow-Methods\", \"GET, POST, PATCH, QUERY, OPTIONS\");\n raw.setHeader(\"Access-Control-Allow-Headers\", \"Authorization, Content-Type, If-None-Match\");\n raw.setHeader(\"Access-Control-Expose-Headers\", \"ETag, Accept-Query\");\n\n c.res.headers.set(\"Access-Control-Allow-Origin\", allowedOrigin);\n c.res.headers.set(\"Vary\", \"Origin\");\n c.res.headers.set(\"Access-Control-Allow-Methods\", \"GET, POST, PATCH, QUERY, OPTIONS\");\n c.res.headers.set(\n \"Access-Control-Allow-Headers\",\n \"Authorization, Content-Type, If-None-Match\",\n );\n c.res.headers.set(\"Access-Control-Expose-Headers\", \"ETag, Accept-Query\");\n }\n\n if (c.req.method === \"OPTIONS\") {\n return c.newResponse(null, allowedOrigin ? 204 : 403);\n }\n\n await next();\n };\n};\n","import type { ErrorHandler } from \"hono\";\nimport type { AppEnv } from \"../app\";\n\nexport const errorMiddleware: ErrorHandler<AppEnv> = (err, c) => {\n const message = err instanceof Error ? err.message : \"Internal server error\";\n return c.json({ error: message }, 500);\n};\n","import { Hono } from \"hono\";\nimport type { AppEnv } from \"../app\";\nimport type { ApiDeps } from \"../types/api-deps\";\n\nconst ALREADY_HANDLED = 597;\nconst alreadyHandled = () => new Response(null, { status: ALREADY_HANDLED });\n\nexport const createBrowseRoutes = (deps: ApiDeps) => {\n const app = new Hono<AppEnv>();\n\n app.get(\"/browse\", async (c) => {\n const url = new URL(c.req.url);\n await deps.handleBrowse(url, c.env.outgoing);\n return alreadyHandled();\n });\n\n app.post(\"/browse/mkdir\", async (c) => {\n await deps.handleMkdir(c.env.incoming, c.env.outgoing);\n return alreadyHandled();\n });\n\n return app;\n};\n","import { Hono } from \"hono\";\nimport type { AppEnv } from \"../app\";\nimport type { ApiDeps } from \"../types/api-deps\";\n\nconst ALREADY_HANDLED = 597;\nconst alreadyHandled = () => new Response(null, { status: ALREADY_HANDLED });\n\nexport const createConversationRoutes = (deps: ApiDeps) => {\n const app = new Hono<AppEnv>();\n\n app.get(\"/count\", async (c) => {\n const url = new URL(c.req.url);\n await deps.handleConversationsCount(url, c.env.outgoing);\n return alreadyHandled();\n });\n\n // QUERY (RFC 10008): safe + idempotent + cacheable like GET, but the query\n // travels in a JSON body instead of ?q= — this endpoint's input is a single\n // search string, exactly what QUERY was designed to carry. Must be\n // registered before the greedy \"/:id{.+}\" catch-all or Hono would swallow\n // \"<id>/search-target\" as a conversation id.\n app.on(\"QUERY\", \"/:id{.+}/search-target\", async (c) => {\n const id = c.req.param(\"id\");\n await deps.handleSearchTarget(id, c.env.incoming, c.env.outgoing);\n return alreadyHandled();\n });\n\n app.get(\"/:id{.+}\", async (c) => {\n const id = c.req.param(\"id\");\n const url = new URL(c.req.url);\n const ifNoneMatch = c.req.header(\"if-none-match\");\n await deps.handleGetConversation(id, url, c.env.outgoing, ifNoneMatch);\n return alreadyHandled();\n });\n\n app.get(\"/\", async (c) => {\n const url = new URL(c.req.url);\n await deps.handleListConversations(url, c.env.outgoing);\n return alreadyHandled();\n });\n\n return app;\n};\n","import { Hono } from \"hono\";\nimport { getVersion } from \"../../version\";\nimport type { AppEnv } from \"../app\";\n\nexport const createHealthRoutes = () => {\n const app = new Hono<AppEnv>();\n\n app.get(\"/\", (c) => c.json({ ok: true, version: getVersion() }));\n\n return app;\n};\n","// Runtime version reporting.\n//\n// The version string is NOT baked into the compiled bundle. Each installer\n// (Homebrew, auto-updater, scripts/deploy.sh) writes a `version.txt` next to\n// the script it activates, so the binary reports the correct version even\n// when the tarball it was built from carried a stale package.json.\n//\n// Resolution order:\n// 1. Read `<dirname(process.argv[1])>/version.txt` — set by the installer.\n// 2. Read `<dirname(process.argv[1])>/../version.txt` — the built CLI is a\n// symlink whose realpath resolves one level into $INSTALL_DIR/releases/,\n// so the installer's version.txt sits in the parent dir.\n// 3. Fall back to `<dirname(process.argv[1])>/../package.json` with a\n// `+source` suffix — covers source-tree runs (vitest, ts-node,\n// `npm run dev`) where no installer has stamped a version.\n// 4. If all fail, return \"0.0.0+unknown\" so callers never crash on a\n// missing version.\n\nimport { readFileSync, realpathSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\nlet cached: string | undefined;\n\nexport function getVersion(): string {\n if (cached !== undefined) return cached;\n cached = resolveVersion();\n return cached;\n}\n\n// Exposed for tests to clear the memoization between cases.\nexport function resetVersionCache(): void {\n cached = undefined;\n}\n\nfunction resolveVersion(): string {\n const scriptPath = process.argv[1] ?? \"\";\n const here = scriptPath ? dirname(scriptPath) : process.cwd();\n // Also resolve the realpath so shims (e.g. /opt/homebrew/bin/tb-streamer →\n // ~/.threadbase/cli.js → releases/cli.<sha>.cjs) don't cause version.txt\n // lookups to land in the wrong directory.\n let realHere = here;\n try {\n realHere = dirname(realpathSync(scriptPath));\n } catch {}\n // Deduplicate: if realHere === here we don't want to check the same dirs twice.\n const searchDirs =\n realHere === here\n ? [here, join(here, \"..\")]\n : [here, join(here, \"..\"), realHere, join(realHere, \"..\")];\n // Check both the script directory and its parent — the installer places\n // version.txt in $INSTALL_DIR (~/.threadbase/) but the built CLI is a\n // symlink whose realpath resolves into $INSTALL_DIR/releases/, so `here`\n // ends up one level too deep.\n for (const dir of searchDirs) {\n try {\n const v = readFileSync(join(dir, \"version.txt\"), \"utf8\").trim();\n if (v) return v;\n } catch {}\n }\n try {\n const pkg = JSON.parse(readFileSync(join(here, \"..\", \"package.json\"), \"utf8\")) as {\n version?: string;\n };\n if (pkg.version) return `${pkg.version}+source`;\n } catch {}\n return \"0.0.0+unknown\";\n}\n","import { closeSync, existsSync, fstatSync, openSync, readSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { Hono } from \"hono\";\nimport { installDir } from \"../../lifecycle/constants\";\nimport { getLogger } from \"../../logger\";\n\nconst logger = getLogger(\"logs-api\");\n\ntype LogSource = \"stdout\" | \"stderr\" | \"dev\";\n\nfunction resolveLogPath(source: LogSource): string {\n return join(installDir(), \"logs\", `${source}.log`);\n}\n\nfunction pickDefaultSource(): LogSource {\n // Prefer live prod stdout when present and non-empty; fall back to stderr/dev.\n for (const source of [\"stdout\", \"stderr\", \"dev\"] as const) {\n const p = resolveLogPath(source);\n if (existsSync(p) && statSync(p).size > 0) return source;\n }\n return \"stdout\";\n}\n\n/** Read the last `limit` non-empty content lines starting after `since` line index. */\nfunction readLogLines(\n filePath: string,\n sinceOffset: number,\n limit: number,\n): { lines: string[]; offset: number; total: number } {\n if (!existsSync(filePath)) {\n return { lines: [], offset: 0, total: 0 };\n }\n\n const fd = openSync(filePath, \"r\");\n try {\n const { size } = fstatSync(fd);\n if (size === 0) return { lines: [], offset: 0, total: 0 };\n\n // Cap read window so huge stdout.log (100MB+) stays responsive.\n const maxBytes = Math.min(size, 2 * 1024 * 1024);\n const start = size - maxBytes;\n const buf = Buffer.alloc(maxBytes);\n readSync(fd, buf, 0, maxBytes, start);\n let text = buf.toString(\"utf8\");\n if (start > 0) {\n const firstNl = text.indexOf(\"\\n\");\n if (firstNl >= 0) text = text.slice(firstNl + 1);\n }\n\n const allLines = text.split(\"\\n\").filter((line) => line.trim() && !line.startsWith(\"===\"));\n\n let lines: string[];\n let newOffset: number;\n\n if (sinceOffset > 0 && sinceOffset < allLines.length) {\n lines = allLines.slice(sinceOffset, sinceOffset + limit);\n newOffset = sinceOffset + lines.length;\n } else if (sinceOffset >= allLines.length && sinceOffset > 0) {\n lines = [];\n newOffset = allLines.length;\n } else {\n lines = allLines.slice(-limit);\n newOffset = allLines.length;\n }\n\n return { lines, offset: newOffset, total: allLines.length };\n } finally {\n closeSync(fd);\n }\n}\n\nexport function createLogsRoutes() {\n const app = new Hono();\n\n app.get(\"/\", (c) => {\n try {\n const sourceParam = (c.req.query(\"source\") || \"\").toLowerCase();\n const source: LogSource =\n sourceParam === \"stdout\" || sourceParam === \"stderr\" || sourceParam === \"dev\"\n ? sourceParam\n : pickDefaultSource();\n\n const logPath = resolveLogPath(source);\n const sinceOffset = parseInt(c.req.query(\"since\") || \"0\", 10);\n const limit = Math.min(parseInt(c.req.query(\"limit\") || \"100\", 10) || 100, 1000);\n\n if (!existsSync(logPath)) {\n return c.json({\n logs: [],\n message: `No log file found for source=${source}`,\n offset: 0,\n total: 0,\n source,\n });\n }\n\n const { lines, offset, total } = readLogLines(logPath, sinceOffset, limit);\n const stats = statSync(logPath);\n\n return c.json({\n logs: lines,\n offset,\n total,\n hasMore: offset < total,\n source,\n fileSize: stats.size,\n fileModified: stats.mtime.toISOString(),\n });\n } catch (error) {\n logger.error(\"Failed to read logs\", { error: String(error) });\n return c.json(\n {\n error: \"Failed to read logs\",\n logs: [],\n offset: 0,\n total: 0,\n },\n 500,\n );\n }\n });\n\n app.get(\"/meta\", (c) => {\n try {\n const sources = ([\"stdout\", \"stderr\", \"dev\"] as const).map((source) => {\n const logPath = resolveLogPath(source);\n if (!existsSync(logPath)) {\n return { source, exists: false, total: 0, fileSize: 0 };\n }\n const stats = statSync(logPath);\n return {\n source,\n exists: true,\n fileSize: stats.size,\n fileModified: stats.mtime.toISOString(),\n };\n });\n\n return c.json({\n defaultSource: pickDefaultSource(),\n sources,\n });\n } catch (error) {\n logger.error(\"Failed to read log metadata\", { error: String(error) });\n return c.json({ error: \"Failed to read log metadata\", exists: false }, 500);\n }\n });\n\n return app;\n}\n","import { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport const LAUNCHD_LABEL = \"com.ronen.threadbase\";\nexport const TASK_NAME = process.env.THREADBASE_TASK_NAME ?? \"Threadbase\";\nexport const DEFAULT_PROD_PORT = 8766;\n\nexport function installDir(): string {\n return process.env.THREADBASE_INSTALL_DIR ?? join(homedir(), \".threadbase\");\n}\n\nexport function markerPath(): string {\n return join(installDir(), \"prod-suspended.json\");\n}\n\nexport function prefsPath(): string {\n return join(installDir(), \"dev-prefs.json\");\n}\n\nexport function activeLink(): string {\n return join(installDir(), \"cli.js\");\n}\n","import { spawn } from \"node:child_process\";\nimport { createHmac, timingSafeEqual } from \"node:crypto\";\nimport { Hono } from \"hono\";\nimport type { IncomingMessage } from \"http\";\nimport { hostname } from \"os\";\nimport { loadUpdateConfig } from \"../../config/update-config\";\nimport { getLogger } from \"../../logger\";\nimport { getVersion } from \"../../version\";\nimport type { AppEnv } from \"../app\";\nimport type { ApiDeps } from \"../types/api-deps\";\n\nfunction readJsonBody(req: IncomingMessage): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk) => chunks.push(chunk));\n req.on(\"end\", () => {\n try {\n const raw = Buffer.concat(chunks).toString(\"utf-8\");\n resolve(raw ? JSON.parse(raw) : {});\n } catch {\n reject(new Error(\"Invalid JSON body\"));\n }\n });\n req.on(\"error\", reject);\n });\n}\n\nfunction readRawBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk) => chunks.push(chunk));\n req.on(\"end\", () => resolve(Buffer.concat(chunks).toString(\"utf-8\")));\n req.on(\"error\", reject);\n });\n}\n\nfunction verifyWebhookSignature(body: string, header: string | undefined, secret: string): boolean {\n if (!header) return false;\n const provided = header.startsWith(\"sha256=\") ? header.slice(7) : header;\n const expected = createHmac(\"sha256\", secret).update(body).digest(\"hex\");\n const a = Buffer.from(provided, \"utf-8\");\n const b = Buffer.from(expected, \"utf-8\");\n if (a.length !== b.length) return false;\n return timingSafeEqual(a, b);\n}\n\nconst clientLog = getLogger(\"client\");\n\ntype ClientLogEntry = {\n level?: \"debug\" | \"info\" | \"warn\" | \"error\";\n msg?: string;\n ts?: string;\n tag?: string;\n fields?: Record<string, unknown>;\n};\n\nexport const createMiscRoutes = (\n deps: Pick<\n ApiDeps,\n \"publicUrl\" | \"sessionStore\" | \"ptyAttachedIds\" | \"rotateApiKey\" | \"localNoAuth\"\n >,\n) => {\n const app = new Hono<AppEnv>();\n\n app.get(\"/api/info\", (c) => {\n const ptyIds = deps.ptyAttachedIds();\n return c.json({\n version: getVersion(),\n machineName: hostname(),\n platform: process.platform,\n activeSessions: deps.sessionStore.list(ptyIds).filter((s) => s.status === \"running\").length,\n publicUrl: deps.publicUrl,\n });\n });\n\n app.get(\"/api/profiles\", (c) => c.json([]));\n\n app.post(\"/api/auth/rotate\", (c) => {\n // Block rotation when localNoAuth is on — any localhost process could\n // call this and lock out the legitimate owner.\n if (deps.localNoAuth) {\n return c.json({ error: \"key rotation is disabled while localNoAuth is active\" }, 403);\n }\n const { newKey, persisted } = deps.rotateApiKey();\n return c.json({\n apiKey: newKey,\n persisted,\n ...(persisted\n ? {}\n : {\n warning:\n \"Key rotated in memory only. The server was started with --api-key, so the \" +\n \"old key will be restored on restart. Remove --api-key and let the server \" +\n \"manage the key via ~/.threadbase/server.yaml for rotation to survive restarts.\",\n }),\n });\n });\n\n app.post(\"/api/push/register\", (c) => c.json({ ok: true }));\n\n // Webhook for auto-update. Triggered by the release CI (or any caller that\n // knows webhook_secret) to make this server pull the new release without\n // waiting for the next poll. Enabled only when webhook_secret is set in\n // ~/.threadbase/update.yaml. HMAC-SHA256 of the raw body using that secret\n // must match the X-Threadbase-Signature header.\n app.post(\"/api/__update\", async (c) => {\n const cfg = loadUpdateConfig();\n if (!cfg?.webhook_secret) {\n return c.json({ error: \"webhook disabled\" }, 404);\n }\n\n let body: string;\n try {\n body = await readRawBody(c.env.incoming);\n } catch {\n return c.json({ error: \"could not read body\" }, 400);\n }\n\n const sig = c.req.header(\"x-threadbase-signature\");\n if (!verifyWebhookSignature(body, sig, cfg.webhook_secret)) {\n return c.json({ error: \"invalid signature\" }, 401);\n }\n\n const cliPath = process.argv[1];\n if (!cliPath) {\n return c.json({ error: \"cannot resolve updater path\" }, 500);\n }\n const child = spawn(process.execPath, [cliPath, \"update\", \"--force\"], {\n detached: true,\n stdio: \"ignore\",\n });\n child.unref();\n\n return c.json({ accepted: true, pid: child.pid }, 202);\n });\n\n app.post(\"/api/__client-log\", async (c) => {\n const ua = c.req.header(\"user-agent\") ?? \"\";\n let body: { entries?: ClientLogEntry[] } = {};\n try {\n body = (await readJsonBody(c.env.incoming)) as { entries?: ClientLogEntry[] };\n } catch {\n return c.json({ ok: false, error: \"invalid json\" }, 400);\n }\n const entries = Array.isArray(body.entries) ? body.entries : [];\n for (const e of entries) {\n const level =\n e.level === \"debug\" || e.level === \"warn\" || e.level === \"error\" ? e.level : \"info\";\n clientLog[level](`[client] ${e.tag ?? \"log\"}: ${e.msg ?? \"\"}`, {\n clientTs: e.ts,\n tag: e.tag,\n ua,\n ...(e.fields ?? {}),\n });\n }\n return c.json({ ok: true, accepted: entries.length });\n });\n\n return app;\n};\n","import { readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { parse as parseYaml } from \"yaml\";\nimport { type UpdateConfig, UpdateConfigSchema } from \"../schemas/updateConfig.schema\";\n\nconst DEFAULT_CONFIG_PATH = join(homedir(), \".threadbase\", \"update.yaml\");\n\nexport interface LoadUpdateConfigOptions {\n path?: string;\n}\n\n/**\n * Loads ~/.threadbase/update.yaml. Returns null when the file does not exist\n * (auto-update disabled). Throws on malformed YAML or schema-invalid content\n * so misconfiguration is loud rather than silently disabling updates.\n */\nexport function loadUpdateConfig(opts: LoadUpdateConfigOptions = {}): UpdateConfig | null {\n const path = opts.path ?? DEFAULT_CONFIG_PATH;\n\n let raw: string;\n try {\n raw = readFileSync(path, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw err;\n }\n\n const parsed: unknown = parseYaml(raw);\n if (parsed === null || parsed === undefined) {\n throw new Error(`update.yaml at ${path} is empty — github_repo is required`);\n }\n\n return UpdateConfigSchema.parse(parsed);\n}\n\nexport { DEFAULT_CONFIG_PATH as UPDATE_CONFIG_PATH };\n","import { z } from \"zod\";\n\nexport const UpdateConfigSchema = z\n .object({\n auto_update: z.boolean().default(false),\n channel: z.enum([\"stable\", \"next\"]).default(\"stable\"),\n allow: z.array(z.enum([\"patch\", \"minor\", \"major\"])).default([\"patch\", \"minor\"]),\n poll_interval_minutes: z.number().int().min(0).default(60),\n defer_if_active_sessions: z.boolean().default(true),\n github_repo: z.string().regex(/^[^/]+\\/[^/]+$/, \"github_repo must be 'owner/name'\"),\n webhook_secret: z.string().min(1).nullable().default(null),\n })\n .strict();\n\nexport type UpdateConfig = z.infer<typeof UpdateConfigSchema>;\n","import { Hono } from \"hono\";\nimport type { AppEnv } from \"../app\";\nimport type { ApiDeps } from \"../types/api-deps\";\n\nconst ALREADY_HANDLED = 597;\nconst alreadyHandled = () => new Response(null, { status: ALREADY_HANDLED });\n\nexport const createPairRoutes = (deps: ApiDeps) => {\n const app = new Hono<AppEnv>();\n\n app.post(\"/start\", (c) => {\n deps.handlePairStart(c.env.outgoing);\n return alreadyHandled();\n });\n\n // /api/pair/exchange is public — auth middleware skips it\n app.post(\"/exchange\", async (c) => {\n await deps.handlePairExchange(c.env.incoming, c.env.outgoing);\n return alreadyHandled();\n });\n\n return app;\n};\n","import { Hono } from \"hono\";\nimport type { AppEnv } from \"../app\";\nimport type { ApiDeps } from \"../types/api-deps\";\n\nconst ALREADY_HANDLED = 597;\nconst alreadyHandled = () => new Response(null, { status: ALREADY_HANDLED });\n\nexport const createProjectRoutes = (deps: ApiDeps) => {\n const app = new Hono<AppEnv>();\n\n app.get(\"/\", (c) => {\n const url = new URL(c.req.url);\n deps.handleListProjects(url, c.env.outgoing);\n return alreadyHandled();\n });\n\n app.get(\"/popular\", (c) => {\n const url = new URL(c.req.url);\n deps.handleGetPopularProjects(url, c.env.outgoing);\n return alreadyHandled();\n });\n\n return app;\n};\n","import { Hono } from \"hono\";\nimport type { AppEnv } from \"../app\";\nimport type { ApiDeps } from \"../types/api-deps\";\n\nconst ALREADY_HANDLED = 597;\nconst alreadyHandled = () => new Response(null, { status: ALREADY_HANDLED });\n\nexport const createScannerRoutes = (deps: ApiDeps) => {\n const app = new Hono<AppEnv>();\n\n app.get(\"/api/search\", async (c) => {\n const url = new URL(c.req.url);\n await deps.handleSearch(url, c.env.outgoing);\n return alreadyHandled();\n });\n\n return app;\n};\n","import { Hono } from \"hono\";\nimport type { AppEnv } from \"../app\";\nimport type { ApiDeps } from \"../types/api-deps\";\n\n// Sentinel status used to signal that the handler already wrote to the Node\n// ServerResponse directly. writeHonoResponse in server.ts skips piping when\n// it sees this status.\nexport const ALREADY_HANDLED = 597;\nconst alreadyHandled = () => new Response(null, { status: ALREADY_HANDLED });\n\nexport const createSessionRoutes = (deps: ApiDeps) => {\n const app = new Hono<AppEnv>();\n\n app.get(\"/count\", (c) => {\n deps.handleSessionsCount(c.env.outgoing);\n return alreadyHandled();\n });\n\n app.get(\"/recents\", (c) => {\n const url = new URL(c.req.url);\n deps.handleGetRecentSessions(url, c.env.outgoing);\n return alreadyHandled();\n });\n\n app.get(\"/names\", (c) => {\n deps.handleGetSessionNames(c.env.outgoing);\n return alreadyHandled();\n });\n\n app.post(\"/resume\", async (c) => {\n await deps.handleResume(c.env.incoming, c.env.outgoing);\n return alreadyHandled();\n });\n\n app.post(\"/start\", async (c) => {\n await deps.handleStartSession(c.env.incoming, c.env.outgoing);\n return alreadyHandled();\n });\n\n app.get(\"/\", async (c) => {\n const url = new URL(c.req.url);\n await deps.handleListSessions(url, c.env.outgoing);\n return alreadyHandled();\n });\n\n app.get(\"/:id/output\", (c) => {\n deps.handleGetOutput(c.req.param(\"id\"), c.env.outgoing);\n return alreadyHandled();\n });\n\n app.post(\"/:id/input\", async (c) => {\n await deps.handleSendInput(c.req.param(\"id\"), c.env.incoming, c.env.outgoing);\n return alreadyHandled();\n });\n\n app.post(\"/:id/answer\", async (c) => {\n await deps.handleSendAnswer(c.req.param(\"id\"), c.env.incoming, c.env.outgoing);\n return alreadyHandled();\n });\n\n app.post(\"/:id/files\", async (c) => {\n await deps.handleUploadFile(c.req.param(\"id\"), c.env.incoming, c.env.outgoing);\n return alreadyHandled();\n });\n\n app.post(\"/:id/cancel\", (c) => {\n deps.handleCancel(c.req.param(\"id\"), c.env.outgoing);\n return alreadyHandled();\n });\n\n app.patch(\"/:id/name\", async (c) => {\n await deps.handleSetSessionName(c.req.param(\"id\"), c.env.incoming, c.env.outgoing);\n return alreadyHandled();\n });\n\n app.post(\"/:id/adopt\", async (c) => {\n await deps.handleAdopt(c.req.param(\"id\"), c.env.outgoing);\n return alreadyHandled();\n });\n\n app.post(\"/:id/stop\", async (c) => {\n await deps.handleStopSession(c.req.param(\"id\"), c.env.outgoing);\n return alreadyHandled();\n });\n\n app.get(\"/:id\", (c) => {\n deps.handleGetSession(c.req.param(\"id\"), c.env.outgoing);\n return alreadyHandled();\n });\n\n return app;\n};\n","import { Hono } from \"hono\";\nimport type { UpgradeWebSocket } from \"hono/ws\";\nimport type { WebSocket } from \"ws\";\nimport type { AppEnv } from \"../app\";\nimport type { ApiDeps } from \"../types/api-deps\";\n\nexport const createWsRoutes = (deps: ApiDeps, upgradeWebSocket: UpgradeWebSocket<WebSocket>) => {\n const app = new Hono<AppEnv>();\n\n app.get(\n \"/ws\",\n upgradeWebSocket(() => {\n let openWs: WebSocket | null = null;\n return {\n onOpen(_evt, ws) {\n const raw = ws.raw;\n if (!raw) return;\n openWs = raw;\n deps.handleWsOpen(raw);\n },\n onMessage(evt, _ws) {\n if (openWs) deps.handleWsMessage(openWs, evt.data);\n },\n onClose(_evt, _ws) {\n if (openWs) deps.handleWsClose(openWs);\n },\n };\n }),\n );\n\n return app;\n};\n","import { mkdir, readdir, realpath, stat } from \"fs/promises\";\nimport { join, resolve, sep } from \"path\";\n\n/**\n * Thrown when a browse target is inside the root but does not exist on disk\n * (e.g. a mobile-cached path whose folder was since moved or deleted). Lets the\n * browse handler answer 404 instead of conflating it with an out-of-root 400.\n */\nexport class BrowsePathNotFoundError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"BrowsePathNotFoundError\";\n }\n}\n\nexport async function resolveBrowsePath(browseRoot: string, relativePath: string): Promise<string> {\n const normalizedRoot = resolve(browseRoot);\n // On Unix, if relativePath is already an absolute path under browseRoot, use it directly.\n // Only strip the leading separator for bare names like \"/projectA\" sent by the mobile browse\n // tree — not for full paths like \"/Users/foo/bar\" which are absolute, not drive-root-relative.\n // On Windows we always strip because \"\\foo\" means \"drive root relative\", never a full path.\n let sanitized: string;\n if (\n process.platform !== \"win32\" &&\n relativePath.startsWith(\"/\") &&\n relativePath.length > 1 &&\n relativePath.includes(\"/\", 1)\n ) {\n sanitized = relativePath;\n } else {\n sanitized = relativePath.replace(/^[/\\\\]+/, \"\");\n }\n const target = sanitized ? resolve(normalizedRoot, sanitized) : normalizedRoot;\n // Build the allowed prefix with exactly one separator — normalizedRoot may already end with sep\n // when browseRoot is a drive root (e.g. \"C:\\\"), which would otherwise create a double-sep prefix.\n const rootPrefix = normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`;\n if (!target.startsWith(rootPrefix) && target !== normalizedRoot) {\n throw new Error(\"Path outside browse root\");\n }\n // Verify the path exists; surface a not-found as a typed error so the handler\n // can answer 404 (folder gone) rather than the out-of-root 400 above.\n try {\n await realpath(target);\n } catch (err: any) {\n if (err?.code === \"ENOENT\") {\n throw new BrowsePathNotFoundError(`Path not found: ${target}`);\n }\n throw err;\n }\n return target;\n}\n\nexport async function listDirectories(absolutePath: string): Promise<Array<{ name: string }>> {\n const entries = await readdir(absolutePath, { withFileTypes: true });\n return entries\n .filter((e) => e.isDirectory())\n .map((e) => ({ name: e.name }))\n .sort((a, b) => a.name.localeCompare(b.name));\n}\n\nexport async function createDirectory(parentAbsolutePath: string, name: string): Promise<string> {\n if (name.includes(\"/\") || name.includes(\"\\\\\") || name === \"..\" || name === \".\") {\n throw new Error(\"Invalid directory name\");\n }\n const target = join(parentAbsolutePath, name);\n try {\n const s = await stat(target);\n if (s.isDirectory()) throw new Error(\"Directory already exists\");\n } catch (err: any) {\n if (err.code !== \"ENOENT\") throw err;\n }\n await mkdir(target);\n return target;\n}\n","import {\n type ConversationMessage,\n type ConversationMeta,\n createJsonlParseState,\n type FileStatEntry,\n type JsonlParseState,\n parseJsonlLine,\n} from \"@threadbase-sh/scanner\";\nimport Database from \"better-sqlite3\";\nimport { closeSync, existsSync, mkdirSync, openSync, readSync, type Stats, statSync } from \"fs\";\nimport { open as openAsync } from \"fs/promises\";\nimport { dirname } from \"path\";\nimport { setImmediate as yieldToEventLoop } from \"timers/promises\";\nimport { runSqliteMigrations } from \"./db/sqlite-migrate\";\nimport { CLAUDE_CODE_PROVIDER } from \"./providers\";\nimport {\n DEFAULT_AGENT_ENTRYPOINTS,\n isAgentFile,\n isAgentLine,\n} from \"./services/conversations/isAgentConversation\";\nimport { fileIdentity, type LineSpan, splitCompleteLines } from \"./utils/fileIdentity\";\n\nexport interface ConversationCacheOptions {\n // When true, drop conversations whose JSONL came from an agent entrypoint.\n // Default false to preserve legacy behavior.\n filterAgentConversations?: boolean;\n // Set of `entrypoint` values to treat as agent traffic. Defaults to\n // DEFAULT_AGENT_ENTRYPOINTS ({ sdk-cli, claude-vscode }).\n agentEntrypoints?: ReadonlySet<string>;\n // Fired the first time an agent JSONL is detected for a given file path\n // (from updateFromLine). Lets the server unwatch the file.\n onAgentFileDetected?: (filePath: string) => void;\n}\n\nexport interface ConversationListItem {\n id: string;\n filePath: string;\n projectId: string | null;\n projectPath: string | null;\n projectName: string | null;\n title: string | null;\n model: string | null;\n account: string | null;\n branch: string | null;\n messageCount: number;\n lastActivity: string;\n firstMessage: string | null;\n lastMessage: string | null;\n preview: string | null;\n source: string | null;\n provider: \"claude-code\" | \"codex-cli\";\n}\n\nexport interface CachedTailMessage {\n role: string;\n timestamp: string;\n text: string;\n content?: unknown[];\n}\n\nexport interface CachedTail {\n conversationId: string;\n messages: CachedTailMessage[];\n tailSize: number;\n}\n\n/** A row of `conversation_file_state` — per-file offset-index resume state. */\nexport interface FileStateRow {\n path: string;\n identity: string;\n size: number;\n mtime_ms: number;\n byte_offset: number;\n last_message_index: number;\n}\n\n/** A row of `conversation_message_index` — one indexed message's byte span. */\nexport interface MessageIndexRow {\n conversation_id: string;\n message_index: number;\n byte_offset: number;\n byte_length: number;\n uuid: string | null;\n role: string | null;\n ts: number | null;\n}\n\nexport interface ScannerMeta {\n id: string;\n sessionId?: string;\n filePath: string;\n projectPath?: string;\n projectName?: string;\n title?: string;\n model?: string;\n account?: string;\n gitBranch?: string;\n messageCount?: number;\n timestamp?: string;\n firstMessage?: unknown;\n lastMessage?: unknown;\n preview?: string;\n provider?: \"claude-code\" | \"codex-cli\";\n}\n\ninterface MetaRow {\n id: string;\n file_path: string;\n project_id: string | null;\n project_path: string | null;\n project_name: string | null;\n title: string | null;\n model: string | null;\n account: string | null;\n branch: string | null;\n message_count: number;\n last_activity: number | null;\n first_message: string | null;\n last_message: string | null;\n preview: string | null;\n source: string | null;\n provider: \"claude-code\" | \"codex-cli\";\n updated_at: number;\n scanner_meta_json: string | null;\n}\n\ninterface TailRow {\n conversation_id: string;\n messages_json: string;\n tail_size: number;\n updated_at: number;\n}\n\ntype ContentBlock = { type: string; text?: string; [key: string]: unknown };\n\ninterface JsonlLine {\n role?: string;\n type?: string;\n timestamp?: string;\n // Set by Claude Code / Agent SDK on every real message line. \"cli\" = human\n // interactive Claude Code; \"sdk-cli\" = Claude Agent SDK / claude-mem / hooks.\n entrypoint?: string;\n // Project context: the scanner sets `cwd` from any line that carries it\n // (attachment, metadata, user, assistant). The live watcher must do the\n // same — otherwise skeleton rows persist with NULL project_path.\n cwd?: string;\n slug?: string;\n // Real Claude JSONL emits either an array of blocks or a raw string. Normalize\n // via `normalizeContent` before consuming.\n content?: ContentBlock[] | string;\n message?: {\n role?: string;\n content?: ContentBlock[] | string;\n };\n}\n\n// Last three path segments — mirrors @threadbase-sh/scanner's\n// `getShortProjectName`. Inlined here because the scanner does not export it.\nfunction shortProjectName(fullPath: string): string {\n const parts = fullPath.split(/[/\\\\]/).filter(Boolean);\n return parts.slice(-3).join(\"/\");\n}\n\nfunction normalizeContent(raw: ContentBlock[] | string | null | undefined): ContentBlock[] {\n if (Array.isArray(raw)) return raw;\n if (typeof raw === \"string\") return [{ type: \"text\", text: raw }];\n return [];\n}\n\nconst SCHEMA = `\nCREATE TABLE IF NOT EXISTS conversation_meta (\n id TEXT PRIMARY KEY,\n file_path TEXT NOT NULL,\n project_path TEXT,\n project_name TEXT,\n title TEXT,\n model TEXT,\n account TEXT,\n branch TEXT,\n message_count INTEGER DEFAULT 0,\n last_activity INTEGER,\n first_message TEXT,\n last_message TEXT,\n preview TEXT,\n updated_at INTEGER NOT NULL\n);\nCREATE INDEX IF NOT EXISTS idx_meta_last_activity ON conversation_meta(last_activity DESC);\nCREATE INDEX IF NOT EXISTS idx_meta_project ON conversation_meta(project_path);\n\nCREATE TABLE IF NOT EXISTS conversation_tail (\n conversation_id TEXT PRIMARY KEY REFERENCES conversation_meta(id) ON DELETE CASCADE,\n messages_json TEXT NOT NULL,\n tail_size INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS session_names (\n session_id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n updated_at INTEGER NOT NULL\n);\n`;\n\nexport class ConversationCache {\n private db: Database.Database;\n private tailSize: number;\n private fileIndex = new Map<string, string>();\n private fileIndexLoaded = false;\n // Per-file scanner parse state for the incremental offset-index writer. The\n // reducer is stateful across lines (pending tool_uses, latest timestamp), so\n // it must persist between watcher reads of the same file. Cleared on\n // truncation/backfill.\n private indexParseState = new Map<string, JsonlParseState>();\n // Single-flight guard for backfillIndex — concurrent detail requests for the\n // same cold file await one walk, not N. Entry dropped on settle.\n private backfillInFlight = new Map<string, Promise<void>>();\n // Monotonically increasing counter for tail updated_at — guarantees strict\n // ordering even when multiple updateFromLine() calls land within the same ms.\n private tailSeq = Date.now();\n // Monotonically increasing counter for session_names updated_at.\n private nameSeq = Date.now();\n\n private stmts: {\n getById: Database.Statement;\n getFullById: Database.Statement;\n updateMeta: Database.Statement;\n updateMetaBatch: Database.Statement;\n insertSkeleton: Database.Statement;\n backfillSkeletonProject: Database.Statement;\n upsertFull: Database.Statement;\n getTail: Database.Statement;\n hasTail: Database.Statement;\n upsertTail: Database.Statement;\n list: Database.Statement;\n count: Database.Statement;\n listByProject: Database.Statement;\n countByProject: Database.Statement;\n listByProvider: Database.Statement;\n countByProvider: Database.Statement;\n deleteById: Database.Statement;\n deleteTailById: Database.Statement;\n deleteAll: Database.Statement;\n deleteTailAll: Database.Statement;\n getIdByFilePath: Database.Statement;\n getProviderByFilePath: Database.Statement;\n allFilePaths: Database.Statement;\n allFileStats: Database.Statement;\n allScannerStatCacheRows: Database.Statement;\n updateScannerCache: Database.Statement;\n getFileMetadata: Database.Statement;\n upsertFileMetadata: Database.Statement;\n upsertSessionName: Database.Statement;\n getSessionName: Database.Statement;\n listSessionNames: Database.Statement;\n setConversationProjectId: Database.Statement;\n markAsStreamer: Database.Statement;\n getLatestConversation: Database.Statement;\n listConversationsForProjectBackfill: Database.Statement;\n hasOrphanProjectId: Database.Statement;\n popularProjects: Database.Statement;\n getFileState: Database.Statement;\n upsertFileState: Database.Statement;\n deleteFileState: Database.Statement;\n deleteMessageIndex: Database.Statement;\n insertMessageIndexRow: Database.Statement;\n getMessageIndexWindow: Database.Statement;\n getIndexedMessageCount: Database.Statement;\n };\n\n private migrationsDir?: string;\n\n // When true, ingestion drops conversations whose JSONL `entrypoint` belongs\n // to `agentEntrypoints`. See isAgentConversation.ts.\n private filterAgentConversations = false;\n private agentEntrypoints: ReadonlySet<string> = DEFAULT_AGENT_ENTRYPOINTS;\n private onAgentFileDetected?: (filePath: string) => void;\n\n private constructor(\n db: Database.Database,\n tailSize: number,\n migrationsDir?: string,\n options?: ConversationCacheOptions,\n ) {\n this.migrationsDir = migrationsDir;\n this.db = db;\n this.tailSize = tailSize;\n this.filterAgentConversations = options?.filterAgentConversations ?? false;\n this.agentEntrypoints = options?.agentEntrypoints ?? DEFAULT_AGENT_ENTRYPOINTS;\n this.onAgentFileDetected = options?.onAgentFileDetected;\n db.exec(SCHEMA);\n runSqliteMigrations(db, this.migrationsDir);\n this.stmts = {\n getById: db.prepare(\"SELECT id FROM conversation_meta WHERE id = ?\"),\n getFullById: db.prepare(\"SELECT * FROM conversation_meta WHERE id = ?\"),\n updateMeta: db.prepare(\n \"UPDATE conversation_meta SET message_count = message_count + 1, last_activity = ?, last_message = ?, updated_at = ? WHERE id = ?\",\n ),\n // Batch equivalent of updateMeta: bumps message_count by N in one write\n // (used by updateFromLines so a burst of appended lines is one UPDATE).\n updateMetaBatch: db.prepare(\n \"UPDATE conversation_meta SET message_count = message_count + @inc, last_activity = @last_activity, last_message = @last_message, updated_at = @updated_at WHERE id = @id\",\n ),\n insertSkeleton: db.prepare(\n \"INSERT OR IGNORE INTO conversation_meta (id, file_path, message_count, updated_at) VALUES (?, ?, 1, ?)\",\n ),\n // Fills project_path / project_name / title on a row whose columns are\n // still NULL. Never overwrites scanner-populated values — the scanner's\n // upsertFromScannerMeta remains authoritative for those columns.\n backfillSkeletonProject: db.prepare(\n `UPDATE conversation_meta\n SET project_path = COALESCE(project_path, @project_path),\n project_name = COALESCE(project_name, @project_name),\n title = COALESCE(title, @title)\n WHERE id = @id\n AND (project_path IS NULL OR project_name IS NULL OR title IS NULL)`,\n ),\n upsertFull: db.prepare(`\n INSERT INTO conversation_meta\n (id, file_path, project_path, project_name, title, model, account, branch,\n message_count, last_activity, first_message, last_message, preview, updated_at,\n mtime_ms, file_size, provider, scanner_meta_json)\n VALUES\n (@id, @file_path, @project_path, @project_name, @title, @model, @account, @branch,\n @message_count, @last_activity, @first_message, @last_message, @preview, @updated_at,\n @mtime_ms, @file_size, @provider, @scanner_meta_json)\n ON CONFLICT(id) DO UPDATE SET\n file_path = excluded.file_path,\n project_path = excluded.project_path,\n project_name = excluded.project_name,\n title = excluded.title,\n model = excluded.model,\n account = excluded.account,\n branch = excluded.branch,\n -- message_count is incremented by live tailing but recounted from\n -- scratch by a scanner rescan; a stale rescan must not carry a live\n -- session's count backwards, so take the max instead of overwriting.\n message_count = MAX(conversation_meta.message_count, excluded.message_count),\n last_activity = excluded.last_activity,\n first_message = excluded.first_message,\n last_message = excluded.last_message,\n preview = excluded.preview,\n updated_at = excluded.updated_at,\n mtime_ms = excluded.mtime_ms,\n file_size = excluded.file_size,\n provider = excluded.provider,\n scanner_meta_json = excluded.scanner_meta_json\n WHERE conversation_meta.updated_at < excluded.updated_at\n `),\n getTail: db.prepare(\"SELECT * FROM conversation_tail WHERE conversation_id = ?\"),\n hasTail: db.prepare(\"SELECT 1 FROM conversation_tail WHERE conversation_id = ? LIMIT 1\"),\n upsertTail: db.prepare(`\n INSERT INTO conversation_tail (conversation_id, messages_json, tail_size, updated_at)\n VALUES (?, ?, ?, ?)\n ON CONFLICT(conversation_id) DO UPDATE SET\n messages_json = excluded.messages_json,\n tail_size = excluded.tail_size,\n updated_at = excluded.updated_at\n WHERE conversation_tail.updated_at < excluded.updated_at\n `),\n list: db.prepare(\n \"SELECT * FROM conversation_meta ORDER BY last_activity DESC LIMIT ? OFFSET ?\",\n ),\n count: db.prepare(\"SELECT COUNT(*) as n FROM conversation_meta\"),\n listByProject: db.prepare(\n \"SELECT * FROM conversation_meta WHERE project_path = ? ORDER BY last_activity DESC LIMIT ? OFFSET ?\",\n ),\n countByProject: db.prepare(\n \"SELECT COUNT(*) as n FROM conversation_meta WHERE project_path = ?\",\n ),\n listByProvider: db.prepare(\n \"SELECT * FROM conversation_meta WHERE provider = ? ORDER BY last_activity DESC LIMIT ? OFFSET ?\",\n ),\n countByProvider: db.prepare(\"SELECT COUNT(*) as n FROM conversation_meta WHERE provider = ?\"),\n deleteById: db.prepare(\"DELETE FROM conversation_meta WHERE id = ?\"),\n deleteTailById: db.prepare(\"DELETE FROM conversation_tail WHERE conversation_id = ?\"),\n deleteAll: db.prepare(\"DELETE FROM conversation_meta\"),\n deleteTailAll: db.prepare(\"DELETE FROM conversation_tail\"),\n getIdByFilePath: db.prepare(\"SELECT id FROM conversation_meta WHERE file_path = ?\"),\n getProviderByFilePath: db.prepare(\n \"SELECT provider FROM conversation_meta WHERE file_path = ?\",\n ),\n allFilePaths: db.prepare(\"SELECT id, file_path FROM conversation_meta\"),\n allFileStats: db.prepare(\n \"SELECT file_path, mtime_ms, file_size FROM conversation_meta WHERE mtime_ms IS NOT NULL AND file_size IS NOT NULL\",\n ),\n allScannerStatCacheRows: db.prepare(\n \"SELECT file_path, mtime_ms, file_size, scanner_meta_json FROM conversation_meta WHERE mtime_ms IS NOT NULL AND file_size IS NOT NULL AND scanner_meta_json IS NOT NULL\",\n ),\n updateScannerCache: db.prepare(\n \"UPDATE conversation_meta SET mtime_ms = ?, file_size = ?, scanner_meta_json = ? WHERE id = ?\",\n ),\n getFileMetadata: db.prepare(\n \"SELECT mtime_ms, file_size, is_agent, agent_entrypoints_key FROM conversation_file_metadata WHERE file_path = ?\",\n ),\n upsertFileMetadata: db.prepare(`\n INSERT INTO conversation_file_metadata\n (file_path, mtime_ms, file_size, is_agent, agent_entrypoints_key, updated_at)\n VALUES\n (@file_path, @mtime_ms, @file_size, @is_agent, @agent_entrypoints_key, @updated_at)\n ON CONFLICT(file_path) DO UPDATE SET\n mtime_ms = excluded.mtime_ms,\n file_size = excluded.file_size,\n is_agent = excluded.is_agent,\n agent_entrypoints_key = excluded.agent_entrypoints_key,\n updated_at = excluded.updated_at\n `),\n upsertSessionName: db.prepare(`\n INSERT INTO session_names (session_id, name, updated_at)\n VALUES (?, ?, ?)\n ON CONFLICT(session_id) DO UPDATE SET\n name = excluded.name,\n updated_at = excluded.updated_at\n WHERE session_names.updated_at < excluded.updated_at\n `),\n getSessionName: db.prepare(\"SELECT name FROM session_names WHERE session_id = ?\"),\n listSessionNames: db.prepare(\"SELECT session_id, name FROM session_names\"),\n setConversationProjectId: db.prepare(\n \"UPDATE conversation_meta SET project_id = ? WHERE id = ?\",\n ),\n markAsStreamer: db.prepare(\"UPDATE conversation_meta SET source = 'streamer' WHERE id = ?\"),\n getLatestConversation: db.prepare(\n \"SELECT id, last_activity FROM conversation_meta WHERE last_activity IS NOT NULL ORDER BY last_activity DESC, id DESC LIMIT 1\",\n ),\n listConversationsForProjectBackfill: db.prepare(\n \"SELECT id, project_path, project_id, last_activity FROM conversation_meta WHERE project_path IS NOT NULL\",\n ),\n hasOrphanProjectId: db.prepare(\n \"SELECT 1 FROM conversation_meta WHERE project_id IS NULL AND project_path IS NOT NULL LIMIT 1\",\n ),\n popularProjects: db.prepare(\n `SELECT project_path, project_name, COUNT(*) as cnt\n FROM conversation_meta\n WHERE project_path IS NOT NULL\n GROUP BY project_path\n ORDER BY cnt DESC\n LIMIT ?`,\n ),\n getFileState: db.prepare(\"SELECT * FROM conversation_file_state WHERE path = ?\"),\n upsertFileState: db.prepare(\n `INSERT INTO conversation_file_state\n (path, identity, size, mtime_ms, byte_offset, last_message_index)\n VALUES (@path, @identity, @size, @mtime_ms, @byte_offset, @last_message_index)\n ON CONFLICT(path) DO UPDATE SET\n identity = excluded.identity,\n size = excluded.size,\n mtime_ms = excluded.mtime_ms,\n byte_offset = excluded.byte_offset,\n last_message_index = excluded.last_message_index`,\n ),\n deleteFileState: db.prepare(\"DELETE FROM conversation_file_state WHERE path = ?\"),\n deleteMessageIndex: db.prepare(\n \"DELETE FROM conversation_message_index WHERE conversation_id = ?\",\n ),\n insertMessageIndexRow: db.prepare(\n `INSERT INTO conversation_message_index\n (conversation_id, message_index, byte_offset, byte_length, uuid, role, ts)\n VALUES (@conversation_id, @message_index, @byte_offset, @byte_length, @uuid, @role, @ts)\n ON CONFLICT(conversation_id, message_index) DO UPDATE SET\n byte_offset = excluded.byte_offset,\n byte_length = excluded.byte_length,\n uuid = excluded.uuid,\n role = excluded.role,\n ts = excluded.ts`,\n ),\n getMessageIndexWindow: db.prepare(\n `SELECT message_index, byte_offset, byte_length, uuid, role, ts\n FROM conversation_message_index\n WHERE conversation_id = ? AND message_index >= ? AND message_index < ?\n ORDER BY message_index ASC`,\n ),\n getIndexedMessageCount: db.prepare(\n \"SELECT COUNT(*) as cnt FROM conversation_message_index WHERE conversation_id = ?\",\n ),\n };\n }\n\n /**\n * Expose the underlying handle so projects/cache_metadata repositories can\n * share the same connection. Internal API; not part of the public surface.\n */\n getDatabase(): Database.Database {\n return this.db;\n }\n\n // ── Offset index (design 1b) ────────────────────────────────────────────\n // conversation_file_state + conversation_message_index back the windowed\n // detail read path (SQL window select + pread of byte ranges). All methods\n // are thin wrappers over the prepared statements above.\n\n getFileState(path: string): FileStateRow | null {\n return (this.stmts.getFileState.get(path) as FileStateRow | undefined) ?? null;\n }\n\n upsertFileState(row: FileStateRow): void {\n this.stmts.upsertFileState.run(row);\n }\n\n /** Drop a file's index rows + file_state (truncation / identity change). */\n deleteFileIndex(path: string, conversationId: string): void {\n const tx = this.db.transaction(() => {\n this.stmts.deleteMessageIndex.run(conversationId);\n this.stmts.deleteFileState.run(path);\n });\n tx();\n this.indexParseState.delete(path);\n }\n\n /** Append/replace index rows in one transaction. */\n appendMessageIndexRows(rows: MessageIndexRow[]): void {\n const tx = this.db.transaction((batch: MessageIndexRow[]) => {\n for (const r of batch) this.stmts.insertMessageIndexRow.run(r);\n });\n tx(rows);\n }\n\n /** Rows for message_index in [fromIndex, toIndex), ordered ascending. */\n getMessageIndexWindow(\n conversationId: string,\n fromIndex: number,\n toIndex: number,\n ): MessageIndexRow[] {\n return this.stmts.getMessageIndexWindow.all(\n conversationId,\n fromIndex,\n toIndex,\n ) as MessageIndexRow[];\n }\n\n getIndexedMessageCount(conversationId: string): number {\n return (this.stmts.getIndexedMessageCount.get(conversationId) as { cnt: number }).cnt;\n }\n\n /**\n * Conversation id for a JSONL path — the filename stem (matches the pseudo-id\n * updateFromLine derives and the uuid the detail read path resolves). The\n * offset index keys on this so the window select and the cursor agree.\n */\n static conversationIdForFile(filePath: string): string {\n return (\n filePath\n .split(/[/\\\\]/)\n .pop()\n ?.replace(/\\.jsonl$/, \"\") ?? filePath\n );\n }\n\n // The offset index understands only claude-code JSONL: parseJsonlLine is the\n // claude-code reducer, so a codex file \"indexes\" as zero messages and the\n // resulting file_state serves empty windows for a real conversation (the\n // silent-wrong-data bug hotfixed after 1.28.0). Non-claude providers are\n // excluded from the index entirely; the scanner serves them (it routes each\n // provider to its own parser).\n private isIndexableFile(filePath: string): boolean {\n // Resolve by file_path, NOT by conversationIdForFile: codex rollout files\n // are named rollout-<ts>-<uuid>.jsonl, so the filename stem is not the\n // meta row's id and an id lookup silently misses. A file with no meta row\n // at all is NOT indexable — provider unknown means indexing is unsafe; a\n // later request backfills once the meta row exists.\n const row = this.stmts.getProviderByFilePath.get(filePath) as\n | { provider: string | null }\n | undefined;\n if (!row) return false;\n return (row.provider ?? CLAUDE_CODE_PROVIDER) === CLAUDE_CODE_PROVIDER;\n }\n\n /**\n * Incremental offset-index writer: extend the index for a burst of appended\n * lines (one watcher read) using their byte spans. Each line is classified\n * with the scanner's parseJsonlLine (a running per-file reducer state), so the\n * message ordering can never drift from the scanner's. Message lines get an\n * index row at the next message_index; non-message lines (summary/sidecar)\n * get no row but still advance byte_offset. file_state is updated to the end\n * of the last consumed span.\n *\n * Requires an up-to-date `stat` (identity/size/mtime) for the file so the read\n * path can detect truncation/replacement.\n *\n * `readFrom` is the absolute byte offset the watcher read started at, and\n * `endOffset` is where it ended (readFrom + consumed, i.e. the watcher's new\n * entry.offset). CONTIGUITY GUARD: the read must begin exactly where the index\n * left off (`readFrom === existing.byte_offset`, or 0 with no state). If it\n * doesn't — the watcher attached at EOF after the server was down, or an\n * append raced an in-flight backfill — extending would assign wrong\n * message_index values over a hole. In that case this writes nothing and\n * returns null so the caller drops the index and backfills.\n *\n * On success returns the message_index assigned to each input span (null for a\n * non-message line) so the caller can stamp WS `seq`. Empty array when spans\n * is empty. `endOffset` is stored verbatim as byte_offset so the watcher's\n * offset and file_state.byte_offset are the same number by construction.\n */\n extendMessageIndex(\n filePath: string,\n spans: LineSpan[],\n stat: Stats,\n readFrom: number,\n endOffset: number,\n ): (number | null)[] | null {\n // Non-indexable provider: write nothing, but return all-null seqs (not\n // null) — a null return means \"decline\" and would send the caller into an\n // endless drop-and-backfill loop for a file that can never be indexed.\n if (!this.isIndexableFile(filePath)) return spans.map(() => null);\n\n const existing = this.getFileState(filePath);\n const expectedStart = existing?.byte_offset ?? 0;\n // Non-contiguous read → the index would develop a hole with wrong indices.\n // Decline; the caller drops + backfills.\n if (readFrom !== expectedStart) return null;\n\n if (spans.length === 0) return [];\n const convId = ConversationCache.conversationIdForFile(filePath);\n\n let state = this.indexParseState.get(filePath);\n if (!state) {\n state = createJsonlParseState();\n this.indexParseState.set(filePath, state);\n }\n\n let nextIndex = existing ? existing.last_message_index + 1 : 0;\n const rows: MessageIndexRow[] = [];\n const seqs: (number | null)[] = [];\n\n for (const span of spans) {\n const msg = parseJsonlLine(span.text, state);\n if (!msg) {\n seqs.push(null); // summary/sidecar/malformed → no index row, no seq\n continue;\n }\n rows.push({\n conversation_id: convId,\n message_index: nextIndex,\n byte_offset: span.byteOffset,\n byte_length: span.byteLength,\n uuid: msg.uuid ?? null,\n role: msg.role ?? null,\n ts: msg.timestamp ? Date.parse(msg.timestamp) || null : null,\n });\n seqs.push(nextIndex);\n nextIndex++;\n }\n\n const tx = this.db.transaction(() => {\n for (const r of rows) this.stmts.insertMessageIndexRow.run(r);\n this.stmts.upsertFileState.run({\n path: filePath,\n identity: fileIdentity(stat),\n size: stat.size,\n mtime_ms: Math.round(stat.mtimeMs),\n // Store the watcher's end offset verbatim — same number as entry.offset,\n // so the next read's contiguity check compares like-for-like (never\n // false-positive on a read that ended in trailing empty lines).\n byte_offset: endOffset,\n last_message_index: nextIndex - 1,\n });\n });\n tx();\n return seqs;\n }\n\n clearIndexParseState(filePath: string): void {\n this.indexParseState.delete(filePath);\n }\n\n /**\n * On-demand full backfill of the offset index for a file with no/stale\n * file_state (cold conversation, or after a truncation/replacement). Rebuilds\n * from byte 0: drops any existing rows, walks the whole file in chunks with a\n * running parse state, yields to the event loop every ~1000 lines so a large\n * file never blocks, and writes index rows + file_state.\n *\n * Single-flighted per path: concurrent callers await the same walk. The\n * triggering detail request is served by the scanner fallback while this runs.\n */\n backfillIndex(filePath: string): Promise<void> {\n const inFlight = this.backfillInFlight.get(filePath);\n if (inFlight) return inFlight;\n const walk = this.runBackfill(filePath).finally(() => {\n this.backfillInFlight.delete(filePath);\n });\n this.backfillInFlight.set(filePath, walk);\n return walk;\n }\n\n private async runBackfill(filePath: string): Promise<void> {\n const convId = ConversationCache.conversationIdForFile(filePath);\n // Reset any partial/stale state before rebuilding from scratch. For a\n // non-indexable (non-claude) file the purge IS the whole job — it also\n // heals a poisoned row left by a pre-hotfix backfill — so return before\n // walking the file.\n this.deleteFileIndex(filePath, convId);\n this.indexParseState.delete(filePath);\n if (!this.isIndexableFile(filePath)) return;\n\n const CHUNK = 256 * 1024;\n const YIELD_EVERY = 1000;\n const state = createJsonlParseState();\n const fh = await openAsync(filePath, \"r\");\n let fileOffset = 0; // absolute byte offset of `carry`'s first byte\n let carry = Buffer.alloc(0); // bytes after the last \"\\n\" of the previous chunk\n let nextIndex = 0;\n let linesSinceYield = 0;\n let lastConsumedEnd = 0; // absolute byte offset just past the last full line\n let stat: Stats;\n\n try {\n stat = await fh.stat();\n const buf = Buffer.alloc(CHUNK);\n for (;;) {\n const { bytesRead } = await fh.read(buf, 0, CHUNK, null);\n if (bytesRead === 0) break;\n const combined =\n carry.length > 0\n ? Buffer.concat([carry, buf.subarray(0, bytesRead)])\n : buf.subarray(0, bytesRead);\n const { spans, consumed } = splitCompleteLines(combined, fileOffset);\n\n const rows: MessageIndexRow[] = [];\n for (const span of spans) {\n const msg = parseJsonlLine(span.text, state);\n linesSinceYield++;\n if (msg) {\n rows.push({\n conversation_id: convId,\n message_index: nextIndex,\n byte_offset: span.byteOffset,\n byte_length: span.byteLength,\n uuid: msg.uuid ?? null,\n role: msg.role ?? null,\n ts: msg.timestamp ? Date.parse(msg.timestamp) || null : null,\n });\n nextIndex++;\n }\n if (linesSinceYield >= YIELD_EVERY) {\n linesSinceYield = 0;\n await yieldToEventLoop();\n }\n }\n if (rows.length > 0) this.appendMessageIndexRows(rows);\n\n lastConsumedEnd = fileOffset + consumed;\n // Keep the unconsumed remainder (a torn line at the chunk boundary).\n // Copy it — `combined` may be a view into the reused read buffer, which\n // the next fh.read overwrites.\n carry = Buffer.from(combined.subarray(consumed));\n fileOffset += consumed;\n }\n } finally {\n await fh.close();\n }\n\n this.upsertFileState({\n path: filePath,\n identity: fileIdentity(stat),\n size: stat.size,\n mtime_ms: Math.round(stat.mtimeMs),\n byte_offset: lastConsumedEnd,\n last_message_index: nextIndex - 1,\n });\n // Seed the incremental writer's state so subsequent appends continue the\n // same reducer instead of re-parsing from scratch.\n this.indexParseState.set(filePath, state);\n }\n\n /**\n * Windowed detail read straight from the offset index — the hot path.\n * Returns the parsed messages for message_index in [fromIndex, toIndex) plus\n * the total indexed count, or null when the index can't serve this file (no\n * file_state, identity/size mismatch = truncation/replacement, or cold index)\n * so the caller falls back to the scanner and enqueues a backfill.\n *\n * On a match it SQL-selects the window's byte ranges and preads exactly those\n * ranges from the JSONL (never the whole file), parsing only the sliced lines.\n * Returns messages in the same ConversationMessage shape parseJsonlLine\n * produces during a scan, so the payload is identical to the scanner path.\n */\n readMessageWindow(\n filePath: string,\n fromIndex: number,\n toIndex: number,\n ): { messages: ConversationMessage[]; total: number; fromIndex: number } | null {\n const fileState = this.getFileState(filePath);\n if (!fileState) return null;\n\n let stat: Stats;\n try {\n stat = statSync(filePath);\n } catch {\n return null;\n }\n // The index is authoritative only up to byte_offset. Decline unless it\n // covers the whole file exactly:\n // - identity changed → file replaced;\n // - size < byte_offset → truncated;\n // - size > byte_offset → the file grew past the index (untailed messages\n // at the tail, e.g. an append with no live watcher extending the index).\n // Serving a slice in that last case silently drops the appended messages —\n // the exact live-append bug this feature exists to fix. In every mismatch\n // the caller drops the index + backfills and falls back to the scanner.\n if (fileIdentity(stat) !== fileState.identity || stat.size !== fileState.byte_offset) {\n return null;\n }\n // A file_state that covered the file but indexed no messages is a poisoned\n // row (pre-hotfix backfill of a non-claude file) — serving it would render\n // a real conversation empty. Belt-and-braces: also decline non-indexable\n // providers outright in case a poisoned row carries a nonzero count. The\n // decline routes the caller to backfill, which purges the row.\n if (fileState.last_message_index < 0 || !this.isIndexableFile(filePath)) {\n return null;\n }\n\n const total = fileState.last_message_index + 1;\n const from = Math.max(0, fromIndex);\n const to = Math.min(toIndex, total);\n if (to <= from) return { messages: [], total, fromIndex: from };\n\n const rows = this.getMessageIndexWindow(\n ConversationCache.conversationIdForFile(filePath),\n from,\n to,\n );\n if (rows.length === 0) return { messages: [], total, fromIndex: from };\n\n const messages: ConversationMessage[] = [];\n const fd = openSync(filePath, \"r\");\n try {\n // A fresh parse state per window (not seeded by replaying earlier lines —\n // that would re-read from byte 0 and defeat the index). parseJsonlLine's\n // per-line ConversationMessage output does not depend on the cross-line\n // reducer state in the current scanner, so a windowed read is byte-\n // identical to a full contiguous parse — pinned by\n // offset-index-read.test.ts (\"windowed read of a tool_use → tool_result\n // pair matches a full contiguous parse\"). If a future scanner makes the\n // per-line shape state-dependent, that test fails rather than silently\n // serving a slightly-less-enriched payload.\n const state = createJsonlParseState();\n for (const row of rows) {\n const buf = Buffer.alloc(row.byte_length);\n readSync(fd, buf, 0, row.byte_length, row.byte_offset);\n const msg = parseJsonlLine(buf.toString(\"utf-8\"), state);\n if (msg) messages.push(msg);\n }\n } finally {\n closeSync(fd);\n }\n\n return { messages, total, fromIndex: from };\n }\n\n private agentEntrypointsKey(): string {\n return [...this.agentEntrypoints].sort().join(\",\");\n }\n\n private classifyAgentFile(filePath: string, mtimeMs: number, fileSize: number): boolean {\n if (this.agentEntrypoints.size === 0) return false;\n\n const entrypointsKey = this.agentEntrypointsKey();\n const cached = this.stmts.getFileMetadata.get(filePath) as\n | {\n mtime_ms: number;\n file_size: number;\n is_agent: number;\n agent_entrypoints_key: string;\n }\n | undefined;\n if (\n cached &&\n cached.mtime_ms === mtimeMs &&\n cached.file_size === fileSize &&\n cached.agent_entrypoints_key === entrypointsKey\n ) {\n return cached.is_agent === 1;\n }\n\n const isAgent = isAgentFile(filePath, this.agentEntrypoints);\n this.stmts.upsertFileMetadata.run({\n file_path: filePath,\n mtime_ms: mtimeMs,\n file_size: fileSize,\n is_agent: isAgent ? 1 : 0,\n agent_entrypoints_key: entrypointsKey,\n updated_at: Date.now(),\n });\n return isAgent;\n }\n\n isAgentFileCached(filePath: string): boolean {\n let s: ReturnType<typeof statSync>;\n try {\n s = statSync(filePath);\n } catch {\n return false;\n }\n return this.classifyAgentFile(filePath, s.mtimeMs, s.size);\n }\n\n static open(\n dbPath: string,\n tailSize = 10,\n migrationsDir?: string,\n options?: ConversationCacheOptions,\n ): ConversationCache {\n mkdirSync(dirname(dbPath), { recursive: true });\n const db = new Database(dbPath);\n db.pragma(\"journal_mode = WAL\");\n db.pragma(\"foreign_keys = ON\");\n return new ConversationCache(db, tailSize, migrationsDir, options);\n }\n\n close(): void {\n this.db.close();\n }\n\n getPopularProjects(limit: number): Array<{ path: string; name: string; sessionCount: number }> {\n const rows = this.stmts.popularProjects.all(limit) as Array<{\n project_path: string;\n project_name: string | null;\n cnt: number;\n }>;\n return rows.map((r) => ({\n path: r.project_path,\n name: r.project_name ?? r.project_path.split(/[/\\\\]/).pop() ?? r.project_path,\n sessionCount: r.cnt,\n }));\n }\n\n private ensureFileIndex(): void {\n if (this.fileIndexLoaded) return;\n const rows = this.stmts.allFilePaths.all() as Array<{ id: string; file_path: string }>;\n for (const row of rows) {\n this.fileIndex.set(row.file_path, row.id);\n }\n this.fileIndexLoaded = true;\n }\n\n updateFromLine(filePath: string, rawLine: string): void {\n let line: JsonlLine;\n try {\n line = JSON.parse(rawLine);\n } catch {\n return;\n }\n\n if (this.filterAgentConversations && isAgentLine(line, this.agentEntrypoints)) {\n this.deleteByFilePath(filePath);\n this.onAgentFileDetected?.(filePath);\n return;\n }\n\n const role = line.role ?? line.type;\n const isMessage = role === \"user\" || role === \"assistant\";\n\n this.ensureFileIndex();\n\n // Skip lines that carry neither a message nor project context — there's\n // nothing for us to record.\n if (!isMessage && !line.cwd && !line.slug) return;\n\n let convId = this.fileIndex.get(filePath);\n if (!convId) {\n const pseudoId =\n filePath\n .split(/[/\\\\]/)\n .pop()\n ?.replace(/\\.jsonl$/, \"\") ?? filePath;\n this.stmts.insertSkeleton.run(pseudoId, filePath, 0);\n this.fileIndex.set(filePath, pseudoId);\n convId = pseudoId;\n }\n\n // Backfill project_path / project_name / title from cwd on any line that\n // carries it. COALESCE inside the SQL ensures we never overwrite a\n // scanner-populated value. Without this, the chokidar watcher leaves\n // skeleton rows with NULL project context, which renders as blank cards\n // on the mobile Recents tab.\n if (line.cwd || line.slug) {\n const projectPath = line.cwd ?? null;\n const projectName = projectPath ? shortProjectName(projectPath) : null;\n const title = line.slug ?? projectName ?? null;\n this.stmts.backfillSkeletonProject.run({\n id: convId,\n project_path: projectPath,\n project_name: projectName,\n title,\n });\n }\n\n if (!isMessage) return;\n\n const timestamp = line.timestamp ?? new Date().toISOString();\n const activityMs = new Date(timestamp).getTime();\n if (Number.isNaN(activityMs)) return;\n\n const contentBlocks = normalizeContent(line.message?.content ?? line.content);\n const text = contentBlocks.find((b) => b.type === \"text\")?.text?.slice(0, 200) ?? \"\";\n const lastMessage = JSON.stringify({ role, timestamp, text });\n const seq = ++this.tailSeq;\n\n const result = this.stmts.updateMeta.run(activityMs, lastMessage, seq, convId);\n if (result.changes === 0) return;\n\n const tailRow = this.stmts.getTail.get(convId) as TailRow | undefined;\n const msgs: CachedTailMessage[] = tailRow\n ? (JSON.parse(tailRow.messages_json) as CachedTailMessage[])\n : [];\n\n msgs.push({ role, timestamp, text, content: contentBlocks });\n if (msgs.length > this.tailSize) msgs.splice(0, msgs.length - this.tailSize);\n\n this.stmts.upsertTail.run(convId, JSON.stringify(msgs), msgs.length, seq);\n }\n\n /**\n * Batched form of updateFromLine: applies a burst of newly-appended lines\n * (one chokidar read) in a single transaction with one message_count bump,\n * one meta write, and one tail read/write — instead of 2-4 synchronous\n * writes per line. Semantics are identical to replaying each line through\n * updateFromLine in order: the agent filter short-circuits the whole batch,\n * project context is backfilled last-wins, message_count increases by the\n * number of surviving message lines, and last_activity/last_message reflect\n * the final message line.\n */\n updateFromLines(filePath: string, rawLines: string[]): void {\n // Classify all lines first (outside the transaction). A single watched\n // file maps to one conversation, so we accumulate into scalars.\n let sawProjectContext = false;\n let backfillProjectPath: string | null = null;\n let backfillProjectName: string | null = null;\n let backfillTitle: string | null = null;\n let msgCount = 0;\n let lastActivityMs: number | null = null;\n let lastMessage: string | null = null;\n const newTail: CachedTailMessage[] = [];\n\n for (const rawLine of rawLines) {\n let line: JsonlLine;\n try {\n line = JSON.parse(rawLine);\n } catch {\n continue;\n }\n\n // The first agent line nukes the file and aborts the whole batch —\n // matches updateFromLine's per-line return.\n if (this.filterAgentConversations && isAgentLine(line, this.agentEntrypoints)) {\n this.deleteByFilePath(filePath);\n this.onAgentFileDetected?.(filePath);\n return;\n }\n\n const role = line.role ?? line.type;\n const isMessage = role === \"user\" || role === \"assistant\";\n\n // Skip lines that carry neither a message nor project context.\n if (!isMessage && !line.cwd && !line.slug) continue;\n\n if (line.cwd || line.slug) {\n sawProjectContext = true;\n // First-wins per column, mirroring updateFromLine's per-line replay:\n // backfillSkeletonProject COALESCEs each column independently, so the\n // first non-null value seen for a column sticks and later lines can't\n // override it. Accumulating last-wins here would diverge from per-line\n // replay when a conversation's cwd/slug changes mid-batch.\n const lineProjectPath = line.cwd ?? null;\n const lineProjectName = lineProjectPath ? shortProjectName(lineProjectPath) : null;\n const lineTitle = line.slug ?? lineProjectName ?? null;\n backfillProjectPath ??= lineProjectPath;\n backfillProjectName ??= lineProjectName;\n backfillTitle ??= lineTitle;\n }\n\n if (!isMessage) continue;\n\n const timestamp = line.timestamp ?? new Date().toISOString();\n const activityMs = new Date(timestamp).getTime();\n if (Number.isNaN(activityMs)) continue;\n\n const contentBlocks = normalizeContent(line.message?.content ?? line.content);\n const text = contentBlocks.find((b) => b.type === \"text\")?.text?.slice(0, 200) ?? \"\";\n msgCount += 1;\n lastActivityMs = activityMs;\n lastMessage = JSON.stringify({ role, timestamp, text });\n newTail.push({ role, timestamp, text, content: contentBlocks });\n }\n\n // Nothing recordable in this batch.\n if (!sawProjectContext && msgCount === 0) return;\n\n this.ensureFileIndex();\n\n let convId = this.fileIndex.get(filePath);\n if (!convId) {\n const pseudoId =\n filePath\n .split(/[/\\\\]/)\n .pop()\n ?.replace(/\\.jsonl$/, \"\") ?? filePath;\n this.stmts.insertSkeleton.run(pseudoId, filePath, 0);\n this.fileIndex.set(filePath, pseudoId);\n convId = pseudoId;\n }\n const id = convId;\n\n const apply = this.db.transaction(() => {\n if (sawProjectContext) {\n this.stmts.backfillSkeletonProject.run({\n id,\n project_path: backfillProjectPath,\n project_name: backfillProjectName,\n title: backfillTitle,\n });\n }\n\n if (msgCount === 0) return;\n\n const seq = ++this.tailSeq;\n const result = this.stmts.updateMetaBatch.run({\n inc: msgCount,\n last_activity: lastActivityMs,\n last_message: lastMessage,\n updated_at: seq,\n id,\n });\n if (result.changes === 0) return;\n\n const tailRow = this.stmts.getTail.get(id) as TailRow | undefined;\n const msgs: CachedTailMessage[] = tailRow\n ? (JSON.parse(tailRow.messages_json) as CachedTailMessage[])\n : [];\n msgs.push(...newTail);\n if (msgs.length > this.tailSize) msgs.splice(0, msgs.length - this.tailSize);\n\n this.stmts.upsertTail.run(id, JSON.stringify(msgs), msgs.length, seq);\n });\n apply();\n }\n\n // Returns the IDs of rows actually upserted (i.e. excluding any agent JSONLs\n // skipped by the filter). The server's warm-up loop uses this to populate\n // tails only for IDs that have a parent conversation_meta row — otherwise\n // the conversation_tail FK fires and aborts the warm-up (regression covered\n // in conversation-cache.test.ts).\n upsertFromScannerMeta(metas: ScannerMeta[]): string[] {\n const filter = this.filterAgentConversations;\n const upsertedIds: string[] = [];\n const run = this.db.transaction((items: ScannerMeta[]) => {\n for (const m of items) {\n const id =\n m.sessionId ||\n m.id\n .split(\"/\")\n .pop()\n ?.replace(/\\.jsonl$/, \"\") ||\n m.id;\n const lastActivityMs = m.timestamp ? new Date(m.timestamp).getTime() : null;\n let mtimeMs: number | null = null;\n let fileSize: number | null = null;\n try {\n const s = statSync(m.filePath);\n mtimeMs = s.mtimeMs;\n fileSize = s.size;\n } catch {\n // file disappeared between scan and upsert — store without stat\n }\n const seq = ++this.tailSeq;\n if (\n filter &&\n mtimeMs !== null &&\n fileSize !== null &&\n this.classifyAgentFile(m.filePath, mtimeMs, fileSize)\n ) {\n continue;\n }\n const scannerMetaJson = JSON.stringify(m);\n this.stmts.upsertFull.run({\n id,\n file_path: m.filePath,\n project_path: m.projectPath ?? null,\n project_name: m.projectName ?? null,\n title: m.title ?? m.projectName ?? null,\n model: m.model ?? null,\n account: m.account ?? null,\n branch: m.gitBranch ?? null,\n message_count: m.messageCount ?? 0,\n last_activity: lastActivityMs,\n first_message: m.firstMessage ? JSON.stringify(m.firstMessage) : null,\n last_message: m.lastMessage ? JSON.stringify(m.lastMessage) : null,\n preview: m.preview ?? null,\n updated_at: seq,\n mtime_ms: mtimeMs,\n file_size: fileSize,\n provider: m.provider ?? CLAUDE_CODE_PROVIDER,\n scanner_meta_json: scannerMetaJson,\n });\n this.stmts.updateScannerCache.run(mtimeMs, fileSize, scannerMetaJson, id);\n if (this.fileIndexLoaded) this.fileIndex.set(m.filePath, id);\n upsertedIds.push(id);\n }\n });\n run(metas);\n return upsertedIds;\n }\n\n // Returns true if a row was deleted. Used by the prune-on-startup step and\n // by updateFromLine when a previously-cached file turns out to be an agent\n // JSONL.\n deleteByFilePath(filePath: string): boolean {\n const row = this.stmts.getIdByFilePath.get(filePath) as { id: string } | undefined;\n if (!row) return false;\n this.stmts.deleteTailById.run(row.id);\n const result = this.stmts.deleteById.run(row.id);\n this.fileIndex.delete(filePath);\n return result.changes > 0;\n }\n\n // Reads the last `tailSize` qualifying lines from a JSONL file and writes them\n // to conversation_tail. Reads backward in 8 KB chunks so memory usage is\n // bounded regardless of file size. Uses updated_at=0 so any live\n // updateFromLine() call (which uses Date.now()) always wins the upsert.\n // Returns false if the file cannot be read or the tail already exists.\n populateTailFromFile(convId: string, filePath: string): boolean {\n if (this.stmts.hasTail.get(convId)) return false;\n\n let fileSize: number;\n let fd: number;\n try {\n fileSize = statSync(filePath).size;\n fd = openSync(filePath, \"r\");\n } catch {\n return false;\n }\n\n const CHUNK = 8192;\n const buf = Buffer.allocUnsafe(CHUNK);\n let pos = fileSize;\n let partial = \"\";\n const lines: string[] = [];\n\n try {\n while (pos > 0 && lines.length < this.tailSize * 4) {\n const toRead = Math.min(CHUNK, pos);\n pos -= toRead;\n readSync(fd, buf, 0, toRead, pos);\n const chunk = buf.subarray(0, toRead).toString(\"utf8\");\n const combined = chunk + partial;\n const parts = combined.split(\"\\n\");\n // parts[0] may be a partial line — keep it for the next iteration\n partial = parts[0];\n for (let i = parts.length - 1; i >= 1; i--) {\n lines.push(parts[i]);\n }\n }\n if (partial) lines.push(partial);\n } finally {\n closeSync(fd);\n }\n\n const msgs: CachedTailMessage[] = [];\n for (let i = 0; i < lines.length && msgs.length < this.tailSize; i++) {\n const line = lines[i].trim();\n if (!line) continue;\n let parsed: JsonlLine;\n try {\n parsed = JSON.parse(line);\n } catch {\n continue;\n }\n const role = parsed.role ?? parsed.type;\n if (!role) continue;\n const timestamp = parsed.timestamp ?? \"\";\n const contentBlocks = normalizeContent(parsed.message?.content ?? parsed.content);\n const text = contentBlocks.find((b) => b.type === \"text\")?.text?.slice(0, 200) ?? \"\";\n msgs.unshift({ role, timestamp, text, content: contentBlocks });\n }\n if (msgs.length === 0) return false;\n this.stmts.upsertTail.run(convId, JSON.stringify(msgs), msgs.length, 0);\n return true;\n }\n\n listConversations(opts: { project?: string; provider?: string; limit: number; offset: number }): {\n conversations: ConversationListItem[];\n total: number;\n } {\n const { project, provider, limit, offset } = opts;\n let total: number;\n let rows: MetaRow[];\n\n if (project) {\n total = (this.stmts.countByProject.get(project) as { n: number }).n;\n rows = limit === 0 ? [] : (this.stmts.listByProject.all(project, limit, offset) as MetaRow[]);\n } else if (provider) {\n total = (this.stmts.countByProvider.get(provider) as { n: number }).n;\n rows =\n limit === 0 ? [] : (this.stmts.listByProvider.all(provider, limit, offset) as MetaRow[]);\n } else {\n total = (this.stmts.count.get() as { n: number }).n;\n rows = limit === 0 ? [] : (this.stmts.list.all(limit, offset) as MetaRow[]);\n }\n\n return {\n total,\n conversations: rows.map((r) => ({\n id: r.id,\n filePath: r.file_path,\n projectId: r.project_id,\n projectPath: r.project_path,\n projectName: r.project_name,\n title: r.title,\n model: r.model,\n account: r.account,\n branch: r.branch,\n messageCount: r.message_count,\n lastActivity: r.last_activity\n ? new Date(r.last_activity).toISOString()\n : new Date(0).toISOString(),\n firstMessage: r.first_message,\n lastMessage: r.last_message,\n preview: r.preview,\n source: r.source,\n provider: r.provider ?? CLAUDE_CODE_PROVIDER,\n })),\n };\n }\n\n /** Returns a map of filePath → { mtimeMs, size } for all rows that have\n * stat data stored. Used by the server to build the statCache passed to\n * ConversationScanner.scan() so unchanged files are skipped. */\n getFileStats(): Map<string, { mtimeMs: number; size: number }> {\n const rows = this.stmts.allFileStats.all() as Array<{\n file_path: string;\n mtime_ms: number;\n file_size: number;\n }>;\n const map = new Map<string, { mtimeMs: number; size: number }>();\n for (const r of rows) {\n map.set(r.file_path, { mtimeMs: r.mtime_ms, size: r.file_size });\n }\n return map;\n }\n\n getScannerStatCache(): Map<string, { stat: FileStatEntry; meta: ConversationMeta }> {\n const rows = this.stmts.allScannerStatCacheRows.all() as Array<{\n file_path: string;\n mtime_ms: number;\n file_size: number;\n scanner_meta_json: string;\n }>;\n const map = new Map<string, { stat: FileStatEntry; meta: ConversationMeta }>();\n for (const r of rows) {\n try {\n const meta = JSON.parse(r.scanner_meta_json) as ConversationMeta;\n map.set(r.file_path, {\n stat: { mtimeMs: r.mtime_ms, size: r.file_size },\n meta,\n });\n } catch {\n // Ignore malformed legacy/cache rows; the scanner will parse the file.\n }\n }\n return map;\n }\n\n getMetaById(id: string): ConversationListItem | null {\n const row = this.stmts.getFullById.get(id) as MetaRow | undefined;\n if (!row) return null;\n return {\n id: row.id,\n filePath: row.file_path,\n projectId: row.project_id,\n projectPath: row.project_path,\n projectName: row.project_name,\n title: row.title,\n model: row.model,\n account: row.account,\n branch: row.branch,\n messageCount: row.message_count,\n lastActivity: row.last_activity\n ? new Date(row.last_activity).toISOString()\n : new Date(0).toISOString(),\n firstMessage: row.first_message,\n lastMessage: row.last_message,\n preview: row.preview,\n source: row.source,\n provider: row.provider ?? CLAUDE_CODE_PROVIDER,\n };\n }\n\n setConversationProjectId(conversationId: string, projectId: string): void {\n this.stmts.setConversationProjectId.run(projectId, conversationId);\n }\n\n markAsStreamer(id: string): void {\n this.stmts.markAsStreamer.run(id);\n }\n\n getLatestConversation(): { id: string; lastActivity: string | null } | null {\n const row = this.stmts.getLatestConversation.get() as\n | { id: string; last_activity: number | null }\n | undefined;\n if (!row) return null;\n return {\n id: row.id,\n lastActivity: row.last_activity ? new Date(row.last_activity).toISOString() : null,\n };\n }\n\n hasOrphanProjectId(): boolean {\n return this.stmts.hasOrphanProjectId.get() !== undefined;\n }\n\n listConversationsForProjectBackfill(): Array<{\n id: string;\n projectPath: string | null;\n projectId: string | null;\n lastActivity: string | null;\n }> {\n const rows = this.stmts.listConversationsForProjectBackfill.all() as Array<{\n id: string;\n project_path: string | null;\n project_id: string | null;\n last_activity: number | null;\n }>;\n return rows.map((r) => ({\n id: r.id,\n projectPath: r.project_path,\n projectId: r.project_id,\n lastActivity: r.last_activity ? new Date(r.last_activity).toISOString() : null,\n }));\n }\n\n getConversationTail(id: string): CachedTail | null {\n const row = this.stmts.getTail.get(id) as TailRow | undefined;\n if (!row) return null;\n return {\n conversationId: id,\n messages: JSON.parse(row.messages_json) as CachedTailMessage[],\n tailSize: row.tail_size,\n };\n }\n\n hasConversation(id: string): boolean {\n return !!this.stmts.getById.get(id);\n }\n\n upsertSessionName(sessionId: string, name: string): void {\n this.stmts.upsertSessionName.run(sessionId, name, ++this.nameSeq);\n }\n\n getSessionName(sessionId: string): string | null {\n const row = this.stmts.getSessionName.get(sessionId) as { name: string } | undefined;\n return row?.name ?? null;\n }\n\n listSessionNames(): Record<string, string> {\n const rows = this.stmts.listSessionNames.all() as { session_id: string; name: string }[];\n return Object.fromEntries(rows.map((r) => [r.session_id, r.name]));\n }\n\n invalidate(id?: string): void {\n if (id) {\n this.stmts.deleteTailById.run(id);\n this.stmts.deleteById.run(id);\n if (this.fileIndexLoaded) {\n for (const [fp, cid] of this.fileIndex) {\n if (cid === id) {\n this.fileIndex.delete(fp);\n break;\n }\n }\n }\n } else {\n this.stmts.deleteTailAll.run();\n this.stmts.deleteAll.run();\n this.fileIndex.clear();\n }\n }\n\n /**\n * Drop the cached row for a file. Two callers with opposite intent:\n * - a directory-watch \"change\" event (the file was appended to) — pass\n * `skipIfTailed: true`. A cached tail means the row is being actively\n * maintained from the file's real content by the live-tail\n * (updateFromLines) or warm-up path, fresher than any scanner-derived\n * view. Both watchers fire on the same append with no ordering guarantee;\n * without this guard the invalidate can land after the tail write and wipe\n * the just-cached row, flickering the conversation out of\n * /api/conversations on nearly every message (CRITICAL #2). The debounced\n * rescan still re-derives metadata, so skipping the eager drop loses\n * nothing.\n * - a genuine unlink (the file is gone) — leave `skipIfTailed` false so the\n * row is always removed, otherwise a deleted session ghosts in the cache.\n */\n invalidateByFilePath(filePath: string, opts?: { skipIfTailed?: boolean }): string | null {\n const row = this.stmts.getIdByFilePath.get(filePath) as { id: string } | undefined;\n if (!row) return null;\n if (opts?.skipIfTailed && this.stmts.hasTail.get(row.id)) return null;\n this.invalidate(row.id);\n return row.id;\n }\n\n /**\n * Drop rows whose `file_path` no longer exists on disk AND which have no\n * cached tail to fall back to. Rows with a tail are left alone so\n * `handleGetConversation` can still serve the cached tail even when the\n * JSONL has been deleted.\n */\n pruneGhostFiles(exists: (filePath: string) => boolean = existsSync): string[] {\n const rows = this.stmts.allFilePaths.all() as { id: string; file_path: string }[];\n const ghosts: string[] = [];\n const prune = this.db.transaction((ids: string[]) => {\n for (const id of ids) {\n this.stmts.deleteTailById.run(id);\n this.stmts.deleteById.run(id);\n }\n });\n for (const row of rows) {\n if (exists(row.file_path)) continue;\n if (this.stmts.hasTail.get(row.id)) continue;\n ghosts.push(row.id);\n }\n if (ghosts.length > 0) {\n prune(ghosts);\n if (this.fileIndexLoaded) {\n for (const id of ghosts) {\n for (const [fp, cid] of this.fileIndex) {\n if (cid === id) {\n this.fileIndex.delete(fp);\n break;\n }\n }\n }\n }\n }\n return ghosts;\n }\n\n /**\n * Reconcile the cache against the authoritative set of conversation file\n * paths a fresh scan surfaced: drop any cached row whose `file_path` is not\n * in `livePaths` (removed from disk, or now filtered out — e.g. became an\n * agent JSONL). This is the \"removed conversations\" half of a ?refresh=1\n * reconcile; the additions/updates half is upsertFromScannerMeta.\n *\n * Skip semantics depend on whether the file still exists on disk:\n * - File GONE from disk → always removed, tail or not. This matches the old\n * invalidate()+rebuild behavior (a deleted conversation must disappear on\n * refresh) and keeps refresh=1 truthful about removals. NOTE: this is an\n * INTENTIONAL divergence from pruneGhostFiles(), which KEEPS tailed ghosts\n * so their cached history stays viewable on a background prune. refresh=1\n * has the opposite contract (mobile relies on removals being reflected), so\n * do not \"unify\" the two — they serve different purposes.\n * - File STILL on disk but absent from `livePaths` → the CRITICAL #2 race:\n * the scan snapshot predates a just-created (and now live-tailed) file.\n * A tailed row here is actively maintained from real content, so it is\n * kept — dropping it would flicker the active conversation out of\n * /api/conversations. An untailed on-disk row not in the snapshot is a\n * transient scan/discovery gap; it is left alone (not removed) and the\n * next reconcile picks it up, rather than risk removing a real file the\n * scan simply hasn't surfaced yet.\n * Returns the removed IDs.\n */\n reconcileDeletions(\n livePaths: Set<string>,\n opts?: { exists?: (filePath: string) => boolean },\n ): string[] {\n const exists = opts?.exists ?? existsSync;\n const rows = this.stmts.allFilePaths.all() as { id: string; file_path: string }[];\n const removed: string[] = [];\n const drop = this.db.transaction((ids: string[]) => {\n for (const id of ids) {\n this.stmts.deleteTailById.run(id);\n this.stmts.deleteById.run(id);\n }\n });\n for (const row of rows) {\n if (livePaths.has(row.file_path)) continue;\n // Not in the scan snapshot. If the file is gone from disk, it's a genuine\n // deletion — remove it. If it still exists, this is a scan/discovery gap\n // (the CRITICAL #2 race for live files); leave it for the next reconcile.\n if (exists(row.file_path)) continue;\n removed.push(row.id);\n }\n if (removed.length > 0) {\n drop(removed);\n if (this.fileIndexLoaded) {\n for (const id of removed) {\n for (const [fp, cid] of this.fileIndex) {\n if (cid === id) {\n this.fileIndex.delete(fp);\n break;\n }\n }\n }\n }\n }\n return removed;\n }\n}\n","import type Database from \"better-sqlite3\";\nimport { readdirSync, readFileSync } from \"fs\";\nimport { dirname, join } from \"path\";\nimport { fileURLToPath } from \"url\";\n\nfunction getMigrationsDir(): string {\n if (typeof import.meta !== \"undefined\" && import.meta.url) {\n return dirname(fileURLToPath(import.meta.url));\n }\n return __dirname;\n}\n\nconst SCHEMA_MIGRATIONS_SQL = `\nCREATE TABLE IF NOT EXISTS schema_migrations (\n id TEXT PRIMARY KEY,\n applied_at TEXT NOT NULL\n);\n`;\n\nexport interface SqliteMigrationRunResult {\n applied: string[];\n skipped: string[];\n}\n\nexport function runSqliteMigrations(\n db: Database.Database,\n migrationsDir?: string,\n): SqliteMigrationRunResult {\n db.exec(SCHEMA_MIGRATIONS_SQL);\n\n const dir = migrationsDir ?? join(getMigrationsDir(), \"migrations\");\n const files = readdirSync(dir)\n .filter((f) => f.endsWith(\".sql\"))\n .sort();\n\n const appliedRows = db.prepare(\"SELECT id FROM schema_migrations\").all() as Array<{ id: string }>;\n const appliedSet = new Set(appliedRows.map((r) => r.id));\n\n const recordApplied = db.prepare(\"INSERT INTO schema_migrations (id, applied_at) VALUES (?, ?)\");\n\n const applied: string[] = [];\n const skipped: string[] = [];\n\n for (const file of files) {\n if (appliedSet.has(file)) {\n skipped.push(file);\n continue;\n }\n const sql = readFileSync(join(dir, file), \"utf-8\");\n const tx = db.transaction(() => {\n db.exec(sql);\n recordApplied.run(file, new Date().toISOString());\n });\n tx();\n applied.push(file);\n }\n\n return { applied, skipped };\n}\n","import { closeSync, openSync, readSync, statSync } from \"fs\";\n\n// Default agent entrypoints. Override via THREADBASE_AGENT_ENTRYPOINTS\n// (comma-separated). Interactive Claude Code emits entrypoint=\"cli\" and is\n// never in this set.\n// - sdk-cli → Claude Agent SDK, claude-mem, hook-spawned automation\n// - claude-vscode → VS Code extension when invoked headlessly (memory\n// summarizers, etc.). Real interactive VS Code sessions\n// also use this value, so toggling via env var lets users\n// keep them visible if they want.\nexport const DEFAULT_AGENT_ENTRYPOINTS: ReadonlySet<string> = new Set([\"sdk-cli\", \"claude-vscode\"]);\n\n// Chunked scan: read 64 KB at a time with early-exit. We look for the first\n// `\"entrypoint\":` occurrence in the file:\n// - matches an agent marker → return true\n// - matches some other entrypoint value (e.g. \"cli\") → return false\n// - never appears → return false\n// The entrypoint is fixed per-conversation, so the first occurrence is\n// authoritative. This keeps both agent and human files fast (typical first\n// hit is within the first chunk), while still tolerating long housekeeping\n// prefixes (observed agent markers as deep as 2.3 MB in 4.5 MB observer files).\nconst CHUNK_BYTES = 64 * 1024;\nconst ENTRYPOINT_PROBE = `\"entrypoint\":`;\n// Overlap consecutive chunks so a marker that straddles the boundary is still\n// found. The longest entrypoint we look for is ~24 chars; 64 bytes overlap is\n// plenty.\nconst CHUNK_OVERLAP = 64;\n\n// Per-file decision cache, keyed by `${filePath}::${sortedEntrypointsKey}` so\n// changing the set invalidates entries. Filled lazily; cleared on restart.\nconst fileDecisionCache = new Map<string, boolean>();\n\nfunction markersFor(entrypoints: ReadonlySet<string>): string[] {\n return [...entrypoints].map((e) => `\"entrypoint\":\"${e}\"`);\n}\n\nfunction cacheKey(filePath: string, entrypoints: ReadonlySet<string>): string {\n return `${filePath}::${[...entrypoints].sort().join(\",\")}`;\n}\n\nexport function isAgentLine(\n line: { entrypoint?: string },\n entrypoints: ReadonlySet<string> = DEFAULT_AGENT_ENTRYPOINTS,\n): boolean {\n return line.entrypoint !== undefined && entrypoints.has(line.entrypoint);\n}\n\nexport function isAgentFile(\n filePath: string,\n entrypoints: ReadonlySet<string> = DEFAULT_AGENT_ENTRYPOINTS,\n): boolean {\n if (entrypoints.size === 0) return false;\n const key = cacheKey(filePath, entrypoints);\n const cached = fileDecisionCache.get(key);\n if (cached !== undefined) return cached;\n\n let fd: number;\n try {\n fd = openSync(filePath, \"r\");\n } catch {\n return false;\n }\n\n try {\n const fileSize = statSync(filePath).size;\n if (fileSize === 0) {\n fileDecisionCache.set(key, false);\n return false;\n }\n\n const markers = markersFor(entrypoints);\n const buf = Buffer.allocUnsafe(CHUNK_BYTES);\n let offset = 0;\n let carry = \"\";\n\n while (offset < fileSize) {\n const toRead = Math.min(CHUNK_BYTES, fileSize - offset);\n const got = readSync(fd, buf, 0, toRead, offset);\n if (got <= 0) break;\n const chunk = carry + buf.toString(\"utf8\", 0, got);\n\n // Agent match wins.\n for (const marker of markers) {\n if (chunk.includes(marker)) {\n fileDecisionCache.set(key, true);\n return true;\n }\n }\n\n // If we see ANY entrypoint field, it's per-conversation and stable —\n // since none of the agent markers matched, this is a non-agent file.\n // Stops 11 MB human JSONLs from being read in full.\n if (chunk.includes(ENTRYPOINT_PROBE)) {\n fileDecisionCache.set(key, false);\n return false;\n }\n\n carry = chunk.slice(-CHUNK_OVERLAP);\n offset += got;\n }\n\n fileDecisionCache.set(key, false);\n return false;\n } catch {\n return false;\n } finally {\n closeSync(fd);\n }\n}\n\n// Comma-separated parser. Empty string → empty set (agents shown in practice\n// even when includeAgents=false, since no entrypoint qualifies).\nexport function parseAgentEntrypointsEnv(raw: string | undefined): ReadonlySet<string> {\n if (raw === undefined) return DEFAULT_AGENT_ENTRYPOINTS;\n const parts = raw\n .split(\",\")\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n return new Set(parts);\n}\n\nexport function clearAgentFileCacheForTests(): void {\n fileDecisionCache.clear();\n}\n","import { createHash } from \"crypto\";\nimport type { Stats } from \"fs\";\n\n/**\n * Stable identity for a JSONL file, used to detect replacement (a new file at\n * the same path) vs. mere growth. Prefer the inode (dev:ino) — cheap and exact.\n * When the inode is unavailable (ino === 0, e.g. some network/virtual FS), fall\n * back to a fingerprint of the first bytes supplied by the caller.\n */\nexport function fileIdentity(stat: Pick<Stats, \"dev\" | \"ino\">, headBytes?: Buffer): string {\n if (stat.ino && stat.ino > 0) return `inode:${stat.dev}:${stat.ino}`;\n const head = headBytes ?? Buffer.alloc(0);\n return `fp:${createHash(\"sha1\").update(head).digest(\"hex\")}`;\n}\n\nexport interface LineSpan {\n /** Absolute byte offset of the line's first byte in the file. */\n byteOffset: number;\n /** Byte length of the line content, excluding the trailing \"\\n\". */\n byteLength: number;\n /** The decoded line text (no trailing \"\\n\"). */\n text: string;\n}\n\nexport interface SplitResult {\n spans: LineSpan[];\n /**\n * Bytes consumed = up to and including the last \"\\n\" in `buf`. A trailing\n * partial line (no newline yet) is NOT consumed, so the caller's byte offset\n * never advances past an incomplete line. Those bytes arrive again, whole, on\n * the next read.\n */\n consumed: number;\n}\n\n/**\n * Split a buffer of appended bytes into complete-line spans with absolute byte\n * offsets. `baseOffset` is the file position of `buf`'s first byte.\n *\n * Only lines terminated by \"\\n\" produce a span; the remainder after the final\n * newline is left for the next read (torn-write safety, design §7.5 #4). Empty\n * lines (a bare \"\\n\") produce no span but are still consumed.\n */\nexport function splitCompleteLines(buf: Buffer, baseOffset: number): SplitResult {\n const spans: LineSpan[] = [];\n let lineStart = 0; // index within buf where the current line begins\n let consumed = 0;\n\n for (let i = 0; i < buf.length; i++) {\n if (buf[i] !== 0x0a) continue; // \"\\n\"\n const lineLen = i - lineStart; // excludes the newline\n if (lineLen > 0) {\n spans.push({\n byteOffset: baseOffset + lineStart,\n byteLength: lineLen,\n text: buf.toString(\"utf-8\", lineStart, i),\n });\n }\n lineStart = i + 1;\n consumed = lineStart; // up to and including this \"\\n\"\n }\n\n return { spans, consumed };\n}\n","import type Database from \"better-sqlite3\";\n\nexport type CacheMetadataKey =\n | \"last_conversation_id\"\n | \"last_conversation_created_at\"\n | \"projects_last_indexed_at\"\n | \"conversations_last_indexed_at\"\n | \"conversations_dirty\";\n\nexport class CacheMetadataRepository {\n private get: Database.Statement;\n private upsert: Database.Statement;\n private del: Database.Statement;\n\n constructor(db: Database.Database) {\n this.get = db.prepare(\"SELECT value FROM cache_metadata WHERE key = ?\");\n this.upsert = db.prepare(`\n INSERT INTO cache_metadata (key, value, updated_at)\n VALUES (?, ?, ?)\n ON CONFLICT(key) DO UPDATE SET\n value = excluded.value,\n updated_at = excluded.updated_at\n `);\n this.del = db.prepare(\"DELETE FROM cache_metadata WHERE key = ?\");\n }\n\n getCacheMetadata(key: CacheMetadataKey): string | null {\n const row = this.get.get(key) as { value: string } | undefined;\n return row?.value ?? null;\n }\n\n setCacheMetadata(key: CacheMetadataKey, value: string): void {\n this.upsert.run(key, value, new Date().toISOString());\n }\n\n deleteCacheMetadata(key: CacheMetadataKey): void {\n this.del.run(key);\n }\n}\n","import type { ConversationCache } from \"../../conversation-cache\";\n\n/**\n * Thin repository wrapper around ConversationCache for the project-id flow.\n * The cache is the source of truth for conversation rows; this just exposes\n * a stable, repo-style API for services that don't want to know about the\n * cache class directly.\n */\nexport class ConversationsRepository {\n constructor(private cache: ConversationCache) {}\n\n updateConversationProjectId(args: { conversationId: string; projectId: string }): void {\n this.cache.setConversationProjectId(args.conversationId, args.projectId);\n }\n\n listConversationsForProjectBackfill() {\n return this.cache.listConversationsForProjectBackfill();\n }\n\n getLatestConversation() {\n return this.cache.getLatestConversation();\n }\n\n hasOrphanRows(): boolean {\n return this.cache.hasOrphanProjectId();\n }\n}\n","import type Database from \"better-sqlite3\";\nimport { randomUUID } from \"crypto\";\nimport type { Project } from \"../../schemas/project.schema\";\nimport { canonicalizeProjectPath } from \"../../utils/canonicalizeProjectPath\";\n\ninterface ProjectRow {\n id: string;\n path: string;\n name: string | null;\n last_conversation_id: string | null;\n last_conversation_created_at: string | null;\n last_indexed_at: string | null;\n latest_message_at: string | null;\n latest_message_id: string | null;\n message_count: number;\n created_at: string;\n updated_at: string;\n}\n\nfunction rowToProject(row: ProjectRow): Project {\n return {\n id: row.id,\n path: row.path,\n name: row.name,\n lastConversationId: row.last_conversation_id,\n lastConversationCreatedAt: row.last_conversation_created_at,\n lastIndexedAt: row.last_indexed_at,\n latestMessageAt: row.latest_message_at,\n latestMessageId: row.latest_message_id,\n messageCount: row.message_count,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n };\n}\n\nexport interface UpsertProjectInput {\n lastConversationId?: string | null;\n lastConversationCreatedAt?: string | null;\n latestMessageAt?: string | null;\n latestMessageId?: string | null;\n name?: string | null;\n}\n\nexport class ProjectsRepository {\n private getByPath: Database.Statement;\n private getById: Database.Statement;\n private listAll: Database.Statement;\n private insert: Database.Statement;\n private update: Database.Statement;\n\n constructor(db: Database.Database) {\n this.getByPath = db.prepare(\"SELECT * FROM projects WHERE path = ?\");\n this.getById = db.prepare(\"SELECT * FROM projects WHERE id = ?\");\n this.listAll = db.prepare(\"SELECT * FROM projects ORDER BY updated_at DESC\");\n this.insert = db.prepare(`\n INSERT INTO projects (\n id, path, name,\n last_conversation_id, last_conversation_created_at, last_indexed_at,\n latest_message_at, latest_message_id, message_count,\n created_at, updated_at\n ) VALUES (\n @id, @path, @name,\n @last_conversation_id, @last_conversation_created_at, @last_indexed_at,\n @latest_message_at, @latest_message_id, @message_count,\n @created_at, @updated_at\n )\n `);\n this.update = db.prepare(`\n UPDATE projects SET\n name = COALESCE(@name, name),\n last_conversation_id = COALESCE(@last_conversation_id, last_conversation_id),\n last_conversation_created_at = COALESCE(@last_conversation_created_at, last_conversation_created_at),\n last_indexed_at = COALESCE(@last_indexed_at, last_indexed_at),\n latest_message_at = COALESCE(@latest_message_at, latest_message_at),\n latest_message_id = COALESCE(@latest_message_id, latest_message_id),\n updated_at = @updated_at\n WHERE id = @id\n `);\n }\n\n getProjectByPath(rawPath: string): Project | null {\n const path = canonicalizeProjectPath(rawPath);\n const row = this.getByPath.get(path) as ProjectRow | undefined;\n return row ? rowToProject(row) : null;\n }\n\n getProjectById(id: string): Project | null {\n const row = this.getById.get(id) as ProjectRow | undefined;\n return row ? rowToProject(row) : null;\n }\n\n listProjects(): Project[] {\n return (this.listAll.all() as ProjectRow[]).map(rowToProject);\n }\n\n /**\n * Insert a project at the given canonical path, or update its metadata\n * if one already exists. Returns the persisted Project row.\n *\n * Idempotent: passing the same path twice returns the same project id.\n */\n upsertProjectByPath(rawPath: string, input: UpsertProjectInput = {}): Project {\n const path = canonicalizeProjectPath(rawPath);\n const now = new Date().toISOString();\n\n const existing = this.getByPath.get(path) as ProjectRow | undefined;\n if (existing) {\n this.update.run({\n id: existing.id,\n name: input.name ?? null,\n last_conversation_id: input.lastConversationId ?? null,\n last_conversation_created_at: input.lastConversationCreatedAt ?? null,\n last_indexed_at: now,\n latest_message_at: input.latestMessageAt ?? null,\n latest_message_id: input.latestMessageId ?? null,\n updated_at: now,\n });\n return rowToProject(this.getById.get(existing.id) as ProjectRow);\n }\n\n const id = randomUUID();\n this.insert.run({\n id,\n path,\n name: input.name ?? deriveNameFromPath(path),\n last_conversation_id: input.lastConversationId ?? null,\n last_conversation_created_at: input.lastConversationCreatedAt ?? null,\n last_indexed_at: now,\n latest_message_at: input.latestMessageAt ?? null,\n latest_message_id: input.latestMessageId ?? null,\n message_count: 0,\n created_at: now,\n updated_at: now,\n });\n return rowToProject(this.getById.get(id) as ProjectRow);\n }\n}\n\nfunction deriveNameFromPath(path: string): string | null {\n const parts = path.split(/[\\\\/]/).filter(Boolean);\n return parts.length > 0 ? parts[parts.length - 1] : null;\n}\n","/**\n * Canonicalize a project path so the same project always dedupes to the\n * same key, regardless of trailing slashes or surrounding whitespace.\n *\n * Rules:\n * - Trim surrounding whitespace\n * - Remove trailing forward or back slashes (one or more)\n *\n * Do NOT lowercase: project paths can be case-sensitive on Linux/macOS\n * and lowercasing them would silently merge two distinct real projects.\n */\nexport function canonicalizeProjectPath(projectPath: string): string {\n return projectPath.trim().replace(/[\\\\/]+$/, \"\");\n}\n","import type { SessionStore } from \"../../session-store\";\n\n/**\n * Sessions live in-memory in SessionStore (Postgres-backed persistence\n * was dropped per the SQLite-only direction). This repository wraps the\n * store with a stable, project-id-aware API for the new services.\n */\nexport class SessionsRepository {\n constructor(private store: SessionStore) {}\n\n updateSessionProjectId(args: { sessionId: string; projectId: string }): void {\n this.store.updateManaged(args.sessionId, { projectId: args.projectId });\n }\n\n listManagedSessions() {\n return this.store.listManaged();\n }\n}\n","import type pg from \"pg\";\n\nexport interface UploadRecord {\n id: string;\n sessionId: string;\n filePath: string;\n originalName: string;\n mimeType: string;\n sizeBytes: number;\n}\n\nexport async function recordUpload(\n pool: pg.Pool | null,\n instanceId: string | null,\n row: UploadRecord,\n): Promise<void> {\n if (!pool) return;\n await pool.query(\n `INSERT INTO session_uploads\n (id, session_id, instance_id, file_path, original_name, mime_type, size_bytes)\n VALUES ($1, $2, $3, $4, $5, $6, $7)`,\n [\n row.id,\n row.sessionId,\n instanceId,\n row.filePath,\n row.originalName,\n row.mimeType,\n row.sizeBytes,\n ],\n );\n}\n","import { readdirSync, statSync } from \"fs\";\nimport type { ServerResponse } from \"http\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\n\nfunction decodeProjectPath(dirName: string): string {\n // Claude encodes project paths by replacing '/' with '-', starting from root.\n // e.g. '-Users-foo-Desktop-dev-myproject' → '/Users/foo/Desktop/dev/myproject'\n return dirName.replace(/-/g, \"/\");\n}\n\nexport function handleListProjects(url: URL, res: ServerResponse): void {\n const limit = Math.max(1, parseInt(url.searchParams.get(\"limit\") ?? \"50\", 10) || 50);\n const offset = Math.max(0, parseInt(url.searchParams.get(\"offset\") ?? \"0\", 10) || 0);\n\n const projectsDir = join(homedir(), \".claude\", \"projects\");\n\n let entries: Array<{ name: string; path: string; dirName: string; mtime: number }>;\n try {\n entries = readdirSync(projectsDir)\n .map((dirName) => {\n const fullPath = join(projectsDir, dirName);\n let mtime = 0;\n try {\n mtime = statSync(fullPath).mtimeMs;\n } catch {\n // ignore stat errors — directory may have been removed\n }\n const path = decodeProjectPath(dirName);\n const name = path.split(\"/\").filter(Boolean).pop() ?? dirName;\n return { name, path, dirName, mtime };\n })\n .sort((a, b) => b.mtime - a.mtime);\n } catch {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ projects: [], total: 0 }));\n return;\n }\n\n const total = entries.length;\n const page = entries\n .slice(offset, offset + limit)\n .map(({ name, path, dirName }) => ({ name, path, dirName }));\n\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ projects: page, total }));\n}\n","import { randomBytes } from \"crypto\";\n\nexport interface PairTokenRecord {\n token: string;\n expiresAt: number;\n used: boolean;\n}\n\nexport interface MintResult {\n token: string;\n expiresAt: number;\n expiresInSeconds: number;\n}\n\nexport interface ConsumeOk {\n ok: true;\n}\n\nexport interface ConsumeErr {\n ok: false;\n reason: \"unknown\" | \"expired\" | \"used\";\n}\n\nexport type ConsumeResult = ConsumeOk | ConsumeErr;\n\nconst DEFAULT_TTL_SECONDS = 180;\nconst SWEEP_INTERVAL_MS = 60_000;\n\nexport class PairTokenStore {\n private current: PairTokenRecord | null = null;\n private readonly ttlMs: number;\n private sweepTimer: ReturnType<typeof setInterval> | null = null;\n\n constructor(opts: { ttlSeconds?: number; autoSweep?: boolean } = {}) {\n this.ttlMs = (opts.ttlSeconds ?? DEFAULT_TTL_SECONDS) * 1000;\n if (opts.autoSweep !== false) {\n this.sweepTimer = setInterval(() => this.sweep(), SWEEP_INTERVAL_MS);\n this.sweepTimer.unref?.();\n }\n }\n\n mint(): MintResult {\n const token = `pt_${randomBytes(16).toString(\"hex\")}`;\n const expiresAt = Date.now() + this.ttlMs;\n this.current = { token, expiresAt, used: false };\n return {\n token,\n expiresAt,\n expiresInSeconds: Math.floor(this.ttlMs / 1000),\n };\n }\n\n consume(token: string): ConsumeResult {\n const record = this.current;\n if (!record || record.token !== token) return { ok: false, reason: \"unknown\" };\n if (Date.now() > record.expiresAt) {\n this.current = null;\n return { ok: false, reason: \"expired\" };\n }\n if (record.used) return { ok: false, reason: \"used\" };\n record.used = true;\n return { ok: true };\n }\n\n peek(): PairTokenRecord | null {\n return this.current;\n }\n\n clear(): void {\n this.current = null;\n }\n\n sweep(): void {\n if (this.current && Date.now() > this.current.expiresAt) {\n this.current = null;\n }\n }\n\n dispose(): void {\n if (this.sweepTimer) clearInterval(this.sweepTimer);\n this.sweepTimer = null;\n this.current = null;\n }\n}\n","import nacl from \"tweetnacl\";\nimport naclUtil from \"tweetnacl-util\";\n\nexport interface SealedPayload {\n ciphertext: string;\n nonce: string;\n ephemeralPublicKey: string;\n}\n\n/**\n * Encrypts a plaintext to a recipient's X25519 public key using NaCl box with\n * an ephemeral sender keypair. The phone holds the recipient private key in\n * its own memory and decrypts with nacl.box.open(ciphertext, nonce, ephemeralPublicKey, recipientPrivateKey).\n *\n * The wire format is custom (not libsodium crypto_box_seal) but cryptographically equivalent.\n */\nexport function seal(plaintext: string, recipientPublicKeyBase64: string): SealedPayload {\n const recipientPk = naclUtil.decodeBase64(recipientPublicKeyBase64);\n if (recipientPk.length !== nacl.box.publicKeyLength) {\n throw new Error(\n `clientPublicKey must be ${nacl.box.publicKeyLength} bytes (got ${recipientPk.length})`,\n );\n }\n const ephemeral = nacl.box.keyPair();\n const nonce = nacl.randomBytes(nacl.box.nonceLength);\n const message = naclUtil.decodeUTF8(plaintext);\n const cipher = nacl.box(message, nonce, recipientPk, ephemeral.secretKey);\n return {\n ciphertext: naclUtil.encodeBase64(cipher),\n nonce: naclUtil.encodeBase64(nonce),\n ephemeralPublicKey: naclUtil.encodeBase64(ephemeral.publicKey),\n };\n}\n","import chokidar, { type FSWatcher } from \"chokidar\";\nimport { statSync } from \"fs\";\nimport { open, stat } from \"fs/promises\";\nimport { type LineSpan, splitCompleteLines } from \"../../utils/fileIdentity\";\n\nexport interface ConversationWatcherEvents {\n /** Fires once per new newline-terminated line appended to a watched file. */\n onNewLine?: (filePath: string, line: string) => void;\n /**\n * Fires once per chokidar read with ALL new lines from that read, batched.\n * When set, it REPLACES the per-line onNewLine dispatch for that file —\n * callers pick one. Lets a burst of appended lines collapse into a single\n * downstream cache write + WebSocket broadcast.\n */\n onNewLines?: (filePath: string, lines: string[]) => void;\n /**\n * Like onNewLines but also carries each line's absolute byte span in the\n * file (for the offset index). Fires ALONGSIDE onNewLines/onNewLine (it does\n * not replace them) so the cache tail write and the index extend can both\n * consume the same read. `readFrom` is the absolute byte offset the read\n * started at; `spans` are complete lines only (a torn trailing line is held\n * for the next read).\n */\n onNewLineSpans?: (\n filePath: string,\n spans: LineSpan[],\n readFrom: number,\n endOffset: number,\n ) => void;\n /** Fires when chokidar reports an add/change/unlink at the directory level. */\n onConversationChanged?: (filePath: string) => void | Promise<void>;\n /** Fires when a tailed file is deleted (per-file watcher unlink event). */\n onFileDeleted?: (filePath: string) => void;\n /** Reported errors per file. */\n onError?: (filePath: string, error: Error) => void;\n}\n\ninterface WatchedFile {\n watcher: FSWatcher;\n offset: number;\n // Async-read re-entrancy guard: `reading` is set while a readNewLines is in\n // flight; `pending` records that a change arrived during that read so the\n // in-flight loop re-runs once more after it finishes.\n reading: boolean;\n pending: boolean;\n}\n\n/**\n * Chokidar-backed replacement for src/file-watcher.ts.\n *\n * - watch(filePath) → tail a single JSONL file, emitting onNewLine\n * for each appended line.\n * - watchDirectory(dir) → mark cache dirty on add/change/unlink events\n * for any file inside the directory.\n *\n * Per the refactor plan, file watching is an OPTIMIZATION; correctness\n * still relies on refresh=1 / the latest HDD conversation id check.\n */\nexport class ConversationWatcher {\n private files = new Map<string, WatchedFile>();\n private directories = new Map<string, FSWatcher>();\n private onNewLine: ConversationWatcherEvents[\"onNewLine\"];\n private onNewLines: ConversationWatcherEvents[\"onNewLines\"];\n private onNewLineSpans: ConversationWatcherEvents[\"onNewLineSpans\"];\n private onConversationChanged: ConversationWatcherEvents[\"onConversationChanged\"];\n private onFileDeleted: ConversationWatcherEvents[\"onFileDeleted\"];\n private onError: ConversationWatcherEvents[\"onError\"];\n\n constructor(events: ConversationWatcherEvents = {}) {\n this.onNewLine = events.onNewLine;\n this.onNewLines = events.onNewLines;\n this.onNewLineSpans = events.onNewLineSpans;\n this.onConversationChanged = events.onConversationChanged;\n this.onFileDeleted = events.onFileDeleted;\n this.onError = events.onError;\n }\n\n watch(filePath: string): void {\n if (this.files.has(filePath)) return;\n\n let offset: number;\n try {\n offset = statSync(filePath).size;\n } catch {\n offset = 0;\n }\n\n const watcher = chokidar.watch(filePath, {\n ignoreInitial: true,\n awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 25 },\n });\n\n watcher.on(\"change\", () => {\n void this.readNewLines(filePath);\n });\n watcher.on(\"add\", () => {\n void this.readNewLines(filePath);\n });\n watcher.on(\"unlink\", () => this.onFileDeleted?.(filePath));\n watcher.on(\"error\", (err) => {\n const error = err instanceof Error ? err : new Error(String(err));\n this.onError?.(filePath, error);\n });\n\n this.files.set(filePath, { watcher, offset, reading: false, pending: false });\n }\n\n unwatch(filePath: string): void {\n const entry = this.files.get(filePath);\n if (!entry) return;\n void entry.watcher.close();\n this.files.delete(filePath);\n }\n\n /**\n * Re-drive the tail read for a file that's already being tailed. A per-file\n * chokidar handle can die silently (fs.watch stops firing after inode churn)\n * while the coarser directory watcher keeps reporting changes — calling this\n * from the directory-event path makes the tail self-healing. Reads are\n * offset-based and coalesced, so a redundant poke after a normal change\n * event is a cheap stat + no-op. Returns false for untailed paths.\n */\n poke(filePath: string): boolean {\n if (!this.files.has(filePath)) return false;\n void this.readNewLines(filePath);\n return true;\n }\n\n /**\n * Watch a directory of conversation JSONL files. Fires\n * onConversationChanged for any add/change/unlink event so the caller\n * can mark the cache dirty without scanning everything immediately.\n */\n watchDirectory(directory: string): void {\n if (this.directories.has(directory)) return;\n const watcher = chokidar.watch(directory, {\n ignoreInitial: true,\n awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 },\n });\n const fire = (filePath: string) => {\n void Promise.resolve(this.onConversationChanged?.(filePath)).catch(() => {\n // best-effort\n });\n };\n watcher.on(\"add\", fire);\n watcher.on(\"change\", fire);\n watcher.on(\"unlink\", fire);\n watcher.on(\"error\", (err) => {\n const error = err instanceof Error ? err : new Error(String(err));\n this.onError?.(directory, error);\n });\n this.directories.set(directory, watcher);\n }\n\n unwatchDirectory(directory: string): void {\n const watcher = this.directories.get(directory);\n if (!watcher) return;\n void watcher.close();\n this.directories.delete(directory);\n }\n\n dispose(): void {\n for (const [path] of this.files) this.unwatch(path);\n for (const [dir] of this.directories) this.unwatchDirectory(dir);\n }\n\n private async readNewLines(filePath: string): Promise<void> {\n const entry = this.files.get(filePath);\n if (!entry) return;\n\n // Coalesce: if a read is already running, flag that another change arrived;\n // the in-flight loop will pick up the freshly-appended bytes before exiting.\n if (entry.reading) {\n entry.pending = true;\n return;\n }\n entry.reading = true;\n\n try {\n // Loop so bytes appended during a read (or while dispatching) are caught\n // without re-entering — preserves offset correctness under async I/O.\n for (;;) {\n const st = await stat(filePath);\n // size <= offset also covers truncation/rotation, exactly as before.\n if (st.size <= entry.offset) break;\n\n const readFrom = entry.offset;\n const bytesToRead = st.size - readFrom;\n const buf = Buffer.alloc(bytesToRead);\n const fh = await open(filePath, \"r\");\n try {\n await fh.read(buf, 0, bytesToRead, readFrom);\n } finally {\n await fh.close();\n }\n // Split into complete lines only, with absolute byte spans. Advance by\n // `consumed` (up to the last \"\\n\"), NOT `bytesToRead`: a trailing\n // partial line (no \"\\n\" yet) is left unconsumed so `offset` never moves\n // past an unparsed line — it arrives whole on the next read. The file\n // may have grown again mid-read; the next loop iteration's stat catches\n // the rest.\n const { spans, consumed } = splitCompleteLines(buf, readFrom);\n entry.offset = readFrom + consumed;\n\n // The watcher may have been closed while we awaited; don't emit then.\n if (!this.files.has(filePath)) return;\n\n const lines = spans.map((s) => s.text);\n // The spans callback (offset index) fires alongside the text callbacks\n // — they consume the same read, so a burst extends the index and writes\n // the tail in one pass.\n // Pass both the start (readFrom) and the post-read end offset\n // (entry.offset = readFrom + consumed) so the index can enforce\n // contiguity and store the same offset the watcher tracks.\n if (spans.length > 0) {\n this.onNewLineSpans?.(filePath, spans, readFrom, entry.offset);\n }\n if (this.onNewLines) {\n this.onNewLines(filePath, lines);\n } else {\n for (const line of lines) this.onNewLine?.(filePath, line);\n }\n\n if (entry.pending) {\n entry.pending = false;\n continue;\n }\n break;\n }\n } catch (err) {\n this.onError?.(filePath, err instanceof Error ? err : new Error(String(err)));\n } finally {\n entry.reading = false;\n // If a change arrived while this read was in flight but the loop exited\n // before consuming it (e.g. an error broke out of the loop), the pending\n // bytes would otherwise sit unread until the next write. Re-arm once so\n // they're picked up — guarded by files.has so a disposed watcher stays put.\n if (entry.pending && this.files.has(filePath)) {\n entry.pending = false;\n void this.readNewLines(filePath);\n }\n }\n }\n}\n","export interface SearchTargetMatch {\n messageIndex: number;\n uuid: string | null;\n snippet: string;\n /**\n * All matching message indexes, ascending. When any plain-text match exists\n * only text matches are listed (they are the navigable/highlightable ones);\n * otherwise the thinking/tool fallback matches are. Capped at the LAST\n * MAX_MATCH_INDEXES entries — navigation starts at the tail-most match.\n */\n matchIndexes: number[];\n /** Uncapped true match count (the \"of M\" in the client counter). */\n totalMatches: number;\n}\n\n// Structural view of the scanner's parsed message — mirrors the fields\n// handleGetConversation already reads off it, so no scanner import is needed.\nexport interface SearchableMessage {\n text?: string;\n uuid?: string | null;\n isThinking?: boolean;\n thinkingContent?: string;\n metadata?: {\n toolUseBlocks?: Array<{ input?: unknown }>;\n toolResults?: Array<{ content?: unknown }>;\n };\n}\n\nconst SNIPPET_CONTEXT = 60;\nconst MAX_MATCH_INDEXES = 1000;\n\nfunction buildSnippet(source: string, matchStart: number, matchLength: number): string {\n const start = Math.max(0, matchStart - SNIPPET_CONTEXT);\n const end = Math.min(source.length, matchStart + matchLength + SNIPPET_CONTEXT);\n const prefix = start > 0 ? \"…\" : \"\";\n const suffix = end < source.length ? \"…\" : \"\";\n return `${prefix}${source.slice(start, end).replace(/\\s+/g, \" \").trim()}${suffix}`;\n}\n\n// Thinking and tool payloads are searchable so a hit inside a tool card can\n// still anchor the scroll, even though mobile only highlights plain text.\nfunction extendedBody(m: SearchableMessage): string {\n const parts: string[] = [];\n if (m.isThinking && m.thinkingContent) parts.push(m.thinkingContent);\n for (const b of m.metadata?.toolUseBlocks ?? []) {\n if (b.input !== undefined) parts.push(JSON.stringify(b.input));\n }\n for (const r of m.metadata?.toolResults ?? []) {\n if (r.content !== undefined) parts.push(JSON.stringify(r.content));\n }\n return parts.join(\"\\n\");\n}\n\nfunction collectMatches(\n messages: SearchableMessage[],\n needle: string,\n body: (m: SearchableMessage) => string,\n): number[] {\n const indexes: number[] = [];\n for (let i = 0; i < messages.length; i++) {\n if (body(messages[i]).toLowerCase().includes(needle)) indexes.push(i);\n }\n return indexes;\n}\n\n// Resolves a search hit to its matching messages: the anchor is the LAST\n// message chronologically whose body contains the query (case-insensitive\n// literal substring — no tokenization, and no regex built from user input).\n// Text matches are preferred over thinking/tool matches because only text is\n// visually highlightable on mobile.\nexport function findSearchTarget(\n messages: SearchableMessage[],\n query: string,\n): SearchTargetMatch | null {\n const needle = query.toLowerCase();\n\n let body = (m: SearchableMessage) => m.text ?? \"\";\n let matches = collectMatches(messages, needle, body);\n if (matches.length === 0) {\n body = extendedBody;\n matches = collectMatches(messages, needle, body);\n }\n if (matches.length === 0) return null;\n\n const anchorIndex = matches[matches.length - 1];\n const anchorBody = body(messages[anchorIndex]);\n const at = anchorBody.toLowerCase().indexOf(needle);\n return {\n messageIndex: anchorIndex,\n uuid: messages[anchorIndex].uuid ?? null,\n snippet: buildSnippet(anchorBody, at, query.length),\n matchIndexes: matches.slice(-MAX_MATCH_INDEXES),\n totalMatches: matches.length,\n };\n}\n","import { existsSync } from \"fs\";\nimport type { ConversationCache } from \"../../conversation-cache\";\n\nexport interface PruneAgentConversationsResult {\n scanned: number;\n pruned: number;\n missing: number;\n}\n\n// Walks conversation_meta once at startup and deletes any row whose JSONL has\n// the sdk-cli marker. Idempotent. Safe to call on every boot — the second run\n// will simply find nothing to delete.\nexport function pruneAgentConversations(cache: ConversationCache): PruneAgentConversationsResult {\n const db = cache.getDatabase();\n const rows = db.prepare(\"SELECT id, file_path FROM conversation_meta\").all() as Array<{\n id: string;\n file_path: string;\n }>;\n\n let pruned = 0;\n let missing = 0;\n for (const row of rows) {\n if (!existsSync(row.file_path)) {\n missing += 1;\n continue;\n }\n if (cache.isAgentFileCached(row.file_path)) {\n cache.deleteByFilePath(row.file_path);\n pruned += 1;\n }\n }\n\n return { scanned: rows.length, pruned, missing };\n}\n","/**\n * Resolve a user-visible title for a ProjectChat row.\n *\n * Mobile (ConversationListItem.tsx) renders `title?.trim() || pathSuffix ||\n * ''`, so when title and projectPath are both blank the row appears empty.\n * This helper produces a non-empty title for every input by falling back\n * through title → slug-ish projectName → path suffix → short id.\n */\nexport function deriveProjectChatTitle(input: {\n title: string | null | undefined;\n projectName: string | null | undefined;\n projectPath: string | null | undefined;\n id: string;\n}): string {\n const trimmed = input.title?.trim();\n if (trimmed) return trimmed;\n const name = input.projectName?.trim();\n if (name) return name;\n const pathSuffix = input.projectPath\n ? input.projectPath.split(/[/\\\\]/).filter(Boolean).slice(-2).join(\"/\")\n : \"\";\n if (pathSuffix) return pathSuffix;\n return `Untitled · ${input.id.slice(0, 8)}`;\n}\n","import type { AskOption, AskQuestion } from \"../../types\";\n\ntype ContentBlock = {\n type: string;\n name?: string;\n id?: string;\n input?: unknown;\n [key: string]: unknown;\n};\n\ninterface JsonlLineShape {\n content?: ContentBlock[] | string;\n message?: { content?: ContentBlock[] | string };\n}\n\nfunction normalizeContent(raw: ContentBlock[] | string | null | undefined): ContentBlock[] {\n if (Array.isArray(raw)) return raw;\n if (typeof raw === \"string\") return [{ type: \"text\", text: raw }];\n return [];\n}\n\nfunction coerceOptions(raw: unknown): AskOption[] | null {\n if (!Array.isArray(raw)) return null;\n const out: AskOption[] = [];\n for (const o of raw) {\n if (o && typeof o === \"object\" && typeof (o as AskOption).label === \"string\") {\n const opt = o as { label: string; description?: unknown; preview?: unknown };\n out.push({\n label: opt.label,\n description: typeof opt.description === \"string\" ? opt.description : \"\",\n ...(typeof opt.preview === \"string\" ? { preview: opt.preview } : {}),\n });\n }\n }\n return out.length > 0 ? out : null;\n}\n\nfunction coerceQuestions(raw: unknown): AskQuestion[] | null {\n if (!Array.isArray(raw)) return null;\n const out: AskQuestion[] = [];\n for (const q of raw) {\n if (!q || typeof q !== \"object\") continue;\n const qq = q as {\n question?: unknown;\n header?: unknown;\n multiSelect?: unknown;\n options?: unknown;\n };\n const options = coerceOptions(qq.options);\n if (typeof qq.question !== \"string\" || !options) continue;\n out.push({\n question: qq.question,\n header: typeof qq.header === \"string\" ? qq.header : \"\",\n multiSelect: qq.multiSelect === true,\n options,\n });\n }\n return out.length > 0 ? out : null;\n}\n\nexport function detectAskUserQuestion(\n rawLine: string,\n): { toolUseId: string; questions: AskQuestion[] } | null {\n let parsed: JsonlLineShape;\n try {\n parsed = JSON.parse(rawLine) as JsonlLineShape;\n } catch {\n return null;\n }\n const blocks = normalizeContent(parsed.message?.content ?? parsed.content);\n for (const b of blocks) {\n if (b.type === \"tool_use\" && b.name === \"AskUserQuestion\" && typeof b.id === \"string\") {\n const input = b.input as { questions?: unknown } | undefined;\n const questions = coerceQuestions(input?.questions);\n if (questions) return { toolUseId: b.id, questions };\n }\n }\n return null;\n}\n","import type { AskQuestion, WSMessage } from \"../../types\";\nimport { detectAskUserQuestion } from \"./detectAskUserQuestion\";\n\nexport interface PendingQuestion {\n toolUseId: string;\n questions: AskQuestion[];\n}\n\n// Pure decision: given the new lines for a session, returns the question messages\n// to broadcast and the pending-question records to store. No I/O.\nexport function questionsFromLines(\n sessionId: string,\n lines: string[],\n): {\n messages: Extract<WSMessage, { type: \"question\" }>[];\n pending: PendingQuestion[];\n} {\n const messages: Extract<WSMessage, { type: \"question\" }>[] = [];\n const pending: PendingQuestion[] = [];\n for (const line of lines) {\n const detected = detectAskUserQuestion(line);\n if (detected) {\n messages.push({\n type: \"question\",\n sessionId,\n toolUseId: detected.toolUseId,\n questions: detected.questions,\n });\n pending.push(detected);\n }\n }\n return { messages, pending };\n}\n\n// Pure decision: should the JSONL-flush path (re-)broadcast this question?\n//\n// The live-screen detector may have already broadcast the same question under a\n// synthetic `screen:…` toolUseId. When that JSONL line lands with the REAL\n// toolUseId we must re-broadcast — even though the content is identical — so the\n// client swaps the stale screen id for the real one. Otherwise the client\n// answers with the screen id and resolveAnswer rejects it as tool_use_mismatch.\n//\n// - Not shown before (different/absent content key) → broadcast (new card).\n// - Shown before AND the toolUseId changed → broadcast (re-sync the id).\n// - Shown before AND same toolUseId → suppress (true duplicate).\nexport function shouldBroadcastQuestion(args: {\n newContentKey: string;\n lastContentKey: string | undefined;\n newToolUseId: string;\n priorToolUseId: string | undefined;\n}): boolean {\n const alreadyShown = args.lastContentKey === args.newContentKey;\n if (!alreadyShown) return true;\n return args.priorToolUseId !== args.newToolUseId;\n}\n","import type { AskQuestion } from \"../../types\";\n\nconst DOWN = \"\\x1b[B\";\nconst ENTER = \"\\r\";\n\nexport class UnknownOptionError extends Error {\n constructor(\n public readonly question: string,\n public readonly value: string,\n ) {\n super(`No option labelled \"${value}\" for question \"${question}\"`);\n this.name = \"UnknownOptionError\";\n }\n}\n\n// v1: single-select only. Cursor starts at index 0; N downs + Enter selects index N.\n// Multi-question calls replay each question's block in the order Claude presents them.\nexport function answersToKeystrokes(\n questions: AskQuestion[],\n answers: Record<string, string | string[]>,\n): string {\n let out = \"\";\n for (const q of questions) {\n const raw = answers[q.question];\n if (raw === undefined) {\n throw new Error(`Missing answer for question \"${q.question}\"`);\n }\n const label = Array.isArray(raw) ? raw[0] : raw; // v1 ignores extra (multiSelect is v2)\n const target = q.options.findIndex((o) => o.label === label);\n if (target < 0) throw new UnknownOptionError(q.question, label);\n out += DOWN.repeat(target) + ENTER;\n }\n return out;\n}\n","import type { AskQuestion } from \"../../types\";\nimport { answersToKeystrokes, UnknownOptionError } from \"./answersToKeystrokes\";\n\nexport type AnswerResolution =\n | { ok: true; keys: string }\n | { ok: false; reason: \"no_pending_question\" | \"tool_use_mismatch\" | \"unknown_option\" };\n\nexport function resolveAnswer(\n pending: { toolUseId: string; questions: AskQuestion[] } | undefined,\n body: { toolUseId?: unknown; answers?: unknown },\n): AnswerResolution {\n if (!pending) return { ok: false, reason: \"no_pending_question\" };\n if (typeof body.toolUseId !== \"string\" || body.toolUseId !== pending.toolUseId) {\n return { ok: false, reason: \"tool_use_mismatch\" };\n }\n const answers = (body.answers ?? {}) as Record<string, string | string[]>;\n try {\n return { ok: true, keys: answersToKeystrokes(pending.questions, answers) };\n } catch (e) {\n if (e instanceof UnknownOptionError) return { ok: false, reason: \"unknown_option\" };\n throw e;\n }\n}\n","import { createProgressDedupeLRU } from \"./agent/dedupe\";\nimport { CLAUDE_CODE_PROVIDER } from \"./providers\";\nimport type {\n DiscoveredProcess,\n ManagedSession,\n SessionCursor,\n SessionListPage,\n SessionListQuery,\n SessionResponse,\n SessionSortKey,\n SortOrder,\n} from \"./types\";\n\nexport class SessionStore {\n private managed = new Map<string, ManagedSession>();\n private discovered = new Map<number, DiscoveredProcess>();\n\n addManaged(session: ManagedSession): void {\n this.managed.set(session.id, session);\n }\n\n /**\n * Multi-agent mode only. Attach a dedupe LRU to a session record. Idempotent —\n * calling twice keeps the existing LRU (and its contents).\n */\n initAgentSession(sessionId: string, dedupeCapacity: number): void {\n const session = this.managed.get(sessionId);\n if (!session) return;\n if (session.progressDedupeIds) return;\n session.progressDedupeIds = createProgressDedupeLRU(dedupeCapacity);\n }\n\n updateManaged(sessionId: string, updates: Partial<ManagedSession>): ManagedSession | null {\n const session = this.managed.get(sessionId);\n if (!session) return null;\n Object.assign(session, updates);\n return session;\n }\n\n removeManaged(sessionId: string): boolean {\n return this.managed.delete(sessionId);\n }\n\n getManaged(sessionId: string): ManagedSession | null {\n return this.managed.get(sessionId) ?? null;\n }\n\n setDiscovered(processes: DiscoveredProcess[]): void {\n this.discovered.clear();\n for (const proc of processes) {\n this.discovered.set(proc.pid, proc);\n }\n }\n\n listManaged(): ManagedSession[] {\n return Array.from(this.managed.values());\n }\n\n // Build the session list: live PTY sessions (managed) merged with externally\n // discovered Claude processes. Managed sessions keyed by JSONL UUID take\n // priority — discovered processes with the same UUID are skipped.\n list(ptyAttachedIds: Set<string>): SessionResponse[] {\n const results: SessionResponse[] = [];\n const seenIds = new Set<string>();\n\n for (const s of this.managed.values()) {\n results.push(managedToResponse(s, ptyAttachedIds.has(s.id)));\n seenIds.add(s.id);\n }\n\n for (const d of this.discovered.values()) {\n if (!d.conversationId) continue;\n if (seenIds.has(d.conversationId)) continue;\n results.push(discoveredToResponse(d, d.conversationId));\n seenIds.add(d.conversationId);\n }\n\n return results;\n }\n\n get(sessionId: string, ptyAttachedIds: Set<string>): SessionResponse | null {\n const managed = this.managed.get(sessionId);\n if (managed) return managedToResponse(managed, ptyAttachedIds.has(sessionId));\n\n for (const d of this.discovered.values()) {\n if (d.conversationId === sessionId) return discoveredToResponse(d, sessionId);\n }\n\n return null;\n }\n\n // Paginated, sorted, filtered view over the same merged data list() exposes.\n // The discovery cache can mutate the underlying set between requests, so the\n // cursor encodes both the chosen sort key value and the session id as a\n // tiebreaker. New sessions appearing mid-scan are picked up on the next\n // refetch — see plan for caveats.\n paginate(ptyAttachedIds: Set<string>, query: SessionListQuery): SessionListPage {\n const all = this.list(ptyAttachedIds);\n\n const filtered = query.status?.length\n ? all.filter((s) => query.status?.includes(s.status))\n : all;\n\n const sorted = [...filtered].sort(makeComparator(query.sortBy, query.order));\n\n const total = sorted.length;\n\n const startIdx = query.cursor\n ? findCursorBoundary(sorted, decodeCursor(query.cursor), query.sortBy, query.order)\n : 0;\n const page = sorted.slice(startIdx, startIdx + query.limit);\n const last = page[page.length - 1];\n const nextCursor =\n last && startIdx + page.length < sorted.length\n ? encodeCursor({ k: getSortValue(last, query.sortBy) ?? \"\", id: last.id })\n : null;\n\n return { sessions: page, nextCursor, total };\n }\n}\n\n// Cursor encoding is opaque to clients: base64url(JSON({ k, id })).\nexport function encodeCursor(c: SessionCursor): string {\n return Buffer.from(JSON.stringify(c), \"utf8\").toString(\"base64url\");\n}\n\nexport function decodeCursor(s: string): SessionCursor {\n let parsed: unknown;\n try {\n parsed = JSON.parse(Buffer.from(s, \"base64url\").toString(\"utf8\"));\n } catch {\n throw new Error(\"INVALID_CURSOR\");\n }\n if (\n !parsed ||\n typeof parsed !== \"object\" ||\n typeof (parsed as SessionCursor).id !== \"string\" ||\n ![\"string\", \"number\"].includes(typeof (parsed as SessionCursor).k)\n ) {\n throw new Error(\"INVALID_CURSOR\");\n }\n return parsed as SessionCursor;\n}\n\nfunction getSortValue(s: SessionResponse, key: SessionSortKey): string | number | undefined {\n switch (key) {\n case \"startedAt\":\n return s.startedAt;\n case \"lastActivityAt\":\n return s.lastActivityAt ?? s.startedAt;\n case \"projectName\":\n return s.projectName;\n case \"status\":\n return s.status;\n }\n}\n\nfunction compareValues(a: string | number | undefined, b: string | number | undefined): number {\n // Undefined sorts last regardless of order; callers normalise via getSortValue\n // so this branch only fires for genuinely missing values.\n if (a === undefined && b === undefined) return 0;\n if (a === undefined) return 1;\n if (b === undefined) return -1;\n if (typeof a === \"number\" && typeof b === \"number\") return a - b;\n return String(a).localeCompare(String(b));\n}\n\nfunction makeComparator(key: SessionSortKey, order: SortOrder) {\n const dir = order === \"asc\" ? 1 : -1;\n return (a: SessionResponse, b: SessionResponse): number => {\n const cmp = compareValues(getSortValue(a, key), getSortValue(b, key)) * dir;\n if (cmp !== 0) return cmp;\n // Tiebreaker on id is always ascending so the total order is stable\n // regardless of `order`.\n return a.id.localeCompare(b.id);\n };\n}\n\n// Returns the index of the first element strictly *after* the cursor under the\n// chosen ordering. Linear scan is fine: total session counts are in the\n// hundreds, not millions.\nfunction findCursorBoundary(\n sorted: SessionResponse[],\n cursor: SessionCursor,\n key: SessionSortKey,\n order: SortOrder,\n): number {\n const dir = order === \"asc\" ? 1 : -1;\n for (let i = 0; i < sorted.length; i++) {\n const item = sorted[i];\n const cmp = compareValues(getSortValue(item, key), cursor.k) * dir;\n if (cmp > 0) return i;\n if (cmp === 0 && item.id.localeCompare(cursor.id) > 0) return i;\n }\n return sorted.length;\n}\n\nfunction managedToResponse(s: ManagedSession, ptyAttached: boolean): SessionResponse {\n return {\n id: s.id,\n conversationId: s.id,\n provider: s.provider ?? CLAUDE_CODE_PROVIDER,\n status: s.status,\n projectPath: s.projectPath,\n projectName: s.projectName,\n branch: s.branch,\n lastOutput: s.lastOutput,\n elapsedMs: (s.completedAt ?? new Date()).getTime() - s.startedAt.getTime(),\n promptCount: s.promptCount,\n startedAt: s.startedAt.toISOString(),\n completedAt: s.completedAt?.toISOString() ?? null,\n ptyAttached,\n ...(s.projectId != null && { projectId: s.projectId }),\n ...(s.sessionName != null && { sessionName: s.sessionName }),\n ...(s.model != null && { model: s.model }),\n ...(s.account != null && { account: s.account }),\n ...(s.messageCount != null && { messageCount: s.messageCount }),\n ...(s.preview != null && { preview: s.preview }),\n ...(s.firstMessageText != null && { firstMessageText: s.firstMessageText }),\n ...(s.firstMessageAt != null && { firstMessageAt: s.firstMessageAt.toISOString() }),\n ...(s.lastMessageText != null && { lastMessageText: s.lastMessageText }),\n ...(s.lastMessageAt != null && { lastMessageAt: s.lastMessageAt.toISOString() }),\n ...(s.lastActivityAt != null && { lastActivityAt: s.lastActivityAt.toISOString() }),\n ...(s.filePath != null && { filePath: s.filePath }),\n ...(s.failureReason != null && { failureReason: s.failureReason }),\n ...(s.resumedFromConversationId != null && {\n resumedFromConversationId: s.resumedFromConversationId,\n }),\n ...(s.boundConversationId != null && { boundConversationId: s.boundConversationId }),\n };\n}\n\nfunction discoveredToResponse(d: DiscoveredProcess, conversationId: string): SessionResponse {\n return {\n id: conversationId,\n conversationId,\n provider: CLAUDE_CODE_PROVIDER,\n status: \"idle\",\n projectPath: d.projectPath,\n projectName: d.projectName,\n branch: d.branch,\n lastOutput: \"\",\n elapsedMs: Date.now() - d.startedAt.getTime(),\n promptCount: 0,\n startedAt: d.startedAt.toISOString(),\n completedAt: null,\n ptyAttached: false,\n pid: d.pid,\n };\n}\n","import { randomBytes } from \"crypto\";\nimport { mkdir, writeFile } from \"fs/promises\";\nimport heicConvert from \"heic-convert\";\nimport { join } from \"path\";\n\nconst UPLOAD_DIR_NAME = \".threadbase-uploads\";\nconst MAX_BYTES = 25 * 1024 * 1024; // 25MB\n\nconst HEIC_MIMES = new Set([\"image/heic\", \"image/heif\"]);\n\nconst MIME_TO_EXT: Record<string, string> = {\n \"image/jpeg\": \".jpg\",\n \"image/png\": \".png\",\n \"image/gif\": \".gif\",\n \"image/webp\": \".webp\",\n \"image/heic\": \".jpg\",\n \"image/heif\": \".jpg\",\n \"application/pdf\": \".pdf\",\n \"application/msword\": \".doc\",\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": \".docx\",\n \"text/plain\": \".txt\",\n \"text/javascript\": \".js\",\n \"application/typescript\": \".ts\",\n \"application/json\": \".json\",\n \"text/csv\": \".csv\",\n};\n\nexport interface SaveUploadInput {\n sessionId: string;\n projectPath: string;\n originalName: string;\n mimeType: string;\n dataBase64: string;\n}\n\nexport interface SavedUpload {\n id: string;\n filePath: string;\n originalName: string;\n mimeType: string;\n sizeBytes: number;\n}\n\nexport async function saveUploadFile(input: SaveUploadInput): Promise<SavedUpload> {\n let buffer = Buffer.from(input.dataBase64, \"base64\");\n if (buffer.length === 0) throw new Error(\"Empty file\");\n if (buffer.length > MAX_BYTES) throw new Error(`File exceeds ${MAX_BYTES} bytes`);\n\n let { mimeType } = input;\n let originalName = input.originalName;\n\n if (HEIC_MIMES.has(mimeType)) {\n buffer = Buffer.from(await heicConvert({ buffer, format: \"JPEG\", quality: 0.85 }));\n mimeType = \"image/jpeg\";\n originalName = originalName.replace(/\\.(heic|heif)$/i, \".jpg\");\n }\n\n const id = `up_${randomBytes(8).toString(\"hex\")}`;\n const safeName = sanitizeFilename(originalName) || `file${MIME_TO_EXT[mimeType] ?? \"\"}`;\n const dir = join(input.projectPath, UPLOAD_DIR_NAME, input.sessionId);\n await mkdir(dir, { recursive: true });\n\n const filePath = join(dir, `${Date.now()}-${id}-${safeName}`);\n await writeFile(filePath, buffer);\n\n return {\n id,\n filePath,\n originalName: safeName,\n mimeType,\n sizeBytes: buffer.length,\n };\n}\n\nfunction sanitizeFilename(name: string): string {\n // Take only the basename (block path traversal)\n const base = name.split(/[\\\\/]/).pop() ?? \"\";\n // Strip leading dots; keep all printable Unicode (charCode >= 32, != 127)\n const cleaned = base\n .replace(/^\\.+/, \"\")\n .split(\"\")\n .filter((c) => c.charCodeAt(0) >= 32 && c.charCodeAt(0) !== 127)\n .join(\"\");\n return cleaned;\n}\n","/**\n * Codex rollout JSONL → Claude-shaped conversation lines for WS clients.\n *\n * Mobile's `parseLineToMessage` only understands Claude Code JSONL\n * (`type: \"user\"|\"assistant\"`, `message.role`, text blocks). Codex writes\n * `response_item` / `event_msg` / `session_meta` instead, so raw Codex lines\n * never become live bubbles. This helper normalizes chat-bearing Codex lines\n * into the Claude shape and drops everything else (including the duplicate\n * `event_msg` copies of each turn and `developer` role payloads).\n */\n\ntype CodexContentBlock = {\n type?: string;\n text?: string;\n};\n\nfunction extractCodexText(content: unknown): string {\n if (typeof content === \"string\") return content.trim();\n if (!Array.isArray(content)) return \"\";\n return content\n .map((item) => {\n if (typeof item === \"string\") return item;\n const block = item as CodexContentBlock;\n const t = block?.type;\n if (\n (t === \"input_text\" || t === \"output_text\" || t === \"text\") &&\n typeof block.text === \"string\"\n ) {\n return block.text;\n }\n return \"\";\n })\n .filter(Boolean)\n .join(\"\")\n .trim();\n}\n\n/**\n * Returns a Claude-shaped JSONL line, or null when the input is not a\n * user/assistant chat message that clients should render.\n */\nexport function normalizeCodexLineToClaudeShape(line: string): string | null {\n let entry: {\n type?: string;\n timestamp?: string;\n payload?: {\n type?: string;\n role?: string;\n content?: unknown;\n id?: string;\n };\n };\n try {\n entry = JSON.parse(line);\n } catch {\n return null;\n }\n\n if (entry.type !== \"response_item\") return null;\n const payload = entry.payload;\n if (payload?.type !== \"message\") return null;\n\n const role = payload.role;\n if (role !== \"user\" && role !== \"assistant\") return null;\n\n const text = extractCodexText(payload.content);\n if (!text) return null;\n\n // Synthetic Codex context dumps (AGENTS.md / permissions instructions) are\n // written as `role: user` before any real user turn. Hide them from the live\n // overlay so the chat doesn't open with fake user bubbles.\n if (role === \"user\" && isCodexInjectedContext(text)) return null;\n\n const timestamp =\n typeof entry.timestamp === \"string\" ? entry.timestamp : new Date().toISOString();\n // Prefer payload.id when present; otherwise derive a stable-enough id from\n // timestamp+role+text prefix so WS seenIds can dedupe without colliding\n // across consecutive turns in the same second.\n const uuid =\n typeof payload.id === \"string\" && payload.id.length > 0\n ? payload.id\n : `codex-${role}-${timestamp}-${hashPrefix(text)}`;\n\n return JSON.stringify({\n type: role,\n uuid,\n timestamp,\n message: {\n role,\n content: [{ type: \"text\", text }],\n },\n });\n}\n\n/** True when a Codex rollout line is Codex-shaped (not Claude JSONL). */\nexport function isCodexRolloutLine(line: string): boolean {\n try {\n const entry = JSON.parse(line) as { type?: string };\n return (\n entry.type === \"response_item\" ||\n entry.type === \"event_msg\" ||\n entry.type === \"session_meta\" ||\n entry.type === \"turn_context\"\n );\n } catch {\n return false;\n }\n}\n\n/**\n * Map a batch of raw JSONL lines to client-facing lines. Codex batches are\n * normalized (and filtered); Claude batches pass through unchanged so seq\n * alignment is preserved.\n */\nexport function toClientConversationLines(lines: string[]): string[] {\n if (lines.length === 0) return lines;\n // Heuristic: if any line in the batch is Codex-shaped, treat the whole batch\n // as Codex (a mixed batch shouldn't happen — one file, one provider).\n const codex = lines.some(isCodexRolloutLine);\n if (!codex) return lines;\n const out: string[] = [];\n for (const line of lines) {\n const normalized = normalizeCodexLineToClaudeShape(line);\n if (normalized) out.push(normalized);\n }\n return out;\n}\n\nexport function isCodexInjectedContext(text: string): boolean {\n // AGENTS.md / instruction dumps Codex prepends as the first \"user\" turn.\n if (text.startsWith(\"# AGENTS.md\") || text.includes(\"<INSTRUCTIONS>\")) return true;\n // Permissions / sandbox preamble sometimes arrives as a giant user-role blob.\n if (\n text.startsWith(\"<permissions instructions>\") ||\n text.includes(\"Filesystem sandboxing defines\")\n ) {\n return true;\n }\n // Streamer-injected prompts passed as Codex CLI argv (no --system-prompt flag).\n // DEFAULT_SYSTEM_PROMPT + BROWSE_SYSTEM_PROMPT land as role:user in the rollout.\n if (text.includes(\"limit the options to at most 3\")) return true;\n if (text.includes(\"You are working within the project boundary:\")) return true;\n if (\n text.includes(\n \"Do not read, write, or execute commands that access files or directories outside this boundary\",\n )\n ) {\n return true;\n }\n return false;\n}\n\nfunction hashPrefix(text: string): string {\n // Short non-crypto fingerprint for id uniqueness within a session.\n let h = 0;\n const sample = text.slice(0, 64);\n for (let i = 0; i < sample.length; i++) {\n h = (h * 31 + sample.charCodeAt(i)) | 0;\n }\n return Math.abs(h).toString(36);\n}\n","import { createHash } from \"node:crypto\";\n\n/**\n * Inputs that uniquely identify a conversation's current state for the\n * purpose of a conditional fetch. These all live on the parsed `Conversation`\n * and must be read AFTER `findConversationByUuid`'s staleness refresh — never\n * from a pre-refresh snapshot — so the validator reflects the same state the\n * response body would.\n */\nexport interface ConversationEtagInput {\n filePath: string;\n messageCount: number;\n timestamp: string;\n}\n\n/**\n * Derive a stable, opaque ETag for a conversation. The client never parses it;\n * it only echoes the value back via `If-None-Match`. Wrapping the inputs in a\n * hash keeps the formula changeable without the client caring.\n */\nexport function computeConversationEtag({\n filePath,\n messageCount,\n timestamp,\n}: ConversationEtagInput): string {\n const digest = createHash(\"sha1\")\n .update(`${filePath}:${messageCount}:${timestamp}`)\n .digest(\"hex\")\n .slice(0, 16);\n return `\"${digest}\"`;\n}\n","import { compareDesc, isValid, parseISO } from \"date-fns\";\n\n/**\n * Parse an ISO timestamp string. Returns null for null/undefined input or\n * an unparseable value rather than throwing — callers can fall back.\n */\nexport function parseIsoDateOrNull(value?: string | null): Date | null {\n if (!value) return null;\n const parsed = parseISO(value);\n return isValid(parsed) ? parsed : null;\n}\n\n/**\n * Compare two ISO timestamp strings descending. Null/undefined values sort\n * last regardless of order. Used for ProjectChat list ordering and latest\n * conversation selection.\n */\nexport function compareIsoDesc(a?: string | null, b?: string | null): number {\n const dateA = parseIsoDateOrNull(a);\n const dateB = parseIsoDateOrNull(b);\n\n if (!dateA && !dateB) return 0;\n if (!dateA) return 1;\n if (!dateB) return -1;\n\n return compareDesc(dateA, dateB);\n}\n","import { parseIsoDateOrNull } from \"./dates\";\n\n// The on-disk mtime can run slightly ahead of the last message's own timestamp\n// (filesystem flush latency, non-message trailer lines like pr-link/mode that\n// the scanner ignores for `timestamp` but still bump mtime). Require the file\n// to be at least this much newer than the snapshot before forcing a re-scan, so\n// a single read doesn't churn the whole scanner index on every detail fetch.\nconst STALENESS_TOLERANCE_MS = 1000;\n\n/**\n * Decide whether a scanned conversation snapshot is stale relative to the\n * JSONL on disk. The scanner memoizes both its metadata index and parsed\n * conversations for the server's lifetime, so a conversation that grows after\n * the initial scan keeps serving the startup snapshot from\n * `/api/conversations/{id}`. We compare the file's mtime against the\n * snapshot's last-activity timestamp to detect that drift.\n *\n * Returns true when the file is meaningfully newer than the snapshot (a\n * re-scan is warranted). Returns false when the snapshot is current, when the\n * snapshot timestamp is unparseable (nothing to compare against — leave it),\n * or when mtime is null (stat failed — don't churn the index on a transient\n * read error).\n */\nexport function isScannedSnapshotStale(\n snapshotTimestamp: string | null | undefined,\n fileMtimeMs: number | null,\n): boolean {\n if (fileMtimeMs == null) return false;\n const snapshotDate = parseIsoDateOrNull(snapshotTimestamp);\n if (!snapshotDate) return false;\n return fileMtimeMs - snapshotDate.getTime() > STALENESS_TOLERANCE_MS;\n}\n","/**\n * The scanner fires onProgress once per file, so a large scan would otherwise\n * broadcast thousands of scan_progress WebSocket frames. This throttle collapses\n * them to at most one per whole-percent step, plus a guaranteed final frame when\n * the scan completes (scanned === total). Caps frames at ~101 regardless of the\n * file count, while always delivering the terminal 100% update.\n *\n * Returns a predicate: call it with each (scanned, total) tick; it returns true\n * when that tick should be broadcast. Stateful — create one per scan.\n */\nexport function createScanProgressThrottle(): (scanned: number, total: number) => boolean {\n let lastPercent = -1;\n return (scanned: number, total: number): boolean => {\n // total === 0 (empty scan): emit once so clients see a terminal tick.\n if (total <= 0) {\n if (lastPercent === 100) return false;\n lastPercent = 100;\n return true;\n }\n const isFinal = scanned >= total;\n const percent = Math.floor((scanned / total) * 100);\n if (isFinal) {\n // Always let the final tick through, even if its percent already fired.\n lastPercent = 100;\n return true;\n }\n if (percent === lastPercent) return false;\n lastPercent = percent;\n return true;\n };\n}\n","import type { WebSocket } from \"ws\";\nimport type { WSMessage } from \"./types\";\n\nconst PING_INTERVAL_MS = 30_000;\n// How long to wait for a pong before treating the socket as dead.\n// Must be less than PING_INTERVAL_MS.\nconst PONG_TIMEOUT_MS = 10_000;\n\nexport class WSHub {\n private clients = new Set<WebSocket>();\n private pingTimer: ReturnType<typeof setInterval> | null = null;\n // Per-socket pong-timeout handle; set when ping is sent, cleared on pong/close.\n private pongTimers = new Map<WebSocket, ReturnType<typeof setTimeout>>();\n\n addClient(ws: WebSocket): void {\n this.clients.add(ws);\n\n ws.on(\"pong\", () => {\n const t = this.pongTimers.get(ws);\n if (t) {\n clearTimeout(t);\n this.pongTimers.delete(ws);\n }\n });\n\n ws.on(\"close\", () => {\n const t = this.pongTimers.get(ws);\n if (t) {\n clearTimeout(t);\n this.pongTimers.delete(ws);\n }\n this.clients.delete(ws);\n });\n\n ws.on(\"error\", () => {\n const t = this.pongTimers.get(ws);\n if (t) {\n clearTimeout(t);\n this.pongTimers.delete(ws);\n }\n this.clients.delete(ws);\n });\n\n if (!this.pingTimer && this.clients.size > 0) {\n this.startPing();\n }\n }\n\n broadcast(message: WSMessage): void {\n const data = JSON.stringify(message);\n const dead: WebSocket[] = [];\n\n for (const client of this.clients) {\n try {\n if (client.readyState === client.OPEN) {\n client.send(data);\n } else {\n dead.push(client);\n }\n } catch {\n dead.push(client);\n }\n }\n\n for (const client of dead) {\n this.clients.delete(client);\n }\n }\n\n unicast(ws: WebSocket, message: WSMessage): void {\n try {\n if (ws.readyState === ws.OPEN) {\n ws.send(JSON.stringify(message));\n }\n } catch {\n this.clients.delete(ws);\n }\n }\n\n get connectionCount(): number {\n return this.clients.size;\n }\n\n dispose(): void {\n if (this.pingTimer) {\n clearInterval(this.pingTimer);\n this.pingTimer = null;\n }\n for (const [, t] of this.pongTimers) {\n clearTimeout(t);\n }\n this.pongTimers.clear();\n for (const client of this.clients) {\n try {\n // terminate() (not close()) so the underlying TCP socket dies\n // immediately. A graceful close() only sends a close frame and waits\n // for the peer's reply — a slow/backgrounded client would keep the\n // connection (and thus the HTTP listener's port) alive until the peer\n // ACKs, which is what stalled shutdown and caused EADDRINUSE on the\n // next deploy.\n client.terminate();\n } catch {\n // Already closed\n }\n }\n this.clients.clear();\n }\n\n private startPing(): void {\n this.pingTimer = setInterval(() => {\n if (this.clients.size === 0 && this.pingTimer) {\n clearInterval(this.pingTimer);\n this.pingTimer = null;\n return;\n }\n for (const client of this.clients) {\n if (client.readyState !== client.OPEN) continue;\n // WS protocol ping — client must reply with a pong frame. If no pong\n // arrives within PONG_TIMEOUT_MS the socket is considered dead and\n // terminated. This is what detects iOS silently killing the TCP\n // connection without delivering a close frame to the JS layer.\n client.ping();\n const t = setTimeout(() => {\n this.pongTimers.delete(client);\n client.terminate();\n }, PONG_TIMEOUT_MS);\n this.pongTimers.set(client, t);\n }\n }, PING_INTERVAL_MS);\n }\n}\n"],"mappings":"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","names":["resolve","join","readFileSync","dirname","join","pool","basename","existsSync","homedir","join","mkdirSync","readFileSync","writeFileSync","homedir","dirname","join","run","resolve","lastNonBlank","existsSync","Terminal","randomUUID","existsSync","basename","OPTION_RE","BOX_ONLY_RE","BOX_ONLY_RE","OPTION_RE","BOX_ONLY_RE","OUTPUT_BUFFER_MAX","INPUT_HISTORY_MAX","PTY_COLS","PTY_ROWS","SCREEN_SCROLLBACK","QUIET_DETECT_MS","SUBMIT_BYTES","digestBytes","pty","loadPty","createScreen","Terminal","toPublicSession","basename","randomUUID","resolve","stripAnsi","existsSync","basename","platform","basename","dirname","platform","basename","resolve","dirname","existsSync","readdirSync","readFileSync","statSync","realpath","homedir","dirname","join","crypto","crypto","log","existsSync","join","join","existsSync","Hono","Hono","Hono","ALREADY_HANDLED","alreadyHandled","Hono","readFileSync","dirname","join","dirname","join","readFileSync","Hono","existsSync","join","Hono","homedir","join","join","homedir","logger","join","existsSync","Hono","timingSafeEqual","Hono","readFileSync","homedir","join","join","homedir","readFileSync","resolve","readRawBody","timingSafeEqual","Hono","Hono","ALREADY_HANDLED","alreadyHandled","Hono","ALREADY_HANDLED","alreadyHandled","Hono","ALREADY_HANDLED","alreadyHandled","Hono","ALREADY_HANDLED","alreadyHandled","Hono","Hono","mkdir","join","closeSync","existsSync","mkdirSync","openSync","readSync","statSync","dirname","readdirSync","readFileSync","dirname","join","fileURLToPath","getMigrationsDir","closeSync","openSync","readSync","statSync","cached","stat","stat","statSync","openSync","readSync","closeSync","cached","mkdirSync","dirname","run","existsSync","randomUUID","randomUUID","pool","readdirSync","statSync","homedir","join","randomBytes","statSync","stat","statSync","stat","existsSync","normalizeContent","ENTER","randomBytes","mkdir","join","createHash","join","homedir","realpath","statSync","dirname","resolve","ALREADY_HANDLED","hostname","scanner","metas","total","adapted","stat","existsSync","readdirSync","filePath","availability","messagesPayload","resp","cached","body","readFileSync"]}
1
+ {"version":3,"sources":["../src/agent/agent-client.ts","../src/agent/agent-config.ts","../src/agent/conversation-writer.ts","../src/agent/dedupe.ts","../src/api/routes/progress.routes.ts","../src/auth.ts","../src/db/config.ts","../src/db/migrations.ts","../src/db/pool.ts","../src/live-session-manager.ts","../src/codex-pty-runner.ts","../src/logger.ts","../src/platform.ts","../src/providers.ts","../src/services/questions/codexGateAnswers.ts","../src/utils/debounce.ts","../src/pty-manager.ts","../src/services/questions/detectPermissionGate.ts","../src/services/questions/detectQuestionFromScreen.ts","../src/services/questions/detectShellPrompt.ts","../src/process-discovery.ts","../src/server.ts","../node_modules/nanoid/index.js","../node_modules/nanoid/url-alphabet/index.js","../src/agent/errors.ts","../src/agent/history-mapper.ts","../src/agent/payload-guard.ts","../src/agent/handle-send-agent-input.ts","../src/agent/handle-start-agent-session.ts","../src/api/app.ts","../src/api/middleware/auth.middleware.ts","../src/api/middleware/cors.middleware.ts","../src/api/middleware/error.middleware.ts","../src/api/routes/browse.routes.ts","../src/api/routes/conversations.routes.ts","../src/api/routes/health.routes.ts","../src/version.ts","../src/api/routes/logs.routes.ts","../src/lifecycle/constants.ts","../src/api/routes/misc.routes.ts","../src/config/update-config.ts","../src/schemas/updateConfig.schema.ts","../src/api/routes/pair.routes.ts","../src/api/routes/projects.routes.ts","../src/api/routes/scanner.routes.ts","../src/api/routes/sessions.routes.ts","../src/api/routes/ws.routes.ts","../src/browse.ts","../src/conversation-cache.ts","../src/db/sqlite-migrate.ts","../src/services/conversations/isAgentConversation.ts","../src/utils/fileIdentity.ts","../src/db/repositories/cacheMetadata.repository.ts","../src/db/repositories/conversations.repository.ts","../src/db/repositories/projects.repository.ts","../src/utils/canonicalizeProjectPath.ts","../src/db/repositories/sessions.repository.ts","../src/db/upload-records.ts","../src/handlers/handleListProjects.ts","../src/pair-store.ts","../src/seal.ts","../src/services/conversations/conversationWatcher.ts","../src/services/conversations/findSearchTarget.ts","../src/services/conversations/pruneAgentConversations.ts","../src/services/projectChats/deriveProjectChatTitle.ts","../src/services/questions/detectAskUserQuestion.ts","../src/services/questions/questionBroadcast.ts","../src/services/questions/answersToKeystrokes.ts","../src/services/questions/resolveAnswer.ts","../src/session-store.ts","../src/uploads.ts","../src/utils/codexConversationLine.ts","../src/utils/conversationEtag.ts","../src/utils/dates.ts","../src/utils/isScannedSnapshotStale.ts","../src/utils/scanProgressThrottle.ts","../src/ws-hub.ts"],"sourcesContent":["// src/agent/agent-client.ts\n//\n// Thin Temporal client wrapper used by tb-streamer in multi-agent mode.\n// Does NOT import workflow code from tb-multi-agent — we identify the\n// workflow and its signals/queries by name. The workflow's wire contract\n// lives in @threadbase-sh/agent-types.\n\nimport type { Client } from \"@temporalio/client\";\nimport type { UserInputSignal } from \"@threadbase-sh/agent-types\";\n\n// `defineSignal` / `defineQuery` only ship in `@temporalio/workflow` (a worker-\n// side package we deliberately do NOT pull into the streamer). The handle\n// methods `.signal(...)` and `.query(...)` accept either a plain string OR a\n// definition object with `{ type, name }`. We construct minimal definition\n// objects here — Temporal matches by name, the rest of the shape is virtual\n// branding.\n//\n// Same identifiers as tb-multi-agent's src/workflows/signals.ts.\nconst userInputSignal = { type: \"signal\", name: \"userInput\" } as unknown as {\n type: \"signal\";\n name: \"userInput\";\n};\nconst stageQuery = { type: \"query\", name: \"stage\" } as unknown as {\n type: \"query\";\n name: \"stage\";\n};\n\nconst ORCHESTRATOR_WORKFLOW_TYPE = \"orchestratorWorkflow\";\n\nexport interface AgentClient {\n startSession(sessionId: string): Promise<string>;\n sendUserInput(sessionId: string, payload: UserInputSignal): Promise<void>;\n endSession(sessionId: string): Promise<void>;\n getSessionStage(sessionId: string): Promise<string>;\n}\n\nexport interface AgentClientOpts {\n temporalClient: Client;\n taskQueue: string;\n}\n\nconst sessionWorkflowId = (sessionId: string): string => `session-${sessionId}`;\n\nexport function createAgentClient({ temporalClient, taskQueue }: AgentClientOpts): AgentClient {\n return {\n async startSession(sessionId: string): Promise<string> {\n const handle = await temporalClient.workflow.start(ORCHESTRATOR_WORKFLOW_TYPE, {\n taskQueue,\n workflowId: sessionWorkflowId(sessionId),\n args: [sessionId],\n workflowIdReusePolicy: \"REJECT_DUPLICATE\",\n } as any);\n return handle.workflowId;\n },\n async sendUserInput(sessionId: string, payload: UserInputSignal): Promise<void> {\n await temporalClient.workflow\n .getHandle(sessionWorkflowId(sessionId))\n .signal(userInputSignal as any, payload);\n },\n async endSession(sessionId: string): Promise<void> {\n await temporalClient.workflow.getHandle(sessionWorkflowId(sessionId)).cancel();\n },\n async getSessionStage(sessionId: string): Promise<string> {\n return temporalClient.workflow\n .getHandle(sessionWorkflowId(sessionId))\n .query(stageQuery as any);\n },\n };\n}\n","// src/agent/agent-config.ts\n//\n// Runtime config for multi-agent mode. Read once at server startup so we don't\n// thread env-var lookups through the rest of the codebase.\n\nexport interface AgentConfig {\n enabled: boolean;\n temporal: {\n address: string;\n namespace: string;\n taskQueue: string;\n };\n webhook: {\n hmacSecret: string;\n timestampSkewSeconds: number;\n };\n dedupe: {\n perSessionCapacity: number;\n };\n payload: {\n limitBytes: number;\n trajectoryLogBytes: number;\n trajectoryLogTurns: number;\n };\n sessionBusyRetryMs: number;\n conversationsDir: string;\n}\n\nconst DEFAULTS = {\n TEMPORAL_ADDRESS: \"localhost:7233\",\n TEMPORAL_NAMESPACE: \"default\",\n TEMPORAL_TASK_QUEUE: \"agent-tasks\",\n PROGRESS_HMAC_SECRET: \"dev-secret-change-me\",\n PROGRESS_WEBHOOK_TIMESTAMP_SKEW_SECONDS: \"300\",\n PROGRESS_DEDUPE_CAPACITY: \"1024\",\n AGENT_PAYLOAD_LIMIT_BYTES: \"1572864\", // 1.5 MB — 75% of Temporal's 2 MB ceiling\n AGENT_TRAJECTORY_LOG_BYTES: \"512000\", // 500 KB\n AGENT_TRAJECTORY_LOG_TURNS: \"20\", // First turn count for trajectory WARN\n AGENT_SESSION_BUSY_RETRY_MS: \"1000\",\n};\n\nfunction isTruthy(v: string | undefined): boolean {\n if (v === undefined) return false;\n return v === \"true\" || v === \"1\" || v === \"yes\" || v === \"on\";\n}\n\nexport function readAgentConfig(env: NodeJS.ProcessEnv = process.env): AgentConfig {\n const enabled = isTruthy(env.MULTI_AGENT_FLOW);\n return {\n enabled,\n temporal: {\n address: env.TEMPORAL_ADDRESS ?? DEFAULTS.TEMPORAL_ADDRESS,\n namespace: env.TEMPORAL_NAMESPACE ?? DEFAULTS.TEMPORAL_NAMESPACE,\n taskQueue: env.TEMPORAL_TASK_QUEUE ?? DEFAULTS.TEMPORAL_TASK_QUEUE,\n },\n webhook: {\n hmacSecret: env.PROGRESS_HMAC_SECRET ?? DEFAULTS.PROGRESS_HMAC_SECRET,\n timestampSkewSeconds: Number(\n env.PROGRESS_WEBHOOK_TIMESTAMP_SKEW_SECONDS ??\n DEFAULTS.PROGRESS_WEBHOOK_TIMESTAMP_SKEW_SECONDS,\n ),\n },\n dedupe: {\n perSessionCapacity: Number(env.PROGRESS_DEDUPE_CAPACITY ?? DEFAULTS.PROGRESS_DEDUPE_CAPACITY),\n },\n payload: {\n limitBytes: Number(env.AGENT_PAYLOAD_LIMIT_BYTES ?? DEFAULTS.AGENT_PAYLOAD_LIMIT_BYTES),\n trajectoryLogBytes: Number(\n env.AGENT_TRAJECTORY_LOG_BYTES ?? DEFAULTS.AGENT_TRAJECTORY_LOG_BYTES,\n ),\n trajectoryLogTurns: Number(\n env.AGENT_TRAJECTORY_LOG_TURNS ?? DEFAULTS.AGENT_TRAJECTORY_LOG_TURNS,\n ),\n },\n sessionBusyRetryMs: Number(\n env.AGENT_SESSION_BUSY_RETRY_MS ?? DEFAULTS.AGENT_SESSION_BUSY_RETRY_MS,\n ),\n // Mirrors ServerConfig.cacheDir's parent — the actual JSONL directory.\n // We read it from env here; the conversation writer takes the resolved\n // value from ServerConfig in Task 9.\n conversationsDir: env.THREADBASE_CONVERSATIONS_DIR ?? \"\",\n };\n}\n","// src/agent/conversation-writer.ts\n//\n// Persists assistant turns to JSONL when the worker's final agent_output for a\n// turn arrives. The existing ConversationCache + ConversationWatcher then\n// ingest the line via the existing watcher pipeline — see spec §6.3.\n\nimport { appendFile, mkdir } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\n\nexport interface AppendArgs {\n sessionId: string;\n turnId: string;\n content: string;\n reviewerOverruled?: boolean;\n}\n\nexport interface ConversationWriter {\n appendAssistantTurn(args: AppendArgs): Promise<void>;\n}\n\nexport function createConversationWriter(opts: { baseDir: string }): ConversationWriter {\n const { baseDir } = opts;\n\n return {\n async appendAssistantTurn(args: AppendArgs): Promise<void> {\n if (!args.content || args.content.length === 0) {\n throw new Error(\"ConversationWriter: refusing to write empty assistant turn\");\n }\n const file = join(baseDir, `${args.sessionId}.jsonl`);\n await mkdir(dirname(file), { recursive: true });\n\n const record = {\n role: \"assistant\" as const,\n turnId: args.turnId,\n content: args.content,\n timestamp: Date.now(),\n ...(args.reviewerOverruled ? { reviewerOverruled: true } : {}),\n };\n\n const line = `${JSON.stringify(record)}\\n`;\n await appendFile(file, line, { encoding: \"utf8\" });\n },\n };\n}\n","// src/agent/dedupe.ts\n//\n// Bounded LRU for per-session progress-event dedupe. Implementation uses the\n// fact that Map iterates in insertion order — re-inserting a key moves it to\n// the end, which is exactly LRU semantics with no extra bookkeeping.\n//\n// Spec §7.1: this is the milestone-B dedupe. The map lives on the session\n// record and dies with the session. Postgres-backed durability is option D,\n// deferred — see tb-multi-agent docs/plans/postgres-dedupe.md.\n\nexport interface ProgressDedupeLRU {\n hasSeen(eventId: string): boolean;\n readonly size: number;\n}\n\nexport function createProgressDedupeLRU(capacity: number): ProgressDedupeLRU {\n if (!Number.isFinite(capacity) || capacity < 1) {\n throw new Error(`dedupe LRU capacity must be >= 1, got ${capacity}`);\n }\n const map = new Map<string, true>();\n\n return {\n hasSeen(eventId: string): boolean {\n if (map.has(eventId)) {\n // Refresh recency: remove + reinsert moves to most-recent position.\n map.delete(eventId);\n map.set(eventId, true);\n return true;\n }\n map.set(eventId, true);\n if (map.size > capacity) {\n // Evict the oldest entry (the first key in insertion order).\n const oldest = map.keys().next().value;\n if (oldest !== undefined) map.delete(oldest);\n }\n return false;\n },\n get size(): number {\n return map.size;\n },\n };\n}\n","// src/api/routes/progress.routes.ts\n//\n// Webhook receiver for worker → tb-streamer progress events.\n//\n// Auth: HMAC over the raw request body, header X-Progress-Signature.\n// Auth bypass: the auth middleware skips this prefix because validation\n// happens inside the handler (mirrors /api/__update).\n//\n// Idempotency: per-session LRU on the ManagedSession record. Duplicates\n// return 200 with deduped:true and do not broadcast.\n\nimport crypto from \"node:crypto\";\nimport type { IncomingMessage } from \"node:http\";\nimport type { AgentOutputPayload, ProgressEvent, Stage } from \"@threadbase-sh/agent-types\";\nimport { Hono } from \"hono\";\nimport type { WSMessage } from \"../../types\";\nimport type { AppEnv } from \"../app\";\nimport type { ApiDeps } from \"../types/api-deps\";\n\nfunction readRawBody(req: IncomingMessage): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk) => chunks.push(chunk));\n req.on(\"end\", () => resolve(Buffer.concat(chunks)));\n req.on(\"error\", reject);\n });\n}\n\ninterface AgentDeps {\n sessionStore: {\n getManaged: (sessionId: string) => {\n id: string;\n progressDedupeIds?: { hasSeen: (id: string) => boolean };\n currentTurnId?: string | null;\n } | null;\n };\n wsHub: { broadcast: (m: WSMessage) => void };\n conversationWriter: {\n appendAssistantTurn: (a: {\n sessionId: string;\n turnId: string;\n content: string;\n reviewerOverruled?: boolean;\n }) => Promise<void>;\n } | null;\n agentConfig: {\n enabled: boolean;\n webhook: { hmacSecret: string; timestampSkewSeconds: number };\n dedupe: { perSessionCapacity: number };\n };\n}\n\nfunction verifySignature(rawBody: Buffer, signature: string, secret: string): boolean {\n if (!signature || signature.length === 0) return false;\n const expected = crypto.createHmac(\"sha256\", secret).update(rawBody).digest(\"hex\");\n if (expected.length !== signature.length) return false;\n try {\n return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));\n } catch {\n return false;\n }\n}\n\nfunction isWithinSkew(timestampHeader: string | undefined, skewSeconds: number): boolean {\n if (!timestampHeader) return true; // header optional in milestone B\n const t = Number(timestampHeader);\n if (!Number.isFinite(t)) return false;\n const now = Math.floor(Date.now() / 1000);\n return Math.abs(now - t) <= skewSeconds;\n}\n\nfunction stageToRole(stage: Stage | string | undefined): \"worker\" | \"reviewer\" | \"signoff\" {\n if (stage === \"review\") return \"reviewer\";\n if (stage === \"sign-off\") return \"signoff\";\n return \"worker\";\n}\n\nexport const createProgressRoutes = (deps: ApiDeps & AgentDeps) => {\n const app = new Hono<AppEnv>();\n\n app.post(\"/sessions/:sessionId/progress\", async (c) => {\n if (!deps.agentConfig.enabled) {\n return c.json({ error: \"multi-agent mode not enabled\" }, 404);\n }\n const sessionId = c.req.param(\"sessionId\");\n const session = deps.sessionStore.getManaged(sessionId);\n if (!session) {\n return c.json({ error: \"unknown session\" }, 404);\n }\n\n // Read raw body from the underlying Node IncomingMessage stream — mirrors\n // /api/__update. Hono's c.req.arrayBuffer() returns empty when the request\n // arrives via @hono/node-server's bindings, leaving HMAC verification with\n // the wrong byte buffer. In tests (app.request), c.env.incoming is absent\n // and arrayBuffer() works fine — fall back to it.\n let rawBuf: Buffer;\n try {\n const incoming = c.env?.incoming;\n rawBuf = incoming ? await readRawBody(incoming) : Buffer.from(await c.req.arrayBuffer());\n } catch {\n return c.json({ error: \"could not read body\" }, 400);\n }\n const sigHeader = c.req.header(\"x-progress-signature\") ?? \"\";\n if (!verifySignature(rawBuf, sigHeader, deps.agentConfig.webhook.hmacSecret)) {\n return c.json({ error: \"unauthorized\" }, 401);\n }\n if (\n !isWithinSkew(\n c.req.header(\"x-progress-timestamp\"),\n deps.agentConfig.webhook.timestampSkewSeconds,\n )\n ) {\n return c.json({ error: \"stale timestamp\" }, 401);\n }\n\n let event: ProgressEvent;\n try {\n event = JSON.parse(rawBuf.toString(\"utf8\")) as ProgressEvent;\n } catch {\n return c.json({ error: \"bad json\" }, 400);\n }\n if (!event.eventId || !event.sessionId || !event.turnId) {\n return c.json({ error: \"missing required fields\" }, 400);\n }\n\n // Dedupe (per spec §7.1). If the session lacks a dedupe map (e.g., it was\n // created in PTY mode and re-used), the receiver still works — every event\n // is treated as new.\n if (session.progressDedupeIds?.hasSeen(event.eventId)) {\n return c.json({ ok: true, deduped: true }, 200);\n }\n\n // ─── Translate to WSMessage and broadcast ───────────────────────────\n if (event.type === \"stage_transition\") {\n const msg: WSMessage = {\n type: \"session_update\",\n sessionId: event.sessionId,\n // turnId disambiguates which turn this stage applies to. Critical\n // for `queued` (identifies the waiting turn) and `rework` (with\n // reworkAttempt). Existing single-turn consumers can ignore it.\n turnId: event.turnId,\n // Existing session_update consumers expect status; we leave it\n // undefined here (stage is the new-only field).\n stage: event.stage,\n reworkAttempt: event.reworkAttempt,\n stalledSinceMs: 0,\n } as WSMessage;\n deps.wsHub.broadcast(msg);\n } else if (event.type === \"agent_output\") {\n const payload = (event.payload ?? {}) as unknown as AgentOutputPayload;\n const msg: WSMessage = {\n type: \"agent_output\",\n sessionId: event.sessionId,\n turnId: event.turnId,\n role: stageToRole(event.stage),\n content: payload.content ?? \"\",\n partial: payload.partial,\n reviewerOverruled: payload.reviewerOverruled,\n stage: event.stage,\n reworkAttempt: event.reworkAttempt,\n } as WSMessage;\n deps.wsHub.broadcast(msg);\n\n // Persist final answer to JSONL.\n if (event.stage === \"done\" && deps.conversationWriter && payload.content) {\n await deps.conversationWriter.appendAssistantTurn({\n sessionId: event.sessionId,\n turnId: event.turnId,\n content: payload.content,\n reviewerOverruled: payload.reviewerOverruled,\n });\n }\n\n // Release the session lock when the turn completes (spec §6).\n if (event.stage === \"done\" && session.currentTurnId === event.turnId) {\n (session as { currentTurnId: string | null }).currentTurnId = null;\n }\n } else if (event.type === \"terminal_failure\") {\n const reason = (event.payload as { reason?: string } | undefined)?.reason ?? \"unknown\";\n const msg: WSMessage = {\n type: \"turn_failure\",\n sessionId: event.sessionId,\n turnId: event.turnId,\n reason,\n } as WSMessage;\n deps.wsHub.broadcast(msg);\n\n // Release the session lock on failure (spec §6).\n if (session.currentTurnId === event.turnId) {\n (session as { currentTurnId: string | null }).currentTurnId = null;\n }\n }\n\n return c.json({ ok: true }, 200);\n });\n\n return app;\n};\n","import { randomBytes, timingSafeEqual } from \"crypto\";\nimport { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\n\n// Resolved per call (not frozen at module load) so tests can redirect writes\n// away from the real config via THREADBASE_CONFIG_DIR. Without this, importing\n// this module freezes the path to the real home before any test can sandbox it,\n// and a rotate/set-key test would clobber the live ~/.threadbase/server.yaml.\nfunction configDir(): string {\n return process.env.THREADBASE_CONFIG_DIR ?? join(homedir(), \".threadbase\");\n}\nfunction configFile(): string {\n return join(configDir(), \"server.yaml\");\n}\n\nexport function generateApiKey(): string {\n return `tb_${randomBytes(16).toString(\"hex\")}`;\n}\n\nexport function validateApiKey(provided: string, expected: string): boolean {\n const a = Buffer.from(provided);\n const b = Buffer.from(expected);\n if (a.length !== b.length) return false;\n return timingSafeEqual(a, b);\n}\n\nexport function loadOrCreateApiKey(): string {\n try {\n const content = readFileSync(configFile(), \"utf-8\");\n const match = content.match(/api_key:\\s*(.+)/);\n if (match?.[1]) return match[1].trim();\n } catch {\n // File doesn't exist, create one\n }\n\n const key = generateApiKey();\n mkdirSync(configDir(), { recursive: true });\n writeFileSync(configFile(), `api_key: ${key}\\n`, \"utf-8\");\n return key;\n}\n\nexport function loadBrowseRoot(): string | undefined {\n try {\n const content = readFileSync(configFile(), \"utf-8\");\n const match = content.match(/browse_root:\\s*(.+)/);\n if (match?.[1]) return match[1].trim();\n } catch {\n // File doesn't exist or not readable\n }\n return undefined;\n}\n\nexport function loadPublicUrl(): string | undefined {\n try {\n const content = readFileSync(configFile(), \"utf-8\");\n const match = content.match(/public_url:\\s*(.+)/);\n if (match?.[1]) return match[1].trim();\n } catch {\n // File doesn't exist or not readable\n }\n return undefined;\n}\n\nexport function loadBrowserCors(): string | undefined {\n try {\n const content = readFileSync(configFile(), \"utf-8\");\n const match = content.match(/browser_cors:\\s*(.+)/);\n if (match?.[1]) return match[1].trim();\n } catch {\n // File doesn't exist or not readable\n }\n return undefined;\n}\n\nexport function loadCacheDir(): string | undefined {\n try {\n const content = readFileSync(configFile(), \"utf-8\");\n const match = content.match(/cache_dir:\\s*(.+)/);\n if (match?.[1]) return match[1].trim();\n } catch {\n // File doesn't exist or not readable\n }\n return undefined;\n}\n\nexport function loadTailSize(): number | undefined {\n try {\n const content = readFileSync(configFile(), \"utf-8\");\n const match = content.match(/tail_size:\\s*(\\d+)/);\n if (match?.[1]) return Number.parseInt(match[1], 10);\n } catch {\n // File doesn't exist or not readable\n }\n return undefined;\n}\n\n// Auto-kill grace period in ms. 0 disables the automatic hold-on-disconnect\n// timer entirely (explicit hold_session still works); a positive value sets the\n// delay; unset falls through to DEFAULT_PTY_GRACE_PERIOD_MS.\nexport function loadPtyGracePeriodMs(): number | undefined {\n try {\n const content = readFileSync(configFile(), \"utf-8\");\n const match = content.match(/pty_grace_period_ms:\\s*(\\d+)/);\n if (match?.[1]) return Number.parseInt(match[1], 10);\n } catch {\n // File doesn't exist or not readable\n }\n return undefined;\n}\n\nexport function loadDefaultPermissionMode(): \"acceptEdits\" | \"manual\" | undefined {\n try {\n const content = readFileSync(configFile(), \"utf-8\");\n const match = content.match(/default_permission_mode:\\s*(\\S+)/);\n const value = match?.[1]?.trim();\n if (value === \"acceptEdits\" || value === \"manual\") return value;\n } catch {\n // File doesn't exist or not readable\n }\n return undefined;\n}\n\n// Persists the first-run prompt's answer so subsequent `serve` invocations\n// don't ask again (loadDefaultPermissionMode() returning a value is the\n// \"already configured\" signal cli/index.ts checks before prompting).\nexport function setDefaultPermissionMode(mode: \"acceptEdits\" | \"manual\"): void {\n const file = configFile();\n mkdirSync(configDir(), { recursive: true });\n\n let content = \"\";\n try {\n content = readFileSync(file, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") throw err;\n // file does not exist; we'll create it\n }\n\n const line = `default_permission_mode: ${mode}`;\n let updated: string;\n if (/^default_permission_mode:\\s*.+$/m.test(content)) {\n updated = content.replace(/^default_permission_mode:\\s*.+$/m, line);\n } else if (content.length === 0 || content.endsWith(\"\\n\")) {\n updated = `${content}${line}\\n`;\n } else {\n updated = `${content}\\n${line}\\n`;\n }\n\n const tmpFile = `${file}.tmp`;\n writeFileSync(tmpFile, updated, { encoding: \"utf-8\", mode: 0o600 });\n chmodSync(tmpFile, 0o600);\n renameSync(tmpFile, file);\n}\n\nexport type PublicUrlValidation = { ok: true; normalized: string } | { ok: false; error: string };\n\nexport function validatePublicUrl(raw: string): PublicUrlValidation {\n let parsed: URL;\n try {\n parsed = new URL(raw);\n } catch {\n return { ok: false, error: `Invalid URL: ${raw}` };\n }\n const localHosts = new Set([\"localhost\", \"127.0.0.1\", \"::1\"]);\n if (parsed.protocol === \"https:\") {\n return { ok: true, normalized: stripTrailingSlash(parsed.toString()) };\n }\n if (parsed.protocol === \"http:\" && localHosts.has(parsed.hostname)) {\n return { ok: true, normalized: stripTrailingSlash(parsed.toString()) };\n }\n return {\n ok: false,\n error: `publicUrl must be https:// (got ${parsed.protocol}//). Plain http is only allowed for localhost.`,\n };\n}\n\nfunction stripTrailingSlash(url: string): string {\n return url.endsWith(\"/\") ? url.slice(0, -1) : url;\n}\n\nexport function setApiKey(key: string): void {\n const file = configFile();\n mkdirSync(configDir(), { recursive: true });\n\n let content = \"\";\n try {\n content = readFileSync(file, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") throw err;\n // file does not exist; we'll create it\n }\n\n const apiKeyLine = `api_key: ${key}`;\n let updated: string;\n if (/^api_key:\\s*.+$/m.test(content)) {\n updated = content.replace(/^api_key:\\s*.+$/m, apiKeyLine);\n } else if (content.length === 0 || content.endsWith(\"\\n\")) {\n updated = `${content}${apiKeyLine}\\n`;\n } else {\n updated = `${content}\\n${apiKeyLine}\\n`;\n }\n\n const tmpFile = `${file}.tmp`;\n writeFileSync(tmpFile, updated, { encoding: \"utf-8\", mode: 0o600 });\n chmodSync(tmpFile, 0o600);\n renameSync(tmpFile, file);\n}\n","export interface DbConfig {\n connectionString: string;\n max: number;\n ssl?: string;\n statementTimeout?: number;\n instanceId: string;\n}\n\nexport function isDbEnabled(): boolean {\n const url = process.env.THREADBASE_DATABASE_URL;\n return typeof url === \"string\" && url.length > 0;\n}\n\nexport function getInstanceId(): string {\n return process.env.THREADBASE_INSTANCE_ID || require(\"os\").hostname();\n}\n\nexport function getDbConfig(): DbConfig | null {\n if (!isDbEnabled()) return null;\n\n const connectionString = process.env.THREADBASE_DATABASE_URL ?? \"\";\n const poolMax = Number.parseInt(process.env.THREADBASE_DATABASE_POOL_MAX ?? \"\", 10);\n const stmtTimeout = Number.parseInt(\n process.env.THREADBASE_DATABASE_STATEMENT_TIMEOUT_MS ?? \"\",\n 10,\n );\n const ssl = process.env.THREADBASE_DATABASE_SSL || undefined;\n\n return {\n connectionString,\n max: Number.isNaN(poolMax) ? 10 : poolMax,\n ssl,\n statementTimeout: Number.isNaN(stmtTimeout) ? undefined : stmtTimeout,\n instanceId: getInstanceId(),\n };\n}\n","import { readdirSync, readFileSync } from \"fs\";\nimport { dirname, join } from \"path\";\nimport type pg from \"pg\";\nimport { fileURLToPath } from \"url\";\n\nfunction getMigrationsDir(): string {\n // ESM: import.meta.url is available\n if (import.meta.url) {\n return dirname(fileURLToPath(import.meta.url));\n }\n // CJS: __dirname is available (injected by tsup)\n return __dirname;\n}\n\nexport async function runMigrations(pool: pg.Pool, migrationsDir?: string): Promise<void> {\n await pool.query(`\n CREATE TABLE IF NOT EXISTS _migrations (\n id SERIAL PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n )\n `);\n\n const { rows: applied } = await pool.query<{ name: string }>(\n \"SELECT name FROM _migrations ORDER BY name\",\n );\n const appliedSet = new Set(applied.map((r) => r.name));\n\n const dir = migrationsDir ?? join(getMigrationsDir(), \"pg-migrations\");\n const files = readdirSync(dir)\n .filter((f) => f.endsWith(\".sql\"))\n .sort();\n\n for (const file of files) {\n if (appliedSet.has(file)) continue;\n\n const sql = readFileSync(join(dir, file), \"utf-8\");\n await pool.query(sql);\n await pool.query(\"INSERT INTO _migrations (name) VALUES ($1)\", [file]);\n }\n}\n","import type { Pool as PoolType } from \"pg\";\nimport type { DbConfig } from \"./config\";\n\nexport function maskConnectionString(url: string): string {\n try {\n const parsed = new URL(url);\n // Check if password field is present (including empty password like \"user:@host\")\n const hasPasswordField =\n parsed.username.length > 0 && url.includes(`${parsed.username}:`) && url.includes(\"@\");\n if (hasPasswordField) {\n parsed.password = \"***\";\n }\n return parsed.toString();\n } catch {\n return \"***masked***\";\n }\n}\n\nexport async function createPool(config: DbConfig): Promise<PoolType> {\n const pg = await import(\"pg\");\n const { Pool } = pg.default ?? pg;\n\n const poolConfig: ConstructorParameters<typeof Pool>[0] = {\n connectionString: config.connectionString,\n max: config.max,\n };\n\n if (config.ssl === \"require\") {\n poolConfig.ssl = { rejectUnauthorized: false };\n } else if (config.ssl === \"disable\") {\n poolConfig.ssl = false;\n }\n\n if (config.statementTimeout) {\n poolConfig.statement_timeout = config.statementTimeout;\n }\n\n return new Pool(poolConfig);\n}\n","import { basename } from \"path\";\nimport { CodexPtyRunner } from \"./codex-pty-runner\";\nimport { CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER, type ProviderName } from \"./providers\";\nimport { PTYManager } from \"./pty-manager\";\nimport type {\n ManagedSession,\n PTYManagerOptions,\n SessionRunner,\n StartFreshSessionOptions,\n StartSessionOptions,\n UserMessage,\n} from \"./types\";\n\nexport class LiveSessionManager {\n private runners: Map<ProviderName, SessionRunner>;\n\n constructor(options: PTYManagerOptions = {}) {\n this.runners = new Map<ProviderName, SessionRunner>([\n [CLAUDE_CODE_PROVIDER, new PTYManager(options)],\n [CODEX_CLI_PROVIDER, new CodexPtyRunner(options)],\n ]);\n }\n\n async start(\n sessionId: string,\n options: StartSessionOptions & { provider?: ProviderName },\n ): Promise<ManagedSession> {\n const provider = options.provider ?? CLAUDE_CODE_PROVIDER;\n const runner = this.assertSupportedProvider(provider, options.projectPath);\n return runner.start(sessionId, options);\n }\n\n async startFresh(\n options: StartFreshSessionOptions & { provider?: ProviderName },\n ): Promise<ManagedSession> {\n const provider = options.provider ?? CLAUDE_CODE_PROVIDER;\n const runner = this.assertSupportedProvider(provider, options.projectPath);\n return runner.startFresh(options);\n }\n\n sendInput(sessionId: string, input: string): number {\n return this.runnerFor(sessionId).sendInput(sessionId, input);\n }\n\n sendKeys(sessionId: string, keys: string): void {\n this.runnerFor(sessionId).sendKeys(sessionId, keys);\n }\n\n cancel(sessionId: string): void {\n this.runnerFor(sessionId).cancel(sessionId);\n }\n\n killPid(pid: number): void {\n for (const runner of this.runners.values()) {\n runner.killPid(pid);\n }\n }\n\n // putOnHold tolerates an unknown sessionId (PTYManager.putOnHold is a no-op\n // when the session isn't in its map), so — unlike the other session-keyed\n // methods — route to the owning runner when found, otherwise broadcast to\n // every runner rather than throwing; this matches the pre-extraction\n // behavior of delegating straight through with no existence check.\n putOnHold(sessionId: string): void {\n for (const runner of this.runners.values()) {\n if (runner.hasSession(sessionId) || runner.getSession(sessionId)) {\n runner.putOnHold(sessionId);\n return;\n }\n }\n for (const runner of this.runners.values()) {\n runner.putOnHold(sessionId);\n }\n }\n\n getOutput(sessionId: string): string {\n return this.runnerFor(sessionId).getOutput(sessionId);\n }\n\n getOutputLines(sessionId: string, maxLines: number): Promise<string[]> {\n return this.runnerFor(sessionId).getOutputLines(sessionId, maxLines);\n }\n\n getInputHistory(sessionId: string): UserMessage[] {\n return this.runnerFor(sessionId).getInputHistory(sessionId);\n }\n\n getSession(sessionId: string): ManagedSession | null {\n for (const runner of this.runners.values()) {\n const session = runner.getSession(sessionId);\n if (session) return session;\n }\n return null;\n }\n\n hasSession(sessionId: string): boolean {\n for (const runner of this.runners.values()) {\n if (runner.hasSession(sessionId)) return true;\n }\n return false;\n }\n\n listSessions(): ManagedSession[] {\n return Array.from(this.runners.values()).flatMap((runner) => runner.listSessions());\n }\n\n dispose(): void {\n for (const runner of this.runners.values()) {\n runner.dispose();\n }\n }\n\n // Look up which runner owns a session. Only one runner exists today, so\n // this is a linear scan across hasSession()/getSession() rather than a\n // separate session→provider index — see task-1-brief.md.\n private runnerFor(sessionId: string): SessionRunner {\n for (const runner of this.runners.values()) {\n if (runner.hasSession(sessionId) || runner.getSession(sessionId)) return runner;\n }\n throw new Error(`Session not found: ${sessionId}`);\n }\n\n private assertSupportedProvider(provider: ProviderName, projectPath: string): SessionRunner {\n const runner = this.runners.get(provider);\n if (runner) return runner;\n const err = new Error(\n `Live ${provider} sessions are not implemented yet for ${basename(projectPath)}`,\n );\n (err as Error & { statusCode?: number }).statusCode = 501;\n throw err;\n }\n}\n","import { Terminal } from \"@xterm/headless\";\nimport { randomUUID } from \"crypto\";\nimport { existsSync } from \"fs\";\nimport { basename } from \"path\";\nimport { getLogger, type Logger } from \"./logger\";\nimport { resolveCodexExe } from \"./platform\";\nimport { CODEX_CLI_PROVIDER } from \"./providers\";\nimport {\n type CodexGateType,\n rememberedGateDigit,\n saveGateAnswer,\n} from \"./services/questions/codexGateAnswers\";\nimport type {\n ManagedSession,\n PermissionOption,\n PTYManagerOptions,\n SessionRunner,\n StartFreshSessionOptions,\n StartSessionOptions,\n UserMessage,\n} from \"./types\";\nimport { debounce } from \"./utils/debounce\";\n\nconst OUTPUT_BUFFER_MAX = 65536;\n\n// Cap on recorded user messages per session (drop oldest); mirrors\n// pty-manager.ts INPUT_HISTORY_MAX.\nconst INPUT_HISTORY_MAX = 50;\n\n// PTY geometry — same as pty-manager.ts. The headless render terminal\n// (session.screen) MUST match these so Codex's absolute cursor moves\n// (ESC[<row>;<col>H) resolve to the same screen coordinates the real TUI is\n// painting against.\nconst PTY_COLS = 120;\nconst PTY_ROWS = 40;\nconst SCREEN_SCROLLBACK = 1000;\n\n// Phase 0 findings (live PTY probe, not spec): Codex's status bar renders\n// \"Ready\" (case-sensitive) once the session is actually usable, e.g.\n// \"gpt-5.5 medium · /path · gpt-5.5 · medium · Ready · Wo…\". Two other\n// observed states — \"Starting\" (MCP servers loading; the compose box `›`\n// prefix is ALREADY visible here, so `›` alone is not a valid readiness\n// signal) and \"Working\" (mid-turn) — must NOT be treated as ready.\nexport const CODEX_PROMPT_READY_TEXT = \"Ready\";\n\n// Phase 0: a brand-new `--cd <dir>` shows a blocking directory-trust gate on\n// first-ever launch — a rendered screen containing this text, with options\n// \"1. Yes, continue\" / \"2. No, quit\" and a \"Press enter to continue\" footer.\n// Does not appear on `codex resume` or on later launches in an\n// already-trusted directory (Codex persists trust in ~/.codex/config.toml).\nexport const CODEX_TRUST_GATE_REGEX = /trust the contents/i;\n\n// Codex's hooks-review gate: shown pre-boot (fresh start AND resume) whenever\n// a configured hook is new/changed vs the trusted hashes in\n// ~/.codex/config.toml [hooks.state]. Options \"1. Review hooks\" (highlighted\n// default) / \"2. Trust all and continue\" / \"3. Continue without trusting\",\n// with a \"Press enter to confirm or esc to go back\" footer. Live-probe\n// verified: a digit keypress selects AND confirms instantly (no Enter).\nexport const CODEX_HOOKS_GATE_REGEX = /hooks need review/i;\n\n// Re-run screen detection this long after the PTY goes quiet — a session\n// blocked on a gate (or a status bar whose \"Ready\" got truncated) may never\n// produce another chunk to trigger detection. Same value/rationale as\n// pty-manager.ts QUIET_DETECT_MS. On quiet with pendingReady still set we mark\n// ready anyway: the mobile client is better off inside the session watching\n// live boot output than stuck on a spinner.\nconst QUIET_DETECT_MS = 500;\n\n// Flat backstop from spawn: \"Ready\" lives at the END of a single status line\n// whose prefix (dir · repo · branch · diffstats) can exceed PTY_COLS, in which\n// case the marker is truncated off-screen and can NEVER match (live-probe\n// verified). Must stay below server.ts START_READY_TIMEOUT_MS (10s) so the\n// start request resolves 200-with-session rather than 202-pending.\nconst CODEX_READY_FALLBACK_MS = 8_000;\n\nconst SUBMIT_BYTES = \"\\r\";\n\n// Delay between the input write and the submit \\r. Same value as Claude's\n// SUBMIT_DELAY_MS (pty-manager.ts) — no bracketed-paste wrap needed for\n// Codex (Phase 0: plain keystrokes are accepted directly into the compose\n// box), but we still yield an event-loop tick before Enter for consistency\n// with the observed-stable Claude pattern.\nconst CODEX_SUBMIT_DELAY_MS = 16;\n\nfunction digestBytes(s: string): string {\n const escaped = s\n .replace(new RegExp(String.fromCharCode(0x1b), \"g\"), \"\\\\x1b\")\n .replace(/\\r/g, \"\\\\r\")\n .replace(/\\n/g, \"\\\\n\")\n .replace(/\\t/g, \"\\\\t\");\n if (escaped.length <= 200) return escaped;\n return `${escaped.slice(0, 100)}…[${escaped.length - 200}B omitted]…${escaped.slice(-100)}`;\n}\n\n// Build the question card for a gate, broadcast over the existing `permission`\n// WS transport (mobile already renders these as tappable cards). Real options\n// keep their literal on-screen digits; the synthetic \"remember\" variants\n// continue the numbering and are intercepted in sendKeys() — they never reach\n// the PTY as-is. answerKeys mirrors `${index}\\r` so old clients (index\n// fallback) and new clients (answerKeys) send identical bytes.\n// \"1. Review hooks\" is deliberately omitted: a per-hook review screen is a\n// desktop affordance with no workable mobile rendering.\nfunction gateCard(\n gate: CodexGateType,\n lines: string[],\n): { prompt: string; options: PermissionOption[] } {\n if (gate === \"hooks\") {\n const countLine = lines.find((l) => /new or changed/i.test(l))?.trim();\n return {\n prompt: [\n \"Hooks need review\",\n countLine,\n \"Hooks can run outside the sandbox after you trust them.\",\n ]\n .filter(Boolean)\n .join(\" — \"),\n options: [\n { index: 2, label: \"Trust all and continue\", answerKeys: \"2\\r\" },\n { index: 3, label: \"Continue without trusting (hooks won't run)\", answerKeys: \"3\\r\" },\n {\n index: 4,\n label: \"Trust all and continue (remember for all projects)\",\n answerKeys: \"4\\r\",\n },\n {\n index: 5,\n label: \"Continue without trusting (remember for all projects)\",\n answerKeys: \"5\\r\",\n },\n ],\n };\n }\n return {\n prompt:\n lines.find((l) => CODEX_TRUST_GATE_REGEX.test(l))?.trim() ??\n \"Do you trust the contents of this directory?\",\n options: [\n { index: 1, label: \"Yes, continue\", answerKeys: \"1\\r\" },\n { index: 2, label: \"No, quit\", answerKeys: \"2\\r\" },\n { index: 3, label: \"Yes, continue (remember for all projects)\", answerKeys: \"3\\r\" },\n ],\n };\n}\n\n// node-pty is a native addon — import dynamically to allow graceful failure\nlet pty: typeof import(\"node-pty\") | null = null;\n\nasync function loadPty(): Promise<typeof import(\"node-pty\")> {\n if (pty) return pty;\n try {\n pty = await import(\"node-pty\");\n return pty;\n } catch (err) {\n throw new Error(\n \"node-pty is required for PTY management but failed to load. \" +\n \"Ensure it is installed: npm install node-pty\\n\" +\n `Original error: ${err}`,\n );\n }\n}\n\ninterface InternalSession extends ManagedSession {\n process: any; // node-pty IPty\n outputBuffer: Buffer;\n // Headless terminal rendering the raw PTY stream into a real screen grid —\n // same rationale as pty-manager.ts: Codex's absolute-cursor repaints\n // scramble raw byte order, so readiness/trust-gate detection reads the\n // rendered screen, not the raw chunk.\n screen: Terminal;\n // Ground-truth user messages submitted to this PTY, oldest-first, capped at\n // INPUT_HISTORY_MAX. Recorded in writeSubmit(); replayed via getInputHistory().\n inputHistory: UserMessage[];\n}\n\nfunction createScreen(): Terminal {\n return new Terminal({\n cols: PTY_COLS,\n rows: PTY_ROWS,\n scrollback: SCREEN_SCROLLBACK,\n allowProposedApi: true,\n });\n}\n\nexport class CodexPtyRunner implements SessionRunner {\n private sessions = new Map<string, InternalSession>();\n private onOutput: PTYManagerOptions[\"onOutput\"];\n private onStatusChange: PTYManagerOptions[\"onStatusChange\"];\n private onReady: PTYManagerOptions[\"onReady\"];\n // Broadcasts Codex's blocking startup gates (directory trust, hooks review)\n // as question cards; null dismisses the card once the gate leaves the screen.\n private onPermissionChange: PTYManagerOptions[\"onPermissionChange\"];\n private onLiveQuestion: PTYManagerOptions[\"onLiveQuestion\"];\n private onLiveQuestionGone: PTYManagerOptions[\"onLiveQuestionGone\"];\n private onUserMessage: PTYManagerOptions[\"onUserMessage\"];\n private log: Logger;\n // Tracks sessions whose PTY has spawned but Codex hasn't yet reached its\n // \"Ready\" status bar — i.e. onReady hasn't fired.\n private pendingReady = new Set<string>();\n // Inputs received via sendInput() while the session was still pendingReady.\n // Flushed in arrival order once Codex reaches Ready.\n private queuedInputs = new Map<string, string[]>();\n // Gate currently on a session's screen (card broadcast, unanswered). While\n // set, queued-input flushes are held — a flushed digit would CONFIRM a\n // dialog option — and sendKeys() intercepts remember-variant digits.\n private openGate = new Map<string, CodexGateType>();\n // `${sessionId}:${gate}` once a gate has been actioned (auto-answered or\n // card broadcast) — dedupes repaints of the same dialog.\n private gateActioned = new Set<string>();\n // Per-session trailing debounce re-armed on every chunk; on quiet, re-runs\n // screen detection so a blocked/truncated boot still reaches ready.\n private quietCheckers = new Map<string, ReturnType<typeof debounce<[]>>>();\n // Per-session flat backstop from spawn (CODEX_READY_FALLBACK_MS).\n private readyFallbackTimers = new Map<string, NodeJS.Timeout>();\n // In-flight start()/startFresh() calls keyed by sessionId. A second\n // concurrent resume for the same session (double-tap, client retry) awaits\n // the first call's promise instead of spawning a duplicate PTY (CRITICAL #3).\n private startPromises = new Map<string, Promise<ManagedSession>>();\n\n constructor(options: PTYManagerOptions = {}) {\n this.onOutput = options.onOutput;\n this.onStatusChange = options.onStatusChange;\n this.onReady = options.onReady;\n this.onPermissionChange = options.onPermissionChange;\n this.onLiveQuestion = options.onLiveQuestion;\n this.onLiveQuestionGone = options.onLiveQuestionGone;\n this.onUserMessage = options.onUserMessage;\n this.log = options.logger ?? getLogger(\"codex-pty\");\n }\n\n // Resume an existing Codex session. sessionId is the Codex-persisted\n // session_meta.payload.id (Phase 0, Section 8) — Codex has no fresh-session\n // equivalent of --session-id, so start() always means \"resume\".\n async start(sessionId: string, options: StartSessionOptions): Promise<ManagedSession> {\n // Guard the check-then-spawn: a second concurrent resume for the same\n // sessionId must not race past both checks and spawn a second PTY. See\n // PTYManager.start() for the identical pattern (CRITICAL #3).\n const existing = this.sessions.get(sessionId);\n if (existing) return toPublicSession(existing);\n\n const inFlight = this.startPromises.get(sessionId);\n if (inFlight) return inFlight;\n\n const promise = this.doStart(sessionId, options).finally(() => {\n this.startPromises.delete(sessionId);\n });\n this.startPromises.set(sessionId, promise);\n return promise;\n }\n\n private async doStart(sessionId: string, options: StartSessionOptions): Promise<ManagedSession> {\n const nodePty = await loadPty();\n const projectName = options.projectName ?? basename(options.projectPath);\n\n const proc = nodePty.spawn(\n resolveCodexExe(),\n [\"resume\", sessionId, \"--cd\", options.projectPath, \"--no-alt-screen\"],\n {\n name: \"xterm-256color\",\n cols: PTY_COLS,\n rows: PTY_ROWS,\n cwd: options.projectPath,\n env: process.env as Record<string, string>,\n },\n );\n\n const session: InternalSession = {\n id: sessionId,\n provider: CODEX_CLI_PROVIDER,\n projectPath: options.projectPath,\n projectName,\n branch: options.branch ?? \"\",\n status: \"running\",\n startedAt: new Date(),\n completedAt: null,\n promptCount: 0,\n lastOutput: \"\",\n process: proc,\n outputBuffer: Buffer.alloc(0),\n screen: createScreen(),\n inputHistory: [],\n };\n\n this.sessions.set(sessionId, session);\n this.pendingReady.add(sessionId);\n this.armReadyFallback(sessionId);\n\n proc.onData((data: string) => {\n this.handleOutput(sessionId, data);\n });\n\n proc.onExit(({ exitCode }: { exitCode: number }) => {\n this.pendingReady.delete(sessionId);\n this.handleExit(sessionId, exitCode);\n });\n\n return toPublicSession(session);\n }\n\n // Start a brand-new Codex session. Codex has no --session-id equivalent for\n // a fresh launch — it assigns its own id, discovered later (Task 3's\n // binding logic). This runner generates a local placeholder id for the\n // ManagedSession handle only.\n async startFresh(options: StartFreshSessionOptions): Promise<ManagedSession> {\n const nodePty = await loadPty();\n const sessionId = randomUUID();\n const projectName = options.projectName ?? basename(options.projectPath);\n\n // Codex CLI has no `--system-prompt` flag (unlike Claude). Its only\n // launch-time injection point is the positional `[PROMPT]` argument, which\n // Codex processes as the opening turn. Pass the server-built prompt\n // (default + browse-root boundary + client prompt) there so the safety\n // boundary and client instructions aren't silently dropped for Codex\n // sessions. Positional arg goes last, after all `[OPTIONS]`.\n const args = [\"--cd\", options.projectPath, \"--no-alt-screen\"];\n if (options.systemPrompt) {\n args.push(options.systemPrompt);\n }\n\n const proc = nodePty.spawn(resolveCodexExe(), args, {\n name: \"xterm-256color\",\n cols: PTY_COLS,\n rows: PTY_ROWS,\n cwd: options.projectPath,\n env: process.env as Record<string, string>,\n });\n\n const session: InternalSession = {\n id: sessionId,\n provider: CODEX_CLI_PROVIDER,\n projectPath: options.projectPath,\n projectName,\n branch: \"\",\n status: \"running\",\n startedAt: new Date(),\n completedAt: null,\n promptCount: 0,\n lastOutput: \"\",\n process: proc,\n outputBuffer: Buffer.alloc(0),\n screen: createScreen(),\n inputHistory: [],\n };\n\n this.sessions.set(sessionId, session);\n this.pendingReady.add(sessionId);\n this.armReadyFallback(sessionId);\n\n proc.onData((data: string) => {\n this.handleOutput(sessionId, data);\n });\n\n proc.onExit(({ exitCode }: { exitCode: number }) => {\n this.pendingReady.delete(sessionId);\n this.handleExit(sessionId, exitCode);\n });\n\n return toPublicSession(session);\n }\n\n // Flat backstop: if neither the \"Ready\" marker nor the quiet-checker settled\n // the session within CODEX_READY_FALLBACK_MS of spawn, mark it ready anyway\n // so start requests resolve and mobile can watch the boot live. unref() so a\n // pending timer never holds the process open.\n private armReadyFallback(sessionId: string): void {\n const timer = setTimeout(() => {\n this.readyFallbackTimers.delete(sessionId);\n const session = this.sessions.get(sessionId);\n if (session?.status === \"running\" && this.pendingReady.has(sessionId)) {\n this.markReady(sessionId, session, \"fallback:timeout\");\n }\n }, CODEX_READY_FALLBACK_MS);\n timer.unref?.();\n this.readyFallbackTimers.set(sessionId, timer);\n }\n\n // Write raw key bytes directly to the PTY, same as PTYManager.sendKeys.\n sendKeys(sessionId: string, keys: string): void {\n const session = this.sessions.get(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n if (session.status === \"idle\") {\n throw new Error(`Session is idle (no active PTY): ${sessionId}`);\n }\n if (session.status === \"waiting_input\") {\n session.status = \"running\";\n this.onStatusChange?.(toPublicSession(session));\n }\n const gate = this.openGate.get(sessionId);\n const digit = gate ? /^([0-9])\\r?$/.exec(keys)?.[1] : undefined;\n const out = gate && digit ? this.resolveGateAnswer(sessionId, gate, digit) : keys;\n this.log.info(\n `[codex.keys.write] ${sessionId.slice(0, 8)} bytes=${out.length} digest=${digestBytes(out)}`,\n { event: \"codex.keys_write\", sessionId, byteLen: out.length },\n );\n session.process.write(out);\n session.lastActivityAt = new Date();\n }\n\n // Map a gate-card digit to the PTY bytes that answer the real dialog,\n // persisting the choice when the digit was a synthetic \"remember for all\n // projects\" option (those numbers don't exist on the actual dialog and must\n // never reach codex). The trailing \\r mobile sends is dropped: a digit alone\n // selects AND confirms (live-probe verified), and a stray Enter would land\n // on whatever screen follows.\n private resolveGateAnswer(sessionId: string, gate: CodexGateType, digit: string): string {\n let real = digit;\n let remembered = false;\n if (gate === \"hooks\" && digit === \"4\") {\n saveGateAnswer(\"codexHooksGate\", \"trust_all\");\n real = \"2\";\n remembered = true;\n } else if (gate === \"hooks\" && digit === \"5\") {\n saveGateAnswer(\"codexHooksGate\", \"continue_untrusted\");\n real = \"3\";\n remembered = true;\n } else if (gate === \"trust\" && digit === \"3\") {\n saveGateAnswer(\"codexTrustGate\", \"yes\");\n real = \"1\";\n remembered = true;\n }\n this.log.info(`[codex.gate_answer] ${sessionId.slice(0, 8)} ${gate} digit=${real}`, {\n event: \"codex.gate_answer\",\n sessionId,\n gate,\n digit: real,\n remembered,\n });\n return real;\n }\n\n sendInput(sessionId: string, input: string): number {\n const session = this.sessions.get(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n if (session.status === \"idle\") {\n throw new Error(`Session is idle (no active PTY): ${sessionId}`);\n }\n // Codex hasn't reached Ready yet — queue and flush once it does. Same\n // rationale as PTYManager: writing into the raw PTY mid-boot risks the\n // startup/trust-gate UI swallowing the keystrokes.\n if (this.pendingReady.has(sessionId)) {\n const queue = this.queuedInputs.get(sessionId) ?? [];\n queue.push(input);\n this.queuedInputs.set(sessionId, queue);\n session.lastActivityAt = new Date();\n session.promptCount++;\n this.log.warn(\n `[codex.input.queued] ${sessionId.slice(0, 8)} promptCount=${session.promptCount} queueLen=${queue.length}`,\n {\n event: \"codex.input_queued\",\n sessionId,\n promptCount: session.promptCount,\n queueLen: queue.length,\n inputLen: input.length,\n },\n );\n return session.promptCount;\n }\n if (session.status === \"waiting_input\") {\n session.status = \"running\";\n this.onStatusChange?.(toPublicSession(session));\n }\n this.writeSubmit(sessionId, session, input, \"direct\", session.promptCount + 1);\n session.lastActivityAt = new Date();\n session.promptCount++;\n return session.promptCount;\n }\n\n // Write the input as plain bytes (no bracketed-paste wrap — Phase 0\n // confirmed Codex accepts plain keystrokes), then submit \\r after a short\n // delay so Codex's TUI gets an event-loop tick to process the input first.\n private writeSubmit(\n sessionId: string,\n session: InternalSession,\n input: string,\n path: \"direct\" | \"flush\",\n promptCount: number,\n ): void {\n this.recordUserMessage(session, input);\n this.log.info(\n `[codex.input.write] ${sessionId.slice(0, 8)} promptCount=${promptCount} bytes=${input.length} digest=${digestBytes(input)}`,\n {\n event: \"codex.input_write\",\n sessionId,\n promptCount,\n byteLen: input.length,\n digest: digestBytes(input),\n path,\n phase: \"input\",\n },\n );\n session.process.write(input);\n setTimeout(() => {\n const current = this.sessions.get(sessionId);\n if (!current || current !== session) return;\n this.log.info(\n `[codex.input.submit] ${sessionId.slice(0, 8)} promptCount=${promptCount} digest=\\\\r`,\n {\n event: \"codex.input_write\",\n sessionId,\n promptCount,\n byteLen: SUBMIT_BYTES.length,\n digest: \"\\\\r\",\n path,\n phase: \"submit\",\n },\n );\n current.process.write(SUBMIT_BYTES);\n }, CODEX_SUBMIT_DELAY_MS);\n }\n\n // Drain any inputs sent while the session was still pendingReady, writing\n // them in arrival order now that Codex is Ready. No-op while a gate dialog\n // is open (a flushed digit would confirm a dialog option) or while still\n // pendingReady (markReady drains it) — the gate-close path re-drives it for\n // the ready-with-gate-open case.\n private flushQueuedInputs(sessionId: string): void {\n if (this.openGate.has(sessionId) || this.pendingReady.has(sessionId)) return;\n const queue = this.queuedInputs.get(sessionId);\n if (!queue || queue.length === 0) return;\n this.queuedInputs.delete(sessionId);\n const session = this.sessions.get(sessionId);\n if (!session) return;\n this.log.info(\n `[codex.flush] ${sessionId.slice(0, 8)} flushing ${queue.length} queued input(s)`,\n {\n event: \"codex.flush_queued\",\n sessionId,\n queueLen: queue.length,\n },\n );\n queue.forEach((input, i) => {\n const writeAt = i * CODEX_SUBMIT_DELAY_MS * 2;\n if (writeAt === 0) {\n this.writeSubmit(sessionId, session, input, \"flush\", session.promptCount);\n } else {\n setTimeout(() => {\n const current = this.sessions.get(sessionId);\n if (!current || current !== session) return;\n this.writeSubmit(sessionId, session, input, \"flush\", session.promptCount);\n }, writeAt);\n }\n });\n }\n\n // SIGINT produces a clean exitCode=0 exit (Phase 0 — confirmed).\n cancel(sessionId: string): void {\n const session = this.sessions.get(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n session.process.kill(\"SIGINT\");\n }\n\n killPid(pid: number): void {\n try {\n process.kill(pid, \"SIGTERM\");\n } catch {\n // Process may already be gone\n }\n }\n\n // Kill the PTY and mark the session idle. Mirrors PTYManager.putOnHold.\n putOnHold(sessionId: string): void {\n const session = this.sessions.get(sessionId);\n if (!session) return;\n this.pendingReady.delete(sessionId);\n this.queuedInputs.delete(sessionId);\n this.clearSessionDetectors(sessionId);\n try {\n session.process.kill(\"SIGINT\");\n } catch {\n // already dead\n }\n session.status = \"idle\";\n session.completedAt = new Date();\n session.screen.dispose();\n this.sessions.delete(sessionId);\n this.onStatusChange?.(toPublicSession(session));\n }\n\n // Drop a session's detection state: quiet-checker, ready-fallback timer,\n // gate bookkeeping — and dismiss a still-open gate card so mobile doesn't\n // keep rendering a question for a dead PTY.\n private clearSessionDetectors(sessionId: string): void {\n this.quietCheckers.get(sessionId)?.cancel();\n this.quietCheckers.delete(sessionId);\n const timer = this.readyFallbackTimers.get(sessionId);\n if (timer) clearTimeout(timer);\n this.readyFallbackTimers.delete(sessionId);\n if (this.openGate.delete(sessionId)) {\n this.onPermissionChange?.(sessionId, null);\n }\n this.gateActioned.delete(`${sessionId}:hooks`);\n this.gateActioned.delete(`${sessionId}:trust`);\n }\n\n getOutput(sessionId: string): string {\n const session = this.sessions.get(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n return session.outputBuffer.toString(\"utf-8\");\n }\n\n // Render the last `maxLines` rows of the session's screen in true on-screen\n // order — same flush-then-read technique as PTYManager.getOutputLines.\n async getOutputLines(sessionId: string, maxLines: number): Promise<string[]> {\n const session = this.sessions.get(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n await new Promise<void>((resolve) => session.screen.write(\"\", () => resolve()));\n\n const buf = session.screen.buffer.active;\n const lines: string[] = [];\n for (let y = 0; y < buf.length; y++) {\n lines.push(buf.getLine(y)?.translateToString(true) ?? \"\");\n }\n while (lines.length > 0 && lines[lines.length - 1] === \"\") {\n lines.pop();\n }\n return lines.slice(-maxLines);\n }\n\n getInputHistory(sessionId: string): UserMessage[] {\n return this.sessions.get(sessionId)?.inputHistory ?? [];\n }\n\n // Record a submitted user message as ground truth and fire onUserMessage.\n // Called from writeSubmit (direct and flush paths) — never from sendKeys.\n private recordUserMessage(session: InternalSession, text: string): void {\n const ts = Date.now();\n session.inputHistory.push({ text, ts });\n if (session.inputHistory.length > INPUT_HISTORY_MAX) {\n session.inputHistory.shift();\n }\n this.onUserMessage?.(session.id, text, ts);\n }\n\n getSession(sessionId: string): ManagedSession | null {\n const session = this.sessions.get(sessionId);\n return session ? toPublicSession(session) : null;\n }\n\n hasSession(sessionId: string): boolean {\n return this.sessions.has(sessionId);\n }\n\n listSessions(): ManagedSession[] {\n return Array.from(this.sessions.values()).map(toPublicSession);\n }\n\n dispose(): void {\n for (const session of this.sessions.values()) {\n try {\n session.process.kill();\n } catch {\n // Process may already be dead\n }\n session.screen.dispose();\n }\n for (const sessionId of Array.from(this.quietCheckers.keys())) {\n this.clearSessionDetectors(sessionId);\n }\n for (const timer of this.readyFallbackTimers.values()) {\n clearTimeout(timer);\n }\n this.sessions.clear();\n this.pendingReady.clear();\n this.queuedInputs.clear();\n this.openGate.clear();\n this.gateActioned.clear();\n this.quietCheckers.clear();\n this.readyFallbackTimers.clear();\n }\n\n private handleOutput(sessionId: string, data: string): void {\n const session = this.sessions.get(sessionId);\n if (!session) return;\n\n const chunk = Buffer.from(data, \"utf-8\");\n session.outputBuffer = Buffer.concat([session.outputBuffer, chunk]);\n if (session.outputBuffer.length > OUTPUT_BUFFER_MAX) {\n session.outputBuffer = session.outputBuffer.subarray(\n session.outputBuffer.length - OUTPUT_BUFFER_MAX,\n );\n }\n\n // Render into the headless screen so detectReady()/getOutputLines() read\n // true on-screen order — Codex paints via absolute cursor-position\n // escapes that split words across non-contiguous PTY writes (Phase 0),\n // so raw substring matching on `data` would miss things.\n session.screen.write(data);\n session.lastOutput = stripAnsi(data);\n\n this.onOutput?.(sessionId, data);\n\n this.detectScreenState(sessionId, \"chunk\").catch((err) => {\n this.log.warn(\"[codex.ready_detect] failed\", {\n event: \"codex.ready_detect_failed\",\n sessionId,\n err,\n });\n });\n\n // Re-arm the quiet-checker on every chunk. A session blocked on a gate\n // dialog (or whose status-bar \"Ready\" is truncated off-screen) may never\n // produce the chunk that would trigger detection — re-run after\n // QUIET_DETECT_MS of silence instead.\n let quiet = this.quietCheckers.get(sessionId);\n if (!quiet) {\n quiet = debounce(() => {\n this.detectScreenState(sessionId, \"quiet\").catch((err) => {\n this.log.warn(\"[codex.ready_detect] failed\", {\n event: \"codex.ready_detect_failed\",\n sessionId,\n err,\n });\n });\n }, QUIET_DETECT_MS);\n this.quietCheckers.set(sessionId, quiet);\n }\n quiet();\n }\n\n // Renders the session's headless screen and drives both detections:\n // - Gates (directory trust, hooks review) — checked on EVERY pass,\n // independent of pendingReady, so a gate appearing after ready is still\n // surfaced and a gate leaving the screen closes its card.\n // - Readiness — the \"Ready\" status-bar marker while pendingReady, plus the\n // quiet path: after QUIET_DETECT_MS of PTY silence a still-pending\n // session is marked ready anyway (`›` alone is NOT a marker — Phase 0 —\n // but a quiet boot screen is more useful to the user live than a\n // spinner, and \"Ready\" may be truncated off the 120-col status bar).\n private async detectScreenState(sessionId: string, trigger: \"chunk\" | \"quiet\"): Promise<void> {\n const session = this.sessions.get(sessionId);\n if (!session || session.status === \"idle\") return;\n\n const lines = await this.getOutputLines(sessionId, PTY_ROWS);\n const screenText = lines.join(\"\\n\");\n\n // ── Gates ──────────────────────────────────────────────────────\n const gate: CodexGateType | null = CODEX_HOOKS_GATE_REGEX.test(screenText)\n ? \"hooks\"\n : CODEX_TRUST_GATE_REGEX.test(screenText)\n ? \"trust\"\n : null;\n\n if (gate) {\n this.handleGate(sessionId, session, gate, lines);\n } else if (this.openGate.delete(sessionId)) {\n // The dialog left the screen (answered via card, keys, or desktop) —\n // dismiss the card and release inputs held while it was open.\n this.onPermissionChange?.(sessionId, null);\n this.flushQueuedInputs(sessionId);\n }\n\n // ── Readiness ──────────────────────────────────────────────────\n if (session.status !== \"running\" || !this.pendingReady.has(sessionId)) return;\n const lastNonBlank = [...lines].reverse().find((l) => l.trim() !== \"\") ?? \"\";\n if (lastNonBlank.includes(CODEX_PROMPT_READY_TEXT)) {\n this.markReady(sessionId, session, `marker:${CODEX_PROMPT_READY_TEXT}`);\n } else if (trigger === \"quiet\") {\n this.markReady(sessionId, session, \"quiet:timeout\");\n }\n }\n\n // Answer a gate from the persisted remember-store, or surface it as a\n // question card over the permission transport. Actioned once per session and\n // gate type — repaints of the same dialog neither re-write nor re-broadcast.\n private handleGate(\n sessionId: string,\n session: InternalSession,\n gate: CodexGateType,\n lines: string[],\n ): void {\n const key = `${sessionId}:${gate}`;\n if (this.gateActioned.has(key)) return;\n this.gateActioned.add(key);\n\n const remembered = rememberedGateDigit(gate);\n if (remembered) {\n this.log.info(`[codex.gate_auto_answer] ${sessionId.slice(0, 8)} ${gate} → ${remembered}`, {\n event: \"codex.gate_auto_answer\",\n sessionId,\n gate,\n digit: remembered,\n });\n session.process.write(remembered);\n return;\n }\n\n this.openGate.set(sessionId, gate);\n const card = gateCard(gate, lines);\n this.log.info(`[codex.gate_prompt] ${sessionId.slice(0, 8)} ${gate}`, {\n event: \"codex.gate_prompt\",\n sessionId,\n gate,\n prompt: card.prompt,\n });\n this.onPermissionChange?.(sessionId, card);\n }\n\n private markReady(sessionId: string, session: InternalSession, reason: string): void {\n session.lastActivityAt = new Date();\n session.status = \"waiting_input\";\n // `reason=quiet:timeout`/`fallback:timeout` in volume would mean the\n // status-bar marker regressed (e.g. a Codex TUI redesign) — keep logged.\n this.log.info(`[codex.ready] ${sessionId.slice(0, 8)} ${reason}`, {\n event: \"codex.ready\",\n sessionId,\n reason,\n });\n this.onStatusChange?.(toPublicSession(session));\n if (this.pendingReady.has(sessionId)) {\n this.pendingReady.delete(sessionId);\n this.flushQueuedInputs(sessionId);\n this.onReady?.(toPublicSession(session));\n }\n }\n\n private handleExit(sessionId: string, exitCode: number): void {\n const session = this.sessions.get(sessionId);\n if (!session) return;\n\n session.completedAt = new Date();\n session.status = \"idle\";\n\n // Instant exit with no output — diagnose the most likely cause.\n const elapsedMs = session.completedAt.getTime() - session.startedAt.getTime();\n if (exitCode !== 0 && elapsedMs < 2000 && session.lastOutput === \"\") {\n if (!existsSync(session.projectPath)) {\n session.failureReason = `Project directory not found: ${session.projectPath}`;\n } else {\n session.failureReason = `Codex process exited immediately (code ${exitCode}).`;\n }\n }\n\n this.onStatusChange?.(toPublicSession(session));\n session.screen.dispose();\n this.sessions.delete(sessionId);\n this.queuedInputs.delete(sessionId);\n this.clearSessionDetectors(sessionId);\n }\n}\n\nfunction toPublicSession(s: InternalSession): ManagedSession {\n return {\n id: s.id,\n provider: s.provider ?? CODEX_CLI_PROVIDER,\n projectPath: s.projectPath,\n projectName: s.projectName,\n branch: s.branch,\n status: s.status,\n startedAt: s.startedAt,\n completedAt: s.completedAt,\n promptCount: s.promptCount,\n lastOutput: s.lastOutput,\n ...(s.failureReason != null && { failureReason: s.failureReason }),\n ...(s.lastActivityAt != null && { lastActivityAt: s.lastActivityAt }),\n ...(s.filePath != null && { filePath: s.filePath }),\n };\n}\n\n// Strip ANSI escape sequences for clean text preview — identical to\n// pty-manager.ts's stripAnsi.\nfunction stripAnsi(str: string): string {\n // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional ANSI stripping\n return str.replace(/\\x1b\\[[0-9;]*[a-zA-Z]/g, \"\").replace(/\\x1b\\][^\\x07]*\\x07/g, \"\");\n}\n","import pino, { type Logger as PinoLogger } from \"pino\";\n\nexport type LogDest = \"console\" | \"pino\" | \"both\";\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nconst baseLogger: PinoLogger = pino({\n level: process.env.LOG_LEVEL ?? \"info\",\n base: { service: \"tb-streamer\" },\n timestamp: pino.stdTimeFunctions.isoTime,\n redact: {\n paths: [\"req.headers.authorization\", \"req.headers.cookie\", 'req.headers[\"x-api-key\"]'],\n censor: \"[redacted]\",\n },\n});\n\nexport interface Logger {\n debug(msg: string, fields?: Record<string, unknown>, dest?: LogDest): void;\n info(msg: string, fields?: Record<string, unknown>, dest?: LogDest): void;\n warn(msg: string, fields?: Record<string, unknown>, dest?: LogDest): void;\n error(msg: string, fields?: Record<string, unknown>, dest?: LogDest): void;\n log(level: LogLevel, msg: string, fields?: Record<string, unknown>, dest?: LogDest): void;\n pino: PinoLogger;\n}\n\nfunction emit(\n pinoChild: PinoLogger,\n level: LogLevel,\n msg: string,\n fields: Record<string, unknown> | undefined,\n dest: LogDest,\n): void {\n if (dest === \"pino\" || dest === \"both\") {\n if (fields && Object.keys(fields).length > 0) pinoChild[level](fields, msg);\n else pinoChild[level](msg);\n }\n if (dest === \"console\" || dest === \"both\") {\n const consoleMethod: \"log\" | \"warn\" | \"error\" =\n level === \"error\" ? \"error\" : level === \"warn\" ? \"warn\" : \"log\";\n console[consoleMethod](msg);\n }\n}\n\nfunction build(pinoChild: PinoLogger): Logger {\n return {\n debug: (m, f, d = \"both\") => emit(pinoChild, \"debug\", m, f, d),\n info: (m, f, d = \"both\") => emit(pinoChild, \"info\", m, f, d),\n warn: (m, f, d = \"both\") => emit(pinoChild, \"warn\", m, f, d),\n error: (m, f, d = \"both\") => emit(pinoChild, \"error\", m, f, d),\n log: (lvl, m, f, d = \"both\") => emit(pinoChild, lvl, m, f, d),\n pino: pinoChild,\n };\n}\n\nexport function getLogger(component?: string): Logger {\n return build(component ? baseLogger.child({ component }) : baseLogger);\n}\n\nexport const logger: Logger = build(baseLogger);\n","import { execFileSync } from \"child_process\";\nimport { existsSync } from \"fs\";\nimport { homedir, platform } from \"os\";\nimport { join } from \"path\";\n\nexport const isWindows = platform() === \"win32\";\n\n// ─── Claude executable resolution ─────────────────────────────────────────────\n// On Windows, Task Scheduler strips PATH to bare system directories, so\n// `claude` alone will not resolve. We try where.exe first, then fall back to\n// well-known install locations before giving up and returning the bare name.\n//\n// On macOS, launchd inherits PATH=/usr/bin:/bin:/usr/sbin:/sbin by default,\n// which excludes both Homebrew prefixes. Without an explicit fallback,\n// node-pty's execvp(\"claude\", …) fails with ENOENT and every session\n// dies in milliseconds — see docs/troubleshooting.md. The plist's\n// EnvironmentVariables block is the primary fix; this is defense in depth.\n\nlet _claudeExe: string | undefined;\n\nexport function resolveClaudeExe(): string {\n if (_claudeExe !== undefined) return _claudeExe;\n\n if (isWindows) {\n try {\n const found = execFileSync(\"where.exe\", [\"claude\"], {\n encoding: \"utf-8\",\n windowsHide: true,\n timeout: 3000,\n })\n .trim()\n .split(\"\\n\")[0]\n .trim();\n if (found) {\n _claudeExe = found;\n return _claudeExe;\n }\n } catch {}\n\n const candidates = [\n join(homedir(), \".local\", \"bin\", \"claude.exe\"),\n join(\n process.env.LOCALAPPDATA ?? join(homedir(), \"AppData\", \"Local\"),\n \"Microsoft\",\n \"WindowsApps\",\n \"claude.exe\",\n ),\n ];\n for (const p of candidates) {\n if (existsSync(p)) {\n _claudeExe = p;\n return _claudeExe;\n }\n }\n } else {\n try {\n const found = execFileSync(\"/usr/bin/which\", [\"claude\"], {\n encoding: \"utf-8\",\n timeout: 3000,\n })\n .trim()\n .split(\"\\n\")[0]\n .trim();\n if (found && existsSync(found)) {\n _claudeExe = found;\n return _claudeExe;\n }\n } catch {}\n\n const candidates = [\n \"/opt/homebrew/bin/claude\",\n \"/usr/local/bin/claude\",\n join(homedir(), \".local\", \"bin\", \"claude\"),\n ];\n for (const p of candidates) {\n if (existsSync(p)) {\n _claudeExe = p;\n return _claudeExe;\n }\n }\n }\n\n _claudeExe = \"claude\";\n return _claudeExe;\n}\n\n// ─── Codex executable resolution ──────────────────────────────────────────────\n// Mirrors resolveClaudeExe() exactly, swapped for the `codex` binary. Same\n// rationale: launchd/Task Scheduler strip PATH down to system directories, so\n// an explicit which/where.exe + well-known-path fallback is needed.\n\nlet _codexExe: string | undefined;\n\nexport function resolveCodexExe(): string {\n if (_codexExe !== undefined) return _codexExe;\n\n if (isWindows) {\n try {\n const found = execFileSync(\"where.exe\", [\"codex\"], {\n encoding: \"utf-8\",\n windowsHide: true,\n timeout: 3000,\n })\n .trim()\n .split(\"\\n\")[0]\n .trim();\n if (found) {\n _codexExe = found;\n return _codexExe;\n }\n } catch {}\n\n const candidates = [\n join(homedir(), \".local\", \"bin\", \"codex.exe\"),\n join(\n process.env.LOCALAPPDATA ?? join(homedir(), \"AppData\", \"Local\"),\n \"Microsoft\",\n \"WindowsApps\",\n \"codex.exe\",\n ),\n ];\n for (const p of candidates) {\n if (existsSync(p)) {\n _codexExe = p;\n return _codexExe;\n }\n }\n } else {\n try {\n const found = execFileSync(\"/usr/bin/which\", [\"codex\"], {\n encoding: \"utf-8\",\n timeout: 3000,\n })\n .trim()\n .split(\"\\n\")[0]\n .trim();\n if (found && existsSync(found)) {\n _codexExe = found;\n return _codexExe;\n }\n } catch {}\n\n const candidates = [\n \"/opt/homebrew/bin/codex\",\n \"/usr/local/bin/codex\",\n join(homedir(), \".local\", \"bin\", \"codex\"),\n ];\n for (const p of candidates) {\n if (existsSync(p)) {\n _codexExe = p;\n return _codexExe;\n }\n }\n }\n\n _codexExe = \"codex\";\n return _codexExe;\n}\n\n// ─── execHidden ────────────────────────────────────────────────────────────────\n// Thin wrapper around execFileSync that adds windowsHide: true on Windows so\n// spawned child processes (where.exe, tasklist, wmic, pgrep, git …) don't\n// flash a console window.\n\ntype SyncOptions = Parameters<typeof execFileSync>[2];\n\nexport function execHidden(\n file: string,\n args: string[],\n opts?: SyncOptions & { encoding: \"utf-8\" },\n): string {\n return execFileSync(file, args, {\n windowsHide: isWindows,\n ...opts,\n }) as string;\n}\n","export const CLAUDE_CODE_PROVIDER = \"claude-code\" as const;\nexport const CODEX_CLI_PROVIDER = \"codex-cli\" as const;\nexport type ProviderName = typeof CLAUDE_CODE_PROVIDER | typeof CODEX_CLI_PROVIDER;\n\nexport function isProviderName(value: unknown): value is ProviderName {\n return value === CLAUDE_CODE_PROVIDER || value === CODEX_CLI_PROVIDER;\n}\n\n// Resolve a provider for a runner lookup. A `??` chain only defends against\n// null/undefined, so a present-but-unknown value (e.g. the legacy 'threadbase'\n// default from an old scanner-era cache) sails through and 501s at\n// assertSupportedProvider. Coerce anything that isn't a real runner to Claude Code.\nexport function coerceProviderForRunner(value: unknown): ProviderName {\n return isProviderName(value) ? value : CLAUDE_CODE_PROVIDER;\n}\n\n// Codex resume is implemented and verified (Phase 0: `codex resume <id>\n// --cd <dir>` replays the prior transcript end-to-end) — codex-cli now\n// defers to the same availability check as claude-code (project path\n// present, etc.) instead of forcing resumable=false unconditionally.\n// `provider` is kept in the signature (unused) so call sites don't need to\n// change if resumability ever needs to differentiate by provider again.\nexport function isProviderResumable(\n _provider: string | null | undefined,\n availabilityResumable: boolean,\n): boolean {\n return availabilityResumable;\n}\n","// Persisted answers for Codex's blocking startup gates (directory trust,\n// hooks review). When mobile answers a gate card with a \"remember for all\n// projects\" option, the choice lands here and future gates are auto-answered\n// without a card. One tiny JSON file; read at gate time so edits (or tests\n// via THREADBASE_CONFIG_DIR) apply without a restart.\n\nimport { mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { dirname, join } from \"path\";\n\nexport type CodexGateType = \"hooks\" | \"trust\";\n\nexport interface GateAnswers {\n codexHooksGate?: \"trust_all\" | \"continue_untrusted\";\n codexTrustGate?: \"yes\";\n}\n\nfunction gateAnswersPath(): string {\n const dir = process.env.THREADBASE_CONFIG_DIR ?? join(homedir(), \".threadbase\");\n return join(dir, \"gate-answers.json\");\n}\n\nexport function loadGateAnswers(): GateAnswers {\n try {\n const parsed = JSON.parse(readFileSync(gateAnswersPath(), \"utf-8\"));\n return parsed && typeof parsed === \"object\" ? (parsed as GateAnswers) : {};\n } catch {\n return {};\n }\n}\n\nexport function saveGateAnswer<K extends keyof GateAnswers>(key: K, value: GateAnswers[K]): void {\n const path = gateAnswersPath();\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, `${JSON.stringify({ ...loadGateAnswers(), [key]: value }, null, 2)}\\n`);\n}\n\n/**\n * The digit that answers a gate per the remembered choice, or null when the\n * user must be asked. Digits are the dialogs' literal on-screen numbers\n * (probe-verified: a digit keypress selects AND confirms, no Enter needed).\n */\nexport function rememberedGateDigit(gate: CodexGateType): string | null {\n const answers = loadGateAnswers();\n if (gate === \"hooks\") {\n if (answers.codexHooksGate === \"trust_all\") return \"2\";\n if (answers.codexHooksGate === \"continue_untrusted\") return \"3\";\n return null;\n }\n return answers.codexTrustGate === \"yes\" ? \"1\" : null;\n}\n","/**\n * Trailing-edge debounce: collapses calls that arrive within `waitMs` of each\n * other into a single invocation, scheduled `waitMs` after the most recent\n * call. The last call's arguments win.\n *\n * The returned function carries two extra methods:\n * - cancel(): drop any pending invocation (use on shutdown so a timer\n * doesn't keep the process alive).\n * - flush(): run any pending invocation immediately (useful in tests).\n */\nexport function debounce<A extends unknown[]>(\n fn: (...args: A) => void,\n waitMs: number,\n): ((...args: A) => void) & { flush: () => void; cancel: () => void } {\n let timer: ReturnType<typeof setTimeout> | null = null;\n let lastArgs: A | null = null;\n\n const run = () => {\n timer = null;\n if (lastArgs) {\n const args = lastArgs;\n lastArgs = null;\n fn(...args);\n }\n };\n\n const debounced = (...args: A): void => {\n lastArgs = args;\n if (timer) clearTimeout(timer);\n timer = setTimeout(run, waitMs);\n };\n\n debounced.cancel = (): void => {\n if (timer) clearTimeout(timer);\n timer = null;\n lastArgs = null;\n };\n\n debounced.flush = (): void => {\n if (timer) {\n clearTimeout(timer);\n run();\n }\n };\n\n return debounced;\n}\n","import { Terminal } from \"@xterm/headless\";\nimport { randomUUID } from \"crypto\";\nimport { existsSync } from \"fs\";\nimport { basename } from \"path\";\nimport { getLogger, type Logger } from \"./logger\";\nimport { resolveClaudeExe } from \"./platform\";\nimport { CLAUDE_CODE_PROVIDER } from \"./providers\";\nimport { hasPermissionOsc, scrapePermissionGate } from \"./services/questions/detectPermissionGate\";\nimport {\n detectQuestionFromScreen,\n questionContentKey,\n} from \"./services/questions/detectQuestionFromScreen\";\nimport { detectShellPrompt } from \"./services/questions/detectShellPrompt\";\nimport type {\n ManagedSession,\n PTYManagerOptions,\n SessionRunner,\n StartFreshSessionOptions,\n StartSessionOptions,\n UserMessage,\n} from \"./types\";\nimport { debounce } from \"./utils/debounce\";\n\nconst OUTPUT_BUFFER_MAX = 65536;\n\n// Cap on recorded user messages per session (drop oldest). Bounds memory on a\n// long-lived session; replay only needs the recent tail for ownership.\nconst INPUT_HISTORY_MAX = 50;\n\n// PTY geometry. The headless render terminal (session.screen) MUST match these\n// so Claude's absolute cursor moves (ESC[<row>;<col>H) resolve to the same\n// screen coordinates the real TUI is painting against.\nconst PTY_COLS = 120;\nconst PTY_ROWS = 40;\n// Scrollback depth for the render terminal. Replay reads up to maxLines (200)\n// from the rendered buffer, so keep enough history above the viewport.\nconst SCREEN_SCROLLBACK = 1000;\n\n// Markers that indicate Claude has reached an interactive prompt and is ready\n// for user input. The TUI has at least two startup variants:\n// - \"Tips\" banner: renders a box with corner ╭ characters\n// - Connector/MCP-status splash: no box at all; shows ❯ as the prompt arrow\n// We treat either marker in a chunk as \"ready\". False-positives are harmless —\n// the worst case is flushing queued input slightly early, which Claude buffers.\nconst CLAUDE_PROMPT_MARKERS = [\"╭\", \"❯\"] as const;\n\n// If a fresh session has produced any output but neither prompt marker has\n// fired within this window, flush queued input anyway. Defense against future\n// TUI changes that drop both markers from the boot screen.\nconst PROMPT_MARKER_FALLBACK_MS = 10_000;\n\n// Re-run ready/prompt detection this long after the PTY goes quiet, instead of\n// waiting for another chunk (which may never come if Claude is blocked on a\n// prompt) or the full PROMPT_MARKER_FALLBACK_MS. 500ms was picked from real\n// [pty.chunk] gap logs: p50/p75 inter-chunk gaps while status=running are\n// 31ms/467ms, so 500ms clears normal streaming pauses without adding\n// meaningful perceived latency (see docs/postmortems or session notes for the\n// gapMs sample this was based on).\nconst QUIET_DETECT_MS = 500;\n\n// Build the two byte sequences for a paste-then-submit. We deliberately split\n// the paste body and the trailing \\r into two separate PTY writes (see\n// writeSubmit() below) so Claude's TUI gets one event-loop tick to process the\n// paste (clear input buffer, render `Pasting…`) before Enter arrives.\n//\n// Why bracketed paste at all: Claude's TUI enables bracketed paste mode\n// (\\x1b[?2004h) at startup. Content between \\x1b[200~ and \\x1b[201~ is\n// committed as a single insertion without triggering autocomplete or key\n// bindings. Without this wrap an input like \"@<path>\" opens the mention\n// picker and the trailing \\r gets consumed as \"accept completion\" rather\n// than \"submit\" — see docs/postmortems/2026-05-20-pty-bracketed-paste-fix.md.\n//\n// Why split paste and \\r: on 2026-05-27 a follow-up stuck session\n// (39118d3e) showed the bracketed-paste wrap was being written but the\n// trailing \\r still didn't submit — Claude's TUI was mid-render of a\n// startup status banner (\"Update available\", \"192 skill descriptions\n// dropped\", etc.) when the bytes arrived, and the whole chunk landed in\n// the wrong handler context. Splitting the write lets the TUI ingest the\n// paste in one tick (the data event runs after current render finishes)\n// before the next tick delivers the Enter.\nfunction buildPasteBytes(input: string): string {\n return `\\x1b[200~${input}\\x1b[201~`;\n}\n\nconst SUBMIT_BYTES = \"\\r\";\n\n// Delay between the paste write and the submit \\r. We need to yield the event\n// loop at least once so Claude's TUI processes the paste before Enter lands;\n// a small real-time delay is more robust against the TUI batching renders\n// across multiple data events. Kept tiny so user-perceived latency is nil.\nconst SUBMIT_DELAY_MS = 16;\n\n// writeSubmit() polls in SUBMIT_DELAY_MS steps and only writes \\r once the PTY\n// has been quiet (no chunk) for a full SUBMIT_DELAY_MS — see writeSubmit() for\n// the redraw race this closes. Cap the wait so a genuinely wedged/silent PTY\n// still gets its \\r rather than hanging forever.\nconst SUBMIT_MAX_WAIT_MS = 500;\n\nfunction digestBytes(s: string): string {\n // Replace control chars with their hex form so logs are grep-able.\n // Building the regex via RegExp() sidesteps a Biome lint rule that flags\n // literal control characters in regex literals.\n const escaped = s\n .replace(new RegExp(String.fromCharCode(0x1b), \"g\"), \"\\\\x1b\")\n .replace(/\\r/g, \"\\\\r\")\n .replace(/\\n/g, \"\\\\n\")\n .replace(/\\t/g, \"\\\\t\");\n if (escaped.length <= 200) return escaped;\n return `${escaped.slice(0, 100)}…[${escaped.length - 200}B omitted]…${escaped.slice(-100)}`;\n}\n\n// node-pty is a native addon — import dynamically to allow graceful failure\nlet pty: typeof import(\"node-pty\") | null = null;\n\nasync function loadPty(): Promise<typeof import(\"node-pty\")> {\n if (pty) return pty;\n try {\n pty = await import(\"node-pty\");\n return pty;\n } catch (err) {\n throw new Error(\n \"node-pty is required for PTY management but failed to load. \" +\n \"Ensure it is installed: npm install node-pty\\n\" +\n `Original error: ${err}`,\n );\n }\n}\n\ninterface InternalSession extends ManagedSession {\n process: any; // node-pty IPty\n outputBuffer: Buffer;\n // Headless terminal that renders the raw PTY stream into a real screen grid.\n // getOutputLines() reads its rendered buffer so replay reflects true screen\n // order rather than raw byte order (which Claude's absolute-cursor repaints\n // scramble — see getOutputLines for the desync this fixes).\n screen: Terminal;\n // Ground-truth user messages submitted to this PTY, oldest-first, capped at\n // INPUT_HISTORY_MAX. Recorded in writeSubmit(); replayed via getInputHistory().\n inputHistory: UserMessage[];\n}\n\nfunction createScreen(): Terminal {\n return new Terminal({\n cols: PTY_COLS,\n rows: PTY_ROWS,\n scrollback: SCREEN_SCROLLBACK,\n allowProposedApi: true,\n });\n}\n\n// Build the environment for a spawned `claude` process. The Anthropic API key\n// is injected only here — never exported into the streamer's global process\n// env — so it does not leak into unrelated child processes. CLAUDE_API_KEY\n// (the Fly secret) is mapped to ANTHROPIC_MODEL's sibling, ANTHROPIC_API_KEY,\n// which the CLI reads. If CLAUDE_API_KEY is unset, nothing is added and the CLI\n// falls back to its own auth (e.g. an interactive login).\nfunction buildSpawnEnv(): Record<string, string> {\n const env = { ...process.env } as Record<string, string>;\n if (env.CLAUDE_API_KEY) {\n env.ANTHROPIC_API_KEY = env.CLAUDE_API_KEY;\n }\n // If the streamer itself was launched from inside a Claude Code session,\n // it inherits that session's markers (CLAUDECODE, CLAUDE_CODE_SESSION_ID,\n // CLAUDE_CODE_CHILD_SESSION, ...). A spawned `claude` seeing them treats\n // itself as a nested child session and never persists a project JSONL —\n // the session \"works\" in the terminal but has no conversation to stream.\n for (const key of Object.keys(env)) {\n if (key === \"CLAUDECODE\" || key.startsWith(\"CLAUDE_CODE_\")) {\n delete env[key];\n }\n }\n return env;\n}\n\nexport class PTYManager implements SessionRunner {\n private sessions = new Map<string, InternalSession>();\n private onOutput: PTYManagerOptions[\"onOutput\"];\n private onStatusChange: PTYManagerOptions[\"onStatusChange\"];\n private onReady: PTYManagerOptions[\"onReady\"];\n private onPermissionChange: PTYManagerOptions[\"onPermissionChange\"];\n private onLiveQuestion: PTYManagerOptions[\"onLiveQuestion\"];\n private onLiveQuestionGone: PTYManagerOptions[\"onLiveQuestionGone\"];\n private onUserMessage: PTYManagerOptions[\"onUserMessage\"];\n // Per-session permission-gate state. True between an OSC 777 (gate open) and\n // the next prompt-ready without a fresh 777 (gate closed). Prevents\n // re-broadcasting open/close on every chunk.\n private permissionOpen = new Set<string>();\n // Content key of the last AskUserQuestion broadcast from the rendered screen,\n // per session — de-dupes the same menu firing on consecutive repaints.\n private lastScreenQuestionKey = new Map<string, string>();\n // Content key of the last unstructured shell prompt (detectShellPrompt)\n // broadcast per session — present between open and resolve so we can clear it\n // on a prompt-ready/marker return and de-dupe consecutive repaints. Modelled\n // on permissionOpen but keyed by content (a shell prompt has no OSC trigger).\n private shellPromptOpen = new Map<string, string>();\n // Tracks sessions (both fresh and resume) whose PTY has spawned but Claude\n // hasn't yet reached an interactive prompt — i.e. onReady hasn't fired.\n private pendingReady = new Set<string>();\n // Inputs received via sendInput() while the session was still in pendingReady.\n // Flushed in arrival order once Claude reaches its first prompt. Without this,\n // input written into the raw PTY mid-boot is consumed by Claude's startup TUI\n // (welcome banner / first-run modals on fresh, JSONL restore on resume) and\n // silently lost — the \"dot bug\".\n private queuedInputs = new Map<string, string[]>();\n private log: Logger;\n // Timestamp of first PTY chunk per session; drives the prompt-marker fallback\n // when neither ╭ nor ❯ shows up within PROMPT_MARKER_FALLBACK_MS.\n private firstChunkAt = new Map<string, number>();\n // Per-session chunk counter and last-chunk timestamp. Diagnostic-only,\n // feeds the [pty.chunk] log lines so we can trace whether Claude responded\n // to a given input or fell silent. Reset on dispose().\n private chunkIndex = new Map<string, number>();\n private lastChunkAt = new Map<string, number>();\n // Per-session debounced \"went quiet\" checker, re-armed on every chunk. Fires\n // QUIET_DETECT_MS after the last chunk so ready/prompt detection doesn't\n // wait for another chunk that may never arrive (Claude blocked on input).\n private quietCheckers = new Map<string, ReturnType<typeof debounce>>();\n // In-flight start()/startFresh() calls keyed by sessionId. A second\n // concurrent resume for the same session (double-tap, client retry) awaits\n // the first call's promise instead of spawning a duplicate PTY (CRITICAL #3).\n private startPromises = new Map<string, Promise<ManagedSession>>();\n\n constructor(options: PTYManagerOptions = {}) {\n this.onOutput = options.onOutput;\n this.onStatusChange = options.onStatusChange;\n this.onReady = options.onReady;\n this.onPermissionChange = options.onPermissionChange;\n this.onLiveQuestion = options.onLiveQuestion;\n this.onLiveQuestionGone = options.onLiveQuestionGone;\n this.onUserMessage = options.onUserMessage;\n this.log = options.logger ?? getLogger(\"pty\");\n }\n\n // Resume an existing Claude conversation. sessionId is the JSONL UUID.\n //\n // options.permissionMode defaults to `acceptEdits` rather than\n // `--dangerously-skip-permissions`/`bypassPermissions`. Both suppress\n // file-edit prompts, but in an interactive (TUI) launch the skip-permissions\n // flag renders a blocking \"Bypass Permissions mode\" warning menu on every\n // boot that no known ~/.claude.json flag suppressed (as of Claude CLI\n // v2.1.x) — the session never reaches a usable prompt, so the mobile app\n // shows an empty/stuck screen. `acceptEdits` auto-approves file edits\n // without that warning gate, while still prompting for shell commands.\n // `manual` (prompt for everything) is the only other mode callers may pass;\n // `bypassPermissions`/`plan`/`dontAsk`/`auto` are not supported here.\n // (The other first-run gates — onboarding/theme, workspace trust,\n // custom-API-key — are cleared by the seeded ~/.claude.json in\n // docker/entrypoint.sh.) startFresh() uses the same default for the same reason.\n async start(sessionId: string, options: StartSessionOptions): Promise<ManagedSession> {\n // Guard the check-then-spawn: a second concurrent resume for the same\n // sessionId (double-tap, client retry — server.ts's own hasSession check\n // has an await gap before it calls start()) must not race past both\n // checks and spawn a second PTY. Returning the first call's in-flight\n // promise serializes the spawn; an already-running session short-circuits\n // without touching doStart at all.\n const existing = this.sessions.get(sessionId);\n if (existing) return toPublicSession(existing);\n\n const inFlight = this.startPromises.get(sessionId);\n if (inFlight) return inFlight;\n\n const promise = this.doStart(sessionId, options).finally(() => {\n this.startPromises.delete(sessionId);\n });\n this.startPromises.set(sessionId, promise);\n return promise;\n }\n\n private async doStart(sessionId: string, options: StartSessionOptions): Promise<ManagedSession> {\n const nodePty = await loadPty();\n const projectName = options.projectName ?? basename(options.projectPath);\n\n const proc = nodePty.spawn(\n resolveClaudeExe(),\n [\n \"--permission-mode\",\n options.permissionMode ?? \"acceptEdits\",\n \"--settings\",\n '{\"spinnerTipsEnabled\":false}',\n \"--resume\",\n sessionId,\n ],\n {\n name: \"xterm-256color\",\n cols: 120,\n rows: 40,\n cwd: options.projectPath,\n env: buildSpawnEnv(),\n },\n );\n\n const session: InternalSession = {\n id: sessionId,\n provider: CLAUDE_CODE_PROVIDER,\n projectPath: options.projectPath,\n projectName,\n branch: options.branch ?? \"\",\n status: \"running\",\n startedAt: new Date(),\n completedAt: null,\n promptCount: 0,\n lastOutput: \"\",\n process: proc,\n outputBuffer: Buffer.alloc(0),\n screen: createScreen(),\n inputHistory: [],\n };\n\n this.sessions.set(sessionId, session);\n // Resume re-uses the same boot path as a fresh launch: --resume replays the\n // JSONL into Claude's TUI, which can take several seconds before the prompt\n // is reachable. Until then, raw pty.write() bytes land in the boot UI and\n // are swallowed (the \"dot bug\" — first message vanishes, second message\n // appears to trigger both). Same pendingReady + flush gating as startFresh.\n this.pendingReady.add(sessionId);\n\n proc.onData((data: string) => {\n this.handleOutput(sessionId, data);\n });\n\n proc.onExit(({ exitCode }: { exitCode: number }) => {\n this.pendingReady.delete(sessionId);\n this.handleExit(sessionId, exitCode);\n });\n\n return toPublicSession(session);\n }\n\n // Start a brand-new Claude session. A stable UUID is generated here and passed\n // to Claude via --session-id so the JSONL filename matches from the start.\n // onReady fires once Claude reaches its first prompt (waiting_input).\n async startFresh(options: StartFreshSessionOptions): Promise<ManagedSession> {\n const nodePty = await loadPty();\n const sessionId = randomUUID();\n const projectName = options.projectName ?? basename(options.projectPath);\n\n // `--permission-mode` defaults to acceptEdits for the same reason as start()\n // above — do not allow bypassPermissions (TUI warning gate). Guarded by\n // __tests__/pty-ready-detection.test.ts.\n const args = [\n \"--permission-mode\",\n options.permissionMode ?? \"acceptEdits\",\n \"--settings\",\n '{\"spinnerTipsEnabled\":false}',\n \"--session-id\",\n sessionId,\n ];\n if (options.systemPrompt) {\n args.push(\"--system-prompt\", options.systemPrompt);\n }\n\n const proc = nodePty.spawn(resolveClaudeExe(), args, {\n name: \"xterm-256color\",\n cols: 120,\n rows: 40,\n cwd: options.projectPath,\n env: buildSpawnEnv(),\n });\n\n const session: InternalSession = {\n id: sessionId,\n provider: CLAUDE_CODE_PROVIDER,\n projectPath: options.projectPath,\n projectName,\n branch: \"\",\n status: \"running\",\n startedAt: new Date(),\n completedAt: null,\n promptCount: 0,\n lastOutput: \"\",\n process: proc,\n outputBuffer: Buffer.alloc(0),\n screen: createScreen(),\n inputHistory: [],\n };\n\n this.sessions.set(sessionId, session);\n this.pendingReady.add(sessionId);\n\n proc.onData((data: string) => {\n this.handleOutput(sessionId, data);\n });\n\n proc.onExit(({ exitCode }: { exitCode: number }) => {\n this.pendingReady.delete(sessionId);\n this.handleExit(sessionId, exitCode);\n });\n\n return toPublicSession(session);\n }\n\n // Write raw key bytes directly to the PTY without bracketed-paste wrapping.\n // Use for control sequences (arrow keys, Enter) that must not be quoted.\n sendKeys(sessionId: string, keys: string): void {\n const session = this.sessions.get(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n if (session.status === \"idle\") {\n throw new Error(`Session is idle (no active PTY): ${sessionId}`);\n }\n if (session.status === \"waiting_input\") {\n session.status = \"running\";\n this.onStatusChange?.(toPublicSession(session));\n }\n this.log.info(\n `[pty.keys.write] ${sessionId.slice(0, 8)} bytes=${keys.length} digest=${digestBytes(keys)}`,\n { event: \"pty.keys_write\", sessionId, byteLen: keys.length },\n );\n session.process.write(keys);\n session.lastActivityAt = new Date();\n }\n\n sendInput(sessionId: string, input: string): number {\n const session = this.sessions.get(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n if (session.status === \"idle\") {\n throw new Error(`Session is idle (no active PTY): ${sessionId}`);\n }\n // Claude is still booting (TUI not yet at first prompt). Writing into the\n // raw PTY now would let the startup UI swallow the keystrokes. Queue and\n // flush in flushQueuedInputs() once the prompt marker fires.\n if (this.pendingReady.has(sessionId)) {\n const queue = this.queuedInputs.get(sessionId) ?? [];\n queue.push(input);\n this.queuedInputs.set(sessionId, queue);\n session.lastActivityAt = new Date();\n session.promptCount++;\n // Surface that input is being held because Claude hasn't yet emitted a\n // prompt marker. If you see this without a corresponding pty.ready\n // follow-up, the marker detection has regressed.\n this.log.warn(\n `[pty.input.queued] ${sessionId.slice(0, 8)} promptCount=${session.promptCount} queueLen=${queue.length}`,\n {\n event: \"pty.input_queued\",\n sessionId,\n promptCount: session.promptCount,\n queueLen: queue.length,\n inputLen: input.length,\n },\n );\n return session.promptCount;\n }\n if (session.status === \"waiting_input\") {\n session.status = \"running\";\n this.onStatusChange?.(toPublicSession(session));\n }\n this.writeSubmit(sessionId, session, input, \"direct\", session.promptCount + 1);\n session.lastActivityAt = new Date();\n session.promptCount++;\n return session.promptCount;\n }\n\n // Two-step paste-then-submit. Writes the bracketed-paste body, then waits\n // for the PTY to go quiet before writing \\r. See buildPasteBytes() for why\n // the split matters.\n //\n // The wait is quiescence-based, not a flat delay: a fixed SUBMIT_DELAY_MS\n // timer (the original fix) still races a TUI that's mid-redraw of its own\n // output (e.g. re-painting right after posting a question) when the paste\n // lands — the timer can elapse and fire \\r while the TUI is still busy,\n // and that \\r gets absorbed by the redraw instead of submitting (see\n // 2026-07 session 14dda340: a \"Yes\" reply was accepted into the input line\n // but never landed as a submitted JSONL turn). Polling in SUBMIT_DELAY_MS\n // steps and only submitting once lastChunkAt hasn't advanced for a full\n // step gives the TUI as many extra ticks as it needs, capped at\n // SUBMIT_MAX_WAIT_MS so a silent/wedged PTY still gets its \\r eventually.\n private writeSubmit(\n sessionId: string,\n session: InternalSession,\n input: string,\n path: \"direct\" | \"flush\",\n promptCount: number,\n ): void {\n this.recordUserMessage(session, input);\n const pasteBytes = buildPasteBytes(input);\n this.log.info(\n `[pty.input.write] ${sessionId.slice(0, 8)} promptCount=${promptCount} bytes=${pasteBytes.length} digest=${digestBytes(pasteBytes)}`,\n {\n event: \"pty.input_write\",\n sessionId,\n promptCount,\n byteLen: pasteBytes.length,\n digest: digestBytes(pasteBytes),\n path,\n phase: \"paste\",\n },\n );\n const pasteAt = Date.now();\n session.process.write(pasteBytes);\n\n const trySubmit = () => {\n const current = this.sessions.get(sessionId);\n if (!current || current !== session) return;\n const now = Date.now();\n const lastChunk = this.lastChunkAt.get(sessionId) ?? pasteAt;\n const quiet = now - lastChunk >= SUBMIT_DELAY_MS;\n const timedOut = now - pasteAt >= SUBMIT_MAX_WAIT_MS;\n if (!quiet && !timedOut) {\n setTimeout(trySubmit, SUBMIT_DELAY_MS);\n return;\n }\n this.log.info(\n `[pty.input.submit] ${sessionId.slice(0, 8)} promptCount=${promptCount} digest=\\\\r waitedMs=${now - pasteAt} timedOut=${timedOut}`,\n {\n event: \"pty.input_write\",\n sessionId,\n promptCount,\n byteLen: SUBMIT_BYTES.length,\n digest: \"\\\\r\",\n path,\n phase: \"submit\",\n waitedMs: now - pasteAt,\n timedOut,\n },\n );\n current.process.write(SUBMIT_BYTES);\n };\n setTimeout(trySubmit, SUBMIT_DELAY_MS);\n }\n\n // Drain any inputs that were sent while the session was still pendingReady,\n // writing them in arrival order now that Claude is at its prompt.\n private flushQueuedInputs(sessionId: string): void {\n const queue = this.queuedInputs.get(sessionId);\n if (!queue || queue.length === 0) return;\n this.queuedInputs.delete(sessionId);\n const session = this.sessions.get(sessionId);\n if (!session) return;\n this.log.info(`[pty.flush] ${sessionId.slice(0, 8)} flushing ${queue.length} queued input(s)`, {\n event: \"pty.flush_queued\",\n sessionId,\n queueLen: queue.length,\n });\n // Chain queued inputs serially: each writeSubmit() defers its \\r by\n // SUBMIT_DELAY_MS, and we further stagger subsequent inputs by 2x the\n // delay so paste/submit pairs don't interleave on the wire. Two queued\n // inputs is rare in practice (user tapped Send twice during the brief\n // boot window), but ordering must still produce two distinct submits.\n queue.forEach((input, i) => {\n const writeAt = i * SUBMIT_DELAY_MS * 2;\n if (writeAt === 0) {\n this.writeSubmit(sessionId, session, input, \"flush\", session.promptCount);\n } else {\n setTimeout(() => {\n const current = this.sessions.get(sessionId);\n if (!current || current !== session) return;\n this.writeSubmit(sessionId, session, input, \"flush\", session.promptCount);\n }, writeAt);\n }\n });\n }\n\n cancel(sessionId: string): void {\n const session = this.sessions.get(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n session.process.kill(\"SIGINT\");\n }\n\n killPid(pid: number): void {\n try {\n process.kill(pid, \"SIGTERM\");\n } catch {\n // Process may already be gone\n }\n }\n\n // Kill the PTY and mark the session idle. Called by the WS grace timer.\n putOnHold(sessionId: string): void {\n const session = this.sessions.get(sessionId);\n if (!session) return;\n this.pendingReady.delete(sessionId);\n this.queuedInputs.delete(sessionId);\n this.firstChunkAt.delete(sessionId);\n this.permissionOpen.delete(sessionId);\n this.lastScreenQuestionKey.delete(sessionId);\n this.shellPromptOpen.delete(sessionId);\n this.quietCheckers.get(sessionId)?.cancel();\n this.quietCheckers.delete(sessionId);\n try {\n session.process.kill(\"SIGINT\");\n } catch {\n // already dead\n }\n session.status = \"idle\";\n session.completedAt = new Date();\n session.screen.dispose();\n this.sessions.delete(sessionId);\n this.onStatusChange?.(toPublicSession(session));\n }\n\n getOutput(sessionId: string): string {\n const session = this.sessions.get(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n return session.outputBuffer.toString(\"utf-8\");\n }\n\n // Render the last `maxLines` rows of the session's screen in true on-screen\n // order. Reads the headless terminal (fed raw PTY bytes in handleOutput) so\n // Claude's absolute-cursor repaints resolve to where they actually paint —\n // unlike the old raw-byte slice, which scrambled order after a TUI repaint\n // and made replayed conversations appear out of order on resume.\n //\n // Async because xterm parses writes on a deferred tick; we flush pending\n // writes (empty write + callback) before reading so the buffer is current.\n async getOutputLines(sessionId: string, maxLines: number): Promise<string[]> {\n const session = this.sessions.get(sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n await new Promise<void>((resolve) => session.screen.write(\"\", () => resolve()));\n\n const buf = session.screen.buffer.active;\n const lines: string[] = [];\n // buf.length spans scrollback + viewport; iterate the whole thing top-down\n // so the rendered output preserves screen order, then keep the last N.\n for (let y = 0; y < buf.length; y++) {\n lines.push(buf.getLine(y)?.translateToString(true) ?? \"\");\n }\n // Drop trailing blank rows (the unused bottom of the viewport) before\n // trimming to maxLines, so replay isn't padded with empty lines.\n while (lines.length > 0 && lines[lines.length - 1] === \"\") {\n lines.pop();\n }\n return lines.slice(-maxLines);\n }\n\n getInputHistory(sessionId: string): UserMessage[] {\n return this.sessions.get(sessionId)?.inputHistory ?? [];\n }\n\n // Record a submitted user message as ground truth and fire onUserMessage.\n // Called from writeSubmit (both direct and flush paths) — never from\n // sendKeys, so raw keystrokes aren't logged as messages.\n private recordUserMessage(session: InternalSession, text: string): void {\n const ts = Date.now();\n session.inputHistory.push({ text, ts });\n if (session.inputHistory.length > INPUT_HISTORY_MAX) {\n session.inputHistory.shift();\n }\n this.onUserMessage?.(session.id, text, ts);\n }\n\n getSession(sessionId: string): ManagedSession | null {\n const session = this.sessions.get(sessionId);\n return session ? toPublicSession(session) : null;\n }\n\n hasSession(sessionId: string): boolean {\n return this.sessions.has(sessionId);\n }\n\n listSessions(): ManagedSession[] {\n return Array.from(this.sessions.values()).map(toPublicSession);\n }\n\n dispose(): void {\n for (const session of this.sessions.values()) {\n try {\n session.process.kill();\n } catch {\n // Process may already be dead\n }\n session.screen.dispose();\n }\n this.sessions.clear();\n this.firstChunkAt.clear();\n this.chunkIndex.clear();\n this.lastChunkAt.clear();\n for (const quiet of this.quietCheckers.values()) quiet.cancel();\n this.quietCheckers.clear();\n this.permissionOpen.clear();\n this.lastScreenQuestionKey.clear();\n this.shellPromptOpen.clear();\n }\n\n private handleOutput(sessionId: string, data: string): void {\n const session = this.sessions.get(sessionId);\n if (!session) return;\n\n const chunk = Buffer.from(data, \"utf-8\");\n const now = Date.now();\n if (!this.firstChunkAt.has(sessionId)) {\n this.firstChunkAt.set(sessionId, now);\n }\n\n // Per-chunk diagnostic log. Keep until the @<path> submit bug is solved.\n const idx = (this.chunkIndex.get(sessionId) ?? 0) + 1;\n this.chunkIndex.set(sessionId, idx);\n const last = this.lastChunkAt.get(sessionId);\n this.lastChunkAt.set(sessionId, now);\n const gapMs = last == null ? 0 : now - last;\n this.log.info(\n `[pty.chunk] ${sessionId.slice(0, 8)} #${idx} +${chunk.length}B gap=${gapMs}ms status=${session.status} digest=${digestBytes(data)}`,\n {\n event: \"pty.chunk\",\n sessionId,\n chunkIndex: idx,\n chunkBytes: chunk.length,\n gapMs,\n status: session.status,\n pendingReady: this.pendingReady.has(sessionId),\n digest: digestBytes(data),\n },\n );\n\n session.outputBuffer = Buffer.concat([session.outputBuffer, chunk]);\n\n if (session.outputBuffer.length > OUTPUT_BUFFER_MAX) {\n session.outputBuffer = session.outputBuffer.subarray(\n session.outputBuffer.length - OUTPUT_BUFFER_MAX,\n );\n }\n\n // Render into the headless screen so getOutputLines() can reproduce true\n // on-screen order. write() is async (parsed on a later tick) but we never\n // read the screen synchronously after a single chunk — replay only happens\n // on subscribe, long after these writes have drained.\n session.screen.write(data);\n\n const stripped = stripAnsi(data);\n session.lastOutput = stripped;\n const matchedMarker = CLAUDE_PROMPT_MARKERS.find((m) => stripped.includes(m));\n\n if (session.status === \"running\" && matchedMarker) {\n this.markReady(sessionId, session, `marker:${matchedMarker}`);\n } else if (\n session.status === \"running\" &&\n this.pendingReady.has(sessionId) &&\n now - (this.firstChunkAt.get(sessionId) ?? now) >= PROMPT_MARKER_FALLBACK_MS\n ) {\n // Fallback: PTY has produced output for >=10s but neither marker fired.\n // Treat the session as ready so queued input doesn't sit forever.\n this.markReady(sessionId, session, \"fallback:timeout\");\n }\n\n this.onOutput?.(sessionId, data);\n\n // Live interactive-prompt detection from the PTY stream — fires the moment\n // a prompt is on screen, independent of (and ahead of) the JSONL flush.\n // Trigger-gated so we don't scrape the rendered buffer on every chunk:\n // - OSC 777 (raw byte signal) → permission gate opened.\n // - \"Enter to select\" footer (AskUserQuestion menu) → structured question.\n // - prompt-ready marker without a fresh 777 → gate may have closed.\n this.detectLivePrompts(sessionId, data, stripped).catch((err) => {\n this.log.warn(\"[pty.prompt_detect] failed\", {\n event: \"pty.prompt_detect_failed\",\n sessionId,\n err,\n });\n });\n\n // Re-arm the quiet-checker on every chunk. If no further chunk arrives for\n // QUIET_DETECT_MS, re-run the same ready/prompt detection without waiting\n // for a chunk that may never come (Claude blocked on a prompt).\n let quiet = this.quietCheckers.get(sessionId);\n if (!quiet) {\n quiet = debounce(() => this.handleQuiet(sessionId), QUIET_DETECT_MS);\n this.quietCheckers.set(sessionId, quiet);\n }\n quiet();\n }\n\n // Detect permission gates (OSC 777 + scraped options) and AskUserQuestion\n // menus from the rendered screen, firing the additive callbacks. Async because\n // reading the rendered buffer needs the xterm write queue flushed. Pure\n // detection lives in services/questions/*; this only orchestrates triggers,\n // per-session debounce, and the callbacks.\n private async detectLivePrompts(\n sessionId: string,\n rawData: string,\n stripped: string,\n ): Promise<void> {\n const session = this.sessions.get(sessionId);\n if (!session) return;\n\n const oscPermission = hasPermissionOsc(rawData);\n // Footer test on the CURRENT chunk — a cheap trigger only. The authoritative\n // test runs on the full rendered screen below (askFooterOnScreen), because\n // the OSC-777 notify and the \"Enter to select\" footer often arrive in\n // SEPARATE chunks: the OSC lands first (no footer yet), so a chunk-only test\n // misclassifies an AskUserQuestion menu as a permission gate.\n const hasAskFooter = /Enter to select/i.test(stripped);\n const hasPromptMarker = CLAUDE_PROMPT_MARKERS.some((m) => stripped.includes(m));\n // Cheap raw-chunk hint that an UNSTRUCTURED shell prompt may be on screen:\n // a y/N hint, a \"press enter\"/\"continue?\" tail, or a numbered menu row. Just\n // a trigger gate (mirrors hasAskFooter) — the real conservative matching is\n // in detectShellPrompt against the rendered tail. ponytail: substring gate.\n const hasShellPromptHint =\n /[[(]\\s*y\\s*\\/\\s*n\\s*[\\])]|press\\s+(enter|return|any key)|\\bcontinue\\b\\s*\\?|^\\s*(?:❯|>)?\\s*\\d+[.)]\\s+\\S/im.test(\n stripped,\n );\n\n // Nothing to do unless a trigger fired or a prompt we already broadcast is\n // open (so we can detect its close on the next prompt-ready).\n if (\n !oscPermission &&\n !hasAskFooter &&\n !hasShellPromptHint &&\n !this.permissionOpen.has(sessionId) &&\n !this.shellPromptOpen.has(sessionId) &&\n !this.lastScreenQuestionKey.has(sessionId)\n ) {\n return;\n }\n\n const lines = await this.getOutputLines(sessionId, 60);\n\n // Authoritative footer test on the FULL rendered screen (not just the\n // trigger chunk). The \"Enter to select · Tab/Arrow keys to navigate\" footer\n // is unique to an AskUserQuestion menu; a permission gate uses \"Tab to amend\n // · ctrl+e to explain\". When this is on screen the prompt IS an\n // AskUserQuestion — even if an OSC-777 notify also fired — so it must take\n // priority over the permission path (Claude emits OSC 777 for BOTH).\n const askFooterOnScreen = lines.some((l) => /Enter to select/i.test(l));\n\n // Diagnostic: dump the rendered window + detector verdicts when a trigger\n // fires — the on-device source of truth for prompt detection (e.g. the\n // multi-question carousel work). At debug level so it's silent under normal\n // --verbose; enable with LOG_LEVEL=debug.\n if (oscPermission || hasAskFooter || askFooterOnScreen) {\n this.log.debug?.(`[pty.prompt_detect] ${sessionId.slice(0, 8)} trigger`, {\n event: \"pty.prompt_detect\",\n sessionId,\n oscPermission,\n hasAskFooter,\n askFooterOnScreen,\n permGate: oscPermission && !askFooterOnScreen ? scrapePermissionGate(lines) : undefined,\n askQuestion: askFooterOnScreen ? detectQuestionFromScreen(lines) : undefined,\n renderedTail: lines.slice(-25),\n });\n }\n\n // ── Permission gate ──────────────────────────────────────────────\n // Skip entirely when the screen is an AskUserQuestion menu — its OSC-777\n // notify would otherwise be misread as a permission gate.\n if (oscPermission && !askFooterOnScreen) {\n const gate = scrapePermissionGate(lines);\n this.permissionOpen.add(sessionId);\n // Broadcast even if options aren't painted yet (gate: empty options) so\n // the client can show \"Claude needs permission\" immediately; a later\n // repaint with the footer/options re-broadcasts the populated gate.\n this.onPermissionChange?.(sessionId, gate ?? { options: [] });\n } else if (this.permissionOpen.has(sessionId) && !askFooterOnScreen) {\n // Gate was open. If options are still on screen, refresh (cursor moved);\n // if the prompt is ready again and the options are gone, the gate closed.\n const gate = scrapePermissionGate(lines);\n if (gate) {\n this.onPermissionChange?.(sessionId, gate);\n } else if (hasPromptMarker) {\n this.permissionOpen.delete(sessionId);\n this.onPermissionChange?.(sessionId, null);\n }\n }\n\n // ── AskUserQuestion menu (rendered, ahead of JSONL) ──────────────\n // The multi-question TUI's final \"Ready to submit your answers?\" screen is\n // detected and broadcast as a normal card (Submit answers / Cancel) so the\n // user can tap to confirm — the carousel doesn't reliably auto-submit.\n // (Roadmap: robust carousel auto-submit.)\n if (askFooterOnScreen) {\n const detected = detectQuestionFromScreen(lines);\n if (detected) {\n const key = questionContentKey(detected.questions);\n if (this.lastScreenQuestionKey.get(sessionId) !== key) {\n this.lastScreenQuestionKey.set(sessionId, key);\n this.onLiveQuestion?.(sessionId, detected.questions);\n }\n }\n } else if (this.lastScreenQuestionKey.has(sessionId) && hasPromptMarker) {\n // The menu was open but its footer is gone and Claude's prompt marker is\n // back — the question was answered (or dismissed). Clear the screen key\n // and tell the server so the pending question is cancelled; without this\n // the answered menu lingers and a later repaint can re-broadcast it.\n this.lastScreenQuestionKey.delete(sessionId);\n this.onLiveQuestionGone?.(sessionId);\n }\n\n // ── Unstructured shell prompt (read -p \"[y/N]\", CLI picker, … ) ───\n // Last fallback: only when this isn't already a structured permission gate\n // or AskUserQuestion menu. Reuses the `permission` transport so mobile\n // renders a QuestionCard with zero new event handling; each option carries\n // its literal answerKeys (y\\r / n\\r / N\\r) so the client stays dumb.\n if (!oscPermission && !askFooterOnScreen && !this.permissionOpen.has(sessionId)) {\n const shell = detectShellPrompt(lines);\n if (shell) {\n const key = `${shell.prompt}\u0000${shell.options.map((o) => o.label).join(\"\u0000\")}`;\n if (this.shellPromptOpen.get(sessionId) !== key) {\n this.shellPromptOpen.set(sessionId, key);\n this.onPermissionChange?.(sessionId, {\n prompt: shell.prompt,\n options: shell.options,\n });\n }\n } else if (this.shellPromptOpen.has(sessionId) && hasPromptMarker) {\n // Prompt is gone and Claude's marker is back (input was answered) —\n // close the card.\n this.shellPromptOpen.delete(sessionId);\n this.onPermissionChange?.(sessionId, null);\n }\n }\n }\n\n // Fired QUIET_DETECT_MS after the last PTY chunk. Re-runs the same\n // ready/prompt detection handleOutput() runs per-chunk, using the last\n // rendered output — a session blocked on a prompt (or an unmarked boot\n // screen) may never produce another chunk to trigger detection otherwise.\n private handleQuiet(sessionId: string): void {\n const session = this.sessions.get(sessionId);\n if (session?.status !== \"running\") return;\n\n if (this.pendingReady.has(sessionId)) {\n this.markReady(sessionId, session, \"quiet:timeout\");\n }\n\n this.detectLivePrompts(sessionId, \"\", session.lastOutput).catch((err) => {\n this.log.warn(\"[pty.prompt_detect] failed\", {\n event: \"pty.prompt_detect_failed\",\n sessionId,\n err,\n });\n });\n }\n\n // Transition a session from \"running\" to \"waiting_input\", clear pendingReady,\n // and flush any queued input. Idempotent: callers can invoke at any chunk.\n private markReady(sessionId: string, session: InternalSession, reason: string): void {\n session.lastActivityAt = new Date();\n session.status = \"waiting_input\";\n // Log retained on purpose: `reason=fallback:timeout` would be the only\n // signal that Claude's TUI introduced a new boot variant our markers miss.\n const elapsedMs = Date.now() - (this.firstChunkAt.get(sessionId) ?? Date.now());\n this.log.info(`[pty.ready] ${sessionId.slice(0, 8)} ${reason} (elapsed=${elapsedMs}ms)`, {\n event: \"pty.ready\",\n sessionId,\n reason,\n elapsedMs,\n });\n this.onStatusChange?.(toPublicSession(session));\n if (this.pendingReady.has(sessionId)) {\n this.pendingReady.delete(sessionId);\n this.flushQueuedInputs(sessionId);\n this.onReady?.(toPublicSession(session));\n }\n }\n\n private handleExit(sessionId: string, exitCode: number): void {\n const session = this.sessions.get(sessionId);\n if (!session) return;\n\n session.completedAt = new Date();\n session.status = \"idle\";\n\n // Instant exit with no output — diagnose the most likely cause.\n const elapsedMs = session.completedAt.getTime() - session.startedAt.getTime();\n if (exitCode !== 0 && elapsedMs < 2000 && session.lastOutput === \"\") {\n if (!existsSync(session.projectPath)) {\n session.failureReason = `Project directory not found: ${session.projectPath}`;\n } else {\n session.failureReason =\n `Process exited immediately (code ${exitCode}). ` +\n `Check that the Claude binary is installed and accessible.`;\n }\n }\n\n this.onStatusChange?.(toPublicSession(session));\n session.screen.dispose();\n this.sessions.delete(sessionId);\n this.queuedInputs.delete(sessionId);\n this.firstChunkAt.delete(sessionId);\n this.permissionOpen.delete(sessionId);\n this.lastScreenQuestionKey.delete(sessionId);\n this.shellPromptOpen.delete(sessionId);\n this.quietCheckers.get(sessionId)?.cancel();\n this.quietCheckers.delete(sessionId);\n }\n}\n\nfunction toPublicSession(s: InternalSession): ManagedSession {\n return {\n id: s.id,\n provider: s.provider ?? CLAUDE_CODE_PROVIDER,\n projectPath: s.projectPath,\n projectName: s.projectName,\n branch: s.branch,\n status: s.status,\n startedAt: s.startedAt,\n completedAt: s.completedAt,\n promptCount: s.promptCount,\n lastOutput: s.lastOutput,\n ...(s.failureReason != null && { failureReason: s.failureReason }),\n ...(s.lastActivityAt != null && { lastActivityAt: s.lastActivityAt }),\n ...(s.filePath != null && { filePath: s.filePath }),\n };\n}\n\n// Strip ANSI escape sequences for clean text preview\nfunction stripAnsi(str: string): string {\n // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional ANSI stripping\n return str.replace(/\\x1b\\[[0-9;]*[a-zA-Z]/g, \"\").replace(/\\x1b\\][^\\x07]*\\x07/g, \"\");\n}\n","// Permission-gate detection. Two independent signals:\n//\n// 1. OSC 777 escape — the deterministic TRIGGER. The PTY emits\n// `\\x1b]777;notify;Claude Code;Claude needs your permission\\x07`\n// (often tmux-wrapped: `\\x1bPtmux;\\x1b\\x1b]777;notify;…\\x1b\\`) the instant\n// a gate opens. Confirmed in live logs (pty.chunk #53/#193). This tells us\n// a gate is open; it carries no option text.\n//\n// 2. Rendered option scrape — the gate's numbered options are painted into the\n// screen grid via absolute-cursor moves, so they live in the rendered\n// headless buffer (getOutputLines), not the raw byte stream. We read the\n// ACTUAL leading numbers and the `❯` cursor — the numbers are NOT a stable\n// 1-based index (a gate can show \"2. Yes / 3. No\"), which is exactly the\n// \"2 didn't take\" bug this avoids.\n\nexport interface PermissionOption {\n /** The real leading number shown on screen (NOT a 1-based array index). */\n index: number;\n label: string;\n /**\n * Literal keystroke bytes that answer this option, when the number alone\n * isn't the answer (e.g. a y/N shell prompt answers \"y\\r\"/\"n\\r\", not \"1\\r\").\n * Additive: OSC-777 gates omit it and the client answers via `index`; the\n * unstructured shell-prompt path (detectShellPrompt) populates it.\n */\n answerKeys?: string;\n}\n\nexport interface PermissionGate {\n /** Prompt text above the options, e.g. \"Claude needs your permission to use Bash\". */\n prompt?: string;\n /**\n * The descriptive block above the prompt — the tool title, the command, and any\n * action description Claude paints inside the gate box (newline-joined). Lets the\n * client show WHAT is being permitted, not just \"Do you want to proceed?\".\n */\n detail?: string;\n options: PermissionOption[];\n /** Index value (the on-screen number) of the `❯`-highlighted option, if any. */\n cursor?: number;\n}\n\n// OSC 777 with the notify command from Claude Code. We match the stable core\n// (`]777;notify;Claude Code`) rather than the whole tmux-passthrough wrapper so\n// detection survives both the wrapped and unwrapped forms.\n// biome-ignore lint/suspicious/noControlCharactersInRegex: matching the literal OSC 777 escape\nconst OSC_777_RE = /\\x1b\\]777;notify;Claude Code;[^\\x07\\x1b]*/;\n\n/** True if a raw PTY chunk contains the Claude Code OSC 777 permission notify. */\nexport function hasPermissionOsc(rawData: string): boolean {\n return OSC_777_RE.test(rawData);\n}\n\n// A rendered option row: optional `❯` cursor, then \"N. label\". Leading spaces\n// from the box/indent are tolerated (the box gutter is stripped first). We\n// capture N and the label separately.\nconst OPTION_RE = /^\\s*(❯)?\\s*(\\d+)\\.\\s+(.+?)\\s*$/;\n\n// Chrome lines that are never the prompt: footers, box-drawing, prompt arrows.\nconst FOOTER_RE = /Enter to select|Esc to cancel|↑|↓|to navigate|to cancel/i;\nconst BOX_ONLY_RE = /^[\\s│─┌┐└┘├┤┬┴┼╭╮╰╯╱╲=_-]+$/;\nconst PROMPT_ARROW_RE = /^[\\s]*[❯›>]\\s*$/;\n\n// Strip leading and trailing box-drawing gutters (\"│ … │\") so a boxed gate\n// frame parses the same as an unboxed one.\nfunction stripGutter(line: string): string {\n return line.replace(/^\\s*[│|]\\s?/, \"\").replace(/\\s*[│|]\\s*$/, \"\");\n}\n\n/**\n * Scrape the permission gate's options + prompt from rendered screen lines.\n * Returns null when no numbered options are present (not a gate, or not painted\n * yet). Pure — no I/O.\n */\nexport function scrapePermissionGate(lines: string[]): PermissionGate | null {\n const options: PermissionOption[] = [];\n let cursor: number | undefined;\n let firstOptionLine = -1;\n\n for (let i = 0; i < lines.length; i++) {\n const m = OPTION_RE.exec(stripGutter(lines[i]));\n if (!m) continue;\n const index = Number.parseInt(m[2], 10);\n if (!Number.isFinite(index)) continue;\n if (firstOptionLine === -1) firstOptionLine = i;\n if (m[1]) cursor = index; // `❯` marks the highlighted option\n options.push({ index, label: m[3] });\n }\n\n if (options.length === 0) return null;\n\n // Prompt = nearest non-empty, non-chrome line above the first option row.\n let prompt: string | undefined;\n let promptLine = -1;\n for (let i = firstOptionLine - 1; i >= 0; i--) {\n const t = stripGutter(lines[i]).trim();\n if (t.length === 0) continue;\n if (BOX_ONLY_RE.test(lines[i].trim()) || FOOTER_RE.test(t) || PROMPT_ARROW_RE.test(t)) continue;\n prompt = t || undefined;\n promptLine = i;\n break;\n }\n\n // Detail = the descriptive block ABOVE the prompt (tool title + command +\n // action description), newline-joined. The box top, 2+ consecutive blanks (the\n // gap to prior scrollback), or a ~6-line cap ends the block — so a\n // partial-window scrape can't vacuum unrelated terminal output.\n const detail = promptLine > 0 ? scrapeDetail(lines, promptLine) : undefined;\n\n return {\n ...(prompt ? { prompt } : {}),\n ...(detail ? { detail } : {}),\n options,\n ...(cursor !== undefined ? { cursor } : {}),\n };\n}\n\nconst MAX_DETAIL_LINES = 6;\n\nfunction scrapeDetail(lines: string[], promptLine: number): string | undefined {\n const collected: string[] = []; // bottom-up, content lines only\n let blankRun = 0;\n for (let i = promptLine - 1; i >= 0; i--) {\n // Strip the gutter first: a bare \"│\" is an INTERNAL blank line, not a frame\n // edge — only the true border rows (\"╭──╮\" / \"╰──╯\") are box-only once the\n // gutter is removed.\n const t = stripGutter(lines[i]).trim();\n if (BOX_ONLY_RE.test(t)) break; // box top / frame edge\n if (t.length === 0) {\n blankRun++;\n if (blankRun >= 2) break; // gap to prior scrollback\n continue; // single blank: skip (join already separates lines)\n }\n blankRun = 0;\n if (FOOTER_RE.test(t) || PROMPT_ARROW_RE.test(t)) continue; // skip chrome, keep walking\n collected.push(t);\n if (collected.length >= MAX_DETAIL_LINES) break;\n }\n\n return collected.length > 0 ? collected.reverse().join(\"\\n\") : undefined;\n}\n","import type { AskOption, AskQuestion } from \"../../types\";\n\n// Detect a structured AskUserQuestion menu from the RENDERED screen lines\n// (getOutputLines), not JSONL. The JSONL path (detectAskUserQuestion) is\n// structurally late — the question is painted in the TUI before the tool_use\n// block flushes to disk, and a backgrounded sub-agent may write to a different\n// JSONL than the watched session. Scraping the rendered screen fires the moment\n// the menu is on screen.\n//\n// The on-screen AskUserQuestion menu looks like:\n//\n// Which area are you focused on?\n// ❯ 1. macOS / Chrome\n// 2. iOS / Safari\n// …\n// 6. Chat about this\n// Enter to select · Tab/Arrow keys to navigate · Esc to cancel\n//\n// Differences from the JSONL form: no header/description/preview (the screen\n// only shows labels), and the leading number is the on-screen index. We map it\n// to the AskQuestion shape (header/description default to \"\") so the existing\n// `question` WS event and QuestionCard render path are reused unchanged.\n\n// The footer that terminates an AskUserQuestion menu — its presence is the\n// positive signal that this block IS a structured picker (vs a permission gate,\n// which uses a different footer and y/n phrasing).\nconst ASK_FOOTER_RE = /Enter to select/i;\nconst ESC_FOOTER_RE = /Esc to cancel|to navigate/i;\n\n// An option row: optional `❯` cursor + \"N. label\". Tolerates the box indent.\nconst OPTION_RE = /^\\s*(?:❯)?\\s*(\\d+)\\.\\s+(.+?)\\s*$/;\n\n// A question header ends with \"?\" (AskUserQuestion questions are interrogative).\nconst QUESTION_RE = /\\?\\s*$/;\n\nconst BOX_ONLY_RE = /^[\\s│─┌┐└┘├┤┬┴┼╭╮╰╯╱╲=_-]+$/;\n\n// Reject permission-gate option labels so a gate (caught separately via OSC 777)\n// never doubles as a structured question.\nconst PERMISSION_LABEL_RE = /^(Yes|No)\\b/i;\n\n// The final confirmation screen of a multi-question AskUserQuestion TUI:\n// Ready to submit your answers?\n// ❯ 1. Submit answers\n// 2. Cancel\n// Enter to select · …\n// The user already answered each question via cards, so this internal step is\n// auto-confirmed by the PTY layer and must never render as its own card.\nconst SUBMIT_QUESTION_RE = /ready to submit your answers\\?/i;\nconst SUBMIT_OPTION_RE = /^submit answers?$/i;\n\n// Strip leading AND trailing box-drawing gutters (\"│ … │\") so a menu drawn\n// inside a box parses the same as an unboxed one (the trailing gutter matters\n// for the \"?\"-suffix question test).\nfunction stripBoxGutter(line: string): string {\n return line.replace(/^\\s*[│|]\\s?/, \"\").replace(/\\s*[│|]\\s*$/, \"\");\n}\n\n/**\n * Detect an AskUserQuestion menu in rendered screen lines. Returns the question\n * set (single-question; the TUI shows one picker at a time) or null.\n * Pure — no I/O.\n */\nexport function detectQuestionFromScreen(lines: string[]): { questions: AskQuestion[] } | null {\n // Find the footer that closes the menu; scan upward from it.\n let footerIdx = -1;\n for (let i = lines.length - 1; i >= 0; i--) {\n if (ASK_FOOTER_RE.test(lines[i])) {\n footerIdx = i;\n break;\n }\n }\n if (footerIdx === -1) return null;\n\n // Collect option rows above the footer. Real menus interleave each \"N. label\"\n // row with wrapped DESCRIPTION lines and may insert a box border between\n // options, so a non-option line is NOT a hard stop — it's skipped as long as\n // we haven't reached the question line yet. We stop at: the question line (ends\n // with \"?\"), a blank gap once options have started, or the top of the window.\n const options: AskOption[] = [];\n let firstOptionIdx = -1;\n for (let i = footerIdx - 1; i >= 0; i--) {\n const line = lines[i];\n const inner = stripBoxGutter(line);\n const trimmed = inner.trim();\n if (trimmed.length === 0) {\n if (options.length === 0) continue; // gap between footer and options\n break; // blank line above the option block ends it\n }\n if (BOX_ONLY_RE.test(line.trim())) continue; // box border between/around options\n if (ESC_FOOTER_RE.test(line) && options.length === 0) continue;\n // The question line terminates the option block (it's the row right above\n // the first option, after any descriptions). Stop before consuming it.\n if (options.length > 0 && QUESTION_RE.test(trimmed) && !OPTION_RE.test(inner)) break;\n const m = OPTION_RE.exec(inner);\n if (m) {\n const label = m[2].trim();\n if (PERMISSION_LABEL_RE.test(label)) return null; // it's a permission gate\n options.unshift({ label, description: \"\" });\n firstOptionIdx = i;\n }\n // else: a wrapped description / continuation line for the option below it —\n // skip it and keep scanning upward for more options.\n }\n\n if (options.length < 2 || firstOptionIdx === -1) return null;\n\n // Question = nearest non-empty, non-box line above the first option row that\n // ends with \"?\".\n let question: string | undefined;\n for (let i = firstOptionIdx - 1; i >= 0; i--) {\n const raw = stripBoxGutter(lines[i]);\n const trimmed = raw.trim();\n if (trimmed.length === 0) continue;\n if (BOX_ONLY_RE.test(lines[i].trim())) continue;\n if (QUESTION_RE.test(trimmed)) {\n question = trimmed;\n }\n break;\n }\n\n if (!question) return null;\n\n // The multi-question \"Ready to submit your answers?\" confirmation IS surfaced\n // as a real card (Submit answers / Cancel) so the user can tap to confirm —\n // the multi-question carousel doesn't reliably auto-submit. (Roadmap: robust\n // auto-submit.) So we no longer reject it here.\n\n return {\n questions: [{ question, header: \"\", multiSelect: false, options }],\n };\n}\n\n/**\n * True when the rendered screen is the AskUserQuestion \"Ready to submit your\n * answers?\" confirmation (a question line + a \"Submit answers\" option under the\n * Ask footer). Pure — no I/O. Retained as a classifier (e.g. for analytics);\n * the submit screen now renders as a normal card.\n */\nexport function isSubmitConfirmationScreen(lines: string[]): boolean {\n let hasFooter = false;\n let hasSubmitQuestion = false;\n let hasSubmitOption = false;\n for (const line of lines) {\n const inner = stripBoxGutter(line).trim();\n if (ASK_FOOTER_RE.test(line)) hasFooter = true;\n if (SUBMIT_QUESTION_RE.test(inner)) hasSubmitQuestion = true;\n const m = OPTION_RE.exec(inner);\n if (m && SUBMIT_OPTION_RE.test(m[2].trim())) hasSubmitOption = true;\n }\n return hasFooter && hasSubmitQuestion && hasSubmitOption;\n}\n\n/**\n * Stable content key for de-duping a screen-scraped question against the JSONL\n * detection of the same question (they share no toolUseId). Keyed on the\n * question text + option labels.\n */\nexport function questionContentKey(questions: AskQuestion[]): string {\n return questions\n .map((q) => `${q.question} ${q.options.map((o) => o.label).join(\",\")}`)\n .join(\"::\");\n}\n","// Detect UNSTRUCTURED blocking prompts in the rendered tail — the ones that are\n// neither an AskUserQuestion menu (detectQuestionFromScreen) nor an OSC-777\n// permission gate (scrapePermissionGate): a shell `read -p \"… [y/N]\"`, a CLI\n// picker's `(y/n)`, a numbered menu printed by a plain script, etc.\n//\n// Scraped into the SAME structured shape the permission gate emits, so mobile\n// renders it as an ordinary QuestionCard with tappable options. Each option\n// carries the LITERAL keystroke that answers it (`answerKeys`) so the client\n// stays dumb — it just sends those bytes. This is the ONE net-new detector;\n// everything downstream (event shape, mobile rendering, answer-by-keystroke) is\n// reused.\n//\n// Philosophy mirrors the existing detectors: conservative but false-positive\n// tolerant. A spurious card is recoverable (the user ignores it or taps and the\n// keys are harmless); a MISSED prompt strands a blocked PTY. We only fire on a\n// tight set of patterns anchored to the LAST non-blank rendered line.\n\nconst ENTER = \"\\r\";\n\nexport interface ShellPromptOption {\n /** Positional fallback number (1-based). Authoritative answer is `answerKeys`. */\n index: number;\n label: string;\n /** The exact bytes to send to answer this option, e.g. \"y\\r\" or \"2\\r\". */\n answerKeys: string;\n}\n\nexport interface ShellPrompt {\n prompt: string;\n options: ShellPromptOption[];\n}\n\n// A y/N family prompt: the last line ends in a bracketed/parenthesised yes-no\n// hint. Capture which letter is the default (upper-case) only to keep the label\n// order natural — both options are always offered.\n// \"Continue? [y/N]\" \"Overwrite (y/n)?\" \"Proceed [Y/n] \"\nconst YN_RE = /[[(]\\s*y\\s*\\/\\s*n\\s*[\\])]/i;\n\n// A bare confirmation with no explicit options — Enter is the only sensible\n// answer. \"Press enter to continue\", \"Press any key…\", a trailing \"Continue?\".\nconst PRESS_ENTER_RE = /press\\s+(enter|return|any key)/i;\nconst CONTINUE_RE = /\\bcontinue\\b\\s*\\??\\s*$/i;\n\n// A numbered menu row: optional cursor, \"N. label\" or \"N) label\". Same grammar\n// scrapePermissionGate uses, but here the menu is a plain script's output with\n// no OSC-777 and no Claude box.\nconst NUMBERED_RE = /^\\s*(?:❯|>)?\\s*(\\d+)[.)]\\s+(.+?)\\s*$/;\n\n// Lines that are never a prompt: Claude's own box-drawing / menu chrome. If any\n// of these appear in the tail we defer to the structured detectors and bail —\n// this detector is strictly the fallback for plain shell output.\nconst CLAUDE_CHROME_RE = /Enter to select|Esc to cancel|╭|╰|│.*│/;\nconst BOX_ONLY_RE = /^[\\s│─┌┐└┘├┤┬┴┼╭╮╰╯╱╲=_-]+$/;\n\nfunction lastNonBlank(lines: string[]): { text: string; idx: number } | null {\n for (let i = lines.length - 1; i >= 0; i--) {\n const t = lines[i].trim();\n if (t.length > 0) return { text: t, idx: i };\n }\n return null;\n}\n\n/**\n * Scrape an unstructured shell prompt from rendered screen lines. Returns null\n * when the tail isn't a recognised prompt (prose, Claude's own UI, empty).\n * Pure — no I/O.\n */\nexport function detectShellPrompt(lines: string[]): ShellPrompt | null {\n // Defer to the structured detectors if Claude's TUI is on screen. Those paths\n // own AskUserQuestion / permission gates; this is plain-shell only.\n if (lines.some((l) => CLAUDE_CHROME_RE.test(l))) return null;\n\n const last = lastNonBlank(lines);\n if (!last) return null;\n\n // ── y/N family ───────────────────────────────────────────────────\n if (YN_RE.test(last.text)) {\n return {\n prompt: last.text,\n options: [\n { index: 1, label: \"Yes\", answerKeys: `y${ENTER}` },\n { index: 2, label: \"No\", answerKeys: `n${ENTER}` },\n ],\n };\n }\n\n // ── numbered menu ────────────────────────────────────────────────\n // Fire when the last line is itself a numbered row (menu just finished\n // printing) OR when a bare \"Press enter to confirm\"-style footer trails a\n // numbered block (Codex's \"Hooks need review\" trust dialog paints its options\n // above a \"Press enter to confirm or esc to go back\" line). Collect the\n // contiguous numbered block regardless — its options must not collapse into a\n // single generic Continue button (the bare-confirmation branch below).\n const lastNumberedIdx = (() => {\n for (let i = last.idx; i >= 0; i--) {\n if (NUMBERED_RE.test(lines[i])) return i;\n }\n return -1;\n })();\n if (lastNumberedIdx >= 0) {\n const options: ShellPromptOption[] = [];\n for (let i = 0; i <= lastNumberedIdx; i++) {\n const m = NUMBERED_RE.exec(lines[i]);\n if (!m) continue;\n const num = Number.parseInt(m[1], 10);\n if (!Number.isFinite(num)) continue;\n options.push({ index: num, label: m[2].trim(), answerKeys: `${num}${ENTER}` });\n }\n if (options.length >= 2) {\n // Prompt = nearest non-chrome line above the first numbered row.\n const firstRow = lines.findIndex((l) => NUMBERED_RE.test(l));\n let prompt = \"\";\n for (let i = firstRow - 1; i >= 0; i--) {\n const t = lines[i].trim();\n if (t.length === 0 || BOX_ONLY_RE.test(t)) continue;\n prompt = t;\n break;\n }\n return { prompt: prompt || \"Select an option\", options };\n }\n }\n\n // ── bare confirmation (Enter only) ───────────────────────────────\n if (PRESS_ENTER_RE.test(last.text) || CONTINUE_RE.test(last.text)) {\n return {\n prompt: last.text,\n options: [{ index: 1, label: \"Continue\", answerKeys: ENTER }],\n };\n }\n\n return null;\n}\n","import { execFile } from \"child_process\";\nimport { platform } from \"os\";\nimport { basename, dirname } from \"path\";\nimport { isWindows } from \"./platform\";\nimport type { DiscoveredProcess } from \"./types\";\n\nexport async function discoverClaudeProcesses(): Promise<DiscoveredProcess[]> {\n if (platform() === \"win32\") return discoverWindows();\n return discoverUnix();\n}\n\nasync function discoverUnix(): Promise<DiscoveredProcess[]> {\n const pids = await getPidsUnix();\n\n const results = await Promise.all(\n pids.map(async (pid) => {\n try {\n const [cwd, args, startedAt] = await Promise.all([\n getProcessCwdUnix(pid),\n getProcessArgsUnix(pid),\n getProcessStartTimeUnix(pid),\n ]);\n const conversationId = extractResumeId(args);\n\n return {\n pid,\n projectPath: cwd,\n projectName: basename(cwd),\n branch: await readGitBranch(cwd),\n conversationId,\n startedAt,\n } satisfies DiscoveredProcess;\n } catch {\n return null;\n }\n }),\n );\n\n return results.filter((r): r is DiscoveredProcess => r !== null);\n}\n\nasync function discoverWindows(): Promise<DiscoveredProcess[]> {\n const pids = await getPidsWindows();\n\n const results = await Promise.all(\n pids.map(async (pid) => {\n try {\n const info = await getProcessInfoWindows(pid);\n if (!info) return null;\n\n return {\n pid,\n projectPath: info.cwd,\n projectName: basename(info.cwd),\n branch: await readGitBranch(info.cwd),\n conversationId: extractResumeId(info.args),\n startedAt: info.startedAt,\n } satisfies DiscoveredProcess;\n } catch {\n return null;\n }\n }),\n );\n\n return results.filter((r): r is DiscoveredProcess => r !== null);\n}\n\n// ─── Unix Helpers ──────────────────────────────────────────────────\n\nfunction run(\n cmd: string,\n args: string[],\n opts: { cwd?: string; timeout?: number } = {},\n): Promise<string> {\n return new Promise((resolve, reject) => {\n execFile(\n cmd,\n args,\n { windowsHide: isWindows, encoding: \"utf-8\", timeout: opts.timeout ?? 5000, cwd: opts.cwd },\n (err, stdout) => {\n if (err) reject(err);\n else resolve(stdout as string);\n },\n );\n });\n}\n\nasync function getPidsUnix(): Promise<number[]> {\n try {\n const output = await run(\"pgrep\", [\"-x\", \"claude\"]);\n return output\n .trim()\n .split(\"\\n\")\n .filter(Boolean)\n .map((s) => Number.parseInt(s, 10));\n } catch {\n return [];\n }\n}\n\nasync function getProcessCwdUnix(pid: number): Promise<string> {\n const output = await run(\"lsof\", [\"-p\", String(pid), \"-a\", \"-d\", \"cwd\", \"-Fn\"]);\n const match = output.match(/n(.+)/);\n return match?.[1] ?? \"\";\n}\n\nasync function getProcessArgsUnix(pid: number): Promise<string> {\n return (await run(\"ps\", [\"-p\", String(pid), \"-o\", \"args=\"])).trim();\n}\n\nasync function getProcessStartTimeUnix(pid: number): Promise<Date> {\n const raw = (await run(\"ps\", [\"-p\", String(pid), \"-o\", \"lstart=\"])).trim();\n const d = new Date(raw);\n return Number.isNaN(d.getTime()) ? new Date() : d;\n}\n\n// ─── Windows Helpers ───────────────────────────────────────────────\n\nasync function getPidsWindows(): Promise<number[]> {\n try {\n const output = await run(\"tasklist\", [\"/FI\", \"IMAGENAME eq claude.exe\", \"/FO\", \"CSV\", \"/NH\"]);\n return output\n .trim()\n .split(\"\\n\")\n .filter(Boolean)\n .map((line) => {\n const parts = line.split(\",\");\n return Number.parseInt(parts[1]?.replace(/\"/g, \"\") ?? \"0\", 10);\n })\n .filter((pid) => pid > 0);\n } catch {\n return [];\n }\n}\n\nasync function getProcessInfoWindows(\n pid: number,\n): Promise<{ cwd: string; args: string; startedAt: Date } | null> {\n try {\n const output = await run(\"wmic\", [\n \"process\",\n \"where\",\n `ProcessId=${pid}`,\n \"get\",\n \"CommandLine,CreationDate,ExecutablePath\",\n \"/FORMAT:CSV\",\n ]);\n // wmic uses CRLF; split on \\r?\\n so the blank separator line becomes \"\" and is filtered out.\n const lines = output\n .trim()\n .split(/\\r?\\n/)\n .filter((l) => l.trim().length > 0);\n if (lines.length < 2) return null;\n\n const parts = lines[1].split(\",\");\n const args = parts[1] ?? \"\";\n const creationDate = parts[2] ?? \"\";\n\n // WMIC CreationDate format: 20260418153000.000000+000\n const year = creationDate.slice(0, 4);\n const month = creationDate.slice(4, 6);\n const day = creationDate.slice(6, 8);\n const hour = creationDate.slice(8, 10);\n const min = creationDate.slice(10, 12);\n const sec = creationDate.slice(12, 14);\n const startedAt = new Date(`${year}-${month}-${day}T${hour}:${min}:${sec}`);\n if (Number.isNaN(startedAt.getTime())) return null;\n\n // CWD is not directly available via wmic; use the executable path's parent directory as fallback\n const exePath = parts[3] ?? \"\";\n const cwd = exePath ? dirname(exePath) : \"\";\n\n return { cwd, args, startedAt };\n } catch {\n return null;\n }\n}\n\n// ─── Shared Helpers ────────────────────────────────────────────────\n\nfunction extractResumeId(args: string): string | null {\n const match = args.match(/--resume\\s+(\\S+)/);\n return match?.[1] ?? null;\n}\n\nasync function readGitBranch(dir: string): Promise<string> {\n try {\n return (\n await run(\"git\", [\"rev-parse\", \"--abbrev-ref\", \"HEAD\"], { cwd: dir, timeout: 3000 })\n ).trim();\n } catch {\n return \"\";\n }\n}\n","import { createNodeWebSocket } from \"@hono/node-ws\";\nimport { Connection, Client as TemporalClient } from \"@temporalio/client\";\nimport {\n applyIncludeFilter,\n applyPagination,\n applyProjectFilter,\n applySort,\n type Conversation,\n type ConversationMeta,\n ConversationScanner,\n type FileStatEntry,\n type SortOrder,\n search,\n} from \"@threadbase-sh/scanner\";\nimport { EventEmitter } from \"events\";\nimport {\n createReadStream,\n existsSync,\n watch as fsWatch,\n readdirSync,\n readFileSync,\n statSync,\n} from \"fs\";\nimport { realpath } from \"fs/promises\";\nimport type { Hono } from \"hono\";\nimport { createServer, type IncomingMessage, type ServerResponse } from \"http\";\nimport { homedir } from \"os\";\nimport { dirname, join } from \"path\";\nimport { createInterface } from \"readline\";\nimport type { WebSocket } from \"ws\";\nimport { type AgentClient, createAgentClient } from \"./agent/agent-client\";\nimport { type AgentConfig, readAgentConfig } from \"./agent/agent-config\";\nimport { type ConversationWriter, createConversationWriter } from \"./agent/conversation-writer\";\nimport { handleSendAgentInput } from \"./agent/handle-send-agent-input\";\nimport { handleStartAgentSession } from \"./agent/handle-start-agent-session\";\nimport { type AppEnv, createHonoApp } from \"./api/app\";\nimport { ALREADY_HANDLED } from \"./api/routes/sessions.routes\";\nimport { createWsRoutes } from \"./api/routes/ws.routes\";\nimport type { ApiDeps } from \"./api/types/api-deps\";\nimport {\n generateApiKey,\n loadBrowseRoot,\n loadBrowserCors,\n loadCacheDir,\n loadDefaultPermissionMode,\n loadPublicUrl,\n loadTailSize,\n setApiKey,\n validatePublicUrl,\n} from \"./auth\";\nimport {\n BrowsePathNotFoundError,\n createDirectory,\n listDirectories,\n resolveBrowsePath,\n} from \"./browse\";\nimport { ConversationCache, type ConversationListItem } from \"./conversation-cache\";\nimport { createPool, getDbConfig, maskConnectionString, runMigrations } from \"./db\";\nimport { CacheMetadataRepository } from \"./db/repositories/cacheMetadata.repository\";\nimport { ConversationsRepository } from \"./db/repositories/conversations.repository\";\nimport { ProjectsRepository } from \"./db/repositories/projects.repository\";\nimport { SessionsRepository } from \"./db/repositories/sessions.repository\";\nimport { recordUpload } from \"./db/upload-records\";\nimport { handleListProjects } from \"./handlers/handleListProjects\";\nimport { LiveSessionManager } from \"./live-session-manager\";\nimport { getLogger } from \"./logger\";\nimport { PairTokenStore } from \"./pair-store\";\nimport { discoverClaudeProcesses } from \"./process-discovery\";\nimport {\n CLAUDE_CODE_PROVIDER,\n CODEX_CLI_PROVIDER,\n coerceProviderForRunner,\n isProviderName,\n isProviderResumable,\n} from \"./providers\";\nimport { seal } from \"./seal\";\nimport { ConversationWatcher } from \"./services/conversations/conversationWatcher\";\nimport {\n findSearchTarget,\n type SearchableMessage,\n} from \"./services/conversations/findSearchTarget\";\nimport { parseAgentEntrypointsEnv } from \"./services/conversations/isAgentConversation\";\nimport { pruneAgentConversations } from \"./services/conversations/pruneAgentConversations\";\nimport { deriveProjectChatTitle } from \"./services/projectChats/deriveProjectChatTitle\";\nimport { questionContentKey } from \"./services/questions/detectQuestionFromScreen\";\nimport {\n questionsFromLines,\n shouldBroadcastQuestion,\n} from \"./services/questions/questionBroadcast\";\nimport { resolveAnswer } from \"./services/questions/resolveAnswer\";\nimport { SessionStore } from \"./session-store\";\nimport type {\n AskQuestion,\n DiscoveredProcess,\n PermissionOption,\n ServerConfig,\n SessionSortKey,\n SortOrder as SessionSortOrder,\n SessionStatus,\n} from \"./types\";\n\nimport { saveUploadFile } from \"./uploads\";\nimport { isCodexInjectedContext, toClientConversationLines } from \"./utils/codexConversationLine\";\nimport { computeConversationEtag } from \"./utils/conversationEtag\";\nimport { debounce } from \"./utils/debounce\";\nimport { isScannedSnapshotStale } from \"./utils/isScannedSnapshotStale\";\nimport { createScanProgressThrottle } from \"./utils/scanProgressThrottle\";\nimport { WSHub } from \"./ws-hub\";\n\nconst BROWSE_SYSTEM_PROMPT = (browseRoot: string) =>\n `You are working within the project boundary: ${browseRoot}. ` +\n `Do not read, write, or execute commands that access files or directories outside this boundary.`;\n\nconst DEFAULT_SYSTEM_PROMPT =\n \"When presenting options or choices to the user, limit the options to at most 3.\";\n\nconst DEFAULT_PTY_GRACE_PERIOD_MS = 270_000; // 4.5 minutes\n\n// A completed refreshFile within this window is treated as fresh — a retry\n// storm on a live conversation collapses to one parse per window instead of\n// one per request.\nconst REFRESH_TTL_MS = 2000;\n\n// Session start blocks for the PTY to reach waiting_input/idle before\n// responding; past this we fall back to the async 202 shape. Must stay BELOW\n// the mobile client's start-request fetch timeout (15s) — at the old 15s value\n// the client aborted first (\"fetch canceled\") and its retry double-spawned\n// sessions. Ready normally lands well under this: Claude's quiet-checker and\n// Codex's CODEX_READY_FALLBACK_MS (8s) both settle pendingReady first.\nconst START_READY_TIMEOUT_MS = 10_000;\n\n// Default OFF. Set to \"1\" or \"true\" to show Claude Agent SDK / claude-mem\n// runs in /api/conversations and /project-chats.\nexport function parseIncludeAgentsEnv(raw: string | undefined): boolean {\n if (raw === undefined) return false;\n const v = raw.trim().toLowerCase();\n return !(v === \"0\" || v === \"false\" || v === \"no\" || v === \"off\" || v === \"\");\n}\n\nexport class StreamerServer {\n private httpServer: ReturnType<typeof createServer>;\n private ptyManager: LiveSessionManager;\n private sessionStore: SessionStore;\n private wsHub: WSHub;\n private fileWatcher: ConversationWatcher;\n private sessionFileMap = new Map<string, string>(); // sessionId → JSONL filePath\n // Per-file seq assignments from the most recent onNewLineSpans (offset index),\n // handed to the immediately-following onNewLines so it can stamp WS `seq` on\n // the matching conversation_events entries. Same read → same lines order.\n private pendingLineSeqs = new Map<string, (number | null)[]>();\n private pendingQuestions = new Map<string, { toolUseId: string; questions: AskQuestion[] }>();\n // Content key of the AskUserQuestion currently broadcast for a session (from\n // either the rendered screen or JSONL), used to de-dupe the two paths: when\n // the screen detection fires first, the later JSONL flush of the same question\n // is suppressed. Cleared alongside pendingQuestions.\n private pendingQuestionKey = new Map<string, string>();\n // Per-session permission gate currently open (scraped via OSC 777). Parallel\n // to pendingQuestions; mobile answers it by sending the option index via\n // /input { keys }. Cleared when the gate closes.\n private pendingPermission = new Map<\n string,\n { prompt?: string; detail?: string; options: PermissionOption[]; cursor?: number }\n >();\n private scanner: ConversationScanner | null = null;\n // Set when better-sqlite3 is unusable (e.g. node ABI mismatch made\n // ConversationCache.open throw), or when config.scannerPersistent is false\n // (test isolation — the scanner's default SQLite index is a single shared\n // file unscoped by scanProfiles). All scanners are then built with\n // persistent: false so requests serve from disk instead of 500ing on\n // every touch of the scanner's own SQLite index.\n private scannerPersistenceDisabled = false;\n // Tracks every ConversationScanner ever created so close() can shut them all\n // down and release SQLite handles (open handles block temp-dir deletion on Windows).\n private allScanners = new Set<ConversationScanner>();\n private scannerReady: Promise<unknown> | null = null;\n // Set by onConversationChanged while a scan is in-flight; getScanner() does\n // a single rescan after the current one completes instead of restarting it.\n private scannerStale = false;\n // Single-flight + TTL guard around scanner.refreshFile (see refreshFileGuarded).\n // A live file's mtime is always newer than the snapshot, so an unguarded\n // refresh fires on every request and re-parses the whole file from byte 0.\n // Keyed by filePath; entries drop on settle + TTL expiry, so the map stays\n // bounded by the active-file set.\n private refreshInFlight = new Map<string, { promise: Promise<unknown>; completedAt: number }>();\n // True only while bindWithRetry is actively retrying. The persistent\n // listener-level 'error' handler demotes EADDRINUSE to debug during this\n // window so the self-healing kickstart-relaunch race doesn't spam warn.\n private binding = false;\n private cacheReady = false;\n // Every fire-and-forget task that runs a scan and then writes to this.cache\n // in an async continuation (startup warm-up, background count refresh, …).\n // close() awaits all of them before closing this.cache, so a scan's post-scan\n // cache writes (upsertFromScannerMeta / populateTailFromFile / pruneGhostFiles\n // / reconcileDeletions) can never hit a cache.db that was already closed\n // (\"database connection is not open\"), which would otherwise leave the cache\n // empty. Register via trackCacheWrite(); each entry removes itself on settle.\n private inFlightCacheWrites = new Set<Promise<unknown>>();\n private apiKey: string;\n private apiKeySource: \"config\" | \"cli\";\n private localNoAuth: boolean;\n private logMenubarRequests: boolean;\n private verbose: boolean;\n private scanProfiles:\n | Array<{ id: string; label: string; configDir: string; enabled: boolean; emoji: string }>\n | undefined;\n private dbPool: Awaited<ReturnType<typeof createPool>> | null = null;\n private dbInstanceId: string | null = null;\n private disableDb = false;\n private browseRoot: string | null = null;\n private publicUrl: string | null = null;\n private browserCors: string | undefined;\n private pairTokens = new PairTokenStore();\n private exchangeAttempts = new Map<string, number[]>();\n private sessionStartAttempts = new Map<string, number[]>();\n private sessionInputAttempts = new Map<string, number[]>();\n private ptyGracePeriodMs: number;\n private defaultSystemPrompt: string;\n private defaultPermissionMode: \"acceptEdits\" | \"manual\";\n // Map of sessionId → grace timer; fires to kill PTY after WS disconnect\n private ptyGraceTimers = new Map<string, ReturnType<typeof setTimeout>>();\n // Map of sessionId → set of subscribed WS clients\n private sessionSubscribers = new Map<string, Set<WebSocket>>();\n // Map of clientId → WS socket (populated by the \"register\" WS handshake)\n private clientIdToWs = new Map<string, WebSocket>();\n // Reverse map for cleanup on close\n private wsToClientId = new Map<WebSocket, string>();\n private cache: ConversationCache | null = null;\n private projectsRepo: ProjectsRepository | null = null;\n private conversationsRepo: ConversationsRepository | null = null;\n private sessionsRepo: SessionsRepository | null = null;\n private cacheMetadataRepo: CacheMetadataRepository | null = null;\n private discoveryCache: {\n entries: DiscoveredProcess[];\n fetchedAt: number;\n } | null = null;\n private cacheDir: string;\n private tailSize: number;\n private directoryDebounceMs: number;\n // Trailing-debounced trigger that flags the scanner stale after a quiet\n // period, collapsing a burst of directory events into one rescan. Assigned\n // in the constructor body (NOT a field initializer) so directoryDebounceMs\n // is already set when debounce() captures the wait.\n private markScannerStaleDebounced!: ReturnType<typeof debounce>;\n private codexRoots: string[];\n private includeAgents: boolean;\n private agentEntrypoints: ReadonlySet<string>;\n private honoApp: Hono<AppEnv>;\n private log = getLogger(\"server\");\n private agentConfig: AgentConfig;\n private agentClient: AgentClient | null = null;\n private sessionStatusBus = new EventEmitter();\n\n constructor(config: ServerConfig & { apiKey: string }) {\n this.sessionStatusBus.setMaxListeners(0);\n this.apiKey = config.apiKey;\n this.apiKeySource = config.apiKeySource ?? \"config\";\n this.localNoAuth = config.localNoAuth ?? false;\n this.logMenubarRequests = config.logMenubarRequests ?? false;\n if (this.localNoAuth) {\n console.warn(\n \"[WARN] localNoAuth is ENABLED — all requests from localhost bypass authentication. \" +\n \"Do not run with --local-no-auth in shared or production environments.\",\n );\n }\n this.verbose = config.verbose ?? false;\n this.disableDb = config.disableDb ?? false;\n this.scannerPersistenceDisabled = config.scannerPersistent === false;\n this.scanProfiles = config.scanProfiles;\n this.codexRoots = config.codexRoots ?? [join(homedir(), \".codex\", \"sessions\")];\n this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;\n this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;\n this.defaultPermissionMode =\n config.defaultPermissionMode ?? loadDefaultPermissionMode() ?? \"acceptEdits\";\n this.cacheDir = config.cacheDir ?? loadCacheDir() ?? join(homedir(), \".threadbase\", \"cache\");\n this.tailSize = config.tailSize ?? loadTailSize() ?? 10;\n this.directoryDebounceMs =\n parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ??\n config.directoryScanDebounceMs ??\n 1000;\n this.markScannerStaleDebounced = debounce(() => {\n if (this.scannerReady) this.scannerStale = true;\n else this.scanner = null;\n }, this.directoryDebounceMs);\n this.includeAgents = parseIncludeAgentsEnv(process.env.THREADBASE_INCLUDE_AGENTS);\n this.agentEntrypoints = parseAgentEntrypointsEnv(process.env.THREADBASE_AGENT_ENTRYPOINTS);\n\n const rawRoot = process.env.THREADBASE_BROWSE_ROOT ?? loadBrowseRoot() ?? config.browseRoot;\n if (rawRoot) {\n realpath(rawRoot)\n .then((resolved) => {\n this.browseRoot = resolved;\n if (this.verbose) this.log.info(`Browse root: ${resolved}`, { browseRoot: resolved });\n })\n .catch(() => {\n this.log.warn(`Warning: browse root does not exist: ${rawRoot}`, { browseRoot: rawRoot });\n });\n }\n\n const rawPublicUrl = process.env.THREADBASE_PUBLIC_URL ?? config.publicUrl ?? loadPublicUrl();\n if (rawPublicUrl) {\n const result = validatePublicUrl(rawPublicUrl);\n if (result.ok) {\n this.publicUrl = result.normalized;\n if (this.verbose)\n this.log.info(`Public URL: ${this.publicUrl}`, { publicUrl: this.publicUrl });\n } else {\n this.log.warn(`Warning: ${result.error}`, { error: result.error });\n }\n }\n\n this.browserCors = config.browserCors ?? loadBrowserCors();\n\n this.sessionStore = new SessionStore();\n this.wsHub = new WSHub();\n\n this.fileWatcher = new ConversationWatcher({\n onNewLineSpans: (filePath, spans, readFrom, endOffset) => {\n // Offset index: extend the per-message byte-span index with this read's\n // lines. Fires alongside onNewLines (which writes the tail); both\n // consume the same read. Best-effort — a failure here must never break\n // the tail write or WS broadcast.\n if (!this.cache) return;\n const cache = this.cache;\n // No stale seqs from a prior read may leak into this one's WS stamping.\n this.pendingLineSeqs.delete(filePath);\n try {\n const seqs = cache.extendMessageIndex(\n filePath,\n spans,\n statSync(filePath),\n readFrom,\n endOffset,\n );\n if (seqs === null) {\n // Non-contiguous read (watcher attached at EOF after downtime, or an\n // append raced a backfill): drop the file's index and rebuild from\n // scratch. Single-flighted, tracked so close() awaits it. No seqs\n // are stamped for this read — the client refetches on reconcile.\n cache.deleteFileIndex(filePath, ConversationCache.conversationIdForFile(filePath));\n cache.clearIndexParseState(filePath);\n this.trackCacheWrite(\n cache.backfillIndex(filePath).catch((err) => {\n this.log.warn(\"offset-index.backfill_failed\", {\n event: \"offset_index.backfill_failed\",\n filePath,\n trigger: \"noncontiguous-append\",\n err,\n });\n }),\n );\n return;\n }\n // Stash for the onNewLines handler (fires next for the same read) to\n // stamp WS `seq`. spans and lines are the same set in the same order.\n this.pendingLineSeqs.set(filePath, seqs);\n } catch (err) {\n this.log.warn(\"offset-index.extend_failed\", {\n event: \"offset_index.extend_failed\",\n filePath,\n err,\n });\n }\n },\n onNewLines: (filePath, lines) => {\n // One transactional cache write for the whole batch instead of per line.\n this.cache?.updateFromLines(filePath, lines);\n for (const [sessionId, watchedPath] of this.sessionFileMap) {\n if (watchedPath === filePath) {\n // toolUseId the client currently holds for this session (set by the\n // live-screen path as `screen:…`, or a prior JSONL flush). Captured\n // BEFORE the overwrite below so we can detect an id change.\n const priorToolUseId = this.pendingQuestions.get(sessionId)?.toolUseId;\n const { messages, pending } = questionsFromLines(sessionId, lines);\n for (const p of pending) {\n this.pendingQuestions.set(sessionId, p); // last pending wins\n const t = setTimeout(() => {\n if (this.pendingQuestions.get(sessionId)?.toolUseId === p.toolUseId) {\n this.cancelPendingQuestion(sessionId);\n }\n }, 60_000);\n t.unref();\n }\n // De-dupe vs the live-screen path: if the rendered detection already\n // broadcast this exact question (same content key), don't re-render —\n // EXCEPT when the real JSONL toolUseId differs from the synthetic\n // `screen:` id the client holds. The client answers with the id it\n // was given; if it still has the screen id, resolveAnswer rejects the\n // POST as tool_use_mismatch. Re-broadcasting the real id re-syncs the\n // client (mapAskQuestionToBlock just replaces activeQuestion — the\n // card re-renders identically) so answering works.\n for (const m of messages) {\n const key = questionContentKey(m.questions);\n const broadcast = shouldBroadcastQuestion({\n newContentKey: key,\n lastContentKey: this.pendingQuestionKey.get(sessionId),\n newToolUseId: m.toolUseId,\n priorToolUseId,\n });\n this.pendingQuestionKey.set(sessionId, key);\n if (broadcast) this.wsHub.broadcast(m);\n }\n // Additive batched event (one socket write) for newer clients. When\n // the offset index assigned seqs for this read, carry them parallel\n // to lines so a client can map each event to its message_index.\n // Codex rollout lines are normalized to Claude shape here — mobile\n // parseLineToMessage only understands type:user|assistant.\n const seqs = this.pendingLineSeqs.get(filePath);\n this.broadcastConversationLines(sessionId, lines, seqs);\n break;\n }\n }\n // Seqs are consumed for this read; drop them so a later read for a file\n // with no watched session can't reuse a stale mapping.\n this.pendingLineSeqs.delete(filePath);\n },\n onConversationChanged: (filePath) => {\n // A new JSONL appeared (or changed) in a watched project directory.\n // If we hold a per-file tail for it, re-drive the tail read from here\n // too: per-file fs.watch handles can die silently (2026-07-01 incident\n // — tails went permanently quiet while directory events kept flowing),\n // and the directory watcher is the survivor that can heal them.\n this.fileWatcher.poke(filePath);\n // Invalidate only the affected file's cache row immediately (cheap\n // single-row delete; wiping the whole cache on every event would\n // prevent the warm-up from persisting while active sessions write).\n // skipIfTailed: this same append also drives the live-tail watcher's\n // updateFromLines upsert; the two fire with no ordering guarantee, so\n // never delete a row a live tail just wrote (CRITICAL #2).\n this.cache?.invalidateByFilePath(filePath, { skipIfTailed: true });\n // Debounce the global scanner-staleness flip so a burst of directory\n // events during active sessions collapses into one rescan trigger\n // after a quiet period. The debounced callback still checks\n // scannerReady at fire time, preserving the anti-infinite-loop rule\n // (never null scannerReady mid-scan).\n this.markScannerStaleDebounced();\n this.log.debug?.(`Scanner invalidated by directory event: ${filePath}`, {\n filePath,\n event: \"cache.directory_change\",\n });\n },\n onFileDeleted: (filePath) => {\n const id = this.cache?.invalidateByFilePath(filePath);\n if (id)\n this.log.info(`Cache row invalidated after JSONL delete: ${id}`, {\n id,\n filePath,\n event: \"cache.invalidate_on_unlink\",\n });\n },\n });\n\n this.ptyManager = new LiveSessionManager({\n logger: getLogger(\"pty\"),\n onOutput: (sessionId, data) => {\n this.wsHub.broadcast({ type: \"terminal_output\", sessionId, data });\n },\n onUserMessage: (sessionId, text, ts) => {\n this.wsHub.broadcast({ type: \"user_message\", sessionId, text, ts });\n },\n onPermissionChange: (sessionId, gate) => {\n this.handlePermissionChange(sessionId, gate);\n },\n onLiveQuestion: (sessionId, questions) => {\n this.handleLiveQuestion(sessionId, questions);\n },\n onLiveQuestionGone: (sessionId) => {\n // The rendered AskUserQuestion menu closed. Clear the screen-dedupe key\n // and cancel the pending question so the answered card is dismissed and\n // a later repaint can't re-broadcast it. Only acts on a screen-scoped\n // question — once the JSONL flush recorded the real toolUseId, the\n // answer path (handleSendAnswer) already cleared it.\n this.pendingQuestionKey.delete(sessionId);\n const pq = this.pendingQuestions.get(sessionId);\n if (pq?.toolUseId.startsWith(\"screen:\")) {\n this.cancelPendingQuestion(sessionId);\n }\n },\n onReady: (session) => {\n const resp = this.sessionStore.get(session.id, this.ptyAttachedIds());\n if (resp) this.wsHub.broadcast({ type: \"session_ready\", session: resp });\n },\n onStatusChange: (session) => {\n this.sessionStore.updateManaged(session.id, {\n status: session.status,\n completedAt: session.completedAt,\n ...(session.lastActivityAt != null && { lastActivityAt: session.lastActivityAt }),\n });\n // Refresh the scanner index at the end of each Claude turn so the\n // conversation is searchable with up-to-date content immediately.\n if (session.status === \"waiting_input\" || session.status === \"idle\") {\n const filePath = this.sessionFileMap.get(session.id);\n if (filePath) {\n this.getScanner()\n .then((scanner) => this.refreshFileGuarded(scanner, filePath))\n .then((meta) => {\n // meta === null means the guard coalesced/skipped this refresh\n // (already in flight or within the TTL) — not a real result.\n this.log.info(\"scanner.refreshFile: ok\", {\n event: \"scanner.refresh\",\n sessionId: session.id,\n filePath,\n trigger: session.status,\n messageCount: meta?.messageCount,\n });\n })\n .catch((err) => {\n this.log.warn(\"scanner.refreshFile: failed\", {\n event: \"scanner.refresh_failed\",\n sessionId: session.id,\n filePath,\n trigger: session.status,\n err,\n });\n });\n }\n }\n // Stop watching JSONL when PTY exits (session goes idle)\n if (session.status === \"idle\") {\n const filePath = this.sessionFileMap.get(session.id);\n if (filePath) {\n this.fileWatcher.unwatch(filePath);\n this.sessionFileMap.delete(session.id);\n this.cancelPendingQuestion(session.id);\n }\n // A gone PTY can never have an open gate; clear silently.\n this.pendingPermission.delete(session.id);\n }\n const resp = this.sessionStore.get(session.id, this.ptyAttachedIds());\n if (resp) {\n this.wsHub.broadcast({ type: \"session_update\", session: resp });\n }\n this.sessionStatusBus.emit(`status:${session.id}`, session.status);\n },\n });\n\n // ─── Multi-agent mode bootstrap ──────────────────────────────────\n // When MULTI_AGENT_FLOW is on, construct the Temporal client + JSONL\n // writer. We use Connection.lazy() so the constructor stays sync —\n // the actual gRPC connection happens on first RPC.\n this.agentConfig = readAgentConfig();\n const agentConfig = this.agentConfig;\n let conversationWriter: ConversationWriter | null = null;\n if (agentConfig.enabled) {\n const connection = Connection.lazy({\n address: agentConfig.temporal.address,\n });\n const temporalClient = new TemporalClient({\n connection,\n namespace: agentConfig.temporal.namespace,\n });\n this.agentClient = createAgentClient({\n temporalClient,\n taskQueue: agentConfig.temporal.taskQueue,\n });\n // JSONL goes next to (not inside) the SQLite cacheDir, mirroring the\n // existing convention: ~/.threadbase/conversations/.\n const conversationsBaseDir =\n agentConfig.conversationsDir || join(dirname(this.cacheDir), \"conversations\");\n conversationWriter = createConversationWriter({\n baseDir: conversationsBaseDir,\n });\n }\n const agentClient = this.agentClient;\n\n const self = this;\n const apiDeps: ApiDeps = {\n // ponytail: getter so rotateApiKey() takes effect without restarting the server\n get apiKey() {\n return self.apiKey;\n },\n localNoAuth: this.localNoAuth,\n logMenubarRequests: this.logMenubarRequests,\n rotateApiKey: () => this.rotateApiKey(),\n publicUrl: this.publicUrl,\n browseRoot: this.browseRoot,\n browserCors: this.browserCors,\n ptyManager: this.ptyManager,\n sessionStore: this.sessionStore,\n wsHub: this.wsHub,\n cache: () => this.cache,\n projectsRepo: () => this.projectsRepo,\n conversationsRepo: () => this.conversationsRepo,\n sessionsRepo: () => this.sessionsRepo,\n cacheMetadataRepo: () => this.cacheMetadataRepo,\n ptyAttachedIds: () => this.ptyAttachedIds(),\n handleListSessions: (url, res) => this.handleListSessions(url, res),\n handleSessionsCount: (res) => this.handleSessionsCount(res),\n handleGetRecentSessions: (url, res) => this.handleGetRecentSessions(url, res),\n handleGetSessionNames: (res) => this.handleGetSessionNames(res),\n handleGetSession: (id, res) => this.handleGetSession(id, res),\n handleGetOutput: (id, res) => this.handleGetOutput(id, res),\n handleSendInput: (id, req, res) => this.handleSendInput(id, req, res),\n handleSendAnswer: (id, req, res) => this.handleSendAnswer(id, req, res),\n handleCancel: (id, res) => this.handleCancel(id, res),\n handleStopSession: (id, res) => this.handleStopSession(id, res),\n handleSetSessionName: (id, req, res) => this.handleSetSessionName(id, req, res),\n handleUploadFile: (id, req, res) => this.handleUploadFile(id, req, res),\n handleAdopt: (id, res) => this.handleAdopt(id, res),\n handleResume: (req, res) => this.handleResume(req, res),\n handleStartSession: (req, res) => this.handleStartSession(req, res),\n handleListConversations: (url, res) => this.handleListConversations(url, res),\n handleConversationsCount: (url, res) => this.handleConversationsCount(url, res),\n handleGetConversation: (id, url, res, ifNoneMatch) =>\n this.handleGetConversation(id, url, res, ifNoneMatch),\n handleSearch: (url, res) => this.handleSearch(url, res),\n handleSearchTarget: (id, req, res) => this.handleSearchTarget(id, req, res),\n handleListProjects: (url, res) => handleListProjects(url, res),\n handleGetPopularProjects: (url, res) => this.handleGetPopularProjects(url, res),\n handlePairStart: (res) => this.handlePairStart(res),\n handlePairExchange: (req, res) => this.handlePairExchange(req, res),\n handleBrowse: (url, res) => this.handleBrowse(url, res),\n handleMkdir: (req, res) => this.handleMkdir(req, res),\n handleWsOpen: (ws) => {\n this.wsHub.addClient(ws);\n const sessions = this.sessionStore.list(this.ptyAttachedIds());\n ws.send(JSON.stringify({ type: \"session_list\", sessions }));\n if (this.cacheReady) {\n ws.send(JSON.stringify({ type: \"cache_ready\" }));\n }\n },\n handleWsMessage: async (ws, raw) => {\n try {\n const msg = JSON.parse(String(raw));\n if (msg.type === \"register\" && typeof msg.clientId === \"string\") {\n const oldClientId = this.wsToClientId.get(ws);\n if (oldClientId) this.clientIdToWs.delete(oldClientId);\n this.clientIdToWs.set(msg.clientId, ws);\n this.wsToClientId.set(ws, msg.clientId);\n }\n if (msg.type === \"subscribe_session\" && typeof msg.sessionId === \"string\") {\n this.addSessionSubscriber(msg.sessionId, ws);\n if (this.ptyManager.hasSession(msg.sessionId)) {\n const lines = await this.ptyManager.getOutputLines(msg.sessionId, 200);\n const userMessages = this.ptyManager.getInputHistory(msg.sessionId);\n ws.send(\n JSON.stringify({\n type: \"terminal_replay\",\n sessionId: msg.sessionId,\n lines,\n userMessages,\n }),\n );\n }\n // A gate/question can open before the client finishes subscribing\n // (Codex's startup gates fire within ~500ms of spawn) — broadcast()\n // only reaches already-subscribed sockets, so a card that opened in\n // that window is otherwise lost forever. Replay pending state the\n // same way terminal_replay does above.\n const pendingGate = this.pendingPermission.get(msg.sessionId);\n if (pendingGate) {\n this.log.info(`[ws.replay_permission] ${msg.sessionId.slice(0, 8)}`, {\n event: \"ws.replay_permission\",\n sessionId: msg.sessionId,\n });\n ws.send(\n JSON.stringify({\n type: \"permission\",\n sessionId: msg.sessionId,\n ...(pendingGate.prompt ? { prompt: pendingGate.prompt } : {}),\n ...(pendingGate.detail ? { detail: pendingGate.detail } : {}),\n options: pendingGate.options,\n ...(pendingGate.cursor !== undefined ? { cursor: pendingGate.cursor } : {}),\n }),\n );\n }\n const pendingQuestion = this.pendingQuestions.get(msg.sessionId);\n if (pendingQuestion) {\n this.log.info(`[ws.replay_question] ${msg.sessionId.slice(0, 8)}`, {\n event: \"ws.replay_question\",\n sessionId: msg.sessionId,\n });\n ws.send(\n JSON.stringify({\n type: \"question\",\n sessionId: msg.sessionId,\n toolUseId: pendingQuestion.toolUseId,\n questions: pendingQuestion.questions,\n }),\n );\n }\n }\n if (msg.type === \"hold_session\" && typeof msg.sessionId === \"string\") {\n this.startGraceTimer(msg.sessionId, 0);\n }\n } catch {\n // malformed JSON, ignore\n }\n },\n handleWsClose: (ws) => {\n const clientId = this.wsToClientId.get(ws);\n if (clientId) {\n this.clientIdToWs.delete(clientId);\n this.wsToClientId.delete(ws);\n }\n for (const [sessionId, subscribers] of this.sessionSubscribers) {\n subscribers.delete(ws);\n // ptyGracePeriodMs === 0 disables the automatic hold-on-disconnect\n // timer; an explicit hold_session message still holds immediately.\n if (subscribers.size === 0 && this.ptyGracePeriodMs > 0) {\n this.startGraceTimer(sessionId, this.ptyGracePeriodMs);\n }\n }\n },\n agentClient,\n conversationWriter,\n agentConfig,\n };\n\n this.httpServer = createServer((req, res) => this.handleRequest(req, res));\n\n // Defense-in-depth against unhandled socket errors that would otherwise\n // crash the process with \"Unhandled 'error' event\":\n //\n // 1. 'clientError' fires when the http parser rejects a request (bad\n // headers, etc.). Default behavior destroys the socket, but a stale\n // handler could leak. We respond 400 (or destroy on any I/O error)\n // and never throw.\n this.httpServer.on(\"clientError\", (_err, socket) => {\n try {\n socket.end(\"HTTP/1.1 400 Bad Request\\r\\nConnection: close\\r\\n\\r\\n\");\n } catch {\n socket.destroy();\n }\n });\n // 2. Listener-level 'error' (port in use, etc.) — log instead of crashing.\n this.httpServer.on(\"error\", (err) => {\n const e = err as NodeJS.ErrnoException;\n // While bindWithRetry is retrying, each failed listen() attempt also\n // reaches this persistent handler. That EADDRINUSE is the expected,\n // self-healing kickstart race — log it at debug, not warn, so boots stay\n // quiet. Genuine runtime errors (and the final give-up) still warn.\n if (this.binding && e.code === \"EADDRINUSE\") {\n this.log.debug?.(`httpServer error during bind: ${err.message}`, {\n error: err.message,\n event: \"http.server_error\",\n });\n return;\n }\n this.log.warn(`httpServer error: ${err.message}`, {\n error: err.message,\n event: \"http.server_error\",\n });\n });\n // 3. The WebSocket upgrade race that caused real prod crashes:\n // @hono/node-ws registers an 'upgrade' listener that does `await\n // app.request(...)` before promoting the socket. If the peer RSTs\n // during the await, the raw net.Socket emits 'error' with no listener,\n // crashing the process. Registering our own 'upgrade' listener FIRST\n // attaches a noop 'error' handler to the raw socket so the upgrade\n // abort becomes a harmless event. Node fires upgrade listeners in\n // registration order, so this must be wired before injectWebSocket().\n this.httpServer.on(\"upgrade\", (_req, socket) => {\n socket.on(\"error\", () => {\n // Intentional: a RST during the WS handshake is normal client\n // behavior (network blip, peer kill). The socket is already torn\n // down; we just need to absorb the event so Node doesn't crash.\n });\n });\n\n // createNodeWebSocket needs the real Hono app (it calls app.request() on\n // upgrade). Resolve the chicken-and-egg by creating the app without WS\n // routes first, handing it to createNodeWebSocket, then mounting the WS\n // route onto the same app instance.\n this.honoApp = createHonoApp(apiDeps);\n const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app: this.honoApp });\n this.honoApp.route(\"/\", createWsRoutes(apiDeps, upgradeWebSocket));\n injectWebSocket(this.httpServer);\n }\n\n // ─── PTY Grace Timer ────────────────────────────────────────────\n\n private ptyAttachedIds(): Set<string> {\n return new Set(this.ptyManager.listSessions().map((s) => s.id));\n }\n\n /**\n * Send a session_list to only the client that triggered this HTTP request\n * (identified by X-Client-Id header → registered WS socket). Falls back to\n * a full broadcast if no match exists (old clients, or no WS registered yet).\n */\n private broadcastOrUnicastSessionList(req: IncomingMessage): void {\n const clientId = req.headers[\"x-client-id\"];\n const ws = typeof clientId === \"string\" ? this.clientIdToWs.get(clientId) : undefined;\n const payload = {\n type: \"session_list\" as const,\n sessions: this.sessionStore.list(this.ptyAttachedIds()),\n };\n if (ws) {\n this.wsHub.unicast(ws, payload);\n } else {\n this.wsHub.broadcast(payload);\n }\n }\n\n private addSessionSubscriber(sessionId: string, ws: WebSocket): void {\n let subs = this.sessionSubscribers.get(sessionId);\n if (!subs) {\n subs = new Set();\n this.sessionSubscribers.set(sessionId, subs);\n }\n subs.add(ws);\n // Cancel any pending grace timer since someone is now watching\n const existing = this.ptyGraceTimers.get(sessionId);\n if (existing) {\n clearTimeout(existing);\n this.ptyGraceTimers.delete(sessionId);\n }\n }\n\n private startGraceTimer(sessionId: string, delayMs: number): void {\n const existing = this.ptyGraceTimers.get(sessionId);\n if (existing) clearTimeout(existing);\n\n const timer = setTimeout(() => {\n this.ptyGraceTimers.delete(sessionId);\n if (this.ptyManager.hasSession(sessionId)) {\n // Never interrupt a session mid-response. A `running` PTY is actively\n // streaming a Claude turn that hasn't flushed to the JSONL yet; killing\n // it (SIGINT) would lose the in-flight answer. Re-arm the grace timer\n // and re-check after another grace period — it only becomes eligible\n // for hold once it settles back to waiting_input/idle.\n const resp = this.sessionStore.get(sessionId, this.ptyAttachedIds());\n if (resp?.status === \"running\") {\n this.log.info(\n `[grace] session ${sessionId} still running, deferring hold`,\n { sessionId, event: \"pty.grace_defer\" },\n \"pino\",\n );\n this.startGraceTimer(sessionId, delayMs);\n return;\n }\n this.sessionSubscribers.delete(sessionId);\n this.log.info(\n `[grace] killing idle PTY for ${sessionId}`,\n { sessionId, event: \"pty.grace_kill\" },\n \"pino\",\n );\n this.ptyManager.putOnHold(sessionId);\n const held = this.sessionStore.get(sessionId, this.ptyAttachedIds());\n if (held) this.wsHub.broadcast({ type: \"session_update\", session: held });\n } else {\n this.sessionSubscribers.delete(sessionId);\n }\n }, delayMs);\n\n this.ptyGraceTimers.set(sessionId, timer);\n }\n\n get port(): number {\n const addr = this.httpServer.address();\n return typeof addr === \"object\" && addr ? addr.port : 0;\n }\n\n async listen(port: number, opts?: { awaitReady?: boolean }): Promise<void> {\n // DB is still used for upload records and other non-session purposes.\n // Session state is no longer persisted to DB.\n const dbConfig = this.disableDb ? null : getDbConfig();\n if (dbConfig) {\n this.dbPool = await createPool(dbConfig);\n this.dbInstanceId = dbConfig.instanceId;\n const masked = maskConnectionString(dbConfig.connectionString);\n this.log.info(`Database enabled: ${masked}`, {\n connectionString: masked,\n instanceId: dbConfig.instanceId,\n });\n this.log.info(`Instance ID: ${dbConfig.instanceId}`, { instanceId: dbConfig.instanceId });\n await runMigrations(this.dbPool);\n this.log.info(\"Database migrations applied\", { event: \"db.migrations_applied\" });\n }\n\n // Bind with bounded retry. `launchctl kickstart -k` kills the old prod\n // instance and relaunches immediately; even after the old process has\n // exited cleanly, the kernel can hold :PORT in a transient teardown state\n // for a beat, so the fresh instance's first bind can race into EADDRINUSE.\n // Retrying with a short backoff absorbs that window instead of leaving the\n // process listener-less (the old behavior: the listener-level 'error'\n // handler logged EADDRINUSE once and gave up, failing the deploy\n // healthcheck). On the final attempt we let the error propagate so a\n // genuinely occupied port still surfaces loudly.\n await this.bindWithRetry(port);\n\n const warmUp = new Promise<void>((resolveWarm) => {\n {\n this.log.info(`Streamer server listening on port ${port}`, {\n port,\n event: \"server.listening\",\n });\n try {\n this.cache = ConversationCache.open(\n join(this.cacheDir, \"cache.db\"),\n this.tailSize,\n undefined,\n {\n filterAgentConversations: !this.includeAgents,\n agentEntrypoints: this.agentEntrypoints,\n onAgentFileDetected: (fp) => this.fileWatcher.unwatch(fp),\n },\n );\n if (!this.includeAgents) {\n const result = pruneAgentConversations(this.cache);\n if (result.pruned > 0 || result.missing > 0) {\n this.log.info(\n `Agent conversation prune: scanned=${result.scanned} pruned=${result.pruned} missing=${result.missing}`,\n { ...result, event: \"cache.prune_agents\" },\n );\n }\n }\n const db = this.cache.getDatabase();\n this.projectsRepo = new ProjectsRepository(db);\n this.conversationsRepo = new ConversationsRepository(this.cache);\n this.sessionsRepo = new SessionsRepository(this.sessionStore);\n this.cacheMetadataRepo = new CacheMetadataRepository(db);\n // Watch the active profile dirs (or ~/.claude/projects as fallback) so\n // new JSONL files created after startup are discovered and the scanner\n // and cache are invalidated without a restart. projectsDirs() is the\n // shared source of truth with findJsonlPath's degraded-mode discovery.\n for (const dir of this.projectsDirs()) {\n this.fileWatcher.watchDirectory(dir);\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n // Loud, not swallowed: without the cache every /api/conversations*\n // request falls back to slower disk-only scans. The most common\n // cause is a better-sqlite3 ABI mismatch (node_modules built against\n // a different Node) — name the fix so it isn't rediscovered from a\n // bare failure. The serve preflight (check-sqlite-abi.ts) catches\n // the ABI case before we ever get here; this covers a cache that\n // dies for any other reason mid-run.\n const abiMismatch =\n message.includes(\"NODE_MODULE_VERSION\") ||\n message.includes(\"was compiled against a different Node.js version\");\n this.log.error(\n `ConversationCache failed to open — running WITHOUT cache; ` +\n `/api/conversations, /api/conversations/count and /project-chats serve from disk (degraded).` +\n (abiMismatch ? ` Fix: npm rebuild better-sqlite3` : \"\") +\n ` (${message})`,\n { error: message, abiMismatch, event: \"cache.open_failed\" },\n );\n // The scanner's persistent index uses the same better-sqlite3 module;\n // fall back to in-memory scans so requests keep working from disk.\n this.scannerPersistenceDisabled = true;\n }\n // Use a dedicated scanner for warm-up, independent of this.scanner, so\n // that onConversationChanged invalidations during the scan cannot cause\n // getScanner() to restart indefinitely and leave the warm-up stuck.\n const warmupStatCache = this.buildStatCache(null);\n // Scanner 0.9.4 reads statCache only in non-persistent scans.\n const warmupScanner = this.newScanner(warmupStatCache ? { persistent: false } : undefined);\n this.allScanners.add(warmupScanner);\n // Throttle the per-file onProgress firings to ~one frame per whole\n // percent (plus the final tick) so a large scan doesn't flood every\n // WebSocket client with thousands of scan_progress messages.\n const shouldEmitProgress = createScanProgressThrottle();\n const scanOpts = {\n ...(this.scanProfiles ? { profiles: this.scanProfiles } : {}),\n ...this.codexScanOpts(),\n ...(warmupStatCache ? { statCache: warmupStatCache } : {}),\n };\n warmupScanner\n .scan({\n ...scanOpts,\n onProgress: (scanned, total) => {\n if (shouldEmitProgress(scanned, total)) {\n this.wsHub.broadcast({ type: \"scan_progress\", scanned, total });\n }\n },\n })\n .then(async () => {\n // Adopt the warm-up scan as the live scanner so the first real\n // request reuses it instead of paying for a second full scan.\n // Success path only — adopting a scanner whose scan rejected would\n // pair a broken engine with a resolved scannerReady, making every\n // later request throw instantly. Guard: only adopt if nothing else\n // already owns the slot.\n if (!this.scannerReady && !this.scanner) {\n this.scanner = warmupScanner;\n this.scannerReady = Promise.resolve();\n }\n if (!this.cache) return;\n const metas = [...warmupScanner.getMetadataCache().values()] as any[];\n // upsertFromScannerMeta returns IDs of rows actually upserted\n // (excluding agent JSONLs skipped when includeAgents=false).\n // Warming tails for filtered-out IDs would hit the\n // conversation_tail.conversation_id → conversation_meta(id) FK\n // and abort the whole warm-up before pruneGhostFiles can run.\n const upsertedIds = new Set(this.cache.upsertFromScannerMeta(metas));\n const tailTargets: Array<{ id: string; filePath: string }> = [];\n for (const m of metas) {\n if (!m.filePath) continue;\n const id =\n m.sessionId ||\n m.id\n ?.split(\"/\")\n .pop()\n ?.replace(/\\.jsonl$/, \"\") ||\n m.id;\n if (upsertedIds.has(id)) tailTargets.push({ id, filePath: m.filePath });\n }\n const BATCH = 50;\n let tailFailures = 0;\n for (let i = 0; i < tailTargets.length; i += BATCH) {\n const batch = tailTargets.slice(i, i + BATCH);\n for (const t of batch) {\n try {\n this.cache.populateTailFromFile(t.id, t.filePath);\n } catch (err) {\n // Benign race: the live ConversationWatcher runs during\n // warm-up, so an active session writing/deleting its JSONL\n // fires invalidateByFilePath() → invalidate(id), which deletes\n // the conversation_meta row we just upserted. The follow-up\n // tail insert then trips the conversation_tail → conversation_meta\n // FK. Skipping is correct — the row was invalidated and gets\n // re-upserted on the next scan, and pruneGhostFiles (below)\n // reconciles any file that was genuinely deleted. We must not\n // throw here or pruneGhostFiles never runs. Logged at info (not\n // debug) so the failing id+reason is visible by default.\n tailFailures += 1;\n this.log.info(\n `populateTailFromFile skipped for ${t.id}: ${\n err instanceof Error ? err.message : String(err)\n }`,\n { id: t.id, event: \"cache.warmup_tail_failed\" },\n );\n }\n }\n await new Promise<void>((r) => setImmediate(r));\n }\n if (tailFailures > 0) {\n this.log.warn(\n `Warm-up: ${tailFailures}/${tailTargets.length} tail populates skipped (see info logs for ids)`,\n {\n failures: tailFailures,\n total: tailTargets.length,\n event: \"cache.warmup_tail_failures\",\n },\n );\n }\n const pruned = this.cache.pruneGhostFiles();\n this.log.info(`Startup ghost prune: removed ${pruned.length} stale cache rows`, {\n count: pruned.length,\n event: \"cache.prune_ghosts\",\n });\n })\n .catch((err) => {\n const message = err instanceof Error ? err.message : String(err);\n this.log.warn(`Startup cache warm-up failed: ${message}`, {\n error: message,\n event: \"cache.warmup_failed\",\n });\n })\n .finally(() => {\n this.cacheReady = true;\n this.wsHub.broadcast({ type: \"cache_ready\" });\n resolveWarm();\n });\n }\n });\n // Track the warm-up's scan→cache-write chain so close() can await it.\n this.trackCacheWrite(warmUp);\n if (opts?.awaitReady) await warmUp;\n }\n\n // Bind the HTTP listener, retrying on a transient EADDRINUSE. See the call\n // site in listen() for why the race exists (kickstart -k relaunch). Total\n // worst case ≈ 6 × 500 ms = 3 s before the final attempt rethrows.\n private async bindWithRetry(port: number, attempts = 6, delayMs = 500): Promise<void> {\n this.binding = true;\n try {\n await this.bindWithRetryLoop(port, attempts, delayMs);\n } finally {\n this.binding = false;\n }\n }\n\n private async bindWithRetryLoop(port: number, attempts: number, delayMs: number): Promise<void> {\n for (let attempt = 1; attempt <= attempts; attempt++) {\n try {\n await new Promise<void>((resolve, reject) => {\n const onError = (err: NodeJS.ErrnoException) => {\n this.httpServer.removeListener(\"listening\", onListening);\n reject(err);\n };\n const onListening = () => {\n this.httpServer.removeListener(\"error\", onError);\n resolve();\n };\n this.httpServer.once(\"error\", onError);\n this.httpServer.once(\"listening\", onListening);\n this.httpServer.listen(port);\n });\n return;\n } catch (err) {\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"EADDRINUSE\" && attempt === attempts) {\n // Final attempt exhausted on a still-busy port: this is a genuine\n // failure (not the self-healing kickstart race), so surface it once\n // before rethrowing.\n this.log.error(\n `port ${port} still busy (EADDRINUSE) after ${attempts} attempts; giving up`,\n { port, attempts, event: \"server.bind_failed\" },\n );\n }\n if (e.code !== \"EADDRINUSE\" || attempt === attempts) throw err;\n // Routine kickstart-relaunch race: log at debug (invisible by default)\n // since bindWithRetry recovers on its own within the attempt budget.\n this.log.debug?.(\n `port ${port} busy (EADDRINUSE), retry ${attempt}/${attempts - 1} in ${delayMs}ms`,\n { port, attempt, event: \"server.bind_retry\" },\n );\n await new Promise<void>((r) => setTimeout(r, delayMs));\n }\n }\n }\n\n // Register a fire-and-forget task that writes to this.cache after a scan, so\n // close() can await it before closing cache.db. Removes itself on settle. The\n // caller keeps its own error handling; this wrapper swallows rejections so a\n // failed task never rejects close()'s Promise.all.\n private trackCacheWrite(task: Promise<unknown>): void {\n const guarded = task.catch(() => undefined);\n this.inFlightCacheWrites.add(guarded);\n void guarded.finally(() => {\n this.inFlightCacheWrites.delete(guarded);\n });\n }\n\n // Single-flight + TTL wrapper for scanner.refreshFile. Both call sites (the\n // detail-stale branch and the per-turn refresh) route through here so that\n // N stacked retries on a live, actively-appended file cost one parse, not N:\n // - a refresh already in flight for the path → await the same promise;\n // - a refresh that settled within REFRESH_TTL_MS → skip, return null (the\n // caller keeps serving the current snapshot);\n // - otherwise start one, cache the promise, and stamp completedAt on settle.\n // The map is bounded by the active-file set (entries only live while a\n // refresh is in flight or within its TTL and are overwritten on the next\n // refresh of the same path).\n private refreshFileGuarded(\n scanner: ConversationScanner,\n filePath: string,\n ): Promise<Awaited<ReturnType<ConversationScanner[\"refreshFile\"]>> | null> {\n const existing = this.refreshInFlight.get(filePath);\n if (existing) {\n const settled = existing.completedAt > 0;\n if (!settled) {\n // In flight: coalesce onto the same parse.\n return existing.promise as Promise<Awaited<\n ReturnType<ConversationScanner[\"refreshFile\"]>\n > | null>;\n }\n if (Date.now() - existing.completedAt < REFRESH_TTL_MS) {\n // Completed recently enough: serve the snapshot, skip the parse.\n return Promise.resolve(null);\n }\n }\n const entry = { promise: Promise.resolve<unknown>(null), completedAt: 0 };\n entry.promise = scanner.refreshFile(filePath).finally(() => {\n entry.completedAt = Date.now();\n });\n this.refreshInFlight.set(filePath, entry);\n return entry.promise as Promise<Awaited<ReturnType<ConversationScanner[\"refreshFile\"]>> | null>;\n }\n\n async close(): Promise<void> {\n for (const timer of this.ptyGraceTimers.values()) clearTimeout(timer);\n this.ptyGraceTimers.clear();\n this.markScannerStaleDebounced.cancel();\n // Wait for every fire-and-forget scan→cache-write task to finish before\n // tearing anything down. Their post-scan steps write to this.cache\n // (upsert / populateTail / pruneGhostFiles); closing cache.db under them\n // throws \"database connection is not open\" and leaves the cache empty\n // (deterministic once Stage 4's dir-mtime gate widened the scan window).\n // Snapshot the set — entries remove themselves as they settle.\n await Promise.all([...this.inFlightCacheWrites]);\n // Close all scanner SQLite connections before the cache so file handles are\n // released on Windows (open handles block temp-dir deletion in tests).\n // scanner.close() is async (scanner >=0.9.2): it awaits any in-flight scan\n // before releasing the DB handle, so a fire-and-forget refresh scan can't be\n // shut mid-indexAll(). Await all so handles are torn down only after scans\n // settle.\n await Promise.all([...this.allScanners].map((s) => s.close()));\n this.allScanners.clear();\n this.scanner = null;\n this.cache?.close();\n this.ptyManager.dispose();\n this.fileWatcher.dispose();\n this.wsHub.dispose();\n this.pairTokens.dispose();\n if (this.dbPool) {\n await this.dbPool.end();\n }\n // Force any sockets that survived wsHub.dispose() (e.g. a half-open\n // connection mid-upgrade) to close, so httpServer.close()'s callback —\n // which only fires once every connection drains — can't hang. Without\n // this the old process keeps :PORT bound until launchd's SIGKILL, and the\n // freshly-started instance hits EADDRINUSE. Guarded for Node < 18.2.\n this.httpServer.closeAllConnections?.();\n return new Promise((resolve) => {\n // Belt-and-suspenders: never let process exit block forever on the\n // listener close. The port is released the moment closeAllConnections()\n // runs; the timeout only guards against an unforeseen lingering socket.\n const timer = setTimeout(resolve, 2000);\n this.httpServer.close(() => {\n clearTimeout(timer);\n resolve();\n });\n });\n }\n\n // ─── Request Router ────────────────────────────────────────────\n\n private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const host = req.headers.host ?? \"localhost\";\n const webReq = new Request(`http://${host}${req.url ?? \"/\"}`, {\n method: req.method ?? \"GET\",\n headers: req.headers as Record<string, string>,\n });\n const honoRes = await this.honoApp.fetch(webReq, { incoming: req, outgoing: res });\n if (honoRes.status !== ALREADY_HANDLED) {\n await writeHonoResponse(honoRes, res);\n }\n }\n\n // ─── Handlers ──────────────────────────────────────────────────\n\n private handlePairStart(res: ServerResponse): void {\n const minted = this.pairTokens.mint();\n json(res, 200, {\n token: minted.token,\n expiresAt: minted.expiresAt,\n expiresInSeconds: minted.expiresInSeconds,\n publicUrl: this.publicUrl,\n });\n }\n\n private async handlePairExchange(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const ct = req.headers[\"content-type\"] ?? \"\";\n if (!String(ct).toLowerCase().includes(\"application/json\")) {\n json(res, 415, { error: \"Content-Type: application/json required\" });\n return;\n }\n\n const ip = req.socket.remoteAddress ?? \"unknown\";\n if (!this.checkExchangeRateLimit(ip)) {\n json(res, 429, { error: \"Too many pair exchange attempts; try again in a minute\" });\n return;\n }\n\n let body: any;\n try {\n body = await readBody(req);\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Invalid body\";\n json(res, 400, { error: message });\n return;\n }\n\n const { token, clientPublicKey } = body ?? {};\n if (typeof token !== \"string\" || typeof clientPublicKey !== \"string\") {\n json(res, 400, { error: \"Missing token or clientPublicKey\" });\n return;\n }\n\n const result = this.pairTokens.consume(token);\n if (!result.ok) {\n json(res, 401, { error: `Pair token ${result.reason}` });\n return;\n }\n\n let sealed: ReturnType<typeof seal>;\n try {\n sealed = seal(this.apiKey, clientPublicKey);\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Invalid clientPublicKey\";\n json(res, 400, { error: message });\n return;\n }\n\n const { hostname } = require(\"os\");\n const ts = new Date().toISOString();\n this.log.info(`[pair] token exchanged from ${ip} at ${ts}`, {\n event: \"pair.token_exchanged\",\n ip,\n ts,\n });\n\n json(res, 200, {\n ciphertext: sealed.ciphertext,\n nonce: sealed.nonce,\n ephemeralPublicKey: sealed.ephemeralPublicKey,\n publicUrl: this.publicUrl,\n machineName: hostname(),\n });\n }\n\n private rotateApiKey(): { newKey: string; persisted: boolean } {\n const oldKey = this.apiKey;\n const newKey = generateApiKey();\n // Only persist to server.yaml when the key came from there.\n // If --api-key was passed on the CLI, the flag wins on restart and\n // would silently revert to the old key — so skip the write and let\n // the caller know via the response.\n const persisted = this.apiKeySource === \"config\";\n if (persisted) setApiKey(newKey);\n this.apiKey = newKey;\n this.log.info(\"API key rotated\", {\n event: \"auth.api_key_rotated\",\n oldKeyMasked: `${oldKey.slice(0, 6)}…`,\n newKeyMasked: `${newKey.slice(0, 6)}…`,\n persisted,\n });\n return { newKey, persisted };\n }\n\n private checkRateLimit(\n map: Map<string, number[]>,\n key: string,\n limit: number,\n windowMs: number,\n ): boolean {\n const now = Date.now();\n const arr = (map.get(key) ?? []).filter((t) => now - t < windowMs);\n if (arr.length >= limit) {\n map.set(key, arr);\n return false;\n }\n arr.push(now);\n map.set(key, arr);\n // TTL-evict the entry once the window expires so the map doesn't grow unbounded.\n setTimeout(() => {\n const remaining = (map.get(key) ?? []).filter((t) => Date.now() - t < windowMs);\n if (remaining.length === 0) map.delete(key);\n else map.set(key, remaining);\n }, windowMs);\n return true;\n }\n\n private checkExchangeRateLimit(ip: string): boolean {\n return this.checkRateLimit(this.exchangeAttempts, ip, 5, 60_000);\n }\n\n private checkSessionStartRateLimit(ip: string): boolean {\n // 10 new sessions per minute per client IP\n return this.checkRateLimit(this.sessionStartAttempts, ip, 10, 60_000);\n }\n\n private checkSessionInputRateLimit(sessionId: string): boolean {\n // 500 keystrokes per minute per session\n return this.checkRateLimit(this.sessionInputAttempts, sessionId, 500, 60_000);\n }\n\n private async handleListConversations(url: URL, res: ServerResponse): Promise<void> {\n const limit = intParam(url, \"limit\", 50);\n const offset = intParam(url, \"offset\", 0);\n const sort = (url.searchParams.get(\"sort\") ?? \"recent\") as SortOrder;\n const project = url.searchParams.get(\"project\") ?? undefined;\n const providerFilter = url.searchParams.get(\"provider\") ?? undefined;\n const bustCache = url.searchParams.get(\"refresh\") === \"1\";\n\n // refresh=1 is a RECONCILE, not a wipe. The old path did\n // cache.invalidate() (deletes every row, including live-tailed ones) +\n // scanner=null (discards the warm scanner and forces a cold cache\n // rebuild). Instead: run one fresh full-glob scan (fullRescan bypasses the\n // scanner's dir-mtime gate — an explicit user refresh is the \"check for\n // real\" signal), then reconcile the cache from disk truth: upsert what\n // exists (newest-wins, so a concurrent live line still takes precedence),\n // and drop only the rows whose files no longer exist. Live-tailed rows are\n // never blanket-deleted, so an active conversation can't flicker out.\n if (bustCache && this.cache) {\n const scanner = await this.rescanForRefresh();\n const metas = [...scanner.getMetadataCache().values()];\n try {\n this.cache.upsertFromScannerMeta(metas as any[]);\n const livePaths = new Set(\n metas.map((m) => m.filePath).filter((p): p is string => Boolean(p)),\n );\n this.cache.reconcileDeletions(livePaths);\n } catch (err) {\n this.log.warn(\n `refresh reconcile failed: ${err instanceof Error ? err.message : String(err)}`,\n { event: \"conversations.reconcile_failed\" },\n );\n }\n }\n\n if (this.cache) {\n const { conversations, total } = this.cache.listConversations({\n project,\n provider: providerFilter,\n limit,\n offset,\n });\n const adapted = conversations.map((c) => ({\n id: c.id,\n title: deriveProjectChatTitle({\n title: c.title,\n projectName: c.projectName,\n projectPath: c.projectPath,\n id: c.id,\n }),\n sessionName: undefined as string | undefined,\n filePath: c.filePath,\n projectPath: c.projectPath,\n branch: c.branch ?? undefined,\n account: c.account ?? undefined,\n preview: c.preview ?? undefined,\n messageCount: c.messageCount,\n lastActivity: c.lastActivity,\n firstMessage: c.firstMessage ? (JSON.parse(c.firstMessage) as unknown) : undefined,\n lastMessage: c.lastMessage ? (JSON.parse(c.lastMessage) as unknown) : undefined,\n model: c.model ?? undefined,\n provider: c.provider ?? CLAUDE_CODE_PROVIDER,\n }));\n json(res, 200, { conversations: adapted, hasMore: offset + limit < total, offset, total });\n return;\n }\n\n const scanner = await this.getScanner();\n let metas = [...scanner.getMetadataCache().values()];\n metas = applyIncludeFilter(metas, \"conversations\");\n if (project) metas = applyProjectFilter(metas, project);\n if (providerFilter)\n metas = metas.filter((m) => (m.provider ?? CLAUDE_CODE_PROVIDER) === providerFilter);\n metas = applySort(metas, sort);\n const total = metas.length;\n const page = applyPagination(metas, limit, offset);\n\n const adapted = (page.items as ConversationMeta[]).map((c) => {\n const id =\n c.sessionId ||\n c.id\n .split(\"/\")\n .pop()\n ?.replace(/\\.jsonl$/, \"\") ||\n c.id;\n return {\n id,\n title: deriveProjectChatTitle({\n title: c.sessionName,\n projectName: c.projectName,\n projectPath: c.projectPath,\n id,\n }),\n sessionName: c.sessionName || undefined,\n filePath: c.filePath,\n projectPath: c.projectPath,\n branch: c.gitBranch ?? undefined,\n account: c.account,\n preview: c.preview || undefined,\n messageCount: c.messageCount,\n lastActivity: c.timestamp,\n firstMessage: c.firstMessage ?? undefined,\n lastMessage: c.lastMessage ?? undefined,\n model: c.model ?? undefined,\n provider: (c as any).provider ?? CLAUDE_CODE_PROVIDER,\n };\n });\n json(res, 200, { conversations: adapted, hasMore: offset + limit < total, offset, total });\n }\n\n private async handleConversationsCount(url: URL, res: ServerResponse): Promise<void> {\n const project = url.searchParams.get(\"project\") ?? undefined;\n const providerFilter = url.searchParams.get(\"provider\") ?? undefined;\n const bustCache = url.searchParams.get(\"refresh\") === \"1\";\n\n // refresh=1 historically forced a full synchronous scan() to recount from\n // disk. On a cold/empty index that scan walks every JSONL and blocks ~16s,\n // tripping mobile's request timeout into a false \"unreachable\". Mirror the\n // detail path's skipStaleRescan stance: serve the indexed/cached total\n // immediately and reconcile from disk in the BACKGROUND so the count stays\n // fast regardless of refresh.\n if (this.cache) {\n const { total } = this.cache.listConversations({\n project,\n provider: providerFilter,\n limit: 0,\n offset: 0,\n });\n json(res, 200, { total });\n if (bustCache) this.refreshCountInBackground();\n return;\n }\n\n const scanner = await this.getScanner(true);\n let metas = [...scanner.getMetadataCache().values()];\n metas = applyIncludeFilter(metas, \"conversations\");\n if (project) metas = applyProjectFilter(metas, project);\n if (providerFilter)\n metas = metas.filter((m) => (m.provider ?? CLAUDE_CODE_PROVIDER) === providerFilter);\n json(res, 200, { total: metas.length });\n }\n\n // Fire-and-forget full rescan that reconciles the SQLite cache from disk so a\n // later count reflects new/removed conversations. Never awaited by the request\n // path — refresh=1 returns the cached total synchronously and this catches up.\n private refreshCountInBackground(): void {\n // Tracked so close() awaits this scan→cache-write before closing cache.db.\n this.trackCacheWrite(\n (async () => {\n try {\n const scanner = await this.getFreshScanner();\n if (this.cache) {\n this.cache.upsertFromScannerMeta([...scanner.getMetadataCache().values()] as any[]);\n }\n } catch (err) {\n this.log.warn(\n `Background count refresh failed: ${err instanceof Error ? err.message : String(err)}`,\n { event: \"count.refresh_failed\" },\n );\n }\n })(),\n );\n }\n\n private handleSessionsCount(res: ServerResponse): void {\n json(res, 200, { total: this.sessionStore.list(this.ptyAttachedIds()).length });\n }\n\n private handleGetRecentSessions(url: URL, res: ServerResponse): void {\n const limit = intParam(url, \"limit\", 20);\n if (!this.cache) {\n json(res, 200, { sessions: [], total: 0 });\n return;\n }\n const { conversations } = this.cache.listConversations({ limit, offset: 0 });\n // Items here are conversation cache rows, not live sessions in SessionStore.\n // The `type` discriminator lets mobile route taps through /api/sessions/resume\n // (which spawns a fresh PTY) instead of GET /api/sessions/:id (which 404s).\n const sessions = conversations.map((c) => ({\n type: \"conversation\" as const,\n id: c.id,\n status: \"idle\" as const,\n ptyAttached: false,\n projectId: c.projectId ?? undefined,\n projectPath: c.projectPath ?? \"\",\n projectName: c.projectName ?? \"\",\n branch: c.branch ?? undefined,\n lastOutput: \"\",\n elapsedMs: 0,\n promptCount: c.messageCount,\n startedAt: c.lastActivity,\n lastActivityAt: c.lastActivity,\n }));\n json(res, 200, { sessions, total: sessions.length });\n }\n\n private handleGetPopularProjects(url: URL, res: ServerResponse): void {\n const limit = intParam(url, \"limit\", 20);\n if (!this.cache) {\n json(res, 200, { projects: [], total: 0 });\n return;\n }\n const projects = this.cache.getPopularProjects(limit);\n json(res, 200, { projects, total: projects.length });\n }\n\n private buildStatCache(\n previousScanner: ConversationScanner | null,\n ): Map<string, { stat: FileStatEntry; meta: ConversationMeta }> | undefined {\n if (!this.cache) return undefined;\n if (!previousScanner) {\n const persisted = this.cache.getScannerStatCache();\n return persisted.size > 0 ? persisted : undefined;\n }\n const dbStats = this.cache.getFileStats();\n if (dbStats.size === 0) return undefined;\n const metaByPath = new Map<string, ConversationMeta>();\n if (previousScanner) {\n for (const meta of previousScanner.getMetadataCache().values()) {\n if (meta.filePath) metaByPath.set(meta.filePath, meta);\n }\n }\n const statCache = new Map<string, { stat: FileStatEntry; meta: ConversationMeta }>();\n for (const [filePath, stat] of dbStats) {\n const meta = metaByPath.get(filePath);\n if (meta) statCache.set(filePath, { stat, meta });\n }\n return statCache.size > 0 ? statCache : undefined;\n }\n\n // Returns the provider + codexRoots fragment to spread into every scan()/search() call.\n // codexRoots=[] disables codex scanning (safe no-op per scanner contract).\n private codexScanOpts() {\n return {\n providers: [CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER],\n codexRoots: this.codexRoots,\n };\n }\n\n // skipStaleRescan: when an indexed scanner already exists, return it directly\n // even if scannerStale is set, leaving the flag untouched so the next\n // list-level call still rescans. The single-conversation detail path passes\n // this — its per-file refreshFile (in findConversationByUuid) already\n // reconciles the one conversation being requested, so paying a full-tree\n // rescan just because some OTHER file changed is the stall this avoids.\n private newScanner(\n options?: ConstructorParameters<typeof ConversationScanner>[0],\n ): ConversationScanner {\n return new ConversationScanner(\n options ?? (this.scannerPersistenceDisabled ? { persistent: false } : undefined),\n );\n }\n\n private async getScanner(skipStaleRescan = false): Promise<ConversationScanner> {\n if (this.scannerReady) {\n await this.scannerReady;\n // onConversationChanged may have nulled this.scanner while we awaited —\n // if so, fall through and create a fresh one.\n if (this.scanner) {\n if (skipStaleRescan) return this.scanner;\n // If file events arrived while the scan was running, do one rescan now\n // rather than serving a stale result. The stale flag is cleared first so\n // any events during the rescan trigger another pass on the next call.\n if (this.scannerStale) {\n this.scannerStale = false;\n this.scanner = null;\n this.scannerReady = null;\n return this.getScanner();\n }\n return this.scanner;\n }\n }\n this.scannerStale = false;\n const statCache = this.buildStatCache(this.scanner);\n // Scanner 0.9.4 reads statCache only in non-persistent scans.\n this.scanner = this.newScanner(statCache ? { persistent: false } : undefined);\n this.allScanners.add(this.scanner);\n this.scannerReady = this.scanner.scan({\n ...(this.scanProfiles ? { profiles: this.scanProfiles } : {}),\n ...this.codexScanOpts(),\n ...(statCache ? { statCache } : {}),\n });\n try {\n await this.scannerReady;\n } catch (err) {\n // Don't memoize the rejection — a stored rejected promise would make\n // every future request replay this error instantly instead of retrying.\n this.scanner = null;\n this.scannerReady = null;\n throw err;\n }\n // Capture before returning — onConversationChanged could null this.scanner\n // in the microtask between the await and the return.\n const scanner = this.scanner;\n if (!scanner) return this.getScanner();\n return scanner;\n }\n\n private async getFreshScanner(): Promise<ConversationScanner> {\n this.scanner = null;\n this.scannerReady = null;\n return this.getScanner();\n }\n\n // refresh=1's scan: reuse the WARM scanner and re-run its scan with\n // fullRescan:true — the escape hatch that bypasses the scanner's\n // dir-mtime discovery gate, since an explicit user pull-to-refresh is exactly\n // the \"don't trust the gate, check disk for real\" signal. Unlike\n // getFreshScanner() this does NOT discard the warm scanner. scannerReady is\n // only ever reassigned to a live scan promise (never nulled mid-scan), so the\n // getScanner() anti-infinite-loop guard is preserved.\n private async rescanForRefresh(): Promise<ConversationScanner> {\n // Let any in-flight scan finish first so we don't run two scans on the same\n // index concurrently.\n if (this.scannerReady) await this.scannerReady;\n // A full rescan supersedes any pending staleness.\n this.scannerStale = false;\n if (!this.scanner) {\n this.scanner = new ConversationScanner();\n this.allScanners.add(this.scanner);\n }\n const scanner = this.scanner;\n this.scannerReady = scanner.scan({\n ...(this.scanProfiles ? { profiles: this.scanProfiles } : {}),\n ...this.codexScanOpts(),\n fullRescan: true,\n });\n await this.scannerReady;\n return scanner;\n }\n\n /**\n * The projects dirs disk discovery should walk — the single source of truth\n * for \"where do this server's JSONLs live\", mirroring the warm-up watcher\n * (see listen()). Derived from the enabled scanProfiles' configDirs, or the\n * real ~/.claude/projects when no profiles are configured. An all-disabled\n * profile set intentionally yields [] (nothing to discover), matching the\n * watcher — it does NOT fall back to home in that case.\n */\n private projectsDirs(): string[] {\n if (this.scanProfiles && this.scanProfiles.length > 0) {\n return this.scanProfiles.filter((p) => p.enabled).map((p) => join(p.configDir, \"projects\"));\n }\n return [join(homedir(), \".claude\", \"projects\")];\n }\n\n private findJsonlPath(uuid: string): string | null {\n const filename = `${uuid}.jsonl`;\n for (const projectsDir of this.projectsDirs()) {\n if (!existsSync(projectsDir)) continue;\n for (const dir of readdirSync(projectsDir)) {\n const fp = join(projectsDir, dir, filename);\n if (existsSync(fp)) return fp;\n const projectDir = join(projectsDir, dir);\n try {\n for (const sub of readdirSync(projectDir)) {\n const subagentPath = join(projectDir, sub, \"subagents\", filename);\n if (existsSync(subagentPath)) return subagentPath;\n }\n } catch {\n // Not a directory or no access\n }\n }\n }\n return null;\n }\n\n private async readCwdFromJsonl(filePath: string): Promise<string | null> {\n return new Promise((resolve) => {\n const rl = createInterface({ input: createReadStream(filePath), crlfDelay: Infinity });\n let found = false;\n rl.on(\"line\", (line) => {\n if (found) return;\n try {\n const entry = JSON.parse(line);\n if (entry.cwd) {\n found = true;\n rl.close();\n resolve(entry.cwd as string);\n }\n } catch {\n // skip malformed lines\n }\n });\n rl.on(\"close\", () => {\n if (!found) resolve(null);\n });\n rl.on(\"error\", () => resolve(null));\n });\n }\n\n /**\n * Live Codex sessions keep `SessionResponse.conversationId === managed.id`\n * (stable deep-link / PTY key) and store the rollout UUID separately as\n * `boundConversationId`. REST history is indexed under the rollout UUID, so\n * resolve the placeholder → bound id before looking up the scanner.\n */\n private resolveConversationLookupId(uuid: string): string {\n const managed = this.sessionStore.getManaged(uuid);\n if (managed?.boundConversationId) return managed.boundConversationId;\n return uuid;\n }\n\n /** File path for a live managed session (placeholder id or bound Codex id). */\n private findLiveSessionFilePath(uuid: string): string | null {\n const direct = this.sessionFileMap.get(uuid);\n if (direct) return direct;\n for (const s of this.sessionStore.listManaged()) {\n if (s.boundConversationId === uuid) {\n return this.sessionFileMap.get(s.id) ?? null;\n }\n }\n return null;\n }\n\n /** True when a conversation UUID is the bound rollout of a live PTY session. */\n private isBoundConversationLive(boundId: string): boolean {\n for (const s of this.sessionStore.listManaged()) {\n if (s.boundConversationId === boundId && this.ptyManager.hasSession(s.id)) {\n return true;\n }\n }\n return false;\n }\n\n /**\n * Broadcast conversation JSONL lines to WS clients. Codex rollout lines are\n * normalized to the Claude `type:user|assistant` shape mobile understands;\n * Claude lines pass through unchanged so seq alignment stays intact.\n */\n private broadcastConversationLines(\n sessionId: string,\n lines: string[],\n seqs?: (number | null)[] | null,\n ): void {\n const clientLines = toClientConversationLines(lines);\n if (clientLines.length === 0) return;\n const seqsOk = !!seqs && seqs.length === lines.length && clientLines.length === lines.length;\n this.wsHub.broadcast({\n type: \"conversation_events\",\n sessionId,\n lines: clientLines,\n ...(seqsOk ? { seqs } : {}),\n });\n // ...plus per-line conversation_event so older mobile clients,\n // which only know that shape, keep working byte-for-byte.\n for (const line of clientLines) {\n this.wsHub.broadcast({ type: \"conversation_event\", sessionId, line });\n }\n }\n\n private async findConversationByUuid(uuid: string): Promise<Conversation | null> {\n const lookupId = this.resolveConversationLookupId(uuid);\n\n // Cold-start fast path: until the warm-up scan has populated this.scanner\n // (this.scannerReady is null), do NOT trigger a full scan to answer a\n // single-conversation request — that scan walks every JSONL on disk and is\n // the 20s+ stall that makes mobile abort. Resolve the file directly\n // (findJsonlPath is an O(project-dirs) walk) and parse just that one file.\n // The warm-up scan keeps running in the background; once it adopts the\n // scanner, subsequent requests use the indexed hot path below.\n if (!this.scannerReady && !this.scanProfiles) {\n const filePath =\n this.findJsonlPath(lookupId) ??\n this.findLiveSessionFilePath(uuid) ??\n this.findLiveSessionFilePath(lookupId);\n if (filePath) {\n const account = this.cache?.getMetaById(lookupId)?.account ?? undefined;\n const coldScanner = this.scanner ?? this.newScanner();\n const page = await coldScanner.parseSingleFilePage(filePath, account, {\n limit: Number.MAX_SAFE_INTEGER,\n });\n if (page) return page.conversation;\n }\n // No JSONL on disk (or unparseable). Return null WITHOUT triggering a\n // full scan — confirming not-found is not worth the 20s stall. The\n // caller's cache-tail fallback / 404 self-heal handles it.\n return null;\n }\n\n // Use the existing indexed scanner without honoring the global scannerStale\n // full-rescan: the per-file refreshFile below reconciles the one\n // conversation we care about, so a sibling file changing must not stall this\n // single-conversation request behind a full-tree rescan.\n const scanner = await this.getScanner(true);\n const fromIndex = await scanner.getConversation(lookupId);\n if (fromIndex) {\n // Live-session bypass: a conversation with a live PTY is exactly the case\n // that stalls — its mtime is always newer than the snapshot, so the stale\n // check below would refresh on every request. Serve the current snapshot\n // with no stale-check and no refresh. The live client is on WS receiving\n // conversation_event lines; the (TTL-throttled) turn-end refresh advances\n // the snapshot server-side; mobile refetches once on the running →\n // not-running transition, which is the reconcile point.\n // Codex: PTY is keyed by the placeholder session id, while the scanner\n // indexes the bound rollout UUID — check both.\n if (\n this.ptyManager.hasSession(uuid) ||\n this.ptyManager.hasSession(lookupId) ||\n this.isBoundConversationLive(lookupId)\n ) {\n // Codex live sessions: the scanner LRU snapshot is often frozen at bind\n // time (mtime always looks \"live\", so SWR never refreshes). Re-parse the\n // watched rollout so REST history includes turns written after bind.\n // Claude keeps the cheap bypass — its offset index + WS seq path stay fresh.\n const livePath =\n this.findLiveSessionFilePath(uuid) ??\n this.findLiveSessionFilePath(lookupId) ??\n fromIndex.filePath ??\n null;\n const isCodexLive =\n this.isBoundConversationLive(lookupId) ||\n this.sessionStore.getManaged(uuid)?.provider === CODEX_CLI_PROVIDER;\n if (isCodexLive && livePath) {\n try {\n const account =\n this.cache?.getMetaById(lookupId)?.account ??\n (fromIndex as { account?: string }).account ??\n undefined;\n const page = await scanner.parseSingleFilePage(livePath, account, {\n limit: Number.MAX_SAFE_INTEGER,\n });\n if (page?.conversation) return page.conversation;\n } catch (err) {\n this.log.warn(\"codex.live_reparse_failed\", {\n event: \"codex.live_reparse_failed\",\n conversationId: lookupId,\n filePath: livePath,\n err,\n });\n }\n }\n return fromIndex;\n }\n // The scanner memoizes both its metadata index and parsed conversations\n // for the server's lifetime. A conversation that grows after the initial\n // scan (the chokidar watcher keeps the SQLite cache fresh, but never the\n // scanner) keeps serving the startup snapshot here — so the detail/info\n // view shows a stale message count + last activity that disagrees with\n // the list view and with what --resume actually replays.\n //\n // Stale-while-revalidate: since a snapshot already exists, respond from it\n // immediately and refresh the one file's indexes in the background\n // (single-flighted + TTL-throttled via refreshFileGuarded, tracked so\n // close() awaits it). The next request after the refresh settles sees the\n // fresh data. Only a conversation with NO snapshot pays the parse\n // synchronously (the getConversation-null fallthrough below), so a cold\n // thundering herd costs one parse, not N.\n if (fromIndex.filePath && this.isConversationSnapshotStale(fromIndex)) {\n const filePath = fromIndex.filePath;\n this.trackCacheWrite(\n this.refreshFileGuarded(scanner, filePath).catch((err) => {\n this.log.warn(\"scanner.refreshFile: failed\", {\n event: \"scanner.refresh_failed\",\n conversationId: uuid,\n filePath,\n trigger: \"detail-swr\",\n err,\n });\n }),\n );\n }\n return fromIndex;\n }\n\n if (this.scanProfiles) return null;\n\n const filePath =\n this.findJsonlPath(lookupId) ??\n this.findLiveSessionFilePath(uuid) ??\n this.findLiveSessionFilePath(lookupId);\n if (!filePath) return null;\n this.scanner = null;\n this.scannerReady = null;\n const freshScanner = await this.getScanner();\n return freshScanner.getConversation(lookupId);\n }\n\n // True when the JSONL on disk is meaningfully newer than the scanned\n // snapshot's last-activity timestamp — i.e. the file grew after the scan.\n private isConversationSnapshotStale(conv: Conversation): boolean {\n if (!conv.filePath) return false;\n let mtimeMs: number | null = null;\n try {\n mtimeMs = statSync(conv.filePath).mtimeMs;\n } catch {\n // Stat failed (file moved/deleted mid-flight) — don't force a re-scan.\n return false;\n }\n return isScannedSnapshotStale(conv.timestamp, mtimeMs);\n }\n\n private async handleGetConversation(\n id: string,\n url: URL,\n res: ServerResponse,\n ifNoneMatch?: string,\n ): Promise<void> {\n // Try the scanner first (has full content including tool_use blocks).\n // Fall back to the cache tail only when the scanner can't find the file —\n // e.g. a conversation that existed in a previous run but whose JSONL was deleted.\n const conversation = await this.findConversationByUuid(id);\n\n if (!conversation && this.cache) {\n // Only `before_index` indicates the client is paginating backward (asking\n // for messages older than a cursor) — `msg_limit` is just page size and is\n // sent on the first page too. The tail fallback should serve any first-page\n // request when the JSONL is missing, regardless of msg_limit.\n const isFirstLoad = !url.searchParams.has(\"before_index\");\n if (isFirstLoad) {\n const tail = this.cache.getConversationTail(id);\n if (tail && tail.messages.length > 0) {\n const cachedMeta = this.cache.getMetaById(id);\n const cachedProvider = cachedMeta?.provider ?? CLAUDE_CODE_PROVIDER;\n const availability = classifyResumability(cachedMeta?.projectPath);\n const messagesPayload = tail.messages.map((m, idx) => ({\n message_index: idx,\n role: m.role,\n timestamp: m.timestamp,\n text: m.text,\n tool_calls: [] as unknown[],\n content: (m.content ?? []).filter((b: any) => b.type !== \"text\"),\n }));\n json(res, 200, {\n meta: {\n id,\n profile_id: cachedMeta?.account ?? undefined,\n project_name: cachedMeta?.projectName ?? undefined,\n project_path: cachedMeta?.projectPath ?? undefined,\n file_path: cachedMeta?.filePath ?? undefined,\n last_updated_at: cachedMeta?.lastActivity ?? undefined,\n message_count: cachedMeta?.messageCount ?? undefined,\n provider: cachedProvider,\n resumable: isProviderResumable(cachedProvider, availability.resumable),\n ...(availability.unavailable_reason && {\n unavailable_reason: availability.unavailable_reason,\n }),\n },\n messages: messagesPayload,\n message_pagination: {\n total: tail.tailSize,\n before_index: tail.tailSize,\n from_index: 0,\n has_more_older: false,\n next_before_index: null,\n },\n });\n return;\n }\n }\n }\n\n if (!conversation) {\n // Self-heal: the row is a ghost (JSONL gone, no usable tail). Drop it so\n // the next list refresh doesn't keep offering this id to clients.\n this.cache?.invalidate(id);\n json(res, 404, { error: \"Conversation not found\", code: \"not_found\" });\n return;\n }\n\n // Compute the conditional-fetch validator from the RESOLVED conversation —\n // findConversationByUuid has already done its staleness refresh above, so\n // these fields reflect the same state the body would. Computing it from a\n // pre-refresh snapshot would let us hand out a 304 against stale data.\n const etagSource = conversation as unknown as {\n filePath: string;\n messageCount: number;\n timestamp: string;\n };\n // Fold the offset index's count into the validator: when the index is\n // fresher than the scanner snapshot (a live/appended file), the ETag must\n // change so a client holding the old tail doesn't get a 304 against grown\n // content. Cheap count lookup; the window read below reuses the same number.\n const indexedCount =\n etagSource.filePath && this.cache\n ? this.cache.getIndexedMessageCount(\n ConversationCache.conversationIdForFile(etagSource.filePath),\n )\n : 0;\n const etagMessageCount = Math.max(etagSource.messageCount, indexedCount);\n const etag = computeConversationEtag({\n filePath: etagSource.filePath,\n messageCount: etagMessageCount,\n timestamp: etagSource.timestamp,\n });\n\n // Only the first page (\"is the conversation as a whole still current?\")\n // participates in the freshness check. Older pages are immutable history —\n // a back-page request (before_index set) always returns its 200 body, never\n // a 304, even when the client echoes a matching If-None-Match. Anchored and\n // after-windows also always return 200: their ETag inputs are identical to\n // the tail page's, so honoring If-None-Match here would 304 a client that\n // holds the tail page but is asking for a different window.\n const isFirstPage =\n !url.searchParams.has(\"before_index\") &&\n !url.searchParams.has(\"anchor_index\") &&\n !url.searchParams.has(\"after_index\");\n if (isFirstPage && ifNoneMatch && ifNoneMatch === etag) {\n // This is a direct-`ServerResponse` write, so the Hono CORS middleware's\n // headers don't reach it — set the expose header here so a cross-origin\n // client can read the validator off the 304 too.\n res.writeHead(304, { ETag: etag, \"Access-Control-Expose-Headers\": \"ETag\" });\n res.end();\n return;\n }\n\n // Codex writes AGENTS.md / permissions dumps as role:user before any real\n // turn. Drop them from the REST payload so the chat opens as user→agent\n // rather than fake-user→fake-user→agent. Heuristic is Codex-specific text;\n // Claude messages never match.\n const filtered = conversation.messages.filter(\n (m) => !(m.role === \"user\" && typeof m.text === \"string\" && isCodexInjectedContext(m.text)),\n );\n const total = filtered.length;\n\n const hasAnchor = url.searchParams.has(\"anchor_index\");\n const hasAfter = url.searchParams.has(\"after_index\");\n const usePaging =\n url.searchParams.has(\"msg_limit\") ||\n url.searchParams.has(\"before_index\") ||\n hasAnchor ||\n hasAfter;\n\n let slice = filtered;\n let fromIdx = 0;\n let messagePagination: Record<string, unknown> | undefined;\n // Set to the offset index's total when it served this response, so the meta\n // block can reflect the freshly-indexed count/timestamp instead of the\n // (possibly stale) scanner snapshot.\n let indexTotal: number | null = null;\n\n if (usePaging) {\n const limit = Math.min(Math.max(intParam(url, \"msg_limit\", 80), 1), 500);\n let beforeIndex = total;\n let scanLimit = limit;\n let anchorIndex: number | null = null;\n let newerPaging = false;\n // True only when the after_index branch actually ran (before_index takes\n // precedence over after_index, so `hasAfter` alone isn't enough).\n let usedAfterIndex = false;\n if (url.searchParams.has(\"before_index\")) {\n beforeIndex = intParam(url, \"before_index\", total);\n beforeIndex = Math.min(Math.max(beforeIndex, 0), total);\n } else if (hasAfter) {\n // Newer-direction page: [after_index, after_index + limit). The paged\n // reader is end-anchored, so cap its limit at the window width — a full\n // `limit` near the tail would widen the window backward over rows the\n // client already has (duplicate message_index rows on mobile).\n const from = Math.min(Math.max(intParam(url, \"after_index\", 0), 0), total);\n beforeIndex = Math.min(total, from + limit);\n scanLimit = beforeIndex - from;\n newerPaging = true;\n usedAfterIndex = true;\n } else if (hasAnchor) {\n // Centered window around the anchor, clamped into [0, total-1] — a\n // stale index from search must still open the conversation, never 400.\n // Near the tail the window widens backward so it stays full-size.\n anchorIndex = Math.min(\n Math.max(intParam(url, \"anchor_index\", 0), 0),\n Math.max(0, total - 1),\n );\n const from = Math.max(0, anchorIndex - Math.floor(limit / 2));\n beforeIndex = Math.min(total, from + limit);\n newerPaging = true;\n }\n // A plain tail request (no explicit cursor) means \"the newest `limit`\n // messages\". Its window was derived from the scanner's snapshot `total`,\n // which lags a live, actively-appended file — the exact case the offset\n // index exists to serve. When the index is fresher than the snapshot,\n // anchor the tail on the INDEX's total so newly-appended messages aren't\n // dropped by a stale upper bound.\n const isTailRequest = !url.searchParams.has(\"before_index\") && !hasAfter && !hasAnchor;\n const indexFilePath = (conversation as { filePath?: string }).filePath;\n if (isTailRequest && indexFilePath && this.cache) {\n const indexed = this.cache.getIndexedMessageCount(\n ConversationCache.conversationIdForFile(indexFilePath),\n );\n if (indexed > beforeIndex) {\n beforeIndex = indexed;\n }\n }\n const windowStart = Math.max(0, beforeIndex - scanLimit);\n\n // Offset-index fast path: when the index is warm and matches the file on\n // disk, serve the window straight from SQLite + pread of the exact byte\n // ranges — no scanner, no re-parse. Falls through to the scanner (and\n // enqueues a backfill) on any miss/mismatch so the response is never\n // wrong. Only for the linear paging windows (before/after/tail); the\n // anchored-search window keeps using the scanner's reader.\n const indexWindow =\n scanLimit > 0 && !hasAnchor && indexFilePath && this.cache\n ? this.cache.readMessageWindow(indexFilePath, windowStart, beforeIndex)\n : null;\n if (!indexWindow && indexFilePath && this.cache && !hasAnchor) {\n // Cold/stale index for a file we page linearly → backfill in the\n // background (tracked so close() awaits it) for next time. The current\n // request is served by the scanner path below.\n this.trackCacheWrite(\n this.cache.backfillIndex(indexFilePath).catch((err) => {\n this.log.warn(\"offset-index.backfill_failed\", {\n event: \"offset_index.backfill_failed\",\n conversationId: id,\n filePath: indexFilePath,\n err,\n });\n }),\n );\n }\n\n // Only consult the scanner's paged reader when it's already warm. On the\n // cold path `conversation` came from the single-file fast path and holds\n // every message in memory, so slice it locally — calling getScanner()\n // here would trigger the full scan the fast path exists to avoid. Pass\n // skipStaleRescan: this is the same single-conversation detail path, whose\n // refreshFile already reconciled the one file we page here, so a sibling\n // file's stale flag must not stall this read behind a full-tree rescan.\n // Prefer the offset-index window when warm (Claude). Otherwise slice the\n // in-memory `filtered` list — do NOT call getConversationPage here.\n // That helper re-reads the scanner LRU (unfiltered, often stale for live\n // Codex) and would bypass isCodexInjectedContext, which is exactly how\n // mobile's ?msg_limit=80 path lost real user turns / showed PTY-only UI.\n const page = indexWindow;\n if (indexWindow) indexTotal = indexWindow.total;\n const start = page?.fromIndex ?? windowStart;\n slice = page?.messages ?? filtered.slice(start, beforeIndex);\n fromIdx = start;\n const effectiveTotal = page?.total ?? total;\n messagePagination = {\n total: effectiveTotal,\n before_index: beforeIndex,\n from_index: start,\n has_more_older: start > 0,\n next_before_index: start > 0 ? start : null,\n };\n if (anchorIndex != null) messagePagination.anchor_index = anchorIndex;\n if (newerPaging) {\n messagePagination.has_more_newer = beforeIndex < effectiveTotal;\n messagePagination.next_after_index = beforeIndex < effectiveTotal ? beforeIndex : null;\n }\n // Delta-validity token: an after_index delta carries the conversation's\n // current etag so a client can detect that its stored cursor is stale\n // (etag mismatch → discard the cursor, refetch the tail). Only on the\n // forward-delta path (before_index takes precedence, so gate on the flag\n // not merely hasAfter); additive, so old clients ignore it.\n if (usedAfterIndex) {\n messagePagination.etag = etag;\n }\n }\n\n const messagesPayload = slice.map((m: any, localIdx: number) => {\n const content: unknown[] = [];\n if (m.isThinking) {\n content.push({\n type: \"thinking\",\n thinking: m.thinkingContent ?? \"\",\n signature: m.thinkingSignature,\n });\n }\n for (const b of m.metadata?.toolUseBlocks ?? []) {\n content.push({ type: \"tool_use\", id: b.id, name: b.name, input: b.input });\n }\n for (const r of m.metadata?.toolResults ?? []) {\n content.push({\n type: \"tool_result\",\n tool_use_id: r.toolUseId,\n content: JSON.stringify(r.content),\n is_error: r.isError ?? false,\n });\n }\n return {\n uuid: m.uuid ?? null,\n message_index: fromIdx + localIdx,\n role: m.role,\n timestamp: m.timestamp,\n text: m.text,\n tool_calls: m.metadata?.toolUses ?? [],\n has_images: m.hasImages ?? false,\n parent_uuid: m.parentUuid ?? null,\n permission_mode: m.permissionMode ?? null,\n is_sidechain: m.isSidechain ?? false,\n is_tool_result: m.isToolResult ?? false,\n attachment: m.attachment ?? null,\n content,\n };\n });\n\n const conv = conversation as any;\n const cachedConvMeta = this.cache?.getMetaById(id);\n const convProvider = coerceProviderForRunner(conv.provider ?? cachedConvMeta?.provider);\n const availability = classifyResumability(conv.projectPath);\n // When the offset index served a fresher view than the scanner snapshot,\n // the meta (message_count / last_updated_at) must reflect what was actually\n // served — otherwise meta disagrees with the messages array. Prefer the\n // index total and the newest served message's timestamp.\n const metaMessageCount =\n indexTotal != null && indexTotal > conv.messageCount ? indexTotal : conv.messageCount;\n const metaLastUpdatedAt =\n indexTotal != null && indexTotal > conv.messageCount\n ? (slice.at(-1)?.timestamp ?? conv.timestamp)\n : conv.timestamp;\n const body: Record<string, unknown> = {\n meta: {\n id,\n profile_id: conv.account,\n project_name: conv.projectName,\n project_path: conv.projectPath,\n file_path: conv.filePath,\n last_updated_at: metaLastUpdatedAt,\n message_count: metaMessageCount,\n last_prompt: conv.lastPrompt ?? undefined,\n provider: convProvider,\n resumable: isProviderResumable(convProvider, availability.resumable),\n ...(availability.unavailable_reason && {\n unavailable_reason: availability.unavailable_reason,\n }),\n },\n messages: messagesPayload,\n };\n if (messagePagination) body.message_pagination = messagePagination;\n if (conv.turnDurations?.length) {\n body.turn_durations = conv.turnDurations.map((d: any) => ({\n duration_ms: d.durationMs,\n message_count: d.messageCount,\n uuid: d.uuid,\n }));\n }\n // Always expose the ETag on the 200 so the client can store it and send it\n // back as If-None-Match next time. Old clients ignore the header. This is a\n // direct-`ServerResponse` write that bypasses the Hono CORS middleware, so\n // the expose header is set here too — without it a cross-origin client\n // can't read ETag.\n res.writeHead(200, {\n \"Content-Type\": \"application/json\",\n ETag: etag,\n \"Access-Control-Expose-Headers\": \"ETag\",\n });\n res.end(JSON.stringify(body));\n }\n\n // Resolves an active search query to the message a client should anchor to\n // inside one conversation. Matching is body-only (text first, then\n // thinking/tool payloads) — a metadata-only search hit (project path, title)\n // has no scroll target and returns 404 search_target_not_found.\n //\n // Implements HTTP QUERY (RFC 10008): the search query travels in a JSON\n // request body instead of a URL query param — QUERY is safe + idempotent +\n // cacheable like GET, but (like POST) can carry a body, which fits this\n // endpoint's single-string input exactly. `Accept-Query` advertises the\n // supported request media type per the spec.\n private async handleSearchTarget(\n id: string,\n req: IncomingMessage,\n res: ServerResponse,\n ): Promise<void> {\n const contentType = (req.headers[\"content-type\"] ?? \"\").split(\";\")[0].trim();\n if (contentType && contentType !== \"application/json\") {\n res.setHeader(\"Accept-Query\", \"application/json\");\n json(res, 415, {\n error: \"Unsupported Content-Type; expected application/json\",\n code: \"unsupported_media_type\",\n });\n return;\n }\n\n let body: unknown;\n try {\n body = await readBody(req);\n } catch {\n res.setHeader(\"Accept-Query\", \"application/json\");\n json(res, 422, { error: \"Malformed JSON body\", code: \"invalid_query\" });\n return;\n }\n\n const q =\n typeof (body as { q?: unknown })?.q === \"string\" ? (body as { q: string }).q.trim() : \"\";\n if (!q) {\n res.setHeader(\"Accept-Query\", \"application/json\");\n json(res, 422, { error: \"Missing or empty query field: q\", code: \"invalid_query\" });\n return;\n }\n if (q.length > 256) {\n res.setHeader(\"Accept-Query\", \"application/json\");\n json(res, 422, { error: \"Query too long (max 256 characters)\", code: \"invalid_query\" });\n return;\n }\n\n const conversation = await this.findConversationByUuid(id);\n if (!conversation) {\n json(res, 404, { error: \"Conversation not found\", code: \"not_found\" });\n return;\n }\n\n const target = findSearchTarget(conversation.messages as unknown as SearchableMessage[], q);\n if (!target) {\n json(res, 404, { error: \"No message body matches query\", code: \"search_target_not_found\" });\n return;\n }\n\n res.setHeader(\"Accept-Query\", \"application/json\");\n json(res, 200, {\n query: q,\n message_index: target.messageIndex,\n uuid: target.uuid,\n snippet: target.snippet,\n match_indexes: target.matchIndexes,\n total_matches: target.totalMatches,\n });\n }\n\n private async handleSearch(url: URL, res: ServerResponse): Promise<void> {\n const q = url.searchParams.get(\"q\") ?? \"\";\n if (!q) {\n json(res, 400, { error: \"Missing query parameter: q\" });\n return;\n }\n\n const limit = intParam(url, \"limit\", 50);\n const scanner = await this.getScanner();\n const results = await search(\n q,\n {\n limit,\n include: \"conversations\",\n ...(this.scanProfiles ? { profiles: this.scanProfiles } : {}),\n ...this.codexScanOpts(),\n },\n scanner,\n );\n const adapted = results.map((r: any) => ({\n // Use sessionId so the id matches /api/conversations and resolves via\n // findConversationByUuid — a client can round-trip a search result into\n // GET /api/conversations/:id or the search-target QUERY. The old\n // filename-stem derivation produced an id no other endpoint recognized.\n id: r.meta.sessionId || r.meta.id,\n title: r.meta.projectName,\n sessionName: r.meta.sessionName || undefined,\n filePath: r.meta.filePath,\n projectPath: r.meta.projectPath,\n branch: r.meta.gitBranch ?? undefined,\n account: r.meta.account,\n preview: r.meta.preview || undefined,\n messageCount: r.meta.messageCount,\n lastActivity: r.meta.timestamp,\n firstMessage: r.meta.firstMessage ?? undefined,\n lastMessage: r.meta.lastMessage ?? undefined,\n provider: r.meta.provider ?? CLAUDE_CODE_PROVIDER,\n }));\n json(res, 200, {\n conversations: adapted,\n hasMore: false,\n offset: 0,\n total: adapted.length,\n });\n }\n\n private async handleListSessions(url: URL, res: ServerResponse): Promise<void> {\n const DISCOVERY_TTL_MS = 15_000;\n const now = Date.now();\n\n if (!this.discoveryCache || now - this.discoveryCache.fetchedAt >= DISCOVERY_TTL_MS) {\n try {\n const discovered = await discoverClaudeProcesses();\n this.sessionStore.setDiscovered(discovered);\n this.discoveryCache = { entries: discovered, fetchedAt: now };\n } catch {\n // Discovery is best-effort\n }\n }\n\n // Backwards compat: a bare GET /api/sessions returns the legacy plain\n // array. Any pagination param switches to the new envelope.\n const hasPaginationParams =\n url.searchParams.has(\"limit\") ||\n url.searchParams.has(\"cursor\") ||\n url.searchParams.has(\"sortBy\") ||\n url.searchParams.has(\"order\") ||\n url.searchParams.has(\"status\");\n\n if (!hasPaginationParams) {\n json(res, 200, this.sessionStore.list(this.ptyAttachedIds()));\n return;\n }\n\n const parsed = parseSessionListQuery(url);\n if (\"error\" in parsed) {\n json(res, 400, { error: parsed.error });\n return;\n }\n\n try {\n const page = this.sessionStore.paginate(this.ptyAttachedIds(), parsed.query);\n json(res, 200, page);\n } catch (err) {\n if (err instanceof Error && err.message === \"INVALID_CURSOR\") {\n json(res, 400, { error: \"Invalid cursor\" });\n return;\n }\n throw err;\n }\n }\n\n private handleGetSession(sessionId: string, res: ServerResponse): void {\n const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());\n if (session) {\n if (!existsSync(session.projectPath)) {\n session.failureReason = `Project directory not found: ${session.projectPath}`;\n }\n json(res, 200, session);\n return;\n }\n // Fall back to the conversation cache: older mobile builds tap recents\n // entries via GET /api/sessions/:id even though those IDs are conversation\n // UUIDs, not live sessions. Returning a resumable shape (status=on_hold)\n // lets the mobile open flow proceed to /api/sessions/resume.\n const conversation = this.cache?.getMetaById(sessionId);\n if (conversation) {\n json(res, 200, conversationToResumableSession(conversation));\n return;\n }\n json(res, 404, { error: \"Session not found\" });\n }\n\n private async handleResume(req: IncomingMessage, res: ServerResponse): Promise<void> {\n this.discoveryCache = null;\n const body = await readBody(req);\n // Accept both sessionId (new) and conversationId (legacy alias)\n const sessionId: string | undefined = body.sessionId ?? body.conversationId;\n\n if (!sessionId) {\n json(res, 400, { error: \"Missing sessionId\" });\n return;\n }\n\n // If a PTY is already running for this session, return it immediately\n if (this.ptyManager.hasSession(sessionId)) {\n const resp = this.sessionStore.get(sessionId, this.ptyAttachedIds());\n if (resp) {\n json(res, 200, resp);\n return;\n }\n }\n\n // Authoritative cwd comes from the JSONL itself — the file Claude looks\n // up by filename when processing --resume. The scanner index can return a\n // stale or wrong path (e.g. …/tb-mobile/android vs …/tb-mobile), so we\n // read the first cwd field directly, mirroring tb-scanner/src/parser.ts.\n const jsonlPath = this.findJsonlPath(sessionId);\n const jsonlCwd = jsonlPath ? await this.readCwdFromJsonl(jsonlPath) : null;\n\n const conv = await this.findConversationByUuid(sessionId);\n const projectPath: string = jsonlCwd ?? (conv as any)?.projectPath;\n if (!projectPath) {\n if (!conv && !jsonlPath) {\n // The cache can keep a \"tailed ghost\" row (JSONL deleted out-of-band,\n // e.g. bulk branch/worktree cleanup) so its history stays viewable via\n // GET /:id — see pruneGhostFiles(). It can never be resumed though, so\n // give a distinct, non-retryable reason instead of a generic 404 that\n // sends mobile into a retry loop.\n json(res, 404, {\n error: \"Conversation history file is missing; it can no longer be resumed\",\n code: \"history_file_missing\",\n });\n return;\n }\n json(res, 400, { error: \"Could not determine project path\" });\n return;\n }\n\n // Same provider-resolution fallback as the conversation-detail path\n // (server.ts ~1685): `conv` (the full Conversation shape) doesn't carry\n // provider, so fall back to the cached metadata, then default to Claude.\n const cachedConvMeta = this.cache?.getMetaById(sessionId);\n const provider = coerceProviderForRunner((conv as any)?.provider ?? cachedConvMeta?.provider);\n\n const session = await this.ptyManager.start(sessionId, {\n provider,\n projectPath,\n projectName: body.projectName,\n branch: body.branch,\n permissionMode: this.defaultPermissionMode,\n });\n\n this.sessionStore.addManaged(session);\n\n // Watch the conversation's JSONL file for structured events\n void this.watchConversationFile(sessionId);\n\n const resp = this.sessionStore.get(session.id, this.ptyAttachedIds());\n this.broadcastOrUnicastSessionList(req);\n\n json(res, 201, resp ?? session);\n\n // Enrich session metadata and update DB in background (fire-and-forget).\n // The conversation history is already in the JSONL; DB writes are\n // bookkeeping that can happen asynchronously without blocking the response.\n this.enrichResumedSessionAsync(sessionId, projectPath, conv);\n }\n\n private enrichResumedSessionAsync(sessionId: string, projectPath: string, conv: any): void {\n try {\n const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());\n if (!session) return;\n\n if (conv) {\n session.sessionName = conv.sessionName ?? undefined;\n session.messageCount = conv.messageCount ?? 0;\n session.account = conv.account ?? undefined;\n session.filePath = conv.filePath ?? undefined;\n }\n\n if (!this.cache || !this.projectsRepo || !this.conversationsRepo) return;\n\n // Single SQLite read covers model, preview, timestamps, and projectId —\n // no scanner round-trip needed; these fields are already cached.\n const cached = this.cache.getMetaById(sessionId);\n if (cached) {\n session.model = cached.model ?? undefined;\n session.preview = cached.preview ?? undefined;\n const first = cached.firstMessage ? JSON.parse(cached.firstMessage as string) : null;\n const last = cached.lastMessage ? JSON.parse(cached.lastMessage as string) : null;\n session.firstMessageText = first?.text ?? undefined;\n session.firstMessageAt = first?.timestamp\n ? new Date(first.timestamp).toISOString()\n : undefined;\n session.lastMessageText = last?.text ?? undefined;\n session.lastMessageAt = last?.timestamp\n ? new Date(last.timestamp).toISOString()\n : undefined;\n }\n\n let resolvedProjectId: string | null = cached?.projectId ?? null;\n if (!resolvedProjectId) {\n const project = this.projectsRepo.upsertProjectByPath(projectPath);\n resolvedProjectId = project.id;\n this.conversationsRepo.updateConversationProjectId({\n conversationId: sessionId,\n projectId: project.id,\n });\n }\n if (resolvedProjectId) {\n session.projectId = resolvedProjectId;\n session.resumedFromConversationId = sessionId;\n }\n } catch (err) {\n // ponytail: log but don't crash; session is already live and usable\n console.error(`[enrichResumedSessionAsync] ${sessionId}:`, err);\n }\n }\n\n private async handleSendInput(\n sessionId: string,\n req: IncomingMessage,\n res: ServerResponse,\n ): Promise<void> {\n if (!this.checkSessionInputRateLimit(sessionId)) {\n json(res, 429, { error: \"Too many input requests for this session. Please slow down.\" });\n return;\n }\n if (this.agentConfig.enabled) {\n const body = await readBody(req);\n const cache = this.cache;\n if (!cache) {\n json(res, 503, {\n error: \"Conversation cache is not available\",\n code: \"INTERNAL_ERROR\",\n });\n return;\n }\n const result = await handleSendAgentInput(sessionId, body, {\n sessionStore: this.sessionStore,\n cache,\n // biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true\n agentClient: this.agentClient!,\n agentConfig: this.agentConfig,\n });\n json(res, result.status, result.body);\n return;\n }\n const body = await readBody(req);\n const { input, keys } = body;\n\n if (typeof keys === \"string\") {\n // Raw key bytes (e.g. arrow navigation for interactive prompts).\n // These bypass bracketed-paste wrapping — caller is responsible for\n // sending well-formed escape sequences.\n try {\n this.ptyManager.sendKeys(sessionId, keys);\n const updated = this.sessionStore.get(sessionId, this.ptyAttachedIds());\n if (updated) {\n this.wsHub.broadcast({ type: \"session_update\", session: updated });\n }\n json(res, 200, { ok: true });\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Failed to send keys\";\n json(res, 400, { error: message });\n }\n return;\n }\n\n if (typeof input !== \"string\") {\n json(res, 400, { error: \"Missing input field\" });\n return;\n }\n\n try {\n const promptCount = this.ptyManager.sendInput(sessionId, input);\n this.sessionStore.updateManaged(sessionId, { promptCount });\n const updated = this.sessionStore.get(sessionId, this.ptyAttachedIds());\n if (updated) {\n this.wsHub.broadcast({ type: \"session_update\", session: updated });\n }\n // Index the user's new message immediately so it's searchable right away.\n const filePath = this.sessionFileMap.get(sessionId);\n if (filePath) {\n this.getScanner()\n .then((scanner) => scanner.refreshFile(filePath))\n .then((meta) => {\n this.log.info(\"scanner.refreshFile: ok\", {\n event: \"scanner.refresh\",\n sessionId,\n filePath,\n trigger: \"sendInput\",\n messageCount: meta?.messageCount,\n });\n })\n .catch((err) => {\n this.log.warn(\"scanner.refreshFile: failed\", {\n event: \"scanner.refresh_failed\",\n sessionId,\n filePath,\n trigger: \"sendInput\",\n err,\n });\n });\n }\n json(res, 200, { ok: true });\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Failed to send input\";\n json(res, 400, { error: message });\n }\n }\n\n private cancelPendingQuestion(sessionId: string): void {\n const pq = this.pendingQuestions.get(sessionId);\n if (!pq) return;\n this.pendingQuestions.delete(sessionId);\n this.pendingQuestionKey.delete(sessionId);\n this.wsHub.broadcast({ type: \"question_cancelled\", sessionId, toolUseId: pq.toolUseId });\n }\n\n // Live AskUserQuestion detected from the rendered screen (ahead of JSONL).\n // Broadcasts the `question` event immediately and records the content key so\n // the later JSONL flush of the same question is de-duped. We synthesize a\n // screen-scoped toolUseId; the JSONL path overwrites pendingQuestions with the\n // real toolUseId when it lands, so answering works once JSONL catches up.\n private handleLiveQuestion(sessionId: string, questions: AskQuestion[]): void {\n const key = questionContentKey(questions);\n if (this.pendingQuestionKey.get(sessionId) === key) return; // already shown\n const toolUseId = `screen:${sessionId}:${key.length}`;\n this.pendingQuestions.set(sessionId, { toolUseId, questions });\n this.pendingQuestionKey.set(sessionId, key);\n this.wsHub.broadcast({ type: \"question\", sessionId, toolUseId, questions });\n }\n\n // Permission gate opened/closed (OSC 777 + scraped options). Broadcasts the\n // additive `permission` / `permission_cancelled` events. Mobile answers by\n // sending the chosen option index via /input { keys } (e.g. \"2\\r\").\n private handlePermissionChange(\n sessionId: string,\n gate: {\n prompt?: string;\n detail?: string;\n options: PermissionOption[];\n cursor?: number;\n } | null,\n ): void {\n if (gate === null) {\n if (!this.pendingPermission.has(sessionId)) return;\n this.pendingPermission.delete(sessionId);\n this.wsHub.broadcast({ type: \"permission_cancelled\", sessionId });\n return;\n }\n this.pendingPermission.set(sessionId, gate);\n const subscriberCount = this.sessionSubscribers.get(sessionId)?.size ?? 0;\n this.log.info(\n `[ws.broadcast_permission] ${sessionId.slice(0, 8)} subscribers=${subscriberCount}`,\n { event: \"ws.broadcast_permission\", sessionId, subscriberCount },\n );\n this.wsHub.broadcast({\n type: \"permission\",\n sessionId,\n ...(gate.prompt ? { prompt: gate.prompt } : {}),\n ...(gate.detail ? { detail: gate.detail } : {}),\n options: gate.options,\n ...(gate.cursor !== undefined ? { cursor: gate.cursor } : {}),\n });\n }\n\n private async handleSendAnswer(\n sessionId: string,\n req: IncomingMessage,\n res: ServerResponse,\n ): Promise<void> {\n const body = await readBody(req);\n const pending = this.pendingQuestions.get(sessionId);\n const resolution = resolveAnswer(pending, body);\n if (!resolution.ok) {\n json(res, 400, { ok: false, reason: resolution.reason });\n return;\n }\n // pending is guaranteed defined when resolution.ok is true (resolveAnswer guards it)\n const toolUseId = pending?.toolUseId ?? \"\";\n try {\n this.ptyManager.sendKeys(sessionId, resolution.keys);\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Failed to send answer\";\n json(res, 400, { ok: false, reason: message });\n return;\n }\n this.pendingQuestions.delete(sessionId);\n this.wsHub.broadcast({ type: \"question_cancelled\", sessionId, toolUseId });\n json(res, 200, { ok: true });\n }\n\n private async handleUploadFile(\n sessionId: string,\n req: IncomingMessage,\n res: ServerResponse,\n ): Promise<void> {\n const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());\n if (!session) {\n json(res, 404, { error: \"Session not found\" });\n return;\n }\n if (!session.projectPath) {\n json(res, 400, { error: \"Session has no project path\" });\n return;\n }\n\n const body = await readBody(req);\n const { filename, mimeType, dataBase64 } = body ?? {};\n if (\n typeof filename !== \"string\" ||\n typeof mimeType !== \"string\" ||\n typeof dataBase64 !== \"string\"\n ) {\n json(res, 400, { error: \"Missing filename, mimeType, or dataBase64\" });\n return;\n }\n\n try {\n const saved = await saveUploadFile({\n sessionId,\n projectPath: session.projectPath,\n originalName: filename,\n mimeType,\n dataBase64,\n });\n\n try {\n await recordUpload(this.dbPool, this.dbInstanceId, {\n id: saved.id,\n sessionId,\n filePath: saved.filePath,\n originalName: saved.originalName,\n mimeType: saved.mimeType,\n sizeBytes: saved.sizeBytes,\n });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n this.log.warn(\n `[uploads] DB record failed: ${message}`,\n { event: \"uploads.db_record_failed\", error: message },\n \"pino\",\n );\n }\n\n json(res, 201, {\n id: saved.id,\n path: saved.filePath,\n originalName: saved.originalName,\n mimeType: saved.mimeType,\n sizeBytes: saved.sizeBytes,\n });\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Upload failed\";\n json(res, 400, { error: message });\n }\n }\n\n private handleGetOutput(sessionId: string, res: ServerResponse): void {\n // Return PTY ring buffer if a PTY is attached; otherwise return empty\n // so clients render \"no buffered output\" instead of an error.\n try {\n const output = this.ptyManager.getOutput(sessionId);\n json(res, 200, { output });\n } catch {\n json(res, 200, { output: \"\" });\n }\n }\n\n private handleCancel(sessionId: string, res: ServerResponse): void {\n this.discoveryCache = null;\n try {\n this.ptyManager.cancel(sessionId);\n json(res, 200, { ok: true });\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Failed to cancel\";\n json(res, 400, { error: message });\n }\n }\n\n private async handleStopSession(sessionId: string, res: ServerResponse): Promise<void> {\n const STOP_TIMEOUT_MS = 5000;\n\n const session = this.ptyManager.getSession(sessionId);\n if (!session) {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Session not found\" }));\n return;\n }\n\n if (session.status === \"idle\") {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"already_idle\", sessionId }));\n return;\n }\n\n res.writeHead(200, {\n \"Content-Type\": \"application/x-ndjson\",\n \"Transfer-Encoding\": \"chunked\",\n \"Cache-Control\": \"no-cache\",\n \"X-Accel-Buffering\": \"no\",\n });\n res.write(`${JSON.stringify({ event: \"stopping\", sessionId })}\\n`);\n\n const idlePromise = new Promise<\"idle\">((resolve) => {\n const handler = (status: string) => {\n if (status === \"idle\") {\n this.sessionStatusBus.off(`status:${sessionId}`, handler);\n resolve(\"idle\");\n }\n };\n this.sessionStatusBus.on(`status:${sessionId}`, handler);\n });\n\n const timeoutPromise = new Promise<\"timeout\">((resolve) =>\n setTimeout(() => resolve(\"timeout\"), STOP_TIMEOUT_MS),\n );\n\n this.ptyManager.putOnHold(sessionId);\n this.discoveryCache = null;\n\n const outcome = await Promise.race([idlePromise, timeoutPromise]);\n\n if (outcome === \"idle\") {\n res.write(`${JSON.stringify({ event: \"stopped\", sessionId })}\\n`);\n } else {\n res.write(`${JSON.stringify({ event: \"timeout\", sessionId })}\\n`);\n this.log.warn(\n `[stop] session ${sessionId.slice(0, 8)} did not idle within ${STOP_TIMEOUT_MS}ms`,\n );\n }\n\n res.end();\n }\n\n private async handleAdopt(sessionId: string, res: ServerResponse): Promise<void> {\n // Refresh discovery so we have the latest metadata\n const discovered = await discoverClaudeProcesses();\n this.sessionStore.setDiscovered(discovered);\n this.discoveryCache = null;\n\n const discSession = this.sessionStore.get(sessionId, this.ptyAttachedIds());\n if (!discSession || discSession.ptyAttached) {\n json(res, 404, { error: \"Discovered session not found\" });\n return;\n }\n\n const { projectPath, projectName, branch } = discSession;\n const convId = discSession.id;\n\n if (discSession.pid == null) {\n json(res, 400, { error: \"Session has no known PID\" });\n return;\n }\n\n // Kill the external process\n this.ptyManager.killPid(discSession.pid);\n\n // Start a new managed session, resuming the conversation\n const session = await this.ptyManager.start(convId, {\n projectPath,\n projectName,\n branch,\n permissionMode: this.defaultPermissionMode,\n });\n\n this.sessionStore.addManaged(session);\n void this.watchConversationFile(session.id);\n\n this.wsHub.broadcast({\n type: \"session_list\",\n sessions: this.sessionStore.list(this.ptyAttachedIds()),\n });\n\n json(res, 201, { sessionId: session.id });\n }\n\n private async handleStartSession(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const ip = req.socket?.remoteAddress ?? \"unknown\";\n if (!this.checkSessionStartRateLimit(ip)) {\n json(res, 429, {\n error: \"Too many session start requests. Please wait before trying again.\",\n });\n return;\n }\n if (this.agentConfig.enabled) {\n const body = await readBody(req);\n const result = await handleStartAgentSession(body, {\n sessionStore: this.sessionStore,\n // biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true\n agentClient: this.agentClient!,\n conversationsDir: this.cacheDir ? join(dirname(this.cacheDir), \"conversations\") : \"\",\n agentConfig: this.agentConfig,\n });\n json(res, result.status, result.body);\n if (result.status === 200) {\n this.broadcastOrUnicastSessionList(req);\n }\n return;\n }\n const body = await readBody(req);\n const { path: relativePath, provider: requestedProvider, systemPrompt: clientPrompt } = body;\n\n if (requestedProvider !== undefined && !isProviderName(requestedProvider)) {\n json(res, 400, { error: \"Invalid provider\" });\n return;\n }\n const provider = requestedProvider ?? CLAUDE_CODE_PROVIDER;\n\n if (!this.browseRoot) {\n json(res, 403, {\n error: \"File browsing not configured. Set browseRoot on the server.\",\n code: \"BROWSE_ROOT_NOT_SET\",\n });\n return;\n }\n\n if (typeof relativePath !== \"string\") {\n json(res, 400, { error: \"Missing path field\" });\n return;\n }\n\n let resolvedPath: string;\n try {\n resolvedPath = await resolveBrowsePath(this.browseRoot, relativePath);\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Invalid path\";\n json(res, 400, { error: message });\n return;\n }\n\n this.discoveryCache = null;\n\n const systemPromptParts = [\n this.defaultSystemPrompt,\n BROWSE_SYSTEM_PROMPT(this.browseRoot),\n typeof clientPrompt === \"string\" ? clientPrompt : null,\n ].filter(Boolean);\n\n try {\n const session = await this.ptyManager.startFresh({\n provider,\n projectPath: resolvedPath,\n projectName: body.projectName,\n systemPrompt: systemPromptParts.join(\"\\n\"),\n permissionMode: this.defaultPermissionMode,\n });\n\n this.sessionStore.addManaged(session);\n\n // Block for the PTY to actually reach waiting_input (or fail) so the\n // caller gets a trustworthy status instead of navigating on a guess.\n // Races against the same fallback window pty-manager itself uses for\n // prompt-marker detection, plus margin — if neither settles in time we\n // fall back to the old fire-and-forget shape rather than hang the request.\n const readyOrFailed = new Promise<\"ready\" | \"failed\">((resolve) => {\n const handler = (status: string) => {\n if (status === \"waiting_input\" || status === \"idle\") {\n this.sessionStatusBus.off(`status:${session.id}`, handler);\n resolve(status === \"waiting_input\" ? \"ready\" : \"failed\");\n }\n };\n this.sessionStatusBus.on(`status:${session.id}`, handler);\n });\n const timeoutPromise = new Promise<\"timeout\">((resolve) =>\n setTimeout(() => resolve(\"timeout\"), START_READY_TIMEOUT_MS),\n );\n\n const outcome = await Promise.race([readyOrFailed, timeoutPromise]);\n const current = this.sessionStore.get(session.id, this.ptyAttachedIds());\n\n if (outcome === \"ready\" && current) {\n json(res, 200, { session: current });\n } else if (outcome === \"failed\" && current) {\n json(res, 502, {\n id: session.id,\n status: \"idle\",\n error: current.failureReason ?? \"Session exited before becoming ready\",\n });\n } else {\n // Timeout, or session vanished from the store — old async contract.\n json(res, 202, { id: session.id, status: \"pending\" });\n }\n\n if (provider === CODEX_CLI_PROVIDER) {\n // Wire up rollout-file binding once Codex creates its persisted session.\n this.watchForCodexRollout(session.id, resolvedPath);\n } else {\n // Wire up JSONL watching once Claude creates the conversation file.\n this.watchForJsonl(session.id, resolvedPath);\n }\n\n this.broadcastOrUnicastSessionList(req);\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Failed to start session\";\n const statusCode =\n typeof (err as Error & { statusCode?: unknown }).statusCode === \"number\"\n ? (err as Error & { statusCode: number }).statusCode\n : 500;\n this.log.error(`[start] failed to start session: ${message}`, {\n event: \"session.start_failed\",\n error: message,\n });\n json(res, statusCode, { error: message });\n }\n }\n\n // ─── Project linking ─────────────────────────────────────────────\n\n private linkSessionToProject(sessionId: string, projectPath: string, filePath: string): void {\n if (!this.projectsRepo || !this.conversationsRepo || !this.sessionsRepo || !this.cache) {\n return;\n }\n try {\n const project = this.projectsRepo.upsertProjectByPath(projectPath, {\n lastConversationId: sessionId,\n lastConversationCreatedAt: new Date().toISOString(),\n });\n // The conversation row may not exist yet (Claude is still writing the\n // JSONL). Best-effort: only link if the row is present.\n if (this.cache.hasConversation(sessionId)) {\n this.conversationsRepo.updateConversationProjectId({\n conversationId: sessionId,\n projectId: project.id,\n });\n }\n this.sessionsRepo.updateSessionProjectId({\n sessionId,\n projectId: project.id,\n });\n if (this.cacheMetadataRepo) {\n this.cacheMetadataRepo.setCacheMetadata(\"last_conversation_id\", sessionId);\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n this.log.warn(`[projects] failed to link session to project: ${message}`, {\n event: \"session.project_link_failed\",\n sessionId,\n projectPath,\n filePath,\n error: message,\n });\n }\n }\n\n // ─── File Watcher Wiring ─────────────────────────────────────────\n\n private async watchConversationFile(sessionId: string): Promise<void> {\n try {\n const conversation = await this.findConversationByUuid(sessionId);\n if (conversation?.filePath) {\n this.sessionFileMap.set(sessionId, conversation.filePath);\n this.fileWatcher.watch(conversation.filePath);\n }\n } catch {\n // Best-effort: if we can't find the JSONL file, raw terminal output still works\n }\n }\n\n // Watch the project directory for the JSONL file Claude creates for sessionId.\n // Once found, wire up structured event streaming. No rekeying needed — the UUID\n // was passed to Claude via --session-id so the filename matches from the start.\n private watchForJsonl(sessionId: string, projectPath: string): void {\n const encoded = projectPath.replace(/[/\\\\:.]/g, \"-\");\n const projectsDir = join(homedir(), \".claude\", \"projects\", encoded);\n const expectedFile = `${sessionId}.jsonl`;\n const filePath = join(projectsDir, expectedFile);\n const deadline = Date.now() + 120_000;\n\n let watcher: ReturnType<typeof fsWatch> | null = null;\n const cleanup = () => {\n try {\n watcher?.close();\n } catch {\n /* ignore */\n }\n };\n\n const tryWire = () => {\n if (!this.ptyManager.hasSession(sessionId)) {\n cleanup();\n return;\n }\n if (Date.now() > deadline) {\n cleanup();\n return;\n }\n\n // Primary: Claude named the file after the session UUID\n let resolvedFilePath = existsSync(filePath) ? filePath : null;\n\n // Fallback: Claude resumed an existing conversation — the JSONL it writes\n // to will be a different UUID. Pick the most recently modified JSONL in\n // the directory that was touched within the last 5 seconds (session just started).\n if (!resolvedFilePath && existsSync(projectsDir)) {\n try {\n const now = Date.now();\n const recent = readdirSync(projectsDir)\n .filter((f) => f.endsWith(\".jsonl\"))\n .map((f) => ({ f, mtime: statSync(join(projectsDir, f)).mtimeMs }))\n .filter(({ mtime }) => now - mtime < 5_000)\n .sort((a, b) => b.mtime - a.mtime)[0];\n if (recent) resolvedFilePath = join(projectsDir, recent.f);\n } catch {\n /* ignore */\n }\n }\n\n if (!resolvedFilePath) return;\n\n cleanup();\n this.sessionFileMap.set(sessionId, resolvedFilePath);\n this.fileWatcher.watch(resolvedFilePath);\n\n // Broadcast any lines already written before the watcher started — Claude\n // can finish writing the JSONL in the same tick as the watcher wires up,\n // so chokidar won't emit a change event for those lines.\n try {\n const existing = readFileSync(resolvedFilePath, \"utf8\").split(\"\\n\").filter(Boolean);\n if (existing.length > 0) {\n this.broadcastConversationLines(sessionId, existing);\n }\n } catch {\n /* ignore — file may not be readable yet; watcher will catch future writes */\n }\n\n if (this.scannerReady) {\n this.scannerStale = true;\n } else {\n this.scanner = null;\n }\n this.linkSessionToProject(sessionId, projectPath, resolvedFilePath);\n this.cache?.markAsStreamer(sessionId);\n this.log.info(\n `[startFresh] wired JSONL for ${sessionId}`,\n { event: \"session.jsonl_wired\", sessionId, filePath: resolvedFilePath },\n \"pino\",\n );\n };\n\n tryWire();\n if (this.sessionFileMap.has(sessionId)) return; // already found\n\n try {\n require(\"fs\").mkdirSync(projectsDir, { recursive: true });\n watcher = fsWatch(projectsDir, tryWire);\n watcher.on(\"error\", cleanup);\n } catch {\n // fs.watch not available (e.g. in tests), ignore\n }\n }\n\n // Codex-equivalent of watchForJsonl(). Differs because Codex has no\n // filename-encoded session id (it assigns its own persisted id) and its\n // rollout files live under a date-nested directory\n // (~/.codex/sessions/<YYYY>/<MM>/<DD>/rollout-*.jsonl) that Codex creates\n // itself — it may not exist yet when this function is first called, so we\n // poll rather than fs.watch a not-yet-existent directory. Per Phase 0\n // findings, the rollout file appears within ~1s of process spawn (after\n // any directory-trust gate is cleared), well before any user input.\n private watchForCodexRollout(sessionId: string, projectPath: string): void {\n const deadline = Date.now() + 120_000;\n const now = new Date();\n const dateDir = join(\n String(now.getFullYear()),\n String(now.getMonth() + 1).padStart(2, \"0\"),\n String(now.getDate()).padStart(2, \"0\"),\n );\n\n // When this placeholder session started. Used to reject a stale same-cwd\n // rollout that Codex wrote before this session launched — the cwd match\n // alone can't tell a fresh rollout from a seconds-old one. 5s of slack\n // absorbs clock skew between our clock and Codex's session_meta timestamp.\n const sessionStartedAtMs =\n (this.sessionStore.getManaged(sessionId)?.startedAt?.getTime() ?? Date.now()) - 5_000;\n\n let intervalHandle: ReturnType<typeof setInterval> | null = null;\n const cleanup = () => {\n if (intervalHandle) clearInterval(intervalHandle);\n intervalHandle = null;\n };\n\n // Read a candidate file's session_meta first line; accept only if its cwd\n // matches this session's projectPath and it was created at/after this\n // session started. Guards against picking up an unrelated concurrent Codex\n // session's rollout, or a stale same-cwd rollout from an earlier run, in\n // the same date-nested directory. Returns { id, createdAtMs } or null.\n const matchesProjectPath = (\n candidatePath: string,\n ): { id: string; createdAtMs: number } | null => {\n try {\n const firstLine = readFileSync(candidatePath, \"utf8\").split(\"\\n\", 1)[0];\n if (!firstLine) return null;\n const parsed = JSON.parse(firstLine);\n if (parsed?.type !== \"session_meta\") return null;\n const payload = parsed.payload ?? {};\n if (payload.cwd !== projectPath) return null;\n if (typeof payload.id !== \"string\") return null;\n // payload.timestamp is Codex's session-creation time; fall back to the\n // outer envelope timestamp if absent.\n const createdIso = payload.timestamp ?? parsed.timestamp;\n const createdAtMs = typeof createdIso === \"string\" ? Date.parse(createdIso) : Number.NaN;\n if (Number.isNaN(createdAtMs) || createdAtMs < sessionStartedAtMs) return null;\n return { id: payload.id, createdAtMs };\n } catch {\n return null;\n }\n };\n\n const tryWire = () => {\n if (!this.ptyManager.hasSession(sessionId)) {\n cleanup();\n return;\n }\n if (Date.now() > deadline) {\n cleanup();\n return;\n }\n\n // Codex ids already bound to another live placeholder — never bind two\n // placeholders to the same rollout (e.g. two Codex sessions started in\n // the same project inside the mtime window).\n const boundElsewhere = new Set(\n this.sessionStore\n .listManaged()\n .filter((s) => s.id !== sessionId && s.boundConversationId != null)\n .map((s) => s.boundConversationId as string),\n );\n\n for (const root of this.codexRoots) {\n const sessionsDir = join(root, dateDir);\n if (!existsSync(sessionsDir)) continue;\n\n let candidateFiles: string[];\n try {\n candidateFiles = readdirSync(sessionsDir).filter((f) => f.endsWith(\".jsonl\"));\n } catch {\n continue;\n }\n\n const nowMs = Date.now();\n const recentCandidates = candidateFiles\n .map((f) => ({ f, mtime: statSync(join(sessionsDir, f)).mtimeMs }))\n .filter(({ mtime }) => nowMs - mtime < 10_000)\n .sort((a, b) => b.mtime - a.mtime);\n\n for (const { f } of recentCandidates) {\n const candidatePath = join(sessionsDir, f);\n const match = matchesProjectPath(candidatePath);\n if (!match) continue;\n if (boundElsewhere.has(match.id)) continue;\n const codexSessionId = match.id;\n\n cleanup();\n this.sessionStore.updateManaged(sessionId, { boundConversationId: codexSessionId });\n\n // Wire the bound rollout into the live update path: tail it for\n // structured events and replay anything already written before the\n // watcher attached (mirrors watchForJsonl()). Without this the bound\n // Codex JSONL is never live-streamed to clients.\n this.sessionFileMap.set(sessionId, candidatePath);\n this.fileWatcher.watch(candidatePath);\n try {\n const existing = readFileSync(candidatePath, \"utf8\").split(\"\\n\").filter(Boolean);\n if (existing.length > 0) {\n this.broadcastConversationLines(sessionId, existing);\n }\n } catch {\n /* ignore — file may not be readable yet; watcher will catch future writes */\n }\n\n if (this.scannerReady) {\n this.scannerStale = true;\n } else {\n this.scanner = null;\n }\n this.linkSessionToProject(sessionId, projectPath, candidatePath);\n this.cache?.markAsStreamer(sessionId);\n\n // Push the binding to subscribers now — the async discovery means the\n // session_update at start time carried no boundConversationId.\n const resp = this.sessionStore.get(sessionId, this.ptyAttachedIds());\n if (resp) {\n this.wsHub.broadcast({ type: \"session_update\", session: resp });\n }\n\n this.log.info(\n `[startFresh] bound Codex rollout for ${sessionId}`,\n {\n event: \"session.codex_rollout_bound\",\n sessionId,\n boundConversationId: codexSessionId,\n filePath: candidatePath,\n },\n \"pino\",\n );\n return;\n }\n }\n };\n\n tryWire();\n if (!intervalHandle && Date.now() <= deadline) {\n // Only keep polling if tryWire() didn't already find + cleanup() the match.\n const alreadyBound = this.sessionStore.getManaged(sessionId)?.boundConversationId != null;\n if (!alreadyBound) {\n intervalHandle = setInterval(tryWire, 250);\n }\n }\n }\n\n private async handleBrowse(url: URL, res: ServerResponse): Promise<void> {\n if (!this.browseRoot) {\n json(res, 403, {\n error: \"File browsing not configured. Set browseRoot on the server.\",\n code: \"BROWSE_ROOT_NOT_SET\",\n });\n return;\n }\n const relativePath = url.searchParams.get(\"path\") ?? \"\";\n try {\n const resolved = await resolveBrowsePath(this.browseRoot, relativePath);\n const directories = await listDirectories(resolved);\n json(res, 200, { path: relativePath, directories });\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Browse failed\";\n if (err instanceof BrowsePathNotFoundError) {\n json(res, 404, { error: message, code: \"PATH_NOT_FOUND\" });\n return;\n }\n json(res, 400, { error: message });\n }\n }\n\n private async handleMkdir(req: IncomingMessage, res: ServerResponse): Promise<void> {\n if (!this.browseRoot) {\n json(res, 403, {\n error: \"File browsing not configured. Set browseRoot on the server.\",\n code: \"BROWSE_ROOT_NOT_SET\",\n });\n return;\n }\n const body = await readBody(req);\n const { path: relativePath, name } = body;\n if (!name || typeof name !== \"string\") {\n json(res, 400, { error: \"Missing name field\" });\n return;\n }\n try {\n const parentPath = await resolveBrowsePath(this.browseRoot, relativePath ?? \"\");\n await createDirectory(parentPath, name);\n const parentRelative = relativePath ?? \"\";\n const created = parentRelative ? `${parentRelative}/${name}` : name;\n json(res, 201, { created });\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Failed to create directory\";\n if (message.includes(\"already exists\")) {\n json(res, 409, { error: message });\n } else if (message.includes(\"Invalid directory name\")) {\n json(res, 400, { error: message });\n } else {\n json(res, 400, { error: message });\n }\n }\n }\n\n private async handleSetSessionName(\n sessionId: string,\n req: IncomingMessage,\n res: ServerResponse,\n ): Promise<void> {\n if (!this.cache) {\n json(res, 503, { error: \"Cache not available\" });\n return;\n }\n let parsed: { name?: string };\n try {\n parsed = await readBody(req);\n } catch {\n json(res, 400, { error: \"Invalid JSON\" });\n return;\n }\n const name = parsed.name?.trim();\n if (!name) {\n json(res, 400, { error: \"name is required\" });\n return;\n }\n this.cache.upsertSessionName(sessionId, name);\n json(res, 200, { ok: true });\n }\n\n private handleGetSessionNames(res: ServerResponse): void {\n if (!this.cache) {\n json(res, 200, {});\n return;\n }\n json(res, 200, this.cache.listSessionNames());\n }\n}\n\n// ─── Utilities ─────────────────────────────────────────────────────\n\n// Classify whether a conversation can be resumed from the project directory\n// (cwd) the session ran in. Shared by the detail handler and the\n// resumable-session shape. A conversation's JSONL parses fine even when its\n// cwd is gone, so callers still serve the full history — this only flags that\n// resume would fail and why. Returns optional meta fields older clients\n// ignore: cwd exists → resumable; gone → not resumable, with a\n// worktree-specific reason when the path was a git worktree (now removed).\nfunction classifyResumability(cwd: string | null | undefined): {\n resumable: boolean;\n unavailable_reason?: \"path_missing\" | \"worktree_removed\";\n} {\n if (!cwd) return { resumable: true };\n if (existsSync(cwd)) return { resumable: true };\n const ranInWorktree = /\\/\\.worktrees\\//.test(cwd) || /\\/\\.claude\\/worktrees\\//.test(cwd);\n return {\n resumable: false,\n unavailable_reason: ranInWorktree ? \"worktree_removed\" : \"path_missing\",\n };\n}\n\nfunction conversationToResumableSession(c: ConversationListItem) {\n const availability = classifyResumability(c.projectPath);\n const provider = c.provider ?? CLAUDE_CODE_PROVIDER;\n return {\n type: \"conversation\" as const,\n id: c.id,\n conversationId: c.id,\n status: \"on_hold\" as const,\n ptyAttached: false,\n projectId: c.projectId ?? undefined,\n projectPath: c.projectPath ?? \"\",\n projectName: c.projectName ?? \"\",\n branch: c.branch ?? undefined,\n lastOutput: \"\",\n elapsedMs: 0,\n promptCount: c.messageCount,\n startedAt: c.lastActivity,\n completedAt: null,\n lastActivityAt: c.lastActivity,\n ...(c.title != null && { sessionName: c.title }),\n ...(c.model != null && { model: c.model }),\n ...(c.account != null && { account: c.account }),\n messageCount: c.messageCount,\n ...(c.preview != null && { preview: c.preview }),\n ...(c.firstMessage != null && { firstMessageText: c.firstMessage }),\n ...(c.lastMessage != null && { lastMessageText: c.lastMessage }),\n filePath: c.filePath,\n provider,\n resumable: isProviderResumable(provider, availability.resumable),\n ...(availability.unavailable_reason && {\n unavailable_reason: availability.unavailable_reason,\n }),\n };\n}\n\nfunction json(res: ServerResponse, status: number, data: unknown): void {\n res.writeHead(status, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(data));\n}\n\nasync function writeHonoResponse(honoRes: Response, res: ServerResponse): Promise<void> {\n const headers: Record<string, string> = {};\n honoRes.headers.forEach((value, key) => {\n headers[key] = value;\n });\n res.writeHead(honoRes.status, headers);\n if (honoRes.body) {\n const reader = honoRes.body.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n res.write(value);\n }\n } finally {\n reader.releaseLock();\n }\n }\n res.end();\n}\n\n// Parse THREADBASE_DIR_SCAN_DEBOUNCE_MS → a non-negative integer, or undefined\n// when unset/invalid so the caller can fall through to config/default.\nfunction parseDirScanDebounceEnv(raw: string | undefined): number | undefined {\n if (raw == null || raw === \"\") return undefined;\n const parsed = Number.parseInt(raw, 10);\n return Number.isNaN(parsed) || parsed < 0 ? undefined : parsed;\n}\n\nfunction intParam(url: URL, name: string, defaultValue: number): number {\n const val = url.searchParams.get(name);\n if (!val) return defaultValue;\n const parsed = Number.parseInt(val, 10);\n return Number.isNaN(parsed) ? defaultValue : parsed;\n}\n\nconst VALID_SORT_KEYS: SessionSortKey[] = [\"startedAt\", \"lastActivityAt\", \"projectName\", \"status\"];\nconst VALID_ORDERS: SessionSortOrder[] = [\"asc\", \"desc\"];\nconst VALID_STATUSES: SessionStatus[] = [\"running\", \"waiting_input\", \"idle\"];\n\nconst SESSIONS_DEFAULT_LIMIT = 200;\nconst SESSIONS_MAX_LIMIT = 500;\n\ntype ParsedSessionListQuery = { query: import(\"./types\").SessionListQuery } | { error: string };\n\nfunction parseSessionListQuery(url: URL): ParsedSessionListQuery {\n const limitRaw = url.searchParams.get(\"limit\");\n let limit = SESSIONS_DEFAULT_LIMIT;\n if (limitRaw !== null) {\n const n = Number.parseInt(limitRaw, 10);\n if (!Number.isFinite(n) || n < 1 || n > SESSIONS_MAX_LIMIT) {\n return { error: `limit must be 1..${SESSIONS_MAX_LIMIT}` };\n }\n limit = n;\n }\n\n const sortByRaw = url.searchParams.get(\"sortBy\") ?? \"startedAt\";\n if (!VALID_SORT_KEYS.includes(sortByRaw as SessionSortKey)) {\n return { error: `sortBy must be one of ${VALID_SORT_KEYS.join(\",\")}` };\n }\n const sortBy = sortByRaw as SessionSortKey;\n\n const orderRaw = url.searchParams.get(\"order\") ?? \"desc\";\n if (!VALID_ORDERS.includes(orderRaw as SessionSortOrder)) {\n return { error: `order must be asc or desc` };\n }\n const order = orderRaw as SessionSortOrder;\n\n const statusRaw = url.searchParams.get(\"status\");\n let status: SessionStatus[] | undefined;\n if (statusRaw) {\n const parts = statusRaw\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n for (const p of parts) {\n if (!VALID_STATUSES.includes(p as SessionStatus)) {\n return { error: `status entry \"${p}\" is invalid` };\n }\n }\n status = parts as SessionStatus[];\n }\n\n const cursor = url.searchParams.get(\"cursor\") ?? undefined;\n\n return { query: { limit, sortBy, order, status, cursor } };\n}\n\nfunction readBody(req: IncomingMessage): Promise<any> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk) => chunks.push(chunk));\n req.on(\"end\", () => {\n try {\n const raw = Buffer.concat(chunks).toString(\"utf-8\");\n resolve(raw ? JSON.parse(raw) : {});\n } catch {\n reject(new Error(\"Invalid JSON body\"));\n }\n });\n req.on(\"error\", reject);\n });\n}\n","import crypto from 'crypto'\n\nimport { urlAlphabet } from './url-alphabet/index.js'\n\nconst POOL_SIZE_MULTIPLIER = 128\nlet pool, poolOffset\n\nlet fillPool = bytes => {\n if (bytes < 0) throw new RangeError('Wrong ID size')\n try {\n if (!pool || pool.length < bytes) {\n pool = Buffer.allocUnsafe(bytes * POOL_SIZE_MULTIPLIER)\n crypto.randomFillSync(pool)\n poolOffset = 0\n } else if (poolOffset + bytes > pool.length) {\n crypto.randomFillSync(pool)\n poolOffset = 0\n }\n } catch (e) {\n pool = undefined\n throw e\n }\n poolOffset += bytes\n}\n\nlet random = bytes => {\n fillPool((bytes |= 0))\n return pool.subarray(poolOffset - bytes, poolOffset)\n}\n\nlet customRandom = (alphabet, defaultSize, getRandom) => {\n let mask = (2 << (31 - Math.clz32((alphabet.length - 1) | 1))) - 1\n\n\n let step = Math.ceil((1.6 * mask * defaultSize) / alphabet.length)\n\n return (size = defaultSize) => {\n let id = ''\n while (true) {\n let bytes = getRandom(step)\n let i = step\n while (i--) {\n id += alphabet[bytes[i] & mask] || ''\n if (id.length === size) return id\n }\n }\n }\n}\n\nlet customAlphabet = (alphabet, size = 21) =>\n customRandom(alphabet, size, random)\n\nlet nanoid = (size = 21) => {\n fillPool((size |= 0))\n let id = ''\n for (let i = poolOffset - size; i < poolOffset; i++) {\n id += urlAlphabet[pool[i] & 63]\n }\n return id\n}\n\nexport { nanoid, customAlphabet, customRandom, urlAlphabet, random }\n","let urlAlphabet =\n 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict'\n\nexport { urlAlphabet }\n","// src/agent/errors.ts\n//\n// Structured error codes for multi-agent HTTP endpoints.\n// Existing PTY-mode endpoints keep their unstructured {error: \"msg\"} shape;\n// the retrofit is captured in tb-multi-agent/docs/plans/structured-error-codes-retrofit.md.\n\nexport const AgentErrorCode = {\n SESSION_NOT_FOUND: \"SESSION_NOT_FOUND\",\n SESSION_HISTORY_FULL: \"SESSION_HISTORY_FULL\",\n SESSION_BUSY: \"SESSION_BUSY\",\n INVALID_SESSION_STATE: \"INVALID_SESSION_STATE\",\n CONVERSATION_NOT_FOUND: \"CONVERSATION_NOT_FOUND\",\n INPUT_REQUIRED: \"INPUT_REQUIRED\",\n INVALID_BODY: \"INVALID_BODY\",\n TEMPORAL_UNAVAILABLE: \"TEMPORAL_UNAVAILABLE\",\n NOT_APPLICABLE_IN_MULTI_AGENT_MODE: \"NOT_APPLICABLE_IN_MULTI_AGENT_MODE\",\n INTERNAL_ERROR: \"INTERNAL_ERROR\",\n} as const;\n\nexport type AgentErrorCode = (typeof AgentErrorCode)[keyof typeof AgentErrorCode];\n\n/**\n * Build a structured error response. `error` and `code` are canonical;\n * `extra` may carry hint fields like `retryAfterMs` or `limitBytes` but\n * cannot override the canonical fields.\n */\nexport function agentErrorResponse(\n code: AgentErrorCode,\n message: string,\n extra: Record<string, unknown> = {},\n): { error: string; code: AgentErrorCode } & Record<string, unknown> {\n return { ...extra, error: message, code };\n}\n","// src/agent/history-mapper.ts\n//\n// Converts a CachedTail (the shape ConversationCache returns) into a\n// ConversationTurn[] (the shape the worker's UserInputSignal expects).\n//\n// Rules per spec §4:\n// - Text blocks: keep, concatenate with \"\\n\".\n// - tool_use, tool_result, thinking blocks: drop.\n// - Messages with empty content after stripping: drop.\n// - Unknown roles: drop with WARN.\n\nimport type { ConversationTurn } from \"@threadbase-sh/agent-types\";\nimport type { CachedTail } from \"../conversation-cache\";\nimport { getLogger } from \"../logger\";\n\nconst log = getLogger(\"agent.history-mapper\");\n\ntype ContentBlock =\n | { type: \"text\"; text: string }\n | { type: \"tool_use\"; id: string; name: string; input: unknown }\n | { type: \"tool_result\"; tool_use_id: string; content: unknown }\n | { type: \"thinking\"; thinking: string }\n | { type: string; [key: string]: unknown };\n\nfunction extractText(message: { text?: string; content?: unknown[] | null }): string {\n // Prefer structured content[] over flat text if both are present.\n if (Array.isArray(message.content)) {\n const blocks = message.content as ContentBlock[];\n const textParts: string[] = [];\n for (const block of blocks) {\n if (\n block &&\n typeof block === \"object\" &&\n block.type === \"text\" &&\n typeof block.text === \"string\"\n ) {\n textParts.push(block.text);\n }\n // tool_use / tool_result / thinking: ignored by design.\n }\n return textParts.join(\"\\n\");\n }\n return typeof message.text === \"string\" ? message.text : \"\";\n}\n\nexport function mapTailToConversationTurns(tail: CachedTail | null): ConversationTurn[] {\n if (!tail || !Array.isArray(tail.messages) || tail.messages.length === 0) {\n return [];\n }\n\n const turns: ConversationTurn[] = [];\n for (const message of tail.messages) {\n const role = message.role;\n if (role !== \"user\" && role !== \"assistant\") {\n log.warn(\"unknown role in tail; skipping\", {\n role,\n conversationId: tail.conversationId,\n });\n continue;\n }\n const content = extractText(message);\n if (!content || content.length === 0) {\n continue;\n }\n turns.push({ role, content });\n }\n return turns;\n}\n","// src/agent/payload-guard.ts\n//\n// Per spec §5: enforce a 1.5 MB ceiling on UserInputSignal payloads (75% of\n// Temporal's 2 MB hard limit) and emit trajectory WARN logs as a session\n// approaches the wall.\n\nimport type { UserInputSignal } from \"@threadbase-sh/agent-types\";\n\nexport interface PayloadMeasurement {\n bytes: number;\n exceedsLimit: boolean;\n}\n\n/**\n * Serialize the signal and measure its byte size. Returns both the count and\n * whether it exceeds the supplied limit.\n *\n * Callers should refuse the input and return 413 SESSION_HISTORY_FULL when\n * `exceedsLimit` is true.\n */\nexport function measureSignalPayload(\n signal: UserInputSignal,\n limitBytes: number,\n): PayloadMeasurement {\n const bytes = Buffer.byteLength(JSON.stringify(signal), \"utf8\");\n return { bytes, exceedsLimit: bytes > limitBytes };\n}\n\nexport interface TrajectoryConfig {\n trajectoryLogBytes: number;\n trajectoryLogTurns: number;\n}\n\n/**\n * Trajectory log trigger. Fires when EITHER:\n * - The session has reached a turn count that's a multiple of 5, starting at\n * `trajectoryLogTurns` (default 20), OR\n * - The composed signal is >= `trajectoryLogBytes` (default 500 KB) regardless\n * of turn count.\n *\n * Returning true means the caller should emit a WARN log line with current\n * size + turn count + percentage-of-limit info.\n */\nexport function shouldLogTrajectory(\n turnCount: number,\n bytes: number,\n cfg: TrajectoryConfig,\n): boolean {\n if (bytes >= cfg.trajectoryLogBytes) return true;\n if (turnCount < cfg.trajectoryLogTurns) return false;\n return (turnCount - cfg.trajectoryLogTurns) % 5 === 0;\n}\n","// src/agent/handle-send-agent-input.ts\n//\n// Pure function for multi-agent user-input. Server.ts wraps this in HTTP\n// plumbing. Implements spec §3.2 + §5 (payload guard) + §6 (lock check).\n\nimport type { UserInputSignal } from \"@threadbase-sh/agent-types\";\nimport { nanoid } from \"nanoid\";\nimport type { ConversationCache } from \"../conversation-cache\";\nimport { getLogger } from \"../logger\";\nimport type { ManagedSession } from \"../types\";\nimport type { AgentClient } from \"./agent-client\";\nimport type { AgentConfig } from \"./agent-config\";\nimport { AgentErrorCode, agentErrorResponse } from \"./errors\";\nimport { mapTailToConversationTurns } from \"./history-mapper\";\nimport { measureSignalPayload, shouldLogTrajectory } from \"./payload-guard\";\n\nconst log = getLogger(\"agent.send-input\");\n\nexport interface SendInputBody {\n text?: string;\n}\n\nexport interface SendAgentInputDeps {\n sessionStore: {\n getManaged: (sessionId: string) => ManagedSession | null;\n };\n cache: ConversationCache;\n agentClient: AgentClient;\n agentConfig: AgentConfig;\n}\n\nexport interface SendAgentInputResult {\n status: number;\n body: Record<string, unknown>;\n}\n\nexport async function handleSendAgentInput(\n sessionId: string,\n body: SendInputBody,\n deps: SendAgentInputDeps,\n): Promise<SendAgentInputResult> {\n // 1. Validate body\n if (typeof body.text !== \"string\" || body.text.length === 0) {\n return {\n status: 400,\n body: agentErrorResponse(\n AgentErrorCode.INPUT_REQUIRED,\n \"Body must contain a non-empty `text` field\",\n ),\n };\n }\n\n // 2. Look up session\n const session = deps.sessionStore.getManaged(sessionId);\n if (!session) {\n return {\n status: 404,\n body: agentErrorResponse(AgentErrorCode.SESSION_NOT_FOUND, `Session ${sessionId} not found`),\n };\n }\n\n // 3. Session-busy check\n if (session.currentTurnId) {\n return {\n status: 429,\n body: agentErrorResponse(\n AgentErrorCode.SESSION_BUSY,\n \"A turn is already in flight; retry shortly\",\n { retryAfterMs: deps.agentConfig.sessionBusyRetryMs },\n ),\n };\n }\n\n const turnId = nanoid();\n // 4. Acquire the lock by setting currentTurnId before any I/O.\n session.currentTurnId = turnId;\n\n // 5. Build conversation history from cache\n const conversationId = session.conversationId ?? session.id;\n const tail = deps.cache.getConversationTail(conversationId);\n const conversationHistory = mapTailToConversationTurns(tail);\n\n // 6. Compose signal\n const signal: UserInputSignal = {\n turnId,\n prompt: body.text,\n conversationHistory,\n };\n\n // 7. Payload-size guard\n const measurement = measureSignalPayload(signal, deps.agentConfig.payload.limitBytes);\n const turnCount = conversationHistory.length;\n if (shouldLogTrajectory(turnCount, measurement.bytes, deps.agentConfig.payload)) {\n log.warn(`session payload trajectory`, {\n sessionId,\n turnCount,\n observedBytes: measurement.bytes,\n limitBytes: deps.agentConfig.payload.limitBytes,\n pctOfLimit: Math.round((measurement.bytes / deps.agentConfig.payload.limitBytes) * 100),\n });\n }\n if (measurement.exceedsLimit) {\n session.currentTurnId = null; // release lock — no signal will be sent\n return {\n status: 413,\n body: agentErrorResponse(\n AgentErrorCode.SESSION_HISTORY_FULL,\n \"Conversation history exceeds payload limit\",\n {\n limitBytes: deps.agentConfig.payload.limitBytes,\n observedBytes: measurement.bytes,\n },\n ),\n };\n }\n\n // 8. Send signal\n try {\n await deps.agentClient.sendUserInput(sessionId, signal);\n } catch (err) {\n session.currentTurnId = null; // release lock on failure\n const message = err instanceof Error ? err.message : \"Temporal unavailable\";\n return {\n status: 503,\n body: agentErrorResponse(AgentErrorCode.TEMPORAL_UNAVAILABLE, message),\n };\n }\n\n return { status: 202, body: { turnId, status: \"queued\" } };\n}\n","// src/agent/handle-start-agent-session.ts\n//\n// Pure function (deps + body in, response out) for multi-agent session\n// creation. Server.ts wraps this in HTTP plumbing.\n\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { nanoid } from \"nanoid\"; // already a transitive dep of tb-streamer\nimport type { ManagedSession } from \"../types\";\nimport type { AgentClient } from \"./agent-client\";\nimport type { AgentConfig } from \"./agent-config\";\nimport { AgentErrorCode, agentErrorResponse } from \"./errors\";\n\nexport interface StartSessionBody {\n conversationId?: string;\n}\n\nexport interface StartAgentSessionDeps {\n sessionStore: {\n addManaged: (session: ManagedSession) => void;\n initAgentSession: (sessionId: string, dedupeCapacity: number) => void;\n };\n agentClient: AgentClient;\n conversationsDir: string;\n agentConfig: AgentConfig;\n}\n\nexport interface StartAgentSessionResult {\n status: number;\n body: Record<string, unknown>;\n}\n\n/**\n * Validate body shape. Accept only `{}` or `{conversationId: string}`.\n * Anything else is `INVALID_BODY`.\n */\nfunction validateBody(body: unknown): { ok: true; conversationId: string | null } | { ok: false } {\n if (body === null || body === undefined || typeof body !== \"object\") {\n return { ok: false };\n }\n const keys = Object.keys(body);\n if (keys.length === 0) {\n return { ok: true, conversationId: null };\n }\n if (keys.length === 1 && keys[0] === \"conversationId\") {\n const v = (body as { conversationId: unknown }).conversationId;\n if (typeof v === \"string\" && v.length > 0) {\n return { ok: true, conversationId: v };\n }\n }\n return { ok: false };\n}\n\nexport async function handleStartAgentSession(\n body: unknown,\n deps: StartAgentSessionDeps,\n): Promise<StartAgentSessionResult> {\n const parsed = validateBody(body);\n if (!parsed.ok) {\n return {\n status: 400,\n body: agentErrorResponse(\n AgentErrorCode.INVALID_BODY,\n \"Body must be {} or {conversationId: string}\",\n ),\n };\n }\n\n let conversationId = parsed.conversationId;\n if (conversationId) {\n // Resume — JSONL must exist\n const jsonlPath = join(deps.conversationsDir, `${conversationId}.jsonl`);\n if (!existsSync(jsonlPath)) {\n return {\n status: 404,\n body: agentErrorResponse(\n AgentErrorCode.CONVERSATION_NOT_FOUND,\n `No conversation found for id ${conversationId}`,\n ),\n };\n }\n }\n\n const sessionId = nanoid();\n if (!conversationId) conversationId = sessionId;\n\n // Build a minimal ManagedSession. PTY-specific fields stay undefined/null;\n // the spec (§3.3) says they're returned as null in multi-agent mode.\n const now = new Date();\n const session: ManagedSession = {\n id: sessionId,\n conversationId,\n projectPath: \"\",\n projectName: \"\",\n branch: \"\",\n status: \"running\",\n startedAt: now,\n completedAt: null,\n promptCount: 0,\n lastOutput: \"\",\n currentTurnId: null,\n };\n\n try {\n deps.sessionStore.addManaged(session);\n deps.sessionStore.initAgentSession(sessionId, deps.agentConfig.dedupe.perSessionCapacity);\n await deps.agentClient.startSession(sessionId);\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Temporal unavailable\";\n return {\n status: 503,\n body: agentErrorResponse(AgentErrorCode.TEMPORAL_UNAVAILABLE, message),\n };\n }\n\n return {\n status: 200,\n body: { sessionId, conversationId, status: \"running\" },\n };\n}\n","import type { HttpBindings } from \"@hono/node-server\";\nimport { Hono } from \"hono\";\nimport type { UpgradeWebSocket } from \"hono/ws\";\nimport type { WebSocket } from \"ws\";\nimport { getLogger } from \"../logger\";\nimport { authMiddleware } from \"./middleware/auth.middleware\";\nimport { corsMiddleware } from \"./middleware/cors.middleware\";\nimport { errorMiddleware } from \"./middleware/error.middleware\";\nimport { createBrowseRoutes } from \"./routes/browse.routes\";\nimport { createConversationRoutes } from \"./routes/conversations.routes\";\nimport { createHealthRoutes } from \"./routes/health.routes\";\nimport { createLogsRoutes } from \"./routes/logs.routes\";\nimport { createMiscRoutes } from \"./routes/misc.routes\";\nimport { createPairRoutes } from \"./routes/pair.routes\";\nimport { createProgressRoutes } from \"./routes/progress.routes\";\nimport { createProjectRoutes } from \"./routes/projects.routes\";\nimport { createScannerRoutes } from \"./routes/scanner.routes\";\nimport { createSessionRoutes } from \"./routes/sessions.routes\";\nimport { createWsRoutes } from \"./routes/ws.routes\";\nimport type { ApiDeps } from \"./types/api-deps\";\n\nexport type AppEnv = {\n Bindings: HttpBindings;\n Variables: {\n requestId?: string;\n validatedBody?: unknown;\n validatedQuery?: unknown;\n };\n};\n\nexport const createHonoApp = (deps: ApiDeps, upgradeWebSocket?: UpgradeWebSocket<WebSocket>) => {\n const app = new Hono<AppEnv>();\n const httpLog = getLogger(\"http\");\n\n app.use(\"*\", async (c, next) => {\n const start = Date.now();\n const ua = c.req.header(\"user-agent\") ?? \"\";\n await next();\n if (!deps.logMenubarRequests && c.req.header(\"x-client\") === \"menubar\") return;\n const ms = Date.now() - start;\n httpLog.info(`[req] ${c.req.method} ${c.req.path} → ${c.res.status} ${ms}ms`, {\n method: c.req.method,\n path: c.req.path,\n status: c.res.status,\n ms,\n ua,\n event: \"http.request\",\n });\n });\n app.use(\"*\", corsMiddleware(deps.browserCors));\n app.use(\"*\", authMiddleware(deps));\n app.onError(errorMiddleware);\n\n app.route(\"/healthz\", createHealthRoutes());\n app.route(\"/\", createMiscRoutes(deps));\n app.route(\"/api/sessions\", createSessionRoutes(deps));\n app.route(\"/api/conversations\", createConversationRoutes(deps));\n app.route(\"/api/projects\", createProjectRoutes(deps));\n app.route(\"/api/pair\", createPairRoutes(deps));\n app.route(\"/api\", createBrowseRoutes(deps));\n app.route(\"/\", createScannerRoutes(deps));\n app.route(\"/internal\", createProgressRoutes(deps));\n app.route(\"/api/logs\", createLogsRoutes());\n\n if (upgradeWebSocket) {\n app.route(\"/\", createWsRoutes(deps, upgradeWebSocket));\n }\n\n return app;\n};\n","import type { MiddlewareHandler } from \"hono\";\nimport { validateApiKey } from \"../../auth\";\nimport type { AppEnv } from \"../app\";\nimport type { ApiDeps } from \"../types/api-deps\";\n\nfunction isLocalRequest(remoteAddr: string | undefined): boolean {\n const addr = remoteAddr ?? \"\";\n return addr === \"127.0.0.1\" || addr === \"::1\" || addr === \"::ffff:127.0.0.1\";\n}\n\nconst PUBLIC_PATHS = new Set([\"/healthz\"]);\n// Localhost-only unauthenticated paths (menubar logs viewer).\nconst LOCAL_ONLY_PATHS = new Set([\"/api/logs\", \"/api/logs/meta\"]);\n// /api/__update uses HMAC signature auth instead of Bearer; skip the\n// Bearer-token middleware so the route handler can validate the signature.\nconst PUBLIC_POST_PATHS = new Set([\"/api/pair/exchange\", \"/api/__update\"]);\n// /internal/sessions/:sessionId/progress also uses HMAC (Progress webhook),\n// and the sessionId is dynamic so we match by prefix.\nconst PUBLIC_POST_PREFIXES = [\"/internal/sessions/\"];\n\nexport const authMiddleware =\n (deps: Pick<ApiDeps, \"apiKey\" | \"localNoAuth\">): MiddlewareHandler<AppEnv> =>\n async (c, next) => {\n const path = new URL(c.req.url).pathname;\n const method = c.req.method;\n const isPublicPostPath =\n method === \"POST\" &&\n (PUBLIC_POST_PATHS.has(path) || PUBLIC_POST_PREFIXES.some((p) => path.startsWith(p)));\n if (PUBLIC_PATHS.has(path) || isPublicPostPath) {\n await next();\n return;\n }\n\n const remoteAddr = c.env.incoming?.socket?.remoteAddress;\n if (LOCAL_ONLY_PATHS.has(path) && isLocalRequest(remoteAddr)) {\n await next();\n return;\n }\n\n if (deps.localNoAuth) {\n if (isLocalRequest(remoteAddr)) {\n await next();\n return;\n }\n }\n\n const authorization = c.req.header(\"authorization\");\n if (authorization?.startsWith(\"Bearer \")) {\n const token = authorization.slice(7);\n if (validateApiKey(token, deps.apiKey)) {\n await next();\n return;\n }\n }\n\n const key = c.req.query(\"key\");\n if (key && validateApiKey(key, deps.apiKey)) {\n await next();\n return;\n }\n\n return c.json({ error: \"Unauthorized\" }, 401);\n };\n","import type { MiddlewareHandler } from \"hono\";\nimport type { AppEnv } from \"../app\";\n\n// Local dev origins allowed when browser CORS is enabled.\nconst DEFAULT_DEV_ORIGINS = [\n \"http://localhost:8081\",\n \"http://localhost:19006\",\n \"http://localhost:3000\",\n];\n\n// Browser CORS is OFF by default: without an Origin allow-list, no web page can\n// make authenticated requests even if it obtains the API key out-of-band. Set\n// THREADBASE_ALLOW_BROWSER_CORS to enable (any of: 1, true, yes, on), and\n// optionally to a comma-separated origin list to allow origins beyond the\n// localhost dev defaults, e.g. THREADBASE_ALLOW_BROWSER_CORS=https://app.example.com\n// Mobile requests aren't browser-originated (no Origin header) and are unaffected.\nexport function resolveAllowedOrigins(raw: string | undefined): Set<string> | null {\n if (!raw) return null;\n const trimmed = raw.trim();\n const lower = trimmed.toLowerCase();\n if (lower === \"0\" || lower === \"false\" || lower === \"no\" || lower === \"off\" || trimmed === \"\") {\n return null;\n }\n\n const origins = new Set(DEFAULT_DEV_ORIGINS);\n // Treat plain on/off tokens as \"just enable the defaults\"; anything else is an\n // explicit origin list to add on top.\n if (![\"1\", \"true\", \"yes\", \"on\"].includes(lower)) {\n for (const o of trimmed.split(\",\")) {\n const origin = o.trim();\n if (origin) origins.add(origin);\n }\n }\n return origins;\n}\n\n// `configValue` is the resolved server.yaml `browser_cors:` setting (if any);\n// THREADBASE_ALLOW_BROWSER_CORS always takes precedence, matching the\n// env-over-yaml precedence used for browseRoot/publicUrl/etc. in server.ts.\nexport const corsMiddleware = (configValue?: string): MiddlewareHandler<AppEnv> => {\n const allowedOrigins = resolveAllowedOrigins(\n process.env.THREADBASE_ALLOW_BROWSER_CORS ?? configValue,\n );\n\n return async (c, next) => {\n const origin = c.req.header(\"origin\");\n const allowedOrigin = allowedOrigins && origin && allowedOrigins.has(origin) ? origin : null;\n\n if (allowedOrigin) {\n // Set on the raw ServerResponse too: many handlers write directly to\n // c.env.outgoing and return the ALREADY_HANDLED sentinel, so Hono never\n // pipes c.res.headers onto the actual response. writeHead() merges (does\n // not clear) setHeader()-set headers, so these survive the direct write.\n const raw = c.env.outgoing;\n raw.setHeader(\"Access-Control-Allow-Origin\", allowedOrigin);\n raw.setHeader(\"Vary\", \"Origin\");\n raw.setHeader(\"Access-Control-Allow-Methods\", \"GET, POST, PATCH, QUERY, OPTIONS\");\n raw.setHeader(\"Access-Control-Allow-Headers\", \"Authorization, Content-Type, If-None-Match\");\n raw.setHeader(\"Access-Control-Expose-Headers\", \"ETag, Accept-Query\");\n\n c.res.headers.set(\"Access-Control-Allow-Origin\", allowedOrigin);\n c.res.headers.set(\"Vary\", \"Origin\");\n c.res.headers.set(\"Access-Control-Allow-Methods\", \"GET, POST, PATCH, QUERY, OPTIONS\");\n c.res.headers.set(\n \"Access-Control-Allow-Headers\",\n \"Authorization, Content-Type, If-None-Match\",\n );\n c.res.headers.set(\"Access-Control-Expose-Headers\", \"ETag, Accept-Query\");\n }\n\n if (c.req.method === \"OPTIONS\") {\n return c.newResponse(null, allowedOrigin ? 204 : 403);\n }\n\n await next();\n };\n};\n","import type { ErrorHandler } from \"hono\";\nimport type { AppEnv } from \"../app\";\n\nexport const errorMiddleware: ErrorHandler<AppEnv> = (err, c) => {\n const message = err instanceof Error ? err.message : \"Internal server error\";\n return c.json({ error: message }, 500);\n};\n","import { Hono } from \"hono\";\nimport type { AppEnv } from \"../app\";\nimport type { ApiDeps } from \"../types/api-deps\";\n\nconst ALREADY_HANDLED = 597;\nconst alreadyHandled = () => new Response(null, { status: ALREADY_HANDLED });\n\nexport const createBrowseRoutes = (deps: ApiDeps) => {\n const app = new Hono<AppEnv>();\n\n app.get(\"/browse\", async (c) => {\n const url = new URL(c.req.url);\n await deps.handleBrowse(url, c.env.outgoing);\n return alreadyHandled();\n });\n\n app.post(\"/browse/mkdir\", async (c) => {\n await deps.handleMkdir(c.env.incoming, c.env.outgoing);\n return alreadyHandled();\n });\n\n return app;\n};\n","import { Hono } from \"hono\";\nimport type { AppEnv } from \"../app\";\nimport type { ApiDeps } from \"../types/api-deps\";\n\nconst ALREADY_HANDLED = 597;\nconst alreadyHandled = () => new Response(null, { status: ALREADY_HANDLED });\n\nexport const createConversationRoutes = (deps: ApiDeps) => {\n const app = new Hono<AppEnv>();\n\n app.get(\"/count\", async (c) => {\n const url = new URL(c.req.url);\n await deps.handleConversationsCount(url, c.env.outgoing);\n return alreadyHandled();\n });\n\n // QUERY (RFC 10008): safe + idempotent + cacheable like GET, but the query\n // travels in a JSON body instead of ?q= — this endpoint's input is a single\n // search string, exactly what QUERY was designed to carry. Must be\n // registered before the greedy \"/:id{.+}\" catch-all or Hono would swallow\n // \"<id>/search-target\" as a conversation id.\n app.on(\"QUERY\", \"/:id{.+}/search-target\", async (c) => {\n const id = c.req.param(\"id\");\n await deps.handleSearchTarget(id, c.env.incoming, c.env.outgoing);\n return alreadyHandled();\n });\n\n app.get(\"/:id{.+}\", async (c) => {\n const id = c.req.param(\"id\");\n const url = new URL(c.req.url);\n const ifNoneMatch = c.req.header(\"if-none-match\");\n await deps.handleGetConversation(id, url, c.env.outgoing, ifNoneMatch);\n return alreadyHandled();\n });\n\n app.get(\"/\", async (c) => {\n const url = new URL(c.req.url);\n await deps.handleListConversations(url, c.env.outgoing);\n return alreadyHandled();\n });\n\n return app;\n};\n","import { Hono } from \"hono\";\nimport { getVersion } from \"../../version\";\nimport type { AppEnv } from \"../app\";\n\nexport const createHealthRoutes = () => {\n const app = new Hono<AppEnv>();\n\n app.get(\"/\", (c) => c.json({ ok: true, version: getVersion() }));\n\n return app;\n};\n","// Runtime version reporting.\n//\n// The version string is NOT baked into the compiled bundle. Each installer\n// (Homebrew, auto-updater, scripts/deploy.sh) writes a `version.txt` next to\n// the script it activates, so the binary reports the correct version even\n// when the tarball it was built from carried a stale package.json.\n//\n// Resolution order:\n// 1. Read `<dirname(process.argv[1])>/version.txt` — set by the installer.\n// 2. Read `<dirname(process.argv[1])>/../version.txt` — the built CLI is a\n// symlink whose realpath resolves one level into $INSTALL_DIR/releases/,\n// so the installer's version.txt sits in the parent dir.\n// 3. Fall back to `<dirname(process.argv[1])>/../package.json` with a\n// `+source` suffix — covers source-tree runs (vitest, ts-node,\n// `npm run dev`) where no installer has stamped a version.\n// 4. If all fail, return \"0.0.0+unknown\" so callers never crash on a\n// missing version.\n\nimport { readFileSync, realpathSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\nlet cached: string | undefined;\n\nexport function getVersion(): string {\n if (cached !== undefined) return cached;\n cached = resolveVersion();\n return cached;\n}\n\n// Exposed for tests to clear the memoization between cases.\nexport function resetVersionCache(): void {\n cached = undefined;\n}\n\nfunction resolveVersion(): string {\n const scriptPath = process.argv[1] ?? \"\";\n const here = scriptPath ? dirname(scriptPath) : process.cwd();\n // Also resolve the realpath so shims (e.g. /opt/homebrew/bin/tb-streamer →\n // ~/.threadbase/cli.js → releases/cli.<sha>.cjs) don't cause version.txt\n // lookups to land in the wrong directory.\n let realHere = here;\n try {\n realHere = dirname(realpathSync(scriptPath));\n } catch {}\n // Deduplicate: if realHere === here we don't want to check the same dirs twice.\n const searchDirs =\n realHere === here\n ? [here, join(here, \"..\")]\n : [here, join(here, \"..\"), realHere, join(realHere, \"..\")];\n // Check both the script directory and its parent — the installer places\n // version.txt in $INSTALL_DIR (~/.threadbase/) but the built CLI is a\n // symlink whose realpath resolves into $INSTALL_DIR/releases/, so `here`\n // ends up one level too deep.\n for (const dir of searchDirs) {\n try {\n const v = readFileSync(join(dir, \"version.txt\"), \"utf8\").trim();\n if (v) return v;\n } catch {}\n }\n try {\n const pkg = JSON.parse(readFileSync(join(here, \"..\", \"package.json\"), \"utf8\")) as {\n version?: string;\n };\n if (pkg.version) return `${pkg.version}+source`;\n } catch {}\n return \"0.0.0+unknown\";\n}\n","import { closeSync, existsSync, fstatSync, openSync, readSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { Hono } from \"hono\";\nimport { installDir } from \"../../lifecycle/constants\";\nimport { getLogger } from \"../../logger\";\n\nconst logger = getLogger(\"logs-api\");\n\ntype LogSource = \"stdout\" | \"stderr\" | \"dev\";\n\nfunction resolveLogPath(source: LogSource): string {\n return join(installDir(), \"logs\", `${source}.log`);\n}\n\nfunction pickDefaultSource(): LogSource {\n // Prefer live prod stdout when present and non-empty; fall back to stderr/dev.\n for (const source of [\"stdout\", \"stderr\", \"dev\"] as const) {\n const p = resolveLogPath(source);\n if (existsSync(p) && statSync(p).size > 0) return source;\n }\n return \"stdout\";\n}\n\n/** Read the last `limit` non-empty content lines starting after `since` line index. */\nfunction readLogLines(\n filePath: string,\n sinceOffset: number,\n limit: number,\n): { lines: string[]; offset: number; total: number } {\n if (!existsSync(filePath)) {\n return { lines: [], offset: 0, total: 0 };\n }\n\n const fd = openSync(filePath, \"r\");\n try {\n const { size } = fstatSync(fd);\n if (size === 0) return { lines: [], offset: 0, total: 0 };\n\n // Cap read window so huge stdout.log (100MB+) stays responsive.\n const maxBytes = Math.min(size, 2 * 1024 * 1024);\n const start = size - maxBytes;\n const buf = Buffer.alloc(maxBytes);\n readSync(fd, buf, 0, maxBytes, start);\n let text = buf.toString(\"utf8\");\n if (start > 0) {\n const firstNl = text.indexOf(\"\\n\");\n if (firstNl >= 0) text = text.slice(firstNl + 1);\n }\n\n const allLines = text.split(\"\\n\").filter((line) => line.trim() && !line.startsWith(\"===\"));\n\n let lines: string[];\n let newOffset: number;\n\n if (sinceOffset > 0 && sinceOffset < allLines.length) {\n lines = allLines.slice(sinceOffset, sinceOffset + limit);\n newOffset = sinceOffset + lines.length;\n } else if (sinceOffset >= allLines.length && sinceOffset > 0) {\n lines = [];\n newOffset = allLines.length;\n } else {\n lines = allLines.slice(-limit);\n newOffset = allLines.length;\n }\n\n return { lines, offset: newOffset, total: allLines.length };\n } finally {\n closeSync(fd);\n }\n}\n\nexport function createLogsRoutes() {\n const app = new Hono();\n\n app.get(\"/\", (c) => {\n try {\n const sourceParam = (c.req.query(\"source\") || \"\").toLowerCase();\n const source: LogSource =\n sourceParam === \"stdout\" || sourceParam === \"stderr\" || sourceParam === \"dev\"\n ? sourceParam\n : pickDefaultSource();\n\n const logPath = resolveLogPath(source);\n const sinceOffset = parseInt(c.req.query(\"since\") || \"0\", 10);\n const limit = Math.min(parseInt(c.req.query(\"limit\") || \"100\", 10) || 100, 1000);\n\n if (!existsSync(logPath)) {\n return c.json({\n logs: [],\n message: `No log file found for source=${source}`,\n offset: 0,\n total: 0,\n source,\n });\n }\n\n const { lines, offset, total } = readLogLines(logPath, sinceOffset, limit);\n const stats = statSync(logPath);\n\n return c.json({\n logs: lines,\n offset,\n total,\n hasMore: offset < total,\n source,\n fileSize: stats.size,\n fileModified: stats.mtime.toISOString(),\n });\n } catch (error) {\n logger.error(\"Failed to read logs\", { error: String(error) });\n return c.json(\n {\n error: \"Failed to read logs\",\n logs: [],\n offset: 0,\n total: 0,\n },\n 500,\n );\n }\n });\n\n app.get(\"/meta\", (c) => {\n try {\n const sources = ([\"stdout\", \"stderr\", \"dev\"] as const).map((source) => {\n const logPath = resolveLogPath(source);\n if (!existsSync(logPath)) {\n return { source, exists: false, total: 0, fileSize: 0 };\n }\n const stats = statSync(logPath);\n return {\n source,\n exists: true,\n fileSize: stats.size,\n fileModified: stats.mtime.toISOString(),\n };\n });\n\n return c.json({\n defaultSource: pickDefaultSource(),\n sources,\n });\n } catch (error) {\n logger.error(\"Failed to read log metadata\", { error: String(error) });\n return c.json({ error: \"Failed to read log metadata\", exists: false }, 500);\n }\n });\n\n return app;\n}\n","import { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport const LAUNCHD_LABEL = \"com.ronen.threadbase\";\nexport const TASK_NAME = process.env.THREADBASE_TASK_NAME ?? \"Threadbase\";\nexport const DEFAULT_PROD_PORT = 8766;\n\nexport function installDir(): string {\n return process.env.THREADBASE_INSTALL_DIR ?? join(homedir(), \".threadbase\");\n}\n\nexport function markerPath(): string {\n return join(installDir(), \"prod-suspended.json\");\n}\n\nexport function prefsPath(): string {\n return join(installDir(), \"dev-prefs.json\");\n}\n\nexport function activeLink(): string {\n return join(installDir(), \"cli.js\");\n}\n","import { spawn } from \"node:child_process\";\nimport { createHmac, timingSafeEqual } from \"node:crypto\";\nimport { Hono } from \"hono\";\nimport type { IncomingMessage } from \"http\";\nimport { hostname } from \"os\";\nimport { loadUpdateConfig } from \"../../config/update-config\";\nimport { getLogger } from \"../../logger\";\nimport { getVersion } from \"../../version\";\nimport type { AppEnv } from \"../app\";\nimport type { ApiDeps } from \"../types/api-deps\";\n\nfunction readJsonBody(req: IncomingMessage): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk) => chunks.push(chunk));\n req.on(\"end\", () => {\n try {\n const raw = Buffer.concat(chunks).toString(\"utf-8\");\n resolve(raw ? JSON.parse(raw) : {});\n } catch {\n reject(new Error(\"Invalid JSON body\"));\n }\n });\n req.on(\"error\", reject);\n });\n}\n\nfunction readRawBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk) => chunks.push(chunk));\n req.on(\"end\", () => resolve(Buffer.concat(chunks).toString(\"utf-8\")));\n req.on(\"error\", reject);\n });\n}\n\nfunction verifyWebhookSignature(body: string, header: string | undefined, secret: string): boolean {\n if (!header) return false;\n const provided = header.startsWith(\"sha256=\") ? header.slice(7) : header;\n const expected = createHmac(\"sha256\", secret).update(body).digest(\"hex\");\n const a = Buffer.from(provided, \"utf-8\");\n const b = Buffer.from(expected, \"utf-8\");\n if (a.length !== b.length) return false;\n return timingSafeEqual(a, b);\n}\n\nconst clientLog = getLogger(\"client\");\n\ntype ClientLogEntry = {\n level?: \"debug\" | \"info\" | \"warn\" | \"error\";\n msg?: string;\n ts?: string;\n tag?: string;\n fields?: Record<string, unknown>;\n};\n\nexport const createMiscRoutes = (\n deps: Pick<\n ApiDeps,\n \"publicUrl\" | \"sessionStore\" | \"ptyAttachedIds\" | \"rotateApiKey\" | \"localNoAuth\"\n >,\n) => {\n const app = new Hono<AppEnv>();\n\n app.get(\"/api/info\", (c) => {\n const ptyIds = deps.ptyAttachedIds();\n return c.json({\n version: getVersion(),\n machineName: hostname(),\n platform: process.platform,\n activeSessions: deps.sessionStore.list(ptyIds).filter((s) => s.status === \"running\").length,\n publicUrl: deps.publicUrl,\n });\n });\n\n app.get(\"/api/profiles\", (c) => c.json([]));\n\n app.post(\"/api/auth/rotate\", (c) => {\n // Block rotation when localNoAuth is on — any localhost process could\n // call this and lock out the legitimate owner.\n if (deps.localNoAuth) {\n return c.json({ error: \"key rotation is disabled while localNoAuth is active\" }, 403);\n }\n const { newKey, persisted } = deps.rotateApiKey();\n return c.json({\n apiKey: newKey,\n persisted,\n ...(persisted\n ? {}\n : {\n warning:\n \"Key rotated in memory only. The server was started with --api-key, so the \" +\n \"old key will be restored on restart. Remove --api-key and let the server \" +\n \"manage the key via ~/.threadbase/server.yaml for rotation to survive restarts.\",\n }),\n });\n });\n\n app.post(\"/api/push/register\", (c) => c.json({ ok: true }));\n\n // Webhook for auto-update. Triggered by the release CI (or any caller that\n // knows webhook_secret) to make this server pull the new release without\n // waiting for the next poll. Enabled only when webhook_secret is set in\n // ~/.threadbase/update.yaml. HMAC-SHA256 of the raw body using that secret\n // must match the X-Threadbase-Signature header.\n app.post(\"/api/__update\", async (c) => {\n const cfg = loadUpdateConfig();\n if (!cfg?.webhook_secret) {\n return c.json({ error: \"webhook disabled\" }, 404);\n }\n\n let body: string;\n try {\n body = await readRawBody(c.env.incoming);\n } catch {\n return c.json({ error: \"could not read body\" }, 400);\n }\n\n const sig = c.req.header(\"x-threadbase-signature\");\n if (!verifyWebhookSignature(body, sig, cfg.webhook_secret)) {\n return c.json({ error: \"invalid signature\" }, 401);\n }\n\n const cliPath = process.argv[1];\n if (!cliPath) {\n return c.json({ error: \"cannot resolve updater path\" }, 500);\n }\n const child = spawn(process.execPath, [cliPath, \"update\", \"--force\"], {\n detached: true,\n stdio: \"ignore\",\n });\n child.unref();\n\n return c.json({ accepted: true, pid: child.pid }, 202);\n });\n\n app.post(\"/api/__client-log\", async (c) => {\n const ua = c.req.header(\"user-agent\") ?? \"\";\n let body: { entries?: ClientLogEntry[] } = {};\n try {\n body = (await readJsonBody(c.env.incoming)) as { entries?: ClientLogEntry[] };\n } catch {\n return c.json({ ok: false, error: \"invalid json\" }, 400);\n }\n const entries = Array.isArray(body.entries) ? body.entries : [];\n for (const e of entries) {\n const level =\n e.level === \"debug\" || e.level === \"warn\" || e.level === \"error\" ? e.level : \"info\";\n clientLog[level](`[client] ${e.tag ?? \"log\"}: ${e.msg ?? \"\"}`, {\n clientTs: e.ts,\n tag: e.tag,\n ua,\n ...(e.fields ?? {}),\n });\n }\n return c.json({ ok: true, accepted: entries.length });\n });\n\n return app;\n};\n","import { readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { parse as parseYaml } from \"yaml\";\nimport { type UpdateConfig, UpdateConfigSchema } from \"../schemas/updateConfig.schema\";\n\nconst DEFAULT_CONFIG_PATH = join(homedir(), \".threadbase\", \"update.yaml\");\n\nexport interface LoadUpdateConfigOptions {\n path?: string;\n}\n\n/**\n * Loads ~/.threadbase/update.yaml. Returns null when the file does not exist\n * (auto-update disabled). Throws on malformed YAML or schema-invalid content\n * so misconfiguration is loud rather than silently disabling updates.\n */\nexport function loadUpdateConfig(opts: LoadUpdateConfigOptions = {}): UpdateConfig | null {\n const path = opts.path ?? DEFAULT_CONFIG_PATH;\n\n let raw: string;\n try {\n raw = readFileSync(path, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw err;\n }\n\n const parsed: unknown = parseYaml(raw);\n if (parsed === null || parsed === undefined) {\n throw new Error(`update.yaml at ${path} is empty — github_repo is required`);\n }\n\n return UpdateConfigSchema.parse(parsed);\n}\n\nexport { DEFAULT_CONFIG_PATH as UPDATE_CONFIG_PATH };\n","import { z } from \"zod\";\n\nexport const UpdateConfigSchema = z\n .object({\n auto_update: z.boolean().default(false),\n channel: z.enum([\"stable\", \"next\"]).default(\"stable\"),\n allow: z.array(z.enum([\"patch\", \"minor\", \"major\"])).default([\"patch\", \"minor\"]),\n poll_interval_minutes: z.number().int().min(0).default(60),\n defer_if_active_sessions: z.boolean().default(true),\n github_repo: z.string().regex(/^[^/]+\\/[^/]+$/, \"github_repo must be 'owner/name'\"),\n webhook_secret: z.string().min(1).nullable().default(null),\n })\n .strict();\n\nexport type UpdateConfig = z.infer<typeof UpdateConfigSchema>;\n","import { Hono } from \"hono\";\nimport type { AppEnv } from \"../app\";\nimport type { ApiDeps } from \"../types/api-deps\";\n\nconst ALREADY_HANDLED = 597;\nconst alreadyHandled = () => new Response(null, { status: ALREADY_HANDLED });\n\nexport const createPairRoutes = (deps: ApiDeps) => {\n const app = new Hono<AppEnv>();\n\n app.post(\"/start\", (c) => {\n deps.handlePairStart(c.env.outgoing);\n return alreadyHandled();\n });\n\n // /api/pair/exchange is public — auth middleware skips it\n app.post(\"/exchange\", async (c) => {\n await deps.handlePairExchange(c.env.incoming, c.env.outgoing);\n return alreadyHandled();\n });\n\n return app;\n};\n","import { Hono } from \"hono\";\nimport type { AppEnv } from \"../app\";\nimport type { ApiDeps } from \"../types/api-deps\";\n\nconst ALREADY_HANDLED = 597;\nconst alreadyHandled = () => new Response(null, { status: ALREADY_HANDLED });\n\nexport const createProjectRoutes = (deps: ApiDeps) => {\n const app = new Hono<AppEnv>();\n\n app.get(\"/\", (c) => {\n const url = new URL(c.req.url);\n deps.handleListProjects(url, c.env.outgoing);\n return alreadyHandled();\n });\n\n app.get(\"/popular\", (c) => {\n const url = new URL(c.req.url);\n deps.handleGetPopularProjects(url, c.env.outgoing);\n return alreadyHandled();\n });\n\n return app;\n};\n","import { Hono } from \"hono\";\nimport type { AppEnv } from \"../app\";\nimport type { ApiDeps } from \"../types/api-deps\";\n\nconst ALREADY_HANDLED = 597;\nconst alreadyHandled = () => new Response(null, { status: ALREADY_HANDLED });\n\nexport const createScannerRoutes = (deps: ApiDeps) => {\n const app = new Hono<AppEnv>();\n\n app.get(\"/api/search\", async (c) => {\n const url = new URL(c.req.url);\n await deps.handleSearch(url, c.env.outgoing);\n return alreadyHandled();\n });\n\n return app;\n};\n","import { Hono } from \"hono\";\nimport type { AppEnv } from \"../app\";\nimport type { ApiDeps } from \"../types/api-deps\";\n\n// Sentinel status used to signal that the handler already wrote to the Node\n// ServerResponse directly. writeHonoResponse in server.ts skips piping when\n// it sees this status.\nexport const ALREADY_HANDLED = 597;\nconst alreadyHandled = () => new Response(null, { status: ALREADY_HANDLED });\n\nexport const createSessionRoutes = (deps: ApiDeps) => {\n const app = new Hono<AppEnv>();\n\n app.get(\"/count\", (c) => {\n deps.handleSessionsCount(c.env.outgoing);\n return alreadyHandled();\n });\n\n app.get(\"/recents\", (c) => {\n const url = new URL(c.req.url);\n deps.handleGetRecentSessions(url, c.env.outgoing);\n return alreadyHandled();\n });\n\n app.get(\"/names\", (c) => {\n deps.handleGetSessionNames(c.env.outgoing);\n return alreadyHandled();\n });\n\n app.post(\"/resume\", async (c) => {\n await deps.handleResume(c.env.incoming, c.env.outgoing);\n return alreadyHandled();\n });\n\n app.post(\"/start\", async (c) => {\n await deps.handleStartSession(c.env.incoming, c.env.outgoing);\n return alreadyHandled();\n });\n\n app.get(\"/\", async (c) => {\n const url = new URL(c.req.url);\n await deps.handleListSessions(url, c.env.outgoing);\n return alreadyHandled();\n });\n\n app.get(\"/:id/output\", (c) => {\n deps.handleGetOutput(c.req.param(\"id\"), c.env.outgoing);\n return alreadyHandled();\n });\n\n app.post(\"/:id/input\", async (c) => {\n await deps.handleSendInput(c.req.param(\"id\"), c.env.incoming, c.env.outgoing);\n return alreadyHandled();\n });\n\n app.post(\"/:id/answer\", async (c) => {\n await deps.handleSendAnswer(c.req.param(\"id\"), c.env.incoming, c.env.outgoing);\n return alreadyHandled();\n });\n\n app.post(\"/:id/files\", async (c) => {\n await deps.handleUploadFile(c.req.param(\"id\"), c.env.incoming, c.env.outgoing);\n return alreadyHandled();\n });\n\n app.post(\"/:id/cancel\", (c) => {\n deps.handleCancel(c.req.param(\"id\"), c.env.outgoing);\n return alreadyHandled();\n });\n\n app.patch(\"/:id/name\", async (c) => {\n await deps.handleSetSessionName(c.req.param(\"id\"), c.env.incoming, c.env.outgoing);\n return alreadyHandled();\n });\n\n app.post(\"/:id/adopt\", async (c) => {\n await deps.handleAdopt(c.req.param(\"id\"), c.env.outgoing);\n return alreadyHandled();\n });\n\n app.post(\"/:id/stop\", async (c) => {\n await deps.handleStopSession(c.req.param(\"id\"), c.env.outgoing);\n return alreadyHandled();\n });\n\n app.get(\"/:id\", (c) => {\n deps.handleGetSession(c.req.param(\"id\"), c.env.outgoing);\n return alreadyHandled();\n });\n\n return app;\n};\n","import { Hono } from \"hono\";\nimport type { UpgradeWebSocket } from \"hono/ws\";\nimport type { WebSocket } from \"ws\";\nimport type { AppEnv } from \"../app\";\nimport type { ApiDeps } from \"../types/api-deps\";\n\nexport const createWsRoutes = (deps: ApiDeps, upgradeWebSocket: UpgradeWebSocket<WebSocket>) => {\n const app = new Hono<AppEnv>();\n\n app.get(\n \"/ws\",\n upgradeWebSocket(() => {\n let openWs: WebSocket | null = null;\n return {\n onOpen(_evt, ws) {\n const raw = ws.raw;\n if (!raw) return;\n openWs = raw;\n deps.handleWsOpen(raw);\n },\n onMessage(evt, _ws) {\n if (openWs) deps.handleWsMessage(openWs, evt.data);\n },\n onClose(_evt, _ws) {\n if (openWs) deps.handleWsClose(openWs);\n },\n };\n }),\n );\n\n return app;\n};\n","import { mkdir, readdir, realpath, stat } from \"fs/promises\";\nimport { join, resolve, sep } from \"path\";\n\n/**\n * Thrown when a browse target is inside the root but does not exist on disk\n * (e.g. a mobile-cached path whose folder was since moved or deleted). Lets the\n * browse handler answer 404 instead of conflating it with an out-of-root 400.\n */\nexport class BrowsePathNotFoundError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"BrowsePathNotFoundError\";\n }\n}\n\nexport async function resolveBrowsePath(browseRoot: string, relativePath: string): Promise<string> {\n const normalizedRoot = resolve(browseRoot);\n // On Unix, if relativePath is already an absolute path under browseRoot, use it directly.\n // Only strip the leading separator for bare names like \"/projectA\" sent by the mobile browse\n // tree — not for full paths like \"/Users/foo/bar\" which are absolute, not drive-root-relative.\n // On Windows we always strip because \"\\foo\" means \"drive root relative\", never a full path.\n let sanitized: string;\n if (\n process.platform !== \"win32\" &&\n relativePath.startsWith(\"/\") &&\n relativePath.length > 1 &&\n relativePath.includes(\"/\", 1)\n ) {\n sanitized = relativePath;\n } else {\n sanitized = relativePath.replace(/^[/\\\\]+/, \"\");\n }\n const target = sanitized ? resolve(normalizedRoot, sanitized) : normalizedRoot;\n // Build the allowed prefix with exactly one separator — normalizedRoot may already end with sep\n // when browseRoot is a drive root (e.g. \"C:\\\"), which would otherwise create a double-sep prefix.\n const rootPrefix = normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`;\n if (!target.startsWith(rootPrefix) && target !== normalizedRoot) {\n throw new Error(\"Path outside browse root\");\n }\n // Verify the path exists; surface a not-found as a typed error so the handler\n // can answer 404 (folder gone) rather than the out-of-root 400 above.\n try {\n await realpath(target);\n } catch (err: any) {\n if (err?.code === \"ENOENT\") {\n throw new BrowsePathNotFoundError(`Path not found: ${target}`);\n }\n throw err;\n }\n return target;\n}\n\nexport async function listDirectories(absolutePath: string): Promise<Array<{ name: string }>> {\n const entries = await readdir(absolutePath, { withFileTypes: true });\n return entries\n .filter((e) => e.isDirectory())\n .map((e) => ({ name: e.name }))\n .sort((a, b) => a.name.localeCompare(b.name));\n}\n\nexport async function createDirectory(parentAbsolutePath: string, name: string): Promise<string> {\n if (name.includes(\"/\") || name.includes(\"\\\\\") || name === \"..\" || name === \".\") {\n throw new Error(\"Invalid directory name\");\n }\n const target = join(parentAbsolutePath, name);\n try {\n const s = await stat(target);\n if (s.isDirectory()) throw new Error(\"Directory already exists\");\n } catch (err: any) {\n if (err.code !== \"ENOENT\") throw err;\n }\n await mkdir(target);\n return target;\n}\n","import {\n type ConversationMessage,\n type ConversationMeta,\n createJsonlParseState,\n type FileStatEntry,\n type JsonlParseState,\n parseJsonlLine,\n} from \"@threadbase-sh/scanner\";\nimport Database from \"better-sqlite3\";\nimport { closeSync, existsSync, mkdirSync, openSync, readSync, type Stats, statSync } from \"fs\";\nimport { open as openAsync } from \"fs/promises\";\nimport { dirname } from \"path\";\nimport { setImmediate as yieldToEventLoop } from \"timers/promises\";\nimport { runSqliteMigrations } from \"./db/sqlite-migrate\";\nimport { CLAUDE_CODE_PROVIDER } from \"./providers\";\nimport {\n DEFAULT_AGENT_ENTRYPOINTS,\n isAgentFile,\n isAgentLine,\n} from \"./services/conversations/isAgentConversation\";\nimport { fileIdentity, type LineSpan, splitCompleteLines } from \"./utils/fileIdentity\";\n\nexport interface ConversationCacheOptions {\n // When true, drop conversations whose JSONL came from an agent entrypoint.\n // Default false to preserve legacy behavior.\n filterAgentConversations?: boolean;\n // Set of `entrypoint` values to treat as agent traffic. Defaults to\n // DEFAULT_AGENT_ENTRYPOINTS ({ sdk-cli, claude-vscode }).\n agentEntrypoints?: ReadonlySet<string>;\n // Fired the first time an agent JSONL is detected for a given file path\n // (from updateFromLine). Lets the server unwatch the file.\n onAgentFileDetected?: (filePath: string) => void;\n}\n\nexport interface ConversationListItem {\n id: string;\n filePath: string;\n projectId: string | null;\n projectPath: string | null;\n projectName: string | null;\n title: string | null;\n model: string | null;\n account: string | null;\n branch: string | null;\n messageCount: number;\n lastActivity: string;\n firstMessage: string | null;\n lastMessage: string | null;\n preview: string | null;\n source: string | null;\n provider: \"claude-code\" | \"codex-cli\";\n}\n\nexport interface CachedTailMessage {\n role: string;\n timestamp: string;\n text: string;\n content?: unknown[];\n}\n\nexport interface CachedTail {\n conversationId: string;\n messages: CachedTailMessage[];\n tailSize: number;\n}\n\n/** A row of `conversation_file_state` — per-file offset-index resume state. */\nexport interface FileStateRow {\n path: string;\n identity: string;\n size: number;\n mtime_ms: number;\n byte_offset: number;\n last_message_index: number;\n}\n\n/** A row of `conversation_message_index` — one indexed message's byte span. */\nexport interface MessageIndexRow {\n conversation_id: string;\n message_index: number;\n byte_offset: number;\n byte_length: number;\n uuid: string | null;\n role: string | null;\n ts: number | null;\n}\n\nexport interface ScannerMeta {\n id: string;\n sessionId?: string;\n filePath: string;\n projectPath?: string;\n projectName?: string;\n title?: string;\n model?: string;\n account?: string;\n gitBranch?: string;\n messageCount?: number;\n timestamp?: string;\n firstMessage?: unknown;\n lastMessage?: unknown;\n preview?: string;\n provider?: \"claude-code\" | \"codex-cli\";\n}\n\ninterface MetaRow {\n id: string;\n file_path: string;\n project_id: string | null;\n project_path: string | null;\n project_name: string | null;\n title: string | null;\n model: string | null;\n account: string | null;\n branch: string | null;\n message_count: number;\n last_activity: number | null;\n first_message: string | null;\n last_message: string | null;\n preview: string | null;\n source: string | null;\n provider: \"claude-code\" | \"codex-cli\";\n updated_at: number;\n scanner_meta_json: string | null;\n}\n\ninterface TailRow {\n conversation_id: string;\n messages_json: string;\n tail_size: number;\n updated_at: number;\n}\n\ntype ContentBlock = { type: string; text?: string; [key: string]: unknown };\n\ninterface JsonlLine {\n role?: string;\n type?: string;\n timestamp?: string;\n // Set by Claude Code / Agent SDK on every real message line. \"cli\" = human\n // interactive Claude Code; \"sdk-cli\" = Claude Agent SDK / claude-mem / hooks.\n entrypoint?: string;\n // Project context: the scanner sets `cwd` from any line that carries it\n // (attachment, metadata, user, assistant). The live watcher must do the\n // same — otherwise skeleton rows persist with NULL project_path.\n cwd?: string;\n slug?: string;\n // Real Claude JSONL emits either an array of blocks or a raw string. Normalize\n // via `normalizeContent` before consuming.\n content?: ContentBlock[] | string;\n message?: {\n role?: string;\n content?: ContentBlock[] | string;\n };\n}\n\n// Last three path segments — mirrors @threadbase-sh/scanner's\n// `getShortProjectName`. Inlined here because the scanner does not export it.\nfunction shortProjectName(fullPath: string): string {\n const parts = fullPath.split(/[/\\\\]/).filter(Boolean);\n return parts.slice(-3).join(\"/\");\n}\n\nfunction normalizeContent(raw: ContentBlock[] | string | null | undefined): ContentBlock[] {\n if (Array.isArray(raw)) return raw;\n if (typeof raw === \"string\") return [{ type: \"text\", text: raw }];\n return [];\n}\n\nconst SCHEMA = `\nCREATE TABLE IF NOT EXISTS conversation_meta (\n id TEXT PRIMARY KEY,\n file_path TEXT NOT NULL,\n project_path TEXT,\n project_name TEXT,\n title TEXT,\n model TEXT,\n account TEXT,\n branch TEXT,\n message_count INTEGER DEFAULT 0,\n last_activity INTEGER,\n first_message TEXT,\n last_message TEXT,\n preview TEXT,\n updated_at INTEGER NOT NULL\n);\nCREATE INDEX IF NOT EXISTS idx_meta_last_activity ON conversation_meta(last_activity DESC);\nCREATE INDEX IF NOT EXISTS idx_meta_project ON conversation_meta(project_path);\n\nCREATE TABLE IF NOT EXISTS conversation_tail (\n conversation_id TEXT PRIMARY KEY REFERENCES conversation_meta(id) ON DELETE CASCADE,\n messages_json TEXT NOT NULL,\n tail_size INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS session_names (\n session_id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n updated_at INTEGER NOT NULL\n);\n`;\n\nexport class ConversationCache {\n private db: Database.Database;\n private tailSize: number;\n private fileIndex = new Map<string, string>();\n private fileIndexLoaded = false;\n // Per-file scanner parse state for the incremental offset-index writer. The\n // reducer is stateful across lines (pending tool_uses, latest timestamp), so\n // it must persist between watcher reads of the same file. Cleared on\n // truncation/backfill.\n private indexParseState = new Map<string, JsonlParseState>();\n // Single-flight guard for backfillIndex — concurrent detail requests for the\n // same cold file await one walk, not N. Entry dropped on settle.\n private backfillInFlight = new Map<string, Promise<void>>();\n // Monotonically increasing counter for tail updated_at — guarantees strict\n // ordering even when multiple updateFromLine() calls land within the same ms.\n private tailSeq = Date.now();\n // Monotonically increasing counter for session_names updated_at.\n private nameSeq = Date.now();\n\n private stmts: {\n getById: Database.Statement;\n getFullById: Database.Statement;\n updateMeta: Database.Statement;\n updateMetaBatch: Database.Statement;\n insertSkeleton: Database.Statement;\n backfillSkeletonProject: Database.Statement;\n upsertFull: Database.Statement;\n getTail: Database.Statement;\n hasTail: Database.Statement;\n upsertTail: Database.Statement;\n list: Database.Statement;\n count: Database.Statement;\n listByProject: Database.Statement;\n countByProject: Database.Statement;\n listByProvider: Database.Statement;\n countByProvider: Database.Statement;\n deleteById: Database.Statement;\n deleteTailById: Database.Statement;\n deleteAll: Database.Statement;\n deleteTailAll: Database.Statement;\n getIdByFilePath: Database.Statement;\n getProviderByFilePath: Database.Statement;\n allFilePaths: Database.Statement;\n allFileStats: Database.Statement;\n allScannerStatCacheRows: Database.Statement;\n updateScannerCache: Database.Statement;\n getFileMetadata: Database.Statement;\n upsertFileMetadata: Database.Statement;\n upsertSessionName: Database.Statement;\n getSessionName: Database.Statement;\n listSessionNames: Database.Statement;\n setConversationProjectId: Database.Statement;\n markAsStreamer: Database.Statement;\n getLatestConversation: Database.Statement;\n listConversationsForProjectBackfill: Database.Statement;\n hasOrphanProjectId: Database.Statement;\n popularProjects: Database.Statement;\n getFileState: Database.Statement;\n upsertFileState: Database.Statement;\n deleteFileState: Database.Statement;\n deleteMessageIndex: Database.Statement;\n insertMessageIndexRow: Database.Statement;\n getMessageIndexWindow: Database.Statement;\n getIndexedMessageCount: Database.Statement;\n };\n\n private migrationsDir?: string;\n\n // When true, ingestion drops conversations whose JSONL `entrypoint` belongs\n // to `agentEntrypoints`. See isAgentConversation.ts.\n private filterAgentConversations = false;\n private agentEntrypoints: ReadonlySet<string> = DEFAULT_AGENT_ENTRYPOINTS;\n private onAgentFileDetected?: (filePath: string) => void;\n\n private constructor(\n db: Database.Database,\n tailSize: number,\n migrationsDir?: string,\n options?: ConversationCacheOptions,\n ) {\n this.migrationsDir = migrationsDir;\n this.db = db;\n this.tailSize = tailSize;\n this.filterAgentConversations = options?.filterAgentConversations ?? false;\n this.agentEntrypoints = options?.agentEntrypoints ?? DEFAULT_AGENT_ENTRYPOINTS;\n this.onAgentFileDetected = options?.onAgentFileDetected;\n db.exec(SCHEMA);\n runSqliteMigrations(db, this.migrationsDir);\n this.stmts = {\n getById: db.prepare(\"SELECT id FROM conversation_meta WHERE id = ?\"),\n getFullById: db.prepare(\"SELECT * FROM conversation_meta WHERE id = ?\"),\n updateMeta: db.prepare(\n \"UPDATE conversation_meta SET message_count = message_count + 1, last_activity = ?, last_message = ?, updated_at = ? WHERE id = ?\",\n ),\n // Batch equivalent of updateMeta: bumps message_count by N in one write\n // (used by updateFromLines so a burst of appended lines is one UPDATE).\n updateMetaBatch: db.prepare(\n \"UPDATE conversation_meta SET message_count = message_count + @inc, last_activity = @last_activity, last_message = @last_message, updated_at = @updated_at WHERE id = @id\",\n ),\n insertSkeleton: db.prepare(\n \"INSERT OR IGNORE INTO conversation_meta (id, file_path, message_count, updated_at) VALUES (?, ?, 1, ?)\",\n ),\n // Fills project_path / project_name / title on a row whose columns are\n // still NULL. Never overwrites scanner-populated values — the scanner's\n // upsertFromScannerMeta remains authoritative for those columns.\n backfillSkeletonProject: db.prepare(\n `UPDATE conversation_meta\n SET project_path = COALESCE(project_path, @project_path),\n project_name = COALESCE(project_name, @project_name),\n title = COALESCE(title, @title)\n WHERE id = @id\n AND (project_path IS NULL OR project_name IS NULL OR title IS NULL)`,\n ),\n upsertFull: db.prepare(`\n INSERT INTO conversation_meta\n (id, file_path, project_path, project_name, title, model, account, branch,\n message_count, last_activity, first_message, last_message, preview, updated_at,\n mtime_ms, file_size, provider, scanner_meta_json)\n VALUES\n (@id, @file_path, @project_path, @project_name, @title, @model, @account, @branch,\n @message_count, @last_activity, @first_message, @last_message, @preview, @updated_at,\n @mtime_ms, @file_size, @provider, @scanner_meta_json)\n ON CONFLICT(id) DO UPDATE SET\n file_path = excluded.file_path,\n project_path = excluded.project_path,\n project_name = excluded.project_name,\n title = excluded.title,\n model = excluded.model,\n account = excluded.account,\n branch = excluded.branch,\n -- message_count is incremented by live tailing but recounted from\n -- scratch by a scanner rescan; a stale rescan must not carry a live\n -- session's count backwards, so take the max instead of overwriting.\n message_count = MAX(conversation_meta.message_count, excluded.message_count),\n last_activity = excluded.last_activity,\n first_message = excluded.first_message,\n last_message = excluded.last_message,\n preview = excluded.preview,\n updated_at = excluded.updated_at,\n mtime_ms = excluded.mtime_ms,\n file_size = excluded.file_size,\n provider = excluded.provider,\n scanner_meta_json = excluded.scanner_meta_json\n WHERE conversation_meta.updated_at < excluded.updated_at\n `),\n getTail: db.prepare(\"SELECT * FROM conversation_tail WHERE conversation_id = ?\"),\n hasTail: db.prepare(\"SELECT 1 FROM conversation_tail WHERE conversation_id = ? LIMIT 1\"),\n upsertTail: db.prepare(`\n INSERT INTO conversation_tail (conversation_id, messages_json, tail_size, updated_at)\n VALUES (?, ?, ?, ?)\n ON CONFLICT(conversation_id) DO UPDATE SET\n messages_json = excluded.messages_json,\n tail_size = excluded.tail_size,\n updated_at = excluded.updated_at\n WHERE conversation_tail.updated_at < excluded.updated_at\n `),\n list: db.prepare(\n \"SELECT * FROM conversation_meta ORDER BY last_activity DESC LIMIT ? OFFSET ?\",\n ),\n count: db.prepare(\"SELECT COUNT(*) as n FROM conversation_meta\"),\n listByProject: db.prepare(\n \"SELECT * FROM conversation_meta WHERE project_path = ? ORDER BY last_activity DESC LIMIT ? OFFSET ?\",\n ),\n countByProject: db.prepare(\n \"SELECT COUNT(*) as n FROM conversation_meta WHERE project_path = ?\",\n ),\n listByProvider: db.prepare(\n \"SELECT * FROM conversation_meta WHERE provider = ? ORDER BY last_activity DESC LIMIT ? OFFSET ?\",\n ),\n countByProvider: db.prepare(\"SELECT COUNT(*) as n FROM conversation_meta WHERE provider = ?\"),\n deleteById: db.prepare(\"DELETE FROM conversation_meta WHERE id = ?\"),\n deleteTailById: db.prepare(\"DELETE FROM conversation_tail WHERE conversation_id = ?\"),\n deleteAll: db.prepare(\"DELETE FROM conversation_meta\"),\n deleteTailAll: db.prepare(\"DELETE FROM conversation_tail\"),\n getIdByFilePath: db.prepare(\"SELECT id FROM conversation_meta WHERE file_path = ?\"),\n getProviderByFilePath: db.prepare(\n \"SELECT provider FROM conversation_meta WHERE file_path = ?\",\n ),\n allFilePaths: db.prepare(\"SELECT id, file_path FROM conversation_meta\"),\n allFileStats: db.prepare(\n \"SELECT file_path, mtime_ms, file_size FROM conversation_meta WHERE mtime_ms IS NOT NULL AND file_size IS NOT NULL\",\n ),\n allScannerStatCacheRows: db.prepare(\n \"SELECT file_path, mtime_ms, file_size, scanner_meta_json FROM conversation_meta WHERE mtime_ms IS NOT NULL AND file_size IS NOT NULL AND scanner_meta_json IS NOT NULL\",\n ),\n updateScannerCache: db.prepare(\n \"UPDATE conversation_meta SET mtime_ms = ?, file_size = ?, scanner_meta_json = ? WHERE id = ?\",\n ),\n getFileMetadata: db.prepare(\n \"SELECT mtime_ms, file_size, is_agent, agent_entrypoints_key FROM conversation_file_metadata WHERE file_path = ?\",\n ),\n upsertFileMetadata: db.prepare(`\n INSERT INTO conversation_file_metadata\n (file_path, mtime_ms, file_size, is_agent, agent_entrypoints_key, updated_at)\n VALUES\n (@file_path, @mtime_ms, @file_size, @is_agent, @agent_entrypoints_key, @updated_at)\n ON CONFLICT(file_path) DO UPDATE SET\n mtime_ms = excluded.mtime_ms,\n file_size = excluded.file_size,\n is_agent = excluded.is_agent,\n agent_entrypoints_key = excluded.agent_entrypoints_key,\n updated_at = excluded.updated_at\n `),\n upsertSessionName: db.prepare(`\n INSERT INTO session_names (session_id, name, updated_at)\n VALUES (?, ?, ?)\n ON CONFLICT(session_id) DO UPDATE SET\n name = excluded.name,\n updated_at = excluded.updated_at\n WHERE session_names.updated_at < excluded.updated_at\n `),\n getSessionName: db.prepare(\"SELECT name FROM session_names WHERE session_id = ?\"),\n listSessionNames: db.prepare(\"SELECT session_id, name FROM session_names\"),\n setConversationProjectId: db.prepare(\n \"UPDATE conversation_meta SET project_id = ? WHERE id = ?\",\n ),\n markAsStreamer: db.prepare(\"UPDATE conversation_meta SET source = 'streamer' WHERE id = ?\"),\n getLatestConversation: db.prepare(\n \"SELECT id, last_activity FROM conversation_meta WHERE last_activity IS NOT NULL ORDER BY last_activity DESC, id DESC LIMIT 1\",\n ),\n listConversationsForProjectBackfill: db.prepare(\n \"SELECT id, project_path, project_id, last_activity FROM conversation_meta WHERE project_path IS NOT NULL\",\n ),\n hasOrphanProjectId: db.prepare(\n \"SELECT 1 FROM conversation_meta WHERE project_id IS NULL AND project_path IS NOT NULL LIMIT 1\",\n ),\n popularProjects: db.prepare(\n `SELECT project_path, project_name, COUNT(*) as cnt\n FROM conversation_meta\n WHERE project_path IS NOT NULL\n GROUP BY project_path\n ORDER BY cnt DESC\n LIMIT ?`,\n ),\n getFileState: db.prepare(\"SELECT * FROM conversation_file_state WHERE path = ?\"),\n upsertFileState: db.prepare(\n `INSERT INTO conversation_file_state\n (path, identity, size, mtime_ms, byte_offset, last_message_index)\n VALUES (@path, @identity, @size, @mtime_ms, @byte_offset, @last_message_index)\n ON CONFLICT(path) DO UPDATE SET\n identity = excluded.identity,\n size = excluded.size,\n mtime_ms = excluded.mtime_ms,\n byte_offset = excluded.byte_offset,\n last_message_index = excluded.last_message_index`,\n ),\n deleteFileState: db.prepare(\"DELETE FROM conversation_file_state WHERE path = ?\"),\n deleteMessageIndex: db.prepare(\n \"DELETE FROM conversation_message_index WHERE conversation_id = ?\",\n ),\n insertMessageIndexRow: db.prepare(\n `INSERT INTO conversation_message_index\n (conversation_id, message_index, byte_offset, byte_length, uuid, role, ts)\n VALUES (@conversation_id, @message_index, @byte_offset, @byte_length, @uuid, @role, @ts)\n ON CONFLICT(conversation_id, message_index) DO UPDATE SET\n byte_offset = excluded.byte_offset,\n byte_length = excluded.byte_length,\n uuid = excluded.uuid,\n role = excluded.role,\n ts = excluded.ts`,\n ),\n getMessageIndexWindow: db.prepare(\n `SELECT message_index, byte_offset, byte_length, uuid, role, ts\n FROM conversation_message_index\n WHERE conversation_id = ? AND message_index >= ? AND message_index < ?\n ORDER BY message_index ASC`,\n ),\n getIndexedMessageCount: db.prepare(\n \"SELECT COUNT(*) as cnt FROM conversation_message_index WHERE conversation_id = ?\",\n ),\n };\n }\n\n /**\n * Expose the underlying handle so projects/cache_metadata repositories can\n * share the same connection. Internal API; not part of the public surface.\n */\n getDatabase(): Database.Database {\n return this.db;\n }\n\n // ── Offset index (design 1b) ────────────────────────────────────────────\n // conversation_file_state + conversation_message_index back the windowed\n // detail read path (SQL window select + pread of byte ranges). All methods\n // are thin wrappers over the prepared statements above.\n\n getFileState(path: string): FileStateRow | null {\n return (this.stmts.getFileState.get(path) as FileStateRow | undefined) ?? null;\n }\n\n upsertFileState(row: FileStateRow): void {\n this.stmts.upsertFileState.run(row);\n }\n\n /** Drop a file's index rows + file_state (truncation / identity change). */\n deleteFileIndex(path: string, conversationId: string): void {\n const tx = this.db.transaction(() => {\n this.stmts.deleteMessageIndex.run(conversationId);\n this.stmts.deleteFileState.run(path);\n });\n tx();\n this.indexParseState.delete(path);\n }\n\n /** Append/replace index rows in one transaction. */\n appendMessageIndexRows(rows: MessageIndexRow[]): void {\n const tx = this.db.transaction((batch: MessageIndexRow[]) => {\n for (const r of batch) this.stmts.insertMessageIndexRow.run(r);\n });\n tx(rows);\n }\n\n /** Rows for message_index in [fromIndex, toIndex), ordered ascending. */\n getMessageIndexWindow(\n conversationId: string,\n fromIndex: number,\n toIndex: number,\n ): MessageIndexRow[] {\n return this.stmts.getMessageIndexWindow.all(\n conversationId,\n fromIndex,\n toIndex,\n ) as MessageIndexRow[];\n }\n\n getIndexedMessageCount(conversationId: string): number {\n return (this.stmts.getIndexedMessageCount.get(conversationId) as { cnt: number }).cnt;\n }\n\n /**\n * Conversation id for a JSONL path — the filename stem (matches the pseudo-id\n * updateFromLine derives and the uuid the detail read path resolves). The\n * offset index keys on this so the window select and the cursor agree.\n */\n static conversationIdForFile(filePath: string): string {\n return (\n filePath\n .split(/[/\\\\]/)\n .pop()\n ?.replace(/\\.jsonl$/, \"\") ?? filePath\n );\n }\n\n // The offset index understands only claude-code JSONL: parseJsonlLine is the\n // claude-code reducer, so a codex file \"indexes\" as zero messages and the\n // resulting file_state serves empty windows for a real conversation (the\n // silent-wrong-data bug hotfixed after 1.28.0). Non-claude providers are\n // excluded from the index entirely; the scanner serves them (it routes each\n // provider to its own parser).\n private isIndexableFile(filePath: string): boolean {\n // Resolve by file_path, NOT by conversationIdForFile: codex rollout files\n // are named rollout-<ts>-<uuid>.jsonl, so the filename stem is not the\n // meta row's id and an id lookup silently misses. A file with no meta row\n // at all is NOT indexable — provider unknown means indexing is unsafe; a\n // later request backfills once the meta row exists.\n const row = this.stmts.getProviderByFilePath.get(filePath) as\n | { provider: string | null }\n | undefined;\n if (!row) return false;\n return (row.provider ?? CLAUDE_CODE_PROVIDER) === CLAUDE_CODE_PROVIDER;\n }\n\n /**\n * Incremental offset-index writer: extend the index for a burst of appended\n * lines (one watcher read) using their byte spans. Each line is classified\n * with the scanner's parseJsonlLine (a running per-file reducer state), so the\n * message ordering can never drift from the scanner's. Message lines get an\n * index row at the next message_index; non-message lines (summary/sidecar)\n * get no row but still advance byte_offset. file_state is updated to the end\n * of the last consumed span.\n *\n * Requires an up-to-date `stat` (identity/size/mtime) for the file so the read\n * path can detect truncation/replacement.\n *\n * `readFrom` is the absolute byte offset the watcher read started at, and\n * `endOffset` is where it ended (readFrom + consumed, i.e. the watcher's new\n * entry.offset). CONTIGUITY GUARD: the read must begin exactly where the index\n * left off (`readFrom === existing.byte_offset`, or 0 with no state). If it\n * doesn't — the watcher attached at EOF after the server was down, or an\n * append raced an in-flight backfill — extending would assign wrong\n * message_index values over a hole. In that case this writes nothing and\n * returns null so the caller drops the index and backfills.\n *\n * On success returns the message_index assigned to each input span (null for a\n * non-message line) so the caller can stamp WS `seq`. Empty array when spans\n * is empty. `endOffset` is stored verbatim as byte_offset so the watcher's\n * offset and file_state.byte_offset are the same number by construction.\n */\n extendMessageIndex(\n filePath: string,\n spans: LineSpan[],\n stat: Stats,\n readFrom: number,\n endOffset: number,\n ): (number | null)[] | null {\n // Non-indexable provider: write nothing, but return all-null seqs (not\n // null) — a null return means \"decline\" and would send the caller into an\n // endless drop-and-backfill loop for a file that can never be indexed.\n if (!this.isIndexableFile(filePath)) return spans.map(() => null);\n\n const existing = this.getFileState(filePath);\n const expectedStart = existing?.byte_offset ?? 0;\n // Non-contiguous read → the index would develop a hole with wrong indices.\n // Decline; the caller drops + backfills.\n if (readFrom !== expectedStart) return null;\n\n if (spans.length === 0) return [];\n const convId = ConversationCache.conversationIdForFile(filePath);\n\n let state = this.indexParseState.get(filePath);\n if (!state) {\n state = createJsonlParseState();\n this.indexParseState.set(filePath, state);\n }\n\n let nextIndex = existing ? existing.last_message_index + 1 : 0;\n const rows: MessageIndexRow[] = [];\n const seqs: (number | null)[] = [];\n\n for (const span of spans) {\n const msg = parseJsonlLine(span.text, state);\n if (!msg) {\n seqs.push(null); // summary/sidecar/malformed → no index row, no seq\n continue;\n }\n rows.push({\n conversation_id: convId,\n message_index: nextIndex,\n byte_offset: span.byteOffset,\n byte_length: span.byteLength,\n uuid: msg.uuid ?? null,\n role: msg.role ?? null,\n ts: msg.timestamp ? Date.parse(msg.timestamp) || null : null,\n });\n seqs.push(nextIndex);\n nextIndex++;\n }\n\n const tx = this.db.transaction(() => {\n for (const r of rows) this.stmts.insertMessageIndexRow.run(r);\n this.stmts.upsertFileState.run({\n path: filePath,\n identity: fileIdentity(stat),\n size: stat.size,\n mtime_ms: Math.round(stat.mtimeMs),\n // Store the watcher's end offset verbatim — same number as entry.offset,\n // so the next read's contiguity check compares like-for-like (never\n // false-positive on a read that ended in trailing empty lines).\n byte_offset: endOffset,\n last_message_index: nextIndex - 1,\n });\n });\n tx();\n return seqs;\n }\n\n clearIndexParseState(filePath: string): void {\n this.indexParseState.delete(filePath);\n }\n\n /**\n * On-demand full backfill of the offset index for a file with no/stale\n * file_state (cold conversation, or after a truncation/replacement). Rebuilds\n * from byte 0: drops any existing rows, walks the whole file in chunks with a\n * running parse state, yields to the event loop every ~1000 lines so a large\n * file never blocks, and writes index rows + file_state.\n *\n * Single-flighted per path: concurrent callers await the same walk. The\n * triggering detail request is served by the scanner fallback while this runs.\n */\n backfillIndex(filePath: string): Promise<void> {\n const inFlight = this.backfillInFlight.get(filePath);\n if (inFlight) return inFlight;\n const walk = this.runBackfill(filePath).finally(() => {\n this.backfillInFlight.delete(filePath);\n });\n this.backfillInFlight.set(filePath, walk);\n return walk;\n }\n\n private async runBackfill(filePath: string): Promise<void> {\n const convId = ConversationCache.conversationIdForFile(filePath);\n // Reset any partial/stale state before rebuilding from scratch. For a\n // non-indexable (non-claude) file the purge IS the whole job — it also\n // heals a poisoned row left by a pre-hotfix backfill — so return before\n // walking the file.\n this.deleteFileIndex(filePath, convId);\n this.indexParseState.delete(filePath);\n if (!this.isIndexableFile(filePath)) return;\n\n const CHUNK = 256 * 1024;\n const YIELD_EVERY = 1000;\n const state = createJsonlParseState();\n const fh = await openAsync(filePath, \"r\");\n let fileOffset = 0; // absolute byte offset of `carry`'s first byte\n let carry = Buffer.alloc(0); // bytes after the last \"\\n\" of the previous chunk\n let nextIndex = 0;\n let linesSinceYield = 0;\n let lastConsumedEnd = 0; // absolute byte offset just past the last full line\n let stat: Stats;\n\n try {\n stat = await fh.stat();\n const buf = Buffer.alloc(CHUNK);\n for (;;) {\n const { bytesRead } = await fh.read(buf, 0, CHUNK, null);\n if (bytesRead === 0) break;\n const combined =\n carry.length > 0\n ? Buffer.concat([carry, buf.subarray(0, bytesRead)])\n : buf.subarray(0, bytesRead);\n const { spans, consumed } = splitCompleteLines(combined, fileOffset);\n\n const rows: MessageIndexRow[] = [];\n for (const span of spans) {\n const msg = parseJsonlLine(span.text, state);\n linesSinceYield++;\n if (msg) {\n rows.push({\n conversation_id: convId,\n message_index: nextIndex,\n byte_offset: span.byteOffset,\n byte_length: span.byteLength,\n uuid: msg.uuid ?? null,\n role: msg.role ?? null,\n ts: msg.timestamp ? Date.parse(msg.timestamp) || null : null,\n });\n nextIndex++;\n }\n if (linesSinceYield >= YIELD_EVERY) {\n linesSinceYield = 0;\n await yieldToEventLoop();\n }\n }\n if (rows.length > 0) this.appendMessageIndexRows(rows);\n\n lastConsumedEnd = fileOffset + consumed;\n // Keep the unconsumed remainder (a torn line at the chunk boundary).\n // Copy it — `combined` may be a view into the reused read buffer, which\n // the next fh.read overwrites.\n carry = Buffer.from(combined.subarray(consumed));\n fileOffset += consumed;\n }\n } finally {\n await fh.close();\n }\n\n this.upsertFileState({\n path: filePath,\n identity: fileIdentity(stat),\n size: stat.size,\n mtime_ms: Math.round(stat.mtimeMs),\n byte_offset: lastConsumedEnd,\n last_message_index: nextIndex - 1,\n });\n // Seed the incremental writer's state so subsequent appends continue the\n // same reducer instead of re-parsing from scratch.\n this.indexParseState.set(filePath, state);\n }\n\n /**\n * Windowed detail read straight from the offset index — the hot path.\n * Returns the parsed messages for message_index in [fromIndex, toIndex) plus\n * the total indexed count, or null when the index can't serve this file (no\n * file_state, identity/size mismatch = truncation/replacement, or cold index)\n * so the caller falls back to the scanner and enqueues a backfill.\n *\n * On a match it SQL-selects the window's byte ranges and preads exactly those\n * ranges from the JSONL (never the whole file), parsing only the sliced lines.\n * Returns messages in the same ConversationMessage shape parseJsonlLine\n * produces during a scan, so the payload is identical to the scanner path.\n */\n readMessageWindow(\n filePath: string,\n fromIndex: number,\n toIndex: number,\n ): { messages: ConversationMessage[]; total: number; fromIndex: number } | null {\n const fileState = this.getFileState(filePath);\n if (!fileState) return null;\n\n let stat: Stats;\n try {\n stat = statSync(filePath);\n } catch {\n return null;\n }\n // The index is authoritative only up to byte_offset. Decline unless it\n // covers the whole file exactly:\n // - identity changed → file replaced;\n // - size < byte_offset → truncated;\n // - size > byte_offset → the file grew past the index (untailed messages\n // at the tail, e.g. an append with no live watcher extending the index).\n // Serving a slice in that last case silently drops the appended messages —\n // the exact live-append bug this feature exists to fix. In every mismatch\n // the caller drops the index + backfills and falls back to the scanner.\n if (fileIdentity(stat) !== fileState.identity || stat.size !== fileState.byte_offset) {\n return null;\n }\n // A file_state that covered the file but indexed no messages is a poisoned\n // row (pre-hotfix backfill of a non-claude file) — serving it would render\n // a real conversation empty. Belt-and-braces: also decline non-indexable\n // providers outright in case a poisoned row carries a nonzero count. The\n // decline routes the caller to backfill, which purges the row.\n if (fileState.last_message_index < 0 || !this.isIndexableFile(filePath)) {\n return null;\n }\n\n const total = fileState.last_message_index + 1;\n const from = Math.max(0, fromIndex);\n const to = Math.min(toIndex, total);\n if (to <= from) return { messages: [], total, fromIndex: from };\n\n const rows = this.getMessageIndexWindow(\n ConversationCache.conversationIdForFile(filePath),\n from,\n to,\n );\n if (rows.length === 0) return { messages: [], total, fromIndex: from };\n\n const messages: ConversationMessage[] = [];\n const fd = openSync(filePath, \"r\");\n try {\n // A fresh parse state per window (not seeded by replaying earlier lines —\n // that would re-read from byte 0 and defeat the index). parseJsonlLine's\n // per-line ConversationMessage output does not depend on the cross-line\n // reducer state in the current scanner, so a windowed read is byte-\n // identical to a full contiguous parse — pinned by\n // offset-index-read.test.ts (\"windowed read of a tool_use → tool_result\n // pair matches a full contiguous parse\"). If a future scanner makes the\n // per-line shape state-dependent, that test fails rather than silently\n // serving a slightly-less-enriched payload.\n const state = createJsonlParseState();\n for (const row of rows) {\n const buf = Buffer.alloc(row.byte_length);\n readSync(fd, buf, 0, row.byte_length, row.byte_offset);\n const msg = parseJsonlLine(buf.toString(\"utf-8\"), state);\n if (msg) messages.push(msg);\n }\n } finally {\n closeSync(fd);\n }\n\n return { messages, total, fromIndex: from };\n }\n\n private agentEntrypointsKey(): string {\n return [...this.agentEntrypoints].sort().join(\",\");\n }\n\n private classifyAgentFile(filePath: string, mtimeMs: number, fileSize: number): boolean {\n if (this.agentEntrypoints.size === 0) return false;\n\n const entrypointsKey = this.agentEntrypointsKey();\n const cached = this.stmts.getFileMetadata.get(filePath) as\n | {\n mtime_ms: number;\n file_size: number;\n is_agent: number;\n agent_entrypoints_key: string;\n }\n | undefined;\n if (\n cached &&\n cached.mtime_ms === mtimeMs &&\n cached.file_size === fileSize &&\n cached.agent_entrypoints_key === entrypointsKey\n ) {\n return cached.is_agent === 1;\n }\n\n const isAgent = isAgentFile(filePath, this.agentEntrypoints);\n this.stmts.upsertFileMetadata.run({\n file_path: filePath,\n mtime_ms: mtimeMs,\n file_size: fileSize,\n is_agent: isAgent ? 1 : 0,\n agent_entrypoints_key: entrypointsKey,\n updated_at: Date.now(),\n });\n return isAgent;\n }\n\n isAgentFileCached(filePath: string): boolean {\n let s: ReturnType<typeof statSync>;\n try {\n s = statSync(filePath);\n } catch {\n return false;\n }\n return this.classifyAgentFile(filePath, s.mtimeMs, s.size);\n }\n\n static open(\n dbPath: string,\n tailSize = 10,\n migrationsDir?: string,\n options?: ConversationCacheOptions,\n ): ConversationCache {\n mkdirSync(dirname(dbPath), { recursive: true });\n const db = new Database(dbPath);\n db.pragma(\"journal_mode = WAL\");\n db.pragma(\"foreign_keys = ON\");\n return new ConversationCache(db, tailSize, migrationsDir, options);\n }\n\n close(): void {\n this.db.close();\n }\n\n getPopularProjects(limit: number): Array<{ path: string; name: string; sessionCount: number }> {\n const rows = this.stmts.popularProjects.all(limit) as Array<{\n project_path: string;\n project_name: string | null;\n cnt: number;\n }>;\n return rows.map((r) => ({\n path: r.project_path,\n name: r.project_name ?? r.project_path.split(/[/\\\\]/).pop() ?? r.project_path,\n sessionCount: r.cnt,\n }));\n }\n\n private ensureFileIndex(): void {\n if (this.fileIndexLoaded) return;\n const rows = this.stmts.allFilePaths.all() as Array<{ id: string; file_path: string }>;\n for (const row of rows) {\n this.fileIndex.set(row.file_path, row.id);\n }\n this.fileIndexLoaded = true;\n }\n\n updateFromLine(filePath: string, rawLine: string): void {\n let line: JsonlLine;\n try {\n line = JSON.parse(rawLine);\n } catch {\n return;\n }\n\n if (this.filterAgentConversations && isAgentLine(line, this.agentEntrypoints)) {\n this.deleteByFilePath(filePath);\n this.onAgentFileDetected?.(filePath);\n return;\n }\n\n const role = line.role ?? line.type;\n const isMessage = role === \"user\" || role === \"assistant\";\n\n this.ensureFileIndex();\n\n // Skip lines that carry neither a message nor project context — there's\n // nothing for us to record.\n if (!isMessage && !line.cwd && !line.slug) return;\n\n let convId = this.fileIndex.get(filePath);\n if (!convId) {\n const pseudoId =\n filePath\n .split(/[/\\\\]/)\n .pop()\n ?.replace(/\\.jsonl$/, \"\") ?? filePath;\n this.stmts.insertSkeleton.run(pseudoId, filePath, 0);\n this.fileIndex.set(filePath, pseudoId);\n convId = pseudoId;\n }\n\n // Backfill project_path / project_name / title from cwd on any line that\n // carries it. COALESCE inside the SQL ensures we never overwrite a\n // scanner-populated value. Without this, the chokidar watcher leaves\n // skeleton rows with NULL project context, which renders as blank cards\n // on the mobile Recents tab.\n if (line.cwd || line.slug) {\n const projectPath = line.cwd ?? null;\n const projectName = projectPath ? shortProjectName(projectPath) : null;\n const title = line.slug ?? projectName ?? null;\n this.stmts.backfillSkeletonProject.run({\n id: convId,\n project_path: projectPath,\n project_name: projectName,\n title,\n });\n }\n\n if (!isMessage) return;\n\n const timestamp = line.timestamp ?? new Date().toISOString();\n const activityMs = new Date(timestamp).getTime();\n if (Number.isNaN(activityMs)) return;\n\n const contentBlocks = normalizeContent(line.message?.content ?? line.content);\n const text = contentBlocks.find((b) => b.type === \"text\")?.text?.slice(0, 200) ?? \"\";\n const lastMessage = JSON.stringify({ role, timestamp, text });\n const seq = ++this.tailSeq;\n\n const result = this.stmts.updateMeta.run(activityMs, lastMessage, seq, convId);\n if (result.changes === 0) return;\n\n const tailRow = this.stmts.getTail.get(convId) as TailRow | undefined;\n const msgs: CachedTailMessage[] = tailRow\n ? (JSON.parse(tailRow.messages_json) as CachedTailMessage[])\n : [];\n\n msgs.push({ role, timestamp, text, content: contentBlocks });\n if (msgs.length > this.tailSize) msgs.splice(0, msgs.length - this.tailSize);\n\n this.stmts.upsertTail.run(convId, JSON.stringify(msgs), msgs.length, seq);\n }\n\n /**\n * Batched form of updateFromLine: applies a burst of newly-appended lines\n * (one chokidar read) in a single transaction with one message_count bump,\n * one meta write, and one tail read/write — instead of 2-4 synchronous\n * writes per line. Semantics are identical to replaying each line through\n * updateFromLine in order: the agent filter short-circuits the whole batch,\n * project context is backfilled last-wins, message_count increases by the\n * number of surviving message lines, and last_activity/last_message reflect\n * the final message line.\n */\n updateFromLines(filePath: string, rawLines: string[]): void {\n // Classify all lines first (outside the transaction). A single watched\n // file maps to one conversation, so we accumulate into scalars.\n let sawProjectContext = false;\n let backfillProjectPath: string | null = null;\n let backfillProjectName: string | null = null;\n let backfillTitle: string | null = null;\n let msgCount = 0;\n let lastActivityMs: number | null = null;\n let lastMessage: string | null = null;\n const newTail: CachedTailMessage[] = [];\n\n for (const rawLine of rawLines) {\n let line: JsonlLine;\n try {\n line = JSON.parse(rawLine);\n } catch {\n continue;\n }\n\n // The first agent line nukes the file and aborts the whole batch —\n // matches updateFromLine's per-line return.\n if (this.filterAgentConversations && isAgentLine(line, this.agentEntrypoints)) {\n this.deleteByFilePath(filePath);\n this.onAgentFileDetected?.(filePath);\n return;\n }\n\n const role = line.role ?? line.type;\n const isMessage = role === \"user\" || role === \"assistant\";\n\n // Skip lines that carry neither a message nor project context.\n if (!isMessage && !line.cwd && !line.slug) continue;\n\n if (line.cwd || line.slug) {\n sawProjectContext = true;\n // First-wins per column, mirroring updateFromLine's per-line replay:\n // backfillSkeletonProject COALESCEs each column independently, so the\n // first non-null value seen for a column sticks and later lines can't\n // override it. Accumulating last-wins here would diverge from per-line\n // replay when a conversation's cwd/slug changes mid-batch.\n const lineProjectPath = line.cwd ?? null;\n const lineProjectName = lineProjectPath ? shortProjectName(lineProjectPath) : null;\n const lineTitle = line.slug ?? lineProjectName ?? null;\n backfillProjectPath ??= lineProjectPath;\n backfillProjectName ??= lineProjectName;\n backfillTitle ??= lineTitle;\n }\n\n if (!isMessage) continue;\n\n const timestamp = line.timestamp ?? new Date().toISOString();\n const activityMs = new Date(timestamp).getTime();\n if (Number.isNaN(activityMs)) continue;\n\n const contentBlocks = normalizeContent(line.message?.content ?? line.content);\n const text = contentBlocks.find((b) => b.type === \"text\")?.text?.slice(0, 200) ?? \"\";\n msgCount += 1;\n lastActivityMs = activityMs;\n lastMessage = JSON.stringify({ role, timestamp, text });\n newTail.push({ role, timestamp, text, content: contentBlocks });\n }\n\n // Nothing recordable in this batch.\n if (!sawProjectContext && msgCount === 0) return;\n\n this.ensureFileIndex();\n\n let convId = this.fileIndex.get(filePath);\n if (!convId) {\n const pseudoId =\n filePath\n .split(/[/\\\\]/)\n .pop()\n ?.replace(/\\.jsonl$/, \"\") ?? filePath;\n this.stmts.insertSkeleton.run(pseudoId, filePath, 0);\n this.fileIndex.set(filePath, pseudoId);\n convId = pseudoId;\n }\n const id = convId;\n\n const apply = this.db.transaction(() => {\n if (sawProjectContext) {\n this.stmts.backfillSkeletonProject.run({\n id,\n project_path: backfillProjectPath,\n project_name: backfillProjectName,\n title: backfillTitle,\n });\n }\n\n if (msgCount === 0) return;\n\n const seq = ++this.tailSeq;\n const result = this.stmts.updateMetaBatch.run({\n inc: msgCount,\n last_activity: lastActivityMs,\n last_message: lastMessage,\n updated_at: seq,\n id,\n });\n if (result.changes === 0) return;\n\n const tailRow = this.stmts.getTail.get(id) as TailRow | undefined;\n const msgs: CachedTailMessage[] = tailRow\n ? (JSON.parse(tailRow.messages_json) as CachedTailMessage[])\n : [];\n msgs.push(...newTail);\n if (msgs.length > this.tailSize) msgs.splice(0, msgs.length - this.tailSize);\n\n this.stmts.upsertTail.run(id, JSON.stringify(msgs), msgs.length, seq);\n });\n apply();\n }\n\n // Returns the IDs of rows actually upserted (i.e. excluding any agent JSONLs\n // skipped by the filter). The server's warm-up loop uses this to populate\n // tails only for IDs that have a parent conversation_meta row — otherwise\n // the conversation_tail FK fires and aborts the warm-up (regression covered\n // in conversation-cache.test.ts).\n upsertFromScannerMeta(metas: ScannerMeta[]): string[] {\n const filter = this.filterAgentConversations;\n const upsertedIds: string[] = [];\n const run = this.db.transaction((items: ScannerMeta[]) => {\n for (const m of items) {\n const id =\n m.sessionId ||\n m.id\n .split(\"/\")\n .pop()\n ?.replace(/\\.jsonl$/, \"\") ||\n m.id;\n const lastActivityMs = m.timestamp ? new Date(m.timestamp).getTime() : null;\n let mtimeMs: number | null = null;\n let fileSize: number | null = null;\n try {\n const s = statSync(m.filePath);\n mtimeMs = s.mtimeMs;\n fileSize = s.size;\n } catch {\n // file disappeared between scan and upsert — store without stat\n }\n const seq = ++this.tailSeq;\n if (\n filter &&\n mtimeMs !== null &&\n fileSize !== null &&\n this.classifyAgentFile(m.filePath, mtimeMs, fileSize)\n ) {\n continue;\n }\n const scannerMetaJson = JSON.stringify(m);\n this.stmts.upsertFull.run({\n id,\n file_path: m.filePath,\n project_path: m.projectPath ?? null,\n project_name: m.projectName ?? null,\n title: m.title ?? m.projectName ?? null,\n model: m.model ?? null,\n account: m.account ?? null,\n branch: m.gitBranch ?? null,\n message_count: m.messageCount ?? 0,\n last_activity: lastActivityMs,\n first_message: m.firstMessage ? JSON.stringify(m.firstMessage) : null,\n last_message: m.lastMessage ? JSON.stringify(m.lastMessage) : null,\n preview: m.preview ?? null,\n updated_at: seq,\n mtime_ms: mtimeMs,\n file_size: fileSize,\n provider: m.provider ?? CLAUDE_CODE_PROVIDER,\n scanner_meta_json: scannerMetaJson,\n });\n this.stmts.updateScannerCache.run(mtimeMs, fileSize, scannerMetaJson, id);\n if (this.fileIndexLoaded) this.fileIndex.set(m.filePath, id);\n upsertedIds.push(id);\n }\n });\n run(metas);\n return upsertedIds;\n }\n\n // Returns true if a row was deleted. Used by the prune-on-startup step and\n // by updateFromLine when a previously-cached file turns out to be an agent\n // JSONL.\n deleteByFilePath(filePath: string): boolean {\n const row = this.stmts.getIdByFilePath.get(filePath) as { id: string } | undefined;\n if (!row) return false;\n this.stmts.deleteTailById.run(row.id);\n const result = this.stmts.deleteById.run(row.id);\n this.fileIndex.delete(filePath);\n return result.changes > 0;\n }\n\n // Reads the last `tailSize` qualifying lines from a JSONL file and writes them\n // to conversation_tail. Reads backward in 8 KB chunks so memory usage is\n // bounded regardless of file size. Uses updated_at=0 so any live\n // updateFromLine() call (which uses Date.now()) always wins the upsert.\n // Returns false if the file cannot be read or the tail already exists.\n populateTailFromFile(convId: string, filePath: string): boolean {\n if (this.stmts.hasTail.get(convId)) return false;\n\n let fileSize: number;\n let fd: number;\n try {\n fileSize = statSync(filePath).size;\n fd = openSync(filePath, \"r\");\n } catch {\n return false;\n }\n\n const CHUNK = 8192;\n const buf = Buffer.allocUnsafe(CHUNK);\n let pos = fileSize;\n let partial = \"\";\n const lines: string[] = [];\n\n try {\n while (pos > 0 && lines.length < this.tailSize * 4) {\n const toRead = Math.min(CHUNK, pos);\n pos -= toRead;\n readSync(fd, buf, 0, toRead, pos);\n const chunk = buf.subarray(0, toRead).toString(\"utf8\");\n const combined = chunk + partial;\n const parts = combined.split(\"\\n\");\n // parts[0] may be a partial line — keep it for the next iteration\n partial = parts[0];\n for (let i = parts.length - 1; i >= 1; i--) {\n lines.push(parts[i]);\n }\n }\n if (partial) lines.push(partial);\n } finally {\n closeSync(fd);\n }\n\n const msgs: CachedTailMessage[] = [];\n for (let i = 0; i < lines.length && msgs.length < this.tailSize; i++) {\n const line = lines[i].trim();\n if (!line) continue;\n let parsed: JsonlLine;\n try {\n parsed = JSON.parse(line);\n } catch {\n continue;\n }\n const role = parsed.role ?? parsed.type;\n if (!role) continue;\n const timestamp = parsed.timestamp ?? \"\";\n const contentBlocks = normalizeContent(parsed.message?.content ?? parsed.content);\n const text = contentBlocks.find((b) => b.type === \"text\")?.text?.slice(0, 200) ?? \"\";\n msgs.unshift({ role, timestamp, text, content: contentBlocks });\n }\n if (msgs.length === 0) return false;\n this.stmts.upsertTail.run(convId, JSON.stringify(msgs), msgs.length, 0);\n return true;\n }\n\n listConversations(opts: { project?: string; provider?: string; limit: number; offset: number }): {\n conversations: ConversationListItem[];\n total: number;\n } {\n const { project, provider, limit, offset } = opts;\n let total: number;\n let rows: MetaRow[];\n\n if (project) {\n total = (this.stmts.countByProject.get(project) as { n: number }).n;\n rows = limit === 0 ? [] : (this.stmts.listByProject.all(project, limit, offset) as MetaRow[]);\n } else if (provider) {\n total = (this.stmts.countByProvider.get(provider) as { n: number }).n;\n rows =\n limit === 0 ? [] : (this.stmts.listByProvider.all(provider, limit, offset) as MetaRow[]);\n } else {\n total = (this.stmts.count.get() as { n: number }).n;\n rows = limit === 0 ? [] : (this.stmts.list.all(limit, offset) as MetaRow[]);\n }\n\n return {\n total,\n conversations: rows.map((r) => ({\n id: r.id,\n filePath: r.file_path,\n projectId: r.project_id,\n projectPath: r.project_path,\n projectName: r.project_name,\n title: r.title,\n model: r.model,\n account: r.account,\n branch: r.branch,\n messageCount: r.message_count,\n lastActivity: r.last_activity\n ? new Date(r.last_activity).toISOString()\n : new Date(0).toISOString(),\n firstMessage: r.first_message,\n lastMessage: r.last_message,\n preview: r.preview,\n source: r.source,\n provider: r.provider ?? CLAUDE_CODE_PROVIDER,\n })),\n };\n }\n\n /** Returns a map of filePath → { mtimeMs, size } for all rows that have\n * stat data stored. Used by the server to build the statCache passed to\n * ConversationScanner.scan() so unchanged files are skipped. */\n getFileStats(): Map<string, { mtimeMs: number; size: number }> {\n const rows = this.stmts.allFileStats.all() as Array<{\n file_path: string;\n mtime_ms: number;\n file_size: number;\n }>;\n const map = new Map<string, { mtimeMs: number; size: number }>();\n for (const r of rows) {\n map.set(r.file_path, { mtimeMs: r.mtime_ms, size: r.file_size });\n }\n return map;\n }\n\n getScannerStatCache(): Map<string, { stat: FileStatEntry; meta: ConversationMeta }> {\n const rows = this.stmts.allScannerStatCacheRows.all() as Array<{\n file_path: string;\n mtime_ms: number;\n file_size: number;\n scanner_meta_json: string;\n }>;\n const map = new Map<string, { stat: FileStatEntry; meta: ConversationMeta }>();\n for (const r of rows) {\n try {\n const meta = JSON.parse(r.scanner_meta_json) as ConversationMeta;\n map.set(r.file_path, {\n stat: { mtimeMs: r.mtime_ms, size: r.file_size },\n meta,\n });\n } catch {\n // Ignore malformed legacy/cache rows; the scanner will parse the file.\n }\n }\n return map;\n }\n\n getMetaById(id: string): ConversationListItem | null {\n const row = this.stmts.getFullById.get(id) as MetaRow | undefined;\n if (!row) return null;\n return {\n id: row.id,\n filePath: row.file_path,\n projectId: row.project_id,\n projectPath: row.project_path,\n projectName: row.project_name,\n title: row.title,\n model: row.model,\n account: row.account,\n branch: row.branch,\n messageCount: row.message_count,\n lastActivity: row.last_activity\n ? new Date(row.last_activity).toISOString()\n : new Date(0).toISOString(),\n firstMessage: row.first_message,\n lastMessage: row.last_message,\n preview: row.preview,\n source: row.source,\n provider: row.provider ?? CLAUDE_CODE_PROVIDER,\n };\n }\n\n setConversationProjectId(conversationId: string, projectId: string): void {\n this.stmts.setConversationProjectId.run(projectId, conversationId);\n }\n\n markAsStreamer(id: string): void {\n this.stmts.markAsStreamer.run(id);\n }\n\n getLatestConversation(): { id: string; lastActivity: string | null } | null {\n const row = this.stmts.getLatestConversation.get() as\n | { id: string; last_activity: number | null }\n | undefined;\n if (!row) return null;\n return {\n id: row.id,\n lastActivity: row.last_activity ? new Date(row.last_activity).toISOString() : null,\n };\n }\n\n hasOrphanProjectId(): boolean {\n return this.stmts.hasOrphanProjectId.get() !== undefined;\n }\n\n listConversationsForProjectBackfill(): Array<{\n id: string;\n projectPath: string | null;\n projectId: string | null;\n lastActivity: string | null;\n }> {\n const rows = this.stmts.listConversationsForProjectBackfill.all() as Array<{\n id: string;\n project_path: string | null;\n project_id: string | null;\n last_activity: number | null;\n }>;\n return rows.map((r) => ({\n id: r.id,\n projectPath: r.project_path,\n projectId: r.project_id,\n lastActivity: r.last_activity ? new Date(r.last_activity).toISOString() : null,\n }));\n }\n\n getConversationTail(id: string): CachedTail | null {\n const row = this.stmts.getTail.get(id) as TailRow | undefined;\n if (!row) return null;\n return {\n conversationId: id,\n messages: JSON.parse(row.messages_json) as CachedTailMessage[],\n tailSize: row.tail_size,\n };\n }\n\n hasConversation(id: string): boolean {\n return !!this.stmts.getById.get(id);\n }\n\n upsertSessionName(sessionId: string, name: string): void {\n this.stmts.upsertSessionName.run(sessionId, name, ++this.nameSeq);\n }\n\n getSessionName(sessionId: string): string | null {\n const row = this.stmts.getSessionName.get(sessionId) as { name: string } | undefined;\n return row?.name ?? null;\n }\n\n listSessionNames(): Record<string, string> {\n const rows = this.stmts.listSessionNames.all() as { session_id: string; name: string }[];\n return Object.fromEntries(rows.map((r) => [r.session_id, r.name]));\n }\n\n invalidate(id?: string): void {\n if (id) {\n this.stmts.deleteTailById.run(id);\n this.stmts.deleteById.run(id);\n if (this.fileIndexLoaded) {\n for (const [fp, cid] of this.fileIndex) {\n if (cid === id) {\n this.fileIndex.delete(fp);\n break;\n }\n }\n }\n } else {\n this.stmts.deleteTailAll.run();\n this.stmts.deleteAll.run();\n this.fileIndex.clear();\n }\n }\n\n /**\n * Drop the cached row for a file. Two callers with opposite intent:\n * - a directory-watch \"change\" event (the file was appended to) — pass\n * `skipIfTailed: true`. A cached tail means the row is being actively\n * maintained from the file's real content by the live-tail\n * (updateFromLines) or warm-up path, fresher than any scanner-derived\n * view. Both watchers fire on the same append with no ordering guarantee;\n * without this guard the invalidate can land after the tail write and wipe\n * the just-cached row, flickering the conversation out of\n * /api/conversations on nearly every message (CRITICAL #2). The debounced\n * rescan still re-derives metadata, so skipping the eager drop loses\n * nothing.\n * - a genuine unlink (the file is gone) — leave `skipIfTailed` false so the\n * row is always removed, otherwise a deleted session ghosts in the cache.\n */\n invalidateByFilePath(filePath: string, opts?: { skipIfTailed?: boolean }): string | null {\n const row = this.stmts.getIdByFilePath.get(filePath) as { id: string } | undefined;\n if (!row) return null;\n if (opts?.skipIfTailed && this.stmts.hasTail.get(row.id)) return null;\n this.invalidate(row.id);\n return row.id;\n }\n\n /**\n * Drop rows whose `file_path` no longer exists on disk AND which have no\n * cached tail to fall back to. Rows with a tail are left alone so\n * `handleGetConversation` can still serve the cached tail even when the\n * JSONL has been deleted.\n */\n pruneGhostFiles(exists: (filePath: string) => boolean = existsSync): string[] {\n const rows = this.stmts.allFilePaths.all() as { id: string; file_path: string }[];\n const ghosts: string[] = [];\n const prune = this.db.transaction((ids: string[]) => {\n for (const id of ids) {\n this.stmts.deleteTailById.run(id);\n this.stmts.deleteById.run(id);\n }\n });\n for (const row of rows) {\n if (exists(row.file_path)) continue;\n if (this.stmts.hasTail.get(row.id)) continue;\n ghosts.push(row.id);\n }\n if (ghosts.length > 0) {\n prune(ghosts);\n if (this.fileIndexLoaded) {\n for (const id of ghosts) {\n for (const [fp, cid] of this.fileIndex) {\n if (cid === id) {\n this.fileIndex.delete(fp);\n break;\n }\n }\n }\n }\n }\n return ghosts;\n }\n\n /**\n * Reconcile the cache against the authoritative set of conversation file\n * paths a fresh scan surfaced: drop any cached row whose `file_path` is not\n * in `livePaths` (removed from disk, or now filtered out — e.g. became an\n * agent JSONL). This is the \"removed conversations\" half of a ?refresh=1\n * reconcile; the additions/updates half is upsertFromScannerMeta.\n *\n * Skip semantics depend on whether the file still exists on disk:\n * - File GONE from disk → always removed, tail or not. This matches the old\n * invalidate()+rebuild behavior (a deleted conversation must disappear on\n * refresh) and keeps refresh=1 truthful about removals. NOTE: this is an\n * INTENTIONAL divergence from pruneGhostFiles(), which KEEPS tailed ghosts\n * so their cached history stays viewable on a background prune. refresh=1\n * has the opposite contract (mobile relies on removals being reflected), so\n * do not \"unify\" the two — they serve different purposes.\n * - File STILL on disk but absent from `livePaths` → the CRITICAL #2 race:\n * the scan snapshot predates a just-created (and now live-tailed) file.\n * A tailed row here is actively maintained from real content, so it is\n * kept — dropping it would flicker the active conversation out of\n * /api/conversations. An untailed on-disk row not in the snapshot is a\n * transient scan/discovery gap; it is left alone (not removed) and the\n * next reconcile picks it up, rather than risk removing a real file the\n * scan simply hasn't surfaced yet.\n * Returns the removed IDs.\n */\n reconcileDeletions(\n livePaths: Set<string>,\n opts?: { exists?: (filePath: string) => boolean },\n ): string[] {\n const exists = opts?.exists ?? existsSync;\n const rows = this.stmts.allFilePaths.all() as { id: string; file_path: string }[];\n const removed: string[] = [];\n const drop = this.db.transaction((ids: string[]) => {\n for (const id of ids) {\n this.stmts.deleteTailById.run(id);\n this.stmts.deleteById.run(id);\n }\n });\n for (const row of rows) {\n if (livePaths.has(row.file_path)) continue;\n // Not in the scan snapshot. If the file is gone from disk, it's a genuine\n // deletion — remove it. If it still exists, this is a scan/discovery gap\n // (the CRITICAL #2 race for live files); leave it for the next reconcile.\n if (exists(row.file_path)) continue;\n removed.push(row.id);\n }\n if (removed.length > 0) {\n drop(removed);\n if (this.fileIndexLoaded) {\n for (const id of removed) {\n for (const [fp, cid] of this.fileIndex) {\n if (cid === id) {\n this.fileIndex.delete(fp);\n break;\n }\n }\n }\n }\n }\n return removed;\n }\n}\n","import type Database from \"better-sqlite3\";\nimport { readdirSync, readFileSync } from \"fs\";\nimport { dirname, join } from \"path\";\nimport { fileURLToPath } from \"url\";\n\nfunction getMigrationsDir(): string {\n if (typeof import.meta !== \"undefined\" && import.meta.url) {\n return dirname(fileURLToPath(import.meta.url));\n }\n return __dirname;\n}\n\nconst SCHEMA_MIGRATIONS_SQL = `\nCREATE TABLE IF NOT EXISTS schema_migrations (\n id TEXT PRIMARY KEY,\n applied_at TEXT NOT NULL\n);\n`;\n\nexport interface SqliteMigrationRunResult {\n applied: string[];\n skipped: string[];\n}\n\nexport function runSqliteMigrations(\n db: Database.Database,\n migrationsDir?: string,\n): SqliteMigrationRunResult {\n db.exec(SCHEMA_MIGRATIONS_SQL);\n\n const dir = migrationsDir ?? join(getMigrationsDir(), \"migrations\");\n const files = readdirSync(dir)\n .filter((f) => f.endsWith(\".sql\"))\n .sort();\n\n const appliedRows = db.prepare(\"SELECT id FROM schema_migrations\").all() as Array<{ id: string }>;\n const appliedSet = new Set(appliedRows.map((r) => r.id));\n\n const recordApplied = db.prepare(\"INSERT INTO schema_migrations (id, applied_at) VALUES (?, ?)\");\n\n const applied: string[] = [];\n const skipped: string[] = [];\n\n for (const file of files) {\n if (appliedSet.has(file)) {\n skipped.push(file);\n continue;\n }\n const sql = readFileSync(join(dir, file), \"utf-8\");\n const tx = db.transaction(() => {\n db.exec(sql);\n recordApplied.run(file, new Date().toISOString());\n });\n tx();\n applied.push(file);\n }\n\n return { applied, skipped };\n}\n","import { closeSync, openSync, readSync, statSync } from \"fs\";\n\n// Default agent entrypoints. Override via THREADBASE_AGENT_ENTRYPOINTS\n// (comma-separated). Interactive Claude Code emits entrypoint=\"cli\" and is\n// never in this set.\n// - sdk-cli → Claude Agent SDK, claude-mem, hook-spawned automation\n// - claude-vscode → VS Code extension when invoked headlessly (memory\n// summarizers, etc.). Real interactive VS Code sessions\n// also use this value, so toggling via env var lets users\n// keep them visible if they want.\nexport const DEFAULT_AGENT_ENTRYPOINTS: ReadonlySet<string> = new Set([\"sdk-cli\", \"claude-vscode\"]);\n\n// Chunked scan: read 64 KB at a time with early-exit. We look for the first\n// `\"entrypoint\":` occurrence in the file:\n// - matches an agent marker → return true\n// - matches some other entrypoint value (e.g. \"cli\") → return false\n// - never appears → return false\n// The entrypoint is fixed per-conversation, so the first occurrence is\n// authoritative. This keeps both agent and human files fast (typical first\n// hit is within the first chunk), while still tolerating long housekeeping\n// prefixes (observed agent markers as deep as 2.3 MB in 4.5 MB observer files).\nconst CHUNK_BYTES = 64 * 1024;\nconst ENTRYPOINT_PROBE = `\"entrypoint\":`;\n// Overlap consecutive chunks so a marker that straddles the boundary is still\n// found. The longest entrypoint we look for is ~24 chars; 64 bytes overlap is\n// plenty.\nconst CHUNK_OVERLAP = 64;\n\n// Per-file decision cache, keyed by `${filePath}::${sortedEntrypointsKey}` so\n// changing the set invalidates entries. Filled lazily; cleared on restart.\nconst fileDecisionCache = new Map<string, boolean>();\n\nfunction markersFor(entrypoints: ReadonlySet<string>): string[] {\n return [...entrypoints].map((e) => `\"entrypoint\":\"${e}\"`);\n}\n\nfunction cacheKey(filePath: string, entrypoints: ReadonlySet<string>): string {\n return `${filePath}::${[...entrypoints].sort().join(\",\")}`;\n}\n\nexport function isAgentLine(\n line: { entrypoint?: string },\n entrypoints: ReadonlySet<string> = DEFAULT_AGENT_ENTRYPOINTS,\n): boolean {\n return line.entrypoint !== undefined && entrypoints.has(line.entrypoint);\n}\n\nexport function isAgentFile(\n filePath: string,\n entrypoints: ReadonlySet<string> = DEFAULT_AGENT_ENTRYPOINTS,\n): boolean {\n if (entrypoints.size === 0) return false;\n const key = cacheKey(filePath, entrypoints);\n const cached = fileDecisionCache.get(key);\n if (cached !== undefined) return cached;\n\n let fd: number;\n try {\n fd = openSync(filePath, \"r\");\n } catch {\n return false;\n }\n\n try {\n const fileSize = statSync(filePath).size;\n if (fileSize === 0) {\n fileDecisionCache.set(key, false);\n return false;\n }\n\n const markers = markersFor(entrypoints);\n const buf = Buffer.allocUnsafe(CHUNK_BYTES);\n let offset = 0;\n let carry = \"\";\n\n while (offset < fileSize) {\n const toRead = Math.min(CHUNK_BYTES, fileSize - offset);\n const got = readSync(fd, buf, 0, toRead, offset);\n if (got <= 0) break;\n const chunk = carry + buf.toString(\"utf8\", 0, got);\n\n // Agent match wins.\n for (const marker of markers) {\n if (chunk.includes(marker)) {\n fileDecisionCache.set(key, true);\n return true;\n }\n }\n\n // If we see ANY entrypoint field, it's per-conversation and stable —\n // since none of the agent markers matched, this is a non-agent file.\n // Stops 11 MB human JSONLs from being read in full.\n if (chunk.includes(ENTRYPOINT_PROBE)) {\n fileDecisionCache.set(key, false);\n return false;\n }\n\n carry = chunk.slice(-CHUNK_OVERLAP);\n offset += got;\n }\n\n fileDecisionCache.set(key, false);\n return false;\n } catch {\n return false;\n } finally {\n closeSync(fd);\n }\n}\n\n// Comma-separated parser. Empty string → empty set (agents shown in practice\n// even when includeAgents=false, since no entrypoint qualifies).\nexport function parseAgentEntrypointsEnv(raw: string | undefined): ReadonlySet<string> {\n if (raw === undefined) return DEFAULT_AGENT_ENTRYPOINTS;\n const parts = raw\n .split(\",\")\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n return new Set(parts);\n}\n\nexport function clearAgentFileCacheForTests(): void {\n fileDecisionCache.clear();\n}\n","import { createHash } from \"crypto\";\nimport type { Stats } from \"fs\";\n\n/**\n * Stable identity for a JSONL file, used to detect replacement (a new file at\n * the same path) vs. mere growth. Prefer the inode (dev:ino) — cheap and exact.\n * When the inode is unavailable (ino === 0, e.g. some network/virtual FS), fall\n * back to a fingerprint of the first bytes supplied by the caller.\n */\nexport function fileIdentity(stat: Pick<Stats, \"dev\" | \"ino\">, headBytes?: Buffer): string {\n if (stat.ino && stat.ino > 0) return `inode:${stat.dev}:${stat.ino}`;\n const head = headBytes ?? Buffer.alloc(0);\n return `fp:${createHash(\"sha1\").update(head).digest(\"hex\")}`;\n}\n\nexport interface LineSpan {\n /** Absolute byte offset of the line's first byte in the file. */\n byteOffset: number;\n /** Byte length of the line content, excluding the trailing \"\\n\". */\n byteLength: number;\n /** The decoded line text (no trailing \"\\n\"). */\n text: string;\n}\n\nexport interface SplitResult {\n spans: LineSpan[];\n /**\n * Bytes consumed = up to and including the last \"\\n\" in `buf`. A trailing\n * partial line (no newline yet) is NOT consumed, so the caller's byte offset\n * never advances past an incomplete line. Those bytes arrive again, whole, on\n * the next read.\n */\n consumed: number;\n}\n\n/**\n * Split a buffer of appended bytes into complete-line spans with absolute byte\n * offsets. `baseOffset` is the file position of `buf`'s first byte.\n *\n * Only lines terminated by \"\\n\" produce a span; the remainder after the final\n * newline is left for the next read (torn-write safety, design §7.5 #4). Empty\n * lines (a bare \"\\n\") produce no span but are still consumed.\n */\nexport function splitCompleteLines(buf: Buffer, baseOffset: number): SplitResult {\n const spans: LineSpan[] = [];\n let lineStart = 0; // index within buf where the current line begins\n let consumed = 0;\n\n for (let i = 0; i < buf.length; i++) {\n if (buf[i] !== 0x0a) continue; // \"\\n\"\n const lineLen = i - lineStart; // excludes the newline\n if (lineLen > 0) {\n spans.push({\n byteOffset: baseOffset + lineStart,\n byteLength: lineLen,\n text: buf.toString(\"utf-8\", lineStart, i),\n });\n }\n lineStart = i + 1;\n consumed = lineStart; // up to and including this \"\\n\"\n }\n\n return { spans, consumed };\n}\n","import type Database from \"better-sqlite3\";\n\nexport type CacheMetadataKey =\n | \"last_conversation_id\"\n | \"last_conversation_created_at\"\n | \"projects_last_indexed_at\"\n | \"conversations_last_indexed_at\"\n | \"conversations_dirty\";\n\nexport class CacheMetadataRepository {\n private get: Database.Statement;\n private upsert: Database.Statement;\n private del: Database.Statement;\n\n constructor(db: Database.Database) {\n this.get = db.prepare(\"SELECT value FROM cache_metadata WHERE key = ?\");\n this.upsert = db.prepare(`\n INSERT INTO cache_metadata (key, value, updated_at)\n VALUES (?, ?, ?)\n ON CONFLICT(key) DO UPDATE SET\n value = excluded.value,\n updated_at = excluded.updated_at\n `);\n this.del = db.prepare(\"DELETE FROM cache_metadata WHERE key = ?\");\n }\n\n getCacheMetadata(key: CacheMetadataKey): string | null {\n const row = this.get.get(key) as { value: string } | undefined;\n return row?.value ?? null;\n }\n\n setCacheMetadata(key: CacheMetadataKey, value: string): void {\n this.upsert.run(key, value, new Date().toISOString());\n }\n\n deleteCacheMetadata(key: CacheMetadataKey): void {\n this.del.run(key);\n }\n}\n","import type { ConversationCache } from \"../../conversation-cache\";\n\n/**\n * Thin repository wrapper around ConversationCache for the project-id flow.\n * The cache is the source of truth for conversation rows; this just exposes\n * a stable, repo-style API for services that don't want to know about the\n * cache class directly.\n */\nexport class ConversationsRepository {\n constructor(private cache: ConversationCache) {}\n\n updateConversationProjectId(args: { conversationId: string; projectId: string }): void {\n this.cache.setConversationProjectId(args.conversationId, args.projectId);\n }\n\n listConversationsForProjectBackfill() {\n return this.cache.listConversationsForProjectBackfill();\n }\n\n getLatestConversation() {\n return this.cache.getLatestConversation();\n }\n\n hasOrphanRows(): boolean {\n return this.cache.hasOrphanProjectId();\n }\n}\n","import type Database from \"better-sqlite3\";\nimport { randomUUID } from \"crypto\";\nimport type { Project } from \"../../schemas/project.schema\";\nimport { canonicalizeProjectPath } from \"../../utils/canonicalizeProjectPath\";\n\ninterface ProjectRow {\n id: string;\n path: string;\n name: string | null;\n last_conversation_id: string | null;\n last_conversation_created_at: string | null;\n last_indexed_at: string | null;\n latest_message_at: string | null;\n latest_message_id: string | null;\n message_count: number;\n created_at: string;\n updated_at: string;\n}\n\nfunction rowToProject(row: ProjectRow): Project {\n return {\n id: row.id,\n path: row.path,\n name: row.name,\n lastConversationId: row.last_conversation_id,\n lastConversationCreatedAt: row.last_conversation_created_at,\n lastIndexedAt: row.last_indexed_at,\n latestMessageAt: row.latest_message_at,\n latestMessageId: row.latest_message_id,\n messageCount: row.message_count,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n };\n}\n\nexport interface UpsertProjectInput {\n lastConversationId?: string | null;\n lastConversationCreatedAt?: string | null;\n latestMessageAt?: string | null;\n latestMessageId?: string | null;\n name?: string | null;\n}\n\nexport class ProjectsRepository {\n private getByPath: Database.Statement;\n private getById: Database.Statement;\n private listAll: Database.Statement;\n private insert: Database.Statement;\n private update: Database.Statement;\n\n constructor(db: Database.Database) {\n this.getByPath = db.prepare(\"SELECT * FROM projects WHERE path = ?\");\n this.getById = db.prepare(\"SELECT * FROM projects WHERE id = ?\");\n this.listAll = db.prepare(\"SELECT * FROM projects ORDER BY updated_at DESC\");\n this.insert = db.prepare(`\n INSERT INTO projects (\n id, path, name,\n last_conversation_id, last_conversation_created_at, last_indexed_at,\n latest_message_at, latest_message_id, message_count,\n created_at, updated_at\n ) VALUES (\n @id, @path, @name,\n @last_conversation_id, @last_conversation_created_at, @last_indexed_at,\n @latest_message_at, @latest_message_id, @message_count,\n @created_at, @updated_at\n )\n `);\n this.update = db.prepare(`\n UPDATE projects SET\n name = COALESCE(@name, name),\n last_conversation_id = COALESCE(@last_conversation_id, last_conversation_id),\n last_conversation_created_at = COALESCE(@last_conversation_created_at, last_conversation_created_at),\n last_indexed_at = COALESCE(@last_indexed_at, last_indexed_at),\n latest_message_at = COALESCE(@latest_message_at, latest_message_at),\n latest_message_id = COALESCE(@latest_message_id, latest_message_id),\n updated_at = @updated_at\n WHERE id = @id\n `);\n }\n\n getProjectByPath(rawPath: string): Project | null {\n const path = canonicalizeProjectPath(rawPath);\n const row = this.getByPath.get(path) as ProjectRow | undefined;\n return row ? rowToProject(row) : null;\n }\n\n getProjectById(id: string): Project | null {\n const row = this.getById.get(id) as ProjectRow | undefined;\n return row ? rowToProject(row) : null;\n }\n\n listProjects(): Project[] {\n return (this.listAll.all() as ProjectRow[]).map(rowToProject);\n }\n\n /**\n * Insert a project at the given canonical path, or update its metadata\n * if one already exists. Returns the persisted Project row.\n *\n * Idempotent: passing the same path twice returns the same project id.\n */\n upsertProjectByPath(rawPath: string, input: UpsertProjectInput = {}): Project {\n const path = canonicalizeProjectPath(rawPath);\n const now = new Date().toISOString();\n\n const existing = this.getByPath.get(path) as ProjectRow | undefined;\n if (existing) {\n this.update.run({\n id: existing.id,\n name: input.name ?? null,\n last_conversation_id: input.lastConversationId ?? null,\n last_conversation_created_at: input.lastConversationCreatedAt ?? null,\n last_indexed_at: now,\n latest_message_at: input.latestMessageAt ?? null,\n latest_message_id: input.latestMessageId ?? null,\n updated_at: now,\n });\n return rowToProject(this.getById.get(existing.id) as ProjectRow);\n }\n\n const id = randomUUID();\n this.insert.run({\n id,\n path,\n name: input.name ?? deriveNameFromPath(path),\n last_conversation_id: input.lastConversationId ?? null,\n last_conversation_created_at: input.lastConversationCreatedAt ?? null,\n last_indexed_at: now,\n latest_message_at: input.latestMessageAt ?? null,\n latest_message_id: input.latestMessageId ?? null,\n message_count: 0,\n created_at: now,\n updated_at: now,\n });\n return rowToProject(this.getById.get(id) as ProjectRow);\n }\n}\n\nfunction deriveNameFromPath(path: string): string | null {\n const parts = path.split(/[\\\\/]/).filter(Boolean);\n return parts.length > 0 ? parts[parts.length - 1] : null;\n}\n","/**\n * Canonicalize a project path so the same project always dedupes to the\n * same key, regardless of trailing slashes or surrounding whitespace.\n *\n * Rules:\n * - Trim surrounding whitespace\n * - Remove trailing forward or back slashes (one or more)\n *\n * Do NOT lowercase: project paths can be case-sensitive on Linux/macOS\n * and lowercasing them would silently merge two distinct real projects.\n */\nexport function canonicalizeProjectPath(projectPath: string): string {\n return projectPath.trim().replace(/[\\\\/]+$/, \"\");\n}\n","import type { SessionStore } from \"../../session-store\";\n\n/**\n * Sessions live in-memory in SessionStore (Postgres-backed persistence\n * was dropped per the SQLite-only direction). This repository wraps the\n * store with a stable, project-id-aware API for the new services.\n */\nexport class SessionsRepository {\n constructor(private store: SessionStore) {}\n\n updateSessionProjectId(args: { sessionId: string; projectId: string }): void {\n this.store.updateManaged(args.sessionId, { projectId: args.projectId });\n }\n\n listManagedSessions() {\n return this.store.listManaged();\n }\n}\n","import type pg from \"pg\";\n\nexport interface UploadRecord {\n id: string;\n sessionId: string;\n filePath: string;\n originalName: string;\n mimeType: string;\n sizeBytes: number;\n}\n\nexport async function recordUpload(\n pool: pg.Pool | null,\n instanceId: string | null,\n row: UploadRecord,\n): Promise<void> {\n if (!pool) return;\n await pool.query(\n `INSERT INTO session_uploads\n (id, session_id, instance_id, file_path, original_name, mime_type, size_bytes)\n VALUES ($1, $2, $3, $4, $5, $6, $7)`,\n [\n row.id,\n row.sessionId,\n instanceId,\n row.filePath,\n row.originalName,\n row.mimeType,\n row.sizeBytes,\n ],\n );\n}\n","import { readdirSync, statSync } from \"fs\";\nimport type { ServerResponse } from \"http\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\n\nfunction decodeProjectPath(dirName: string): string {\n // Claude encodes project paths by replacing '/' with '-', starting from root.\n // e.g. '-Users-foo-Desktop-dev-myproject' → '/Users/foo/Desktop/dev/myproject'\n return dirName.replace(/-/g, \"/\");\n}\n\nexport function handleListProjects(url: URL, res: ServerResponse): void {\n const limit = Math.max(1, parseInt(url.searchParams.get(\"limit\") ?? \"50\", 10) || 50);\n const offset = Math.max(0, parseInt(url.searchParams.get(\"offset\") ?? \"0\", 10) || 0);\n\n const projectsDir = join(homedir(), \".claude\", \"projects\");\n\n let entries: Array<{ name: string; path: string; dirName: string; mtime: number }>;\n try {\n entries = readdirSync(projectsDir)\n .map((dirName) => {\n const fullPath = join(projectsDir, dirName);\n let mtime = 0;\n try {\n mtime = statSync(fullPath).mtimeMs;\n } catch {\n // ignore stat errors — directory may have been removed\n }\n const path = decodeProjectPath(dirName);\n const name = path.split(\"/\").filter(Boolean).pop() ?? dirName;\n return { name, path, dirName, mtime };\n })\n .sort((a, b) => b.mtime - a.mtime);\n } catch {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ projects: [], total: 0 }));\n return;\n }\n\n const total = entries.length;\n const page = entries\n .slice(offset, offset + limit)\n .map(({ name, path, dirName }) => ({ name, path, dirName }));\n\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ projects: page, total }));\n}\n","import { randomBytes } from \"crypto\";\n\nexport interface PairTokenRecord {\n token: string;\n expiresAt: number;\n used: boolean;\n}\n\nexport interface MintResult {\n token: string;\n expiresAt: number;\n expiresInSeconds: number;\n}\n\nexport interface ConsumeOk {\n ok: true;\n}\n\nexport interface ConsumeErr {\n ok: false;\n reason: \"unknown\" | \"expired\" | \"used\";\n}\n\nexport type ConsumeResult = ConsumeOk | ConsumeErr;\n\nconst DEFAULT_TTL_SECONDS = 180;\nconst SWEEP_INTERVAL_MS = 60_000;\n\nexport class PairTokenStore {\n private current: PairTokenRecord | null = null;\n private readonly ttlMs: number;\n private sweepTimer: ReturnType<typeof setInterval> | null = null;\n\n constructor(opts: { ttlSeconds?: number; autoSweep?: boolean } = {}) {\n this.ttlMs = (opts.ttlSeconds ?? DEFAULT_TTL_SECONDS) * 1000;\n if (opts.autoSweep !== false) {\n this.sweepTimer = setInterval(() => this.sweep(), SWEEP_INTERVAL_MS);\n this.sweepTimer.unref?.();\n }\n }\n\n mint(): MintResult {\n const token = `pt_${randomBytes(16).toString(\"hex\")}`;\n const expiresAt = Date.now() + this.ttlMs;\n this.current = { token, expiresAt, used: false };\n return {\n token,\n expiresAt,\n expiresInSeconds: Math.floor(this.ttlMs / 1000),\n };\n }\n\n consume(token: string): ConsumeResult {\n const record = this.current;\n if (!record || record.token !== token) return { ok: false, reason: \"unknown\" };\n if (Date.now() > record.expiresAt) {\n this.current = null;\n return { ok: false, reason: \"expired\" };\n }\n if (record.used) return { ok: false, reason: \"used\" };\n record.used = true;\n return { ok: true };\n }\n\n peek(): PairTokenRecord | null {\n return this.current;\n }\n\n clear(): void {\n this.current = null;\n }\n\n sweep(): void {\n if (this.current && Date.now() > this.current.expiresAt) {\n this.current = null;\n }\n }\n\n dispose(): void {\n if (this.sweepTimer) clearInterval(this.sweepTimer);\n this.sweepTimer = null;\n this.current = null;\n }\n}\n","import nacl from \"tweetnacl\";\nimport naclUtil from \"tweetnacl-util\";\n\nexport interface SealedPayload {\n ciphertext: string;\n nonce: string;\n ephemeralPublicKey: string;\n}\n\n/**\n * Encrypts a plaintext to a recipient's X25519 public key using NaCl box with\n * an ephemeral sender keypair. The phone holds the recipient private key in\n * its own memory and decrypts with nacl.box.open(ciphertext, nonce, ephemeralPublicKey, recipientPrivateKey).\n *\n * The wire format is custom (not libsodium crypto_box_seal) but cryptographically equivalent.\n */\nexport function seal(plaintext: string, recipientPublicKeyBase64: string): SealedPayload {\n const recipientPk = naclUtil.decodeBase64(recipientPublicKeyBase64);\n if (recipientPk.length !== nacl.box.publicKeyLength) {\n throw new Error(\n `clientPublicKey must be ${nacl.box.publicKeyLength} bytes (got ${recipientPk.length})`,\n );\n }\n const ephemeral = nacl.box.keyPair();\n const nonce = nacl.randomBytes(nacl.box.nonceLength);\n const message = naclUtil.decodeUTF8(plaintext);\n const cipher = nacl.box(message, nonce, recipientPk, ephemeral.secretKey);\n return {\n ciphertext: naclUtil.encodeBase64(cipher),\n nonce: naclUtil.encodeBase64(nonce),\n ephemeralPublicKey: naclUtil.encodeBase64(ephemeral.publicKey),\n };\n}\n","import chokidar, { type FSWatcher } from \"chokidar\";\nimport { statSync } from \"fs\";\nimport { open, stat } from \"fs/promises\";\nimport { type LineSpan, splitCompleteLines } from \"../../utils/fileIdentity\";\n\nexport interface ConversationWatcherEvents {\n /** Fires once per new newline-terminated line appended to a watched file. */\n onNewLine?: (filePath: string, line: string) => void;\n /**\n * Fires once per chokidar read with ALL new lines from that read, batched.\n * When set, it REPLACES the per-line onNewLine dispatch for that file —\n * callers pick one. Lets a burst of appended lines collapse into a single\n * downstream cache write + WebSocket broadcast.\n */\n onNewLines?: (filePath: string, lines: string[]) => void;\n /**\n * Like onNewLines but also carries each line's absolute byte span in the\n * file (for the offset index). Fires ALONGSIDE onNewLines/onNewLine (it does\n * not replace them) so the cache tail write and the index extend can both\n * consume the same read. `readFrom` is the absolute byte offset the read\n * started at; `spans` are complete lines only (a torn trailing line is held\n * for the next read).\n */\n onNewLineSpans?: (\n filePath: string,\n spans: LineSpan[],\n readFrom: number,\n endOffset: number,\n ) => void;\n /** Fires when chokidar reports an add/change/unlink at the directory level. */\n onConversationChanged?: (filePath: string) => void | Promise<void>;\n /** Fires when a tailed file is deleted (per-file watcher unlink event). */\n onFileDeleted?: (filePath: string) => void;\n /** Reported errors per file. */\n onError?: (filePath: string, error: Error) => void;\n}\n\ninterface WatchedFile {\n watcher: FSWatcher;\n offset: number;\n // Async-read re-entrancy guard: `reading` is set while a readNewLines is in\n // flight; `pending` records that a change arrived during that read so the\n // in-flight loop re-runs once more after it finishes.\n reading: boolean;\n pending: boolean;\n}\n\n/**\n * Chokidar-backed replacement for src/file-watcher.ts.\n *\n * - watch(filePath) → tail a single JSONL file, emitting onNewLine\n * for each appended line.\n * - watchDirectory(dir) → mark cache dirty on add/change/unlink events\n * for any file inside the directory.\n *\n * Per the refactor plan, file watching is an OPTIMIZATION; correctness\n * still relies on refresh=1 / the latest HDD conversation id check.\n */\nexport class ConversationWatcher {\n private files = new Map<string, WatchedFile>();\n private directories = new Map<string, FSWatcher>();\n private onNewLine: ConversationWatcherEvents[\"onNewLine\"];\n private onNewLines: ConversationWatcherEvents[\"onNewLines\"];\n private onNewLineSpans: ConversationWatcherEvents[\"onNewLineSpans\"];\n private onConversationChanged: ConversationWatcherEvents[\"onConversationChanged\"];\n private onFileDeleted: ConversationWatcherEvents[\"onFileDeleted\"];\n private onError: ConversationWatcherEvents[\"onError\"];\n\n constructor(events: ConversationWatcherEvents = {}) {\n this.onNewLine = events.onNewLine;\n this.onNewLines = events.onNewLines;\n this.onNewLineSpans = events.onNewLineSpans;\n this.onConversationChanged = events.onConversationChanged;\n this.onFileDeleted = events.onFileDeleted;\n this.onError = events.onError;\n }\n\n watch(filePath: string): void {\n if (this.files.has(filePath)) return;\n\n let offset: number;\n try {\n offset = statSync(filePath).size;\n } catch {\n offset = 0;\n }\n\n const watcher = chokidar.watch(filePath, {\n ignoreInitial: true,\n awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 25 },\n });\n\n watcher.on(\"change\", () => {\n void this.readNewLines(filePath);\n });\n watcher.on(\"add\", () => {\n void this.readNewLines(filePath);\n });\n watcher.on(\"unlink\", () => this.onFileDeleted?.(filePath));\n watcher.on(\"error\", (err) => {\n const error = err instanceof Error ? err : new Error(String(err));\n this.onError?.(filePath, error);\n });\n\n this.files.set(filePath, { watcher, offset, reading: false, pending: false });\n }\n\n unwatch(filePath: string): void {\n const entry = this.files.get(filePath);\n if (!entry) return;\n void entry.watcher.close();\n this.files.delete(filePath);\n }\n\n /**\n * Re-drive the tail read for a file that's already being tailed. A per-file\n * chokidar handle can die silently (fs.watch stops firing after inode churn)\n * while the coarser directory watcher keeps reporting changes — calling this\n * from the directory-event path makes the tail self-healing. Reads are\n * offset-based and coalesced, so a redundant poke after a normal change\n * event is a cheap stat + no-op. Returns false for untailed paths.\n */\n poke(filePath: string): boolean {\n if (!this.files.has(filePath)) return false;\n void this.readNewLines(filePath);\n return true;\n }\n\n /**\n * Watch a directory of conversation JSONL files. Fires\n * onConversationChanged for any add/change/unlink event so the caller\n * can mark the cache dirty without scanning everything immediately.\n */\n watchDirectory(directory: string): void {\n if (this.directories.has(directory)) return;\n const watcher = chokidar.watch(directory, {\n ignoreInitial: true,\n awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 },\n });\n const fire = (filePath: string) => {\n void Promise.resolve(this.onConversationChanged?.(filePath)).catch(() => {\n // best-effort\n });\n };\n watcher.on(\"add\", fire);\n watcher.on(\"change\", fire);\n watcher.on(\"unlink\", fire);\n watcher.on(\"error\", (err) => {\n const error = err instanceof Error ? err : new Error(String(err));\n this.onError?.(directory, error);\n });\n this.directories.set(directory, watcher);\n }\n\n unwatchDirectory(directory: string): void {\n const watcher = this.directories.get(directory);\n if (!watcher) return;\n void watcher.close();\n this.directories.delete(directory);\n }\n\n dispose(): void {\n for (const [path] of this.files) this.unwatch(path);\n for (const [dir] of this.directories) this.unwatchDirectory(dir);\n }\n\n private async readNewLines(filePath: string): Promise<void> {\n const entry = this.files.get(filePath);\n if (!entry) return;\n\n // Coalesce: if a read is already running, flag that another change arrived;\n // the in-flight loop will pick up the freshly-appended bytes before exiting.\n if (entry.reading) {\n entry.pending = true;\n return;\n }\n entry.reading = true;\n\n try {\n // Loop so bytes appended during a read (or while dispatching) are caught\n // without re-entering — preserves offset correctness under async I/O.\n for (;;) {\n const st = await stat(filePath);\n // size <= offset also covers truncation/rotation, exactly as before.\n if (st.size <= entry.offset) break;\n\n const readFrom = entry.offset;\n const bytesToRead = st.size - readFrom;\n const buf = Buffer.alloc(bytesToRead);\n const fh = await open(filePath, \"r\");\n try {\n await fh.read(buf, 0, bytesToRead, readFrom);\n } finally {\n await fh.close();\n }\n // Split into complete lines only, with absolute byte spans. Advance by\n // `consumed` (up to the last \"\\n\"), NOT `bytesToRead`: a trailing\n // partial line (no \"\\n\" yet) is left unconsumed so `offset` never moves\n // past an unparsed line — it arrives whole on the next read. The file\n // may have grown again mid-read; the next loop iteration's stat catches\n // the rest.\n const { spans, consumed } = splitCompleteLines(buf, readFrom);\n entry.offset = readFrom + consumed;\n\n // The watcher may have been closed while we awaited; don't emit then.\n if (!this.files.has(filePath)) return;\n\n const lines = spans.map((s) => s.text);\n // The spans callback (offset index) fires alongside the text callbacks\n // — they consume the same read, so a burst extends the index and writes\n // the tail in one pass.\n // Pass both the start (readFrom) and the post-read end offset\n // (entry.offset = readFrom + consumed) so the index can enforce\n // contiguity and store the same offset the watcher tracks.\n if (spans.length > 0) {\n this.onNewLineSpans?.(filePath, spans, readFrom, entry.offset);\n }\n if (this.onNewLines) {\n this.onNewLines(filePath, lines);\n } else {\n for (const line of lines) this.onNewLine?.(filePath, line);\n }\n\n if (entry.pending) {\n entry.pending = false;\n continue;\n }\n break;\n }\n } catch (err) {\n this.onError?.(filePath, err instanceof Error ? err : new Error(String(err)));\n } finally {\n entry.reading = false;\n // If a change arrived while this read was in flight but the loop exited\n // before consuming it (e.g. an error broke out of the loop), the pending\n // bytes would otherwise sit unread until the next write. Re-arm once so\n // they're picked up — guarded by files.has so a disposed watcher stays put.\n if (entry.pending && this.files.has(filePath)) {\n entry.pending = false;\n void this.readNewLines(filePath);\n }\n }\n }\n}\n","export interface SearchTargetMatch {\n messageIndex: number;\n uuid: string | null;\n snippet: string;\n /**\n * All matching message indexes, ascending. When any plain-text match exists\n * only text matches are listed (they are the navigable/highlightable ones);\n * otherwise the thinking/tool fallback matches are. Capped at the LAST\n * MAX_MATCH_INDEXES entries — navigation starts at the tail-most match.\n */\n matchIndexes: number[];\n /** Uncapped true match count (the \"of M\" in the client counter). */\n totalMatches: number;\n}\n\n// Structural view of the scanner's parsed message — mirrors the fields\n// handleGetConversation already reads off it, so no scanner import is needed.\nexport interface SearchableMessage {\n text?: string;\n uuid?: string | null;\n isThinking?: boolean;\n thinkingContent?: string;\n metadata?: {\n toolUseBlocks?: Array<{ input?: unknown }>;\n toolResults?: Array<{ content?: unknown }>;\n };\n}\n\nconst SNIPPET_CONTEXT = 60;\nconst MAX_MATCH_INDEXES = 1000;\n\nfunction buildSnippet(source: string, matchStart: number, matchLength: number): string {\n const start = Math.max(0, matchStart - SNIPPET_CONTEXT);\n const end = Math.min(source.length, matchStart + matchLength + SNIPPET_CONTEXT);\n const prefix = start > 0 ? \"…\" : \"\";\n const suffix = end < source.length ? \"…\" : \"\";\n return `${prefix}${source.slice(start, end).replace(/\\s+/g, \" \").trim()}${suffix}`;\n}\n\n// Thinking and tool payloads are searchable so a hit inside a tool card can\n// still anchor the scroll, even though mobile only highlights plain text.\nfunction extendedBody(m: SearchableMessage): string {\n const parts: string[] = [];\n if (m.isThinking && m.thinkingContent) parts.push(m.thinkingContent);\n for (const b of m.metadata?.toolUseBlocks ?? []) {\n if (b.input !== undefined) parts.push(JSON.stringify(b.input));\n }\n for (const r of m.metadata?.toolResults ?? []) {\n if (r.content !== undefined) parts.push(JSON.stringify(r.content));\n }\n return parts.join(\"\\n\");\n}\n\nfunction collectMatches(\n messages: SearchableMessage[],\n needle: string,\n body: (m: SearchableMessage) => string,\n): number[] {\n const indexes: number[] = [];\n for (let i = 0; i < messages.length; i++) {\n if (body(messages[i]).toLowerCase().includes(needle)) indexes.push(i);\n }\n return indexes;\n}\n\n// Resolves a search hit to its matching messages: the anchor is the LAST\n// message chronologically whose body contains the query (case-insensitive\n// literal substring — no tokenization, and no regex built from user input).\n// Text matches are preferred over thinking/tool matches because only text is\n// visually highlightable on mobile.\nexport function findSearchTarget(\n messages: SearchableMessage[],\n query: string,\n): SearchTargetMatch | null {\n const needle = query.toLowerCase();\n\n let body = (m: SearchableMessage) => m.text ?? \"\";\n let matches = collectMatches(messages, needle, body);\n if (matches.length === 0) {\n body = extendedBody;\n matches = collectMatches(messages, needle, body);\n }\n if (matches.length === 0) return null;\n\n const anchorIndex = matches[matches.length - 1];\n const anchorBody = body(messages[anchorIndex]);\n const at = anchorBody.toLowerCase().indexOf(needle);\n return {\n messageIndex: anchorIndex,\n uuid: messages[anchorIndex].uuid ?? null,\n snippet: buildSnippet(anchorBody, at, query.length),\n matchIndexes: matches.slice(-MAX_MATCH_INDEXES),\n totalMatches: matches.length,\n };\n}\n","import { existsSync } from \"fs\";\nimport type { ConversationCache } from \"../../conversation-cache\";\n\nexport interface PruneAgentConversationsResult {\n scanned: number;\n pruned: number;\n missing: number;\n}\n\n// Walks conversation_meta once at startup and deletes any row whose JSONL has\n// the sdk-cli marker. Idempotent. Safe to call on every boot — the second run\n// will simply find nothing to delete.\nexport function pruneAgentConversations(cache: ConversationCache): PruneAgentConversationsResult {\n const db = cache.getDatabase();\n const rows = db.prepare(\"SELECT id, file_path FROM conversation_meta\").all() as Array<{\n id: string;\n file_path: string;\n }>;\n\n let pruned = 0;\n let missing = 0;\n for (const row of rows) {\n if (!existsSync(row.file_path)) {\n missing += 1;\n continue;\n }\n if (cache.isAgentFileCached(row.file_path)) {\n cache.deleteByFilePath(row.file_path);\n pruned += 1;\n }\n }\n\n return { scanned: rows.length, pruned, missing };\n}\n","/**\n * Resolve a user-visible title for a ProjectChat row.\n *\n * Mobile (ConversationListItem.tsx) renders `title?.trim() || pathSuffix ||\n * ''`, so when title and projectPath are both blank the row appears empty.\n * This helper produces a non-empty title for every input by falling back\n * through title → slug-ish projectName → path suffix → short id.\n */\nexport function deriveProjectChatTitle(input: {\n title: string | null | undefined;\n projectName: string | null | undefined;\n projectPath: string | null | undefined;\n id: string;\n}): string {\n const trimmed = input.title?.trim();\n if (trimmed) return trimmed;\n const name = input.projectName?.trim();\n if (name) return name;\n const pathSuffix = input.projectPath\n ? input.projectPath.split(/[/\\\\]/).filter(Boolean).slice(-2).join(\"/\")\n : \"\";\n if (pathSuffix) return pathSuffix;\n return `Untitled · ${input.id.slice(0, 8)}`;\n}\n","import type { AskOption, AskQuestion } from \"../../types\";\n\ntype ContentBlock = {\n type: string;\n name?: string;\n id?: string;\n input?: unknown;\n [key: string]: unknown;\n};\n\ninterface JsonlLineShape {\n content?: ContentBlock[] | string;\n message?: { content?: ContentBlock[] | string };\n}\n\nfunction normalizeContent(raw: ContentBlock[] | string | null | undefined): ContentBlock[] {\n if (Array.isArray(raw)) return raw;\n if (typeof raw === \"string\") return [{ type: \"text\", text: raw }];\n return [];\n}\n\nfunction coerceOptions(raw: unknown): AskOption[] | null {\n if (!Array.isArray(raw)) return null;\n const out: AskOption[] = [];\n for (const o of raw) {\n if (o && typeof o === \"object\" && typeof (o as AskOption).label === \"string\") {\n const opt = o as { label: string; description?: unknown; preview?: unknown };\n out.push({\n label: opt.label,\n description: typeof opt.description === \"string\" ? opt.description : \"\",\n ...(typeof opt.preview === \"string\" ? { preview: opt.preview } : {}),\n });\n }\n }\n return out.length > 0 ? out : null;\n}\n\nfunction coerceQuestions(raw: unknown): AskQuestion[] | null {\n if (!Array.isArray(raw)) return null;\n const out: AskQuestion[] = [];\n for (const q of raw) {\n if (!q || typeof q !== \"object\") continue;\n const qq = q as {\n question?: unknown;\n header?: unknown;\n multiSelect?: unknown;\n options?: unknown;\n };\n const options = coerceOptions(qq.options);\n if (typeof qq.question !== \"string\" || !options) continue;\n out.push({\n question: qq.question,\n header: typeof qq.header === \"string\" ? qq.header : \"\",\n multiSelect: qq.multiSelect === true,\n options,\n });\n }\n return out.length > 0 ? out : null;\n}\n\nexport function detectAskUserQuestion(\n rawLine: string,\n): { toolUseId: string; questions: AskQuestion[] } | null {\n let parsed: JsonlLineShape;\n try {\n parsed = JSON.parse(rawLine) as JsonlLineShape;\n } catch {\n return null;\n }\n const blocks = normalizeContent(parsed.message?.content ?? parsed.content);\n for (const b of blocks) {\n if (b.type === \"tool_use\" && b.name === \"AskUserQuestion\" && typeof b.id === \"string\") {\n const input = b.input as { questions?: unknown } | undefined;\n const questions = coerceQuestions(input?.questions);\n if (questions) return { toolUseId: b.id, questions };\n }\n }\n return null;\n}\n","import type { AskQuestion, WSMessage } from \"../../types\";\nimport { detectAskUserQuestion } from \"./detectAskUserQuestion\";\n\nexport interface PendingQuestion {\n toolUseId: string;\n questions: AskQuestion[];\n}\n\n// Pure decision: given the new lines for a session, returns the question messages\n// to broadcast and the pending-question records to store. No I/O.\nexport function questionsFromLines(\n sessionId: string,\n lines: string[],\n): {\n messages: Extract<WSMessage, { type: \"question\" }>[];\n pending: PendingQuestion[];\n} {\n const messages: Extract<WSMessage, { type: \"question\" }>[] = [];\n const pending: PendingQuestion[] = [];\n for (const line of lines) {\n const detected = detectAskUserQuestion(line);\n if (detected) {\n messages.push({\n type: \"question\",\n sessionId,\n toolUseId: detected.toolUseId,\n questions: detected.questions,\n });\n pending.push(detected);\n }\n }\n return { messages, pending };\n}\n\n// Pure decision: should the JSONL-flush path (re-)broadcast this question?\n//\n// The live-screen detector may have already broadcast the same question under a\n// synthetic `screen:…` toolUseId. When that JSONL line lands with the REAL\n// toolUseId we must re-broadcast — even though the content is identical — so the\n// client swaps the stale screen id for the real one. Otherwise the client\n// answers with the screen id and resolveAnswer rejects it as tool_use_mismatch.\n//\n// - Not shown before (different/absent content key) → broadcast (new card).\n// - Shown before AND the toolUseId changed → broadcast (re-sync the id).\n// - Shown before AND same toolUseId → suppress (true duplicate).\nexport function shouldBroadcastQuestion(args: {\n newContentKey: string;\n lastContentKey: string | undefined;\n newToolUseId: string;\n priorToolUseId: string | undefined;\n}): boolean {\n const alreadyShown = args.lastContentKey === args.newContentKey;\n if (!alreadyShown) return true;\n return args.priorToolUseId !== args.newToolUseId;\n}\n","import type { AskQuestion } from \"../../types\";\n\nconst DOWN = \"\\x1b[B\";\nconst ENTER = \"\\r\";\n\nexport class UnknownOptionError extends Error {\n constructor(\n public readonly question: string,\n public readonly value: string,\n ) {\n super(`No option labelled \"${value}\" for question \"${question}\"`);\n this.name = \"UnknownOptionError\";\n }\n}\n\n// v1: single-select only. Cursor starts at index 0; N downs + Enter selects index N.\n// Multi-question calls replay each question's block in the order Claude presents them.\nexport function answersToKeystrokes(\n questions: AskQuestion[],\n answers: Record<string, string | string[]>,\n): string {\n let out = \"\";\n for (const q of questions) {\n const raw = answers[q.question];\n if (raw === undefined) {\n throw new Error(`Missing answer for question \"${q.question}\"`);\n }\n const label = Array.isArray(raw) ? raw[0] : raw; // v1 ignores extra (multiSelect is v2)\n const target = q.options.findIndex((o) => o.label === label);\n if (target < 0) throw new UnknownOptionError(q.question, label);\n out += DOWN.repeat(target) + ENTER;\n }\n return out;\n}\n","import type { AskQuestion } from \"../../types\";\nimport { answersToKeystrokes, UnknownOptionError } from \"./answersToKeystrokes\";\n\nexport type AnswerResolution =\n | { ok: true; keys: string }\n | { ok: false; reason: \"no_pending_question\" | \"tool_use_mismatch\" | \"unknown_option\" };\n\nexport function resolveAnswer(\n pending: { toolUseId: string; questions: AskQuestion[] } | undefined,\n body: { toolUseId?: unknown; answers?: unknown },\n): AnswerResolution {\n if (!pending) return { ok: false, reason: \"no_pending_question\" };\n if (typeof body.toolUseId !== \"string\" || body.toolUseId !== pending.toolUseId) {\n return { ok: false, reason: \"tool_use_mismatch\" };\n }\n const answers = (body.answers ?? {}) as Record<string, string | string[]>;\n try {\n return { ok: true, keys: answersToKeystrokes(pending.questions, answers) };\n } catch (e) {\n if (e instanceof UnknownOptionError) return { ok: false, reason: \"unknown_option\" };\n throw e;\n }\n}\n","import { createProgressDedupeLRU } from \"./agent/dedupe\";\nimport { CLAUDE_CODE_PROVIDER } from \"./providers\";\nimport type {\n DiscoveredProcess,\n ManagedSession,\n SessionCursor,\n SessionListPage,\n SessionListQuery,\n SessionResponse,\n SessionSortKey,\n SortOrder,\n} from \"./types\";\n\nexport class SessionStore {\n private managed = new Map<string, ManagedSession>();\n private discovered = new Map<number, DiscoveredProcess>();\n\n addManaged(session: ManagedSession): void {\n this.managed.set(session.id, session);\n }\n\n /**\n * Multi-agent mode only. Attach a dedupe LRU to a session record. Idempotent —\n * calling twice keeps the existing LRU (and its contents).\n */\n initAgentSession(sessionId: string, dedupeCapacity: number): void {\n const session = this.managed.get(sessionId);\n if (!session) return;\n if (session.progressDedupeIds) return;\n session.progressDedupeIds = createProgressDedupeLRU(dedupeCapacity);\n }\n\n updateManaged(sessionId: string, updates: Partial<ManagedSession>): ManagedSession | null {\n const session = this.managed.get(sessionId);\n if (!session) return null;\n Object.assign(session, updates);\n return session;\n }\n\n removeManaged(sessionId: string): boolean {\n return this.managed.delete(sessionId);\n }\n\n getManaged(sessionId: string): ManagedSession | null {\n return this.managed.get(sessionId) ?? null;\n }\n\n setDiscovered(processes: DiscoveredProcess[]): void {\n this.discovered.clear();\n for (const proc of processes) {\n this.discovered.set(proc.pid, proc);\n }\n }\n\n listManaged(): ManagedSession[] {\n return Array.from(this.managed.values());\n }\n\n // Build the session list: live PTY sessions (managed) merged with externally\n // discovered Claude processes. Managed sessions keyed by JSONL UUID take\n // priority — discovered processes with the same UUID are skipped.\n list(ptyAttachedIds: Set<string>): SessionResponse[] {\n const results: SessionResponse[] = [];\n const seenIds = new Set<string>();\n\n for (const s of this.managed.values()) {\n results.push(managedToResponse(s, ptyAttachedIds.has(s.id)));\n seenIds.add(s.id);\n }\n\n for (const d of this.discovered.values()) {\n if (!d.conversationId) continue;\n if (seenIds.has(d.conversationId)) continue;\n results.push(discoveredToResponse(d, d.conversationId));\n seenIds.add(d.conversationId);\n }\n\n return results;\n }\n\n get(sessionId: string, ptyAttachedIds: Set<string>): SessionResponse | null {\n const managed = this.managed.get(sessionId);\n if (managed) return managedToResponse(managed, ptyAttachedIds.has(sessionId));\n\n for (const d of this.discovered.values()) {\n if (d.conversationId === sessionId) return discoveredToResponse(d, sessionId);\n }\n\n return null;\n }\n\n // Paginated, sorted, filtered view over the same merged data list() exposes.\n // The discovery cache can mutate the underlying set between requests, so the\n // cursor encodes both the chosen sort key value and the session id as a\n // tiebreaker. New sessions appearing mid-scan are picked up on the next\n // refetch — see plan for caveats.\n paginate(ptyAttachedIds: Set<string>, query: SessionListQuery): SessionListPage {\n const all = this.list(ptyAttachedIds);\n\n const filtered = query.status?.length\n ? all.filter((s) => query.status?.includes(s.status))\n : all;\n\n const sorted = [...filtered].sort(makeComparator(query.sortBy, query.order));\n\n const total = sorted.length;\n\n const startIdx = query.cursor\n ? findCursorBoundary(sorted, decodeCursor(query.cursor), query.sortBy, query.order)\n : 0;\n const page = sorted.slice(startIdx, startIdx + query.limit);\n const last = page[page.length - 1];\n const nextCursor =\n last && startIdx + page.length < sorted.length\n ? encodeCursor({ k: getSortValue(last, query.sortBy) ?? \"\", id: last.id })\n : null;\n\n return { sessions: page, nextCursor, total };\n }\n}\n\n// Cursor encoding is opaque to clients: base64url(JSON({ k, id })).\nexport function encodeCursor(c: SessionCursor): string {\n return Buffer.from(JSON.stringify(c), \"utf8\").toString(\"base64url\");\n}\n\nexport function decodeCursor(s: string): SessionCursor {\n let parsed: unknown;\n try {\n parsed = JSON.parse(Buffer.from(s, \"base64url\").toString(\"utf8\"));\n } catch {\n throw new Error(\"INVALID_CURSOR\");\n }\n if (\n !parsed ||\n typeof parsed !== \"object\" ||\n typeof (parsed as SessionCursor).id !== \"string\" ||\n ![\"string\", \"number\"].includes(typeof (parsed as SessionCursor).k)\n ) {\n throw new Error(\"INVALID_CURSOR\");\n }\n return parsed as SessionCursor;\n}\n\nfunction getSortValue(s: SessionResponse, key: SessionSortKey): string | number | undefined {\n switch (key) {\n case \"startedAt\":\n return s.startedAt;\n case \"lastActivityAt\":\n return s.lastActivityAt ?? s.startedAt;\n case \"projectName\":\n return s.projectName;\n case \"status\":\n return s.status;\n }\n}\n\nfunction compareValues(a: string | number | undefined, b: string | number | undefined): number {\n // Undefined sorts last regardless of order; callers normalise via getSortValue\n // so this branch only fires for genuinely missing values.\n if (a === undefined && b === undefined) return 0;\n if (a === undefined) return 1;\n if (b === undefined) return -1;\n if (typeof a === \"number\" && typeof b === \"number\") return a - b;\n return String(a).localeCompare(String(b));\n}\n\nfunction makeComparator(key: SessionSortKey, order: SortOrder) {\n const dir = order === \"asc\" ? 1 : -1;\n return (a: SessionResponse, b: SessionResponse): number => {\n const cmp = compareValues(getSortValue(a, key), getSortValue(b, key)) * dir;\n if (cmp !== 0) return cmp;\n // Tiebreaker on id is always ascending so the total order is stable\n // regardless of `order`.\n return a.id.localeCompare(b.id);\n };\n}\n\n// Returns the index of the first element strictly *after* the cursor under the\n// chosen ordering. Linear scan is fine: total session counts are in the\n// hundreds, not millions.\nfunction findCursorBoundary(\n sorted: SessionResponse[],\n cursor: SessionCursor,\n key: SessionSortKey,\n order: SortOrder,\n): number {\n const dir = order === \"asc\" ? 1 : -1;\n for (let i = 0; i < sorted.length; i++) {\n const item = sorted[i];\n const cmp = compareValues(getSortValue(item, key), cursor.k) * dir;\n if (cmp > 0) return i;\n if (cmp === 0 && item.id.localeCompare(cursor.id) > 0) return i;\n }\n return sorted.length;\n}\n\nfunction managedToResponse(s: ManagedSession, ptyAttached: boolean): SessionResponse {\n return {\n id: s.id,\n conversationId: s.id,\n provider: s.provider ?? CLAUDE_CODE_PROVIDER,\n status: s.status,\n projectPath: s.projectPath,\n projectName: s.projectName,\n branch: s.branch,\n lastOutput: s.lastOutput,\n elapsedMs: (s.completedAt ?? new Date()).getTime() - s.startedAt.getTime(),\n promptCount: s.promptCount,\n startedAt: s.startedAt.toISOString(),\n completedAt: s.completedAt?.toISOString() ?? null,\n ptyAttached,\n ...(s.projectId != null && { projectId: s.projectId }),\n ...(s.sessionName != null && { sessionName: s.sessionName }),\n ...(s.model != null && { model: s.model }),\n ...(s.account != null && { account: s.account }),\n ...(s.messageCount != null && { messageCount: s.messageCount }),\n ...(s.preview != null && { preview: s.preview }),\n ...(s.firstMessageText != null && { firstMessageText: s.firstMessageText }),\n ...(s.firstMessageAt != null && { firstMessageAt: s.firstMessageAt.toISOString() }),\n ...(s.lastMessageText != null && { lastMessageText: s.lastMessageText }),\n ...(s.lastMessageAt != null && { lastMessageAt: s.lastMessageAt.toISOString() }),\n ...(s.lastActivityAt != null && { lastActivityAt: s.lastActivityAt.toISOString() }),\n ...(s.filePath != null && { filePath: s.filePath }),\n ...(s.failureReason != null && { failureReason: s.failureReason }),\n ...(s.resumedFromConversationId != null && {\n resumedFromConversationId: s.resumedFromConversationId,\n }),\n ...(s.boundConversationId != null && { boundConversationId: s.boundConversationId }),\n };\n}\n\nfunction discoveredToResponse(d: DiscoveredProcess, conversationId: string): SessionResponse {\n return {\n id: conversationId,\n conversationId,\n provider: CLAUDE_CODE_PROVIDER,\n status: \"idle\",\n projectPath: d.projectPath,\n projectName: d.projectName,\n branch: d.branch,\n lastOutput: \"\",\n elapsedMs: Date.now() - d.startedAt.getTime(),\n promptCount: 0,\n startedAt: d.startedAt.toISOString(),\n completedAt: null,\n ptyAttached: false,\n pid: d.pid,\n };\n}\n","import { randomBytes } from \"crypto\";\nimport { mkdir, writeFile } from \"fs/promises\";\nimport heicConvert from \"heic-convert\";\nimport { join } from \"path\";\n\nconst UPLOAD_DIR_NAME = \".threadbase-uploads\";\nconst MAX_BYTES = 25 * 1024 * 1024; // 25MB\n\nconst HEIC_MIMES = new Set([\"image/heic\", \"image/heif\"]);\n\nconst MIME_TO_EXT: Record<string, string> = {\n \"image/jpeg\": \".jpg\",\n \"image/png\": \".png\",\n \"image/gif\": \".gif\",\n \"image/webp\": \".webp\",\n \"image/heic\": \".jpg\",\n \"image/heif\": \".jpg\",\n \"application/pdf\": \".pdf\",\n \"application/msword\": \".doc\",\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": \".docx\",\n \"text/plain\": \".txt\",\n \"text/javascript\": \".js\",\n \"application/typescript\": \".ts\",\n \"application/json\": \".json\",\n \"text/csv\": \".csv\",\n};\n\nexport interface SaveUploadInput {\n sessionId: string;\n projectPath: string;\n originalName: string;\n mimeType: string;\n dataBase64: string;\n}\n\nexport interface SavedUpload {\n id: string;\n filePath: string;\n originalName: string;\n mimeType: string;\n sizeBytes: number;\n}\n\nexport async function saveUploadFile(input: SaveUploadInput): Promise<SavedUpload> {\n let buffer = Buffer.from(input.dataBase64, \"base64\");\n if (buffer.length === 0) throw new Error(\"Empty file\");\n if (buffer.length > MAX_BYTES) throw new Error(`File exceeds ${MAX_BYTES} bytes`);\n\n let { mimeType } = input;\n let originalName = input.originalName;\n\n if (HEIC_MIMES.has(mimeType)) {\n buffer = Buffer.from(await heicConvert({ buffer, format: \"JPEG\", quality: 0.85 }));\n mimeType = \"image/jpeg\";\n originalName = originalName.replace(/\\.(heic|heif)$/i, \".jpg\");\n }\n\n const id = `up_${randomBytes(8).toString(\"hex\")}`;\n const safeName = sanitizeFilename(originalName) || `file${MIME_TO_EXT[mimeType] ?? \"\"}`;\n const dir = join(input.projectPath, UPLOAD_DIR_NAME, input.sessionId);\n await mkdir(dir, { recursive: true });\n\n const filePath = join(dir, `${Date.now()}-${id}-${safeName}`);\n await writeFile(filePath, buffer);\n\n return {\n id,\n filePath,\n originalName: safeName,\n mimeType,\n sizeBytes: buffer.length,\n };\n}\n\nfunction sanitizeFilename(name: string): string {\n // Take only the basename (block path traversal)\n const base = name.split(/[\\\\/]/).pop() ?? \"\";\n // Strip leading dots; keep all printable Unicode (charCode >= 32, != 127)\n const cleaned = base\n .replace(/^\\.+/, \"\")\n .split(\"\")\n .filter((c) => c.charCodeAt(0) >= 32 && c.charCodeAt(0) !== 127)\n .join(\"\");\n return cleaned;\n}\n","/**\n * Codex rollout JSONL → Claude-shaped conversation lines for WS clients.\n *\n * Mobile's `parseLineToMessage` only understands Claude Code JSONL\n * (`type: \"user\"|\"assistant\"`, `message.role`, text blocks). Codex writes\n * `response_item` / `event_msg` / `session_meta` instead, so raw Codex lines\n * never become live bubbles. This helper normalizes chat-bearing Codex lines\n * into the Claude shape and drops everything else (including the duplicate\n * `event_msg` copies of each turn and `developer` role payloads).\n */\n\ntype CodexContentBlock = {\n type?: string;\n text?: string;\n};\n\nfunction extractCodexText(content: unknown): string {\n if (typeof content === \"string\") return content.trim();\n if (!Array.isArray(content)) return \"\";\n return content\n .map((item) => {\n if (typeof item === \"string\") return item;\n const block = item as CodexContentBlock;\n const t = block?.type;\n if (\n (t === \"input_text\" || t === \"output_text\" || t === \"text\") &&\n typeof block.text === \"string\"\n ) {\n return block.text;\n }\n return \"\";\n })\n .filter(Boolean)\n .join(\"\")\n .trim();\n}\n\n/**\n * Returns a Claude-shaped JSONL line, or null when the input is not a\n * user/assistant chat message that clients should render.\n */\nexport function normalizeCodexLineToClaudeShape(line: string): string | null {\n let entry: {\n type?: string;\n timestamp?: string;\n payload?: {\n type?: string;\n role?: string;\n content?: unknown;\n id?: string;\n };\n };\n try {\n entry = JSON.parse(line);\n } catch {\n return null;\n }\n\n if (entry.type !== \"response_item\") return null;\n const payload = entry.payload;\n if (payload?.type !== \"message\") return null;\n\n const role = payload.role;\n if (role !== \"user\" && role !== \"assistant\") return null;\n\n const text = extractCodexText(payload.content);\n if (!text) return null;\n\n // Synthetic Codex context dumps (AGENTS.md / permissions instructions) are\n // written as `role: user` before any real user turn. Hide them from the live\n // overlay so the chat doesn't open with fake user bubbles.\n if (role === \"user\" && isCodexInjectedContext(text)) return null;\n\n const timestamp =\n typeof entry.timestamp === \"string\" ? entry.timestamp : new Date().toISOString();\n // Prefer payload.id when present; otherwise derive a stable-enough id from\n // timestamp+role+text prefix so WS seenIds can dedupe without colliding\n // across consecutive turns in the same second.\n const uuid =\n typeof payload.id === \"string\" && payload.id.length > 0\n ? payload.id\n : `codex-${role}-${timestamp}-${hashPrefix(text)}`;\n\n return JSON.stringify({\n type: role,\n uuid,\n timestamp,\n message: {\n role,\n content: [{ type: \"text\", text }],\n },\n });\n}\n\n/** True when a Codex rollout line is Codex-shaped (not Claude JSONL). */\nexport function isCodexRolloutLine(line: string): boolean {\n try {\n const entry = JSON.parse(line) as { type?: string };\n return (\n entry.type === \"response_item\" ||\n entry.type === \"event_msg\" ||\n entry.type === \"session_meta\" ||\n entry.type === \"turn_context\"\n );\n } catch {\n return false;\n }\n}\n\n/**\n * Map a batch of raw JSONL lines to client-facing lines. Codex batches are\n * normalized (and filtered); Claude batches pass through unchanged so seq\n * alignment is preserved.\n */\nexport function toClientConversationLines(lines: string[]): string[] {\n if (lines.length === 0) return lines;\n // Heuristic: if any line in the batch is Codex-shaped, treat the whole batch\n // as Codex (a mixed batch shouldn't happen — one file, one provider).\n const codex = lines.some(isCodexRolloutLine);\n if (!codex) return lines;\n const out: string[] = [];\n for (const line of lines) {\n const normalized = normalizeCodexLineToClaudeShape(line);\n if (normalized) out.push(normalized);\n }\n return out;\n}\n\nexport function isCodexInjectedContext(text: string): boolean {\n // AGENTS.md / instruction dumps Codex prepends as the first \"user\" turn.\n if (text.startsWith(\"# AGENTS.md\") || text.includes(\"<INSTRUCTIONS>\")) return true;\n // Permissions / sandbox preamble sometimes arrives as a giant user-role blob.\n if (\n text.startsWith(\"<permissions instructions>\") ||\n text.includes(\"Filesystem sandboxing defines\")\n ) {\n return true;\n }\n // Streamer-injected prompts passed as Codex CLI argv (no --system-prompt flag).\n // DEFAULT_SYSTEM_PROMPT + BROWSE_SYSTEM_PROMPT land as role:user in the rollout.\n if (text.includes(\"limit the options to at most 3\")) return true;\n if (text.includes(\"You are working within the project boundary:\")) return true;\n if (\n text.includes(\n \"Do not read, write, or execute commands that access files or directories outside this boundary\",\n )\n ) {\n return true;\n }\n return false;\n}\n\nfunction hashPrefix(text: string): string {\n // Short non-crypto fingerprint for id uniqueness within a session.\n let h = 0;\n const sample = text.slice(0, 64);\n for (let i = 0; i < sample.length; i++) {\n h = (h * 31 + sample.charCodeAt(i)) | 0;\n }\n return Math.abs(h).toString(36);\n}\n","import { createHash } from \"node:crypto\";\n\n/**\n * Inputs that uniquely identify a conversation's current state for the\n * purpose of a conditional fetch. These all live on the parsed `Conversation`\n * and must be read AFTER `findConversationByUuid`'s staleness refresh — never\n * from a pre-refresh snapshot — so the validator reflects the same state the\n * response body would.\n */\nexport interface ConversationEtagInput {\n filePath: string;\n messageCount: number;\n timestamp: string;\n}\n\n/**\n * Derive a stable, opaque ETag for a conversation. The client never parses it;\n * it only echoes the value back via `If-None-Match`. Wrapping the inputs in a\n * hash keeps the formula changeable without the client caring.\n */\nexport function computeConversationEtag({\n filePath,\n messageCount,\n timestamp,\n}: ConversationEtagInput): string {\n const digest = createHash(\"sha1\")\n .update(`${filePath}:${messageCount}:${timestamp}`)\n .digest(\"hex\")\n .slice(0, 16);\n return `\"${digest}\"`;\n}\n","import { compareDesc, isValid, parseISO } from \"date-fns\";\n\n/**\n * Parse an ISO timestamp string. Returns null for null/undefined input or\n * an unparseable value rather than throwing — callers can fall back.\n */\nexport function parseIsoDateOrNull(value?: string | null): Date | null {\n if (!value) return null;\n const parsed = parseISO(value);\n return isValid(parsed) ? parsed : null;\n}\n\n/**\n * Compare two ISO timestamp strings descending. Null/undefined values sort\n * last regardless of order. Used for ProjectChat list ordering and latest\n * conversation selection.\n */\nexport function compareIsoDesc(a?: string | null, b?: string | null): number {\n const dateA = parseIsoDateOrNull(a);\n const dateB = parseIsoDateOrNull(b);\n\n if (!dateA && !dateB) return 0;\n if (!dateA) return 1;\n if (!dateB) return -1;\n\n return compareDesc(dateA, dateB);\n}\n","import { parseIsoDateOrNull } from \"./dates\";\n\n// The on-disk mtime can run slightly ahead of the last message's own timestamp\n// (filesystem flush latency, non-message trailer lines like pr-link/mode that\n// the scanner ignores for `timestamp` but still bump mtime). Require the file\n// to be at least this much newer than the snapshot before forcing a re-scan, so\n// a single read doesn't churn the whole scanner index on every detail fetch.\nconst STALENESS_TOLERANCE_MS = 1000;\n\n/**\n * Decide whether a scanned conversation snapshot is stale relative to the\n * JSONL on disk. The scanner memoizes both its metadata index and parsed\n * conversations for the server's lifetime, so a conversation that grows after\n * the initial scan keeps serving the startup snapshot from\n * `/api/conversations/{id}`. We compare the file's mtime against the\n * snapshot's last-activity timestamp to detect that drift.\n *\n * Returns true when the file is meaningfully newer than the snapshot (a\n * re-scan is warranted). Returns false when the snapshot is current, when the\n * snapshot timestamp is unparseable (nothing to compare against — leave it),\n * or when mtime is null (stat failed — don't churn the index on a transient\n * read error).\n */\nexport function isScannedSnapshotStale(\n snapshotTimestamp: string | null | undefined,\n fileMtimeMs: number | null,\n): boolean {\n if (fileMtimeMs == null) return false;\n const snapshotDate = parseIsoDateOrNull(snapshotTimestamp);\n if (!snapshotDate) return false;\n return fileMtimeMs - snapshotDate.getTime() > STALENESS_TOLERANCE_MS;\n}\n","/**\n * The scanner fires onProgress once per file, so a large scan would otherwise\n * broadcast thousands of scan_progress WebSocket frames. This throttle collapses\n * them to at most one per whole-percent step, plus a guaranteed final frame when\n * the scan completes (scanned === total). Caps frames at ~101 regardless of the\n * file count, while always delivering the terminal 100% update.\n *\n * Returns a predicate: call it with each (scanned, total) tick; it returns true\n * when that tick should be broadcast. Stateful — create one per scan.\n */\nexport function createScanProgressThrottle(): (scanned: number, total: number) => boolean {\n let lastPercent = -1;\n return (scanned: number, total: number): boolean => {\n // total === 0 (empty scan): emit once so clients see a terminal tick.\n if (total <= 0) {\n if (lastPercent === 100) return false;\n lastPercent = 100;\n return true;\n }\n const isFinal = scanned >= total;\n const percent = Math.floor((scanned / total) * 100);\n if (isFinal) {\n // Always let the final tick through, even if its percent already fired.\n lastPercent = 100;\n return true;\n }\n if (percent === lastPercent) return false;\n lastPercent = percent;\n return true;\n };\n}\n","import type { WebSocket } from \"ws\";\nimport type { WSMessage } from \"./types\";\n\nconst PING_INTERVAL_MS = 30_000;\n// How long to wait for a pong before treating the socket as dead.\n// Must be less than PING_INTERVAL_MS.\nconst PONG_TIMEOUT_MS = 10_000;\n\nexport class WSHub {\n private clients = new Set<WebSocket>();\n private pingTimer: ReturnType<typeof setInterval> | null = null;\n // Per-socket pong-timeout handle; set when ping is sent, cleared on pong/close.\n private pongTimers = new Map<WebSocket, ReturnType<typeof setTimeout>>();\n\n addClient(ws: WebSocket): void {\n this.clients.add(ws);\n\n ws.on(\"pong\", () => {\n const t = this.pongTimers.get(ws);\n if (t) {\n clearTimeout(t);\n this.pongTimers.delete(ws);\n }\n });\n\n ws.on(\"close\", () => {\n const t = this.pongTimers.get(ws);\n if (t) {\n clearTimeout(t);\n this.pongTimers.delete(ws);\n }\n this.clients.delete(ws);\n });\n\n ws.on(\"error\", () => {\n const t = this.pongTimers.get(ws);\n if (t) {\n clearTimeout(t);\n this.pongTimers.delete(ws);\n }\n this.clients.delete(ws);\n });\n\n if (!this.pingTimer && this.clients.size > 0) {\n this.startPing();\n }\n }\n\n broadcast(message: WSMessage): void {\n const data = JSON.stringify(message);\n const dead: WebSocket[] = [];\n\n for (const client of this.clients) {\n try {\n if (client.readyState === client.OPEN) {\n client.send(data);\n } else {\n dead.push(client);\n }\n } catch {\n dead.push(client);\n }\n }\n\n for (const client of dead) {\n this.clients.delete(client);\n }\n }\n\n unicast(ws: WebSocket, message: WSMessage): void {\n try {\n if (ws.readyState === ws.OPEN) {\n ws.send(JSON.stringify(message));\n }\n } catch {\n this.clients.delete(ws);\n }\n }\n\n get connectionCount(): number {\n return this.clients.size;\n }\n\n dispose(): void {\n if (this.pingTimer) {\n clearInterval(this.pingTimer);\n this.pingTimer = null;\n }\n for (const [, t] of this.pongTimers) {\n clearTimeout(t);\n }\n this.pongTimers.clear();\n for (const client of this.clients) {\n try {\n // terminate() (not close()) so the underlying TCP socket dies\n // immediately. A graceful close() only sends a close frame and waits\n // for the peer's reply — a slow/backgrounded client would keep the\n // connection (and thus the HTTP listener's port) alive until the peer\n // ACKs, which is what stalled shutdown and caused EADDRINUSE on the\n // next deploy.\n client.terminate();\n } catch {\n // Already closed\n }\n }\n this.clients.clear();\n }\n\n private startPing(): void {\n this.pingTimer = setInterval(() => {\n if (this.clients.size === 0 && this.pingTimer) {\n clearInterval(this.pingTimer);\n this.pingTimer = null;\n return;\n }\n for (const client of this.clients) {\n if (client.readyState !== client.OPEN) continue;\n // WS protocol ping — client must reply with a pong frame. If no pong\n // arrives within PONG_TIMEOUT_MS the socket is considered dead and\n // terminated. This is what detects iOS silently killing the TCP\n // connection without delivering a close frame to the JS layer.\n client.ping();\n const t = setTimeout(() => {\n this.pongTimers.delete(client);\n client.terminate();\n }, PONG_TIMEOUT_MS);\n this.pongTimers.set(client, t);\n }\n }, PING_INTERVAL_MS);\n }\n}\n"],"mappings":"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","names":["resolve","join","readFileSync","dirname","join","pool","basename","existsSync","homedir","join","mkdirSync","readFileSync","writeFileSync","homedir","dirname","join","run","resolve","lastNonBlank","existsSync","Terminal","randomUUID","existsSync","basename","OPTION_RE","BOX_ONLY_RE","BOX_ONLY_RE","OPTION_RE","BOX_ONLY_RE","OUTPUT_BUFFER_MAX","INPUT_HISTORY_MAX","PTY_COLS","PTY_ROWS","SCREEN_SCROLLBACK","QUIET_DETECT_MS","SUBMIT_BYTES","digestBytes","pty","loadPty","createScreen","Terminal","toPublicSession","basename","randomUUID","resolve","stripAnsi","existsSync","basename","platform","basename","dirname","platform","basename","resolve","dirname","existsSync","readdirSync","readFileSync","statSync","realpath","homedir","dirname","join","crypto","crypto","log","existsSync","join","join","existsSync","Hono","Hono","Hono","ALREADY_HANDLED","alreadyHandled","Hono","readFileSync","dirname","join","dirname","join","readFileSync","Hono","existsSync","join","Hono","homedir","join","join","homedir","logger","join","existsSync","Hono","timingSafeEqual","Hono","readFileSync","homedir","join","join","homedir","readFileSync","resolve","readRawBody","timingSafeEqual","Hono","Hono","ALREADY_HANDLED","alreadyHandled","Hono","ALREADY_HANDLED","alreadyHandled","Hono","ALREADY_HANDLED","alreadyHandled","Hono","ALREADY_HANDLED","alreadyHandled","Hono","Hono","mkdir","join","closeSync","existsSync","mkdirSync","openSync","readSync","statSync","dirname","readdirSync","readFileSync","dirname","join","fileURLToPath","getMigrationsDir","closeSync","openSync","readSync","statSync","cached","stat","stat","statSync","openSync","readSync","closeSync","cached","mkdirSync","dirname","run","existsSync","randomUUID","randomUUID","pool","readdirSync","statSync","homedir","join","randomBytes","statSync","stat","statSync","stat","existsSync","normalizeContent","ENTER","randomBytes","mkdir","join","createHash","join","homedir","realpath","statSync","dirname","resolve","ALREADY_HANDLED","hostname","scanner","metas","total","adapted","stat","existsSync","readdirSync","filePath","availability","messagesPayload","resp","cached","body","readFileSync"]}