{"version":3,"sources":["../src/index.ts","../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/claude-flags.ts","../src/logger.ts","../src/feature-flags.ts","../src/db/config.ts","../src/db/migrations.ts","../src/db/pool.ts","../src/live-session-manager.ts","../src/codex-pty-runner.ts","../src/platform.ts","../src/providers.ts","../src/pty-shared.ts","../src/services/questions/codexGateAnswers.ts","../src/services/questions/codexScreen.ts","../src/services/questions/parseAgentPhase.ts","../src/utils/debounce.ts","../src/cursor-pty-runner.ts","../src/pty-host/protocol.ts","../src/pty-host/remote-session-runner.ts","../src/pty-manager.ts","../src/services/questions/detectPermissionGate.ts","../src/services/questions/detectPromptSuggestion.ts","../src/services/questions/detectQuestionFromScreen.ts","../src/services/questions/detectShellPrompt.ts","../src/services/questions/detectStartupChoiceGate.ts","../src/services/questions/permissionAnswerKeys.ts","../src/utils/deriveSessionName.ts","../src/process-discovery.ts","../src/schemas/prompt.schema.ts","../src/server.ts","../src/access-probe.ts","../src/api/app.ts","../src/db/repositories/devices.repository.ts","../src/services/security/capabilities.ts","../src/e2ee/context.ts","../src/e2ee/protocol.ts","../src/e2ee/record.ts","../src/e2ee/rest-window.ts","../src/api/middleware/auth.middleware.ts","../src/api/middleware/e2ee-envelope.middleware.ts","../src/api/middleware/error.middleware.ts","../src/api/middleware/cors.middleware.ts","../src/api/routes/backup.routes.ts","../src/services/backup/backup.ts","../src/version.ts","../src/api/routes/browse.routes.ts","../src/api/routes/cacheAlert.routes.ts","../src/schemas/cacheAlert.schema.ts","../src/api/routes/config.routes.ts","../src/schemas/claudeFlags.schema.ts","../src/api/routes/conversations.routes.ts","../src/api/routes/devices.routes.ts","../src/api/routes/diagnostics.routes.ts","../src/services/diagnostics/diagnostics.ts","../src/services/sessions/resumeIdentity.ts","../src/services/sessions/rehydrateSessions.ts","../src/utils/bootToken.ts","../src/api/routes/e2ee.routes.ts","../src/e2ee/noise.ts","../src/e2ee/pair-request.ts","../src/e2ee/replay-cache.ts","../src/server-identity.ts","../src/api/rate-limit.ts","../src/api/routes/misc.routes.ts","../src/config/update-config.ts","../src/schemas/updateConfig.schema.ts","../src/db/repositories/push.repository.ts","../src/schemas/notification-prefs.schema.ts","../src/services/push/apnsClient.ts","../src/services/push/notificationCopy.ts","../src/api/routes/health.routes.ts","../src/api/routes/logs.routes.ts","../src/lifecycle/constants.ts","../src/api/routes/pair.routes.ts","../src/api/routes/projects.routes.ts","../src/api/routes/providers.routes.ts","../src/services/providers/providerHealth.ts","../src/services/providers/capabilities.ts","../src/api/routes/scanner.routes.ts","../src/api/routes/sessions.routes.ts","../src/api/routes/ws.routes.ts","../src/api/handlers/conversations.handlers.ts","../src/conversation-cache.ts","../src/db/query-timing.ts","../src/db/sqlite-migrate.ts","../src/services/conversations/classification.ts","../src/utils/codexConversationLine.ts","../src/utils/claudeShapedLine.ts","../src/utils/cursorConversationLine.ts","../src/services/conversations/isAgentConversation.ts","../src/utils/canonicalizeFilePath.ts","../src/utils/fileIdentity.ts","../src/services/conversations/findSearchTarget.ts","../src/services/conversations/inheritedHistory.ts","../src/services/projectChats/deriveProjectChatTitle.ts","../src/services/search/searchQuery.ts","../src/session-watchers.ts","../src/cursor-transcript-watch.ts","../src/utils/conversationEtag.ts","../src/utils/scanProgressThrottle.ts","../src/api/handlers/http-helpers.ts","../src/api/handlers/sessions.handlers.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/browse.ts","../src/db/upload-records.ts","../src/services/prompts/promptRegistry.ts","../src/services/prompts/ptyPromptAdapter.ts","../src/services/questions/parseStatusLine.ts","../src/services/questions/answersToKeystrokes.ts","../src/services/questions/resolveAnswer.ts","../src/services/sessions/codexRolloutOwner.ts","../src/services/sessions/conversationBusy.ts","../src/utils/canonicalizeProjectPath.ts","../src/services/sessions/idempotency.ts","../src/uploads.ts","../src/db/repositories/cacheMetadata.repository.ts","../src/db/repositories/conversations.repository.ts","../src/db/repositories/managed-sessions.repository.ts","../src/db/repositories/projects.repository.ts","../src/db/repositories/sessions.repository.ts","../src/db/runtime-store.ts","../src/e2ee/pair-payload.ts","../src/external-tails.ts","../src/pair-store.ts","../src/pty-host/spawn-host.ts","../src/pty-host/socket.ts","../src/scanner-manager.ts","../src/services/cache/cacheMetadata.ts","../src/utils/dates.ts","../src/services/projects/ensureProjectsForConversations.ts","../src/services/conversations/refreshConversationCache.ts","../src/services/conversations/shouldRefreshProjectsFromHdd.ts","../src/utils/isScannedSnapshotStale.ts","../src/seal.ts","../src/server-wiring.ts","../src/handlers/handleListProjects.ts","../src/services/cache-integrity/cacheIntegrityMonitor.ts","../src/services/cache-integrity/alertStore.ts","../src/services/cache-integrity/backup.ts","../src/services/conversations/conversationWatcher.ts","../src/services/conversations/pruneAgentConversations.ts","../src/services/host-pressure/hostPressure.ts","../src/services/push/notificationPrefs.ts","../src/services/push/expoPushSender.ts","../src/services/push/liveActivityContentState.ts","../src/services/push/liveActivityNotifier.ts","../src/services/push/liveActivitySender.ts","../src/services/push/liveActivityRenewal.ts","../src/services/push/waitingInputNotifier.ts","../src/services/questions/detectAskUserQuestion.ts","../src/services/questions/questionBroadcast.ts","../src/services/sessions/codexFormatCanary.ts","../src/session-registry-boot.ts","../src/lifecycle/process-liveness.ts","../src/services/sessions/autoResumeOnBoot.ts","../src/services/sessions/reconcileSessions.ts","../src/types.ts","../src/session-store.ts","../src/ws-hub.ts"],"sourcesContent":["export {\n  type AgentClient,\n  type AgentClientOpts,\n  createAgentClient,\n} from \"./agent/agent-client\";\n// Multi-agent mode surface. Consumed by tb-multi-agent integration tests and\n// by any external integrator wiring up the agent pipeline. See\n// docs/multi-agent-mode.md for operator-facing context.\nexport { type AgentConfig, readAgentConfig } from \"./agent/agent-config\";\nexport {\n  type AppendArgs,\n  type ConversationWriter,\n  createConversationWriter,\n} from \"./agent/conversation-writer\";\nexport { createProgressDedupeLRU, type ProgressDedupeLRU } from \"./agent/dedupe\";\nexport { createProgressRoutes } from \"./api/routes/progress.routes\";\nexport { generateApiKey, loadOrCreateApiKey, validateApiKey } from \"./auth\";\nexport type { DbConfig } from \"./db\";\nexport { createPool, getDbConfig, isDbEnabled, maskConnectionString } from \"./db\";\nexport { LiveSessionManager } from \"./live-session-manager\";\nexport { discoverClaudeProcesses } from \"./process-discovery\";\nexport {\n  CLAUDE_CODE_PROVIDER,\n  CODEX_CLI_PROVIDER,\n  CURSOR_PROVIDER,\n  canonicalizeProviderName,\n  commandNameForProvider,\n  isProviderName,\n  isProviderResumable,\n  LEGACY_CURSOR_PROVIDER,\n  PROVIDER_NAMES,\n  type ProviderName,\n} from \"./providers\";\nexport { PTYManager } from \"./pty-manager\";\nexport {\n  PROMPT_SCHEMA_VERSION,\n  type Prompt,\n  type PromptAnswer,\n  PromptAnswerSchema,\n  type PromptOption,\n  PromptOptionSchema,\n  type PromptQuestion,\n  PromptQuestionSchema,\n  type PromptResponse,\n  PromptResponseSchema,\n  PromptSchema,\n} from \"./schemas/prompt.schema\";\nexport { StreamerServer } from \"./server\";\nexport { ConversationWatcher } from \"./services/conversations/conversationWatcher\";\nexport { SessionStore } from \"./session-store\";\nexport * from \"./types\";\nexport { WSHub } from \"./ws-hub\";\n","// 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\";\nimport type { AccessServiceToken } from \"./access-probe\";\nimport {\n  type ClaudeFlagValues,\n  isPermissionMode,\n  type PermissionMode,\n  validateFlagValues,\n} from \"./claude-flags\";\nimport { type FeatureFlagValues, validateFeatureFlagValues } from \"./feature-flags\";\nimport { getLogger } from \"./logger\";\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  // 0600 + chmod, matching setConfigValue and setApiKey below. Without the mode\n  // this line created the file at the process umask — typically 0644 — so the\n  // FIRST server.yaml on a machine held the API key world-readable until some\n  // later write happened to rewrite it at 0600. A server that never rotates its\n  // key never gets that write.\n  writeFileSync(configFile(), `api_key: ${key}\\n`, { encoding: \"utf-8\", mode: 0o600 });\n  // writeFileSync's mode only applies when it CREATES the file, and it is\n  // masked by the umask; chmod is what makes the permission unconditional.\n  chmodSync(configFile(), 0o600);\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(): PermissionMode | 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 (isPermissionMode(value)) return value;\n  } catch {\n    // File doesn't exist or not readable\n  }\n  return undefined;\n}\n\n/**\n * Write (or delete) a single `key: value` line in server.yaml.\n *\n * server.yaml is a flat, regex-parsed file — no YAML library — so every value\n * must stay on ONE line. Passing `undefined` removes the key rather than\n * writing a bare `key:` that the readers would then match with an empty value.\n *\n * The write is atomic (tmp + rename) and 0600 because this file holds the API\n * key. Every setter in this module goes through here.\n */\nfunction setConfigValue(key: string, value: string | undefined): 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 lineRe = new RegExp(`^${key}:\\\\s*.*$\\\\n?`, \"m\");\n  let updated: string;\n  if (value === undefined) {\n    updated = content.replace(lineRe, \"\");\n  } else {\n    const line = `${key}: ${value}`;\n    if (lineRe.test(content)) {\n      updated = content.replace(lineRe, `${line}\\n`);\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\n  const tmpFile = `${file}.tmp`;\n  writeFileSync(tmpFile, updated, { encoding: \"utf-8\", mode: 0o600 });\n  chmodSync(tmpFile, 0o600);\n  renameSync(tmpFile, file);\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: PermissionMode): void {\n  setConfigValue(\"default_permission_mode\", mode);\n}\n\n/**\n * Whether interrupted sessions are re-started automatically at boot\n * (persistence plan Phase 7a).\n *\n * **Tri-state on purpose.** `undefined` means the key is absent — the user has\n * never been asked — and is what triggers the one-time prompt. It is not the\n * same as `false`, which is a real answer and must never be re-asked.\n * Collapsing the two would either nag on every boot or silently decide for\n * someone who was never given the choice.\n *\n * Not a feature flag: feature flags gate behaviour *we* are unsure about, while\n * this is a user preference with a persisted answer — the\n * `default_permission_mode` shape, not the `codexSystemPrompt` shape.\n */\nexport function loadAutoResumeOnBoot(): boolean | undefined {\n  try {\n    const content = readFileSync(configFile(), \"utf-8\");\n    // Anchored and exact: only a literal `true`/`false` is an answer. A typo\n    // must read as \"never asked\" and re-prompt, rather than being coerced into\n    // enabling unattended agent starts.\n    const match = content.match(/^auto_resume_on_boot:\\s*(true|false)\\s*$/m);\n    if (match) return match[1] === \"true\";\n  } catch {\n    // File doesn't exist or not readable\n  }\n  return undefined;\n}\n\nexport function setAutoResumeOnBoot(value: boolean): void {\n  setConfigValue(\"auto_resume_on_boot\", String(value));\n}\n\n/**\n * Allowlisted Claude CLI flags, stored as ONE line of JSON:\n *\n *     claude_flags: {\"permissionMode\":\"bypassPermissions\",\"addDir\":[\"/srv/a b\"]}\n *\n * JSON rather than a bespoke encoding because JSON.stringify already escapes\n * colons, quotes and spaces (killing the whole quoting-bug class) and emits no\n * raw newlines, so the one-line invariant holds by construction. It is also a\n * valid YAML flow mapping, so the file still parses if a real YAML reader is\n * ever pointed at it.\n *\n * A malformed line yields {} plus a warning rather than throwing: server.yaml is\n * hand-editable and a typo must never stop the server from booting.\n */\nexport function loadClaudeFlags(): ClaudeFlagValues {\n  try {\n    const content = readFileSync(configFile(), \"utf-8\");\n    const match = content.match(/^claude_flags:\\s*(.+)$/m);\n    if (!match?.[1]) return {};\n    return validateFlagValues(JSON.parse(match[1].trim()));\n  } catch (err) {\n    if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n      getLogger(\"auth\").warn(`Ignoring unreadable claude_flags in server.yaml: ${String(err)}`, {\n        event: \"config.claude_flags_parse_failed\",\n      });\n    }\n    return {};\n  }\n}\n\nexport function setClaudeFlags(values: ClaudeFlagValues): void {\n  const safe = validateFlagValues(values);\n  setConfigValue(\"claude_flags\", Object.keys(safe).length === 0 ? undefined : JSON.stringify(safe));\n}\n\n/** Free-text argv appended after the allowlisted flags. Unvalidated by design. */\nexport function loadClaudeExtraArgs(): string | undefined {\n  try {\n    const content = readFileSync(configFile(), \"utf-8\");\n    const match = content.match(/^claude_extra_args:\\s*(.+)$/m);\n    const value = match?.[1]?.trim();\n    return value && value.length > 0 ? value : undefined;\n  } catch {\n    // File doesn't exist or not readable\n  }\n  return undefined;\n}\n\n/**\n * Throws on an embedded newline rather than silently sanitizing: a newline would\n * corrupt the flat one-line-per-key file, and the caller (the HTTP layer) should\n * surface that to the user as a validation error instead of quietly rewriting\n * what they typed.\n */\nexport function setClaudeExtraArgs(text: string | undefined): void {\n  const trimmed = text?.trim();\n  if (trimmed && /[\\r\\n]/.test(trimmed)) {\n    throw new Error(\"claude_extra_args must not contain newlines\");\n  }\n  setConfigValue(\"claude_extra_args\", trimmed && trimmed.length > 0 ? trimmed : undefined);\n}\n\n/**\n * Server feature flags, stored as ONE line of JSON — same encoding and the same\n * reasons as claude_flags above:\n *\n *     feature_flags: {\"codexSystemPrompt\":true}\n *\n * Keys must match `FEATURE_FLAGS` object keys (`ptyHost`), not env var names\n * (`THREADBASE_FEATURE_PTY_HOST`). Unknown keys are dropped.\n *\n * Read-only from this module's perspective: nothing writes this key, because\n * flags resolve at boot and there is no runtime-mutation endpoint. Add a\n * setFeatureFlags() alongside a PUT, if one is ever added.\n *\n * A malformed line yields {} plus a warning rather than throwing, so a typo in a\n * hand-edited server.yaml costs the flag, not the boot.\n */\n/**\n * Cloudflare Access service-token credentials, for the boot probe in\n * `access-probe.ts`.\n *\n * One line, JSON, so it reads the same way `feature_flags:` does:\n *\n *     access_service_token: {\"client_id\":\"…\",\"client_secret\":\"…\"}\n *\n * A credential in server.yaml is a real consideration, and the file is already\n * `chmod 600` because it holds the API key. This is the operator's own token for\n * their own edge, it is useless without the hostname it belongs to, and the\n * alternative — the streamer holding it in memory only — cannot survive the\n * restart that the probe runs on.\n *\n * A malformed line costs the probe's second half, not the boot: the gate is\n * still detected and reported, just without the \"does my token satisfy it\"\n * answer.\n */\nexport function loadAccessServiceToken(): AccessServiceToken | undefined {\n  try {\n    const content = readFileSync(configFile(), \"utf-8\");\n    const match = content.match(/^access_service_token:\\s*(.+)$/m);\n    if (!match?.[1]) return undefined;\n    const parsed = JSON.parse(match[1].trim()) as Record<string, unknown>;\n    const clientId = parsed.client_id;\n    const clientSecret = parsed.client_secret;\n    if (typeof clientId !== \"string\" || typeof clientSecret !== \"string\") return undefined;\n    if (!clientId || !clientSecret) return undefined;\n    return { clientId, clientSecret };\n  } catch (err) {\n    if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n      // The value is never logged, only the fact that it could not be read.\n      getLogger(\"auth\").warn(\"Ignoring unreadable access_service_token in server.yaml\", {\n        event: \"config.access_service_token_parse_failed\",\n      });\n    }\n    return undefined;\n  }\n}\n\nexport function loadFeatureFlags(): FeatureFlagValues {\n  try {\n    const content = readFileSync(configFile(), \"utf-8\");\n    const match = content.match(/^feature_flags:\\s*(.+)$/m);\n    if (!match?.[1]) return {};\n    return validateFeatureFlagValues(JSON.parse(match[1].trim()));\n  } catch (err) {\n    if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n      getLogger(\"auth\").warn(`Ignoring unreadable feature_flags in server.yaml: ${String(err)}`, {\n        event: \"config.feature_flags_parse_failed\",\n      });\n    }\n    return {};\n  }\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","// Per-server Claude Code CLI flags: the allowlist registry, validation, and\n// argv construction.\n//\n// The registry is SERVER-owned and shipped to clients (mobile) over\n// `GET /api/config/claude-flags`. Mobile must not hardcode its own copy: only\n// the streamer knows which `claude` binary is installed locally, so a client-side\n// copy drifts the moment the CLI is upgraded — and the failure is silent (the\n// phone offers a flag the local CLI rejects, and the PTY dies at spawn with a\n// stderr the mobile UI never surfaces).\n//\n// `id` is deliberately NOT the CLI spelling. It is the stable key used in\n// server.yaml and over the wire, so a future CLI rename is a one-line change\n// here instead of a config migration.\n\n/** Claude Code `--permission-mode` values, as accepted by CLI v2.1.x. */\nexport const PERMISSION_MODES = [\n  \"acceptEdits\",\n  \"auto\",\n  \"bypassPermissions\",\n  \"manual\",\n  \"dontAsk\",\n  \"plan\",\n] as const;\n\nexport type PermissionMode = (typeof PERMISSION_MODES)[number];\n\nexport function isPermissionMode(value: unknown): value is PermissionMode {\n  return typeof value === \"string\" && (PERMISSION_MODES as readonly string[]).includes(value);\n}\n\n// Modes that disable the human-in-the-loop confirmation entirely. Two\n// consequences, both load-bearing:\n//   1. buildSettingsJson() adds skipDangerousModePermissionPrompt so the\n//      blocking \"Bypass Permissions mode\" boot gate never strands the PTY.\n//   2. Clients render an explicit confirmation before enabling one.\nexport const DANGEROUS_PERMISSION_MODES: readonly PermissionMode[] = [\n  \"bypassPermissions\",\n  \"dontAsk\",\n];\n\nexport function isDangerousPermissionMode(mode: PermissionMode): boolean {\n  return DANGEROUS_PERMISSION_MODES.includes(mode);\n}\n\n/**\n * The mode sessions will actually spawn with, for callers that need to know\n * before any spawn happens (the boot warning). Mirrors the precedence\n * `StreamerServer.spawnFlagOverrides()` applies — `claudeFlags.permissionMode`\n * wins over the `--default-permission-mode` fallback — so the two must not\n * drift: a warning that reads the fallback would stay silent while a\n * `claude_flags:` bypass is what actually reaches argv.\n */\nexport function effectivePermissionMode(\n  flags: ClaudeFlagValues | undefined,\n  fallback: string | undefined,\n): PermissionMode | undefined {\n  const candidate = flags?.permissionMode ?? fallback;\n  return isPermissionMode(candidate) ? candidate : undefined;\n}\n\n/** Claude Code `--effort` levels, as accepted by CLI v2.1.x. */\nexport const EFFORT_LEVELS = [\"low\", \"medium\", \"high\", \"xhigh\", \"max\"] as const;\n\nexport type EffortLevel = (typeof EFFORT_LEVELS)[number];\n\nexport function isEffortLevel(value: unknown): value is EffortLevel {\n  return typeof value === \"string\" && (EFFORT_LEVELS as readonly string[]).includes(value);\n}\n\nexport type FlagValueType = \"boolean\" | \"string\" | \"enum\" | \"list\";\n\n/** How risky enabling a flag is. Drives the client's confirmation UX. */\nexport type FlagRisk = \"low\" | \"elevated\" | \"dangerous\";\n\nexport interface FlagDefinition {\n  /** Stable config/wire key. Never the CLI spelling. */\n  id: string;\n  /** The literal CLI token, e.g. \"--add-dir\". */\n  flag: string;\n  valueType: FlagValueType;\n  /** Allowed values when valueType === \"enum\". */\n  enumValues?: readonly string[];\n  /**\n   * Baseline risk. `permissionMode` is the exception: it is only dangerous for\n   * the values in DANGEROUS_PERMISSION_MODES, so clients must call\n   * `flagValueRisk()` rather than reading this field directly.\n   */\n  risk: FlagRisk;\n}\n\nexport type ClaudeFlagValue = string | string[] | boolean;\nexport type ClaudeFlagValues = Record<string, ClaudeFlagValue>;\n\n// v1 allowlist. Every argv form here was verified against Claude Code v2.1.218.\n//\n// Deliberately excluded:\n//   --append-system-prompt  collides with the existing defaultSystemPrompt /\n//                           options.systemPrompt path; two writers of the system\n//                           prompt is a bug waiting to happen.\n//   --allow-dangerously-skip-permissions\n//                           a strictly weaker duplicate of\n//                           `--permission-mode bypassPermissions`; two paths to\n//                           one outcome is worse than one.\n//   --bare, --agent         no demonstrated need.\n//   --max-budget-usd, --fallback-model\n//                           BOTH ARE \"(only works with --print)\" per `claude\n//                           --help`, and this server never passes --print — it\n//                           spawns interactive PTY sessions. They were in the v1\n//                           allowlist and were silent no-ops there: validated,\n//                           persisted to server.yaml, emitted onto argv, and\n//                           ignored by the CLI. --max-budget-usd was the worse\n//                           of the two, because it was documented as the\n//                           runaway-spend bound for bypass mode — a cap that\n//                           never existed. Do not re-add either without first\n//                           confirming the flag applies without --print.\n// All remain reachable through the free-text extra-args escape hatch.\nexport const CLAUDE_FLAGS: readonly FlagDefinition[] = [\n  {\n    id: \"permissionMode\",\n    flag: \"--permission-mode\",\n    valueType: \"enum\",\n    enumValues: PERMISSION_MODES,\n    risk: \"low\",\n  },\n  { id: \"addDir\", flag: \"--add-dir\", valueType: \"list\", risk: \"elevated\" },\n  { id: \"allowedTools\", flag: \"--allowedTools\", valueType: \"list\", risk: \"elevated\" },\n  { id: \"disallowedTools\", flag: \"--disallowedTools\", valueType: \"list\", risk: \"low\" },\n  { id: \"model\", flag: \"--model\", valueType: \"string\", risk: \"low\" },\n  { id: \"effort\", flag: \"--effort\", valueType: \"enum\", enumValues: EFFORT_LEVELS, risk: \"low\" },\n];\n\n// Ids the spawn paths emit as explicit positionals rather than letting\n// buildFlagArgs append them — see PTYManager.doStart/startFresh, which always\n// pass `--permission-mode`, `--model` and `--effort`. Emitting them here too\n// would put a duplicate flag on the command line.\nconst SPAWN_POSITIONAL_FLAG_IDS = new Set([\"permissionMode\", \"model\", \"effort\"]);\n\nexport function findFlag(id: string): FlagDefinition | undefined {\n  return CLAUDE_FLAGS.find((f) => f.id === id);\n}\n\n/**\n * Effective risk of a specific value. Only `permissionMode` is value-dependent\n * (`acceptEdits` is routine; `bypassPermissions` is not).\n */\nexport function flagValueRisk(id: string, value: ClaudeFlagValue): FlagRisk {\n  const def = findFlag(id);\n  if (!def) return \"low\";\n  if (def.id === \"permissionMode\") {\n    return isPermissionMode(value) && isDangerousPermissionMode(value) ? \"dangerous\" : \"low\";\n  }\n  return def.risk;\n}\n\n/**\n * Drop everything that isn't a known id carrying a well-typed value.\n *\n * This is a TRUST BOUNDARY, not a convenience: values arrive from a\n * user-editable server.yaml and from HTTP, and they become process argv. Never\n * skip it. Unknown/invalid entries are dropped rather than throwing so one bad\n * key can't prevent the server from booting.\n */\nexport function validateFlagValues(raw: unknown): ClaudeFlagValues {\n  if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return {};\n  const out: ClaudeFlagValues = {};\n  for (const [id, value] of Object.entries(raw as Record<string, unknown>)) {\n    const def = findFlag(id);\n    if (!def) continue;\n    switch (def.valueType) {\n      case \"boolean\":\n        if (typeof value === \"boolean\") out[id] = value;\n        break;\n      case \"enum\":\n        if (typeof value === \"string\" && def.enumValues?.includes(value)) out[id] = value;\n        break;\n      case \"string\":\n        if (typeof value === \"string\" && value.trim().length > 0) out[id] = value.trim();\n        break;\n      case \"list\": {\n        if (!Array.isArray(value)) break;\n        const items = value\n          .filter((v): v is string => typeof v === \"string\" && v.trim().length > 0)\n          .map((v) => v.trim());\n        if (items.length > 0) out[id] = items;\n        break;\n      }\n    }\n  }\n  return out;\n}\n\n/**\n * Split a free-text extra-args string into argv tokens, honouring single and\n * double quotes so a path with spaces survives. Deliberately not a full shell\n * parser — no variable expansion, no globbing, no escapes beyond the quotes.\n */\nexport function tokenizeExtraArgs(input: string | undefined): string[] {\n  if (!input) return [];\n  const tokens: string[] = [];\n  let current = \"\";\n  let quote: '\"' | \"'\" | null = null;\n  let started = false;\n\n  for (const ch of input) {\n    if (quote) {\n      if (ch === quote) quote = null;\n      else current += ch;\n      continue;\n    }\n    if (ch === '\"' || ch === \"'\") {\n      quote = ch;\n      started = true;\n      continue;\n    }\n    if (/\\s/.test(ch)) {\n      if (started) {\n        tokens.push(current);\n        current = \"\";\n        started = false;\n      }\n      continue;\n    }\n    current += ch;\n    started = true;\n  }\n  if (started) tokens.push(current);\n  return tokens;\n}\n\n/**\n * Turn validated flag values + extra args into argv tokens.\n *\n * The SPAWN_POSITIONAL_FLAG_IDS are intentionally NOT emitted here: both PTY\n * spawn paths already pass them as explicit positionals, and emitting them\n * twice would put a duplicate flag on the command line. They flow through\n * `options.permissionMode` / `options.model` / `options.effort` instead, which\n * StreamerServer.spawnFlagOverrides() sources from these same flag values.\n *\n * Extra args land LAST so the escape hatch can override anything the allowlist\n * set.\n */\nexport function buildFlagArgs(values: ClaudeFlagValues | undefined, extraArgs?: string): string[] {\n  const args: string[] = [];\n  const safe = validateFlagValues(values ?? {});\n\n  for (const def of CLAUDE_FLAGS) {\n    if (SPAWN_POSITIONAL_FLAG_IDS.has(def.id)) continue;\n    const value = safe[def.id];\n    if (value === undefined) continue;\n    if (def.valueType === \"boolean\") {\n      if (value === true) args.push(def.flag);\n      continue;\n    }\n    if (Array.isArray(value)) {\n      // Variadic form (`--allowedTools A B`) — verified against v2.1.218.\n      args.push(def.flag, ...value);\n      continue;\n    }\n    args.push(def.flag, String(value));\n  }\n\n  args.push(...tokenizeExtraArgs(extraArgs));\n  return args;\n}\n\n// The `--settings` JSON every PTY session is spawned with.\n//\n// `skipDangerousModePermissionPrompt` is the fix for the blocking\n// \"Bypass Permissions mode\" warning menu (\"1. No, exit\" / \"2. Yes, I accept\").\n// Without it a bypass-mode session stalls on that gate forever and mobile shows\n// an empty screen. Probe-verified on Claude Code v2.1.218: with the key set the\n// session boots straight to a usable prompt. Only added for the modes that can\n// actually trigger the gate, so a normal session's settings blob is unchanged.\nexport function buildSettingsJson(permissionMode: PermissionMode): string {\n  const settings: Record<string, unknown> = { spinnerTipsEnabled: false };\n  if (isDangerousPermissionMode(permissionMode)) {\n    settings.skipDangerousModePermissionPrompt = true;\n  }\n  return JSON.stringify(settings);\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\n/**\n * Where a line goes when the caller doesn't pass an explicit `dest`.\n *\n * Under a supervisor (launchd, systemd, Task Scheduler, Docker) fd 1 is a file\n * or a pipe. There the console half of the old `\"both\"` default was an\n * unstructured duplicate of the pino line — and because `console` has no level\n * of its own, it also printed every `debug` call pino had already filtered out.\n * Those two effects were ~40% of a 261 MB stdout.log, the single loudest line\n * being a `debug` one that could not be turned off by any setting.\n *\n * At a human terminal the tradeoff inverts: the pretty line is the useful one\n * and the JSON is the noise. An explicit `dest` still overrides both cases,\n * which is what the CLI's user-facing output (banners, QR, `prod doctor`) uses.\n */\nfunction defaultDest(): LogDest {\n  return process.stdout.isTTY ? \"console\" : \"pino\";\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 = defaultDest()) => emit(pinoChild, \"debug\", m, f, d),\n    info: (m, f, d = defaultDest()) => emit(pinoChild, \"info\", m, f, d),\n    warn: (m, f, d = defaultDest()) => emit(pinoChild, \"warn\", m, f, d),\n    error: (m, f, d = defaultDest()) => emit(pinoChild, \"error\", m, f, d),\n    log: (lvl, m, f, d = defaultDest()) => 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","// Server feature flags: the registry, the config sources, and boot-time resolution.\n//\n// A feature flag gates behaviour we are not ready to make unconditional — an\n// experiment, a provider quirk, a migration half-step. It is deliberately NOT\n// the same mechanism as src/claude-flags.ts: those are CLI arguments handed to\n// a spawned `claude` process, these change how the STREAMER itself behaves.\n//\n// Two rules make this safe to expose to a hand-editable server.yaml:\n//   1. Booleans only. A flag is on or off; anything richer belongs in a real\n//      config field with its own validation.\n//   2. Resolution happens ONCE, at boot. There is no runtime mutation and no\n//      PUT endpoint — a flag change means a restart, same as every other\n//      startup-resolved setting (ptyGracePeriodMs, cacheDir, …). That keeps a\n//      flag's value stable for the lifetime of a process, so no code has to\n//      reason about it changing underneath a live session.\n\nimport { getLogger } from \"./logger\";\n\n/** Spec for one flag in the keyed registry. The id is the object key, not a field. */\nexport interface FeatureFlagSpec {\n  /** Shipped to clients alongside the values so a UI can render it. */\n  description: string;\n  default: boolean;\n  /** Full env var name. */\n  env: string;\n}\n\n/** Wire shape: the keyed spec plus its id, for GET /api/config/feature-flags. */\nexport interface FeatureFlagDefinition extends FeatureFlagSpec {\n  /** Stable config/wire key. Used in server.yaml, on the CLI, and over HTTP. */\n  id: FeatureFlagId;\n}\n\n/**\n * Values arriving from a config source. PARTIAL on purpose: server.yaml, the\n * CLI and ServerConfig each speak about the flags they mention and stay silent\n * on the rest, which is what lets the precedence chain fall through.\n */\nexport type FeatureFlagValues = Partial<Record<FeatureFlagId, boolean>>;\n\n/** Every flag, resolved. Total — see resolveFeatureFlags(). */\nexport type ResolvedFeatureFlags = Record<FeatureFlagId, boolean>;\n\n/**\n * Which rung of the precedence chain decided a flag.\n *\n * Kept because the resolved boolean alone cannot answer \"why is this on?\", and\n * that is the first question asked of a flag that is on when nobody expected it.\n * `override` is the legacy ServerConfig field (see resolveFeatureFlags).\n */\nexport type FeatureFlagSource = \"override\" | \"env\" | \"cli\" | \"yaml\" | \"default\";\n\nexport interface FeatureFlagResolution {\n  values: ResolvedFeatureFlags;\n  sources: Record<FeatureFlagId, FeatureFlagSource>;\n}\n\n/**\n * The registry, keyed by flag name.\n *\n * Prefer `FEATURE_FLAGS.ptyHost` over a string lookup. A typo is a compile\n * error; `findFeatureFlag(\"…\")` is only for untrusted yaml/CLI tokens.\n *\n * `as const` + `keyof` is what makes `flags.ptyHsot` a compile error instead of\n * `undefined`. A TS enum would add a runtime object without a stronger type.\n */\nexport const FEATURE_FLAGS = {\n  subagentSessions: {\n    description:\n      \"Include provider-created Claude Code and Codex child sessions in history and session APIs.\",\n    default: false,\n    env: \"THREADBASE_FEATURE_SUBAGENT_SESSIONS\",\n  },\n  codexSystemPrompt: {\n    description:\n      \"Send the built system prompt to fresh Codex sessions. Off by default: Codex has no \" +\n      \"--system-prompt flag, so the prompt goes in the positional [PROMPT] argument, which \" +\n      \"Codex treats as the user's opening turn rather than a system-level instruction.\",\n    default: false,\n    env: \"THREADBASE_FEATURE_CODEX_SYSTEM_PROMPT\",\n  },\n  sessionRehydration: {\n    description:\n      \"Seed the session list at boot with sessions a previous streamer run left behind, so a \" +\n      \"restart leaves them one tap from resuming instead of silently gone. On by default, with \" +\n      \"a kill switch: it changes what GET /api/sessions contains.\",\n    default: true,\n    env: \"THREADBASE_FEATURE_SESSION_REHYDRATION\",\n  },\n  liveActivityPush: {\n    description:\n      \"Drive iOS Live Activity surfaces for running sessions. Off by default: the streamer half \" +\n      \"needs an APNs p8 and a registered push-to-start token, and without both, mobile falls back \" +\n      \"to starting the activity locally — which freezes the moment the app backgrounds and \" +\n      \"expires silently after ~8h. Mobile reads this flag and skips its local path too, so one \" +\n      \"switch turns the whole surface off.\",\n    default: false,\n    env: \"THREADBASE_FEATURE_LIVE_ACTIVITY_PUSH\",\n  },\n  e2ee: {\n    description:\n      \"Application-layer encryption between a paired device and this server, independent of TLS, \" +\n      \"so a tunnel or a LAN observer on the path carries ciphertext. On by default (stage 2): it \" +\n      \"is negotiated per device, never forced, because released mobile builds cannot be \" +\n      \"force-updated and a server that demanded it would break every one of them. `required` \" +\n      \"(stage 3, refusing plaintext from any client) is a separate, still-unmade decision.\",\n    default: true,\n    env: \"THREADBASE_FEATURE_E2EE\",\n  },\n  accessProbe: {\n    description:\n      \"At boot, ask this server's own public URL what an unauthenticated device would get, and warn \" +\n      \"if an edge gate (Cloudflare Access) answers instead of the server. On by default because the \" +\n      \"failure it catches is silent and expensive: a sealed request carries no Authorization header, \" +\n      \"so an interactive Access application refuses it before the tunnel and the device reports that \" +\n      \"pairing failed — blaming the server, which never saw the request. One probe, one line, never a \" +\n      \"retry and never a refusal to start.\",\n    default: true,\n    env: \"THREADBASE_FEATURE_ACCESS_PROBE\",\n  },\n  ptyHost: {\n    description:\n      \"Keep live PTYs in a separate host process so a streamer restart can reconnect without \" +\n      \"restarting the agents. Off by default until cross-platform behavior is qualified.\",\n    default: false,\n    env: \"THREADBASE_FEATURE_PTY_HOST\",\n  },\n} as const satisfies Record<string, FeatureFlagSpec>;\n\n/**\n * The registry's ids as a union, derived rather than declared.\n *\n * This is what makes `flags.ptyHsot` a compile error instead of `undefined`.\n * Under the old `Record<string, boolean>` an index signature accepted any key,\n * so a typo in a consumer read as falsy and silently disabled the feature it\n * was meant to gate — the exact failure the \"total map\" contract exists to\n * prevent, reachable through the one door that contract left open.\n */\nexport type FeatureFlagId = keyof typeof FEATURE_FLAGS;\n\n/**\n * The yaml / `--feature` ids, in registry order.\n *\n * These are the `FEATURE_FLAGS` object keys — not the `THREADBASE_FEATURE_*`\n * env names. `feature_flags: {\"ptyHost\":true}` is valid;\n * `{\"THREADBASE_FEATURE_PTY_HOST\":true}` is dropped as unknown.\n */\nexport const FEATURE_FLAG_IDS = Object.keys(FEATURE_FLAGS) as FeatureFlagId[];\n\n/**\n * Ordered list for iteration and the HTTP registry.\n *\n * The wire shape stays an array of `{ id, description, default, env }` so a\n * keyed in-process registry is not a breaking GET /api/config/feature-flags\n * change. Each `id` is the matching `FEATURE_FLAGS` key.\n */\nexport const FEATURE_FLAG_LIST: readonly FeatureFlagDefinition[] = FEATURE_FLAG_IDS.map((id) => ({\n  id,\n  ...FEATURE_FLAGS[id],\n}));\n\nexport function isFeatureFlagId(id: string): id is FeatureFlagId {\n  return Object.hasOwn(FEATURE_FLAGS, id);\n}\n\n/** Typed lookup. The flag is always present; unknown names do not type-check. */\nexport function getFeatureFlag<K extends FeatureFlagId>(id: K) {\n  return { id, ...FEATURE_FLAGS[id] };\n}\n\n/**\n * Look a flag up by an unvalidated string (yaml keys, `--feature` tokens).\n *\n * In-process code should use `FEATURE_FLAGS.ptyHost` or `getFeatureFlag(\"ptyHost\")`.\n */\nexport function findFeatureFlag(id: string): FeatureFlagDefinition | undefined {\n  if (!isFeatureFlagId(id)) return undefined;\n  return getFeatureFlag(id);\n}\n\n/**\n * Parse a boolean env var, tri-state.\n *\n * `undefined` means \"this variable did not speak\" — distinct from `false` — so\n * an unset var lets the next precedence rung (CLI, then yaml) decide instead of\n * silently forcing the flag off.\n *\n * That tri-state return is why this does not reuse one of the existing env\n * parsers (`parseIncludeAgentsEnv` in server.ts, `isTruthy` in\n * agent/agent-config.ts, the inline sets in api/middleware/cors.middleware.ts):\n * all three collapse \"absent\" into a boolean, which is exactly the distinction\n * the precedence chain needs. Consolidating those three is a separate change.\n */\nexport function parseBooleanEnv(raw: string | undefined): boolean | undefined {\n  if (raw === undefined) return undefined;\n  const v = raw.trim().toLowerCase();\n  if (v === \"\") return false;\n  return !(v === \"0\" || v === \"false\" || v === \"no\" || v === \"off\");\n}\n\n/**\n * Drop everything that isn't a known id carrying a boolean.\n *\n * A TRUST BOUNDARY, mirroring validateFlagValues() in claude-flags.ts: values\n * arrive from a user-editable server.yaml. Unknown ids and ill-typed values are\n * dropped with a warning rather than throwing, so one stale or fat-fingered key\n * can never stop the server from booting.\n *\n * Note the deliberate non-coercion: `\"true\"` (a string) is dropped, not read as\n * true. A value that isn't already a boolean means the writer misunderstood the\n * format, and guessing at intent is how a flag silently ends up on.\n */\nexport function validateFeatureFlagValues(raw: unknown): FeatureFlagValues {\n  if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return {};\n  const out: FeatureFlagValues = {};\n  const dropped: string[] = [];\n  for (const [id, value] of Object.entries(raw as Record<string, unknown>)) {\n    const def = findFeatureFlag(id);\n    if (!def || typeof value !== \"boolean\") {\n      dropped.push(id);\n      continue;\n    }\n    out[def.id] = value;\n  }\n  if (dropped.length > 0) {\n    getLogger(\"feature-flags\").warn(\n      `Ignoring unknown or non-boolean feature flags: ${dropped.join(\", \")}. ` +\n        `Known ids (FEATURE_FLAGS keys, not env names): ${FEATURE_FLAG_IDS.join(\", \")}`,\n      {\n        event: \"config.feature_flags_dropped\",\n        dropped,\n        known: FEATURE_FLAG_IDS,\n      },\n    );\n  }\n  return out;\n}\n\n/**\n * Parse repeatable `--feature <id=bool>` CLI tokens.\n *\n * Errors are returned rather than thrown so the caller owns the exit: a typo on\n * the command line should print one legible message and stop, not surface a\n * stack trace. Unlike the yaml path this is strict — a CLI typo is a mistake the\n * operator is standing right there to fix, whereas a stale yaml key must not\n * block an unattended boot.\n */\nexport function parseFeatureFlagArgs(entries: string[]): {\n  values: FeatureFlagValues;\n  errors: string[];\n} {\n  const values: FeatureFlagValues = {};\n  const errors: string[] = [];\n\n  for (const entry of entries) {\n    const eq = entry.indexOf(\"=\");\n    const id = (eq === -1 ? entry : entry.slice(0, eq)).trim();\n    // A bare `--feature someFlag` reads as \"turn it on\", matching how bare\n    // boolean flags behave everywhere else.\n    const rawValue =\n      eq === -1\n        ? \"true\"\n        : entry\n            .slice(eq + 1)\n            .trim()\n            .toLowerCase();\n\n    const def = findFeatureFlag(id);\n    if (!def) {\n      errors.push(\n        `Unknown feature flag \"${id}\". Known flags (FEATURE_FLAGS keys, not env names): ${FEATURE_FLAG_IDS.join(\", \")}`,\n      );\n      continue;\n    }\n    if (rawValue !== \"true\" && rawValue !== \"false\") {\n      errors.push(`Invalid value \"${rawValue}\" for feature flag \"${id}\" — expected true/false`);\n      continue;\n    }\n    values[def.id] = rawValue === \"true\";\n  }\n\n  return { values, errors };\n}\n\n/**\n * Resolve every flag once, at boot. Precedence, highest first:\n *\n *   override  →  env  →  CLI  →  server.yaml  →  registry default\n *\n * Env beats the CLI so an operator can flip a flag on a supervised instance\n * (launchd/systemd/Task Scheduler) whose argv is fixed — the same reason\n * THREADBASE_ALLOW_BROWSER_CORS overrides browser_cors: in server.yaml.\n *\n * `override` is the legacy explicit ServerConfig field (codexSystemPromptEnabled),\n * kept so embedders and tests that set it directly keep working. It is a real\n * rung here rather than a mutation applied to the finished map afterwards: the\n * old shape meant the resolver was not actually the single source of truth, and\n * an override was invisible to anything reporting where a value came from.\n *\n * The returned `values` map is TOTAL: every registry id is present, defaults\n * filled in. Callers therefore index it without a `?? default`, and a flag added\n * later cannot reach a boolean branch as `undefined` just because an older\n * server.yaml predates it. `sources` is total for the same reason.\n */\nexport function resolveFeatureFlags(opts?: {\n  override?: FeatureFlagValues;\n  cli?: FeatureFlagValues;\n  yaml?: FeatureFlagValues;\n  env?: NodeJS.ProcessEnv;\n}): FeatureFlagResolution {\n  const env = opts?.env ?? process.env;\n  const values = {} as ResolvedFeatureFlags;\n  const sources = {} as Record<FeatureFlagId, FeatureFlagSource>;\n\n  for (const def of FEATURE_FLAG_LIST) {\n    // Written as an ordered list rather than a `??` chain because the chain\n    // discards which rung won, and that is the answer to the only question\n    // anyone asks of a surprising flag.\n    const rungs: ReadonlyArray<[FeatureFlagSource, boolean | undefined]> = [\n      [\"override\", opts?.override?.[def.id]],\n      [\"env\", parseBooleanEnv(env[def.env])],\n      [\"cli\", opts?.cli?.[def.id]],\n      [\"yaml\", opts?.yaml?.[def.id]],\n    ];\n    const won = rungs.find(([, v]) => v !== undefined);\n    values[def.id] = won ? (won[1] as boolean) : def.default;\n    sources[def.id] = won ? won[0] : \"default\";\n  }\n\n  return { values, sources };\n}\n\n/** Registry ids whose resolved value differs from the registry default. */\nexport function nonDefaultFeatureFlags(values: ResolvedFeatureFlags): FeatureFlagId[] {\n  return FEATURE_FLAG_LIST.filter((f) => values[f.id] !== f.default).map((f) => f.id);\n}\n\n/**\n * One line describing the whole resolution: `id=value(source)`, every flag.\n *\n * Replaces a boot log that printed only the ids differing from their defaults\n * under the heading \"Feature flags active\". That heading was wrong for any\n * flag whose default is ON — disabling `sessionRehydration` made it appear in\n * a list of active flags, stating the opposite of what had happened. Printing\n * the value removes the ambiguity, and printing every flag means the log\n * answers \"what was this process running with\" instead of only ever hinting.\n */\nexport function describeFeatureFlags(resolution: FeatureFlagResolution): string {\n  return FEATURE_FLAG_LIST.map(\n    (f) => `${f.id}=${resolution.values[f.id]}(${resolution.sources[f.id]})`,\n  ).join(\" \");\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 { CursorPtyRunner } from \"./cursor-pty-runner\";\nimport { locateProviderExe } from \"./platform\";\nimport {\n  CLAUDE_CODE_PROVIDER,\n  CODEX_CLI_PROVIDER,\n  CURSOR_PROVIDER,\n  commandNameForProvider,\n  type ProviderName,\n} from \"./providers\";\nimport type { HostHeartbeatState, HostTransport } from \"./pty-host/protocol\";\nimport { RemoteSessionRunner } from \"./pty-host/remote-session-runner\";\nimport { PTYManager } from \"./pty-manager\";\nimport type {\n  ManagedSession,\n  PTYManagerOptions,\n  SessionRunner,\n  StartForkSessionOptions,\n  StartFreshSessionOptions,\n  StartSessionOptions,\n  UserMessage,\n} from \"./types\";\n\nexport class LiveSessionManager {\n  private runners: Map<ProviderName, SessionRunner>;\n  private remoteRunner: RemoteSessionRunner | null = null;\n  private options: PTYManagerOptions;\n\n  constructor(options: PTYManagerOptions = {}) {\n    this.options = options;\n    this.runners = new Map<ProviderName, SessionRunner>([\n      [CLAUDE_CODE_PROVIDER, new PTYManager(options)],\n      [CODEX_CLI_PROVIDER, new CodexPtyRunner(options)],\n      [CURSOR_PROVIDER, new CursorPtyRunner(options)],\n    ]);\n  }\n\n  async useRemoteRunner(transport: HostTransport): Promise<ManagedSession[]> {\n    const remote = await RemoteSessionRunner.connect(transport, this.options);\n    await Promise.all(\n      remote.listSessions().map((session) => remote.hydrateInputHistory(session.id)),\n    );\n    for (const runner of this.runners.values()) runner.dispose();\n    this.remoteRunner = remote;\n    return remote.listSessions();\n  }\n\n  isRemote(): boolean {\n    return this.remoteRunner !== null;\n  }\n\n  startRemoteHeartbeat(getState: () => HostHeartbeatState): void {\n    this.remoteRunner?.startHeartbeat(getState);\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    this.assertProviderInstalled(provider);\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    this.assertProviderInstalled(provider);\n    return runner.startFresh(options);\n  }\n\n  /**\n   * Fork an existing conversation into a new session. Codex-only: `codex fork`\n   * has no Claude Code equivalent, and there is no safe generic fallback — a\n   * silent downgrade to resume would attach to the very writer the caller is\n   * trying to leave alone.\n   */\n  async startFork(\n    options: StartForkSessionOptions & { provider?: ProviderName },\n  ): Promise<ManagedSession> {\n    const provider = options.provider ?? CODEX_CLI_PROVIDER;\n    const runner = this.remoteRunner ?? this.runners.get(provider);\n    if (!(runner instanceof CodexPtyRunner)) {\n      const err = new Error(\n        this.remoteRunner\n          ? \"Forking is not supported while sessions are hosted by the pty-host\"\n          : `Forking is not supported for ${provider} sessions`,\n      );\n      (err as Error & { statusCode?: number }).statusCode = 501;\n      throw err;\n    }\n    this.assertProviderInstalled(provider);\n    return runner.startFork(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  sendRawKeys(sessionId: string, keys: string): void {\n    this.runnerFor(sessionId).sendRawKeys(sessionId, keys);\n  }\n\n  resize(sessionId: string, cols: number, rows: number): void {\n    this.runnerFor(sessionId).resize(sessionId, cols, rows);\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.activeRunners()) {\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, signal: NodeJS.Signals = \"SIGINT\"): void {\n    for (const runner of this.activeRunners()) {\n      if (runner.hasSession(sessionId) || runner.getSession(sessionId)) {\n        runner.putOnHold(sessionId, signal);\n        return;\n      }\n    }\n    for (const runner of this.activeRunners()) {\n      runner.putOnHold(sessionId, signal);\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.activeRunners()) {\n      const session = runner.getSession(sessionId);\n      if (session) return session;\n    }\n    return null;\n  }\n\n  // Scans rather than using runnerFor(): the registry records a pid on a\n  // best-effort basis, so an unknown session must return null rather than\n  // throw the way the input-routing methods do.\n  getPid(sessionId: string): number | null {\n    for (const runner of this.activeRunners()) {\n      const pid = runner.getPid(sessionId);\n      if (pid != null) return pid;\n    }\n    return null;\n  }\n\n  hasSession(sessionId: string): boolean {\n    for (const runner of this.activeRunners()) {\n      if (runner.hasSession(sessionId)) return true;\n    }\n    return false;\n  }\n\n  listSessions(): ManagedSession[] {\n    return this.activeRunners().flatMap((runner) => runner.listSessions());\n  }\n\n  dispose(): void {\n    for (const runner of this.activeRunners()) {\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.activeRunners()) {\n      if (runner.hasSession(sessionId) || runner.getSession(sessionId)) return runner;\n    }\n    throw new Error(`Session not found: ${sessionId}`);\n  }\n\n  /**\n   * Refuse before spawning when the provider's CLI is not on this machine.\n   *\n   * Without this the spawn \"succeeds\": on POSIX execvp fails inside the forked\n   * child, so a session appears, exits ~12ms later with code 1 and no output,\n   * and the caller is told only that it \"exited before becoming ready\" — or,\n   * on the Claude resume path, is told nothing at all, since that path answers\n   * 200 before the process has had a chance to die. Every start route funnels\n   * through here, so one check covers start, resume, adopt and fork.\n   *\n   * 503, not 500: the request was well-formed and the fault is this machine's\n   * environment. `code` is what mobile branches on (it reads `errBody.code`),\n   * and `PROVIDER_NOT_INSTALLED` is a remediation string it already knows.\n   */\n  private assertProviderInstalled(provider: ProviderName): void {\n    if (locateProviderExe(provider) !== null) return;\n    // Written to be read by a person, not parsed: mobile shows `error` verbatim\n    // in its \"Failed to start\" alert, so it names the command to install and\n    // the other thing that produces this — a CLI the streamer's PATH cannot\n    // see, which is the usual shape under launchd/Task Scheduler.\n    const command = commandNameForProvider(provider);\n    const err = new Error(\n      `The ${command} command was not found on this server. Install the ${provider} CLI, ` +\n        \"or make sure it is on the PATH the streamer runs with.\",\n    );\n    (err as Error & { statusCode?: number; code?: string }).statusCode = 503;\n    (err as Error & { statusCode?: number; code?: string }).code = \"PROVIDER_NOT_INSTALLED\";\n    throw err;\n  }\n\n  private assertSupportedProvider(provider: ProviderName, projectPath: string): SessionRunner {\n    if (this.remoteRunner) return this.remoteRunner;\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  private activeRunners(): SessionRunner[] {\n    return this.remoteRunner ? [this.remoteRunner] : [...this.runners.values()];\n  }\n}\n","import { randomUUID } from \"crypto\";\nimport { existsSync } from \"fs\";\nimport { basename } from \"path\";\nimport { getLogger, type Logger } from \"./logger\";\nimport { clearCodexExeCache, resolveCodexExe } from \"./platform\";\nimport { CODEX_CLI_PROVIDER } from \"./providers\";\nimport {\n  createScreen,\n  type InternalSession,\n  loadPty,\n  PTY_COLS,\n  PTY_ROWS,\n  stripAnsi,\n} from \"./pty-shared\";\nimport {\n  type CodexGateType,\n  rememberedGateDigit,\n  saveGateAnswer,\n} from \"./services/questions/codexGateAnswers\";\nimport {\n  CODEX_ACTIVE_WRITER_CODE,\n  CODEX_ACTIVE_WRITER_RE,\n  CODEX_HOOKS_GATE_REGEX,\n  CODEX_PROMPT_READY_TEXT,\n  CODEX_TRUST_GATE_REGEX,\n  type CodexBlockingPrompt,\n  codexScreenBlocksComposer,\n  codexScreenLooksIdle,\n  codexScreenShowsReady,\n  codexStatusBarLine,\n  detectCodexBlockingPrompt,\n  detectCodexCommandApproval,\n  detectCodexPicker,\n  gateCard,\n} from \"./services/questions/codexScreen\";\nimport { parseAgentPhase } from \"./services/questions/parseAgentPhase\";\nimport type {\n  AgentPhase,\n  ManagedSession,\n  PTYManagerOptions,\n  SessionRunner,\n  StartForkSessionOptions,\n  StartFreshSessionOptions,\n  StartSessionOptions,\n  StatusSource,\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, the render terminal, node-pty loading, the session shape and\n// ANSI stripping are shared with pty-manager.ts — see pty-shared.ts.\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. Quiet alone does NOT mark boot ready\n// (mirrors PTYManager.handleQuiet): silence during MCP Starting used to\n// disarm the input queue and let \\r land as a compose newline.\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 — but only\n// when the screen is not still Starting/Working/MCP-booting; otherwise we\n// re-arm so a slow MCP boot cannot be mistaken for Ready.\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 wait for PTY quiescence before Enter (see writeSubmit).\nconst CODEX_SUBMIT_DELAY_MS = 16;\n\n// Cap on how long writeSubmit will wait for the PTY to go quiet before\n// forcing \\r — mirrors pty-manager.ts SUBMIT_MAX_WAIT_MS.\nconst CODEX_SUBMIT_MAX_WAIT_MS = 500;\n\n// After sendInput flips waiting_input → running, if Working never appears\n// within this window the turn never started (\\r absorbed as a compose\n// newline, truncated Ready boot, etc.). Recover so grace/hold and mobile\n// are not stuck on `running`. Same 2s used by the Ready-stale path.\nconst CODEX_SUBMIT_STALE_MS = 2_000;\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 onPhaseChange: PTYManagerOptions[\"onPhaseChange\"];\n  private onReady: PTYManagerOptions[\"onReady\"];\n  // Every Codex prompt the client can answer — startup gates (directory trust,\n  // hooks review), command approvals, and the rate-limit model picker — is\n  // broadcast through this one channel; null dismisses the card once the prompt\n  // leaves the screen.\n  //\n  // Deliberately NOT onLiveQuestion/onLiveQuestionGone, which is Claude's\n  // AskUserQuestion transport. Both channels land on the same mobile\n  // QuestionCard, and the permission one is the correct fit for Codex: its menus\n  // are answered by the option's real on-screen number (parseCodexNumberedOptions\n  // emits `answerKeys: \"2\\r\"`), which is exactly what `permissionIndices` carries\n  // and what AskUserQuestion's down-arrow-count model cannot express. Wiring the\n  // question channel as well would be a second path to the same card.\n  private onPermissionChange: PTYManagerOptions[\"onPermissionChange\"];\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  // After a user submit: if Working never appears, recover from stuck `running`.\n  private submitWatchTimers = new Map<string, NodeJS.Timeout>();\n  // Wall-clock of the last PTY chunk per session — writeSubmit waits until\n  // this hasn't advanced for CODEX_SUBMIT_DELAY_MS before writing \\r.\n  private lastChunkAt = new Map<string, number>();\n  // Sessions that have shown a Working status bar since the last user submit.\n  // Mid-session Ready→waiting_input only fires after this, so a still-painted\n  // Ready bar immediately after sendInput cannot flip status back before the\n  // turn starts (which would let grace/hold kill a live turn).\n  private turnBusy = new Set<string>();\n  // Usage-limit / rate-limit menus — content key for deduped permission cards.\n  private openBlockingPrompt = new Map<string, string>();\n  // Command-approval cards are independent of quota cards: both use the\n  // permission transport, but a repaint/removal of one must not suppress the\n  // other detector's state.\n  private openCommandApproval = new Map<string, string>();\n  // Codex's own numbered pickers (the sign-in screen before login, plain\n  // menus). Keyed by content so TUI repaints do not re-broadcast (#868).\n  private openPicker = new Map<string, string>();\n  // Last codex.screen fingerprint per session — only emit when it changes so\n  // MCP boot redraw storms don't flood the log.\n  private lastScreenLog = new Map<string, string>();\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.onPhaseChange = options.onPhaseChange;\n    this.onReady = options.onReady;\n    this.onPermissionChange = options.onPermissionChange;\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    // `sessionId` stays the runner's map key — only argv carries the\n    // provider-side id, so a resumed Codex session keeps the placeholder id\n    // its client already navigated to.\n    return this.launch(\n      sessionId,\n      [\"resume\", options.resumeId ?? sessionId, \"--cd\", options.projectPath, \"--no-alt-screen\"],\n      options,\n    );\n  }\n\n  // Spawn a Codex PTY under `sessionId` and wire up the shared boot machinery\n  // (screen, ready fallback, output/exit handlers). The only difference between\n  // resume, fresh and fork is argv.\n  private async launch(\n    sessionId: string,\n    args: string[],\n    options: { projectPath: string; projectName?: string; branch?: string },\n  ): Promise<ManagedSession> {\n    const nodePty = await loadPty();\n    const projectName = options.projectName ?? basename(options.projectPath);\n\n    let proc: ReturnType<typeof nodePty.spawn>;\n    try {\n      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    } catch (err) {\n      // See resolveClaudeExe's clearClaudeExeCache() in platform.ts — same\n      // memoize-then-invalidate-on-spawn-failure rationale for Codex.\n      clearCodexExeCache();\n      throw err;\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      statusSource: \"spawn\",\n      statusUpdatedAt: new Date(),\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 sessionId = randomUUID();\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    return this.launch(sessionId, args, options);\n  }\n\n  /**\n   * Fork an existing Codex conversation into a new, independently-owned one\n   * (`codex fork <session-id>`).\n   *\n   * This is the recovery path for a rollout Codex will not let us resume: fork\n   * starts a *new* rollout seeded from the source's history and never touches\n   * the source's writer, so the terminal / VS Code / desktop client that owns\n   * it keeps running untouched. Like a fresh start, Codex assigns the new\n   * rollout id itself — the returned session is keyed by a local placeholder\n   * until watchForCodexRollout binds the real id.\n   */\n  async startFork(options: StartForkSessionOptions): Promise<ManagedSession> {\n    const sessionId = randomUUID();\n    return this.launch(\n      sessionId,\n      [\"fork\", options.forkFromId, \"--cd\", options.projectPath, \"--no-alt-screen\"],\n      options,\n    );\n  }\n\n  // Flat backstop: if the \"Ready\" marker never appears within\n  // CODEX_READY_FALLBACK_MS of spawn (truncated status bar), mark ready once\n  // the screen is no longer Starting/Working/MCP-booting. Re-arms while the\n  // boot is still busy so a slow MCP load cannot be mistaken for Ready.\n  // unref() so a pending timer never holds the process open.\n  private armReadyFallback(sessionId: string): void {\n    const timer = setTimeout(() => {\n      this.readyFallbackTimers.delete(sessionId);\n      void this.tryReadyFallback(sessionId);\n    }, CODEX_READY_FALLBACK_MS);\n    timer.unref?.();\n    this.readyFallbackTimers.set(sessionId, timer);\n  }\n\n  private async tryReadyFallback(sessionId: string): Promise<void> {\n    const session = this.sessions.get(sessionId);\n    if (session?.status !== \"running\" || !this.pendingReady.has(sessionId)) return;\n\n    // No PTY output at all — nothing busy to inspect; settle so start() can\n    // resolve and queued input is not held forever (same role as Claude's\n    // flat fallback on a silent boot).\n    if (session.outputBuffer.length === 0) {\n      this.markReady(sessionId, session, \"timeout-fallback\", \"fallback:timeout\");\n      return;\n    }\n\n    try {\n      const lines = await this.getOutputLines(sessionId, PTY_ROWS);\n      if (!this.pendingReady.has(sessionId)) return;\n      const busy = codexScreenBlocksComposer(lines);\n      this.log.info(`[codex.ready_fallback] ${sessionId.slice(0, 8)} busy=${busy}`, {\n        event: \"codex.ready_fallback\",\n        sessionId,\n        busy,\n        hasReady: codexScreenShowsReady(lines),\n      });\n      if (busy) {\n        this.armReadyFallback(sessionId);\n        return;\n      }\n      // Truncated Ready bar still needs a compose prompt (or Ready itself) —\n      // a model-only status line mid-paint must not disarm the input queue.\n      if (!codexScreenShowsReady(lines) && !codexScreenLooksIdle(lines)) {\n        this.armReadyFallback(sessionId);\n        return;\n      }\n      this.markReady(sessionId, session, \"timeout-fallback\", \"fallback:timeout\");\n    } catch (err) {\n      this.log.warn(\"[codex.ready_fallback] failed\", {\n        event: \"codex.ready_fallback_failed\",\n        sessionId,\n        err,\n      });\n      // Keep trying — a transient screen-read failure must not strand boot.\n      if (this.pendingReady.has(sessionId)) this.armReadyFallback(sessionId);\n    }\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      session.statusSource = \"user-input\";\n      session.statusUpdatedAt = new Date();\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    // A picker digit selects AND confirms (verified on codex-cli 0.154.0), so a\n    // trailing \\r an older client appends would land on the NEXT screen.\n    const pickerDigit =\n      !gate && this.openPicker.has(sessionId) ? /^(\\d+)\\r?$/.exec(keys)?.[1] : undefined;\n    const out =\n      gate && digit ? this.resolveGateAnswer(sessionId, gate, digit) : (pickerDigit ?? keys);\n    this.log.info(`[codex.keys.write] ${sessionId.slice(0, 8)} bytes=${out.length}`, {\n      event: \"codex.keys_write\",\n      sessionId,\n      byteLen: out.length,\n    });\n    session.process.write(out);\n    session.lastActivityAt = new Date();\n  }\n\n  sendRawKeys(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\") throw new Error(`Session is idle (no active PTY): ${sessionId}`);\n    this.log.info(`[codex.raw_keys.write] ${sessionId.slice(0, 8)} bytes=${keys.length}`, {\n      event: \"codex.raw_keys_write\",\n      sessionId,\n      byteLen: keys.length,\n    });\n    session.process.write(keys);\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). Current Codex trust/hooks pickers highlight on the\n  // digit and wait for Enter (`Press enter to continue`); write both.\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}\\r`;\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    // Hold input while booting OR while a dialog owns the screen — a digit\n    // flushed into a Codex approval card would confirm an option; plain text\n    // mid-boot lands as compose newlines once \\r is treated as Enter.\n    if (\n      this.pendingReady.has(sessionId) ||\n      this.openGate.has(sessionId) ||\n      this.openCommandApproval.has(sessionId) ||\n      this.openPicker.has(sessionId)\n    ) {\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      session.statusSource = \"user-input\";\n      session.statusUpdatedAt = new Date();\n      this.onStatusChange?.(toPublicSession(session));\n    }\n    this.turnBusy.delete(sessionId);\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 once the PTY\n  // has been quiet for CODEX_SUBMIT_DELAY_MS. A flat delay fired \\r into a\n  // still-repainting TUI and the Enter became a compose newline instead of a\n  // turn submit (same pathology Claude's quiescence wait fixed).\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}`,\n      {\n        event: \"codex.input_write\",\n        sessionId,\n        promptCount,\n        byteLen: input.length,\n        path,\n        phase: \"input\",\n      },\n    );\n    const writeAt = Date.now();\n    session.process.write(input);\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) ?? writeAt;\n      const quiet = now - lastChunk >= CODEX_SUBMIT_DELAY_MS;\n      const timedOut = now - writeAt >= CODEX_SUBMIT_MAX_WAIT_MS;\n      if (!quiet && !timedOut) {\n        setTimeout(trySubmit, CODEX_SUBMIT_DELAY_MS);\n        return;\n      }\n      this.log.info(\n        `[codex.input.submit] ${sessionId.slice(0, 8)} promptCount=${promptCount} digest=\\\\r waitedMs=${now - writeAt} timedOut=${timedOut}`,\n        {\n          event: \"codex.input_write\",\n          sessionId,\n          promptCount,\n          byteLen: SUBMIT_BYTES.length,\n          digest: \"\\\\r\",\n          path,\n          phase: \"submit\",\n          waitedMs: now - writeAt,\n          timedOut,\n        },\n      );\n      current.process.write(SUBMIT_BYTES);\n      this.armSubmitWatch(sessionId);\n    };\n    setTimeout(trySubmit, CODEX_SUBMIT_DELAY_MS);\n  }\n\n  // If Working never appears after \\r, the turn did not start — recover from\n  // stuck `running` even when no further PTY chunks re-arm the quiet checker\n  // (session ddc67b57: one post-submit chunk, then silence forever).\n  private armSubmitWatch(sessionId: string): void {\n    const prev = this.submitWatchTimers.get(sessionId);\n    if (prev) clearTimeout(prev);\n    const timer = setTimeout(() => {\n      this.submitWatchTimers.delete(sessionId);\n      void this.trySubmitStaleRecovery(sessionId);\n    }, CODEX_SUBMIT_STALE_MS);\n    timer.unref?.();\n    this.submitWatchTimers.set(sessionId, timer);\n  }\n\n  private async trySubmitStaleRecovery(sessionId: string): Promise<void> {\n    const session = this.sessions.get(sessionId);\n    if (session?.status !== \"running\") return;\n    if (this.turnBusy.has(sessionId)) return;\n    if (session.statusSource !== \"user-input\") return;\n\n    try {\n      const lines = await this.getOutputLines(sessionId, PTY_ROWS);\n      if (session.status !== \"running\" || this.turnBusy.has(sessionId)) return;\n      if (codexScreenBlocksComposer(lines)) {\n        // Still Starting/MCP — give the turn more time.\n        this.armSubmitWatch(sessionId);\n        return;\n      }\n      this.log.info(`[codex.submit_stale] ${sessionId.slice(0, 8)} recovering`, {\n        event: \"codex.submit_stale\",\n        sessionId,\n      });\n      this.markReady(sessionId, session, \"quiet-fallback\", \"submit-stale\");\n    } catch (err) {\n      this.log.warn(\"[codex.submit_stale] failed\", {\n        event: \"codex.submit_stale_failed\",\n        sessionId,\n        err,\n      });\n      this.armSubmitWatch(sessionId);\n    }\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 (\n      this.openGate.has(sessionId) ||\n      this.openCommandApproval.has(sessionId) ||\n      this.openPicker.has(sessionId) ||\n      this.pendingReady.has(sessionId)\n    ) {\n      return;\n    }\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  /**\n   * Resize a live PTY. Silent when the session is unknown or has no process:\n   * a terminal emitting SIGWINCH races session exit by nature, and throwing\n   * would turn an ordinary window drag into an error the caller must catch.\n   */\n  resize(sessionId: string, cols: number, rows: number): void {\n    const session = this.sessions.get(sessionId);\n    if (!session?.process) return;\n    if (!Number.isInteger(cols) || !Number.isInteger(rows) || cols < 1 || rows < 1) return;\n    try {\n      session.process.resize(cols, rows);\n      // The headless render terminal MUST track the PTY (see pty-shared.ts):\n      // getOutputLines reads its grid for replay, and the gate detectors scrape\n      // it. Resizing one without the other desyncs every absolute cursor move\n      // the TUI makes — which would break gate detection, the reason a managed\n      // session exists at all.\n      session.screen.resize(cols, rows);\n    } catch (err) {\n      // node-pty throws if the fd closed between the guard above and here.\n      // The session is going away; a resize for it is moot, not an error.\n      this.log.debug(`[pty.resize.failed] ${sessionId.slice(0, 8)}`, {\n        event: \"pty.resize_failed\",\n        sessionId,\n        err,\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. Mirrors PTYManager.putOnHold.\n  putOnHold(sessionId: string, signal: NodeJS.Signals = \"SIGINT\"): 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(signal);\n    } catch {\n      // already dead\n    }\n    session.status = \"idle\";\n    session.statusSource = \"shutdown\";\n    session.statusUpdatedAt = new Date();\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    const submitWatch = this.submitWatchTimers.get(sessionId);\n    if (submitWatch) clearTimeout(submitWatch);\n    this.submitWatchTimers.delete(sessionId);\n    this.lastChunkAt.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    this.turnBusy.delete(sessionId);\n    if (this.openBlockingPrompt.delete(sessionId)) {\n      this.onPermissionChange?.(sessionId, null);\n    }\n    if (this.openCommandApproval.delete(sessionId)) {\n      this.onPermissionChange?.(sessionId, null);\n    }\n    this.lastScreenLog.delete(sessionId);\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  // OS pid of the spawned agent, or null if the session isn't live here.\n  // Mirrors PTYManager.getPid — see there for why the registry records it.\n  getPid(sessionId: string): number | null {\n    return this.sessions.get(sessionId)?.process?.pid ?? null;\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    for (const timer of this.submitWatchTimers.values()) {\n      clearTimeout(timer);\n    }\n    this.submitWatchTimers.clear();\n    this.lastChunkAt.clear();\n    this.turnBusy.clear();\n    this.openBlockingPrompt.clear();\n    this.openCommandApproval.clear();\n    this.lastScreenLog.clear();\n  }\n\n  private handleOutput(sessionId: string, data: string): void {\n    const session = this.sessions.get(sessionId);\n    if (!session) return;\n\n    this.lastChunkAt.set(sessionId, Date.now());\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 — boot (pendingReady) requires the \"Ready\" status-bar marker\n  //     (quiet alone never settles boot — Starting shows `›` already). Mid-\n  //     session, running → waiting_input after Working then Ready (or a stale\n  //     Ready recovery if the turn never started).\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    // ── Writer lock ────────────────────────────────────────────────\n    // Codex refused to attach: another client owns this rollout. Only during\n    // boot — the message can only be produced before the session is usable,\n    // and treating a later appearance (a user pasting the error text, say) as\n    // fatal would kill a live session.\n    if (this.pendingReady.has(sessionId) && CODEX_ACTIVE_WRITER_RE.test(screenText)) {\n      this.failStartup(\n        sessionId,\n        session,\n        CODEX_ACTIVE_WRITER_CODE,\n        \"This Codex session is already open in another client\",\n      );\n      return;\n    }\n\n    // Phase refinement, off the screen read the detectors below already share.\n    // Only while running: markReady clears the phase at turn end, and reading\n    // it in any other state would let a stale screen re-assert one after that\n    // clear. Placed after the writer-lock return so a refused startup, which\n    // never reaches waiting_input, cannot leave a phase behind.\n    if (session.status === \"running\") {\n      this.setPhase(sessionId, session, parseAgentPhase(lines, CODEX_CLI_PROVIDER));\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    // ── Command approval ──────────────────────────────────────────\n    // Trust/hooks dialogs own the screen before this one can appear. Do not\n    // inspect their rendered content as an approval card, even if an old\n    // command-approval repaint remains in the scrollback buffer.\n    const commandApproval = gate ? null : detectCodexCommandApproval(lines);\n    if (commandApproval) {\n      this.handleCommandApproval(sessionId, commandApproval);\n    } else if (this.openCommandApproval.delete(sessionId)) {\n      this.onPermissionChange?.(sessionId, null);\n      this.flushQueuedInputs(sessionId);\n    }\n\n    // ── Usage / rate limits ────────────────────────────────────────\n    const blocking = detectCodexBlockingPrompt(lines);\n    if (blocking) {\n      // Soft tip alone is informational during boot / healthy idle — only\n      // elevate it when a user submit is stuck in `running` without Working\n      // (hard-limit text never appears; tip is the only quota signal).\n      const elevateSoft =\n        !blocking.soft ||\n        (session.status === \"running\" &&\n          session.statusSource === \"user-input\" &&\n          !this.turnBusy.has(sessionId) &&\n          !this.pendingReady.has(sessionId));\n      if (elevateSoft) {\n        this.handleBlockingPrompt(sessionId, session, blocking);\n      }\n    } else if (this.openBlockingPrompt.delete(sessionId)) {\n      this.onPermissionChange?.(sessionId, null);\n    }\n\n    // ── Numbered picker ───────────────────────────────────────────\n    // Last of the card detectors: every dialog above owns the screen while it\n    // is up. Without this, Codex's sign-in picker raised no card at all and\n    // nothing refused composer text, so a sent message picked the highlighted\n    // option and started an auth flow (#868).\n    const picker = gate || commandApproval || blocking ? null : detectCodexPicker(lines);\n    if (picker) {\n      this.handlePicker(sessionId, picker);\n    } else if (this.openPicker.delete(sessionId)) {\n      this.onPermissionChange?.(sessionId, null);\n      this.flushQueuedInputs(sessionId);\n    }\n\n    // ── Readiness ──────────────────────────────────────────────────\n    const hasReady = codexScreenShowsReady(lines);\n    const busy = codexScreenBlocksComposer(lines);\n    const bar = codexStatusBarLine(lines);\n\n    // State-change trail for Starting/Ready/usage investigations. Info (not\n    // debug) so a default LOG_LEVEL=info prod log captures it; fingerprint\n    // dedupe keeps MCP redraw storms from flooding the file.\n    const screenFp = [\n      this.pendingReady.has(sessionId) ? \"1\" : \"0\",\n      session.status,\n      hasReady ? \"1\" : \"0\",\n      busy ? \"1\" : \"0\",\n      blocking ? (blocking.soft ? \"soft\" : \"1\") : \"0\",\n      bar.slice(0, 80),\n    ].join(\"|\");\n    if (this.lastScreenLog.get(sessionId) !== screenFp) {\n      this.lastScreenLog.set(sessionId, screenFp);\n      // Metadata only: the bottom rendered line feeds the fingerprint above but\n      // is screen content and must not be logged.\n      this.log.info(\n        `[codex.screen] ${sessionId.slice(0, 8)} pending=${this.pendingReady.has(sessionId)} ` +\n          `status=${session.status} ready=${hasReady} busy=${busy} usage=${Boolean(blocking)}`,\n        {\n          event: \"codex.screen\",\n          sessionId,\n          trigger: _trigger,\n          pendingReady: this.pendingReady.has(sessionId),\n          status: session.status,\n          hasReady,\n          busy,\n          usageHit: Boolean(blocking),\n          usageSoft: Boolean(blocking?.soft),\n        },\n      );\n    }\n\n    if (this.pendingReady.has(sessionId)) {\n      // Boot: only the Ready marker settles pendingReady on a chunk/quiet\n      // pass. The flat fallback covers a truncated status bar once MCP boot\n      // lines are gone — quiet-during-Starting must never disarm the queue.\n      if (hasReady) {\n        this.markReady(sessionId, session, \"prompt-marker\", `marker:${CODEX_PROMPT_READY_TEXT}`);\n      }\n      return;\n    }\n\n    // Mid-session: after sendInput flipped waiting_input → running, flip back\n    // only once we've observed Working (turn actually started) and Ready has\n    // returned. A stale Ready still on screen right after submit must not\n    // undo the running status — grace/hold would then kill a live turn.\n    if (session.status === \"running\") {\n      if (/\\bWorking\\b/.test(bar)) {\n        this.turnBusy.add(sessionId);\n        const watch = this.submitWatchTimers.get(sessionId);\n        if (watch) clearTimeout(watch);\n        this.submitWatchTimers.delete(sessionId);\n      }\n\n      if (hasReady && this.turnBusy.has(sessionId)) {\n        this.turnBusy.delete(sessionId);\n        this.markReady(sessionId, session, \"prompt-marker\", `marker:${CODEX_PROMPT_READY_TEXT}`);\n      } else if (\n        !this.turnBusy.has(sessionId) &&\n        !busy &&\n        session.statusSource === \"user-input\" &&\n        session.statusUpdatedAt != null &&\n        Date.now() - session.statusUpdatedAt.getTime() >= CODEX_SUBMIT_STALE_MS\n      ) {\n        // Submit never started a turn (Enter absorbed, no Ready on screen,\n        // etc.) — recover so the session is not stuck \"running\" forever.\n        // Does not require Ready: fallback boots often never paint it.\n        this.markReady(sessionId, session, \"quiet-fallback\", \"submit-stale\");\n      }\n    }\n  }\n\n  // Surface quota / rate-limit screens as permission cards and stop leaving\n  // the session stuck in `running` while Codex waits for a menu pick.\n  private handleBlockingPrompt(\n    sessionId: string,\n    session: InternalSession,\n    blocking: CodexBlockingPrompt,\n  ): void {\n    const key = `${blocking.prompt}\\0${blocking.detail ?? \"\"}\\0${blocking.options.map((o) => o.index).join(\",\")}`;\n    const prev = this.openBlockingPrompt.get(sessionId);\n    if (prev !== key) {\n      this.openBlockingPrompt.set(sessionId, key);\n      session.failureReason = blocking.detail\n        ? `${blocking.prompt} ${blocking.detail}`\n        : blocking.prompt;\n      this.log.info(`[codex.usage_limit] ${sessionId.slice(0, 8)}`, {\n        event: \"codex.usage_limit\",\n        sessionId,\n      });\n      this.onPermissionChange?.(sessionId, blocking);\n    }\n    if (session.status === \"running\") {\n      this.turnBusy.delete(sessionId);\n      this.markReady(sessionId, session, \"quiet-fallback\", \"usage-limit\");\n    }\n  }\n\n  // Surface Codex's EXEC card through the existing permission event. The\n  // rendered screen is authoritative: keying it by content suppresses TUI\n  // repaint repeats and the absence path above clears it only after the card\n  // has left the screen.\n  private handleCommandApproval(sessionId: string, approval: CodexBlockingPrompt): void {\n    const key = `${approval.detail ?? \"\"}\\0${approval.options.map((o) => o.answerKeys).join(\",\")}`;\n    if (this.openCommandApproval.get(sessionId) === key) return;\n    this.openCommandApproval.set(sessionId, key);\n    this.log.info(`[codex.command_approval] ${sessionId.slice(0, 8)}`, {\n      event: \"codex.command_approval\",\n      sessionId,\n    });\n    this.onPermissionChange?.(sessionId, approval);\n  }\n\n  // Surface a numbered picker over the permission transport. Keyed by content:\n  // the picker repaints as its ASCII banner animates, and only a real change of\n  // prompt or options is a new card.\n  private handlePicker(sessionId: string, picker: CodexBlockingPrompt): void {\n    const key = `${picker.prompt}\\0${picker.options.map((o) => `${o.index}.${o.label}`).join(\",\")}`;\n    if (this.openPicker.get(sessionId) === key) return;\n    this.openPicker.set(sessionId, key);\n    this.log.info(\n      `[codex.picker_prompt] ${sessionId.slice(0, 8)} options=${picker.options.length}`,\n      {\n        event: \"codex.picker_prompt\",\n        sessionId,\n        optionCount: picker.options.length,\n      },\n    );\n    this.onPermissionChange?.(sessionId, picker);\n  }\n\n  // Answer a gate from the persisted remember-store, or surface it as a\n  // question card over the permission transport. Auto-answer is one write;\n  // if the dialog is still painted on the next scrape, fall through to a\n  // card so a digit-only Codex picker cannot swallow the prompt.\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.openGate.get(sessionId) === gate) return;\n\n    const remembered = rememberedGateDigit(gate);\n    if (remembered && !this.gateActioned.has(key)) {\n      this.gateActioned.add(key);\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}\\r`);\n      return;\n    }\n\n    if (this.gateActioned.has(key) && !remembered) return;\n\n    this.gateActioned.add(key);\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    });\n    this.onPermissionChange?.(sessionId, card);\n  }\n\n  /**\n   * Record the agent's phase and notify only on a real change. The guard is\n   * load-bearing, not an optimisation: detectScreenState runs on every chunk,\n   * so an unguarded setter would fire the WS frame behind this several times a\n   * second for a whole turn while reporting the same value. Same contract as\n   * PTYManager.setPhase — the two runners deliberately stay separate classes.\n   */\n  private setPhase(sessionId: string, session: InternalSession, phase: AgentPhase | null): void {\n    const next = phase ?? null;\n    if ((session.subStatus ?? null) === next) return;\n    session.subStatus = next;\n    this.onPhaseChange?.(sessionId, next);\n  }\n\n  private markReady(\n    sessionId: string,\n    session: InternalSession,\n    source: StatusSource,\n    reason: string,\n  ): void {\n    session.lastActivityAt = new Date();\n    session.status = \"waiting_input\";\n    // C3: same vocabulary as the Claude runner — a status reached by a timer\n    // must be distinguishable from one reached by observing a marker.\n    session.statusSource = source;\n    session.statusUpdatedAt = new Date();\n    // Turn end clears the phase. The exit edge is not an output event, so it\n    // cannot be read off the screen — without this the phase latches on any\n    // session that stops emitting, the bug tb-mobile PR #647 shipped. Below the\n    // source assignment for the same reason as PTYManager.markReady.\n    this.setPhase(sessionId, session, null);\n    // `reason=fallback:timeout`/`quiet:soft-idle` in volume would mean the\n    // status-bar Ready 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    const wasPending = this.pendingReady.delete(sessionId);\n    const submitWatch = this.submitWatchTimers.get(sessionId);\n    if (submitWatch) clearTimeout(submitWatch);\n    this.submitWatchTimers.delete(sessionId);\n    if (wasPending) {\n      const timer = this.readyFallbackTimers.get(sessionId);\n      if (timer) clearTimeout(timer);\n      this.readyFallbackTimers.delete(sessionId);\n      this.flushQueuedInputs(sessionId);\n      this.onReady?.(toPublicSession(session));\n    }\n  }\n\n  /**\n   * Tear down a session that failed before it ever became usable, and report\n   * the reason in machine-readable form.\n   *\n   * Deliberately NOT markReady + exit: the caller must be able to tell a\n   * never-started session from a live one, `onReady` must not fire (no\n   * `session_ready` for a failed start), and every piece of per-session state —\n   * queue, timers, quiet-checker, gate cards, screen — has to go, since the\n   * session is removed from the map and nothing will collect it later.\n   */\n  private failStartup(\n    sessionId: string,\n    session: InternalSession,\n    code: string,\n    message: string,\n  ): void {\n    this.log.warn(`[codex.start_failed] ${sessionId.slice(0, 8)} ${code}`, {\n      event: \"codex.start_failed\",\n      sessionId,\n      code,\n      message,\n    });\n    session.failureCode = code;\n    session.failureReason = message;\n    session.status = \"idle\";\n    session.statusSource = \"process-exit\";\n    session.statusUpdatedAt = new Date();\n    session.completedAt = new Date();\n\n    this.pendingReady.delete(sessionId);\n    this.queuedInputs.delete(sessionId);\n    this.clearSessionDetectors(sessionId);\n    // Removed before the callback: the server reacts by reading the runner,\n    // and must not see a session that is already dead.\n    this.sessions.delete(sessionId);\n    try {\n      // Codex normally exits on its own here; SIGINT covers the case where it\n      // sits on the error screen instead. handleExit no-ops — session is gone.\n      session.process.kill(\"SIGINT\");\n    } catch {\n      // Already dead.\n    }\n    session.screen.dispose();\n    this.onStatusChange?.(toPublicSession(session));\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    session.statusSource = \"process-exit\";\n    session.statusUpdatedAt = new Date();\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.failureCode != null && { failureCode: s.failureCode }),\n    ...(s.lastActivityAt != null && { lastActivityAt: s.lastActivityAt }),\n    ...(s.statusSource != null && { statusSource: s.statusSource }),\n    ...(s.statusUpdatedAt != null && { statusUpdatedAt: s.statusUpdatedAt }),\n    ...(s.filePath != null && { filePath: s.filePath }),\n    // Unconditional — see PTYManager's toPublicSession: a streamer re-adopting\n    // a surviving pty-host's sessions mid-turn has no other source for the\n    // phase, and the host's change guard will not re-emit it.\n    subStatus: s.subStatus ?? null,\n  };\n}\n","import { execFileSync } from \"child_process\";\nimport { accessSync, constants, existsSync, statSync } from \"fs\";\nimport { homedir, platform } from \"os\";\nimport { delimiter, join } from \"path\";\nimport { CODEX_CLI_PROVIDER, CURSOR_PROVIDER, type ProviderName } from \"./providers\";\n\nexport const isWindows = platform() === \"win32\";\n\n// where.exe matches any file literally named after the query, including\n// extension-less POSIX shim scripts that npm installs alongside <name>.cmd /\n// <name>.ps1 for git-bash/WSL. Windows CreateProcess can't execute those\n// (they have no PE header), so picking where.exe's first line unconditionally\n// risks handing node-pty a script instead of a binary — surfacing as\n// \"Cannot create process, error code: 193\". Restrict matches to extensions\n// CreateProcess can actually launch.\nconst WINDOWS_EXECUTABLE_EXTENSIONS = new Set([\".exe\", \".cmd\", \".bat\"]);\n\nfunction isWindowsExecutablePath(path: string): boolean {\n  const dot = path.lastIndexOf(\".\");\n  if (dot < 0) return false;\n  return WINDOWS_EXECUTABLE_EXTENSIONS.has(path.slice(dot).toLowerCase());\n}\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\n// Resolution is memoized for the process lifetime (see below), so a bad\n// resolution otherwise stays bad until restart. Call this after a spawn\n// using the cached path fails, so the next attempt re-resolves instead of\n// repeating the same broken path on every retry/resume.\nexport function clearClaudeExeCache(): void {\n  _claudeExe = undefined;\n}\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\")\n        .map((line) => line.trim())\n        .find(isWindowsExecutablePath);\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\n// Mirrors clearClaudeExeCache() above — same memoize-then-invalidate-on-\n// spawn-failure rationale, swapped for the `codex` binary.\nexport function clearCodexExeCache(): void {\n  _codexExe = undefined;\n}\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\")\n        .map((line) => line.trim())\n        .find(isWindowsExecutablePath);\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// ─── Cursor executable resolution ─────────────────────────────────────────────\n// Cursor CLI's published command is `agent` (cursor.com/install). Some\n// installs also ship `cursor-agent`. Same launchd/Task Scheduler PATH\n// problem as the other two resolvers.\n\nlet _cursorExe: string | undefined;\n\nexport function clearCursorExeCache(): void {\n  _cursorExe = undefined;\n}\n\nfunction resolveNamedAgentCommand(command: string): string | null {\n  if (isWindows) {\n    try {\n      const found = execFileSync(\"where.exe\", [command], {\n        encoding: \"utf-8\",\n        windowsHide: true,\n        timeout: 3000,\n      })\n        .trim()\n        .split(\"\\n\")\n        .map((line) => line.trim())\n        .find(isWindowsExecutablePath);\n      if (found) return found;\n    } catch {}\n\n    const candidates = [\n      join(homedir(), \".local\", \"bin\", `${command}.exe`),\n      join(\n        process.env.LOCALAPPDATA ?? join(homedir(), \"AppData\", \"Local\"),\n        \"Microsoft\",\n        \"WindowsApps\",\n        `${command}.exe`,\n      ),\n    ];\n    for (const p of candidates) {\n      if (existsSync(p)) return p;\n    }\n    return null;\n  }\n\n  try {\n    const found = execFileSync(\"/usr/bin/which\", [command], {\n      encoding: \"utf-8\",\n      timeout: 3000,\n    })\n      .trim()\n      .split(\"\\n\")[0]\n      .trim();\n    if (found && existsSync(found)) return found;\n  } catch {}\n\n  const candidates = [\n    `/opt/homebrew/bin/${command}`,\n    `/usr/local/bin/${command}`,\n    join(homedir(), \".local\", \"bin\", command),\n  ];\n  for (const p of candidates) {\n    if (existsSync(p)) return p;\n  }\n  return null;\n}\n\nexport function resolveCursorExe(): string {\n  if (_cursorExe !== undefined) return _cursorExe;\n\n  const found = resolveNamedAgentCommand(\"agent\") ?? resolveNamedAgentCommand(\"cursor-agent\");\n  _cursorExe = found ?? \"agent\";\n  return _cursorExe;\n}\n\n// ─── Is the provider actually installed? ──────────────────────────────────────\n// Neither resolver above can fail. Each exhausts its lookups and then returns\n// the bare command name, which is handed to execvp/CreateProcess to try its own\n// luck against PATH. That fallback is load-bearing — a box whose /usr/bin/which\n// is absent (slim containers) resolves nothing here yet spawns perfectly well —\n// so \"resolution returned a bare name\" is NOT evidence the CLI is missing, and\n// must not be used to answer \"is this provider installed?\".\n//\n// The consequence of having no answer at all: a missing CLI spawns \"fine\" on\n// POSIX (execvp fails inside the forked child), the session exits ~12ms later\n// with code 1 and no output, and every caller reports something other than the\n// one fact that matters. Both provider-availability endpoints were gated on a\n// throw that cannot happen, so they reported a CLI that is not on the machine\n// as installed.\n\nfunction isExecutableFile(path: string): boolean {\n  try {\n    if (!statSync(path).isFile()) return false;\n    accessSync(path, constants.X_OK);\n    return true;\n  } catch {\n    return false;\n  }\n}\n\n/**\n * Where `exe` — a resolver's output — actually lives, or null if nowhere.\n *\n * A value carrying a path separator is checked directly; a bare command name is\n * searched along PATH exactly the way execvp will search it. node-pty inherits\n * the streamer's own environment (`buildSpawnEnv`), so the PATH walked here is\n * the PATH the spawned child gets — which is what makes this predictive of the\n * spawn rather than merely correlated with it.\n */\nexport function locateExecutable(exe: string): string | null {\n  if (/[\\\\/]/.test(exe)) return isExecutableFile(exe) ? exe : null;\n\n  // Windows resolves a bare name through an extension list, and tries the name\n  // as written first — `node.exe` must not be searched for as `node.exe.exe`.\n  // The literal is only tried when it already carries an extension CreateProcess\n  // can launch, which is the same filter the where.exe lookup above applies:\n  // an extension-less shim sitting on PATH is not a launchable command, however\n  // executable its permissions look.\n  const names = isWindows\n    ? [\n        ...(isWindowsExecutablePath(exe) ? [exe] : []),\n        ...[...WINDOWS_EXECUTABLE_EXTENSIONS].map((ext) => `${exe}${ext}`),\n      ]\n    : [exe];\n  for (const dir of (process.env.PATH ?? \"\").split(delimiter)) {\n    if (!dir) continue;\n    for (const name of names) {\n      const candidate = join(dir, name);\n      if (isExecutableFile(candidate)) return candidate;\n    }\n  }\n  return null;\n}\n\n/** Where this provider's CLI lives on this machine, or null if it is absent. */\nexport function locateProviderExe(provider: ProviderName): string | null {\n  const resolved =\n    provider === CODEX_CLI_PROVIDER\n      ? resolveCodexExe()\n      : provider === CURSOR_PROVIDER\n        ? resolveCursorExe()\n        : resolveClaudeExe();\n  const found = locateExecutable(resolved);\n  if (found === null) {\n    // Resolution is memoized for the process lifetime, so a path that has since\n    // been uninstalled would otherwise keep answering \"missing\" until the next\n    // restart — including after the user reinstalls to fix exactly this.\n    if (provider === CODEX_CLI_PROVIDER) clearCodexExeCache();\n    else if (provider === CURSOR_PROVIDER) clearCursorExeCache();\n    else clearClaudeExeCache();\n  }\n  return found;\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 const CURSOR_PROVIDER = \"cursor\" as const;\n/** Live PTY on main shipped this wire name; accept it and emit `cursor`. */\nexport const LEGACY_CURSOR_PROVIDER = \"cursor-cli\" as const;\n\nexport const PROVIDER_NAMES = [CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER, CURSOR_PROVIDER] as const;\n\nexport type ProviderName = (typeof PROVIDER_NAMES)[number];\n\nexport function canonicalizeProviderName(value: unknown): ProviderName | undefined {\n  if (value === LEGACY_CURSOR_PROVIDER) return CURSOR_PROVIDER;\n  if (typeof value === \"string\" && (PROVIDER_NAMES as readonly string[]).includes(value)) {\n    return value as ProviderName;\n  }\n  return undefined;\n}\n\nexport function isProviderName(value: unknown): value is ProviderName {\n  return canonicalizeProviderName(value) !== undefined;\n}\n\n/** The argv[0] we look for / tell the user to install. */\nexport function commandNameForProvider(provider: ProviderName): string {\n  switch (canonicalizeProviderName(provider) ?? provider) {\n    case CLAUDE_CODE_PROVIDER:\n      return \"claude\";\n    case CODEX_CLI_PROVIDER:\n      return \"codex\";\n    case CURSOR_PROVIDER:\n      return \"agent\";\n    default:\n      return \"claude\";\n  }\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 canonicalizeProviderName(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","import { Terminal } from \"@xterm/headless\";\nimport type { ManagedSession, UserMessage } from \"./types\";\n\n/**\n * Plumbing shared by the two PTY runners (`pty-manager.ts` for Claude,\n * `codex-pty-runner.ts` for Codex). These were duplicated byte-for-byte in both\n * files; the copies drifted only in their comments.\n *\n * Deliberately plumbing only. The two runners' *detection* logic stays\n * provider-specific and is not shared: Claude signals readiness with OSC 777\n * plus prompt markers, Codex with rendered status-bar predicates and no OSC at\n * all. Merging those would couple two independent provider contracts.\n */\n\n// PTY geometry. The headless render terminal (session.screen) MUST match these\n// so a provider's absolute cursor moves (ESC[<row>;<col>H) resolve to the same\n// screen coordinates the real TUI is painting against.\nexport const PTY_COLS = 120;\nexport const PTY_ROWS = 40;\n// Scrollback depth for the render terminal.\nexport const SCREEN_SCROLLBACK = 1000;\n// Everything the render terminal can hold: its scrollback plus the viewport.\n// This is what `subscribe_session` replays — \"as much scrollback as the session\n// still has\", not a number picked independently of it. The client keeps its own\n// retention cap (tb-mobile's VirtualTerminal, 10 000 rows), which is larger, so\n// this terminal is the binding limit on both ends and neither side has to know\n// the other's number.\nexport const REPLAY_MAX_LINES = SCREEN_SCROLLBACK + PTY_ROWS;\n\n// node-pty is a native addon — import dynamically to allow graceful failure\nlet pty: typeof import(\"node-pty\") | null = null;\n\nexport async 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\nexport interface 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 both providers' absolute-cursor\n  // repaints 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\nexport function 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// Strip ANSI escape sequences for clean text preview\nexport function 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","// 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 * the runner appends Enter because current Codex only highlights on the digit.\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","import type { PermissionOption } from \"../../types\";\nimport type { CodexGateType } from \"./codexGateAnswers\";\n\n/**\n * Codex's rendered-screen regexes and predicates: readiness, busy/boot state,\n * blocking prompts (trust gate, hooks review, usage limits) and the cards built\n * from them. Pure functions of the rendered lines — no PTY, no session state.\n *\n * Lives here rather than in codex-pty-runner.ts for symmetry with the Claude\n * side, whose equivalents are already in this directory (detectPermissionGate,\n * detectQuestionFromScreen, detectShellPrompt, parseStatusLine).\n */\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// Status-bar words that mean the composer is not accepting submits. `\\b`\n// matches at a `/`, so these alone do NOT stop a cwd like `.../Working/repo`\n// from false-hitting — test them against codexStatusBarWords(), not the raw bar.\nexport const CODEX_BUSY_STATUS_RE = /\\b(?:Starting|Working)\\b/;\n\n// The busy half that actually means \"a turn is in flight\". \"Starting\" is MCP\n// boot, not a turn, so the two must not be conflated wherever the distinction\n// matters (agent phase); for composer gating they are the same and\n// CODEX_BUSY_STATUS_RE covers both.\nexport const CODEX_WORKING_STATUS_RE = /\\bWorking\\b/;\n\n// Boot progress lines that appear *above* the status bar while MCP servers\n// are still loading. The status bar may already omit \"Starting\" (or truncate\n// \"Ready\") during this window — treating quiet as ready here is what stranded\n// keystrokes in the compose box as newlines (session 61928fac).\n//\n// Do NOT match post-boot warnings (\"MCP startup incomplete\", \"not logged in\",\n// \"timed out after\"): those stay on screen after Codex is usable. Matching\n// them blocked Ready forever (session fbb5eb36) and left mobile stuck in\n// `running` with a spinning send affordance.\nexport const CODEX_MCP_BOOT_RE = /Booting MCP|Starting MCP servers/i;\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// Codex's single-writer lock: `codex resume` (or `codex fork`) refuses to\n// attach to a rollout another process already holds open, printing\n// \"already has an active writer (code -32600)\" and going no further. Codex\n// itself is the authority here — no pre-flight can be, because the owner may\n// be a TUI, VS Code, or the desktop app, and it can attach between our probe\n// and our spawn.\n//\n// Both spellings are accepted: the message is rendered into a 120-col TUI and\n// can wrap or truncate, so the JSON-RPC code is the more durable half. Matching\n// runs against the RENDERED screen, so a message split across PTY chunks (or\n// reordered by absolute-cursor repaints) still matches.\nexport const CODEX_ACTIVE_WRITER_RE = /already has an active writer|-32600/;\n\n/** `failureCode` set on a session killed by the writer-lock detection above. */\nexport const CODEX_ACTIVE_WRITER_CODE = \"codex_active_writer\";\n\n// Hard usage-limit error Codex prints when the account quota is exhausted.\nexport const CODEX_USAGE_LIMIT_RE = /you(?:'ve| have) hit your usage limit/i;\n\n// Soft tip Codex paints when a reset is available — often the only quota\n// signal on screen when a turn never starts (hard-limit text never appears).\nexport const CODEX_USAGE_RESET_TIP_RE = /usage limit reset available/i;\n\n// Interactive model-switch menu shown when a turn would exceed rate limits.\nexport const CODEX_RATE_LIMIT_MENU_RE = /approaching rate limits/i;\n\nexport interface CodexBlockingPrompt {\n  prompt: string;\n  detail?: string;\n  options: PermissionOption[];\n  /** Tip-only (not a hard limit / rate-limit menu). Surfaced after a failed turn. */\n  soft?: boolean;\n}\n\n// Codex command approvals are rendered as an EXEC card. Unlike the startup\n// gates above, selecting approval is a literal `y` and rejecting is Escape;\n// the numbered rows are presentation only. Require all of the card's stable\n// chrome so a command or approval word left in transcript scrollback cannot\n// become a permission card.\nconst CODEX_COMMAND_APPROVAL_HEADING_RE = /^\\s*E\\s*X\\s*E\\s*C\\s*$/i;\nconst CODEX_COMMAND_APPROVAL_ENV_RE = /^\\s*Environment:\\s*.+/i;\nconst CODEX_COMMAND_APPROVAL_REASON_RE = /^\\s*Reason:\\s*.+/i;\nconst CODEX_COMMAND_APPROVAL_CONFIRM_RE =\n  /(?:enter|return) to (?:confirm|approve)|press (?:enter|return) to (?:confirm|approve)/i;\nconst CODEX_COMMAND_APPROVAL_YES_RE = /^\\s*›?\\s*\\d+\\.\\s*yes\\b/i;\nconst CODEX_COMMAND_APPROVAL_NO_RE = /^\\s*›?\\s*\\d+\\.\\s*no\\b/i;\n\n/**\n * Detect Codex's rendered command-approval card. This deliberately does not\n * reuse parseCodexNumberedOptions: the visible 1/2 rows do not answer this\n * dialog, while `y` and Escape do.\n */\nexport function detectCodexCommandApproval(lines: string[]): CodexBlockingPrompt | null {\n  // The newest dialog is the last EXEC heading in the rendered buffer; an\n  // earlier one may be stale transcript content above a repaint.\n  let heading = -1;\n  for (let i = 0; i < lines.length; i++) {\n    if (CODEX_COMMAND_APPROVAL_HEADING_RE.test(lines[i])) heading = i;\n  }\n  if (heading < 0) return null;\n  if (\n    lines.some((line) => CODEX_USAGE_LIMIT_RE.test(line) || CODEX_RATE_LIMIT_MENU_RE.test(line))\n  ) {\n    return null;\n  }\n\n  const card = lines.slice(heading);\n  if (\n    !card.some((line) => CODEX_COMMAND_APPROVAL_ENV_RE.test(line)) ||\n    !card.some((line) => CODEX_COMMAND_APPROVAL_REASON_RE.test(line)) ||\n    !card.some((line) => CODEX_COMMAND_APPROVAL_YES_RE.test(line)) ||\n    !card.some((line) => CODEX_COMMAND_APPROVAL_NO_RE.test(line)) ||\n    !card.some((line) => CODEX_COMMAND_APPROVAL_CONFIRM_RE.test(line))\n  ) {\n    return null;\n  }\n\n  const detail = card\n    .filter((line) => {\n      const trimmed = line.trim();\n      return (\n        trimmed.length > 0 &&\n        !CODEX_COMMAND_APPROVAL_HEADING_RE.test(line) &&\n        !CODEX_COMMAND_APPROVAL_YES_RE.test(line) &&\n        !CODEX_COMMAND_APPROVAL_NO_RE.test(line) &&\n        !CODEX_COMMAND_APPROVAL_CONFIRM_RE.test(line)\n      );\n    })\n    .join(\"\\n\");\n\n  return {\n    prompt: \"Codex requests command approval\",\n    ...(detail ? { detail } : {}),\n    options: [\n      { index: 1, label: \"Yes\", answerKeys: \"y\" },\n      { index: 2, label: \"No\", answerKeys: \"\\x1b\" },\n    ],\n  };\n}\n\n// A Codex picker row: an optional selection cursor (Codex paints \">\", the\n// compose prefix is \"›\"), the number, and the label.\nconst CODEX_PICKER_ROW_RE = /^\\s*([>›❯])?\\s*(\\d+)\\.\\s+(.+?)\\s*$/;\n// Codex's compose prefix. A picker owns the screen instead of the composer, so\n// a compose line below a numbered block means the block is transcript output.\nconst CODEX_COMPOSE_LINE_RE = /^\\s*›/;\n// Lines between two option rows: Codex prints one indented description line\n// and one blank. More than that is prose, not a menu.\nconst MAX_PICKER_ROW_GAP = 2;\n\n/**\n * Claim Codex's own numbered picker — the sign-in screen shown before login\n * (\"1. Sign in with ChatGPT / 2. Sign in with Device Code / 3. Provide your own\n * API key\"), and any menu of that shape (#868). Codex's other dialogs have\n * their own detectors and are checked before this one.\n *\n * Rows may be separated by one indented description line each, which is what\n * defeats the Claude-side readers: scrapePermissionGate stops at the first\n * non-option line, and detectShellPrompt's bare-confirmation branch turns the\n * trailing \"Press enter to continue\" into a single Continue option whose Enter\n * silently picks the highlighted row — signing the user in on one tap.\n *\n * Answer keys are the bare digit: verified live on codex-cli 0.154.0, a digit\n * selects AND confirms, the same as Codex's trust and hooks gates.\n */\nexport function detectCodexPicker(lines: string[]): CodexBlockingPrompt | null {\n  const screenText = lines.join(\"\\n\");\n  if (CODEX_TRUST_GATE_REGEX.test(screenText) || CODEX_HOOKS_GATE_REGEX.test(screenText)) {\n    return null;\n  }\n  if (codexScreenShowsReady(lines)) return null;\n\n  const rows: { line: number; index: number; label: string; cursor: boolean }[] = [];\n  for (let i = 0; i < lines.length; i++) {\n    const m = CODEX_PICKER_ROW_RE.exec(lines[i]);\n    if (m) rows.push({ line: i, index: Number(m[2]), label: m[3], cursor: Boolean(m[1]) });\n  }\n  if (rows.length < 2) return null;\n\n  // The LAST contiguous 1..N run: an earlier menu may sit in the scrollback.\n  let start = rows.length - 1;\n  while (start > 0 && rows[start - 1].index === rows[start].index - 1) start--;\n  const block = rows.slice(start);\n  if (block.length < 2 || block[0].index !== 1) return null;\n  if (block.filter((r) => r.cursor).length !== 1) return null;\n  for (let i = 1; i < block.length; i++) {\n    // Codex separates its rows with a description line AND a blank line, so a\n    // blank inside the block is normal — rejecting it is what made the first\n    // version of this detector inert against the real screen (#868).\n    if (block[i].line - block[i - 1].line - 1 > MAX_PICKER_ROW_GAP) return null;\n  }\n  // A compose line below the block means this is transcript output, not a live\n  // picker; the composer and a picker never share the screen.\n  for (let i = block[block.length - 1].line + 1; i < lines.length; i++) {\n    if (CODEX_COMPOSE_LINE_RE.test(lines[i])) return null;\n  }\n\n  // The intro sits above the block, separated from it by the same blank rows\n  // Codex puts between the options — skip those before collecting it, or the\n  // card carries a placeholder instead of Codex's own words.\n  let above = block[0].line - 1;\n  while (above >= 0 && lines[above].trim() === \"\") above--;\n  const intro: string[] = [];\n  for (let i = above; i >= 0 && intro.length < 3; i--) {\n    const t = lines[i].trim();\n    if (t === \"\") break;\n    intro.unshift(t);\n  }\n  const prompt = intro.length > 0 ? intro[intro.length - 1] : \"Codex is asking you to choose\";\n  const detail = intro.slice(0, -1).join(\"\\n\");\n  return {\n    prompt,\n    ...(detail ? { detail } : {}),\n    options: block.map((r) => ({ index: r.index, label: r.label, answerKeys: `${r.index}` })),\n  };\n}\n\n/** Parse Codex's numbered TUI menus (`1. …`, `› 2. …`). */\nexport function parseCodexNumberedOptions(lines: string[]): PermissionOption[] {\n  const options: PermissionOption[] = [];\n  for (const line of lines) {\n    const m = /^\\s*[>›❯]?\\s*(\\d+)\\.\\s+(.+?)\\s*$/.exec(line.trimEnd());\n    if (!m) continue;\n    const label = m[2].replace(/\\s*\\(selected\\)\\s*$/i, \"\").trim();\n    options.push({ index: Number(m[1]), label, answerKeys: `${m[1]}\\r` });\n  }\n  return options;\n}\n\n/**\n * Usage-limit errors, the soft \"reset available\" tip, and the \"Approaching\n * rate limits\" model-picker menu. Surfaced over the permission transport so\n * mobile shows a card instead of spinning forever in `running`.\n */\nexport function detectCodexBlockingPrompt(lines: string[]): CodexBlockingPrompt | null {\n  const screenText = lines.join(\"\\n\");\n  const usageLine = lines.find((l) => CODEX_USAGE_LIMIT_RE.test(l))?.trim();\n  const tipLine = lines.find((l) => CODEX_USAGE_RESET_TIP_RE.test(l))?.trim();\n  const rateMenu = CODEX_RATE_LIMIT_MENU_RE.test(screenText);\n  if (!usageLine && !rateMenu && !tipLine) return null;\n\n  const soft = !usageLine && !rateMenu && Boolean(tipLine);\n  const prompt =\n    usageLine ??\n    lines.find((l) => CODEX_RATE_LIMIT_MENU_RE.test(l))?.trim() ??\n    tipLine ??\n    \"Codex usage limit reached\";\n  const detail = lines.find((l) => /try again at/i.test(l))?.trim();\n  const options = parseCodexNumberedOptions(lines);\n  if (options.length === 0) {\n    options.push({ index: 1, label: soft ? \"Dismiss\" : \"OK\", answerKeys: \"\\x1b\" });\n  }\n  return { prompt, ...(detail ? { detail } : {}), options, ...(soft ? { soft: true } : {}) };\n}\n\n/** Last non-blank rendered line — Codex paints the status bar there. */\nexport function codexStatusBarLine(lines: string[]): string {\n  return [...lines].reverse().find((l) => l.trim() !== \"\") ?? \"\";\n}\n\n/**\n * The status bar with its path-shaped `·` fields (containing `/` or `\\`, or\n * starting with `~`) dropped, for every status-word test. The bar carries\n * the cwd at no fixed position — the field count varies — so filtering by shape\n * is the only position-independent way to keep a project path out of the match.\n */\nexport function codexStatusBarWords(lines: string[]): string {\n  return codexStatusBarLine(lines)\n    .split(\"·\")\n    .filter((f) => !/[/\\\\]/.test(f) && !f.trim().startsWith(\"~\"))\n    .join(\"·\");\n}\n\n/**\n * True while the screen shows something that precedes a turn rather than being\n * one: a blocking gate dialog, or MCP boot progress. Both keep the composer\n * shut, but neither is the agent doing work — the status-bar half of\n * codexScreenBlocksComposer cannot tell them apart on its own, and reading\n * only that half is how the agent phase reported \"working\" during boot.\n */\nexport function codexScreenPreTurn(lines: string[]): boolean {\n  const screenText = lines.join(\"\\n\");\n  if (CODEX_HOOKS_GATE_REGEX.test(screenText) || CODEX_TRUST_GATE_REGEX.test(screenText)) {\n    return true;\n  }\n  return CODEX_MCP_BOOT_RE.test(screenText);\n}\n\n/**\n * True while Codex must not receive composer keystrokes: gate dialogs, MCP\n * boot progress, or a Starting/Working status bar. Shared by boot detection,\n * the flat fallback, and mid-session idle re-detect.\n */\nexport function codexScreenBlocksComposer(lines: string[]): boolean {\n  if (codexScreenPreTurn(lines)) return true;\n  return CODEX_BUSY_STATUS_RE.test(codexStatusBarWords(lines));\n}\n\n/** Authoritative Ready: status-bar word present and screen not otherwise busy. */\nexport function codexScreenShowsReady(lines: string[]): boolean {\n  if (codexScreenBlocksComposer(lines)) return false;\n  return codexStatusBarWords(lines).includes(CODEX_PROMPT_READY_TEXT);\n}\n\n/**\n * Soft idle for a truncated Ready bar: compose `›` / `>` is up and nothing\n * busy is on screen. Not used as a sole boot-ready signal on quiet — `›`\n * appears during Starting — but the flat fallback requires it (or Ready)\n * before settling so a model-only status line is not mistaken for usable.\n */\nexport function codexScreenLooksIdle(lines: string[]): boolean {\n  if (codexScreenBlocksComposer(lines)) return false;\n  if (codexScreenShowsReady(lines)) return true;\n  // Compose prefix: `›` (Codex default) or `>` (some Windows/ConPTY paints).\n  // Require start-of-line so URLs like `https://…` do not false-hit.\n  return lines.some((l) => /^\\s*[›>]\\s/.test(l) || l.includes(\"›\"));\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.\nexport function 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","// Agent-phase detection. Answers \"what is the agent doing right now\" for a\n// session whose status is already `running` — a refinement of that status, not\n// a state with its own lifecycle.\n//\n// Pure — no I/O. Operates on rendered screen lines (getOutputLines), because\n// the TUI paints its footer with absolute-cursor moves and it does not exist as\n// a contiguous run of bytes in the raw PTY stream. This is the same constraint\n// parseStatusLine documents, and the reason mobile's own attempt at this\n// (tb-mobile PR #647) failed: it searched a client-side emulator that resolves\n// absolute cursor moves against the wrong rows.\n//\n// Returns `null` when no phase is recognised. Callers must treat that as \"no\n// phase\", never as \"unchanged\" — a derive that holds its previous value is how\n// an indicator latches on a finished turn.\n//\n// Why phase is readable from a repainted footer at all, when parseStatusLine\n// deliberately refuses to forward the elapsed counter from that same line: the\n// counter is a continuous function of wall-clock time sampled at output events,\n// so between samples it is simply wrong and the error grows without bound. A\n// phase is a step function whose transitions ARE output events — the agent\n// starts streaming *because* tokens began painting — so sampling at output\n// events is exact for it, not approximate. The one exception is the exit edge,\n// which is not an output event; that is why the phase is cleared out-of-band in\n// markReady() rather than inferred from the screen going quiet.\n\nimport type { ProviderName } from \"../../providers\";\nimport { CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER } from \"../../providers\";\nimport type { AgentPhase } from \"../../types\";\nimport { CODEX_WORKING_STATUS_RE, codexScreenPreTurn, codexStatusBarWords } from \"./codexScreen\";\n\n/**\n * Codex's status bar is binary: a turn walks Ready → Working → Ready with no\n * intermediate state observed on a live PTY probe, so reporting anything finer\n * would be invention. `working` is the only phase this provider can support.\n *\n * Two screens keep the composer shut without a turn being in flight, and both\n * would otherwise read as `working` before the user has submitted anything:\n * a blocking startup gate (directory trust, hooks review), and MCP boot — whose\n * status bar says \"Starting\", which is why this tests Working alone rather than\n * the composer-gating CODEX_BUSY_STATUS_RE that covers both words.\n */\nfunction codexPhase(lines: string[]): AgentPhase | null {\n  if (codexScreenPreTurn(lines)) return null;\n  return CODEX_WORKING_STATUS_RE.test(codexStatusBarWords(lines)) ? \"working\" : null;\n}\n\n/**\n * Claude's footer carries a richer phase, but the marker set has not yet been\n * re-verified against a fresh PTY capture — the candidate grammar was derived\n * from two captured turns, and the distinction it rests on (output-token `↓`\n * markers, which parseStatusLine does not read, versus the input-token `↑`\n * counter, which it deliberately rejects) is too load-bearing to build on\n * unconfirmed. Reporting no phase is correct until that lands: the indicator\n * simply does not render, which is the pre-feature behaviour.\n */\nfunction claudePhase(_lines: string[]): AgentPhase | null {\n  return null;\n}\n\n/**\n * Derive the agent's phase from a rendered screen.\n *\n * An unrecognised provider yields `null` rather than falling back to a\n * provider-specific grammar. `getTerminalChromeFilter` already establishes that\n * instinct — \"prefer passthrough over wrong Claude filters\" — and the opposite\n * default is what let a wrong grammar run against the wrong provider in #647.\n */\nexport function parseAgentPhase(lines: string[], provider: ProviderName): AgentPhase | null {\n  if (provider === CODEX_CLI_PROVIDER) return codexPhase(lines);\n  if (provider === CLAUDE_CODE_PROVIDER) return claudePhase(lines);\n  return 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 { randomUUID } from \"crypto\";\nimport { existsSync } from \"fs\";\nimport { basename } from \"path\";\nimport { getLogger, type Logger } from \"./logger\";\nimport { clearCursorExeCache, resolveCursorExe } from \"./platform\";\nimport { CURSOR_PROVIDER } from \"./providers\";\nimport {\n  createScreen,\n  type InternalSession,\n  loadPty,\n  PTY_COLS,\n  PTY_ROWS,\n  stripAnsi,\n} from \"./pty-shared\";\nimport type {\n  ManagedSession,\n  PTYManagerOptions,\n  SessionRunner,\n  StartFreshSessionOptions,\n  StartSessionOptions,\n  StatusSource,\n  UserMessage,\n} from \"./types\";\nimport { debounce } from \"./utils/debounce\";\n\nconst OUTPUT_BUFFER_MAX = 65536;\nconst INPUT_HISTORY_MAX = 50;\nconst QUIET_DETECT_MS = 500;\nconst CURSOR_READY_FALLBACK_MS = 8_000;\nconst SUBMIT_BYTES = \"\\r\";\n/** Ctrl+U — kill the compose line before pasting the next turn. */\nconst CLEAR_COMPOSE_BYTES = \"\\x15\";\nconst CURSOR_SUBMIT_DELAY_MS = 16;\nconst CURSOR_SUBMIT_MAX_WAIT_MS = 500;\nconst CURSOR_SUBMIT_STALE_MS = 2_000;\n\n/**\n * Cursor CLI (`agent`) PTY runner.\n *\n * Spawn/resume flags come from the published CLI: `--workspace`, `--trust`\n * (headless, skip the workspace-trust prompt), `--resume=<id>`, positional\n * opening prompt. TUI detection is deliberately generic: we have no verified\n * Ready/gate scrape, so boot settles on quiet or the 8s fallback, and a turn\n * returns to waiting_input after submit-stale silence.\n *\n * Input clears the compose line (`Ctrl+U`) before writing text — Cursor leaves\n * the previous prompt editable, and a bare write would concatenate turns.\n */\nexport class CursorPtyRunner 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 onUserMessage: PTYManagerOptions[\"onUserMessage\"];\n  private log: Logger;\n  private pendingReady = new Set<string>();\n  private queuedInputs = new Map<string, string[]>();\n  private quietCheckers = new Map<string, ReturnType<typeof debounce<[]>>>();\n  private readyFallbackTimers = new Map<string, NodeJS.Timeout>();\n  private submitWatchTimers = new Map<string, NodeJS.Timeout>();\n  private lastChunkAt = new Map<string, number>();\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.onUserMessage = options.onUserMessage;\n    this.log = options.logger ?? getLogger();\n  }\n\n  async start(sessionId: string, options: StartSessionOptions): Promise<ManagedSession> {\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 resumeId = options.resumeId ?? sessionId;\n    return this.launch(\n      sessionId,\n      this.baseArgs(options.projectPath, [`--resume=${resumeId}`]),\n      options,\n    );\n  }\n\n  async startFresh(options: StartFreshSessionOptions): Promise<ManagedSession> {\n    const sessionId = randomUUID();\n    const args = this.baseArgs(options.projectPath);\n    if (options.systemPrompt) args.push(options.systemPrompt);\n    return this.launch(sessionId, args, options);\n  }\n\n  private baseArgs(projectPath: string, extra: string[] = []): string[] {\n    // `--trust` is headless-only and skips the workspace prompt; it is not\n    // permission-gate scraping. `--workspace` is the documented project root.\n    return [\"--workspace\", projectPath, \"--trust\", ...extra];\n  }\n\n  private async launch(\n    sessionId: string,\n    args: string[],\n    options: { projectPath: string; projectName?: string; branch?: string },\n  ): Promise<ManagedSession> {\n    const nodePty = await loadPty();\n    const projectName = options.projectName ?? basename(options.projectPath);\n\n    let proc: ReturnType<typeof nodePty.spawn>;\n    try {\n      proc = nodePty.spawn(resolveCursorExe(), 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    } catch (err) {\n      clearCursorExeCache();\n      throw err;\n    }\n\n    const session: InternalSession = {\n      id: sessionId,\n      provider: CURSOR_PROVIDER,\n      projectPath: options.projectPath,\n      projectName,\n      branch: options.branch ?? \"\",\n      status: \"running\",\n      statusSource: \"spawn\",\n      statusUpdatedAt: new Date(),\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    proc.onExit(({ exitCode }: { exitCode: number }) => {\n      this.pendingReady.delete(sessionId);\n      this.handleExit(sessionId, exitCode);\n    });\n\n    return toPublicSession(session);\n  }\n\n  private armReadyFallback(sessionId: string): void {\n    const timer = setTimeout(() => {\n      this.readyFallbackTimers.delete(sessionId);\n      this.tryReadyFallback(sessionId);\n    }, CURSOR_READY_FALLBACK_MS);\n    timer.unref?.();\n    this.readyFallbackTimers.set(sessionId, timer);\n  }\n\n  private tryReadyFallback(sessionId: string): void {\n    const session = this.sessions.get(sessionId);\n    if (session?.status !== \"running\" || !this.pendingReady.has(sessionId)) return;\n    this.markReady(sessionId, session, \"timeout-fallback\", \"fallback:timeout\");\n  }\n\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      session.statusSource = \"user-input\";\n      session.statusUpdatedAt = new Date();\n      this.onStatusChange?.(toPublicSession(session));\n    }\n    session.process.write(keys);\n    session.lastActivityAt = new Date();\n  }\n\n  sendRawKeys(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\") throw new Error(`Session is idle (no active PTY): ${sessionId}`);\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    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      return session.promptCount;\n    }\n    if (session.status === \"waiting_input\") {\n      session.status = \"running\";\n      session.statusSource = \"user-input\";\n      session.statusUpdatedAt = new Date();\n      this.onStatusChange?.(toPublicSession(session));\n    }\n    this.writeSubmit(sessionId, session, input);\n    session.lastActivityAt = new Date();\n    session.promptCount++;\n    return session.promptCount;\n  }\n\n  private writeSubmit(sessionId: string, session: InternalSession, input: string): void {\n    this.recordUserMessage(session, input);\n    const writeAt = Date.now();\n    // Cursor's TUI leaves the previous prompt in the compose box after a turn.\n    // Writing the next input on top concatenates (\"Commit it\" + \"Yes, commit it\"\n    // → \"Commit itYes, commit it\") and that smashed string is what lands in\n    // agent-transcripts. Clear the line first (same kill-line byte readline\n    // uses), then write the new text, then \\r once the PTY is quiet.\n    session.process.write(CLEAR_COMPOSE_BYTES + input);\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) ?? writeAt;\n      const quiet = now - lastChunk >= CURSOR_SUBMIT_DELAY_MS;\n      const timedOut = now - writeAt >= CURSOR_SUBMIT_MAX_WAIT_MS;\n      if (!quiet && !timedOut) {\n        setTimeout(trySubmit, CURSOR_SUBMIT_DELAY_MS);\n        return;\n      }\n      current.process.write(SUBMIT_BYTES);\n      this.armSubmitWatch(sessionId);\n    };\n    setTimeout(trySubmit, CURSOR_SUBMIT_DELAY_MS);\n  }\n\n  private armSubmitWatch(sessionId: string): void {\n    const prev = this.submitWatchTimers.get(sessionId);\n    if (prev) clearTimeout(prev);\n    const timer = setTimeout(() => {\n      this.submitWatchTimers.delete(sessionId);\n      const session = this.sessions.get(sessionId);\n      if (session?.status !== \"running\") return;\n      if (session.statusSource !== \"user-input\") return;\n      this.markReady(sessionId, session, \"quiet-fallback\", \"submit-stale\");\n    }, CURSOR_SUBMIT_STALE_MS);\n    timer.unref?.();\n    this.submitWatchTimers.set(sessionId, timer);\n  }\n\n  private flushQueuedInputs(sessionId: string): void {\n    if (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    queue.forEach((input, i) => {\n      const writeAt = i * CURSOR_SUBMIT_DELAY_MS * 2;\n      const fire = () => {\n        const current = this.sessions.get(sessionId);\n        if (!current || current !== session) return;\n        this.writeSubmit(sessionId, session, input);\n      };\n      if (writeAt === 0) fire();\n      else setTimeout(fire, writeAt);\n    });\n  }\n\n  resize(sessionId: string, cols: number, rows: number): void {\n    const session = this.sessions.get(sessionId);\n    if (!session?.process) return;\n    if (!Number.isInteger(cols) || !Number.isInteger(rows) || cols < 1 || rows < 1) return;\n    try {\n      session.process.resize(cols, rows);\n      session.screen.resize(cols, rows);\n    } catch (err) {\n      this.log.debug(`[pty.resize.failed] ${sessionId.slice(0, 8)}`, {\n        event: \"pty.resize_failed\",\n        sessionId,\n        err,\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  putOnHold(sessionId: string, signal: NodeJS.Signals = \"SIGINT\"): 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(signal);\n    } catch {\n      // already dead\n    }\n    session.status = \"idle\";\n    session.statusSource = \"shutdown\";\n    session.statusUpdatedAt = new Date();\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  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  getPid(sessionId: string): number | null {\n    return this.sessions.get(sessionId)?.process?.pid ?? null;\n  }\n\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()) clearTimeout(timer);\n    for (const timer of this.submitWatchTimers.values()) clearTimeout(timer);\n    this.sessions.clear();\n    this.pendingReady.clear();\n    this.queuedInputs.clear();\n    this.quietCheckers.clear();\n    this.readyFallbackTimers.clear();\n    this.submitWatchTimers.clear();\n    this.lastChunkAt.clear();\n  }\n\n  private handleOutput(sessionId: string, data: string): void {\n    const session = this.sessions.get(sessionId);\n    if (!session) return;\n\n    this.lastChunkAt.set(sessionId, Date.now());\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    session.screen.write(data);\n    session.lastOutput = stripAnsi(data);\n    this.onOutput?.(sessionId, data);\n\n    let quiet = this.quietCheckers.get(sessionId);\n    if (!quiet) {\n      quiet = debounce(() => {\n        this.detectQuiet(sessionId);\n      }, QUIET_DETECT_MS);\n      this.quietCheckers.set(sessionId, quiet);\n    }\n    quiet();\n  }\n\n  private detectQuiet(sessionId: string): void {\n    const session = this.sessions.get(sessionId);\n    if (!session || session.status === \"idle\") return;\n    if (this.pendingReady.has(sessionId)) {\n      this.markReady(sessionId, session, \"quiet-fallback\", \"quiet:boot\");\n    }\n  }\n\n  private markReady(\n    sessionId: string,\n    session: InternalSession,\n    source: StatusSource,\n    reason: string,\n  ): void {\n    session.lastActivityAt = new Date();\n    session.status = \"waiting_input\";\n    session.statusSource = source;\n    session.statusUpdatedAt = new Date();\n    this.log.info(`[cursor.ready] ${sessionId.slice(0, 8)} ${reason}`, {\n      event: \"cursor.ready\",\n      sessionId,\n      reason,\n    });\n    this.onStatusChange?.(toPublicSession(session));\n    const wasPending = this.pendingReady.delete(sessionId);\n    const submitWatch = this.submitWatchTimers.get(sessionId);\n    if (submitWatch) clearTimeout(submitWatch);\n    this.submitWatchTimers.delete(sessionId);\n    if (wasPending) {\n      const timer = this.readyFallbackTimers.get(sessionId);\n      if (timer) clearTimeout(timer);\n      this.readyFallbackTimers.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    session.statusSource = \"process-exit\";\n    session.statusUpdatedAt = new Date();\n\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 = `Cursor agent 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  private clearSessionDetectors(sessionId: string): void {\n    this.quietCheckers.get(sessionId)?.cancel();\n    this.quietCheckers.delete(sessionId);\n    const ready = this.readyFallbackTimers.get(sessionId);\n    if (ready) clearTimeout(ready);\n    this.readyFallbackTimers.delete(sessionId);\n    const watch = this.submitWatchTimers.get(sessionId);\n    if (watch) clearTimeout(watch);\n    this.submitWatchTimers.delete(sessionId);\n    this.lastChunkAt.delete(sessionId);\n  }\n}\n\nfunction toPublicSession(s: InternalSession): ManagedSession {\n  return {\n    id: s.id,\n    provider: s.provider ?? CURSOR_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.failureCode != null && { failureCode: s.failureCode }),\n    ...(s.lastActivityAt != null && { lastActivityAt: s.lastActivityAt }),\n    ...(s.statusSource != null && { statusSource: s.statusSource }),\n    ...(s.statusUpdatedAt != null && { statusUpdatedAt: s.statusUpdatedAt }),\n    ...(s.filePath != null && { filePath: s.filePath }),\n    subStatus: s.subStatus ?? null,\n  };\n}\n","import type { ProviderName } from \"../providers\";\nimport type {\n  AgentPhase,\n  AskQuestion,\n  ManagedSession,\n  PermissionOption,\n  StartFreshSessionOptions,\n  StartSessionOptions,\n  UserMessage,\n} from \"../types\";\n\n/**\n * Wire protocol between the streamer and `tb-streamer pty-host`\n * (persistence plan Phase 6a).\n * See docs/plans/live-sessions-persistence-plan.md §4 and\n * docs/architecture/2026-07-24-durable-session-runtime.md (alternative D).\n *\n * The point of the host is that the PTY master fd is held by a process that is\n * NOT being restarted. Everything the audit lists as lost with the streamer —\n * node-pty, the ring buffer, the xterm screen, inputHistory, pendingReady,\n * queuedInputs, and the prompt/gate detectors — lives on the host side of this\n * boundary; the streamer keeps only a mirror it can rebuild from `status`.\n *\n * Newline-delimited JSON rather than a binary framing: PTY output is already\n * being JSON-escaped for the WebSocket hub a layer above, the volumes are a\n * terminal's worth of bytes rather than a video stream, and a protocol you can\n * read with `nc` is worth more during an incident than the bytes it saves.\n *\n * Two message directions, never mixed:\n *\n *  - **Requests** (streamer → host) each carry an `id` and are answered by\n *    exactly one `Response` with the same `id`.\n *  - **Events** (host → streamer) are unsolicited and carry no `id`. They are\n *    the transport for what are callbacks in `PTYManagerOptions` today; the\n *    detectors that fire them run in the host, so every one of those callbacks\n *    needs an event here or the feature silently stops working when the flag\n *    is on. That is why this list is longer than the sketch in the plan.\n *\n * `resize` was deliberately absent until there was a caller: sessions spawn at\n * the fixed `PTY_COLS`/`PTY_ROWS` and every headless consumer assumes that\n * size. An attached local terminal is the caller that needs it — it has a real\n * size of its own — so the verb exists now. Sessions still spawn at the\n * constants; nothing resizes unless something attached asks.\n */\n\n/**\n * Bumped on any incompatible change to the shapes below. Version 2 adds the\n * heartbeat and shutdown controls required for host supervision. Version 3\n * adds the `phase-change` event. Version 4 adds host-owned prompt occurrence\n * ids and current detector snapshots so a reconnect does not lose a prompt\n * that opened before the new streamer subscribed. Version 5 adds `raw_keys`.\n * Version 6 adds `resize`; an older host rejects either as an unknown verb.\n */\nexport const PTY_HOST_PROTOCOL_VERSION = 6;\n\nexport interface HostHeartbeatState {\n  registryState: \"known\" | \"unknown\";\n  referencedSessionIds: string[];\n}\n\n/** `spawn` with a null sessionId means startFresh — the host assigns the id. */\nexport type SpawnRequest = {\n  id: number;\n  type: \"spawn\";\n  provider: ProviderName;\n} & (\n  | { sessionId: string; options: StartSessionOptions }\n  | { sessionId: null; options: StartFreshSessionOptions }\n);\n\nexport type HostRequest =\n  | SpawnRequest\n  /** A submitted user message. Answered with the session's new promptCount. */\n  | { id: number; type: \"write\"; sessionId: string; input: string }\n  /** Raw keystrokes — never recorded as a user message. */\n  | { id: number; type: \"keys\"; sessionId: string; keys: string }\n  /** Navigation keys that must not change a waiting prompt's session status. */\n  | { id: number; type: \"raw_keys\"; sessionId: string; keys: string }\n  /**\n   * Set a session's PTY dimensions. Answered once applied; the host treats an\n   * unknown session as a no-op rather than an error, because a terminal\n   * emitting SIGWINCH races session exit by nature.\n   */\n  | { id: number; type: \"resize\"; sessionId: string; cols: number; rows: number }\n  /** Begin receiving events. Sent once per connection after status passes the version check. */\n  | { id: number; type: \"subscribe\" }\n  /** The rendered screen, newest `maxLines` rows, in true on-screen order. */\n  | { id: number; type: \"replay\"; sessionId: string; maxLines: number }\n  | { id: number; type: \"input-history\"; sessionId: string }\n  | { id: number; type: \"cancel\"; sessionId: string }\n  /**\n   * Stop a session. `hold` is `putOnHold` (SIGINT, history intact, resumable);\n   * without it, and addressed by `pid`, it is `killPid`. One verb because the\n   * host's answer is the same either way: that process is no longer running.\n   */\n  | { id: number; type: \"kill\"; sessionId: string; hold: true }\n  | { id: number; type: \"kill\"; pid: number; hold?: false }\n  | ({ id: number; type: \"heartbeat\" } & HostHeartbeatState)\n  /** Stop this host after acknowledging the request. Used only on version skew. */\n  | { id: number; type: \"shutdown-host\" }\n  /** Every session the host owns. Seeds the streamer's mirror on connect. */\n  | { id: number; type: \"status\" };\n\nexport type HostRequestType = HostRequest[\"type\"];\n\n/**\n * One response per request, correlated by `id`.\n *\n * A failure is a value, not a dropped message: a request that never gets a\n * response leaves the caller's promise pending forever, which is how a\n * transport bug turns into a hung session start rather than an error.\n */\nexport type HostResponse =\n  | { id: number; ok: true; result: unknown }\n  | { id: number; ok: false; error: string };\n\n/**\n * Unsolicited host → streamer messages. One per `PTYManagerOptions` callback,\n * plus `exit`, which is `onStatusChange` with the process actually gone.\n */\nexport type HostEvent =\n  | { type: \"event\"; event: \"output\"; sessionId: string; data: string }\n  | { type: \"event\"; event: \"status-change\"; session: ManagedSession }\n  | { type: \"event\"; event: \"ready\"; session: ManagedSession }\n  | {\n      type: \"event\";\n      event: \"permission-change\";\n      sessionId: string;\n      gate: {\n        prompt?: string;\n        detail?: string;\n        options: PermissionOption[];\n        cursor?: number;\n      } | null;\n      occurrenceId?: string;\n    }\n  /**\n   * Agent phase changed within a running turn, including to `null` at turn\n   * end. Carries the same always-present/nullable contract as the wire field:\n   * absence must never mean \"cleared\", because the consumer merges state.\n   */\n  | { type: \"event\"; event: \"phase-change\"; sessionId: string; phase: AgentPhase | null }\n  /**\n   * Composer next-prompt suggestion changed (`null` = cleared). Additive host ->\n   * server event, so PTY_HOST_PROTOCOL_VERSION is deliberately not bumped: a\n   * bump makes connect() shut down a surviving host and its sessions, and an\n   * older streamer's event switch simply ignores an unknown event.\n   */\n  | { type: \"event\"; event: \"prompt-suggestion-change\"; sessionId: string; text: string | null }\n  | {\n      type: \"event\";\n      event: \"live-question\";\n      sessionId: string;\n      questions: AskQuestion[];\n      occurrenceId: string;\n    }\n  | { type: \"event\"; event: \"live-question-gone\"; sessionId: string }\n  | { type: \"event\"; event: \"user-message\"; sessionId: string; text: string; ts: number }\n  | { type: \"event\"; event: \"exit\"; sessionId: string; exitCode: number };\n\nexport type HostMessage = HostResponse | HostEvent;\n\nexport function isHostEvent(message: HostMessage): message is HostEvent {\n  return \"type\" in message && message.type === \"event\";\n}\n\n/**\n * A request minus the `id` the sender assigns.\n *\n * Distributive on purpose: a plain `Omit<HostRequest, \"id\">` collapses the\n * union into its common keys, which loses `sessionId`, `provider` and the rest\n * — so every call site fails to type-check against a request shape that is\n * actually correct.\n */\nexport type HostRequestBody = HostRequest extends infer T\n  ? T extends { id: number }\n    ? Omit<T, \"id\">\n    : never\n  : never;\n\n/**\n * A session plus the one thing `ManagedSession` does not carry: the OS pid.\n *\n * The in-process runners read it off the node-pty handle, which lives on the\n * host now — and the durable registry needs it at spawn to record something a\n * later boot can probe. It is fixed for the life of a session, so `spawn` and\n * `status` are the only messages that carry it.\n */\nexport interface HostSession {\n  session: ManagedSession;\n  pid: number | null;\n}\n\n/** Result shapes, named so both ends agree on what a given request answers. */\nexport interface StatusResult {\n  protocolVersion: number;\n  sessions: HostSession[];\n  promptSnapshots?: HostPromptSnapshot[];\n}\nexport interface SubscribeResult {\n  promptSnapshots: HostPromptSnapshot[];\n}\n\nexport type HostPromptSnapshot =\n  | {\n      kind: \"permission\";\n      sessionId: string;\n      occurrenceId: string;\n      gate: NonNullable<\n        Parameters<NonNullable<import(\"../types\").PTYManagerOptions[\"onPermissionChange\"]>>[1]\n      >;\n    }\n  | {\n      kind: \"question\";\n      sessionId: string;\n      occurrenceId: string;\n      questions: AskQuestion[];\n    };\nexport interface InputHistoryResult {\n  history: UserMessage[];\n}\nexport interface ReplayResult {\n  lines: string[];\n  /** The raw ring buffer, so a reconnecting streamer can restore `getOutput`. */\n  output: string;\n}\n\n/**\n * A duplex line channel. Deliberately not a socket: the runner is tested\n * against an in-memory pair, and PR 7 supplies the real one over a unix socket\n * (POSIX) or named pipe (Windows).\n */\nexport interface HostTransport {\n  send(line: string): void;\n  onLine(handler: (line: string) => void): void;\n  onClose(handler: () => void): void;\n  close(): void;\n}\n\n/** `Date` survives JSON as a string; every session field that is one is listed here. */\nconst SESSION_DATE_FIELDS = [\n  \"startedAt\",\n  \"completedAt\",\n  \"statusUpdatedAt\",\n  \"lastActivityAt\",\n  \"firstMessageAt\",\n  \"lastMessageAt\",\n] as const;\n\n/**\n * Restore a `ManagedSession` that crossed the wire.\n *\n * JSON turns every `Date` into a string, and `ManagedSession.startedAt` is read\n * as a Date all over the server (`elapsedMs` arithmetic, `toISOString()`).\n * Without this the failure is a string where a Date is expected — which does\n * not throw, it produces `NaN` elapsed times and a crash only at the\n * `.toISOString()` call sites.\n */\nexport function reviveSession(raw: unknown): ManagedSession {\n  const s = { ...(raw as Record<string, unknown>) };\n  for (const field of SESSION_DATE_FIELDS) {\n    const value = s[field];\n    if (typeof value === \"string\") s[field] = new Date(value);\n  }\n  return s as unknown as ManagedSession;\n}\n\nexport function encodeMessage(message: HostRequest | HostMessage): string {\n  return `${JSON.stringify(message)}\\n`;\n}\n\n/**\n * Reassemble newline-delimited JSON from arbitrarily-chunked reads.\n *\n * A socket splits wherever it likes, so \"one chunk is one message\" holds right\n * up until a PTY burst makes it false. Stateful for exactly that reason: the\n * tail of a chunk is held until its newline arrives.\n */\nexport class LineDecoder {\n  private buffer = \"\";\n\n  push(chunk: string): string[] {\n    this.buffer += chunk;\n    const lines = this.buffer.split(\"\\n\");\n    // The final element is whatever followed the last newline — an empty string\n    // when the chunk ended cleanly, a partial message otherwise. Either way it\n    // is not a complete line yet.\n    this.buffer = lines.pop() ?? \"\";\n    return lines.filter((line) => line.length > 0);\n  }\n}\n","import type { ProviderName } from \"../providers\";\nimport type {\n  ManagedSession,\n  PTYManagerOptions,\n  SessionRunner,\n  StartFreshSessionOptions,\n  StartSessionOptions,\n  UserMessage,\n} from \"../types\";\nimport {\n  encodeMessage,\n  type HostEvent,\n  type HostHeartbeatState,\n  type HostMessage,\n  type HostRequest,\n  type HostRequestBody,\n  type HostSession,\n  type HostTransport,\n  type InputHistoryResult,\n  isHostEvent,\n  LineDecoder,\n  PTY_HOST_PROTOCOL_VERSION,\n  type ReplayResult,\n  reviveSession,\n  type StatusResult,\n  type SubscribeResult,\n} from \"./protocol\";\n\nexport class PtyHostProtocolMismatchError extends Error {\n  constructor(\n    readonly hostVersion: number,\n    readonly streamerVersion: number,\n  ) {\n    super(\n      `pty-host protocol ${hostVersion} is incompatible with streamer protocol ${streamerVersion}`,\n    );\n    this.name = \"PtyHostProtocolMismatchError\";\n  }\n}\n\nexport const HOST_HEARTBEAT_INTERVAL_MS = 10_000;\nexport const HOST_HEARTBEAT_REQUEST_TIMEOUT_MS = 5_000;\nexport const HOST_SHUTDOWN_REQUEST_TIMEOUT_MS = 1_000;\n\n/**\n * `SessionRunner` backed by an out-of-process pty-host (plan Phase 6a).\n *\n * `LiveSessionManager` selects this runner at boot when the `ptyHost` feature\n * flag is enabled; the default-off path keeps the in-process runners.\n *\n * **The mirror is the whole design problem.** `SessionRunner` is mostly\n * synchronous — `hasSession`, `getPid`, `listSessions`, `getOutput` and\n * `sendInput` all return values, not promises — and they are called from\n * request handlers that cannot await a socket round-trip. So the runner keeps a\n * local copy of session state, seeded by `status` on connect and kept current\n * by pushed events. Reads answer from the mirror; only mutations and the two\n * genuinely async methods cross the wire.\n *\n * The consequence worth stating: a mirror can be stale, and the honest failure\n * for a stale mirror is the same one an in-process runner gives for an unknown\n * session — throw. It must never invent a session, and it must never answer\n * \"no such session\" for one the host is holding, which is why `status` is\n * awaited before the runner is handed out (see `connect`).\n */\nexport class RemoteSessionRunner implements SessionRunner {\n  private transport: HostTransport;\n  private options: PTYManagerOptions;\n  private decoder = new LineDecoder();\n  private nextRequestId = 1;\n  private pending = new Map<\n    number,\n    {\n      resolve: (value: unknown) => void;\n      reject: (err: Error) => void;\n      timeout: ReturnType<typeof setTimeout> | null;\n    }\n  >();\n\n  /** The mirror. Rebuilt wholesale by `status`, patched by events. */\n  private sessions = new Map<string, ManagedSession>();\n  /** Ring buffers, fed by `output` events so `getOutput` stays synchronous. */\n  private output = new Map<string, string>();\n  private inputHistory = new Map<string, UserMessage[]>();\n  /** Fixed for a session's lifetime, so only spawn and status carry it. */\n  private pids = new Map<string, number | null>();\n  private closed = false;\n  private heartbeatTimer: ReturnType<typeof setInterval> | null = null;\n  private heartbeatInFlight = false;\n\n  /**\n   * The only supported way to build one: a runner whose mirror has not been\n   * seeded yet would answer `hasSession` with a confident, wrong `false` for\n   * every session the host is holding — which reads as \"the agent is gone\" and\n   * routes the user to start a new one.\n   */\n  static async connect(\n    transport: HostTransport,\n    options: PTYManagerOptions = {},\n  ): Promise<RemoteSessionRunner> {\n    const runner = new RemoteSessionRunner(transport, options);\n    const status = await runner.readStatus();\n    if (status.protocolVersion !== PTY_HOST_PROTOCOL_VERSION) {\n      try {\n        await runner.request({ type: \"shutdown-host\" }, HOST_SHUTDOWN_REQUEST_TIMEOUT_MS);\n      } catch (err) {\n        options.logger?.warn(\"[pty-host] incompatible host did not acknowledge shutdown\", {\n          event: \"pty_host.shutdown_failed\",\n          err,\n        });\n      } finally {\n        runner.dispose();\n      }\n      throw new PtyHostProtocolMismatchError(status.protocolVersion, PTY_HOST_PROTOCOL_VERSION);\n    }\n    const subscribed = (await runner.request({ type: \"subscribe\" })) as SubscribeResult;\n    runner.refreshMirror(status);\n    runner.restorePromptSnapshots({\n      ...status,\n      promptSnapshots: subscribed.promptSnapshots ?? status.promptSnapshots,\n    });\n    return runner;\n  }\n\n  private constructor(transport: HostTransport, options: PTYManagerOptions) {\n    this.transport = transport;\n    this.options = options;\n    transport.onLine((line) => this.handleLine(line));\n    transport.onClose(() => this.handleClose());\n  }\n\n  // ─── Transport plumbing ──────────────────────────────────────────\n\n  private handleLine(line: string): void {\n    for (const complete of this.decoder.push(line)) {\n      let message: HostMessage;\n      try {\n        message = JSON.parse(complete) as HostMessage;\n      } catch {\n        // A malformed line is the host's bug, not a reason to tear down every\n        // live session. Drop it and keep reading.\n        this.options.logger?.warn(\"[pty-host] dropped unparseable message\", {\n          event: \"pty_host.bad_message\",\n        });\n        continue;\n      }\n      if (isHostEvent(message)) {\n        this.handleEvent(message);\n        continue;\n      }\n      const waiter = this.pending.get(message.id);\n      if (!waiter) continue;\n      this.pending.delete(message.id);\n      if (waiter.timeout) clearTimeout(waiter.timeout);\n      if (message.ok) waiter.resolve(message.result);\n      else waiter.reject(new Error(message.error));\n    }\n  }\n\n  /**\n   * Fail every in-flight request when the socket drops.\n   *\n   * Without this each one stays pending forever and the caller — a session\n   * start, an input write — hangs rather than erroring. PR 9 adds reconnection;\n   * until then a dropped host is a hard failure that says so.\n   */\n  private handleClose(): void {\n    if (this.closed) return;\n    this.closed = true;\n    this.stopHeartbeat();\n    const err = new Error(\"pty-host connection closed\");\n    for (const waiter of this.pending.values()) {\n      if (waiter.timeout) clearTimeout(waiter.timeout);\n      waiter.reject(err);\n    }\n    this.pending.clear();\n  }\n\n  private request(body: HostRequestBody, timeoutMs?: number): Promise<unknown> {\n    if (this.closed) return Promise.reject(new Error(\"pty-host connection closed\"));\n    const id = this.nextRequestId++;\n    return new Promise((resolve, reject) => {\n      const timeout =\n        timeoutMs === undefined\n          ? null\n          : setTimeout(() => {\n              if (!this.pending.delete(id)) return;\n              reject(new Error(`${body.type} timed out after ${timeoutMs}ms`));\n            }, timeoutMs);\n      timeout?.unref?.();\n      this.pending.set(id, { resolve, reject, timeout });\n      this.transport.send(encodeMessage({ ...body, id } as HostRequest));\n    });\n  }\n\n  /**\n   * Fire-and-forget for the synchronous parts of `SessionRunner`.\n   *\n   * `sendKeys`, `cancel`, `killPid` and `putOnHold` all return void, so there is\n   * no channel to report a failure through even if we waited for one. The\n   * response is still consumed — an unhandled rejection would take the process\n   * down over a keystroke that failed to land.\n   */\n  private fireAndForget(body: HostRequestBody): void {\n    this.request(body).catch((err) => {\n      this.options.logger?.warn(\"[pty-host] request failed\", {\n        event: \"pty_host.request_failed\",\n        type: body.type,\n        err,\n      });\n    });\n  }\n\n  private async readStatus(): Promise<StatusResult> {\n    return (await this.request({ type: \"status\" })) as StatusResult;\n  }\n\n  private refreshMirror(status: StatusResult): void {\n    this.sessions = new Map();\n    this.pids = new Map();\n    for (const entry of status.sessions) {\n      const session = reviveSession(entry.session);\n      this.sessions.set(session.id, session);\n      this.pids.set(session.id, entry.pid);\n    }\n  }\n\n  private restorePromptSnapshots(status: StatusResult): void {\n    for (const snapshot of status.promptSnapshots ?? []) {\n      if (snapshot.kind === \"permission\") {\n        this.options.onPermissionChange?.(snapshot.sessionId, snapshot.gate, snapshot.occurrenceId);\n      } else {\n        this.options.onLiveQuestion?.(\n          snapshot.sessionId,\n          snapshot.questions,\n          snapshot.occurrenceId,\n        );\n      }\n    }\n  }\n\n  async heartbeat(\n    state: HostHeartbeatState,\n    timeoutMs: number = HOST_HEARTBEAT_REQUEST_TIMEOUT_MS,\n  ): Promise<void> {\n    await this.request({ type: \"heartbeat\", ...state }, timeoutMs);\n  }\n\n  startHeartbeat(\n    getState: () => HostHeartbeatState,\n    intervalMs: number = HOST_HEARTBEAT_INTERVAL_MS,\n  ): void {\n    this.stopHeartbeat();\n    const send = () => {\n      if (this.closed || this.heartbeatInFlight) return;\n      this.heartbeatInFlight = true;\n      void Promise.resolve()\n        .then(() => this.heartbeat(getState()))\n        .catch((err) => {\n          if (this.closed) return;\n          this.options.logger?.warn(\"[pty-host] heartbeat failed\", {\n            event: \"pty_host.heartbeat_failed\",\n            err,\n          });\n        })\n        .finally(() => {\n          this.heartbeatInFlight = false;\n        });\n    };\n    send();\n    this.heartbeatTimer = setInterval(send, intervalMs);\n    this.heartbeatTimer.unref?.();\n  }\n\n  private stopHeartbeat(): void {\n    if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);\n    this.heartbeatTimer = null;\n  }\n\n  // ─── Events ──────────────────────────────────────────────────────\n\n  private handleEvent(event: HostEvent): void {\n    switch (event.event) {\n      case \"output\": {\n        this.output.set(event.sessionId, (this.output.get(event.sessionId) ?? \"\") + event.data);\n        this.options.onOutput?.(event.sessionId, event.data);\n        break;\n      }\n      case \"status-change\": {\n        const session = reviveSession(event.session);\n        if (session.status === \"idle\" && session.completedAt != null) {\n          this.sessions.delete(session.id);\n          this.pids.delete(session.id);\n          this.output.delete(session.id);\n          this.inputHistory.delete(session.id);\n        } else {\n          this.sessions.set(session.id, session);\n        }\n        this.options.onStatusChange?.(session);\n        break;\n      }\n      case \"ready\": {\n        const session = reviveSession(event.session);\n        this.sessions.set(session.id, session);\n        this.options.onReady?.(session);\n        break;\n      }\n      case \"permission-change\":\n        this.options.onPermissionChange?.(event.sessionId, event.gate, event.occurrenceId);\n        break;\n      case \"phase-change\":\n        this.options.onPhaseChange?.(event.sessionId, event.phase);\n        break;\n      case \"prompt-suggestion-change\":\n        this.options.onPromptSuggestionChange?.(event.sessionId, event.text);\n        break;\n      case \"live-question\":\n        this.options.onLiveQuestion?.(event.sessionId, event.questions, event.occurrenceId);\n        break;\n      case \"live-question-gone\":\n        this.options.onLiveQuestionGone?.(event.sessionId);\n        break;\n      case \"user-message\": {\n        const history = this.inputHistory.get(event.sessionId) ?? [];\n        history.push({ text: event.text, ts: event.ts });\n        this.inputHistory.set(event.sessionId, history);\n        this.options.onUserMessage?.(event.sessionId, event.text, event.ts);\n        break;\n      }\n      case \"exit\": {\n        // Drop it from the mirror, matching what the in-process runners do on\n        // exit — `handleExit` deletes from their map, which is what makes a\n        // held session read as absent rather than as `status: \"idle\"`.\n        this.sessions.delete(event.sessionId);\n        this.pids.delete(event.sessionId);\n        this.output.delete(event.sessionId);\n        this.inputHistory.delete(event.sessionId);\n        break;\n      }\n    }\n  }\n\n  // ─── SessionRunner ───────────────────────────────────────────────\n\n  async start(sessionId: string, options: StartSessionOptions): Promise<ManagedSession> {\n    const provider = (options as { provider?: ProviderName }).provider ?? \"claude-code\";\n    return this.adopt(await this.request({ type: \"spawn\", provider, sessionId, options }));\n  }\n\n  async startFresh(options: StartFreshSessionOptions): Promise<ManagedSession> {\n    const provider = (options as { provider?: ProviderName }).provider ?? \"claude-code\";\n    return this.adopt(await this.request({ type: \"spawn\", provider, sessionId: null, options }));\n  }\n\n  /**\n   * Take a spawn answer into the mirror.\n   *\n   * The pid lands here and nowhere else in the live path: `recordSessionSpawn`\n   * reads it immediately after start to write the durable registry row, and a\n   * null there costs the next boot its ability to probe whether the agent\n   * outlived us.\n   */\n  private adopt(raw: unknown): ManagedSession {\n    const entry = raw as HostSession;\n    const session = reviveSession(entry.session);\n    this.sessions.set(session.id, session);\n    this.pids.set(session.id, entry.pid);\n    return session;\n  }\n\n  /**\n   * Returns the mirror's promptCount, optimistically incremented.\n   *\n   * The interface is synchronous, so there is no way to return the host's\n   * authoritative count. The increment matches what an in-process runner does\n   * for the same call, and the next `status-change` event overwrites it — so a\n   * mirror that guessed wrong is corrected within one round trip rather than\n   * drifting.\n   */\n  sendInput(sessionId: string, input: string): number {\n    const session = this.requireSession(sessionId);\n    this.fireAndForget({ type: \"write\", sessionId, input });\n    session.promptCount += 1;\n    return session.promptCount;\n  }\n\n  sendKeys(sessionId: string, keys: string): void {\n    this.requireSession(sessionId);\n    this.fireAndForget({ type: \"keys\", sessionId, keys });\n  }\n\n  sendRawKeys(sessionId: string, keys: string): void {\n    this.requireSession(sessionId);\n    this.fireAndForget({ type: \"raw_keys\", sessionId, keys });\n  }\n\n  resize(sessionId: string, cols: number, rows: number): void {\n    // No requireSession: unlike keys, a resize for a session that just exited\n    // is meaningless rather than wrong, and SIGWINCH races exit by nature.\n    if (!Number.isInteger(cols) || !Number.isInteger(rows) || cols < 1 || rows < 1) return;\n    this.fireAndForget({ type: \"resize\", sessionId, cols, rows });\n  }\n\n  cancel(sessionId: string): void {\n    this.fireAndForget({ type: \"cancel\", sessionId });\n  }\n\n  killPid(pid: number): void {\n    this.fireAndForget({ type: \"kill\", pid });\n  }\n\n  // ponytail: `signal` is accepted for interface parity but not forwarded —\n  // the \"kill\" wire message has no signal field (PTY_HOST_PROTOCOL_VERSION 6),\n  // so a host-backed session always gets the host's existing SIGINT hold\n  // regardless of what's asked for. Bump the protocol if a forced SIGKILL\n  // needs to reach a session running under `ptyHost` (off by default).\n  putOnHold(sessionId: string, _signal: NodeJS.Signals = \"SIGINT\"): void {\n    this.fireAndForget({ type: \"kill\", sessionId, hold: true });\n    // Matches PTYManager.putOnHold, which deletes from its map — a held session\n    // must read as absent from the runner, not as an idle one it still owns.\n    this.sessions.delete(sessionId);\n    this.pids.delete(sessionId);\n  }\n\n  getOutput(sessionId: string): string {\n    this.requireSession(sessionId);\n    return this.output.get(sessionId) ?? \"\";\n  }\n\n  async getOutputLines(sessionId: string, maxLines: number): Promise<string[]> {\n    const result = (await this.request({ type: \"replay\", sessionId, maxLines })) as ReplayResult;\n    // The host renders from the xterm screen it owns; seed the local ring buffer\n    // from the same answer so a reconnecting streamer's getOutput is not empty.\n    if (typeof result.output === \"string\") this.output.set(sessionId, result.output);\n    return result.lines;\n  }\n\n  /**\n   * Synchronous, so it answers from the mirror rather than the host.\n   *\n   * Seeded lazily: `user-message` events append as they arrive, and a session\n   * this streamer did not start has none until `hydrateInputHistory` fetches\n   * them. Empty is the same answer an in-process runner gives for an unknown\n   * session, so a caller cannot tell \"none yet\" from \"not fetched\" — which is\n   * why the fetch is explicit rather than hidden behind this getter.\n   */\n  getInputHistory(sessionId: string): UserMessage[] {\n    return this.inputHistory.get(sessionId) ?? [];\n  }\n\n  /** Pull a session's recorded messages from the host into the mirror. */\n  async hydrateInputHistory(sessionId: string): Promise<UserMessage[]> {\n    const result = (await this.request({\n      type: \"input-history\",\n      sessionId,\n    })) as InputHistoryResult;\n    this.inputHistory.set(sessionId, result.history);\n    return result.history;\n  }\n\n  getPid(sessionId: string): number | null {\n    return this.pids.get(sessionId) ?? null;\n  }\n\n  getSession(sessionId: string): ManagedSession | null {\n    return this.sessions.get(sessionId) ?? null;\n  }\n\n  hasSession(sessionId: string): boolean {\n    return this.sessions.has(sessionId);\n  }\n\n  listSessions(): ManagedSession[] {\n    return [...this.sessions.values()];\n  }\n\n  /**\n   * Drops this streamer's connection and nothing else.\n   *\n   * Emphatically NOT the in-process `dispose()`, which signals every child. The\n   * entire point of the host is that its PTYs outlive the streamer, so tearing\n   * them down here would spend the feature to implement a method name.\n   */\n  dispose(): void {\n    this.handleClose();\n    this.transport.close();\n  }\n\n  private requireSession(sessionId: string): ManagedSession {\n    const session = this.sessions.get(sessionId);\n    if (!session) throw new Error(`Session not found: ${sessionId}`);\n    return session;\n  }\n}\n","import { randomUUID } from \"crypto\";\nimport { existsSync } from \"fs\";\nimport { basename } from \"path\";\nimport { buildFlagArgs, buildSettingsJson } from \"./claude-flags\";\nimport { getLogger, type Logger } from \"./logger\";\nimport { clearClaudeExeCache, resolveClaudeExe } from \"./platform\";\nimport { CLAUDE_CODE_PROVIDER } from \"./providers\";\nimport { createScreen, type InternalSession, loadPty, PTY_ROWS, stripAnsi } from \"./pty-shared\";\nimport {\n  detectGateScreen,\n  detectPickerScreen,\n  hasPermissionOsc,\n  hasWaitingForInputOsc,\n  type PermissionGate,\n  permissionContentKey,\n  scrapePermissionGate,\n} from \"./services/questions/detectPermissionGate\";\nimport { readPromptSuggestion } from \"./services/questions/detectPromptSuggestion\";\nimport {\n  detectQuestionFromScreen,\n  questionContentKey,\n} from \"./services/questions/detectQuestionFromScreen\";\nimport { detectShellPrompt } from \"./services/questions/detectShellPrompt\";\nimport { detectStartupChoiceGate } from \"./services/questions/detectStartupChoiceGate\";\nimport { parseAgentPhase } from \"./services/questions/parseAgentPhase\";\nimport { isPermissionAnswer } from \"./services/questions/permissionAnswerKeys\";\nimport type {\n  AgentPhase,\n  ManagedSession,\n  PTYManagerOptions,\n  SessionRunner,\n  StartFreshSessionOptions,\n  StartSessionOptions,\n  StatusSource,\n  UserMessage,\n} from \"./types\";\nimport { debounce } from \"./utils/debounce\";\nimport { deriveSessionName } from \"./utils/deriveSessionName\";\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 render terminal, node-pty loading, the session shape and\n// ANSI stripping are shared with codex-pty-runner.ts — see pty-shared.ts.\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// Claude's turn signal: OSC 9;4 (terminal progress) — `9;4;3` when a turn\n// starts, `9;4;0` exactly when it ends, including across a permission gate\n// (verified on Claude Code v2.1.278). Mid-conversation the `❯` input box stays\n// painted for the whole turn, so the marker alone flipped the session to\n// waiting_input ~50ms after every submit and the \"waiting for your input\" push\n// fired as the agent started. While a turn is open the markers are ignored.\n// biome-ignore lint/suspicious/noControlCharactersInRegex: matching the literal OSC 9;4 escape\nconst CLAUDE_PROGRESS_RE = /\\x1b\\]9;4;(\\d)/g;\n\n// A submit opens the turn before Claude's `9;4;3` arrives (~300ms later),\n// because the echo of the pasted prompt already carries `❯`. A submit that\n// starts no turn at all (a slash command) never sends one, so the optimistic\n// open lapses after this long and readiness is re-checked from the screen.\nconst TURN_START_GRACE_MS = 3_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 CLAUDE_READY_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// Raw tail carried across chunks for the OSC 777 regexes. node-pty chunk\n// boundaries are arbitrary, and the ~54-char notify escape matched NEITHER\n// half when split. 128 chars covers the escape with margin, tmux-wrapped or\n// not.\nconst OSC_TAIL_CHARS = 128;\n\n// Ceiling on unsolicited rendered-screen scrapes: a chunk's arrival alone\n// may trigger a full detection pass at most this often. Exists because the\n// gate paint has NO reliable chunk-level signal (the TUI paints\n// cursor-addressed fragments and the box gutter defeats the hint regex)\n// while Claude debounces its OSC 777 notify ~6s — see\n// docs/superpowers/specs/2026-08-08-paint-time-gate-detection-design.md.\n// 300ms = half the blink cadence of a waiting gate (~450-620ms observed), so\n// worst-case added card latency is one tick and scrape work is ≤ ~3/s per\n// active session (idle sessions emit no chunks, so they never scrape).\nconst SCRAPE_THROTTLE_MS = 300;\n\n// Flat backstop armed at spawn, mirroring CODEX_READY_FALLBACK_MS. It replaces\n// a chunk-gated 10s check that could only fire if another chunk happened to\n// arrive — useless for the case it was written for, a boot that falls silent\n// without ever painting a marker. A timer fires either way.\n//\n// The quiet-checker cannot fill this role: a resume replaying a large JSONL is\n// quiet for well over QUIET_DETECT_MS *before* the TUI reads stdin, so treating\n// that silence as readiness disarms the input queue during exactly the window\n// it exists to protect. Must stay below server.ts START_READY_TIMEOUT_MS (10s)\n// so a start request resolves on our verdict rather than racing its timeout.\nconst CLAUDE_READY_FALLBACK_MS = 8_000;\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\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  // Claude Code reads TERM to decide whether the terminal can render its Unicode\n  // chrome, and with TERM absent it paints ASCII fallbacks: `>` for the composer\n  // prompt instead of `❯`, and `>` for the gate selection cursor. The composer\n  // one is the damaging half — CLAUDE_PROMPT_MARKERS is `[\"╭\", \"❯\"]`, so with\n  // neither glyph ever painted `hasPromptMarker` is permanently false, a session\n  // never leaves `running`, and mobile shows \"Working\" for a session that has\n  // been idle for hours.\n  //\n  // TERM is absent here for the normal deployment, not an exotic one: a\n  // supervised streamer (Task Scheduler / launchd / systemd) inherits no TERM of\n  // its own, and node-pty's `name` option sets TERM on POSIX but does not reach\n  // the child through Windows ConPTY. So the spawn declares `xterm-256color` in\n  // `name` while the child sees no TERM at all — this makes the two agree.\n  //\n  // Measured A/B, same binary, same `name`, same cwd, only the env differing:\n  // TERM absent -> the composer paints `> `; TERM=xterm-256color -> `❯ `.\n  // (`WT_SESSION` flips it too, which is why a hand-run `claude` in Windows\n  // Terminal never showed this and only the supervised spawn did.)\n  if (!env.TERM) {\n    env.TERM = \"xterm-256color\";\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 onPhaseChange: PTYManagerOptions[\"onPhaseChange\"];\n  private onPromptSuggestionChange: PTYManagerOptions[\"onPromptSuggestionChange\"];\n  private onLiveQuestion: PTYManagerOptions[\"onLiveQuestion\"];\n  private onLiveQuestionGone: PTYManagerOptions[\"onLiveQuestionGone\"];\n  private onUserMessage: PTYManagerOptions[\"onUserMessage\"];\n  // Per-session permission-gate state: the gate currently open on a session's\n  // screen. Set between an OSC 777 / paint-time claim (gate open) and the next\n  // prompt-ready without a fresh 777 (gate closed). Prevents re-broadcasting\n  // open/close on every chunk. Holds the gate itself rather than just the id so\n  // sendKeys can tell whether the bytes it is writing are that gate's answer.\n  private permissionOpen = new Map<string, PermissionGate>();\n  // Last chunk's raw tail per session — prepended to the next chunk before\n  // the OSC regex test so a split escape still matches. Consumed on match.\n  private oscTail = new Map<string, string>();\n  // Open turn per session, from OSC 9;4: \"submitted\" between our submit and\n  // Claude's 9;4;3, \"working\" after it. Absent = no turn open.\n  private turnOpen = new Map<string, \"submitted\" | \"working\">();\n  // Sessions whose Claude has emitted OSC 9;4 at all — an older CLI that never\n  // does keeps the marker-only behaviour instead of waiting on a signal.\n  private progressSeen = new Set<string>();\n  // An OSC 9;4 escape cut off at the end of the previous chunk, if any.\n  private progressTail = new Map<string, string>();\n  // When the last full detection pass ran per session (any trigger) — the\n  // clock the SCRAPE_THROTTLE_MS ceiling is measured against.\n  private lastDetectAt = new Map<string, number>();\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  // Content key (permissionContentKey, cursor excluded — see arm 3) of the\n  // gate that was still painted on screen when arm 2 closed it. Suppresses\n  // arm 3 re-claiming that same box from a later trigger-less/throttled pass\n  // before Claude erases it — otherwise the paint-time claim reopens the\n  // card it just closed. Cleared once detectGateScreen sees the box is gone,\n  // so a genuinely new (even content-identical) gate is claimed normally.\n  private closedGateKey = 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; used for the [pty.ready] elapsed\n  // measurement so a slow boot is visible in the logs.\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  // Per-session flat backstop from the first chunk (CLAUDE_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.onPhaseChange = options.onPhaseChange;\n    this.onPromptSuggestionChange = options.onPromptSuggestionChange;\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` — the safe default that\n  // auto-approves file edits while still prompting for shell commands. All six\n  // Claude CLI modes are accepted (see PERMISSION_MODES in claude-flags.ts).\n  //\n  // On the bypass modes: `bypassPermissions`/`dontAsk` DO trigger a blocking\n  // \"Bypass Permissions mode\" warning menu at boot (\"1. No, exit\" /\n  // \"2. Yes, I accept\") which would strand the PTY and leave mobile on an empty\n  // screen. buildSettingsJson() suppresses it by adding\n  // `skipDangerousModePermissionPrompt` to the `--settings` blob for exactly\n  // those modes — probe-verified on Claude Code v2.1.218. We never pass\n  // `--dangerously-skip-permissions`; bypass is requested via --permission-mode.\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 permissionMode = options.permissionMode ?? \"acceptEdits\";\n    const args = [\n      \"--permission-mode\",\n      permissionMode,\n      \"--settings\",\n      buildSettingsJson(permissionMode),\n      \"--model\",\n      options.model ?? \"sonnet\",\n      \"--effort\",\n      options.effort ?? \"low\",\n      \"--resume\",\n      sessionId,\n    ];\n    // Allowlisted per-server flags, then the free-text escape hatch — last so it\n    // can override anything above it.\n    args.push(...buildFlagArgs(options.claudeFlags, options.claudeExtraArgs));\n\n    let proc: ReturnType<typeof nodePty.spawn>;\n    try {\n      proc = nodePty.spawn(resolveClaudeExe(), args, {\n        name: \"xterm-256color\",\n        cols: 120,\n        rows: 40,\n        cwd: options.projectPath,\n        env: buildSpawnEnv(),\n      });\n    } catch (err) {\n      // The cached path resolved to something CreateProcess can't launch\n      // (stale after a reinstall, or a since-fixed bad match) — clear it so\n      // the next start/resume attempt re-resolves instead of repeating the\n      // same failure indefinitely.\n      clearClaudeExeCache();\n      throw err;\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      ...(options.effort != null && { effort: options.effort }),\n      status: \"running\",\n      statusSource: \"spawn\",\n      statusUpdatedAt: new Date(),\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    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 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. Guarded by __tests__/pty-ready-detection.test.ts.\n    const permissionMode = options.permissionMode ?? \"acceptEdits\";\n    const args = [\n      \"--permission-mode\",\n      permissionMode,\n      \"--settings\",\n      buildSettingsJson(permissionMode),\n      \"--model\",\n      options.model ?? \"sonnet\",\n      \"--effort\",\n      options.effort ?? \"low\",\n      \"--session-id\",\n      sessionId,\n    ];\n    if (options.systemPrompt) {\n      args.push(\"--system-prompt\", options.systemPrompt);\n    }\n    // Allowlisted per-server flags, then the free-text escape hatch — last so it\n    // can override anything above it.\n    args.push(...buildFlagArgs(options.claudeFlags, options.claudeExtraArgs));\n\n    let proc: ReturnType<typeof nodePty.spawn>;\n    try {\n      proc = nodePty.spawn(resolveClaudeExe(), args, {\n        name: \"xterm-256color\",\n        cols: 120,\n        rows: 40,\n        cwd: options.projectPath,\n        env: buildSpawnEnv(),\n      });\n    } catch (err) {\n      // See the analogous catch in doStart() above.\n      clearClaudeExeCache();\n      throw err;\n    }\n\n    const session: InternalSession = {\n      id: sessionId,\n      provider: CLAUDE_CODE_PROVIDER,\n      projectPath: options.projectPath,\n      projectName,\n      branch: \"\",\n      ...(options.effort != null && { effort: options.effort }),\n      status: \"running\",\n      statusSource: \"spawn\",\n      statusUpdatedAt: new Date(),\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  // 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      // Observed by construction: we are the ones writing the input.\n      session.statusSource = \"user-input\";\n      session.statusUpdatedAt = new Date();\n      // Sending anything consumes the suggestion — don't wait for a scrape.\n      this.setPromptSuggestion(sessionId, session, null);\n      this.onStatusChange?.(toPublicSession(session));\n    }\n    this.log.info(`[pty.keys.write] ${sessionId.slice(0, 8)} bytes=${keys.length}`, {\n      event: \"pty.keys_write\",\n      sessionId,\n      byteLen: keys.length,\n    });\n    session.process.write(keys);\n    session.lastActivityAt = new Date();\n    // These bytes answer the gate this session has open, so the box is gone as\n    // soon as Claude reads them. The detector cannot see that: it closes on the\n    // end-of-turn OSC or a prompt marker, and an approved tool run produces\n    // neither until the whole turn ends — which left the card up for the rest of\n    // the turn. Close it here, the one choke point every *card* answer goes\n    // through, so `permissionOpen` and the server's `pendingPermission` retire\n    // together (onPermissionChange(null) is what broadcasts permission_cancelled).\n    //\n    // NOT every way a gate gets answered: `/input` splits on the body field, so\n    // a `{input}` composer send reaches sendInput() instead and still closes the\n    // old way, at end of turn. That is deliberate — pattern-matching composer\n    // prose into a gate answer is the false positive this rule exists to avoid.\n    //\n    // Optimistic by design: `closedGateKey` is deliberately NOT set here. If the\n    // keystroke doesn't take and the box stays painted, the paint-time claim\n    // reopens the card on the next pass — a gate that is still on screen must\n    // never stay hidden. If it did take, the box is gone and nothing reclaims it.\n    const openGate = this.permissionOpen.get(sessionId);\n    if (openGate && isPermissionAnswer(openGate, keys)) {\n      this.permissionOpen.delete(sessionId);\n      this.onPermissionChange?.(sessionId, null);\n    }\n  }\n\n  // Navigation keys move a focused picker without submitting user input. Keep\n  // its waiting_input status intact so clients retain the prompt identity for\n  // a multi-key sequence (for example Down, Down, Enter).\n  sendRawKeys(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\") throw new Error(`Session is idle (no active PTY): ${sessionId}`);\n    this.log.info(`[pty.raw_keys.write] ${sessionId.slice(0, 8)} bytes=${keys.length}`, {\n      event: \"pty.raw_keys_write\",\n      sessionId,\n      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      // Observed by construction: we are the ones writing the input.\n      session.statusSource = \"user-input\";\n      session.statusUpdatedAt = new Date();\n      // Sending anything consumes the suggestion — don't wait for a scrape.\n      this.setPromptSuggestion(sessionId, session, null);\n      this.onStatusChange?.(toPublicSession(session));\n    }\n    this.openTurnOnSubmit(sessionId);\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}`,\n      {\n        event: \"pty.input_write\",\n        sessionId,\n        promptCount,\n        byteLen: pasteBytes.length,\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  /**\n   * Resize a live PTY. Silent when the session is unknown or has no process:\n   * a terminal emitting SIGWINCH races session exit by nature, and throwing\n   * would turn an ordinary window drag into an error the caller must catch.\n   */\n  resize(sessionId: string, cols: number, rows: number): void {\n    const session = this.sessions.get(sessionId);\n    if (!session?.process) return;\n    if (!Number.isInteger(cols) || !Number.isInteger(rows) || cols < 1 || rows < 1) return;\n    try {\n      session.process.resize(cols, rows);\n      // The headless render terminal MUST track the PTY (see pty-shared.ts):\n      // getOutputLines reads its grid for replay, and the gate detectors scrape\n      // it. Resizing one without the other desyncs every absolute cursor move\n      // the TUI makes — which would break gate detection, the reason a managed\n      // session exists at all.\n      session.screen.resize(cols, rows);\n    } catch (err) {\n      // node-pty throws if the fd closed between the guard above and here.\n      // The session is going away; a resize for it is moot, not an error.\n      this.log.debug(`[pty.resize.failed] ${sessionId.slice(0, 8)}`, {\n        event: \"pty.resize_failed\",\n        sessionId,\n        err,\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  // `signal` defaults to SIGINT (graceful); pass SIGKILL to force-kill a\n  // process that isn't responding to it.\n  putOnHold(sessionId: string, signal: NodeJS.Signals = \"SIGINT\"): 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.oscTail.delete(sessionId);\n    this.turnOpen.delete(sessionId);\n    this.progressSeen.delete(sessionId);\n    this.progressTail.delete(sessionId);\n    this.lastDetectAt.delete(sessionId);\n    this.lastScreenQuestionKey.delete(sessionId);\n    this.shellPromptOpen.delete(sessionId);\n    this.closedGateKey.delete(sessionId);\n    this.quietCheckers.get(sessionId)?.cancel();\n    this.quietCheckers.delete(sessionId);\n    this.clearReadyFallback(sessionId);\n    try {\n      session.process.kill(signal);\n    } catch {\n      // already dead\n    }\n    session.status = \"idle\";\n    session.statusSource = \"shutdown\";\n    session.statusUpdatedAt = new Date();\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  // OS pid of the spawned agent, or null if the session isn't live here. The\n  // durable registry records this so a later streamer run can probe whether the\n  // process outlived it. Liveness alone is never identity — a recycled pid is\n  // why the registry stores a cmdline alongside it.\n  getPid(sessionId: string): number | null {\n    return this.sessions.get(sessionId)?.process?.pid ?? null;\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    // First message of the session: derive a title the same way the scanner\n    // would once it observes this line, so a live session has a name from its\n    // very first turn rather than waiting on a resume/rescan to compute it.\n    if (session.firstMessageText === undefined) {\n      session.firstMessageText = text;\n      session.sessionName = deriveSessionName(text);\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    for (const timer of this.readyFallbackTimers.values()) clearTimeout(timer);\n    this.readyFallbackTimers.clear();\n    this.permissionOpen.clear();\n    this.oscTail.clear();\n    this.turnOpen.clear();\n    this.progressSeen.clear();\n    this.progressTail.clear();\n    this.lastDetectAt.clear();\n    this.lastScreenQuestionKey.clear();\n    this.shellPromptOpen.clear();\n    this.closedGateKey.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}`,\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      },\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    const turnEnded = this.trackProgress(sessionId, data);\n\n    if (session.status === \"running\" && matchedMarker && !this.turnOpen.has(sessionId)) {\n      this.markReady(sessionId, session, \"prompt-marker\", `marker:${matchedMarker}`);\n    }\n    // The turn-end chunk rarely repaints the box, so ask the screen.\n    if (turnEnded && session.status === \"running\") this.recheckReadyLogged(sessionId);\n    // The no-marker backstop lives in armReadyFallback() rather than here: this\n    // branch only ran when a chunk happened to arrive, so a boot that fell\n    // silent before showing a marker was never rescued by it.\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 with a throttle floor so we don't scrape the rendered\n    // buffer on every chunk:\n    //   - OSC 777 (raw byte signal, tail-carried across chunks) → gate opened.\n    //   - \"Enter to select\" footer (AskUserQuestion menu) → structured question.\n    //   - an already-open gate/prompt keeps passing, so its close is seen (the\n    //     prompt-ready marker without a fresh 777 is what marks it closed).\n    //   - otherwise, at most every SCRAPE_THROTTLE_MS, a chunk alone triggers\n    //     a pass — how a painted gate is claimed ~6s before its OSC arrives.\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    // OSC escapes can split across node-pty chunk boundaries — test against\n    // the previous chunk's tail + this chunk. The tail carries the ROLLING\n    // WINDOW (not just the last chunk), so a split across three+ chunks still\n    // matches once the final piece arrives. The tail is consumed on a match\n    // so the quiet path (rawData === \"\") can never re-fire a seen escape, and\n    // left untouched by the quiet path so a pending partial isn't dropped.\n    const oscWindow = (this.oscTail.get(sessionId) ?? \"\") + rawData;\n    const oscPermission = hasPermissionOsc(oscWindow);\n    // Claude finished its turn. Authoritative \"no gate is open\" signal — it\n    // arrives even when no further chunk will (the turn is over), so it is the\n    // only thing that can close a gate whose options never painted.\n    const oscWaitingForInput = hasWaitingForInputOsc(oscWindow);\n    if (rawData !== \"\") {\n      if (oscPermission || oscWaitingForInput) this.oscTail.delete(sessionId);\n      else this.oscTail.set(sessionId, oscWindow.slice(-OSC_TAIL_CHARS));\n    }\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    // A suggestion is painted as SGR 2 (faint) after the turn's last chunk and\n    // can land inside the throttle window of the previous pass; handleQuiet only\n    // re-scans a `running` session, so without this trigger it would be missed\n    // for good. Also keep scanning while one is shown so its clear is seen.\n    const suggestionTrigger =\n      session.status === \"waiting_input\" &&\n      (rawData.includes(\"\\x1b[2m\") || session.promptSuggestion != null);\n\n    // Paint-time throttle: when the last full pass is ≥ SCRAPE_THROTTLE_MS\n    // old, a chunk's arrival alone is enough to scrape — the gate paint has\n    // no reliable chunk-level signal and its OSC arrives ~6s late.\n    const nowMs = Date.now();\n    const scrapeDue = nowMs - (this.lastDetectAt.get(sessionId) ?? 0) >= SCRAPE_THROTTLE_MS;\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      !oscWaitingForInput &&\n      !hasAskFooter &&\n      !hasShellPromptHint &&\n      !suggestionTrigger &&\n      !scrapeDue &&\n      !this.permissionOpen.has(sessionId) &&\n      !this.shellPromptOpen.has(sessionId) &&\n      !this.lastScreenQuestionKey.has(sessionId)\n    ) {\n      return;\n    }\n    this.lastDetectAt.set(sessionId, nowMs);\n\n    const lines = await this.getOutputLines(sessionId, 60);\n\n    // Phase refinement, off the screen read the detectors below already share.\n    // Only while the turn is running: the phase describes what the agent is\n    // doing *within* `running`, and markReady clears it at turn end. Reading it\n    // in any other state would let a stale screen re-assert a phase after the\n    // clear, which is the latch this design exists to prevent.\n    const phaseSession = this.sessions.get(sessionId);\n    if (phaseSession?.status === \"running\") {\n      this.setPhase(sessionId, phaseSession, parseAgentPhase(lines, CLAUDE_CODE_PROVIDER));\n    }\n\n    // Composer suggestion, read from cell attributes off the same flushed screen\n    // (getOutputLines awaited the xterm write queue). Not `waiting_input` -> no\n    // suggestion, which also clears one left over from before a new turn.\n    this.setPromptSuggestion(\n      sessionId,\n      session,\n      session.status === \"waiting_input\" ? readPromptSuggestion(session.screen) : null,\n    );\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: detector verdicts when a trigger fires. Metadata only — no\n    // rendered lines, no scraped prompt/question text — so it stays safe at any\n    // log level; prompt and answer content must never enter logs. At debug\n    // level so it's silent under normal --verbose; enable with LOG_LEVEL=debug.\n    if (oscPermission || hasAskFooter || askFooterOnScreen) {\n      const permGate = oscPermission && !askFooterOnScreen ? scrapePermissionGate(lines) : null;\n      const askQuestion = askFooterOnScreen ? detectQuestionFromScreen(lines) : null;\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        permGateDetected: permGate !== null,\n        permGateOptionCount: permGate?.options.length ?? 0,\n        askQuestionDetected: askQuestion !== null,\n        askQuestionCount: askQuestion?.questions.length ?? 0,\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.set(sessionId, gate ?? { options: [] });\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 turn ended or the prompt is ready again with the options gone,\n      // the gate closed. The end-of-turn notify is checked FIRST and without a\n      // prompt-marker requirement: it is the last chunk of the turn, so a gate\n      // still waiting on a marker here would never close at all.\n      const gate =\n        detectGateScreen(lines) ?? detectPickerScreen(lines) ?? detectStartupChoiceGate(lines);\n      // \"Gate is gone\" must survive a mid-repaint tick: detectGateScreen needs\n      // the footer, which can be briefly absent while the box repaints, and\n      // hasPromptMarker matches the box's own ╭/❯ glyphs — together those would\n      // close a live gate. Require BOTH detectors to see nothing before closing;\n      // the refresh below stays strict so prose is never broadcast as options.\n      const stillPainted = gate !== null || scrapePermissionGate(lines) !== null;\n      if (oscWaitingForInput || (!stillPainted && hasPromptMarker)) {\n        this.permissionOpen.delete(sessionId);\n        this.onPermissionChange?.(sessionId, null);\n        // Record what's still on screen (if anything) so arm 3 doesn't\n        // reclaim this exact box on a later trigger-less pass before Claude\n        // erases it — see closedGateKey.\n        if (gate) {\n          this.closedGateKey.set(sessionId, permissionContentKey({ ...gate, cursor: undefined }));\n        } else {\n          this.closedGateKey.delete(sessionId);\n        }\n      } else if (gate) {\n        // Keep the stored gate the one actually on screen: sendKeys tests the\n        // incoming bytes against its option indices, and a refresh can replace\n        // the box with a different gate.\n        this.permissionOpen.set(sessionId, gate);\n        this.onPermissionChange?.(sessionId, gate);\n      }\n    } else if (!askFooterOnScreen && !oscWaitingForInput) {\n      // Paint-time claim: the gate is on screen but Claude's OSC 777 notify\n      // (debounced ~6s upstream) hasn't arrived. detectGateScreen anchors on\n      // the gate footer + a Yes/No option label so a numbered list in prose\n      // can't open a card; detectPickerScreen claims an unboxed picker with no\n      // footer (a ❯ cursor and no composer below it). Downstream is the OSC\n      // path unchanged — same broadcast, same dedupe, same close signals. A\n      // still-painted box that arm 2 closed on an EARLIER pass reaches this\n      // arm on the very next trigger-less/throttled tick (arm 3 is an else-if\n      // of arm 2, so it can never run in the SAME pass as the close) —\n      // closedGateKey suppresses reclaiming that exact content until Claude\n      // erases the box.\n      const gate =\n        detectGateScreen(lines) ?? detectPickerScreen(lines) ?? detectStartupChoiceGate(lines);\n      if (gate) {\n        const key = permissionContentKey({ ...gate, cursor: undefined });\n        if (this.closedGateKey.get(sessionId) !== key) {\n          this.permissionOpen.set(sessionId, gate);\n          this.onPermissionChange?.(sessionId, gate);\n        }\n      } else {\n        // Box is gone — clear the suppression so a later, even\n        // content-identical, gate is claimed normally.\n        this.closedGateKey.delete(sessionId);\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) return;\n    if (session.status !== \"running\") {\n      // A blocking startup gate (workspace trust) paints its own `❯` cursor on\n      // the selected option, which readiness cannot tell from a composer prompt\n      // — so the session settles to `waiting_input` at boot with the gate still\n      // on screen. The screen then goes static, so the per-chunk paint-time\n      // claim never runs again and the gate is never broadcast: it reaches the\n      // client as raw terminal text with no way to answer it but raw keys.\n      //\n      // Scan once more here for exactly that case. Bounded the same way every\n      // other unsolicited scrape is (the SCRAPE_THROTTLE_MS gate inside\n      // detectLivePrompts), and skipped when a card is already open so an\n      // answered gate is not re-claimed.\n      if (session.status === \"waiting_input\" && !this.permissionOpen.has(sessionId)) {\n        this.detectLivePrompts(sessionId, \"\", session.lastOutput).catch((err) => {\n          this.log.warn(\"[pty.prompt_detect] idle rescan failed\", {\n            event: \"pty.prompt_detect_failed\",\n            sessionId,\n            err,\n          });\n        });\n      }\n      return;\n    }\n\n    if (this.pendingReady.has(sessionId)) {\n      // Silence is NOT evidence of readiness while booting. `--resume` replays\n      // the JSONL and can sit quiet for far longer than QUIET_DETECT_MS before\n      // the TUI reads stdin at all; marking ready here disarmed the input\n      // queue mid-boot, and the bytes then sat unconsumed in the tty buffer\n      // until the TUI started reading — surfacing as the NEXT message silently\n      // carrying the previous one as a prefix. Ask the screen instead, the\n      // same question the settled branch below asks, and leave the flat\n      // backstop (armReadyFallback) to break a genuine deadlock.\n      this.recheckReadyFromScreen(sessionId).catch((err) => {\n        this.log.warn(\"[pty.ready] boot screen recheck failed\", {\n          event: \"pty.ready_recheck_failed\",\n          sessionId,\n          err,\n        });\n      });\n    } else {\n      // handleOutput() only checks the triggering chunk for a prompt marker,\n      // but Claude's TUI does differential repaints and doesn't always\n      // retransmit the box border once it's already drawn — so a\n      // mid-conversation return to idle can go undetected forever if the last\n      // chunk didn't happen to carry it. Re-derive readiness from the actual\n      // rendered screen instead of waiting for a chunk that may never come.\n      this.recheckReadyFromScreen(sessionId).catch((err) => {\n        this.log.warn(\"[pty.ready] screen recheck failed\", {\n          event: \"pty.ready_recheck_failed\",\n          sessionId,\n          err,\n        });\n      });\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  /** Update the open turn from OSC 9;4 in this chunk; true when it just closed. */\n  private trackProgress(sessionId: string, data: string): boolean {\n    // Prepend an escape the previous chunk cut off, so a split one still counts.\n    const window = (this.progressTail.get(sessionId) ?? \"\") + data;\n    let state: string | undefined;\n    for (const m of window.matchAll(CLAUDE_PROGRESS_RE)) state = m[1];\n    // Carry only a trailing escape too short to be a whole `ESC]9;4;N` (7\n    // chars), so a complete one is never counted twice.\n    const esc = window.lastIndexOf(\"\\x1b\");\n    const rest = esc >= 0 ? window.slice(esc) : \"\";\n    if (rest.length < 7) this.progressTail.set(sessionId, rest);\n    else this.progressTail.delete(sessionId);\n    if (state === undefined) return false;\n    this.progressSeen.add(sessionId);\n    if (state !== \"0\") {\n      this.turnOpen.set(sessionId, \"working\");\n      return false;\n    }\n    return this.turnOpen.delete(sessionId);\n  }\n\n  private openTurnOnSubmit(sessionId: string): void {\n    if (!this.progressSeen.has(sessionId)) return;\n    this.turnOpen.set(sessionId, \"submitted\");\n    const timer = setTimeout(() => {\n      if (this.turnOpen.get(sessionId) !== \"submitted\") return;\n      this.turnOpen.delete(sessionId);\n      this.recheckReadyLogged(sessionId);\n    }, TURN_START_GRACE_MS);\n    timer.unref?.();\n  }\n\n  private recheckReadyLogged(sessionId: string): void {\n    this.recheckReadyFromScreen(sessionId).catch((err) => {\n      this.log.warn(\"[pty.ready] screen recheck failed\", {\n        event: \"pty.ready_recheck_failed\",\n        sessionId,\n        err,\n      });\n    });\n  }\n\n  // Re-check the rendered screen (not just the last chunk) for a prompt\n  // marker. Only meaningful once pendingReady is already clear — the boot\n  // fallback above covers the first prompt after spawn/resume. Scoped to a\n  // full viewport (PTY_ROWS), not just the last few lines: \"on screen\" means\n  // whatever a user attached to this PTY would currently see.\n  private async recheckReadyFromScreen(sessionId: string): Promise<void> {\n    const session = this.sessions.get(sessionId);\n    if (session?.status !== \"running\") return;\n    const lines = await this.getOutputLines(sessionId, PTY_ROWS);\n    const matchedMarker = CLAUDE_PROMPT_MARKERS.find((m) => lines.some((l) => l.includes(m)));\n    if (matchedMarker && session.status === \"running\" && !this.turnOpen.has(sessionId)) {\n      this.markReady(sessionId, session, \"screen-marker\", `quiet:screen-marker:${matchedMarker}`);\n    }\n  }\n\n  // Flat backstop from the first chunk: if neither a prompt marker nor the\n  // screen recheck settles the session within CLAUDE_READY_FALLBACK_MS, mark it\n  // ready anyway so start requests resolve and queued input is not held\n  // forever. This is what makes the quiet-checker safe to be strict — a boot\n  // variant whose marker we cannot see still recovers, just 8s later instead of\n  // 500ms sooner and wrong. unref() so it 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, \"timeout-fallback\", \"fallback:timeout\");\n      }\n    }, CLAUDE_READY_FALLBACK_MS);\n    timer.unref?.();\n    this.readyFallbackTimers.set(sessionId, timer);\n  }\n\n  private clearReadyFallback(sessionId: string): void {\n    const timer = this.readyFallbackTimers.get(sessionId);\n    if (timer) clearTimeout(timer);\n    this.readyFallbackTimers.delete(sessionId);\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  /**\n   * Record the agent's phase and notify only on a real change.\n   *\n   * The change guard is load-bearing, not an optimisation: the scrape pass runs\n   * on every chunk, so an unguarded setter would fire the callback — and the\n   * WS frame behind it — several times a second for the entire duration of a\n   * turn while reporting the same value.\n   */\n  private setPhase(sessionId: string, session: InternalSession, phase: AgentPhase | null): void {\n    const next = phase ?? null;\n    if ((session.subStatus ?? null) === next) return;\n    session.subStatus = next;\n    this.onPhaseChange?.(sessionId, next);\n  }\n\n  /** Record the composer suggestion and notify only on a real change. */\n  private setPromptSuggestion(\n    sessionId: string,\n    session: InternalSession,\n    text: string | null,\n  ): void {\n    if ((session.promptSuggestion ?? null) === text) return;\n    session.promptSuggestion = text;\n    this.onPromptSuggestionChange?.(sessionId, text);\n  }\n\n  private markReady(\n    sessionId: string,\n    session: InternalSession,\n    source: StatusSource,\n    reason: string,\n  ): void {\n    this.clearReadyFallback(sessionId);\n    session.lastActivityAt = new Date();\n    session.status = \"waiting_input\";\n    // C3: record HOW we concluded this, not just that we did. A\n    // `timeout-fallback` here means no marker ever appeared and we assumed —\n    // previously indistinguishable on the wire from an observed marker.\n    session.statusSource = source;\n    session.statusUpdatedAt = new Date();\n    // Turn end clears the phase. This is the only place it can happen\n    // correctly: the exit edge is not an output event, so it cannot be read off\n    // the screen — Claude's TUI does differential repaints and a return to idle\n    // can go undetected forever if the last chunk didn't carry the marker.\n    // Without this the phase latches on any session that stops emitting, which\n    // is the bug tb-mobile PR #647 shipped. markReady is the single idempotent\n    // running -> waiting_input transition, and it is driven by the waiting-for-\n    // input OSC, which arrives even when no further chunk will.\n    // Kept below the source assignment so it stays inside the window\n    // status-confidence.test.ts scans, and so onPhaseChange never observes a\n    // session whose status has moved but whose statusSource has not.\n    this.setPhase(sessionId, session, null);\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    session.statusSource = \"process-exit\";\n    session.statusUpdatedAt = new Date();\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.oscTail.delete(sessionId);\n    this.turnOpen.delete(sessionId);\n    this.progressSeen.delete(sessionId);\n    this.progressTail.delete(sessionId);\n    this.lastDetectAt.delete(sessionId);\n    this.lastScreenQuestionKey.delete(sessionId);\n    this.shellPromptOpen.delete(sessionId);\n    this.closedGateKey.delete(sessionId);\n    this.quietCheckers.get(sessionId)?.cancel();\n    this.quietCheckers.delete(sessionId);\n    this.clearReadyFallback(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.statusSource != null && { statusSource: s.statusSource }),\n    ...(s.statusUpdatedAt != null && { statusUpdatedAt: s.statusUpdatedAt }),\n    ...(s.filePath != null && { filePath: s.filePath }),\n    ...(s.sessionName != null && { sessionName: s.sessionName }),\n    ...(s.effort != null && { effort: s.effort }),\n    ...(s.firstMessageText != null && { firstMessageText: s.firstMessageText }),\n    // Unconditional: this shape crosses the pty-host boundary, and a streamer\n    // re-adopting a surviving host's sessions mid-turn has no other source for\n    // the phase — no snapshot or replay event carries it, and setPhase's change\n    // guard means the host will never re-emit it for the rest of that turn.\n    subStatus: s.subStatus ?? null,\n    promptSuggestion: s.promptSuggestion ?? null,\n  };\n}\n","// Permission-gate detection. Three independent signals:\n//\n//   1. Rendered gate signature — the PRIMARY, paint-time trigger\n//      (detectGateScreen): gate footer + numbered Yes/No options in the\n//      rendered screen. Fires within one scrape throttle tick of the paint.\n//\n//   2. OSC 777 escape — the deterministic FALLBACK 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\\`) — but\n//      ~6s AFTER the gate paints (measured 2026-08-08, see\n//      docs/superpowers/specs/2026-08-08-paint-time-gate-detection-design.md).\n//      Still load-bearing: it covers a gate whose options never painted, and\n//      its \"waiting for your input\" body is the authoritative close signal.\n//\n//   3. Rendered option scrape — the payload builder. The gate's numbered\n//      options are painted via absolute-cursor moves, so they live in the\n//      rendered headless buffer (getOutputLines), not the raw byte stream. We\n//      read the ACTUAL leading numbers and the `❯` cursor — the numbers are\n//      NOT a stable 1-based index (a gate can show \"2. Yes / 3. No\"), which\n//      is exactly the \"2 didn't take\" bug this avoids. The scrape takes the\n//      LAST option block scanning bottom-up, since a numbered list in Claude's\n//      prose above the gate can match the same option shape and must not be\n//      swept in.\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/**\n * Stable content key for de-duping repeated broadcasts of the same gate across\n * PTY repaint ticks. Includes cursor — moving the on-screen selection is a\n * real update the client needs, so it must produce a new key.\n */\nexport function permissionContentKey(gate: {\n  prompt?: string;\n  detail?: string;\n  options: PermissionOption[];\n  cursor?: number;\n}): string {\n  return `${gate.prompt ?? \"\"}::${gate.detail ?? \"\"}::${gate.options\n    .map((o) => `${o.index}.${o.label}`)\n    .join(\",\")}::${gate.cursor ?? \"\"}`;\n}\n\n/**\n * The gate's IDENTITY key: the content key with the cursor stripped.\n *\n * Cursor belongs in the broadcast dedupe key (moving the highlight is a real\n * update) but never in identity — the same gate with a different highlight is\n * the same gate. `closedGateKey` in pty-manager.ts already draws exactly this\n * line; this names it so the answer route and the client agree on one spelling.\n *\n * Content-derived, not instance-derived: it distinguishes gates with different\n * prompt/detail/options, NOT two consecutive gates whose content is identical.\n */\nexport function permissionGateKey(gate: {\n  prompt?: string;\n  detail?: string;\n  options: PermissionOption[];\n}): string {\n  return permissionContentKey({ ...gate, cursor: undefined });\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//\n// The BODY is load-bearing: Claude Code emits OSC 777 for two different events,\n// and only one of them is a gate.\n//\n//   \"Claude needs your permission\"      → a real gate; options paint later.\n//   \"Claude is waiting for your input\"  → the turn ENDED; there is no gate.\n//\n// Matching the prefix alone conflated the two, so an end-of-turn notify opened a\n// permission card that could never populate or close: scrapePermissionGate finds\n// no options (correctly — there are none), the caller broadcasts `{options: []}`,\n// and the recovery path needs a prompt marker from a LATER chunk that never\n// arrives because the turn is over. Result: a stuck \"Claude needs your\n// permission\" card with only Cancel. Anchor on the permission body instead.\n// biome-ignore lint/suspicious/noControlCharactersInRegex: matching the literal OSC 777 escape\nconst OSC_777_PERMISSION_RE = /\\x1b\\]777;notify;Claude Code;[^\\x07\\x1b]*needs your permission/;\n\n// biome-ignore lint/suspicious/noControlCharactersInRegex: matching the literal OSC 777 escape\nconst OSC_777_WAITING_RE = /\\x1b\\]777;notify;Claude Code;[^\\x07\\x1b]*waiting for your input/;\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_PERMISSION_RE.test(rawData);\n}\n\n/**\n * True if a raw PTY chunk contains the OSC 777 \"waiting for your input\" notify —\n * Claude finished its turn. Not a gate: it is the positive signal that any gate\n * we were tracking is over, which is what lets a stuck card close without\n * waiting for a prompt marker that will never come.\n */\nexport function hasWaitingForInputOsc(rawData: string): boolean {\n  return OSC_777_WAITING_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.\n//\n// The cursor may be `❯`, `›` or ASCII `>`. Claude Code paints it as a plain `>`\n// (U+003E) on Windows — measured from a real gate's PTY bytes, which render the\n// highlighted row as \" > 1. Yes\" against \"   2. No\" for the rest. Accepting only\n// `❯` dropped that row from the options entirely, and the prompt search below\n// then picked it up as the question — so the plain \"Yes\" never reached the\n// client and \"switch to auto mode\" was left as the only way to approve.\n// PROMPT_ARROW_RE below and codexScreen.ts already accept all three.\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 * Scrapes the LAST option block on screen, scanning bottom-up: a stale\n * numbered list sitting in scrollback above the real gate (e.g. Claude's own\n * prose) matches the same option shape and must not be vacuumed in with it.\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 cursorRows = 0;\n  let firstOptionLine = -1;\n\n  for (let i = lines.length - 1; i >= 0; i--) {\n    const stripped = stripGutter(lines[i]);\n    const m = OPTION_RE.exec(stripped);\n    if (m) {\n      const index = Number.parseInt(m[2], 10);\n      if (!Number.isFinite(index)) continue;\n      firstOptionLine = i; // overwritten as we walk up — ends up topmost\n      if (m[1]) {\n        cursor = index; // the glyph marks the highlighted option\n        cursorRows++;\n      }\n      options.unshift({ index, label: m[3] }); // keep top-to-bottom screen order\n      continue;\n    }\n    if (options.length === 0) continue; // still below the block: footer, box bottom, blanks\n    // Inside the block: a blank line or the box edge marks its top boundary.\n    if (stripped.trim().length === 0 || BOX_ONLY_RE.test(lines[i].trim())) break;\n    // otherwise: a wrapped option label or the prompt line — keep walking\n  }\n\n  if (options.length === 0) return null;\n\n  // A selection cursor marks exactly ONE row; a line PREFIX marks every row.\n  // Markdown blockquotes are the case that matters — `> 1. Rebase` / `> 2. Squash`\n  // in Claude's prose is option-shaped and carries a `>` on both rows — but the\n  // rule is about the shape, not the glyph, so a quoted `❯` list is caught too.\n  // Reporting a highlight here would be wrong data on its own, and it is what\n  // let detectPickerScreen claim such a block whenever no composer rule\n  // happened to sit below it. The two guards are complementary: this one\n  // rejects quoted prose (every row marked), the composer-rule test rejects\n  // typed or pasted text (one row marked, by the composer's own `❯` prompt).\n  if (cursorRows > 1) cursor = undefined;\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\n// ── Paint-time gate signature ──────────────────────────────────────\n// The OSC 777 notify is debounced ~6s by Claude Code (measured 2026-08-08,\n// see docs/superpowers/specs/2026-08-08-paint-time-gate-detection-design.md),\n// so waiting for it makes the mobile card lag a fully painted gate. This\n// classifier claims a gate from the RENDERED screen alone. All three anchors\n// must hold — the Yes/No label test is what keeps a numbered list in Claude's\n// prose from opening a card.\n\n// Gate footer (\"Esc to cancel · Tab to amend · ctrl+e to explain\"); \"esc to\n// cancel\" is the stable core across versions.\nconst GATE_FOOTER_RE = /esc to cancel/i;\n// AskUserQuestion footer — that path has priority (its footer also contains\n// \"Esc to cancel\", so this must be tested first).\nconst ASK_MENU_FOOTER_RE = /Enter to select/i;\n// Same family test detectQuestionFromScreen uses in reverse to REJECT gates.\nconst YES_NO_LABEL_RE = /^(yes|no)\\b/i;\n\n/**\n * Claim a permission gate from rendered screen lines without an OSC 777.\n * Returns the scraped gate when the full gate signature is present:\n * ≥2 numbered options, the gate footer, no Ask-menu footer, and at least one\n * Yes/No-family option label. Pure — no I/O.\n */\nexport function detectGateScreen(lines: string[]): PermissionGate | null {\n  if (lines.some((l) => ASK_MENU_FOOTER_RE.test(l))) return null;\n  if (!lines.some((l) => GATE_FOOTER_RE.test(l))) return null;\n  const gate = scrapePermissionGate(lines);\n  if (!gate || gate.options.length < 2) return null;\n  if (!gate.options.some((o) => YES_NO_LABEL_RE.test(o.label))) return null;\n  return gate;\n}\n\n// Claude Code's composer draws its \"❯\" prompt between two full-width ─ rules.\n// A live picker replaces the composer, so a rule below a numbered block means\n// the block is not a picker the user is being asked to answer.\nconst COMPOSER_RULE_RE = /^\\s*─{8,}\\s*$/;\n\n/**\n * Claim an unboxed numbered picker from rendered screen lines — one that prints\n * no gate footer, so detectGateScreen never sees it (Claude Code's first-run\n * theme picker, which follows its options with a preview and a \"Syntax theme\"\n * line; #863). The ❯ selection cursor is what separates a live picker from a\n * numbered list in prose (#821). It is not enough on its own: ❯ is also the\n * composer's prompt glyph, so typed or pasted \"1. …\" text renders as a\n * cursor-marked numbered block between the composer's rules, and an answered\n * menu stays painted above a live composer (#724). Both leave a composer rule\n * below the cursor row; a live picker does not. Boxed blocks stay\n * detectGateScreen's. Pure — no I/O.\n */\nexport function detectPickerScreen(lines: string[]): PermissionGate | null {\n  if (lines.some((l) => ASK_MENU_FOOTER_RE.test(l))) return null;\n  const gate = scrapePermissionGate(lines);\n  if (!gate || gate.cursor === undefined || gate.options.length < 2) return null;\n  let cursorRow = -1;\n  for (let i = lines.length - 1; i >= 0; i--) {\n    const m = OPTION_RE.exec(stripGutter(lines[i]));\n    if (m?.[1] && Number.parseInt(m[2], 10) === gate.cursor) {\n      cursorRow = i;\n      break;\n    }\n  }\n  if (cursorRow < 0 || /^\\s*│/.test(lines[cursorRow])) return null;\n  for (let i = cursorRow + 1; i < lines.length; i++) {\n    if (COMPOSER_RULE_RE.test(lines[i])) return null;\n  }\n  return gate;\n}\n","import type { Terminal } from \"@xterm/headless\";\n\n// Claude Code's composer draws its \"❯\" prompt on the row between two full-width\n// ─ rules. A predicted next prompt (\"add type hints and a docstring\") is\n// painted into that row as SGR 2 (faint) text — `ESC[2m … ESC[22m`, verified\n// against Claude Code v2.1.278 — where text the user typed carries no dim flag.\n// The rendered *text* is identical either way, so the only thing that tells a\n// suggestion from real input is the cell attribute, which the text scrape\n// (translateToString) throws away.\nconst COMPOSER_RULE_RE = /^\\s*─{8,}\\s*$/;\nconst PROMPT_GLYPH = \"❯ \";\n// Older Claude Code builds put a dim example prompt in an empty composer\n// (`Try \"fix typecheck errors\"`, v2.1.6). That is a tip, not a prediction.\nconst PLACEHOLDER_TIP_RE = /^Try \"/;\n\n/**\n * Read the bottom-most composer row of a headless screen and return its text\n * iff every non-blank cell after the prompt glyph is dim; otherwise `null`.\n * Reads the visible viewport only, so scrollback (an old `❯ <user message>`\n * echo, which is never dim and never rule-bracketed) cannot match.\n */\nexport function readPromptSuggestion(term: Terminal): string | null {\n  const buf = term.buffer.active;\n  const text = (y: number) => buf.getLine(y)?.translateToString(true) ?? \"\";\n  for (let y = buf.baseY + term.rows - 2; y > buf.baseY; y--) {\n    const row = text(y);\n    if (!row.startsWith(PROMPT_GLYPH)) continue;\n    if (!COMPOSER_RULE_RE.test(text(y - 1)) || !COMPOSER_RULE_RE.test(text(y + 1))) continue;\n    const suggestion = row.slice(PROMPT_GLYPH.length).trim();\n    if (!suggestion || PLACEHOLDER_TIP_RE.test(suggestion)) return null;\n    const line = buf.getLine(y);\n    // By column, not string index: a wide glyph spans two cells but one char.\n    for (let x = PROMPT_GLYPH.length; x < term.cols; x++) {\n      const cell = line?.getCell(x);\n      if (cell?.getChars().trim() && !cell.isDim()) return null;\n    }\n    return suggestion;\n  }\n  return null;\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 checkbox a multi-select picker paints beside each selectable option\n// (\"[ ] Python\" before a toggle, \"[✔] Python\" after). Its presence is the ONLY\n// on-screen evidence that a picker is multi-select: the TUI draws a\n// multi-select form and a single-select menu with the same footer, the same\n// \"N.\" numbering and the same ❯ cursor, so nothing else here distinguishes\n// them. `Chat about this` — the escape hatch — carries no checkbox, so ANY\n// marked option means multi-select; requiring all of them would miss every\n// real form.\nconst KNOWN_STATE_MARKER_RE = /^(?:\\[(?:[ xX*]|✓|✔)\\]|[☐☑☒])\\s*/;\n\n// A short bracketed token we do NOT recognise, treated as a state marker we\n// cannot read rather than as label text. This is what makes the classifier fail\n// towards unanswerable: without it, an unfamiliar marker (a future TUI painting\n// \"( )\" radios, say) would fall through to single-select and the answer route\n// would write a keystroke for a shape it never understood. The ≤3-character\n// bound is deliberate — real markers are one or two glyphs, while a longer\n// bracketed token (\"[draft] …\") is far more likely to be genuine label text.\nconst UNKNOWN_STATE_MARKER_RE = /^(?:\\[[^\\]]{0,3}\\]|\\([^)]{0,3}\\))\\s*/;\n\n// Split a rendered option label into its state marker (if any) and the label\n// proper. A recognised marker is stripped — it is presentation, and leaving it\n// in the payload both ships \"[ ]\" to clients as if it were part of the option\n// text and makes questionContentKey unstable, so merely ticking a box on screen\n// reads as a different question and mints a new prompt.\nfunction stripStateMarker(label: string): { label: string; marked: boolean } {\n  if (KNOWN_STATE_MARKER_RE.test(label)) {\n    return { label: label.replace(KNOWN_STATE_MARKER_RE, \"\"), marked: true };\n  }\n  // Unrecognised: flag it, but leave the text alone — we do not know that the\n  // token is a marker, only that we cannot rule it out.\n  if (UNKNOWN_STATE_MARKER_RE.test(label)) return { label, marked: true };\n  return { label, marked: false };\n}\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  let sawStateMarker = false;\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 rendered = m[2].trim();\n      if (PERMISSION_LABEL_RE.test(rendered)) return null; // it's a permission gate\n      const { label, marked } = stripStateMarker(rendered);\n      if (marked) sawStateMarker = true;\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  // multiSelect carries two cases that must land in the same place: a picker we\n  // positively recognised as multi-select, and one whose option markers we could\n  // not read. Both are unanswerable by the keystroke path — answersToKeystrokes\n  // throws UnsupportedPromptShapeError before building a keystroke, and the\n  // contract publishes inputMode \"multi\", which released clients already fail\n  // closed on. The contract has no third value for \"shape unknown\", and this is\n  // the bucket that writes zero bytes.\n  return {\n    questions: [{ question, header: \"\", multiSelect: sawStateMarker, 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 * True when an AskUserQuestion menu is still painted on the rendered screen.\n * Cheaper and far more forgiving than detectQuestionFromScreen: it asks only\n * \"is the picker up?\", so a menu whose options wrap oddly still reads as\n * present. Used to reject an answer for a menu that already closed. Pure.\n */\nexport function isQuestionMenuOnScreen(lines: string[]): boolean {\n  return lines.some((line) => ASK_FOOTER_RE.test(line));\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 mostly recoverable (the user ignores it), though\n// tapping one sends real keystrokes — a numbered card scraped out of prose types\n// its digit in as a prompt; 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). The block must be\n  // CONTIGUOUS — a real menu has no prose between rows, unlike a numbered list\n  // in ordinary prose (e.g. an explanation with \"1. ...\" / \"2. ...\" points\n  // separated by paragraphs) — and must reach the true tail of the screen, not\n  // a numbered block buried under trailing prose.\n  const lastNumberedIdx = (() => {\n    let i = last.idx;\n    if (!NUMBERED_RE.test(lines[i])) {\n      // Allow exactly one trailing non-numbered footer line (the bare\n      // \"Press enter to confirm\" case) between the tail and the block.\n      if (i > 0 && (PRESS_ENTER_RE.test(lines[i].trim()) || CONTINUE_RE.test(lines[i].trim()))) {\n        i--;\n      } else {\n        return -1;\n      }\n    }\n    while (i >= 0 && !NUMBERED_RE.test(lines[i]) && lines[i].trim().length === 0) i--;\n    return i >= 0 && NUMBERED_RE.test(lines[i]) ? i : -1;\n  })();\n  if (lastNumberedIdx >= 0) {\n    const options: ShellPromptOption[] = [];\n    let firstRow = lastNumberedIdx;\n    for (let i = lastNumberedIdx; i >= 0; i--) {\n      const m = NUMBERED_RE.exec(lines[i]);\n      if (!m) break; // contiguous block ends at the first non-numbered line\n      const num = Number.parseInt(m[1], 10);\n      if (!Number.isFinite(num)) break;\n      options.unshift({ index: num, label: m[2].trim(), answerKeys: `${num}${ENTER}` });\n      firstRow = i;\n    }\n    // A real menu enumerates 1..n. A numbered block scraped out of the middle\n    // of prose does not: an assistant message listing open questions 1-8 wraps\n    // item 5 onto a continuation line, which ends the contiguous run above\n    // item 6 — so the \"menu\" starts at 6, and answering it types \"6\\r\" into\n    // the session as a prompt. Trade-off: this also rejects a real menu whose\n    // first rows scrolled off screen; widen only if that is ever observed.\n    if (options.length >= 2 && options.every((o, i) => o.index === i + 1)) {\n      // Prompt = nearest non-chrome line above the first numbered row.\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 type { PermissionGate, PermissionOption } from \"./detectPermissionGate\";\n\n// Claude Code's blocking startup choices — the workspace-trust gate above all —\n// are a vertical list with NO leading numbers:\n//\n//   Accessing workspace:\n//   C:\\Users\\PC\\Desktop\\dev\\autokitteh\n//   Quick safety check: Is this a project you created or one you trust? …\n//   Security guide\n//   ❯ No, exit\n//     Yes, I trust this folder\n//   Enter to confirm · Esc to cancel\n//\n// `scrapePermissionGate` requires `N.` on every row, so it returns null here and\n// every detector built on it refuses the screen. Nothing broadcasts a card, the\n// raw text falls through to the transcript, and the only way to answer from a\n// phone is raw keys — on a prompt that grants read, edit and execute rights over\n// a directory. Codex already raises its equivalent as a card\n// (`codexGateAnswers.ts`); this is the Claude side of that.\n//\n// Measured against Claude Code v2.1.278 by clearing `hasTrustDialogAccepted` for\n// a scratch project: the cursor starts on the FIRST option, `\\x1b[B` moves it\n// down one row and `\\x1b[A` moves it back up, and `\\r` confirms whatever the\n// cursor is on. A digit does nothing — which is why these options carry explicit\n// `answerKeys` rather than relying on the index.\n\n/** Footer unique to this family. \"Enter to select\" is an AskUserQuestion menu. */\nconst CONFIRM_FOOTER_RE = /enter to confirm/i;\nconst ASK_MENU_FOOTER_RE = /Enter to select/i;\n\n/** A row carrying the selection cursor, in either the Unicode or ASCII form. */\nconst CURSOR_ROW_RE = /^(\\s*)([❯›>])(\\s+)(\\S.*?)\\s*$/;\n/** Any numbered row — those belong to `detectGateScreen`, not here. */\nconst NUMBERED_RE = /^\\s*(?:[❯›>]\\s*)?\\d+\\.\\s+\\S/;\n\nconst DOWN = \"\\x1b[B\";\nconst UP = \"\\x1b[A\";\n\n/**\n * Keystrokes that move the selection from `cursor` to `target` and confirm.\n * Pure arithmetic on row offsets — the list is vertical and wraps nowhere that\n * matters, so the shortest path is a straight run of arrows.\n */\nexport function startupChoiceAnswerKeys(cursor: number, target: number): string {\n  const delta = target - cursor;\n  const step = delta > 0 ? DOWN : UP;\n  return step.repeat(Math.abs(delta)) + \"\\r\";\n}\n\n/**\n * Claim an unnumbered startup choice from rendered screen lines.\n *\n * The block is anchored on the cursor row: options are the contiguous rows whose\n * text begins at exactly the cursor row's text column. That column test is what\n * separates the options from the prose above them — \"Security guide\" sits one\n * column to the left of \"No, exit\", because the cursor glyph and its trailing\n * space are what indent the labels. Returns null unless the confirm footer is\n * present and at least two options share that column. Pure — no I/O.\n */\nexport function detectStartupChoiceGate(lines: string[]): PermissionGate | null {\n  if (lines.some((l) => ASK_MENU_FOOTER_RE.test(l))) return null;\n\n  const footer = lines.findIndex((l) => CONFIRM_FOOTER_RE.test(l));\n  if (footer < 0) return null;\n\n  // Exactly one cursor row, and it must be above the footer.\n  const cursorRows = lines\n    .map((l, i) => (i < footer && CURSOR_ROW_RE.test(l) ? i : -1))\n    .filter((i) => i >= 0);\n  if (cursorRows.length !== 1) return null;\n  const cursorRow = cursorRows[0];\n\n  const m = CURSOR_ROW_RE.exec(lines[cursorRow]);\n  if (!m) return null;\n  // Numbered blocks are detectGateScreen's; never claim one here.\n  if (NUMBERED_RE.test(lines[cursorRow])) return null;\n\n  const labelColumn = m[1].length + m[2].length + m[3].length;\n  const rowLabel = (line: string): string | null => {\n    if (NUMBERED_RE.test(line)) return null;\n    const indent = line.length - line.trimStart().length;\n    if (indent !== labelColumn) return null;\n    const label = line.trim();\n    return label.length > 0 ? label : null;\n  };\n\n  // Walk out from the cursor row while rows keep starting at the label column.\n  const rows: { row: number; label: string }[] = [{ row: cursorRow, label: m[4] }];\n  for (let i = cursorRow - 1; i >= 0; i--) {\n    const label = rowLabel(lines[i]);\n    if (label === null) break;\n    rows.unshift({ row: i, label });\n  }\n  for (let i = cursorRow + 1; i < footer; i++) {\n    const label = rowLabel(lines[i]);\n    if (label === null) break;\n    rows.push({ row: i, label });\n  }\n  if (rows.length < 2) return null;\n\n  const cursorIndex = rows.findIndex((r) => r.row === cursorRow) + 1;\n  const options: PermissionOption[] = rows.map((r, i) => ({\n    index: i + 1,\n    label: r.label,\n    answerKeys: startupChoiceAnswerKeys(cursorIndex, i + 1),\n  }));\n\n  // Prompt = nearest non-empty line above the block that is not the frame.\n  let prompt: string | undefined;\n  for (let i = rows[0].row - 1; i >= 0; i--) {\n    const t = lines[i].trim();\n    if (t.length === 0) continue;\n    if (/^[\\s─━-]+$/.test(t)) continue;\n    prompt = t;\n    break;\n  }\n\n  return {\n    ...(prompt ? { prompt } : {}),\n    options,\n    cursor: cursorIndex,\n  };\n}\n","// Build the keystrokes that answer a permission gate by its ON-SCREEN number.\n//\n// Permission gates show options like \"2. Yes / 3. No\" — the visible number is\n// NOT a stable 1-based index, so we send the actual number + Enter, never a\n// down-arrow count (that's the AskUserQuestion path, answersToKeystrokes).\n// Sent via the existing `/api/sessions/:id/input` { keys } route → sendKeys\n// (raw bytes), NOT bracketed-paste `/input` text (no blanket \\n).\n//\n// Mobile already does this directly (`POST /input { keys: \\`${index}\\r\\` }`),\n// so this helper exists for the server-side contract + tests.\n\nconst ENTER = \"\\r\";\n\nexport function permissionAnswerKeys(index: number): string {\n  if (!Number.isInteger(index) || index < 0) {\n    throw new Error(`Invalid permission option index: ${index}`);\n  }\n  return `${index}${ENTER}`;\n}\n\n// The inverse: do these raw keystroke bytes answer THIS gate?\n//\n// Used to close a gate the moment its answer is written to the PTY, instead of\n// waiting for the detector to notice the box is gone — after an approved tool\n// starts running there is no end-of-turn OSC and no ╭/❯ prompt marker, so the\n// detector's close condition can't fire until the whole turn ends.\n//\n// Deliberately strict, exact-match only. A false positive retires a LIVE gate\n// on every connected client, which is worse than the lateness this fixes, so\n// anything ambiguous stays for the detector to resolve: arrow keys (cursor\n// moves, answers nothing), a bare Enter (accepts the highlight, but we don't\n// track which option Claude has highlighted), a number this gate doesn't\n// offer, and a gate whose options haven't painted yet (`options: []`) all\n// return false.\nexport function isPermissionAnswer(\n  gate: { options: { index: number; answerKeys?: string }[] },\n  keys: string,\n): boolean {\n  return gate.options.some((o) => keys === (o.answerKeys ?? permissionAnswerKeys(o.index)));\n}\n","/**\n * Derive a session title from its first user message.\n *\n * Mirrors `@threadbase-sh/scanner`'s `deriveSessionNameFromFirstMessage`\n * (first line of the first user message, truncated) so a live session gets\n * the same title a resumed/scanned one would compute from the same JSONL —\n * without waiting on the scanner or cache to observe the line.\n */\nexport function deriveSessionName(firstMessageText: string): string {\n  const firstLine = firstMessageText.split(\"\\n\", 1)[0]?.trim() ?? \"\";\n  return firstLine.slice(0, 80);\n}\n","import { execFile } from \"child_process\";\nimport { platform } from \"os\";\nimport { basename, dirname } from \"path\";\nimport { isWindows } from \"./platform\";\nimport {\n  CLAUDE_CODE_PROVIDER,\n  CODEX_CLI_PROVIDER,\n  CURSOR_PROVIDER,\n  type ProviderName,\n} from \"./providers\";\nimport type { DiscoveredProcess } from \"./types\";\n\nexport async function discoverClaudeProcesses(): Promise<DiscoveredProcess[]> {\n  if (platform() === \"win32\") return discoverWindows();\n  return discoverUnix();\n}\n\n// ─── Command-line identification ───────────────────────────────────\n\n// The npm-installed CLI runs as `node <...>/claude-code/cli.js`, so matching on\n// the process NAME alone (the old `pgrep -x claude` / `IMAGENAME eq claude.exe`)\n// misses every shim install — the process is called `node`. Identify by what is\n// actually being executed instead.\nconst CLAUDE_CLI_SCRIPT = /claude-code[\\\\/](?:cli|index)\\.(?:js|mjs|cjs)$/i;\nconst JS_RUNTIMES = new Set([\"node\", \"node.exe\", \"bun\", \"bun.exe\", \"deno\", \"deno.exe\"]);\n\n// Split a command line into tokens, honouring double-quoted paths (Windows\n// installs live under \"C:\\Program Files\\...\" and would otherwise split apart).\nexport function tokenizeCommandLine(commandLine: string): string[] {\n  const tokens: string[] = [];\n  let current = \"\";\n  let quoted = false;\n  for (const ch of commandLine) {\n    if (ch === '\"') {\n      quoted = !quoted;\n      continue;\n    }\n    if (!quoted && (ch === \" \" || ch === \"\\t\")) {\n      if (current) tokens.push(current);\n      current = \"\";\n      continue;\n    }\n    current += ch;\n  }\n  if (current) tokens.push(current);\n  return tokens;\n}\n\n// A command line is just text and may carry a path in either separator form,\n// so `path.basename` cannot be used here — it honours only the host's\n// separator and would leave a Windows path intact when parsed on POSIX. The\n// script-path regex above is separator-agnostic for the same reason.\nfunction exeBaseName(token: string): string {\n  const cut = Math.max(token.lastIndexOf(\"/\"), token.lastIndexOf(\"\\\\\"));\n  return cut === -1 ? token : token.slice(cut + 1);\n}\n\n/**\n * Whether a command line is a Claude Code CLI process.\n *\n * Accepts a native binary invoked by any path, and a JS runtime hosting the\n * published CLI entry point. Deliberately conservative: it inspects only the\n * executable and the script it runs, never the whole string, so an unrelated\n * process that merely mentions \"claude\" in a flag (an editor session, this\n * streamer itself) is not mistaken for an agent.\n */\nexport function looksLikeClaudeProcess(commandLine: string): boolean {\n  const tokens = tokenizeCommandLine(commandLine);\n  if (tokens.length === 0) return false;\n\n  const exe = exeBaseName(tokens[0]).toLowerCase();\n  if (exe === \"claude\" || exe === \"claude.exe\") return true;\n\n  if (JS_RUNTIMES.has(exe)) {\n    // The script is the first non-flag argument (`node --flag script.js ...`).\n    for (const raw of tokens.slice(1)) {\n      if (raw.startsWith(\"-\")) continue;\n      return CLAUDE_CLI_SCRIPT.test(raw);\n    }\n  }\n  return false;\n}\n\n/**\n * Codex subcommands that are never an interactive session.\n *\n * A deny-list, arrived at the hard way. The obvious allow-list — \"the\n * subcommand must be `resume` or absent\" — cannot be written correctly, because\n * finding \"the subcommand\" means knowing which flags take a value: `codex --cd\n * /srv/app` would read the path as the subcommand and reject a real session,\n * and there is no reliable enumeration of Codex's value-taking flags to fix\n * that with.\n *\n * Over-accepting is the risk a deny-list carries, and here it is bounded. A\n * process only becomes adoptable if it also states a rollout id, which is what\n * `codex resume <uuid>` does and what no non-interactive subcommand does — so a\n * future non-interactive verb missing from this list is discovered but never\n * reaches adopt, because SessionStore drops a discovered process that names no\n * conversation.\n */\nconst CODEX_NON_INTERACTIVE_SUBCOMMANDS = new Set([\n  \"app-server\",\n  \"exec\",\n  \"mcp\",\n  \"proto\",\n  \"login\",\n  \"logout\",\n  \"completion\",\n]);\n\n/**\n * Whether a command line is an interactive Codex CLI session.\n *\n * Far more selective than the Claude matcher has to be, because \"codex\" is a\n * crowded name on a machine that has the ChatGPT desktop app: its Electron\n * framework is literally `Codex Framework.framework`, its helpers are\n * `Codex (Renderer)` and `Codex (Service)`, and `~/.codex/` holds a Chrome\n * extension host and a computer-use service. Measured on one developer machine,\n * 21 processes matched the word \"codex\" and only 3 were sessions.\n *\n * Two discriminators do the work:\n *\n *  - The executable's base name must be exactly `codex`, which drops every\n *    Electron helper and the `codex-code-mode-host` sidecar (which does have a\n *    TTY, so liveness alone would not have separated it).\n *  - No token may be a non-interactive subcommand. This is the load-bearing\n *    one: the ChatGPT app's `/Applications/ChatGPT.app/Contents/Resources/codex`\n *    and the VS Code extension's bundled binary are both named exactly `codex`,\n *    and only `app-server` distinguishes them from a session someone is typing\n *    at. Scanning every token rather than locating the subcommand means a\n *    value that happens to equal one of these names costs a missed session,\n *    never a wrongly claimed one.\n */\nexport function looksLikeCodexProcess(commandLine: string): boolean {\n  const tokens = tokenizeCommandLine(commandLine);\n  if (tokens.length === 0) return false;\n\n  const exe = exeBaseName(tokens[0]).toLowerCase();\n  if (exe !== \"codex\" && exe !== \"codex.exe\") return false;\n\n  // A macOS bundle path contains spaces, so tokenizing splits it mid-path and\n  // the first token can END at a directory called \"Codex\" — which is exactly\n  // what `.../Frameworks/Codex Framework.framework/.../Codex (Renderer)` and\n  // `~/.codex/computer-use/Codex Computer Use.app/...` do. Their base name then\n  // reads as `codex` and the executable check alone lets them through. No real\n  // CLI invocation has a bundle marker anywhere in it.\n  if (tokens.some((token) => /\\.(?:app|framework)[\\\\/]/i.test(token))) return false;\n\n  for (const token of tokens.slice(1)) {\n    if (CODEX_NON_INTERACTIVE_SUBCOMMANDS.has(token.toLowerCase())) return false;\n  }\n  return true;\n}\n\nconst CURSOR_NON_INTERACTIVE_SUBCOMMANDS = new Set([\n  \"login\",\n  \"logout\",\n  \"mcp\",\n  \"sandbox\",\n  \"worker\",\n  \"acp\",\n  \"update\",\n  \"create-chat\",\n  \"generate-rule\",\n  \"rule\",\n  \"help\",\n  \"about\",\n  \"status\",\n  \"whoami\",\n  \"models\",\n  \"install-shell-integration\",\n  \"uninstall-shell-integration\",\n]);\n\nexport function looksLikeCursorProcess(commandLine: string): boolean {\n  const tokens = tokenizeCommandLine(commandLine);\n  if (tokens.length === 0) return false;\n\n  const exe = exeBaseName(tokens[0]).toLowerCase();\n  if (\n    exe !== \"agent\" &&\n    exe !== \"agent.exe\" &&\n    exe !== \"cursor-agent\" &&\n    exe !== \"cursor-agent.exe\"\n  ) {\n    return false;\n  }\n\n  for (const token of tokens.slice(1)) {\n    if (CURSOR_NON_INTERACTIVE_SUBCOMMANDS.has(token.toLowerCase())) return false;\n  }\n  return true;\n}\n\n/** Which agent a command line is, or null when it is neither. */\nexport function providerForCommandLine(commandLine: string): ProviderName | null {\n  if (looksLikeClaudeProcess(commandLine)) return CLAUDE_CODE_PROVIDER;\n  if (looksLikeCodexProcess(commandLine)) return CODEX_CLI_PROVIDER;\n  if (looksLikeCursorProcess(commandLine)) return CURSOR_PROVIDER;\n  return null;\n}\n\n// Codex resumes by positional rollout id (`codex resume <uuid>`), not by the\n// `--resume <id>` flag Claude uses, so the Claude extractor cannot see it.\n// Anchored on a UUID shape rather than \"the token after resume\": `codex resume\n// --last` picks the most recent session and carries no id at all, and adopting\n// a session under the id \"--last\" would name a conversation that does not exist.\nconst CODEX_ROLLOUT_ID =\n  /\\bresume\\s+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\\b/i;\n\nexport function extractCodexResumeId(args: string): string | null {\n  return args.match(CODEX_ROLLOUT_ID)?.[1] ?? null;\n}\n\n/** The conversation id a discovered process is working on, if it states one. */\nexport function extractConversationId(args: string, provider: ProviderName): string | null {\n  return provider === CODEX_CLI_PROVIDER ? extractCodexResumeId(args) : extractResumeId(args);\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        // Re-derived here rather than carried from the sweep: the pgrep\n        // fallback matches on name alone, so this is the only point at which\n        // `codex app-server` can be told apart from a session.\n        const provider = providerForCommandLine(args);\n        if (provider === null) return null;\n\n        return {\n          pid,\n          provider,\n          projectPath: cwd,\n          projectName: basename(cwd),\n          branch: await readGitBranch(cwd),\n          conversationId: extractConversationId(args, provider),\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  // Preferred: one CIM query for every candidate process. This finds npm-shim\n  // installs (which run as node.exe) and drops the wmic dependency — wmic is\n  // removed on current Windows 11 builds, where the legacy path below silently\n  // yields nothing. Falls back to tasklist+wmic if PowerShell/CIM is unavailable.\n  const viaCim = await discoverWindowsViaCim();\n  if (viaCim) return viaCim;\n\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        const provider = providerForCommandLine(info.args);\n        if (provider === null) return null;\n\n        return {\n          pid,\n          provider,\n          projectPath: info.cwd,\n          projectName: basename(info.cwd),\n          branch: await readGitBranch(info.cwd),\n          conversationId: extractConversationId(info.args, provider),\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\n// Parse `ps -eo pid=,args=` output into pids whose command line is an agent CLI.\n// Exported for tests — this is where shim installs (running as `node`) are\n// recovered, which a name-only `pgrep -x claude` can never see.\nexport function parsePsOutput(stdout: string): number[] {\n  const pids: number[] = [];\n  for (const line of stdout.split(\"\\n\")) {\n    const trimmed = line.trim();\n    if (!trimmed) continue;\n    const match = trimmed.match(/^(\\d+)\\s+(.*)$/);\n    if (!match) continue;\n    const pid = Number.parseInt(match[1], 10);\n    if (!(pid > 0)) continue;\n    if (providerForCommandLine(match[2]) !== null) pids.push(pid);\n  }\n  return pids;\n}\n\nasync function getPidsUnix(): Promise<number[]> {\n  // One `ps` sweep matched on the command line. Falls back to the historical\n  // name-only pgrep if ps is unavailable; that path finds native-binary installs\n  // only, which is still better than returning nothing.\n  try {\n    return parsePsOutput(await run(\"ps\", [\"-eo\", \"pid=,args=\"]));\n  } catch {\n    // fall through\n  }\n  // Name-only fallback, one sweep per agent. It cannot see shim installs (those\n  // run as `node`) and cannot tell an interactive `codex` from the ChatGPT\n  // app's `codex app-server`, so the per-pid args re-read in discoverUnix is\n  // what actually filters this path — it drops anything that is neither agent.\n  const pids: number[] = [];\n  for (const name of [\"claude\", \"codex\"]) {\n    try {\n      const output = await run(\"pgrep\", [\"-x\", name]);\n      for (const line of output.trim().split(\"\\n\")) {\n        const pid = Number.parseInt(line, 10);\n        if (pid > 0) pids.push(pid);\n      }\n    } catch {\n      // No match for this agent, or no pgrep at all. Try the next one.\n    }\n  }\n  return pids;\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\n/**\n * Full command line of a single live process, or \"\" if it cannot be read.\n *\n * Used by the boot reconciler's pid-reuse guard: a recorded pid being alive is\n * never sufficient evidence that the process is still ours, because pids get\n * recycled. The caller matches its stored token against this before claiming a\n * surviving session — and treats an unreadable command line as \"cannot confirm\"\n * rather than \"confirmed\", so a failure here can never promote an unrelated\n * process into a managed session.\n */\nexport async function getProcessArgs(pid: number): Promise<string> {\n  if (!Number.isInteger(pid) || pid < 1) return \"\";\n  try {\n    if (platform() === \"win32\") {\n      const info = await getProcessInfoWindows(pid);\n      return info?.args ?? \"\";\n    }\n    return await getProcessArgsUnix(pid);\n  } catch {\n    // Process vanished between the liveness probe and this call, or ps/wmic is\n    // unavailable. Either way we cannot confirm identity.\n    return \"\";\n  }\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\ninterface CimProcess {\n  ProcessId: number;\n  CommandLine: string | null;\n  CreationDate: string | null;\n}\n\n// Parse the CIM/PowerShell JSON payload. Exported for tests: the shape differs\n// for a single result (bare object) vs several (array), and CreationDate can\n// arrive as an ISO string or as the /Date(ms)/ serialization.\nexport function parseCimProcesses(stdout: string): CimProcess[] {\n  const trimmed = stdout.trim();\n  if (!trimmed) return [];\n  const parsed = JSON.parse(trimmed);\n  return Array.isArray(parsed) ? parsed : [parsed];\n}\n\nexport function parseCimDate(value: string | null): Date {\n  if (value) {\n    const epoch = value.match(/\\/Date\\((\\d+)\\)\\//);\n    if (epoch) return new Date(Number(epoch[1]));\n    const d = new Date(value);\n    if (!Number.isNaN(d.getTime())) return d;\n  }\n  return new Date();\n}\n\nasync function discoverWindowsViaCim(): Promise<DiscoveredProcess[] | null> {\n  let stdout: string;\n  try {\n    // WQL filter at the provider — dumping every Win32_Process as JSON is what\n    // made GET /api/sessions take tens of seconds on a busy Windows box. Claude\n    // still runs as claude.exe or under a JS runtime (npm shim); looksLikeClaudeProcess\n    // below drops unrelated node/bun/deno processes.\n    stdout = await run(\"powershell.exe\", [\n      \"-NoProfile\",\n      \"-NonInteractive\",\n      \"-Command\",\n      \"Get-CimInstance Win32_Process -Filter \\\"Name = 'claude.exe' OR Name = 'claude' OR Name = 'codex.exe' OR Name = 'codex' OR Name = 'node.exe' OR Name = 'bun.exe' OR Name = 'deno.exe'\\\" | Select-Object ProcessId,CommandLine,CreationDate | ConvertTo-Json -Compress\",\n    ]);\n  } catch {\n    return null; // PowerShell unavailable — let the caller fall back.\n  }\n\n  let rows: CimProcess[];\n  try {\n    rows = parseCimProcesses(stdout);\n  } catch {\n    return null;\n  }\n\n  const results: DiscoveredProcess[] = [];\n  for (const row of rows) {\n    const commandLine = row.CommandLine ?? \"\";\n    if (!commandLine) continue;\n    // The CIM filter above matches on process name, which cannot separate an\n    // interactive `codex` from the ChatGPT app's `codex app-server`. This does.\n    const provider = providerForCommandLine(commandLine);\n    if (provider === null) continue;\n    // Windows exposes no process CWD (neither CIM nor wmic carries it), so the\n    // project path is genuinely unknown here rather than guessed. The previous\n    // code substituted the executable's own directory, which reported an\n    // unrelated install path as the user's project.\n    results.push({\n      pid: row.ProcessId,\n      provider,\n      projectPath: \"\",\n      projectName: \"\",\n      branch: \"\",\n      conversationId: extractConversationId(commandLine, provider),\n      startedAt: parseCimDate(row.CreationDate),\n    });\n  }\n  return results;\n}\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\n// Pull the conversation id out of a command line. Handles the forms Claude Code\n// accepts — `--resume <id>`, `--resume=<id>`, and the `-r` short flag — and\n// refuses a value that is itself a flag: `claude --resume --model opus` has no\n// id, and capturing \"--model\" would surface a session under a garbage id.\nexport function extractResumeId(args: string): string | null {\n  const eq = args.match(/(?:--resume|-r)=(\\S+)/);\n  if (eq?.[1] && !eq[1].startsWith(\"-\")) return eq[1];\n  const spaced = args.match(/(?:--resume|-r)\\s+(\\S+)/);\n  const candidate = spaced?.[1];\n  if (!candidate || candidate.startsWith(\"-\")) return null;\n  return candidate;\n}\n\nasync function readGitBranch(dir: string): Promise<string> {\n  // An empty cwd would make execFile inherit OUR working directory, so an\n  // unreadable process reported the streamer's own branch as if it were its own\n  // (observed live). No directory, no answer.\n  if (!dir) return \"\";\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 { z } from \"zod\";\n\nexport const PROMPT_SCHEMA_VERSION = 1 as const;\n\nconst OpaqueIdSchema = z.string().trim().min(1).max(200);\nconst MeaningfulStringSchema = z.string().trim().min(1);\n\nexport const PromptOptionSchema = z.object({\n  optionId: OpaqueIdSchema,\n  label: MeaningfulStringSchema,\n  description: z.string().optional(),\n  preview: z.string().optional(),\n});\n\nexport const PromptQuestionSchema = z\n  .object({\n    questionId: OpaqueIdSchema,\n    text: MeaningfulStringSchema,\n    header: z.string().optional(),\n    inputMode: z.enum([\"single\", \"multi\", \"text\"]),\n    options: z.array(PromptOptionSchema),\n    allowOther: z.boolean(),\n    secret: z.union([z.boolean(), z.literal(\"unknown\")]),\n  })\n  .superRefine((question, ctx) => {\n    const optionIds = question.options.map((option) => option.optionId);\n    if (new Set(optionIds).size !== optionIds.length) {\n      ctx.addIssue({\n        code: \"custom\",\n        message: \"optionId values must be unique\",\n        path: [\"options\"],\n      });\n    }\n    if (question.inputMode === \"text\" && question.options.length !== 0) {\n      ctx.addIssue({\n        code: \"custom\",\n        message: \"text questions cannot carry options\",\n        path: [\"options\"],\n      });\n    }\n    if (question.inputMode !== \"text\" && question.options.length === 0) {\n      ctx.addIssue({\n        code: \"custom\",\n        message: \"select questions require options\",\n        path: [\"options\"],\n      });\n    }\n  });\n\nconst TERMINAL_PROMPT_STATES = new Set([\"resolved\", \"cancelled\", \"expired\", \"unavailable\"]);\n\nexport const PromptSchema = z\n  .object({\n    schemaVersion: z.literal(PROMPT_SCHEMA_VERSION),\n    sessionId: OpaqueIdSchema,\n    promptId: OpaqueIdSchema,\n    revision: z.number().int().positive(),\n    state: z.enum([\"open\", \"updated\", \"resolved\", \"cancelled\", \"expired\", \"unavailable\"]),\n    terminalReason: MeaningfulStringSchema.optional(),\n    intent: z.enum([\"approval\", \"question\"]),\n    title: z.string().optional(),\n    message: z.string().optional(),\n    detail: z.string().optional(),\n    questions: z.array(PromptQuestionSchema).min(1),\n    answerRequirement: z.enum([\"blocking\", \"non_blocking\", \"unknown\"]),\n    expiresAt: z.string().datetime({ offset: true }).nullable(),\n    provenance: z.object({\n      source: z.enum([\"provider\", \"screen\", \"transcript\", \"synthetic\"]),\n      confidence: z.enum([\"authoritative\", \"inferred\"]),\n    }),\n  })\n  .superRefine((prompt, ctx) => {\n    if (![prompt.title, prompt.message, prompt.detail].some((value) => value?.trim())) {\n      ctx.addIssue({\n        code: \"custom\",\n        message: \"prompt requires a meaningful title, message, or detail\",\n        path: [\"message\"],\n      });\n    }\n    const questionIds = prompt.questions.map((question) => question.questionId);\n    if (new Set(questionIds).size !== questionIds.length) {\n      ctx.addIssue({\n        code: \"custom\",\n        message: \"questionId values must be unique\",\n        path: [\"questions\"],\n      });\n    }\n    const optionIds = prompt.questions.flatMap((question) =>\n      question.options.map((option) => option.optionId),\n    );\n    if (new Set(optionIds).size !== optionIds.length) {\n      ctx.addIssue({\n        code: \"custom\",\n        message: \"optionId values must be unique within a prompt\",\n        path: [\"questions\"],\n      });\n    }\n    const terminal = TERMINAL_PROMPT_STATES.has(prompt.state);\n    if (terminal !== (prompt.terminalReason !== undefined)) {\n      ctx.addIssue({\n        code: \"custom\",\n        message: terminal\n          ? \"terminal prompts require terminalReason\"\n          : \"actionable prompts cannot carry terminalReason\",\n        path: [\"terminalReason\"],\n      });\n    }\n  });\n\nconst OptionResponseSchema = z.object({\n  questionId: OpaqueIdSchema,\n  optionIds: z\n    .array(OpaqueIdSchema)\n    .min(1)\n    .refine((ids) => new Set(ids).size === ids.length, \"optionIds must be unique\"),\n  text: z.never().optional(),\n});\n\nconst TextResponseSchema = z.object({\n  questionId: OpaqueIdSchema,\n  text: z.string(),\n  optionIds: z.never().optional(),\n});\n\nexport const PromptResponseSchema = z.union([OptionResponseSchema, TextResponseSchema]);\n\nexport const PromptAnswerSchema = z\n  .object({\n    promptId: OpaqueIdSchema,\n    revision: z.number().int().positive(),\n    responses: z.array(PromptResponseSchema).min(1),\n    idempotencyKey: OpaqueIdSchema,\n  })\n  .superRefine((answer, ctx) => {\n    const questionIds = answer.responses.map((response) => response.questionId);\n    if (new Set(questionIds).size !== questionIds.length) {\n      ctx.addIssue({\n        code: \"custom\",\n        message: \"each questionId can be answered only once\",\n        path: [\"responses\"],\n      });\n    }\n  });\n\nexport type Prompt = z.infer<typeof PromptSchema>;\nexport type PromptQuestion = z.infer<typeof PromptQuestionSchema>;\nexport type PromptOption = z.infer<typeof PromptOptionSchema>;\nexport type PromptAnswer = z.infer<typeof PromptAnswerSchema>;\nexport type PromptResponse = z.infer<typeof PromptResponseSchema>;\n","import { Connection, Client as TemporalClient } from \"@temporalio/client\";\nimport { randomUUID } from \"crypto\";\nimport { EventEmitter } from \"events\";\nimport { existsSync, readdirSync, readFileSync, statSync } from \"fs\";\nimport { realpath } from \"fs/promises\";\nimport type { Hono } from \"hono\";\nimport { createServer, type IncomingMessage, type ServerResponse } from \"http\";\nimport { homedir, hostname } from \"os\";\nimport { dirname, join } from \"path\";\nimport type { WebSocket } from \"ws\";\nimport { describeAccessProbe, probeAccessGate, safeHost } from \"./access-probe\";\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 { type AppEnv, createHonoApp } from \"./api/app\";\nimport { ConversationHandlers } from \"./api/handlers/conversations.handlers\";\nimport { json, readBody, writeHonoResponse } from \"./api/handlers/http-helpers\";\nimport { SessionHandlers } from \"./api/handlers/sessions.handlers\";\nimport { describeE2eeCapability, E2EE_OFF_SWITCH } from \"./api/routes/misc.routes\";\nimport { ALREADY_HANDLED } from \"./api/routes/sessions.routes\";\nimport { mountWebSocket } from \"./api/routes/ws.routes\";\nimport {\n  generateApiKey,\n  loadAccessServiceToken,\n  loadBrowseRoot,\n  loadBrowserCors,\n  loadCacheDir,\n  loadClaudeExtraArgs,\n  loadClaudeFlags,\n  loadDefaultPermissionMode,\n  loadFeatureFlags,\n  loadPublicUrl,\n  loadTailSize,\n  setApiKey,\n  setClaudeExtraArgs,\n  setClaudeFlags,\n  validatePublicUrl,\n} from \"./auth\";\nimport {\n  BrowsePathNotFoundError,\n  createDirectory,\n  listDirectories,\n  listFiles,\n  resolveBrowsePath,\n} from \"./browse\";\nimport {\n  CLAUDE_FLAGS,\n  type ClaudeFlagValues,\n  EFFORT_LEVELS,\n  type EffortLevel,\n  isEffortLevel,\n  isPermissionMode,\n  type PermissionMode,\n  validateFlagValues,\n} from \"./claude-flags\";\nimport { ConversationCache } from \"./conversation-cache\";\nimport { listCursorTranscriptWatchDirs } from \"./cursor-transcript-watch\";\nimport { createPool, getDbConfig, maskConnectionString, runMigrations } from \"./db\";\nimport { CacheMetadataRepository } from \"./db/repositories/cacheMetadata.repository\";\nimport { ConversationsRepository } from \"./db/repositories/conversations.repository\";\nimport { DevicesRepository } from \"./db/repositories/devices.repository\";\nimport { ManagedSessionsRepository } from \"./db/repositories/managed-sessions.repository\";\nimport { ProjectsRepository } from \"./db/repositories/projects.repository\";\nimport { PushRepository } from \"./db/repositories/push.repository\";\nimport { SessionsRepository } from \"./db/repositories/sessions.repository\";\nimport { RuntimeStore, resolveRuntimeDbPath } from \"./db/runtime-store\";\nimport {\n  type HandshakeResponderState,\n  keyPairFrom,\n  PAIR_PROLOGUE,\n  pskFromPairToken,\n  readMessage1,\n  writeMessage2,\n} from \"./e2ee/noise\";\nimport {\n  type E2eePairRegistration,\n  encodeE2eeMsg2Payload,\n  parseE2eeMsg1Payload,\n} from \"./e2ee/pair-payload\";\nimport {\n  E2EE_EXCHANGE_VERSION,\n  type E2eeExchangeRequest,\n  type E2eeRequestError,\n  parseE2eeRequest,\n} from \"./e2ee/pair-request\";\nimport { type ExternalTailEntry, ExternalTailManager } from \"./external-tails\";\nimport {\n  describeFeatureFlags,\n  FEATURE_FLAG_LIST,\n  type FeatureFlagId,\n  type FeatureFlagSource,\n  nonDefaultFeatureFlags,\n  type ResolvedFeatureFlags,\n  resolveFeatureFlags,\n} from \"./feature-flags\";\nimport { LiveSessionManager } from \"./live-session-manager\";\nimport { getLogger } from \"./logger\";\nimport { PairTokenStore } from \"./pair-store\";\nimport { locateProviderExe } from \"./platform\";\nimport {\n  CLAUDE_CODE_PROVIDER,\n  CODEX_CLI_PROVIDER,\n  coerceProviderForRunner,\n  PROVIDER_NAMES,\n  type ProviderName,\n} from \"./providers\";\nimport { PtyHostProtocolMismatchError } from \"./pty-host/remote-session-runner\";\nimport { connectOrSpawnHost } from \"./pty-host/spawn-host\";\nimport { ScannerManager } from \"./scanner-manager\";\nimport { seal } from \"./seal\";\nimport { loadOrCreateServerIdentity } from \"./server-identity\";\nimport {\n  clearExpiredPendingPrompt,\n  createApiDeps,\n  createConversationWatcherEvents,\n  createLiveSessionOptions,\n  type PendingPermission,\n  type PendingQuestion,\n} from \"./server-wiring\";\nimport { setCacheMetadata } from \"./services/cache/cacheMetadata\";\nimport { CacheIntegrityMonitor } from \"./services/cache-integrity/cacheIntegrityMonitor\";\nimport { ConversationWatcher } from \"./services/conversations/conversationWatcher\";\nimport { parseAgentEntrypointsEnv } from \"./services/conversations/isAgentConversation\";\nimport { pruneAgentConversations } from \"./services/conversations/pruneAgentConversations\";\nimport { refreshConversationCache } from \"./services/conversations/refreshConversationCache\";\nimport {\n  createHostPressureMonitor,\n  type HostPressureMonitor,\n} from \"./services/host-pressure/hostPressure\";\nimport { PromptRegistry } from \"./services/prompts/promptRegistry\";\nimport {\n  ApnsClient,\n  describeMissingApnsCredentials,\n  readApnsCredentialsFromEnv,\n} from \"./services/push/apnsClient\";\nimport { ExpoPushSender } from \"./services/push/expoPushSender\";\nimport { LiveActivityNotifier } from \"./services/push/liveActivityNotifier\";\nimport { LiveActivityRenewalScheduler } from \"./services/push/liveActivityRenewal\";\nimport { LiveActivitySender } from \"./services/push/liveActivitySender\";\nimport { WaitingInputNotifier } from \"./services/push/waitingInputNotifier\";\nimport { questionContentKey } from \"./services/questions/detectQuestionFromScreen\";\nimport {\n  questionsFromLines,\n  shouldBroadcastQuestion,\n} from \"./services/questions/questionBroadcast\";\nimport { sweepCodexFormat } from \"./services/sessions/codexFormatCanary\";\nimport type { CodexOwnerSource } from \"./services/sessions/codexRolloutOwner\";\nimport { type BusySignal, resolveResumeBusyWindowMs } from \"./services/sessions/conversationBusy\";\nimport { IdempotencyStore } from \"./services/sessions/idempotency\";\nimport type { ReconcileVerdict } from \"./services/sessions/reconcileSessions\";\nimport { resumeIdForRow } from \"./services/sessions/resumeIdentity\";\nimport { SessionRegistryBoot } from \"./session-registry-boot\";\nimport { SessionStore } from \"./session-store\";\nimport { SessionWatchers } from \"./session-watchers\";\nimport type {\n  AskQuestion,\n  DiscoveredProcess,\n  ManagedSession,\n  ServerConfig,\n  ServerWarmingUpResponse,\n  ServerWarmupState,\n  SessionResponse,\n} from \"./types\";\nimport { canonicalizeFilePath, toNativeFilePath } from \"./utils/canonicalizeFilePath\";\nimport { toClientConversationLines } from \"./utils/codexConversationLine\";\nimport { parseIsoDateOrNull } from \"./utils/dates\";\nimport { createScanProgressThrottle } from \"./utils/scanProgressThrottle\";\nimport { getVersion } from \"./version\";\nimport { WSHub } from \"./ws-hub\";\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 `running` session is deferred (not held) so a mid-response turn is never\n// interrupted. But a PTY that never settles back to waiting_input (e.g. its\n// last line was a status bar with no prompt marker) would re-arm the grace\n// timer forever and leak. Cap consecutive defers: after this many, hold anyway.\nexport const GRACE_MAX_DEFERS = 4;\n\n// Idle reaper. Replaces \"kill the PTY because nobody is subscribed\" with \"kill\n// the PTY because the *agent* has done nothing for a long time\" — a socket\n// closing says nothing about whether work is in flight, but silence from the\n// agent itself does. See docs/architecture/2026-07-24-durable-session-runtime.md.\n//\n// 6h is deliberately far above the old 4.5-minute grace period: the reaper is a\n// resource backstop for abandoned sessions, not a session-lifetime policy. A\n// session is only ever eligible while settled (waiting_input/idle) — a `running`\n// PTY is never reaped no matter how long it has been running, because a long\n// silent turn is exactly the work this runtime exists to protect.\nexport const IDLE_REAP_AFTER_MS = 6 * 60 * 60 * 1000;\n// How often the sweep runs. Coarse on purpose — reaping 5 minutes late costs\n// nothing, and a frequent timer on an idle server does not earn its wakeups.\nexport const IDLE_REAP_SWEEP_MS = 5 * 60 * 1000;\n\n/**\n * How often the Codex rollout format is sampled. Hourly: the thing being\n * watched changes with a Codex release, not minute to minute, and a sweep\n * reads a few files' opening lines.\n */\nconst CODEX_FORMAT_CANARY_INTERVAL_MS = 60 * 60 * 1000;\n\n// Accepted `--model` / `/model` values: an alias (\"opus\", \"sonnet\") or a full\n// model name (\"claude-opus-4-5\"). Deliberately strict — this string is written\n// straight into a live PTY by applyLiveSessionSetting, so anything that could\n// terminate the slash command (\\r, \\n) or start another word must be rejected\n// rather than escaped.\nconst MODEL_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;\n\n// ─── Session lifecycle handlers ──────────────────────────────────────────────\n// Same arrangement: the adopt kill-wait and the resume discovery bound live\n// beside the handlers that use them, and are re-exported here because callers\n// (and tests) import them from this module.\nexport {\n  ADOPT_KILL_TIMEOUT_MS,\n  RESUME_DISCOVERY_TIMEOUT_MS,\n  waitForProcessExit,\n} from \"./api/handlers/sessions.handlers\";\n// ─── External (non-PTY) live tails ───────────────────────────────────────────\n// Constants and behaviour live in ./external-tails with the manager that uses\n// them; re-exported here because callers (and tests) import them from this\n// module.\nexport {\n  EXTERNAL_ACTIVE_WRITING_MS,\n  EXTERNAL_TAIL_IDLE_MS,\n  EXTERNAL_TAIL_MAX,\n  EXTERNAL_TAIL_RECENCY_MS,\n} from \"./external-tails\";\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\n/**\n * Why a resume did not happen, in the vocabulary of the thing that failed\n * rather than of HTTP (plan Phase 7c).\n *\n * `resumeSession` has two callers with nothing in common at the response layer:\n * one writes a status code, the other writes a log line. Neither can be the\n * owner of these distinctions, so they live here.\n */\nexport type ResumeFailure =\n  | { ok: false; reason: \"history_file_missing\" }\n  | { ok: false; reason: \"no_project_path\" }\n  | {\n      ok: false;\n      reason: \"conversation_busy\";\n      /** Which signals fired — carried verbatim so the 409 body is unchanged. */\n      detectedBy: BusySignal[];\n      lastActivityMs: number | null;\n      likelyOwner: \"external\" | \"unknown\";\n    }\n  /**\n   * Codex's own single-writer lock said no — either an open handle on the exact\n   * rollout (pre-flight) or the `-32600` error Codex printed after spawn.\n   *\n   * Separate from `conversation_busy` because the two are not the same claim:\n   * that one is a heuristic a caller may override with `force`, this one is the\n   * provider refusing, which no flag of ours can bypass.\n   */\n  | {\n      ok: false;\n      reason: \"codex_session_active\";\n      detectedBy: BusySignal[];\n      lastActivityMs: number | null;\n      ownerPid?: number;\n      ownerSource?: CodexOwnerSource;\n    }\n  /** Codex exited or errored during startup for some other reason. */\n  | { ok: false; reason: \"codex_start_failed\"; failureReason: string };\n\nexport type ResumeOutcome =\n  | ResumeFailure\n  | {\n      ok: true;\n      /** The session was already live here — nothing was spawned. */\n      alreadyRunning: true;\n      session: null;\n      response: SessionResponse;\n    }\n  | {\n      ok: true;\n      alreadyRunning: false;\n      session: ManagedSession;\n      /** Null only if the store lost the session between spawn and read. */\n      response: SessionResponse | null;\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  // canonical JSONL path → live tail on a file NO PTY session owns (an external\n  // agent is writing it). Deliberately separate from sessionFileMap so managed\n  // session semantics — terminal_output, session_update, question cards — are\n  // untouched: an external tail only ever pushes transcript lines.\n  private externalTails = new Map<string, ExternalTailEntry>();\n  // Drives the map above; the map itself stays a server field because it is\n  // also read directly here (agent-file eviction, close()).\n  private externalTailManager: ExternalTailManager;\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  /**\n   * A session's PTY size, recorded only when something resizes it away from the\n   * spawn defaults. Absent means \"still PTY_COLS x PTY_ROWS\", which is why this\n   * is a sparse map rather than a field on every session: resize is opt-in and\n   * rare, and the default is what every reader already assumes.\n   */\n  private sessionGeometry = new Map<string, { cols: number; rows: number }>();\n  // `origin` records whether the pending question came from the live PTY-screen\n  // path (handleLiveQuestion) or a JSONL flush. A JSONL-derived question must\n  // never clobber a PTY-originated one for a DIFFERENT question — an external\n  // agent appending an AskUserQuestion into a shared conversation would\n  // otherwise misroute the answer into this streamer's PTY.\n  private pendingQuestions = new Map<string, PendingQuestion>();\n  // Sessions resumed past a detected collision (busy probe said busy, caller\n  // forced). JSONL-derived actionable question cards are suppressed for these\n  // because a line in the shared file may have been written by the other owner.\n  private contendedSessions = new Set<string>();\n  // conversationId → ms epoch when THIS streamer's PTY for it last went idle.\n  // Lets the resume collision probe tell our own trailing JSONL writes (a\n  // hold → resume round trip) apart from another owner's. Pruned on write so it\n  // cannot grow without bound across a long-lived process.\n  private selfPtyEndedAt = new Map<string, number>();\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<string, PendingPermission>();\n  // Content key (prompt + detail + options + cursor) of the permission gate\n  // currently broadcast for a session — mirrors pendingQuestionKey so a PTY\n  // repaint of the same gate doesn't re-broadcast on every tick. Cleared\n  // alongside pendingPermission.\n  private pendingPermissionKey = new Map<string, string>();\n  private promptRegistry: PromptRegistry;\n  // Scanner lifecycle, freshness state and the cache↔disk reconcile.\n  private scannerManager: ScannerManager;\n  // Binds a live session to the JSONL/rollout its provider writes.\n  private sessionWatchers: SessionWatchers;\n  // Boot/shutdown lifecycle of the durable session registry.\n  private registryBoot: SessionRegistryBoot;\n  // Conversation list/count/detail/search reads and the JSONL/cwd resolvers.\n  private conversationHandlers: ConversationHandlers;\n  // Session list/get/start/resume/fork/adopt/input/answer/upload/stop handlers.\n  private sessionHandlers: SessionHandlers;\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 activeWarmups = new Map<number, ServerWarmupState>([[0, \"startup\"]]);\n  private nextWarmupId = 1;\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 host: string | undefined;\n  // Skip the startup warm-up scan (test hook; see ServerConfig.skipStartupWarmup).\n  private skipStartupWarmup: boolean;\n  private autoResumeOnBoot: boolean;\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  // Resolved once at boot; see src/feature-flags.ts. Total map — every registry\n  // id is present, so indexing it never yields undefined.\n  private featureFlags: ResolvedFeatureFlags;\n  // Which rung of the precedence chain decided each flag. Reported at boot and\n  // over GET /api/config/feature-flags — the resolved boolean alone cannot say\n  // whether a value came from the environment, the CLI, server.yaml or nowhere.\n  private featureFlagSources: Record<FeatureFlagId, FeatureFlagSource>;\n  // Derived from featureFlags.codexSystemPrompt. Kept as its own field so the\n  // read site in startFresh() is unchanged.\n  private codexSystemPromptEnabled: boolean;\n  private defaultPermissionMode: PermissionMode;\n  private defaultModel: string;\n  private defaultEffort: EffortLevel;\n  // Allowlisted Claude CLI flags + free-text escape hatch, applied to every\n  // spawn. Resolved once at startup (flag → server.yaml), then mutated in place\n  // by PUT /api/config/claude-flags so a change applies to the next session\n  // without a restart.\n  private claudeFlags: ClaudeFlagValues;\n  private claudeExtraArgs: string | undefined;\n  // True when the values came from server.yaml (and so a write persists).\n  // False when they were pinned by a CLI flag, mirroring the api-key rotate\n  // contract: the write still takes effect in memory but won't survive restart.\n  private claudeFlagsPersistable: boolean;\n  // Map of sessionId → grace timer; fires to kill PTY after WS disconnect\n  private ptyGraceTimers = new Map<string, ReturnType<typeof setTimeout>>();\n  // Consecutive grace-timer defers for a still-`running` session (see\n  // GRACE_MAX_DEFERS). Reset when a subscriber reconnects or the PTY settles.\n  private ptyGraceDeferCounts = new Map<string, number>();\n  // Session ids that should be putOnHold at the next waiting_input/idle.\n  // Same lifetime as ptyGraceTimers: in-memory, dropped on close/restart.\n  // Last writer wins against the grace timer — never both armed. Value\n  // records whether THIS arming should ignore watchers, both at fire time\n  // and against a later subscriber connecting while it's still armed, and\n  // whether the cached conversation should be soft-deleted once it fires.\n  private holdWhenIdle = new Map<string, { ignoreWatchers: boolean; deleteAfter: boolean }>();\n  // Map of sessionId → set of subscribed WS clients\n  private sessionSubscribers = new Map<string, Set<WebSocket>>();\n  // sessionId → wall-clock ms of the last PTY chunk. Written from onOutput for\n  // every provider; read only by the idle reaper. Entries are dropped when the\n  // session leaves the runner (reap/exit/hold).\n  private lastAgentChunkAt = new Map<string, number>();\n  // sessionId → last terminal_output seq broadcast (starts at 1, per session).\n  // Stamped on every terminal_output/terminal_replay so a client can detect a\n  // stale chunk delivered after a reconnect race instead of trusting raw WS\n  // arrival order. Entries dropped alongside lastAgentChunkAt.\n  private terminalSeq = new Map<string, number>();\n  // Recently accepted input idempotency keys (C4). A retried POST replays its\n  // original outcome instead of submitting the prompt to the agent twice.\n  private idempotency = new IdempotencyStore();\n  // sessionId → lifecycle verdict from boot reconciliation. Only holds sessions\n  // this run did NOT spawn; live ones derive their lifecycle from ptyAttached.\n  private sessionVerdicts = new Map<string, ReconcileVerdict>();\n  // Periodic sweep that releases PTYs no agent is using. Null until listen().\n  private idleReaperTimer: ReturnType<typeof setInterval> | null = null;\n  private codexFormatCanaryTimer: ReturnType<typeof setInterval> | null = null;\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 cacheMonitor: CacheIntegrityMonitor | null = null;\n  private hostPressureMonitor: HostPressureMonitor | null = null;\n  private projectsRepo: ProjectsRepository | null = null;\n  private conversationsRepo: ConversationsRepository | null = null;\n  private sessionsRepo: SessionsRepository | null = null;\n  // Durable session registry (C1 Phase 2). Null when runtime.db failed to open\n  // — persistence degrades to today's in-memory-only behaviour rather than\n  // taking the server down with it, so every write goes through `?.`. Note the\n  // handle is runtime.db, NOT the conversation cache: a cache failure used to\n  // null this repo and silently disable all session persistence.\n  private managedSessionsRepo: ManagedSessionsRepository | null = null;\n  private runtimeStore: RuntimeStore | null = null;\n  // Identifies this streamer run. A registry row carrying a different id is a\n  // session that outlived the process that started it.\n  private readonly streamerInstanceId = randomUUID();\n  private cacheMetadataRepo: CacheMetadataRepository | null = null;\n  // Push registration + delivery state (C7). Null when the cache DB failed to\n  // open — registration then degrades to a no-op rather than 500ing.\n  private pushRepo: PushRepository | null = null;\n\n  // Paired-device registry (C5). Null when the cache DB failed to open — auth\n  // then falls back to the shared API key alone, which is the pre-C5 behaviour.\n  private devicesRepo: DevicesRepository | null = null;\n  // Live Activity push (Feature 12). Null when APNS_KEY is unset — the ordinary\n  // case on a dev machine and in CI, where the feature is simply off. Missing an\n  // optional push credential must never stop the server from booting.\n  private apnsClient: ApnsClient | null = null;\n  private liveActivityNotifier: LiveActivityNotifier | null = null;\n  private liveActivityRenewal: LiveActivityRenewalScheduler | null = null;\n  // \"Your turn\" notifications over Expo's relay (#528). Needs no credential of\n  // its own, so unlike the Live Activity path it is on wherever the cache DB\n  // opened — with no registered device it simply sends nothing.\n  private waitingInputNotifier: WaitingInputNotifier | null = null;\n  private expoPushSender: ExpoPushSender | null = null;\n  private discoveryCache: {\n    entries: DiscoveredProcess[];\n    fetchedAt: number;\n  } | null = null;\n  // Single-flight for process discovery. Mobile polls GET /api/sessions and\n  // retries on timeout; without this, every concurrent request starts its own\n  // Windows CIM scan (observed: overlapping 80–100s /api/sessions responses).\n  private discoveryInFlight: Promise<DiscoveredProcess[]> | null = null;\n  private cacheDir: string;\n  private runtimeDbPath: string;\n  private tailSize: number;\n  private directoryDebounceMs: number;\n  private codexRoots: string[];\n  private cursorRoots: 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.host = config.host;\n    this.skipStartupWarmup = config.skipStartupWarmup ?? false;\n    this.autoResumeOnBoot = config.autoResumeOnBoot ?? false;\n    this.scanProfiles = config.scanProfiles;\n    this.codexRoots = config.codexRoots ?? [join(homedir(), \".codex\", \"sessions\")];\n    this.cursorRoots = config.cursorRoots ?? [join(homedir(), \".cursor\", \"projects\")];\n    this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;\n    this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;\n    // env > CLI > server.yaml > registry default, then the legacy explicit\n    // field on top (see ServerConfig.codexSystemPromptEnabled).\n    const flagResolution = resolveFeatureFlags({\n      override:\n        config.codexSystemPromptEnabled === undefined\n          ? undefined\n          : { codexSystemPrompt: config.codexSystemPromptEnabled },\n      cli: config.featureFlags,\n      yaml: loadFeatureFlags(),\n    });\n    this.featureFlags = flagResolution.values;\n    this.featureFlagSources = flagResolution.sources;\n    this.codexSystemPromptEnabled = this.featureFlags.codexSystemPrompt;\n    this.defaultPermissionMode =\n      config.defaultPermissionMode ?? loadDefaultPermissionMode() ?? \"acceptEdits\";\n    this.defaultModel = config.defaultModel ?? \"sonnet\";\n    this.defaultEffort = config.defaultEffort ?? \"low\";\n    this.claudeFlagsPersistable = config.claudeFlags === undefined;\n    this.claudeFlags = config.claudeFlags ?? loadClaudeFlags();\n    this.claudeExtraArgs = config.claudeExtraArgs ?? loadClaudeExtraArgs();\n    this.cacheDir = config.cacheDir ?? loadCacheDir() ?? join(homedir(), \".threadbase\", \"cache\");\n    // Sibling of server.yaml, deliberately NOT under cache/ — see runtime-store.ts.\n    this.runtimeDbPath = resolveRuntimeDbPath(config.runtimeDbPath);\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.scannerManager = new ScannerManager({\n      scanProfiles: this.scanProfiles,\n      codexRoots: this.codexRoots,\n      cursorRoots: this.cursorRoots,\n      directoryDebounceMs: this.directoryDebounceMs,\n      persistenceDisabled: config.scannerPersistent === false,\n      // Thunks, not values: these are opened during listen() and rebound by\n      // the integrity monitor's reset-and-rescan.\n      cache: () => this.cache,\n      cacheMonitor: () => this.cacheMonitor,\n      projectsRepo: () => this.projectsRepo,\n      conversationsRepo: () => this.conversationsRepo,\n      cacheMetadataRepo: () => this.cacheMetadataRepo,\n      trackCacheWrite: (task) => this.trackCacheWrite(task),\n    });\n    this.registryBoot = new SessionRegistryBoot({\n      // Thunks for the same reason ScannerManager takes them: the registry repo\n      // is opened during listen(), and `log`/`resumeSession` are swapped on the\n      // server instance by tests.\n      log: () => this.log,\n      ptyManager: () => this.ptyManager,\n      sessionStore: () => this.sessionStore,\n      featureFlags: () => this.featureFlags,\n      autoResumeOnBoot: () => this.autoResumeOnBoot,\n      managedSessionsRepo: () => this.managedSessionsRepo,\n      cache: () => this.cache,\n      streamerInstanceId: this.streamerInstanceId,\n      sessionVerdicts: this.sessionVerdicts,\n      selfPtyEndedAt: this.selfPtyEndedAt,\n      resumeSession: (opts) => this.sessionHandlers.resumeSession(opts),\n      watchConversationFile: (sessionId, historyId) =>\n        this.sessionWatchers.watchConversationFile(sessionId, historyId),\n      broadcastSessionList: () => this.wsHub.broadcast(this.sessionListPayload()),\n      resolveConversationTarget: (sessionId) => this.resolveConversationTarget(sessionId),\n    });\n    this.includeAgents = parseIncludeAgentsEnv(process.env.THREADBASE_INCLUDE_AGENTS);\n    this.agentEntrypoints = parseAgentEntrypointsEnv(process.env.THREADBASE_AGENT_ENTRYPOINTS);\n\n    // Every flag, every boot, with its value and the rung that decided it.\n    //\n    // This used to print only the ids differing from their defaults, under the\n    // heading \"Feature flags active\" — which stated the opposite of the truth\n    // for a flag defaulting ON: disabling sessionRehydration listed it as\n    // active. It also went silent on a stock boot, so the log could never\n    // answer \"what was this process actually running with\", only hint at it.\n    // One line for four booleans is affordable; being wrong is not.\n    this.log.info(`Feature flags: ${describeFeatureFlags(flagResolution)}`, {\n      event: \"config.feature_flags\",\n      values: this.featureFlags,\n      sources: this.featureFlagSources,\n      nonDefault: nonDefaultFeatureFlags(this.featureFlags),\n    });\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      (id, session) =>\n        (this.featureFlags.subagentSessions || session?.isSubagent !== true) &&\n        (this.cache?.isVisible(id) ?? true),\n    );\n    this.wsHub = new WSHub();\n    this.promptRegistry = new PromptRegistry({\n      emit: (event) =>\n        this.wsHub.broadcastToClients(this.sessionSubscribers.get(event.sessionId) ?? [], event),\n      onExpire: (prompt) =>\n        clearExpiredPendingPrompt(\n          {\n            pendingPermission: this.pendingPermission,\n            pendingPermissionKey: this.pendingPermissionKey,\n            pendingQuestions: this.pendingQuestions,\n            pendingQuestionKey: this.pendingQuestionKey,\n            sessionSubscribers: this.sessionSubscribers,\n            wsHub: this.wsHub,\n          },\n          prompt,\n        ),\n    });\n\n    this.fileWatcher = new ConversationWatcher(\n      createConversationWatcherEvents({\n        sessionFileMap: this.sessionFileMap,\n        pendingLineSeqs: this.pendingLineSeqs,\n        scannerManager: this.scannerManager,\n        // Thunks, not values: the cache is opened during listen(),\n        // externalTailManager is constructed below, and fileWatcher is the\n        // watcher these very events are being handed to.\n        cache: () => this.cache,\n        log: () => this.log,\n        fileWatcher: () => this.fileWatcher,\n        externalTailManager: () => this.externalTailManager,\n        trackCacheWrite: (task) => this.trackCacheWrite(task),\n        processJsonlQuestions: (sessionId, lines) => this.processJsonlQuestions(sessionId, lines),\n        broadcastConversationLines: (sessionId, lines, seqs) =>\n          this.broadcastConversationLines(sessionId, lines, seqs),\n      }),\n    );\n\n    this.externalTailManager = new ExternalTailManager({\n      tails: this.externalTails,\n      sessionFileMap: this.sessionFileMap,\n      fileWatcher: this.fileWatcher,\n      wsHub: this.wsHub,\n      // Thunks, not values: these are opened during listen() and rebound by\n      // the integrity monitor's reset-and-rescan.\n      cache: () => this.cache,\n      cacheMonitor: () => this.cacheMonitor,\n      broadcastConversationLines: (sessionId, lines, seqs) =>\n        this.broadcastConversationLines(sessionId, lines, seqs),\n    });\n\n    this.ptyManager = new LiveSessionManager(\n      createLiveSessionOptions({\n        sessionGeometry: this.sessionGeometry,\n        sessionStore: this.sessionStore,\n        wsHub: this.wsHub,\n        fileWatcher: this.fileWatcher,\n        scannerManager: this.scannerManager,\n        sessionStatusBus: this.sessionStatusBus,\n        sessionFileMap: this.sessionFileMap,\n        sessionSubscribers: this.sessionSubscribers,\n        lastAgentChunkAt: this.lastAgentChunkAt,\n        terminalSeq: this.terminalSeq,\n        pendingQuestions: this.pendingQuestions,\n        pendingQuestionKey: this.pendingQuestionKey,\n        pendingPermission: this.pendingPermission,\n        pendingPermissionKey: this.pendingPermissionKey,\n        promptRegistry: this.promptRegistry,\n        contendedSessions: this.contendedSessions,\n        // Thunks, not values: sessionHandlers is constructed below, the\n        // registry repo and the push notifiers are bound during listen(), and\n        // tests swap `log` on the server instance.\n        log: () => this.log,\n        sessionHandlers: () => this.sessionHandlers,\n        managedSessionsRepo: () => this.managedSessionsRepo,\n        liveActivityNotifier: () => this.liveActivityNotifier,\n        waitingInputNotifier: () => this.waitingInputNotifier,\n        ptyAttachedIds: () => this.ptyAttachedIds(),\n        cancelPendingQuestion: (sessionId) => this.cancelPendingQuestion(sessionId),\n        rememberSelfPtyEnded: (conversationId) => this.rememberSelfPtyEnded(conversationId),\n        maybeFireHoldWhenIdle: (session) => this.maybeFireHoldWhenIdle(session),\n      }),\n    );\n\n    this.sessionWatchers = new SessionWatchers({\n      ptyManager: this.ptyManager,\n      sessionStore: this.sessionStore,\n      wsHub: this.wsHub,\n      fileWatcher: this.fileWatcher,\n      sessionFileMap: this.sessionFileMap,\n      scannerManager: this.scannerManager,\n      codexRoots: this.codexRoots,\n      cursorRoots: this.cursorRoots,\n      // Thunks, not values: these are opened during listen() and rebound by\n      // the integrity monitor's reset-and-rescan.\n      cache: () => this.cache,\n      projectsRepo: () => this.projectsRepo,\n      conversationsRepo: () => this.conversationsRepo,\n      sessionsRepo: () => this.sessionsRepo,\n      cacheMetadataRepo: () => this.cacheMetadataRepo,\n      managedSessionsRepo: () => this.managedSessionsRepo,\n      findConversationByUuid: (uuid) => this.conversationHandlers.findConversationByUuid(uuid),\n      broadcastConversationLines: (sessionId, lines, seqs) =>\n        this.broadcastConversationLines(sessionId, lines, seqs),\n      ptyAttachedIds: () => this.ptyAttachedIds(),\n    });\n\n    this.conversationHandlers = new ConversationHandlers({\n      includeSubagentSessions: () => this.featureFlags.subagentSessions,\n      scannerManager: this.scannerManager,\n      sessionStore: this.sessionStore,\n      ptyManager: this.ptyManager,\n      wsHub: this.wsHub,\n      scanProfiles: this.scanProfiles,\n      // Thunks for the same reason the managers above take them: the cache is\n      // opened during listen() and rebound by the integrity monitor's\n      // reset-and-rescan, and tests swap `log` on the server instance.\n      cache: () => this.cache,\n      log: () => this.log,\n      rejectIfWarmingUp: (res) => this.rejectIfWarmingUp(res),\n      withWarmup: (state, operation) => this.withWarmup(state, operation),\n      trackCacheWrite: (task) => this.trackCacheWrite(task),\n      resolveConversationLookupId: (uuid) => this.resolveConversationLookupId(uuid),\n      findLiveSessionFilePath: (uuid) => this.findLiveSessionFilePath(uuid),\n      isBoundConversationLive: (boundId) => this.isBoundConversationLive(boundId),\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    this.sessionHandlers = new SessionHandlers({\n      includeSubagentSessions: () => this.featureFlags.subagentSessions,\n      // Collaborators the constructor already built. The Maps and Sets are\n      // passed by reference on purpose: they stay StreamerServer state, and a\n      // mutation from a handler is the same mutation the WS/PTY callbacks here\n      // — and the tests that reach in via `(server as any)` — observe.\n      sessionStore: this.sessionStore,\n      ptyManager: this.ptyManager,\n      wsHub: this.wsHub,\n      scannerManager: this.scannerManager,\n      sessionWatchers: this.sessionWatchers,\n      registryBoot: this.registryBoot,\n      externalTailManager: this.externalTailManager,\n      idempotency: this.idempotency,\n      sessionStatusBus: this.sessionStatusBus,\n      sessionFileMap: this.sessionFileMap,\n      pendingQuestions: this.pendingQuestions,\n      promptRegistry: this.promptRegistry,\n      pendingQuestionKey: this.pendingQuestionKey,\n      pendingPermission: this.pendingPermission,\n      pendingPermissionKey: this.pendingPermissionKey,\n      contendedSessions: this.contendedSessions,\n      selfPtyEndedAt: this.selfPtyEndedAt,\n      sessionSubscribers: this.sessionSubscribers,\n      agentConfig: this.agentConfig,\n      agentClient: this.agentClient,\n      defaultSystemPrompt: this.defaultSystemPrompt,\n      codexSystemPromptEnabled: this.codexSystemPromptEnabled,\n      cacheDir: this.cacheDir,\n      // Thunks for the same reason ConversationHandlers takes them: bound after\n      // construction (cache, dbPool, browseRoot), mutated at runtime by\n      // PUT /api/config/claude-flags, or swapped on the instance by tests.\n      cache: () => this.cache,\n      log: () => this.log,\n      browseRoot: () => this.browseRoot,\n      claudeFlags: () => this.claudeFlags,\n      claudeExtraArgs: () => this.claudeExtraArgs,\n      dbPool: () => this.dbPool,\n      dbInstanceId: () => this.dbInstanceId,\n      discoveryCache: () => this.discoveryCache,\n      setDiscoveryCache: (value) => {\n        this.discoveryCache = value;\n      },\n      discoveryInFlight: () => this.discoveryInFlight,\n      setDiscoveryInFlight: (value) => {\n        this.discoveryInFlight = value;\n      },\n      rejectIfWarmingUp: (res) => this.rejectIfWarmingUp(res),\n      ptyAttachedIds: () => this.ptyAttachedIds(),\n      withReconciledLifecycle: (sessions) => this.withReconciledLifecycle(sessions),\n      broadcastOrUnicastSessionList: (req) => this.broadcastOrUnicastSessionList(req),\n      checkSessionStartRateLimit: (ip) => this.checkSessionStartRateLimit(ip),\n      checkSessionInputRateLimit: (sessionId) => this.checkSessionInputRateLimit(sessionId),\n      spawnFlagOverrides: () => this.spawnFlagOverrides(),\n      resolveConversationTarget: (sessionId) => this.resolveConversationTarget(sessionId),\n      waitForStartupOutcome: (sessionId, timeoutMs) =>\n        this.waitForStartupOutcome(sessionId, timeoutMs),\n      forgetSession: (sessionId) => this.forgetSession(sessionId),\n      abandonFailedStart: (sessionId) => this.abandonFailedStart(sessionId),\n      armHoldWhenIdle: (sessionId, opts) => this.armHoldWhenIdle(sessionId, opts),\n      enrichResumedSessionAsync: (sessionId, projectPath, conv) =>\n        this.enrichResumedSessionAsync(sessionId, projectPath, conv),\n      findJsonlPath: (uuid) => this.conversationHandlers.findJsonlPath(uuid),\n      readCwdFromJsonl: (filePath) => this.conversationHandlers.readCwdFromJsonl(filePath),\n    });\n\n    const apiDeps = createApiDeps({\n      sessionGeometry: this.sessionGeometry,\n      // Values where the literal captured values: publicUrl/browseRoot are the\n      // construction-time reads they always were (realpath resolves later and\n      // deliberately does not update these).\n      apiKey: () => this.apiKey,\n      localNoAuth: this.localNoAuth,\n      logMenubarRequests: this.logMenubarRequests,\n      publicUrl: this.publicUrl,\n      browseRoot: this.browseRoot,\n      browserCors: this.browserCors,\n      ptyGracePeriodMs: this.ptyGracePeriodMs,\n      rotateApiKey: () => this.rotateApiKey(),\n      claudeFlagsConfig: () => this.getClaudeFlagsConfig(),\n      featureFlagsConfig: () => this.getFeatureFlagsConfig(),\n      setClaudeFlagsConfig: (values, extraArgs) => this.setClaudeFlagsConfig(values, extraArgs),\n      ptyManager: this.ptyManager,\n      sessionStore: this.sessionStore,\n      wsHub: this.wsHub,\n      sessionHandlers: this.sessionHandlers,\n      conversationHandlers: this.conversationHandlers,\n      // Thunks for the same reason the handler classes take them: the stores\n      // open during listen() and are rebound by the integrity monitor's\n      // reset-and-rescan, and tests swap methods on the server instance.\n      cache: () => this.cache,\n      cacheMonitor: () => this.cacheMonitor,\n      hostPressureMonitor: () => this.hostPressureMonitor,\n      pushRepo: () => this.pushRepo,\n      liveActivityPushEnabled: () => this.liveActivityNotifier !== null,\n      expoPushEnabled: () => this.waitingInputNotifier !== null,\n      expoPushSender: () => this.expoPushSender,\n      devicesRepo: () => this.devicesRepo,\n      projectsRepo: () => this.projectsRepo,\n      conversationsRepo: () => this.conversationsRepo,\n      sessionsRepo: () => this.sessionsRepo,\n      cacheMetadataRepo: () => this.cacheMetadataRepo,\n      runtimeStore: () => this.runtimeStore,\n      managedSessionsRepo: () => this.managedSessionsRepo,\n      sessionVerdicts: () => this.sessionVerdicts,\n      log: () => this.log,\n      ptyAttachedIds: () => this.ptyAttachedIds(),\n      withReconciledLifecycle: (sessions) => this.withReconciledLifecycle(sessions),\n      currentWarmupState: () => this.currentWarmupState(),\n      addSessionSubscriber: (sessionId, ws) => this.addSessionSubscriber(sessionId, ws),\n      removeSessionSubscriber: (sessionId, ws) => this.removeSessionSubscriber(sessionId, ws),\n      startGraceTimer: (sessionId, delayMs) => this.startGraceTimer(sessionId, delayMs),\n      armHoldWhenIdle: (sessionId) => this.armHoldWhenIdle(sessionId),\n      handleSessionsCount: (res) => this.handleSessionsCount(res),\n      applyLiveSessionSetting: (id, req, res, setting) =>\n        this.applyLiveSessionSetting(id, req, res, setting),\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      clientIdToWs: this.clientIdToWs,\n      wsToClientId: this.wsToClientId,\n      sessionSubscribers: this.sessionSubscribers,\n      terminalSeq: this.terminalSeq,\n      pendingPermission: this.pendingPermission,\n      pendingQuestions: this.pendingQuestions,\n      promptRegistry: this.promptRegistry,\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    mountWebSocket(this.honoApp, this.httpServer, apiDeps);\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  // Record that our own PTY for `conversationId` just ended. Entries older than\n  // the busy window can never change a verdict, so drop them as we go rather\n  // than accumulating one per conversation for the process's lifetime.\n  private rememberSelfPtyEnded(conversationId: string): void {\n    const now = Date.now();\n    const cutoff = now - resolveResumeBusyWindowMs();\n    for (const [id, at] of this.selfPtyEndedAt) {\n      if (at < cutoff) this.selfPtyEndedAt.delete(id);\n    }\n    this.selfPtyEndedAt.set(conversationId, now);\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 = this.sessionListPayload();\n    if (ws) {\n      this.wsHub.unicast(ws, payload);\n    } else {\n      this.wsHub.broadcast(payload);\n    }\n  }\n\n  private sessionListPayload(): { type: \"session_list\"; sessions: readonly SessionResponse[] } {\n    return {\n      type: \"session_list\" as const,\n      sessions: this.withReconciledLifecycle(this.sessionStore.list(this.ptyAttachedIds())),\n    };\n  }\n\n  /**\n   * Overlay boot-reconciliation verdicts onto session responses.\n   *\n   * A session left by a previous run is not in the in-memory store, so\n   * SessionStore cannot classify it — it only ever sees what this run spawned.\n   * Discovery may still surface the process, in which case the reconciler knows\n   * strictly more about it than discovery does: it can tell `detached` (alive\n   * and confirmed ours) from `orphaned` (alive but identity unconfirmed), which\n   * a pid enumeration alone cannot.\n   *\n   * Only applied when the session is NOT live here: a session this run owns has\n   * an authoritative lifecycle already, and a stale verdict must never override\n   * it.\n   */\n  private withReconciledLifecycle(\n    sessions: readonly SessionResponse[],\n  ): readonly SessionResponse[] {\n    if (this.sessionVerdicts.size === 0) return sessions;\n    return sessions.map((s) => {\n      if (s.ptyAttached) return s;\n      const verdict = this.sessionVerdicts.get(s.id);\n      if (!verdict) return s;\n      return { ...s, lifecycle: verdict.lifecycle, lifecycleSource: \"reconcile\" as const };\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. Reset the\n    // defer count too so the next disconnect starts a fresh defer budget.\n    const existing = this.ptyGraceTimers.get(sessionId);\n    if (existing) {\n      clearTimeout(existing);\n      this.ptyGraceTimers.delete(sessionId);\n    }\n    this.ptyGraceDeferCounts.delete(sessionId);\n    // A latch armed with ignoreWatchers stays armed through a later connect —\n    // \"ignore them\" means ignore watchers who show up after arming too, not\n    // just the ones present at arm time.\n    const armed = this.holdWhenIdle.get(sessionId);\n    if (armed && !armed.ignoreWatchers) {\n      this.holdWhenIdle.delete(sessionId);\n      this.log.info(\n        `[hold-when-idle] cancelled ${sessionId} (subscribe)`,\n        { sessionId, event: \"pty.hold_when_idle_cancel\", reason: \"subscribe\" },\n        \"pino\",\n      );\n    }\n  }\n\n  private removeSessionSubscriber(sessionId: string, ws: WebSocket): void {\n    const subs = this.sessionSubscribers.get(sessionId);\n    if (!subs) return;\n    subs.delete(ws);\n    if (subs.size === 0) this.sessionSubscribers.delete(sessionId);\n  }\n\n  /**\n   * Bring up Live Activity push, if credentials are present (Feature 12).\n   *\n   * APNS_KEY absent is the ordinary case on a dev machine and in CI, so this\n   * logs once at info and leaves the feature off rather than failing: the server\n   * must not refuse to boot over a missing optional push credential.\n   *\n   * The key is read from the environment as PEM contents and never from a path\n   * on disk; neither it nor any device token is ever logged.\n   */\n  private initLiveActivityPush(pushRepo: PushRepository): void {\n    // Logged rather than returned silently: a box with APNS_KEY configured used\n    // to print \"Live Activity push enabled\" here, so an operator who flips the\n    // flag off needs the credential to look ignored on purpose, not missing.\n    if (!this.featureFlags.liveActivityPush) {\n      this.log.info(\n        \"Live Activity push is disabled by the liveActivityPush feature flag. \" +\n          \"Enable it with THREADBASE_FEATURE_LIVE_ACTIVITY_PUSH=1, --feature liveActivityPush=true, \" +\n          \"or feature_flags: in server.yaml.\",\n        { event: \"live_activity.disabled\" },\n      );\n      return;\n    }\n\n    const creds = readApnsCredentialsFromEnv();\n    if (!creds) {\n      const why = describeMissingApnsCredentials();\n      if (why) this.log.info(why, { event: \"live_activity.disabled\" });\n      return;\n    }\n\n    this.apnsClient = new ApnsClient(creds);\n    const sender = new LiveActivitySender(this.apnsClient, pushRepo);\n    // Identifies this streamer to mobile, which shows several servers at once.\n    // Matches the id used for DB-persisted session scoping.\n    const serverId = process.env.THREADBASE_INSTANCE_ID ?? hostname();\n    this.liveActivityNotifier = new LiveActivityNotifier(sender, serverId, hostname());\n    // Re-arms pending renewals from the DB. Started here rather than lazily\n    // because the deadlines were persisted precisely so a restart inside an\n    // 8-hour window does not drop them.\n    this.liveActivityRenewal = new LiveActivityRenewalScheduler({\n      repo: pushRepo,\n      sender,\n      sessionStore: this.sessionStore,\n      serverId,\n      serverLabel: hostname(),\n    });\n    this.liveActivityRenewal.start();\n    // Host is logged (it selects sandbox vs production, a routine source of\n    // \"why is nothing arriving\") but no credential material is.\n    this.log.info(\"Live Activity push enabled\", {\n      event: \"live_activity.enabled\",\n      host: creds.host,\n      topic: `${creds.bundleId}.push-type.liveactivity`,\n    });\n  }\n\n  /**\n   * Bring up \"your turn\" notifications over Expo's relay (#528).\n   *\n   * Unconditional, unlike Live Activity push: Expo holds the app's APNs and FCM\n   * credentials, so a self-hosted streamer needs no credential of its own. The\n   * access token is optional and only relevant if the Expo project has enhanced\n   * security enabled — requiring one would lock out every self-hoster, since\n   * they do not own the project. It is never logged.\n   */\n  private initWaitingInputPush(pushRepo: PushRepository): void {\n    const sender = new ExpoPushSender(pushRepo, process.env.THREADBASE_EXPO_ACCESS_TOKEN);\n    this.expoPushSender = sender;\n    this.waitingInputNotifier = new WaitingInputNotifier(sender);\n  }\n\n  /** Whether any live socket is subscribed to this session — \"someone is looking\". */\n  private hasSessionSubscriber(sessionId: string): boolean {\n    const subs = this.sessionSubscribers.get(sessionId);\n    if (!subs) return false;\n    for (const ws of subs) {\n      if (ws.readyState === ws.OPEN) return true;\n    }\n    return false;\n  }\n\n  /**\n   * Log any Codex rollout assumption that no longer holds.\n   *\n   * Deliberately log-only. A format change is not something the streamer can\n   * repair or route around, and a WS alert or a blocked request would turn an\n   * informational signal into an outage. What it buys is that the next silent\n   * empty conversation is preceded by a line naming the assumption that moved.\n   */\n  private runCodexFormatCanary(): void {\n    try {\n      const report = sweepCodexFormat(this.codexRoots, {\n        readdirSync,\n        statSync,\n        existsSync,\n        readFileSync,\n        join,\n      });\n      for (const finding of report.findings) {\n        this.log.warn(`[codex-format] ${finding.code}: ${finding.detail}`, {\n          event: \"codex.format_drift\",\n          code: finding.code,\n          detail: finding.detail,\n          filePath: finding.filePath,\n        });\n      }\n    } catch (err) {\n      // A canary must never be the thing that breaks the daemon it watches.\n      this.log.warn(\"codex format canary failed\", { event: \"codex.format_canary_failed\", err });\n    }\n  }\n\n  /**\n   * Release PTYs whose agent has been silent past IDLE_REAP_AFTER_MS.\n   *\n   * This is the bound that lets handleWsClose stop arming kill timers. The\n   * distinction that matters: the old timer measured how long nobody was\n   * *watching*, which is uncorrelated with whether work is in flight. This\n   * measures how long the *agent* has produced nothing, and only ever considers\n   * sessions that are already settled — a `running` PTY is skipped regardless of\n   * age, so a long silent turn is never interrupted.\n   *\n   * Exposed (not private) so tests can drive one sweep deterministically instead\n   * of waiting on the interval.\n   */\n  reapIdleSessions(now: number = Date.now()): string[] {\n    if (this.ptyManager.isRemote()) return [];\n    const reaped: string[] = [];\n    for (const session of this.ptyManager.listSessions()) {\n      // Never touch a session mid-turn. This is the whole point.\n      if (session.status === \"running\") continue;\n\n      // Fall back to startedAt so a session that never produced a chunk is\n      // still eligible eventually — otherwise a PTY that failed to emit\n      // anything would be immortal.\n      const lastActive =\n        this.lastAgentChunkAt.get(session.id) ??\n        session.lastActivityAt?.getTime() ??\n        session.startedAt.getTime();\n\n      if (now - lastActive < IDLE_REAP_AFTER_MS) continue;\n\n      this.log.info(\n        `[reap] releasing idle PTY for ${session.id} (idle ${Math.round((now - lastActive) / 60_000)}m)`,\n        { sessionId: session.id, event: \"pty.idle_reap\", idleMs: now - lastActive },\n        \"pino\",\n      );\n      this.ptyManager.putOnHold(session.id);\n      this.lastAgentChunkAt.delete(session.id);\n      this.terminalSeq.delete(session.id);\n      this.idempotency.clear(session.id);\n      this.sessionSubscribers.delete(session.id);\n      reaped.push(session.id);\n\n      const held = this.sessionStore.get(session.id, this.ptyAttachedIds());\n      if (held) this.wsHub.broadcast({ type: \"session_update\", session: held });\n    }\n    return reaped;\n  }\n\n  private startGraceTimer(sessionId: string, delayMs: number): void {\n    // Last writer wins: a bare hold_session clears a waiting_input latch.\n    this.holdWhenIdle.delete(sessionId);\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        //\n        // But bound the deferral: a PTY that never settles (its last line was a\n        // status bar with no prompt marker) would re-arm forever and leak.\n        // After GRACE_MAX_DEFERS consecutive defers, hold it anyway.\n        const resp = this.sessionStore.get(sessionId, this.ptyAttachedIds());\n        if (resp?.status === \"running\") {\n          const defers = (this.ptyGraceDeferCounts.get(sessionId) ?? 0) + 1;\n          if (defers <= GRACE_MAX_DEFERS) {\n            this.ptyGraceDeferCounts.set(sessionId, defers);\n            this.log.info(\n              `[grace] session ${sessionId} still running, deferring hold (${defers}/${GRACE_MAX_DEFERS})`,\n              { sessionId, event: \"pty.grace_defer\", defers, maxDefers: GRACE_MAX_DEFERS },\n              \"pino\",\n            );\n            this.startGraceTimer(sessionId, delayMs);\n            return;\n          }\n          this.log.warn(\n            `[grace] session ${sessionId} exceeded ${GRACE_MAX_DEFERS} defers, holding anyway`,\n            { sessionId, event: \"pty.grace_defer_cap\", defers, maxDefers: GRACE_MAX_DEFERS },\n            \"pino\",\n          );\n        }\n        this.ptyGraceDeferCounts.delete(sessionId);\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.ptyGraceDeferCounts.delete(sessionId);\n        this.sessionSubscribers.delete(sessionId);\n      }\n    }, delayMs);\n\n    this.ptyGraceTimers.set(sessionId, timer);\n  }\n\n  private clearGrace(sessionId: string): void {\n    const existing = this.ptyGraceTimers.get(sessionId);\n    if (existing) {\n      clearTimeout(existing);\n      this.ptyGraceTimers.delete(sessionId);\n    }\n    this.ptyGraceDeferCounts.delete(sessionId);\n  }\n\n  /**\n   * Arm the in-app \"Kill on idle\" latch: hold now if already settled, otherwise\n   * on the next running → waiting_input (or idle). No grace delay, no defer cap.\n   * A subscribed leaving socket must not block an immediate hold.\n   */\n  private armHoldWhenIdle(\n    sessionId: string,\n    opts: { ignoreWatchers?: boolean; deleteAfter?: boolean } = {},\n  ): \"held\" | \"armed\" | \"no_session\" {\n    if (!this.ptyManager.hasSession(sessionId)) return \"no_session\";\n    this.clearGrace(sessionId);\n    const ignoreWatchers = opts.ignoreWatchers ?? false;\n    const deleteAfter = opts.deleteAfter ?? false;\n    const status =\n      this.ptyManager.getSession(sessionId)?.status ??\n      this.sessionStore.getManaged(sessionId)?.status;\n    if (status === \"waiting_input\" || status === \"idle\") {\n      this.holdWhenIdle.delete(sessionId);\n      this.ptyManager.putOnHold(sessionId);\n      this.forgetIfEmptyUnused(sessionId);\n      if (deleteAfter) this.softDeleteConversation(sessionId);\n      return \"held\";\n    }\n    this.holdWhenIdle.set(sessionId, { ignoreWatchers, deleteAfter });\n    this.log.info(\n      `[hold-when-idle] armed ${sessionId}`,\n      { sessionId, event: \"pty.hold_when_idle_armed\", ignoreWatchers, deleteAfter },\n      \"pino\",\n    );\n    return \"armed\";\n  }\n\n  /**\n   * Fire the Kill-on-idle latch from the shared onStatusChange funnel.\n   * Delete first so the ensuing idle transition cannot re-enter.\n   */\n  private maybeFireHoldWhenIdle(session: { id: string; status: string }): void {\n    if (session.status !== \"waiting_input\" && session.status !== \"idle\") return;\n    const armed = this.holdWhenIdle.get(session.id);\n    if (!armed) return;\n    this.holdWhenIdle.delete(session.id);\n    if (!armed.ignoreWatchers && this.hasSessionSubscriber(session.id)) {\n      this.log.info(\n        `[hold-when-idle] cancelled ${session.id} (subscriber)`,\n        { sessionId: session.id, event: \"pty.hold_when_idle_cancel\", reason: \"subscriber\" },\n        \"pino\",\n      );\n      return;\n    }\n    this.log.info(\n      `[hold-when-idle] holding ${session.id}`,\n      { sessionId: session.id, event: \"pty.hold_when_idle_fire\" },\n      \"pino\",\n    );\n    this.ptyManager.putOnHold(session.id);\n    this.forgetIfEmptyUnused(session.id);\n    if (armed.deleteAfter) this.softDeleteConversation(session.id);\n  }\n\n  private forgetIfEmptyUnused(sessionId: string): void {\n    this.sessionHandlers.forgetIfEmptyUnused(sessionId);\n  }\n\n  private softDeleteConversation(sessionId: string): void {\n    this.sessionHandlers.softDeleteConversation(sessionId);\n  }\n\n  get port(): number {\n    const addr = this.httpServer.address();\n    return typeof addr === \"object\" && addr ? addr.port : 0;\n  }\n\n  private currentWarmupState(): ServerWarmupState | null {\n    let current: ServerWarmupState | null = null;\n    for (const state of this.activeWarmups.values()) current = state;\n    return current;\n  }\n\n  private beginWarmup(state: ServerWarmupState): number {\n    const id = this.nextWarmupId++;\n    this.activeWarmups.set(id, state);\n    return id;\n  }\n\n  private finishWarmup(id: number): void {\n    if (!this.activeWarmups.delete(id) || this.activeWarmups.size > 0) return;\n    this.wsHub.broadcast({ type: \"cache_ready\" });\n  }\n\n  private async withWarmup<T>(state: ServerWarmupState, operation: () => Promise<T>): Promise<T> {\n    const id = this.beginWarmup(state);\n    try {\n      return await operation();\n    } finally {\n      this.finishWarmup(id);\n    }\n  }\n\n  private rejectIfWarmingUp(res: ServerResponse): boolean {\n    const warmupState = this.currentWarmupState();\n    if (!warmupState) return false;\n    const body: ServerWarmingUpResponse = {\n      error: \"Server is warming up\",\n      code: \"SERVER_WARMING_UP\",\n      warmupState,\n    };\n    json(res, 503, body);\n    return true;\n  }\n\n  /**\n   * Say which provider CLIs this machine can actually launch.\n   *\n   * The operator cannot discover this case unaided: under launchd/Task\n   * Scheduler the service inherits a stripped PATH, so a CLI that works\n   * perfectly in their terminal is invisible to the service, and every session\n   * start dies milliseconds in. `/api/diagnostics` answers it too, but only for\n   * someone who already suspects it.\n   *\n   * Availability only, never a version — `--version` costs a process spawn per\n   * provider (85ms for claude here) and belongs on the first request that wants\n   * it, not on boot.\n   *\n   * Called AFTER the port is bound, which is not cosmetic. This is the first\n   * caller of the exe resolvers in the process, so the memo is cold by\n   * definition and each provider pays one synchronous `which` / `where.exe`\n   * (platform.ts) with a 3s timeout. On POSIX that is 3ms found, 7ms missing.\n   * Windows is the risk — `where.exe` is slower, `execFileSync` blocks the\n   * event loop, and Task Scheduler's stripped PATH is exactly where a miss\n   * pays the full timeout — so the worst case is ~6s of two blocking lookups.\n   * After `listen()` that delays the first requests on a box that cannot start\n   * a session anyway; before it, it would have delayed binding the port.\n   */\n  private logProviderAvailability(): void {\n    for (const provider of PROVIDER_NAMES) {\n      if (locateProviderExe(provider)) {\n        this.log.info(`Provider ${provider}: found`, { event: \"config.provider\", provider });\n      } else {\n        this.log.warn(`Provider ${provider}: not found on PATH — sessions cannot start`, {\n          event: \"config.provider_missing\",\n          provider,\n        });\n      }\n    }\n  }\n\n  async listen(port: number, opts?: { awaitReady?: boolean }): Promise<void> {\n    if (this.featureFlags.ptyHost) {\n      // Degrade to in-process runners rather than refusing to boot.\n      //\n      // `connectOrSpawnHost` rejects after ~5s if the host never accepts a\n      // connection — a broken node-pty in the child, an unwritable socket\n      // directory, a half-dead host still holding the path. Unhandled, that\n      // makes an experimental, default-off flag the one subsystem that can stop\n      // the streamer from starting at all, when every other optional subsystem\n      // here (runtime store, cache, reconciliation) logs and continues.\n      //\n      // Safe to fall through: `useRemoteRunner` only disposes the in-process\n      // runners *after* a successful connect, so on this path they are still\n      // the live ones and nothing has been adopted.\n      try {\n        let sessions: ManagedSession[] | null = null;\n        for (let attempt = 0; attempt < 2; attempt += 1) {\n          const transport = await connectOrSpawnHost({\n            instanceId: process.env.THREADBASE_INSTANCE_ID ?? hostname(),\n          });\n          try {\n            sessions = await this.ptyManager.useRemoteRunner(transport);\n            break;\n          } catch (err) {\n            if (!(err instanceof PtyHostProtocolMismatchError) || attempt > 0) throw err;\n            this.log.info(`[pty-host] replaced incompatible protocol ${err.hostVersion}`, {\n              event: \"pty_host.protocol_replaced\",\n              hostVersion: err.hostVersion,\n              streamerVersion: err.streamerVersion,\n            });\n          }\n        }\n        if (!sessions) throw new Error(\"pty-host replacement did not produce a compatible host\");\n        for (const session of sessions) {\n          this.sessionStore.addManaged({ ...session, reconciled: true });\n        }\n        this.log.info(`[pty-host] re-adopted ${sessions.length} live session(s)`, {\n          event: \"pty_host.sessions_adopted\",\n          sessions: sessions.length,\n        });\n      } catch (err) {\n        // Error, not warn: the operator asked for the host and is not getting\n        // it, so sessions will not survive the next restart. Boot continues.\n        this.log.error(\n          \"[pty-host] could not attach; falling back to in-process PTYs for this run\",\n          { event: \"pty_host.attach_failed\", err },\n        );\n      }\n    }\n\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, this.host);\n\n    // unref() so an idle server with no other work can still exit — this timer\n    // must never be the reason the process stays alive.\n    if (!this.ptyManager.isRemote()) {\n      this.idleReaperTimer = setInterval(() => this.reapIdleSessions(), IDLE_REAP_SWEEP_MS);\n      this.idleReaperTimer.unref?.();\n    }\n\n    // Codex owns the rollout format and does not version it. Sample the newest\n    // files periodically so a change is a log line rather than a user reporting\n    // an empty conversation weeks later. Reads at most a few files' opening\n    // lines; never blocks a request and never changes behaviour.\n    if (this.codexRoots.length > 0) {\n      this.runCodexFormatCanary();\n      this.codexFormatCanaryTimer = setInterval(\n        () => this.runCodexFormatCanary(),\n        CODEX_FORMAT_CANARY_INTERVAL_MS,\n      );\n      this.codexFormatCanaryTimer.unref?.();\n    }\n\n    // Informational only: samples cheap OS + event-loop signals and broadcasts\n    // host_pressure on a level change. Never holds, kills, or refuses sessions.\n    this.hostPressureMonitor = createHostPressureMonitor(\n      this.wsHub,\n      () => this.ptyAttachedIds().size,\n    );\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          ...(this.host !== undefined && { host: this.host }),\n        });\n        this.logProviderAvailability();\n        // Opened BEFORE and INDEPENDENTLY of the conversation cache. These two\n        // used to share a handle, so the documented better-sqlite3 ABI mismatch\n        // — which the cache catch below tolerates by design — silently took the\n        // session registry with it: recordSpawn/recordStatus/recordShutdownState\n        // all became no-ops with no separate signal. Each store now fails, and\n        // logs, on its own.\n        try {\n          this.runtimeStore = RuntimeStore.open(this.runtimeDbPath);\n          this.managedSessionsRepo = new ManagedSessionsRepository(this.runtimeStore.getDatabase());\n          // Devices live here, not in the cache. Two consequences beyond\n          // surviving `cache clear`: the registry no longer depends on the\n          // conversation cache opening at all — a cache failure used to null\n          // devicesRepo and silently drop every device to the shared-key path —\n          // and it is now durable enough for a client to present the device\n          // token as its only credential.\n          this.devicesRepo = new DevicesRepository(this.runtimeStore.getDatabase());\n        } catch (err) {\n          const message = err instanceof Error ? err.message : String(err);\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            `Runtime store failed to open — session persistence DISABLED; ` +\n              `sessions will not survive a restart.` +\n              (abiMismatch ? ` Fix: npm rebuild better-sqlite3` : \"\") +\n              ` (${message})`,\n            { error: message, abiMismatch, path: this.runtimeDbPath, event: \"runtime.open_failed\" },\n          );\n        }\n        this.warnIfE2eeDisabled();\n        void this.probeAccessGate();\n        if (this.ptyManager.isRemote()) {\n          this.ptyManager.startRemoteHeartbeat(() => {\n            if (!this.managedSessionsRepo) {\n              return { registryState: \"unknown\", referencedSessionIds: [] };\n            }\n            const referencedSessionIds = this.ptyManager\n              .listSessions()\n              .filter((session) => this.managedSessionsRepo?.get(session.id)?.completed_at == null)\n              .map((session) => session.id);\n            return { registryState: \"known\", referencedSessionIds };\n          });\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              includeSubagentSessions: this.featureFlags.subagentSessions,\n              agentEntrypoints: this.agentEntrypoints,\n              onAgentFileDetected: (fp) => {\n                this.fileWatcher.unwatch(fp);\n                // Release the external-tail slot too, otherwise an agent JSONL\n                // holds a capped slot forever with a watcher that's already closed.\n                this.externalTails.delete(canonicalizeFilePath(fp));\n              },\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          // One-time lift of pre-split tables out of cache.db. Never fatal: a\n          // failed copy costs one boot of post-restart visibility, not the\n          // cache. The two are no longer symmetric — managed_sessions is copied\n          // and left behind, devices are MOVED (see importLegacyDevices) — so\n          // they are handled separately rather than through one loop.\n          try {\n            const copied = this.runtimeStore?.importLegacyManagedSessions(db) ?? 0;\n            if (copied > 0) {\n              this.log.info(`Copied ${copied} managed session row(s) from cache.db to runtime.db`, {\n                copied,\n                table: \"managed_sessions\",\n                event: \"runtime.legacy_import\",\n              });\n            }\n          } catch (err) {\n            this.log.warn(\"[registry] legacy managed_sessions copy failed\", {\n              event: \"runtime.legacy_import_failed\",\n              table: \"managed_sessions\",\n              err,\n            });\n          }\n          try {\n            const result = this.runtimeStore?.importLegacyDevices(db);\n            if (result && result.copied > 0) {\n              // Says whether the source rows were removed, because this is the\n              // one import that deletes user data — a device label is\n              // user-supplied — and an erasure should leave a trace.\n              this.log.info(\n                `Moved ${result.copied} device row(s) from cache.db to runtime.db` +\n                  (result.purged\n                    ? \"; removed the cache-side copy\"\n                    : \"; KEPT the cache-side copy (row count did not match after copy)\"),\n                {\n                  copied: result.copied,\n                  purged: result.purged,\n                  table: \"devices\",\n                  event: \"runtime.legacy_import\",\n                },\n              );\n            }\n          } catch (err) {\n            this.log.warn(\"[registry] legacy devices move failed\", {\n              event: \"runtime.legacy_import_failed\",\n              table: \"devices\",\n              err,\n            });\n          }\n          this.cacheMetadataRepo = new CacheMetadataRepository(db);\n          this.pushRepo = new PushRepository(db);\n\n          this.initLiveActivityPush(this.pushRepo);\n          this.initWaitingInputPush(this.pushRepo);\n          // Cache-integrity drift monitor. reset_rescan rebuilds from a fresh\n          // scan via the same machinery ?refresh=1 uses (rescanForRefresh).\n          this.cacheMonitor = new CacheIntegrityMonitor(\n            this.cache,\n            this.wsHub,\n            this.log,\n            this.cacheDir,\n            async () => {\n              const scanner = await this.scannerManager.rescanForRefresh();\n              return [...scanner.getMetadataCache().values()] as never;\n            },\n            (operation) => {\n              const reset = this.withWarmup(\"cache_reset\", operation);\n              this.trackCacheWrite(reset);\n              return reset;\n            },\n          );\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.scannerManager.projectsDirs()) {\n            this.fileWatcher.watchDirectory(dir);\n          }\n          // Codex rollouts too (P4.a). Previously only the Claude projects dirs\n          // were watched, so an externally-launched Codex session produced NO\n          // event at all — it never even flipped the scanner-stale flag, making\n          // it strictly pull-only. The roots are date-partitioned\n          // (<root>/YYYY/MM/DD), so watch the root and let chokidar recurse.\n          for (const dir of this.codexRoots) {\n            if (!existsSync(dir)) continue;\n            this.fileWatcher.watchDirectory(dir);\n          }\n          // Cursor agent-transcripts (opt-in cursorRoots, default\n          // ~/.cursor/projects/<slug>/agent-transcripts). Watch only those\n          // folders — chokidar on the projects root also sees canvases and\n          // node_modules and will EMFILE a machine with many Cursor worktrees.\n          for (const transcripts of listCursorTranscriptWatchDirs(this.cursorRoots)) {\n            this.fileWatcher.watchDirectory(transcripts);\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.scannerManager.disablePersistence();\n        }\n        if (this.ptyManager.isRemote()) this.registryBoot.refreshHostedSessionsFromRegistry();\n        // Classify whatever previous runs left behind. Fire-and-forget: it only\n        // populates a diagnostic map, and blocking startup on `ps` probes would\n        // delay the listener for no correctness gain. Runs after the cache block\n        // so it sees any rows the legacy copy just brought across, and outside\n        // it so a cache failure no longer skips reconciliation.\n        void this.registryBoot.reconcilePreviousSessions().then(async (v) => {\n          const recoverableRows = this.registryBoot.rehydratePreviousSessions(v);\n          await this.registryBoot.autoResumePreviousSessions(recoverableRows);\n          // Last, so retention can never delete a row this boot still wanted:\n          // reconciliation has finished probing, rehydration has finished\n          // seeding, and auto-resume has made its attempts before removal.\n          this.registryBoot.pruneTerminalSessions();\n        });\n        // Opt out of the warm-up scan entirely (test hook). The cache and\n        // repositories above are already open, so conversation endpoints serve an\n        // empty cache instead of throwing; only the scan and its dependent cache\n        // writes are skipped. Must still finish the warm-up bookkeeping, or\n        // close() would await a promise that never settles.\n        if (this.skipStartupWarmup) {\n          this.log.debug?.(\"startup warm-up scan skipped (skipStartupWarmup)\", {\n            event: \"cache.warmup_skipped\",\n          });\n          this.finishWarmup(0);\n          resolveWarm();\n          return;\n        }\n        // Use a dedicated scanner for warm-up, independent of this.scannerManager.current, 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.scannerManager.buildStatCache(null);\n        // Scanner 0.9.4 reads statCache only in non-persistent scans, and a\n        // cold start has none — see the note in ScannerManager.get (#876).\n        const warmupScanner = this.scannerManager.newScanner({ persistent: false });\n        this.scannerManager.track(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.scannerManager.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            this.scannerManager.adoptIfUnclaimed(warmupScanner);\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            // Detect cache/disk drift before the routine ghost prune. If a\n            // pending alert is raised, freeze — skip pruneGhostFiles until a\n            // human resolves it; otherwise prune exactly as before. The\n            // on-disk reconcile below is part of the same write path, so it\n            // stays inside the freeze too.\n            await this.cacheMonitor?.runDetection();\n            if (this.cacheMonitor?.pending) {\n              this.log.warn(\"Startup ghost prune skipped — cache integrity alert pending\", {\n                fingerprint: this.cacheMonitor.pending.fingerprint,\n                event: \"cache.prune_ghosts_frozen\",\n              });\n            } else {\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              if (this.projectsRepo && this.conversationsRepo && this.cacheMetadataRepo) {\n                refreshConversationCache({\n                  cache: this.cache,\n                  projectsRepo: this.projectsRepo,\n                  conversationsRepo: this.conversationsRepo,\n                  cacheMetadataRepo: this.cacheMetadataRepo,\n                });\n              } else if (this.cacheMetadataRepo) {\n                setCacheMetadata(\n                  this.cacheMetadataRepo,\n                  \"conversations_last_indexed_at\",\n                  new Date().toISOString(),\n                );\n              }\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.finishWarmup(0);\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(\n    port: number,\n    host?: string,\n    attempts = 6,\n    delayMs = 500,\n  ): Promise<void> {\n    this.binding = true;\n    try {\n      await this.bindWithRetryLoop(port, host, attempts, delayMs);\n    } finally {\n      this.binding = false;\n    }\n  }\n\n  private async bindWithRetryLoop(\n    port: number,\n    host: string | undefined,\n    attempts: number,\n    delayMs: number,\n  ): 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          if (host === undefined) {\n            this.httpServer.listen(port);\n          } else {\n            this.httpServer.listen(port, host);\n          }\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            {\n              port,\n              attempts,\n              event: \"server.bind_failed\",\n              ...(host !== undefined && { host }),\n            },\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\", ...(host !== undefined && { host }) },\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  async close(): Promise<void> {\n    for (const timer of this.ptyGraceTimers.values()) clearTimeout(timer);\n    this.ptyGraceTimers.clear();\n    this.holdWhenIdle.clear();\n    if (this.idleReaperTimer) {\n      clearInterval(this.idleReaperTimer);\n      this.idleReaperTimer = null;\n    }\n    if (this.codexFormatCanaryTimer) {\n      clearInterval(this.codexFormatCanaryTimer);\n      this.codexFormatCanaryTimer = null;\n    }\n    this.hostPressureMonitor?.dispose();\n    this.hostPressureMonitor = null;\n    this.lastAgentChunkAt.clear();\n    this.terminalSeq.clear();\n    // An in-process runner is about to kill its children, so record that before\n    // dispose() and before runtimeStore.close() takes the registry handle away.\n    // A remote runner does the opposite: disconnect first so no late host event\n    // can write through a closed handle, and leave its live registry rows alone.\n    if (this.ptyManager.isRemote()) this.ptyManager.dispose();\n    else this.registryBoot.recordShutdownState();\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 this.scannerManager.close();\n    this.cache?.close();\n    this.runtimeStore?.close();\n    if (!this.ptyManager.isRemote()) this.ptyManager.dispose();\n    this.fileWatcher.dispose();\n    this.externalTails.clear();\n    this.promptRegistry.dispose();\n    this.wsHub.dispose();\n    this.pairTokens.dispose();\n    // The APNs HTTP/2 session is long-lived by design, so it keeps the event\n    // loop alive until closed explicitly.\n    this.liveActivityRenewal?.stop();\n    this.apnsClient?.close();\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    // GET /api/info already answers whether this build can perform the Noise\n    // handshake (describeE2eeCapability, design.md §6.2/§6.3) — this endpoint\n    // just never consulted it, so a build reporting `enabled: false` would\n    // still run the handshake below if a request carried an `e2ee` field.\n    // Reuse the same function rather than a second constant or expression:\n    // if the two answers can drift, they eventually will.\n    const e2eeEnabled = describeE2eeCapability(this.featureFlags.e2ee).enabled;\n\n    // Optional, and its absence is the ordinary case. A released tb-mobile\n    // build sends no `e2ee` at all and must pair exactly as it does today, so\n    // this is an explicit branch rather than an optional-chaining accident —\n    // a silent skip reads as a bug to the next person, who tightens it into a\n    // rejection and breaks every old app in the field.\n    //\n    // When the capability is off, the field is ignored outright rather than\n    // parsed and validated: this endpoint is public and unauthenticated, and\n    // `parseE2eeRequest`'s header comment is explicit that everything it does\n    // runs on bytes an attacker chose before anything has authenticated them.\n    // A build with nothing to gain from that field should not run it either.\n    let e2eeRequest: E2eeExchangeRequest | null = null;\n    if (e2eeEnabled) {\n      try {\n        e2eeRequest = parseE2eeRequest(body?.e2ee);\n      } catch (err) {\n        const e = err as E2eeRequestError;\n        json(res, 400, { error: e.message, code: e.code });\n        return;\n      }\n    }\n\n    // Reject a bad token before doing any work, but do NOT spend it yet.\n    //\n    // Spending a token that a later step then fails on costs the user their\n    // whole pairing: the token is single-use, so their retry gets `401 Pair\n    // token used` — which is the signal design.md §2.6 designates as QR-replay\n    // detection. Giving that signal a common benign cause is how it stops being\n    // believed, and a malformed `clientPublicKey` was enough to trigger it.\n    const precheck = this.pairTokens.wouldConsume(token);\n    if (!precheck.ok) {\n      if (precheck.reason === \"used\") {\n        // The §2.6 detection signal, made observable. Until now a replayed\n        // token reached the client as a 401 and reached the operator's log as\n        // silence — and the operator is the one who can act on it, since a user\n        // whose pairing failed is not reading HTTP status codes.\n        //\n        // Carries `ip` and nothing else the request log does not already have.\n        // The token is deliberately absent: it is live credential material\n        // until it expires.\n        this.log.warn(\n          \"[pair] a pair token was replayed. If you did not just pair a device, \" +\n            \"check the paired-devices list and revoke anything you do not recognise.\",\n          { event: \"pair.token_replayed\", ip },\n        );\n      }\n      json(res, 401, { error: `Pair token ${precheck.reason}` });\n      return;\n    }\n\n    // The handshake runs BEFORE `seal`, for two reasons that both matter.\n    //\n    // `seal` materialises this machine's shared API key in memory. Doing that\n    // for a caller who has not authenticated is worse than not doing it, even\n    // though the response is never sent — so an E2EE client proves itself\n    // first, and only then is the legacy credential built for it.\n    //\n    // And a failing handshake must not spend the token. A malformed or hostile\n    // `msg1` would otherwise hand anyone who photographed the QR a denial of\n    // service they did not have: burn the token, and the legitimate phone's\n    // pairing dies with it. Same spine as the ordering above — the token is\n    // spent when the exchange succeeds, not when it is attempted.\n    let handshake: HandshakeResponderState | null = null;\n    let registration: E2eePairRegistration | null = null;\n    if (e2eeRequest) {\n      try {\n        handshake = readMessage1({\n          staticKeyPair: keyPairFrom(loadOrCreateServerIdentity().privateKey),\n          // The pair token binds this handshake to the scanned QR. Derivation\n          // is specified in design.md §2.4 and pinned by a committed vector\n          // that tb-mobile checks against independently.\n          psk: pskFromPairToken(token),\n          // Explicit: `prologue` is a required parameter, because defaulting it\n          // through `??` let a polluted prototype make this handshake accept an\n          // `/open` message (§11).\n          prologue: PAIR_PROLOGUE,\n          message1: e2eeRequest.message1,\n        });\n      } catch {\n        // Deliberately one code for every handshake failure. Distinguishing\n        // \"wrong static key\" from \"wrong PSK\" from \"tampered ciphertext\" would\n        // tell an attacker which half of their guess was right, and the client\n        // has the same remedy in all three: scan a fresh code.\n        //\n        // The caught error is dropped rather than surfaced, for the same reason.\n        json(res, 400, {\n          error: \"E2EE handshake failed. Scan a fresh pairing code and try again.\",\n          code: \"E2EE_HANDSHAKE_FAILED\",\n        });\n        return;\n      }\n\n      // The registration inputs, taken from inside the AEAD (design.md §2.4).\n      //\n      // Read here rather than after the token is spent, and the placement is\n      // the point: a payload this build cannot read must cost the client a\n      // retry, not their pair token. Same spine as the handshake above — the\n      // token is spent when the exchange succeeds, not when it is attempted.\n      //\n      // The error is surfaced with its code, unlike the handshake failure. It\n      // leaks nothing: reaching this line already proves the caller completed\n      // the handshake, so it is a real client with a shape disagreement, and\n      // telling it which is what lets it be fixed.\n      try {\n        registration = parseE2eeMsg1Payload(handshake.payload);\n      } catch (err) {\n        const e = err as E2eeRequestError;\n        json(res, 400, { error: e.message, code: e.code });\n        return;\n      }\n    }\n\n    let sealed: ReturnType<typeof seal>;\n    try {\n      sealed = seal(this.apiKey, clientPublicKey);\n    } catch (err) {\n      // The token is deliberately still unspent here. The client can fix its\n      // key and retry with the same QR.\n      const message = err instanceof Error ? err.message : \"Invalid clientPublicKey\";\n      json(res, 400, { error: message });\n      return;\n    }\n\n    // Spend it, now that everything that can fail on client input has passed.\n    //\n    // Consuming late grants an attacker nothing: a token that is not the live\n    // one fails `wouldConsume`'s `unknown` branch above, before any\n    // cryptography runs, and `checkExchangeRateLimit` already bounds attempts\n    // to five per minute per IP. Anyone who reaches this line was holding the\n    // real token when they started.\n    //\n    // EVERYTHING BETWEEN `wouldConsume` AND `consume` MUST STAY SYNCHRONOUS.\n    // `PairTokenStore` takes no lock, so the single-use guarantee here rests\n    // entirely on Node running one callback to completion: a single `await` in\n    // this gap returns control to the event loop and lets two concurrent\n    // requests carrying the same token both pass the check and both pair.\n    // `seal` is synchronous for that reason, and anything added between these\n    // two calls has to be too — an `await auditLog(...)` with an excellent\n    // justification is the shape this breaks in.\n    // `__tests__/pair-endpoints.test.ts` asserts no macrotask runs in the gap.\n    const result = this.pairTokens.consume(token);\n    if (!result.ok) {\n      // Cannot fail today: nothing yields between the check above and here, so\n      // no other request can have spent this token in the gap. Checked anyway\n      // because if that invariant is ever broken the failure is silent — two\n      // devices paired from one single-use token, no error, no log — and three\n      // lines is a cheap price for making it loud instead.\n      json(res, 401, { error: `Pair token ${result.reason}` });\n      return;\n    }\n\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    // Mint a per-device credential alongside the shared key (C5). The sealed\n    // apiKey is still returned so an existing client keeps working unchanged;\n    // a client that understands deviceToken can use the narrower credential and\n    // become individually revocable.\n    //\n    // Best-effort on the legacy path: a registry failure must not break pairing,\n    // which would lock the user out of their own server. Losing the row costs\n    // revocability for that device, not access. The E2EE path is the opposite\n    // and is handled just below.\n    //\n    // Where the inputs come from is the whole of GATE 4. `/api/pair/exchange` is\n    // public and unauthenticated, so an intermediary can rename a device or\n    // widen `readOnly` in the outer JSON on the way past; inside the AEAD it\n    // cannot. So a completed handshake means the authenticated payload decides,\n    // and the outer copies are not consulted at all.\n    //\n    // Written as an explicit branch rather than `registration?.deviceName ??\n    // body.deviceName`: the `??` would fall through to the outer name whenever\n    // the authenticated payload deliberately carried none, which is the same\n    // substitution wearing a nullish coalesce.\n    let device: { deviceId: string; deviceToken: string; capabilities: string[] } | null = null;\n    try {\n      const name = registration\n        ? registration.deviceName\n        : typeof body?.deviceName === \"string\"\n          ? body.deviceName.slice(0, 100)\n          : null;\n      const readOnly = registration ? registration.readOnly : body?.readOnly === true;\n      const preset = readOnly ? \"read-only\" : \"full\";\n      device =\n        this.devicesRepo?.register({\n          publicKey: clientPublicKey,\n          name,\n          preset,\n          // Recorded only when the handshake authenticated it. The static key\n          // comes out of the transcript, never off the wire as a claim — that\n          // is the difference between a device identified by a key it proved\n          // it holds and one identified by a string it sent.\n          //\n          // Setting it also sets `e2ee_required`, so a device that has once\n          // paired encrypted is pinned and never served plaintext again.\n          ...(handshake && {\n            e2eeStaticPub: handshake.initiatorStaticPub.toString(\"base64\"),\n            e2eeVersion: E2EE_EXCHANGE_VERSION,\n          }),\n        }) ?? null;\n    } catch (err) {\n      this.log.warn(\"[pair] device registration failed; pairing continues\", {\n        event: \"pair.device_register_failed\",\n        err,\n      });\n    }\n\n    // On the E2EE path, registration is mandatory rather than best-effort.\n    //\n    // Message 2 carries the `deviceId` and `deviceToken` a new client uses as\n    // its only credential — it ignores the outer compatibility copies — so a\n    // pairing that cannot produce them has nothing to tell the client. Answering\n    // 200 anyway would return a key-pinned result with no usable device: the\n    // server has recorded the static key and set the downgrade lock, the phone\n    // believes it paired, and the failure surfaces in the record layer weeks\n    // later, far from this line.\n    //\n    // Covers a null `devicesRepo` as well as a throwing one — `?? null` above\n    // turns an unopened runtime.db into a quiet `null`, which is the same\n    // half-provisioned outcome by a different route.\n    //\n    // The pair token is already spent by this point and cannot be un-spent: the\n    // consume ordering above is load-bearing for the single-use guarantee, and\n    // the device row cannot exist before it. So the honest cost of refusing here\n    // is that the user scans a fresh code, which is strictly better than a\n    // success they cannot act on.\n    if (handshake && !device) {\n      this.log.error(\"[pair] E2EE pairing failed: the device could not be registered\", {\n        event: \"pair.e2ee_registration_failed\",\n        ip,\n      });\n      json(res, 500, {\n        error:\n          \"Pairing failed: this server could not register the device. \" +\n          \"Scan a fresh pairing code and try again.\",\n        code: \"E2EE_REGISTRATION_FAILED\",\n      });\n      return;\n    }\n\n    // Message 2, written last because its payload carries the `deviceId` and\n    // the device row cannot exist until the token has been spent.\n    //\n    // Failing here would be a server fault rather than a client one, and it\n    // comes after the token is already gone — so it must not 500 the pairing\n    // and lose the device the client is about to be told about. The client\n    // sees a reply with no `e2ee` field, which its own pin turns into a\n    // visible refusal rather than a silent plaintext pairing.\n    let e2eeResponse: { v: number; noise: string } | null = null;\n    if (handshake && device) {\n      try {\n        const { message2 } = writeMessage2(\n          handshake,\n          // Every result a new client persists or presents as verified, so it\n          // never has to trust the outer, unauthenticated copy of a credential.\n          // The outer response still carries those copies for released builds;\n          // this is what the new client actually reads.\n          encodeE2eeMsg2Payload({\n            deviceId: device.deviceId,\n            deviceToken: device.deviceToken,\n            capabilities: device.capabilities,\n            publicUrl: this.publicUrl,\n            machineName: hostname(),\n            serverVersion: getVersion(),\n          }),\n        );\n        e2eeResponse = { v: E2EE_EXCHANGE_VERSION, noise: message2.toString(\"base64\") };\n      } catch (err) {\n        this.log.warn(\"[pair] E2EE response could not be written; pairing continues\", {\n          event: \"pair.e2ee_response_failed\",\n          err,\n        });\n      }\n    }\n\n    json(res, 200, {\n      // Unchanged and still sent on the E2EE path, deliberately. An older app\n      // is the only thing that can read these and it cannot be force-updated\n      // (docs/compatibility/tb-mobile.md); a new app ignores them and uses the\n      // Noise result. The response grows a field, it never loses one.\n      ciphertext: sealed.ciphertext,\n      nonce: sealed.nonce,\n      ephemeralPublicKey: sealed.ephemeralPublicKey,\n      // Advertises, does not dictate. This is what the server believes its\n      // public address to be; the address the client talks to is the one its\n      // user typed or scanned. Do not build anything on the assumption that a\n      // client adopts this.\n      //\n      // Mobile used to resolve its server address as `publicUrl ?? typedUrl`,\n      // so this field silently replaced what the user entered — and since the\n      // reply is unauthenticated before E2EE, one response could relocate a\n      // device permanently (TB-S-13). Fixed client-side in threadbase-mobile#720;\n      // still sent, now recorded rather than applied.\n      //\n      // Released builds that predate that fix DO still adopt it, so changing\n      // this value moves where old devices talk. It is not a free field.\n      publicUrl: this.publicUrl,\n      machineName: hostname(),\n      ...(device && {\n        deviceId: device.deviceId,\n        deviceToken: device.deviceToken,\n        capabilities: device.capabilities,\n      }),\n      // Additive. Absent means this pairing is plaintext — either the client\n      // never asked, or writing the reply failed — and a client that asked for\n      // encryption must treat its absence as a refusal, not as consent.\n      ...(e2eeResponse && { e2ee: e2eeResponse }),\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  /**\n   * The registry ships with the values so a client renders the list from one\n   * round-trip, same as getClaudeFlagsConfig().\n   *\n   * Deliberately no `persisted` field: unlike claude-flags there is no PUT, and\n   * the absence of that field is the signal that this endpoint is read-only.\n   */\n  /**\n   * The boot warning for a run that turned transport encryption off from any\n   * explicit, non-default source.\n   *\n   * D-8 wanted no env var to hold encryption off invisibly; that guarantee\n   * turned out to be unenforceable without breaking the registry's uniform\n   * five-rung resolution (see dilemmas.md D-8's resolution note), so this is\n   * the replacement: every explicit \"off\" — `override`, `env`, `cli`, `yaml` —\n   * announces itself the same way `--no-e2ee` always has. `default` is exempt:\n   * off-by-default is not a decision anyone made this boot, and warning on\n   * every default boot before stage 2 flips it would be noise with nothing\n   * behind it.\n   *\n   * Fires regardless of the pinned count, including at zero — a leading\n   * indicator (warn the moment the box boots plaintext) beats a lagging one\n   * (warn only once a device has already paired against it).\n   *\n   * Through the console dest as well as the JSON log, because the person who\n   * set the source is watching a terminal, and a warning they have to grep for\n   * is a warning that arrives after the incident. It says what is readable and\n   * by whom — the tunnel's edge terminates TLS, so \"we are behind HTTPS\" is not\n   * an answer — and how many paired devices this run will refuse.\n   *\n   * Called after `devicesRepo` opens, since the count is the point. Silent when\n   * the repo did not open: a boot already shouting about a failed runtime store\n   * does not need a second line saying it also cannot count.\n   */\n  private warnIfE2eeDisabled(): void {\n    if (this.featureFlags.e2ee) return;\n    const source = this.featureFlagSources.e2ee;\n    if (source === \"default\") return;\n    const repo = this.devicesRepo;\n    if (!repo) return;\n    // Live rows only — a revoked device is already refused and counting it\n    // would inflate the number that is meant to make someone stop and think.\n    const pinned = repo.list().filter((d) => d.e2ee && d.revokedAt == null).length;\n    // The caveat is in the text rather than in a comment because the number is\n    // read by a person deciding whether to proceed. A pairing whose msg2 was\n    // lost leaves a row that is pinned but has never connected (#744), so this\n    // counts intent to encrypt, not devices in anyone's hand.\n    const devices =\n      pinned === 0\n        ? \"no paired device requires it\"\n        : `${pinned} paired device${pinned === 1 ? \"\" : \"s\"} require${pinned === 1 ? \"s\" : \"\"} it and will be refused (rows that never completed pairing are counted too)`;\n    // `both`, not `console`: the person who set the source is watching a\n    // terminal, and the person reading the JSON log afterwards is asking why a\n    // device was refused. `console` alone would answer only the first of them —\n    // and the brief for this flag asks specifically for `e2ee.disabled` in the\n    // JSON log.\n    this.log.warn(\n      `Transport encryption is OFF for this run (${E2EE_OFF_SWITCH[source]}): traffic on the path is readable, including at the Cloudflare edge, and ${devices}.`,\n      { event: \"e2ee.disabled\", reason: source, pinnedDevices: pinned },\n      \"both\",\n    );\n  }\n\n  /**\n   * Ask this server's own public URL what an unauthenticated device would get.\n   *\n   * Fire-and-forget, deliberately: the probe must never delay `listen()` or\n   * refuse a boot. It reports a misconfiguration the operator can fix, and a\n   * server that will not start is a worse outcome than one that warns.\n   *\n   * Silent unless there is something to say. No public URL means no edge to\n   * probe; e2ee off means no device would be refused by one; an unreachable URL\n   * is a different problem with its own symptoms, and warning about it here\n   * would cry wolf on every laptop that is merely offline.\n   */\n  private async probeAccessGate(): Promise<void> {\n    if (!this.featureFlags.accessProbe) return;\n    if (!this.publicUrl) return;\n    if (!describeE2eeCapability(this.featureFlags.e2ee).enabled) return;\n\n    const result = await probeAccessGate({\n      publicUrl: this.publicUrl,\n      serviceToken: loadAccessServiceToken(),\n    });\n    const message = describeAccessProbe(result);\n    if (!message) return;\n\n    // Console as well as the JSON log: the operator who put Access in front of\n    // this server is the only person who can remove it, and they are looking at\n    // a terminal, not at a log aggregator.\n    this.log.warn(\n      message,\n      {\n        event: \"access.gate_detected\",\n        publicUrl: this.publicUrl,\n        // The login URL carries a signed JWT in its query string. The host is the\n        // diagnostic; the rest is not ours to write down.\n        gateHost: safeHost(result.kind === \"gated\" ? result.location : undefined),\n        serviceTokenAccepted: result.kind === \"gated\" ? result.serviceTokenAccepted : undefined,\n      },\n      \"both\",\n    );\n  }\n\n  private getFeatureFlagsConfig(): {\n    registry: typeof FEATURE_FLAG_LIST;\n    values: ResolvedFeatureFlags;\n    sources: Record<FeatureFlagId, FeatureFlagSource>;\n  } {\n    // `sources` is additive — older clients ignore it. It exists so a support\n    // question (\"why is this on?\") is answerable over HTTP instead of requiring\n    // shell access to read the environment, the argv and server.yaml by hand.\n    return {\n      registry: FEATURE_FLAG_LIST,\n      values: this.featureFlags,\n      sources: this.featureFlagSources,\n    };\n  }\n\n  private getClaudeFlagsConfig(): {\n    registry: typeof CLAUDE_FLAGS;\n    values: ClaudeFlagValues;\n    extraArgs: string | null;\n    persisted: boolean;\n  } {\n    return {\n      registry: CLAUDE_FLAGS,\n      values: this.claudeFlags,\n      extraArgs: this.claudeExtraArgs ?? null,\n      persisted: this.claudeFlagsPersistable,\n    };\n  }\n\n  /**\n   * Replace the per-server flag set. Applies to the NEXT spawn — a live PTY\n   * keeps the argv it was started with.\n   *\n   * Mirrors rotateApiKey(): when the values were pinned by a CLI flag we still\n   * apply them in memory but skip the server.yaml write, because the flag would\n   * win again on restart and silently revert them.\n   *\n   * Logged with old→new at info level on purpose: this can disable the\n   * permission prompts entirely, so it needs a forensic trail.\n   */\n  private setClaudeFlagsConfig(\n    values: ClaudeFlagValues,\n    extraArgs: string | undefined,\n  ): { values: ClaudeFlagValues; extraArgs: string | null; persisted: boolean } {\n    const safe = validateFlagValues(values);\n    const previous = { values: this.claudeFlags, extraArgs: this.claudeExtraArgs };\n\n    if (this.claudeFlagsPersistable) {\n      // setClaudeExtraArgs throws on an embedded newline; let it propagate so\n      // the route answers 400 rather than writing a corrupt config line.\n      setClaudeExtraArgs(extraArgs);\n      setClaudeFlags(safe);\n    }\n    this.claudeFlags = safe;\n    this.claudeExtraArgs = extraArgs?.trim() ? extraArgs.trim() : undefined;\n\n    this.log.info(\"Claude CLI flags updated\", {\n      event: \"config.claude_flags_updated\",\n      persisted: this.claudeFlagsPersistable,\n      previousValues: previous.values,\n      previousExtraArgs: previous.extraArgs ?? null,\n      values: this.claudeFlags,\n      extraArgs: this.claudeExtraArgs ?? null,\n    });\n\n    return {\n      values: this.claudeFlags,\n      extraArgs: this.claudeExtraArgs ?? null,\n      persisted: this.claudeFlagsPersistable,\n    };\n  }\n\n  /**\n   * The three spawn options that a configured claude-flag can override, with\n   * the boot-time CLI/yaml default as the fallback. Spread into every\n   * start/resume/adopt call so all three paths agree.\n   *\n   * These ids are excluded from buildFlagArgs (SPAWN_POSITIONAL_FLAG_IDS)\n   * precisely because they arrive here instead — the PTY spawn paths pass them\n   * as explicit positionals, so emitting them from the allowlist too would\n   * duplicate the flag.\n   *\n   * Narrowed with the type guards rather than cast: ClaudeFlagValues is a loose\n   * Record by design, and while validateFlagValues already guarantees the shape\n   * on the way in, TypeScript cannot see that through the record.\n   */\n  private spawnFlagOverrides(): {\n    permissionMode: PermissionMode;\n    model: string;\n    effort: EffortLevel;\n  } {\n    const mode = this.claudeFlags.permissionMode;\n    const model = this.claudeFlags.model;\n    const effort = this.claudeFlags.effort;\n    return {\n      permissionMode: isPermissionMode(mode) ? mode : this.defaultPermissionMode,\n      model: typeof model === \"string\" ? model : this.defaultModel,\n      effort: isEffortLevel(effort) ? effort : this.defaultEffort,\n    };\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 handleSessionsCount(res: ServerResponse): void {\n    if (this.rejectIfWarmingUp(res)) return;\n    // Recovered stubs are excluded: this badge means \"sessions this streamer is\n    // running\", and a restart must not inflate it with everything it could\n    // offer to resume. The updater's active-session probe reads the same number.\n    const total = this.sessionStore\n      .list(this.ptyAttachedIds())\n      .filter((s) => s.ownership !== \"historical\").length;\n    json(res, 200, { total });\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 and Cursor\n   * transcript lines are normalized to the Claude `type:user|assistant` shape\n   * mobile understands; Claude lines pass through unchanged.\n   */\n  private broadcastConversationLines(\n    sessionId: string,\n    lines: string[],\n    seqs?: (number | null)[] | null,\n  ): void {\n    // Filtered seqs come back parallel to the kept lines, so a normalized batch\n    // keeps its message_index instead of losing every seq to one dropped line.\n    const { lines: clientLines, seqs: clientSeqs } = toClientConversationLines(lines, seqs);\n    if (clientLines.length === 0) return;\n    this.wsHub.broadcast({\n      type: \"conversation_events\",\n      sessionId,\n      lines: clientLines,\n      ...(clientSeqs ? { seqs: clientSeqs } : {}),\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  /**\n   * Resolve a client-supplied session/conversation id into everything needed to\n   * launch against it: the id the PROVIDER filed the history under, that\n   * history's path, the project cwd, and which CLI owns it.\n   *\n   * Shared by resume and fork so the two can never disagree about identity —\n   * which for Codex is the whole difficulty: the id a client navigated to may\n   * be a local placeholder, and only the registry knows the rollout id behind\n   * it.\n   */\n  private async resolveConversationTarget(sessionId: string): Promise<\n    | ResumeFailure\n    | {\n        ok: true;\n        historyId: string;\n        jsonlPath: string | null;\n        historyPath: string | null;\n        conv: any;\n        projectPath: string;\n        provider: ProviderName;\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    // A fresh Codex session uses a local placeholder id while Codex indexes\n    // history under the rollout id it assigned itself. Resolve that managed\n    // binding before any conversation lookup: findConversationByUuid can serve\n    // the placeholder through the in-memory alias, but that does not make the\n    // placeholder a valid `codex resume` target. The live session keeps the id\n    // the client navigated to; only argv and history lookups use the bound id.\n    const row = this.managedSessionsRepo?.get(sessionId) ?? null;\n    const liveSession = this.sessionStore.getManaged(sessionId);\n    const resumeId =\n      (row ? resumeIdForRow(row) : null) ??\n      (liveSession?.provider === CODEX_CLI_PROVIDER\n        ? (liveSession.boundConversationId ?? null)\n        : null);\n    const historyId = resumeId ?? sessionId;\n    if (await this.conversationHandlers.isExcludedSubagent(historyId)) {\n      return { ok: false, reason: \"history_file_missing\" };\n    }\n    const managedProvider = row?.provider ?? liveSession?.provider;\n    const jsonlPath = this.conversationHandlers.findJsonlPath(historyId);\n    const conv = await this.conversationHandlers.findConversationByUuid(historyId);\n\n    // Cold boot resolves nothing through the scanner yet, so a persisted Codex\n    // rollout has to come from the cache. Without this a boot-time resume of a\n    // Codex session reads as history_file_missing purely because the warm-up\n    // has not run, and auto-resume permanently skips a session that is fine.\n    const cachedConvMeta = this.cache?.getMetaById(historyId);\n    if (\n      !this.featureFlags.subagentSessions &&\n      (row?.is_subagent === 1 || liveSession?.isSubagent || cachedConvMeta?.isSubagent)\n    ) {\n      return { ok: false, reason: \"history_file_missing\" };\n    }\n    const cachedPath = cachedConvMeta?.filePath ? toNativeFilePath(cachedConvMeta.filePath) : null;\n    const cachedCodexPath =\n      (managedProvider === CODEX_CLI_PROVIDER || cachedConvMeta?.provider === CODEX_CLI_PROVIDER) &&\n      cachedPath != null &&\n      existsSync(cachedPath)\n        ? cachedPath\n        : null;\n    const jsonlCwd = jsonlPath ? await this.conversationHandlers.readCwdFromJsonl(jsonlPath) : null;\n    const projectPath: string =\n      jsonlCwd ??\n      (conv as any)?.projectPath ??\n      (cachedCodexPath ? cachedConvMeta?.projectPath : null);\n    if (!projectPath) {\n      // Nothing at all resolved — the history file is gone, not merely\n      // unreadable. Distinguished from \"path unknown\" because it is permanent:\n      // the caller must not retry it.\n      if (!conv && !jsonlPath && !cachedCodexPath) {\n        return { ok: false, reason: \"history_file_missing\" };\n      }\n      return { ok: false, reason: \"no_project_path\" };\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    // …and, when the fallback above fired, the managed session's provider as a\n    // last resort: neither lookup is keyed by the placeholder id, so without it\n    // a Codex placeholder would default to Claude and spawn the wrong CLI. The\n    // live session covers the supported degraded mode where runtime.db failed.\n    const provider = coerceProviderForRunner(\n      (conv as any)?.provider ?? cachedConvMeta?.provider ?? managedProvider,\n    );\n\n    return {\n      ok: true,\n      historyId,\n      jsonlPath,\n      // findJsonlPath() only knows Claude's `<uuid>.jsonl` layout under\n      // ~/.claude/projects; a Codex rollout lives in a date-nested directory\n      // under a name it chose, so its path only ever comes from the indexed\n      // conversation. Kept separate from `jsonlPath` deliberately: feeding it to\n      // conversationBusy() would newly arm the mtime heuristic for Codex, which\n      // is exactly the over-broad signal the report ruled out.\n      historyPath:\n        jsonlPath ?? ((conv as any)?.filePath as string | undefined) ?? cachedCodexPath ?? null,\n      conv,\n      projectPath,\n      provider,\n    };\n  }\n\n  /**\n   * Block until a freshly spawned session reaches `waiting_input` (ready) or\n   * `idle` (failed), or until `timeoutMs` elapses with the process still alive.\n   *\n   * \"timeout\" is not an error: it is the pre-existing asynchronous contract —\n   * the session keeps booting and the caller answers with a pending shape.\n   */\n  private waitForStartupOutcome(\n    sessionId: string,\n    timeoutMs: number,\n  ): Promise<{ outcome: \"ready\" | \"failed\" | \"timeout\"; session: ManagedSession | null }> {\n    return new Promise((resolve) => {\n      let timer: NodeJS.Timeout | null = null;\n      const handler = (status: string, session?: ManagedSession) => {\n        if (status !== \"waiting_input\" && status !== \"idle\") return;\n        this.sessionStatusBus.off(`status:${sessionId}`, handler);\n        if (timer) clearTimeout(timer);\n        resolve({\n          outcome: status === \"waiting_input\" ? \"ready\" : \"failed\",\n          session: session ?? null,\n        });\n      };\n      this.sessionStatusBus.on(`status:${sessionId}`, handler);\n      timer = setTimeout(() => {\n        this.sessionStatusBus.off(`status:${sessionId}`, handler);\n        resolve({ outcome: \"timeout\", session: null });\n      }, timeoutMs);\n      timer.unref?.();\n    });\n  }\n\n  /**\n   * Drop every trace of a managed session: in-memory store, durable registry\n   * row, and the collision-probe markers that would otherwise outlive it.\n   *\n   * Used when a start never became usable (`abandonFailedStart`) and when stop\n   * is asked to discard an empty session that has no cached conversation. The\n   * registry delete is load-bearing — `rehydrateSessions` will bring the row\n   * back on the next boot if it remains.\n   *\n   * Callers that kill the PTY (`putOnHold`) must do that *first*: onStatusChange\n   * on idle writes `selfPtyEndedAt` and a registry status, and those have to\n   * be cleared here afterwards.\n   */\n  private forgetSession(sessionId: string): void {\n    this.sessionStore.removeManaged(sessionId);\n    this.selfPtyEndedAt.delete(sessionId);\n    this.contendedSessions.delete(sessionId);\n    try {\n      this.managedSessionsRepo?.delete(sessionId);\n    } catch (err) {\n      this.log.warn(\"[registry] failed to drop a session\", {\n        event: \"registry.forget_failed\",\n        sessionId,\n        err,\n      });\n    }\n  }\n\n  /**\n   * Drop every trace of a session that never became usable.\n   *\n   * The runner has already torn itself down (failStartup / handleExit); what\n   * remains is server-side bookkeeping that would otherwise leave a dead\n   * session in the list, a registry row claiming a spawn, and a `selfPtyEndedAt`\n   * marker that would suppress the mtime collision signal on the NEXT resume —\n   * i.e. it would help hide the very owner we just collided with.\n   */\n  private abandonFailedStart(sessionId: string): void {\n    this.forgetSession(sessionId);\n  }\n\n  private enrichResumedSessionAsync(sessionId: string, projectPath: string, conv: any): void {\n    try {\n      // Writes go through updateManaged: sessionStore.get() would hand back a\n      // response copy and every assignment below would be silently discarded.\n      // The store holds `ManagedSession`, so the timestamps are Dates here —\n      // managedToResponse serializes them, as it already does for startedAt.\n      if (!this.sessionStore.getManaged(sessionId)) return;\n\n      if (conv) {\n        this.sessionStore.updateManaged(sessionId, {\n          sessionName: conv.sessionName ?? undefined,\n          messageCount: conv.messageCount ?? 0,\n          account: conv.account ?? undefined,\n          filePath: conv.filePath ?? undefined,\n        });\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        const first = cached.firstMessage ? JSON.parse(cached.firstMessage as string) : null;\n        const last = cached.lastMessage ? JSON.parse(cached.lastMessage as string) : null;\n        this.sessionStore.updateManaged(sessionId, {\n          model: cached.model ?? undefined,\n          preview: cached.preview ?? undefined,\n          firstMessageText: first?.text ?? undefined,\n          // parseIsoDateOrNull, not `new Date()`: an unparseable cached\n          // timestamp must land as absent, not as an Invalid Date that\n          // managedToResponse would throw on when it calls .toISOString().\n          firstMessageAt: parseIsoDateOrNull(first?.timestamp) ?? undefined,\n          lastMessageText: last?.text ?? undefined,\n          lastMessageAt: parseIsoDateOrNull(last?.timestamp) ?? undefined,\n        });\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        this.sessionStore.updateManaged(sessionId, {\n          projectId: resolvedProjectId,\n          resumedFromConversationId: sessionId,\n        });\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  // Store + broadcast AskUserQuestion cards found in a JSONL batch for a watched\n  // session. Two P0 safety guards on top of the screen/JSONL de-dupe:\n  //   (a) contended file → suppress JSONL-derived cards entirely (a line may be\n  //       the OTHER owner's question); the streamer's own PTY questions still\n  //       arrive via the live-screen path (handleLiveQuestion), not suppressed.\n  //   (b) a JSONL question must never clobber a PTY-screen question that is a\n  //       DIFFERENT question — answering it would type into this streamer's PTY.\n  //       Same-content re-syncs (screen synthetic id → real toolUseId) still pass.\n  private processJsonlQuestions(sessionId: string, lines: string[]): void {\n    // toolUseId the client currently holds for this session (set by the\n    // live-screen path as `screen:…`, or a prior JSONL flush). Captured BEFORE\n    // the overwrite below so we can detect an id change.\n    const priorPending = this.pendingQuestions.get(sessionId);\n    const priorToolUseId = priorPending?.toolUseId;\n    const contended = this.contendedSessions.has(sessionId);\n    const priorPtyKey =\n      priorPending?.origin === \"pty\" ? questionContentKey(priorPending.questions) : null;\n    const foreignVsPty = (questions: AskQuestion[]): boolean =>\n      priorPtyKey !== null && questionContentKey(questions) !== priorPtyKey;\n    // An ANSWERED question keeps its menu's key while nothing is pending — every\n    // answer path keeps it until the menu leaves the screen (#724). A JSONL flush\n    // of that same content landing after the answer is the answered question\n    // arriving late, not a new one: minting it opened a fresh actionable prompt\n    // (and re-rendered a legacy card) that the 60s timer below only cancelled a\n    // minute later — #724's 60-second phantom.\n    const answeredKey = priorPending ? null : (this.pendingQuestionKey.get(sessionId) ?? null);\n    const answeredLate = (questions: AskQuestion[]): boolean =>\n      answeredKey !== null && questionContentKey(questions) === answeredKey;\n    const { messages, pending } = questionsFromLines(sessionId, lines);\n    for (const p of pending) {\n      if (contended || foreignVsPty(p.questions) || answeredLate(p.questions)) continue;\n      // Preserve a same-question re-sync's \"pty\" origin so a later foreign JSONL\n      // question still can't clobber it.\n      const origin: \"pty\" | \"jsonl\" =\n        priorPtyKey !== null && questionContentKey(p.questions) === priorPtyKey ? \"pty\" : \"jsonl\";\n      this.sessionHandlers.handleJsonlQuestion(sessionId, p.toolUseId, p.questions, origin);\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 `screen:`\n    // id the client holds. The client answers with the id it was given; if it\n    // still has the screen id, resolveAnswer rejects the POST as\n    // tool_use_mismatch. Re-broadcasting the real id re-syncs the client\n    // (mapAskQuestionToBlock just replaces activeQuestion — the card re-renders\n    // identically) so answering works.\n    for (const m of messages) {\n      // Same suppression as the pending loop: never render a JSONL card for a\n      // contended file, nor a foreign question over a live PTY one.\n      if (contended || foreignVsPty(m.questions) || answeredLate(m.questions)) continue;\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      // Prompt content goes to the session's subscribers only; a late\n      // subscriber gets it from the subscribe replay.\n      if (broadcast) this.wsHub.broadcastToClients(this.sessionSubscribers.get(sessionId) ?? [], m);\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    const prompt = this.promptRegistry.get(pq.promptId);\n    if (prompt?.state === \"open\" || prompt?.state === \"updated\") {\n      this.promptRegistry.transition(pq.promptId, \"cancelled\", \"provider_closed\");\n    }\n    this.wsHub.broadcastToClients(this.sessionSubscribers.get(sessionId) ?? [], {\n      type: \"question_cancelled\",\n      sessionId,\n      toolUseId: pq.toolUseId,\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, files] = await Promise.all([\n        listDirectories(resolved),\n        listFiles(resolved),\n      ]);\n      json(res, 200, { path: relativePath, directories, files });\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  /**\n   * Retarget a LIVE session's model or effort by typing the corresponding\n   * Claude Code slash command into its PTY.\n   *\n   * There is no CLI or IPC channel for this — `--model`/`--effort` are spawn\n   * arguments — so the interactive `/model <x>` / `/effort <y>` commands are the\n   * only way to change a session already running. Both accept an argument and\n   * apply it without opening the picker (verified against Claude Code v2.1.220).\n   *\n   * Answers 202, not 200: the value is applied by the TUI on its next render, so\n   * there is nothing truthful to echo back synchronously. Clients confirm with\n   * `GET /api/sessions/:id`, which scrapes the applied value off the live status\n   * line.\n   */\n  private async applyLiveSessionSetting(\n    sessionId: string,\n    req: IncomingMessage,\n    res: ServerResponse,\n    setting: \"model\" | \"effort\",\n  ): Promise<void> {\n    // Both putOnHold() and handleExit() DELETE the session from the runner's\n    // map, so \"no live PTY\" reads as absent here, not as status \"idle\". Fall back\n    // to the registry to tell a held/exited session (409, resume it) apart from\n    // an id that was never ours (404) — mobile holds sessions routinely via the\n    // grace timer, and answering 404 for one it can see in its list is a lie.\n    const session = this.ptyManager.getSession(sessionId);\n    if (!session) {\n      const known = this.sessionStore.getManaged(sessionId);\n      if (known) {\n        json(res, 409, {\n          error: \"Session has no live PTY; resume it first\",\n          code: \"SESSION_IDLE\",\n        });\n        return;\n      }\n      json(res, 404, { error: \"Session not found\" });\n      return;\n    }\n    if ((session.provider ?? CLAUDE_CODE_PROVIDER) !== CLAUDE_CODE_PROVIDER) {\n      json(res, 501, {\n        error: `Setting ${setting} on a ${session.provider} session is not supported`,\n        code: \"UNSUPPORTED_PROVIDER\",\n      });\n      return;\n    }\n    // Mid-turn the composer is not accepting a slash command, so the injected\n    // text would be swallowed or garbled. Make the caller wait for the turn.\n    if (session.status === \"running\") {\n      json(res, 409, {\n        error: \"Session is mid-turn; retry once it is waiting for input\",\n        code: \"SESSION_BUSY\",\n      });\n      return;\n    }\n\n    let parsed: { model?: unknown; effort?: unknown };\n    try {\n      parsed = await readBody(req);\n    } catch {\n      json(res, 400, { error: \"Invalid JSON\" });\n      return;\n    }\n\n    // TRUST BOUNDARY: this value is written as raw bytes into a live terminal.\n    // An unvalidated \\r would end the slash command and let the rest of the\n    // string run as a second, attacker-chosen command. Validate, never escape.\n    let value: string;\n    if (setting === \"effort\") {\n      if (!isEffortLevel(parsed.effort)) {\n        json(res, 400, {\n          error: `effort must be one of ${EFFORT_LEVELS.join(\", \")}`,\n        });\n        return;\n      }\n      value = parsed.effort;\n    } else {\n      if (typeof parsed.model !== \"string\" || !MODEL_NAME_RE.test(parsed.model)) {\n        json(res, 400, {\n          error:\n            \"model must be an alias or full model name (letters, digits, dot, dash, underscore)\",\n        });\n        return;\n      }\n      value = parsed.model;\n    }\n\n    try {\n      this.ptyManager.sendKeys(sessionId, `/${setting} ${value}\\r`);\n    } catch (err) {\n      json(res, 400, { error: err instanceof Error ? err.message : \"Failed to write to session\" });\n      return;\n    }\n    this.log.info(`Live session ${setting} set to ${value}`, {\n      event: \"session.setting_applied\",\n      sessionId,\n      setting,\n      value,\n    });\n    // Record what we asked for and tell subscribers, so the WS carries the new\n    // value instead of leaving every client to re-GET the session. Only\n    // `effort` is stored: `model` is owned by the scanner, and overwriting it\n    // here with a spawn-time alias would clobber the resolved name.\n    //\n    // Optimistic by nature — 202 means the TUI has not applied it yet. The\n    // status-line scrape on GET /api/sessions/:id stays authoritative and wins\n    // if the two ever disagree.\n    if (setting === \"effort\") {\n      this.sessionStore.updateManaged(sessionId, { effort: value });\n      const updated = this.sessionStore.get(sessionId, this.ptyAttachedIds());\n      if (updated) this.wsHub.broadcast({ type: \"session_update\", session: updated });\n    }\n    json(res, 202, { id: sessionId, [setting]: value });\n  }\n}\n\n// ─── Utilities ─────────────────────────────────────────────────────\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","// Does an edge gate stand between paired devices and this server?\n//\n// **Why the streamer asks this at all.** A sealed request carries no\n// `Authorization` header — that is the point of the envelope — and an\n// interactive Cloudflare Access application rejects credential-less requests at\n// the edge, before the tunnel. Measured on hardware (D2 row 9, 2026-09-02): with\n// Access in front of a tunnelled streamer, `POST /api/e2ee/open` never arrives;\n// the device receives an HTML redirect to a login page where message 2 belongs,\n// and pairing fails closed with \"this server offered an encrypted pairing and\n// then did not finish it\". Nothing in that message points at the gate, so the\n// operator regenerates pairing codes forever.\n//\n// The server is the one component that can see both sides: it knows its own\n// public URL and can ask the edge what an unauthenticated device would get. So\n// it asks once, at boot, and says so plainly.\n\n/** What the probe found. */\nexport type AccessProbeResult =\n  /** No gate: an unauthenticated request reached this server. */\n  | { kind: \"open\"; status: number }\n  /** A gate answered instead of this server. `location` is the login redirect. */\n  | { kind: \"gated\"; status: number; location: string; serviceTokenAccepted?: boolean }\n  /** The probe could not tell. Never a warning — an unreachable public URL is a\n   *  different problem with its own symptoms, and guessing would cry wolf. */\n  | { kind: \"unknown\"; reason: string };\n\nexport interface AccessServiceToken {\n  clientId: string;\n  clientSecret: string;\n}\n\n/**\n * A redirect to Cloudflare's Access login is the signature: any 30x whose\n * `location` names the `/cdn-cgi/access/login` path. Matched on the path rather\n * than the hostname because the team domain is per-account\n * (`<team>.cloudflareaccess.com`) and a custom domain is allowed.\n */\nfunction isAccessRedirect(status: number, location: string | null): location is string {\n  if (status < 300 || status >= 400 || !location) return false;\n  return location.includes(\"/cdn-cgi/access/login\");\n}\n\n/**\n * One request to this server's own public URL, from outside its own process.\n *\n * `/healthz` deliberately: it needs no credential, so a non-200 says something\n * about the *path in front of* the server rather than about authorization.\n * `redirect: \"manual\"` because the answer IS the redirect — following it would\n * fetch a login page and lose the evidence.\n */\nexport async function probeAccessGate(args: {\n  publicUrl: string;\n  serviceToken?: AccessServiceToken;\n  fetchImpl?: typeof fetch;\n  timeoutMs?: number;\n}): Promise<AccessProbeResult> {\n  const doFetch = args.fetchImpl ?? fetch;\n  const base = args.publicUrl.replace(/\\/$/, \"\");\n  const timeoutMs = args.timeoutMs ?? 5000;\n\n  const once = async (headers?: Record<string, string>) => {\n    const controller = new AbortController();\n    const timer = setTimeout(() => controller.abort(), timeoutMs);\n    try {\n      return await doFetch(`${base}/healthz`, {\n        method: \"GET\",\n        redirect: \"manual\",\n        signal: controller.signal,\n        ...(headers ? { headers } : {}),\n      });\n    } finally {\n      clearTimeout(timer);\n    }\n  };\n\n  let res: Response;\n  try {\n    res = await once();\n  } catch (err) {\n    // Includes the timeout. An unreachable public URL is not a gate.\n    return { kind: \"unknown\", reason: err instanceof Error ? err.message : String(err) };\n  }\n\n  const location = res.headers.get(\"location\");\n  if (!isAccessRedirect(res.status, location)) {\n    return { kind: \"open\", status: res.status };\n  }\n\n  // A gate is there. If the operator configured a service token, the useful\n  // question is no longer \"is there a gate\" but \"does my token get through it\",\n  // because that is the difference between a working rollout and a broken one.\n  if (!args.serviceToken) {\n    return { kind: \"gated\", status: res.status, location };\n  }\n  try {\n    const withToken = await once({\n      \"CF-Access-Client-Id\": args.serviceToken.clientId,\n      \"CF-Access-Client-Secret\": args.serviceToken.clientSecret,\n    });\n    const tokenLocation = withToken.headers.get(\"location\");\n    return {\n      kind: \"gated\",\n      status: res.status,\n      location,\n      serviceTokenAccepted: !isAccessRedirect(withToken.status, tokenLocation),\n    };\n  } catch {\n    // The unauthenticated probe already answered the question that matters.\n    // A failed second request leaves `serviceTokenAccepted` absent, which the\n    // message below reads as \"not verified\" rather than as \"rejected\".\n    return { kind: \"gated\", status: res.status, location };\n  }\n}\n\n/**\n * The operator-facing sentence. Separate from the probe so a test can read the\n * words rather than assert on a shape, and so the wording can change without\n * touching the network code.\n */\nexport function describeAccessProbe(result: AccessProbeResult): string | null {\n  if (result.kind !== \"gated\") return null;\n  const head =\n    \"An edge gate (Cloudflare Access) answers requests to this server's public URL, so encrypted \" +\n    \"devices cannot reach it: a sealed request carries no Authorization header by design, and the \" +\n    \"gate refuses it before it arrives. Pairing will fail with a message that blames the server.\";\n  if (result.serviceTokenAccepted === true) {\n    return `${head} The configured Access service token DOES satisfy the gate, so devices presenting it can pair.`;\n  }\n  if (result.serviceTokenAccepted === false) {\n    return `${head} The configured Access service token does NOT satisfy the gate — check that a Service Auth policy names it.`;\n  }\n  return `${head} Fix it by removing Access from this hostname, or by adding a service token and a Service Auth policy for it.`;\n}\n\n/**\n * The host of the login redirect, and nothing else.\n *\n * That URL carries a signed JWT in its query string — identity metadata about\n * the account and the request. The host (`<team>.cloudflareaccess.com`) is the\n * diagnostic an operator needs; the rest is not ours to write into a log file\n * that ends up attached to bug reports.\n */\nexport function safeHost(location: string | undefined): string | undefined {\n  if (!location) return undefined;\n  try {\n    return new URL(location).host;\n  } catch {\n    return undefined;\n  }\n}\n","import type { HttpBindings } from \"@hono/node-server\";\nimport { Hono } from \"hono\";\nimport type { UpgradeWebSocket } from \"hono/ws\";\nimport type { ServerResponse } from \"http\";\nimport type { WebSocket } from \"ws\";\nimport type { E2eeContext } from \"../e2ee/context\";\nimport { getLogger } from \"../logger\";\nimport type { Principal } from \"../services/security/capabilities\";\nimport { authMiddleware } from \"./middleware/auth.middleware\";\nimport { corsMiddleware } from \"./middleware/cors.middleware\";\nimport { e2eeEnvelopeMiddleware } from \"./middleware/e2ee-envelope.middleware\";\nimport { errorMiddleware } from \"./middleware/error.middleware\";\nimport { createBackupRoutes } from \"./routes/backup.routes\";\nimport { createBrowseRoutes } from \"./routes/browse.routes\";\nimport { createCacheAlertRoutes } from \"./routes/cacheAlert.routes\";\nimport { createConfigRoutes } from \"./routes/config.routes\";\nimport { createConversationRoutes } from \"./routes/conversations.routes\";\nimport { createDeviceRoutes } from \"./routes/devices.routes\";\nimport { createDiagnosticsRoutes } from \"./routes/diagnostics.routes\";\nimport { createE2eeRoutes } from \"./routes/e2ee.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 { createProviderRoutes } from \"./routes/providers.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    /** Who is making this request (C5). Set by authMiddleware. */\n    principal?: Principal;\n    /**\n     * The E2EE context this request authenticated under, if any.\n     *\n     * **ONE declaration, deliberately, for two setters.** W1b's `/ws` upgrade\n     * and X-server's REST unseal both need this field, and both branches added\n     * it independently; two declarations of the same optional key merge in\n     * TypeScript and compile, right up until the two comments disagree about\n     * what the field means. Set by `authMiddleware` when a `/ws` upgrade\n     * presents a live ticket and read by the WebSocket route; set by\n     * `e2eeEnvelopeMiddleware` when a REST request unseals under `X-TB-Ctx`.\n     *\n     * It is here rather than in a module-level map because the setter and the\n     * reader are separated by an `await` — a shared variable would let two\n     * concurrent upgrades swap contexts.\n     *\n     * Absent on every plaintext request, which is what `refuseUnsealedIfPinned`\n     * reads as \"this request was not sealed\".\n     */\n    e2eeContext?: E2eeContext;\n  };\n};\n\n/** Routes that write to `c.env.outgoing` themselves return this sentinel. */\nconst ALREADY_HANDLED = 597;\n\n/**\n * Query parameters whose value is reduced REGARDLESS of its shape.\n *\n * The numeric rule below is a heuristic — it keeps pagination readable — and a\n * heuristic is the wrong thing to protect a credential with. `?ticket=` was\n * covered only by the accident that a base64url ticket is essentially never all\n * digits, and \"essentially never\" is exactly the probability-instead-of-\n * invariant argument the nonce design rejects (NONCE-DESIGN §10):\n *\n *     summarizeQuery({ ticket: \"Xk9_2bQz-aR4\" })   →  ticket=_\n *     summarizeQuery({ ticket: \"84719203847192\" })  →  ticket=84719203847192\n *\n * Listing the keys instead protects `?key=` and every future secret-bearing\n * parameter rather than one generator's alphabet. Matched case-insensitively so\n * a `?apiKey=` cannot slip past on capitalisation.\n */\nconst SENSITIVE_QUERY_KEYS = new Set([\"ticket\", \"key\", \"token\", \"apikey\", \"api_key\"]);\n\n/**\n * The query string with only its numeric values kept.\n *\n * Pagination is the thing the log needs to show (`limit`, `offset`,\n * `before_index`, `msg_limit`), and those are numbers. Everything else —\n * `?path=/Users/...`, ids, search terms — is a value we deliberately do not\n * log, so the key is kept and the value is dropped. Returns undefined for a\n * bare path so the field stays absent rather than empty.\n *\n * Deliberately NOT blanket numeric redaction: `limit=50` must keep logging its\n * value, or the fix costs the diagnostics this field exists for.\n */\nexport function summarizeQuery(query: Record<string, string>): string | undefined {\n  const keys = Object.keys(query).sort();\n  if (keys.length === 0) return undefined;\n  return keys\n    .map((k) => {\n      const keep = !SENSITIVE_QUERY_KEYS.has(k.toLowerCase()) && /^-?\\d+$/.test(query[k]);\n      return `${k}=${keep ? query[k] : \"_\"}`;\n    })\n    .join(\"&\");\n}\n\n/**\n * Count the bytes a handler writes straight to the Node response.\n *\n * There is no Content-Length to read: the direct-write handlers stream JSON\n * into `c.env.outgoing` and Node uses chunked encoding. Patching write/end for\n * the life of the request is the only place the size is observable.\n */\nfunction countResponseBytes(res: ServerResponse): () => number {\n  let bytes = 0;\n  const add = (chunk: unknown) => {\n    if (typeof chunk === \"string\") bytes += Buffer.byteLength(chunk);\n    else if (chunk instanceof Uint8Array) bytes += chunk.byteLength;\n  };\n  const write = res.write as (...args: unknown[]) => boolean;\n  const end = res.end as (...args: unknown[]) => ServerResponse;\n  res.write = function (this: ServerResponse, ...args: unknown[]) {\n    add(args[0]);\n    return write.apply(this, args);\n  } as typeof res.write;\n  res.end = function (this: ServerResponse, ...args: unknown[]) {\n    add(args[0]);\n    return end.apply(this, args);\n  } as typeof res.end;\n  return () => bytes;\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    // Absent only when the app is driven by Hono's test client rather than\n    // served over Node; the request must still be logged, minus the size.\n    const outgoing = c.env?.outgoing as ServerResponse | undefined;\n    const bytesWritten = outgoing && countResponseBytes(outgoing);\n    await next();\n    const ms = Date.now() - start;\n    // A direct-write route's Hono status is the 597 sentinel, not the status\n    // the client got — which is why 96% of these lines used to read `→ 597`.\n    // The real one is on the Node response the handler wrote to.\n    const handled = c.res.status === ALREADY_HANDLED && outgoing !== undefined;\n    const status = handled ? (outgoing as ServerResponse).statusCode : c.res.status;\n    // Drop the menubar's healthy poll, and only that. GET /healthz every 5s is\n    // the app's sole request (vendor/menubar src/renderer/renderer.js), so at\n    // the measured 285B per line it is ~17 280 lines ≈ 4.9MB/day — roughly 5×\n    // the rate of everything else this log records, against a 32MB cap that\n    // log-cap.ts only applies at a --prod boot. Until this predicate carried a\n    // path and a status it dropped EVERY menubar request: a menubar 401/404/500\n    // left no trace at all, which is invisible to exactly the log archaeology\n    // that finds bugs like the /api/conversations 404s (#778).\n    if (\n      !deps.logMenubarRequests &&\n      c.req.header(\"x-client\") === \"menubar\" &&\n      c.req.path === \"/healthz\" &&\n      status < 400\n    ) {\n      return;\n    }\n    const qs = summarizeQuery(c.req.query());\n    // Only the direct-write path has finished writing by now; a Hono-piped\n    // response is serialized after this middleware returns, so its size is not\n    // knowable here. Omit the field rather than log a confident 0.\n    const bytes = handled && bytesWritten ? bytesWritten() : undefined;\n    httpLog.info(`[req] ${c.req.method} ${c.req.path} → ${status} ${ms}ms`, {\n      method: c.req.method,\n      path: c.req.path,\n      status,\n      ms,\n      ua,\n      ...(qs ? { qs } : {}),\n      ...(bytes === undefined ? {} : { bytes }),\n      event: \"http.request\",\n    });\n  });\n  app.use(\"*\", corsMiddleware(deps.browserCors));\n  // AFTER cors, BEFORE auth. A preflight must be answerable without a context,\n  // and D-9 puts the unseal ahead of authentication so the credential travels\n  // sealed rather than in a plaintext header on every request.\n  app.use(\"*\", e2eeEnvelopeMiddleware(deps));\n  app.use(\"*\", authMiddleware(deps));\n  app.onError(errorMiddleware);\n\n  app.route(\"/healthz\", createHealthRoutes(deps));\n  app.route(\"/api/diagnostics\", createDiagnosticsRoutes(deps));\n  app.route(\"/\", createMiscRoutes(deps));\n  app.route(\"/api/sessions\", createSessionRoutes(deps));\n  app.route(\"/api/conversations\", createConversationRoutes(deps));\n  app.route(\"/api/cache/alert\", createCacheAlertRoutes(deps));\n  app.route(\"/api/config\", createConfigRoutes(deps));\n  app.route(\"/api/projects\", createProjectRoutes(deps));\n  app.route(\"/api/providers\", createProviderRoutes());\n  app.route(\"/api/devices\", createDeviceRoutes(deps));\n  app.route(\"/api/backup\", createBackupRoutes(deps));\n  app.route(\"/api/pair\", createPairRoutes(deps));\n  app.route(\"/api/e2ee\", createE2eeRoutes(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 Database from \"better-sqlite3\";\nimport { createHash, randomBytes, randomUUID, timingSafeEqual } from \"crypto\";\nimport {\n  type Capability,\n  type CapabilityPreset,\n  capabilitiesForPreset,\n  isCapability,\n} from \"../../services/security/capabilities\";\n\n/**\n * Paired-device registry (C5).\n * See docs/architecture/2026-07-24-device-identity-and-capabilities.md.\n *\n * Stores identity and authority for each paired device. Never stores a usable\n * credential: only the SHA-256 of a device token, so reading this table cannot\n * impersonate a device.\n */\n\nexport interface DeviceRow {\n  device_id: string;\n  public_key: string;\n  token_hash: string;\n  name: string | null;\n  capabilities: string;\n  created_at: number;\n  last_seen_at: number | null;\n  revoked_at: number | null;\n  /** Noise static public key, base64. Null on every row that predates E2EE. */\n  e2ee_static_pub: string | null;\n  /** The downgrade lock. 1 once this device has completed a handshake. */\n  e2ee_required: number;\n  e2ee_version: number | null;\n}\n\n/** A device as reported over the API. Deliberately carries no credential. */\nexport interface DeviceView {\n  deviceId: string;\n  name: string | null;\n  capabilities: Capability[];\n  createdAt: number;\n  lastSeenAt: number | null;\n  revokedAt: number | null;\n  /**\n   * Whether this device is pinned to encryption.\n   *\n   * A boolean, never the key and never a hash of it: `GET /api/devices` exists\n   * so a user can spot a device they did not pair, and answering \"is this one\n   * encrypted\" needs no key material to do it. Publishing the static key would\n   * hand an attacker who reached this endpoint the value that identifies a\n   * device.\n   */\n  e2ee: boolean;\n}\n\nexport interface RegisteredDevice {\n  deviceId: string;\n  /** Returned to the client exactly once, at pairing. Never persisted raw. */\n  deviceToken: string;\n  capabilities: Capability[];\n}\n\n/** Device tokens are opaque high-entropy strings; 32 bytes matches the API key. */\nexport function generateDeviceToken(): string {\n  return randomBytes(32).toString(\"base64url\");\n}\n\nexport function hashDeviceToken(token: string): string {\n  return createHash(\"sha256\").update(token).digest(\"hex\");\n}\n\n/**\n * Constant-time comparison of two token hashes, matching `validateApiKey`'s\n * discipline. Both inputs are fixed-length hex digests, so a length mismatch\n * means a malformed value rather than a secret-dependent branch.\n */\nexport function safeHashEquals(a: string, b: string): boolean {\n  if (a.length !== b.length) return false;\n  return timingSafeEqual(Buffer.from(a, \"utf8\"), Buffer.from(b, \"utf8\"));\n}\n\n/**\n * Parse the stored capability JSON, dropping anything this build does not\n * recognize. A downgrade must never silently grant a capability it cannot\n * enforce — unknown entries are discarded rather than trusted.\n */\nexport function parseCapabilities(raw: string): Capability[] {\n  try {\n    const parsed: unknown = JSON.parse(raw);\n    if (!Array.isArray(parsed)) return [];\n    return parsed.filter(isCapability);\n  } catch {\n    return [];\n  }\n}\n\nexport function toDeviceView(row: DeviceRow): DeviceView {\n  return {\n    deviceId: row.device_id,\n    name: row.name,\n    capabilities: parseCapabilities(row.capabilities),\n    createdAt: row.created_at,\n    lastSeenAt: row.last_seen_at,\n    revokedAt: row.revoked_at,\n    // Reported from `e2ee_required` rather than from the key's presence. The\n    // two agree today, but they answer different questions — the key is what a\n    // handshake is checked against, `e2ee_required` is whether plaintext is\n    // refused — and it is the second one a user is asking about.\n    e2ee: row.e2ee_required === 1,\n  };\n}\n\nexport class DevicesRepository {\n  private insertStmt: Database.Statement;\n  private byTokenHashStmt: Database.Statement;\n  private byIdStmt: Database.Statement;\n  private listStmt: Database.Statement;\n  private revokeStmt: Database.Statement;\n  private touchStmt: Database.Statement;\n  private deleteStmt: Database.Statement;\n  private deleteRevokedStmt: Database.Statement;\n  private byE2eeStaticPubStmt: Database.Statement;\n  private repairStmt: Database.Statement;\n\n  constructor(db: Database.Database) {\n    this.insertStmt = db.prepare(`\n      INSERT INTO devices (\n        device_id, public_key, token_hash, name, capabilities, created_at,\n        e2ee_static_pub, e2ee_required, e2ee_version\n      ) VALUES (\n        @device_id, @public_key, @token_hash, @name, @capabilities, @created_at,\n        @e2ee_static_pub, @e2ee_required, @e2ee_version\n      )\n    `);\n    this.byE2eeStaticPubStmt = db.prepare(\"SELECT * FROM devices WHERE e2ee_static_pub = ?\");\n    // A re-pair of a device we already know by its static key.\n    //\n    // `created_at` is deliberately NOT touched: it is what a user reads on the\n    // paired-devices screen to spot a device they did not pair, and refreshing\n    // it would erase the evidence that screen exists for.\n    //\n    // `revoked_at` IS cleared. The alternative — leaving it set — makes a\n    // pairing that visibly succeeds produce a device whose every request\n    // 401s, with nothing explaining why. Re-pairing requires a live pair\n    // token minted on that machine, so it is an authorized act by the same\n    // person who revoked it. Decided 2026-08-16; recorded in design.md §4.4.\n    this.repairStmt = db.prepare(`\n      UPDATE devices SET\n        public_key = @public_key,\n        token_hash = @token_hash,\n        name = @name,\n        capabilities = @capabilities,\n        e2ee_required = 1,\n        e2ee_version = @e2ee_version,\n        revoked_at = NULL\n      WHERE device_id = @device_id\n    `);\n    this.byTokenHashStmt = db.prepare(\"SELECT * FROM devices WHERE token_hash = ?\");\n    this.byIdStmt = db.prepare(\"SELECT * FROM devices WHERE device_id = ?\");\n    this.listStmt = db.prepare(\"SELECT * FROM devices ORDER BY created_at DESC\");\n    this.revokeStmt = db.prepare(\"UPDATE devices SET revoked_at = ? WHERE device_id = ?\");\n    this.touchStmt = db.prepare(\"UPDATE devices SET last_seen_at = ? WHERE device_id = ?\");\n    this.deleteStmt = db.prepare(\"DELETE FROM devices WHERE device_id = ?\");\n    this.deleteRevokedStmt = db.prepare(\"DELETE FROM devices WHERE revoked_at IS NOT NULL\");\n  }\n\n  /**\n   * Record a newly paired device and mint its token.\n   *\n   * The raw token is returned to the caller and never stored — this is the only\n   * moment it exists outside the client.\n   */\n  register(args: {\n    publicKey: string;\n    name?: string | null;\n    preset?: CapabilityPreset;\n    now?: number;\n    /**\n     * The device's Noise static public key, base64, when pairing completed a\n     * handshake. Absent on a plaintext pairing, which stays exactly as it is\n     * today: no key, no pin, no behaviour change.\n     */\n    e2eeStaticPub?: string;\n    e2eeVersion?: number;\n  }): RegisteredDevice {\n    const deviceToken = generateDeviceToken();\n    const capabilities = capabilitiesForPreset(args.preset ?? \"full\");\n    const now = args.now ?? Date.now();\n\n    // A re-pair from the same phone presents the same static key, and the\n    // unique index makes a second row impossible — so this updates rather than\n    // inserts. Doing it as look-up-then-write rather than an upsert because the\n    // existing device_id has to be returned, and because `ON CONFLICT` against\n    // a PARTIAL index needs its WHERE clause repeated, which is a thing to get\n    // wrong for no gain. Safe without a transaction: better-sqlite3 is\n    // synchronous and nothing yields between the two statements.\n    const existing = args.e2eeStaticPub\n      ? (this.byE2eeStaticPubStmt.get(args.e2eeStaticPub) as DeviceRow | undefined)\n      : undefined;\n\n    if (existing) {\n      this.repairStmt.run({\n        device_id: existing.device_id,\n        public_key: args.publicKey,\n        token_hash: hashDeviceToken(deviceToken),\n        name: args.name ?? null,\n        capabilities: JSON.stringify(capabilities),\n        e2ee_version: args.e2eeVersion ?? null,\n      });\n      return { deviceId: existing.device_id, deviceToken, capabilities };\n    }\n\n    const deviceId = randomUUID();\n    this.insertStmt.run({\n      device_id: deviceId,\n      public_key: args.publicKey,\n      token_hash: hashDeviceToken(deviceToken),\n      name: args.name ?? null,\n      capabilities: JSON.stringify(capabilities),\n      created_at: now,\n      e2ee_static_pub: args.e2eeStaticPub ?? null,\n      // The downgrade lock, set in the same write that records the key. Once\n      // set, nothing a client can send clears it (design.md §6.3) — which is\n      // what makes it a lock rather than a preference.\n      e2ee_required: args.e2eeStaticPub ? 1 : 0,\n      e2ee_version: args.e2eeVersion ?? null,\n    });\n\n    return { deviceId, deviceToken, capabilities };\n  }\n\n  /** The device that owns a Noise static key, or null. */\n  getByE2eeStaticPub(staticPub: string): DeviceRow | null {\n    return (this.byE2eeStaticPubStmt.get(staticPub) as DeviceRow | undefined) ?? null;\n  }\n\n  /**\n   * Resolve a presented token to a device, or null.\n   *\n   * Returns null for a revoked device, so revocation takes effect on the very\n   * next request with no cache to go stale.\n   */\n  authenticate(token: string): DeviceRow | null {\n    const hash = hashDeviceToken(token);\n    const row = this.byTokenHashStmt.get(hash) as DeviceRow | undefined;\n    if (!row) return null;\n    // The lookup is already an indexed equality match on a hash; the explicit\n    // constant-time compare guards the value we actually act on.\n    if (!safeHashEquals(row.token_hash, hash)) return null;\n    if (row.revoked_at != null) return null;\n    return row;\n  }\n\n  get(deviceId: string): DeviceRow | null {\n    return (this.byIdStmt.get(deviceId) as DeviceRow | undefined) ?? null;\n  }\n\n  /** All devices, including revoked ones — an audit surface needs the history. */\n  list(): DeviceView[] {\n    return (this.listStmt.all() as DeviceRow[]).map(toDeviceView);\n  }\n\n  /** Revoke one device. Others are untouched — no key rotation, no collateral. */\n  revoke(deviceId: string, now: number = Date.now()): boolean {\n    return this.revokeStmt.run(now, deviceId).changes > 0;\n  }\n\n  /**\n   * Erase one device's record outright.\n   *\n   * Deliberately separate from `revoke`, which is a soft delete that keeps the\n   * row so `list()` can show what happened. That audit trail is the right\n   * default — but it meant a `devices` row, including the user-supplied `name`\n   * (\"Ronen's iPhone\"), had no removal path at all once the registry moved to\n   * runtime.db, which no command deletes. This is that path.\n   *\n   * Erasure is NOT revocation: deleting a row frees its `token_hash`, so a\n   * device whose token is still on a phone somewhere stops being *known* rather\n   * than being *refused*. Revoke first, delete second, is the safe order, and\n   * `deleteRevoked()` exists so that is the easy thing to do.\n   */\n  delete(deviceId: string): boolean {\n    return this.deleteStmt.run(deviceId).changes > 0;\n  }\n\n  /**\n   * Erase every already-revoked device. The bulk companion to `delete`, and the\n   * one that is safe by construction: a revoked device is already refused, so\n   * removing its row cannot restore access to anything.\n   *\n   * Returns the number of rows removed.\n   */\n  deleteRevoked(): number {\n    return this.deleteRevokedStmt.run().changes;\n  }\n\n  touch(deviceId: string, now: number = Date.now()): void {\n    this.touchStmt.run(now, deviceId);\n  }\n}\n","/**\n * Scoped device capabilities (C5).\n * See docs/architecture/2026-07-24-device-identity-and-capabilities.md.\n *\n * Authorization was all-or-nothing: authMiddleware asked one question — is this\n * token the API key — and answered 401 or full access. A device paired merely to\n * glance at session status held exactly the authority of the one driving the\n * agent, because there was only ever one credential and no principal to scope.\n */\n\nexport const CAPABILITIES = [\n  \"history:read\", // read conversations, search\n  \"session:control\", // start, resume, send input, interrupt\n  \"fs:browse\", // browse the project tree\n  \"fs:upload\", // upload files into a project\n  \"notifications\", // register for push\n  \"admin\", // rotate keys, manage devices\n] as const;\n\nexport type Capability = (typeof CAPABILITIES)[number];\n\nexport function isCapability(value: unknown): value is Capability {\n  return typeof value === \"string\" && (CAPABILITIES as readonly string[]).includes(value);\n}\n\n/**\n * Every capability, `admin` included. What a normal driving device gets.\n *\n * It withheld `admin` originally, on the reasoning that a device should run the\n * agent without being able to rotate credentials or revoke its siblings. That is\n * a sound rule for a server shared between people, and this one is not: the\n * phone IS the administration surface. Mobile's paired-devices screen\n * (`GET /api/devices`), backup and restore (`/api/backup/*`) and the model and\n * effort settings (`/api/config/claude-flags`) are all admin-gated, so a device\n * without `admin` loses four working screens the moment it starts presenting its\n * own token instead of the shared key.\n *\n * `read-only` is where the narrowing lives, and it still does.\n */\nexport const FULL_CAPABILITIES: Capability[] = [\n  \"history:read\",\n  \"session:control\",\n  \"fs:browse\",\n  \"fs:upload\",\n  \"notifications\",\n  \"admin\",\n];\n\n/**\n * Read-only monitoring. The mode that makes a \"just show me what's happening\"\n * device safe to pair, and the one C5 explicitly calls for.\n */\nexport const READ_ONLY_CAPABILITIES: Capability[] = [\"history:read\"];\n\nexport type CapabilityPreset = \"full\" | \"read-only\";\n\nexport function capabilitiesForPreset(preset: CapabilityPreset): Capability[] {\n  return preset === \"read-only\" ? [...READ_ONLY_CAPABILITIES] : [...FULL_CAPABILITIES];\n}\n\n/**\n * The principal behind a request.\n *\n * `legacy` is the shared API key: it predates device identity, so it carries the\n * full preset and no device id. Keeping it working is what lets this ship\n * without breaking every already-paired device.\n */\nexport interface Principal {\n  kind: \"device\" | \"legacy\";\n  deviceId?: string;\n  capabilities: Capability[];\n}\n\nexport function legacyPrincipal(): Principal {\n  // The shared API key is the OWNER's credential — it is what pairs new devices\n  // and rotates the key itself. It therefore holds `admin` in addition to the\n  // full preset. Devices are the things that get scoped; the key that mints\n  // them cannot be, or the owner could no longer administer their own server.\n  return { kind: \"legacy\", capabilities: [...FULL_CAPABILITIES] };\n}\n\nexport function hasCapability(principal: Principal, required: Capability): boolean {\n  return principal.capabilities.includes(required);\n}\n\n/**\n * Capability required to reach a route.\n *\n * Matching is longest-prefix so a specific rule beats a general one. Returning\n * `null` means \"no rule\".\n *\n * **The caller ALLOWS in that case — it does not deny.** `authMiddleware` calls\n * `next()` for an unclassified path so an unknown route still 404s, because a\n * 403 would tell an authenticated caller that a path it cannot name might\n * exist. This comment previously claimed the opposite, which mattered: a reader\n * trusting it would believe a route was guarded by omission when it is not.\n *\n * The fail-closed guarantee is therefore a BUILD-TIME one, enforced by\n * `__tests__/capabilities.test.ts` (\"every mounted route is classified\"), which\n * reads both the mount prefixes in `api/app.ts` and the path literals in\n * `api/routes/*.ts`. A new endpoint with no mapping fails that test rather than\n * being denied at runtime. If you change this function's contract, change that\n * test and this comment together — they are the whole guarantee.\n */\nconst ROUTE_CAPABILITIES: ReadonlyArray<[prefix: string, capability: Capability]> = [\n  // Most specific first for readability; matching sorts by length anyway.\n  [\"/api/sessions/\", \"session:control\"],\n  [\"/api/sessions\", \"history:read\"], // listing sessions is a read\n  [\"/api/conversations\", \"history:read\"],\n  [\"/api/projects\", \"history:read\"],\n  [\"/api/search\", \"history:read\"],\n  [\"/api/providers\", \"history:read\"],\n  [\"/api/browse\", \"fs:browse\"],\n  [\"/api/upload\", \"fs:upload\"],\n  [\"/api/push\", \"notifications\"],\n  [\"/api/devices\", \"admin\"],\n  [\"/api/config\", \"admin\"],\n  [\"/api/auth/rotate\", \"admin\"],\n  [\"/api/backup\", \"admin\"],\n  // Server identity and capability discovery. A read-only device must be able\n  // to see WHICH server it is talking to and what it supports, or it cannot\n  // render anything at all.\n  [\"/api/info\", \"history:read\"],\n  [\"/api/profiles\", \"history:read\"],\n  [\"/api/diagnostics\", \"history:read\"],\n  [\"/api/cache/alert\", \"history:read\"],\n  // Client log shipping: any authenticated client may report its own errors.\n  // Gating this behind a capability would silence diagnostics from exactly the\n  // devices most likely to be misbehaving.\n  [\"/api/__client-log\", \"history:read\"],\n  // Logs viewer is localhost-only and already bypasses this middleware; the\n  // mapping exists so a remote request is classified rather than denied as\n  // unclassified.\n  [\"/api/logs\", \"admin\"],\n  // Pairing routes other than the public exchange (e.g. minting a token).\n  [\"/api/pair\", \"admin\"],\n  // The transport handshake. `POST /api/e2ee/open` is public and bypasses this\n  // middleware entirely (`PUBLIC_POST_PATHS`) — the Noise handshake IS the\n  // authentication. The rule exists so a stray method on the same path is\n  // denied by rule rather than by omission, which is what the \"every mounted\n  // route is classified\" test enforces (NONCE-DESIGN §18).\n  [\"/api/e2ee\", \"admin\"],\n  // The live WebSocket. Subscribing is a read — terminal output, session\n  // updates, conversation events. Control still flows through the HTTP input\n  // routes, which carry their own capability check, so a read-only device can\n  // watch a session stream without being able to drive it.\n  [\"/ws\", \"history:read\"],\n  // Progress webhook (multi-agent). Authenticated by HMAC in the handler and\n  // already skipped by the middleware; classified so a stray request is denied\n  // by rule rather than as \"unclassified\".\n  [\"/internal/sessions\", \"admin\"],\n];\n\nexport function requiredCapability(path: string, method: string): Capability | null {\n  // A read of the session list is not the same authority as driving a session.\n  // GET /api/sessions/:id and its sub-resources are reads; writes to them are\n  // control. Without this split a read-only device could not see a session at\n  // all, which would make the mode useless.\n  if (path.startsWith(\"/api/sessions\") && (method === \"GET\" || method === \"HEAD\")) {\n    return \"history:read\";\n  }\n\n  let best: { len: number; cap: Capability } | null = null;\n  for (const [prefix, cap] of ROUTE_CAPABILITIES) {\n    if (path.startsWith(prefix) && (best === null || prefix.length > best.len)) {\n      best = { len: prefix.length, cap };\n    }\n  }\n  return best?.cap ?? null;\n}\n","// Transport contexts: what a completed `/api/e2ee/open` handshake leaves\n// behind, and the only place a `ctxId` ever resolves to keys.\n//\n// specs/end-to-end-encryption/NONCE-DESIGN.md §8 is the design of record here\n// and it supersedes design.md §4.3 on two points that would otherwise have\n// shipped a bug:\n//\n//   1. **Two contexts per device, not one.** A WebSocket context bound to\n//      exactly one socket, and a long-lived REST context. A single device-wide\n//      context that died with the socket would take the 2 s HTTP replay\n//      fallback down with it — that fallback runs *precisely* when the socket\n//      is unavailable — and, worse, a context SHARED by both would\n//      sequence-violate itself into a close loop: frames in flight when a\n//      socket drops are lost, so under the strict counter (§5 R2) the first\n//      frame after a reconnect is a gap, the client closes, reconnects, gaps\n//      again, and never recovers.\n//   2. **No grace window.** A socket's context is destroyed at its close. A\n//      reconnect opens a NEW context — new `ctxId`, new keys, counters\n//      legitimately at 0 — which is not a counter reset: the invariant scopes\n//      uniqueness per context.\n//\n// Contexts are IN-MEMORY ONLY. They do not survive a streamer restart, which is\n// what stops an old capture from ever being replayed into a new run.\n//\n// A key is never REPLACED inside a context (§6): 24 h, 1 GiB and a foreground\n// past threshold all mean \"open a new context and retire the old one\", so there\n// is no rekey here, no `bytesSealed`, and no key generation for a concurrent\n// REST receiver to have to guess at.\n\nimport { type KeyObject, randomBytes } from \"crypto\";\nimport { type DeviceRow, parseCapabilities } from \"../db/repositories/devices.repository\";\nimport type { Principal } from \"../services/security/capabilities\";\nimport type { TrafficKeys } from \"./noise\";\nimport { E2EE_CTX_UNKNOWN, own, redactKeyMaterial, unpooled } from \"./protocol\";\nimport {\n  CHANNEL_REST_REQUEST,\n  CHANNEL_WS,\n  type Channel,\n  CTX_ID_BYTES,\n  createRecordState,\n  DIRECTION_C2S,\n  DIRECTION_S2C,\n  RecordError,\n  type RecordState,\n  RestResponseSealer,\n} from \"./record\";\nimport { RestReceiveWindow } from \"./rest-window\";\n\n/**\n * The header a client presents its single-use WebSocket ticket in.\n *\n * **A header, never a query parameter** (§10). `?ticket=` lands in every ingress\n * access log — Cloudflare logs full request URLs — and single-use plus thirty\n * seconds bounds that damage without removing it. React Native's `WebSocket`\n * takes custom headers, so the ticket never needs to touch a URL at all; a\n * browser, which cannot, uses `Sec-WebSocket-Protocol` instead (below). The\n * property that buys is stronger than redaction: there is nothing to redact,\n * because `http.request` logs a method, a path and a summarised query, never a\n * header.\n *\n * It lives HERE rather than beside the route because `auth.middleware.ts` reads\n * it too — a ticketed upgrade authenticates by its ticket (§13) — and a route\n * module is not something the auth middleware should have to import.\n */\nexport const TICKET_HEADER = \"x-tb-ticket\";\n\n/**\n * The browser spelling of the same ticket: a `Sec-WebSocket-Protocol` offer.\n *\n * A browser `WebSocket` cannot set `X-TB-Ticket`, but `new WebSocket(url,\n * protocols)` sets `Sec-WebSocket-Protocol`, which is still a header — so \"never\n * a query parameter\" holds for browsers too. The client offers exactly\n * `threadbase-e2ee-v1, tb-ticket.<ticket>`; the server consumes the ticket with\n * the header path's semantics and selects ONLY `E2EE_WS_SUBPROTOCOL` in the 101\n * (`mountWebSocket` pins that), because `ws` would otherwise echo the first\n * offer and a browser that offered protocols fails a 101 that selects none.\n * A ticket (22 base64url characters) is a valid RFC 6455 token as it stands.\n */\nexport const E2EE_WS_SUBPROTOCOL = \"threadbase-e2ee-v1\";\nexport const TICKET_SUBPROTOCOL_PREFIX = \"tb-ticket.\";\n\nconst SUBPROTOCOL_TOKEN = /^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$/;\n\n/**\n * `Sec-WebSocket-Protocol` as a list, or `null` where no socket can follow: an\n * offer `ws` would refuse (a non-token or a duplicate), or a `tb-ticket.` offer\n * without `E2EE_WS_SUBPROTOCOL`, for which the 101 selects nothing and a browser\n * drops the socket. Checked BEFORE a ticket is consumed: `ws` parses the header\n * only after the app has answered, so either offer would otherwise spend a\n * ticket, promote its context, and open no socket.\n */\nexport function parseSubprotocols(header: string | undefined): string[] | null {\n  if (header === undefined) return [];\n  const offered = header.split(\",\").map((p) => p.trim());\n  const valid =\n    offered.every((p) => SUBPROTOCOL_TOKEN.test(p)) &&\n    new Set(offered).size === offered.length &&\n    (offered.includes(E2EE_WS_SUBPROTOCOL) ||\n      !offered.some((p) => p.startsWith(TICKET_SUBPROTOCOL_PREFIX)));\n  return valid ? offered : null;\n}\n\n/** §8: a provisional context, and its ticket, die at 30 s. */\nexport const TICKET_TTL_MS = 30_000;\n/** A socket outlives this only if it is still open; it is a backstop. */\nexport const WS_CONTEXT_TTL_MS = 24 * 60 * 60 * 1000;\n/** §8: a REST context is destroyed at 24 h and the client re-opens. */\nexport const REST_CONTEXT_TTL_MS = 24 * 60 * 60 * 1000;\n\n/** §8: cap live contexts per device, evicting by usefulness. */\nexport const MAX_WS_CONTEXTS_PER_DEVICE = 4;\nexport const MAX_REST_CONTEXTS_PER_DEVICE = 2;\n\n/**\n * How long an evicted context keeps answering before it is swept (§8).\n *\n * \"Eviction honours the drain\": a context destroyed the instant its\n * replacement registers kills a request that is in flight on it, which is the\n * opposite of the short drain §6 promises. Ten seconds is far longer than any\n * REST round trip on this product and far shorter than the 30 s provisional\n * TTL, so a drained context never outlives the window it was evicted in.\n * NONCE-DESIGN names the rule and not a number; this is the number.\n */\nexport const CONTEXT_DRAIN_MS = 10_000;\n\nexport type ContextKind = \"ws\" | \"rest\";\n\n/**\n * A fresh context handle: 16 random bytes, base64url, 22 characters.\n *\n * **Server-assigned, never derived** (§12). The earlier\n * `HKDF(h_ss, \"tb-e2ee-ctx-id\", 16)` was fine as a server-side detail and wrong\n * as a contract: it pinned no salt/info/IKM roles, the client has no HKDF of\n * that shape, and since the server returns `ctxId` in msg2 anyway a deriving\n * client would hold a second source of truth to disagree with. It was also\n * circular in practice — the transcript hash a client would derive from is\n * computed over the very payload the `ctxId` has to travel in.\n */\nexport function newCtxId(): { raw: Buffer; id: string } {\n  // UNPOOLED: `ctxId` is public by design and travels on every sealed record,\n  // and a pooled public Buffer exposes the shared 8 KiB block it was carved\n  // from — which is how a registry walk reached live traffic keys without\n  // touching a key-bearing class at all (§13).\n  const raw = unpooled(randomBytes(CTX_ID_BYTES));\n  return { raw, id: raw.toString(\"base64url\") };\n}\n\n/**\n * When a context of this kind, opened now, stops resolving — before it has been\n * used for anything.\n *\n * Every context starts PROVISIONAL and dies at the ticket TTL (§8). An `IK`\n * msg1 carries no freshness, so anyone who captured one valid `/open` msg1 can\n * replay it: each replay passes \"fail closed on the device row\", because the\n * static key genuinely is a known device, and allocates a context and a ticket\n * for two DH and one AEAD. The attacker never gets keys — msg2 needs `D_priv` —\n * so this is pure allocation, the D-9 class, on a public endpoint. Without this\n * rule a socket context whose ticket is never consumed has no end of life at\n * all.\n */\nexport function provisionalExpiresAt(now: number): number {\n  return now + TICKET_TTL_MS;\n}\n\n/** When a context that HAS been used stops resolving. */\nexport function contextExpiresAt(kind: ContextKind, now: number): number {\n  return now + (kind === \"ws\" ? WS_CONTEXT_TTL_MS : REST_CONTEXT_TTL_MS);\n}\n\nexport interface E2eeContext {\n  /** The wire handle: base64url, 22 characters. What `X-TB-Ctx` carries. */\n  readonly ctxId: string;\n  /** The same value as the 16 raw bytes the AAD binds. */\n  readonly ctxIdRaw: Buffer;\n  readonly deviceId: string;\n  readonly kind: ContextKind;\n  readonly createdAt: number;\n  /** Moves out to the full lifetime once the context is first used (§8). */\n  readonly expiresAt: number;\n  /** True until the ticket is consumed or a request unseals under it (§8). */\n  readonly provisional: boolean;\n  /** Set when the context has been evicted and is draining; null otherwise (§8). */\n  readonly retireAt: number | null;\n  /** The moment this context stops resolving: its expiry, or its drain deadline. */\n  deadline(): number;\n  /** The sending half for a channel. Throws for a channel this kind does not carry. */\n  sendState(channel: Channel): RecordState;\n  /** The receiving half for a channel. Throws for a channel this kind does not carry. */\n  receiveState(channel: Channel): RecordState;\n  /** REST only: unseal a request and record its counter as answerable (§13(a)). */\n  unsealRequest(frame: Buffer, target: Buffer): Buffer;\n  /** REST only: seal the one response that request is owed (§13(a)). */\n  sealResponse(requestCounter: bigint, plaintext: Buffer, target: Buffer): Buffer;\n  /** First authenticated use. Promotes out of provisional. */\n  markUsed(now?: number): void;\n}\n\nconst contextInvalidators = new WeakMap<E2eeContext, () => void>();\n\nclass Context implements E2eeContext {\n  readonly ctxId: string;\n  readonly ctxIdRaw: Buffer;\n  readonly deviceId: string;\n  readonly kind: ContextKind;\n  readonly createdAt: number;\n  expiresAt: number;\n  provisional = true;\n  retireAt: number | null = null;\n\n  // Receive state is keyed by (context, CHANNEL) rather than by context alone,\n  // because the two channels have genuinely different rules: strict `expected`\n  // on the WebSocket, a sliding window on REST (§8, design.md §3.4).\n  readonly #send = new Map<Channel, RecordState>();\n  readonly #receive = new Map<Channel, RecordState>();\n  // Not `readonly`: W1b's invalidator nulls this to make destruction real\n  // rather than a map deletion. See `contextInvalidators` below.\n  #responses: RestResponseSealer | null;\n  /**\n   * REST contexts only; `null` on a socket context, and not merely unused\n   * there. A window a `ws` context carried would be an invitation to relax §5\n   * R2 to share an implementation, which is the one change this split exists to\n   * prevent.\n   *\n   * **Deliberately NOT cleared by the invalidator, and `readonly` says so.**\n   * `unsealRequest` opens with `requireRest()`, which throws once `#responses`\n   * is null, so an invalidated context refuses before the window is ever\n   * consulted — there is no path on which a stale window can admit a counter.\n   * It also holds no key material, only a bitmap of counters already seen. The\n   * one thing clearing it would buy is a second place to get destruction\n   * wrong.\n   */\n  readonly #window: RestReceiveWindow | null;\n\n  constructor(args: {\n    ctxIdRaw: Buffer;\n    ctxId: string;\n    deviceId: string;\n    kind: ContextKind;\n    keys: TrafficKeys;\n    now: number;\n  }) {\n    this.ctxIdRaw = unpooled(args.ctxIdRaw);\n    this.ctxId = args.ctxId;\n    this.deviceId = args.deviceId;\n    this.kind = args.kind;\n    this.createdAt = args.now;\n    this.expiresAt = provisionalExpiresAt(args.now);\n\n    const state = (key: KeyObject, direction: Direction, channel: Channel) =>\n      createRecordState({ key, ctxId: this.ctxIdRaw, direction, channel });\n\n    if (args.kind === \"ws\") {\n      this.#send.set(CHANNEL_WS, state(args.keys.serverToClient, DIRECTION_S2C, CHANNEL_WS));\n      this.#receive.set(CHANNEL_WS, state(args.keys.clientToServer, DIRECTION_C2S, CHANNEL_WS));\n      this.#responses = null;\n      this.#window = null;\n    } else {\n      // The REST receive path is WINDOWED, not strict. This state still does\n      // every check a record gets — bounds, version, `ctxId`, direction,\n      // channel, target hash, tag — but its sequence rule is not `expected`:\n      // `unsealRequest` below drives it through `unsealUnchecked` and hands the\n      // authenticated counter to `#window`, an RFC-6479-style 1024-bit bitmap\n      // (design.md §3.4). React Query issues concurrent requests, so an\n      // out-of-order arrival here is a network event and not a protocol\n      // violation.\n      //\n      // The acceptance rule is replaced on THIS state and nothing else: the\n      // WebSocket's strictness (§5 R2) is untouched, and `unsealUnchecked`\n      // refuses every channel but this one so the seam cannot reach it.\n      this.#receive.set(\n        CHANNEL_REST_REQUEST,\n        state(args.keys.clientToServer, DIRECTION_C2S, CHANNEL_REST_REQUEST),\n      );\n      this.#window = new RestReceiveWindow();\n      // Responses have NO counter of their own: each echoes the counter of the\n      // request it answers (§13(a)).\n      this.#responses = new RestResponseSealer({\n        key: args.keys.serverToClient,\n        ctxId: this.ctxIdRaw,\n      });\n    }\n\n    contextInvalidators.set(this, () => {\n      this.#send.clear();\n      this.#receive.clear();\n      this.#responses = null;\n    });\n\n    // Holds record states, which hold traffic keys. Redacted through the same\n    // helper as everything else that carries key material, so a fifth\n    // key-bearing object cannot be added with a fourth private convention.\n    redactKeyMaterial(\n      this,\n      () =>\n        `E2eeContext { ctxId: ${this.ctxId}, kind: ${this.kind}, provisional: ${this.provisional}, keys: <#private, in its record states> }`,\n    );\n  }\n\n  sendState(channel: Channel): RecordState {\n    const s = this.#send.get(channel);\n    if (!s) throw new RecordError(E2EE_CTX_UNKNOWN, `no send state for channel ${channel}`);\n    return s;\n  }\n\n  receiveState(channel: Channel): RecordState {\n    const s = this.#receive.get(channel);\n    if (!s) throw new RecordError(E2EE_CTX_UNKNOWN, `no receive state for channel ${channel}`);\n    return s;\n  }\n\n  unsealRequest(frame: Buffer, target: Buffer): Buffer {\n    const sealer = this.requireRest();\n    // A REST context is built with a window and a sealer together, so\n    // `requireRest()` above has already refused every context that has neither.\n    // This narrows the type; it is not a second opinion about what a REST\n    // context is, and there is no path on which it fires.\n    const window = this.#window;\n    if (!window) throw new RecordError(E2EE_CTX_UNKNOWN, \"not a REST context\");\n    const state = this.receiveState(CHANNEL_REST_REQUEST);\n    // **Authenticate first, then decide (§5 R2 ordering).** The counter the\n    // window judges is the one that came OUT of the AEAD, never one read from a\n    // header beforehand: a pre-authentication check would make\n    // `E2EE_SEQUENCE_VIOLATION` an unauthenticated verdict about the peer and\n    // buys no protection, since the same attacker can as cheaply send garbage\n    // carrying the right counter.\n    //\n    // And it must come from the return value rather than from `state.counter`.\n    // The strict path could read the counter first because `expected` and the\n    // frame's counter are equal by definition there; on the window path they\n    // are not, and the difference is silent — every test still passes for the\n    // first request in a context.\n    const { plaintext, counter } = state.unsealUnchecked(frame, target);\n    // The window owns acceptance: `unsealUnchecked` advances nothing, so a\n    // repeat, an already-received counter or one that has fallen out of the\n    // window is refused HERE, before anything is armed to answer it.\n    window.admit(counter);\n    // Acceptance is recorded ONLY on the success path, which is what makes\n    // §13(a) enforceable rather than a rule a middleware has to remember: a\n    // request the window or the AEAD rejected can never be answered with a\n    // sealed body, because no counter was ever accepted for it.\n    sealer.accept(counter);\n    this.markUsed();\n    return plaintext;\n  }\n\n  sealResponse(requestCounter: bigint, plaintext: Buffer, target: Buffer): Buffer {\n    return this.requireRest().seal(requestCounter, plaintext, target);\n  }\n\n  deadline(): number {\n    return this.retireAt === null ? this.expiresAt : Math.min(this.expiresAt, this.retireAt);\n  }\n\n  /** Evicted: keep answering for the drain, then go (§8). */\n  retire(at: number): void {\n    this.retireAt = this.retireAt === null ? at : Math.min(this.retireAt, at);\n  }\n\n  markUsed(now: number = Date.now()): void {\n    if (!this.provisional) return;\n    this.provisional = false;\n    // Measured from first use, not from the open: a ticket consumed at 29 s\n    // should not leave a socket 29 seconds short of its day.\n    this.expiresAt = contextExpiresAt(this.kind, now);\n  }\n\n  private requireRest(): RestResponseSealer {\n    if (!this.#responses) {\n      throw new RecordError(E2EE_CTX_UNKNOWN, \"not a REST context\");\n    }\n    return this.#responses;\n  }\n}\n\nfunction invalidateContext(context: E2eeContext): void {\n  contextInvalidators.get(context)?.();\n}\n\ntype Direction = typeof DIRECTION_C2S | typeof DIRECTION_S2C;\n\n/** What registry destruction reports to the caller (§8). */\nexport interface DestroyedContexts {\n  /** WS contexts that were still indexed when destruction began. */\n  socketCtxIds: string[];\n  restCtxIds: string[];\n  /** Unconsumed tickets dropped, so a revoked device cannot still upgrade. */\n  tickets: number;\n}\n\n/**\n * Every live context on this process, and the WS tickets bound to them.\n *\n * In-memory by design (§8). One instance per server — `contextRegistry()` below\n * — because the `/api/e2ee/open` route writes to it and the WebSocket upgrade\n * and the REST middleware read from it, and a second registry would mean a\n * context that exists for one of them and not the others.\n */\nexport class E2eeContextRegistry {\n  readonly #contexts = new Map<string, Context>();\n  readonly #byDevice = new Map<string, Set<string>>();\n  /**\n   * Live WS tickets, `#private` for the same reason the traffic keys are.\n   *\n   * A ticket is a credential by §10's own reasoning — single-use, 30 seconds,\n   * and the thing that authorises a socket upgrade — and this table rendered in\n   * full under `inspect(registry, { customInspect: false, showHidden: true })`:\n   * every live ticket in the process, in one call, from the object a\n   * diagnostics dump is most likely to reach for.\n   */\n  readonly #tickets = new Map<string, { ctxId: string; expiresAt: number }>();\n\n  constructor() {\n    // Inspecting the registry used to print every live context's traffic keys\n    // in ONE call — the widest version of the same leak, and the reason this is\n    // redacted at the container as well as at the leaves.\n    redactKeyMaterial(\n      this,\n      () =>\n        `E2eeContextRegistry { contexts: ${this.#contexts.size}, devices: ${this.#byDevice.size}, tickets: ${this.#tickets.size} }`,\n    );\n  }\n\n  /** Live contexts. For tests and for a future diagnostics line — never a key. */\n  get size(): number {\n    return this.#contexts.size;\n  }\n\n  /** Unconsumed, unexpired tickets. Tests only. */\n  get ticketCount(): number {\n    return this.#tickets.size;\n  }\n\n  open(args: {\n    deviceId: string;\n    kind: ContextKind;\n    ctxIdRaw: Buffer;\n    ctxId: string;\n    keys: TrafficKeys;\n    now?: number;\n  }): E2eeContext {\n    // Off the ARGUMENT: a polluted `Object.prototype.now` in the past makes\n    // every provisional context outlive the 30 s TTL that is §8's whole bound.\n    const now = own(args, \"now\") ?? Date.now();\n    // Sweep this device's dead contexts FIRST, and the ticket table with them.\n    //\n    // Nothing else collects them. `get()` prunes only the one `ctxId` it was\n    // asked for, so a context nobody ever looks up — which is exactly what a\n    // replayed msg1 produces, since the attacker cannot read msg2 and so never\n    // presents the ticket — stayed in both maps for good. The cap `retire()`d\n    // the older ones and they lingered anyway: memory growing without ceiling,\n    // defeating the very bound §8 wrote the provisional TTL to provide.\n    this.sweepDevice(args.deviceId, now);\n    this.sweepTickets(now);\n\n    // §8: cap per device, and **the context being opened is never an eviction\n    // candidate**. It is provisional by definition, so a naive \"provisional\n    // first\" ordering sorted it to the front of its own queue and the open\n    // evicted itself: a device holding four live sockets got a fifth that died\n    // at the drain deadline and could never open a usable one — consuming the\n    // ticket could not save it, because `deadline()` is\n    // `min(expiresAt, retireAt)`. The candidates are therefore computed from\n    // the OTHER live contexts, before the new one is inserted.\n    //\n    // A re-open storm after a restart, or a foreground racing a silence-timer\n    // reconnect, both legitimately produce a second context — the newer wins\n    // and the older is retired here rather than accumulating. Not a\n    // replace-on-open: two sockets briefly overlapping during a reconnect is\n    // ordinary, and destroying the live one would turn a reconnect into a\n    // failure of the connection it was replacing.\n    //\n    // **Eviction is by usefulness, not by age.** Sorting on `createdAt` alone\n    // picks the context that has been serving traffic all session and keeps two\n    // opened a second ago and never used — precisely inverted in the case that\n    // matters, a replay storm. Provisional (never authenticated) goes first;\n    // only then the oldest live one.\n    //\n    // And it honours the drain: the victim is marked for deletion at\n    // `now + CONTEXT_DRAIN_MS` rather than destroyed under a request already in\n    // flight on it. A draining context no longer counts against the cap, so the\n    // cap bounds *live* contexts and the overhang is bounded by the drain.\n    const cap = args.kind === \"ws\" ? MAX_WS_CONTEXTS_PER_DEVICE : MAX_REST_CONTEXTS_PER_DEVICE;\n    const candidates = (this.forDevice(args.deviceId) as Context[])\n      .filter((c) => c.kind === args.kind && c.retireAt === null)\n      .sort((a, b) => {\n        if (a.provisional !== b.provisional) return a.provisional ? -1 : 1;\n        return a.createdAt - b.createdAt;\n      });\n    // `+ 1` is the context about to be inserted: it counts against the cap, and\n    // is not in the list it could be evicted from.\n    for (const evicted of candidates.slice(0, Math.max(0, candidates.length + 1 - cap))) {\n      evicted.retire(now + CONTEXT_DRAIN_MS);\n    }\n\n    const context = new Context({\n      ctxIdRaw: args.ctxIdRaw,\n      ctxId: args.ctxId,\n      deviceId: args.deviceId,\n      kind: args.kind,\n      keys: args.keys,\n      now,\n    });\n    this.#contexts.set(context.ctxId, context);\n    let ids = this.#byDevice.get(args.deviceId);\n    if (!ids) {\n      ids = new Set();\n      this.#byDevice.set(args.deviceId, ids);\n    }\n    ids.add(context.ctxId);\n    return context;\n  }\n\n  /**\n   * Resolve a `ctxId`, or `null` for one that is unknown, expired, or lost to a\n   * restart — all three of which the caller reports as `E2EE_CTX_UNKNOWN`,\n   * because all three are recoverable by one transparent re-handshake and none\n   * of them is a revocation the client must surface (§9).\n   *\n   * The map lookup IS the first thing that runs, and nothing is allocated on\n   * the way to a rejection (§10). This is called before authentication, on a\n   * value an attacker chose.\n   */\n  get(ctxId: string, now: number = Date.now()): E2eeContext | null {\n    const context = this.#contexts.get(ctxId);\n    if (!context) return null;\n    if (now >= context.deadline()) {\n      this.destroy(ctxId);\n      return null;\n    }\n    return context;\n  }\n\n  /** Every live context for a device. */\n  forDevice(deviceId: string): E2eeContext[] {\n    const ids = this.#byDevice.get(deviceId);\n    if (!ids) return [];\n    const out: E2eeContext[] = [];\n    for (const id of ids) {\n      const context = this.#contexts.get(id);\n      if (context) out.push(context);\n    }\n    return out;\n  }\n\n  /**\n   * Destroy one context and any ticket bound to it.\n   *\n   * The socket's close calls this for its own context and NOTHING else: a\n   * device's REST context is unaffected by its socket going away, which is the\n   * whole reason there are two (§8).\n   */\n  destroy(ctxId: string): boolean {\n    // Tickets go FIRST and unconditionally. `/api/e2ee/open` issues a ticket\n    // before it can register the context — the traffic keys do not exist until\n    // msg2 has been written — so its failure path calls this for a `ctxId` the\n    // map has never seen. Returning early there would leave a live ticket\n    // bound to a context that will never exist, which is precisely what that\n    // call site calls this to prevent.\n    for (const [ticket, entry] of this.#tickets) {\n      if (entry.ctxId === ctxId) this.#tickets.delete(ticket);\n    }\n    const context = this.#contexts.get(ctxId);\n    if (!context) return false;\n    invalidateContext(context);\n    this.#contexts.delete(ctxId);\n    this.#byDevice.get(context.deviceId)?.delete(ctxId);\n    return true;\n  }\n\n  /**\n   * Release one exact context owner without deleting a replacement that reused\n   * the same identifier.\n   */\n  destroyOwned(context: E2eeContext): boolean {\n    if (this.#contexts.get(context.ctxId) !== context) {\n      invalidateContext(context);\n      return false;\n    }\n    return this.destroy(context.ctxId);\n  }\n\n  /**\n   * Destroy every context for a device and report what the caller must finish.\n   *\n   * `POST /api/devices/:id/revoke` calls this before the socket owner closes\n   * every hub reference for the device (design.md §4.4, point 3). The returned\n   * ids are accounting, not the close list: a drained context can already be\n   * absent here while its socket is still attached to the hub.\n   */\n  destroyDevice(deviceId: string): DestroyedContexts {\n    const out: DestroyedContexts = { socketCtxIds: [], restCtxIds: [], tickets: 0 };\n    for (const context of this.forDevice(deviceId)) {\n      out.tickets += this.ticketsFor(context.ctxId);\n      if (this.destroy(context.ctxId)) {\n        (context.kind === \"ws\" ? out.socketCtxIds : out.restCtxIds).push(context.ctxId);\n      }\n    }\n    this.#byDevice.delete(deviceId);\n    return out;\n  }\n\n  /**\n   * Mint a single-use, 30-second WS ticket bound to a `ctxId`.\n   *\n   * Issued INSIDE the encrypted msg2 payload, so the long-term credential never\n   * appears in a URL again — and §10 asks the client to carry it in a WebSocket\n   * header rather than a query parameter, because a URL lands in every ingress\n   * access log.\n   *\n   * Deliberately independent of whether the context is registered yet:\n   * `/api/e2ee/open` has to name the ticket in the payload it is about to seal,\n   * and the traffic keys the context needs only exist once that message has\n   * been written. A ticket whose context never materialised resolves to a\n   * `ctxId` the registry does not know, which is the ordinary\n   * `E2EE_CTX_UNKNOWN` path.\n   */\n  issueTicket(ctxId: string, now: number = Date.now()): string {\n    this.sweepTickets(now);\n    // 16 bytes → exactly 22 base64url characters, the §12 encoding.\n    const ticket = randomBytes(16).toString(\"base64url\");\n    this.#tickets.set(ticket, { ctxId, expiresAt: now + TICKET_TTL_MS });\n    return ticket;\n  }\n\n  /**\n   * Spend a ticket. Returns its `ctxId` exactly once; every later call — and\n   * every concurrent one, since this is synchronous and Node runs it to\n   * completion — gets `null`.\n   *\n   * Consuming a ticket IS the socket context's first authenticated use, so it\n   * promotes the context out of provisional (§8).\n   */\n  consumeTicket(ticket: string, now: number = Date.now()): string | null {\n    const entry = this.#tickets.get(ticket);\n    if (!entry) return null;\n    // Deleted whether or not it was still valid: a presented ticket is spent.\n    this.#tickets.delete(ticket);\n    if (now >= entry.expiresAt) return null;\n    this.#contexts.get(entry.ctxId)?.markUsed(now);\n    return entry.ctxId;\n  }\n\n  /** Drop everything. A streamer restart does this by existing; tests need a call. */\n  clear(): void {\n    for (const context of this.#contexts.values()) invalidateContext(context);\n    this.#contexts.clear();\n    this.#byDevice.clear();\n    this.#tickets.clear();\n  }\n\n  private ticketsFor(ctxId: string): number {\n    let n = 0;\n    for (const entry of this.#tickets.values()) if (entry.ctxId === ctxId) n++;\n    return n;\n  }\n\n  /** Drop every context of one device whose deadline has passed. */\n  private sweepDevice(deviceId: string, now: number): void {\n    for (const context of this.forDevice(deviceId)) {\n      if (now >= context.deadline()) this.destroy(context.ctxId);\n    }\n  }\n\n  private sweepTickets(now: number): void {\n    for (const [ticket, entry] of this.#tickets) {\n      if (now >= entry.expiresAt) this.#tickets.delete(ticket);\n    }\n  }\n}\n\n// One registry per process. `/api/e2ee/open` writes to it; the WebSocket\n// upgrade and the REST middleware (both later PRs) read from it. It is a module\n// singleton rather than an `ApiDeps` field because contexts are process-local\n// by definition and threading it through the server wiring would put a\n// non-serialisable, restart-scoped object into the dependency record for no\n// gain — the route factory takes an override so a test uses its own.\nlet shared: E2eeContextRegistry | null = null;\n\nexport function contextRegistry(): E2eeContextRegistry {\n  if (!shared) shared = new E2eeContextRegistry();\n  return shared;\n}\n\nexport interface E2eeRequiredRefusal {\n  status: 426;\n  body: { error: string; code: \"E2EE_REQUIRED\" };\n}\n\n/**\n * The 426 answer, in ONE place.\n *\n * A device that has once paired encrypted is pinned (`e2ee_required`), and a\n * pinned device must never be served plaintext: it gets `426`, never a `401`\n * and never a plaintext answer (design.md §6.3, §8). The WebSocket upgrade and\n * the REST unseal middleware are both later PRs and both consume this — neither\n * re-implements it, because two copies of a downgrade rule is one copy that can\n * be forgotten.\n *\n * Returns `null` when the request is fine: it was sealed, or the caller is not\n * a pinned device. An unpinned device and the legacy shared key keep working\n * exactly as they do today.\n *\n * **The limit, stated rather than discovered later.** The pin is per DEVICE, so\n * this can only enforce it against a caller that resolved to a device\n * principal. A pinned phone that presents the SHARED api key resolves to\n * `legacy` — indistinguishable from the owner's laptop — and is let through\n * here. Closing that is the WebSocket upgrade's job, where `?key=` and a ticket\n * are separable, and the REST middleware's, where a pinned device has an\n * `X-TB-Ctx` to be absent.\n */\nexport function refuseUnsealedIfPinned(args: {\n  principal: Principal | null | undefined;\n  /** Just the lookup — the caller already holds the repository. */\n  devicesRepo: { get(deviceId: string): DeviceRow | null } | null | undefined;\n  /** The context this request resolved to; `null` means the request was plaintext. */\n  context: E2eeContext | null | undefined;\n}): E2eeRequiredRefusal | null {\n  if (args.context) return null;\n\n  const deviceId = args.principal?.kind === \"device\" ? args.principal.deviceId : undefined;\n  if (!deviceId) return null;\n\n  // **Fail closed from here down.** This used to end in `row?.e2ee_required !== 1`\n  // over `devicesRepo?.get(id) ?? null`, which answered \"serve it in the clear\"\n  // for a null repository, a missing row, and every value other than the exact\n  // number 1 — `true`, `\"1\"`, `undefined`. A pinned device got plaintext in all\n  // of them, from the one guard two other tracks consume for exactly this\n  // decision. A downgrade guard that defaults to allowing the downgrade is not\n  // a guard.\n  //\n  // So: unpinned is a POSITIVE answer that the store must give. Anything else —\n  // no store, an unreadable store, no row, a value that is not the number 0 —\n  // is a refusal.\n  let row: DeviceRow | null;\n  try {\n    row = args.devicesRepo?.get(deviceId) ?? null;\n  } catch {\n    // An unreadable registry cannot prove this device is unpinned.\n    return refusal();\n  }\n  if (!args.devicesRepo || !row) return refusal();\n  // `Object.hasOwn`, then the value. Reading `row.e2ee_required` bare is a\n  // prototype-chain read — the last one left in `src/e2ee/*`, and it is in the\n  // downgrade guard: `Object.prototype.e2ee_required = 0` would answer \"unpinned\"\n  // for every device whose row does not carry the column. Absent still refuses.\n  if (!Object.hasOwn(row, \"e2ee_required\")) return refusal();\n  return row.e2ee_required === 0 ? null : refusal();\n}\n\nfunction refusal(): E2eeRequiredRefusal {\n  return {\n    status: 426,\n    body: {\n      error:\n        \"This device is paired for end-to-end encryption and cannot be served in the clear. \" +\n        \"Open an encrypted context with POST /api/e2ee/open and retry.\",\n      code: \"E2EE_REQUIRED\",\n    },\n  };\n}\n\n/**\n * What a caller needs from `devicesRepo` to authenticate a context.\n *\n * Structural, not the concrete repository: the two callers hold it through\n * different dependency records, and a narrow shape is also what lets a test\n * drive the refusals without a database.\n */\nexport interface DeviceLookup {\n  get(deviceId: string): DeviceRow | null;\n  authenticate(credential: string): DeviceRow | null;\n}\n\n/**\n * A `Principal` with its `deviceId` present.\n *\n * `Principal.deviceId` is optional, because a `legacy` principal has none. A\n * context always names a device, so this is the honest return type — and it is\n * what lets a caller read `principal.deviceId` without a cast or a `?.`, which\n * matters because that field is the one the invariant is stated over.\n */\nexport type DevicePrincipal = Principal & { kind: \"device\"; deviceId: string };\n\n/**\n * The verdict `authenticateContext` returns. **Frozen at W1b's merge**: the\n * REST unseal middleware is built against this exact text, so a variant renamed\n * here is a coordinated change in two tracks, not a refactor.\n *\n * A failure carries a `reason` and nothing else — no status, no body, no\n * message. The two callers answer the same verdict differently: REST maps it to\n * an HTTP status, the WebSocket upgrade maps it to a close reason. Putting HTTP\n * policy inside a two-consumer helper is how one caller's answer silently\n * becomes the other's.\n *\n * The reasons, and what each one covers — stated because each is a fail-open\n * path a consumer cannot close from its own side once this freezes:\n *\n * - **`device-revoked`** — the context names a device whose row is **missing**\n *   OR whose `revoked_at` is set. §10: absent is not the same as invalid, and\n *   neither is success. Do not read \"revoked\" narrowly and add a row-not-found\n *   success path; a context whose device has vanished authenticates nobody.\n * - **`credential-mismatch`** — a credential was presented beside the context\n *   and does not name the context's device. **Including one that names NO\n *   device**: the shared API key is a mismatch, not an exemption. \"Names\n *   another device\" read literally would exclude the case that matters most.\n * - **`no-device-store`** — there is no device registry, or reading it threw.\n *   A refusal, never a success: *a downgrade guard that defaults to allowing\n *   the downgrade is not a guard.*\n *\n *   **Its own arm, deliberately, and not folded into `device-revoked`.** The\n *   two are not the same fact: \"this device is revoked\" is a statement about\n *   the DEVICE, and \"I could not consult the store\" is a statement about US.\n *   Collapsing them tells the caller — and every log built from the caller —\n *   that a device was revoked when what actually happened is that our own\n *   storage was unreadable. That is precisely the defect §9 split\n *   `E2EE_SEAL_FAILED` from `E2EE_SEQUENCE_VIOLATION` to prevent: a\n *   server-side fault reported as a claim about the peer. Both arms still\n *   refuse and both are terminal, so nothing about behaviour changes — only\n *   what the caller is told, which is the entire point.\n *\n * \"No such context\" is deliberately NOT a reason. Both callers resolve the\n * context before calling, so a not-found is theirs to answer — and taking the\n * resolved object also closes a race the `ctxId` shape had, where a context\n * could expire between the caller's own lookup and the helper's.\n */\nexport type E2eeContextAuth =\n  | { ok: true; principal: DevicePrincipal }\n  | { ok: false; reason: \"device-revoked\" | \"credential-mismatch\" | \"no-device-store\" };\n\n/**\n * Turn a resolved context into the principal the request runs as — or refuse.\n *\n * **The tail every sealed entry point shares**, in one place because it has two\n * callers: the WebSocket upgrade resolves its context from a single-use\n * `X-TB-Ticket`, the REST unseal middleware resolves its from `X-TB-Ctx`, and\n * from that point on the decision is identical. Forking a copy is how the two\n * would come to disagree about which of them re-checks `revoked_at`.\n *\n * The property it exists to create, stated so a test can assert it:\n *\n * > **A context-attached connection's `principal.deviceId` always equals its\n * > `context.deviceId`**, and a credential presented beside a context must name\n * > that same device or the connection is refused.\n *\n * The principal is therefore built from the CONTEXT's own device row and never\n * from the credential — there is no path here on which the two can differ,\n * which is stronger than checking that they agree.\n *\n * Three refusals, all fail-closed:\n *\n *   - **no row, or `revoked_at` set** → `403 E2EE_DEVICE_REVOKED`. Re-checked\n *     per connection rather than trusted from the handshake: a device revoked\n *     between `/api/e2ee/open` and its next request holds a handle that is\n *     still valid on its face (§10). Absent and revoked are the same refusal\n *     and neither is success;\n *   - **a credential naming another device** → `401`. \"Header device ≠ context\n *     device\" must not be undefined behaviour at a trust boundary: two answers\n *     to \"who is this\" is not a request to resolve by preferring one;\n *   - **the SHARED api key beside a context** → `401` too. It names no device,\n *     so it is a mismatch rather than an exemption — the one reading of this\n *     rule that would otherwise let the stage-3 shared-key problem through a\n *     door that had just been closed.\n *\n * **This function has no side effects: it returns a verdict and never applies\n * one.** The rule its callers follow, which is the reason and not merely the\n * behaviour:\n *\n * > **Destroy a context only when the trigger is a fact in our own database\n * > that an attacker cannot forge — `revoked_at`. Never on a mismatched\n * > credential, which is an attacker-supplied header.**\n *\n * `X-TB-Ctx` carries the `ctxId` in a PLAINTEXT header on every sealed request,\n * so it is visible to exactly the on-path party this design assumes exists.\n * Destroying on a mismatch would let anyone who sees one request kill that\n * device's context over and over: forge a credential beside the observed id,\n * watch the victim re-open, read the new id, repeat. The safeguard becomes the\n * weapon.\n *\n * The property has to hold at ANY placement on EITHER channel, so it is this\n * function's job not to destroy rather than each caller's job to remember. A\n * refusal reports its `reason`, and a caller destroys on `device-revoked` and\n * on nothing else.\n */\nexport function authenticateContext(args: {\n  /** Already resolved by the caller — from a spent ticket, or from `X-TB-Ctx`. */\n  context: E2eeContext;\n  devicesRepo: DeviceLookup | null | undefined;\n  /**\n   * The credential presented beside the context, if any.\n   *\n   * `undefined` is the ORDINARY case and an ordinary success: §13(b) says a\n   * sealed REST request carries no `Authorization` at all, and a ticketed\n   * upgrade carries none either. The mismatch check is conditional on a\n   * credential being present; its absence is never a special case.\n   */\n  presented: string | undefined;\n}): E2eeContextAuth {\n  // Never throws out of here. A store that is missing, or that throws on read,\n  // cannot prove this device is live, and \"could not check\" must not resolve to\n  // \"let it through\" — the same rule `refuseUnsealedIfPinned` learned.\n  if (!args.devicesRepo) return { ok: false, reason: \"no-device-store\" };\n\n  // **Every read of the store is inside this `try`, including the reads OF the\n  // row it returns.** `DeviceLookup` is a structural contract another\n  // repository implements, so a conforming-but-hostile lookup is the ordinary\n  // case to survive, not an exotic one: a row whose `revoked_at` is an accessor\n  // that throws puts the throw at the property read, not at the call. A `try`\n  // around only `get()` leaves that read bare, and \"never throws\" then holds\n  // against the store we happen to have rather than against the contract we\n  // published.\n  let live: { deviceId: string; capabilities: string } | null;\n  try {\n    const row: DeviceRow | null = args.devicesRepo.get(args.context.deviceId) ?? null;\n    // Absent and revoked are the same refusal, and neither is success (§10).\n    // `Object.hasOwn` before the value, like its sibling: a row that does not\n    // carry the column must not read as live through a prototype-chain hit.\n    // Both the `hasOwn` and the value read are inside the `try`.\n    live =\n      !row || !Object.hasOwn(row, \"revoked_at\") || row.revoked_at != null\n        ? null\n        : { deviceId: row.device_id, capabilities: row.capabilities };\n  } catch {\n    return { ok: false, reason: \"no-device-store\" };\n  }\n  if (!live) return { ok: false, reason: \"device-revoked\" };\n\n  if (args.presented !== undefined) {\n    let namedDeviceId: string | undefined;\n    try {\n      namedDeviceId = args.devicesRepo.authenticate(args.presented)?.device_id;\n    } catch {\n      return { ok: false, reason: \"no-device-store\" };\n    }\n    // The credential must name the CONTEXT's device. Compared against the\n    // context and not against the row, for the same reason the principal is\n    // built from the context below.\n    //\n    // A credential naming NO device — the shared API key — lands here too. It\n    // is a mismatch, not an exemption.\n    if (namedDeviceId !== args.context.deviceId) {\n      return { ok: false, reason: \"credential-mismatch\" };\n    }\n  }\n\n  // **The invariant: `principal.deviceId === context.deviceId`, by\n  // CONSTRUCTION.**\n  //\n  // The id comes from the context itself, never from the row and never from the\n  // credential. §13 claims \"there is no path on which the two can differ, which\n  // is stronger than checking that they agree\" — and while this read\n  // `row.device_id` that claim was only true as long as\n  // `get(id).device_id === id`, which is a property of whichever store is\n  // plugged in rather than of this function. A conforming `DeviceLookup` that\n  // returns a row for a different device was enough to break it. The row is\n  // still what proves the device is live and still supplies the capabilities;\n  // it just does not get to name who this is.\n  return {\n    ok: true,\n    principal: {\n      kind: \"device\",\n      deviceId: args.context.deviceId,\n      capabilities: parseCapabilities(live.capabilities),\n    },\n  };\n}\n","import { createSecretKey, type KeyObject } from \"crypto\";\n\n// The two things every E2EE module and both repositories have to agree on: the\n// envelope version, and the four rejection codes.\n//\n// They live here rather than in a route module because the record layer imports\n// them, and `record.ts` importing `api/routes/misc.routes.ts` would invert the\n// dependency direction — a crypto primitive would pull in Hono, the push\n// repository and the update config to learn the number `1`\n// (specs/end-to-end-encryption/NONCE-DESIGN.md §4).\n\n/**\n * Envelope version this build speaks.\n *\n * The single canonical copy. It is the `version` byte of every record's AAD\n * (NONCE-DESIGN §4), the `v` of `/api/pair/exchange`'s and `/api/e2ee/open`'s\n * `e2ee` field, and the `version` reported by `GET /api/info`.\n *\n * There were two hand-synced copies of this number — `E2EE_PROTOCOL_VERSION` in\n * `api/routes/misc.routes.ts` and `E2EE_EXCHANGE_VERSION` in `pair-request.ts`,\n * kept equal by a comment. W1a collapsed them into this one rather than adding\n * a third: a version that can disagree with itself is a version that eventually\n * does, and the failure (\"decryption failed\") points nowhere near the cause.\n */\nexport const E2EE_PROTOCOL_VERSION = 1;\n\n/**\n * The four rejection codes, FROZEN at W1a's tag (NONCE-DESIGN §9).\n *\n * tb-mobile consumes these strings. Renaming one is a coordinated change in\n * both repositories, not a refactor.\n *\n * The distinctions are the point, and each one was chosen because collapsing it\n * would tell a client the wrong thing:\n *\n * - `E2EE_CTX_UNKNOWN` is **recoverable**. The context is unknown, expired, or\n *   was lost to a streamer restart. The client re-handshakes once and retries.\n * - `E2EE_DEVICE_REVOKED` is a **hard failure**. Surface it; never retry.\n *   \"Absent\" and \"invalid\" are different answers, and a restart the client can\n *   silently recover from must never look like a revocation it must surface.\n * - `E2EE_SEQUENCE_VIOLATION` is a **claim about the peer**: a repeat, a gap or\n *   a reorder on a channel where none of the three is possible. It is the\n *   WebSocket close reason, so a client can tell a policy close from a drop.\n * - `E2EE_SEAL_FAILED` is a **server-side fault** — it could not seal or unseal\n *   a frame it should have been able to. Deliberately not the same code as a\n *   sequence violation: collapsing them tells a client its own frames were\n *   wrong when the server was at fault, and two failures behind one code was a\n *   P1 in the prior program.\n */\nexport const E2EE_CTX_UNKNOWN = \"E2EE_CTX_UNKNOWN\";\nexport const E2EE_DEVICE_REVOKED = \"E2EE_DEVICE_REVOKED\";\nexport const E2EE_SEQUENCE_VIOLATION = \"E2EE_SEQUENCE_VIOLATION\";\nexport const E2EE_SEAL_FAILED = \"E2EE_SEAL_FAILED\";\n\nexport type E2eeRejectionCode =\n  | typeof E2EE_CTX_UNKNOWN\n  | typeof E2EE_DEVICE_REVOKED\n  | typeof E2EE_SEQUENCE_VIOLATION\n  | typeof E2EE_SEAL_FAILED;\n\n/** Every frozen code, for a test that asserts the set has not drifted. */\nexport const E2EE_REJECTION_CODES: readonly E2eeRejectionCode[] = [\n  E2EE_CTX_UNKNOWN,\n  E2EE_DEVICE_REVOKED,\n  E2EE_SEQUENCE_VIOLATION,\n  E2EE_SEAL_FAILED,\n];\n\n/**\n * Install a readable `util.inspect` summary on an object that holds key\n * material.\n *\n * **This is for legibility, not for secrecy.** The keys themselves are\n * ECMAScript `#private` fields, which are not properties at all: invisible to\n * `showHidden`, to `customInspect: false`, to `Object.getOwnPropertyDescriptors`,\n * to spread and to `structuredClone`. That is what makes them safe against a\n * rendering mode nobody thought of.\n *\n * The history is the argument. Hiding rested first on\n * `defineProperty(enumerable: false)` — beaten by `{ showHidden: true }` — and\n * then on this handler — beaten by `{ customInspect: false }`. Together the two\n * flags rendered every traffic key in the process, and on the context registry\n * that was every live context's key in one call. Three defeats of the same\n * approach is the approach being wrong, so secrecy moved into the language and\n * this stayed only to keep a dump readable.\n *\n * Summaries print SHAPE — lengths, counts, counters — and never bytes, not even\n * a prefix.\n */\nexport function redactKeyMaterial(target: object, summary: () => string): void {\n  Object.defineProperty(target, Symbol.for(\"nodejs.util.inspect.custom\"), {\n    enumerable: false,\n    value: summary,\n  });\n}\n\n/**\n * Read an optional argument from the ARGUMENT, never from the prototype chain.\n *\n * `args.x ?? fallback` and `args.x || fallback` both walk the prototype chain,\n * so a single `Object.prototype.x = …` anywhere in the process — a dependency's\n * bad day, a test helper, a JSON parse into a bare object — silently supplies a\n * value that no caller passed. On this module that is not a nuisance: a\n * polluted `prologue` collapses §11's domain separation and lets a pairing\n * message be read as an `/open` one, and a polluted `ephemeral` pins every\n * handshake to one attacker-chosen `e`, which by §8's own rule is definitionally\n * a replay.\n *\n * `Object.hasOwn` asks the object and nothing above it.\n */\nexport function own<T, K extends keyof T>(args: T, key: K): T[K] | undefined {\n  return Object.hasOwn(args as object, key as PropertyKey) ? args[key] : undefined;\n}\n\n/**\n * The one byte-length guard, used for every fixed-width secret and handle.\n *\n * **`.length` is not byte length for a typed array.** `new Float64Array(32)`\n * has `.length === 32` and `byteLength === 256`, so a `psk.length !== 32` check\n * accepted it and completed a full `IKpsk1` handshake binding 256 zero bytes —\n * a binding over a constant, which is exactly what §11 rejected, reached\n * through a guard rather than a decision. A 32-character string passes a\n * `.length` check too and is not bytes at all.\n *\n * So the check is: a real byte array (`Uint8Array`, which `Buffer` extends),\n * one byte per element, and exactly the expected `byteLength`.\n *\n * The predicate narrows to `Uint8Array`, NOT to `Buffer`. A `Buffer` is one\n * kind of byte view; a client built on `@stablelib` hands over plain\n * `Uint8Array`s, and §13 says that client calls the AAD builder directly. While\n * this said `value is Buffer`, a correct-length plain view passed the guard and\n * then died inside the builder on `target.copy is not a function` — a\n * `TypeError` outside §9's taxonomy, for input that was right.\n *\n * The thrower is a parameter because each module owns its error type and its\n * rejection code; a shared error class here would flatten `RecordError`'s codes\n * into something no caller can act on.\n */\nexport function isBytes(value: unknown, length: number): value is Uint8Array {\n  return (\n    value instanceof Uint8Array && value.BYTES_PER_ELEMENT === 1 && value.byteLength === length\n  );\n}\n\nexport function assertBytes(\n  value: unknown,\n  length: number,\n  name: string,\n  fail: (message: string) => Error,\n): asserts value is Uint8Array {\n  if (!isBytes(value, length)) {\n    throw fail(`${name} must be exactly ${length} bytes`);\n  }\n}\n\n/**\n * A copy that shares no allocation with anything else.\n *\n * `Buffer.allocUnsafe`, `Buffer.from(string)` and `Buffer.concat` all carve out\n * of Node's shared 8 KiB pool, and a Buffer's `.buffer`/`byteOffset` expose that\n * whole pool. So a PUBLIC buffer allocated near a secret one hands the secret to\n * anyone who walks it — which is how the context registry printed live traffic\n * keys through `ctxIdRaw`, without touching a key-bearing class at all.\n * `#private` closes nothing when a private buffer shares an allocation with a\n * public one.\n *\n * `allocUnsafeSlow` allocates outside the pool, so a public buffer made this way\n * neighbours nothing.\n */\nexport function unpooled(source: Uint8Array): Buffer {\n  const copy = Buffer.allocUnsafeSlow(source.byteLength);\n  copy.set(source);\n  return copy;\n}\n\n/**\n * Import secret bytes into a `KeyObject` and wipe the JS-heap copy.\n *\n * A `KeyObject`'s material lives in OpenSSL's memory, not on the JS heap: there\n * is no property to render, no pool to walk, and nothing for a serializer to\n * reach. `createCipheriv`/`createDecipheriv` take one directly, so the bytes\n * never need to come back.\n *\n * The intermediate copy is unpooled and zeroed immediately, so the window in\n * which the key exists as JS-visible bytes is this function's body.\n */\nexport function importSecret(bytes: Uint8Array): KeyObject {\n  const copy = unpooled(bytes);\n  try {\n    return createSecretKey(copy);\n  } finally {\n    copy.fill(0);\n  }\n}\n","// The record layer: one sealed frame in, one plaintext out.\n//\n// Written against specs/end-to-end-encryption/NONCE-DESIGN.md, which is the\n// design of record and outranks design.md wherever the two disagree. Every rule\n// below names the section it comes from, because \"why is it done this way\" must\n// be answerable without re-deriving the protocol.\n//\n// Four things this file deliberately does NOT do (NONCE-DESIGN §14):\n//\n//   - it does not reuse `chachaNonce` from noise.ts. That is Noise's own nonce\n//     encoding (4 zero bytes then a LITTLE-endian counter, spec §12.3); this\n//     layer's is `direction(4) || counter(8)` BIG-endian. Two layers, two\n//     encodings, both correct for their own specification;\n//   - it does not build on `CipherState`. That object's `n` belongs to the\n//     handshake and is reset by every `MixKey`; the record layer owns a counter\n//     that is never reset for any reason;\n//   - it takes no counter from a caller. `seal(plaintext)` and `unseal(frame)`\n//     are the only two ways the counter moves, so there is exactly one place\n//     the invariant can be broken instead of one per call site (§5 R4). The one\n//     sanctioned exception is `RestResponseSealer`, below, whose \"counter\" is\n//     an echo of a request that was already accepted rather than a sequence\n//     the sender chooses;\n//   - it has no `rekey()`. A key is never replaced inside a context: a new key\n//     is a new context (§6). The invariant that buys is one sentence — *one\n//     counter value, once, per direction, per context* — with no epoch field\n//     and no key generation for a receiver to guess at.\n\nimport { createCipheriv, createDecipheriv, createHash, type KeyObject } from \"crypto\";\nimport {\n  assertBytes,\n  E2EE_CTX_UNKNOWN,\n  E2EE_PROTOCOL_VERSION,\n  E2EE_SEAL_FAILED,\n  E2EE_SEQUENCE_VIOLATION,\n  type E2eeRejectionCode,\n  importSecret,\n  isBytes,\n  own,\n  redactKeyMaterial,\n  unpooled,\n} from \"./protocol\";\n\n/** Client → server. The value is the first 4 bytes of every c2s nonce. */\nexport const DIRECTION_C2S = 0x00000001;\n/** Server → client. */\nexport const DIRECTION_S2C = 0x00000002;\nexport type Direction = typeof DIRECTION_C2S | typeof DIRECTION_S2C;\n\nexport const CHANNEL_WS = 0x01;\nexport const CHANNEL_REST_REQUEST = 0x02;\nexport const CHANNEL_REST_RESPONSE = 0x03;\nexport type Channel =\n  | typeof CHANNEL_WS\n  | typeof CHANNEL_REST_REQUEST\n  | typeof CHANNEL_REST_RESPONSE;\n\nexport const KEY_BYTES = 32;\nexport const CTX_ID_BYTES = 16;\nexport const TAG_BYTES = 16;\nexport const NONCE_BYTES = 12;\n/** `version(1) || ctxId(16) || direction(4) || counter(8) || channel(1)` (§4). */\nexport const HEADER_BYTES = 1 + CTX_ID_BYTES + 4 + 8 + 1;\n/** The REST AAD suffix: `sha256(method || \"\\n\" || path || \"\\n\" || query)` (§4). */\nexport const TARGET_HASH_BYTES = 32;\n\n/**\n * The counter ceiling. A sender AT this value refuses rather than wrapping (§7).\n *\n * Unreachable in practice — at D-3's measured ~1.6 MB/s budget it is on the\n * order of 10^11 years — and asserted precisely so it can never become a silent\n * wrap. The cost of refusing at `2^64 - 1` rather than after it is one unused\n * counter value out of 2^64.\n *\n * The refusal leaves the state unchanged, so there is no recovery that keeps\n * the context: the caller destroys it and the client opens a new one (§7).\n */\nexport const MAX_COUNTER = 2n ** 64n - 1n;\n\n/**\n * Ceiling on a frame, checked before anything is parsed or decrypted.\n *\n * NONCE-DESIGN does not name a number; this one is a size no legitimate frame\n * reaches (the largest thing the hub sends is a terminal replay, tens of KB)\n * while staying far enough above it that a bound is never the reason a real\n * message fails. Same shape as `NOISE_MAX_MESSAGE_BYTES`: bound first, allocate\n * second, never allocate in proportion to an attacker-supplied length (D-9).\n *\n * §10 is explicit that on the WebSocket this is a check AFTER allocation —\n * `@hono/node-ws` assembles the frame with `ws`'s 100 MiB default before any of\n * this runs — and that closing that gap is W1b's, with its own per-direction\n * ceilings. This constant is not that bound and must not be mistaken for it.\n */\nexport const MAX_RECORD_BYTES = 4 * 1024 * 1024;\n\nexport class RecordError extends Error {\n  readonly code: E2eeRejectionCode;\n\n  constructor(code: E2eeRejectionCode, message: string) {\n    super(message);\n    this.name = \"RecordError\";\n    this.code = code;\n  }\n}\n\n/**\n * `direction(4) || counter(8)`, big-endian, and never random (§2).\n *\n * A counter makes nonce reuse an invariant a test asserts on rather than a\n * birthday bound argued about in review (D-2). Each direction has its own key\n * AND its own label, so a record can never be reflected back at its sender: the\n * reflected frame is decrypted with the wrong key *and* carries the wrong\n * direction in both its nonce and its AAD.\n */\nexport function recordNonce(direction: Direction, counter: bigint): Buffer {\n  const nonce = Buffer.alloc(NONCE_BYTES);\n  nonce.writeUInt32BE(direction, 0);\n  nonce.writeBigUInt64BE(counter, 4);\n  return nonce;\n}\n\n/**\n * The REST AAD suffix (§4): `sha256(method || \"\\n\" || path || \"\\n\" || query)`.\n *\n * Paths and query stay plaintext (D-7), so without this nothing in the AAD\n * binds *what a sealed body is for*: an on-path attacker re-points a sealed\n * `POST /api/sessions/A/input` at session B, the body authenticates, and the\n * server runs the user's own keystrokes against a different session. Same for\n * `/cancel`, `/stop`, `/permission/answer` and `prune_all`.\n *\n * It is computed by both sides from the request line and never transmitted, so\n * the wire header stays 30 bytes.\n *\n * `query` is the raw query string WITHOUT the leading `?`, empty when there is\n * none — the two sides must agree on that spelling exactly or every sealed\n * request fails to authenticate with no other diagnostic.\n */\nexport function restTargetHash(method: string, path: string, query: string): Buffer {\n  return createHash(\"sha256\")\n    .update(`${method.toUpperCase()}\\n${path}\\n${query}`, \"utf-8\")\n    .digest();\n}\n\n/**\n * The target hash for a request, taken from the RAW wire request-target.\n *\n * **This is the `ctxId`-encoding trap one layer down** (§4), so the inputs are\n * pinned rather than described: an implementation that normalises anything here\n * rejects a legitimate request with `E2EE_SEAL_FAILED` and nothing else to\n * debug it.\n *\n *   method  upper-case ASCII, as sent\n *   path    the raw request-target path — percent-encoding PRESERVED, never\n *           decoded, never normalised. `/api/conversations/a%2Fb` and\n *           `/api/conversations/a/b` are different targets and hash differently\n *   query   the raw substring after `?`, verbatim: original parameter order,\n *           original `+` vs `%20`, duplicates kept, nothing sorted or\n *           re-serialised. The empty string when there is no `?`\n *\n * The server MUST read this from `c.env.incoming.url` — the bytes Node received\n * — and NEVER from Hono's `c.req.path`, which is percent-decoded, nor from a\n * re-serialised `URLSearchParams`, whose ordering and escaping do not round\n * trip.\n *\n * **The client hashes the ORIGIN-FORM target, not the absolute URL it fetches.**\n * That is `/api/sessions?limit=50`, never `https://host/api/sessions?limit=50`:\n * scheme, host and port are not in the hash. A client that passes the URL it is\n * about to fetch produces a different digest for every request, and each one\n * fails with `E2EE_SEAL_FAILED` and nothing else to debug — the precise trap §4\n * exists to prevent, reintroduced by a sentence. The fixture pins it:\n * `restTargetCanonicalization.hashInputUtf8` in\n * `__tests__/fixtures/e2ee-record-vectors.json` begins with the method and a\n * bare `/`.\n */\nexport function restTargetHashFromUrl(method: string, rawUrl: string): Buffer {\n  const q = rawUrl.indexOf(\"?\");\n  const path = q === -1 ? rawUrl : rawUrl.slice(0, q);\n  const query = q === -1 ? \"\" : rawUrl.slice(q + 1);\n  return restTargetHash(method, path, query);\n}\n\nexport interface RecordHeader {\n  version: number;\n  /**\n   * Any byte view, not only a `Buffer`. A `@stablelib`-based client hands over\n   * plain `Uint8Array`s and §13 has it calling this builder directly.\n   */\n  ctxId: Uint8Array;\n  direction: Direction;\n  counter: bigint;\n  channel: Channel;\n}\n\n/**\n * The AAD: the 30-byte plaintext header, plus the 32-byte target hash on the\n * REST channels (§4).\n *\n * The header travels in the clear and is authenticated, so an intermediary can\n * neither rewrite a sequence number nor re-point a record at another context.\n *\n * Exported because the interop fixtures publish it and a client implementation\n * has to reproduce it byte for byte.\n */\nexport function recordHeader(header: RecordHeader): Buffer {\n  assertBytes(header.ctxId, CTX_ID_BYTES, \"ctxId\", (m) => new RecordError(E2EE_SEAL_FAILED, m));\n  const wire = Buffer.alloc(HEADER_BYTES);\n  wire.writeUInt8(header.version, 0);\n  // `.set`, not `.copy`: `copy` is Buffer-only, so a correct-length plain\n  // `Uint8Array` — exactly what the client track produces — threw\n  // `TypeError: target.copy is not a function` AFTER passing the guard. Right\n  // input, wrong crash, outside §9's taxonomy.\n  wire.set(header.ctxId, 1);\n  wire.writeUInt32BE(header.direction, 17);\n  wire.writeBigUInt64BE(header.counter, 21);\n  wire.writeUInt8(header.channel, 29);\n  return wire;\n}\n\n/**\n * The AAD: the 30-byte header, plus the 32-byte target hash on the REST\n * channels (§4).\n *\n * **This function enforces the target rule itself.** The client track consumes\n * the AAD BUILDER, not the wrapper one layer up, so a rule checked only in\n * `assertTarget` is a rule that implementation never receives — and a forgotten\n * target then yields a silently unbound AAD on the two channels that exist to\n * bind one. `recordHeader` above is the wire bytes and carries no such rule,\n * because a header is not an AAD.\n */\nexport function recordAad(header: RecordHeader, target?: Uint8Array): Buffer {\n  validateTarget(header.channel, target);\n  const wire = recordHeader(header);\n  if (!target) return wire;\n  const aad = Buffer.alloc(HEADER_BYTES + TARGET_HASH_BYTES);\n  aad.set(wire, 0);\n  aad.set(target, HEADER_BYTES);\n  return aad;\n}\n\n/**\n * A target is required, and exactly 32 bytes, on the REST channels; forbidden\n * on the socket.\n *\n * Byte LENGTH, not `.length`: `new Float64Array(32)` has `.length === 32` and\n * 256 bytes behind it, and a 32-character string has neither. `isBytes` is the\n * one place that distinction lives (§11).\n */\nfunction validateTarget(channel: Channel, target: Uint8Array | undefined): void {\n  if (!channelBindsTarget(channel)) {\n    if (target !== undefined) {\n      throw new RecordError(E2EE_SEAL_FAILED, `channel ${channel} takes no request target`);\n    }\n    return;\n  }\n  if (!isBytes(target, TARGET_HASH_BYTES)) {\n    throw new RecordError(\n      E2EE_SEAL_FAILED,\n      `channel ${channel} requires a ${TARGET_HASH_BYTES}-byte request target`,\n    );\n  }\n}\n\n/** Whether a channel's records bind a request target (§4). */\nexport function channelBindsTarget(channel: Channel): boolean {\n  return channel === CHANNEL_REST_REQUEST || channel === CHANNEL_REST_RESPONSE;\n}\n\n/**\n * A target is required on REST and forbidden on the socket.\n *\n * Both directions of the mistake are refused rather than defaulted: a missing\n * target on REST would silently unbind the request line the suffix exists to\n * bind, and a target on the socket would be a value the peer has no way to\n * reproduce.\n */\nfunction assertTarget(channel: Channel, target: Uint8Array | undefined): void {\n  // A thin wrapper over the builder's own rule, kept only so a refusal happens\n  // BEFORE any state moves — the response sealer spends an acceptance it can\n  // never re-arm, so a check that fires deep inside `recordAad` would fire too\n  // late. Same function, so the two can never disagree.\n  validateTarget(channel, target);\n}\n\nexport interface RecordStateOptions {\n  /** 32-byte traffic key for THIS direction, or one already imported. */\n  key: Buffer | KeyObject;\n  /** The context handle, raw 16 bytes. */\n  ctxId: Buffer;\n  direction: Direction;\n  channel: Channel;\n  /**\n   * INTERNAL — tests only. A construction-time counter seed, and the one narrow\n   * exception NONCE-DESIGN §5 R4 states explicitly: the §7 exhaustion test has\n   * to place a counter near `2^64 - 1`, which it cannot do a frame at a time.\n   *\n   * This is not the forbidden shape. The seed sets a starting point ONCE, at\n   * construction; `seal` and `unseal` still take no counter and remain the sole\n   * advancers. A `seal(counter, …)` signature stays forbidden.\n   */\n  initialCounter?: bigint;\n}\n\n/**\n * One direction of one channel of one context: a key, a label, and the counter\n * that belongs to them.\n *\n * A state both seals and unseals with its single counter, because a state is\n * one direction — the sending side calls `seal`, the receiving side calls\n * `unseal`, and neither ever calls the other. Two states per channel, built by\n * `context.ts`, is what keeps the two counters independent.\n */\nexport class RecordState {\n  /** `#private`: not a property, so no rendering mode can reach it. */\n  /** OpenSSL-side, not a JS Buffer: there is nothing left on the heap to find. */\n  readonly #k: KeyObject;\n  /**\n   * `#private`, because the counter is as sensitive as the key.\n   *\n   * It was TypeScript-`private`, i.e. an ordinary property at runtime, so\n   * `(state as any).n = 0n` performed the counter reset §14 forbids — and this\n   * ships as an untyped artefact, where `as any` is just how you write it. We\n   * moved the key material and stopped; the state that makes a nonce unique\n   * never moved.\n   */\n  #n: bigint;\n  /**\n   * Public by design — the AAD binds it and callers read it.\n   *\n   * UNPOOLED, therefore. A pooled public Buffer hands out a window onto the\n   * shared 8 KiB allocation its neighbours live in, which is how a registry\n   * walk reached live key bytes without touching a key-bearing class at all.\n   */\n  readonly ctxId: Buffer;\n  readonly direction: Direction;\n  readonly channel: Channel;\n\n  constructor(options: RecordStateOptions) {\n    const fail = (m: string) => new RecordError(E2EE_SEAL_FAILED, m);\n    assertBytes(options.ctxId, CTX_ID_BYTES, \"ctxId\", fail);\n    this.#k = toSecret(options.key, fail);\n    redactKeyMaterial(\n      this,\n      () =>\n        `RecordState { channel: ${this.channel}, direction: ${this.direction}, counter: ${this.#n}, key: <#private> }`,\n    );\n    this.ctxId = unpooled(options.ctxId);\n    this.direction = options.direction;\n    this.channel = options.channel;\n    this.#n = own(options, \"initialCounter\") ?? 0n;\n  }\n\n  /** The next counter this state will use. Read-only: nothing outside sets it. */\n  get counter(): bigint {\n    return this.#n;\n  }\n\n  /**\n   * Seal one record. The counter advances by exactly 1 AFTER success, never\n   * before (§5 R1).\n   *\n   * Returns `header(30) || ciphertext || tag(16)`.\n   */\n  seal(plaintext: Buffer, target?: Buffer): Buffer {\n    assertTarget(this.channel, target);\n    // Refuse rather than wrap (§7). Checked before the header is built, so a\n    // refusal costs nothing and leaves the state exactly as it was — which is\n    // what lets the caller destroy the context rather than repair it.\n    if (this.#n >= MAX_COUNTER) {\n      throw new RecordError(\n        E2EE_SEAL_FAILED,\n        \"record counter exhausted; refusing to send rather than reuse a nonce\",\n      );\n    }\n    const frame = sealWith(\n      this.#k,\n      this.ctxId,\n      this.direction,\n      this.channel,\n      this.#n,\n      plaintext,\n      target,\n    );\n    this.#n += 1n;\n    return frame;\n  }\n\n  /**\n   * Unseal one record, or throw.\n   *\n   * **Authenticate first, then compare the counter (§5 R2 ordering).** The\n   * nonce is built from the header either way, so the AEAD can run before the\n   * sequence check — and it must. Checking the counter first would make\n   * `E2EE_SEQUENCE_VIOLATION` an *unauthenticated verdict about the peer*:\n   * anyone who can inject a frame reads `ctxId` from a previous plaintext\n   * header, sends garbage with a wrong counter, and the server logs a sequence\n   * violation naming a device that did nothing and closes its socket. It buys\n   * no DoS protection either — the same attacker can as cheaply send a frame\n   * with the *right* counter, which is authenticated anyway.\n   *\n   * Strict once authenticated: `counter == expected` exactly, no window (§5 R2).\n   * A WebSocket runs over one TCP connection, so it is ordered and gap-free by\n   * construction; a repeat, a gap or a reorder is a protocol violation.\n   *\n   * A rejected frame advances NOTHING (§5 R3).\n   */\n  unseal(frame: Buffer, target?: Buffer): Buffer {\n    const { plaintext, counter } = this.openFrame(frame, target);\n\n    // Only now — the frame is proven to come from the peer, so a sequence\n    // violation is a true claim about the peer rather than about an injector.\n    if (counter !== this.#n) {\n      throw new RecordError(\n        E2EE_SEQUENCE_VIOLATION,\n        `record counter ${counter} is not the expected ${this.#n}`,\n      );\n    }\n    this.#n += 1n;\n    return plaintext;\n  }\n\n  /**\n   * The AEAD step WITHOUT the sequence check — the sanctioned seam for the REST\n   * sliding-window receiver (§13).\n   *\n   * `unseal` enforces strict `expected`, which is right for the socket and\n   * wrong for a channel React Query drives concurrently. The REST track needs\n   * the authenticated plaintext *and* the counter the frame claimed so its\n   * 1024-bit window can decide acceptance. Exposing that here rather than\n   * leaving it to be improvised is the whole point: the alternative is a second\n   * implementation of nonce and AAD assembly in another module, which is\n   * exactly how two implementations come to disagree.\n   *\n   * Three properties this deliberately keeps:\n   *\n   *   - it is the REST REQUEST channel only. On the socket a window is a\n   *     protocol violation (§5 R2), and a seam that could relax it there would\n   *     be a hole in the rule this layer exists to hold;\n   *   - it advances NOTHING. The window owns acceptance and replay bookkeeping,\n   *     so a state that advanced here would be two authorities on one counter;\n   *   - every other check still runs — bounds, version, `ctxId`, direction,\n   *     channel, the target hash and the tag. Only the sequence rule is the\n   *     caller's.\n   *\n   * The caller must therefore still refuse a counter its window rejects, and\n   * must never seal a response for one (§13(a)).\n   */\n  unsealUnchecked(frame: Buffer, target: Buffer): { plaintext: Buffer; counter: bigint } {\n    if (this.channel !== CHANNEL_REST_REQUEST) {\n      throw new RecordError(\n        E2EE_SEAL_FAILED,\n        \"unsealUnchecked is the REST request channel's seam; the socket's counter is strict\",\n      );\n    }\n    return this.openFrame(frame, target);\n  }\n\n  /**\n   * Everything both receive paths share: bounds, header checks, and the AEAD.\n   *\n   * One parser, two policies. The sequence rule is the only thing that differs\n   * between the socket and REST, so it is the only thing left to the callers —\n   * a second copy of the header checks is how the two channels would drift into\n   * disagreeing about what a frame even is.\n   */\n  private openFrame(frame: Buffer, target?: Buffer): { plaintext: Buffer; counter: bigint } {\n    assertTarget(this.channel, target);\n    // Bounds first, on the length of the buffer we were handed — nothing is\n    // parsed until the frame could plausibly be one (D-9).\n    // `byteLength`, not `.length` — the units this module insists on everywhere\n    // else. For a `Buffer` they agree; for any other byte view handed to a\n    // public entry point they need not, and this is the D-9 bound.\n    if (frame.byteLength < HEADER_BYTES + TAG_BYTES) {\n      throw new RecordError(E2EE_SEAL_FAILED, \"record shorter than its header and tag\");\n    }\n    if (frame.byteLength > MAX_RECORD_BYTES) {\n      throw new RecordError(E2EE_SEAL_FAILED, \"record too large\");\n    }\n\n    const header = frame.subarray(0, HEADER_BYTES);\n    const version = header.readUInt8(0);\n    if (version !== E2EE_PROTOCOL_VERSION) {\n      throw new RecordError(E2EE_SEAL_FAILED, `unsupported record version ${version}`);\n    }\n    // The context check stays FIRST, before the AEAD and before any allocation:\n    // a frame addressed elsewhere is the cheapest possible rejection and must\n    // not cost a ChaCha20 pass over an attacker-sized buffer (§10). It is\n    // observable from outside as the CODE — `E2EE_CTX_UNKNOWN`, not\n    // `E2EE_SEAL_FAILED`, which is what a check placed after the AEAD would\n    // produce. §9: the log line for this says *misaddressed*, not \"unknown\" —\n    // the frame names a context, just not this one.\n    if (!header.subarray(1, 1 + CTX_ID_BYTES).equals(this.ctxId)) {\n      throw new RecordError(E2EE_CTX_UNKNOWN, \"record is addressed to another context\");\n    }\n    if (header.readUInt32BE(17) !== this.direction) {\n      throw new RecordError(E2EE_SEAL_FAILED, \"record carries the wrong direction\");\n    }\n    if (header.readUInt8(29) !== this.channel) {\n      throw new RecordError(E2EE_SEAL_FAILED, \"record carries the wrong channel\");\n    }\n\n    const counter = header.readBigUInt64BE(21);\n    return {\n      plaintext: openWith(this.#k, this.direction, counter, frame, header, target),\n      counter,\n    };\n  }\n}\n\n/**\n * Seals a REST response under the counter of the request it answers (§13(a)).\n *\n * **This is the one sanctioned `seal(counter, …)` shape** and §5 R4 says why it\n * is not the forbidden one: R4 governs *sequence* counters — a value the sender\n * chooses and advances — and a response echo is not one. The value is dictated\n * by a request that was already accepted, and this is a distinct class from\n * `RecordState`, so a caller cannot reach a sequence counter through it.\n *\n * Nonce uniqueness for `(k_s2c, 2‖counter)` rests entirely on the rule below:\n *\n * > **At most one sealed response per accepted request counter.** A request\n * > rejected by the window or by the AEAD gets a PLAINTEXT error and never a\n * > sealed body — including through the framework's error path.\n *\n * So `accept()` is called only by a successful request unseal, and `seal()`\n * spends that acceptance. A response for a counter that was never accepted, or\n * a second response for one, is refused here rather than trusted to a caller.\n *\n * The alternative — a second sender counter for responses — was rejected: the\n * response would then not be bound to its request at all, and because the\n * client issues concurrent requests an on-path attacker could swap two in-flight\n * sealed responses within one context, both authenticating with fresh counters.\n */\nexport class RestResponseSealer {\n  /** `#private`: not a property, so no rendering mode can reach it. */\n  readonly #k: KeyObject;\n  readonly #ctxId: Buffer;\n  /** Counters accepted and not yet answered. Bounded by the window below. */\n  readonly #outstanding = new Set<bigint>();\n  /**\n   * RFC-6479-style bitmap of counters already ANSWERED, over the last\n   * `WINDOW_COUNTERS` positions ending at `acceptedHighWater`.\n   *\n   * This is the class's own memory of what it has done, and it is the whole\n   * reason the nonce invariant holds here. Until it existed, `accept()` armed\n   * unconditionally and `seal()` recorded nothing, so\n   * `accept(7) → seal(7) → accept(7) → seal(7)` produced two records under\n   * `(k_s2c, 2‖7)`: keystream reuse, demonstrated by an adversary as\n   * `xor(c1, c2) === xor(p1, p2)`. What made it unreachable in practice was the\n   * STRICT receive counter one layer up — precisely the code §13 schedules the\n   * sliding window to replace. An invariant held by a layer scheduled for\n   * replacement is not held.\n   *\n   * A bitmap rather than a capped Set for two reasons: it is bounded by\n   * construction, so there is no overflow table to shed entries into a\n   * non-recoverable dead end (the earlier `evicted` Set did exactly that); and\n   * it is the shape the REST track needs for the receive window, so the two\n   * halves of §13 agree instead of being invented twice.\n   */\n  readonly #answeredBits: Uint8Array;\n  /** Highest counter ever accepted. `-1n` means \"nothing yet\". */\n  /**\n   * `#private` with the rest of the window state: an attacker who can write\n   * `sealer.acceptedHighWater = -1n; sealer.answeredBits.fill(0)` re-arms every\n   * answered counter, which is keystream reuse under `(k_s2c, 2‖counter)` —\n   * the one failure this design exists to prevent, reached without touching a\n   * key at all.\n   */\n  #acceptedHighWater = -1n;\n\n  /**\n   * How far behind the high-water mark a counter is still tracked.\n   *\n   * Well above any realistic concurrency, and the same width as the 1024-bit\n   * REST receive window — deliberately, because a counter that window will\n   * still accept must be one this can still answer.\n   */\n  static readonly WINDOW_COUNTERS = 1024;\n  /** @deprecated Kept as the old name for one release; same number. */\n  static readonly MAX_OUTSTANDING = RestResponseSealer.WINDOW_COUNTERS;\n\n  constructor(options: { key: Buffer | KeyObject; ctxId: Buffer }) {\n    const fail = (m: string) => new RecordError(E2EE_SEAL_FAILED, m);\n    assertBytes(options.ctxId, CTX_ID_BYTES, \"ctxId\", fail);\n    this.#k = toSecret(options.key, fail);\n    this.#ctxId = unpooled(options.ctxId);\n    this.#answeredBits = new Uint8Array(RestResponseSealer.WINDOW_COUNTERS / 8);\n    redactKeyMaterial(\n      this,\n      () =>\n        `RestResponseSealer { outstanding: ${this.#outstanding.size}, highWater: ${this.#acceptedHighWater}, key: <#private> }`,\n    );\n  }\n\n  /**\n   * Arm exactly one response for a request counter that was just accepted.\n   *\n   * A second acceptance of one counter is an upstream bug, and it must never\n   * mint a second nonce: it is refused here rather than trusted to whatever\n   * sits above.\n   */\n  accept(counter: bigint): void {\n    if (this.#outstanding.has(counter)) {\n      throw new RecordError(\n        E2EE_SEAL_FAILED,\n        \"that request counter is already accepted and awaiting its one response\",\n      );\n    }\n    if (this.belowWindow(counter)) {\n      // Cannot prove it was never answered, so it is refused — but as the\n      // RECOVERABLE code: this is the saturation edge, and a dead end here is\n      // the failure §13(a) forbids.\n      throw new RecordError(\n        E2EE_CTX_UNKNOWN,\n        \"that request counter is further behind than this context tracks; re-open and retry\",\n      );\n    }\n    if (this.isAnswered(counter)) {\n      throw new RecordError(\n        E2EE_SEAL_FAILED,\n        \"that request counter has already been answered; it can never be answered again\",\n      );\n    }\n    this.advanceTo(counter);\n    this.#outstanding.add(counter);\n  }\n\n  /** Whether a response may still be sealed for this counter. */\n  isOutstanding(counter: bigint): boolean {\n    return this.#outstanding.has(counter);\n  }\n\n  seal(requestCounter: bigint, plaintext: Buffer, target: Buffer): Buffer {\n    // The response channel binds its request target too (§4), and this class\n    // never checked it: a caller could seal a REST response with no target or a\n    // short one, and the binding was silently absent on the one path nothing\n    // exercised — the path the REST middleware track will call. `target` is\n    // non-optional in the signature AND checked at runtime, because a type is\n    // not a check for a module another repository consumes.\n    assertTarget(CHANNEL_REST_RESPONSE, target);\n    if (!this.#outstanding.delete(requestCounter)) {\n      if (this.belowWindow(requestCounter)) {\n        throw new RecordError(\n          E2EE_CTX_UNKNOWN,\n          \"that request counter is further behind than this context tracks; re-open and retry\",\n        );\n      }\n      if (this.isAnswered(requestCounter)) {\n        throw new RecordError(\n          E2EE_SEAL_FAILED,\n          \"that request counter has already been answered; sealing again would reuse a nonce\",\n        );\n      }\n      if (requestCounter <= this.#acceptedHighWater) {\n        // Accepted at some point and no longer tracked: recoverable.\n        throw new RecordError(\n          E2EE_CTX_UNKNOWN,\n          \"no accepted request is waiting on that counter; re-open and retry\",\n        );\n      }\n      throw new RecordError(\n        E2EE_SEAL_FAILED,\n        \"no accepted request is waiting on that counter; a rejected request gets a plaintext error\",\n      );\n    }\n    // Marked BEFORE the record exists, so no ordering leaves the counter\n    // re-armable if `sealWith` throws.\n    this.markAnswered(requestCounter);\n    return sealWith(\n      this.#k,\n      this.#ctxId,\n      DIRECTION_S2C,\n      CHANNEL_REST_RESPONSE,\n      requestCounter,\n      plaintext,\n      target,\n    );\n  }\n\n  // ── the window ───────────────────────────────────────────────────\n  //\n  // Bits are indexed modulo the window width, so sliding forward must CLEAR the\n  // positions the window newly covers or a wrapped index reads as a stale\n  // \"answered\". Everything that falls out of the window is refused rather than\n  // forgotten, which is what makes bounded memory safe here.\n\n  private bit(counter: bigint): { index: number; mask: number } {\n    const position = Number(counter % BigInt(RestResponseSealer.WINDOW_COUNTERS));\n    return { index: position >> 3, mask: 1 << (position & 7) };\n  }\n\n  private belowWindow(counter: bigint): boolean {\n    return (\n      this.#acceptedHighWater >= 0n &&\n      counter + BigInt(RestResponseSealer.WINDOW_COUNTERS) <= this.#acceptedHighWater\n    );\n  }\n\n  /**\n   * Whether this counter is recorded as answered.\n   *\n   * **Only meaningful at or below the high-water mark.** Bits are indexed\n   * modulo the window width and are cleared as the window slides forward, so a\n   * counter ABOVE the mark reads a bit belonging to a position the window has\n   * not reached yet — 1024 counters ago, not this one. Reading it unguarded is\n   * how the first draft of this class refused a perfectly fresh counter as\n   * \"already answered\", which its own test caught.\n   */\n  private isAnswered(counter: bigint): boolean {\n    if (counter > this.#acceptedHighWater || this.belowWindow(counter)) return false;\n    const { index, mask } = this.bit(counter);\n    return (this.#answeredBits[index] & mask) !== 0;\n  }\n\n  private markAnswered(counter: bigint): void {\n    const { index, mask } = this.bit(counter);\n    this.#answeredBits[index] |= mask;\n  }\n\n  private advanceTo(counter: bigint): void {\n    if (counter <= this.#acceptedHighWater) return;\n    const width = BigInt(RestResponseSealer.WINDOW_COUNTERS);\n    if (this.#acceptedHighWater < 0n || counter - this.#acceptedHighWater >= width) {\n      this.#answeredBits.fill(0);\n    } else {\n      for (let c = this.#acceptedHighWater + 1n; c <= counter; c++) {\n        const { index, mask } = this.bit(c);\n        this.#answeredBits[index] &= ~mask;\n      }\n    }\n    this.#acceptedHighWater = counter;\n    // Outstanding entries that fell out of the window are dropped, so this set\n    // is bounded too. Dropping is safe because `accept` refuses to re-arm them\n    // and `seal` answers them with the recoverable code.\n    for (const pending of this.#outstanding) {\n      if (this.belowWindow(pending)) this.#outstanding.delete(pending);\n    }\n  }\n}\n\n/**\n * Build a record state.\n *\n * The factory rather than the constructor is what callers use, so the\n * `initialCounter` seam stays visible as a named option in one place\n * (NONCE-DESIGN §5 R4) instead of spreading through `new RecordState(...)` call\n * sites.\n */\nexport function createRecordState(options: RecordStateOptions): RecordState {\n  return new RecordState(options);\n}\n\n// ─── shared primitives ──────────────────────────────────────────────\n\n/**\n * A traffic key as a `KeyObject`, from either a Buffer or an already-imported\n * key.\n *\n * Accepting a Buffer keeps every existing call site working while guaranteeing\n * that what the object STORES is never a JS Buffer: the caller's bytes are\n * copied, imported, and the copy wiped, so no key survives on the heap for a\n * pool walk to find.\n */\nfunction toSecret(key: Buffer | KeyObject, fail: (m: string) => Error): KeyObject {\n  if (typeof key === \"object\" && key !== null && !isBytes(key, KEY_BYTES)) {\n    // An already-imported key, or a mistake. A secret KeyObject is accepted only\n    // at the right WIDTH: `type === \"secret\"` is the presence-vs-length class\n    // surviving in the one guard that does not route through `isBytes`, and a\n    // 16-byte secret would otherwise be installed as a traffic key.\n    const imported = key as KeyObject;\n    if (\"type\" in key && imported.type === \"secret\") {\n      if (imported.symmetricKeySize !== KEY_BYTES) {\n        throw fail(`traffic key must be exactly ${KEY_BYTES} bytes`);\n      }\n      return imported;\n    }\n    throw fail(`traffic key must be exactly ${KEY_BYTES} bytes`);\n  }\n  assertBytes(key, KEY_BYTES, \"traffic key\", fail);\n  return importSecret(key);\n}\n\nfunction sealWith(\n  key: KeyObject,\n  ctxId: Buffer,\n  direction: Direction,\n  channel: Channel,\n  counter: bigint,\n  plaintext: Buffer,\n  target: Buffer | undefined,\n): Buffer {\n  const framing = { version: E2EE_PROTOCOL_VERSION, ctxId, direction, counter, channel };\n  // The 30-byte plaintext header is what goes on the wire; the AAD is that plus\n  // the 32-byte target hash on the REST channels. Both come from `recordAad`,\n  // and the target is passed INTO it rather than concatenated beside it —\n  // otherwise `recordAad`'s \"target hash must be 32 bytes\" check is unreachable\n  // from every seal path, which is exactly how a short or absent target got\n  // through on channel 0x03.\n  const header = recordHeader(framing);\n  const aad = recordAad(framing, target);\n  const cipher = createCipheriv(\"chacha20-poly1305\", key, recordNonce(direction, counter), {\n    authTagLength: TAG_BYTES,\n  });\n  cipher.setAAD(aad, { plaintextLength: plaintext.length });\n  const body = cipher.update(plaintext);\n  const rest = cipher.final();\n  const tag = cipher.getAuthTag();\n  // Assembled UNPOOLED. `Buffer.concat` carves out of the shared pool, and a\n  // frame is public: handing one out would hand out a window onto whatever the\n  // pool holds next to it (§13).\n  const frame = Buffer.allocUnsafeSlow(header.length + body.length + rest.length + tag.length);\n  header.copy(frame, 0);\n  body.copy(frame, header.length);\n  rest.copy(frame, header.length + body.length);\n  tag.copy(frame, header.length + body.length + rest.length);\n  return frame;\n}\n\nfunction openWith(\n  key: KeyObject,\n  direction: Direction,\n  counter: bigint,\n  frame: Buffer,\n  header: Buffer,\n  target: Buffer | undefined,\n): Buffer {\n  const body = frame.subarray(HEADER_BYTES, frame.length - TAG_BYTES);\n  const tag = frame.subarray(frame.length - TAG_BYTES);\n  const decipher = createDecipheriv(\"chacha20-poly1305\", key, recordNonce(direction, counter), {\n    authTagLength: TAG_BYTES,\n  });\n  // The AAD is the header AS RECEIVED — that is what makes the header\n  // authenticated rather than merely present — plus the target the receiver\n  // computed from the request line it is actually serving. Built through\n  // `recordAad` on both sides, so the 32-byte target check is reachable here\n  // too rather than only on the seal path.\n  decipher.setAAD(\n    target\n      ? recordAad(\n          {\n            version: header.readUInt8(0),\n            ctxId: header.subarray(1, 1 + CTX_ID_BYTES),\n            direction: header.readUInt32BE(17) as Direction,\n            counter: header.readBigUInt64BE(21),\n            channel: header.readUInt8(29) as Channel,\n          },\n          target,\n        )\n      : header,\n    { plaintextLength: body.length },\n  );\n  decipher.setAuthTag(tag);\n  try {\n    return Buffer.concat([decipher.update(body), decipher.final()]);\n  } catch {\n    // Nothing advances. §5 R3.\n    throw new RecordError(E2EE_SEAL_FAILED, \"record failed authentication\");\n  }\n}\n","// The REST request channel's replay window: an RFC-6479-style sliding bitmap\n// over authenticated counters.\n//\n// Written against specs/end-to-end-encryption/NONCE-DESIGN.md §5, §9 and §13,\n// and design.md §3.4. The two channels get genuinely different rules and this\n// class is one half of that split:\n//\n//   - the WebSocket receiver stays STRICT — `counter == expected`, no window\n//     (§5 R2). A window there would forfeit the property that makes replay\n//     structurally impossible on the highest-volume channel, so nothing in this\n//     file is reachable from `CHANNEL_WS`: `RecordState.unsealUnchecked`, the\n//     only seam that hands a counter to this class, refuses every channel but\n//     the REST request one;\n//   - the REST receiver needs a window because HTTP requests are concurrent and\n//     React Query issues them concurrently. A strict counter rejects a\n//     perfectly legitimate out-of-order arrival (design.md §3.4).\n//\n// This class decides ACCEPTANCE and nothing else. It advances no counter, holds\n// no key, and never sees a nonce, an AAD or a frame: it is handed a counter the\n// AEAD has already authenticated, and answers \"record it\" or \"throw\". Ordering\n// matters and is the caller's to get right — authenticate first, then decide\n// (§5 R2 ordering) — because a pre-authentication counter check makes\n// `E2EE_SEQUENCE_VIOLATION` an unauthenticated verdict about the peer and buys\n// no protection at all.\n\nimport { E2EE_CTX_UNKNOWN, E2EE_SEQUENCE_VIOLATION } from \"./protocol\";\nimport { MAX_COUNTER, RecordError, RestResponseSealer } from \"./record\";\n\n/**\n * The receive-side sliding window for one REST context's request channel.\n *\n * `admit(counter)` either returns — the counter is accepted and recorded — or\n * throws a `RecordError` carrying one of the two §9 codes that apply here:\n *\n *   - `E2EE_SEQUENCE_VIOLATION` for a counter this window can PROVE it has\n *     already seen, and for one outside the counter's own range. A claim about\n *     the peer, and true only because the frame was authenticated first;\n *   - `E2EE_CTX_UNKNOWN` for one that fell out of the window. It cannot be\n *     proven a replay — the bit that would say so has been reused by a counter\n *     1024 positions later — so it gets the RECOVERABLE code, which is what\n *     `RestResponseSealer.accept` already answers on its own below-window edge.\n *     A client that lags this far behind re-opens and retries rather than\n *     dead-ending.\n */\nexport class RestReceiveWindow {\n  /**\n   * The width, in counters. Deliberately the SAME width as the response\n   * sealer's, and taken from it rather than repeated as a literal.\n   *\n   * §13(a) is what couples them: at most one sealed response per accepted\n   * request counter, and a counter this window still accepts must therefore be\n   * one the sealer can still answer. Two literals would let the widths drift in\n   * a later edit, and the failure that drift produces is not a rejected request\n   * — it is a request accepted here and refused there, i.e. an accepted request\n   * that can never be answered.\n   */\n  static readonly WINDOW_COUNTERS = RestResponseSealer.WINDOW_COUNTERS;\n\n  /**\n   * `#private`, both of them, for the reason §13 states verbatim: *\"The state\n   * that makes a nonce unique is as sensitive as the key … with them as\n   * ordinary properties, one assignment re-arms every answered counter, which\n   * is keystream reuse.\"*\n   *\n   * TypeScript `private` is NOT sufficient — it is an ordinary own property at\n   * runtime, and this module is consumed from a repository that has no types at\n   * all. `window.highWater = -1n` would re-admit every counter this context has\n   * ever seen, each of which the sealer would then be asked to answer a second\n   * time under `(k_s2c, 2‖counter)`.\n   */\n  readonly #seenBits: Uint8Array;\n  /** Highest counter ever admitted. `-1n` means \"nothing yet\". */\n  #highWater = -1n;\n\n  constructor() {\n    this.#seenBits = new Uint8Array(RestReceiveWindow.WINDOW_COUNTERS / 8);\n  }\n\n  /**\n   * Judge one AUTHENTICATED counter. Returns on acceptance; throws otherwise.\n   *\n   * The four outcomes, in the order they are decided — and the order is\n   * load-bearing. Below-window is tested BEFORE the bit, because a counter that\n   * has fallen out of the window reads a bit that now belongs to the counter\n   * 1024 positions later: whatever that bit says is about a different counter,\n   * so the range check has to answer first.\n   */\n  admit(counter: bigint): void {\n    // A counter outside the range a nonce can even encode. `unsealUnchecked`\n    // reads its counter with `readBigUInt64BE`, so this is unreachable from the\n    // wire and is here for every other caller: a bigint is not bounded by its\n    // type the way a `uint64` field is.\n    if (counter < 0n || counter > MAX_COUNTER) {\n      throw new RecordError(\n        E2EE_SEQUENCE_VIOLATION,\n        `record counter ${counter} is outside the range of a record nonce`,\n      );\n    }\n\n    if (counter > this.#highWater) {\n      this.#advanceTo(counter);\n      this.#markSeen(counter);\n      return;\n    }\n\n    if (this.#belowWindow(counter)) {\n      throw new RecordError(\n        E2EE_CTX_UNKNOWN,\n        `record counter ${counter} is further behind than this context tracks; re-open and retry`,\n      );\n    }\n\n    if (this.#isSeen(counter)) {\n      throw new RecordError(\n        E2EE_SEQUENCE_VIOLATION,\n        `record counter ${counter} has already been received`,\n      );\n    }\n\n    this.#markSeen(counter);\n  }\n\n  // ── the window ───────────────────────────────────────────────────\n  //\n  // The same ARITHMETIC as `RestResponseSealer`'s answered bitmap, deliberately:\n  // the two halves of §13 have to agree about what \"inside the window\" means,\n  // and `#bit`, `#belowWindow` and `#advanceTo` agree by being the same\n  // computation rather than by two independent derivations that happen to match\n  // today.\n  //\n  // **The guard placement deliberately differs, and this is the one difference.**\n  // `RestResponseSealer.isAnswered` re-tests the range itself and returns\n  // `false` outside it; `#isSeen` does not, because `admit` has already\n  // established both preconditions before it asks. Neither shape can fail open:\n  // both rejection tests sit in front of `#markSeen`, so a counter failing\n  // either one never reaches acceptance under any ordering of the two. What the\n  // order decides is *which* §9 code a below-window counter with a stale set bit\n  // receives — and §9 requires the recoverable `E2EE_CTX_UNKNOWN`, because a\n  // counter whose slot now belongs to a counter 1024 positions later is one we\n  // cannot prove anything about, and `E2EE_SEQUENCE_VIOLATION` is a claim about\n  // the peer. `__tests__/e2ee-rest-window.test.ts::decides below-window before\n  // it reads the bit` pins that.\n  //\n  // The helpers are `#private` as well as the state — `#advanceTo` clears bits,\n  // so a reachable one is the same re-arming assignment the fields are private\n  // to prevent.\n\n  #bit(counter: bigint): { index: number; mask: number } {\n    const position = Number(counter % BigInt(RestReceiveWindow.WINDOW_COUNTERS));\n    return { index: position >> 3, mask: 1 << (position & 7) };\n  }\n\n  #belowWindow(counter: bigint): boolean {\n    return (\n      this.#highWater >= 0n &&\n      counter + BigInt(RestReceiveWindow.WINDOW_COUNTERS) <= this.#highWater\n    );\n  }\n\n  /**\n   * Whether this counter is recorded as received.\n   *\n   * **Only meaningful at or below the high-water mark and inside the window.**\n   * Bits are indexed modulo the width, so a counter outside that range reads a\n   * bit belonging to a different counter entirely. `admit` establishes both\n   * before it asks, and this deliberately does NOT re-test them the way\n   * `RestResponseSealer.isAnswered` does — see the block comment above.\n   */\n  #isSeen(counter: bigint): boolean {\n    const { index, mask } = this.#bit(counter);\n    return (this.#seenBits[index] & mask) !== 0;\n  }\n\n  #markSeen(counter: bigint): void {\n    const { index, mask } = this.#bit(counter);\n    this.#seenBits[index] |= mask;\n  }\n\n  /**\n   * Slide the window forward, CLEARING every position it newly covers.\n   *\n   * The clear is the load-bearing half. Bits are indexed modulo the width, so\n   * without it counter `c + 1024` reads the bit `c` set and is refused as a\n   * replay it has nothing to do with — a legitimate request rejected, and one\n   * that stays rejected for as long as the client keeps counting.\n   *\n   * The cost is O(min(delta, width)), never O(delta): an advance of more than\n   * one full width has overwritten every position anyway, so it clears the\n   * whole bitmap in one call instead of looping. A context whose client jumps\n   * 10^9 counters must not spend 10^9 iterations proving that.\n   */\n  #advanceTo(counter: bigint): void {\n    const width = BigInt(RestReceiveWindow.WINDOW_COUNTERS);\n    if (this.#highWater < 0n || counter - this.#highWater >= width) {\n      this.#seenBits.fill(0);\n    } else {\n      for (let c = this.#highWater + 1n; c <= counter; c++) {\n        const { index, mask } = this.#bit(c);\n        this.#seenBits[index] &= ~mask;\n      }\n    }\n    this.#highWater = counter;\n  }\n}\n","import type { MiddlewareHandler } from \"hono\";\nimport { validateApiKey } from \"../../auth\";\nimport { parseCapabilities } from \"../../db/repositories/devices.repository\";\nimport {\n  authenticateContext,\n  contextRegistry,\n  E2EE_WS_SUBPROTOCOL,\n  parseSubprotocols,\n  refuseUnsealedIfPinned,\n  TICKET_HEADER,\n  TICKET_SUBPROTOCOL_PREFIX,\n} from \"../../e2ee/context\";\nimport { E2EE_DEVICE_REVOKED } from \"../../e2ee/protocol\";\nimport { getLogger } from \"../../logger\";\nimport {\n  hasCapability,\n  legacyPrincipal,\n  type Principal,\n  requiredCapability,\n} from \"../../services/security/capabilities\";\nimport type { AppEnv } from \"../app\";\nimport type { ApiDeps } from \"../types/api-deps\";\n\nconst log = getLogger(\"e2ee\");\n\n/** The one path a WebSocket ticket authenticates. */\nconst WS_PATH = \"/ws\";\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\n// `/healthz` is open to LOCAL callers only — the menubar poll, the deploy\n// healthcheck and the updater's restart probe all hit\n// `http://127.0.0.1:8766/healthz` with no credential and must keep working.\n// `remoteAddress` cannot separate those from an external probe: behind the\n// Cloudflare tunnel every request arrives from 127.0.0.1 and the streamer reads\n// no forwarded-IP header (see `e2ee.routes.ts` §8). But cloudflared injects\n// `Cf-Connecting-Ip` on every request it proxies, and a genuine loopback caller\n// sets none — so the header's PRESENCE is the \"came through the tunnel\" signal.\n// A tunneled `/healthz` therefore falls through to the same key/e2ee gate as\n// every other route (encrypted context, Bearer, or `?key=`), while a local one\n// stays open. An external probe cannot strip the header (cloudflared adds it at\n// its own edge), and a local process forging it only denies its own `/healthz`\n// — never a bypass.\nconst HEALTHZ_PATH = \"/healthz\";\nconst CF_TUNNEL_HEADER = \"cf-connecting-ip\";\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.\n// /api/e2ee/open is public for the same reason /api/pair/exchange is: the\n// handshake IS the authentication, and the credential the middleware would look\n// for only exists inside it (design.md §3.5, NONCE-DESIGN §10).\nconst PUBLIC_POST_PATHS = new Set([\"/api/pair/exchange\", \"/api/__update\", \"/api/e2ee/open\"]);\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\" | \"devicesRepo\">): 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    // A local `/healthz` (no cloudflared header) is open; a tunneled one falls\n    // through to the gate below, where an e2ee context, a Bearer, or a `?key=`\n    // authenticates it and a bare probe gets 401.\n    const isLocalHealthz = path === HEALTHZ_PATH && c.req.header(CF_TUNNEL_HEADER) === undefined;\n    if (isLocalHealthz || 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    // `--local-no-auth` gives any loopback caller the owner's authority. It used\n    // to grant that by returning next() right here — before the capability check\n    // below and before c.set(\"principal\") — so the capability layer saw no\n    // principal at all, and the WebSocket guard took its null-principal path\n    // instead of an authorization decision. Resolving the caller to the owner\n    // principal leaves the access identical and puts it through the same checks\n    // as every other caller.\n    const loopbackOwner = deps.localNoAuth && isLocalRequest(remoteAddr);\n\n    const authorization = c.req.header(\"authorization\");\n    const bearer = authorization?.startsWith(\"Bearer \") ? authorization.slice(7) : undefined;\n    const queryKey = c.req.query(\"key\") ?? undefined;\n    const presented = bearer ?? queryKey;\n\n    // ─── A ticketed WebSocket upgrade authenticates by its ticket ───────\n    //\n    // NONCE-DESIGN §13: **no `Authorization` travels on a ticketed upgrade.**\n    // The ticket came out of a Noise handshake against the device's own static\n    // key and resolves to a context that names the device, so the long-term\n    // credential has nothing left to prove — and sending it anyway would put a\n    // device token on the wire on every reconnect, which is the exact leak the\n    // sealed-credential ordering exists to close.\n    //\n    // **The consume is a synchronous map delete with no `await` in front of\n    // it** (§14). Nothing above this line awaits, and `consumeTicket` reads and\n    // deletes in one uninterrupted turn — so two concurrent upgrades presenting\n    // one ticket cannot both see it, whatever order Node runs them in.\n    //\n    // A ticket that does not resolve — spent, expired, or lost to a restart —\n    // is terminal for THIS upgrade. Falling through to a bearer or `?key=`\n    // would turn a failed sealed attempt into a legacy plaintext socket. The\n    // client recovers by opening a new context and ticket.\n    // Mirror @hono/node-ws's upgrade predicate exactly: looser spends a ticket\n    // without a socket; stricter can downgrade an upgrade to legacy auth.\n    const isWsUpgrade =\n      method === \"GET\" &&\n      path === WS_PATH &&\n      c.req.header(\"upgrade\")?.toLowerCase() === \"websocket\";\n    //\n    // A browser presents the same ticket as a `tb-ticket.<ticket>` subprotocol\n    // offer (see `E2EE_WS_SUBPROTOCOL`). Everything above holds for it\n    // unchanged: the parse below is synchronous, and the consume is the same\n    // call. Offering `threadbase-e2ee-v1` at all declares a sealed attempt, so\n    // an offer with no ticket is refused rather than served as legacy — that is\n    // also what makes the static protocol selection in `mountWebSocket` honest:\n    // the protocol can only be selected on an upgrade a ticket authenticated.\n    const offered = isWsUpgrade ? parseSubprotocols(c.req.header(\"sec-websocket-protocol\")) : [];\n    if (offered === null) return c.json({ error: \"Bad Request\" }, 400);\n    const presentedTickets = offered\n      .filter((p) => p.startsWith(TICKET_SUBPROTOCOL_PREFIX))\n      .map((p) => p.slice(TICKET_SUBPROTOCOL_PREFIX.length));\n    const headerTicket = isWsUpgrade ? c.req.header(TICKET_HEADER) : undefined;\n    if (headerTicket !== undefined) presentedTickets.push(headerTicket);\n    if (presentedTickets.length > 1) {\n      // Two tickets — header plus subprotocol, or two offers — is refused, not\n      // resolved by preferring one: the same rule as a ticket beside a\n      // credential naming another device, since \"which one authenticates\"\n      // should never be a precedence a client can be unsure of. Every presented\n      // ticket is still spent, and a context this orphans is destroyed, exactly\n      // as the capability refusal below does: only a caller holding a ticket\n      // can resolve one, so the destroy is never someone else's to trigger.\n      const registry = contextRegistry();\n      for (const presented of presentedTickets) {\n        const spent = registry.consumeTicket(presented);\n        if (spent) registry.destroy(spent);\n      }\n      return c.json({ error: \"Unauthorized\" }, 401);\n    }\n    const ticket = presentedTickets[0] ?? (offered.includes(E2EE_WS_SUBPROTOCOL) ? \"\" : undefined);\n    if (ticket !== undefined) {\n      const registry = contextRegistry();\n      const ctxId = registry.consumeTicket(ticket);\n      const context = ctxId ? registry.get(ctxId) : null;\n      if (!context) return c.json({ error: \"Unauthorized\" }, 401);\n      // The shared tail, in `context.ts` because the REST unseal middleware\n      // runs exactly the same decision off `X-TB-Ctx`: device row, per-request\n      // `revoked_at`, credential-must-name-the-same-device, principal built\n      // from the CONTEXT. The helper returns a pure verdict; each caller maps\n      // the reason to its own response and applies the permitted lifecycle\n      // effect.\n      const auth = authenticateContext({\n        context,\n        devicesRepo: deps.devicesRepo(),\n        presented,\n      });\n      if (!auth.ok) {\n        // **Mapping the verdict to an answer is the CALLER's job**, which is\n        // why the helper returns a `reason` and no status: the REST\n        // middleware maps the same three onto HTTP, this maps them onto an\n        // upgrade refusal, and neither imposes its policy on the other.\n        //\n        // **Destroy only on the unforgeable trigger.** `revoked_at` (and a\n        // missing row) is a fact in our own database; a mismatched credential\n        // is a header an attacker chose, and destroying on it turns the\n        // safeguard into the weapon — on the REST channel `ctxId` travels in\n        // a plaintext header, so anyone on path could forge a credential\n        // beside an observed id and kill that device's context on repeat.\n        //\n        // Destroying here on a mismatch would in fact be harmless, since this\n        // ticket is already spent and there is no second attempt to deny. It\n        // is still not done, for SYMMETRY with the REST caller: a helper\n        // whose two consumers apply different effects to one verdict is how\n        // the dangerous behaviour gets copied from the harmless one later.\n        if (auth.reason === \"device-revoked\") registry.destroy(context.ctxId);\n        log.warn(\"[e2ee] upgrade refused by context authentication\", {\n          event: \"e2ee.upgrade_refused\",\n          reason: auth.reason,\n        });\n        if (auth.reason === \"device-revoked\") {\n          return c.json({ error: \"This device is not paired\", code: E2EE_DEVICE_REVOKED }, 403);\n        }\n        if (auth.reason === \"no-device-store\") {\n          // Transient, and deliberately NOT `E2EE_DEVICE_REVOKED`: §9 defines\n          // that code as a hard failure the client must never retry, and a\n          // registry that could not be read says nothing about the pairing.\n          // Same shape `/api/devices` already uses when its repo is missing.\n          return c.json(\n            { error: \"Device registry is unavailable\", code: \"STORE_UNAVAILABLE\" },\n            503,\n          );\n        }\n        return c.json({ error: \"Unauthorized\" }, 401);\n      }\n\n      c.set(\"e2eeContext\", context);\n      // A ticket AUTHENTICATES; it does not authorize. The capability check is\n      // the same one every other caller passes.\n      const required = requiredCapability(path, method);\n      if (required !== null && !hasCapability(auth.principal, required)) {\n        // **Destroy the context this refusal orphans.** `consumeTicket`\n        // already spent the ticket AND called `markUsed`, which promotes the\n        // context out of provisional and moves its deadline from the 30 s\n        // ticket TTL to the full 24 h lifetime. Refusing without destroying\n        // therefore leaves a context nobody can ever attach to — the ticket is\n        // gone — sitting for a day and counting against the device's cap,\n        // which is the opposite of what promoting it was for.\n        //\n        // Safe on this channel: the trigger is our own capability table, not\n        // a header an attacker chose, and the ticket is already spent so\n        // there is no second attempt to deny.\n        registry.destroy(context.ctxId);\n        return c.json({ error: \"Forbidden\", code: \"MISSING_CAPABILITY\", required }, 403);\n      }\n      try {\n        deps.devicesRepo()?.touch(auth.principal.deviceId);\n      } catch {\n        // A liveness stamp must never deny a valid upgrade.\n      }\n      c.set(\"principal\", auth.principal);\n      await next();\n      return;\n    }\n\n    // ─── A sealed REST request already resolved its own principal ──────\n    //\n    // `e2eeEnvelopeMiddleware` runs ahead of this one (D-9: the unseal sits in\n    // front of authentication so the credential travels sealed) and, on a\n    // request that unsealed under `X-TB-Ctx`, has already set the principal\n    // from the CONTEXT's device row after re-checking `revoked_at`. Resolving\n    // it again here would be worse than redundant: a sealed request carries no\n    // `Authorization` at all (§13(b)), so the resolution below would find no\n    // credential and 401 a request that authenticated correctly.\n    //\n    // **What is skipped is credential RESOLUTION, and nothing else.** The\n    // capability check still runs, on the context's principal, exactly as it\n    // does for every other caller. Read this as \"skip the rest of the\n    // middleware\" and a read-only device gains full write authority the moment\n    // it seals — `c.set(\"principal\", …)` below happens AFTER the capability\n    // gate, and not at all on the `required === null` early return, so a\n    // principal being present says nothing about authority having been checked.\n    //\n    // The 403 this can produce is sealed on the way out by the envelope\n    // middleware's response path, like every other answer that request is owed.\n    // A few focused middleware harnesses provide only the pre-existing\n    // Context surface; production Hono contexts always provide `get`.\n    const contextPrincipal = c.get?.(\"principal\");\n    if (contextPrincipal) {\n      const requiredForContext = requiredCapability(path, method);\n      if (requiredForContext !== null && !hasCapability(contextPrincipal, requiredForContext)) {\n        return c.json(\n          { error: \"Forbidden\", code: \"MISSING_CAPABILITY\", required: requiredForContext },\n          403,\n        );\n      }\n      await next();\n      return;\n    }\n\n    if (!presented && !loopbackOwner) {\n      return c.json({ error: \"Unauthorized\" }, 401);\n    }\n\n    // Resolve the caller to a principal (C5).\n    //\n    // Device tokens are tried FIRST so a device credential is never mistaken\n    // for the shared key, then the shared key falls back to a `legacy`\n    // principal holding the full preset. Keeping the shared key working is what\n    // lets this ship without breaking every already-paired device.\n    let principal: Principal | null = null;\n\n    const device = presented ? (deps.devicesRepo()?.authenticate(presented) ?? null) : null;\n    if (device) {\n      principal = {\n        kind: \"device\",\n        deviceId: device.device_id,\n        capabilities: parseCapabilities(device.capabilities),\n      };\n      // Best-effort liveness stamp; a failure here must not deny a valid request.\n      try {\n        deps.devicesRepo()?.touch(device.device_id);\n      } catch {\n        // ignore\n      }\n    } else if (presented && validateApiKey(presented, deps.apiKey)) {\n      principal = legacyPrincipal();\n    } else if (loopbackOwner) {\n      // No credential needed from loopback under the flag — and a wrong one does\n      // not demote it either, which is what the old unconditional bypass did.\n      principal = legacyPrincipal();\n    }\n\n    if (!principal) {\n      return c.json({ error: \"Unauthorized\" }, 401);\n    }\n\n    // ─── Downgrade enforcement: 426, never 401 (design.md §6.3) ────────\n    //\n    // We are on the PLAINTEXT branch by construction — the sealed branch\n    // returned above with a context principal — so a caller that reaches here\n    // resolved a credential and presented no `X-TB-Ctx`. If that credential\n    // names a device whose row is pinned (`e2ee_required`), it has already\n    // completed a handshake once and must never be served in the clear again.\n    //\n    // **426 and not 401, and the difference is not cosmetic.**\n    // `docs/compatibility/tb-mobile.md` maps 401 onto the re-authentication UI,\n    // so answering a downgrade with 401 would send a correctly-paired phone to\n    // a login screen it cannot satisfy — the credential is fine; the transport\n    // is not. 426 tells it to open a context and retry, which is a thing it can\n    // actually do.\n    //\n    // Through W1a's shared helper, never a second implementation: two copies of\n    // a downgrade rule is one copy that can be forgotten. Its stated limit\n    // carries over unchanged — a pinned device presenting the SHARED api key\n    // resolves to `legacy` with no device row, so the pin cannot bite. That is\n    // the stage-3 shared-key problem and it is R's, not this PR's.\n    const downgrade = refuseUnsealedIfPinned({\n      principal,\n      devicesRepo: deps.devicesRepo?.(),\n      context: c.get?.(\"e2eeContext\"),\n    });\n    if (downgrade) {\n      // The code and the status, and nothing about the request. A downgrade\n      // refusal names no body, no key and no context.\n      log.warn(\"[e2ee] pinned device refused a plaintext request\", {\n        event: \"e2ee.rest_downgrade_refused\",\n      });\n      return c.json(downgrade.body, downgrade.status);\n    }\n\n    // Capability check.\n    const required = requiredCapability(path, method);\n    if (required === null) {\n      // Authenticated but unclassified. Fall through to the router rather than\n      // denying: an unknown path must still 404, because answering 403 would\n      // tell an authenticated caller that a route it cannot name might exist.\n      //\n      // This is NOT a hole — every mounted /api route is classified, and a test\n      // asserts that. A genuinely new route added without a mapping reaches its\n      // handler, so the fail-closed guarantee lives in that test rather than in\n      // a runtime deny that would break 404s.\n      await next();\n      return;\n    }\n    if (!hasCapability(principal, required)) {\n      return c.json({ error: \"Forbidden\", code: \"MISSING_CAPABILITY\", required }, 403);\n    }\n\n    c.set(\"principal\", principal);\n    await next();\n  };\n","// The REST envelope: unseal on the way in, seal on the way out.\n//\n// Written against specs/end-to-end-encryption/NONCE-DESIGN.md §4, §5, §9, §10\n// and §13, design.md §3.2/§3.4/§3.6, and dilemmas D-7 and D-9.\n//\n// **It sits between `corsMiddleware` and `authMiddleware`** (`api/app.ts`).\n// After CORS, because a preflight must be answerable without a context — an\n// `OPTIONS` carries no `X-TB-Ctx` and never will. Before auth, because that is\n// the whole point of the ordering: the credential travels *inside* the envelope\n// (D-9, §13(b)), so authentication cannot run until the body is plaintext.\n//\n// That places it on the pre-authentication attack surface, which is what the\n// ladder below is for. **The one line everything follows from:**\n//\n//   > every refusal before a successful `unsealRequest` is plaintext;\n//   > everything from a successful `unsealRequest` onward is sealed.\n//\n// A rejection must never carry a sealed body, because a sealed body would mean\n// spending a response counter on a request that was never accepted — and §13(a)\n// makes \"at most one sealed response per accepted request counter\" the rule\n// nonce uniqueness for `(k_s2c, 2‖counter)` rests on.\n//\n// **This middleware never touches `sealer.accept`.** `Context.unsealRequest` is\n// its sole caller, and that is what keeps the receive window's high-water mark\n// and the response sealer's acceptance set in lockstep. A second caller\n// anywhere produces requests that are accepted and can never be answered.\n\nimport type { MiddlewareHandler } from \"hono\";\nimport type { ContentfulStatusCode } from \"hono/utils/http-status\";\nimport { IncomingMessage, type OutgoingHttpHeaders, type ServerResponse } from \"http\";\nimport { authenticateContext, contextRegistry, type E2eeContextRegistry } from \"../../e2ee/context\";\nimport {\n  E2EE_CTX_UNKNOWN,\n  E2EE_DEVICE_REVOKED,\n  E2EE_SEAL_FAILED,\n  E2EE_SEQUENCE_VIOLATION,\n  type E2eeRejectionCode,\n} from \"../../e2ee/protocol\";\nimport {\n  CTX_ID_BYTES,\n  MAX_RECORD_BYTES,\n  RecordError,\n  restTargetHashFromUrl,\n} from \"../../e2ee/record\";\nimport { getLogger } from \"../../logger\";\nimport { hasCapability, requiredCapability } from \"../../services/security/capabilities\";\nimport type { AppEnv } from \"../app\";\nimport type { ApiDeps } from \"../types/api-deps\";\nimport { errorMiddleware } from \"./error.middleware\";\n\nconst log = getLogger(\"e2ee\");\n\n/** Routes that write to `c.env.outgoing` themselves return this sentinel. */\nconst ALREADY_HANDLED = 597;\n\n/** Pure marker: its presence says \"this request is sealed\", nothing more. */\nexport const HEADER_MARKER = \"x-tb-e2ee\";\n/** The `ctxId`, base64url unpadded — 16 bytes is exactly 22 characters. */\nexport const HEADER_CTX = \"x-tb-ctx\";\n/** The request counter, decimal. Read early (§9 needs it), acted on late (§5). */\nexport const HEADER_SEQ = \"x-tb-seq\";\n/**\n * A sealed record whose HTTP framing cannot carry a body, base64url.\n *\n * The rule is one rule in both directions: bodiless requests (React Native\n * drops a `GET` body) and the one bodiless response (`304`) put the record\n * here; everything whose framing allows a body puts it in the body.\n */\nexport const HEADER_ENVELOPE = \"x-tb-env\";\n\n/** 16 raw bytes, base64url unpadded. Checked as a shape, never decoded. */\nconst CTX_ID_PATTERN = new RegExp(`^[A-Za-z0-9_-]{${Math.ceil((CTX_ID_BYTES * 4) / 3)}}$`);\n\n/**\n * A decimal counter that can fit `2^64 - 1`, checked before `BigInt` sees it.\n *\n * `BigInt(\"1e999\")` and `BigInt(\" 12 \")` both succeed, and `BigInt` on a long\n * digit string is not free — 20 digits is the width of the largest `uint64`, so\n * anything longer cannot be a counter and is refused without the conversion.\n */\nconst SEQ_PATTERN = /^(?:0|[1-9][0-9]{0,19})$/;\nconst MAX_SEQ = 2n ** 64n - 1n;\n\n/**\n * Ceiling on the header-carried envelope, in ENCODED characters.\n *\n * A header-carried record answers a bodiless request, whose plaintext is empty\n * or near it: header(30) + tag(16) is 46 bytes, 62 base64url characters. 1024\n * is generous slack and still an order of magnitude under Node's 16 KiB header\n * limit, so this bound is the one that fires rather than a 431 from the parser\n * — which is what makes it testable, and testable is the only way it stays\n * true.\n *\n * **Bounded on the encoded length, not the decoded one**, exactly as\n * `pair-request.ts` argues: `Buffer.from(s, \"base64url\")` allocates in\n * proportion to `s`, so measuring the result performs the allocation the bound\n * exists to prevent.\n */\nexport const MAX_ENVELOPE_HEADER_CHARS = 1024;\n\n/**\n * Ceiling on the body-carried envelope, taken from the record layer rather than\n * repeated: a frame over `MAX_RECORD_BYTES` is refused by `openFrame` anyway,\n * and a second literal is how the two drift into disagreement.\n */\nexport const MAX_ENVELOPE_BODY_BYTES = MAX_RECORD_BYTES;\n\n/** Where the counter sits in a record header (§4): version, ctxId, direction. */\nconst COUNTER_OFFSET = 1 + CTX_ID_BYTES + 4;\n\n/** The §9 code → HTTP status map, in one place so no call site invents one. */\nfunction statusFor(code: E2eeRejectionCode): ContentfulStatusCode {\n  switch (code) {\n    // Recoverable: the client re-handshakes once and retries. 409 rather than\n    // 401, which would send mobile round its re-auth UI for a context that\n    // simply expired.\n    case E2EE_CTX_UNKNOWN:\n      return 409;\n    case E2EE_DEVICE_REVOKED:\n      return 403;\n    default:\n      return 400;\n  }\n}\n\n/**\n * Whether a response with this status can carry a body at all.\n *\n * `304` and `204` cannot, and Node will silently drop anything written to one.\n * A sealed record for such a response therefore travels in `X-TB-Env` — the\n * same frozen rule the request side uses for a bodiless `GET`. The CORS\n * preflight's `204` never reaches here: `corsMiddleware` answers it first.\n */\nfunction canCarryBody(status: number): boolean {\n  return status !== 204 && status !== 304 && status >= 200;\n}\n\n/**\n * Read the body, refusing one that grows past `maxBytes`, memory flat.\n *\n * The same shape `e2ee.routes.ts` uses on `/api/e2ee/open` and for the same\n * reason (§10): the shared `readBody` concatenates every chunk before anything\n * looks at a size, so on a pre-authentication path a multi-GB POST is fully\n * buffered before any bound applies. `Content-Length` refuses the honest\n * oversized body before a byte arrives; the running total refuses the dishonest\n * one, and past the cap the chunks are DROPPED as they arrive rather than\n * collected — the connection stays healthy enough to carry the 413 back.\n */\nclass BodyTooLarge extends Error {}\n\nfunction readBoundedBody(req: IncomingMessage, maxBytes: number): Promise<Buffer> {\n  return new Promise((resolve, reject) => {\n    let chunks: Buffer[] = [];\n    let size = 0;\n    let refused = false;\n    req.on(\"data\", (chunk: Buffer) => {\n      if (refused) return;\n      size += chunk.length;\n      if (size > maxBytes) {\n        refused = true;\n        chunks = [];\n        reject(new BodyTooLarge(\"sealed request body is too large\"));\n        return;\n      }\n      chunks.push(chunk);\n    });\n    req.on(\"end\", () => {\n      if (refused) return;\n      resolve(Buffer.concat(chunks));\n    });\n    req.on(\"error\", reject);\n  });\n}\n\n/**\n * Hand the plaintext to BOTH body-read paths in this codebase.\n *\n * There are two, and a middleware that satisfies one and not the other ships a\n * hole shaped like whichever route it did not test:\n *\n *   1. ~15 handlers read the raw Node request — `readBody(c.env.incoming)` in\n *      `sessions.handlers.ts` and friends. The original `IncomingMessage` has\n *      already been drained to ciphertext by the time we get here and cannot be\n *      re-pushed after `end`, so `c.env.incoming` is REPLACED by a real\n *      `IncomingMessage` carrying the plaintext. `env` is a fresh object\n *      literal per request inside `@hono/node-server`, so the assignment is\n *      local to this request.\n *   2. three routes read `c.req.arrayBuffer()`. Hono answers that from\n *      `HonoRequest.bodyCache`, which short-circuits `raw` entirely — so\n *      seeding the cache is what makes `arrayBuffer()`, `text()` and `json()`\n *      all resolve to the plaintext (`#cachedBody` converts between them\n *      through `new Response(body)[key]()`).\n *\n * `Content-Length` is restated and `Transfer-Encoding` dropped, because the\n * plaintext is a different length from the record that carried it and a handler\n * that trusts the header would otherwise be told the ciphertext's size.\n */\nfunction replaceRequestBody(\n  c: Parameters<MiddlewareHandler<AppEnv>>[0],\n  original: IncomingMessage,\n  plaintext: Buffer,\n): void {\n  const replacement = new IncomingMessage(original.socket);\n  replacement.url = original.url;\n  replacement.method = original.method;\n  replacement.httpVersion = original.httpVersion;\n  replacement.httpVersionMajor = original.httpVersionMajor;\n  replacement.httpVersionMinor = original.httpVersionMinor;\n  replacement.rawHeaders = original.rawHeaders;\n  // Spread copies OWN enumerable properties only, so this carries nothing down\n  // from a polluted `Object.prototype` (measured). `url`, `method`,\n  // `httpVersion*` and `rawHeaders` above are own properties of a live\n  // `IncomingMessage` and are read directly for the same reason.\n  const headers = { ...original.headers };\n  delete headers[\"transfer-encoding\"];\n  headers[\"content-length\"] = String(plaintext.byteLength);\n  replacement.headers = headers;\n  // Everything this stream will ever hold is pushed below, so there is nothing\n  // to pull. `IncomingMessage.prototype._read` resumes the underlying socket,\n  // which this object does not own — it is the real request's socket, already\n  // drained and managed by the http parser.\n  (replacement as unknown as { _read: () => void })._read = () => {};\n  replacement.push(plaintext);\n  replacement.push(null);\n  // **`complete` is load-bearing, not cosmetic.** `IncomingMessage._destroy`\n  // reads it as \"was this message fully received\", and a stream that ends\n  // without it is treated as ABORTED — which destroys `this.socket`, and this\n  // object's socket is the real connection. The symptom is a socket hang up on\n  // exactly the requests whose handler read the body through `c.env.incoming`,\n  // with the response already written; everything else passes.\n  replacement.complete = true;\n  c.env.incoming = replacement;\n\n  // `bodyCache` is typed `Partial<Body>`, so its `arrayBuffer` slot types as\n  // the METHOD; at runtime `#cachedBody` stores and awaits a PROMISE there.\n  // The cast is to the runtime contract, not around it.\n  const cache = c.req.bodyCache as { arrayBuffer?: Promise<ArrayBuffer> };\n  cache.arrayBuffer = Promise.resolve(\n    plaintext.buffer.slice(\n      plaintext.byteOffset,\n      plaintext.byteOffset + plaintext.byteLength,\n    ) as ArrayBuffer,\n  );\n}\n\n/**\n * Buffer everything a direct-write handler produces and seal it once, at `end`.\n *\n * ~34 routes write straight to `c.env.outgoing` and return the 597\n * `ALREADY_HANDLED` sentinel, so Hono never sees their response at all. The\n * `write`/`end` pair is the same seam `countResponseBytes` already patches in\n * `app.ts` — that one only counts, this one has to buffer and rewrite the\n * framing (§13's \"not settled here\" list names exactly this).\n *\n * The patches are installed BEFORE `next()`, because a handler can write and\n * end before `next()` returns (several routes do not even await their handler).\n *\n * **The ndjson `stop` stream is buffered like everything else, deliberately.**\n * `POST /api/sessions/:id/stop` is the only `application/x-ndjson` in the tree;\n * it writes a `stopping` line, waits up to 5 s, then writes `stopped` or\n * `timeout`. Sealing per line is NOT available: the AAD is fixed-width with no\n * index field (§4), and the sealer permits exactly one seal per accepted\n * counter (§13(a)) — so a second line would either reuse `(k_s2c, 2‖counter)`\n * or be refused. There is no added wall-clock latency, since the response\n * completes when the handler ends either way. What is lost is the EARLY ARRIVAL\n * of the intermediate `stopping` line: the client learns the stop began only\n * when it learns how it finished. Multi-record ndjson needs an envelope version\n * with an index field, which is a protocol change and not this one.\n */\nfunction interceptDirectWrite(\n  outgoing: ServerResponse,\n  seal: (\n    status: number,\n    headers: OutgoingHttpHeaders,\n    plaintext: Buffer,\n  ) => { status: number; headers: OutgoingHttpHeaders; body?: Buffer },\n): () => void {\n  let status = 200;\n  let headers: OutgoingHttpHeaders = {};\n  const chunks: Buffer[] = [];\n  let done = false;\n\n  // The PREVIOUS functions, not the prototype's: `countResponseBytes` in\n  // `app.ts` has already patched `write`/`end` by the time this runs, and its\n  // byte count has to keep seeing what actually goes out — which is the sealed\n  // record, since that is what the client receives.\n  const prevWriteHead = outgoing.writeHead;\n  const prevWrite = outgoing.write;\n  const prevEnd = outgoing.end;\n  const prevSetHeader = outgoing.setHeader;\n\n  const collect = (chunk: unknown) => {\n    if (typeof chunk === \"string\") chunks.push(Buffer.from(chunk));\n    else if (chunk instanceof Uint8Array) chunks.push(Buffer.from(chunk));\n  };\n\n  outgoing.writeHead = function (this: ServerResponse, code: number, ...rest: unknown[]) {\n    status = code;\n    for (const arg of rest) {\n      if (arg && typeof arg === \"object\") headers = { ...headers, ...(arg as OutgoingHttpHeaders) };\n    }\n    return this;\n  } as typeof outgoing.writeHead;\n\n  // Only the three framing headers are swallowed. Everything else — `ETag`,\n  // `Accept-Query`, the CORS headers `corsMiddleware` already set — is metadata\n  // that stays plaintext by design (§3.2) and must reach the wire unchanged.\n  outgoing.setHeader = function (this: ServerResponse, name: string, value: never) {\n    const lower = name.toLowerCase();\n    if (lower === \"content-type\" || lower === \"content-length\" || lower === \"transfer-encoding\") {\n      return this;\n    }\n    return (prevSetHeader as (n: string, v: never) => ServerResponse).call(this, name, value);\n  } as typeof outgoing.setHeader;\n\n  outgoing.write = function (this: ServerResponse, ...args: unknown[]) {\n    collect(args[0]);\n    const cb = args.find((a) => typeof a === \"function\") as (() => void) | undefined;\n    cb?.();\n    return true;\n  } as typeof outgoing.write;\n\n  outgoing.end = function (this: ServerResponse, ...args: unknown[]) {\n    // A second `end()` is a no-op, not a second seal.\n    //\n    // **This fires on every direct-write route, not on a hypothetical handler\n    // that ends twice.** `responseViaResponseObject` in `@hono/node-server`\n    // takes the 597 sentinel `Response(null, …)`, finds no body and no\n    // `x-already-sent`, and calls `outgoing.writeHead(597, …)` then\n    // `outgoing.end()` — after the handler has already written and ended.\n    // Today that second `writeHead` throws `ERR_HTTP_HEADERS_SENT` and\n    // node-server swallows it in `handleResponseError`; through this patch it\n    // would instead ask the sealer for a SECOND record under a counter already\n    // spent. Removing this line turns every sealed direct-write response into\n    // an unhandled `ERR_HTTP_HEADERS_SENT` (measured: the mutation leaves all\n    // assertions green and the suite exits non-zero on four such rejections).\n    if (done) return this;\n    done = true;\n    collect(args[0]);\n    const cb = args.find((a) => typeof a === \"function\") as (() => void) | undefined;\n    const out = seal(status, headers, Buffer.concat(chunks));\n    (prevWriteHead as (s: number, h: OutgoingHttpHeaders) => ServerResponse).call(\n      this,\n      out.status,\n      out.headers,\n    );\n    const end = prevEnd as (...a: unknown[]) => ServerResponse;\n    if (out.body) end.call(this, out.body, cb);\n    else end.call(this, cb);\n    return this;\n  } as typeof outgoing.end;\n\n  /**\n   * Take the patches back off.\n   *\n   * A Hono-PIPED response is written to `outgoing` by `@hono/node-server` after\n   * this middleware has returned, and it is written through exactly these four\n   * methods. Left installed, they would buffer the response this middleware had\n   * already sealed and ask the sealer for a SECOND record under a counter it\n   * has spent — which the sealer refuses, turning a served request into a 500.\n   * The piped path seals `c.res` instead, and takes these off first.\n   */\n  return () => {\n    outgoing.writeHead = prevWriteHead;\n    outgoing.write = prevWrite;\n    outgoing.end = prevEnd;\n    outgoing.setHeader = prevSetHeader;\n  };\n}\n\nexport const e2eeEnvelopeMiddleware = (\n  /**\n   * The device registry, for §13(b)'s per-request `revoked_at` re-check.\n   *\n   * Read through `deps.devicesRepo()` on every request rather than captured\n   * once: the repository is rebuilt when the database is reopened, and a\n   * captured handle would keep re-checking revocation against a store that is\n   * no longer the live one — which fails OPEN, since a revoked device would\n   * still read as live in the stale copy.\n   */\n  deps: Pick<ApiDeps, \"devicesRepo\">,\n  /** Overridden by tests so a suite gets its own registry rather than the process one. */\n  registry: E2eeContextRegistry = contextRegistry(),\n): MiddlewareHandler<AppEnv> =>\n  async function e2eeEnvelope(c, next) {\n    // ── Rung 1: not a sealed request ────────────────────────────────\n    //\n    // One header read, and then this middleware is not in the picture. An\n    // unpinned device with the shared API key does exactly what it does today:\n    // nothing below has run, no body has been touched, no response method has\n    // been patched.\n    if (c.req.header(HEADER_MARKER) === undefined) return next();\n\n    const refuse = (code: E2eeRejectionCode, message: string, status = statusFor(code)) => {\n      // Plaintext, always: a refusal here is a refusal BEFORE a successful\n      // unseal, so no counter was accepted and no sealed body is owed (§13(a)).\n      // The body carries the code and nothing about what was sent.\n      log.warn(`[e2ee] sealed request refused: ${code}`, { event: \"e2ee.rest_refused\", code });\n      return c.json({ error: message, code }, status);\n    };\n\n    // Not a rung — a precondition. The target hash must come from the raw wire\n    // URL, which only the Node request has; under Hono's test client there is\n    // no `incoming` and no honest way to compute it.\n    const incoming = c.env?.incoming;\n    const outgoing = c.env?.outgoing;\n    if (!incoming || !outgoing) {\n      return refuse(E2EE_SEAL_FAILED, \"sealed requests require the Node request seam\", 400);\n    }\n\n    // ── Rung 2: the ctxId's shape ───────────────────────────────────\n    // Length and charset. Nothing is decoded and nothing is looked up.\n    const ctxId = c.req.header(HEADER_CTX);\n    if (!ctxId || !CTX_ID_PATTERN.test(ctxId)) {\n      return refuse(E2EE_CTX_UNKNOWN, \"X-TB-Ctx is not a context id\");\n    }\n\n    // ── Rung 3: the registry lookup ─────────────────────────────────\n    //\n    // **THE D-9 property.** An unknown `ctxId` costs one `Map.get` and nothing\n    // else: no allocation, and the body is never read. That is why this sits\n    // above every line that touches the request stream rather than below them,\n    // and why the ladder is written as a ladder at all.\n    //\n    // Read it off the structure rather than trusting this sentence: between\n    // rung 1 and this line there is one header read, one `refuse` closure and\n    // two `c.env` property reads. `incoming` is a REFERENCE here and nothing\n    // more — the only call that touches the stream is `readBoundedBody` at rung\n    // 7, and every header this ladder consults is read through `c.req.header`,\n    // which reads the parsed header table and never the stream.\n    const context = registry.get(ctxId);\n    if (!context) return refuse(E2EE_CTX_UNKNOWN, \"no such encrypted context\");\n\n    // ── Rung 4: the channel ─────────────────────────────────────────\n    // A socket context has no window and no response sealer; asking it to\n    // unseal a REST request is a misaddressed frame, not a malformed one.\n    if (context.kind !== \"rest\") {\n      return refuse(E2EE_CTX_UNKNOWN, \"that context is not a REST context\");\n    }\n\n    // ── Rung 5: the counter's shape ─────────────────────────────────\n    //\n    // Read here, ACTED ON at rung 9 — after the AEAD. §5's ordering rule: a\n    // pre-authentication counter check makes `E2EE_SEQUENCE_VIOLATION` an\n    // unauthenticated verdict about the peer and buys no protection, because\n    // the same attacker can as cheaply send garbage carrying the right counter.\n    const seqHeader = c.req.header(HEADER_SEQ);\n    if (!seqHeader || !SEQ_PATTERN.test(seqHeader)) {\n      return refuse(E2EE_SEQUENCE_VIOLATION, \"X-TB-Seq is not a counter\");\n    }\n    const claimedSeq = BigInt(seqHeader);\n    if (claimedSeq > MAX_SEQ) {\n      return refuse(E2EE_SEQUENCE_VIOLATION, \"X-TB-Seq is outside the range of a record nonce\");\n    }\n\n    // ── Rung 5a: exactly one envelope source ────────────────────────\n    //\n    // A presence test, no read. Two sources is not a client this contract\n    // describes: it is someone hoping the two halves are checked by different\n    // code, so that the bound applied to one is skipped on the other. There is\n    // no \"prefer the body\" tie-break, because a tie-break is a choice an\n    // attacker gets to make.\n    //\n    // **Both framing headers are read through `c.req.header`, and that is not\n    // decoration.** Node builds `req.headers` with `Object.prototype`, and a\n    // header absent from the wire is absent as an OWN property, so a bare\n    // `incoming.headers[\"content-length\"]` is a prototype-chain read —\n    // measured on a live request, not reasoned about. Either half of that is an\n    // unauthenticated denial of the whole REST envelope. Both vectors were\n    // MEASURED against a live server rather than reasoned about, and both are\n    // stated at the width they were measured at:\n    //\n    //   - a polluted `transfer-encoding` makes `hasBodyFraming` true for a\n    //     bodiless `GET`, which carries neither framing header on the wire, so\n    //     a legitimate sealed `GET` in `X-TB-Env` is refused as carrying two\n    //     envelopes (400 `E2EE_SEAL_FAILED`);\n    //   - a polluted `content-length` reaches rung 6 for a CHUNKED sealed\n    //     request, which sends no `content-length` at all, and a forged\n    //     declaration over the bound is a 413 before a byte is read.\n    //\n    // Note what is NOT claimed: a request that declares its own\n    // `content-length` is unaffected, because an own property shadows the\n    // prototype. The reachable half is the absent half, which is the general\n    // shape of this bug and the reason it hides — you cannot poison a header\n    // that is there.\n    //\n    // Neither is a bypass; the AEAD and the window are both still below. But\n    // the channel is off the air, it is reachable from any prototype-pollution\n    // sink anywhere in the process by an attacker who never touches the\n    // crypto, and denial is not the lesser outcome.\n    //\n    // The remedy is the framework accessor, not our `own()` helper, and the\n    // difference is that `c.req.header` cannot be refactored back into a\n    // prototype read. `@hono/node-server` 2.1.1 `RequestHeaders#lookupHttp1`\n    // gates on `Object.hasOwn(headers, lowerName)` and returns `null` when the\n    // header is not an own property (`dist/index.mjs:84`); on the paths it\n    // declines — HTTP/2, `set-cookie`, `__proto__`, a non-string raw value — it\n    // falls through to a scan of the raw wire header array, which has no\n    // prototype to poison either. So absence reads as absence by construction\n    // at both layers. `own()` remains correct but is one edit away from being\n    // wrong; this is zero edits away.\n    const envelopeHeader = c.req.header(HEADER_ENVELOPE);\n    const declaredLength = c.req.header(\"content-length\");\n    const hasBodyFraming =\n      c.req.header(\"transfer-encoding\") !== undefined ||\n      (declaredLength !== undefined && declaredLength !== \"0\");\n    if (envelopeHeader !== undefined && hasBodyFraming) {\n      return refuse(E2EE_SEAL_FAILED, \"a sealed request carries one envelope, not two\");\n    }\n    if (envelopeHeader === undefined && !hasBodyFraming) {\n      return refuse(E2EE_SEAL_FAILED, \"a sealed request carries an envelope\");\n    }\n\n    // ── Rungs 6 and 7: the bounds, then the read ────────────────────\n    let frame: Buffer;\n    if (envelopeHeader !== undefined) {\n      // Rung 6, header form: the ENCODED length, before any base64url decode.\n      if (envelopeHeader.length > MAX_ENVELOPE_HEADER_CHARS) {\n        return refuse(E2EE_SEAL_FAILED, \"X-TB-Env is too large\", 413);\n      }\n      frame = Buffer.from(envelopeHeader, \"base64url\");\n      // `Buffer.from` never throws on base64url — it discards what it cannot\n      // decode, so garbage becomes a short buffer rather than an error.\n      if (frame.byteLength === 0) {\n        return refuse(E2EE_SEAL_FAILED, \"X-TB-Env is not valid base64url\");\n      }\n    } else {\n      // Rung 6, body form: the declared length, before a byte is read.\n      const declared = Number(declaredLength);\n      if (Number.isFinite(declared) && declared > MAX_ENVELOPE_BODY_BYTES) {\n        return refuse(E2EE_SEAL_FAILED, \"sealed request body is too large\", 413);\n      }\n      try {\n        // Rung 7: and the running total, for a sender that lied.\n        frame = await readBoundedBody(incoming, MAX_ENVELOPE_BODY_BYTES);\n      } catch (err) {\n        if (err instanceof BodyTooLarge) {\n          return refuse(E2EE_SEAL_FAILED, err.message, 413);\n        }\n        return refuse(E2EE_SEAL_FAILED, \"could not read the sealed request body\", 400);\n      }\n    }\n\n    // ── Rung 8: the AEAD ────────────────────────────────────────────\n    //\n    // **The target comes from the RAW wire URL** — the bytes Node received —\n    // and never from `c.req.path`, which is percent-decoded, nor from a\n    // re-serialised query, whose order and escaping do not round-trip (§4).\n    // `/api/conversations/a%2Fb` and `/api/conversations/a/b` are different\n    // targets and must hash differently; the fixture\n    // `restTargetCanonicalization.decodedPathMustDiffer` is the authority.\n    const target = restTargetHashFromUrl(incoming.method ?? c.req.method, incoming.url ?? \"\");\n    let plaintext: Buffer;\n    try {\n      // `unsealRequest` runs the AEAD, then the window, then the sealer's\n      // acceptance — and it is the SOLE caller of `sealer.accept`. This\n      // middleware never reaches for the sealer itself: the window's high-water\n      // mark and the sealer's acceptance set stay in lockstep only because one\n      // call site advances both.\n      plaintext = context.unsealRequest(frame, target);\n    } catch (err) {\n      if (err instanceof RecordError) return refuse(err.code, \"could not unseal the request\");\n      throw err;\n    }\n\n    // ── Rung 9: the AUTHENTICATED counter against the claimed one ───\n    //\n    // Now, and not before. The counter read here comes out of a frame the AEAD\n    // has already proven came from the peer — it is a field of the AAD, so a\n    // rewrite would have failed the tag — which is what makes\n    // `E2EE_SEQUENCE_VIOLATION` a true claim about the peer rather than a\n    // verdict on anyone who can inject bytes (§9).\n    const counter = frame.readBigUInt64BE(COUNTER_OFFSET);\n    if (counter !== claimedSeq) {\n      return refuse(E2EE_SEQUENCE_VIOLATION, \"X-TB-Seq does not match the sealed counter\");\n    }\n\n    replaceRequestBody(c, incoming, plaintext);\n    c.set(\"e2eeContext\", context);\n\n    /** The one seal this request is owed, and the framing it goes out in. */\n    let spent = false;\n    const sealOnce = (\n      status: number,\n      inHeaders: OutgoingHttpHeaders,\n      body: Buffer,\n    ): { status: number; headers: OutgoingHttpHeaders; body?: Buffer } => {\n      const headers: OutgoingHttpHeaders = {};\n      for (const [k, v] of Object.entries(inHeaders)) {\n        const lower = k.toLowerCase();\n        if (lower === \"content-type\" || lower === \"content-length\") continue;\n        if (lower === \"transfer-encoding\") continue;\n        headers[k] = v;\n      }\n      let record: Buffer;\n      try {\n        if (spent) throw new RecordError(E2EE_SEAL_FAILED, \"this request was already answered\");\n        spent = true;\n        record = context.sealResponse(counter, body, target);\n      } catch {\n        // A server-side fault (§9). Plaintext, because the alternative is a\n        // second seal under a counter already spent — the one failure §13\n        // exists to prevent — and because a client that cannot unseal an error\n        // has no way to learn what happened.\n        const payload = Buffer.from(\n          JSON.stringify({ error: \"Could not seal the response\", code: E2EE_SEAL_FAILED }),\n        );\n        return {\n          status: 500,\n          headers: {\n            ...headers,\n            \"Content-Type\": \"application/json\",\n            \"Content-Length\": payload.byteLength,\n          },\n          body: payload,\n        };\n      }\n      headers[HEADER_MARKER] = \"1\";\n      // **A sealed response must never enter a shared HTTP cache.** It is bound\n      // to ONE accepted request counter (§13(a)), so a cached copy is a record\n      // no later request can unseal — and worse, a cache that revalidates a\n      // sealed `304` applies its headers to the stored `200` and hands the\n      // client that status with a payload that is empty by construction. Field\n      // evidence, iOS build 219 against streamer 1.77.3: seven `304`s on\n      // `/api/conversations/:id`, and a `JSON Parse error: Unexpected end of\n      // input` on the app's messages query — `NSURLCache` revalidating on its\n      // own `If-None-Match`, because nothing in this tree ever said not to.\n      // Set for every sealed response, not just the bodiless one: the framing a\n      // record travels in is not what makes it single-use.\n      headers[\"Cache-Control\"] = \"no-store\";\n      if (!canCarryBody(status)) {\n        // The frozen rule, response side: a record whose framing cannot carry a\n        // body travels base64url in `X-TB-Env`. Such responses in the tree are\n        // the `304` at `conversations.handlers.ts` and the `204` from\n        // `DELETE /api/push/register`; each still owes the one sealed record its\n        // counter was accepted for.\n        headers[HEADER_ENVELOPE] = record.toString(\"base64url\");\n        return { status, headers };\n      }\n      headers[\"Content-Type\"] = \"application/octet-stream\";\n      headers[\"Content-Length\"] = record.byteLength;\n      return { status, headers, body: record };\n    };\n\n    /**\n     * Answer this request with the ONE sealed record its counter is owed.\n     *\n     * Every refusal from here down goes through here and never through\n     * `refuse`. That is §13(a) read from the far side: above the unseal,\n     * plaintext only; below it, sealed only. `unsealRequest` has succeeded, so\n     * the counter is committed and exactly one sealed response is owed — and a\n     * plaintext answer at this point would be an unauthenticated party's word\n     * about an authenticated request.\n     */\n    const refuseSealed = (status: number, payload: Record<string, unknown>): Response => {\n      // Deliberately NOT logged. `refuse` above logs its code because a\n      // pre-unseal refusal says nothing about anyone; these three say who was\n      // refused and why, which is a claim about an identified device, and the\n      // program rule is that no context state reaches a log line.\n      const out = sealOnce(status, {}, Buffer.from(JSON.stringify(payload), \"utf-8\"));\n      const headers = new Headers();\n      for (const [k, v] of Object.entries(out.headers)) {\n        if (v !== undefined) headers.set(k, String(v));\n      }\n      return new Response(out.body ? new Uint8Array(out.body) : null, {\n        status: out.status,\n        headers,\n      });\n    };\n\n    // ── Rung 12: the context principal (§13(b), design.md §4.4) ─────\n    //\n    // The credential presented BESIDE the context, if any. §13(b) says a\n    // sealed request carries none — the context already names the device — so\n    // `undefined` is the ordinary case and an ordinary success. Read the same\n    // two places `authMiddleware` reads, or a device could dodge the mismatch\n    // check by moving its token from the header to the query string.\n    const authorization = c.req.header(\"authorization\");\n    const presented =\n      (authorization?.startsWith(\"Bearer \") ? authorization.slice(7) : undefined) ??\n      c.req.query(\"key\") ??\n      undefined;\n\n    // A PURE verdict. The helper states the fact it found; the HTTP mapping and\n    // the lifecycle effect below are this caller's policy, and the WebSocket\n    // caller maps the same three reasons onto close reasons instead.\n    const auth = authenticateContext({\n      context,\n      devicesRepo: deps.devicesRepo(),\n      presented,\n    });\n\n    if (!auth.ok) {\n      // **Seal FIRST, with the still-live context. Destroy after, and only on\n      // `device-revoked`.**\n      //\n      // This ordering is load-bearing at v1.72.0 and was not at v1.71.0.\n      // W1b shipped real invalidation (streamer #743): `registry.destroy()`\n      // runs `contextInvalidators`, which nulls the context's response sealer,\n      // and `sealResponse` throws through `requireRest()` the moment it is\n      // null. So destroying first does not merely risk a future problem — it\n      // turns this refusal into a 500 the phone cannot decrypt, on the one\n      // code (§9 `E2EE_DEVICE_REVOKED`) the client is told never to retry.\n      const sealed =\n        auth.reason === \"device-revoked\"\n          ? refuseSealed(403, { error: \"This device is not paired\", code: E2EE_DEVICE_REVOKED })\n          : auth.reason === \"no-device-store\"\n            ? // Transient, and deliberately NOT `E2EE_DEVICE_REVOKED`: §9 makes\n              // that a hard failure the client must never retry, and a registry\n              // we could not read says nothing about the pairing. A 403 here\n              // would tell the phone its device was revoked because our disk\n              // faulted. Byte-identical to the WS caller's answer\n              // (`auth.middleware.ts`) and to `devices.routes.ts`.\n              refuseSealed(503, {\n                error: \"Device registry is unavailable\",\n                code: \"STORE_UNAVAILABLE\",\n              })\n            : // `credential-mismatch`, including the shared API key, which names\n              // no device and is a mismatch rather than an exemption.\n              refuseSealed(401, { error: \"Unauthorized\" });\n\n      // **Only `device-revoked` destroys, and the discriminator generalises:\n      // destroy on a fact in our own database that an attacker cannot forge,\n      // never on a header an attacker supplied.** `X-TB-Ctx` travels in\n      // plaintext on every sealed request, so a destroy reachable through a\n      // mismatched credential would let anyone who reads one request kill that\n      // device's context on repeat — forge a credential beside the observed id,\n      // watch the victim re-open, read the new id, repeat. `no-device-store`\n      // says nothing about the device either, and its context also survives.\n      if (auth.reason === \"device-revoked\") registry.destroy(ctxId);\n      return sealed;\n    }\n\n    // The principal is built from the CONTEXT's device id, never from the\n    // credential and never from the row — `principal.deviceId ===\n    // context.deviceId` by construction, which is stronger than checking that\n    // they agree.\n    c.set(\"principal\", auth.principal);\n\n    // ── Rung 13: authority, not just identity ───────────────────────\n    //\n    // A context AUTHENTICATES; it does not authorize. `authMiddleware` skips\n    // only credential RESOLUTION when a principal is already set and still runs\n    // this same check itself, so the two are defence in depth rather than one\n    // check in two places — see the note on the campaign row: neither mutation\n    // reddens alone, by design, and the safeguard is the pair.\n    const required = requiredCapability(new URL(c.req.url).pathname, c.req.method);\n    if (required !== null && !hasCapability(auth.principal, required)) {\n      // Sealed, like every other answer this counter is owed — and the context\n      // SURVIVES. Unlike the WebSocket caller, which destroys here because its\n      // ticket is already spent and the context it promoted would otherwise be\n      // orphaned for 24 h, a REST context is long-lived and addressable again\n      // on the very next request. A read-only device that touches one write\n      // route must not lose the channel it is entitled to use.\n      return refuseSealed(403, { error: \"Forbidden\", code: \"MISSING_CAPABILITY\", required });\n    }\n\n    // Installed BEFORE `next()`: several direct-write routes do not await their\n    // handler, so `end` can fire after `next()` has already returned.\n    const restoreDirectWrite = interceptDirectWrite(outgoing, sealOnce);\n\n    try {\n      await next();\n    } catch (err) {\n      // **A thrown handler still owes its one sealed response.** Hono's\n      // `onError` runs OUTSIDE this middleware — by the time it produces a\n      // response, `next()` has already thrown past here and there is nothing\n      // left to seal it. So the same handler is invoked here and its answer\n      // goes out through the same seal. One implementation of the error shape,\n      // not two.\n      c.res = await errorMiddleware(err as Error, c);\n    }\n\n    // A direct-write route returns the 597 sentinel and has written (or will\n    // write) to `outgoing`; the patched `end` seals it. Nothing to do here —\n    // and the patches stay on, because several routes do not await their\n    // handler and `end` may still be ahead of us.\n    if (c.res.status === ALREADY_HANDLED) return;\n\n    // ── The Hono-piped path ─────────────────────────────────────────\n    //\n    // `@hono/node-server` will write `c.res` to `outgoing` through the same\n    // four methods, so they come off first or the response is sealed twice.\n    restoreDirectWrite();\n    const body = Buffer.from(await c.res.arrayBuffer());\n    const headers: OutgoingHttpHeaders = {};\n    c.res.headers.forEach((value, key) => {\n      headers[key] = value;\n    });\n    const out = sealOnce(c.res.status, headers, body);\n    const outHeaders = new Headers();\n    for (const [k, v] of Object.entries(out.headers)) {\n      if (v !== undefined) outHeaders.set(k, String(v));\n    }\n    // Hono's `set res` copies the OLD response's headers onto the new one, so a\n    // stale `content-length` there would overwrite the sealed one.\n    c.res.headers.delete(\"content-length\");\n    c.res = new Response(out.body ? new Uint8Array(out.body) : null, {\n      status: c.res.status,\n      headers: outHeaders,\n    });\n  };\n","import type { ErrorHandler } from \"hono\";\nimport type { ContentfulStatusCode } from \"hono/utils/http-status\";\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  // Honour the { statusCode, code } convention the session paths already throw\n  // with (LiveSessionManager). Handlers that catch locally never reach here —\n  // start and fork do — but resume and adopt do not, so a refusal they raise\n  // deliberately used to arrive as a bare 500 carrying nothing to branch on.\n  // Bounded to real HTTP codes because `statusCode` is also a property some\n  // libraries hang on their own errors, and one of those bubbling up must not\n  // get to choose the response.\n  const { statusCode, code } = err as Error & { statusCode?: unknown; code?: unknown };\n  const status: ContentfulStatusCode =\n    typeof statusCode === \"number\" && statusCode >= 400 && statusCode <= 599\n      ? (statusCode as ContentfulStatusCode)\n      : 500;\n  return c.json(typeof code === \"string\" ? { error: message, code } : { error: message }, status);\n};\n","import type { MiddlewareHandler } from \"hono\";\nimport type { AppEnv } from \"../app\";\nimport { HEADER_CTX, HEADER_ENVELOPE, HEADER_MARKER, HEADER_SEQ } from \"./e2ee-envelope.middleware\";\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      const headers: Record<string, string> = {\n        \"Access-Control-Allow-Origin\": allowedOrigin,\n        Vary: \"Origin\",\n        // tb-mobile's api-client issues PUT and DELETE too, sealed or not.\n        \"Access-Control-Allow-Methods\": \"GET, POST, PUT, PATCH, DELETE, QUERY, OPTIONS\",\n        // X-Client-Id rides on every tb-mobile REST call; without it here a\n        // browser cancels the request after the preflight (\"Failed to fetch\").\n        // The four envelope headers are what a sealed request sends instead of\n        // Authorization — named from the envelope middleware, not retyped.\n        \"Access-Control-Allow-Headers\": [\n          \"Authorization\",\n          \"Content-Type\",\n          \"If-None-Match\",\n          \"X-Client-Id\",\n          HEADER_MARKER,\n          HEADER_CTX,\n          HEADER_SEQ,\n          HEADER_ENVELOPE,\n        ].join(\", \"),\n        // A sealed response is recognised by its marker, and a bodiless one\n        // (204/304) carries its record in X-TB-Env: a browser hides both\n        // unless they are exposed, and the client then refuses the response.\n        \"Access-Control-Expose-Headers\": [\n          \"ETag\",\n          \"Accept-Query\",\n          HEADER_MARKER,\n          HEADER_ENVELOPE,\n        ].join(\", \"),\n      };\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      // A WebSocket upgrade has no ServerResponse: @hono/node-ws runs it through\n      // the app with `outgoing: undefined`, and browsers always send Origin on\n      // one, so an unguarded setHeader turned every browser socket into a 500.\n      for (const [name, value] of Object.entries(headers)) {\n        c.env.outgoing?.setHeader(name, value);\n        c.res.headers.set(name, value);\n      }\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 { Hono } from \"hono\";\nimport { hostname } from \"os\";\nimport {\n  BACKUP_FORMAT_VERSION,\n  BackupError,\n  type BackupProject,\n  planRestore,\n  remapPaths,\n  validateArchive,\n} from \"../../services/backup/backup\";\nimport { getVersion } from \"../../version\";\nimport type { AppEnv } from \"../app\";\nimport type { ApiDeps } from \"../types/api-deps\";\n\n/**\n * Metadata export and restore (C9 / mobile U11).\n *\n * Only Threadbase metadata is exported. Provider history is authoritative and\n * is not ours; the cache derives from it and rebuilds by scanning. What cannot\n * be reconstructed is project identity — a fresh scan invents new project ids,\n * breaking every deep link and per-project setting that referenced the old ones.\n *\n * Restore is deliberately two-step: a dry run returns the plan so the user can\n * see what would change, and applying requires an explicit flag. A restore that\n * silently rewrites project identity is one nobody can review.\n */\n\nfunction readBody(c: { env: { incoming: NodeJS.ReadableStream } }): Promise<unknown> {\n  return new Promise((resolve, reject) => {\n    const chunks: Buffer[] = [];\n    c.env.incoming.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n    c.env.incoming.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    c.env.incoming.on(\"error\", reject);\n  });\n}\n\nexport const createBackupRoutes = (deps: Pick<ApiDeps, \"projectsRepo\">) => {\n  const app = new Hono<AppEnv>();\n\n  app.get(\"/export\", (c) => {\n    const repo = deps.projectsRepo();\n    if (!repo) {\n      return c.json({ error: \"Project store is unavailable\", code: \"STORE_UNAVAILABLE\" }, 503);\n    }\n\n    const projects: BackupProject[] = repo.listProjects().map((p) => ({\n      id: p.id,\n      path: p.path,\n      name: p.name ?? null,\n      createdAt: p.createdAt,\n      updatedAt: p.updatedAt,\n    }));\n\n    return c.json({\n      manifest: {\n        formatVersion: BACKUP_FORMAT_VERSION,\n        createdAt: new Date().toISOString(),\n        streamerVersion: getVersion(),\n        sourceHost: hostname(),\n        // No endpoint here exports the API key. The flag is recorded so an\n        // archive is self-describing about its own sensitivity rather than\n        // requiring a reader to infer it.\n        includesSecrets: false,\n        counts: { projects: projects.length },\n      },\n      projects,\n    });\n  });\n\n  app.post(\"/restore\", async (c) => {\n    const repo = deps.projectsRepo();\n    if (!repo) {\n      return c.json({ error: \"Project store is unavailable\", code: \"STORE_UNAVAILABLE\" }, 503);\n    }\n\n    let body: { archive?: unknown; pathMap?: unknown; apply?: unknown };\n    try {\n      body = (await readBody(c)) as typeof body;\n    } catch {\n      return c.json({ error: \"Invalid JSON body\", code: \"INVALID_BODY\" }, 400);\n    }\n\n    // Validate the whole archive before anything is applied. Restore rewrites\n    // project identity, so a malformed archive is rejected outright rather than\n    // applied halfway.\n    let archive: ReturnType<typeof validateArchive>;\n    try {\n      archive = validateArchive(body.archive);\n    } catch (err) {\n      if (err instanceof BackupError) {\n        return c.json({ error: err.message, code: err.code }, 400);\n      }\n      throw err;\n    }\n\n    const rules = Array.isArray(body.pathMap)\n      ? (body.pathMap as Array<{ from?: unknown; to?: unknown }>)\n          .filter((r) => typeof r?.from === \"string\" && typeof r?.to === \"string\")\n          .map((r) => ({ from: r.from as string, to: r.to as string }))\n      : [];\n\n    const incoming = rules.length > 0 ? remapPaths(archive.projects, rules) : archive.projects;\n    const existing = repo.listProjects().map((p) => ({ id: p.id, path: p.path }));\n    const plan = planRestore(incoming, existing);\n\n    const summary = {\n      create: plan.create.length,\n      update: plan.update.length,\n      conflict: plan.conflict.length,\n    };\n\n    // Dry run by default. The caller sees exactly what would change before\n    // anything is written.\n    if (body.apply !== true) {\n      return c.json({ applied: false, summary, plan });\n    }\n\n    // A conflict means one path is claimed by two different ids; applying\n    // either would break whichever links used the other. Refuse rather than\n    // pick a silent winner.\n    if (plan.conflict.length > 0) {\n      return c.json(\n        {\n          error: \"Restore has unresolved conflicts\",\n          code: \"RESTORE_CONFLICT\",\n          summary,\n          plan,\n        },\n        409,\n      );\n    }\n\n    let applied = 0;\n    for (const p of [...plan.create, ...plan.update]) {\n      try {\n        repo.upsertProjectByPath(p.path, { name: p.name });\n        applied++;\n      } catch {\n        // Continue rather than aborting midway: a partial restore that reports\n        // its own count is more recoverable than one that stops silently.\n      }\n    }\n\n    return c.json({ applied: true, summary, appliedCount: applied });\n  });\n\n  return app;\n};\n","/**\n * Metadata export and restore (C9).\n *\n * Threadbase owns very little durable state, and knowing exactly which parts\n * matter is the whole design:\n *\n *  - **Provider history** (`~/.claude/projects/*.jsonl`, Codex rollouts) is the\n *    authoritative record of every conversation, and it is NOT ours. It is\n *    written by the provider CLIs and survives independently of this server.\n *    Exporting it would duplicate gigabytes of data the user already has, in a\n *    format we do not control.\n *  - **Threadbase metadata** (the `projects` table: stable project ids,\n *    names, and their mapping to paths) is ours, is small, and cannot be\n *    reconstructed — a fresh scan invents new project ids, breaking every\n *    deep link and per-project setting that referenced the old ones.\n *  - **The cache** (conversation metadata, tails, offset index) is derived\n *    entirely from provider history and is rebuilt by scanning. Exporting it\n *    would bloat a backup with data that regenerates itself.\n *\n * So an export carries metadata only, and the restore path is explicit that\n * conversations come back by rescanning provider history rather than from the\n * archive.\n *\n * Credentials are never exported by default. The API key is a live credential\n * whose disclosure grants full control of the server; a backup file gets copied\n * to cloud storage, pasted into issues, and emailed around.\n */\n\nexport const BACKUP_FORMAT_VERSION = 1;\n\nexport interface BackupProject {\n  id: string;\n  path: string;\n  name: string | null;\n  createdAt: string;\n  updatedAt: string;\n}\n\nexport interface BackupManifest {\n  formatVersion: number;\n  createdAt: string;\n  streamerVersion: string;\n  /** Identity of the machine this was taken from, for path-remap decisions. */\n  sourceHost: string;\n  /**\n   * True when the operator explicitly opted into including secrets. Recorded so\n   * a restore can warn, and so an archive's sensitivity is self-describing\n   * rather than inferred.\n   */\n  includesSecrets: boolean;\n  counts: { projects: number };\n}\n\nexport interface BackupArchive {\n  manifest: BackupManifest;\n  projects: BackupProject[];\n}\n\nexport class BackupError extends Error {\n  constructor(\n    message: string,\n    readonly code: string,\n  ) {\n    super(message);\n  }\n}\n\n/**\n * Validate an archive before any of it is applied.\n *\n * Restore is destructive in the sense that it rewrites project identity, so a\n * malformed archive must be rejected whole rather than applied halfway. This\n * checks shape and version up front; the caller applies inside a transaction.\n */\nexport function validateArchive(input: unknown): BackupArchive {\n  if (!input || typeof input !== \"object\") {\n    throw new BackupError(\"Backup is not an object\", \"INVALID_ARCHIVE\");\n  }\n  const archive = input as Partial<BackupArchive>;\n  const manifest = archive.manifest;\n\n  if (!manifest || typeof manifest !== \"object\") {\n    throw new BackupError(\"Backup is missing its manifest\", \"INVALID_ARCHIVE\");\n  }\n  if (manifest.formatVersion !== BACKUP_FORMAT_VERSION) {\n    // Refuse rather than guess. A future format may mean something different by\n    // the same field names, and silently misreading it would corrupt project\n    // identity in a way the user cannot see.\n    throw new BackupError(\n      `Unsupported backup format version ${String(manifest.formatVersion)}; this build reads version ${BACKUP_FORMAT_VERSION}`,\n      \"UNSUPPORTED_VERSION\",\n    );\n  }\n  if (!Array.isArray(archive.projects)) {\n    throw new BackupError(\"Backup is missing its projects array\", \"INVALID_ARCHIVE\");\n  }\n\n  for (const [i, p] of archive.projects.entries()) {\n    if (!p || typeof p !== \"object\") {\n      throw new BackupError(`Project at index ${i} is not an object`, \"INVALID_ARCHIVE\");\n    }\n    if (typeof p.id !== \"string\" || p.id.length === 0) {\n      throw new BackupError(`Project at index ${i} has no id`, \"INVALID_ARCHIVE\");\n    }\n    if (typeof p.path !== \"string\" || p.path.length === 0) {\n      throw new BackupError(`Project at index ${i} has no path`, \"INVALID_ARCHIVE\");\n    }\n  }\n\n  const ids = new Set(archive.projects.map((p) => p.id));\n  if (ids.size !== archive.projects.length) {\n    // Duplicate ids would make restore order-dependent, so the archive is\n    // rejected rather than silently resolved by last-write-wins.\n    throw new BackupError(\"Backup contains duplicate project ids\", \"INVALID_ARCHIVE\");\n  }\n\n  return archive as BackupArchive;\n}\n\n/**\n * Rewrite project paths from one machine's layout to another's.\n *\n * A backup restored onto a different machine — or the same machine after a home\n * directory move — carries paths that no longer exist. Without remapping, every\n * project points at nothing and the restore is useless.\n *\n * Longest-prefix wins, so a more specific rule beats a general one. Paths that\n * match no rule are left untouched rather than mangled.\n */\nexport function remapPaths(\n  projects: BackupProject[],\n  rules: ReadonlyArray<{ from: string; to: string }>,\n): BackupProject[] {\n  const ordered = [...rules].sort((a, b) => b.from.length - a.from.length);\n  return projects.map((p) => {\n    const rule = ordered.find((r) => p.path === r.from || p.path.startsWith(`${r.from}/`));\n    if (!rule) return p;\n    return { ...p, path: `${rule.to}${p.path.slice(rule.from.length)}` };\n  });\n}\n\n/**\n * Classify how each incoming project relates to what already exists.\n *\n * Returned rather than applied so the caller can show the user what a restore\n * would do before doing it — a restore that silently rewrites project identity\n * is one the user cannot review.\n */\nexport interface RestorePlan {\n  create: BackupProject[];\n  /** Same id, different path — the machine-move case. */\n  update: BackupProject[];\n  /** Path already claimed by a DIFFERENT id. Requires an explicit decision. */\n  conflict: Array<{ incoming: BackupProject; existingId: string }>;\n}\n\nexport function planRestore(\n  incoming: BackupProject[],\n  existing: ReadonlyArray<{ id: string; path: string }>,\n): RestorePlan {\n  const byId = new Map(existing.map((e) => [e.id, e]));\n  const byPath = new Map(existing.map((e) => [e.path, e]));\n\n  const plan: RestorePlan = { create: [], update: [], conflict: [] };\n\n  for (const p of incoming) {\n    const sameId = byId.get(p.id);\n    if (sameId) {\n      if (sameId.path !== p.path) plan.update.push(p);\n      continue;\n    }\n    const samePath = byPath.get(p.path);\n    if (samePath) {\n      // Two different ids for one path: applying either would break whichever\n      // links used the other. The user has to decide.\n      plan.conflict.push({ incoming: p, existingId: samePath.id });\n      continue;\n    }\n    plan.create.push(p);\n  }\n\n  return plan;\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 { 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 { IncomingMessage } from \"http\";\nimport { ResolveCacheAlertSchema } from \"../../schemas/cacheAlert.schema\";\nimport type { AppEnv } from \"../app\";\nimport type { ApiDeps } from \"../types/api-deps\";\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\nexport const createCacheAlertRoutes = (deps: ApiDeps) => {\n  const app = new Hono<AppEnv>();\n\n  // Full pending record (including the missing list) for building a picker.\n  app.get(\"/\", (c) => {\n    const monitor = deps.cacheMonitor();\n    return c.json({ pending: monitor?.pending ?? null });\n  });\n\n  app.post(\"/resolve\", async (c) => {\n    // Read from the raw Node stream (mirrors /api/__update): Hono's body helpers\n    // return empty under @hono/node-server. In tests (app.request), c.env.incoming\n    // is absent — fall back to arrayBuffer().\n    let body: unknown;\n    try {\n      const incoming = c.env?.incoming;\n      const raw = incoming\n        ? await readRawBody(incoming)\n        : Buffer.from(await c.req.arrayBuffer()).toString(\"utf-8\");\n      body = raw ? JSON.parse(raw) : {};\n    } catch {\n      return c.json({ error: \"invalid json\" }, 400);\n    }\n    const parsed = ResolveCacheAlertSchema.safeParse(body);\n    if (!parsed.success) {\n      return c.json({ error: \"invalid body\", details: parsed.error.flatten() }, 400);\n    }\n\n    const monitor = deps.cacheMonitor();\n    if (!monitor) return c.json({ ok: true, alreadyResolved: true });\n\n    const { fingerprint, action, ids } = parsed.data;\n    const result = await monitor.resolve(fingerprint, action, ids);\n\n    if (\"conflict\" in result) {\n      return c.json(\n        { error: \"fingerprint_mismatch\", currentFingerprint: result.currentFingerprint },\n        409,\n      );\n    }\n    if (\"alreadyResolved\" in result) {\n      return c.json({ ok: true, alreadyResolved: true });\n    }\n    return c.json(result);\n  });\n\n  return app;\n};\n","import { z } from \"zod\";\n\n/** Body for POST /api/cache/alert/resolve. */\nexport const ResolveCacheAlertSchema = z\n  .object({\n    fingerprint: z.string(),\n    action: z.enum([\"prune_all\", \"prune_selected\", \"ignore\", \"reset_rescan\"]),\n    ids: z.array(z.string()).optional(),\n  })\n  .refine((v) => v.action !== \"prune_selected\" || (v.ids !== undefined && v.ids.length > 0), {\n    message: \"prune_selected requires a non-empty ids array\",\n    path: [\"ids\"],\n  });\n\nexport type ResolveCacheAlertBody = z.infer<typeof ResolveCacheAlertSchema>;\n","import { Hono } from \"hono\";\nimport type { IncomingMessage } from \"http\";\nimport { ClaudeFlagsBodySchema } from \"../../schemas/claudeFlags.schema\";\nimport type { AppEnv } from \"../app\";\nimport type { ApiDeps } from \"../types/api-deps\";\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\nexport const createConfigRoutes = (\n  deps: Pick<\n    ApiDeps,\n    \"claudeFlagsConfig\" | \"setClaudeFlagsConfig\" | \"featureFlagsConfig\" | \"localNoAuth\"\n  >,\n) => {\n  const app = new Hono<AppEnv>();\n\n  // The registry ships alongside the values so a client renders the form from\n  // one round-trip and can never offer a flag this server doesn't know.\n  app.get(\"/claude-flags\", (c) => c.json(deps.claudeFlagsConfig()));\n\n  // Read-only by design: feature flags resolve at boot (env > CLI > server.yaml\n  // > default), so there is no PUT counterpart. Changing one means a restart.\n  app.get(\"/feature-flags\", (c) => c.json(deps.featureFlagsConfig()));\n\n  app.put(\"/claude-flags\", async (c) => {\n    // Same reasoning as POST /api/auth/rotate: under localNoAuth any process on\n    // the machine can call this unauthenticated, and this endpoint can turn off\n    // Claude's permission prompts for every future session. Refuse rather than\n    // let a local process silently escalate the box.\n    if (deps.localNoAuth) {\n      return c.json({ error: \"claude flag changes are disabled while localNoAuth is active\" }, 403);\n    }\n\n    // Read from the raw Node stream (mirrors /api/cache/alert/resolve): Hono's\n    // body helpers return empty under @hono/node-server. In tests (app.request),\n    // c.env.incoming is absent — fall back to arrayBuffer().\n    let body: unknown;\n    try {\n      const incoming = c.env?.incoming;\n      const raw = incoming\n        ? await readRawBody(incoming)\n        : Buffer.from(await c.req.arrayBuffer()).toString(\"utf-8\");\n      body = raw ? JSON.parse(raw) : {};\n    } catch {\n      return c.json({ error: \"invalid json\" }, 400);\n    }\n\n    const parsed = ClaudeFlagsBodySchema.safeParse(body);\n    if (!parsed.success) {\n      return c.json({ error: \"invalid body\", details: parsed.error.flatten() }, 400);\n    }\n\n    try {\n      const result = deps.setClaudeFlagsConfig(parsed.data.values, parsed.data.extraArgs);\n      return c.json({\n        ...result,\n        ...(result.persisted\n          ? {}\n          : {\n              warning:\n                \"Flags applied in memory only. The server was started with --claude-flag, so \" +\n                \"the CLI values will be restored on restart. Drop the flag and let the server \" +\n                \"manage them via ~/.threadbase/server.yaml for changes to survive restarts.\",\n            }),\n      });\n    } catch (err) {\n      return c.json({ error: err instanceof Error ? err.message : \"could not apply flags\" }, 400);\n    }\n  });\n\n  return app;\n};\n","import { z } from \"zod\";\n\n// Body schema for PUT /api/config/claude-flags.\n//\n// Validated with zod rather than the hand-rolled `typeof` checks used elsewhere\n// in server.ts because this payload becomes process argv — it is a genuine trust\n// boundary, and the values can disable Claude's permission prompts entirely.\n//\n// This layer only enforces the SHAPE (a flat map of scalars/string arrays).\n// Which ids are legal, and which values each id accepts, is decided by\n// validateFlagValues() against the registry in src/claude-flags.ts — one source\n// of truth rather than two that can drift.\nexport const ClaudeFlagsBodySchema = z\n  .object({\n    values: z\n      .record(z.string(), z.union([z.string(), z.boolean(), z.array(z.string())]))\n      .default({}),\n    // A newline would corrupt the flat one-line-per-key server.yaml, so reject\n    // it here with a field error instead of silently stripping it.\n    extraArgs: z\n      .string()\n      .refine((v) => !/[\\r\\n]/.test(v), \"extraArgs must not contain newlines\")\n      .optional(),\n  })\n  .strict();\n\nexport type ClaudeFlagsBody = z.infer<typeof ClaudeFlagsBodySchema>;\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    if (await deps.isExcludedSubagent?.(id))\n      return c.json({ error: \"Conversation not found\" }, 404);\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 { contextRegistry } from \"../../e2ee/context\";\nimport { getLogger } from \"../../logger\";\nimport type { AppEnv } from \"../app\";\nimport type { ApiDeps } from \"../types/api-deps\";\n\nconst log = getLogger(\"e2ee\");\n\n/**\n * Paired-device management (C5 / mobile U10).\n *\n * Revoking a device previously meant rotating the shared API key, which\n * de-authenticated every other device at the same time. These routes make\n * revocation per-device and give the client something to display: which devices\n * exist, what each may do, and when it was last seen.\n *\n * No response ever includes a device token or its hash. A management surface\n * has no reason to hand back a credential, and this endpoint is exactly where\n * an accidental echo would be most damaging.\n */\nexport const createDeviceRoutes = (deps: Pick<ApiDeps, \"devicesRepo\" | \"wsHub\" | \"pushRepo\">) => {\n  const app = new Hono<AppEnv>();\n\n  /**\n   * Make a revocation reach the live process, not just the next request.\n   *\n   * `destroyDevice` drops every context the device still has indexed — N socket\n   * contexts, its REST context, and any unconsumed tickets. The hub closes by\n   * device identity rather than that answer because it can still own a socket\n   * whose context aged out of the registry's drain. Until this line existed, a\n   * revoked device kept a live encrypted socket, with its traffic keys resident\n   * in this process, until the peer happened to go away (design.md §4.4).\n   */\n  const cutLiveContexts = (deviceId: string): void => {\n    const destroyed = contextRegistry().destroyDevice(deviceId);\n    const sockets = deps.wsHub.closeDevice(deviceId);\n    if (destroyed.socketCtxIds.length || destroyed.restCtxIds.length || destroyed.tickets) {\n      log.info(\"[e2ee] revocation destroyed a device's live contexts\", {\n        event: \"e2ee.device_contexts_destroyed\",\n        sockets,\n        rest: destroyed.restCtxIds.length,\n        tickets: destroyed.tickets,\n      });\n    }\n  };\n\n  /**\n   * Revoking or erasing a device deletes its push tokens.\n   *\n   * Cross-database: devices are in runtime.db and push_tokens in cache.db, so\n   * there is no foreign key to lean on and this is the only place the two are\n   * joined. Delete rather than revoke, for the same reason the record itself\n   * goes — a retained row is still a stored delivery credential.\n   *\n   * Silent when the cache is unavailable: a device revoke must never fail\n   * because the conversation cache did not open.\n   */\n  const dropPushTokens = (deviceId: string): void => {\n    const deleted = deps.pushRepo?.()?.deleteForDevice(deviceId) ?? 0;\n    if (deleted) {\n      log.info(\"[push] revocation deleted a device's push tokens\", {\n        event: \"push.device_tokens_deleted\",\n        deviceId,\n        deleted,\n      });\n    }\n  };\n\n  app.get(\"/\", (c) => {\n    const repo = deps.devicesRepo();\n    // Report honestly rather than pretending no devices are paired: an empty\n    // list and \"the registry is unavailable\" mean very different things to a\n    // user deciding whether to revoke something.\n    if (!repo) return c.json({ devices: [], available: false });\n    return c.json({ devices: repo.list(), available: true });\n  });\n\n  app.post(\"/:id/revoke\", (c) => {\n    const repo = deps.devicesRepo();\n    if (!repo) {\n      return c.json({ error: \"Device registry is unavailable\", code: \"STORE_UNAVAILABLE\" }, 503);\n    }\n\n    const id = c.req.param(\"id\");\n    const existing = repo.get(id);\n    if (!existing) return c.json({ error: \"Device not found\" }, 404);\n\n    // Idempotent: revoking an already-revoked device is not an error, so a\n    // client retrying after a dropped response does not see a spurious failure.\n    if (existing.revoked_at != null) {\n      cutLiveContexts(id);\n      dropPushTokens(id);\n      return c.json({ ok: true, alreadyRevoked: true });\n    }\n\n    repo.revoke(id);\n    cutLiveContexts(id);\n    dropPushTokens(id);\n    return c.json({ ok: true, alreadyRevoked: false });\n  });\n\n  /**\n   * Erase a device record, rather than revoking it.\n   *\n   * Exists because there was no way to remove one. `revoke` is a soft delete\n   * that keeps the row for the audit surface, the registry lives in runtime.db,\n   * and no CLI command deletes that file — so a `devices` row, including the\n   * user-supplied name, was permanent. Additive: older clients never call it.\n   *\n   * Refuses a device that is still active. Deleting a live row frees its\n   * token_hash without telling the device anything, so it would stop being\n   * known rather than being refused; revoking first is what actually cuts the\n   * credential. `?force=1` is the escape hatch for someone who means it.\n   */\n  app.delete(\"/:id\", (c) => {\n    const repo = deps.devicesRepo();\n    if (!repo) {\n      return c.json({ error: \"Device registry is unavailable\", code: \"STORE_UNAVAILABLE\" }, 503);\n    }\n\n    const id = c.req.param(\"id\");\n    const existing = repo.get(id);\n    // Idempotent, like revoke: a client retrying after a dropped response gets\n    // the same answer rather than a spurious 404.\n    if (!existing) {\n      cutLiveContexts(id);\n      dropPushTokens(id);\n      return c.json({ ok: true, alreadyDeleted: true });\n    }\n\n    const force = c.req.query(\"force\") === \"1\" || c.req.query(\"force\") === \"true\";\n    if (existing.revoked_at == null && !force) {\n      return c.json(\n        {\n          error: \"Revoke the device before deleting it, or pass ?force=1\",\n          code: \"DEVICE_ACTIVE\",\n        },\n        409,\n      );\n    }\n\n    repo.delete(id);\n    // Same reason as revoke, and the reason it is here too: `?force=1` erases\n    // an ACTIVE row, so without this the credential is gone from the store\n    // while its sealed socket carries on with keys the store can no longer\n    // name. Erasing a device is at least as strong as revoking it.\n    cutLiveContexts(id);\n    dropPushTokens(id);\n    return c.json({ ok: true, alreadyDeleted: false });\n  });\n\n  /** Bulk erase of already-revoked devices. */\n  app.delete(\"/\", (c) => {\n    const repo = deps.devicesRepo();\n    if (!repo) {\n      return c.json({ error: \"Device registry is unavailable\", code: \"STORE_UNAVAILABLE\" }, 503);\n    }\n    const revokedIds = repo\n      .list()\n      .filter((device) => device.revokedAt != null)\n      .map((device) => device.deviceId);\n    const deleted = repo.deleteRevoked();\n    for (const deviceId of revokedIds) {\n      cutLiveContexts(deviceId);\n      dropPushTokens(deviceId);\n    }\n    return c.json({ ok: true, deleted });\n  });\n\n  return app;\n};\n","import { existsSync } from \"fs\";\nimport { Hono } from \"hono\";\nimport { locateProviderExe } from \"../../platform\";\nimport { PROVIDER_NAMES, type ProviderName } from \"../../providers\";\nimport {\n  buildReport,\n  type DiagnosticCheck,\n  redactPath,\n  redactValue,\n} from \"../../services/diagnostics/diagnostics\";\nimport { rehydrateSkipReason } from \"../../services/sessions/rehydrateSessions\";\nimport { currentBootToken } from \"../../utils/bootToken\";\nimport { getVersion } from \"../../version\";\nimport type { AppEnv } from \"../app\";\nimport type { ApiDeps } from \"../types/api-deps\";\n\n/**\n * GET /api/diagnostics — a versioned, redacted health report (C6).\n *\n * `/healthz` answers only \"is the process up\". When a session will not start,\n * that says nothing about why: the provider CLI may be missing, the cache may\n * have failed to open, or the clock may be skewed past the pairing TTL.\n *\n * Every subsystem is checked independently so one failure never masks another,\n * and each carries a stable remediation code the client maps to instructions\n * without parsing English.\n *\n * The response is designed to be pasted into a bug report, so paths are reduced\n * to their last two segments and the whole payload passes through redactValue()\n * before serialization.\n */\n\nfunction providerCheck(name: ProviderName): DiagnosticCheck {\n  try {\n    // Located, not merely resolved, and through the same entry point the\n    // session-start pre-flight uses so the two cannot disagree: neither\n    // resolver can fail — each falls back to the bare command name — so the\n    // failed branch below was unreachable, and this endpoint, whose entire job\n    // is explaining why a session will not start, reported a missing CLI as\n    // installed.\n    const exe = locateProviderExe(name);\n    if (exe !== null) {\n      // Report only that it resolved and roughly where — never the full path,\n      // which carries the username and home layout.\n      return {\n        id: `provider:${name}`,\n        status: \"ok\",\n        summary: `${name} CLI is installed.`,\n        remediation: \"NONE\",\n        detail: { location: redactPath(exe) },\n      };\n    }\n  } catch {\n    // Fall through to the not-installed answer below.\n  }\n  return {\n    id: `provider:${name}`,\n    status: \"failed\",\n    summary: `${name} CLI could not be located. Sessions for this provider cannot start.`,\n    remediation: \"PROVIDER_NOT_INSTALLED\",\n  };\n}\n\nexport const createDiagnosticsRoutes = (deps: ApiDeps) => {\n  const app = new Hono<AppEnv>();\n\n  app.get(\"/\", (c) => {\n    const checks: DiagnosticCheck[] = [];\n\n    checks.push({\n      id: \"streamer\",\n      status: \"ok\",\n      summary: \"Streamer is running.\",\n      remediation: \"NONE\",\n      detail: { version: getVersion(), uptimeSeconds: Math.floor(process.uptime()) },\n    });\n\n    for (const name of PROVIDER_NAMES) {\n      checks.push(providerCheck(name));\n    }\n\n    // The cache backs conversation reads; without it every request falls back\n    // to slower disk-only scans, which is degraded rather than broken.\n    const cacheAlert = deps.cacheMonitor()?.healthzField();\n    checks.push(\n      cacheAlert\n        ? {\n            id: \"cache\",\n            status: \"degraded\",\n            summary: \"Conversation cache reported an integrity alert.\",\n            remediation: \"CACHE_DEGRADED\",\n          }\n        : {\n            id: \"cache\",\n            status: \"ok\",\n            summary: \"Conversation cache is healthy.\",\n            remediation: \"NONE\",\n          },\n    );\n\n    // node-pty is a native addon; if it failed to load, no managed session can\n    // start regardless of provider availability.\n    let ptyOk = true;\n    try {\n      require.resolve(\"node-pty\");\n    } catch {\n      ptyOk = false;\n    }\n    checks.push(\n      ptyOk\n        ? { id: \"pty\", status: \"ok\", summary: \"PTY subsystem is available.\", remediation: \"NONE\" }\n        : {\n            id: \"pty\",\n            status: \"failed\",\n            summary: \"node-pty failed to load, so no managed session can start.\",\n            remediation: \"PTY_UNAVAILABLE\",\n          },\n    );\n\n    const home = process.env.HOME ?? process.env.USERPROFILE ?? \"\";\n    const claudeProjects = home ? `${home}/.claude/projects` : \"\";\n    checks.push(\n      claudeProjects && existsSync(claudeProjects)\n        ? {\n            id: \"filesystem\",\n            status: \"ok\",\n            summary: \"Provider history directory is present.\",\n            remediation: \"NONE\",\n            detail: { location: redactPath(claudeProjects) },\n          }\n        : {\n            id: \"filesystem\",\n            status: \"degraded\",\n            summary: \"Provider history directory was not found; history may be unavailable.\",\n            remediation: \"FS_SCOPE_MISSING\",\n          },\n    );\n\n    // Final redaction pass. Individual checks are written not to include\n    // secrets, but this payload is meant to be shared, so one careless field\n    // must not be able to leak a credential.\n    return c.json(redactValue(buildReport(checks)));\n  });\n\n  /**\n   * GET /api/diagnostics/sessions — why each registry row ended up as it did\n   * (plan Phase 5, gap G11).\n   *\n   * `GET /api/sessions` shows the outcome; this shows the reasoning. \"My\n   * session did not come back after a restart\" is otherwise unanswerable\n   * without the server's logs, and the interesting rows are precisely the ones\n   * that never made it into the session list.\n   *\n   * Read-only and side-effect free: it recomputes the rehydration decision from\n   * the same pure function the boot path uses rather than recording one, so\n   * calling it can never change what a later boot would do.\n   */\n  app.get(\"/sessions\", (c) => {\n    const repo = deps.managedSessionsRepo();\n    if (!repo) {\n      return c.json(\n        { error: \"Session registry is unavailable\", code: \"REGISTRY_UNAVAILABLE\" },\n        503,\n      );\n    }\n\n    const rows = repo.listAll();\n    const verdicts = deps.sessionVerdicts();\n    const bootToken = currentBootToken();\n    const now = Date.now();\n\n    const sessions = rows.map((row) => {\n      const verdict = verdicts.get(row.session_id);\n      const skip = rehydrateSkipReason(row, { now, projectExists: existsSync });\n      return {\n        sessionId: row.session_id,\n        provider: row.provider,\n        status: row.status,\n        statusSource: row.status_source,\n        statusUpdatedAt: new Date(row.status_updated_at).toISOString(),\n        // Whether the recorded pid is probeable at all this boot, not whether\n        // it is alive — a mismatch means the question was never asked.\n        //\n        // NOT named for the boot token it derives from: `redactValue`'s\n        // SECRET_KEY_RE matches any key containing \"token\", so `bootTokenMatches`\n        // was scrubbed to the string \"[redacted]\" and the field shipped useless.\n        // The regex is deliberately over-broad for a payload meant to be pasted\n        // into bug reports, so the field moved rather than the guard.\n        recordedThisBoot: row.boot_token != null && row.boot_token === bootToken,\n        // Absent when this boot never classified the row: a clean restart\n        // stamps completed_at on the way out, which takes it out of the probe\n        // set entirely. That absence is itself the answer.\n        lifecycle: verdict?.lifecycle ?? null,\n        lifecycleReason: verdict?.reason ?? null,\n        rehydrated: skip == null,\n        rehydrateSkipReason: skip,\n        projectExists: existsSync(row.project_path),\n        projectPath: redactPath(row.project_path),\n      };\n    });\n\n    // Same contract as the report above: meant to be pasted into a bug report,\n    // so paths are reduced and the whole payload gets a final redaction pass.\n    return c.json(redactValue({ generatedAt: new Date().toISOString(), sessions }));\n  });\n\n  return app;\n};\n","/**\n * Diagnostics contract (C6).\n *\n * `/healthz` answers one question — is the process up — and returns\n * `{ ok, version }`. When a user's session will not start, that tells them\n * nothing about why: the provider CLI may be missing, the database may have\n * failed to migrate, the PTY subsystem may be unavailable, or the clock may be\n * skewed enough to break token validation.\n *\n * This produces a structured report where each subsystem is checked\n * independently, so one failure never masks another, and every failure carries a\n * STABLE remediation code the client can map to instructions without parsing\n * English.\n *\n * Redaction is a contract requirement, not a nicety: this endpoint is designed\n * to be copied into a bug report. See `redactPath` below.\n */\n\nexport const DIAGNOSTICS_CONTRACT_VERSION = 1;\n\nexport type CheckStatus = \"ok\" | \"degraded\" | \"failed\" | \"unknown\";\n\n/**\n * Stable, machine-readable remediation identifiers.\n *\n * Clients map these to localized instructions, so they are part of the contract:\n * renaming one is a breaking change. New codes may be added; existing ones must\n * keep their meaning.\n */\nexport type RemediationCode =\n  | \"PROVIDER_NOT_INSTALLED\"\n  | \"PROVIDER_VERSION_UNVERIFIED\"\n  | \"DB_UNAVAILABLE\"\n  | \"DB_MIGRATION_PENDING\"\n  | \"PTY_UNAVAILABLE\"\n  | \"CACHE_DEGRADED\"\n  | \"CLOCK_SKEWED\"\n  | \"FS_SCOPE_MISSING\"\n  | \"NONE\";\n\nexport interface DiagnosticCheck {\n  /** Stable identifier for the subsystem, e.g. \"database\". */\n  id: string;\n  status: CheckStatus;\n  /** Human-readable summary. Never contains secrets or full paths. */\n  summary: string;\n  remediation: RemediationCode;\n  /** Structured, redacted detail. Optional. */\n  detail?: Record<string, string | number | boolean | null>;\n}\n\nexport interface DiagnosticsReport {\n  contractVersion: number;\n  generatedAt: string;\n  /** Worst status across all checks — a single field for a client to branch on. */\n  overall: CheckStatus;\n  checks: DiagnosticCheck[];\n}\n\n/**\n * Reduce a filesystem path to its last two segments.\n *\n * A full path leaks the user's home directory layout, their username, and often\n * client or project names. The tail is enough to recognize *which* directory is\n * meant when you already know your own machine, which is all a diagnostic needs.\n */\nexport function redactPath(path: string | null | undefined): string | null {\n  if (!path) return null;\n  const parts = path.split(/[/\\\\]/).filter(Boolean);\n  if (parts.length <= 2) return parts.join(\"/\");\n  return `…/${parts.slice(-2).join(\"/\")}`;\n}\n\n/** Worst status wins, so a single failure is never hidden by surrounding successes. */\nexport function worstStatus(checks: DiagnosticCheck[]): CheckStatus {\n  const rank: Record<CheckStatus, number> = { ok: 0, unknown: 1, degraded: 2, failed: 3 };\n  return checks.reduce<CheckStatus>(\n    (worst, c) => (rank[c.status] > rank[worst] ? c.status : worst),\n    \"ok\",\n  );\n}\n\n/**\n * Clock skew against a trusted reference, in milliseconds.\n *\n * Matters because pair tokens carry a 180-second TTL: a device whose clock is\n * far enough off will have every pairing attempt rejected as expired, with no\n * indication that time is the cause.\n */\nexport const CLOCK_SKEW_WARN_MS = 60_000;\n\nexport function clockSkewCheck(localNow: number, referenceNow: number | null): DiagnosticCheck {\n  if (referenceNow === null) {\n    return {\n      id: \"clock\",\n      status: \"unknown\",\n      summary: \"No trusted time reference was available to compare against.\",\n      remediation: \"NONE\",\n    };\n  }\n  const skewMs = localNow - referenceNow;\n  const magnitude = Math.abs(skewMs);\n  if (magnitude <= CLOCK_SKEW_WARN_MS) {\n    return {\n      id: \"clock\",\n      status: \"ok\",\n      summary: \"System clock is within tolerance.\",\n      remediation: \"NONE\",\n      detail: { skewMs },\n    };\n  }\n  return {\n    id: \"clock\",\n    status: \"degraded\",\n    summary:\n      `System clock differs from the reference by ${Math.round(magnitude / 1000)}s. ` +\n      \"Pairing tokens expire after 180s, so a large skew can make every pairing attempt fail.\",\n    remediation: \"CLOCK_SKEWED\",\n    detail: { skewMs },\n  };\n}\n\nexport function buildReport(checks: DiagnosticCheck[], now: Date = new Date()): DiagnosticsReport {\n  return {\n    contractVersion: DIAGNOSTICS_CONTRACT_VERSION,\n    generatedAt: now.toISOString(),\n    overall: worstStatus(checks),\n    checks,\n  };\n}\n\n/**\n * Recursively strip anything secret-shaped from a value destined for the\n * report.\n *\n * Belt-and-braces: individual checks are written not to include secrets, but\n * this endpoint exists to be pasted into bug reports, so a single careless\n * check must not be able to leak a credential. Key-name matching is deliberately\n * broad — a false positive costs a redacted diagnostic, a false negative costs a\n * leaked key.\n */\nconst SECRET_KEY_RE = /(key|token|secret|password|passwd|credential|authorization|cookie)/i;\n\nexport function redactValue<T>(value: T): T {\n  if (Array.isArray(value)) {\n    return value.map((v) => redactValue(v)) as unknown as T;\n  }\n  if (value && typeof value === \"object\") {\n    const out: Record<string, unknown> = {};\n    for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n      out[k] = SECRET_KEY_RE.test(k) ? \"[redacted]\" : redactValue(v);\n    }\n    return out as unknown as T;\n  }\n  return value;\n}\n","import type { ManagedSessionRow } from \"../../db/repositories/managed-sessions.repository\";\nimport { CODEX_CLI_PROVIDER } from \"../../providers\";\n\n/**\n * Which id can actually resume a registry row (persistence plan Phase 3, gap G6).\n *\n * Claude's session id is always resumable as-is: it was passed to the CLI as\n * `--session-id`, so the JSONL is named after it and `--resume <id>` finds it.\n *\n * Codex has no such flag. A fresh Codex session is keyed by a local placeholder\n * UUID and its real rollout id arrives later, via `watchForCodexRollout`, as\n * `bound_conversation_id`. `codex resume <placeholder>` fails, so the bound id\n * is the only usable one — and a row that never bound has none at all, which is\n * what the `null` here means. Callers must treat `null` as \"not resumable\",\n * never as \"resume by session_id\".\n */\nexport function resumeIdForRow(row: ManagedSessionRow): string | null {\n  if (row.provider !== CODEX_CLI_PROVIDER) return row.session_id;\n  return row.bound_conversation_id;\n}\n","import type { ManagedSessionRow } from \"../../db/repositories/managed-sessions.repository\";\nimport type { ManagedSession, StatusSource } from \"../../types\";\nimport { resumeIdForRow } from \"./resumeIdentity\";\n\n/**\n * Boot rehydration (persistence plan Phase 1, gaps G1/G2/G8).\n * See docs/plans/live-sessions-persistence-plan.md §4.\n *\n * `recordShutdownState()` stamps `completed_at` on every live session as the\n * streamer stops, which takes the row out of the reconciler's probe set. So a\n * *cleanly* restarted session is not classified as anything — it simply\n * disappears from `GET /api/sessions`, because `SessionStore` starts empty and\n * nothing ever seeds it from the registry.\n *\n * This module is the decision half of the fix: which rows come back, and what\n * they look like when they do. Pure functions with no database import, mirroring\n * how `classifySession` is split out of `reconcileSessions` — the decision table\n * is the part worth testing, and it should not need a SQLite file to do it.\n *\n * The stub these produce holds no PTY and never reaches `LiveSessionManager`;\n * it lives in `SessionStore` alone, so the idle reaper and the grace timer\n * (both of which iterate `ptyManager.listSessions()`) cannot see it.\n */\n\n/** Most recovered sessions to seed in one boot. */\nexport const REHYDRATE_MAX = 25;\n\n/** How far back a registry row may reach and still be offered as recoverable. */\nexport const REHYDRATE_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;\n\n/**\n * Registry `status_source` values meaning \"the agent's own process ended it\",\n * as opposed to `shutdown`, which means we did. Both vocabularies appear here:\n * the registry writes `exit`, the wire's StatusSource says `process-exit`.\n */\nconst AGENT_EXIT_SOURCES = new Set([\"exit\", \"process-exit\"]);\n\nexport interface ShouldRehydrateOptions {\n  now: number;\n  /** Whether the row's project directory still exists on disk. */\n  projectExists: (projectPath: string) => boolean;\n  /**\n   * Whether the conversation cache holds a row for this id. Omitted when there\n   * is no cache, which leaves an unprompted row unprovable rather than empty.\n   */\n  hasConversation?: (id: string) => boolean;\n}\n\n/**\n * Why a registry row was not brought back, or `null` when it was.\n *\n * A stable code rather than a sentence: it is logged per row and surfaced by\n * `GET /api/diagnostics/sessions`, where \"my session did not come back\" has to\n * be answerable without reading the source.\n */\nexport type RehydrateSkipReason =\n  | \"never_prompted\"\n  | \"codex_unbound\"\n  | \"project_missing\"\n  | \"too_old\"\n  | \"agent_exited\";\n\n/**\n * Should this registry row come back as a recovered session? `null` means yes.\n *\n * Three ways to answer no, all of them \"resuming this would waste the user's\n * tap\":\n *\n *  - the project directory is gone — `handleResume` would fail to spawn, and\n *    `classifyResumability` already encodes the same rule for conversations;\n *  - the row is older than the window — a week-old session belongs in the\n *    conversations list, not the live-session list;\n *  - the agent exited on its own without recording a failure — it finished, so\n *    there is nothing to recover. A row that recorded a `failureReason` is\n *    deliberately still offered: that one was cut short.\n *\n * Plus a fourth that is not a heuristic but an impossibility: a Codex row that\n * never bound its rollout id has no id that can resume it at all (G6).\n *\n * And a fifth, checked first because the caller deletes its row rather than\n * skipping it: a session that was never prompted and has no cached\n * conversation under any of its ids. Neither provider writes a transcript\n * before the first turn, so the stub would open on \"No messages\" and resume\n * into nothing. Same evidence the stop path's `shouldForgetEmptySession` uses.\n */\nexport function rehydrateSkipReason(\n  row: ManagedSessionRow,\n  opts: ShouldRehydrateOptions,\n): RehydrateSkipReason | null {\n  const { hasConversation } = opts;\n  if (\n    row.prompt_count === 0 &&\n    hasConversation &&\n    ![row.session_id, row.bound_conversation_id, row.resumed_from_conversation_id].some(\n      (id) => id != null && hasConversation(id),\n    )\n  ) {\n    return \"never_prompted\";\n  }\n  if (resumeIdForRow(row) == null) return \"codex_unbound\";\n  if (!opts.projectExists(row.project_path)) return \"project_missing\";\n  if (opts.now - row.status_updated_at > REHYDRATE_WINDOW_MS) return \"too_old\";\n  if (AGENT_EXIT_SOURCES.has(row.status_source) && row.failure_reason == null) {\n    return \"agent_exited\";\n  }\n  return null;\n}\n\n/**\n * Registry row → an idle, non-attached `ManagedSession` stub.\n *\n * `status` is `idle` rather than anything new: a fresh `SessionStatus` value\n * would be rejected by `VALID_STATUSES` on `?status=` and dropped by\n * `SessionStore.paginate`'s filter, making recovered sessions *vanish* from\n * already-shipped mobile clients. The recovered-ness travels on `rehydrated`\n * instead, which `managedToResponse` turns into the existing\n * `ownership: \"historical\"` / `lifecycle: \"resumable\"` pair.\n */\nexport function rowToStubSession(row: ManagedSessionRow): ManagedSession {\n  return {\n    id: row.session_id,\n    isSubagent: row.is_subagent === 1,\n    parentConversationId: row.parent_conversation_id ?? null,\n    provider: row.provider as ManagedSession[\"provider\"],\n    projectPath: row.project_path,\n    projectName: row.project_name,\n    branch: row.branch,\n    // No PTY exists for a stub, so this is the only truthful status.\n    status: \"idle\",\n    startedAt: new Date(row.started_at),\n    completedAt: row.completed_at != null ? new Date(row.completed_at) : null,\n    promptCount: row.prompt_count,\n    lastOutput: \"\",\n    rehydrated: true,\n    ...(row.session_name != null && { sessionName: row.session_name }),\n    ...(row.project_id != null && { projectId: row.project_id }),\n    ...(row.bound_conversation_id != null && { boundConversationId: row.bound_conversation_id }),\n    ...(row.resumed_from_conversation_id != null && {\n      resumedFromConversationId: row.resumed_from_conversation_id,\n    }),\n    ...(row.failure_reason != null && { failureReason: row.failure_reason }),\n    ...(row.last_activity_at != null && { lastActivityAt: new Date(row.last_activity_at) }),\n    // Only `shutdown` crosses over. It is the one registry source that is also a\n    // wire StatusSource *and* that genuinely describes the `idle` above — the\n    // streamer stopped this session. A crashed row still says `transition` over\n    // a `running` status, and copying that here would attach observed-confidence\n    // provenance to a status we derived at boot, so leave it unset instead.\n    ...(row.status_source === \"shutdown\" && {\n      statusSource: \"shutdown\" satisfies StatusSource,\n      statusUpdatedAt: new Date(row.status_updated_at),\n      // `status` above had to flatten to `idle`, which erases whether the agent\n      // was mid-answer when we stopped it. Carried separately so a client can\n      // say \"interrupted mid-response\" without a novel SessionStatus value.\n      // Gated on the same `shutdown` source: a crashed row's `running` is a\n      // frozen value nobody confirmed, not an observation.\n      ...((row.status === \"running\" || row.status === \"waiting_input\") && {\n        interruptedStatus: row.status,\n      }),\n    }),\n  };\n}\n","import { readFileSync } from \"node:fs\";\nimport os from \"node:os\";\n\n/**\n * Machine boot identity — stable within one boot, different across boots.\n * See docs/plans/live-sessions-persistence-plan.md Phase 2.\n *\n * Stored alongside a session's pid so the boot reconciler can tell a pid it can\n * still probe from one that belongs to a previous boot, where the number has\n * since been handed to someone else's process.\n *\n * The token is only ever compared for **equality**, and a mismatch always means\n * \"skip the probe, treat as resumable\" — the harmless direction. So the uptime\n * fallback being imprecise (os.uptime() excludes suspend time on some\n * platforms) can only cost a spurious mismatch within one boot; it cannot\n * manufacture a spurious *match* across a reboot.\n */\n\nlet cached: string | undefined;\n\n/** Computed once per process — this is machine state, not a live reading. */\nexport function currentBootToken(): string {\n  if (cached === undefined) cached = computeBootToken();\n  return cached;\n}\n\nfunction computeBootToken(): string {\n  if (process.platform === \"linux\") {\n    try {\n      // Exact, and involves no clock at all.\n      const bootId = readFileSync(\"/proc/sys/kernel/random/boot_id\", \"utf8\").trim();\n      if (bootId) return bootId;\n    } catch {\n      // Unreadable (container, hardened sysctl) — fall through to the estimate.\n    }\n  }\n  // Approximate boot instant, bucketed to 10s to absorb clock drift.\n  return String(Math.round((Date.now() - os.uptime() * 1000) / 10_000));\n}\n","// `POST /api/e2ee/open` — the transport handshake.\n//\n// A second Noise handshake, this time against the static keys pairing already\n// stored, which leaves behind a transport context and (for a socket) a\n// single-use ticket. design.md §3.5, NONCE-DESIGN §8, §10 and §11.\n//\n// THIS ROUTE IS PUBLIC. It is in `PUBLIC_POST_PATHS`, so everything it parses is\n// bytes an attacker chose, before anything has authenticated them (D-9). The\n// order below is the hardening, and it is not incidental:\n//\n//   1. refuse a source that has already spent its FAILURE budget, before the\n//      handshake runs — an `IK` msg1 carries no freshness, so a captured one\n//      can be replayed for two Diffie-Hellmans each time (§8);\n//   2. bound the body as it arrives — `Content-Length` first, then the bytes\n//      themselves — so nothing is buffered in proportion to what the sender\n//      claims to be sending;\n//   3. bound the base64 BEFORE decoding it (`parseE2eeRequest`);\n//   4. run the handshake, the first point an attacker without a paired device's\n//      static key fails;\n//   5. fail closed on the device row: missing and revoked are both\n//      `E2EE_DEVICE_REVOKED`, and neither is success.\n//\n// The context that comes out is PROVISIONAL — it dies at the 30 s ticket TTL\n// unless something authenticates under it — and per-device caps bound how many\n// a replayer can hold at once.\n\nimport { Hono } from \"hono\";\nimport type { IncomingMessage } from \"http\";\nimport {\n  type ContextKind,\n  contextRegistry,\n  type E2eeContextRegistry,\n  newCtxId,\n  provisionalExpiresAt,\n  REST_CONTEXT_TTL_MS,\n} from \"../../e2ee/context\";\nimport {\n  keyPairFrom,\n  messageEphemeral,\n  OPEN_PROLOGUE,\n  readMessage1,\n  writeMessage2,\n} from \"../../e2ee/noise\";\nimport { E2eeRequestError, parseE2eeRequest } from \"../../e2ee/pair-request\";\nimport { E2EE_DEVICE_REVOKED, E2EE_PROTOCOL_VERSION, own } from \"../../e2ee/protocol\";\nimport { Msg1ReplayCache } from \"../../e2ee/replay-cache\";\nimport { getLogger } from \"../../logger\";\nimport { loadOrCreateServerIdentity } from \"../../server-identity\";\nimport type { AppEnv } from \"../app\";\nimport {\n  createRateBudget,\n  createRateLimiter,\n  OPEN_SOURCE_FAILURE_LIMIT,\n  PAIR_EXCHANGE_LIMIT,\n  PAIR_EXCHANGE_WINDOW_MS,\n} from \"../rate-limit\";\nimport type { ApiDeps } from \"../types/api-deps\";\nimport { describeE2eeCapability } from \"./misc.routes\";\n\n/**\n * Ceiling on the request body, enforced as it arrives rather than after.\n *\n * The only field that carries size is `e2ee.noise`, which `parseE2eeRequest`\n * caps at roughly 5.5 KB of base64 (`NOISE_MAX_MESSAGE_BYTES`). This is that\n * plus room for the JSON around it — deliberately snug, because the whole point\n * of a pre-authentication bound is that it is reached before memory is.\n */\nexport const MAX_OPEN_BODY_BYTES = 8 * 1024;\n\nconst log = getLogger(\"e2ee\");\n\n/**\n * Read a JSON body, refusing one that grows past `maxBytes`.\n *\n * `readBody` in `api/handlers/http-helpers.ts` concatenates every chunk before\n * parsing, so on a public path a multi-GB POST is fully buffered before any\n * bound applies — `pair-request.ts`'s base64 cap runs *after* the whole body is\n * in memory (§10). This is the bounded reader that replaces it here.\n * Retrofitting `/pair/exchange` is a one-line follow-up in another track's file.\n *\n * `Content-Length` is checked first because it costs nothing and refuses the\n * honest oversized body before a byte arrives; the running total is what\n * refuses the dishonest one.\n */\nfunction readBoundedJsonBody(req: IncomingMessage, maxBytes: number): Promise<unknown> {\n  return new Promise((resolve, reject) => {\n    // **`own()`, not a bracket read.** `req.headers` is a raw Node object with\n    // `Object.prototype` on its chain, so `req.headers[\"content-length\"]` on a\n    // request that sent no `Content-Length` returns whatever\n    // `Object.prototype[\"content-length\"]` holds — on this endpoint, which is\n    // public and runs before anything has authenticated the bytes it parses.\n    //\n    // Not a bypass: the running byte total below is the real ceiling and still\n    // refuses an oversized body. What a polluted value buys is a SELF-inflicted\n    // refusal — a bodiless or small request reads as huge and eats a `400` on\n    // the D-9 path.\n    //\n    // The stronger remedy is not to have a bracket read at all: Hono's\n    // `c.req.header()` goes through a fetch-API `Headers`, whose `get()`\n    // answers `null` for an absent name whatever the prototype holds. That is\n    // why the rest of W1b needs no guard here. This one is a raw Node object\n    // reached before Hono's request wrapper, so `own()` is the tool.\n    const declared = Number(own(req.headers, \"content-length\"));\n    if (Number.isFinite(declared) && declared > maxBytes) {\n      reject(new E2eeRequestError(\"E2EE_MALFORMED\", \"request body is too large\"));\n      return;\n    }\n    let chunks: Buffer[] = [];\n    let size = 0;\n    let refused = false;\n    req.on(\"data\", (chunk: Buffer) => {\n      if (refused) return;\n      size += chunk.length;\n      if (size > maxBytes) {\n        // Stop BUFFERING, but keep draining: the remaining chunks are dropped\n        // as they arrive rather than collected, so memory stays flat while the\n        // connection is left healthy enough to carry the 400 back.\n        refused = true;\n        chunks = [];\n        reject(new E2eeRequestError(\"E2EE_MALFORMED\", \"request body is too large\"));\n        return;\n      }\n      chunks.push(chunk);\n    });\n    req.on(\"end\", () => {\n      if (refused) return;\n      try {\n        const raw = Buffer.concat(chunks).toString(\"utf-8\");\n        resolve(raw ? JSON.parse(raw) : {});\n      } catch {\n        reject(new E2eeRequestError(\"E2EE_MALFORMED\", \"invalid JSON body\"));\n      }\n    });\n    req.on(\"error\", reject);\n  });\n}\n\n/**\n * Message 1's authenticated payload.\n *\n * `{ v, kind }`, exactly as NONCE-DESIGN §11 specifies. The two context kinds\n * (§8) differ in what they carry and how long they live, so the handshake has\n * to say which one it wants, and a capability that was never asked for must not\n * be inferred.\n *\n * `kind` lives INSIDE the AEAD rather than in the outer JSON for the same\n * reason the pairing payloads do: the outer body is attacker-modifiable, and a\n * rewritten `kind` would flip a socket context into a REST one.\n *\n * Required, not defaulted: a claim that was never made cannot be defaulted in\n * either direction (design.md §8's reasoning about `readOnly`).\n */\nexport function parseOpenPayload(payload: Buffer): { kind: ContextKind } {\n  let parsed: unknown;\n  try {\n    parsed = JSON.parse(payload.toString(\"utf-8\"));\n  } catch {\n    throw new E2eeRequestError(\"E2EE_MALFORMED\", \"e2ee open payload is not JSON\");\n  }\n  if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n    throw new E2eeRequestError(\"E2EE_MALFORMED\", \"e2ee open payload must be an object\");\n  }\n  const { v, kind } = parsed as { v?: unknown; kind?: unknown };\n  if (v !== E2EE_PROTOCOL_VERSION) {\n    throw new E2eeRequestError(\n      \"E2EE_VERSION_UNSUPPORTED\",\n      `e2ee open payload v ${String(v)} is not supported; this server speaks ${E2EE_PROTOCOL_VERSION}`,\n    );\n  }\n  if (kind !== \"ws\" && kind !== \"rest\") {\n    throw new E2eeRequestError(\"E2EE_MALFORMED\", 'e2ee open payload kind must be \"ws\" or \"rest\"');\n  }\n  return { kind };\n}\n\nexport const createE2eeRoutes = (\n  deps: Pick<ApiDeps, \"devicesRepo\" | \"featureFlagsConfig\">,\n  /** Overridden by tests so a suite gets its own registry rather than the process one. */\n  registry: E2eeContextRegistry = contextRegistry(),\n) => {\n  const app = new Hono<AppEnv>();\n  // Same policy `/api/pair/exchange` enforces, applied to two different keys\n  // for two different jobs (§8).\n  //\n  // `rateLimit` bounds ALLOCATION per authenticated device, after the\n  // handshake has named one.\n  const rateLimit = createRateLimiter({\n    limit: PAIR_EXCHANGE_LIMIT,\n    windowMs: PAIR_EXCHANGE_WINDOW_MS,\n  });\n  // `failures` bounds CPU per source address, before any Diffie-Hellman runs.\n  // It is a separate budget with a separate key because the two bound\n  // different things and neither substitutes for the other.\n  const failures = createRateBudget({\n    limit: OPEN_SOURCE_FAILURE_LIMIT,\n    windowMs: PAIR_EXCHANGE_WINDOW_MS,\n  });\n  // A captured msg1 is replayable for the life of the identity key, and a\n  // replay is a COMPLETE, authentic handshake — so neither budget above sees\n  // it: it never throws, and it resolves to a live device row. Worse, because\n  // it authenticates as the victim, the per-device limiter is what makes the\n  // attack targeted. This is what makes a replay refusable before any\n  // Diffie-Hellman runs.\n  const replays = new Msg1ReplayCache({ ttlMs: REST_CONTEXT_TTL_MS });\n\n  app.post(\"/open\", async (c) => {\n    // Same gate `/api/pair/exchange` applies, and for the same reason: a build\n    // or deployment that reports `enabled: false` on `/api/info` must not run a\n    // pre-authentication handshake anyway. Answered as a 404 so a disabled\n    // server looks like one without the endpoint at all.\n    if (!describeE2eeCapability(deps.featureFlagsConfig().values.e2ee).enabled) {\n      return c.json({ error: \"E2EE is not enabled on this server\", code: \"E2EE_DISABLED\" }, 404);\n    }\n\n    const ip = c.env.incoming?.socket?.remoteAddress ?? \"unknown\";\n    let body: unknown;\n    try {\n      body = await readBoundedJsonBody(c.env.incoming, MAX_OPEN_BODY_BYTES);\n    } catch (err) {\n      const e = err as E2eeRequestError;\n      return c.json({ error: e.message ?? \"Invalid body\", code: e.code ?? \"E2EE_MALFORMED\" }, 400);\n    }\n\n    let request: ReturnType<typeof parseE2eeRequest>;\n    try {\n      request = parseE2eeRequest((body as { e2ee?: unknown } | null)?.e2ee);\n    } catch (err) {\n      const e = err as E2eeRequestError;\n      return c.json({ error: e.message, code: e.code }, 400);\n    }\n    if (!request) {\n      // Unlike `/api/pair/exchange`, absence is not \"an older client\" here:\n      // nothing but an E2EE client has any reason to call this endpoint.\n      return c.json({ error: \"e2ee is required on this endpoint\", code: \"E2EE_MALFORMED\" }, 400);\n    }\n\n    // Checked BEFORE the handshake, because the handshake is the expensive\n    // part: `readMessage1` performs two Diffie-Hellmans on bytes nobody has\n    // authenticated yet, which is the D-9 CPU case on a public endpoint. A\n    // budget consulted after the work has already run bounds nothing.\n    //\n    // Charged only on FAILURE (below), so a device whose handshakes succeed\n    // never spends from it however often it re-opens — the per-device limit is\n    // what governs those.\n    //\n    // **The residual, stated rather than papered over.** This keys on the\n    // socket's `remoteAddress`, and behind a Cloudflare tunnel every request\n    // arrives from 127.0.0.1 — the streamer reads no forwarded-IP header — so\n    // it is one bucket for the whole fleet and cannot tell a flood from the\n    // fleet. A sustained malformed flood from the internet can therefore lock\n    // that tunnel's `/open` for a minute. The control for that is\n    // operator-side, Cloudflare rate limiting or Access in front of the\n    // tunnel, and it belongs in the rollout guide — not in a `429` we pretend\n    // is adequate. The ceiling is set high enough (§8) that no legitimate\n    // device reaches it, which is what keeps this from being the self-DoS it\n    // was at five.\n    if (!failures.check(ip)) {\n      return c.json({ error: \"Too many handshake attempts; try again in a minute\" }, 429);\n    }\n\n    // **Replay check, before any Diffie-Hellman.** `e` is message 1's first\n    // field and travels in the clear, and a legitimate client mints a fresh one\n    // per handshake — so a repeated `e` is definitionally a replay, not a\n    // heuristic for one.\n    //\n    // Charged to the SOURCE and never to the device. Charging the device is\n    // precisely what made this targeted: five replays of one captured message\n    // spent the victim's own minute and pushed it past its context cap, while\n    // every other device carried on.\n    //\n    // **The client is told nothing that distinguishes this from any other\n    // handshake failure, deliberately.** `E2EE_HANDSHAKE_FAILED` is the same\n    // code a wrong static key and a tampered ciphertext get, because telling an\n    // attacker which of its guesses was a replay is worse than the diagnostic\n    // being unavailable. The `e2ee.open_replayed` line is server-side only.\n    //\n    // The cost falls on a client that retries by re-sending the SAME bytes:\n    // it is indistinguishable from a replay and is refused for the life of the\n    // entry. That is a client obligation — every `/open` attempt, including a\n    // retry after a lost response, a timeout or a `429`, runs `writeMessage1`\n    // afresh (§11) — and it is in the contract rather than compensated for\n    // here, because compensating for it means telling replays apart from\n    // retries, which is the thing that cannot be done.\n    const ephemeral = messageEphemeral(request.message1);\n    if (ephemeral && replays.has(ephemeral)) {\n      failures.charge(ip);\n      log.warn(\"[e2ee] open refused: message 1 replays an ephemeral already seen\", {\n        event: \"e2ee.open_replayed\",\n      });\n      return c.json({ error: \"E2EE handshake failed\", code: \"E2EE_HANDSHAKE_FAILED\" }, 400);\n    }\n\n    let handshake: ReturnType<typeof readMessage1>;\n    try {\n      handshake = readMessage1({\n        staticKeyPair: keyPairFrom(loadOrCreateServerIdentity().privateKey),\n        // The psk-less pattern, named EXPLICITLY: `Noise_IK_25519_ChaChaPoly_\n        // SHA256` with the `\"threadbase-e2ee/1 open\"` prologue (§11). Both\n        // halves are domain separation — a captured pairing msg1 cannot be\n        // replayed into here, and the protocol name alone already makes the\n        // two transcripts disjoint. Passing `pattern` rather than omitting\n        // `psk` is what keeps a forgotten argument from selecting a weaker\n        // protocol somewhere else.\n        pattern: \"IK\",\n        message1: request.message1,\n        prologue: OPEN_PROLOGUE,\n      });\n    } catch {\n      // One code for every handshake failure, as at pairing: distinguishing\n      // \"wrong static key\" from \"tampered ciphertext\" would tell an attacker\n      // which half of their guess was right, and the client's remedy is the\n      // same either way. The caught error is dropped for the same reason.\n      //\n      // A failed handshake names nobody, so the cost is charged to the source\n      // address. The check that spends this is at the top of the handshake\n      // block, so the sixth garbage msg1 from one source is refused before its\n      // Diffie-Hellmans run rather than after.\n      failures.charge(ip);\n      // Logged because a device pinned to a DIFFERENT server identity fails\n      // here on every attempt, forever, and until now left nothing behind: the\n      // replay and unknown-device refusals below each wrote a line, this one\n      // wrote none, so the one permanent condition was the one that could not\n      // be told from the wire. `reason` distinguishes it; nothing about the\n      // message is logged, for the same reason the caught error is dropped.\n      //\n      // Safe to log HERE and not on the branches above: this one is charged to\n      // the source budget, so the line rate is bounded by\n      // `OPEN_SOURCE_FAILURE_LIMIT`. The malformed-body and `429` branches\n      // return before or instead of a charge, so logging there would hand an\n      // unauthenticated caller an unbounded write to the operator's disk.\n      log.warn(\"[e2ee] open refused: message 1 did not authenticate\", {\n        event: \"e2ee.open_refused\",\n        reason: \"handshake\",\n      });\n      return c.json({ error: \"E2EE handshake failed\", code: \"E2EE_HANDSHAKE_FAILED\" }, 400);\n    }\n\n    // Fail closed on the row. Absent and revoked are both refusals and neither\n    // is success (§10) — and both answer `E2EE_DEVICE_REVOKED` rather than\n    // `E2EE_CTX_UNKNOWN`, because this is the hard failure a client must\n    // surface, not the recoverable one it silently re-handshakes through (§9).\n    const staticPub = handshake.initiatorStaticPub.toString(\"base64\");\n\n    // Rate-limited HERE, on the authenticated static key, rather than on the\n    // socket's `remoteAddress` (§8). Behind a Cloudflare tunnel every request\n    // arrives from 127.0.0.1 and the streamer reads no forwarded-IP header, so\n    // an IP-keyed bucket degrades to ONE bucket for the whole fleet — it cannot\n    // tell an attacker from the fleet, and it 429s legitimate devices out of\n    // the very re-open storm §8 describes, since each device re-opens twice\n    // after a restart.\n    //\n    // The trade, stated rather than left to be discovered: a replayer still\n    // spends this server two DH per attempt before the check, because the key\n    // it is charged to only exists once the handshake has run. What this bounds\n    // is context and ticket ALLOCATION per device, which is the D-9 concern;\n    // the per-device cap is the other half and the real bound.\n    if (!rateLimit(`key:${staticPub}`)) {\n      // The same status `/pair/exchange` uses for the same policy.\n      return c.json({ error: \"Too many handshake attempts; try again in a minute\" }, 429);\n    }\n\n    const device = deps.devicesRepo()?.getByE2eeStaticPub(staticPub) ?? null;\n    if (!device || device.revoked_at != null) {\n      // The static key is deliberately not logged: it identifies a device.\n      // Charged to the source, because this IS a failure from it. The server's\n      // static key is public, so anyone can mint unlimited WELL-FORMED msg1s\n      // from fresh keypairs; each costs two Diffie-Hellmans and is refused only\n      // here. An adversary ran fifty and tripped nothing, because the budget\n      // was charged solely on a malformed handshake — the flood that costs CPU\n      // went uncounted while the trivial one locked out real devices.\n      failures.charge(ip);\n      log.warn(\"[e2ee] open refused: no live device holds that static key\", {\n        event: \"e2ee.open_refused\",\n        reason: \"unknown_device\",\n      });\n      return c.json(\n        { error: \"This device is not paired for encryption\", code: E2EE_DEVICE_REVOKED },\n        403,\n      );\n    }\n\n    // Recorded only HERE — after the handshake parsed AND the device row\n    // resolved to a live paired device.\n    //\n    // Anywhere earlier and unauthenticated traffic drives the eviction clock:\n    // a well-formed msg1 from a keypair the server has never seen would take a\n    // slot, one permitted source contributes ~43 200 entries a day (~66 % of\n    // capacity), and two fill it inside a day — at which point eviction starts\n    // discarding the OLDEST entries, which is exactly where a captured\n    // victim's msg1 lives. Only messages that authenticated as a real device\n    // can age this cache, and a stranger's replay is refused by the device row\n    // regardless.\n    //\n    // `ephemeral` is non-null on this path: `readMessage1` enforces the same\n    // minimum length.\n    if (ephemeral) replays.record(ephemeral);\n\n    let kind: ContextKind;\n    try {\n      kind = parseOpenPayload(handshake.payload).kind;\n    } catch (err) {\n      const e = err as E2eeRequestError;\n      // Past the handshake, so this caller authenticated as a live device and\n      // the line rate is bounded by the per-device limit rather than by\n      // anything a stranger controls. The code is logged; the payload is not.\n      log.warn(\"[e2ee] open refused: the sealed payload is not a valid open request\", {\n        event: \"e2ee.open_refused\",\n        reason: \"payload\",\n        code: e.code,\n      });\n      return c.json({ error: e.message, code: e.code }, 400);\n    }\n\n    // Server-assigned, 16 random bytes, never derived by the client (§12).\n    const { raw: ctxIdRaw, id: ctxId } = newCtxId();\n    const now = Date.now();\n    // **The advertised deadline is the deadline that applies** (§8, §12). Every\n    // context — socket and REST alike — starts provisional and is collected at\n    // the 30 s ticket TTL unless something authenticates under it, so msg2\n    // carries THAT, plus the `provisional` flag telling the client which kind\n    // of deadline it is holding.\n    //\n    // Advertising the 24 h lifetime here would be a promise the registry does\n    // not keep: a client that opened a REST context and sent its first request\n    // a minute later would get `E2EE_CTX_UNKNOWN` from a context it had been\n    // told it owned for a day. §8's answer to that is the client's — open a\n    // REST context with the request already in hand, never lazily in advance.\n    const expiresAt = provisionalExpiresAt(now);\n    // Only a socket needs a ticket; a REST context is addressed by `X-TB-Ctx`.\n    const ticket = kind === \"ws\" ? registry.issueTicket(ctxId, now) : undefined;\n\n    let message2: Buffer;\n    let keys: ReturnType<typeof writeMessage2>[\"keys\"];\n    try {\n      ({ message2, keys } = writeMessage2(\n        handshake,\n        Buffer.from(\n          JSON.stringify({\n            v: E2EE_PROTOCOL_VERSION,\n            ctxId,\n            expiresAt,\n            provisional: true,\n            ...(ticket && { ticket }),\n          }),\n          \"utf-8\",\n        ),\n      ));\n    } catch (err) {\n      // A server fault. Drop the ticket rather than leaving a live one bound to\n      // a context that will never exist.\n      registry.destroy(ctxId);\n      log.error(\"[e2ee] could not write the open response\", { event: \"e2ee.open_failed\", err });\n      return c.json({ error: \"Could not open an encrypted context\" }, 500);\n    }\n\n    // `consume()` hands the traffic keys over exactly once: the registry gets\n    // OpenSSL key handles and this route keeps nothing (§13).\n    registry.open({ deviceId: device.device_id, kind, ctxIdRaw, ctxId, keys: keys.consume(), now });\n    log.info(`[e2ee] opened a ${kind} context`, { event: \"e2ee.context_opened\", kind });\n\n    // `ctxId`, `expiresAt` and `ticket` travel ONLY inside the sealed payload.\n    // The outer body carries the version and the Noise message and nothing else\n    // — a ticket in an outer field would be a credential in a response body a\n    // proxy can read, which is the leak the ticket exists to close.\n    return c.json({ e2ee: { v: E2EE_PROTOCOL_VERSION, noise: message2.toString(\"base64\") } });\n  });\n\n  return app;\n};\n","// Noise_IKpsk1_25519_ChaChaPoly_SHA256, written against the Noise Protocol\n// Framework specification revision 34.\n//\n// WHY THIS IS HAND-WRITTEN, since hand-written crypto is normally the wrong\n// answer. dilemmas.md D-1 chose the `IK` pattern and named its own flip\n// condition: no maintained Noise implementation that works in React Native\n// *and* Node without a native module. That condition holds (surveyed\n// 2026-08-15 — `noise-protocol` and `noise-handshake` both need\n// `sodium-universal`, which Hermes cannot run; the pure-JS alternatives are\n// either unmaintained or libp2p-coupled). D-1's prescribed fallback is exactly\n// this: an `IK`-shaped exchange written against the spec and reviewed as such,\n// never an ad-hoc design.\n//\n// So: the pattern, the token order, the transcript hash, and the key-mixing\n// order are all decided by the specification rather than by us. Nothing here is\n// invented. Where the spec offers a choice, the choice is stated in a comment\n// with the section it comes from, because \"why is it done this way\" must be\n// answerable without re-deriving the protocol.\n//\n// The client half lives in tb-mobile over @stablelib. The two implementations\n// agree because they are checked against the same committed test vectors\n// (`__tests__/fixtures/noise-ikpsk1-vectors.json`), which is the only thing\n// that catches a transcript divergence between two independent implementations.\n//\n//   IKpsk1:\n//     <- s                        (pre-message: the QR's `spk`)\n//     ...\n//     -> e, es, s, ss, psk        (message 1, from the phone)\n//     <- e, ee, se                (message 2, from the streamer)\n//\n// `psk1` places the `psk` token at the END of the first message (spec §9.3), so\n// the pair token binds the handshake to *this* QR: completing it proves the\n// initiator scanned this code rather than merely that it reached the server.\n\nimport {\n  createCipheriv,\n  createDecipheriv,\n  createHash,\n  createPrivateKey,\n  createPublicKey,\n  diffieHellman,\n  generateKeyPairSync,\n  hkdfSync,\n  type KeyObject,\n} from \"crypto\";\nimport { importSecret, isBytes, own, redactKeyMaterial, unpooled } from \"./protocol\";\n\nexport const NOISE_PROTOCOL_NAME = \"Noise_IKpsk1_25519_ChaChaPoly_SHA256\";\n\n/**\n * The psk-less pattern, used by `POST /api/e2ee/open` (NONCE-DESIGN §11).\n *\n * The transport handshake runs against static keys pairing already stored and\n * has no pair token, so there is no pre-shared secret to mix. A constant public\n * \"PSK\" was considered and rejected: it reduces to plain `IK` anyway and\n * encodes \"there is a PSK here that means nothing\", which a reader takes as a\n * freshness guarantee that does not exist.\n *\n * The name is not decoration — it is the string that seeds `h`, so it is itself\n * domain separation: a message from one pattern cannot be read by the other\n * even before the prologue is considered.\n */\nexport const NOISE_IK_PROTOCOL_NAME = \"Noise_IK_25519_ChaChaPoly_SHA256\";\n\n/**\n * Which of the two patterns a handshake call means.\n *\n * **Stated, never inferred.** This used to be decided by whether `psk` was\n * present, which made a forgotten argument select the WEAKER protocol: a\n * pairing call site that omitted its psk would have silently run plain `IK`\n * and lost the pair-token binding that is the entire reason pairing uses\n * `IKpsk1` (spec §9.3, design.md §2.4). A missing argument must never be a\n * downgrade, so the pattern is named and the two are checked against each\n * other in both directions — a psk handed to `IK` is the same caller confusion\n * wearing the other face.\n *\n * The default is the STRONGER pattern, so an un-updated call site that forgets\n * its psk throws instead of quietly weakening.\n */\nexport type NoisePattern = \"IKpsk1\" | \"IK\";\n\nconst NOISE_PATTERNS: readonly NoisePattern[] = [\"IKpsk1\", \"IK\"];\n\n/**\n * The protocol name for a pattern, or a throw.\n *\n * **Exhaustive, and fail-closed on anything unrecognised.** This used to end in\n * `pattern === \"IKpsk1\" ? A : B`, so a value that matched neither guard —\n * `\"IKPSK1\"`, `\"ik\"`, anything arriving from a config file, a wire field or a\n * client artefact — fell through to psk-less `IK` and silently dropped the\n * pair-token binding. TypeScript rules that out for callers inside this\n * repository; the client track consumes this module as an artefact, and a type\n * is not a runtime check.\n *\n * So there are two layers: a runtime membership test for values that were never\n * typed, and a `never` arm that makes adding a third pattern a compile error\n * here rather than a silent default somewhere else.\n */\n/**\n * A prologue is REQUIRED and must be real bytes — of any length, since the two\n * namespace strings differ in length.\n *\n * Not `assertBytes`, which pins a width: this is the presence-and-type half of\n * the same rule. It is a parameter rather than a default because\n * `args.prologue ?? PAIR_PROLOGUE` is a prototype-chain read, and a polluted\n * `Object.prototype.prologue` made the PAIRING handshake accept an `/open`\n * message — §11's domain separation collapsing through a language default\n * nobody wrote.\n */\nfunction assertPrologue(prologue: unknown): asserts prologue is Buffer {\n  if (!(prologue instanceof Uint8Array) || prologue.BYTES_PER_ELEMENT !== 1) {\n    throw new NoiseError(\"prologue is required: pass PAIR_PROLOGUE or OPEN_PROLOGUE\");\n  }\n}\n\nfunction protocolNameFor(pattern: NoisePattern, psk: Buffer | undefined): string {\n  if (!NOISE_PATTERNS.includes(pattern)) {\n    throw new NoiseError(\n      `Unknown Noise pattern ${JSON.stringify(String(pattern))}; expected \"IKpsk1\" or \"IK\"`,\n    );\n  }\n  switch (pattern) {\n    case \"IKpsk1\":\n      // ONE guard, and it checks BYTE LENGTH.\n      //\n      // Two versions of this were wrong in the same way. `!psk` accepted\n      // `Buffer.alloc(0)`, which is truthy, and completed a full `IKpsk1`\n      // handshake over an empty psk. `psk.length !== 32` then accepted\n      // `new Float64Array(32)`, whose `.length` is 32 and whose byteLength is\n      // 256, binding 256 zero bytes. Both produce the thing §11 rejects — a\n      // wire that says `IKpsk1` over a binding that is a constant — and both\n      // arrived through a guard rather than a decision.\n      //\n      // The defect was never in any single guard. It was in writing the guard\n      // four times, so `isBytes` is now the only place the question is asked.\n      if (!isBytes(psk, HASHLEN)) {\n        throw new NoiseError(\n          `The IKpsk1 pattern requires a ${HASHLEN}-byte psk; pass one, or pattern: \"IK\"`,\n        );\n      }\n      return NOISE_PROTOCOL_NAME;\n    case \"IK\":\n      if (psk) {\n        throw new NoiseError('The IK pattern takes no psk; drop it, or pattern: \"IKpsk1\"');\n      }\n      return NOISE_IK_PROTOCOL_NAME;\n    default: {\n      const unreachable: never = pattern;\n      throw new NoiseError(`Unhandled Noise pattern ${String(unreachable)}`);\n    }\n  }\n}\n\n/**\n * Prologue for the PAIRING handshake, mixed into the transcript before any\n * token (spec §5.3, `MixHash(prologue)`).\n *\n * Not in design.md, and added deliberately. The design has two `IK` handshakes:\n * this one at `/api/pair/exchange` with the pair token as PSK, and a later one\n * at `/api/e2ee/open` against stored static keys with no PSK. Without a\n * prologue the two transcripts differ only by the presence of the PSK, so\n * \"could a message from one be replayed into the other\" becomes a question you\n * answer by reasoning about the PSK. A prologue answers it by construction, and\n * it costs one hash.\n *\n * **`\"threadbase-e2ee/<version> <purpose>\"` is a namespace, not one string.**\n * Its sibling is `\"threadbase-e2ee/1 open\"` for `/api/e2ee/open`, named here\n * rather than left for that phase to invent: half a domain separation is a\n * property nobody has, and a convention chosen once with both instances visible\n * is the one that survives a third handshake. The constant itself is not\n * declared until something uses it.\n *\n * Changing this string is a silent, total incompatibility with tb-mobile — it\n * is hashed into `h` before any token, so the only symptom is \"decryption\n * failed\". It is pinned by a committed vector for that reason.\n */\nexport const PAIR_PROLOGUE = Buffer.from(\"threadbase-e2ee/1 pair\", \"utf-8\");\n\n/**\n * The sibling named above, now that `/api/e2ee/open` exists to use it.\n *\n * Same namespace, different purpose, so a message from the pairing handshake\n * can never be replayed into the transport handshake or the other way round —\n * by construction, rather than by an argument about the PSK.\n */\nexport const OPEN_PROLOGUE = Buffer.from(\"threadbase-e2ee/1 open\", \"utf-8\");\n\n// The `/open` handshake takes NO psk: pass `pattern: \"IK\"` to these calls and\n// omit `psk`. Omitting `psk` alone is NOT how the pattern is selected — that\n// would make a forgotten argument a downgrade — and it throws.\n\nconst DHLEN = 32;\nconst HASHLEN = 32;\nconst TAGLEN = 16;\n/** `e` (32) + `s` sealed (32 + 16) — everything before message 1's payload. */\nexport const NOISE_MESSAGE_1_OVERHEAD = DHLEN + DHLEN + TAGLEN + TAGLEN;\n/**\n * The initiator's ephemeral public key, or `null` when the message is too short\n * to contain one.\n *\n * `e` is the FIRST field of message 1 and travels in the clear (spec §7.4), so\n * it can be read before any Diffie-Hellman — which is what lets\n * `/api/e2ee/open` reject a replayed msg1 without paying for it. Reading it\n * lives here, with the rest of the wire layout, rather than in a route indexing\n * into a buffer by a number it inferred.\n *\n * Returns a VIEW, not a copy: callers must not mutate it.\n */\nexport function messageEphemeral(message1: Buffer): Buffer | null {\n  if (message1.length < NOISE_MESSAGE_1_OVERHEAD) return null;\n  return message1.subarray(0, DHLEN);\n}\n\n/** `e` (32) — everything before message 2's payload. */\nexport const NOISE_MESSAGE_2_OVERHEAD = DHLEN + TAGLEN;\n\n/**\n * Cap on a handshake message, checked BEFORE anything is allocated or parsed.\n *\n * The Noise spec's own transport limit is 65535 bytes. This is far below it\n * because our payloads are a handful of JSON fields, and because\n * `/api/pair/exchange` is a public, unauthenticated endpoint — the same\n * reasoning D-9 applies to the unseal middleware, reaching this far forward.\n */\nexport const NOISE_MAX_MESSAGE_BYTES = 4096;\n\nexport class NoiseError extends Error {\n  constructor(message: string) {\n    super(message);\n    this.name = \"NoiseError\";\n  }\n}\n\n// ─── X25519 key helpers ─────────────────────────────────────────────\n//\n// Raw 32-byte keys are what travels; Node's crypto wants KeyObjects. The JWK\n// route is the same one server-identity.ts uses, so there is one representation\n// of an X25519 key in this codebase rather than two.\n\nexport interface KeyPair {\n  publicKey: KeyObject;\n  privateKey: KeyObject;\n  /** Raw 32 bytes, which is what goes on the wire. */\n  publicKeyRaw: Buffer;\n}\n\nexport function generateKeyPair(): KeyPair {\n  const { publicKey, privateKey } = generateKeyPairSync(\"x25519\");\n  return { publicKey, privateKey, publicKeyRaw: rawPublicKey(publicKey) };\n}\n\n/**\n * A `KeyPair` around a private key this process already holds.\n *\n * The bridge from `loadOrCreateServerIdentity()`, which owns the key file and\n * returns a bare `KeyObject`, to the handshake, which needs the public half\n * alongside it. Kept here rather than in server-identity.ts so that module\n * stays about storing a key rather than about the protocol that uses it.\n */\nexport function keyPairFrom(privateKey: KeyObject): KeyPair {\n  const publicKey = createPublicKey(privateKey);\n  return { publicKey, privateKey, publicKeyRaw: rawPublicKey(publicKey) };\n}\n\n/**\n * Raw 32 bytes out of a KeyObject. JWK OKP `x` is base64url of exactly that.\n *\n * Accepts either half: `createPublicKey` derives the public key from a private\n * one but rejects a KeyObject that is already public, so the type is checked\n * rather than the call being made unconditionally.\n */\nexport function rawPublicKey(key: KeyObject): Buffer {\n  const pub = key.type === \"public\" ? key : createPublicKey(key);\n  const jwk = pub.export({ format: \"jwk\" });\n  if (jwk.crv !== \"X25519\" || typeof jwk.x !== \"string\") {\n    throw new NoiseError(\"Not an X25519 public key\");\n  }\n  return Buffer.from(jwk.x, \"base64url\");\n}\n\n/**\n * A KeyObject from raw bytes, or a `NoiseError`.\n *\n * This is a trust boundary: the bytes are the QR's `spk` on the client side and\n * an attacker-supplied handshake message on the server side. A wrong length is\n * rejected here rather than being padded, truncated, or handed to a DH that\n * would fail somewhere less legible.\n */\nexport function publicKeyFromRaw(raw: Buffer): KeyObject {\n  if (raw.length !== DHLEN) {\n    throw new NoiseError(`X25519 public key must be ${DHLEN} bytes, got ${raw.length}`);\n  }\n  try {\n    return createPublicKey({\n      key: { kty: \"OKP\", crv: \"X25519\", x: raw.toString(\"base64url\") },\n      format: \"jwk\",\n    });\n  } catch {\n    // The input is dropped rather than interpolated: on the responder side it\n    // is attacker-controlled, and echoing it into a log or an error is how a\n    // parser becomes a reflection surface.\n    throw new NoiseError(\"Invalid X25519 public key\");\n  }\n}\n\n/**\n * A keypair from a raw 32-byte private scalar.\n *\n * Exists for the committed test vectors: a vector is only a contract if the\n * keys are fixed, and there is no other way to hand Node a chosen X25519\n * private key. Not used by any production path — every real keypair comes from\n * `generateKeyPair()` or the identity key file.\n *\n * Goes through PKCS#8 DER rather than JWK because a JWK OKP private key must\n * carry a matching `x`, and the point here is to supply only `d`. The prefix is\n * the fixed X25519 PrivateKeyInfo header (RFC 8410): SEQUENCE, version 0,\n * AlgorithmIdentifier 1.3.101.110, then an OCTET STRING wrapping the 32-byte\n * scalar.\n */\nexport function keyPairFromRawPrivate(raw: Buffer): KeyPair {\n  if (raw.length !== DHLEN) {\n    throw new NoiseError(`X25519 private key must be ${DHLEN} bytes, got ${raw.length}`);\n  }\n  const der = Buffer.concat([Buffer.from(\"302e020100300506032b656e04220420\", \"hex\"), raw]);\n  const privateKey = createPrivateKey({ key: der, format: \"der\", type: \"pkcs8\" });\n  const publicKey = createPublicKey(privateKey);\n  return { publicKey, privateKey, publicKeyRaw: rawPublicKey(publicKey) };\n}\n\n/**\n * The pair token as a 32-byte PSK.\n *\n * The token is `pt_<32 hex chars>` — 35 bytes of ASCII, and the spec requires\n * the PSK to be exactly 32. Hashing with a domain-separating label is the\n * standard way to fit it, and the label means this value can never collide with\n * some other use of the same token elsewhere in the system.\n *\n * Both implementations must compute this identically or the handshake fails\n * with no diagnostic beyond \"decryption failed\", which is why it is pinned by a\n * test vector rather than left as an obvious detail.\n */\nexport function pskFromPairToken(token: string): Buffer {\n  return createHash(\"sha256\")\n    .update(\"threadbase-e2ee/1 psk\", \"utf-8\")\n    .update(token, \"utf-8\")\n    .digest();\n}\n\n// ─── CipherState (spec §5.1) ────────────────────────────────────────\n\n/**\n * ChaChaPoly nonce, per spec §12.3: 4 zero bytes then the 8-byte\n * **little-endian** encoding of n.\n *\n * Deliberately NOT the record layer's nonce, which is `direction(4) ||\n * counter(8)` big-endian (design.md §3.3). Two layers, two encodings, both\n * correct for their own specification — do not unify them.\n */\nfunction chachaNonce(n: bigint): Buffer {\n  const nonce = Buffer.alloc(12);\n  nonce.writeBigUInt64LE(n, 4);\n  return nonce;\n}\n\n/**\n * Noise's `CipherState` (spec §5.1).\n *\n * Exported for two reasons, neither of them scaffolding. Phase 3's record layer\n * has to build a transport cipher from the buffers `split()` returns, so this is\n * the type it will construct. And the §5.1 rule that a failed decryption must\n * not advance `n` is unobservable through the handshake API — every AEAD\n * operation there runs at `n = 0`, because each is preceded by a `MixKey` that\n * calls `initializeKey`. Without a way to reach a cipher state directly, that\n * rule is protected by nothing: a \"simplification\" that advances before\n * verifying passes every other test in the suite.\n */\nexport class CipherState {\n  /**\n   * `#private`, not `private`: TypeScript's `private` is a compile-time\n   * annotation over an ordinary own property, which `showHidden` prints and\n   * `getOwnPropertyDescriptors` returns. A `#` field is not a property at all.\n   */\n  #k: Buffer | null = null;\n  private n = 0n;\n\n  constructor() {\n    redactKeyMaterial(this, () => `CipherState { n: ${this.n}, k: <#private> }`);\n  }\n\n  initializeKey(key: Buffer | null): void {\n    this.#k = key;\n    this.n = 0n;\n  }\n\n  hasKey(): boolean {\n    return this.#k !== null;\n  }\n\n  encryptWithAd(ad: Buffer, plaintext: Buffer): Buffer {\n    if (!this.#k) return plaintext;\n    const cipher = createCipheriv(\"chacha20-poly1305\", this.#k, chachaNonce(this.n), {\n      authTagLength: TAGLEN,\n    });\n    cipher.setAAD(ad, { plaintextLength: plaintext.length });\n    const out = Buffer.concat([cipher.update(plaintext), cipher.final(), cipher.getAuthTag()]);\n    this.n += 1n;\n    return out;\n  }\n\n  decryptWithAd(ad: Buffer, ciphertext: Buffer): Buffer {\n    if (!this.#k) return ciphertext;\n    if (ciphertext.length < TAGLEN) throw new NoiseError(\"Ciphertext shorter than its tag\");\n    const body = ciphertext.subarray(0, ciphertext.length - TAGLEN);\n    const tag = ciphertext.subarray(ciphertext.length - TAGLEN);\n    const decipher = createDecipheriv(\"chacha20-poly1305\", this.#k, chachaNonce(this.n), {\n      authTagLength: TAGLEN,\n    });\n    decipher.setAAD(ad, { plaintextLength: body.length });\n    decipher.setAuthTag(tag);\n    let out: Buffer;\n    try {\n      out = Buffer.concat([decipher.update(body), decipher.final()]);\n    } catch {\n      // The nonce is deliberately NOT advanced on failure (spec §5.1). A\n      // handshake that fails is over; advancing would only make a retry fail\n      // differently and hide the cause.\n      throw new NoiseError(\"Decryption failed\");\n    }\n    this.n += 1n;\n    return out;\n  }\n}\n\n// ─── SymmetricState (spec §5.2) ─────────────────────────────────────\n\nclass SymmetricState {\n  /**\n   * The LIVE chaining key. Both traffic keys are `HKDF(ck, \"\")`, so rendering\n   * it hands over the whole session rather than a fragment — it printed at\n   * `util.inspect`'s default depth before this became a `#` field.\n   */\n  #ck: Buffer;\n  #h: Buffer;\n  readonly #cipher = new CipherState();\n\n  constructor(protocolName: string) {\n    const name = Buffer.from(protocolName, \"utf-8\");\n    // Spec §5.2: pad to HASHLEN if it fits, otherwise hash it. Our name is 36\n    // bytes, so this is always the hash branch — written in full anyway, since\n    // the alternative is a constant nobody can check against the spec.\n    this.#h =\n      name.length <= HASHLEN\n        ? Buffer.concat([name, Buffer.alloc(HASHLEN - name.length)])\n        : createHash(\"sha256\").update(name).digest();\n    this.#ck = Buffer.from(this.#h);\n    this.#cipher.initializeKey(null);\n    // `ck` is the LIVE chaining key, and both traffic keys are `HKDF(ck, \"\")` —\n    // so rendering it hands over the whole session, not a fragment of it. It\n    // printed at `util.inspect`'s DEFAULT depth, with no `showHidden` needed,\n    // because this object was the one place the §13 rule had not reached.\n    redactKeyMaterial(this, () => \"SymmetricState { ck, h, cipher: <#private> }\");\n  }\n\n  /**\n   * Noise's HKDF (spec §4.3) is RFC 5869 with `salt = chaining_key`,\n   * `ikm = input_key_material` and an EMPTY info, so Node's `hkdfSync` is\n   * exactly it rather than approximately it. Using the platform's HKDF removes\n   * the HMAC chain that would otherwise be the easiest thing here to get\n   * subtly wrong.\n   */\n  private hkdf(ikm: Buffer, outputs: 2 | 3): Buffer[] {\n    const raw = Buffer.from(hkdfSync(\"sha256\", ikm, this.#ck, Buffer.alloc(0), HASHLEN * outputs));\n    const out: Buffer[] = [];\n    for (let i = 0; i < outputs; i++) out.push(raw.subarray(i * HASHLEN, (i + 1) * HASHLEN));\n    return out;\n  }\n\n  mixKey(ikm: Buffer): void {\n    const [ck, tempK] = this.hkdf(ikm, 2);\n    this.#ck = ck;\n    this.#cipher.initializeKey(tempK);\n  }\n\n  mixHash(data: Buffer): void {\n    this.#h = createHash(\"sha256\").update(this.#h).update(data).digest();\n  }\n\n  /** Spec §5.2. Used only by the `psk` token. */\n  mixKeyAndHash(ikm: Buffer): void {\n    const [ck, tempH, tempK] = this.hkdf(ikm, 3);\n    this.#ck = ck;\n    this.mixHash(tempH);\n    this.#cipher.initializeKey(tempK);\n  }\n\n  encryptAndHash(plaintext: Buffer): Buffer {\n    const ciphertext = this.#cipher.encryptWithAd(this.#h, plaintext);\n    this.mixHash(ciphertext);\n    return ciphertext;\n  }\n\n  decryptAndHash(ciphertext: Buffer): Buffer {\n    const plaintext = this.#cipher.decryptWithAd(this.#h, ciphertext);\n    // Hashes the CIPHERTEXT, not the plaintext, and only after a successful\n    // decrypt — so both sides' transcripts commit to the same bytes.\n    this.mixHash(ciphertext);\n    return plaintext;\n  }\n\n  /** The transcript hash. Both sides must arrive at the same value. */\n  handshakeHash(): Buffer {\n    return Buffer.from(this.#h);\n  }\n\n  /** Spec §5.2 `Split()`: the two directional transport keys. */\n  split(): { k1: Buffer; k2: Buffer } {\n    const [k1, k2] = this.hkdf(Buffer.alloc(0), 2);\n    return { k1: Buffer.from(k1), k2: Buffer.from(k2) };\n  }\n}\n\n// ─── The handshake ──────────────────────────────────────────────────\n\n/**\n * The transport keys a completed handshake yields.\n *\n * **A class with `#private` fields, not an object literal**, because this is\n * the value that TRAVELS — out of `respond()`, through the route, into\n * `createRecordState` — and so it is the one most likely to end up inside an\n * error, a log line or a test diff. As a literal its two traffic keys were\n * ordinary properties: `defineProperty(enumerable: false)` hid them from\n * default `inspect` and `{ showHidden: true }` printed them anyway; the custom\n * inspect handler hid them from that and `{ customInspect: false }` printed\n * them anyway. A `#` field is not a property, so there is no mode to find.\n *\n * The getters are on the prototype, which `inspect` does not walk and\n * `getOwnPropertyDescriptors` does not report, so `keys.clientToServer` reads\n * exactly as it did while rendering nothing.\n */\nexport interface TrafficKeys {\n  /** Initiator → responder. An OpenSSL key handle, never bytes. */\n  readonly clientToServer: KeyObject;\n  /** Responder → initiator. */\n  readonly serverToClient: KeyObject;\n  /** The transcript hash. Public, and an unpooled copy. */\n  readonly handshakeHash: Buffer;\n}\n\n/**\n * The transport keys a completed handshake yields.\n *\n * **The keys are `KeyObject`s and there is no byte getter at all.** Hiding was\n * tried four ways and defeated four times — non-enumerable by `showHidden`, the\n * inspect handler by `customInspect: false`, `#private` fields by\n * `{ getters: true }` reading through the accessors, and finally `#private`\n * with no getter by the ALLOCATION POOL: Node pool-allocates small Buffers, a\n * Buffer's `.buffer` exposes the shared 8 KiB block, and a public Buffer on the\n * same object hands out a window onto the pool the private key was allocated\n * in. Hiding a Buffer cannot work, so no traffic-key bytes exist as a JS Buffer\n * after the handshake: they are imported into OpenSSL and the copies wiped.\n *\n * `consume()` is the only way out and it works once, so ownership of the keys\n * is a fact about the object rather than a convention.\n */\nexport class HandshakeKeys {\n  #clientToServer: KeyObject | null;\n  #serverToClient: KeyObject | null;\n  readonly #handshakeHash: Buffer;\n\n  constructor(handshakeHash: Buffer, clientToServer: Buffer, serverToClient: Buffer) {\n    this.#handshakeHash = unpooled(handshakeHash);\n    this.#clientToServer = importSecret(clientToServer);\n    this.#serverToClient = importSecret(serverToClient);\n    // These are `split()`'s outputs and this object owns them: wiped now, so the\n    // window in which a traffic key exists as JS-visible bytes ends here.\n    clientToServer.fill(0);\n    serverToClient.fill(0);\n    redactKeyMaterial(this, () => \"HandshakeKeys { traffic keys: <KeyObject, off-heap> }\");\n  }\n\n  /**\n   * Hand the keys to the record layer. Once.\n   *\n   * NOT the transcript hash's guard — that is public — but the keys': a second\n   * caller getting the same handles would mean two record layers believing they\n   * own one counter space, which is the shape every nonce rule here exists to\n   * prevent.\n   */\n  consume(): TrafficKeys {\n    const clientToServer = this.#clientToServer;\n    const serverToClient = this.#serverToClient;\n    if (!clientToServer || !serverToClient) {\n      throw new NoiseError(\"These handshake keys have already been consumed\");\n    }\n    this.#clientToServer = null;\n    this.#serverToClient = null;\n    return { clientToServer, serverToClient, handshakeHash: unpooled(this.#handshakeHash) };\n  }\n}\n\nfunction dh(privateKey: KeyObject, publicKey: KeyObject): Buffer {\n  return diffieHellman({ privateKey, publicKey });\n}\n\n/**\n * Message 1, from the phone.\n *\n * Exported from the *server* module although the server never sends one. It is\n * the only way to drive the responder in a test, and it generates the committed\n * vectors the tb-mobile implementation is checked against — a second, drifting\n * initiator written inside a test file is the precise failure two independent\n * implementations invite.\n */\nexport function writeMessage1(args: {\n  staticKeyPair: KeyPair;\n  responderStaticPub: Buffer;\n  /** Required by `IKpsk1`, forbidden by `IK`. */\n  psk?: Buffer;\n  /** Defaults to the stronger `IKpsk1`. `/api/e2ee/open` passes `\"IK\"` (§11). */\n  pattern?: NoisePattern;\n  payload: Buffer;\n  /** REQUIRED: `PAIR_PROLOGUE` or `OPEN_PROLOGUE`. Never defaulted (§11). */\n  prologue: Buffer;\n  /** Test seam only. Production always generates a fresh ephemeral. */\n  ephemeral?: KeyPair;\n}): { message: Buffer; state: HandshakeInitiatorState } {\n  // `Object.hasOwn`, not `??`: `??` walks the prototype chain, so\n  // `Object.prototype.pattern = \"IK\"` silently downgrades every psk-less call\n  // site and breaks pairing. \"The default is the stronger pattern\" has to be\n  // enforced, not commented.\n  const pattern = Object.hasOwn(args, \"pattern\") ? (args.pattern as NoisePattern) : \"IKpsk1\";\n  const state = new SymmetricState(protocolNameFor(pattern, args.psk));\n  // REQUIRED, never defaulted. `args.prologue ??` is a prototype-chain read, so\n  // `Object.prototype.prologue = OPEN_PROLOGUE` made the PAIRING handshake\n  // accept an `/open` message — §11's domain separation collapsing through a\n  // language default nobody wrote.\n  // `own`, not `args.prologue`: making the parameter required is not enough,\n  // because reading it directly is ITSELF the prototype-chain read. With\n  // `Object.prototype.prologue` set, a call that passes none still found one.\n  const prologue = own(args, \"prologue\");\n  assertPrologue(prologue);\n  state.mixHash(prologue);\n\n  const rs = publicKeyFromRaw(args.responderStaticPub);\n  // Pre-message `<- s`: the responder's static key is known in advance, from\n  // the QR. This is what makes the QR an out-of-band authentication channel.\n  state.mixHash(args.responderStaticPub);\n\n  // Read off the ARGUMENT: a polluted `Object.prototype.ephemeral` pins every\n  // handshake to one attacker-chosen `e`, which by §8's own rule is\n  // definitionally a replay.\n  const e = own(args, \"ephemeral\") ?? generateKeyPair();\n\n  // `e`. In a PSK handshake the token also calls MixKey (spec §9.2), so the\n  // ephemeral contributes to the chaining key before any DH — without it the\n  // first message's payload would be protected by the PSK alone.\n  state.mixHash(e.publicKeyRaw);\n  state.mixKey(e.publicKeyRaw);\n\n  // `es` — DH(initiator ephemeral, responder static).\n  state.mixKey(dh(e.privateKey, rs));\n\n  // `s` — the initiator's static key, transmitted ENCRYPTED. That is `IK`'s\n  // identity-hiding property and the reason the pattern was chosen over `XX`.\n  const encryptedStatic = state.encryptAndHash(args.staticKeyPair.publicKeyRaw);\n\n  // `ss` — DH(initiator static, responder static).\n  state.mixKey(dh(args.staticKeyPair.privateKey, rs));\n\n  // `psk` — the pair token. Placed last in message 1 by the `psk1` modifier.\n  // The `IK` pattern has no token here at all (§11), and `protocolNameFor` has\n  // already refused the two incoherent combinations.\n  if (pattern === \"IKpsk1\") state.mixKeyAndHash(args.psk as Buffer);\n\n  const encryptedPayload = state.encryptAndHash(args.payload);\n\n  const initiator: HandshakeInitiatorState = {\n    symmetric: state,\n    ephemeral: e,\n    staticKeyPair: args.staticKeyPair,\n  };\n  // Holds the symmetric state and both keypairs. Redacted for the same reason\n  // and through the same helper (§13).\n  // Its `symmetric` is opaque by construction now, and the keypairs are Node\n  // `KeyObject`s, which never render their bytes. The summary is for reading.\n  redactKeyMaterial(initiator, () => \"HandshakeInitiatorState { keys: <#private> }\");\n\n  return {\n    message: Buffer.concat([e.publicKeyRaw, encryptedStatic, encryptedPayload]),\n    state: initiator,\n  };\n}\n\nexport interface HandshakeInitiatorState {\n  symmetric: SymmetricState;\n  ephemeral: KeyPair;\n  staticKeyPair: KeyPair;\n}\n\n/** Message 2, read by the phone. Completes the handshake. */\nexport function readMessage2(\n  state: HandshakeInitiatorState,\n  message: Buffer,\n): { payload: Buffer; keys: HandshakeKeys } {\n  assertMessageSize(message, NOISE_MESSAGE_2_OVERHEAD);\n  const re = publicKeyFromRaw(message.subarray(0, DHLEN));\n\n  state.symmetric.mixHash(message.subarray(0, DHLEN));\n  state.symmetric.mixKey(message.subarray(0, DHLEN));\n  state.symmetric.mixKey(dh(state.ephemeral.privateKey, re)); // ee\n  state.symmetric.mixKey(dh(state.staticKeyPair.privateKey, re)); // se\n\n  const payload = state.symmetric.decryptAndHash(message.subarray(DHLEN));\n  return { payload, keys: finish(state.symmetric) };\n}\n\nexport interface ResponderResult {\n  /** The initiator's static public key, authenticated by the handshake. */\n  initiatorStaticPub: Buffer;\n  payload: Buffer;\n  message2: Buffer;\n  keys: HandshakeKeys;\n}\n\n/**\n * Half-completed responder state, between reading message 1 and writing 2.\n *\n * Deliberately only ever a local: it is handed straight back into\n * `writeMessage2` within the same synchronous stretch of one request, never\n * stored, never keyed by anything a caller supplies. A responder state that\n * outlived a request would be a thing to allocate and expire on a public\n * endpoint, which is what the single-call shape originally avoided.\n */\nexport interface HandshakeResponderState {\n  symmetric: SymmetricState;\n  /** The initiator's static public key, authenticated by message 1. */\n  initiatorStaticPub: Buffer;\n  /** Message 1's decrypted payload. */\n  payload: Buffer;\n  initiatorEphemeral: KeyObject;\n  initiatorStatic: KeyObject;\n  staticKeyPair: KeyPair;\n}\n\n/**\n * Read message 1. Authenticates the initiator and recovers its static key.\n *\n * Split from `writeMessage2` because the server DOES now have something to do\n * between them: message 2's payload carries the `deviceId`, and the device row\n * cannot be written until the pair token has been spent, which cannot happen\n * until this half has succeeded. The original single-call shape assumed nothing\n * sat in the middle; the pairing handler is the caller that proved otherwise.\n *\n * SYNCHRONOUS on purpose, and it must stay that way: the caller runs this\n * between validating the pair token and consuming it, and `PairTokenStore` has\n * no lock. An `await` in that gap would let two concurrent requests with the\n * same token both pass validation.\n */\nexport function readMessage1(args: {\n  staticKeyPair: KeyPair;\n  /** Required by `IKpsk1`, forbidden by `IK`. */\n  psk?: Buffer;\n  /** Defaults to the stronger `IKpsk1`. `/api/e2ee/open` passes `\"IK\"` (§11). */\n  pattern?: NoisePattern;\n  message1: Buffer;\n  /** REQUIRED: `PAIR_PROLOGUE` or `OPEN_PROLOGUE`. Never defaulted (§11). */\n  prologue: Buffer;\n}): HandshakeResponderState {\n  assertMessageSize(args.message1, NOISE_MESSAGE_1_OVERHEAD);\n\n  // `Object.hasOwn`, not `??`: `??` walks the prototype chain, so\n  // `Object.prototype.pattern = \"IK\"` silently downgrades every psk-less call\n  // site and breaks pairing. \"The default is the stronger pattern\" has to be\n  // enforced, not commented.\n  const pattern = Object.hasOwn(args, \"pattern\") ? (args.pattern as NoisePattern) : \"IKpsk1\";\n  const state = new SymmetricState(protocolNameFor(pattern, args.psk));\n  // REQUIRED, never defaulted. `args.prologue ??` is a prototype-chain read, so\n  // `Object.prototype.prologue = OPEN_PROLOGUE` made the PAIRING handshake\n  // accept an `/open` message — §11's domain separation collapsing through a\n  // language default nobody wrote.\n  // `own`, not `args.prologue`: making the parameter required is not enough,\n  // because reading it directly is ITSELF the prototype-chain read. With\n  // `Object.prototype.prologue` set, a call that passes none still found one.\n  const prologue = own(args, \"prologue\");\n  assertPrologue(prologue);\n  state.mixHash(prologue);\n  state.mixHash(args.staticKeyPair.publicKeyRaw);\n\n  // `e`\n  const reRaw = args.message1.subarray(0, DHLEN);\n  const re = publicKeyFromRaw(reRaw);\n  state.mixHash(reRaw);\n  state.mixKey(reRaw);\n\n  // `es` — from the responder's side, its static against the initiator's ephemeral.\n  state.mixKey(dh(args.staticKeyPair.privateKey, re));\n\n  // `s` — decrypting this is the first point an attacker without the right\n  // static key fails, and it happens before the payload is touched.\n  const encryptedStatic = args.message1.subarray(DHLEN, DHLEN + DHLEN + TAGLEN);\n  const initiatorStaticPub = state.decryptAndHash(encryptedStatic);\n  const rs = publicKeyFromRaw(initiatorStaticPub);\n\n  // `ss`\n  state.mixKey(dh(args.staticKeyPair.privateKey, rs));\n\n  // `psk` — the `IK` pattern has no token here (§11).\n  if (pattern === \"IKpsk1\") state.mixKeyAndHash(args.psk as Buffer);\n\n  const payload = state.decryptAndHash(args.message1.subarray(DHLEN + DHLEN + TAGLEN));\n\n  const responder: HandshakeResponderState = {\n    symmetric: state,\n    initiatorStaticPub,\n    payload,\n    initiatorEphemeral: re,\n    initiatorStatic: rs,\n    staticKeyPair: args.staticKeyPair,\n  };\n  redactKeyMaterial(responder, () => \"HandshakeResponderState { keys: <#private> }\");\n  return responder;\n}\n\n/** Write message 2 and derive the transport keys. Completes the handshake. */\nexport function writeMessage2(\n  state: HandshakeResponderState,\n  responsePayload: Buffer,\n  /** Test seam only. Production always generates a fresh ephemeral. */\n  ephemeral?: KeyPair,\n): { message2: Buffer; keys: HandshakeKeys } {\n  const e = ephemeral ?? generateKeyPair();\n  state.symmetric.mixHash(e.publicKeyRaw);\n  state.symmetric.mixKey(e.publicKeyRaw);\n  state.symmetric.mixKey(dh(e.privateKey, state.initiatorEphemeral)); // ee\n  state.symmetric.mixKey(dh(e.privateKey, state.initiatorStatic)); // se\n\n  const encryptedPayload = state.symmetric.encryptAndHash(responsePayload);\n  return {\n    message2: Buffer.concat([e.publicKeyRaw, encryptedPayload]),\n    keys: finish(state.symmetric),\n  };\n}\n\n/**\n * Both halves in one call.\n *\n * Kept because most callers — every test, and any future responder with nothing\n * to do in the middle — want the whole handshake, and because the two halves\n * being separable should not force every one of them to sequence it by hand.\n */\nexport function respond(args: {\n  staticKeyPair: KeyPair;\n  /** Required by `IKpsk1`, forbidden by `IK`. */\n  psk?: Buffer;\n  /** Defaults to the stronger `IKpsk1`. `/api/e2ee/open` passes `\"IK\"` (§11). */\n  pattern?: NoisePattern;\n  message1: Buffer;\n  /** Built from the authenticated message-1 payload; sealed into message 2. */\n  buildPayload: (initiatorStaticPub: Buffer, payload: Buffer) => Buffer;\n  /** REQUIRED: `PAIR_PROLOGUE` or `OPEN_PROLOGUE`. Never defaulted (§11). */\n  prologue: Buffer;\n  /** Test seam only. */\n  ephemeral?: KeyPair;\n}): ResponderResult {\n  const state = readMessage1(args);\n  const { initiatorStaticPub, payload } = state;\n  const { message2, keys } = writeMessage2(\n    state,\n    args.buildPayload(initiatorStaticPub, payload),\n    // `own`, like the initiator half at `writeMessage1`. Reading `args.ephemeral`\n    // bare is a prototype-chain read, and a polluted `Object.prototype.ephemeral`\n    // pins the RESPONDER's `e` across every `respond()` — two calls producing\n    // byte-identical messages, which by §8's own rule is definitionally a replay.\n    own(args, \"ephemeral\"),\n  );\n\n  return { initiatorStaticPub, payload, message2, keys };\n}\n\nfunction finish(state: SymmetricState): HandshakeKeys {\n  const { k1, k2 } = state.split();\n  // Spec §5.2: the first key is always initiator→responder, whichever side\n  // called Split(). Naming them by direction here means no call site has to\n  // remember which end it is.\n  return new HandshakeKeys(state.handshakeHash(), k1, k2);\n}\n\n/**\n * Size check before anything is parsed or allocated.\n *\n * D-9's rule reaching forward from the unseal middleware to here: this runs on\n * a public, pre-authentication endpoint, so a message is bounded before it is\n * read and nothing is ever allocated in proportion to an attacker-supplied\n * length.\n */\nfunction assertMessageSize(message: Buffer, minimum: number): void {\n  if (message.length < minimum) {\n    throw new NoiseError(`Handshake message too short: ${message.length} < ${minimum}`);\n  }\n  if (message.length > NOISE_MAX_MESSAGE_BYTES) {\n    throw new NoiseError(`Handshake message too large: ${message.length}`);\n  }\n}\n","// Parsing the additive `e2ee` field of a pair-exchange request.\n//\n// `POST /api/pair/exchange` is public and unauthenticated (`PUBLIC_POST_PATHS`\n// in api/middleware/auth.middleware.ts), so everything here runs on bytes an\n// attacker chose, before anything has authenticated them. D-9 states the rule\n// for the Phase 4 unseal middleware and it reaches back to here: reject before\n// allocating, and never size a buffer from an attacker-supplied length.\n//\n// The field is OPTIONAL and its absence is not an error. A released tb-mobile\n// build sends `{ token, clientPublicKey }` and nothing else, cannot be\n// force-updated, and must keep pairing exactly as it does today\n// (docs/compatibility/tb-mobile.md). \"No `e2ee` key\" therefore means \"an older\n// client\", never \"a malformed request\".\n\nimport { NOISE_MAX_MESSAGE_BYTES } from \"./noise\";\nimport { E2EE_PROTOCOL_VERSION } from \"./protocol\";\n\n/**\n * @deprecated Use `E2EE_PROTOCOL_VERSION` from `src/e2ee/protocol.ts`, which is\n * the canonical home. This name survives as a re-export of that one value\n * because `src/server.ts` and `pair-payload.ts` import it and another track is\n * editing `server.ts` — deleting the symbol would widen W1a's diff into files\n * it does not own. `__tests__/e2ee-protocol-version.test.ts` asserts the two\n * names are one value, which is what makes the alias safe rather than a second\n * hand-synced copy (NONCE-DESIGN §4).\n */\nexport const E2EE_EXCHANGE_VERSION = E2EE_PROTOCOL_VERSION;\n\n/**\n * Cap on the base64 text before it is decoded.\n *\n * Checked against the *encoded* length rather than the decoded one, because\n * `Buffer.from(s, \"base64\")` allocates in proportion to `s` — testing the\n * result would mean doing the allocation this bound exists to prevent. base64\n * is 4 characters per 3 bytes; the padding allowance is slack, not precision.\n */\nconst MAX_BASE64_CHARS = Math.ceil((NOISE_MAX_MESSAGE_BYTES * 4) / 3) + 4;\n\nexport class E2eeRequestError extends Error {\n  /** Stable code for the client, so a version mismatch is not a parse failure. */\n  readonly code: \"E2EE_MALFORMED\" | \"E2EE_VERSION_UNSUPPORTED\";\n\n  constructor(code: E2eeRequestError[\"code\"], message: string) {\n    super(message);\n    this.name = \"E2eeRequestError\";\n    this.code = code;\n  }\n}\n\nexport interface E2eeExchangeRequest {\n  version: number;\n  /** Noise `IK` message 1, decoded. */\n  message1: Buffer;\n}\n\n/**\n * `null` for a client that did not ask for encryption; a parsed request for one\n * that did; a throw for one that asked incoherently.\n *\n * The three are deliberately distinct. Collapsing \"absent\" into \"malformed\"\n * breaks every released client, and collapsing \"malformed\" into \"absent\" would\n * silently pair a client that believed it was encrypting — which is the\n * downgrade the whole negotiation exists to prevent, arrived at through a\n * parser rather than an attacker.\n */\nexport function parseE2eeRequest(raw: unknown): E2eeExchangeRequest | null {\n  if (raw === undefined || raw === null) return null;\n  if (typeof raw !== \"object\" || Array.isArray(raw)) {\n    throw new E2eeRequestError(\"E2EE_MALFORMED\", \"e2ee must be an object\");\n  }\n\n  const { v, noise } = raw as { v?: unknown; noise?: unknown };\n\n  // Version first: a client speaking v2 is not a malformed v1 client, and\n  // telling it so is what lets it fall back deliberately rather than retrying.\n  if (typeof v !== \"number\" || !Number.isInteger(v)) {\n    throw new E2eeRequestError(\"E2EE_MALFORMED\", \"e2ee.v must be an integer\");\n  }\n  if (v !== E2EE_PROTOCOL_VERSION) {\n    throw new E2eeRequestError(\n      \"E2EE_VERSION_UNSUPPORTED\",\n      `e2ee.v ${v} is not supported; this server speaks ${E2EE_PROTOCOL_VERSION}`,\n    );\n  }\n\n  if (typeof noise !== \"string\") {\n    throw new E2eeRequestError(\"E2EE_MALFORMED\", \"e2ee.noise must be a base64 string\");\n  }\n  // Before the decode, not after.\n  if (noise.length > MAX_BASE64_CHARS) {\n    throw new E2eeRequestError(\"E2EE_MALFORMED\", \"e2ee.noise is too large\");\n  }\n\n  const message1 = Buffer.from(noise, \"base64\");\n  // `Buffer.from` never throws on base64 — it discards anything it cannot\n  // decode, so garbage silently becomes a short buffer rather than an error.\n  // An empty result from a non-empty string is that failure, and it has to be\n  // caught here or it surfaces later as a confusing handshake error.\n  if (message1.length === 0) {\n    throw new E2eeRequestError(\"E2EE_MALFORMED\", \"e2ee.noise is not valid base64\");\n  }\n\n  return { version: v, message1 };\n}\n","// The `/api/e2ee/open` msg1 replay cache.\n//\n// A Noise `IK` message 1 carries NO freshness: no PSK, no responder challenge,\n// no timestamp. Anyone who captures one valid msg1 can send it again for as\n// long as the server's identity key lives, and every replay is a complete,\n// authentic handshake — so it is neither a handshake failure nor a missing\n// device row, and neither budget on the route charged it. An adversary replayed\n// one captured message a thousand times and ran two thousand X25519 operations.\n//\n// Worse than the cost: because a replay AUTHENTICATES AS THE VICTIM, the\n// per-device limiter §8 asks for is what makes the attack *targeted*. Five\n// replays spent the victim's whole minute and pushed it past its four-socket\n// cap, retiring a live context — from one captured message, against one chosen\n// device, while every other device carried on.\n//\n// **What makes this detectable is the pattern itself.** In `IK`, `e` — the\n// initiator's ephemeral public key — is transmitted in the clear at a fixed\n// offset, and a legitimate client generates a fresh one for every handshake.\n// So a repeated `e` is not a heuristic for a replay; it is definitionally one.\n//\n// The residual, stated rather than left to be discovered: a capture replayed\n// after its entry is evicted, or past the TTL, still costs one DH pair and one\n// per-device slot per attempt. That is bounded by the source and per-device\n// budgets, and a one-minute per-device lockout per five such replays is the\n// accepted floor. The real cure is client-side freshness — a nonce in the msg1\n// payload, bound into the transcript — which is a protocol version away and\n// deliberately not now.\n\nimport { own } from \"./protocol\";\n\n/** ~2 MiB at 32-byte keys. The bound is on entries, not on bytes. */\nexport const MSG1_REPLAY_CACHE_ENTRIES = 65_536;\n\n/**\n * How many expired entries one `record` call may collect.\n *\n * ponytail: amortised prune with a per-call ceiling, not a sweeper. Entries\n * share one TTL and are stored in first-seen order, so the expired ones are\n * always at the front and this is O(1) amortised; the ceiling is there so a\n * burst of simultaneous expiries cannot stall a request. Memory is bounded by\n * the capacity check regardless of how little pruning happens.\n */\nconst MAX_PRUNE_PER_CALL = 128;\n\n/**\n * Remembers the ephemerals of message 1s that reached the handshake.\n *\n * Insertion-ordered and never refreshed on a hit — a hit is a replay, and\n * refreshing it would let an attacker pin a slot indefinitely with traffic that\n * is already being rejected. That is the one place this deliberately differs\n * from a textbook LRU: eviction is by first-seen age, which for entries that\n * are only ever written once is the same thing.\n */\nexport class Msg1ReplayCache {\n  /** ephemeral (base64) → the moment the entry stops counting. */\n  readonly #seen = new Map<string, number>();\n  readonly #capacity: number;\n  readonly #ttlMs: number;\n\n  constructor(options: { capacity?: number; ttlMs: number }) {\n    // Off the ARGUMENT: a polluted `Object.prototype.capacity` of 1 evicts a\n    // captured msg1 back into replayability on the next open.\n    this.#capacity = own(options, \"capacity\") ?? MSG1_REPLAY_CACHE_ENTRIES;\n    this.#ttlMs = options.ttlMs;\n  }\n\n  /** Live entries. For tests and for a diagnostics line — never a key. */\n  get size(): number {\n    return this.#seen.size;\n  }\n\n  /** Whether this ephemeral has already been through the handshake. */\n  has(ephemeral: Buffer, now: number = Date.now()): boolean {\n    const key = ephemeral.toString(\"base64\");\n    const expiresAt = this.#seen.get(key);\n    if (expiresAt === undefined) return false;\n    if (now >= expiresAt) {\n      this.#seen.delete(key);\n      return false;\n    }\n    return true;\n  }\n\n  /**\n   * Record an ephemeral that actually reached the handshake.\n   *\n   * Called only after `readMessage1` has succeeded, so garbage that never\n   * parsed cannot fill the cache — the bound protects the thing the bound is\n   * for.\n   */\n  record(ephemeral: Buffer, now: number = Date.now()): void {\n    let pruned = 0;\n    for (const [key, expiresAt] of this.#seen) {\n      if (now < expiresAt || pruned >= MAX_PRUNE_PER_CALL) break;\n      this.#seen.delete(key);\n      pruned++;\n    }\n    while (this.#seen.size >= this.#capacity) {\n      const oldest = this.#seen.keys().next();\n      if (oldest.done) break;\n      this.#seen.delete(oldest.value);\n    }\n    this.#seen.set(ephemeral.toString(\"base64\"), now + this.#ttlMs);\n  }\n\n  /** A streamer restart does this by existing; tests need a call. */\n  clear(): void {\n    this.#seen.clear();\n  }\n}\n","// The streamer's long-term X25519 identity key.\n//\n// It exists so a client can tell WHICH server answered its pair exchange. Today\n// the QR carries no server key material, so the phone opens the sealed box with\n// whatever `ephemeralPublicKey` came back (src/seal.ts) — a man in the middle\n// answers just as convincingly as the real server. Putting the public half in\n// the QR makes the printed code an out-of-band channel for the server's\n// identity, which is what the Noise `IK` handshake later builds on\n// (specs/end-to-end-encryption/design.md §2.2, §2.3).\n//\n// This module is only the identity half. Nothing here encrypts anything and no\n// handshake uses the key yet. That scoping is deliberate: publishing a public\n// key is additive and cannot break a released client.\n\nimport {\n  createHash,\n  createPrivateKey,\n  createPublicKey,\n  generateKeyPairSync,\n  type KeyObject,\n} from \"crypto\";\nimport { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { dirname, join } from \"path\";\nimport { getLogger } from \"./logger\";\n\n/**\n * On-disk format version.\n *\n * Rotation is out of scope, but the version is the difference between rotating\n * later and re-pairing every device: a future file holding a key *set* can be\n * told apart from this one holding a single key, without guessing from shape.\n */\nconst IDENTITY_FILE_VERSION = 1;\n\ninterface IdentityFile {\n  v: number;\n  createdAt: string;\n  key: JsonWebKey;\n}\n\nexport interface ServerIdentity {\n  /** Raw X25519 public key, base64url, 43 chars. Public: the QR, /api/info, logs. */\n  publicKey: string;\n  /** Never leaves the process — not a log line, not a response, not an error message. */\n  privateKey: KeyObject;\n}\n\n/**\n * `~/.threadbase/keys/server-identity.key`.\n *\n * Its own file, not a `server.yaml` key: that file is parsed by single-line\n * regex and users are invited to hand-edit it, which is no place for a private\n * key. The config dir is resolved per call, and inlined rather than imported\n * from auth.ts, matching runtime-store.ts / pty-host/socket.ts /\n * codexGateAnswers.ts — tests redirect it with THREADBASE_CONFIG_DIR.\n */\nexport function serverIdentityKeyPath(): string {\n  const dir = process.env.THREADBASE_CONFIG_DIR ?? join(homedir(), \".threadbase\");\n  return join(dir, \"keys\", \"server-identity.key\");\n}\n\n/**\n * The server identity, generated on first call and stable forever after —\n * across restarts and across API-key rotation.\n *\n * Stability is the whole point. The public half goes into a pair QR, so minting\n * a second key would silently invalidate every device that scanned the first.\n * A file that exists but cannot be read therefore **throws** rather than\n * regenerating: losing this key must be a deliberate act (delete the file), not\n * the recovery path for a bad read.\n */\nexport function loadOrCreateServerIdentity(): ServerIdentity {\n  const path = serverIdentityKeyPath();\n  let raw: string;\n  try {\n    raw = readFileSync(path, \"utf-8\");\n  } catch (err) {\n    if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") throw err;\n    return generateIdentity(path);\n  }\n\n  let privateKey: KeyObject;\n  try {\n    privateKey = createPrivateKey({ key: (JSON.parse(raw) as IdentityFile).key, format: \"jwk\" });\n  } catch {\n    // The caught error is deliberately dropped rather than interpolated: a\n    // JSON.parse failure quotes the offending input, and this file's input is a\n    // private key. The path is enough to act on.\n    throw new Error(\n      `Server identity key at ${path} could not be read. Refusing to generate a new one — ` +\n        \"that would invalidate every paired device. Repair or delete the file deliberately.\",\n    );\n  }\n  return { publicKey: publicKeyOf(privateKey), privateKey };\n}\n\n/** The public half alone, for callers that must never hold the private key. */\nexport function serverIdentityPublicKey(): string {\n  return loadOrCreateServerIdentity().publicKey;\n}\n\n/**\n * The fingerprint a user compares out of band (design.md §2.2): SHA-256 over\n * the raw 32 public-key bytes, truncated to the first 16, printed as 8\n * space-separated groups of 4 lowercase hex characters.\n *\n * Pure function of the base64url string `serverIdentityPublicKey()` returns —\n * decode that, not the JWK or any other encoding, or this hashes different\n * bytes than the phone does and the two fingerprints never match.\n */\nexport function serverIdentityFingerprint(publicKeyBase64url: string): string {\n  const raw = Buffer.from(publicKeyBase64url, \"base64url\");\n  const hex = createHash(\"sha256\").update(raw).digest().subarray(0, 16).toString(\"hex\");\n  return hex.match(/.{4}/g)?.join(\" \") ?? hex;\n}\n\n/** Convenience over the real, on-disk identity key. */\nexport function currentServerIdentityFingerprint(): string {\n  return serverIdentityFingerprint(serverIdentityPublicKey());\n}\n\nfunction generateIdentity(path: string): ServerIdentity {\n  const { privateKey } = generateKeyPairSync(\"x25519\");\n  const file: IdentityFile = {\n    v: IDENTITY_FILE_VERSION,\n    createdAt: new Date().toISOString(),\n    key: privateKey.export({ format: \"jwk\" }),\n  };\n\n  // tmp-then-rename at 0600 — the discipline setConfigValue already uses for\n  // server.yaml (src/auth.ts). writeFileSync's mode only applies when it\n  // creates the file and is masked by the umask, so the chmod is what makes the\n  // permission unconditional.\n  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });\n  const tmp = `${path}.tmp`;\n  writeFileSync(tmp, `${JSON.stringify(file)}\\n`, { encoding: \"utf-8\", mode: 0o600 });\n  chmodSync(tmp, 0o600);\n  renameSync(tmp, path);\n\n  const publicKey = publicKeyOf(privateKey);\n  // The public key is logged on purpose: it is printable by design, and a boot\n  // line is the out-of-band record a user can compare a QR against.\n  getLogger(\"identity\").info(`Generated server identity key ${publicKey}`, {\n    event: \"identity.key_generated\",\n    path,\n  });\n  return { publicKey, privateKey };\n}\n\n/**\n * Derived from the private key rather than read from the file's own `x`, so a\n * file whose public half was edited can never make the server advertise a key\n * it cannot prove possession of.\n */\nfunction publicKeyOf(privateKey: KeyObject): string {\n  const jwk = createPublicKey(privateKey).export({ format: \"jwk\" });\n  // Guards a file holding some other OKP curve (Ed25519 imports fine and its\n  // `x` is the same 43 characters), which would be published as an X25519 key\n  // and fail only later, inside a handshake.\n  if (jwk.crv !== \"X25519\" || typeof jwk.x !== \"string\") {\n    throw new Error(`Server identity key at ${serverIdentityKeyPath()} is not an X25519 key`);\n  }\n  return jwk.x; // JWK OKP `x` is already base64url of the raw 32 bytes\n}\n","/**\n * A sliding-window rate limiter for a public endpoint.\n *\n * **The POLICY is shared through this module; the arithmetic deliberately is\n * not** (NONCE-DESIGN §8). `/open` enforces the same 5 attempts per minute that\n * `/api/pair/exchange` enforces, and takes those numbers from the constants\n * below so the two cannot drift.\n *\n * The bucket arithmetic is a second implementation on purpose:\n * `StreamerServer.checkExchangeRateLimit` is a private method over private\n * per-map state on a class in `src/server.ts`, so sharing the code — rather\n * than the policy — would mean deleting that method and rewiring its three call\n * sites in a file another track owns. Consolidating the two is a named\n * follow-up, and it belongs to whoever next touches `server.ts`. Reaching into\n * that file to \"fix\" this module is the wrong direction.\n *\n * The KEY is the caller's choice and `/open` does not use the IP: NONCE-DESIGN\n * §8 records that behind a Cloudflare tunnel every request arrives from\n * 127.0.0.1, so an IP-keyed bucket is one bucket for the whole fleet. `/open`\n * keys on the authenticated static key instead, and charges the IP only for a\n * handshake that failed and therefore named nobody.\n */\n/** True when the caller may proceed; false when it has spent its budget. */\nexport type RateLimiter = (key: string) => boolean;\n\n/**\n * Same numbers `/api/pair/exchange` enforces today.\n *\n * **A tunable, and named as one.** Five `/open` per minute per device is tight\n * on a flaky network: a foreground, a silence-timer reconnect and a lazy REST\n * open can legitimately arrive as three in one burst, and the client's\n * single-flight (§8) is what keeps that from becoming six. If field data shows\n * legitimate devices hitting the ceiling, this is the number to raise — the\n * per-device context cap, not this, is the bound that actually stops a\n * replayer.\n */\nexport const PAIR_EXCHANGE_LIMIT = 5;\nexport const PAIR_EXCHANGE_WINDOW_MS = 60_000;\n\n/**\n * Failed `/api/e2ee/open` handshakes tolerated per source address per minute.\n *\n * **Deliberately far above the per-device limit, because it bounds a different\n * thing.** The per-device number bounds ALLOCATION and can be tight; this one\n * bounds CPU on a key nobody has authenticated, and behind a Cloudflare tunnel\n * every request arrives from `127.0.0.1` — so a tight number here is a denial\n * of service against the whole fleet rather than against an attacker. At five,\n * an adversary showed that five malformed messages locked out every paired\n * device on that address, including one mid-recovery.\n *\n * Thirty is above anything a real device does — a device's own recovery is two\n * or three handshakes, and they succeed, which costs this budget nothing — and\n * far below what a flood needs to be interesting.\n *\n * A tunable. If field evidence shows legitimate traffic reaching it, raise it;\n * the per-device cap on contexts, not this, is the bound that stops a replayer.\n */\nexport const OPEN_SOURCE_FAILURE_LIMIT = 30;\n\n/** Keys tracked before a full sweep runs. Bounds the map on a spray of IPs. */\nconst SWEEP_AFTER_KEYS = 1024;\n\n/**\n * A budget whose check and charge are SEPARATE.\n *\n * `createRateLimiter` charges every call, which is right when every call is\n * the thing being limited. It is wrong for a failure budget: there, a caller\n * must be able to ask \"does this source still have room?\" before doing the\n * expensive work, and spend from the budget only when that work turned out to\n * be wasted. Fusing the two would charge a legitimate caller for succeeding.\n */\nexport interface RateBudget {\n  /** True when `key` still has room. Charges nothing. */\n  check(key: string): boolean;\n  /** Spend one unit against `key`. */\n  charge(key: string): void;\n}\n\nexport function createRateBudget(options: { limit: number; windowMs: number }): RateBudget {\n  const hits = new Map<string, number[]>();\n\n  const recentFor = (key: string, now: number): number[] => {\n    if (hits.size > SWEEP_AFTER_KEYS) {\n      for (const [k, times] of hits) {\n        if (times.every((t) => now - t >= options.windowMs)) hits.delete(k);\n      }\n    }\n    const recent = (hits.get(key) ?? []).filter((t) => now - t < options.windowMs);\n    hits.set(key, recent);\n    return recent;\n  };\n\n  return {\n    check(key: string): boolean {\n      return recentFor(key, Date.now()).length < options.limit;\n    },\n    charge(key: string): void {\n      const now = Date.now();\n      recentFor(key, now).push(now);\n    },\n  };\n}\n\nexport function createRateLimiter(options: { limit: number; windowMs: number }): RateLimiter {\n  const budget = createRateBudget(options);\n  return (key: string): boolean => {\n    if (!budget.check(key)) return false;\n    budget.charge(key);\n    return true;\n  };\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 {\n  DEFAULT_PUSH_TOKEN_KIND,\n  isPushTokenKind,\n  PUSH_TOKEN_KINDS,\n  tokenState,\n} from \"../../db/repositories/push.repository\";\nimport { E2EE_PROTOCOL_VERSION } from \"../../e2ee/protocol\";\nimport type { FeatureFlagSource } from \"../../feature-flags\";\nimport { getLogger } from \"../../logger\";\nimport { NotificationPrefsSchema } from \"../../schemas/notification-prefs.schema\";\nimport { serverIdentityPublicKey } from \"../../server-identity\";\nimport { describeMissingApnsCredentials } from \"../../services/push/apnsClient\";\nimport { testNotificationBody } from \"../../services/push/notificationCopy\";\nimport { getVersion } from \"../../version\";\nimport type { AppEnv } from \"../app\";\nimport { createRateLimiter } from \"../rate-limit\";\nimport type { ApiDeps } from \"../types/api-deps\";\n\n/**\n * Accept a finite timestamp, reject anything else.\n *\n * These arrive as JSON from a client, so a string, a NaN, or an Infinity is\n * reachable. Storing one would make a renewal deadline that never fires (or\n * fires immediately), so a bad value becomes \"absent\" rather than a poisoned\n * schedule.\n */\nfunction numberOrNull(value: unknown): number | null {\n  return typeof value === \"number\" && Number.isFinite(value) ? value : null;\n}\n\n/**\n * Ceiling on any JSON body `readJsonBody` accepts, enforced as bytes arrive\n * rather than after the whole request is buffered.\n *\n * Shared by all three callers (push register, push unregister, client-log)\n * rather than a per-route bound: none of them legitimately posts more than a\n * few KB, so one ceiling well above any real payload — and far below what\n * would pressure memory — covers all three. A release-build client shipping\n * this is the reason it's needed at all: a dev build behind Metro never sent\n * enough to matter.\n */\nexport const MAX_JSON_BODY_BYTES = 1024 * 1024; // 1 MiB\n\nclass BodyTooLargeError extends Error {}\n\nfunction readJsonBody(\n  req: IncomingMessage,\n  maxBytes: number = MAX_JSON_BODY_BYTES,\n): Promise<unknown> {\n  return new Promise((resolve, reject) => {\n    let chunks: Buffer[] = [];\n    let size = 0;\n    let refused = false;\n    req.on(\"data\", (chunk: Buffer) => {\n      if (refused) return;\n      size += chunk.length;\n      if (size > maxBytes) {\n        // Stop buffering but keep draining, so a slow/chunked oversized body\n        // doesn't grow memory further while the socket stays healthy enough\n        // to carry the 413 back.\n        refused = true;\n        chunks = [];\n        reject(new BodyTooLargeError(\"request body is too large\"));\n        return;\n      }\n      chunks.push(chunk);\n    });\n    req.on(\"end\", () => {\n      if (refused) return;\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\n/**\n * What push this server can actually deliver.\n *\n * Reported on `GET /api/info` and `GET /api/push/health` so a client can hide an\n * affordance the server can never honour instead of registering tokens nothing\n * will ever send to. It cannot be inferred from `/api/push/health`'s `available`,\n * which reports whether the SQLite token store opened — that is `true` on a\n * server holding no APNs credentials at all.\n */\nexport interface PushCapability {\n  /**\n   * Live Activity (ActivityKit) push. True only when APNs credentials resolved\n   * *and* the sender was wired, so it is the same fact the boot log reports as\n   * `live_activity.enabled` / `live_activity.disabled`.\n   */\n  liveActivity: boolean;\n  /**\n   * Ordinary \"your turn\" notifications over Expo's relay (`WaitingInputNotifier`).\n   * True when the notifier was wired, which needs only the push token store:\n   * Expo holds the app's credentials, so no APNs key or env var is involved.\n   * Independent of `liveActivity` — neither implies the other.\n   */\n  notifications: boolean;\n  /**\n   * The server stores and enforces per-device notification preferences\n   * (`PATCH /api/push/preferences`, `POST /api/push/test`). Absent on an older\n   * server, which is how a client tells \"not supported\" from \"turned off\".\n   */\n  preferences: boolean;\n  /** Why `liveActivity` is false; absent when it is true. Names env vars, never values. */\n  liveActivityReason?: string;\n  /** Why `notifications` is false; absent when it is true. */\n  notificationsReason?: string;\n}\n\n/**\n * Describe push capability for a client.\n *\n * `wired` is the server's own wiring state rather than a re-read of the\n * environment: credentials alone are not enough, since each notifier is only\n * built when the push token store opened too.\n */\nexport function describePushCapability(\n  wired: { liveActivity: boolean; notifications: boolean },\n  env: NodeJS.ProcessEnv = process.env,\n): PushCapability {\n  const capability: PushCapability = {\n    liveActivity: wired.liveActivity,\n    notifications: wired.notifications,\n    preferences: wired.notifications,\n  };\n  if (!wired.liveActivity) {\n    // describeMissingApnsCredentials only explains a *credential* gap and\n    // returns null once the credentials are complete — reachable here, because\n    // an unavailable token store disables the feature with the key still set.\n    capability.liveActivityReason =\n      describeMissingApnsCredentials(env) ??\n      \"APNs credentials are set but the push token store is unavailable, so Live Activity \" +\n        \"push is disabled.\";\n  }\n  if (!wired.notifications) {\n    capability.notificationsReason =\n      \"The push token store is unavailable (the SQLite cache failed to open), so \" +\n      \"notifications cannot be sent.\";\n  }\n  return capability;\n}\n\n/**\n * Envelope version this build speaks. The same number the pair QR carries as `v`.\n *\n * @deprecated Re-exported from `src/e2ee/protocol.ts`, which is the canonical\n * home: the record layer needs this constant and a crypto module importing a\n * Hono route module would invert the dependency direction (NONCE-DESIGN §4).\n * The name stays here because released call sites and tests import it from this\n * module.\n */\nexport { E2EE_PROTOCOL_VERSION };\n\n/**\n * Whether this build has the E2EE code path at all.\n *\n * Deliberately a constant rather than the `e2ee` feature flag. `supported` means\n * \"this build speaks the envelope\" (specs/end-to-end-encryption/design.md §6.2),\n * which is a property of the build; whether a given deployment offers it is the\n * flag's job, and `describeE2eeCapability` below requires both. Reporting the\n * flag here would let an operator who switches it on advertise a handshake the\n * build cannot perform — a client would offer to re-pair for encryption and then\n * fail, which is exactly the half-landed break this negotiation exists to remove.\n *\n * True since Phase 2 landed the handshake. Enabling it for a deployment is still\n * a separate, opt-in act: set `THREADBASE_FEATURE_E2EE=1`, `--feature e2ee=true`,\n * or `feature_flags:` in server.yaml.\n *\n * Typed `boolean` rather than inferred so the `&&` below stays a real branch\n * instead of narrowing to a constant.\n */\nconst E2EE_SUPPORTED: boolean = true;\n\n/**\n * Whether a client should encrypt to this server, and why not when it should not.\n *\n * The contract is `push`'s: additive, and **absent means \"older server,\n * unknown\"** rather than \"unsupported\". A client reads `enabled` to decide\n * whether to attempt a handshake — never `supported` alone, which only says the\n * code path exists.\n *\n * `required` is the stage-3 bit (refuse plaintext from *any* client) and is\n * false until that is an explicit product decision. It is reported rather than\n * omitted because an absent field means \"unknown\", and \"unknown\" is the wrong\n * answer to a question this server can answer.\n */\nexport interface E2eeCapability {\n  supported: boolean;\n  enabled: boolean;\n  version: number;\n  required: boolean;\n  /**\n   * This build accepts the WebSocket ticket as a `tb-ticket.<ticket>`\n   * subprotocol offer and selects `threadbase-e2ee-v1`, which is the only way a\n   * browser can present one. A property of the build, like `supported`. Absent\n   * means an older server that only reads `X-TB-Ticket`: a browser client must\n   * refuse encrypted pairing there rather than fall back to plaintext.\n   */\n  wsTicketSubprotocol: boolean;\n  /** Why `enabled` is false while `supported` is true; absent otherwise. */\n  reason?: string;\n}\n\nexport function describeE2eeCapability(\n  flagEnabled: boolean,\n  /**\n   * Which rung of the precedence chain decided the flag, when the caller knows\n   * it. An operator who typed `--no-e2ee` and reads \"set --feature e2ee=true\"\n   * has been answered with the wrong question; naming the rung that actually\n   * decided is the difference between a reason and a template.\n   */\n  flagSource?: FeatureFlagSource,\n): E2eeCapability {\n  const enabled = E2EE_SUPPORTED && flagEnabled;\n  const base = {\n    supported: E2EE_SUPPORTED,\n    enabled,\n    version: E2EE_PROTOCOL_VERSION,\n    required: false,\n    wsTicketSubprotocol: true,\n  };\n  if (enabled) return base;\n  return {\n    ...base,\n    // Always says why, including when `supported` is false. An operator who set\n    // the flag and saw nothing happen has exactly one question, and a field that\n    // goes absent in the case they hit is the field not answering it.\n    reason: E2EE_SUPPORTED ? disabledReason(flagSource) : NO_HANDSHAKE_REASON,\n  };\n}\n\nconst NO_HANDSHAKE_REASON =\n  \"this build carries the capability negotiation but not yet the handshake it gates\";\n\n/**\n * The switch that turned encryption off, in the operator's own terms — the\n * thing they typed or set, never the resolver's rung name. Shared by the boot\n * warning and `/api/info` so the two never describe one switch two ways.\n *\n * `cli` covers both spellings of one switch — `--no-e2ee` and\n * `--feature e2ee=false` land on the same rung. `override` is unreachable for\n * `e2ee` today (the only override rung is `codexSystemPromptEnabled`) but the\n * type demands an answer, and this one is at least true.\n */\nexport const E2EE_OFF_SWITCH: Record<Exclude<FeatureFlagSource, \"default\">, string> = {\n  cli: \"--no-e2ee (or --feature e2ee=false)\",\n  env: \"the THREADBASE_FEATURE_E2EE environment variable\",\n  yaml: \"feature_flags: in server.yaml\",\n  override: \"an explicit server configuration override\",\n};\n\n/**\n * Why encryption is off, in the operator's own terms — names the actual rung\n * that decided it rather than collapsing every non-`cli` source into one\n * generic line (D-8's resolution: the env var can't be hidden from the\n * resolver, so `/api/info` names it plainly instead). `cli` says \"for this\n * run\", because a CLI option cannot outlive the command that typed it; the\n * `default` rung keeps the original text, which names the ways to turn it on.\n */\nfunction disabledReason(source?: FeatureFlagSource): string {\n  if (source === \"cli\") return `disabled by ${E2EE_OFF_SWITCH.cli} for this run`;\n  if (source !== undefined && source !== \"default\") return `disabled by ${E2EE_OFF_SWITCH[source]}`;\n  return (\n    \"disabled by the e2ee feature flag — set THREADBASE_FEATURE_E2EE=1, --feature e2ee=true, \" +\n    \"or feature_flags: in server.yaml\"\n  );\n}\n\n// One line per process, not one per request: `/api/info` is polled, and this\n// module has a 261 MB unrotated-log precedent to respect (CLAUDE.md).\nlet identityKeyFailureLogged = false;\n\n/**\n * This server's identity public key, or `undefined` when the key file cannot be\n * read — which `JSON.stringify` renders as an absent field, exactly what the\n * `/api/info` contract says absent means.\n *\n * Deliberately different from the CLI's answer to the same failure. `/api/info`\n * is how a client discovers capabilities and renders its server list, so a\n * corrupt key file must cost verification and nothing else; turning it into a\n * 500 would take down the whole endpoint over a file unrelated to the rest of\n * the response. The pair banner throws instead, because a QR that cannot carry\n * `spk` is a QR worth refusing to print — and `serve` already degrades that\n * into a warn plus a QR-less banner (`cli/index.ts`).\n */\nexport function describeServerIdentityKey(): string | undefined {\n  try {\n    return serverIdentityPublicKey();\n  } catch (err) {\n    if (!identityKeyFailureLogged) {\n      identityKeyFailureLogged = true;\n      // Safe to interpolate: every error this can throw carries the file path\n      // and fixed text, never the file's contents.\n      getLogger(\"identity\").error(\n        `Server identity key unavailable, so /api/info will omit it: ${err instanceof Error ? err.message : String(err)}`,\n        { event: \"identity.unavailable\" },\n      );\n    }\n    return undefined;\n  }\n}\n\nconst clientLog = getLogger(\"client\");\n\n// A BCP 47 language tag (\"he\", \"en-US\", \"zh-Hant-TW\"), bounded.\nconst LOCALE_TAG_RE = /^[A-Za-z]{2,3}(?:[-_][A-Za-z0-9]{1,8}){0,4}$/;\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\n/**\n * Requests per window this endpoint accepts per authenticated caller. A log\n * sink, not a handshake, so it's generous: legitimate batching (mobile\n * shipping slow-request timings) lands well under one request per second.\n * 120/min gives 2x headroom over \"a batch every second\" before refusing.\n */\nexport const CLIENT_LOG_RATE_LIMIT = 120;\nexport const CLIENT_LOG_RATE_WINDOW_MS = 60_000;\n\n/** Entries beyond this in one batch are dropped, not logged. */\nexport const MAX_CLIENT_LOG_ENTRIES = 200;\n/** Characters kept from an entry's `msg` before the rest is dropped. */\nconst MAX_CLIENT_LOG_MSG_LEN = 2000;\n/** Characters kept from an entry's serialized `fields` before the rest is dropped. */\nconst MAX_CLIENT_LOG_FIELDS_LEN = 4000;\n\nfunction truncateString(value: string, maxLen: number): string {\n  return value.length > maxLen ? `${value.slice(0, maxLen)}…(truncated)` : value;\n}\n\n/**\n * `fields` is an arbitrary client-supplied object, unbounded in shape and\n * depth — stringifying it is what actually bounds the log line it ends up\n * inside, since a nested structure can be small in entry count but huge once\n * rendered.\n */\nfunction truncateFields(\n  fields: Record<string, unknown> | undefined,\n  maxLen: number,\n): Record<string, unknown> {\n  if (!fields) return {};\n  const serialized = JSON.stringify(fields);\n  if (serialized.length <= maxLen) return fields;\n  return { fieldsTruncated: truncateString(serialized, maxLen) };\n}\n\nexport const createMiscRoutes = (\n  deps: Pick<\n    ApiDeps,\n    | \"publicUrl\"\n    | \"sessionStore\"\n    | \"ptyAttachedIds\"\n    | \"rotateApiKey\"\n    | \"localNoAuth\"\n    | \"pushRepo\"\n    | \"liveActivityPushEnabled\"\n    | \"expoPushEnabled\"\n    | \"expoPushSender\"\n    | \"featureFlagsConfig\"\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      // Capability flag: this server serves /api/config/claude-flags. Additive —\n      // older clients ignore it, and clients talking to an older server see it\n      // absent and hide the UI rather than 404ing.\n      claudeFlags: true,\n      // Same contract: this server serves GET /api/config/feature-flags. Lives\n      // here rather than behind /api/config (admin-only) so a read-only client\n      // still learns the server supports flags even if it can't read values.\n      featureFlags: true,\n      // Same contract: this server serves GET /api/projects/summary, which the\n      // Hub's grouped views need before they can draw a tree.\n      projectSummary: true,\n      // The paired-device registry lives in runtime.db, so it survives\n      // `tb-streamer cache clear` and the integrity monitor's reset-and-rescan.\n      // A client may only prefer its scoped device token over the shared API\n      // key when this is true: on an older server the registry is inside\n      // cache.db, where a documented troubleshooting step deletes it and every\n      // device token with it. Absent means \"old server, assume not durable\".\n      devicesDurable: true,\n      // Delivery capability, not endpoint support: whether this server can\n      // actually send a push, so mobile can hide an affordance instead of\n      // registering tokens nothing will ever send to. Absent on older servers,\n      // which a client should read as \"unknown\", not \"unavailable\".\n      push: describePushCapability({\n        liveActivity: deps.liveActivityPushEnabled(),\n        notifications: deps.expoPushEnabled(),\n      }),\n      // This server's long-term X25519 public key, base64url. The same value the\n      // pair QR carries as `spk`, served here so an already-paired client can\n      // learn it without re-scanning. Additive: absent means a server with no\n      // readable identity key, which a client must read as \"cannot verify this\n      // server\" — never as a reason to fail the rest of this response.\n      serverIdentityKey: describeServerIdentityKey(),\n      // Whether to encrypt to this server. Additive, same contract as `push`:\n      // absent means an older server, which a client must read as \"unknown\" and\n      // resolve as today's plaintext path — never as a reason to fail.\n      e2ee: describeE2eeCapability(\n        deps.featureFlagsConfig().values.e2ee,\n        deps.featureFlagsConfig().sources.e2ee,\n      ),\n      // This build samples cheap host signals and pushes `host_pressure` when\n      // the box is starved. Additive capability flag only — live readings stay\n      // off this polled endpoint. Absent means an older server that never\n      // samples. Informational: pressure never holds, kills, or refuses sessions.\n      hostPressure: true,\n      // Fixed-action picker navigation is available at POST /api/sessions/:id/raw-key.\n      // Absent on older streamers, so mobile can explain the required update.\n      rawKeys: true,\n      // Provider-neutral prompt contract: normalized prompt events, opaque ids\n      // and the atomic /prompt/answer route. A prompt_snapshot on subscribe\n      // carries RETAINED prompts, terminal ones included — render on `state`,\n      // not on presence — and an answer retry after that retention window is\n      // answered 404 prompt_not_found rather than the recorded outcome.\n      promptContract: { schemaVersion: 1, atomicAnswer: true },\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  // Push registration (C7). This was a no-op returning { ok: true }: mobile\n  // registered, got success, and nothing was stored — so no notification could\n  // ever be delivered and no failure could be observed. The client had no way\n  // to discover that its \"successful\" registration meant nothing.\n  app.post(\"/api/push/register\", async (c) => {\n    // Read the raw Node request like the sibling routes do — Hono's c.req.json()\n    // does not see a body on this server's request plumbing.\n    const body = (await readJsonBody(c.env.incoming).catch(() => null)) as {\n      token?: unknown;\n      platform?: unknown;\n      deviceId?: unknown;\n      kind?: unknown;\n      activityId?: unknown;\n      sessionId?: unknown;\n      expiresAt?: unknown;\n      staleDate?: unknown;\n      startedAt?: unknown;\n      serverId?: unknown;\n      locale?: unknown;\n      notificationPrefs?: unknown;\n    } | null;\n    const token = body?.token;\n    const platform = body?.platform;\n    // The id the client files this server under, echoed back in the push so a\n    // tap opens the right server. Absent from older clients, which is fine.\n    // Bounded and charset-checked because it is stored and sent to a third\n    // party (Expo) verbatim.\n    const clientServerId = body?.serverId;\n    if (\n      clientServerId !== undefined &&\n      (typeof clientServerId !== \"string\" || !/^[A-Za-z0-9_.:-]{1,128}$/.test(clientServerId))\n    ) {\n      return c.json({ error: \"serverId must be 1-128 characters of [A-Za-z0-9_.:-]\" }, 400);\n    }\n    // The app's display language, so pushes arrive in it. Stored and sent on\n    // to Expo, hence the shape check. A build that predates the field still\n    // gets its device language from Accept-Language (iOS sends one).\n    const bodyLocale = body?.locale;\n    if (\n      bodyLocale !== undefined &&\n      (typeof bodyLocale !== \"string\" || !LOCALE_TAG_RE.test(bodyLocale))\n    ) {\n      return c.json({ error: \"locale must be a BCP 47 language tag\" }, 400);\n    }\n    // Optional: a build that predates preferences sends none and keeps getting\n    // everything. Validated here because it is stored and later drives sends.\n    const parsedPrefs =\n      body?.notificationPrefs === undefined\n        ? undefined\n        : NotificationPrefsSchema.safeParse(body.notificationPrefs);\n    if (parsedPrefs && !parsedPrefs.success) {\n      return c.json(\n        { error: `Invalid notificationPrefs: ${parsedPrefs.error.issues[0]?.message}` },\n        400,\n      );\n    }\n    const headerLocale = c.env.incoming.headers[\"accept-language\"]?.split(/[,;]/)[0]?.trim();\n    const locale =\n      bodyLocale ?? (headerLocale && LOCALE_TAG_RE.test(headerLocale) ? headerLocale : null);\n\n    if (typeof token !== \"string\" || token.length === 0) {\n      return c.json({ error: \"Missing token\" }, 400);\n    }\n    if (platform !== \"ios\" && platform !== \"android\") {\n      return c.json({ error: \"platform must be 'ios' or 'android'\" }, 400);\n    }\n\n    // Omitted kind means a client that predates Live Activities, which can only\n    // be registering an Expo relay token. Released clients cannot be\n    // force-updated, so this default is what keeps them working.\n    const kind = body?.kind === undefined ? DEFAULT_PUSH_TOKEN_KIND : body.kind;\n    if (!isPushTokenKind(kind)) {\n      // Reject rather than silently coercing to Expo: an ActivityKit token\n      // stored as Expo is rejected by the relay at send time with nothing at\n      // registration time to explain why.\n      return c.json(\n        { error: `kind must be one of ${PUSH_TOKEN_KINDS.join(\", \")}`, code: \"INVALID_KIND\" },\n        400,\n      );\n    }\n    // A per-activity token without its activity id cannot be targeted for an\n    // update or an end, so it would be stored and never usable.\n    if (kind === \"liveactivity_update\" && typeof body?.activityId !== \"string\") {\n      return c.json(\n        {\n          error: \"activityId is required for kind 'liveactivity_update'\",\n          code: \"MISSING_ACTIVITY\",\n        },\n        400,\n      );\n    }\n\n    const repo = deps.pushRepo();\n    if (!repo) {\n      // Report honestly rather than claiming success we cannot back — the exact\n      // failure mode this endpoint used to have.\n      return c.json({ error: \"Push registration is unavailable\", code: \"STORE_UNAVAILABLE\" }, 503);\n    }\n\n    // The device id comes from the authenticated principal, not from the body.\n    // A client-chosen id is unverified and tb-mobile sent its own install UUID,\n    // which matches no row in `devices` — so every token was attributed to a\n    // device that does not exist and the revoke cascade below would delete\n    // nothing. The body is still honoured for the shared api key, which names no\n    // device of its own. Register upserts by token and the upsert takes the new\n    // device_id whenever it is non-null, so rows written under the old handler\n    // heal on the next registration rather than needing a migration.\n    const principal = c.get(\"principal\");\n    const deviceId =\n      principal?.kind === \"device\" && principal.deviceId\n        ? principal.deviceId\n        : typeof body?.deviceId === \"string\"\n          ? body.deviceId\n          : null;\n\n    repo.register({\n      token,\n      platform,\n      deviceId,\n      kind,\n      activityId: typeof body?.activityId === \"string\" ? body.activityId : null,\n      sessionId: typeof body?.sessionId === \"string\" ? body.sessionId : null,\n      expiresAt: numberOrNull(body?.expiresAt),\n      staleDate: numberOrNull(body?.staleDate),\n      startedAt: numberOrNull(body?.startedAt),\n      clientServerId,\n      locale,\n      notificationPrefs: parsedPrefs?.data,\n    });\n    return c.json({ ok: true });\n  });\n\n  /**\n   * Change one token's notification preferences without re-registering it.\n   *\n   * Its own route because registering resets failure_streak and revoked_at:\n   * toggling a switch through it would wipe the delivery-health state the\n   * health screen reports. Same device-ownership rule as the delete below; the\n   * shared api key names no device and may set any token.\n   *\n   * 404 rather than the delete's idempotent 204: a client whose token is not\n   * registered here yet must find out its preferences were NOT stored, so it\n   * can register with them. A token owned by another device answers the same\n   * 404 so this cannot be used to probe for tokens.\n   */\n  app.patch(\"/api/push/preferences\", async (c) => {\n    const body = (await readJsonBody(c.env.incoming).catch(() => null)) as {\n      token?: unknown;\n      prefs?: unknown;\n    } | null;\n    const token = body?.token;\n    if (typeof token !== \"string\" || token.length === 0) {\n      return c.json({ error: \"Missing token\" }, 400);\n    }\n    const prefs = NotificationPrefsSchema.safeParse(body?.prefs);\n    if (!prefs.success) {\n      return c.json({ error: `Invalid prefs: ${prefs.error.issues[0]?.message}` }, 400);\n    }\n    const repo = deps.pushRepo();\n    if (!repo) {\n      return c.json({ error: \"Push registration is unavailable\", code: \"STORE_UNAVAILABLE\" }, 503);\n    }\n    const principal = c.get(\"principal\");\n    const deviceId = principal?.kind === \"device\" && principal.deviceId ? principal.deviceId : null;\n    if (!repo.setPrefs(token, prefs.data, deviceId)) {\n      return c.json({ error: \"Unknown token\", code: \"TOKEN_NOT_FOUND\" }, 404);\n    }\n    return c.body(null, 204);\n  });\n\n  /**\n   * Send one real push to a token the caller owns, ignoring its preferences.\n   *\n   * The settings screen's test button used to schedule a LOCAL notification,\n   * which proves the phone can show a banner and nothing about whether this\n   * server can reach it. This goes through Expo like every other push and\n   * reports what came back, so a failing relay or a dead token is visible.\n   * Preferences are bypassed on purpose: a test muted by the user's own quiet\n   * hours would answer \"does delivery work?\" with silence.\n   */\n  app.post(\"/api/push/test\", async (c) => {\n    const body = (await readJsonBody(c.env.incoming).catch(() => null)) as {\n      token?: unknown;\n    } | null;\n    const token = body?.token;\n    if (typeof token !== \"string\" || token.length === 0) {\n      return c.json({ error: \"Missing token\" }, 400);\n    }\n    const repo = deps.pushRepo();\n    const sender = deps.expoPushSender();\n    if (!repo || !sender) {\n      return c.json(\n        { error: \"Push notifications are unavailable\", code: \"STORE_UNAVAILABLE\" },\n        503,\n      );\n    }\n    const principal = c.get(\"principal\");\n    const row = repo.get(token);\n    const ownedElsewhere =\n      principal?.kind === \"device\" &&\n      principal.deviceId &&\n      row?.device_id != null &&\n      row.device_id !== principal.deviceId;\n    if (!row || ownedElsewhere) {\n      return c.json({ error: \"Unknown token\", code: \"TOKEN_NOT_FOUND\" }, 404);\n    }\n    if (row.kind !== \"expo\") {\n      return c.json(\n        { error: \"Only Expo tokens can receive a test push\", code: \"INVALID_KIND\" },\n        400,\n      );\n    }\n\n    const outcome = await sender.sendTo([row], (locale) => ({\n      title: \"Threadbase\",\n      body: testNotificationBody(locale),\n      data: { kind: \"test\" },\n      sound: \"default\",\n      priority: \"high\",\n    }));\n    // Re-read: the send just recorded success or failure against the row, and\n    // that state is the answer the caller wants.\n    const after = repo.get(token);\n    return c.json({\n      ok: outcome.succeeded > 0,\n      attempted: outcome.attempted,\n      succeeded: outcome.succeeded,\n      state: after ? tokenState(after) : \"revoked\",\n    });\n  });\n\n  /**\n   * Unregister a token.\n   *\n   * Same auth as register — `/api/push` maps to the `notifications` capability\n   * in `requiredCapability()`, which matches on path prefix and so classifies\n   * every method on it. An unauthenticated caller gets 401 and a read-only\n   * device 403, without a rule of its own.\n   *\n   * A device principal may only retire its own tokens (or an unattributed one\n   * left by the old register handler). The shared api key names no device, so it\n   * deletes by token alone — it is the owner's credential, and scoping it would\n   * leave a token nothing could ever remove.\n   *\n   * 204 and idempotent: an unknown token is already the state the caller asked\n   * for. Answering 404 would make a retry after a dropped response look like a\n   * failure and strand the client's local state. `kind` is accepted and ignored\n   * — the token is the primary key, so it names exactly one row whatever kind\n   * the client believes it to be.\n   */\n  app.delete(\"/api/push/register\", async (c) => {\n    // Raw Node request like the sibling routes — Hono's c.req.json() does not\n    // see a body on this server's request plumbing.\n    const body = (await readJsonBody(c.env.incoming).catch(() => null)) as {\n      token?: unknown;\n    } | null;\n    const token = body?.token;\n    if (typeof token !== \"string\" || token.length === 0) {\n      return c.json({ error: \"Missing token\" }, 400);\n    }\n\n    const repo = deps.pushRepo();\n    if (!repo) {\n      return c.json({ error: \"Push registration is unavailable\", code: \"STORE_UNAVAILABLE\" }, 503);\n    }\n\n    const principal = c.get(\"principal\");\n    // Return values deliberately ignored: deleting nothing is success here.\n    if (principal?.kind === \"device\" && principal.deviceId) {\n      repo.deleteTokenForDevice(token, principal.deviceId);\n    } else {\n      repo.deleteToken(token);\n    }\n    return c.body(null, 204);\n  });\n\n  // Delivery health for every registered token. Never echoes a token back — it\n  // is a delivery credential, and this endpoint exists to explain state, not to\n  // hand out secrets.\n  app.get(\"/api/push/health\", (c) => {\n    // `available` keeps its original meaning — \"the token store opened\" — because\n    // released mobile builds render it verbatim as \"Push store is available /\n    // unavailable (registration cannot persist)\". Retargeting it at credentials\n    // would make every credential-less server tell users their registrations do\n    // not persist, which is false. Credential state is the additive `push` object.\n    const push = describePushCapability({\n      liveActivity: deps.liveActivityPushEnabled(),\n      notifications: deps.expoPushEnabled(),\n    });\n    const repo = deps.pushRepo();\n    if (!repo) return c.json({ tokens: [], available: false, push });\n    return c.json({ tokens: repo.listHealth(), available: true, push });\n  });\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  const clientLogRateLimit = createRateLimiter({\n    limit: CLIENT_LOG_RATE_LIMIT,\n    windowMs: CLIENT_LOG_RATE_WINDOW_MS,\n  });\n\n  app.post(\"/api/__client-log\", async (c) => {\n    const principal = c.get(\"principal\");\n    const rateLimitKey =\n      principal?.kind === \"device\" && principal.deviceId\n        ? `device:${principal.deviceId}`\n        : \"legacy\";\n    if (!clientLogRateLimit(rateLimitKey)) {\n      return c.json({ ok: false, error: \"rate limited\" }, 429);\n    }\n\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 (err) {\n      if (err instanceof BodyTooLargeError) {\n        return c.json({ ok: false, error: \"request body is too large\" }, 413);\n      }\n      return c.json({ ok: false, error: \"invalid json\" }, 400);\n    }\n    const entries = (Array.isArray(body.entries) ? body.entries : []).slice(\n      0,\n      MAX_CLIENT_LOG_ENTRIES,\n    );\n    for (const e of entries) {\n      const level =\n        e.level === \"debug\" || e.level === \"warn\" || e.level === \"error\" ? e.level : \"info\";\n      const msg = truncateString(e.msg ?? \"\", MAX_CLIENT_LOG_MSG_LEN);\n      clientLog[level](`[client] ${e.tag ?? \"log\"}: ${msg}`, {\n        clientTs: e.ts,\n        tag: e.tag,\n        ua,\n        ...truncateFields(e.fields, MAX_CLIENT_LOG_FIELDS_LEN),\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(1440),\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 type Database from \"better-sqlite3\";\nimport type { NotificationPrefs } from \"../../schemas/notification-prefs.schema\";\n\n/**\n * Push registration and delivery state (C7).\n *\n * `POST /api/push/register` was a no-op returning `{ ok: true }`. Mobile\n * registered, received success, and nothing was stored — so no notification\n * could ever be delivered, no failure could be observed, and the client had no\n * way to discover that its successful registration meant nothing.\n *\n * This records tokens and what happened to them, which is the prerequisite for\n * every other C7 requirement: token health, last success/failure, retries,\n * revocation confirmation, and a test-notification endpoint all need somewhere\n * to read state from.\n */\n\n/**\n * Consecutive failures after which a token is treated as dead.\n *\n * A provider rejecting a token repeatedly means the app was uninstalled or the\n * token rotated. Retrying forever wastes work and, worse, makes the health\n * report read \"failing\" indefinitely instead of \"this device is gone\".\n */\nexport const FAILURE_STREAK_LIMIT = 5;\n\n/**\n * Token kinds. A device supplies three non-interchangeable types, and\n * conflating them fails only at send time with no signal at registration:\n *\n * - `expo` — Expo relay token, for ordinary push notifications.\n * - `liveactivity_start` — ActivityKit push-to-start token. App-wide, one per\n *   device, long-lived. Starts an activity when none exists.\n * - `liveactivity_update` — ActivityKit per-activity update token, issued by\n *   iOS after an activity starts, scoped to that one activity, short-lived.\n */\nexport const PUSH_TOKEN_KINDS = [\"expo\", \"liveactivity_start\", \"liveactivity_update\"] as const;\n\nexport type PushTokenKind = (typeof PUSH_TOKEN_KINDS)[number];\n\n/**\n * The kind assumed when a client does not send one.\n *\n * tb-mobile is released and cannot be force-updated, so an older client posting\n * `{ token, platform }` must keep working. Every such client is registering an\n * Expo relay token, because that is the only kind that existed then.\n */\nexport const DEFAULT_PUSH_TOKEN_KIND: PushTokenKind = \"expo\";\n\nexport function isPushTokenKind(value: unknown): value is PushTokenKind {\n  return typeof value === \"string\" && (PUSH_TOKEN_KINDS as readonly string[]).includes(value);\n}\n\nexport interface PushTokenRow {\n  token: string;\n  platform: string;\n  device_id: string | null;\n  registered_at: number;\n  last_success_at: number | null;\n  last_failure_at: number | null;\n  last_failure_code: string | null;\n  failure_streak: number;\n  revoked_at: number | null;\n  kind: PushTokenKind;\n  activity_id: string | null;\n  session_id: string | null;\n  expires_at: number | null;\n  stale_date: number | null;\n  started_at: number | null;\n  renewed_at: number | null;\n  /** The id the registering client files this server under; null for older clients. */\n  client_server_id: string | null;\n  /** The app's display language (BCP 47); null for older clients. */\n  locale: string | null;\n  /** JSON `NotificationPrefs`; null = the client never sent any, i.e. everything on. */\n  notification_prefs: string | null;\n}\n\n/**\n * Health as reported to a client. Deliberately omits the token itself — a push\n * token is a delivery credential, and a health endpoint has no reason to echo\n * one back.\n */\nexport interface PushTokenHealth {\n  platform: string;\n  deviceId: string | null;\n  registeredAt: number;\n  lastSuccessAt: number | null;\n  lastFailureAt: number | null;\n  lastFailureCode: string | null;\n  failureStreak: number;\n  revokedAt: number | null;\n  /**\n   * Never delivered vs delivering vs failing vs revoked vs expired. The\n   * distinction the user actually needs: \"not yet\" and \"broken\" look identical\n   * without it.\n   */\n  state: \"never-delivered\" | \"healthy\" | \"failing\" | \"dead\" | \"revoked\" | \"expired\";\n  kind: PushTokenKind;\n  /** Present only for per-activity Live Activity tokens. */\n  activityId: string | null;\n  sessionId: string | null;\n  expiresAt: number | null;\n}\n\nexport function tokenState(row: PushTokenRow, now: number = Date.now()): PushTokenHealth[\"state\"] {\n  if (row.revoked_at != null) return \"revoked\";\n  // Expiry before failure state: a per-activity token that lapsed is not\n  // \"broken\", and reporting it as failing would send the user chasing a\n  // delivery problem that does not exist.\n  if (row.expires_at != null && row.expires_at <= now) return \"expired\";\n  if (row.failure_streak >= FAILURE_STREAK_LIMIT) return \"dead\";\n  if (row.failure_streak > 0) return \"failing\";\n  if (row.last_success_at == null) return \"never-delivered\";\n  return \"healthy\";\n}\n\nexport function toHealth(row: PushTokenRow, now: number = Date.now()): PushTokenHealth {\n  return {\n    platform: row.platform,\n    deviceId: row.device_id,\n    registeredAt: row.registered_at,\n    lastSuccessAt: row.last_success_at,\n    lastFailureAt: row.last_failure_at,\n    lastFailureCode: row.last_failure_code,\n    failureStreak: row.failure_streak,\n    revokedAt: row.revoked_at,\n    state: tokenState(row, now),\n    kind: row.kind,\n    activityId: row.activity_id,\n    sessionId: row.session_id,\n    expiresAt: row.expires_at,\n  };\n}\n\nexport class PushRepository {\n  private upsertStmt: Database.Statement;\n  private getStmt: Database.Statement;\n  private listActiveStmt: Database.Statement;\n  private listAllStmt: Database.Statement;\n  private successStmt: Database.Statement;\n  private failureStmt: Database.Statement;\n  private revokeStmt: Database.Statement;\n  private deleteByTokenStmt: Database.Statement;\n  private deleteTokenForDeviceStmt: Database.Statement;\n  private setPrefsStmt: Database.Statement;\n  private setPrefsAnyDeviceStmt: Database.Statement;\n  private deleteByDeviceStmt: Database.Statement;\n  private claimEventStmt: Database.Statement;\n  private markDeliveredStmt: Database.Statement;\n  private listByKindSessionStmt: Database.Statement;\n  private listByKindStmt: Database.Statement;\n  private listRenewableStmt: Database.Statement;\n  private claimRenewalStmt: Database.Statement;\n  private expireStmt: Database.Statement;\n  private expireSessionActivitiesStmt: Database.Statement;\n\n  constructor(db: Database.Database) {\n    // Re-registering the same token updates rather than duplicating. Without\n    // this, one device accumulates rows and receives the same notification\n    // several times.\n    this.upsertStmt = db.prepare(`\n      INSERT INTO push_tokens (\n        token, platform, device_id, registered_at,\n        kind, activity_id, session_id, expires_at, stale_date, started_at,\n        client_server_id, locale, notification_prefs\n      )\n      VALUES (\n        @token, @platform, @device_id, @registered_at,\n        @kind, @activity_id, @session_id, @expires_at, @stale_date, @started_at,\n        @client_server_id, @locale, @notification_prefs\n      )\n      ON CONFLICT(token) DO UPDATE SET\n        platform = excluded.platform,\n        device_id = COALESCE(excluded.device_id, push_tokens.device_id),\n        -- Take a newer id (the client's id for a server changes when its URL is\n        -- edited), keep the stored one when this registration carries none.\n        client_server_id = COALESCE(excluded.client_server_id, push_tokens.client_server_id),\n        locale = COALESCE(excluded.locale, push_tokens.locale),\n        -- A registration that carries no preferences (an older build, or a\n        -- re-register after a token refresh) must not wipe the ones the user set.\n        notification_prefs = COALESCE(excluded.notification_prefs, push_tokens.notification_prefs),\n        registered_at = excluded.registered_at,\n        kind = excluded.kind,\n        activity_id = COALESCE(excluded.activity_id, push_tokens.activity_id),\n        session_id = COALESCE(excluded.session_id, push_tokens.session_id),\n        expires_at = excluded.expires_at,\n        stale_date = excluded.stale_date,\n        -- Preserve the ORIGINAL start across a re-registration. iOS renders its\n        -- own ticking timer from started_at, so overwriting it with a fresh\n        -- value visibly resets the user's elapsed time to zero.\n        started_at = COALESCE(push_tokens.started_at, excluded.started_at),\n        -- A fresh registration clears prior failure state and any revocation:\n        -- the client is telling us this token is live again. renewed_at clears\n        -- too — this is a new activity generation, so it is renewable again.\n        failure_streak = 0,\n        last_failure_at = NULL,\n        last_failure_code = NULL,\n        revoked_at = NULL,\n        renewed_at = NULL\n    `);\n    this.getStmt = db.prepare(\"SELECT * FROM push_tokens WHERE token = ?\");\n    // Expo relay tokens only. An ActivityKit token posted to Expo's relay is\n    // rejected, so the ordinary-notification fan-out must never see one — this\n    // is the query that keeps the three kinds from being conflated.\n    this.listActiveStmt = db.prepare(`\n      SELECT * FROM push_tokens\n       WHERE revoked_at IS NULL AND failure_streak < ${FAILURE_STREAK_LIMIT}\n         AND kind = 'expo'\n       ORDER BY registered_at ASC\n    `);\n    this.listAllStmt = db.prepare(\"SELECT * FROM push_tokens ORDER BY registered_at ASC\");\n    this.successStmt = db.prepare(`\n      UPDATE push_tokens\n         SET last_success_at = @at, failure_streak = 0,\n             last_failure_code = NULL\n       WHERE token = @token\n    `);\n    this.failureStmt = db.prepare(`\n      UPDATE push_tokens\n         SET last_failure_at = @at, last_failure_code = @code,\n             failure_streak = failure_streak + 1\n       WHERE token = @token\n    `);\n    this.revokeStmt = db.prepare(\"UPDATE push_tokens SET revoked_at = ? WHERE token = ?\");\n    this.deleteByTokenStmt = db.prepare(\"DELETE FROM push_tokens WHERE token = ?\");\n    // Ownership-scoped unregister. `device_id IS NULL` is not a widening:\n    // registrations written before the id was taken from the authenticated\n    // principal carry no device, and without this arm a phone could never\n    // retire the token it registered under the old handler.\n    this.deleteTokenForDeviceStmt = db.prepare(\n      \"DELETE FROM push_tokens WHERE token = @token AND (device_id = @device_id OR device_id IS NULL)\",\n    );\n    this.deleteByDeviceStmt = db.prepare(\"DELETE FROM push_tokens WHERE device_id = ?\");\n    // Same ownership term as the delete above. Deliberately its own statement\n    // rather than a re-register: the upsert clears failure_streak and\n    // revoked_at, so changing a toggle through it would wipe delivery health.\n    this.setPrefsStmt = db.prepare(\n      \"UPDATE push_tokens SET notification_prefs = @prefs WHERE token = @token AND (device_id = @device_id OR device_id IS NULL)\",\n    );\n    this.setPrefsAnyDeviceStmt = db.prepare(\n      \"UPDATE push_tokens SET notification_prefs = @prefs WHERE token = @token\",\n    );\n\n    // Live Activity sends are driven by a session status change, so they select\n    // by (kind, session) rather than scanning every token. Expired rows are\n    // excluded here rather than filtered by the caller: a lapsed per-activity\n    // token is not a delivery target, and APNs rejects it.\n    this.listByKindSessionStmt = db.prepare(`\n      SELECT * FROM push_tokens\n       WHERE kind = @kind AND session_id = @session_id\n         AND revoked_at IS NULL AND failure_streak < ${FAILURE_STREAK_LIMIT}\n         AND (expires_at IS NULL OR expires_at > @now)\n       ORDER BY registered_at ASC\n    `);\n    this.listByKindStmt = db.prepare(`\n      SELECT * FROM push_tokens\n       WHERE kind = @kind\n         AND revoked_at IS NULL AND failure_streak < ${FAILURE_STREAK_LIMIT}\n         AND (expires_at IS NULL OR expires_at > @now)\n       ORDER BY registered_at ASC\n    `);\n    // Renewal candidates. Bounded by the partial index on (stale_date) — only\n    // unrenewed rows with a deadline are ever candidates, so the boot-time\n    // re-arm scan stays proportional to pending renewals.\n    this.listRenewableStmt = db.prepare(`\n      SELECT * FROM push_tokens\n       WHERE kind = 'liveactivity_update'\n         AND stale_date IS NOT NULL AND renewed_at IS NULL\n         AND revoked_at IS NULL AND failure_streak < ${FAILURE_STREAK_LIMIT}\n       ORDER BY stale_date ASC\n    `);\n    // The idempotency gate for renewal. Conditional on renewed_at IS NULL so\n    // the first caller gets changes=1 and any concurrent or post-restart\n    // re-armed timer gets 0 and must not send — the same INSERT-OR-IGNORE\n    // reasoning as claimEvent, applied to an UPDATE.\n    this.claimRenewalStmt = db.prepare(`\n      UPDATE push_tokens SET renewed_at = @at\n       WHERE token = @token AND renewed_at IS NULL\n    `);\n    this.expireStmt = db.prepare(\"UPDATE push_tokens SET expires_at = ? WHERE token = ?\");\n    this.expireSessionActivitiesStmt = db.prepare(`\n      UPDATE push_tokens SET expires_at = @at\n       WHERE session_id = @session_id AND kind = 'liveactivity_update'\n         AND (expires_at IS NULL OR expires_at > @at)\n    `);\n\n    // INSERT OR IGNORE is the dedupe: the first caller for an event id inserts\n    // and gets changes=1, any concurrent or retried caller gets 0 and must not\n    // send. Doing this in SQL rather than a read-then-write avoids the race\n    // where two triggers both observe \"not yet sent\".\n    this.claimEventStmt = db.prepare(`\n      INSERT OR IGNORE INTO push_events (event_id, session_id, created_at)\n      VALUES (@event_id, @session_id, @created_at)\n    `);\n    this.markDeliveredStmt = db.prepare(\n      \"UPDATE push_events SET delivered_at = ? WHERE event_id = ?\",\n    );\n  }\n\n  /**\n   * Register or refresh a token.\n   *\n   * `kind` defaults to Expo so a released client posting `{ token, platform }`\n   * keeps working — tb-mobile cannot be force-updated, and every client\n   * predating Live Activities is registering an Expo relay token.\n   *\n   * Several rows per device is normal and intended: a device runs one activity\n   * per live session, each with its own update token. The token itself is the\n   * primary key, so distinct activities never collide.\n   */\n  register(args: {\n    token: string;\n    platform: string;\n    deviceId?: string | null;\n    kind?: PushTokenKind;\n    activityId?: string | null;\n    sessionId?: string | null;\n    expiresAt?: number | null;\n    staleDate?: number | null;\n    startedAt?: number | null;\n    clientServerId?: string | null;\n    locale?: string | null;\n    notificationPrefs?: NotificationPrefs | null;\n    now?: number;\n  }): void {\n    this.upsertStmt.run({\n      token: args.token,\n      platform: args.platform,\n      device_id: args.deviceId ?? null,\n      registered_at: args.now ?? Date.now(),\n      kind: args.kind ?? DEFAULT_PUSH_TOKEN_KIND,\n      activity_id: args.activityId ?? null,\n      session_id: args.sessionId ?? null,\n      expires_at: args.expiresAt ?? null,\n      stale_date: args.staleDate ?? null,\n      started_at: args.startedAt ?? null,\n      client_server_id: args.clientServerId ?? null,\n      locale: args.locale ?? null,\n      notification_prefs: args.notificationPrefs ? JSON.stringify(args.notificationPrefs) : null,\n    });\n  }\n\n  /**\n   * Store new notification preferences for one token, touching nothing else.\n   *\n   * `deviceId` scopes the write to this device's own tokens (or an unattributed\n   * one), so a `notifications`-holding device cannot mute another's. Pass\n   * `null` for the shared api key, which names no device and may set any token.\n   * Returns whether a row matched.\n   */\n  setPrefs(token: string, prefs: NotificationPrefs, deviceId: string | null): boolean {\n    const json = JSON.stringify(prefs);\n    if (deviceId === null)\n      return this.setPrefsAnyDeviceStmt.run({ token, prefs: json }).changes > 0;\n    return this.setPrefsStmt.run({ token, prefs: json, device_id: deviceId }).changes > 0;\n  }\n\n  get(token: string): PushTokenRow | null {\n    return (this.getStmt.get(token) as PushTokenRow | undefined) ?? null;\n  }\n\n  /**\n   * Expo tokens eligible for delivery — not revoked, not past the failure limit.\n   *\n   * Deliberately Expo-only. ActivityKit tokens go over direct APNs with a\n   * different topic and are rejected by Expo's relay, so the ordinary\n   * notification fan-out must not see them.\n   */\n  listDeliverable(): PushTokenRow[] {\n    return this.listActiveStmt.all() as PushTokenRow[];\n  }\n\n  /** Live-activity tokens for one session, eligible for delivery. */\n  listForSession(kind: PushTokenKind, sessionId: string, now: number = Date.now()): PushTokenRow[] {\n    return this.listByKindSessionStmt.all({\n      kind,\n      session_id: sessionId,\n      now,\n    }) as PushTokenRow[];\n  }\n\n  /**\n   * Every deliverable token of one kind.\n   *\n   * Used for push-to-start, which is app-wide rather than session-scoped: the\n   * activity does not exist yet, so there is no per-activity token to look up.\n   */\n  listByKind(kind: PushTokenKind, now: number = Date.now()): PushTokenRow[] {\n    return this.listByKindStmt.all({ kind, now }) as PushTokenRow[];\n  }\n\n  /** Unrenewed activities with a renewal deadline, soonest first. */\n  listRenewable(): PushTokenRow[] {\n    return this.listRenewableStmt.all() as PushTokenRow[];\n  }\n\n  /**\n   * Claim a row for renewal.\n   *\n   * Returns true exactly once per row. A restart re-arms timers from the\n   * persisted deadline, so the same renewal can be attempted twice; the loser\n   * gets false and must not send. Doing this as a conditional UPDATE rather\n   * than read-then-write avoids the race where both attempts observe\n   * \"not yet renewed\".\n   */\n  claimRenewal(token: string, now: number = Date.now()): boolean {\n    return this.claimRenewalStmt.run({ token, at: now }).changes > 0;\n  }\n\n  /** Mark one token expired, so it stops being a delivery target. */\n  expire(token: string, now: number = Date.now()): void {\n    this.expireStmt.run(now, token);\n  }\n\n  /**\n   * Expire every live activity for a session.\n   *\n   * Called when the session ends. Without this, a per-activity token outlives\n   * its session and a later renewal sweep would resurrect an activity for a\n   * session that is already gone.\n   */\n  expireSessionActivities(sessionId: string, now: number = Date.now()): void {\n    this.expireSessionActivitiesStmt.run({ session_id: sessionId, at: now });\n  }\n\n  /** Every token, including dead and revoked ones, for the health report. */\n  listHealth(now: number = Date.now()): PushTokenHealth[] {\n    return (this.listAllStmt.all() as PushTokenRow[]).map((r) => toHealth(r, now));\n  }\n\n  recordSuccess(token: string, now: number = Date.now()): void {\n    this.successStmt.run({ token, at: now });\n  }\n\n  recordFailure(token: string, code: string, now: number = Date.now()): void {\n    this.failureStmt.run({ token, at: now, code });\n  }\n\n  revoke(token: string, now: number = Date.now()): boolean {\n    return this.revokeStmt.run(now, token).changes > 0;\n  }\n\n  /**\n   * Erase one token.\n   *\n   * Delete, not revoke: a retained row is still a stored delivery credential,\n   * and a client unregistering is asking for the credential to be gone, not for\n   * it to be marked dead and kept for the health report.\n   */\n  deleteToken(token: string): boolean {\n    return this.deleteByTokenStmt.run(token).changes > 0;\n  }\n\n  /**\n   * Erase one token, but only if it belongs to this device (or to no device).\n   *\n   * What keeps a `notifications`-holding device from retiring another device's\n   * token: the token value is the only thing the route is given, so without the\n   * ownership term any caller that learns a token can delete it.\n   */\n  deleteTokenForDevice(token: string, deviceId: string): boolean {\n    return this.deleteTokenForDeviceStmt.run({ token, device_id: deviceId }).changes > 0;\n  }\n\n  /**\n   * Erase every token attributed to a device.\n   *\n   * Cross-database by necessity — devices live in runtime.db and tokens in\n   * cache.db — so this is called from the device routes and the CLI, never\n   * joined in SQL.\n   */\n  deleteForDevice(deviceId: string): number {\n    return this.deleteByDeviceStmt.run(deviceId).changes;\n  }\n\n  /**\n   * Claim an event id for delivery.\n   *\n   * Returns true exactly once per event id. A retry, a reconnect\n   * reconciliation, or two triggers firing for the same underlying event all\n   * get false and must not notify — the user should never be told twice about\n   * one thing.\n   */\n  claimEvent(eventId: string, sessionId: string | null, now: number = Date.now()): boolean {\n    return (\n      this.claimEventStmt.run({ event_id: eventId, session_id: sessionId, created_at: now })\n        .changes > 0\n    );\n  }\n\n  markDelivered(eventId: string, now: number = Date.now()): void {\n    this.markDeliveredStmt.run(now, eventId);\n  }\n}\n","import { z } from \"zod\";\n\nconst HHMM = /^([01]\\d|2[0-3]):[0-5]\\d$/;\n\n/** IANA zone names resolve in `Intl`; anything else throws a RangeError. */\nfunction isValidTimeZone(tz: string): boolean {\n  try {\n    new Intl.DateTimeFormat(\"en-US\", { timeZone: tz });\n    return true;\n  } catch {\n    return false;\n  }\n}\n\nconst TimeSchema = z.string().regex(HHMM, \"expected HH:MM, 24-hour\");\n\nexport const QuietWindowSchema = z.object({ from: TimeSchema, to: TimeSchema });\n\nexport const WEEKDAYS = [\"mon\", \"tue\", \"wed\", \"thu\", \"fri\", \"sat\", \"sun\"] as const;\nexport type Weekday = (typeof WEEKDAYS)[number];\n\nexport const QuietHoursSchema = z.object({\n  enabled: z.boolean(),\n  /** The phone's zone. Without it \"22:00\" means the server's clock, not the user's. */\n  tz: z.string().refine(isValidTimeZone, \"unknown IANA time zone\"),\n  default: QuietWindowSchema,\n  /**\n   * A weekday listed here replaces `default` for the window that STARTS that\n   * day. `null` means no quiet hours that day.\n   */\n  days: z.partialRecord(z.enum(WEEKDAYS), QuietWindowSchema.nullable()).optional(),\n});\n\n/**\n * What one device wants to be told about.\n *\n * `waitingInput` gates every push that means \"the agent is waiting on you\" —\n * finished, needs a go-ahead, has a question. `sessionFailed` gates the\n * \"session could not start\" push.\n */\nexport const NotificationPrefsSchema = z.object({\n  waitingInput: z.boolean(),\n  sessionFailed: z.boolean(),\n  quietHours: QuietHoursSchema.optional(),\n});\n\nexport type QuietWindow = z.infer<typeof QuietWindowSchema>;\nexport type QuietHours = z.infer<typeof QuietHoursSchema>;\nexport type NotificationPrefs = z.infer<typeof NotificationPrefsSchema>;\n","import { createSign } from \"node:crypto\";\nimport { type ClientHttp2Session, connect, constants } from \"node:http2\";\nimport { getLogger } from \"../../logger\";\n\n/**\n * Direct APNs sender for ActivityKit Live Activities.\n *\n * ActivityKit cannot go through Expo's push relay: Live Activity updates need a\n * different token type, a `.push-type.liveactivity` topic, and a p8 signing\n * credential. That rules out `expo-server-sdk` and any relay, so this speaks\n * HTTP/2 to APNs directly.\n *\n * Built on `node:http2` and `node:crypto` rather than an APNs SDK — the whole\n * protocol surface used here is one POST with five headers and an ES256 JWT, and\n * a dependency for that would be larger than the code it replaced.\n *\n * Credentials come from the environment only. The signing key is PEM material in\n * `APNS_KEY` (the operator supplies it from 1Password), never a path on disk and\n * never written to a temp file. Neither the key nor any device token is logged.\n */\n\nconst log = getLogger(\"apns\");\n\n/** Sandbox default: the app's `aps-environment` is still `development`. */\nexport const APNS_HOST_SANDBOX = \"api.sandbox.push.apple.com\";\nexport const APNS_HOST_PRODUCTION = \"api.push.apple.com\";\n\n/**\n * APNs rejects a payload over 4 KB.\n *\n * Checked before sending rather than after a rejection, so an oversized payload\n * is a diagnosable local error instead of an opaque 413 from Apple.\n */\nexport const APNS_MAX_PAYLOAD_BYTES = 4096;\n\n/**\n * JWT lifetime. Apple rejects a token older than 1 hour, and reusing one is\n * expected — minting per request would be wasteful and can trip APNs' own\n * \"too many provider token updates\" throttle.\n */\nconst JWT_TTL_SECONDS = 3000;\n\nexport interface ApnsCredentials {\n  /** p8 PEM contents. Never a filesystem path. */\n  key: string;\n  keyId: string;\n  teamId: string;\n  bundleId: string;\n  host: string;\n}\n\nexport interface ApnsSendResult {\n  ok: boolean;\n  status: number;\n  /** APNs `reason`, e.g. `BadDeviceToken` / `ExpiredToken`. */\n  reason?: string;\n  /**\n   * True when APNs says this token will never work again. The caller must stop\n   * using it rather than retrying — retrying a dead token fails forever and\n   * makes the health report read \"failing\" instead of \"this device is gone\".\n   */\n  tokenDead: boolean;\n}\n\n/**\n * APNs reasons meaning the token is permanently unusable.\n *\n * Distinguished from transient failures (503, timeouts) because the two need\n * opposite handling: a dead token must be retired, a transient one must not be.\n */\nconst DEAD_TOKEN_REASONS = new Set([\n  \"BadDeviceToken\",\n  \"DeviceTokenNotForTopic\",\n  \"Unregistered\",\n  \"ExpiredToken\",\n]);\n\nfunction base64url(input: Buffer | string): string {\n  return Buffer.from(input).toString(\"base64url\");\n}\n\n/**\n * Read APNs credentials from the environment.\n *\n * Returns null when `APNS_KEY` is absent, which is the normal case for a\n * developer machine and for CI. Callers treat null as \"Live Activity push is\n * off\" and skip sending — the server must not fail to boot because an optional\n * push credential is missing.\n *\n * Every identifier comes from the environment with no default. Baking one\n * deployment's Apple account into the source would make this package silently\n * sign for the wrong team when someone else deploys it, and the resulting APNs\n * rejection names none of that. Only the host has a default, because sandbox\n * versus production is a deployment choice rather than an account identity.\n *\n * Returns null (rather than throwing) when the key is present but an identifier\n * is missing, so a half-configured environment disables the feature instead of\n * taking the server down. `describeMissingApnsCredentials` explains which one.\n */\nexport function readApnsCredentialsFromEnv(\n  env: NodeJS.ProcessEnv = process.env,\n): ApnsCredentials | null {\n  const key = env.APNS_KEY;\n  if (!key || key.trim().length === 0) return null;\n\n  const keyId = env.APNS_KEY_ID?.trim();\n  const teamId = env.APNS_TEAM_ID?.trim();\n  const bundleId = env.APNS_BUNDLE_ID?.trim();\n  if (!keyId || !teamId || !bundleId) return null;\n\n  const host = env.APNS_HOST ?? APNS_HOST_SANDBOX;\n\n  return { key, keyId, teamId, bundleId, host };\n}\n\n/**\n * Why Live Activity push is unavailable, as a message safe to log.\n *\n * Says which variable is missing and nothing about its value, so a boot log can\n * explain the feature being off without becoming a place secrets leak.\n */\nexport function describeMissingApnsCredentials(\n  env: NodeJS.ProcessEnv = process.env,\n): string | null {\n  if (!env.APNS_KEY || env.APNS_KEY.trim().length === 0) {\n    return (\n      \"APNS_KEY is not set, so Live Activity push is disabled. \" +\n      \"Set it to the p8 key contents (not a path) to enable it.\"\n    );\n  }\n\n  // A key with no identifiers is the harder case to diagnose: it looks\n  // configured, and APNs answers a mismatch with a bare InvalidProviderToken\n  // that names nothing. Say exactly which variable is absent.\n  const missing = (\n    [\n      [\"APNS_KEY_ID\", env.APNS_KEY_ID],\n      [\"APNS_TEAM_ID\", env.APNS_TEAM_ID],\n      [\"APNS_BUNDLE_ID\", env.APNS_BUNDLE_ID],\n    ] as const\n  )\n    .filter(([, value]) => !value || value.trim().length === 0)\n    .map(([name]) => name);\n\n  if (missing.length === 0) return null;\n  return (\n    `APNS_KEY is set but ${missing.join(\", \")} ${missing.length === 1 ? \"is\" : \"are\"} not, ` +\n    \"so Live Activity push is disabled. Under launchd, APNS_KEY_ID is derived from the \" +\n    \"AuthKey_<keyId>.p8 filename; the team and bundle ids must be set explicitly.\"\n  );\n}\n\nexport class ApnsClient {\n  private session: ClientHttp2Session | null = null;\n  private cachedJwt: { token: string; expiresAt: number } | null = null;\n\n  constructor(private readonly creds: ApnsCredentials) {}\n\n  /**\n   * The `apns-topic` for Live Activity pushes.\n   *\n   * The `.push-type.liveactivity` suffix is mandatory and is why the signing key\n   * must be Team Scoped (All Topics) — a key scoped to the bundle id alone\n   * cannot sign this topic.\n   */\n  get topic(): string {\n    return `${this.creds.bundleId}.push-type.liveactivity`;\n  }\n\n  /**\n   * Mint or reuse the provider JWT.\n   *\n   * ES256 over the p8 key. Cached until shortly before expiry: Apple rejects a\n   * token older than an hour, but minting one per request is wasteful and can\n   * trip APNs' provider-token-update throttle.\n   */\n  private getJwt(now: number = Date.now()): string {\n    const nowSeconds = Math.floor(now / 1000);\n    if (this.cachedJwt && this.cachedJwt.expiresAt > nowSeconds + 60) {\n      return this.cachedJwt.token;\n    }\n\n    const header = base64url(JSON.stringify({ alg: \"ES256\", kid: this.creds.keyId, typ: \"JWT\" }));\n    const payload = base64url(JSON.stringify({ iss: this.creds.teamId, iat: nowSeconds }));\n    const signingInput = `${header}.${payload}`;\n\n    // dsaEncoding: \"ieee-p1363\" produces the raw r||s form JWS requires. Node's\n    // default is DER, which APNs rejects as an invalid token — and the error\n    // (\"InvalidProviderToken\") does not hint at the encoding.\n    const signature = createSign(\"SHA256\")\n      .update(signingInput)\n      .sign({ key: this.creds.key, dsaEncoding: \"ieee-p1363\" });\n\n    const token = `${signingInput}.${base64url(signature)}`;\n    this.cachedJwt = { token, expiresAt: nowSeconds + JWT_TTL_SECONDS };\n    return token;\n  }\n\n  /**\n   * Reuse one HTTP/2 session across sends.\n   *\n   * APNs expects a long-lived connection; a fresh TLS handshake per push is slow\n   * and Apple treats connection churn as abuse.\n   */\n  private getSession(): ClientHttp2Session {\n    if (this.session && !this.session.closed && !this.session.destroyed) {\n      return this.session;\n    }\n    const session = connect(`https://${this.creds.host}`);\n    // A connection-level error must not reach the process as an unhandled\n    // 'error' event, which would take the server down over a push failure.\n    session.on(\"error\", (err) => {\n      log.warn(\"apns.session_error\", { event: \"apns.session_error\", err: String(err) });\n    });\n    this.session = session;\n    return session;\n  }\n\n  /**\n   * Send one push.\n   *\n   * Resolves with a result rather than rejecting on an APNs rejection: a\n   * rejected push is an expected outcome the caller must act on (retire the\n   * token), not an exception. Only a genuinely unexpected local failure throws,\n   * and the caller logs it.\n   */\n  async send(args: {\n    deviceToken: string;\n    payload: unknown;\n    /** 10 for a user-visible change, 5 to let iOS batch. */\n    priority?: 5 | 10;\n    /** Seconds since epoch after which APNs stops trying. */\n    expirationSeconds?: number;\n    timeoutMs?: number;\n  }): Promise<ApnsSendResult> {\n    const body = Buffer.from(JSON.stringify(args.payload), \"utf-8\");\n    if (body.byteLength > APNS_MAX_PAYLOAD_BYTES) {\n      // Fail locally with a diagnosable message instead of shipping a payload\n      // Apple will reject with a bare 413.\n      throw new Error(\n        `APNs payload is ${body.byteLength} bytes, over the ${APNS_MAX_PAYLOAD_BYTES} byte limit`,\n      );\n    }\n\n    const session = this.getSession();\n    const headers = {\n      [constants.HTTP2_HEADER_METHOD]: \"POST\",\n      [constants.HTTP2_HEADER_PATH]: `/3/device/${args.deviceToken}`,\n      [constants.HTTP2_HEADER_AUTHORIZATION]: `bearer ${this.getJwt()}`,\n      \"apns-push-type\": \"liveactivity\",\n      \"apns-topic\": this.topic,\n      \"apns-priority\": String(args.priority ?? 10),\n      ...(args.expirationSeconds != null && {\n        \"apns-expiration\": String(args.expirationSeconds),\n      }),\n      [constants.HTTP2_HEADER_CONTENT_TYPE]: \"application/json\",\n      [constants.HTTP2_HEADER_CONTENT_LENGTH]: String(body.byteLength),\n    };\n\n    return new Promise<ApnsSendResult>((resolve, reject) => {\n      const req = session.request(headers);\n      req.setTimeout(args.timeoutMs ?? 10_000, () => {\n        req.close(constants.NGHTTP2_CANCEL);\n        // A timeout is transient — the token stays usable, so tokenDead is\n        // false and the caller must not retire it.\n        resolve({ ok: false, status: 0, reason: \"Timeout\", tokenDead: false });\n      });\n\n      let status = 0;\n      req.on(\"response\", (resHeaders) => {\n        status = Number(resHeaders[constants.HTTP2_HEADER_STATUS] ?? 0);\n      });\n\n      const chunks: Buffer[] = [];\n      req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n      req.on(\"error\", reject);\n      req.on(\"end\", () => {\n        const raw = Buffer.concat(chunks).toString(\"utf-8\");\n        // APNs sends an empty body on success and { reason } on failure.\n        let reason: string | undefined;\n        if (raw.length > 0) {\n          try {\n            reason = (JSON.parse(raw) as { reason?: string }).reason;\n          } catch {\n            // A non-JSON error body is still worth surfacing verbatim rather\n            // than discarding — it is all we would have to diagnose with.\n            reason = raw.slice(0, 200);\n          }\n        }\n        resolve({\n          ok: status === 200,\n          status,\n          reason,\n          tokenDead: reason != null && DEAD_TOKEN_REASONS.has(reason),\n        });\n      });\n\n      req.end(body);\n    });\n  }\n\n  /** Close the shared connection. Called on server shutdown. */\n  close(): void {\n    this.session?.close();\n    this.session = null;\n  }\n}\n","import type { ProviderName } from \"../../providers\";\n\n/**\n * Notification copy, per language.\n *\n * The languages are the ones tb-mobile ships (`locales/`): a push in a language\n * the app itself cannot display would be the only screen in it. Anything else\n * falls back to English.\n *\n * What the copy may say is bounded by the privacy policy: which session and\n * which agent, never what the agent said or what it wants to run. That is why a\n * permission push reads \"needs your go-ahead\" rather than naming the tool, and\n * why a question push does not carry the question. See waitingInputMessage.\n *\n * The non-English strings are machine-written and want a native review.\n */\n\nexport type AttentionKind = \"turn_done\" | \"permission\" | \"question\" | \"failed\";\n\ntype Copy = Record<AttentionKind, (agent: string) => string>;\n\nconst COPY: Record<string, Copy> = {\n  en: {\n    turn_done: (a) => `${a} finished — tap to read the reply and continue.`,\n    permission: (a) => `${a} needs your go-ahead to continue.`,\n    question: (a) => `${a} has a question for you.`,\n    failed: (a) => `${a} could not start.`,\n  },\n  he: {\n    turn_done: (a) => `התשובה של ${a} מוכנה — הקישו כדי לקרוא ולהמשיך.`,\n    permission: (a) => `${a} ממתין לאישור שלך כדי להמשיך.`,\n    question: (a) => `ל-${a} יש שאלה בשבילך.`,\n    failed: (a) => `${a} לא הצליח להתחיל.`,\n  },\n  ar: {\n    turn_done: (a) => `رد ${a} جاهز — اضغط للقراءة والمتابعة.`,\n    permission: (a) => `${a} بانتظار موافقتك للمتابعة.`,\n    question: (a) => `لدى ${a} سؤال لك.`,\n    failed: (a) => `تعذّر تشغيل ${a}.`,\n  },\n  ru: {\n    turn_done: (a) => `Ответ ${a} готов — нажмите, чтобы прочитать и продолжить.`,\n    permission: (a) => `${a} ждёт вашего разрешения, чтобы продолжить.`,\n    question: (a) => `У ${a} есть к вам вопрос.`,\n    failed: (a) => `${a} не удалось запустить.`,\n  },\n};\n\n/** Scannable at a glance in a stack of notifications, in any language. */\nconst TITLE_MARK: Record<AttentionKind, string> = {\n  turn_done: \"✅\",\n  permission: \"✋\",\n  question: \"💬\",\n  failed: \"❌\",\n};\n\n/** \"he-IL\" → \"he\"; unsupported or absent → \"en\". `iw` is Android's legacy Hebrew code. */\nexport function pushLanguage(locale: string | null | undefined): string {\n  const lang = locale?.split(/[-_]/)[0]?.toLowerCase();\n  if (lang === \"iw\") return \"he\";\n  return lang && Object.hasOwn(COPY, lang) ? lang : \"en\";\n}\n\nconst TEST_COPY: Record<string, string> = {\n  en: \"Test notification — push delivery works.\",\n  he: \"התראת בדיקה — שליחת ההתראות עובדת.\",\n  ar: \"إشعار تجريبي — تسليم الإشعارات يعمل.\",\n  ru: \"Тестовое уведомление — доставка работает.\",\n};\n\n/** What the settings screen's \"Send test notification\" delivers. */\nexport function testNotificationBody(locale: string | null | undefined): string {\n  return TEST_COPY[pushLanguage(locale)];\n}\n\nexport function agentLabel(provider: ProviderName | undefined): string {\n  if (provider === \"codex-cli\") return \"Codex\";\n  if (provider === \"cursor\") return \"Cursor\";\n  return \"Claude\";\n}\n\nexport function attentionTitle(kind: AttentionKind, projectName: string): string {\n  return `${TITLE_MARK[kind]} ${projectName || \"Threadbase\"}`;\n}\n\nexport function attentionBody(\n  kind: AttentionKind,\n  provider: ProviderName | undefined,\n  locale: string | null | undefined,\n): string {\n  return COPY[pushLanguage(locale)][kind](agentLabel(provider));\n}\n","import { Hono } from \"hono\";\nimport { getVersion } from \"../../version\";\nimport type { AppEnv } from \"../app\";\nimport type { ApiDeps } from \"../types/api-deps\";\n\nexport const createHealthRoutes = (deps: ApiDeps) => {\n  const app = new Hono<AppEnv>();\n\n  app.get(\"/\", (c) => {\n    const cacheAlert = deps.cacheMonitor()?.healthzField();\n    return c.json({ ok: true, version: getVersion(), ...(cacheAlert ? { cacheAlert } : {}) });\n  });\n\n  return app;\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\n/**\n * Absolute paths to the supervised streamer's stdout/stderr logs — the single\n * source of truth every supervisor backend and deploy script must agree on.\n *\n * macOS points the plist's StandardOutPath/StandardErrorPath here; Windows has\n * no native redirection, so `scripts/deploy.ps1` writes the same two paths into\n * launch.cmd as cmd `>>` targets. That agreement is what `tb-streamer prod logs`\n * depends on, and it is locked by `__tests__/deploy-windows-script.test.ts`.\n */\nexport function logPaths(): { stdout: string; stderr: string } {\n  const dir = join(installDir(), \"logs\");\n  return { stdout: join(dir, \"stdout.log\"), stderr: join(dir, \"stderr.log\") };\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 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  app.get(\"/summary\", (c) => {\n    const url = new URL(c.req.url);\n    deps.handleGetProjectSummaries(url, c.env.outgoing);\n    return alreadyHandled();\n  });\n\n  return app;\n};\n","import { Hono } from \"hono\";\nimport { PROVIDER_NAMES } from \"../../providers\";\nimport { providerHealth } from \"../../services/providers/providerHealth\";\nimport type { AppEnv } from \"../app\";\n\n/**\n * GET /api/providers — what each provider supports, and whether the installed\n * build is one we have verified against (C2).\n *\n * Clients need this to hide actions a provider cannot perform, rather than\n * offering them and failing. Before this, the only capability signal that\n * reached a client was a 501 from LiveSessionManager — i.e. it arrived as a\n * failed action instead of an absent button.\n *\n * Capabilities are per-provider, not per-session, so this is a separate\n * endpoint rather than more fields on SessionResponse.\n *\n * Deliberately exposes no filesystem paths: which binary resolved is an\n * implementation detail, and a resolved path can leak the user's home layout.\n */\nexport const createProviderRoutes = () => {\n  const app = new Hono<AppEnv>();\n\n  app.get(\"/\", async (c) => {\n    const providers = await Promise.all(PROVIDER_NAMES.map((name) => providerHealth(name)));\n\n    return c.json({ providers });\n  });\n\n  return app;\n};\n","import { execFile } from \"child_process\";\nimport { isWindows, locateProviderExe } from \"../../platform\";\nimport {\n  CLAUDE_CODE_PROVIDER,\n  CODEX_CLI_PROVIDER,\n  CURSOR_PROVIDER,\n  type ProviderName,\n} from \"../../providers\";\nimport { capabilitiesFor, type ProviderCapabilities, type VerifiedAgainst } from \"./capabilities\";\n\n/**\n * Provider version detection and compatibility reporting (C2).\n * See docs/architecture/2026-07-24-provider-compatibility.md.\n *\n * Our parsing and TUI detection are calibrated against specific provider\n * versions — the fixtures under __tests__/fixtures/providers/<provider>/<version>\n * record which. This module compares what is actually installed against that,\n * so drift is reported instead of surfacing as a mysteriously stuck session.\n *\n * A warning is never a refusal. A provider working slightly outside our verified\n * range is vastly better than us blocking it.\n */\n\n/** Versions each adapter's fixtures were captured against. */\nexport const VERIFIED_AGAINST: Record<ProviderName, VerifiedAgainst> = {\n  [CLAUDE_CODE_PROVIDER]: { captured: [\"2.1.214\"], min: \"2.1.0\" },\n  [CODEX_CLI_PROVIDER]: { captured: [\"0.140.0-alpha.19\"], min: \"0.140.0\" },\n  // Transcript *shape* era (agent-transcripts JSONL), not a live `agent --version`\n  // pin — we have no captured CLI build yet, so any installed version warns.\n  [CURSOR_PROVIDER]: { captured: [\"2026.1.0\"] },\n};\n\nexport type ProviderWarningCode =\n  /** The CLI is not installed, or not on PATH. */\n  | \"provider_not_found\"\n  /** Installed, but we could not read a version from it. */\n  | \"version_undetectable\"\n  /** Installed and readable, but outside the range our fixtures cover. */\n  | \"version_unverified\";\n\nexport interface ProviderWarning {\n  code: ProviderWarningCode;\n  message: string;\n}\n\nexport interface ProviderHealth {\n  name: ProviderName;\n  available: boolean;\n  version: string | null;\n  verifiedAgainst: VerifiedAgainst;\n  capabilities: ProviderCapabilities;\n  warnings: ProviderWarning[];\n}\n\nconst VERSION_TIMEOUT_MS = 3_000;\n\n/**\n * Run `<exe> --version` and return the first version-looking token.\n *\n * Deliberately tolerant: providers format this line differently and change it\n * between releases, so we scrape a semver-shaped substring rather than assume a\n * layout. Any failure yields null, which the caller treats as \"unverified\"\n * rather than \"incompatible\" — an unreadable version is not evidence of a\n * problem with the provider.\n */\nexport function parseVersionOutput(output: string): string | null {\n  const match = output.match(/\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?/);\n  return match ? match[0] : null;\n}\n\n/**\n * Versions already read, keyed by the executable they were read from.\n *\n * `--version` is the only part of a health check that costs a process spawn\n * (measured on macOS: claude 85ms, codex 13ms), and mobile re-asks every 60s\n * while a user browses. A version cannot change without someone replacing the\n * binary, so this is a fact about the machine rather than about the request.\n *\n * Deliberately no TTL, and deliberately NOT extended to `available`. A stale\n * version can only misword the compatibility warning; a stale `available` would\n * grey out the provider button for a user who just installed the CLI to fix the\n * error telling them it was missing. Availability is re-derived on every call\n * for exactly that reason — it is a PATH walk of stats, and costs nothing.\n *\n * Only successful reads are cached, so a probe that lost to a timeout under\n * load is retried rather than latched as \"unverifiable\" until restart.\n */\nconst versionByExe = new Map<string, string>();\n\nfunction runVersion(exe: string): Promise<string | null> {\n  const cached = versionByExe.get(exe);\n  if (cached !== undefined) return Promise.resolve(cached);\n\n  // Since the CVE-2024-27980 fix, node refuses to spawn a .cmd/.bat without a\n  // shell and throws EINVAL synchronously. npm installs the CLIs as claude.cmd\n  // and resolveClaudeExe() returns exactly that, so on Windows the shell is the\n  // only way to read a version at all. `--version` is a fixed literal and the\n  // resolved path is quoted, so the shell adds no injection surface.\n  const viaShell = isWindows && /\\.(?:cmd|bat)$/i.test(exe);\n  const file = viaShell ? `\"${exe}\"` : exe;\n  return new Promise((resolve) => {\n    execFile(\n      file,\n      [\"--version\"],\n      { timeout: VERSION_TIMEOUT_MS, shell: viaShell, windowsHide: true },\n      (err, stdout, stderr) => {\n        if (err && !stdout && !stderr) return resolve(null);\n        const version = parseVersionOutput(`${stdout}${stderr}`);\n        if (version !== null) versionByExe.set(exe, version);\n        resolve(version);\n      },\n    );\n  });\n}\n\n/**\n * Compare a detected version against the range an adapter claims to cover.\n *\n * Only `min` is enforced as a floor. There is deliberately no upper bound check\n * beyond `max` when set: providers release constantly, and refusing to run\n * against a version merely newer than our newest fixture would break users on\n * every provider update for no evidence of an actual incompatibility.\n */\nexport function compareToVerified(\n  version: string | null,\n  verified: VerifiedAgainst,\n): ProviderWarning | null {\n  if (version === null) {\n    return {\n      code: \"version_undetectable\",\n      message:\n        \"Could not determine the installed version, so compatibility is unverified. \" +\n        \"Parsing and prompt detection may not match this build.\",\n    };\n  }\n  if (verified.captured.includes(version)) return null;\n\n  const below = verified.min != null && compareSemver(version, verified.min) < 0;\n  const above = verified.max != null && compareSemver(version, verified.max) > 0;\n  if (!below && !above && verified.max != null) return null;\n  if (!below && verified.max == null && !isNewerThanAllCaptured(version, verified.captured)) {\n    return null;\n  }\n\n  return {\n    code: \"version_unverified\",\n    message:\n      `Installed version ${version} is outside the range these adapters were verified against ` +\n      `(captured: ${verified.captured.join(\", \")}). It will still run; parsing or prompt ` +\n      \"detection may differ.\",\n  };\n}\n\nfunction isNewerThanAllCaptured(version: string, captured: string[]): boolean {\n  return captured.every((c) => compareSemver(version, c) > 0);\n}\n\n/**\n * Compare two semver-ish strings. Prerelease suffixes sort BELOW the same\n * release (0.140.0-alpha.19 < 0.140.0), matching semver, so a prerelease we\n * captured does not read as newer than the release it precedes.\n */\nexport function compareSemver(a: string, b: string): number {\n  const parse = (v: string) => {\n    const [core, pre] = v.split(\"-\", 2);\n    const nums = core.split(\".\").map((n) => Number.parseInt(n, 10) || 0);\n    return { nums, pre: pre ?? null };\n  };\n  const pa = parse(a);\n  const pb = parse(b);\n  for (let i = 0; i < 3; i++) {\n    const d = (pa.nums[i] ?? 0) - (pb.nums[i] ?? 0);\n    if (d !== 0) return d < 0 ? -1 : 1;\n  }\n  if (pa.pre === pb.pre) return 0;\n  if (pa.pre === null) return 1; // release > prerelease\n  if (pb.pre === null) return -1;\n  return pa.pre < pb.pre ? -1 : 1;\n}\n\n/**\n * Resolve health for one provider. `locateExe` is injected so tests can drive\n * detection without depending on what happens to be installed on the machine;\n * it defaults to the same `locateProviderExe` the session-start pre-flight\n * uses. Sharing that one entry point is load-bearing rather than tidy: it\n * clears the memoized resolution on a miss, so a health check and a start\n * attempt cannot reach opposite conclusions about the same machine — which is\n * what happened when this called `locateExecutable(resolveExe())` directly and\n * left a stale absolute path (the normal case under launchd) in place.\n */\nexport async function providerHealth(\n  name: ProviderName,\n  locateExe: () => string | null = () => locateProviderExe(name),\n  detect: (exe: string) => Promise<string | null> = runVersion,\n): Promise<ProviderHealth> {\n  const verifiedAgainst = VERIFIED_AGAINST[name];\n  const capabilities = capabilitiesFor(name);\n\n  let exe: string | null = null;\n  try {\n    // Located, not merely resolved: neither resolver can fail — each falls back\n    // to the bare command name — so this used to be gated on a throw that never\n    // happens, and `available` was true for a CLI not on the machine at all.\n    // Mobile greys a provider out on `available === false`, so the button\n    // stayed enabled and the failure only surfaced as a session that died\n    // milliseconds after starting.\n    exe = locateExe();\n  } catch {\n    exe = null;\n  }\n  if (exe === null) {\n    return {\n      name,\n      available: false,\n      version: null,\n      verifiedAgainst,\n      capabilities,\n      warnings: [\n        {\n          code: \"provider_not_found\",\n          message: `${name} could not be located. Sessions for this provider cannot start.`,\n        },\n      ],\n    };\n  }\n\n  // A version we cannot read does not prove the CLI is missing — resolveExe\n  // found it. Report it available and flag the compatibility unknown. The guard\n  // sits here rather than inside runVersion because `detect` is an injected\n  // seam: a throwing detector must degrade to \"unverified\" whichever\n  // implementation is behind it, not take GET /api/providers down with it.\n  let version: string | null = null;\n  try {\n    version = await detect(exe);\n  } catch {\n    version = null;\n  }\n  const warning = compareToVerified(version, verifiedAgainst);\n\n  return {\n    name,\n    available: true,\n    version,\n    verifiedAgainst,\n    capabilities,\n    warnings: warning ? [warning] : [],\n  };\n}\n","import {\n  CLAUDE_CODE_PROVIDER,\n  CODEX_CLI_PROVIDER,\n  CURSOR_PROVIDER,\n  canonicalizeProviderName,\n  type ProviderName,\n} from \"../../providers\";\n\n/**\n * Provider capability declarations (C2).\n * See docs/architecture/2026-07-24-provider-compatibility.md.\n *\n * Every field here is a branch that ALREADY exists somewhere in the codebase —\n * this promotes it from an implied code path to queryable data. The point is\n * that a client can ask what a provider supports instead of discovering it by\n * attempting an action and getting a 501 or a silently wrong result.\n */\nexport interface ProviderCapabilities {\n  /**\n   * How a fresh session gets its id.\n   * `explicit`   — we generate it and pass it in (Claude: `--session-id <uuid>`).\n   * `late-bound` — the CLI creates its own and we discover it afterwards\n   *                (Codex: no `--session-id` equivalent, so the rollout id is\n   *                found by watching the sessions dir after spawn).\n   */\n  freshSessionId: \"explicit\" | \"late-bound\";\n  /** Whether the CLI can replay a prior transcript from an id. */\n  resume: \"native\" | \"unsupported\";\n  /**\n   * How a system prompt reaches the CLI.\n   * `flag`       — a dedicated flag (Claude: `--system-prompt`).\n   * `positional` — passed as the opening turn (Codex has no flag).\n   */\n  systemPrompt: \"flag\" | \"positional\" | \"unsupported\";\n  /** We can detect and parse structured question menus from the TUI. */\n  structuredQuestions: boolean;\n  /** We can detect permission/trust gates and answer them programmatically. */\n  permissionGates: boolean;\n  /**\n   * We can send input to a live session. False means read-only observation —\n   * the honest state for a provider whose input path we do not understand.\n   */\n  liveControl: boolean;\n}\n\n/**\n * Provider versions an adapter's parsing and detection were captured against.\n *\n * `captured` lists the versions we hold fixtures for. `min`/`max` bound the\n * range we claim to support. A provider outside that range still runs — it just\n * reports a compatibility warning rather than pretending to be verified.\n */\nexport interface VerifiedAgainst {\n  min?: string;\n  max?: string;\n  captured: string[];\n}\n\n/**\n * Outcome of normalizing one native-history line.\n *\n * The `ignored` / `unknown` split is the reason this type exists. Today every\n * non-chat line returns null (src/utils/codexConversationLine.ts), so \"a\n * session_meta header we deliberately skip\" and \"a shape this adapter has never\n * seen\" are indistinguishable — and a provider schema change therefore renders\n * an empty conversation with no error at all.\n */\nexport type NormalizeResult =\n  /** Recognized chat content, normalized to the client-facing shape. */\n  | { kind: \"message\"; line: string }\n  /** Recognized, but deliberately not rendered (headers, dupes, injected context). */\n  | { kind: \"ignored\"; reason: string }\n  /** NOT recognized. Counted and surfaced as a compatibility signal. */\n  | { kind: \"unknown\"; raw: string; reason: string };\n\n/**\n * The descriptive half of a provider integration. `SessionRunner` (types.ts) is\n * the behavioural half — how to spawn and drive the CLI. This is what the\n * integration claims about itself, so adding a provider means implementing a\n * declared surface rather than copying a runner and hoping.\n */\nexport interface ProviderAdapter {\n  name: ProviderName;\n  capabilities: ProviderCapabilities;\n  verifiedAgainst: VerifiedAgainst;\n  /** Installed provider version, or null when it cannot be determined. */\n  detectVersion(): Promise<string | null>;\n  /** Classify one raw native-history line. Never silently discards. */\n  normalizeLine(line: string): NormalizeResult;\n}\n\n// ─── Declared capabilities ────────────────────────────────────────────\n//\n// Each value below is sourced from existing behaviour, not aspiration:\n//   Claude  — `--session-id` (pty-manager.ts), `--resume`, `--system-prompt`,\n//             OSC-777 gates + \"Enter to select\" menus (services/questions/*).\n//   Codex   — no fresh-session id (codex-pty-runner.ts: \"start() always means\n//             resume\"), `codex resume <id>`, prompt passed positionally, and\n//             trust/hooks gates detected by regex. Codex has no equivalent of\n//             Claude's AskUserQuestion menu that we parse, hence\n//             structuredQuestions: false.\n\nexport const CLAUDE_CODE_CAPABILITIES: ProviderCapabilities = {\n  freshSessionId: \"explicit\",\n  resume: \"native\",\n  systemPrompt: \"flag\",\n  structuredQuestions: true,\n  permissionGates: true,\n  liveControl: true,\n};\n\nexport const CODEX_CLI_CAPABILITIES: ProviderCapabilities = {\n  freshSessionId: \"late-bound\",\n  resume: \"native\",\n  systemPrompt: \"positional\",\n  structuredQuestions: false,\n  permissionGates: true,\n  liveControl: true,\n};\n\n// Cursor CLI (`agent`): `--resume=<chatId>` and a positional opening prompt are\n// documented. Fresh chats mint their own id (no `--session-id`). We do not yet\n// scrape its TUI for questions or permission cards, so those stay false.\nexport const CURSOR_CLI_CAPABILITIES: ProviderCapabilities = {\n  freshSessionId: \"late-bound\",\n  resume: \"native\",\n  systemPrompt: \"positional\",\n  structuredQuestions: false,\n  permissionGates: false,\n  liveControl: true,\n};\n\n/**\n * Capabilities for a provider we do not recognize.\n *\n * This is the generic-terminal fallback: stream bytes, accept input, and claim\n * no semantic understanding. It exists because the current behaviour is worse —\n * `coerceProviderForRunner` (providers.ts) silently maps an unknown provider to\n * claude-code, so an unrecognized CLI gets driven with Claude's argv, Claude's\n * markers, and Claude's env scrubbing. Admitting we don't know beats asserting\n * the wrong thing.\n */\nexport const GENERIC_TERMINAL_CAPABILITIES: ProviderCapabilities = {\n  freshSessionId: \"late-bound\",\n  resume: \"unsupported\",\n  systemPrompt: \"unsupported\",\n  structuredQuestions: false,\n  permissionGates: false,\n  liveControl: true,\n};\n\nexport function capabilitiesFor(provider: ProviderName): ProviderCapabilities {\n  switch (canonicalizeProviderName(provider) ?? provider) {\n    case CLAUDE_CODE_PROVIDER:\n      return CLAUDE_CODE_CAPABILITIES;\n    case CODEX_CLI_PROVIDER:\n      return CODEX_CLI_CAPABILITIES;\n    case CURSOR_PROVIDER:\n      return CURSOR_CLI_CAPABILITIES;\n    default:\n      return GENERIC_TERMINAL_CAPABILITIES;\n  }\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.use(\"/:id/*\", async (c, next) => {\n    if (await deps.isExcludedSubagent?.(c.req.param(\"id\") ?? \"\")) {\n      return c.json({ error: \"Session not found\" }, 404);\n    }\n    await next();\n  });\n  app.use(\"/:id\", async (c, next) => {\n    const id = c.req.param(\"id\") ?? \"\";\n    if (\n      ![\"count\", \"recents\", \"names\", \"resume\", \"start\"].includes(id) &&\n      (await deps.isExcludedSubagent?.(id))\n    ) {\n      return c.json({ error: \"Session not found\" }, 404);\n    }\n    await next();\n  });\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/raw-key\", async (c) => {\n    await deps.handleRawKey(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/prompt/answer\", async (c) => {\n    await deps.handlePromptAnswer(c.req.param(\"id\"), c.env.incoming, c.env.outgoing);\n    return alreadyHandled();\n  });\n\n  app.post(\"/:id/permission/answer\", async (c) => {\n    await deps.handlePermissionAnswer(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.patch(\"/:id/model\", async (c) => {\n    await deps.handleSetSessionModel(c.req.param(\"id\"), c.env.incoming, c.env.outgoing);\n    return alreadyHandled();\n  });\n\n  app.patch(\"/:id/effort\", async (c) => {\n    await deps.handleSetSessionEffort(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/fork\", async (c) => {\n    await deps.handleFork(c.req.param(\"id\"), c.env.incoming, c.env.outgoing);\n    return alreadyHandled();\n  });\n\n  app.post(\"/:id/stop\", async (c) => {\n    const when = c.req.query(\"when\") === \"idle\" ? \"idle\" : \"now\";\n    const ignoreWatchers = c.req.query(\"ignoreWatchers\") === \"true\";\n    const deleteConversation = c.req.query(\"delete\") === \"true\";\n    await deps.handleStopSession(c.req.param(\"id\"), c.env.outgoing, {\n      when,\n      ignoreWatchers,\n      delete: deleteConversation,\n    });\n    return alreadyHandled();\n  });\n\n  app.post(\"/:id/kill\", async (c) => {\n    const deleteConversation = c.req.query(\"delete\") === \"true\";\n    await deps.handleKillSession(c.req.param(\"id\"), c.env.outgoing, { delete: deleteConversation });\n    return alreadyHandled();\n  });\n\n  app.get(\"/:id\", async (c) => {\n    await deps.handleGetSession(c.req.param(\"id\"), c.env.outgoing);\n    return alreadyHandled();\n  });\n\n  return app;\n};\n","import { createNodeWebSocket } from \"@hono/node-ws\";\nimport type { Hono as HonoApp } from \"hono\";\nimport { Hono } from \"hono\";\nimport type { UpgradeWebSocket } from \"hono/ws\";\nimport type { IncomingMessage, Server } from \"http\";\nimport type { WebSocket } from \"ws\";\nimport { contextRegistry, E2EE_WS_SUBPROTOCOL, refuseUnsealedIfPinned } from \"../../e2ee/context\";\nimport { getLogger } from \"../../logger\";\nimport type { AppEnv } from \"../app\";\nimport type { ApiDeps } from \"../types/api-deps\";\n\nconst log = getLogger(\"ws\");\n\nexport const createWsRoutes = (deps: ApiDeps, upgradeWebSocket: UpgradeWebSocket<WebSocket>) => {\n  const app = new Hono<AppEnv>();\n\n  app.get(\n    \"/ws\",\n    // The downgrade refusal runs here, BEFORE `upgradeWebSocket` — once that\n    // middleware has seen `Upgrade: websocket` it answers 101 unconditionally,\n    // and a socket promoted and then closed is not the same answer as a 426 the\n    // client can act on.\n    //\n    // The ticket itself was spent one layer up: `authMiddleware` consumes it\n    // and sets both the principal and the context, because a ticketed upgrade\n    // authenticates BY the ticket and carries no `Authorization` (§13). By the\n    // time this runs, `e2eeContext` is either a live context or nothing.\n    async (c, next) => {\n      // W1a's guard, called and not re-implemented — two copies of a downgrade\n      // rule is one copy that can be forgotten. A pinned device that turns up\n      // with a bearer or `?key=` and no ticket gets 426, never a plaintext\n      // socket.\n      //\n      // **Its stated limit applies here and is not this PR's to close.** The\n      // pin is per DEVICE, so a pinned phone presenting the SHARED api key\n      // resolves to the `legacy` principal with no device row, and the pin\n      // cannot bite. That is the stage-3 shared-key problem.\n      const refusal = refuseUnsealedIfPinned({\n        principal: c.get(\"principal\"),\n        devicesRepo: deps.devicesRepo(),\n        context: c.get(\"e2eeContext\"),\n      });\n      if (refusal) {\n        log.warn(\"[e2ee.upgrade_refused] pinned device presented no sealed context\", {\n          event: \"e2ee.upgrade_refused\",\n        });\n        return c.json(refusal.body, refusal.status);\n      }\n      await next();\n    },\n    // The principal is read here, at the upgrade, and captured for the life of\n    // the socket. authMiddleware sets it because /ws is classified\n    // `history:read`, but it only ever reaches the HTTP request — without\n    // capturing it the socket has no principal at all, so every frame after\n    // the upgrade is unauthorized-by-omission.\n    upgradeWebSocket((c) => {\n      const principal = c.get(\"principal\") ?? null;\n      const context = c.get(\"e2eeContext\");\n      let openWs: WebSocket | null = null;\n      return {\n        onOpen(_evt, ws) {\n          const raw = ws.raw;\n          if (!raw) {\n            // **No orphan without a deadline clock.** The context was attached\n            // by `authMiddleware` and its ticket is spent, but without a raw\n            // socket `addClient` never runs — so nothing arms the 10 s\n            // first-frame deadline, and the one mechanism that collects an\n            // unproven context does not exist for this one. It would sit for\n            // its full lifetime. Every other path that abandons a consumed\n            // context destroys it; this one was the exception.\n            if (context) contextRegistry().destroy(context.ctxId);\n            return;\n          }\n          openWs = raw;\n          deps.handleWsOpen(raw, context);\n        },\n        onMessage(evt, _ws) {\n          if (openWs) deps.handleWsMessage(openWs, evt.data, principal);\n        },\n        onClose(_evt, _ws) {\n          if (openWs) deps.handleWsClose(openWs);\n        },\n      };\n    }),\n  );\n\n  return app;\n};\n\n/**\n * Ceiling on ONE client→server frame, enforced BEFORE the frame is allocated\n * (NONCE-DESIGN §10).\n *\n * Everything a client sends is a control message — `register`,\n * `subscribe_session`, `unsubscribe_session`, `hold_session` — none of which\n * reaches a kilobyte, so this is generous by three orders of magnitude and\n * still small enough that a socket without keys cannot make the process buffer\n * anything worth buffering.\n *\n * The server→client direction is bounded by the CLIENT, which is the only side\n * that can refuse a frame before allocating it; `terminal_replay` is the large\n * one and it is capped upstream by `REPLAY_MAX_LINES`.\n */\nexport const WS_MAX_CLIENT_FRAME_BYTES = 64 * 1024;\n\n/**\n * Strip inherited properties before @hono/node-ws copies raw Node headers with\n * `for...in`. Only bytes Node parsed from the request may reach authentication.\n */\nfunction keepOwnUpgradeHeaders(request: IncomingMessage): void {\n  const headers = Object.create(null) as typeof request.headers;\n  for (const key of Object.keys(request.headers)) headers[key] = request.headers[key];\n  request.headers = headers;\n}\n\n/**\n * Mount `/ws` on an app that is already built, and bind the frame ceiling.\n *\n * **This exists so the bound is wired in exactly one place.** §10 states the\n * gap plainly: `@hono/node-ws` constructs its `WebSocketServer` with\n * `{ noServer: true }` and nothing else, so `ws`'s 100 MiB `maxPayload` default\n * applies and a frame is fully assembled before any record-layer check runs —\n * anyone holding a socket without keys, a ticket thief or a legacy `?key=`\n * client, could push 100 MiB. §10 offers two ways out: build our own server\n * with a bound, or accept the ceiling and reap silent sockets.\n *\n * This is the first, without forking the upgrade path. `@hono/node-ws` RETURNS\n * the server it made, and `ws` reads `this.options.maxPayload` per upgrade in\n * `completeUpgrade` rather than at construction — so lowering it here bounds\n * every socket from the next one onwards. That is the whole \"construct your own\n * server\" outcome in one assignment, with no copy of the upgrade handling to\n * keep in sync. It is receiver-only: `maxPayload` bounds what `ws` will ACCEPT\n * and never what it will send, which is exactly the per-direction split §10\n * asks for.\n *\n * The cost is reaching into a dependency's `options`. That is why this is one\n * function called by both `server.ts` and the sealing tests rather than two\n * copies: a test that stood the wiring up itself would be asserting on its own\n * copy of it.\n */\nexport function mountWebSocket(\n  app: HonoApp<AppEnv>,\n  httpServer: Server,\n  deps: ApiDeps,\n): { wss: ReturnType<typeof createNodeWebSocket>[\"wss\"] } {\n  const { wss, injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app });\n  wss.options.maxPayload = WS_MAX_CLIENT_FRAME_BYTES;\n  // Same per-upgrade read, same reach: `ws` selects the FIRST offered\n  // subprotocol unless `handleProtocols` is set, and a browser offering\n  // `threadbase-e2ee-v1, tb-ticket.<ticket>` in the other order would get its\n  // ticket echoed in the 101. Only the one protocol this server speaks can be\n  // selected; `authMiddleware` refuses any offer of it that no ticket\n  // authenticated, so selecting it is never a claim about a plaintext socket.\n  wss.options.handleProtocols = (offered) =>\n    offered.has(E2EE_WS_SUBPROTOCOL) ? E2EE_WS_SUBPROTOCOL : false;\n  app.route(\"/\", createWsRoutes(deps, upgradeWebSocket));\n  httpServer.prependListener(\"upgrade\", keepOwnUpgradeHeaders);\n  injectWebSocket(httpServer);\n  return { wss };\n}\n","import {\n  applyIncludeFilter,\n  applyPagination,\n  applyProjectFilter,\n  applySort,\n  type Conversation,\n  type ConversationMeta,\n  type SearchMatch,\n  type SortOrder,\n  search,\n} from \"@threadbase-sh/scanner\";\nimport { createReadStream, existsSync, readdirSync } from \"fs\";\nimport type { IncomingMessage, ServerResponse } from \"http\";\nimport { basename, join } from \"path\";\nimport { createInterface } from \"readline\";\nimport { ConversationCache } from \"../../conversation-cache\";\nimport type { LiveSessionManager } from \"../../live-session-manager\";\nimport type { Logger } from \"../../logger\";\nimport {\n  CLAUDE_CODE_PROVIDER,\n  CODEX_CLI_PROVIDER,\n  canonicalizeProviderName,\n  coerceProviderForRunner,\n  isProviderResumable,\n} from \"../../providers\";\nimport type { ScannerManager, ScanProfile } from \"../../scanner-manager\";\nimport { classifyConversationFile } from \"../../services/conversations/classification\";\nimport {\n  findSearchTarget,\n  type SearchableMessage,\n} from \"../../services/conversations/findSearchTarget\";\nimport {\n  type InheritedHistory,\n  resolveInheritedHistory,\n} from \"../../services/conversations/inheritedHistory\";\nimport { deriveProjectChatTitle } from \"../../services/projectChats/deriveProjectChatTitle\";\nimport {\n  applyFilters,\n  type ParsedSearchQuery,\n  paginate,\n  parseSearchQuery,\n  SearchQueryError,\n} from \"../../services/search/searchQuery\";\nimport type { SessionStore } from \"../../session-store\";\nimport { TRANSCRIPT_WATCH_DEADLINE_MS } from \"../../session-watchers\";\nimport type { ManagedSession, ServerWarmupState } from \"../../types\";\nimport { toolResultText } from \"../../utils/claudeShapedLine\";\nimport { isLeadingInjectedContext } from \"../../utils/codexConversationLine\";\nimport { computeConversationEtag } from \"../../utils/conversationEtag\";\nimport { createScanProgressThrottle } from \"../../utils/scanProgressThrottle\";\nimport type { WSHub } from \"../../ws-hub\";\nimport {\n  boolQueryParam,\n  classifyResumability,\n  includeQueryParam,\n  intParam,\n  json,\n  readBody,\n} from \"./http-helpers\";\n\n// Search filters are applied after the scanner returns, so we ask it for more\n// than one page. The multiplier bounds the common case; SEARCH_MAX_SCAN caps a\n// broad query so it cannot pull an unbounded result set into memory.\nconst SEARCH_OVERFETCH = 4;\nconst SEARCH_MAX_SCAN = 1000;\n\n// Ceiling on the `max_bytes` page budget. The number a client asks for is its\n// own business — this only stops a typo'd extra zero from turning into a\n// multi-hundred-MB response. Above the largest conversation observed locally\n// (22.8 MB), so it clamps mistakes rather than legitimate requests.\nconst MAX_BYTES_CEILING = 32 * 1024 * 1024;\n\ninterface SnakeConversationMeta {\n  profile_id?: string;\n  project_name?: string;\n  session_name?: string;\n  project_path?: string;\n  file_path?: string;\n  last_updated_at?: string;\n  message_count?: number;\n}\n\n/**\n * The conversation detail `meta` predates the camelCase wire and names the same\n * concepts differently from `SessionResponse` (`profile_id` vs `account`,\n * `last_updated_at` vs `lastActivityAt`, ...). Released mobile builds read the\n * snake_case keys, so those stay; the camelCase names ride alongside so a\n * client can read one field set from either endpoint.\n */\nfunction withCamelMetaAliases<T extends SnakeConversationMeta>(meta: T) {\n  return {\n    ...meta,\n    account: meta.profile_id,\n    projectName: meta.project_name,\n    sessionName: meta.session_name,\n    projectPath: meta.project_path,\n    filePath: meta.file_path,\n    lastActivityAt: meta.last_updated_at,\n    messageCount: meta.message_count,\n  };\n}\n\n/**\n * Everything ConversationHandlers reads from the server. Collaborators\n * constructed once in the server constructor are passed by reference; anything\n * bound later (the cache opens during listen() and is rebound by the integrity\n * monitor's reset-and-rescan) or swapped by tests (`log`) is a thunk, for the\n * same reason ApiDeps passes `cache: () => ConversationCache | null`.\n *\n * The warm-up gate, the cache-write tracker and the three conversation-id\n * resolvers stay late-bound calls back into the server rather than moved code:\n * they read state (`activeWarmups`, `inFlightCacheWrites`, `sessionFileMap`)\n * that spans well beyond conversations.\n */\nexport type ConversationHandlersDeps = {\n  includeSubagentSessions?: () => boolean;\n  scannerManager: ScannerManager;\n  sessionStore: SessionStore;\n  ptyManager: LiveSessionManager;\n  wsHub: WSHub;\n  scanProfiles: ScanProfile[] | undefined;\n  cache: () => ConversationCache | null;\n  log: () => Logger;\n  rejectIfWarmingUp: (res: ServerResponse) => boolean;\n  withWarmup: <T>(state: ServerWarmupState, operation: () => Promise<T>) => Promise<T>;\n  trackCacheWrite: (task: Promise<unknown>) => void;\n  resolveConversationLookupId: (uuid: string) => string;\n  findLiveSessionFilePath: (uuid: string) => string | null;\n  isBoundConversationLive: (boundId: string) => boolean;\n};\n\n/**\n * The conversation read surface: listing, counting, project summaries, the\n * single-conversation detail fetch (pagination + ETag + stale-while-revalidate)\n * and search — plus the JSONL/cwd resolvers the resume and adopt paths share.\n *\n * Extracted from StreamerServer so conversation work stops editing the server\n * file (see docs/plans/2026-07-12-server-ts-split.md, PR 4). State stays on the\n * server: this class only reads it through `deps`.\n */\nexport class ConversationHandlers {\n  private publicScannerMeta(\n    meta: ConversationMeta,\n    include?: \"all\" | \"conversations\" | \"subagents\",\n  ): ConversationMeta | null {\n    const id = this.cache?.getIdByFilePath(meta.filePath);\n    const row = id ? this.cache?.getMetaById(id) : null;\n    if (row) return this.cache?.isVisible(row.id) ? { ...meta, sessionId: row.id } : null;\n    try {\n      const classification = classifyConversationFile(meta.filePath, meta.provider);\n      const showSubagents =\n        include === \"all\" ||\n        include === \"subagents\" ||\n        (this.deps.includeSubagentSessions?.() ?? false);\n      if (!classification.hasMessages || (classification.isSubagent && !showSubagents)) return null;\n      return {\n        ...meta,\n        sessionId:\n          classification.isSubagent || classification.provider === CODEX_CLI_PROVIDER\n            ? classification.id\n            : meta.sessionId,\n      };\n    } catch {\n      return meta;\n    }\n  }\n  async isExcludedSubagent(id: string): Promise<boolean> {\n    if (this.deps.includeSubagentSessions?.() ?? this.cache?.includeSubagentSessions ?? false)\n      return false;\n    const lookupId = this.deps.resolveConversationLookupId(id);\n    if (this.sessionStore.getManaged(id)?.isSubagent) return true;\n    const meta = this.cache?.getMetaById(lookupId);\n    if (meta?.isSubagent != null) return meta.isSubagent;\n    const filePath = meta?.filePath ?? (await this.locateJsonlPath(id, lookupId));\n    if (!filePath) return false;\n    try {\n      const classification = this.cache\n        ? this.cache.reconcileClassification(filePath)\n        : classifyConversationFile(filePath, meta?.provider);\n      return classification?.isSubagent ?? false;\n    } catch {\n      return false;\n    }\n  }\n  constructor(private deps: ConversationHandlersDeps) {}\n\n  private get scannerManager(): ScannerManager {\n    return this.deps.scannerManager;\n  }\n\n  private get sessionStore(): SessionStore {\n    return this.deps.sessionStore;\n  }\n\n  private get ptyManager(): LiveSessionManager {\n    return this.deps.ptyManager;\n  }\n\n  private get wsHub(): WSHub {\n    return this.deps.wsHub;\n  }\n\n  private get scanProfiles(): ScanProfile[] | undefined {\n    return this.deps.scanProfiles;\n  }\n\n  private get cache(): ConversationCache | null {\n    return this.deps.cache();\n  }\n\n  private get log(): Logger {\n    return this.deps.log();\n  }\n\n  private listFilters(url: URL) {\n    return {\n      project: url.searchParams.get(\"project\") ?? undefined,\n      provider: url.searchParams.get(\"provider\") ?? undefined,\n      include: includeQueryParam(url),\n      isImportedFromClaude: boolQueryParam(url, \"isImportedFromClaude\"),\n      isImportedFromCodex: boolQueryParam(url, \"isImportedFromCodex\"),\n      isImportedFromCursor: boolQueryParam(url, \"isImportedFromCursor\"),\n    };\n  }\n\n  private importFlags(c: {\n    isImportedFromClaude?: boolean;\n    isImportedFromCodex?: boolean;\n    isImportedFromCursor?: boolean;\n  }) {\n    return {\n      isImportedFromClaude: c.isImportedFromClaude === true,\n      isImportedFromCodex: c.isImportedFromCodex === true,\n      isImportedFromCursor: c.isImportedFromCursor === true,\n    };\n  }\n\n  private matchesImportFilters(\n    meta: ConversationMeta,\n    filters: ReturnType<ConversationHandlers[\"listFilters\"]>,\n  ): boolean {\n    if (\n      filters.isImportedFromClaude !== undefined &&\n      Boolean(meta.isImportedFromClaude) !== filters.isImportedFromClaude\n    ) {\n      return false;\n    }\n    if (\n      filters.isImportedFromCodex !== undefined &&\n      Boolean(meta.isImportedFromCodex) !== filters.isImportedFromCodex\n    ) {\n      return false;\n    }\n    if (\n      filters.isImportedFromCursor !== undefined &&\n      Boolean(meta.isImportedFromCursor) !== filters.isImportedFromCursor\n    ) {\n      return false;\n    }\n    return true;\n  }\n\n  async handleListConversations(url: URL, res: ServerResponse): Promise<void> {\n    if (this.deps.rejectIfWarmingUp(res)) return;\n\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 filters = this.listFilters(url);\n    const bustCache = url.searchParams.get(\"refresh\") === \"1\";\n\n    // Reconcile (not wipe): fullRescan bypasses the scanner dir-mtime gate, then\n    // upsert what exists and drop rows whose files are gone. Triggered by\n    // explicit ?refresh=1, directory-watcher scannerStale, or HDD freshness drift.\n    //\n    // Never block the response on a routine reconcile. A full rescan takes\n    // seconds on a large history, and active sessions trip scannerStale on\n    // every JSONL append — so awaiting here stalls the list on every write.\n    // Serve what is cached now and revalidate on disk in the background; the\n    // next poll sees the fresh data (stale-while-revalidate). Block only when\n    // there is nothing to serve (cold cache) or the caller asked for fresh data\n    // explicitly with ?refresh=1.\n    const reconcileMode = this.scannerManager.reconcileMode();\n    if (this.cache && (bustCache || reconcileMode)) {\n      const warm = this.cache.listConversations({ limit: 0, offset: 0 }).total > 0;\n      if (warm && !bustCache) {\n        this.scannerManager.startBackgroundReconcile(reconcileMode ?? \"full\");\n      } else {\n        // Cold cache or explicit refresh: await the rescan. Emit throttled\n        // scan_progress so the client renders a live progress bar during the\n        // wait instead of a frozen one; the routine background path above stays\n        // silent (no onProgress) so normal-use polls never flicker a bar.\n        const shouldEmitProgress = createScanProgressThrottle();\n        const rescan = () =>\n          this.scannerManager.reconcileFromDisk((scanned, total) => {\n            if (shouldEmitProgress(scanned, total)) {\n              this.wsHub.broadcast({ type: \"scan_progress\", scanned, total });\n            }\n          });\n        // Gate with the warm-up state only when the cache is cold: there is\n        // nothing to serve, so the client shows the one-time \"building history\"\n        // screen instead of an empty list. The gate is server-wide, so raising\n        // it for an explicit refresh on a warm cache would 503 every other gated\n        // route (e.g. /api/sessions) for the length of the rescan.\n        if (warm) await rescan();\n        else await this.deps.withWarmup(\"conversation_refresh\", rescan);\n      }\n    }\n\n    if (this.cache) {\n      const { conversations, total } = this.cache.listConversations({\n        ...filters,\n        limit,\n        offset,\n      });\n      const adapted = conversations.map((c) => ({\n        id: c.id,\n        isSubagent: c.isSubagent,\n        parentConversationId: c.parentConversationId,\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        lastActivityAt: 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: canonicalizeProviderName(c.provider) ?? c.provider ?? CLAUDE_CODE_PROVIDER,\n        ...this.importFlags(c),\n      }));\n      json(res, 200, { conversations: adapted, hasMore: offset + limit < total, offset, total });\n      return;\n    }\n\n    const scanner = await this.scannerManager.get();\n    let metas = [...scanner.getMetadataCache().values()];\n    const include =\n      filters.include ??\n      ((this.deps.includeSubagentSessions?.() ?? false) ? \"all\" : \"conversations\");\n    metas = metas.flatMap((meta) => this.publicScannerMeta(meta, include) ?? []);\n    metas = applyIncludeFilter(metas, include);\n    if (filters.project) metas = applyProjectFilter(metas, filters.project);\n    if (filters.provider) {\n      const wanted = canonicalizeProviderName(filters.provider) ?? filters.provider;\n      metas = metas.filter(\n        (m) =>\n          (canonicalizeProviderName(m.provider) ?? m.provider ?? CLAUDE_CODE_PROVIDER) === wanted,\n      );\n    }\n    metas = metas.filter((m) => this.matchesImportFilters(m, filters));\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        isSubagent: c.isSubagent,\n        parentConversationId: c.parentSessionId ?? undefined,\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        lastActivityAt: c.timestamp,\n        firstMessage: c.firstMessage ?? undefined,\n        lastMessage: c.lastMessage ?? undefined,\n        model: c.model ?? undefined,\n        provider: canonicalizeProviderName(c.provider) ?? c.provider ?? CLAUDE_CODE_PROVIDER,\n        ...this.importFlags(c),\n      };\n    });\n    json(res, 200, { conversations: adapted, hasMore: offset + limit < total, offset, total });\n  }\n\n  async handleConversationsCount(url: URL, res: ServerResponse): Promise<void> {\n    if (this.deps.rejectIfWarmingUp(res)) return;\n\n    const filters = this.listFilters(url);\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        ...filters,\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.scannerManager.get(true);\n    let metas = [...scanner.getMetadataCache().values()];\n    const include =\n      filters.include ??\n      ((this.deps.includeSubagentSessions?.() ?? false) ? \"all\" : \"conversations\");\n    metas = metas.flatMap((meta) => this.publicScannerMeta(meta, include) ?? []);\n    metas = applyIncludeFilter(metas, include);\n    if (filters.project) metas = applyProjectFilter(metas, filters.project);\n    if (filters.provider) {\n      const wanted = canonicalizeProviderName(filters.provider) ?? filters.provider;\n      metas = metas.filter(\n        (m) =>\n          (canonicalizeProviderName(m.provider) ?? m.provider ?? CLAUDE_CODE_PROVIDER) === wanted,\n      );\n    }\n    metas = metas.filter((m) => this.matchesImportFilters(m, filters));\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    // Gate only a cold cache, as handleListConversations does: the gate is\n    // server-wide, and a warm cache has data every other route can serve.\n    const warm = (this.cache?.listConversations({ limit: 0, offset: 0 }).total ?? 0) > 0;\n    const rescan = async () => {\n      try {\n        const scanner = await this.scannerManager.getFresh();\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    // Tracked so close() awaits this scan→cache-write before closing cache.db.\n    this.deps.trackCacheWrite(\n      warm ? rescan() : this.deps.withWarmup(\"conversation_refresh\", rescan),\n    );\n  }\n\n  handleGetRecentSessions(url: URL, res: ServerResponse): void {\n    if (this.deps.rejectIfWarmingUp(res)) return;\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      isSubagent: c.isSubagent,\n      parentConversationId: c.parentConversationId,\n      status: \"idle\" as const,\n      ownership: \"historical\" 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  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  handleGetProjectSummaries(url: URL, res: ServerResponse): void {\n    if (this.deps.rejectIfWarmingUp(res)) return;\n    const limit = intParam(url, \"limit\", 200);\n    const offset = intParam(url, \"offset\", 0);\n    if (!this.cache) {\n      // Deliberately not an empty 200: without the cache /api/conversations\n      // falls back to the scanner and still returns rows, so \"no projects\"\n      // would be a lie mobile draws an empty group tree from.\n      json(res, 503, {\n        error: \"Conversation cache unavailable\",\n        code: \"CACHE_UNAVAILABLE\",\n      });\n      return;\n    }\n    const { projects, total } = this.cache.listProjectSummaries({ limit, offset });\n    json(res, 200, { projects, total, offset, hasMore: offset + projects.length < total });\n  }\n\n  findJsonlPath(uuid: string): string | null {\n    const filename = `${uuid}.jsonl`;\n    for (const projectsDir of this.scannerManager.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  /**\n   * Resolve a conversation id to a JSONL path, in order of authority.\n   *\n   * `findJsonlPath` alone answers 64.0% of this machine's 961 conversations and\n   * 0 of 343 Codex ones — it reconstructs `<projectsDir>/<dir>/<uuid>.jsonl`,\n   * which is Claude Code's layout, and a Codex rollout is\n   * `rollout-<ts>-<uuid>.jsonl` under a date path, so that walk cannot match one\n   * by construction.\n   *\n   * That made the cache row the ONLY rung a Codex conversation could use, which\n   * is why the scanner-index rung exists: measured 2026-09-04, 3 of the 50 ids\n   * `GET /api/conversations` was serving 404'd here — every one a Codex rollout\n   * present on disk, listed from the scanner index, with no cache row left. The\n   * ladder's old \"99.7%, only the file-is-gone case remains\" held only while\n   * every Codex row still had its cache entry.\n   */\n  async locateJsonlPath(uuid: string, lookupId: string): Promise<string | null> {\n    // A live PTY owns its file; nothing on disk is more current.\n    const live =\n      this.deps.findLiveSessionFilePath(uuid) ?? this.deps.findLiveSessionFilePath(lookupId);\n    if (live) return live;\n\n    // The path the cache already recorded. Verified rather than trusted: 49 of\n    // 961 rows on one machine pointed at a subagent transcript OF the\n    // conversation instead of the conversation, so trusting this outright would\n    // serve a 56-message sidechain as a 1307-message conversation.\n    const cached = this.cache?.getMetaById(lookupId)?.filePath;\n    if (cached && (await this.isJsonlPathFor(cached, lookupId))) return cached;\n\n    // The scanner's own metadata index — literally the source the conversation\n    // LIST reads (`getMetadataCache()`, used at handleListConversations). Without\n    // this rung the list and the detail disagree: the list offers an id whose\n    // parsed snapshot `getConversation` never built, and neither the cache row\n    // (dropped) nor the Claude-layout walk (wrong shape) can name its file. Same\n    // verification as the cached path; reading `current` never triggers a scan.\n    const indexed = this.scannerManager.current?.getMetadataCache().get(lookupId)?.filePath;\n    if (indexed && (await this.isJsonlPathFor(indexed, lookupId))) return indexed;\n\n    // Codex and Cursor index keys are file paths (`ConversationMeta.id`), while\n    // the client looks up the session uuid. The map above misses that spelling;\n    // sessionId lookup is how the scanner itself resolves `getConversation(uuid)`.\n    const bySession =\n      this.scannerManager.current?.getConversationsBySessionId?.(lookupId)?.[0]?.filePath;\n    if (bySession && (await this.isJsonlPathFor(bySession, lookupId))) return bySession;\n\n    // Claude-layout directory walk, kept as the self-heal for ids the cache\n    // never learned about — and for the 49 above, where it happens to be right.\n    return this.findJsonlPath(lookupId);\n  }\n\n  /**\n   * Does `filePath` actually hold the conversation `requestedId` names?\n   *\n   * The filename settles it for every JSONL provider: Claude writes\n   * `<uuid>.jsonl` (or `agent-<agentId>.jsonl`), Codex writes\n   * `rollout-<ts>-<uuid>.jsonl`, Cursor writes `<runId>.jsonl` under\n   * `agent-transcripts/`.\n   * Only when the name says nothing do we open the file — and there the naive\n   * rule is wrong, because **a Claude subagent transcript carries the PARENT's\n   * `sessionId`**. Matching on `sessionId` alone therefore verifies exactly the\n   * file this check exists to reject, so a sidechain is refused outright unless\n   * it was asked for by its own `agent-<agentId>` name, which the filename\n   * branch above already covers.\n   */\n  async isJsonlPathFor(filePath: string, requestedId: string): Promise<boolean> {\n    if (!existsSync(filePath)) return false;\n    const stem = basename(filePath).replace(/\\.jsonl$/, \"\");\n    if (stem === requestedId || stem.includes(requestedId)) return true;\n\n    const first = await this.readFirstJsonlEntry(filePath);\n    if (!first || first.isSidechain === true) return false;\n    return first.sessionId === requestedId;\n  }\n\n  /** First parseable JSONL line, for identity checks. Null on an empty or unreadable file. */\n  async readFirstJsonlEntry(\n    filePath: string,\n  ): Promise<{ sessionId?: string; isSidechain?: boolean } | null> {\n    return new Promise((resolve) => {\n      const rl = createInterface({ input: createReadStream(filePath), crlfDelay: Infinity });\n      let done = false;\n      rl.on(\"line\", (line) => {\n        if (done) return;\n        try {\n          const entry = JSON.parse(line);\n          done = true;\n          rl.close();\n          resolve(entry as { sessionId?: string; isSidechain?: boolean });\n        } catch {\n          // skip malformed lines\n        }\n      });\n      rl.on(\"close\", () => {\n        if (!done) resolve(null);\n      });\n      rl.on(\"error\", () => resolve(null));\n    });\n  }\n\n  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  async findConversationByUuid(uuid: string): Promise<Conversation | null> {\n    const lookupId = this.deps.resolveConversationLookupId(uuid);\n\n    // Cold-start fast path: until the warm-up scan has populated this.scannerManager.current\n    // (this.scannerManager.ready 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.scannerManager.ready && !this.scanProfiles) {\n      const filePath = await this.locateJsonlPath(uuid, lookupId);\n      if (filePath) {\n        const account = this.cache?.getMetaById(lookupId)?.account ?? undefined;\n        const coldScanner = this.scannerManager.current ?? this.scannerManager.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.scannerManager.get(true);\n    const child = this.cache?.getMetaById(lookupId);\n    if (child?.isSubagent && child.filePath) {\n      const page = await scanner.parseSingleFilePage(child.filePath, child.account ?? undefined, {\n        limit: Number.MAX_SAFE_INTEGER,\n      });\n      return page?.conversation ? { ...page.conversation, sessionId: lookupId } : null;\n    }\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.deps.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.deps.findLiveSessionFilePath(uuid) ??\n          this.deps.findLiveSessionFilePath(lookupId) ??\n          fromIndex.filePath ??\n          null;\n        const isCodexLive =\n          this.deps.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 refreshFileForRead, 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.scannerManager.isConversationSnapshotStale(fromIndex)) {\n        const filePath = fromIndex.filePath;\n        this.deps.trackCacheWrite(\n          this.scannerManager.refreshFileForRead(scanner, filePath).catch((err: unknown) => {\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 = await this.locateJsonlPath(uuid, lookupId);\n    if (!filePath) return null;\n\n    // Mid-full-rescan (or any in-flight scannerReady): do NOT discard the live\n    // scanner and kick a competing getScanner() — that races the shadow rebuild\n    // in rescanForRefresh. Parse just this one file the same way the cold-start\n    // path does (#368).\n    if (this.scannerManager.ready) {\n      const account = this.cache?.getMetaById(lookupId)?.account ?? undefined;\n      const singleFileScanner = this.scannerManager.current ?? this.scannerManager.newScanner();\n      try {\n        const page = await singleFileScanner.parseSingleFilePage(filePath, account, {\n          limit: Number.MAX_SAFE_INTEGER,\n        });\n        if (page?.conversation) return page.conversation;\n      } catch (err) {\n        this.log.warn(\"detail.single_file_parse_failed\", {\n          event: \"detail.single_file_parse_failed\",\n          conversationId: lookupId,\n          filePath,\n          err,\n        });\n      }\n      return null;\n    }\n\n    this.scannerManager.invalidate();\n    const freshScanner = await this.scannerManager.get();\n    return freshScanner.getConversation(lookupId);\n  }\n\n  /**\n   * The 200 body for a session that exists but has written no transcript yet.\n   *\n   * Same shape as the cache-tail fallback in handleGetConversation, with an\n   * empty message list — a client cannot tell \"no turns yet\" from \"a\n   * conversation that happens to be empty\", which is the point: both are a\n   * working session with nothing to show, and neither is an error. There is no\n   * `file_path` on purpose; the file does not exist yet.\n   */\n  private emptyConversationPayload(id: string, session: ManagedSession) {\n    const provider = coerceProviderForRunner(session.provider);\n    const availability = classifyResumability(session.projectPath);\n    return {\n      meta: withCamelMetaAliases({\n        id,\n        profile_id: session.account ?? undefined,\n        project_name: session.projectName,\n        session_name: session.sessionName ?? undefined,\n        project_path: session.projectPath,\n        last_updated_at: (session.lastActivityAt ?? session.startedAt).toISOString(),\n        message_count: 0,\n        provider,\n        resumable: isProviderResumable(provider, availability.resumable),\n        ...(availability.unavailable_reason && {\n          unavailable_reason: availability.unavailable_reason,\n        }),\n      }),\n      messages: [] as unknown[],\n      message_pagination: {\n        total: 0,\n        before_index: 0,\n        from_index: 0,\n        has_more_older: false,\n        next_before_index: null,\n      },\n    };\n  }\n\n  /**\n   * Is this conversation a Codex rollout?\n   *\n   * Two callers, both needing the same fact for different reasons: only Codex\n   * writes a fork link, and only Codex numbers its offset index in a different\n   * space than this handler serves (see the gate in handleGetConversation). A\n   * conversation GET is a hot path, so answering from the cache row keeps every\n   * Claude request from paying an open+read of a first line that can never\n   * contain a link.\n   */\n  private isCodexConversation(conversationId: string, filePath: string): boolean {\n    const provider = this.cache?.getMetaById(\n      this.deps.resolveConversationLookupId(conversationId),\n    )?.provider;\n    if (provider) return provider === CODEX_CLI_PROVIDER;\n    // No cache row yet — a rollout bound seconds ago is exactly that case, and\n    // it is the one this feature exists for. Fall back to the filename, which\n    // is the same signal `isJsonlPathFor` reads: Codex writes\n    // `rollout-<ts>-<uuid>.jsonl`, Claude writes `<uuid>.jsonl`.\n    return basename(filePath).startsWith(\"rollout-\");\n  }\n\n  /**\n   * The stand-in for a conversation whose own file holds no messages yet but\n   * which inherits a history (a fresh fork). Carries the identity fields the\n   * response meta needs; `messages` stays empty because the prefix is merged in\n   * at the filter step, where the served index space is decided.\n   */\n  private shellConversationForInherited(\n    id: string,\n    filePath: string,\n    inherited: InheritedHistory,\n  ): Conversation & { provider: string } {\n    const session =\n      this.sessionStore.getManaged(id) ??\n      this.sessionStore.listManaged().find((s) => s.boundConversationId === id);\n    const meta = this.cache?.getMetaById(id);\n    const newest = inherited.messages.at(-1)?.timestamp;\n    return {\n      id,\n      filePath,\n      account: session?.account ?? meta?.account ?? \"\",\n      projectPath: session?.projectPath ?? meta?.projectPath ?? \"\",\n      projectName: session?.projectName ?? meta?.projectName ?? \"\",\n      sessionName: session?.sessionName ?? meta?.title ?? \"\",\n      sessionId: id,\n      messages: [],\n      fullText: \"\",\n      messageCount: 0,\n      timestamp: inherited.forkedAt ?? newest ?? new Date().toISOString(),\n      provider: CODEX_CLI_PROVIDER,\n    };\n  }\n\n  async handleGetConversation(\n    id: string,\n    url: URL,\n    res: ServerResponse,\n    ifNoneMatch?: string,\n  ): Promise<void> {\n    if (this.deps.rejectIfWarmingUp(res)) return;\n\n    if (await this.isExcludedSubagent(id)) {\n      json(res, 404, { error: \"Conversation not found\" });\n      return;\n    }\n\n    // Soft-deleted: the JSONL may still be on disk (the scanner would happily\n    // find it below), so this has to reject before any of that runs — a\n    // cache-row check alone wouldn't stop the scanner fallback from serving\n    // deleted content straight off the file.\n    if (this.cache?.getMetaById(id)?.deletedAt) {\n      json(res, 404, { error: \"Conversation not found\" });\n      return;\n    }\n\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    // A conversation can inherit its opening history from another file — see\n    // inheritedHistory.ts. Resolved from the file itself (one line for the\n    // common case: not a fork, and cached per path after that), never from our\n    // session records, so a fork the user made in their own terminal reads the\n    // same as one we started.\n    //\n    // Resolved before findConversationByUuid, but that order is no longer\n    // load-bearing and the next await added here is free to break it. It was:\n    // findConversationByUuid fires a stale-while-revalidate refresh in the\n    // background, awaiting anything after it hands the event loop over long\n    // enough for that refresh to COMPLETE, and a completed refresh used to\n    // make the next refresh of the same file inside REFRESH_TTL_MS a no-op —\n    // so a caller that appended a turn and asked for a refresh was told\n    // \"recent enough\" and served the pre-append snapshot. The read and\n    // post-write paths are separate contracts now (ScannerManager\n    // .refreshFileForRead / .refreshFileAfterWrite, #806): a read landing here\n    // cannot throttle a writer out.\n    const ownFilePath = await this.locateJsonlPath(id, this.deps.resolveConversationLookupId(id));\n    const inherited =\n      ownFilePath && this.isCodexConversation(id, ownFilePath)\n        ? await resolveInheritedHistory({\n            filePath: ownFilePath,\n            locateSource: (sourceId) =>\n              this.locateJsonlPath(sourceId, this.deps.resolveConversationLookupId(sourceId)),\n          })\n        : null;\n\n    let conversation = await this.findConversationByUuid(id);\n\n    // A fork with no turns of its own has no transcript to find, but it is not\n    // an empty conversation — its history is the prefix. Stand in a shell for\n    // the scanner's miss so the normal paging path below serves it.\n    if (!conversation && inherited && inherited.messages.length > 0 && ownFilePath) {\n      conversation = this.shellConversationForInherited(id, ownFilePath, inherited);\n    }\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        // A tail row carrying neither text nor content blocks renders as nothing\n        // on a client, so serving it is a 200 that paints a blank conversation —\n        // worse than the 404, which clients already handle. Builds before the\n        // user/assistant role guard in ConversationCache wrote exactly these for\n        // Codex rollouts, keying `role` off the envelope type (event_msg /\n        // response_item), and the rows survive for any conversation whose file\n        // has since stopped changing.\n        const usableTail = (tail?.messages ?? []).filter(\n          (message) => (message.text ?? \"\").length > 0 || (message.content?.length ?? 0) > 0,\n        );\n        if (usableTail.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 = usableTail.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: withCamelMetaAliases({\n              id,\n              profile_id: cachedMeta?.account ?? undefined,\n              project_name: cachedMeta?.projectName ?? undefined,\n              session_name: cachedMeta?.title ?? 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              // Count what was actually served, not what the tail holds: the\n              // filtered rows are gone from the payload, and a total the page\n              // cannot account for is what a client reads as \"more to load\".\n              total: usableTail.length,\n              before_index: usableTail.length,\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      // A session that has never been given a prompt is an EMPTY conversation,\n      // not a missing one. Claude only creates `<sessionId>.jsonl` on the first\n      // user turn — measured 0.0s to 86.9s after `pty.ready` on this machine, and\n      // never at all if the user opens a session and walks away. 404 here was\n      // 62% of every conversation 404 in a three-week production log, and it is\n      // what renders \"Messages failed to load\" on a session that is working fine.\n      // `promptCount === 0` is the same \"unused start\" signal\n      // `shouldForgetEmptySession` uses, and it keeps a real deletion honest: a\n      // session that HAS sent prompts but has no transcript still 404s.\n      // `id` here is what the client requested, which for a bound Codex\n      // session is the rollout UUID (`boundConversationId`), not the\n      // placeholder PTY id `getManaged` is keyed by — check both.\n      const unusedStart =\n        this.sessionStore.getManaged(id) ??\n        this.sessionStore.listManaged().find((s) => s.boundConversationId === id) ??\n        null;\n      //\n      // `promptCount === 0` alone left a hole exactly one prompt wide. Sending\n      // the first prompt is what flips the counter to 1, and it is also what\n      // makes Claude create the file — so for the 0.3s-1.7s between the two this\n      // endpoint 404'd a session that was working fine. Measured on 4 sessions /\n      // 8 of 203 conversation 404s in a 20.5-day log, every one bracketed by\n      // `pty.input_write` -> 404 -> `session.jsonl_wired`; mobile refetches\n      // messages on submit and lands squarely in that window.\n      //\n      // So the in-flight case is its own clause: no transcript bound yet\n      // (`findLiveSessionFilePath` is the sessionFileMap the wiring writes to),\n      // and the session still recently active. Recency is anchored on\n      // `lastActivityAt ?? startedAt` — the same \"is this session live\" reading\n      // `session-store.ts` takes — not on `startedAt` alone, because the gap\n      // from spawn to first prompt is human think time and ran to 405.7s in\n      // production; anchoring on the spawn would leave the hole open for anyone\n      // slow to type. Past the watcher's own deadline with nothing on disk it\n      // 404s again: that is data loss, and it must not be dressed up as empty.\n      const bound = this.deps.findLiveSessionFilePath(id);\n      const activeAt = unusedStart?.lastActivityAt ?? unusedStart?.startedAt;\n      const transcriptPending =\n        !bound &&\n        activeAt != null &&\n        Date.now() - activeAt.getTime() < TRANSCRIPT_WATCH_DEADLINE_MS;\n      if (unusedStart && (unusedStart.promptCount === 0 || transcriptPending)) {\n        json(res, 200, this.emptyConversationPayload(id, unusedStart));\n        return;\n      }\n\n      // Self-heal, but only on PROOF. The row is a ghost when it names a file and\n      // that file is gone. Dropping it on an unexplained miss was the bug that\n      // made this permanent: for a Codex rollout the cache row is the only rung\n      // of locateJsonlPath that can match, so one transient miss deleted the row,\n      // the next request could no longer find the file, and `/api/sessions/:id`\n      // lost the same row as its own fallback — both endpoints 404ing forever on\n      // a conversation still sitting on disk. Invalidate `lookupId`, since that\n      // is the id the row is keyed by; `id` may be a Codex placeholder.\n      const lookupId = this.deps.resolveConversationLookupId(id);\n      const rowPath = this.cache?.getMetaById(lookupId)?.filePath;\n      if (rowPath && !existsSync(rowPath)) this.cache?.invalidate(lookupId);\n\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, and the window read below now shares it: the\n    // index writer applies the same leading-injected-context rule this handler\n    // does, so for Codex as well as Claude this count sits in the served space\n    // rather than one higher per preamble line.\n    //\n    // It is still only a validator — it has to CHANGE when the content does, not\n    // to be a meaningful message count — and both it and the\n    // `etagSource.messageCount` it is maxed against advance on an append, so the\n    // tag cannot freeze and hand out a stale 304. Pinned in\n    // __tests__/codex-meta-count-etag.test.ts.\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    // The prefix is part of what this response serves, so it has to be part of\n    // the validator. Without it a client caches half a conversation under a tag\n    // that cannot change when the other half does. The prefix is immutable, so\n    // its path and cut are enough — no source mtime needed.\n    const etag = computeConversationEtag({\n      filePath: inherited\n        ? `${etagSource.filePath}+${inherited.sourceFilePath ?? inherited.sourceId}@${inherited.ordinalExclusive}`\n        : etagSource.filePath,\n      messageCount: etagMessageCount + (inherited?.messages.length ?? 0),\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    //\n    // Bounded to the LEADING turn, matching the scanner's own rule. Unbounded, a\n    // human who pastes instruction text mid-conversation had their message\n    // silently eaten. The bound is also what keeps this list in the same index\n    // space as the offset index, which applies the identical rule at write time.\n    //\n    // The bound is PER FILE, not per served conversation, which is why the two\n    // filter() calls below each get their own counter: Codex injects at the head\n    // of every rollout, so a fork's own file has its own preamble at ITS index 0\n    // even though that message sits at index N of the stitched conversation. The\n    // scanner bounds the same way — its accumulator is per file.\n    //\n    // The counter counts KEPT messages, not array positions. Position was wrong\n    // for a head of TWO injected lines — the AGENTS.md dump followed by this\n    // server's own argv prompt, measured on 17 of 685 rollouts — because it drops\n    // only the first while the scanner's accumulator (`acc.messageCount === 0`)\n    // and the offset-index writer both drop the whole leading run. A warm index\n    // then served 3 messages under a meta claiming 4, and every message_index\n    // moved by one depending on nothing but whether the index was warm.\n    const servableFilter = () => {\n      let kept = 0;\n      return (m: { role: string; text?: string }) => {\n        if (isLeadingInjectedContext(kept, m.role, m.text)) return false;\n        kept++;\n        return true;\n      };\n    };\n\n    // The inherited prefix is filtered SEPARATELY from the conversation's own\n    // messages, not because the result differs — concatenating then filtering\n    // gives the same list — but because the boundary between the two halves is\n    // only knowable after the filter runs. `through_message_index` names a\n    // served index, and injected-context lines are dropped before indices are\n    // assigned, so counting the prefix beforehand puts the divider in the wrong\n    // place by however many lines the filter removed.\n    const inheritedFiltered = inherited ? inherited.messages.filter(servableFilter()) : [];\n    const filtered = [...inheritedFiltered, ...conversation.messages.filter(servableFilter())];\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      url.searchParams.has(\"max_bytes\") ||\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      // Two different reasons used to exclude Codex here (#824); only one is\n      // gone.\n      //\n      // FIXED: the index counted the AGENTS.md / sandbox preamble this handler\n      // drops, so a window served a fabricated first turn and shifted every\n      // genuine message_index by one. The index writer now applies the same\n      // leading-injected-context rule, so both number a file identically, and\n      // migration 018 drops the pre-filter rows. Plain Codex rollouts get the\n      // fast path back.\n      //\n      // STILL EXCLUDED: a fork. Its inherited prefix lives in the PARENT's file,\n      // and this window only ever covers `indexFilePath` — the fork's own. Taking\n      // it discards `filtered`, the one array carrying the prefix, which is how a\n      // fork came to serve 2 messages under a meta claiming 6. Agreeing on the\n      // space does not help: the prefix is not in this file to be numbered.\n      // Serving a fork from the index needs a read that spans both files, which\n      // is the split-window read that docs/plans/2026-09-07-inherited-conversation\n      // -history.md records as dropped.\n      const servesInheritedPrefix = !!inherited;\n      if (isTailRequest && !servesInheritedPrefix && 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 && !servesInheritedPrefix && indexFilePath && this.cache\n          ? this.cache.readMessageWindow(indexFilePath, windowStart, beforeIndex)\n          : null;\n      if (!indexWindow && !servesInheritedPrefix && indexFilePath && this.cache && !hasAnchor) {\n        // The one line that separates a fast fetch from a slow one. A miss\n        // means this request falls through to the scanner and re-parses the\n        // whole file, which is the entire difference between the 34 ms and\n        // 2877 ms fetches of the same conversation — and it was invisible in\n        // the log, because only the failure case was ever recorded.\n        this.log.info(\n          `[server] offset-index miss ${id} → scanner fallback`,\n          {\n            event: \"offset_index.miss\",\n            conversationId: id,\n            fromIndex: windowStart,\n            toIndex: beforeIndex,\n          },\n          \"pino\",\n        );\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.deps.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: toolResultText(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    // Byte budget. A page is bounded by `msg_limit` (capped at 500), but a\n    // count says nothing about what actually lands in a phone's heap: locally\n    // measured conversations run p50 315 KB and max 22.8 MB, so the same 500\n    // messages can be two orders of magnitude apart in size. `max_bytes` drops\n    // the OLDEST messages of the page until the rest fit, keeping the newest —\n    // the client wants the tail, and it pages backward from there.\n    //\n    // No `truncated` flag: a trimmed page is exactly a page with older messages\n    // behind it, which `has_more_older`/`next_before_index` already say. The\n    // only genuinely new fact is what the budget spent, so that is all that is\n    // added.\n    const requestedMaxBytes = intParam(url, \"max_bytes\", 0);\n    if (requestedMaxBytes > 0 && messagesPayload.length > 0) {\n      const budget = Math.min(requestedMaxBytes, MAX_BYTES_CEILING);\n      let used = 0;\n      let firstKept = messagesPayload.length - 1;\n      for (let i = messagesPayload.length - 1; i >= 0; i--) {\n        // Bytes, not string length: a Hebrew or emoji-heavy conversation is up\n        // to 4x its UTF-16 length on the wire, and undercounting is how a\n        // budget silently stops binding for exactly the users it matters to.\n        const size = Buffer.byteLength(JSON.stringify(messagesPayload[i]));\n        // The newest message is served whatever its size — a blank screen is\n        // worse than an over-budget one, and there is no smaller page to fall\n        // back to.\n        if (i < messagesPayload.length - 1 && used + size > budget) break;\n        used += size;\n        firstKept = i;\n      }\n      if (firstKept > 0) {\n        messagesPayload.splice(0, firstKept);\n        // message_index is absolute (fromIdx + localIdx at map time), so the\n        // remaining entries keep their correct indices; only the page's own\n        // start moves, and with it the cursor a client pages older from.\n        if (messagePagination) {\n          const newFrom = fromIdx + firstKept;\n          messagePagination.from_index = newFrom;\n          messagePagination.has_more_older = newFrom > 0;\n          messagePagination.next_before_index = newFrom > 0 ? newFrom : null;\n        }\n      }\n      if (messagePagination) messagePagination.served_bytes = used;\n    }\n\n    const conv = conversation as any;\n    // `id` may be a live session's PTY placeholder; the cache row is keyed by the\n    // bound transcript id, and a miss here would default the label to claude-code.\n    const cachedConvMeta =\n      this.cache?.getMetaById(this.deps.resolveConversationLookupId(id)) ??\n      this.cache?.getMetaById(id);\n    const convProvider = coerceProviderForRunner(\n      this.sessionStore.getManaged(id)?.provider ?? conv.provider ?? cachedConvMeta?.provider,\n    );\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    //\n    // The base is `total` (= `filtered.length`): the inherited prefix plus this\n    // file's own turns, each through `servableFilter`, so the whole number sits in\n    // the one space this response serves in. Taking it whole is also what counts\n    // the prefix, so a fork's meta does not say \"0 messages\" while its body\n    // carries 21 and the hub row disagrees with the open conversation.\n    //\n    // It replaces `conv.messageCount + inheritedFiltered.length`, which added a\n    // POST-filter prefix length to the scanner's RAW count — for Codex that\n    // count still holds the AGENTS.md / permissions lines `servableFilter` drops —\n    // producing a number in neither space: a fork carrying one injected line in\n    // its own file reported 7 against a body of 6.\n    //\n    // Freshness is then carried across as a DELTA rather than by swapping the\n    // raw `indexTotal` in for the own half, because only the delta is known to\n    // be filter-free. Both `indexTotal` and `total` now count in the served\n    // space for either provider — the index writer applies the same\n    // leading-injected-context rule this handler does — so the delta is exactly\n    // the messages the index has read past the scanner's snapshot, and adding it\n    // cannot reintroduce a preamble line the body does not carry.\n    //\n    // Equivalent to `indexFresh ? indexTotal : total` on every input reachable\n    // today: every Conversation the meta block can see is built with\n    // `messageCount: messages.length` (the scanner's parseConversation /\n    // parseCodexConversation / assemble, and `shellConversationForInherited`).\n    // The delta is preferred because it localizes the \"no messages were\n    // filtered here\" assumption to the newly-appended region instead of\n    // asserting it over the whole conversation.\n    const indexFreshDelta =\n      indexTotal != null && indexTotal > conv.messageCount ? indexTotal - conv.messageCount : 0;\n    const metaMessageCount = total + indexFreshDelta;\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: withCamelMetaAliases({\n        id,\n        isSubagent: this.cache?.getMetaById(id)?.isSubagent,\n        parentConversationId: this.cache?.getMetaById(id)?.parentConversationId,\n        profile_id: conv.account,\n        project_name: conv.projectName,\n        session_name: conv.sessionName || undefined,\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        // Additive: an older client ignores it and simply sees a longer\n        // conversation, which is still the fix. A newer one draws the seam.\n        ...(inherited && {\n          inherited_history: {\n            source_id: inherited.sourceId,\n            source_provider: CODEX_CLI_PROVIDER,\n            through_message_index: inheritedFiltered.length,\n            forked_at: inherited.forkedAt,\n            unavailable_reason: inherited.unavailableReason,\n          },\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  async handleSearchTarget(id: string, req: IncomingMessage, res: ServerResponse): 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  async handleSearch(url: URL, res: ServerResponse): Promise<void> {\n    let parsed: ParsedSearchQuery;\n    try {\n      parsed = parseSearchQuery(url.searchParams);\n    } catch (err) {\n      if (err instanceof SearchQueryError) {\n        json(res, 400, { error: err.message, code: err.code });\n        return;\n      }\n      throw err;\n    }\n    const { q, limit, offset, filters } = parsed;\n    const startedAt = Date.now();\n\n    const scanner = await this.scannerManager.get();\n    const results = await search(\n      q,\n      {\n        // Fetch beyond the requested page: filters below are applied AFTER the\n        // scanner returns, so slicing at `limit` here would drop results that a\n        // later page should contain. Bounded so a broad query cannot pull an\n        // unbounded set into memory.\n        limit: Math.min(offset + limit * SEARCH_OVERFETCH, SEARCH_MAX_SCAN),\n        include: \"conversations\",\n        ...(this.scanProfiles ? { profiles: this.scanProfiles } : {}),\n        ...this.scannerManager.codexScanOpts(),\n      },\n      scanner,\n    );\n    this.cache?.upsertFromScannerMeta(results.map((r: any) => r.meta));\n    const publicResults = results.flatMap((r) => {\n      const meta = this.publicScannerMeta(r.meta);\n      return meta ? [{ ...r, meta }] : [];\n    });\n    const adapted = publicResults.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: this.cache?.getIdByFilePath(r.meta.filePath) ?? 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      // The scanner already computes relevance and match snippets; the previous\n      // adapter discarded both, so results arrived in an unexplained order with\n      // no indication of WHY anything matched.\n      score: r.score,\n      matches: Array.isArray(r.matches)\n        ? r.matches.map((m: SearchMatch) => ({\n            field: m.field,\n            snippet: m.snippet,\n            // Offsets into `snippet` for the matched tokens. Absent on metadata\n            // hits, and on scanners older than the one that added them.\n            highlights: m.highlights,\n          }))\n        : [],\n    }));\n\n    const visible = adapted.filter((r) => this.cache?.isVisible(r.id) ?? true);\n    const page = paginate(applyFilters(visible, filters), offset, limit);\n    json(res, 200, {\n      conversations: page.items,\n      hasMore: page.hasMore,\n      offset: page.offset,\n      total: page.total,\n      // Query timing, so a slow search is diagnosable rather than merely felt.\n      tookMs: Date.now() - startedAt,\n    });\n  }\n}\n","import {\n  type ConversationMessage,\n  type ConversationMeta,\n  createJsonlParseState,\n  type FileStatEntry,\n  type JsonlParseState,\n  parseCodexJsonlLine,\n  parseCursorJsonlLine,\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 { instrumentDatabase, labelStatements } from \"./db/query-timing\";\nimport { runSqliteMigrations } from \"./db/sqlite-migrate\";\nimport { getLogger } from \"./logger\";\nimport {\n  CLAUDE_CODE_PROVIDER,\n  CODEX_CLI_PROVIDER,\n  CURSOR_PROVIDER,\n  canonicalizeProviderName,\n  LEGACY_CURSOR_PROVIDER,\n  type ProviderName,\n} from \"./providers\";\nimport {\n  ConversationClassifier,\n  classifyConversationFile,\n} from \"./services/conversations/classification\";\nimport {\n  DEFAULT_AGENT_ENTRYPOINTS,\n  isAgentFile,\n  isAgentLine,\n} from \"./services/conversations/isAgentConversation\";\nimport { canonicalizeFilePath } from \"./utils/canonicalizeFilePath\";\nimport { isLeadingInjectedContext } from \"./utils/codexConversationLine\";\nimport { fileIdentity, type LineSpan, splitCompleteLines } from \"./utils/fileIdentity\";\n\nexport interface ConversationCacheOptions {\n  includeSubagentSessions?: boolean;\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, sdk-py, 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  hasMessages?: boolean | null;\n  isSubagent?: boolean | null;\n  parentConversationId?: string | null;\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: ProviderName;\n  isImportedFromClaude: boolean;\n  isImportedFromCodex: boolean;\n  isImportedFromCursor: boolean;\n  /** ISO timestamp if soft-deleted from the cache, else null. The JSONL is untouched. */\n  deletedAt: string | null;\n}\n\nexport type ConversationInclude = \"all\" | \"conversations\" | \"subagents\";\n\nexport interface ConversationListFilters {\n  project?: string;\n  provider?: string;\n  include?: ConversationInclude;\n  isImportedFromClaude?: boolean;\n  isImportedFromCodex?: boolean;\n  isImportedFromCursor?: boolean;\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  // The scanner emits the derived conversation name as `sessionName`; the cache\n  // stores it in the `title` column. Callers pass the scanner's ConversationMeta\n  // directly, so accept both here and prefer sessionName when title is absent.\n  sessionName?: 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?: ProviderName;\n  isImportedFromClaude?: boolean;\n  isImportedFromCodex?: boolean;\n  isImportedFromCursor?: boolean;\n}\n\ninterface MetaRow {\n  has_messages: number | null;\n  is_subagent: number | null;\n  parent_conversation_id: string | null;\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: ProviderName;\n  updated_at: number;\n  scanner_meta_json: string | null;\n  is_imported_from_claude: number;\n  is_imported_from_codex: number;\n  is_imported_from_cursor: number;\n  deleted_at: number | 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\"/\"sdk-py\" = Claude Agent SDK / claude-mem /\n  // 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);\nCREATE INDEX IF NOT EXISTS idx_meta_file_path ON conversation_meta(file_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\nconst cacheLog = getLogger(\"cache\");\n\n// Messages read before a window's start, solely to build parse state that is\n// then discarded. Tool_use -> tool_result distance measures p50 1 and max 4\n// across sampled Claude transcripts; 8 is slack, and each extra row is one\n// byte-range read out of an index we already consulted.\nconst PARSE_STATE_LOOKBACK = 8;\n\nexport class ConversationCache {\n  private classifiers = new Map<string, ConversationClassifier>();\n  readonly includeSubagentSessions: boolean;\n  private classifyFull: Database.Statement;\n  private classifyAppend: Database.Statement;\n  private fileAliases: Database.Statement;\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    softDeleteById: 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    allFilePathsWithTitle: Database.Statement;\n    countAllRows: 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    projectSummaries: Database.Statement;\n    projectSummaryCount: 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.includeSubagentSessions = options?.includeSubagentSessions ?? false;\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.fileAliases = db.prepare(\n      \"SELECT id FROM conversation_meta WHERE file_path = ? AND id != ?\",\n    );\n    this.classifyFull = db.prepare(\n      `UPDATE conversation_meta SET has_messages = ?, is_subagent = ?, parent_conversation_id = ? WHERE id = ?`,\n    );\n    this.classifyAppend = db.prepare(`UPDATE conversation_meta SET\n      has_messages = CASE WHEN ? = 1 THEN 1 ELSE has_messages END,\n      is_subagent = ?, parent_conversation_id = ?, provider = ? WHERE id = ?`);\n    // Exactly the columns listConversations' row mapper reads. `SELECT *` also\n    // pulled scanner_meta_json, which averages 5.7 KB per row — 3.5 MB of the\n    // 4.5 MB this table occupies — and is never read on this path: roughly\n    // 285 KB marshalled into V8 strings and discarded per 50-row page.\n    // Measured on a 22 MB cache: 0.38 ms with it, 0.03 ms without.\n    //\n    // getFullById deliberately keeps `SELECT *`: it is a single row by primary\n    // key and its callers want every column.\n    const LIST_COLUMNS =\n      \"id, file_path, project_id, project_path, project_name, title, model, account, branch, message_count, last_activity, first_message, last_message, preview, source, provider, has_messages, is_subagent, parent_conversation_id, is_imported_from_claude, is_imported_from_codex, is_imported_from_cursor\";\n    const visible = `has_messages IS NOT 0 AND deleted_at IS NULL${this.includeSubagentSessions ? \"\" : \" AND is_subagent IS NOT 1\"}`;\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      // last_activity/last_message only move FORWARD: a batch whose newest line\n      // predates the stored last_activity (interleaved writers appending an older\n      // line) must not drag the metadata backward. message_count and updated_at\n      // still advance — a real message was appended and the row did change.\n      updateMetaBatch: db.prepare(\n        `UPDATE conversation_meta SET\n           message_count = message_count + @inc,\n           last_activity = CASE WHEN @last_activity > IFNULL(last_activity, -1)\n                                THEN @last_activity ELSE last_activity END,\n           last_message  = CASE WHEN @last_activity > IFNULL(last_activity, -1)\n                                THEN @last_message ELSE last_message END,\n           updated_at    = @updated_at\n         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           is_imported_from_claude, is_imported_from_codex, is_imported_from_cursor)\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           @is_imported_from_claude, @is_imported_from_codex, @is_imported_from_cursor)\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          is_imported_from_claude = excluded.is_imported_from_claude,\n          is_imported_from_codex = excluded.is_imported_from_codex,\n          is_imported_from_cursor = excluded.is_imported_from_cursor\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 ${LIST_COLUMNS} FROM conversation_meta WHERE ${visible} ORDER BY last_activity DESC LIMIT ? OFFSET ?`,\n      ),\n      count: db.prepare(`SELECT COUNT(*) as n FROM conversation_meta WHERE ${visible}`),\n      listByProject: db.prepare(\n        `SELECT ${LIST_COLUMNS} FROM conversation_meta WHERE ${visible} AND project_path = ? ORDER BY last_activity DESC LIMIT ? OFFSET ?`,\n      ),\n      countByProject: db.prepare(\n        `SELECT COUNT(*) as n FROM conversation_meta WHERE ${visible} AND project_path = ?`,\n      ),\n      listByProvider: db.prepare(\n        `SELECT ${LIST_COLUMNS} FROM conversation_meta WHERE ${visible} AND provider = ? ORDER BY last_activity DESC LIMIT ? OFFSET ?`,\n      ),\n      countByProvider: db.prepare(\n        `SELECT COUNT(*) as n FROM conversation_meta WHERE ${visible} AND provider = ?`,\n      ),\n      deleteById: db.prepare(\"DELETE FROM conversation_meta WHERE id = ?\"),\n      // Only sets it the first time — a repeat call is a no-op (`changes`\n      // reports 0), not a second, later timestamp.\n      softDeleteById: db.prepare(\n        \"UPDATE conversation_meta SET deleted_at = ? WHERE id = ? AND deleted_at IS NULL\",\n      ),\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      allFilePathsWithTitle: db.prepare(\"SELECT id, file_path, title FROM conversation_meta\"),\n      countAllRows: db.prepare(\"SELECT COUNT(*) AS n 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 ${visible} AND 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 AND project_path != '' LIMIT 1\",\n      ),\n      popularProjects: db.prepare(\n        `SELECT project_path, project_name, COUNT(*) as cnt\n         FROM conversation_meta\n         WHERE ${visible} AND project_path IS NOT NULL\n         GROUP BY project_path\n         ORDER BY cnt DESC\n         LIMIT ?`,\n      ),\n      // Same table, same rows and same NULL filter /api/conversations lists\n      // from, so a group's count/last-activity can never disagree with the\n      // page it opens. Bare project_name is the one from the MAX(last_activity)\n      // row (SQLite's documented min/max-aggregate bare-column rule).\n      projectSummaries: db.prepare(\n        `SELECT project_path, project_name, COUNT(*) as cnt, MAX(last_activity) as latest\n         FROM conversation_meta\n         WHERE ${visible} AND project_path IS NOT NULL\n         GROUP BY project_path\n         ORDER BY latest DESC, project_path ASC\n         LIMIT ? OFFSET ?`,\n      ),\n      projectSummaryCount: db.prepare(\n        `SELECT COUNT(DISTINCT project_path) as n FROM conversation_meta WHERE ${visible} AND project_path IS NOT NULL`,\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    // Name every statement above after its key, so a slow-query line says\n    // `getMessageIndexWindow` and not `select:conversation_message_index`.\n    labelStatements(this.stmts);\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  /**\n   * The line reducer for `filePath`, or null when the file must not be indexed.\n   *\n   * Returning the parser and the eligibility together is deliberate: indexing a\n   * file with the wrong provider's reducer does not fail, it indexes ZERO\n   * messages and then serves empty windows for a real conversation — the\n   * silent-wrong-data bug hotfixed after 1.28.0. Making the caller ask for a\n   * parser rather than a boolean removes the arrangement where those two facts\n   * can disagree.\n   *\n   * Claude's reducer is stateful (`extractToolResultBlocks` resolves a\n   * tool_result's type from `pendingToolUses`), so its parser closes over one\n   * `JsonlParseState` per call site. Codex's is stateless — every discriminator\n   * lives on the single line — so it needs none, and a Codex window needs no\n   * lookback to be correct.\n   *\n   * Resolve by file_path, NOT by conversationIdForFile: codex rollout files are\n   * named rollout-<ts>-<uuid>.jsonl, so the filename stem is not the meta row's\n   * id and an id lookup silently misses. A file with no meta row at all is NOT\n   * indexable — provider unknown means indexing is unsafe; a later request\n   * backfills once the meta row exists.\n   * file_path is stored canonicalized (forward slashes), so the lookup key must\n   * be canonicalized too. Passing a native Windows path here matches no row,\n   * which reads as \"not indexable\" and silently disables the offset index for\n   * every conversation.\n   */\n  private lineParserFor(\n    filePath: string,\n    resumeState?: JsonlParseState,\n  ): {\n    parse: (text: string, index: number) => ConversationMessage | null;\n    state: JsonlParseState | null;\n  } | null {\n    const row = this.stmts.getProviderByFilePath.get(canonicalizeFilePath(filePath)) as\n      | { provider: string | null }\n      | undefined;\n    if (!row) return null;\n    // A newly discovered Cursor transcript is first tailed before the\n    // classifier updates conversation_meta.provider. Match the classifier's\n    // path rule here so that the first append cannot consume Cursor lines with\n    // Claude's reducer and advance the offset past them.\n    const provider =\n      (row.provider == null || row.provider === CLAUDE_CODE_PROVIDER) &&\n      /[/\\\\]agent-transcripts[/\\\\]/.test(filePath)\n        ? CURSOR_PROVIDER\n        : (row.provider ?? CLAUDE_CODE_PROVIDER);\n    if (provider === CODEX_CLI_PROVIDER) {\n      // The scanner's line parser is deliberately unfiltered — it is stateless, so\n      // it cannot know whether a line is the LEADING turn, which is the bound its\n      // own message counter applies. The index numbers messages itself, so the\n      // bound belongs here. Without it the index counts Codex's AGENTS.md /\n      // sandbox preamble that the detail handler drops, and the two end up\n      // numbering the same file differently — which is what #824 had to gate\n      // around.\n      //\n      // `index` is the message_index this line would receive, passed by both\n      // writers from persisted state (runBackfill's counter, extendMessageIndex's\n      // last_message_index + 1), so it stays correct when an append resumes\n      // mid-file. A closure counter would reset on resume and silently unbound\n      // the filter.\n      return {\n        parse: (text, index) => {\n          const msg = parseCodexJsonlLine(text);\n          if (!msg) return null;\n          return isLeadingInjectedContext(index, msg.role, msg.text) ? null : msg;\n        },\n        state: null,\n      };\n    }\n    if (provider === CURSOR_PROVIDER) {\n      return {\n        parse: (text, index) => parseCursorJsonlLine(text, index),\n        state: null,\n      };\n    }\n    if (provider !== CLAUDE_CODE_PROVIDER) return null;\n    // `state` is returned so the incremental writer can persist it per file:\n    // a later append must continue this reducer, not restart it.\n    const state = resumeState ?? createJsonlParseState();\n    // Claude has no injected-context filter, so it ignores `index`: its raw and\n    // served spaces have always been the same one.\n    return { parse: (text) => parseJsonlLine(text, state), state };\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    const parser = this.lineParserFor(filePath, this.indexParseState.get(filePath));\n    if (!parser) 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    // Persist only once this call is going to consume the spans: a decline\n    // above must not leave a fresh reducer behind for the next read to resume\n    // from. Claude's reducer carries across watcher reads; Codex has no state\n    // to carry, and stores none.\n    if (parser.state) this.indexParseState.set(filePath, parser.state);\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 = parser.parse(span.text, nextIndex);\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 startedAt = performance.now();\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    const parser = this.lineParserFor(filePath);\n    if (!parser) return;\n\n    const CHUNK = 256 * 1024;\n    const YIELD_EVERY = 1000;\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 = parser.parse(span.text, nextIndex);\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. Codex carries no state.\n    if (parser.state) this.indexParseState.set(filePath, parser.state);\n    // The cold path's only success signal. Until now just `backfill_failed`\n    // was logged, so a full-file re-parse — the difference between a 20 ms\n    // detail fetch and a multi-second one — left no trace when it worked.\n    const ms = Math.round(performance.now() - startedAt);\n    cacheLog.info(\n      `[cache] offset-index backfilled ${convId} ${ms}ms`,\n      {\n        event: \"offset_index.backfill_ok\",\n        conversationId: convId,\n        ms,\n        rows: nextIndex,\n        bytes: stat.size,\n      },\n      \"pino\",\n    );\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 file the reducer could not read) — serving\n    // it would render a real conversation empty. Resolving the parser here is\n    // the same belt-and-braces the old `isIndexableFile` call was: a provider\n    // with no reducer declines outright, even if a poisoned row carries a\n    // nonzero count. The decline routes the caller to backfill, which purges\n    // the row.\n    const parser = this.lineParserFor(filePath);\n    if (fileState.last_message_index < 0 || !parser) {\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    // Read a short prefix before the window purely to build parse state, then\n    // drop it. Claude's `parseJsonlLine` IS state-dependent:\n    // `extractToolResultBlocks` resolves a tool_result's type from\n    // `pendingToolUses`, so a window that\n    // opens between a tool_use and its tool_result serves the result as\n    // \"generic\" — a Bash card rendering with the default icon and label.\n    // Measured at 0.37% of tool_results at the default msg_limit of 80, which\n    // is (tool_result density)/W: short-range pairs still break whenever a\n    // boundary lands between the halves. Pair distance measures p50 1, max 4,\n    // so 8 is slack, and the extra reads are byte-ranges out of the same index.\n    // Codex's reducer is stateless — every discriminator is on the line — so a\n    // Codex window needs no lookback and pays for no extra reads.\n    const lookback = parser.state ? PARSE_STATE_LOOKBACK : 0;\n    const rows = this.getMessageIndexWindow(\n      ConversationCache.conversationIdForFile(filePath),\n      Math.max(0, from - lookback),\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      // Parse the lookback prefix into `state` and discard its messages; only\n      // rows at or after `from` are served. This is what makes a windowed read\n      // match a full contiguous parse for tool types, rather than the claim\n      // that the per-line shape is state-independent — it is not.\n      //\n      // teamInfo remains the one field a window cannot reproduce: the scanner\n      // applies it in a post-pass from a conversation-wide teamName -> TeamInfo\n      // table, and `applyTeamInfo` is not exported to us at all. No lookback\n      // fixes that; it needs the table carried per conversation. Left as a\n      // known gap rather than papered over — see offset-index-read.test.ts.\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        // Rows already exclude anything the writer filtered, so this is the\n        // post-filter index and re-applying the rule is a no-op on a current\n        // index. It only bites on rows written before the filter existed —\n        // which the index version forces to rebuild anyway.\n        const msg = parser.parse(buf.toString(\"utf-8\"), row.message_index);\n        if (msg && row.message_index >= from) 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 = instrumentDatabase(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  /** Every project with at least one cached conversation, most recently active\n   *  first. Paths are the raw `project_path` values, which is what\n   *  /api/conversations?project= matches on exactly — so a summary row is\n   *  always joinable against the page it describes. */\n  listProjectSummaries(opts: { limit: number; offset: number }): {\n    projects: Array<{\n      path: string;\n      name: string;\n      conversationCount: number;\n      lastActivity: string;\n    }>;\n    total: number;\n  } {\n    const total = (this.stmts.projectSummaryCount.get() as { n: number }).n;\n    const rows =\n      opts.limit === 0\n        ? []\n        : (this.stmts.projectSummaries.all(opts.limit, opts.offset) as Array<{\n            project_path: string;\n            project_name: string | null;\n            cnt: number;\n            latest: number | null;\n          }>);\n    return {\n      total,\n      projects: rows.map((r) => ({\n        path: r.project_path,\n        name: r.project_name ?? r.project_path.split(/[/\\\\]/).pop() ?? r.project_path,\n        conversationCount: r.cnt,\n        lastActivity: new Date(r.latest ?? 0).toISOString(),\n      })),\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      // Key by the canonical path so a native-separator lookup (chokidar) hits\n      // the same entry a forward-slash scanner row wrote (P1.a).\n      this.fileIndex.set(canonicalizeFilePath(row.file_path), row.id);\n    }\n    this.fileIndexLoaded = true;\n  }\n\n  /** NULL stays visible until a completed parse proves emptiness. */\n  isVisible(id: string): boolean {\n    const meta = this.getMetaById(id);\n    return (\n      meta?.hasMessages !== false && (this.includeSubagentSessions || meta?.isSubagent !== true)\n    );\n  }\n\n  /**\n   * Every row, ignoring the visibility predicate. Cache health is a property of\n   * the whole cache: `listMissingFiles` counts rows a list query never returns,\n   * so a filtered denominator would divide two different corpora.\n   */\n  countAllRows(): number {\n    return (this.stmts.countAllRows.get() as { n: number }).n;\n  }\n\n  isExcludedSubagent(id: string): boolean {\n    return !this.includeSubagentSessions && this.getMetaById(id)?.isSubagent === true;\n  }\n\n  reconcileClassification(filePath: string): ConversationClassifier | null {\n    try {\n      const id = this.getIdByFilePath(filePath);\n      const meta = id ? this.getMetaById(id) : null;\n      const classification = classifyConversationFile(filePath, meta?.provider);\n      if (meta) {\n        if (classification.isSubagent && classification.id !== meta.id) {\n          const row = this.stmts.getFullById.get(meta.id) as MetaRow;\n          const original = row.scanner_meta_json ? JSON.parse(row.scanner_meta_json) : {};\n          this.upsertFromScannerMeta([{ ...original, id: meta.id, sessionId: meta.id, filePath }]);\n        } else {\n          this.classifyFull.run(\n            Number(classification.hasMessages),\n            Number(classification.isSubagent),\n            classification.parentConversationId,\n            meta.id,\n          );\n          classification.id = meta.id;\n          this.classifiers.delete(canonicalizeFilePath(filePath));\n        }\n      }\n      return classification;\n    } catch {\n      return null;\n    }\n  }\n\n  private classifyAppendedLines(filePath: string, lines: string[]): void {\n    const key = canonicalizeFilePath(filePath);\n    let classifier = this.classifiers.get(key);\n    if (!classifier) {\n      const existingId = this.getIdByFilePath(key);\n      const meta = existingId ? this.getMetaById(existingId) : null;\n      classifier = new ConversationClassifier(key, meta?.provider);\n      if (meta) {\n        classifier.id = meta.id;\n        classifier.hasMessages = meta.hasMessages === true;\n        classifier.isSubagent = meta.isSubagent ?? classifier.isSubagent;\n        classifier.parentConversationId = meta.parentConversationId ?? null;\n      }\n      this.classifiers.set(key, classifier);\n    }\n    for (const line of lines) classifier.append(line);\n    if (\n      !this.getIdByFilePath(key) &&\n      !classifier.hasMessages &&\n      !classifier.isSubagent &&\n      !lines.some((raw) => {\n        try {\n          const line = JSON.parse(raw);\n          return (\n            line &&\n            (line.cwd ||\n              line.slug ||\n              line.type === \"session_meta\" ||\n              line.type === \"user\" ||\n              line.type === \"assistant\" ||\n              line.role === \"user\" ||\n              line.role === \"assistant\")\n          );\n        } catch {\n          return false;\n        }\n      })\n    )\n      return;\n    const current = classifier;\n    this.db.transaction(() => {\n      this.removeFileAliases(key, current.id);\n      this.stmts.insertSkeleton.run(current.id, key, 0);\n      this.classifyAppend.run(\n        Number(current.hasMessages),\n        Number(current.isSubagent),\n        current.parentConversationId,\n        current.provider,\n        current.id,\n      );\n      if (this.fileIndexLoaded) this.fileIndex.set(key, current.id);\n    })();\n  }\n\n  /** Repair both legacy parent-keyed rows and live-tail aliases for this file only. */\n  private removeFileAliases(filePath: string, id: string): void {\n    const aliases = this.fileAliases.all(filePath, id) as { id: string }[];\n    for (const alias of aliases) {\n      this.stmts.deleteTailById.run(alias.id);\n      this.stmts.deleteMessageIndex.run(alias.id);\n      this.stmts.deleteById.run(alias.id);\n    }\n    if (aliases.length) {\n      this.stmts.deleteFileState.run(filePath);\n      this.clearIndexParseState(filePath);\n      this.fileIndex.delete(filePath);\n    }\n  }\n\n  updateFromLine(filePath: string, rawLine: string): void {\n    this.classifyAppendedLines(filePath, [rawLine]);\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    // Canonicalize so a native-separator path (chokidar) keys the same cache row\n    // as the scanner's forward-slash file_path (P1.a).\n    const key = canonicalizeFilePath(filePath);\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(key);\n    if (!convId) {\n      const pseudoId =\n        key\n          .split(/[/\\\\]/)\n          .pop()\n          ?.replace(/\\.jsonl$/, \"\") ?? key;\n      this.stmts.insertSkeleton.run(pseudoId, key, 0);\n      this.fileIndex.set(key, 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 newest message line by timestamp (a monotonic guard keeps them from\n   * moving backward when an interleaved writer appends an older line — P0.3).\n   */\n  updateFromLines(filePath: string, rawLines: string[]): void {\n    this.classifyAppendedLines(filePath, rawLines);\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      // last_activity/last_message track the NEWEST message by timestamp, not the\n      // last line in byte order. Interleaved writers can append an older line\n      // after a newer one; taking byte-order-last would move last_activity\n      // backward and corrupt list ordering / staleness comparisons. The tail\n      // below stays in byte order (unsorted) by design.\n      if (lastActivityMs === null || activityMs > lastActivityMs) {\n        lastActivityMs = activityMs;\n        lastMessage = JSON.stringify({ role, timestamp, text });\n      }\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    // Canonicalize so a native-separator path (chokidar) keys the same cache row\n    // as the scanner's forward-slash file_path (P1.a).\n    const key = canonicalizeFilePath(filePath);\n\n    let convId = this.fileIndex.get(key);\n    if (!convId) {\n      const pseudoId =\n        key\n          .split(/[/\\\\]/)\n          .pop()\n          ?.replace(/\\.jsonl$/, \"\") ?? key;\n      this.stmts.insertSkeleton.run(pseudoId, key, 0);\n      this.fileIndex.set(key, 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        let classification: ConversationClassifier | null = null;\n        try {\n          classification = classifyConversationFile(m.filePath, m.provider);\n        } catch {\n          // A missing or unreadable file is not evidence of an empty history.\n        }\n        const id =\n          classification?.isSubagent || classification?.provider === CODEX_CLI_PROVIDER\n            ? classification.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        // Store the canonical (forward-slash) path so live-tail / directory-event\n        // lookups by native separators hit this row (P1.a). statSync keeps the\n        // original path — the stat is a file-identity read, not a key.\n        const canonicalPath = canonicalizeFilePath(m.filePath);\n        if (classification) this.removeFileAliases(canonicalPath, id);\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        // A JSONL with no `cwd` line yields \"\" from the scanner, not\n        // undefined — `?? null` doesn't catch that, so it used to persist as\n        // an empty string. project_id IS NULL AND project_path IS NOT NULL\n        // matches \"\", which becomes an orphan the backfill can never clear\n        // (it refuses to overwrite a falsy-but-non-null projectPath).\n        const projectPath = m.projectPath?.trim() || null;\n        this.stmts.upsertFull.run({\n          id,\n          file_path: canonicalPath,\n          project_path: projectPath,\n          project_name: m.projectName ?? null,\n          title: m.title ?? m.sessionName ?? 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:\n            canonicalizeProviderName(classification?.provider ?? m.provider) ??\n            CLAUDE_CODE_PROVIDER,\n          scanner_meta_json: scannerMetaJson,\n          is_imported_from_claude: m.isImportedFromClaude ? 1 : 0,\n          is_imported_from_codex: m.isImportedFromCodex ? 1 : 0,\n          is_imported_from_cursor: m.isImportedFromCursor ? 1 : 0,\n        });\n        this.stmts.updateScannerCache.run(mtimeMs, fileSize, scannerMetaJson, id);\n        if (classification) {\n          this.classifyFull.run(\n            Number(classification.hasMessages),\n            Number(classification.isSubagent),\n            classification.parentConversationId,\n            id,\n          );\n          this.classifiers.delete(canonicalPath);\n        }\n        if (this.fileIndexLoaded) this.fileIndex.set(canonicalPath, 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    this.classifiers.delete(canonicalizeFilePath(filePath));\n    // Canonicalize so a native-separator path matches the forward-slash file_path\n    // and fileIndex key (P1.a).\n    const key = canonicalizeFilePath(filePath);\n    const row = this.stmts.getIdByFilePath.get(key) 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(key);\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: ConversationListFilters & { limit: number; offset: number }): {\n    conversations: ConversationListItem[];\n    total: number;\n  } {\n    const { limit, offset } = opts;\n    const { where, params } = this.conversationListWhere(opts);\n    const total = (\n      this.db\n        .prepare(`SELECT COUNT(*) as n FROM conversation_meta WHERE ${where}`)\n        .get(...params) as {\n        n: number;\n      }\n    ).n;\n    const rows =\n      limit === 0\n        ? []\n        : (this.db\n            .prepare(\n              `SELECT ${this.listColumns} FROM conversation_meta WHERE ${where} ORDER BY last_activity DESC LIMIT ? OFFSET ?`,\n            )\n            .all(...params, limit, offset) as MetaRow[]);\n\n    return {\n      total,\n      conversations: rows.map((r) => ({\n        hasMessages: r.has_messages === null ? null : r.has_messages === 1,\n        isSubagent: r.is_subagent === null ? null : r.is_subagent === 1,\n        parentConversationId: r.parent_conversation_id,\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: canonicalizeProviderName(r.provider) ?? r.provider ?? CLAUDE_CODE_PROVIDER,\n        isImportedFromClaude: r.is_imported_from_claude === 1,\n        isImportedFromCodex: r.is_imported_from_codex === 1,\n        isImportedFromCursor: r.is_imported_from_cursor === 1,\n        // The WHERE clause below already excludes deleted rows.\n        deletedAt: null,\n      })),\n    };\n  }\n\n  private get listColumns(): string {\n    return \"id, file_path, project_id, project_path, project_name, title, model, account, branch, message_count, last_activity, first_message, last_message, preview, source, provider, has_messages, is_subagent, parent_conversation_id, is_imported_from_claude, is_imported_from_codex, is_imported_from_cursor\";\n  }\n\n  private conversationListWhere(opts: ConversationListFilters): {\n    where: string;\n    params: unknown[];\n  } {\n    const clauses = [\"has_messages IS NOT 0\", \"deleted_at IS NULL\"];\n    const params: unknown[] = [];\n    const include = opts.include ?? (this.includeSubagentSessions ? \"all\" : \"conversations\");\n    if (include === \"conversations\") clauses.push(\"is_subagent IS NOT 1\");\n    else if (include === \"subagents\") clauses.push(\"is_subagent = 1\");\n    if (opts.project) {\n      clauses.push(\"project_path = ?\");\n      params.push(opts.project);\n    }\n    if (opts.provider) {\n      const provider = canonicalizeProviderName(opts.provider) ?? opts.provider;\n      if (provider === CURSOR_PROVIDER) {\n        clauses.push(\"(provider = ? OR provider = ?)\");\n        params.push(CURSOR_PROVIDER, LEGACY_CURSOR_PROVIDER);\n      } else {\n        clauses.push(\"provider = ?\");\n        params.push(provider);\n      }\n    }\n    if (opts.isImportedFromClaude !== undefined) {\n      clauses.push(\"is_imported_from_claude = ?\");\n      params.push(opts.isImportedFromClaude ? 1 : 0);\n    }\n    if (opts.isImportedFromCodex !== undefined) {\n      clauses.push(\"is_imported_from_codex = ?\");\n      params.push(opts.isImportedFromCodex ? 1 : 0);\n    }\n    if (opts.isImportedFromCursor !== undefined) {\n      clauses.push(\"is_imported_from_cursor = ?\");\n      params.push(opts.isImportedFromCursor ? 1 : 0);\n    }\n    return { where: clauses.join(\" AND \"), params };\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  /**\n   * Conversation id for a JSONL path, or null when no row exists yet. Resolves\n   * by file_path (NOT conversationIdForFile) so codex rollout files — named\n   * rollout-<ts>-<uuid>.jsonl, whose stem is not the row id — resolve correctly.\n   */\n  getIdByFilePath(filePath: string): string | null {\n    const key = canonicalizeFilePath(filePath);\n    const row = this.stmts.getIdByFilePath.get(key) as { id: string } | undefined;\n    return row?.id ?? null;\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      hasMessages: row.has_messages === null ? null : row.has_messages === 1,\n      isSubagent: row.is_subagent === null ? null : row.is_subagent === 1,\n      parentConversationId: row.parent_conversation_id,\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      isImportedFromClaude: row.is_imported_from_claude === 1,\n      isImportedFromCodex: row.is_imported_from_codex === 1,\n      isImportedFromCursor: row.is_imported_from_cursor === 1,\n      deletedAt: row.deleted_at ? new Date(row.deleted_at).toISOString() : null,\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  /**\n   * Hides a conversation from every list/get read path without touching its\n   * JSONL or the row itself — just a `deleted_at` flag, so an upsert from a\n   * later rescan (the file is still on disk) leaves it alone rather than\n   * resurrecting it. Returns true if this call is what set the flag.\n   *\n   * Not preserved across the cache-integrity monitor's `reset_rescan` action,\n   * which wipes and rebuilds `conversation_meta` from scratch — an explicit\n   * operator recovery step, not routine background rescanning.\n   */\n  softDeleteConversation(id: string): boolean {\n    return this.stmts.softDeleteById.run(Date.now(), id).changes > 0;\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      const meta = this.getMetaById(id);\n      if (meta) this.classifiers.delete(canonicalizeFilePath(meta.filePath));\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.classifiers.clear();\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`, which NEVER deletes (upsert-or-leave). A change\n   *    event fires on every external append; deleting here flickers the\n   *    conversation out of /api/conversations — whether it's a live-tailed row\n   *    the updateFromLines/warm-up path just wrote (CRITICAL #2; both watchers\n   *    fire on the same append with no ordering guarantee) OR a refresh-created\n   *    untailed row (a ?refresh=1 upsert never populates a tail, so the old\n   *    \"delete when untailed\" behavior made it vanish on its next append with no\n   *    client action). The live-tail path owns the row's content and the\n   *    debounced rescan re-derives metadata, so leaving the row loses 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    // Canonicalize so a native-separator path (chokidar) matches the\n    // forward-slash file_path a scanner upsert wrote (P1.a).\n    const key = canonicalizeFilePath(filePath);\n    const row = this.stmts.getIdByFilePath.get(key) as { id: string } | undefined;\n    if (!row) return null;\n    // Change path: never delete (upsert-or-leave). Only the unlink path (no opts)\n    // removes the row.\n    if (opts?.skipIfTailed) 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)) {\n        this.reconcileClassification(row.file_path);\n        continue;\n      }\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  /**\n   * Read-only: list cached rows whose `file_path` no longer exists on disk.\n   * Unlike pruneGhostFiles/reconcileDeletions this mutates nothing — it just\n   * reports drift for the CacheIntegrityMonitor to classify. `tailed` flags\n   * rows that still have cached history (which pruneGhostFiles would keep).\n   */\n  listMissingFiles(\n    exists: (filePath: string) => boolean = existsSync,\n  ): { id: string; filePath: string; title: string | null; tailed: boolean }[] {\n    const rows = this.stmts.allFilePathsWithTitle.all() as {\n      id: string;\n      file_path: string;\n      title: string | null;\n    }[];\n    const missing: { id: string; filePath: string; title: string | null; tailed: boolean }[] = [];\n    for (const row of rows) {\n      if (exists(row.file_path)) continue;\n      missing.push({\n        id: row.id,\n        filePath: row.file_path,\n        title: row.title,\n        tailed: !!this.stmts.hasTail.get(row.id),\n      });\n    }\n    return missing;\n  }\n\n  /**\n   * Drop the given conversation ids outright — main row, tail, and message\n   * index — regardless of whether they have a tail. Used by the cache-integrity\n   * resolution actions (prune_all / prune_selected). Returns the count dropped.\n   */\n  dropRowsById(ids: string[]): number {\n    if (ids.length === 0) return 0;\n    const drop = this.db.transaction((toDrop: string[]) => {\n      let n = 0;\n      for (const id of toDrop) {\n        this.stmts.deleteTailById.run(id);\n        this.stmts.deleteMessageIndex.run(id);\n        n += this.stmts.deleteById.run(id).changes;\n      }\n      return n;\n    });\n    const dropped = drop(ids);\n    if (this.fileIndexLoaded) {\n      for (const id of ids) {\n        for (const [fp, cid] of this.fileIndex) {\n          if (cid === id) {\n            this.fileIndex.delete(fp);\n            break;\n          }\n        }\n      }\n    }\n    return dropped;\n  }\n\n  /**\n   * Wipe all cached conversation state — meta, tails, and message index — and\n   * reset the in-memory file index. Only called by the `reset_rescan`\n   * resolution action, which repopulates from a fresh disk scan afterward.\n   */\n  clearAll(): void {\n    this.classifiers.clear();\n    this.db.transaction(() => {\n      this.stmts.deleteTailAll.run();\n      this.stmts.deleteAll.run();\n      this.db.exec(\"DELETE FROM conversation_message_index\");\n    })();\n    this.fileIndex = new Map();\n  }\n}\n","import type Database from \"better-sqlite3\";\nimport { getLogger } from \"../logger\";\n\nconst log = getLogger(\"db\");\n\n/**\n * Slow-query threshold, in milliseconds.\n *\n * Measured, not guessed. Against the live 22 MB `cache.db` on this machine\n * (583 conversations, 38 717 index rows, warm page cache), 3 600 read samples\n * across the twelve statements the hot paths actually run gave p50 0.03 ms,\n * p99 0.87 ms, p99.9 1.43 ms, max 1.83 ms; 1 200 write samples gave p99\n * 0.09 ms with rare WAL-checkpoint spikes to 4.37 ms. So *every* healthy query\n * on this machine finishes inside ~4.4 ms, and the checkpoint spikes are the\n * only thing anywhere near that.\n *\n * 35 ms sits two anchors above that: it is 8x the slowest healthy operation\n * ever observed, so checkpoint spikes can never page anyone, and it is more\n * than the 34 ms *end-to-end* time of the fastest complete conversation fetch\n * measured on this box. A query crossing it therefore cost more on its own\n * than an entire healthy request — which is the point at which a warn line is\n * worth the bytes it occupies in the log.\n *\n * Override with `THREADBASE_DB_SLOW_QUERY_MS`; <= 0 disables slow logging.\n */\nexport const DEFAULT_SLOW_QUERY_MS = 35;\n\n/**\n * Per-statement label, held in a box so `labelStatements` can replace the\n * SQL-derived fallback with the real name after the statements are assigned.\n * A symbol keeps it off `Object.keys`, so nothing that enumerates a statement\n * bag sees it.\n */\nconst LABEL = Symbol(\"tbQueryLabel\");\n\ninterface Labelled {\n  [LABEL]?: { label: string };\n}\n\n/**\n * `verb:table` — what a statement is called before anyone names it.\n *\n * Never the full SQL text (too verbose for a per-query line) and never the\n * bound parameters (they carry file paths and conversation ids).\n */\nfunction deriveLabel(sql: string): string {\n  const verb = /^\\s*(\\w+)/.exec(sql)?.[1]?.toLowerCase() ?? \"sql\";\n  const table = /(?:from|into|update)\\s+([A-Za-z_]\\w*)/i.exec(sql)?.[1] ?? \"?\";\n  return `${verb}:${table}`;\n}\n\nfunction resolveSlowMs(): number {\n  const raw = process.env.THREADBASE_DB_SLOW_QUERY_MS;\n  if (raw === undefined || raw === \"\") return DEFAULT_SLOW_QUERY_MS;\n  const parsed = Number(raw);\n  return Number.isFinite(parsed) ? parsed : DEFAULT_SLOW_QUERY_MS;\n}\n\nfunction record(label: string, ms: number, rows: number, slowMs: number): void {\n  // Structured only (\"pino\"), never `both`. The default dest writes the message\n  // a second time via console.*, and both streams land in the same prod log —\n  // a duplicate line per query is exactly the volume this instrumentation is\n  // supposed to stay under.\n  if (slowMs > 0 && ms >= slowMs) {\n    log.warn(\n      `[db] slow query ${label} ${ms.toFixed(1)}ms rows=${rows}`,\n      { event: \"db.slow_query\", stmt: label, ms: Math.round(ms * 100) / 100, rows },\n      \"pino\",\n    );\n    return;\n  }\n  // Everything else is off by default: a single conversation fetch runs dozens\n  // of statements, so this is only affordable when someone asked for it.\n  if (log.pino.isLevelEnabled(\"debug\")) {\n    log.debug(\n      `[db] ${label} ${ms.toFixed(2)}ms rows=${rows}`,\n      { event: \"db.query\", stmt: label, ms: Math.round(ms * 100) / 100, rows },\n      \"pino\",\n    );\n  }\n}\n\nfunction rowsOf(method: \"get\" | \"all\" | \"run\", result: unknown): number {\n  if (method === \"all\") return Array.isArray(result) ? result.length : 0;\n  if (method === \"run\") return (result as Database.RunResult | undefined)?.changes ?? 0;\n  return result === undefined ? 0 : 1;\n}\n\n/**\n * Time every `.get()` / `.all()` / `.run()` on every statement this connection\n * prepares.\n *\n * Wrapping `prepare` is what makes this one edit per database instead of one\n * per query: `better-sqlite3` is synchronous, so a call's wall time *is* its\n * cost, and every statement in the process — the 45 in ConversationCache, the\n * 4 in RuntimeStore, and the ~40 more inside the repositories that share these\n * handles — is created here. Measured overhead is +0.09 µs on an 8.9 µs call\n * (~1%), which is why it is always on rather than behind a flag.\n *\n * Only the three terminal methods are replaced, as own properties; `iterate`,\n * `pluck`, `raw`, `columns` and `bind` keep resolving to the prototype\n * untouched. A statement that throws is not recorded — the error surfaces on\n * its own path.\n */\nexport function instrumentDatabase(\n  db: Database.Database,\n  options: { slowMs?: number } = {},\n): Database.Database {\n  const slowMs = options.slowMs ?? resolveSlowMs();\n  const prepare = db.prepare.bind(db);\n\n  db.prepare = ((sql: string) => {\n    const stmt = prepare(sql);\n    const box = { label: deriveLabel(sql) };\n    Object.defineProperty(stmt, LABEL, { value: box, configurable: true });\n\n    for (const method of [\"get\", \"all\", \"run\"] as const) {\n      const original = stmt[method].bind(stmt) as (...args: unknown[]) => unknown;\n      Object.defineProperty(stmt, method, {\n        configurable: true,\n        writable: true,\n        value: (...args: unknown[]) => {\n          const started = performance.now();\n          const result = original(...args);\n          record(box.label, performance.now() - started, rowsOf(method, result), slowMs);\n          return result;\n        },\n      });\n    }\n    return stmt;\n  }) as typeof db.prepare;\n\n  return db;\n}\n\n/**\n * Name the statements in a `{ name: Statement }` bag after their keys, so a\n * slow-query line reads `getMessageIndexWindow` rather than\n * `select:conversation_message_index`.\n *\n * Separate from `instrumentDatabase` because `prepare` cannot know the key its\n * result is about to be assigned to. Statements from a connection that was\n * never instrumented are skipped.\n */\nexport function labelStatements(statements: object): void {\n  for (const [name, stmt] of Object.entries(statements)) {\n    const box = (stmt as Labelled | null)?.[LABEL];\n    if (box) box.label = name;\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\n/**\n * Resolve a migrations directory that sits next to this module in both the\n * source tree (`src/db/<name>`) and the bundle (`dist/<name>`, populated by the\n * build's copy step).\n */\nexport function resolveMigrationsDir(name = \"migrations\"): string {\n  return join(getMigrationsDir(), name);\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 ?? resolveMigrationsDir();\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 {\n  createJsonlParseState,\n  parseCodexJsonlLine,\n  parseCursorJsonlLine,\n  parseJsonlLine,\n} from \"@threadbase-sh/scanner\";\nimport { closeSync, openSync, readSync } from \"fs\";\nimport { StringDecoder } from \"string_decoder\";\nimport { z } from \"zod\";\nimport {\n  CLAUDE_CODE_PROVIDER,\n  CODEX_CLI_PROVIDER,\n  CURSOR_PROVIDER,\n  type ProviderName,\n} from \"../../providers\";\nimport { isCodexInjectedContext } from \"../../utils/codexConversationLine\";\n\nconst identityRecord = z.object({\n  type: z.string().optional(),\n  role: z.string().optional(),\n  isSidechain: z.boolean().optional(),\n  agentId: z.string().optional(),\n  sessionId: z.string().optional(),\n  payload: z\n    .object({\n      id: z.string().optional(),\n      source: z.unknown().optional(),\n    })\n    .passthrough()\n    .optional(),\n});\nconst spawnSource = z.object({\n  subagent: z.object({\n    thread_spawn: z.object({ parent_thread_id: z.string().min(1) }),\n  }),\n});\n\nexport class ConversationClassifier {\n  readonly state = createJsonlParseState();\n  hasMessages = false;\n  isSubagent = false;\n  parentConversationId: string | null = null;\n  id: string;\n  provider: ProviderName;\n  private explicitClaudeIdentity = false;\n  private sawIdentity = false;\n  private readonly fileStem: string;\n\n  /**\n   * Nothing later in the file can change the answer: `hasMessages` only ever\n   * goes false→true, and identity settles at Codex's line-0 `session_meta` or\n   * the first Claude line carrying `isSidechain`. A file with no renderable\n   * message is never settled and is read in full — that is how `hasMessages`\n   * earns a definitive 0, and such files are small.\n   */\n  get settled(): boolean {\n    return this.hasMessages && this.sawIdentity;\n  }\n\n  constructor(filePath: string, provider: ProviderName = CLAUDE_CODE_PROVIDER) {\n    this.id =\n      filePath\n        .split(/[/\\\\]/)\n        .pop()\n        ?.replace(/\\.jsonl$/, \"\") ?? filePath;\n    this.fileStem = this.id;\n    this.provider = provider;\n    if (this.provider === CLAUDE_CODE_PROVIDER && /[/\\\\]agent-transcripts[/\\\\]/.test(filePath)) {\n      this.provider = CURSOR_PROVIDER;\n    }\n    this.isSubagent = /[/\\\\]subagents[/\\\\][^/\\\\]+\\.jsonl$/.test(filePath);\n    if (this.isSubagent) {\n      const parts = filePath.split(/[/\\\\]/);\n      const subIdx = parts.lastIndexOf(\"subagents\");\n      if (subIdx > 0) this.parentConversationId = parts[subIdx - 1] ?? null;\n    }\n  }\n\n  append(raw: string) {\n    let value: unknown;\n    try {\n      value = JSON.parse(raw);\n    } catch {\n      return null;\n    }\n    if (value === null || typeof value !== \"object\" || Array.isArray(value)) return null;\n    const parsed = identityRecord.safeParse(value);\n    if (parsed.success) {\n      const entry = parsed.data;\n      if (entry.type === \"session_meta\" && entry.payload) {\n        // Copies under agent-transcripts stay cursor; native Codex rollouts\n        // still flip here. Overwriting would store imported Cursor history as\n        // provider=codex-cli and drop isImportedFromCodex on the list row.\n        if (this.provider !== CURSOR_PROVIDER) {\n          this.provider = \"codex-cli\";\n        }\n        this.sawIdentity = true;\n        if (entry.payload.id) this.id = entry.payload.id;\n        // A `subagent` key is what makes it provider-created; the value varies\n        // (\"review\", {other:…}, {thread_spawn:…}) and only thread_spawn names a\n        // parent. Keying on the full thread_spawn shape classified the other\n        // values as ordinary top-level history and showed them to the user.\n        // A string source (\"cli\", \"vscode\", \"exec\", …) is never a subagent.\n        // Cursor path-based subagent identity wins over Codex source on imports.\n        if (this.provider !== CURSOR_PROVIDER) {\n          const source = entry.payload.source;\n          this.isSubagent = typeof source === \"object\" && source !== null && \"subagent\" in source;\n          const spawn = spawnSource.safeParse(source);\n          this.parentConversationId = spawn.success\n            ? spawn.data.subagent.thread_spawn.parent_thread_id\n            : null;\n        }\n      } else if (\n        (this.provider === CURSOR_PROVIDER && entry.role) ||\n        (this.provider === CLAUDE_CODE_PROVIDER &&\n          !entry.type &&\n          (entry.role === \"user\" || entry.role === \"assistant\"))\n      ) {\n        // Cursor agent-transcripts are `{ role, message }` with no envelope\n        // `type`. Claude lines always carry `type: user|assistant`. Sniffing\n        // here means a cache miss still classifies the file instead of running\n        // it through parseJsonlLine and concluding it has no messages.\n        this.provider = CURSOR_PROVIDER;\n        this.sawIdentity = true;\n      } else if (this.provider === CLAUDE_CODE_PROVIDER && entry.isSidechain !== undefined) {\n        this.sawIdentity = true;\n        if (entry.isSidechain === true && entry.agentId?.trim()) {\n          this.explicitClaudeIdentity = true;\n          this.id = this.fileStem;\n          this.isSubagent = true;\n          this.parentConversationId = entry.sessionId || null;\n        } else if (!this.explicitClaudeIdentity) {\n          this.isSubagent = false;\n        }\n      }\n    }\n    const message =\n      this.provider === CODEX_CLI_PROVIDER\n        ? parseCodexJsonlLine(raw)\n        : this.provider === CURSOR_PROVIDER\n          ? parseCursorJsonlLine(raw)\n          : parseJsonlLine(raw, this.state);\n    if (message?.role === \"user\" && isCodexInjectedContext(message.text ?? \"\")) return null;\n    if (message) this.hasMessages = true;\n    return message;\n  }\n}\n\n/** Bounded memory, using exactly the adapters used by conversation detail. */\nexport function classifyConversationFile(filePath: string, provider?: ProviderName) {\n  const classifier = new ConversationClassifier(filePath, provider ?? CLAUDE_CODE_PROVIDER);\n  const fd = openSync(filePath, \"r\");\n  try {\n    const buffer = Buffer.alloc(64 * 1024);\n    const decoder = new StringDecoder(\"utf8\");\n    let pending = \"\";\n    for (;;) {\n      const length = readSync(fd, buffer, 0, buffer.length, null);\n      if (length === 0) break;\n      pending += decoder.write(buffer.subarray(0, length));\n      let end = pending.indexOf(\"\\n\");\n      while (end >= 0) {\n        classifier.append(pending.slice(0, end));\n        pending = pending.slice(end + 1);\n        end = pending.indexOf(\"\\n\");\n      }\n      // Every scan reads every transcript on the box, so stopping at the first\n      // line that settles both facts is the difference between reading a few\n      // hundred bytes and reading the whole corpus.\n      if (classifier.settled) return classifier;\n    }\n    pending += decoder.end();\n    if (pending.trim()) classifier.append(pending);\n    return classifier;\n  } finally {\n    closeSync(fd);\n  }\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\nimport {\n  isCodexInjectedContext as isCodexFormatPreamble,\n  parseCodexJsonlLine,\n} from \"@threadbase-sh/scanner\";\nimport type { NormalizeResult } from \"../services/providers/capabilities\";\nimport { toClaudeShapedLine } from \"./claudeShapedLine\";\nimport { classifyCursorLine, isCursorTranscriptLine } from \"./cursorConversationLine\";\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// Rollout envelope types this adapter understands. Anything outside this set is\n// reported as `unknown` rather than dropped — a Codex release that renames or\n// adds an envelope must be visible, not silently render an empty conversation.\n// See docs/architecture/2026-07-24-provider-compatibility.md.\nconst KNOWN_CODEX_TYPES = new Set([\"response_item\", \"event_msg\", \"session_meta\", \"turn_context\"]);\n\n/**\n * Classify one Codex rollout line (C2).\n *\n * Distinguishes \"recognized, deliberately not rendered\" (`ignored`) from \"this\n * adapter has never seen this shape\" (`unknown`). Both were `null` before, which\n * is why provider schema drift used to surface as an empty screen with no error.\n *\n * `index` is the line's message_index in its FILE, which is what the leading-turn\n * bound needs. The default of 0 treats an unknown position as leading, so a\n * caller that cannot say keeps the injected-context filter rather than\n * rendering a fake AGENTS.md bubble.\n */\nexport function classifyCodexLine(line: string, index = 0): NormalizeResult {\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 { kind: \"unknown\", raw: line, reason: \"line is not valid JSON\" };\n  }\n\n  if (typeof entry.type !== \"string\" || !KNOWN_CODEX_TYPES.has(entry.type)) {\n    return {\n      kind: \"unknown\",\n      raw: line,\n      reason: `unrecognized rollout envelope type: ${String(entry.type)}`,\n    };\n  }\n\n  // Recognized envelopes that legitimately carry no renderable chat.\n  // event_msg duplicates each turn; session_meta/turn_context are headers.\n  if (entry.type !== \"response_item\") {\n    return { kind: \"ignored\", reason: `${entry.type} carries no chat content` };\n  }\n\n  const payload = entry.payload;\n  if (payload?.type && CODEX_BLOCK_PAYLOADS.has(payload.type)) {\n    return classifyCodexBlockItem(line, entry, payload.type);\n  }\n  if (payload?.type !== \"message\") {\n    return { kind: \"ignored\", reason: `response_item payload is ${String(payload?.type)}` };\n  }\n\n  const role = payload.role;\n  if (role !== \"user\" && role !== \"assistant\") {\n    // `developer` and friends are known Codex roles we deliberately hide.\n    return { kind: \"ignored\", reason: `role ${String(role)} is not rendered` };\n  }\n\n  const text = extractCodexText(payload.content);\n  if (!text) {\n    return { kind: \"ignored\", reason: \"message has no extractable text\" };\n  }\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 — but only at the\n  // head, the same bound REST applies, or a user's pasted text vanishes live.\n  if (isLeadingInjectedContext(index, role, text)) {\n    return { kind: \"ignored\", reason: \"synthetic injected context\" };\n  }\n\n  return { kind: \"message\", line: buildClaudeShapedLine(entry, payload, role, text) };\n}\n\n// Tool calls, their outputs and reasoning. The scanner owns how these render\n// (the same parse serves them over REST), so the live line is built from its\n// result rather than a second reading of the rollout format.\nconst CODEX_BLOCK_PAYLOADS = new Set([\n  \"function_call\",\n  \"custom_tool_call\",\n  \"web_search_call\",\n  \"function_call_output\",\n  \"custom_tool_call_output\",\n  \"reasoning\",\n]);\n\nfunction classifyCodexBlockItem(\n  line: string,\n  entry: { timestamp?: string; payload?: { id?: string; call_id?: string } },\n  payloadType: string,\n): NormalizeResult {\n  const message = parseCodexJsonlLine(line);\n  if (!message) {\n    return { kind: \"ignored\", reason: `${payloadType} has nothing renderable` };\n  }\n  const uuid =\n    message.uuid ??\n    `codex-${payloadType}-${entry.timestamp ?? \"\"}-${entry.payload?.call_id ?? hashPrefix(line)}`;\n  return { kind: \"message\", line: toClaudeShapedLine(message, uuid) };\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 *\n * Thin wrapper over classifyCodexLine so existing callers keep their signature.\n * New code should prefer classifyCodexLine, which explains WHY a line produced\n * nothing.\n */\nexport function normalizeCodexLineToClaudeShape(line: string, index = 0): string | null {\n  const result = classifyCodexLine(line, index);\n  return result.kind === \"message\" ? result.line : null;\n}\n\nfunction buildClaudeShapedLine(\n  entry: { timestamp?: string },\n  payload: { id?: string },\n  role: \"user\" | \"assistant\",\n  text: string,\n): string {\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 and Cursor\n * batches are normalized (and filtered); Claude batches pass through unchanged.\n *\n * `seqs` are the offset index's message_index per line (extendMessageIndex),\n * and they are the only position this batch can be trusted with: a batch is one\n * watcher read, which may start anywhere in the file, so \"first in batch\" is not\n * \"first in conversation\". The index numbers from its persisted\n * last_message_index and declines a read that does not start where it left off,\n * so a seq is a file position, never a batch-relative guess. A line with no seq\n * (the index dropped it, cannot index this file, or declined the read) falls\n * back to leading, which keeps the injected-context filter.\n *\n * The returned `seqs` stay parallel to the returned `lines` — filtered with\n * them — or are null when the caller passed none that line up with `lines`.\n */\nexport function toClientConversationLines(\n  lines: string[],\n  seqs?: (number | null)[] | null,\n): { lines: string[]; seqs: (number | null)[] | null } {\n  const positions = seqs?.length === lines.length ? seqs : null;\n  // Heuristic: the first recognisable line decides the batch's provider (a\n  // mixed batch shouldn't happen — one file, one provider).\n  const classify = lines.some(isCodexRolloutLine)\n    ? (line: string, i: number) => classifyCodexLine(line, positions?.[i] ?? 0)\n    : lines.some(isCursorTranscriptLine)\n      ? (line: string, i: number) => classifyCursorLine(line, positions?.[i])\n      : null;\n  if (!classify) return { lines, seqs: positions };\n  const out: string[] = [];\n  const outSeqs: (number | null)[] = [];\n  for (const [i, line] of lines.entries()) {\n    const result = classify(line, i);\n    if (result.kind !== \"message\") continue;\n    out.push(result.line);\n    outSeqs.push(positions?.[i] ?? null);\n  }\n  return { lines: out, seqs: positions ? outSeqs : null };\n}\n\n/**\n * Prompts THIS server injects, passed as Codex CLI argv because there is no\n * --system-prompt flag. DEFAULT_SYSTEM_PROMPT + BROWSE_SYSTEM_PROMPT land in the\n * rollout as `role: user`.\n *\n * Deliberately NOT in the scanner: recognising our own output is not Codex\n * format knowledge, and a general rollout reader has no business knowing what\n * this server says.\n */\nfunction isStreamerInjectedPrompt(text: string): boolean {\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  return text.includes(\n    \"Do not read, write, or execute commands that access files or directories outside this boundary\",\n  );\n}\n\n/**\n * Text that is machinery rather than a turn the user typed.\n *\n * Two concerns, one question. The Codex-format preambles (AGENTS.md dumps,\n * sandbox blobs) come from the scanner, which owns the rollout format and\n * applies the same predicate when it counts messages — importing it is what\n * keeps its index space and ours identical. The argv prompts above are ours.\n *\n * Callers MUST bound this to the leading turn (see isLeadingInjectedContext).\n */\nexport function isCodexInjectedContext(text: string): boolean {\n  return isCodexFormatPreamble(text) || isStreamerInjectedPrompt(text);\n}\n\n/**\n * The form every caller should actually use.\n *\n * Codex only injects at the head of a rollout, so the skip is bounded to the\n * leading turn — otherwise a human who pastes instruction text mid-conversation\n * has their message silently eaten, and message_count stops matching what they\n * can see. The scanner bounds it the same way (`acc.messageCount === 0`), and\n * the two must agree or the index counts in a different space than we serve.\n */\nexport function isLeadingInjectedContext(\n  index: number,\n  role: string,\n  text: string | undefined,\n): boolean {\n  return index === 0 && role === \"user\" && typeof text === \"string\" && isCodexInjectedContext(text);\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 type { ConversationMessage } from \"@threadbase-sh/scanner\";\n\n/**\n * A scanner-parsed message → one Claude Code JSONL line, the only shape\n * mobile's live parser (`parseLineToMessage`) renders.\n *\n * Content comes from the same scanner parse that serves the message over REST,\n * so a live bubble and its reloaded copy carry the same blocks and ids:\n * thinking, text, tool_use (toolUseBlocks), tool_result (toolResults).\n */\nexport function toClaudeShapedLine(message: ConversationMessage, uuid: string): string {\n  const content: unknown[] = [];\n  if (message.isThinking && message.thinkingContent) {\n    content.push({ type: \"thinking\", thinking: message.thinkingContent });\n  }\n  if (message.text) content.push({ type: \"text\", text: message.text });\n  for (const b of message.metadata?.toolUseBlocks ?? []) {\n    content.push({ type: \"tool_use\", id: b.id, name: b.name, input: b.input });\n  }\n  for (const r of message.metadata?.toolResults ?? []) {\n    content.push({\n      type: \"tool_result\",\n      tool_use_id: r.toolUseId,\n      content: toolResultText(r.content),\n      is_error: r.isError ?? false,\n    });\n  }\n  return JSON.stringify({\n    type: message.role,\n    uuid,\n    timestamp: message.timestamp || new Date().toISOString(),\n    message: { role: message.role, content },\n  });\n}\n\n/**\n * A scanner tool result's content as the string a client renders. Codex results\n * carry their program output as `content.output`: send that verbatim, the way a\n * live Claude tool_result carries its text, not a JSON object whose newlines\n * arrive escaped. Every other result keeps its JSON form.\n */\nexport function toolResultText(content: Record<string, unknown> | undefined): string {\n  const output = content?.output;\n  return typeof output === \"string\" ? output : JSON.stringify(content);\n}\n","/**\n * Cursor CLI agent-transcripts JSONL → Claude-shaped conversation lines.\n *\n * Cursor writes `{ role, message: { content: [{ type, text } | { type:\n * \"tool_use\", name, input }] } }` (no `type` envelope field, no per-line CLI\n * version), plus `{ type: \"turn_ended\" }` markers. Mobile's parser only\n * understands Claude Code JSONL, so chat-bearing lines are rewritten to that\n * shape from the scanner's parse — the same one REST serves them from.\n */\n\nimport { parseCursorJsonlLine } from \"@threadbase-sh/scanner\";\nimport type { NormalizeResult } from \"../services/providers/capabilities\";\nimport { toClaudeShapedLine } from \"./claudeShapedLine\";\n\nconst KNOWN_ROLES = new Set([\"user\", \"assistant\", \"tool\", \"system\"]);\n// Envelope types Cursor writes that carry no chat content.\nconst KNOWN_MARKER_TYPES = new Set([\"turn_ended\"]);\n\ntype CursorEntry = {\n  role?: string;\n  type?: string;\n  message?: { content?: unknown; role?: string };\n};\n\nfunction parseEntry(line: string): CursorEntry | null {\n  try {\n    const entry = JSON.parse(line);\n    return entry && typeof entry === \"object\" ? entry : null;\n  } catch {\n    return null;\n  }\n}\n\n/** True when a line is Cursor agent-transcripts shaped (not Claude or Codex JSONL). */\nexport function isCursorTranscriptLine(line: string): boolean {\n  const entry = parseEntry(line);\n  if (!entry) return false;\n  if (typeof entry.type === \"string\") return KNOWN_MARKER_TYPES.has(entry.type);\n  return typeof entry.role === \"string\" && KNOWN_ROLES.has(entry.role);\n}\n\n/**\n * `index` is the line's message index in its file (the offset index's seq),\n * when the caller knows it. It goes into the message uuid, which is how a\n * client matches this live copy against the same message served over REST.\n */\nexport function classifyCursorLine(line: string, index?: number | null): NormalizeResult {\n  const entry = parseEntry(line);\n  if (!entry) return { kind: \"unknown\", raw: line, reason: \"line is not valid JSON\" };\n\n  if (typeof entry.type === \"string\" && KNOWN_MARKER_TYPES.has(entry.type)) {\n    return { kind: \"ignored\", reason: `${entry.type} carries no chat content` };\n  }\n\n  const role = entry.role ?? entry.message?.role;\n  if (typeof role !== \"string\" || !KNOWN_ROLES.has(role)) {\n    return {\n      kind: \"unknown\",\n      raw: line,\n      reason: `unrecognized cursor transcript role: ${String(role)}`,\n    };\n  }\n\n  if (role !== \"user\" && role !== \"assistant\") {\n    return { kind: \"ignored\", reason: `role ${role} is not rendered` };\n  }\n\n  const message = parseCursorJsonlLine(line, index ?? undefined);\n  if (!message) {\n    return { kind: \"ignored\", reason: \"message has no text or tool calls\" };\n  }\n\n  // The scanner derives the uuid (Cursor lines carry none) from the line and\n  // its index — the same call REST's parse makes, so the ids match.\n  return { kind: \"message\", line: toClaudeShapedLine(message, message.uuid ?? \"\") };\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 (TypeScript), claude-mem, hook-spawned\n//                     automation\n//   - sdk-py       → Claude Agent SDK (Python), same automation, different\n//                     binding — observed on hook-spawned review agents\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([\n  \"sdk-cli\",\n  \"sdk-py\",\n  \"claude-vscode\",\n]);\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 { normalize } from \"path\";\n\n/**\n * File-path identity across the scanner/cache boundary.\n *\n * Two path forms exist in this process and they are NOT interchangeable on\n * Windows:\n *\n *   - **canonical** (forward slashes) — every cache key: conversation_meta\n *     .file_path, the fileIndex, watcher keys, externalTails.\n *   - **native** (the platform separator) — what the scanner emits in\n *     ConversationMeta.filePath, and what chokidar delivers.\n *\n * Both writers of conversation_meta.file_path (upsertFull, insertSkeleton)\n * store the canonical form, so the storage invariant is \"cache keys are always\n * canonical\". The scanner has no such rule: it emits native paths, verified by\n * measurement on win32.\n *\n * The rule when the two meet: **normalize for the comparison, emit in the\n * consumer's form.** Canonicalize both sides to join them, then key the result\n * with whichever form the code receiving it will look up by. Joining a\n * scanner-derived path against a cache-derived one without normalizing fails\n * silently — an empty map or a false, never an exception — and is invisible on\n * POSIX, where the two forms are identical.\n */\n\n/**\n * Canonicalize a path for use as a cache/DB/watcher key.\n *\n * Rules:\n *   - Trim surrounding whitespace\n *   - Convert backslashes to forward slashes\n *\n * Do NOT lowercase: even on case-insensitive Windows filesystems, lowercasing\n * would diverge the key from the scanner's case-preserved file_path. Only the\n * separator is normalized; case is preserved.\n */\nexport function canonicalizeFilePath(filePath: string): string {\n  return filePath.trim().replace(/\\\\/g, \"/\");\n}\n\n/**\n * Convert a canonical path back to the platform-native form.\n *\n * Use only when handing a path *back* to something that keys by native paths —\n * in practice the scanner's statCache, whose entries it looks up by its own\n * ConversationMeta.filePath. A no-op on POSIX; on win32 `normalize` rewrites\n * forward slashes to backslashes.\n */\nexport function toNativeFilePath(filePath: string): string {\n  return normalize(filePath.trim());\n}\n\n/**\n * Build the canonical live-path set that `reconcileDeletions` compares against\n * `conversation_meta.file_path`.\n *\n * Exists as a named function rather than an inline `.map()` so the conversion\n * has one place to be tested and one place to be got wrong: the scanner's\n * filePath is native, the rows it is checked against are canonical, and a\n * missed conversion here means every row falls through to the existsSync\n * backstop instead of being recognised as live.\n */\nexport function canonicalLivePathSet(metas: Iterable<{ filePath?: string | null }>): Set<string> {\n  const live = new Set<string>();\n  for (const meta of metas) {\n    if (meta.filePath) live.add(canonicalizeFilePath(meta.filePath));\n  }\n  return live;\n}\n\n/**\n * Join scanner metadata against canonically-keyed stat rows, producing the map\n * the scanner expects back as `statCache`.\n *\n * This is the boundary rule made concrete: the lookup is done in canonical\n * form (because that is how the rows are keyed) while the result is keyed by\n * the meta's own native path (because that is what the scanner looks up by).\n * Getting either half wrong yields an empty or unusable map and silently\n * disables the scanner's stat cache — no error, just a full re-parse of every\n * conversation on every rescan.\n */\nexport function joinStatCacheByNativePath<TStat, TMeta extends { filePath?: string | null }>(\n  metas: Iterable<TMeta>,\n  canonicalStats: Map<string, TStat>,\n): Map<string, { stat: TStat; meta: TMeta }> {\n  const joined = new Map<string, { stat: TStat; meta: TMeta }>();\n  for (const meta of metas) {\n    if (!meta.filePath) continue;\n    const stat = canonicalStats.get(canonicalizeFilePath(meta.filePath));\n    if (stat) joined.set(meta.filePath, { stat, meta });\n  }\n  return joined;\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","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 { createReadStream } from \"node:fs\";\nimport { createInterface } from \"node:readline\";\nimport { type ConversationMessage, parseCodexJsonlLine } from \"@threadbase-sh/scanner\";\n\n/**\n * A conversation whose history begins in ANOTHER file.\n *\n * `codex fork` does not copy the source transcript. It writes a rollout holding\n * only `session_meta`, pointing back at the parent with `forked_from_id` and\n * `forked_from_ordinal_exclusive` — Codex reads the parent up to the cut and\n * carries on. So the agent has the full context while the transcript we serve\n * has none: the fork is not empty, it is elsewhere.\n *\n * Nothing here is Codex-specific by contract. The link is \"this conversation\n * inherits a prefix of that one\", and a second producer would only need its own\n * `readForkLinkFromLine`.\n */\n\n/** Deepest chain (fork of a fork of a …) we will resolve. */\nconst MAX_CHAIN_DEPTH = 8;\n\n/**\n * Resolved prefixes held in memory. Each entry pins a message array, so this is\n * a memory/parse trade: past the cap, every eviction costs a full re-parse of a\n * parent prefix. 8 was low enough that someone with a handful of forks open\n * thrashed it. Raised rather than replaced with a windowed read — see\n * \"Why the split-window read was dropped\" in\n * docs/plans/2026-09-07-inherited-conversation-history.md.\n */\nconst PREFIX_CACHE_MAX = 32;\n\nexport interface InheritedLink {\n  /** Provider-side id of the conversation this one continues. */\n  sourceId: string;\n  /**\n   * The cut, as the provider writes it: a LINE ordinal, exclusive.\n   *\n   * Not a message index. In a real rollout, ordinals count every envelope line\n   * — token counts, task events, turn context — while only a fraction render as\n   * messages. Translating one to the other is `countMessagesBeforeOrdinal`, and\n   * skipping that translation is the silent failure this module exists to\n   * prevent.\n   */\n  ordinalExclusive: number;\n  forkedAt: string | null;\n}\n\nexport interface InheritedHistory extends InheritedLink {\n  sourceFilePath: string | null;\n  /** The inherited messages, oldest first. Empty when the source is unreadable. */\n  messages: ConversationMessage[];\n  /** Set when the source could not be read; `messages` is then empty. */\n  unavailableReason: \"source_missing\" | null;\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | null {\n  return typeof value === \"object\" && value !== null ? (value as Record<string, unknown>) : null;\n}\n\n/**\n * Read a fork link out of one JSONL line, or null if it carries none.\n *\n * Tolerant by construction, like every provider parser here: malformed JSON, a\n * missing payload, or a partial link (an id with no ordinal) all mean \"no\n * link\", never a throw. A conversation that is not a fork is the overwhelmingly\n * common case and must cost nothing.\n */\nexport function readForkLinkFromLine(line: string): InheritedLink | null {\n  let entry: Record<string, unknown> | null;\n  try {\n    entry = asRecord(JSON.parse(line));\n  } catch {\n    return null;\n  }\n  if (entry?.type !== \"session_meta\") return null;\n\n  const payload = asRecord(entry.payload);\n  if (!payload) return null;\n\n  const sourceId = payload.forked_from_id;\n  const ordinal = payload.forked_from_ordinal_exclusive;\n  if (typeof sourceId !== \"string\" || sourceId.length === 0) return null;\n  if (typeof ordinal !== \"number\" || !Number.isFinite(ordinal) || ordinal < 0) return null;\n\n  const forkedAt = typeof payload.timestamp === \"string\" ? payload.timestamp : null;\n  return { sourceId, ordinalExclusive: ordinal, forkedAt };\n}\n\n/**\n * Whether a file is a fork, and of what, keyed by path.\n *\n * A rollout's first line is written once, at creation, and never rewritten — and\n * the path carries the session uuid, so a path cannot come to mean a different\n * conversation. The answer is therefore permanent, including the \"not a fork\"\n * answer, which is the one nearly every lookup gets.\n *\n * This is what keeps the feature off the hot path: without it every Codex\n * conversation request re-opens the file to re-learn something that cannot have\n * changed, and that latency is not free — it was enough to shift a pre-existing\n * refresh-throttle race in the scanner manager from rare to routine.\n */\nconst linkCache = new Map<string, InheritedLink | null>();\nconst LINK_CACHE_MAX = 512;\n\n/** Test seam: the caches are process-global and would otherwise leak across tests. */\nexport function clearInheritedLinkCache(): void {\n  linkCache.clear();\n}\n\n/**\n * A fork declares itself on its FIRST line, so this reads one line and stops.\n * Returns null for a file that isn't there — an absent file is not a fork.\n */\nexport async function readForkLink(filePath: string): Promise<InheritedLink | null> {\n  const cached = linkCache.get(filePath);\n  if (cached !== undefined) return cached;\n  const link = await readForkLinkUncached(filePath);\n  if (linkCache.size >= LINK_CACHE_MAX) {\n    const oldest = linkCache.keys().next().value;\n    if (oldest !== undefined) linkCache.delete(oldest);\n  }\n  linkCache.set(filePath, link);\n  return link;\n}\n\nasync function readForkLinkUncached(filePath: string): Promise<InheritedLink | null> {\n  return new Promise((resolve) => {\n    let settled = false;\n    const done = (value: InheritedLink | null) => {\n      if (settled) return;\n      settled = true;\n      resolve(value);\n    };\n    let stream: ReturnType<typeof createReadStream>;\n    try {\n      stream = createReadStream(filePath);\n    } catch {\n      done(null);\n      return;\n    }\n    const rl = createInterface({ input: stream, crlfDelay: Infinity });\n    // Closing the readline interface does NOT close the file it reads from, and\n    // this runs on every Codex conversation request — so the stream is destroyed\n    // explicitly. Without it each request leaks a descriptor until GC.\n    const finish = (value: InheritedLink | null) => {\n      // Settle BEFORE closing: rl.close() emits 'close' synchronously, and that\n      // handler resolves with null — so cleaning up first throws the answer away.\n      done(value);\n      rl.close();\n      stream.destroy();\n    };\n    rl.on(\"line\", (line) => {\n      if (line.trim()) finish(readForkLinkFromLine(line));\n    });\n    rl.on(\"close\", () => done(null));\n    rl.on(\"error\", () => finish(null));\n    stream.on(\"error\", () => finish(null));\n  });\n}\n\n/**\n * The messages of `filePath` that precede line ordinal `ordinalExclusive`.\n *\n * The message decision is `parseCodexJsonlLine`, imported rather than\n * reimplemented: it is the same rule the scanner renders with, so the count\n * here cannot drift from what the conversation actually shows. Re-deriving it\n * locally is how a fork ends up displaying turns it never inherited.\n *\n * `ordinal` is read off each line, with the line counter as the fallback for a\n * writer that omits it — in observed rollouts the two are identical (a 331-line\n * file carries ordinals 0–330), so the fallback is a degrade path, not a guess.\n */\nexport async function readMessagesBeforeOrdinal(\n  filePath: string,\n  ordinalExclusive: number,\n): Promise<ConversationMessage[] | null> {\n  const messages: ConversationMessage[] = [];\n  let lineNumber = -1;\n  const stream = createReadStream(filePath);\n  try {\n    const rl = createInterface({ input: stream, crlfDelay: Infinity });\n    for await (const line of rl) {\n      if (!line.trim()) continue;\n      lineNumber++;\n      let ordinal = lineNumber;\n      try {\n        const entry = asRecord(JSON.parse(line));\n        if (entry && typeof entry.ordinal === \"number\") ordinal = entry.ordinal;\n      } catch {\n        // Unparseable line: it renders as nothing either way, and the line\n        // counter still advances, so the cut stays in the right place.\n      }\n      if (ordinal >= ordinalExclusive) {\n        rl.close();\n        break;\n      }\n      const message = parseCodexJsonlLine(line);\n      if (message) messages.push(message);\n    }\n  } catch {\n    return null;\n  } finally {\n    // Same reason as readForkLink: breaking out of the loop leaves the file\n    // open, and this one stops early by design.\n    stream.destroy();\n  }\n  return messages;\n}\n\n/**\n * Prefixes are immutable — everything before the cut is frozen even while the\n * source file keeps growing — so the key needs no mtime and a hit stays valid\n * for the life of the process. Bounded because each entry pins its messages.\n */\nconst prefixCache = new Map<string, ConversationMessage[]>();\n\nfunction cachePrefix(key: string, messages: ConversationMessage[]): void {\n  if (prefixCache.size >= PREFIX_CACHE_MAX) {\n    const oldest = prefixCache.keys().next().value;\n    if (oldest !== undefined) prefixCache.delete(oldest);\n  }\n  prefixCache.set(key, messages);\n}\n\n/** Test seam: the cache is process-global and would otherwise leak across tests. */\nexport function clearInheritedPrefixCache(): void {\n  prefixCache.clear();\n  linkCache.clear();\n}\n\nexport interface ResolveInheritedOptions {\n  /** The conversation being served — the fork, not the source. */\n  filePath: string;\n  /** Resolve a conversation id to its file, or null when it can't be found. */\n  locateSource: (conversationId: string) => Promise<string | null>;\n}\n\n/**\n * Resolve the full inherited prefix for a conversation, following a chain of\n * forks oldest-first.\n *\n * Returns null when the conversation inherits nothing, which is the normal\n * case and costs one line read.\n *\n * A source that cannot be found does NOT fail the request: the fork's own\n * messages are still served, with `unavailableReason` set so the client can say\n * the earlier history is unavailable instead of silently showing a truncated\n * conversation. That degrade is what makes it safe to keep pointing at the\n * source file rather than copying it.\n */\nexport async function resolveInheritedHistory(\n  opts: ResolveInheritedOptions,\n): Promise<InheritedHistory | null> {\n  const link = await readForkLink(opts.filePath);\n  if (!link) return null;\n\n  const messages: ConversationMessage[] = [];\n  const seen = new Set<string>();\n  let current: InheritedLink | null = link;\n  let sourceFilePath: string | null = null;\n  let unavailableReason: \"source_missing\" | null = null;\n  // Each hop's prefix is older than the last, so they are collected newest-first\n  // and reversed once at the end.\n  const segments: ConversationMessage[][] = [];\n\n  for (let depth = 0; current && depth < MAX_CHAIN_DEPTH; depth++) {\n    // A cycle is impossible in a well-formed chain and unbounded work if it\n    // happens anyway. Refuse rather than recurse.\n    if (seen.has(current.sourceId)) {\n      unavailableReason = \"source_missing\";\n      break;\n    }\n    seen.add(current.sourceId);\n\n    const path = await opts.locateSource(current.sourceId);\n    if (depth === 0) sourceFilePath = path;\n    if (!path) {\n      unavailableReason = \"source_missing\";\n      break;\n    }\n\n    const key = `${path}::${current.ordinalExclusive}`;\n    let segment = prefixCache.get(key);\n    if (!segment) {\n      const read = await readMessagesBeforeOrdinal(path, current.ordinalExclusive);\n      if (!read) {\n        unavailableReason = \"source_missing\";\n        break;\n      }\n      segment = read;\n      cachePrefix(key, segment);\n    }\n    segments.push(segment);\n\n    const next: InheritedLink | null = await readForkLink(path);\n    // A source that is itself a fork contributes only the part of ITS history\n    // that precedes this cut — never more than the hop below already took.\n    current = next\n      ? { ...next, ordinalExclusive: Math.min(next.ordinalExclusive, current.ordinalExclusive) }\n      : null;\n  }\n\n  for (let i = segments.length - 1; i >= 0; i--) messages.push(...segments[i]);\n\n  return {\n    ...link,\n    sourceFilePath,\n    messages: unavailableReason ? [] : messages,\n    unavailableReason,\n  };\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 { ProviderName } from \"../../providers\";\nimport { canonicalizeProviderName, isProviderName } from \"../../providers\";\n\n/**\n * Search query parsing, pagination, and filters (C8).\n *\n * `/api/search` returned `{ hasMore: false, offset: 0, total: results.length }`\n * with all three values hardcoded. That was not merely unimplemented — it was\n * actively wrong: the scanner truncates at `limit`, so a query with more matches\n * than the limit reported `hasMore: false` and `total` equal to the truncated\n * count. A client had no way to learn that results were missing, let alone\n * fetch them.\n *\n * The scanner also already computes a relevance `score` and match `snippets`\n * per result, and the endpoint discarded both — so results arrived in an\n * unexplained order with no indication of *why* anything matched.\n */\n\nexport const DEFAULT_SEARCH_LIMIT = 50;\nexport const MAX_SEARCH_LIMIT = 200;\nexport const MAX_QUERY_LENGTH = 256;\n\nexport interface SearchFilters {\n  provider?: ProviderName;\n  projectPath?: string;\n  branch?: string;\n  /** Inclusive lower bound on last activity, epoch ms. */\n  since?: number;\n  /** Inclusive upper bound on last activity, epoch ms. */\n  until?: number;\n}\n\nexport interface ParsedSearchQuery {\n  q: string;\n  limit: number;\n  offset: number;\n  filters: SearchFilters;\n}\n\nexport class SearchQueryError extends Error {\n  constructor(\n    message: string,\n    readonly code: string,\n  ) {\n    super(message);\n  }\n}\n\nfunction intOr(raw: string | null, fallback: number): number {\n  if (raw === null) return fallback;\n  const n = Number.parseInt(raw, 10);\n  return Number.isFinite(n) ? n : fallback;\n}\n\n/**\n * Parse and validate search parameters.\n *\n * Rejects rather than silently clamping an invalid query, so a client that\n * mistypes a filter learns about it instead of receiving plausible-looking\n * results for a query it did not mean. `limit` is the exception: it is clamped,\n * because an over-large limit is a resource question rather than a\n * misunderstanding, and failing a search over it would be unhelpful.\n */\nexport function parseSearchQuery(params: URLSearchParams): ParsedSearchQuery {\n  const q = (params.get(\"q\") ?? \"\").trim();\n  if (!q) {\n    throw new SearchQueryError(\"Missing query parameter: q\", \"invalid_query\");\n  }\n  if (q.length > MAX_QUERY_LENGTH) {\n    throw new SearchQueryError(`Query exceeds ${MAX_QUERY_LENGTH} characters`, \"query_too_long\");\n  }\n\n  const limit = Math.min(\n    Math.max(intOr(params.get(\"limit\"), DEFAULT_SEARCH_LIMIT), 1),\n    MAX_SEARCH_LIMIT,\n  );\n  const offset = Math.max(intOr(params.get(\"offset\"), 0), 0);\n\n  const filters: SearchFilters = {};\n\n  const provider = params.get(\"provider\");\n  if (provider !== null) {\n    if (!isProviderName(provider)) {\n      throw new SearchQueryError(`Unknown provider: ${provider}`, \"invalid_filter\");\n    }\n    filters.provider = canonicalizeProviderName(provider) ?? provider;\n  }\n\n  const projectPath = params.get(\"projectPath\");\n  if (projectPath) filters.projectPath = projectPath;\n\n  const branch = params.get(\"branch\");\n  if (branch) filters.branch = branch;\n\n  for (const [key, field] of [\n    [\"since\", \"since\"],\n    [\"until\", \"until\"],\n  ] as const) {\n    const raw = params.get(key);\n    if (raw === null) continue;\n    const ms = Date.parse(raw);\n    if (Number.isNaN(ms)) {\n      throw new SearchQueryError(`Invalid ${key}: expected an ISO 8601 date`, \"invalid_filter\");\n    }\n    filters[field] = ms;\n  }\n\n  if (filters.since != null && filters.until != null && filters.since > filters.until) {\n    throw new SearchQueryError(\"`since` must not be after `until`\", \"invalid_filter\");\n  }\n\n  return { q, limit, offset, filters };\n}\n\n/** A search result as returned to clients, after adaptation. */\nexport interface AdaptedResult {\n  projectPath?: string;\n  branch?: string;\n  provider?: string;\n  lastActivity?: string | number | null;\n}\n\n/**\n * Apply the filters the scanner cannot express itself.\n *\n * The scanner's SearchOptions supports `provider` but not project, branch, or\n * date bounds, so those are applied here. Filtering after the fact means the\n * scanner's own limit must be raised before slicing — see `handleSearch` — or\n * a filter would silently drop results that a later page should have contained.\n */\nexport function applyFilters<T extends AdaptedResult>(results: T[], filters: SearchFilters): T[] {\n  return results.filter((r) => {\n    if (filters.provider && r.provider !== filters.provider) return false;\n    if (filters.projectPath && r.projectPath !== filters.projectPath) return false;\n    if (filters.branch && r.branch !== filters.branch) return false;\n\n    if (filters.since != null || filters.until != null) {\n      const ts = r.lastActivity == null ? Number.NaN : new Date(r.lastActivity).getTime();\n      // A result with no usable timestamp cannot be shown to satisfy a date\n      // bound, so exclude it rather than guessing.\n      if (Number.isNaN(ts)) return false;\n      if (filters.since != null && ts < filters.since) return false;\n      if (filters.until != null && ts > filters.until) return false;\n    }\n    return true;\n  });\n}\n\nexport interface Page<T> {\n  items: T[];\n  total: number;\n  offset: number;\n  hasMore: boolean;\n}\n\n/**\n * Slice a filtered result set into a page.\n *\n * `total` is the count AFTER filtering and BEFORE slicing, and `hasMore` is\n * derived from it — the two values the previous implementation hardcoded to\n * `results.length` and `false`.\n */\nexport function paginate<T>(results: T[], offset: number, limit: number): Page<T> {\n  const items = results.slice(offset, offset + limit);\n  return {\n    items,\n    total: results.length,\n    offset,\n    hasMore: offset + items.length < results.length,\n  };\n}\n","import { type Conversation, parseCursorJsonlLine } from \"@threadbase-sh/scanner\";\nimport { existsSync, watch as fsWatch, readdirSync, readFileSync, statSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { basename, join } from \"path\";\nimport type { ConversationCache } from \"./conversation-cache\";\nimport { cursorAgentTranscriptsDir } from \"./cursor-transcript-watch\";\nimport type { CacheMetadataRepository } from \"./db/repositories/cacheMetadata.repository\";\nimport type { ConversationsRepository } from \"./db/repositories/conversations.repository\";\nimport type { ManagedSessionsRepository } from \"./db/repositories/managed-sessions.repository\";\nimport type { ProjectsRepository } from \"./db/repositories/projects.repository\";\nimport type { SessionsRepository } from \"./db/repositories/sessions.repository\";\nimport type { LiveSessionManager } from \"./live-session-manager\";\nimport { getLogger } from \"./logger\";\nimport { CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER, CURSOR_PROVIDER } from \"./providers\";\nimport type { ScannerManager } from \"./scanner-manager\";\nimport type { ConversationWatcher } from \"./services/conversations/conversationWatcher\";\nimport type { SessionStore } from \"./session-store\";\nimport type { WSHub } from \"./ws-hub\";\n\n/**\n * How long a transcript watcher keeps waiting for the file its provider will\n * write. Not a measured number: it arrived as `// give up after 2 minutes` in\n * 76cdf9f2, when these watchers existed to re-key a placeholder session id to\n * the real UUID — a mechanism that no longer exists. It survives only as a\n * bound on an abandoned session's watch, and it is deliberately measured from\n * the user's first turn rather than from the spawn, because that turn is what\n * causes the file to exist. Both watchers re-arm it per turn and neither applies\n * it before the first one. See the notes in watchForJsonl/watchForCodexRollout.\n */\nexport const TRANSCRIPT_WATCH_DEADLINE_MS = 120_000;\n\n/**\n * Everything SessionWatchers reads from the server. Collaborators constructed\n * once in the server constructor are passed by reference; the ones opened\n * during listen() (and rebound by the integrity monitor's reset-and-rescan) are\n * thunks, for the same reason ApiDeps passes `cache: () => ConversationCache | null`.\n *\n * `broadcastConversationLines` / `findConversationByUuid` / `ptyAttachedIds`\n * stay late-bound calls back into the server rather than moved code: they are\n * server methods with their own dependencies (and tests spy on them).\n */\nexport type SessionWatchersDeps = {\n  ptyManager: LiveSessionManager;\n  sessionStore: SessionStore;\n  wsHub: WSHub;\n  fileWatcher: ConversationWatcher;\n  /** sessionId → JSONL filePath. Owned by the server; mutated in place here. */\n  sessionFileMap: Map<string, string>;\n  scannerManager: ScannerManager;\n  codexRoots: string[];\n  /** Cursor project roots (`~/.cursor/projects`); empty disables Cursor binding. */\n  cursorRoots: string[];\n  cache: () => ConversationCache | null;\n  projectsRepo: () => ProjectsRepository | null;\n  conversationsRepo: () => ConversationsRepository | null;\n  sessionsRepo: () => SessionsRepository | null;\n  cacheMetadataRepo: () => CacheMetadataRepository | null;\n  managedSessionsRepo: () => ManagedSessionsRepository | null;\n  findConversationByUuid: (uuid: string) => Promise<Conversation | null>;\n  broadcastConversationLines: (\n    sessionId: string,\n    lines: string[],\n    seqs?: (number | null)[] | null,\n  ) => void;\n  ptyAttachedIds: () => Set<string>;\n};\n\n/**\n * Binds a live session to the transcript file its provider writes: finds the\n * JSONL (Claude), rollout (Codex), or agent-transcripts run (Cursor), starts\n * the tail watcher, replays whatever was written before the watcher attached,\n * and links the session to its project.\n *\n * Extracted from StreamerServer so watcher work stops editing the server file\n * (see docs/plans/2026-07-12-server-ts-split.md, PR 5).\n */\nexport class SessionWatchers {\n  private log = getLogger(\"server\");\n\n  constructor(private deps: SessionWatchersDeps) {}\n\n  // ─── Project linking ─────────────────────────────────────────────\n\n  linkSessionToProject(sessionId: string, projectPath: string, filePath: string): void {\n    const projectsRepo = this.deps.projectsRepo();\n    const conversationsRepo = this.deps.conversationsRepo();\n    const sessionsRepo = this.deps.sessionsRepo();\n    const cache = this.deps.cache();\n    if (!projectsRepo || !conversationsRepo || !sessionsRepo || !cache) {\n      return;\n    }\n    try {\n      const project = 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 (cache.hasConversation(sessionId)) {\n        conversationsRepo.updateConversationProjectId({\n          conversationId: sessionId,\n          projectId: project.id,\n        });\n      }\n      sessionsRepo.updateSessionProjectId({\n        sessionId,\n        projectId: project.id,\n      });\n      const cacheMetadataRepo = this.deps.cacheMetadataRepo();\n      if (cacheMetadataRepo) {\n        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  // `historyId` is the id the provider filed the history under, which for a\n  // fresh Codex session is its rollout id rather than our placeholder. The map\n  // stays keyed by `sessionId` — that is what broadcasts resolve against.\n  async watchConversationFile(sessionId: string, historyId = sessionId): Promise<void> {\n    try {\n      const conversation = await this.deps.findConversationByUuid(historyId);\n      if (conversation?.filePath) {\n        this.deps.sessionFileMap.set(sessionId, conversation.filePath);\n        this.deps.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  // Read just the `sessionId` field from a JSONL's first line, used by the\n  // watchForJsonl fallback to confirm a candidate file's identity before\n  // binding it. Reads only up to the first newline so a large actively-written\n  // file isn't slurped in full.\n  readFirstLineSessionId(filePath: string): string | null {\n    try {\n      const content = readFileSync(filePath, \"utf8\");\n      const nl = content.indexOf(\"\\n\");\n      const firstLine = nl === -1 ? content : content.slice(0, nl);\n      if (!firstLine.trim()) return null;\n      const obj = JSON.parse(firstLine);\n      return typeof obj.sessionId === \"string\" ? obj.sessionId : null;\n    } catch {\n      return null;\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  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 armedAt = Date.now();\n    let deadline = armedAt + TRANSCRIPT_WATCH_DEADLINE_MS;\n    let seenPrompts = 0;\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.deps.ptyManager.hasSession(sessionId)) {\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: the `${sessionId}.jsonl` file hasn't appeared yet. Only bind a\n      // candidate whose identity actually matches this session — its filename\n      // stem OR its first-line `sessionId` field must equal our session id.\n      // Claude 'resume' APPENDS to the SAME file with the SAME sessionId\n      // (observed on Claude Code v2.1.215); the previous \"resume writes a NEW\n      // UUID file\" assumption let this bind whichever JSONL was most recently\n      // touched — capturing an actively-written FOREIGN conversation and\n      // re-broadcasting its whole transcript under our session id. mtime is now\n      // only a tiebreaker among already-matching candidates.\n      if (!resolvedFilePath && existsSync(projectsDir)) {\n        try {\n          const now = Date.now();\n          const match = 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            .filter(\n              ({ f }) =>\n                basename(f, \".jsonl\") === sessionId ||\n                this.readFirstLineSessionId(join(projectsDir, f)) === sessionId,\n            )\n            .sort((a, b) => b.mtime - a.mtime)[0];\n          if (match) resolvedFilePath = join(projectsDir, match.f);\n        } catch {\n          /* ignore */\n        }\n      }\n\n      if (!resolvedFilePath) {\n        // The deadline bounds how long we WAIT, not whether we accept a file\n        // that has arrived. Claude writes <sessionId>.jsonl only on the user's\n        // FIRST turn, so what this races is human think time: across a 20.5-day\n        // production log the gap from pty.ready to the first prompt ran 3.6s to\n        // 405.7s, and 17% of spawns were past 120s.\n        //\n        // So the deadline runs from the TURN, never from the spawn, and before\n        // the first turn it does not run at all. Two separate reasons:\n        //\n        //  - Before any prompt the file CANNOT exist yet, so failing to find it\n        //    is not evidence of anything and must not end the watch. This is\n        //    what `fsWatch(projectsDir, …)` made dangerous: the handle is on the\n        //    whole project directory (406 transcripts in one real project), so\n        //    ANY neighbouring session writing its own JSONL fired tryWire, found\n        //    ours absent, and closed our watch for good — after which the user's\n        //    own first prompt bound nothing. The session then silently loses live\n        //    line streaming; `locateJsonlPath` rung 4 still answers REST reads,\n        //    which is why this never surfaced as a visible failure.\n        //  - What the deadline was ever bounding is an ABANDONED session's watch,\n        //    and abandonment is already covered above: `hasSession()` ends the\n        //    watch when the PTY goes (hold, grace, or the 6h idle reaper).\n        //\n        // Past the first turn the deadline is meaningful again — there the file\n        // is genuinely overdue — and it re-arms per turn, matching the identical\n        // reasoning in watchForCodexRollout.\n        const prompts = this.deps.sessionStore.getManaged(sessionId)?.promptCount ?? 0;\n        if (prompts > seenPrompts) {\n          seenPrompts = prompts;\n          deadline = Date.now() + TRANSCRIPT_WATCH_DEADLINE_MS;\n        }\n        if (prompts === 0) return;\n        if (Date.now() > deadline) {\n          this.log.warn(\n            `[startFresh] gave up watching for the JSONL of ${sessionId}`,\n            {\n              event: \"session.transcript_watch_expired\",\n              sessionId,\n              provider: CLAUDE_CODE_PROVIDER,\n              projectPath,\n              waitedMs: Date.now() - armedAt,\n            },\n            \"pino\",\n          );\n          cleanup();\n        }\n        return;\n      }\n\n      cleanup();\n      this.deps.sessionFileMap.set(sessionId, 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. Dump BEFORE\n      // starting the watcher: fileWatcher.watch() seeds its byte offset at the\n      // file's current size, so seeding before the dump makes the next append\n      // re-ship every dumped line (double broadcast). Seeding after the dump\n      // means the watcher starts at the post-dump EOF.\n      try {\n        const existing = readFileSync(resolvedFilePath, \"utf8\").split(\"\\n\").filter(Boolean);\n        if (existing.length > 0) {\n          this.deps.broadcastConversationLines(sessionId, existing);\n        }\n      } catch {\n        /* ignore — file may not be readable yet; watcher will catch future writes */\n      }\n      this.deps.fileWatcher.watch(resolvedFilePath);\n\n      this.deps.scannerManager.markStaleOrDrop();\n      this.linkSessionToProject(sessionId, projectPath, resolvedFilePath);\n      this.deps.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.deps.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.\n  //\n  // The rollout file is created LAZILY, on the user's first turn — not \"within\n  // ~1s of process spawn\", as this comment claimed until the deadline was\n  // investigated. Verified against Codex CLI 0.147.0 on a bound production\n  // rollout: the filename and `session_meta.payload.timestamp` both read the\n  // session-creation time (08:05:49) while the envelope `timestamp` of line 0\n  // reads the first input (08:07:15). Every rollout binding observed in a\n  // 20.5-day log landed ~100ms after the first input, never at spawn, with gaps\n  // of 60.2s / 75.6s / 86.8s. So this races human think time, and unlike the\n  // Claude side there is no recovery: `locateJsonlPath` has no rung that can\n  // reconstruct `rollout-<ts>-<uuid>.jsonl` from a placeholder session id, and\n  // an unbound Codex session also loses resume, fork and boot rehydration.\n  watchForCodexRollout(sessionId: string, projectPath: string): void {\n    let deadline = Date.now() + TRANSCRIPT_WATCH_DEADLINE_MS;\n    let seenPrompts = 0;\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.deps.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.deps.ptyManager.hasSession(sessionId)) {\n        cleanup();\n        return;\n      }\n      // The turn is what creates the rollout, so the deadline runs from the\n      // turn. An abandoned session still stops polling 120s after the spawn,\n      // which is the only thing the deadline was ever bounding.\n      // ponytail: promptCount moves only on POST /api/sessions/:id/input, so a\n      // first prompt composed entirely of raw sendKeys keystrokes is not\n      // covered — every first prompt in the log went through that endpoint.\n      // Widen to a timestamp on the session if that ever stops being true.\n      const prompts = this.deps.sessionStore.getManaged(sessionId)?.promptCount ?? 0;\n      if (prompts > seenPrompts) {\n        seenPrompts = prompts;\n        deadline = Date.now() + TRANSCRIPT_WATCH_DEADLINE_MS;\n      }\n      if (Date.now() > deadline) {\n        this.log.warn(\n          `[startFresh] gave up watching for the Codex rollout of ${sessionId}`,\n          {\n            event: \"session.transcript_watch_expired\",\n            sessionId,\n            provider: CODEX_CLI_PROVIDER,\n            projectPath,\n            promptCount: prompts,\n          },\n          \"pino\",\n        );\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.deps.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.deps.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.deps.sessionStore.updateManaged(sessionId, {\n            boundConversationId: codexSessionId,\n          });\n          // Durably too: after a restart the registry row is the only place\n          // this binding survives, and it is the only id `codex resume` accepts.\n          try {\n            this.deps.managedSessionsRepo()?.recordBinding(sessionId, codexSessionId);\n          } catch (err) {\n            this.log.warn(\"[registry] failed to record Codex rollout binding\", {\n              event: \"registry.binding_write_failed\",\n              sessionId,\n              err,\n            });\n          }\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.deps.sessionFileMap.set(sessionId, candidatePath);\n          this.deps.fileWatcher.watch(candidatePath);\n          try {\n            const existing = readFileSync(candidatePath, \"utf8\").split(\"\\n\").filter(Boolean);\n            if (existing.length > 0) {\n              this.deps.broadcastConversationLines(sessionId, existing);\n            }\n          } catch {\n            /* ignore — file may not be readable yet; watcher will catch future writes */\n          }\n\n          this.deps.scannerManager.markStaleOrDrop();\n          this.linkSessionToProject(sessionId, projectPath, candidatePath);\n          this.deps.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.deps.sessionStore.get(sessionId, this.deps.ptyAttachedIds());\n          if (resp) {\n            this.deps.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 =\n        this.deps.sessionStore.getManaged(sessionId)?.boundConversationId != null;\n      if (!alreadyBound) {\n        intervalHandle = setInterval(tryWire, 250);\n      }\n    }\n  }\n\n  // Cursor-equivalent of watchForCodexRollout(). Cursor mints its own run id\n  // under `~/.cursor/projects/<slug>/agent-transcripts/<runId>/<runId>.jsonl`\n  // and never writes our placeholder into the file (no `sessionId` field).\n  // Without this bind, mobile keeps deep-linking to the placeholder while REST\n  // history is indexed only under the run id — GET /api/conversations/:id 404s\n  // after the empty-unused window closes. Same two-id shape as Codex:\n  // `id`/`conversationId` stay the placeholder; `boundConversationId` is the\n  // run id resume and history use.\n  watchForCursorTranscript(sessionId: string, projectPath: string): void {\n    if (this.deps.cursorRoots.length === 0) return;\n\n    let deadline = Date.now() + TRANSCRIPT_WATCH_DEADLINE_MS;\n    let seenPrompts = 0;\n    const sessionStartedAtMs =\n      (this.deps.sessionStore.getManaged(sessionId)?.startedAt?.getTime() ?? Date.now()) - 5_000;\n    // Threadbase uploads for this PTY land under `.threadbase-uploads/<sessionId>/`\n    // and Cursor's first user turn often @-mentions those paths — a strong signal\n    // when several chats share one project slug.\n    const uploadMarker = `.threadbase-uploads/${sessionId}/`;\n\n    let intervalHandle: ReturnType<typeof setInterval> | null = null;\n    const cleanup = () => {\n      if (intervalHandle) clearInterval(intervalHandle);\n      intervalHandle = null;\n    };\n\n    const tryWire = () => {\n      if (!this.deps.ptyManager.hasSession(sessionId)) {\n        cleanup();\n        return;\n      }\n\n      const prompts = this.deps.sessionStore.getManaged(sessionId)?.promptCount ?? 0;\n      if (prompts > seenPrompts) {\n        seenPrompts = prompts;\n        deadline = Date.now() + TRANSCRIPT_WATCH_DEADLINE_MS;\n      }\n      // Mirror Claude: the file cannot exist before the first turn, so failing\n      // to find it then is not abandonment. Codex still expires from spawn; Cursor\n      // follows the Claude contract because human think time was the production\n      // failure mode (empty 200 → permanent 404 once promptCount > 0 and no bind).\n      if (prompts === 0) return;\n      if (Date.now() > deadline) {\n        this.log.warn(\n          `[startFresh] gave up watching for the Cursor transcript of ${sessionId}`,\n          {\n            event: \"session.transcript_watch_expired\",\n            sessionId,\n            provider: CURSOR_PROVIDER,\n            projectPath,\n            promptCount: prompts,\n          },\n          \"pino\",\n        );\n        cleanup();\n        return;\n      }\n\n      const boundElsewhere = new Set(\n        this.deps.sessionStore\n          .listManaged()\n          .filter((s) => s.id !== sessionId && s.boundConversationId != null)\n          .map((s) => s.boundConversationId as string),\n      );\n\n      type Candidate = { runId: string; filePath: string; mtime: number; mentionsUpload: boolean };\n      const candidates: Candidate[] = [];\n      const nowMs = Date.now();\n\n      for (const root of this.deps.cursorRoots) {\n        const transcriptsDir = cursorAgentTranscriptsDir(root, projectPath);\n        if (!existsSync(transcriptsDir)) continue;\n\n        let runDirs: string[];\n        try {\n          runDirs = readdirSync(transcriptsDir, { withFileTypes: true })\n            .filter((e) => e.isDirectory())\n            .map((e) => e.name);\n        } catch {\n          continue;\n        }\n\n        for (const runId of runDirs) {\n          if (boundElsewhere.has(runId)) continue;\n          const filePath = join(transcriptsDir, runId, `${runId}.jsonl`);\n          if (!existsSync(filePath)) continue;\n\n          let mtime: number;\n          try {\n            mtime = statSync(filePath).mtimeMs;\n          } catch {\n            continue;\n          }\n          // Same recency window as Codex: the poll is continuous, so a just-\n          // written file is always recent when it appears.\n          if (nowMs - mtime >= 10_000) continue;\n          if (mtime < sessionStartedAtMs) continue;\n\n          let mentionsUpload = false;\n          try {\n            // Cap the read — we only need the opening user turn for the upload\n            // path marker, not the whole transcript.\n            const head = readFileSync(filePath, { encoding: \"utf8\" }).slice(0, 8_192);\n            mentionsUpload = head.includes(uploadMarker);\n          } catch {\n            continue;\n          }\n\n          candidates.push({ runId, filePath, mtime, mentionsUpload });\n        }\n      }\n\n      if (candidates.length === 0) return;\n\n      candidates.sort((a, b) => {\n        if (a.mentionsUpload !== b.mentionsUpload) return a.mentionsUpload ? -1 : 1;\n        return b.mtime - a.mtime;\n      });\n      const match = candidates[0];\n\n      cleanup();\n      this.deps.sessionStore.updateManaged(sessionId, {\n        boundConversationId: match.runId,\n      });\n      try {\n        this.deps.managedSessionsRepo()?.recordBinding(sessionId, match.runId);\n      } catch (err) {\n        this.log.warn(\"[registry] failed to record Cursor transcript binding\", {\n          event: \"registry.binding_write_failed\",\n          sessionId,\n          err,\n        });\n      }\n\n      this.deps.sessionFileMap.set(sessionId, match.filePath);\n      this.deps.fileWatcher.watch(match.filePath);\n      try {\n        const existing = readFileSync(match.filePath, \"utf8\").split(\"\\n\").filter(Boolean);\n        if (existing.length > 0) {\n          // The replay starts at the top of the file, so each line's message\n          // index is its count among the lines that parse — the numbering the\n          // offset index and REST use. It goes into the Cursor uuid, which is\n          // what lets the client match these against their history copies.\n          let next = 0;\n          const seqs = existing.map((l) => (parseCursorJsonlLine(l) ? next++ : null));\n          this.deps.broadcastConversationLines(sessionId, existing, seqs);\n        }\n      } catch {\n        /* ignore — file may not be readable yet; watcher will catch future writes */\n      }\n\n      this.deps.scannerManager.markStaleOrDrop();\n      this.linkSessionToProject(sessionId, projectPath, match.filePath);\n      this.deps.cache()?.markAsStreamer(sessionId);\n\n      const resp = this.deps.sessionStore.get(sessionId, this.deps.ptyAttachedIds());\n      if (resp) {\n        this.deps.wsHub.broadcast({ type: \"session_update\", session: resp });\n      }\n\n      this.log.info(\n        `[startFresh] bound Cursor transcript for ${sessionId}`,\n        {\n          event: \"session.cursor_transcript_bound\",\n          sessionId,\n          boundConversationId: match.runId,\n          filePath: match.filePath,\n        },\n        \"pino\",\n      );\n    };\n\n    tryWire();\n    if (!intervalHandle) {\n      const alreadyBound =\n        this.deps.sessionStore.getManaged(sessionId)?.boundConversationId != null;\n      if (!alreadyBound) {\n        intervalHandle = setInterval(tryWire, 250);\n      }\n    }\n  }\n}\n","import { existsSync, readdirSync } from \"fs\";\nimport { join } from \"path\";\n\n/**\n * Cursor stores projects as `~/.cursor/projects/<slug>` where `slug` is the\n * absolute project path with separators (and a Windows drive colon) replaced by\n * `-`, and a leading POSIX `/` dropped:\n * `/Users/me/app` → `Users-me-app`, `C:\\Users\\me\\app` → `C-Users-me-app`.\n * Matches what `@threadbase-sh/scanner`'s `decodeCursorProjectSlug` reverses.\n */\nexport function cursorProjectSlug(projectPath: string): string {\n  // Drop the drive colon before turning separators into `-`, otherwise\n  // `C:\\Users\\me` becomes `C--Users-me` (`:` → `-`, then `/` → `-`).\n  return projectPath.replace(/\\\\/g, \"/\").replace(/^\\/+/, \"\").replace(/:/g, \"\").replace(/\\//g, \"-\");\n}\n\n/**\n * Cursor `cursorRoots` default to `~/.cursor/projects`. Watching that root\n * recursively also sees canvases and `node_modules` and will EMFILE a machine\n * with many Cursor worktrees. Only the `agent-transcripts` folders matter.\n */\nexport function listCursorTranscriptWatchDirs(roots: readonly string[]): string[] {\n  const dirs: string[] = [];\n  for (const dir of roots) {\n    if (!existsSync(dir)) continue;\n    for (const slug of readdirSync(dir, { withFileTypes: true })) {\n      if (!slug.isDirectory()) continue;\n      const transcripts = join(dir, slug.name, \"agent-transcripts\");\n      if (existsSync(transcripts)) dirs.push(transcripts);\n    }\n  }\n  return dirs;\n}\n\n/** `cursorRoots/<slug>/agent-transcripts` for a live session's project path. */\nexport function cursorAgentTranscriptsDir(cursorRoot: string, projectPath: string): string {\n  return join(cursorRoot, cursorProjectSlug(projectPath), \"agent-transcripts\");\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","/**\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 { existsSync } from \"fs\";\nimport type { IncomingMessage, ServerResponse } from \"http\";\nimport type { ConversationListItem } from \"../../conversation-cache\";\nimport { CLAUDE_CODE_PROVIDER, isProviderResumable } from \"../../providers\";\nimport type {\n  SessionListQuery,\n  SessionSortKey,\n  SortOrder as SessionSortOrder,\n  SessionStatus,\n} from \"../../types\";\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).\nexport function 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\nexport function 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    // A cached conversation with no process behind it. Distinguishes \"nobody is\n    // running this\" from an external session that IS live (ownership \"external\").\n    // Match the rehydrated branch of managedToResponse: same conceptual state\n    // (\"resumable, no live process\") must produce the same wire shape (#438).\n    ownership: \"historical\" as const,\n    lifecycle: \"resumable\" as const,\n    lifecycleSource: \"reconcile\" 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    // No PTY behind a cached conversation, so there is no phase — emitted\n    // explicitly for the same reason as in managedToResponse/discoveredToResponse:\n    // the client merges session frames, so an absent key keeps the previous\n    // value and the indicator latches. `GET /api/sessions/:id` serves this\n    // shape whenever the id is a conversation rather than a live session.\n    subStatus: null,\n    promptSuggestion: null,\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\nexport function json(res: ServerResponse, status: number, data: unknown): void {\n  res.writeHead(status, { \"Content-Type\": \"application/json\" });\n  res.end(JSON.stringify(data));\n}\n\nexport async 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\nexport function 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\n/** `refresh=1` style: 1/true → true, 0/false → false, absent/invalid → undefined. */\nexport function boolQueryParam(url: URL, name: string): boolean | undefined {\n  const val = url.searchParams.get(name);\n  if (val == null || val === \"\") return undefined;\n  if (val === \"1\" || val === \"true\") return true;\n  if (val === \"0\" || val === \"false\") return false;\n  return undefined;\n}\n\nexport function includeQueryParam(url: URL): \"all\" | \"conversations\" | \"subagents\" | undefined {\n  const val = url.searchParams.get(\"include\");\n  if (val === \"all\" || val === \"conversations\" || val === \"subagents\") return val;\n  return undefined;\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\nexport type ParsedSessionListQuery = { query: SessionListQuery } | { error: string };\n\nexport function 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\nexport function 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 { randomUUID } from \"crypto\";\nimport type { EventEmitter } from \"events\";\nimport { existsSync } from \"fs\";\nimport type { IncomingMessage, ServerResponse } from \"http\";\nimport { basename, dirname, join } from \"path\";\nimport type { WebSocket } from \"ws\";\nimport { z } from \"zod\";\nimport type { AgentClient } from \"../../agent/agent-client\";\nimport type { AgentConfig } from \"../../agent/agent-config\";\nimport { handleSendAgentInput } from \"../../agent/handle-send-agent-input\";\nimport { handleStartAgentSession } from \"../../agent/handle-start-agent-session\";\nimport { resolveBrowsePath } from \"../../browse\";\nimport type { ClaudeFlagValues, EffortLevel, PermissionMode } from \"../../claude-flags\";\nimport type { ConversationCache } from \"../../conversation-cache\";\nimport type { createPool } from \"../../db\";\nimport { recordUpload } from \"../../db/upload-records\";\nimport type { ExternalTailManager } from \"../../external-tails\";\nimport type { LiveSessionManager } from \"../../live-session-manager\";\nimport type { Logger } from \"../../logger\";\nimport { discoverClaudeProcesses } from \"../../process-discovery\";\nimport {\n  CLAUDE_CODE_PROVIDER,\n  CODEX_CLI_PROVIDER,\n  CURSOR_PROVIDER,\n  canonicalizeProviderName,\n  isProviderName,\n  type ProviderName,\n} from \"../../providers\";\nimport { PTY_ROWS } from \"../../pty-shared\";\nimport type { ScannerManager } from \"../../scanner-manager\";\nimport { type Prompt, PromptAnswerSchema } from \"../../schemas/prompt.schema\";\nimport type { ResumeFailure, ResumeOutcome } from \"../../server\";\nimport type { PendingPermission, PendingQuestion } from \"../../server-wiring\";\nimport { classifyConversationFile } from \"../../services/conversations/classification\";\nimport {\n  type PromptAdapterResult,\n  type PromptAnswerAdapter,\n  type PromptAnswerErrorCode,\n  PromptRegistry,\n} from \"../../services/prompts/promptRegistry\";\nimport {\n  permissionPromptDraft,\n  questionPromptDraft,\n} from \"../../services/prompts/ptyPromptAdapter\";\nimport { capabilitiesFor } from \"../../services/providers/capabilities\";\nimport { CODEX_ACTIVE_WRITER_CODE } from \"../../services/questions/codexScreen\";\nimport {\n  detectGateScreen,\n  permissionContentKey,\n  permissionGateKey,\n  scrapePermissionGate,\n} from \"../../services/questions/detectPermissionGate\";\nimport {\n  isQuestionMenuOnScreen,\n  questionContentKey,\n} from \"../../services/questions/detectQuestionFromScreen\";\nimport { detectShellPrompt } from \"../../services/questions/detectShellPrompt\";\nimport { detectStartupChoiceGate } from \"../../services/questions/detectStartupChoiceGate\";\nimport { parseStatusLine } from \"../../services/questions/parseStatusLine\";\nimport { permissionAnswerKeys } from \"../../services/questions/permissionAnswerKeys\";\nimport { resolveAnswer } from \"../../services/questions/resolveAnswer\";\nimport { type CodexOwnerSource, findRolloutOwner } from \"../../services/sessions/codexRolloutOwner\";\nimport {\n  type BusySignal,\n  conversationBusy,\n  resolveResumeBusyWindowMs,\n} from \"../../services/sessions/conversationBusy\";\nimport type { IdempotencyStore } from \"../../services/sessions/idempotency\";\nimport { readIdempotencyKey } from \"../../services/sessions/idempotency\";\nimport type { SessionRegistryBoot } from \"../../session-registry-boot\";\nimport type { SessionStore } from \"../../session-store\";\nimport type { SessionWatchers } from \"../../session-watchers\";\nimport type {\n  AskQuestion,\n  DiscoveredProcess,\n  ManagedSession,\n  PermissionOption,\n  SessionResponse,\n  WSMessage,\n} from \"../../types\";\nimport { saveUploadFile } from \"../../uploads\";\nimport type { WSHub } from \"../../ws-hub\";\nimport {\n  classifyResumability,\n  conversationToResumableSession,\n  json,\n  parseSessionListQuery,\n  readBody,\n} from \"./http-helpers\";\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\n// Upper bound on the process-discovery half of the resume collision probe.\n// Enumerating processes costs one CIM/wmic query per pid on Windows, so a\n// machine with several CLIs open can take seconds — unacceptable on a path the\n// user is waiting on. Past this we fall back to the jsonl_mtime signal alone.\nexport const RESUME_DISCOVERY_TIMEOUT_MS = 750;\n\n// How long adopt waits for the external process to actually exit after SIGTERM\n// before giving up. Exceeding this means the takeover would spawn a second agent\n// alongside a live one, so adopt aborts instead.\n// How long a discovered-process list stays usable. Shared by the sessions list\n// and the resume collision probe so a resume can reuse a warm enumeration.\nconst DISCOVERY_TTL_MS = 15_000;\n\nexport const ADOPT_KILL_TIMEOUT_MS = 5_000;\nconst ADOPT_KILL_POLL_MS = 100;\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\nconst RawKeySchema = z.object({\n  action: z.enum([\"escape\", \"up\", \"down\", \"left\", \"right\", \"tab\", \"shift_tab\", \"enter\"]),\n  promptId: z.string().trim().min(1).max(200).optional(),\n  confirm: z.literal(true).optional(),\n});\n\nconst RAW_KEY_BYTES = {\n  escape: \"\\x1b\",\n  up: \"\\x1b[A\",\n  down: \"\\x1b[B\",\n  left: \"\\x1b[D\",\n  right: \"\\x1b[C\",\n  tab: \"\\t\",\n  shift_tab: \"\\x1b[Z\",\n  enter: \"\\r\",\n} as const;\n\n// How long a Codex resume/fork waits for an authoritative startup outcome\n// before falling back to the pre-existing \"spawned, still booting\" behaviour.\n//\n// Deliberately shorter than START_READY_TIMEOUT_MS: this window only has to\n// cover Codex FAILING, and the writer-lock refusal happens when it opens the\n// rollout — before the TUI boots at all — not after the 8s ready fallback. A\n// resume that is merely slow to paint still answers 201 and finishes booting in\n// the background, exactly as it did before.\nconst CODEX_STARTUP_TIMEOUT_MS = 4_000;\n\nfunction resolveCodexStartupTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {\n  const raw = env.THREADBASE_CODEX_STARTUP_TIMEOUT_MS;\n  if (raw === undefined) return CODEX_STARTUP_TIMEOUT_MS;\n  const n = Number.parseInt(raw, 10);\n  return Number.isFinite(n) && n >= 0 ? n : CODEX_STARTUP_TIMEOUT_MS;\n}\n\n// Poll until `pid` is gone (signal 0 throws ESRCH) or the timeout elapses.\n// Returns true when the process is confirmed gone. Used by adopt so a takeover\n// never spawns a second agent while the one it replaces is still alive.\nexport async function waitForProcessExit(\n  pid: number,\n  timeoutMs: number,\n  pollMs: number = ADOPT_KILL_POLL_MS,\n): Promise<boolean> {\n  const deadline = Date.now() + timeoutMs;\n  for (;;) {\n    try {\n      process.kill(pid, 0);\n    } catch {\n      return true; // ESRCH (or no longer visible to us) — it is gone.\n    }\n    if (Date.now() >= deadline) return false;\n    await new Promise((resolve) => setTimeout(resolve, pollMs));\n  }\n}\n\n/**\n * The 409 body for a Codex conversation another client already owns.\n *\n * Keeps `code: \"CONVERSATION_BUSY\"` on purpose: released mobile builds switch\n * on that string, and a new top-level code would land as a generic network\n * error with no recovery UI. The Codex-specific truth is carried additively —\n * `reasonCode` names the real cause, and the capability flags say what the\n * client may offer, so nothing has to be inferred from `likelyOwner`.\n *\n * `canForce` is false because `force` only ever bypassed OUR heuristic; Codex's\n * writer lock is enforced inside Codex.\n *\n * `canTakeOver` is offered only when the owner is a standalone `codex` TUI:\n * that process owns exactly one conversation, so killing it costs the user\n * only the turn in flight. It stays false for every other owner — a shared VS\n * Code / desktop app-server hosts unrelated threads, and for an unidentified\n * owner there is nothing to prove it does not.\n */\nfunction codexSessionActiveBody(outcome: {\n  detectedBy: BusySignal[];\n  lastActivityMs: number | null;\n  ownerPid?: number;\n  ownerSource?: CodexOwnerSource;\n}): Record<string, unknown> {\n  return {\n    error: \"This Codex session is already open in another client\",\n    code: \"CONVERSATION_BUSY\",\n    reasonCode: \"CODEX_SESSION_ACTIVE\",\n    provider: CODEX_CLI_PROVIDER,\n    detectedBy: outcome.detectedBy,\n    lastActivityMs: outcome.lastActivityMs,\n    likelyOwner: \"external\",\n    canForce: false,\n    // Offered only against a standalone `codex` TUI, which owns exactly one\n    // conversation. A desktop/VS Code `codex app-server` hosts several\n    // unrelated threads in one process, so killing it to reclaim this\n    // conversation would take the others with it — see codexRolloutOwner.ts.\n    canTakeOver: outcome.ownerSource === \"terminal\",\n    canFork: true,\n    ...(outcome.ownerPid != null && { ownerPid: outcome.ownerPid }),\n    ...(outcome.ownerSource != null && { ownerSource: outcome.ownerSource }),\n  };\n}\n\n/**\n * Prompt-answer taxonomy to HTTP status.\n *\n * The validation class is 400, matching what the legacy `/answer` route already\n * returns for the same \"this answer cannot be applied to this prompt\" family;\n * the state class — the prompt moved on, or never existed here — is 409/404.\n */\nfunction promptAnswerStatus(code: PromptAnswerErrorCode): number {\n  switch (code) {\n    case \"prompt_not_found\":\n      return 404;\n    case \"provider_error\":\n      return 502;\n    case \"unknown_question\":\n    case \"unknown_option\":\n    case \"incomplete_answer\":\n    case \"unsupported_prompt_shape\":\n      return 400;\n    default:\n      return 409;\n  }\n}\n\n/**\n * Everything SessionHandlers reads from the server. Same split as\n * ConversationHandlersDeps: collaborators the server constructor already built\n * are passed by reference (Maps and Sets keep identity, so a mutation here is\n * the same mutation the server and its tests observe), anything bound later or\n * swapped by tests is a thunk, and the handful of methods that stay on\n * StreamerServer are late-bound calls back into it.\n *\n * Nothing here owns state. `pendingQuestions`, `pendingPermission`,\n * `contendedSessions`, `selfPtyEndedAt`, `sessionFileMap`, `sessionSubscribers`\n * and `idempotency` remain StreamerServer instance properties — tests reach\n * into them through `(server as any)`, and the WS/PTY callbacks that also read\n * them never moved.\n */\nexport type SessionHandlersDeps = {\n  sessionStore: SessionStore;\n  ptyManager: LiveSessionManager;\n  wsHub: WSHub;\n  scannerManager: ScannerManager;\n  sessionWatchers: SessionWatchers;\n  registryBoot: SessionRegistryBoot;\n  externalTailManager: ExternalTailManager;\n  idempotency: IdempotencyStore;\n  sessionStatusBus: EventEmitter;\n  sessionFileMap: Map<string, string>;\n  promptRegistry: PromptRegistry;\n  pendingQuestions: Map<string, PendingQuestion>;\n  pendingQuestionKey: Map<string, string>;\n  pendingPermission: Map<string, PendingPermission>;\n  pendingPermissionKey: Map<string, string>;\n  contendedSessions: Set<string>;\n  selfPtyEndedAt: Map<string, number>;\n  sessionSubscribers: Map<string, Set<WebSocket>>;\n  agentConfig: AgentConfig;\n  agentClient: AgentClient | null;\n  defaultSystemPrompt: string;\n  codexSystemPromptEnabled: boolean;\n  cacheDir: string;\n\n  // Late-bound: opened during listen(), rebound by the integrity monitor's\n  // reset-and-rescan, mutated by PUT /api/config/claude-flags, resolved\n  // asynchronously at boot (browseRoot), or swapped on the instance by tests.\n  cache: () => ConversationCache | null;\n  includeSubagentSessions?: () => boolean;\n  log: () => Logger;\n  browseRoot: () => string | null;\n  claudeFlags: () => ClaudeFlagValues;\n  claudeExtraArgs: () => string | undefined;\n  dbPool: () => Awaited<ReturnType<typeof createPool>> | null;\n  dbInstanceId: () => string | null;\n\n  // The discovered-process cache and its single-flight. Read and written from\n  // here, but owned by StreamerServer — close() and the session list read them.\n  discoveryCache: () => { entries: DiscoveredProcess[]; fetchedAt: number } | null;\n  setDiscoveryCache: (value: { entries: DiscoveredProcess[]; fetchedAt: number } | null) => void;\n  discoveryInFlight: () => Promise<DiscoveredProcess[]> | null;\n  setDiscoveryInFlight: (value: Promise<DiscoveredProcess[]> | null) => void;\n\n  // Methods that stay on StreamerServer.\n  rejectIfWarmingUp: (res: ServerResponse) => boolean;\n  ptyAttachedIds: () => Set<string>;\n  withReconciledLifecycle: (sessions: readonly SessionResponse[]) => readonly SessionResponse[];\n  broadcastOrUnicastSessionList: (req: IncomingMessage) => void;\n  checkSessionStartRateLimit: (ip: string) => boolean;\n  checkSessionInputRateLimit: (sessionId: string) => boolean;\n  spawnFlagOverrides: () => {\n    permissionMode: PermissionMode;\n    model: string;\n    effort: EffortLevel;\n  };\n  resolveConversationTarget: (sessionId: string) => Promise<\n    | ResumeFailure\n    | {\n        ok: true;\n        historyId: string;\n        jsonlPath: string | null;\n        historyPath: string | null;\n        conv: any;\n        projectPath: string;\n        provider: ProviderName;\n      }\n  >;\n  waitForStartupOutcome: (\n    sessionId: string,\n    timeoutMs: number,\n  ) => Promise<{ outcome: \"ready\" | \"failed\" | \"timeout\"; session: ManagedSession | null }>;\n  forgetSession: (sessionId: string) => void;\n  abandonFailedStart: (sessionId: string) => void;\n  armHoldWhenIdle: (\n    sessionId: string,\n    opts?: { ignoreWatchers?: boolean; deleteAfter?: boolean },\n  ) => \"held\" | \"armed\" | \"no_session\";\n  enrichResumedSessionAsync: (sessionId: string, projectPath: string, conv: any) => void;\n  findJsonlPath: (uuid: string) => string | null;\n  readCwdFromJsonl: (filePath: string) => Promise<string | null>;\n};\n\n/**\n * The session lifecycle surface: list/get, start, resume, fork, adopt, input,\n * answers, permission gates, uploads, stop/cancel and session names.\n *\n * Extracted from StreamerServer so session work stops editing the server file\n * (see docs/plans/2026-07-12-server-ts-split.md, PR 8). State stays on the\n * server: this class only reads and mutates it through `deps`.\n */\nexport class SessionHandlers {\n  constructor(private deps: SessionHandlersDeps) {}\n\n  private get sessionStore(): SessionStore {\n    return this.deps.sessionStore;\n  }\n\n  private get ptyManager(): LiveSessionManager {\n    return this.deps.ptyManager;\n  }\n\n  private get wsHub(): WSHub {\n    return this.deps.wsHub;\n  }\n\n  private get scannerManager(): ScannerManager {\n    return this.deps.scannerManager;\n  }\n\n  private get sessionWatchers(): SessionWatchers {\n    return this.deps.sessionWatchers;\n  }\n\n  private get registryBoot(): SessionRegistryBoot {\n    return this.deps.registryBoot;\n  }\n\n  private get externalTailManager(): ExternalTailManager {\n    return this.deps.externalTailManager;\n  }\n\n  private get idempotency(): IdempotencyStore {\n    return this.deps.idempotency;\n  }\n\n  private get sessionStatusBus(): EventEmitter {\n    return this.deps.sessionStatusBus;\n  }\n\n  private get sessionFileMap(): Map<string, string> {\n    return this.deps.sessionFileMap;\n  }\n\n  private get pendingQuestions(): SessionHandlersDeps[\"pendingQuestions\"] {\n    return this.deps.pendingQuestions;\n  }\n\n  private get promptRegistry(): PromptRegistry {\n    if (!this.deps.promptRegistry) this.deps.promptRegistry = new PromptRegistry();\n    return this.deps.promptRegistry;\n  }\n\n  private get pendingQuestionKey(): Map<string, string> {\n    return this.deps.pendingQuestionKey;\n  }\n\n  private get pendingPermission(): SessionHandlersDeps[\"pendingPermission\"] {\n    return this.deps.pendingPermission;\n  }\n\n  private get pendingPermissionKey(): Map<string, string> {\n    return this.deps.pendingPermissionKey;\n  }\n\n  private get contendedSessions(): Set<string> {\n    return this.deps.contendedSessions;\n  }\n\n  private get selfPtyEndedAt(): Map<string, number> {\n    return this.deps.selfPtyEndedAt;\n  }\n\n  private get sessionSubscribers(): Map<string, Set<WebSocket>> {\n    return this.deps.sessionSubscribers;\n  }\n\n  // Prompt lifecycle events (question / permission and their cancellations)\n  // carry prompt content and go ONLY to the session's subscribers — never to\n  // every connected socket. A client that subscribes after a prompt opened\n  // gets it from the subscribe replay (server-wiring), not from a broadcast.\n  private broadcastToSession(sessionId: string, message: WSMessage): void {\n    this.wsHub.broadcastToClients(this.sessionSubscribers.get(sessionId) ?? [], message);\n  }\n\n  private get agentConfig(): AgentConfig {\n    return this.deps.agentConfig;\n  }\n\n  private get agentClient(): AgentClient | null {\n    return this.deps.agentClient;\n  }\n\n  private get defaultSystemPrompt(): string {\n    return this.deps.defaultSystemPrompt;\n  }\n\n  private get codexSystemPromptEnabled(): boolean {\n    return this.deps.codexSystemPromptEnabled;\n  }\n\n  private get cacheDir(): string {\n    return this.deps.cacheDir;\n  }\n\n  private get cache(): ConversationCache | null {\n    return this.deps.cache();\n  }\n\n  private get log(): Logger {\n    return this.deps.log();\n  }\n\n  private get browseRoot(): string | null {\n    return this.deps.browseRoot();\n  }\n\n  private get claudeFlags(): ClaudeFlagValues {\n    return this.deps.claudeFlags();\n  }\n\n  private get claudeExtraArgs(): string | undefined {\n    return this.deps.claudeExtraArgs();\n  }\n\n  private get dbPool(): Awaited<ReturnType<typeof createPool>> | null {\n    return this.deps.dbPool();\n  }\n\n  private get dbInstanceId(): string | null {\n    return this.deps.dbInstanceId();\n  }\n\n  private get discoveryCache(): { entries: DiscoveredProcess[]; fetchedAt: number } | null {\n    return this.deps.discoveryCache();\n  }\n\n  private set discoveryCache(value: { entries: DiscoveredProcess[]; fetchedAt: number } | null) {\n    this.deps.setDiscoveryCache(value);\n  }\n\n  private get discoveryInFlight(): Promise<DiscoveredProcess[]> | null {\n    return this.deps.discoveryInFlight();\n  }\n\n  private set discoveryInFlight(value: Promise<DiscoveredProcess[]> | null) {\n    this.deps.setDiscoveryInFlight(value);\n  }\n\n  async handleListSessions(url: URL, res: ServerResponse): Promise<void> {\n    if (this.deps.rejectIfWarmingUp(res)) return;\n\n    await this.refreshDiscovery();\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(\n        res,\n        200,\n        this.externalTailManager.withExternalActivity(\n          this.deps.withReconciledLifecycle(this.sessionStore.list(this.deps.ptyAttachedIds())),\n        ),\n      );\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.deps.ptyAttachedIds(), parsed.query);\n      page.sessions = this.externalTailManager.withExternalActivity(\n        this.deps.withReconciledLifecycle(page.sessions),\n      );\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  /**\n   * Refresh the discovered-process list, sharing one in-flight enumeration\n   * across concurrent callers and honouring the 15s TTL cache.\n   */\n  private async refreshDiscovery(): Promise<DiscoveredProcess[]> {\n    const cached = this.discoveryCache;\n    if (cached && Date.now() - cached.fetchedAt < DISCOVERY_TTL_MS) {\n      return cached.entries;\n    }\n    if (this.discoveryInFlight) {\n      return this.discoveryInFlight;\n    }\n\n    let flight!: Promise<DiscoveredProcess[]>;\n    flight = (async (): Promise<DiscoveredProcess[]> => {\n      try {\n        const discovered = await discoverClaudeProcesses();\n        this.sessionStore.setDiscovered(discovered);\n        this.discoveryCache = { entries: discovered, fetchedAt: Date.now() };\n        return discovered;\n      } catch {\n        // Discovery is best-effort — keep any previous cache rather than\n        // remembering a failure as \"nothing is running\".\n        return this.discoveryCache?.entries ?? [];\n      } finally {\n        if (this.discoveryInFlight === flight) {\n          this.discoveryInFlight = null;\n        }\n      }\n    })();\n\n    this.discoveryInFlight = flight;\n    return flight;\n  }\n\n  async handleGetSession(sessionId: string, res: ServerResponse): Promise<void> {\n    if (this.deps.rejectIfWarmingUp(res)) return;\n    // A response copy, so decorating it means building a new object — see\n    // SessionStore.get(). Persisting anything here would need updateManaged().\n    const base = this.sessionStore.get(sessionId, this.deps.ptyAttachedIds());\n    if (base) {\n      // Apply a boot-reconciliation verdict if one exists for this session and\n      // it isn't live here — the detail screen is where the distinction between\n      // `detached` and `orphaned` actually matters to a user.\n      const reconciled = this.deps.withReconciledLifecycle([base])[0];\n      const session: SessionResponse = {\n        ...base,\n        ...(existsSync(base.projectPath)\n          ? {}\n          : { failureReason: `Project directory not found: ${base.projectPath}` }),\n        lifecycle: reconciled.lifecycle,\n        lifecycleSource: reconciled.lifecycleSource,\n      };\n      // Scrape model/effort/permission-mode off the live PTY's rendered status\n      // line so the client can show them natively instead of parsing terminal\n      // text. Live sessions only, and strictly best-effort: a failed scrape\n      // leaves the fields absent rather than failing the request.\n      if (this.ptyManager.hasSession(sessionId)) {\n        try {\n          const lines = await this.ptyManager.getOutputLines(sessionId, 10);\n          const status = parseStatusLine(lines);\n          // Don't clobber the scanner-provided model with a scraped one.\n          if (session.model == null && status.model) session.model = status.model;\n          if (status.effort) session.effort = status.effort;\n          if (status.permissionMode) session.permissionMode = status.permissionMode;\n        } catch {\n          // PTY raced away between hasSession() and the read — report what we have.\n        }\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 && !conversation.deletedAt) {\n      json(res, 200, conversationToResumableSession(conversation));\n      return;\n    }\n    json(res, 404, { error: \"Session not found\" });\n  }\n\n  async handleResume(req: IncomingMessage, res: ServerResponse): Promise<void> {\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    const outcome = await this.resumeSession({\n      sessionId,\n      force: body.force === true,\n      projectName: body.projectName,\n      branch: body.branch,\n    });\n\n    if (!outcome.ok) {\n      switch (outcome.reason) {\n        case \"history_file_missing\":\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        case \"no_project_path\":\n          json(res, 400, { error: \"Could not determine project path\" });\n          return;\n        case \"conversation_busy\":\n          json(res, 409, {\n            error: \"This conversation looks active in another session\",\n            code: \"CONVERSATION_BUSY\",\n            detectedBy: outcome.detectedBy,\n            lastActivityMs: outcome.lastActivityMs,\n            likelyOwner: outcome.likelyOwner,\n            // Additive capability hints (see docs/compatibility/tb-mobile.md).\n            // Older clients ignore them and keep deriving the same actions from\n            // `likelyOwner`; newer ones must honour these instead of guessing.\n            canForce: true,\n            canTakeOver: outcome.likelyOwner === \"external\",\n            canFork: false,\n          });\n          return;\n        case \"codex_session_active\":\n          json(res, 409, codexSessionActiveBody(outcome));\n          return;\n        case \"codex_start_failed\":\n          json(res, 502, {\n            error: outcome.failureReason,\n            code: \"SESSION_START_FAILED\",\n            provider: CODEX_CLI_PROVIDER,\n          });\n          return;\n      }\n    }\n\n    // Already ours: answer 200 rather than 201, and broadcast nothing — the\n    // session list did not change.\n    if (outcome.alreadyRunning) {\n      json(res, 200, outcome.response);\n      return;\n    }\n\n    this.deps.broadcastOrUnicastSessionList(req);\n    json(res, 201, outcome.response ?? outcome.session);\n  }\n\n  /**\n   * `POST /api/sessions/:id/fork` — continue a conversation this streamer is\n   * not allowed to resume, without touching whoever owns it.\n   *\n   * Codex only (`codex fork <id>`): Claude Code has no equivalent, and there is\n   * no safe generic fallback — quietly resuming instead would attach to the\n   * exact writer the caller is trying to leave alone, which is the failure this\n   * endpoint exists to avoid.\n   *\n   * NOT idempotent by default: every accepted call starts another Codex\n   * process and another rollout. Clients that retry on timeout must send\n   * `idempotencyKey`, which replays the first outcome for 10 minutes (same\n   * store and semantics as `POST /:id/input`).\n   */\n  async handleFork(sessionId: string, req: IncomingMessage, res: ServerResponse): Promise<void> {\n    const body = await readBody(req);\n\n    let idempotencyKey: string | undefined;\n    try {\n      idempotencyKey = readIdempotencyKey(body as Record<string, unknown>);\n    } catch (err) {\n      json(res, 400, { error: err instanceof Error ? err.message : \"Invalid idempotencyKey\" });\n      return;\n    }\n    if (idempotencyKey) {\n      const replayed = this.idempotency.get(sessionId, idempotencyKey);\n      if (replayed) {\n        json(res, replayed.status, replayed.body);\n        return;\n      }\n    }\n\n    const target = await this.deps.resolveConversationTarget(sessionId);\n    if (!target.ok) {\n      if (target.reason === \"history_file_missing\") {\n        json(res, 404, {\n          error: \"Conversation history file is missing; it can no longer be forked\",\n          code: \"history_file_missing\",\n        });\n      } else {\n        json(res, 400, { error: \"Could not determine project path\" });\n      }\n      return;\n    }\n    if (target.provider !== CODEX_CLI_PROVIDER) {\n      json(res, 501, {\n        error: \"Forking is only supported for Codex sessions\",\n        code: \"UNSUPPORTED_PROVIDER\",\n        provider: target.provider,\n      });\n      return;\n    }\n\n    this.discoveryCache = null;\n\n    let session: ManagedSession;\n    try {\n      session = await this.ptyManager.startFork({\n        provider: CODEX_CLI_PROVIDER,\n        // The rollout id, never the placeholder the client navigated to — it is\n        // the only id `codex fork` accepts.\n        forkFromId: target.historyId,\n        projectPath: target.projectPath,\n        projectName: body.projectName,\n        branch: body.branch,\n      });\n    } catch (err) {\n      const message = err instanceof Error ? err.message : \"Failed to fork 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(`[fork] failed to fork ${sessionId}: ${message}`, {\n        event: \"session.fork_failed\",\n        sessionId,\n        error: message,\n      });\n      json(res, statusCode, { error: message, code: \"FORK_FAILED\" });\n      return;\n    }\n\n    // The fork's own id is a placeholder until Codex writes its rollout; the\n    // source id is recorded separately and deliberately never written into\n    // `resumedFromConversationId` — the two histories diverge here.\n    session.forkedFromConversationId = target.historyId;\n    this.sessionStore.addManaged(session);\n    this.registryBoot.recordSessionSpawn(session);\n\n    const { outcome, session: settled } = await this.deps.waitForStartupOutcome(\n      session.id,\n      resolveCodexStartupTimeoutMs(),\n    );\n    if (outcome === \"failed\") {\n      const failed = settled ?? this.sessionStore.getManaged(session.id);\n      this.deps.abandonFailedStart(session.id);\n      if (failed?.failureCode === CODEX_ACTIVE_WRITER_CODE) {\n        // Rare but real: Codex can refuse a fork too (e.g. the source rollout\n        // is mid-write). Same structured collision the resume path returns.\n        json(res, 409, codexSessionActiveBody({ detectedBy: [], lastActivityMs: null }));\n        return;\n      }\n      json(res, 502, {\n        error: failed?.failureReason ?? \"Codex exited before the fork became ready\",\n        code: \"SESSION_START_FAILED\",\n        provider: CODEX_CLI_PROVIDER,\n      });\n      return;\n    }\n\n    // Bind the NEW rollout id. The candidate filter requires a session_meta\n    // created at/after this session started, so it can never re-bind the source.\n    this.sessionWatchers.watchForCodexRollout(session.id, target.projectPath);\n\n    const response = this.sessionStore.get(session.id, this.deps.ptyAttachedIds());\n    const result = {\n      status: outcome === \"ready\" ? 201 : 202,\n      body:\n        outcome === \"ready\"\n          ? (response ?? session)\n          : { id: session.id, status: \"pending\", forkedFromConversationId: target.historyId },\n    };\n    if (idempotencyKey) this.idempotency.set(sessionId, idempotencyKey, result);\n\n    this.log.info(`[fork] forked ${target.historyId} into ${session.id}`, {\n      event: \"session.forked\",\n      sessionId: session.id,\n      forkedFromConversationId: target.historyId,\n      outcome,\n    });\n    this.deps.broadcastOrUnicastSessionList(req);\n    json(res, result.status, result.body);\n  }\n\n  /**\n   * Resume a session, from an HTTP request or from the boot path.\n   *\n   * Extracted from `handleResume` so both callers hit the **same collision\n   * probe** (plan Phase 7c). The probe is what stops this streamer attaching to\n   * a conversation an external terminal already owns; a second, hand-adapted\n   * copy of this sequence in the boot path is how two agents end up appending\n   * to one JSONL at 4am with nobody watching.\n   *\n   * Returns a typed reason rather than writing a response, so the HTTP caller\n   * maps it to a status code and the boot caller logs it.\n   */\n  async resumeSession(opts: {\n    sessionId: string;\n    force?: boolean;\n    projectName?: string;\n    branch?: string;\n  }): Promise<ResumeOutcome> {\n    const { sessionId } = opts;\n    const managed = this.sessionStore.getManaged(sessionId);\n    const includeSubagents =\n      this.deps.includeSubagentSessions?.() ?? this.cache?.includeSubagentSessions ?? false;\n    if (\n      (managed?.isSubagent && !includeSubagents) ||\n      this.cache?.isExcludedSubagent(managed?.boundConversationId ?? sessionId)\n    ) {\n      return { ok: false, reason: \"history_file_missing\" };\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.deps.ptyAttachedIds());\n      if (resp) {\n        return { ok: true, alreadyRunning: true, session: null, response: resp };\n      }\n    }\n\n    const target = await this.deps.resolveConversationTarget(sessionId);\n    if (!target.ok) return target;\n    const { historyId, jsonlPath, historyPath, conv, projectPath, provider } = target;\n    let classification:\n      | { isSubagent?: boolean | null; parentConversationId?: string | null }\n      | null\n      | undefined = this.cache?.getMetaById(historyId);\n    const classificationPath = historyPath || jsonlPath;\n    if (classification?.isSubagent == null && classificationPath) {\n      try {\n        if (provider === CLAUDE_CODE_PROVIDER || provider === CODEX_CLI_PROVIDER) {\n          classification = classifyConversationFile(classificationPath, provider);\n        }\n      } catch {\n        // The existing target/readiness checks own unreadable-history errors.\n      }\n    }\n    if (!includeSubagents && classification?.isSubagent)\n      return { ok: false, reason: \"history_file_missing\" };\n\n    // Codex-only fast path: does another process hold this exact rollout open?\n    // The generic probe below cannot answer that — a Codex owner need not carry\n    // the rollout id in argv, and an owned rollout can sit quiet well past the\n    // mtime window (the 2026-08-09 incident). An open handle is the same\n    // condition Codex's own writer lock rejects, so this is treated as\n    // authoritative and `force` does NOT bypass it: forcing would spawn a PTY\n    // that Codex refuses anyway, and answer 201 for a session that will never\n    // become usable. Fork is the recovery path, not force.\n    if (provider === CODEX_CLI_PROVIDER && historyPath) {\n      const owner = await findRolloutOwner(historyPath);\n      if (owner) {\n        this.log.info(`[resume] codex rollout held by pid ${owner.pid}`, {\n          event: \"session.codex_rollout_busy\",\n          sessionId,\n          historyId,\n          ownerPid: owner.pid,\n          ownerCommand: owner.command,\n        });\n        return {\n          ok: false,\n          reason: \"codex_session_active\",\n          detectedBy: [\"file_handle\"],\n          lastActivityMs: null,\n          ownerPid: owner.pid,\n          ownerSource: owner.source,\n        };\n      }\n    }\n\n    // Pre-flight collision check: refuse to resume a conversation that looks\n    // actively owned elsewhere, unless the caller forces it. Runs AFTER the\n    // hasSession early-return above (a session this streamer already owns still\n    // returns 200). NOTE: this is a one-directional pre-flight guard only —\n    // once this streamer holds the PTY it cannot prevent an external terminal\n    // from attaching to the same conversation afterwards.\n    // Process enumeration is SLOW on Windows (one CIM/wmic query per candidate\n    // pid, seconds when several CLIs are open) and resume is latency-sensitive —\n    // the user is waiting on a spawn. Bound it: past the deadline we proceed\n    // with no process signals rather than making every resume pay the worst\n    // case. jsonl_mtime is the primary signal and is a single stat, so the probe\n    // stays useful even when discovery is dropped.\n    let discovered: DiscoveredProcess[] = [];\n    // Prefer the list GET /api/sessions already keeps warm (15s TTL), including\n    // any in-flight refresh — a slightly stale process list is fine for a\n    // heuristic pre-flight check, and mobile polls sessions often enough that\n    // this is usually a free hit. Bound the wait: past the deadline we proceed\n    // with no process signals rather than making every resume pay the worst\n    // case. jsonl_mtime is the primary signal and is a single stat, so the probe\n    // stays useful even when discovery is dropped.\n    try {\n      discovered = await Promise.race([\n        this.refreshDiscovery(),\n        new Promise<DiscoveredProcess[]>((resolve) =>\n          setTimeout(() => resolve([]), RESUME_DISCOVERY_TIMEOUT_MS).unref?.(),\n        ),\n      ]);\n    } catch {\n      // Discovery is best-effort; the jsonl_mtime signal still applies.\n    }\n    const busy = conversationBusy({\n      // The id another owner's argv would actually carry — for a placeholder\n      // that is the bound rollout id, not the one the client asked for.\n      conversationId: historyId,\n      projectPath,\n      jsonlPath,\n      discovered,\n      windowMs: resolveResumeBusyWindowMs(),\n      selfPtyEndedAt: this.selfPtyEndedAt.get(sessionId) ?? null,\n    });\n    if (busy.busy && opts.force !== true) {\n      return {\n        ok: false,\n        reason: \"conversation_busy\",\n        detectedBy: busy.detectedBy,\n        lastActivityMs: busy.lastActivityMs,\n        likelyOwner: busy.likelyOwner,\n      };\n    }\n    if (busy.busy) {\n      // Forced past a detected collision — mark the session so JSONL-derived\n      // question cards from the shared file are suppressed (they may be authored\n      // by the other owner). Cleared when the PTY settles to idle.\n      this.contendedSessions.add(sessionId);\n    }\n\n    // We are about to change what is running, so the discovered-process snapshot\n    // is now stale — drop it so the next sessions list re-enumerates. Done AFTER\n    // the collision probe, which wants the warm list (a fresh enumeration there\n    // costs seconds of wmic on Windows and resume is latency-sensitive).\n    this.discoveryCache = null;\n\n    const session = await this.ptyManager.start(sessionId, {\n      provider,\n      projectPath,\n      projectName: opts.projectName,\n      branch: opts.branch,\n      // Omitted on every ordinary resume, so argv is unchanged there.\n      ...(historyId !== sessionId && { resumeId: historyId }),\n      claudeFlags: this.claudeFlags,\n      claudeExtraArgs: this.claudeExtraArgs,\n      ...this.deps.spawnFlagOverrides(),\n    });\n    // Carry the binding onto the live session before it is recorded:\n    // recordSpawn writes `bound_conversation_id` from this field, so leaving it\n    // unset would upsert the row back to NULL and strand the *next* resume.\n    if (historyId !== sessionId) session.boundConversationId = historyId;\n\n    this.sessionStore.addManaged(session);\n    this.sessionStore.updateManaged(session.id, {\n      isSubagent: classification?.isSubagent ?? false,\n      parentConversationId: classification?.parentConversationId ?? null,\n    });\n    this.registryBoot.recordSessionSpawn(session);\n\n    // Codex is the only authority on its writer lock, and it reports the\n    // refusal AFTER the process starts. Spawning is therefore not evidence of a\n    // successful resume: wait for a bounded ready-or-failed outcome before\n    // telling the caller this worked. Claude's resume is unchanged — it has no\n    // equivalent lock, and its collision guard is entirely pre-spawn.\n    if (provider === CODEX_CLI_PROVIDER) {\n      const { outcome, session: settled } = await this.deps.waitForStartupOutcome(\n        sessionId,\n        resolveCodexStartupTimeoutMs(),\n      );\n      if (outcome === \"failed\") {\n        const failed = settled ?? this.sessionStore.getManaged(sessionId);\n        this.deps.abandonFailedStart(sessionId);\n        if (failed?.failureCode === CODEX_ACTIVE_WRITER_CODE) {\n          return {\n            ok: false,\n            reason: \"codex_session_active\",\n            detectedBy: [],\n            lastActivityMs: null,\n          };\n        }\n        return {\n          ok: false,\n          reason: \"codex_start_failed\",\n          failureReason: failed?.failureReason ?? \"Codex exited before becoming ready\",\n        };\n      }\n    }\n\n    // Watch the conversation's JSONL file for structured events\n    void this.sessionWatchers.watchConversationFile(sessionId, historyId);\n\n    // Enrich session metadata and update DB (best-effort bookkeeping; the\n    // conversation history is already in the JSONL). Now runs just before the\n    // caller writes its response rather than just after — it is a couple of\n    // local SQLite reads, on a path whose latency budget was set by seconds of\n    // Windows process enumeration, so the move is not measurable.\n    this.deps.enrichResumedSessionAsync(sessionId, projectPath, conv);\n\n    // Read AFTER enrichment: it writes to the store, so the 201 body carries\n    // sessionName/projectId/message metadata instead of the bare spawn shape.\n    const response = this.sessionStore.get(session.id, this.deps.ptyAttachedIds());\n\n    return { ok: true, alreadyRunning: false, session, response };\n  }\n\n  async handleSendInput(\n    sessionId: string,\n    req: IncomingMessage,\n    res: ServerResponse,\n  ): Promise<void> {\n    if (!this.deps.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    // Idempotency (C4). A retry — flaky network, double-tap, client resend on\n    // timeout — must not submit the same prompt to the agent twice. Checked\n    // before ANY write so a replay never reaches the PTY.\n    let idempotencyKey: string | undefined;\n    try {\n      idempotencyKey = readIdempotencyKey(body as Record<string, unknown>);\n    } catch (err) {\n      // A client that sent a malformed key believes it has retry protection;\n      // proceeding without it silently would be worse than rejecting.\n      json(res, 400, { error: err instanceof Error ? err.message : \"Invalid idempotencyKey\" });\n      return;\n    }\n    if (idempotencyKey) {\n      const replayed = this.idempotency.get(sessionId, idempotencyKey);\n      if (replayed) {\n        this.log.info(`[input.replay] ${sessionId.slice(0, 8)} duplicate idempotencyKey`, {\n          event: \"input.idempotent_replay\",\n          sessionId,\n        });\n        json(res, replayed.status, replayed.body);\n        return;\n      }\n    }\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.deps.ptyAttachedIds());\n        if (updated) {\n          this.wsHub.broadcast({ type: \"session_update\", session: updated });\n        }\n        const result = { status: 200, body: { ok: true } };\n        if (idempotencyKey) this.idempotency.set(sessionId, idempotencyKey, result);\n        json(res, result.status, result.body);\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    // Semantic input arbitration. While a permission gate or AskUserQuestion\n    // menu is up, the PTY's cursor is on the picker, so composer text would\n    // commit the highlighted option (live capture: prose typed over an open\n    // card approved a tool call). Refuse before ANY write. `{ keys }` above is\n    // deliberately not arbitrated — Esc and arrow nav are how a card is\n    // dismissed or navigated. The pending maps are the same authority the\n    // answer routes use; no screen re-scrape here, because the Claude scraper\n    // would fail open on Codex's synthesized gates. Not recorded in\n    // idempotency: a resend of the same key after the card is answered must go\n    // through, not replay this refusal.\n    //\n    // Sweeps expired prompts so their onExpire clears the pending maps before the read below.\n    this.promptRegistry.sweepExpired(sessionId);\n    // One narrow exception to \"no screen re-scrape\". A gate whose OSC fired\n    // before its options painted is stored with `options: []` and NO promptId\n    // (handlePermissionChange opens no record for it), so isLive() below reads\n    // it as live forever, and only the detector's close retires it — which\n    // pty-manager evaluates only on a pass driven by new PTY output. A screen\n    // that went quiet left every later composer send refused indefinitely.\n    // For that entry alone, the rendered screen decides: no gate painted means\n    // the entry is stale, so clear it the way the detector's close would and\n    // let the text through; a gate still painted keeps refusing. The Claude\n    // detectors are safe here: only pty-manager's OSC path produces an\n    // optionless entry — every Codex card (gateCard, command approval,\n    // usage-limit) carries options and therefore a promptId — so this never\n    // scrapes a Codex screen.\n    const optionless = this.pendingPermission.get(sessionId);\n    if (\n      optionless &&\n      optionless.promptId === undefined &&\n      !(await this.anyPermissionGateOnScreen(sessionId)) &&\n      // Re-read after the await: a populated repaint may have replaced it.\n      this.pendingPermission.get(sessionId) === optionless\n    ) {\n      this.pendingPermission.delete(sessionId);\n      this.pendingPermissionKey.delete(sessionId);\n      this.broadcastToSession(sessionId, { type: \"permission_cancelled\", sessionId });\n    }\n    // Map membership alone is not authoritative: a prompt closed by a route\n    // other than sweepExpired or the answer paths (e.g. prompt_not_found)\n    // leaves its pendingPermission/pendingQuestions entry behind with no\n    // registry record ever cleaning it up (#757). Only that genuine absence\n    // means \"gone\" — a terminal-but-still-held record (`resolved` awaiting the\n    // detector's teardown, `cancelled` from a failed freshness scrape) is left\n    // alone: the picker may still be on screen, and #703 already covers why\n    // that window keeps refusing.\n    const isLive = (promptId: string | undefined): boolean =>\n      promptId === undefined || this.promptRegistry.get(promptId) !== null;\n    const pendingPermissionEntry = this.pendingPermission.get(sessionId);\n    const pendingQuestionEntry = this.pendingQuestions.get(sessionId);\n    const openPrompt =\n      pendingPermissionEntry && isLive(pendingPermissionEntry.promptId)\n        ? \"permission\"\n        : pendingQuestionEntry && isLive(pendingQuestionEntry.promptId)\n          ? \"question\"\n          : null;\n    if (openPrompt) {\n      // An accepted permission answer writes the keys and resolves the prompt record, but the\n      // entry lives on until the detector sees the gate repaint away. Text in that window is\n      // still refused — the cursor may still be on the picker — yet \"answer the prompt\" is the\n      // wrong thing to tell someone who just answered it. `resolved` is the only registry state\n      // that means the keys were written: a refused answer leaves a `cancelled` record beside a\n      // live entry and must keep reading \"open\". Questions never reach here answered, because\n      // both accept paths delete `pendingQuestions` before returning.\n      const pendingGate = openPrompt === \"permission\" ? pendingPermissionEntry : undefined;\n      const promptState =\n        pendingGate?.promptId !== undefined &&\n        this.promptRegistry.get(pendingGate.promptId)?.state === \"resolved\"\n          ? \"answered\"\n          : \"open\";\n      this.log.info(\n        `[input.prompt_pending] ${sessionId.slice(0, 8)} kind=${openPrompt} state=${promptState}`,\n        {\n          event: \"input.prompt_pending\",\n          sessionId,\n          promptKind: openPrompt,\n          promptState,\n        },\n      );\n      json(res, 409, {\n        ok: false,\n        reason: \"prompt_pending\",\n        promptKind: openPrompt,\n        promptState,\n        error:\n          promptState === \"answered\"\n            ? \"Your answer was sent; wait for the prompt to close before sending text\"\n            : \"A prompt is waiting for an answer; answer or dismiss it before sending text\",\n      });\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.deps.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.scannerManager\n          .get()\n          .then((scanner) => this.scannerManager.refreshFileAfterWrite(scanner, filePath))\n          .then(({ outcome, meta }) => {\n            // Post-write, and through the manager rather than straight at the\n            // scanner: a direct call would be single-flighted onto whatever\n            // parse happened to be running, which may have read the file\n            // before this input reached it.\n            this.log.info(`scanner.refreshFile: ${outcome === \"refreshed\" ? \"ok\" : outcome}`, {\n              event: \"scanner.refresh\",\n              sessionId,\n              filePath,\n              trigger: \"sendInput\",\n              outcome,\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      const result = { status: 200, body: { ok: true } };\n      // Record only on success: a failed write must stay retryable, otherwise a\n      // transient error would be replayed as a permanent one.\n      if (idempotencyKey) this.idempotency.set(sessionId, idempotencyKey, result);\n      json(res, result.status, result.body);\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  // 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  handleLiveQuestion(sessionId: string, questions: AskQuestion[], occurrenceId?: string): void {\n    const key = questionContentKey(questions);\n    // Unchanged repaint: same content and, when the pty-host names the\n    // occurrence, one we already minted a prompt for (the registry keeps it\n    // under that id, answered or not, for PROMPT_TERMINAL_RETENTION_MS). The\n    // key now outlives an answer (#724), so without the occurrence half a\n    // genuinely new host occurrence with identical content would be swallowed\n    // by it — the same identity rule handlePermissionChange applies.\n    if (\n      this.pendingQuestionKey.get(sessionId) === key &&\n      (occurrenceId === undefined || this.promptRegistry.get(occurrenceId) !== null)\n    ) {\n      return; // already shown\n    }\n    const toolUseId = `screen:${sessionId}:${key.length}`;\n    const prior = this.pendingQuestions.get(sessionId);\n    const priorPrompt = prior ? this.promptRegistry.get(prior.promptId) : null;\n    if (priorPrompt?.state === \"open\" || priorPrompt?.state === \"updated\") {\n      this.promptRegistry.transition(priorPrompt.promptId, \"cancelled\", \"replaced\");\n    }\n    const prompt = this.promptRegistry.open(\n      questionPromptDraft(sessionId, questions, \"screen\"),\n      this.questionAnswerAdapter(sessionId),\n      occurrenceId,\n    );\n    this.pendingQuestions.set(sessionId, {\n      toolUseId,\n      questions,\n      origin: \"pty\",\n      promptId: prompt.promptId,\n    });\n    this.pendingQuestionKey.set(sessionId, key);\n    // Counterpart to ws.broadcast_permission below. Shape only — question and\n    // option TEXT must never enter logs, same rule the detectors follow. The\n    // absence of this line is what made a client-scraped card indistinguishable\n    // from one this server sent (#823).\n    const subscriberCount = this.sessionSubscribers.get(sessionId)?.size ?? 0;\n    this.log.info(\n      `[ws.broadcast_question] ${sessionId.slice(0, 8)} subscribers=${subscriberCount}`,\n      {\n        event: \"ws.broadcast_question\",\n        sessionId,\n        subscriberCount,\n        origin: \"screen\",\n        questionCount: questions.length,\n        optionCount: questions[0]?.options.length ?? 0,\n      },\n    );\n    this.broadcastToSession(sessionId, { type: \"question\", sessionId, toolUseId, questions });\n  }\n\n  handleJsonlQuestion(\n    sessionId: string,\n    toolUseId: string,\n    questions: AskQuestion[],\n    origin: \"pty\" | \"jsonl\",\n  ): void {\n    const prior = this.pendingQuestions.get(sessionId);\n    const sameQuestion =\n      prior !== undefined && questionContentKey(prior.questions) === questionContentKey(questions);\n    let prompt: Prompt;\n    if (sameQuestion) {\n      const current = this.promptRegistry.get(prior.promptId);\n      prompt =\n        current?.provenance.source === \"transcript\"\n          ? current\n          : this.promptRegistry.update(\n              prior.promptId,\n              questionPromptDraft(sessionId, questions, \"transcript\"),\n              this.questionAnswerAdapter(sessionId),\n            );\n    } else {\n      const priorPrompt = prior ? this.promptRegistry.get(prior.promptId) : null;\n      if (priorPrompt?.state === \"open\" || priorPrompt?.state === \"updated\") {\n        this.promptRegistry.transition(priorPrompt.promptId, \"cancelled\", \"replaced\");\n      }\n      prompt = this.promptRegistry.open(\n        questionPromptDraft(sessionId, questions, \"transcript\"),\n        this.questionAnswerAdapter(sessionId),\n      );\n    }\n    this.pendingQuestions.set(sessionId, {\n      toolUseId,\n      questions,\n      origin,\n      promptId: prompt.promptId,\n    });\n    // No broadcast on this path — the card reaches the client on the next\n    // GET /api/sessions/:id — so without this the question is pending with\n    // nothing at all in the log.\n    this.log.info(`[question.pending] ${sessionId.slice(0, 8)} origin=${origin}`, {\n      event: \"question.pending\",\n      sessionId,\n      origin,\n      questionCount: questions.length,\n      optionCount: questions[0]?.options.length ?? 0,\n    });\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  handlePermissionChange(\n    sessionId: string,\n    gate: {\n      prompt?: string;\n      detail?: string;\n      options: PermissionOption[];\n      cursor?: number;\n    } | null,\n    occurrenceId?: string,\n  ): void {\n    if (gate === null) {\n      const prior = this.pendingPermission.get(sessionId);\n      if (!prior) return;\n      // providerClosed, not transition: on a scraped gate the answer keys are\n      // what removed the box, and sendKeys fires this synchronously from inside\n      // the write. Deferring it there lets the answer settle `resolved`; a close\n      // with no answer in flight still cancels here, unchanged. The broadcast\n      // below is unconditional either way — the box IS gone, and a legacy client\n      // must be told so whichever way the record settles.\n      if (prior.promptId) this.promptRegistry.providerClosed(prior.promptId, \"provider_closed\");\n      this.pendingPermission.delete(sessionId);\n      this.pendingPermissionKey.delete(sessionId);\n      this.broadcastToSession(sessionId, { type: \"permission_cancelled\", sessionId });\n      return;\n    }\n    const key = permissionContentKey(gate);\n    const prior = this.pendingPermission.get(sessionId);\n    const priorPromptId = prior?.promptId;\n    if (\n      this.pendingPermissionKey.get(sessionId) === key &&\n      (occurrenceId === undefined || prior?.occurrenceId === occurrenceId)\n    ) {\n      return; // unchanged repaint\n    }\n    // Server-owned instance id. permissionGateKey is content-derived and cannot\n    // tell two consecutive identical gates apart; this can. The same identity\n    // as the pending gate (cursor moved, repaint) keeps its id; anything else —\n    // first open, different content, reopen after a close — is a new instance.\n    //\n    // Occurrence is compared to OCCURRENCE, never to promptId. The two were\n    // equal by construction until open() began minting a fresh id for a\n    // replayed occurrence held by a terminal record: after such a reopen the\n    // entry carries the new id while the host still sends the original\n    // occurrence (host.ts keeps it while permissionGateKey is unchanged, and\n    // that key excludes the cursor), so comparing against promptId made every\n    // later repaint a new instance — a cancel+open pair per paint, a new\n    // gateId on the wire, and any in-flight answer settling prompt_cancelled.\n    // `priorPromptId !== undefined` stays: it guards the registry lookup\n    // below, which is a separate question from identity.\n    const samePrompt =\n      prior &&\n      priorPromptId !== undefined &&\n      permissionGateKey(prior) === permissionGateKey(gate) &&\n      (occurrenceId === undefined || prior.occurrenceId === occurrenceId);\n    if (prior && !samePrompt) {\n      const priorPrompt = prior.promptId ? this.promptRegistry.get(prior.promptId) : null;\n      if (priorPrompt?.state === \"open\" || priorPrompt?.state === \"updated\") {\n        this.promptRegistry.transition(priorPrompt.promptId, \"cancelled\", \"replaced\");\n      }\n    }\n    const prompt =\n      gate.options.length === 0\n        ? null\n        : samePrompt\n          ? this.promptRegistry.get(priorPromptId)\n          : this.promptRegistry.open(\n              permissionPromptDraft(sessionId, gate),\n              this.permissionAnswerAdapter(sessionId),\n              occurrenceId,\n            );\n    if (samePrompt && !prompt) throw new Error(\"Pending permission prompt disappeared\");\n    const gateId = prompt?.promptId ?? occurrenceId ?? prior?.gateId ?? randomUUID();\n    this.pendingPermission.set(sessionId, {\n      ...gate,\n      gateId,\n      ...(prompt ? { promptId: prompt.promptId } : {}),\n      ...(occurrenceId !== undefined ? { occurrenceId } : {}),\n    });\n    this.pendingPermissionKey.set(sessionId, key);\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.broadcastToSession(sessionId, {\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      contentKey: permissionGateKey(gate),\n      gateId,\n    });\n  }\n\n  private permissionAnswerAdapter(sessionId: string): PromptAnswerAdapter {\n    return async ({ prompt, answer }): Promise<PromptAdapterResult> => {\n      const gate = this.pendingPermission.get(sessionId);\n      if (!gate || gate.promptId !== prompt.promptId) {\n        return {\n          ok: false,\n          code: \"prompt_unavailable\",\n          terminal: { state: \"unavailable\", reason: \"provider_prompt_missing\" },\n        };\n      }\n      const response = answer.responses[0];\n      const selectedId = response?.optionIds?.[0];\n      const selectedIndex = prompt.questions[0]?.options.findIndex(\n        (option) => option.optionId === selectedId,\n      );\n      if (selectedIndex === undefined || selectedIndex < 0) {\n        return { ok: false, code: \"unknown_option\" };\n      }\n      const option = gate.options[selectedIndex];\n      if (!option) return { ok: false, code: \"unknown_option\" };\n      const provider = this.sessionStore.getManaged(sessionId)?.provider;\n      if (\n        provider !== CODEX_CLI_PROVIDER &&\n        !(await this.permissionGateStillOpen(sessionId, permissionGateKey(gate)))\n      ) {\n        // The terminal below settles the registry record, but nothing else\n        // retires pendingPermission, so /input kept refusing text beside a\n        // cancelled record until retention swept it (the legacy route's\n        // gateClosed() clears it). Clear it here too — but only when NO gate is\n        // painted: a scrape also fails when a different gate has taken the\n        // screen before the detector announced it, and this entry is then what\n        // keeps composer text off that live gate (#703). The entry must still\n        // be THIS prompt, before and after the await: a newer gate that was\n        // registered meanwhile is live and must stay.\n        if (\n          this.pendingPermission.get(sessionId)?.promptId === prompt.promptId &&\n          !(await this.anyPermissionGateOnScreen(sessionId)) &&\n          this.pendingPermission.get(sessionId)?.promptId === prompt.promptId\n        ) {\n          this.pendingPermission.delete(sessionId);\n          this.pendingPermissionKey.delete(sessionId);\n          this.broadcastToSession(sessionId, { type: \"permission_cancelled\", sessionId });\n        }\n        return {\n          ok: false,\n          code: \"prompt_cancelled\",\n          terminal: { state: \"cancelled\", reason: \"provider_closed\" },\n        };\n      }\n      if (this.pendingPermission.get(sessionId)?.promptId !== prompt.promptId) {\n        return { ok: false, code: \"prompt_cancelled\" };\n      }\n      try {\n        this.ptyManager.sendKeys(\n          sessionId,\n          option.answerKeys ?? permissionAnswerKeys(option.index),\n        );\n      } catch {\n        return { ok: false, code: \"provider_error\" };\n      }\n      return { ok: true };\n    };\n  }\n\n  private questionAnswerAdapter(sessionId: string): PromptAnswerAdapter {\n    return async ({ prompt, answer }): Promise<PromptAdapterResult> => {\n      const pending = this.pendingQuestions.get(sessionId);\n      if (!pending || pending.promptId !== prompt.promptId) {\n        return {\n          ok: false,\n          code: \"prompt_unavailable\",\n          terminal: { state: \"unavailable\", reason: \"provider_prompt_missing\" },\n        };\n      }\n      const answers: Record<string, string | string[]> = {};\n      for (const question of prompt.questions) {\n        const response = answer.responses.find((item) => item.questionId === question.questionId);\n        if (!response?.optionIds) return { ok: false, code: \"unsupported_prompt_shape\" };\n        answers[question.text] = response.optionIds.map((optionId) => {\n          const option = question.options.find((item) => item.optionId === optionId);\n          return option?.label ?? \"\";\n        });\n      }\n      const resolution = resolveAnswer(pending, {\n        toolUseId: pending.toolUseId,\n        answers,\n      });\n      if (!resolution.ok) {\n        const code =\n          resolution.reason === \"unknown_option\" ||\n          resolution.reason === \"incomplete_answer\" ||\n          resolution.reason === \"unsupported_prompt_shape\"\n            ? resolution.reason\n            : \"prompt_unavailable\";\n        return { ok: false, code };\n      }\n      if (!(await this.questionMenuStillOpen(sessionId))) {\n        this.pendingQuestions.delete(sessionId);\n        this.pendingQuestionKey.delete(sessionId);\n        this.broadcastToSession(sessionId, {\n          type: \"question_cancelled\",\n          sessionId,\n          toolUseId: pending.toolUseId,\n        });\n        return {\n          ok: false,\n          code: \"prompt_cancelled\",\n          terminal: { state: \"cancelled\", reason: \"provider_closed\" },\n        };\n      }\n      if (this.pendingQuestions.get(sessionId)?.promptId !== prompt.promptId) {\n        return { ok: false, code: \"prompt_cancelled\" };\n      }\n      try {\n        this.ptyManager.sendKeys(sessionId, resolution.keys);\n      } catch {\n        return { ok: false, code: \"provider_error\" };\n      }\n      // pendingQuestionKey is KEPT, exactly as the legacy /answer route keeps\n      // it: a menu still painted after its answer must dedupe as a repaint, not\n      // re-mint as a fresh open prompt for a question already answered (#724).\n      // The menu leaving the screen clears it (onLiveQuestionGone), as does exit.\n      this.pendingQuestions.delete(sessionId);\n      this.broadcastToSession(sessionId, {\n        type: \"question_cancelled\",\n        sessionId,\n        toolUseId: pending.toolUseId,\n      });\n      return { ok: true };\n    };\n  }\n\n  /**\n   * Answer a permission gate — the validated counterpart of POST /:id/input.\n   *\n   * `/input` is a raw-bytes conduit (arrow-key nav uses it too) and stays that\n   * way; this route is the semantic one, mirroring the /answer split. Two\n   * things make it more than validation theatre:\n   *\n   *   - The client sends `{ contentKey, optionIndex }` and NO keystrokes. The\n   *     keys are derived here from our own copy of the gate, so the client's\n   *     key-derivation can never drift from the server's.\n   *   - `optionIndex` is a 0-based POSITION in the frame's `options[]`, NOT\n   *     `options[].index` (the digit painted on screen, 1-based and not always\n   *     contiguous). Both are small integers, so a client sending the digit\n   *     selects a different option — on \"1. Yes / 2. Yes, don't ask again\" the\n   *     digit for \"Yes\" is the position of \"don't ask again\". Optional\n   *     `optionLabel` binds the answer to the option the client displayed: when\n   *     present it must equal `options[optionIndex].label` or the answer is\n   *     refused as unknown_option. Absent, the position alone is trusted\n   *     (released clients).\n   *   - `contentKey` binds the answer to a specific gate. `isPermissionAnswer`\n   *     matches structurally, and approval gates repeat constantly (\"2. Yes /\n   *     3. No\" for every tool call), so without this a delayed answer to gate A\n   *     could be written as gate B's answer — a user approving a bash command\n   *     they never saw, with a 200 and a normal permission_cancelled. Treat the\n   *     check as a security boundary.\n   *\n   * Every refusal happens BEFORE sendKeys. On success we deliberately broadcast\n   * nothing: the PTY-side close (isPermissionAnswer in pty-manager) recognises\n   * the bytes we just wrote and fires permission_cancelled itself.\n   */\n  async handlePermissionAnswer(\n    sessionId: string,\n    req: IncomingMessage,\n    res: ServerResponse,\n  ): Promise<void> {\n    const body = await readBody(req);\n    const contentKey = body?.contentKey;\n    const optionIndex = body?.optionIndex;\n    const gateId = body?.gateId;\n    const optionLabel = body?.optionLabel;\n    if (typeof contentKey !== \"string\" || !Number.isInteger(optionIndex) || optionIndex < 0) {\n      json(res, 400, { ok: false, reason: \"Expected { contentKey: string, optionIndex: number }\" });\n      return;\n    }\n    if (gateId !== undefined && typeof gateId !== \"string\") {\n      json(res, 400, { ok: false, reason: \"Expected gateId to be a string\" });\n      return;\n    }\n    if (optionLabel !== undefined && typeof optionLabel !== \"string\") {\n      json(res, 400, { ok: false, reason: \"Expected optionLabel to be a string\" });\n      return;\n    }\n\n    // The client's card is dead. Clear it everywhere — permission_cancelled is\n    // unconditional here (a no-op for clients showing nothing) because a client\n    // that got this far believes a gate is up and must be told it isn't.\n    const gateClosed = (): void => {\n      const pending = this.pendingPermission.get(sessionId);\n      const prompt = pending?.promptId ? this.promptRegistry.get(pending.promptId) : null;\n      if (prompt?.state === \"open\" || prompt?.state === \"updated\") {\n        this.promptRegistry.transition(prompt.promptId, \"cancelled\", \"provider_closed\");\n      }\n      this.pendingPermission.delete(sessionId);\n      this.pendingPermissionKey.delete(sessionId);\n      this.broadcastToSession(sessionId, { type: \"permission_cancelled\", sessionId });\n      json(res, 409, { ok: false, reason: \"gate_closed\" });\n    };\n\n    // Cheapest first, screen scrape last. Every branch below returns before\n    // sendKeys — that ordering is the point of the route.\n    const gate = this.pendingPermission.get(sessionId);\n    if (!gate) {\n      gateClosed();\n      return;\n    }\n    // Instance check. A stale gateId means the client is answering a gate that\n    // has since closed and reopened — possibly with identical content, which\n    // contentKey alone would wave through. Refuse quietly, exactly like the\n    // content mismatch below: the live gate is fine and must stay up.\n    if (gateId !== undefined && gateId !== gate.gateId) {\n      json(res, 409, { ok: false, reason: \"gate_mismatch\" });\n      return;\n    }\n    if (gateId === undefined) {\n      // Temporary compatibility path for clients that predate gateId. Logged\n      // (metadata only) so its use is measurable and the path can be retired.\n      this.log.info(`[permission.answer_legacy_identity] ${sessionId.slice(0, 8)}`, {\n        event: \"permission.answer_legacy_identity\",\n        sessionId,\n      });\n    }\n    // A DIFFERENT gate is open, and it is legitimately on screen. Refuse, but\n    // broadcast nothing: permission_cancelled is session-wide, so it would\n    // clear a live card on every client, and the pendingPermissionKey dedupe\n    // means the repaint that would restore it may never come — a gate is a\n    // waiting screen. The requesting client clears from the reason instead.\n    // (Same shape as resolveAnswer's tool_use_mismatch, which also stays quiet.)\n    if (permissionGateKey(gate) !== contentKey) {\n      json(res, 409, { ok: false, reason: \"gate_mismatch\" });\n      return;\n    }\n    const option = gate.options[optionIndex];\n    if (!option) {\n      json(res, 409, { ok: false, reason: \"unknown_option\" });\n      return;\n    }\n    // Option check. contentKey and gateId pin the gate, not the choice: a\n    // client that sent options[].index instead of a position lands on another\n    // option of the SAME gate — possibly a persistent grant. Quiet, like\n    // unknown_option: the gate itself is fine and must stay up.\n    if (optionLabel !== undefined && option.label !== optionLabel) {\n      json(res, 409, { ok: false, reason: \"unknown_option\" });\n      return;\n    }\n    // Our copy agrees; now ask the screen, which is fresher than the map — for\n    // Claude. Codex gates are synthesized by its runner from its own TUI\n    // patterns, and the Claude box scraper never matches a Codex screen, so\n    // asking it refused every Codex answer as gate_closed. The Codex runner\n    // clears the pending gate itself when the dialog leaves the screen, so for\n    // Codex the map is the authority and gateId carries instance identity.\n    const provider = this.sessionStore.getManaged(sessionId)?.provider;\n    if (\n      provider !== CODEX_CLI_PROVIDER &&\n      !(await this.permissionGateStillOpen(sessionId, contentKey))\n    ) {\n      gateClosed();\n      return;\n    }\n\n    try {\n      // Same self-close as the prompt route: these keys remove the box, so\n      // sendKeys tears the record down from inside the write and the resolve\n      // below used to find it already `cancelled` — a 200 over a cancelled\n      // record, invisible because this route's status never reads the registry.\n      this.promptRegistry.whileAnswering(gate.promptId ?? \"\", () =>\n        this.ptyManager.sendKeys(\n          sessionId,\n          option.answerKeys ?? permissionAnswerKeys(option.index),\n        ),\n      );\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    const normalized = gate.promptId ? this.promptRegistry.get(gate.promptId) : null;\n    if (normalized?.state === \"open\" || normalized?.state === \"updated\") {\n      this.promptRegistry.transition(normalized.promptId, \"resolved\", \"answered_legacy\");\n    }\n    json(res, 200, { ok: true });\n  }\n\n  /**\n   * Is THIS gate still the one on screen?\n   *\n   * Deliberately stricter than questionMenuStillOpen's \"is a menu up\": the\n   * staleness window this exists to cover (the ~300ms scrape throttle plus the\n   * wait for the next PTY chunk) is exactly where pendingPermission still says\n   * gate A while the screen has moved to gate B — and since approval gates\n   * repeat their shape, \"some gate is open\" would wave that through.\n   *\n   * Reads 60 lines because that is the window the detector that produced the\n   * pending gate uses (pty-manager's scrape); `detail` walks up to 6 lines\n   * above the prompt, so a shorter window can truncate it and manufacture a\n   * mismatch on a healthy gate.\n   *\n   * Best-effort, like questionMenuStillOpen: a session we hold no PTY for, or\n   * one that raced away mid-read, is not ours to veto.\n   */\n  private async permissionGateStillOpen(sessionId: string, contentKey: string): Promise<boolean> {\n    if (!this.ptyManager.hasSession(sessionId)) return true;\n    try {\n      const lines = await this.ptyManager.getOutputLines(sessionId, 60);\n      // Both producers of a Claude-session gate, not just the box scraper: a\n      // card raised by detectShellPrompt (`read -p \"[y/N]\"`, \"press Enter\") is\n      // never a Claude box, so checking scrapePermissionGate alone refused every\n      // answer to one as closed and its Continue could never write the \\r.\n      // Every producer of a Claude-session gate, not just the box scraper: a\n      // card raised by detectShellPrompt, or the unnumbered startup choices\n      // (workspace trust) that no numbered scraper can see, would otherwise be\n      // refused as closed and its answer keys never written.\n      return [\n        scrapePermissionGate(lines),\n        detectShellPrompt(lines),\n        detectStartupChoiceGate(lines),\n      ].some((onScreen) => onScreen !== null && permissionGateKey(onScreen) === contentKey);\n    } catch {\n      return true;\n    }\n  }\n\n  /**\n   * Is ANY Claude permission gate painted? Not permissionGateStillOpen's \"is\n   * THIS gate\": an optionless entry has no content to compare against. The test\n   * is pty-manager's own \"box is still painted\" rule (either detector sees a\n   * gate), so this never retires an entry the detector would have kept — which\n   * also means a numbered list left in prose reads as painted and keeps\n   * refusing, the safe side. A shell prompt counts too: pty-manager raises it\n   * as a card, and composer text typed over a `[y/N]` answers it. Best-effort\n   * in the direction opposite to the answer routes: this may only UNBLOCK\n   * input, so a session with no PTY, or a read that fails, reports a gate.\n   */\n  private async anyPermissionGateOnScreen(sessionId: string): Promise<boolean> {\n    if (!this.ptyManager.hasSession(sessionId)) return true;\n    try {\n      const lines = await this.ptyManager.getOutputLines(sessionId, 60);\n      return (\n        detectGateScreen(lines) !== null ||\n        scrapePermissionGate(lines) !== null ||\n        detectShellPrompt(lines) !== null ||\n        // The unnumbered startup choices (workspace trust) are a gate too, and\n        // none of the scrapers above can see them — without this an answer to\n        // one would be refused as \"no gate on screen\".\n        detectStartupChoiceGate(lines) !== null\n      );\n    } catch {\n      return true;\n    }\n  }\n\n  /** Deliberately narrower than /input { keys }: fixed actions, no arbitrary bytes. */\n  async handleRawKey(sessionId: string, req: IncomingMessage, res: ServerResponse): Promise<void> {\n    const parsed = RawKeySchema.safeParse(await readBody(req));\n    if (!parsed.success) {\n      json(res, 400, { ok: false, code: \"invalid_raw_key\" });\n      return;\n    }\n    const { action, promptId, confirm } = parsed.data;\n    if (!this.ptyManager.hasSession(sessionId)) {\n      json(res, 409, { ok: false, code: \"raw_key_unavailable\" });\n      return;\n    }\n    let focused:\n      | { kind: \"permission\"; promptId: string; contentKey: string }\n      | { kind: \"question\"; promptId: string; toolUseId: string }\n      | null = null;\n    // Binding is opt-in on the payload, not implied by the action. Every other\n    // action must name its prompt (the `!promptId` refusal below); Escape may,\n    // and when it does it gets the same arbitration. Without it, a card's\n    // Cancel whose gate already closed lands at Claude's prompt and interrupts\n    // the turn the user is waiting on. Escape WITHOUT a promptId stays exactly\n    // as blind as before: the raw-keyboard Esc key and mobile's \"interrupt the\n    // agent\" action have no prompt to name and rely on that.\n    if (action !== \"escape\" || promptId !== undefined) {\n      // Pending maps can retain a prompt after its registry record expired or\n      // was retired by another route. Sweep first, then only let a live\n      // registry record authorize bytes to reach the PTY.\n      this.promptRegistry.sweepExpired(sessionId);\n      // The PTY cursor is on a permission gate when both maps coexist. This is\n      // the same arbitration order as composer input; accepting the question\n      // id here would send navigation to a different focused dialog.\n      const permission = this.pendingPermission.get(sessionId);\n      const question = this.pendingQuestions.get(sessionId);\n      const livePermission = permission?.promptId\n        ? this.promptRegistry.get(permission.promptId)\n        : null;\n      const liveQuestion = question?.promptId ? this.promptRegistry.get(question.promptId) : null;\n      if (\n        permission?.promptId &&\n        (livePermission?.state === \"open\" || livePermission?.state === \"updated\")\n      ) {\n        focused = {\n          kind: \"permission\",\n          promptId: permission.promptId,\n          contentKey: permissionGateKey(permission),\n        };\n      } else if (\n        question?.promptId &&\n        (liveQuestion?.state === \"open\" || liveQuestion?.state === \"updated\")\n      ) {\n        focused = { kind: \"question\", promptId: question.promptId, toolUseId: question.toolUseId };\n      }\n      if (!promptId || promptId !== focused?.promptId) {\n        json(res, 409, { ok: false, code: \"raw_key_stale\" });\n        return;\n      }\n      if (action === \"enter\" && confirm !== true) {\n        json(res, 400, { ok: false, code: \"raw_key_confirmation_required\" });\n        return;\n      }\n      // The registry is only an event record; a host-keyboard answer, Esc, or\n      // the next PTY repaint can leave it open briefly after its picker has\n      // gone. Use the same rendered-screen checks as the answer routes before\n      // navigation reaches whatever the terminal is showing now.\n      if (focused?.kind === \"question\" && !(await this.questionMenuStillOpen(sessionId))) {\n        const pending = this.pendingQuestions.get(sessionId);\n        if (pending?.promptId === focused.promptId) {\n          const prompt = this.promptRegistry.get(focused.promptId);\n          if (prompt?.state === \"open\" || prompt?.state === \"updated\") {\n            this.promptRegistry.transition(prompt.promptId, \"cancelled\", \"provider_closed\");\n          }\n          this.pendingQuestions.delete(sessionId);\n          this.pendingQuestionKey.delete(sessionId);\n          this.broadcastToSession(sessionId, {\n            type: \"question_cancelled\",\n            sessionId,\n            toolUseId: focused.toolUseId,\n          });\n        }\n        json(res, 409, { ok: false, code: \"raw_key_stale\" });\n        return;\n      }\n      if (\n        focused?.kind === \"permission\" &&\n        this.sessionStore.getManaged(sessionId)?.provider !== CODEX_CLI_PROVIDER &&\n        !(await this.permissionGateStillOpen(sessionId, focused.contentKey))\n      ) {\n        const pending = this.pendingPermission.get(sessionId);\n        if (pending?.promptId === focused.promptId) {\n          const prompt = this.promptRegistry.get(focused.promptId);\n          if (prompt?.state === \"open\" || prompt?.state === \"updated\") {\n            this.promptRegistry.transition(prompt.promptId, \"cancelled\", \"provider_closed\");\n          }\n          this.pendingPermission.delete(sessionId);\n          this.pendingPermissionKey.delete(sessionId);\n          this.broadcastToSession(sessionId, { type: \"permission_cancelled\", sessionId });\n        }\n        json(res, 409, { ok: false, code: \"raw_key_stale\" });\n        return;\n      }\n    }\n    try {\n      if (action === \"enter\") {\n        // Enter commits the selected option, so it is user input rather than\n        // navigation: mark waiting_input → running before retiring the prompt.\n        this.ptyManager.sendKeys(sessionId, RAW_KEY_BYTES[action]);\n      } else {\n        this.ptyManager.sendRawKeys(sessionId, RAW_KEY_BYTES[action]);\n      }\n      // A bound Escape retires its prompt as deterministically as Enter does,\n      // but as `cancelled`: Esc dismisses the prompt, it does not answer it.\n      // Clearing the dedupe key below is deliberate (for a question, on Escape\n      // only — see there): if the key did not take and the box is still\n      // painted, the detector must be able to show the card again — a gate\n      // still on screen must never stay hidden.\n      if (focused && (action === \"enter\" || action === \"escape\")) {\n        const normalized = this.promptRegistry.get(focused.promptId);\n        if (normalized?.state === \"open\" || normalized?.state === \"updated\") {\n          if (action === \"enter\") {\n            this.promptRegistry.transition(normalized.promptId, \"resolved\", \"raw_key_enter\");\n          } else {\n            this.promptRegistry.transition(normalized.promptId, \"cancelled\", \"raw_key_escape\");\n          }\n        }\n        if (focused.kind === \"permission\") {\n          this.pendingPermission.delete(sessionId);\n          this.pendingPermissionKey.delete(sessionId);\n          this.broadcastToSession(sessionId, { type: \"permission_cancelled\", sessionId });\n        } else {\n          this.pendingQuestions.delete(sessionId);\n          // An answered menu keeps its key like every other answer path, so a\n          // still-painted repaint does not re-mint it (#724). Escape answered\n          // nothing: a menu it failed to close must be able to show again.\n          if (action === \"escape\") this.pendingQuestionKey.delete(sessionId);\n          this.broadcastToSession(sessionId, {\n            type: \"question_cancelled\",\n            sessionId,\n            toolUseId: focused.toolUseId,\n          });\n        }\n      }\n      json(res, 200, { ok: true });\n    } catch {\n      json(res, 409, { ok: false, code: \"raw_key_unavailable\" });\n    }\n  }\n\n  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      // Shapes this PTY path cannot answer (multi-question, multi-select, or an\n      // answer for a question that was never given) fail closed: nothing is\n      // written, and the client is told where the prompt can still be answered.\n      const unanswerable =\n        resolution.reason === \"unsupported_prompt_shape\" ||\n        resolution.reason === \"incomplete_answer\";\n      json(res, 400, {\n        ok: false,\n        reason: resolution.reason,\n        ...(unanswerable\n          ? {\n              error:\n                \"This prompt needs an answer the app cannot give yet; answer it in the terminal\",\n            }\n          : {}),\n      });\n      return;\n    }\n    // pending is guaranteed defined when resolution.ok is true (resolveAnswer guards it)\n    const toolUseId = pending?.toolUseId ?? \"\";\n    // A menu can close without this route answering it — Esc via /input { keys },\n    // an answer typed at the host keyboard, /clear, the model giving up. Nothing\n    // clears pendingQuestions in those cases (onLiveQuestionGone has no producer),\n    // so the client keeps a live-looking card and these keystrokes would be typed\n    // into the prompt box instead of the picker. The rendered screen is the only\n    // authority on whether the picker is still up.\n    if (!(await this.questionMenuStillOpen(sessionId))) {\n      const prompt = this.promptRegistry.get(pending?.promptId ?? \"\");\n      if (prompt?.state === \"open\" || prompt?.state === \"updated\") {\n        this.promptRegistry.transition(prompt.promptId, \"cancelled\", \"provider_closed\");\n      }\n      this.pendingQuestions.delete(sessionId);\n      this.pendingQuestionKey.delete(sessionId);\n      this.broadcastToSession(sessionId, { type: \"question_cancelled\", sessionId, toolUseId });\n      json(res, 409, { ok: false, reason: \"question_gone\" });\n      return;\n    }\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    const normalized = this.promptRegistry.get(pending?.promptId ?? \"\");\n    if (normalized?.state === \"open\" || normalized?.state === \"updated\") {\n      this.promptRegistry.transition(normalized.promptId, \"resolved\", \"answered_legacy\");\n    }\n    this.pendingQuestions.delete(sessionId);\n    this.broadcastToSession(sessionId, { type: \"question_cancelled\", sessionId, toolUseId });\n    json(res, 200, { ok: true });\n  }\n\n  /**\n   * Answer a normalized prompt by its opaque ids.\n   *\n   * Refusals are keyed by `code` — the stable machine taxonomy of the prompt\n   * contract. The released legacy routes (`/answer`, `/permission/answer`) key\n   * theirs by `reason` and keep doing so; a client reads whichever key belongs\n   * to the route it called, and the two vocabularies are not merged.\n   *\n   * Status follows the same split as the legacy routes: a malformed or\n   * unanswerable *request* is 400, a prompt whose *state* refuses the answer is\n   * 409. A retry after PROMPT_TERMINAL_RETENTION_MS answers 404\n   * `prompt_not_found`, not the recorded outcome — the record it would replay\n   * is gone by then.\n   */\n  async handlePromptAnswer(\n    sessionId: string,\n    req: IncomingMessage,\n    res: ServerResponse,\n  ): Promise<void> {\n    const parsed = PromptAnswerSchema.safeParse(await readBody(req));\n    if (!parsed.success) {\n      json(res, 400, { ok: false, code: \"invalid_prompt_answer\" });\n      return;\n    }\n    const outcome = await this.promptRegistry.answer(sessionId, parsed.data);\n    if (outcome.ok) {\n      json(res, 200, outcome);\n      return;\n    }\n    json(res, promptAnswerStatus(outcome.code), outcome);\n  }\n\n  // Best-effort: a session we don't own a PTY for, or one that raced away\n  // mid-read, is not ours to veto — say yes and let the write decide.\n  private async questionMenuStillOpen(sessionId: string): Promise<boolean> {\n    if (!this.ptyManager.hasSession(sessionId)) return true;\n    try {\n      return isQuestionMenuOnScreen(await this.ptyManager.getOutputLines(sessionId, PTY_ROWS));\n    } catch {\n      return true;\n    }\n  }\n\n  async handleUploadFile(\n    sessionId: string,\n    req: IncomingMessage,\n    res: ServerResponse,\n  ): Promise<void> {\n    const session = this.sessionStore.get(sessionId, this.deps.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  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  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  /** Force-kill: SIGKILL instead of /stop's graceful SIGINT. */\n  async handleKillSession(\n    sessionId: string,\n    res: ServerResponse,\n    opts: { delete?: boolean } = {},\n  ): Promise<void> {\n    await this.handleStopSession(sessionId, res, { signal: \"SIGKILL\", delete: opts.delete });\n  }\n\n  async handleStopSession(\n    sessionId: string,\n    res: ServerResponse,\n    opts: {\n      signal?: NodeJS.Signals;\n      when?: \"now\" | \"idle\";\n      ignoreWatchers?: boolean;\n      delete?: boolean;\n    } = {},\n  ): Promise<void> {\n    if (opts.when === \"idle\") {\n      this.handleStopSessionWhenIdle(\n        sessionId,\n        res,\n        opts.ignoreWatchers ?? false,\n        opts.delete ?? false,\n      );\n      return;\n    }\n    const signal = opts.signal ?? \"SIGINT\";\n    const deleteAfter = opts.delete ?? false;\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    // Capture before putOnHold: the runner deletes the live session on hold,\n    // and onStatusChange then writes selfPtyEndedAt / a registry idle row that\n    // forgetSession has to clear afterwards.\n    const shouldForget = this.shouldForgetEmptySession(session);\n\n    if (session.status === \"idle\") {\n      if (shouldForget) this.forgetEmptyStoppedSession(sessionId);\n      if (deleteAfter) this.softDeleteConversation(sessionId);\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, signal);\n    this.discoveryCache = null;\n\n    const outcome = await Promise.race([idlePromise, timeoutPromise]);\n\n    if (shouldForget) this.forgetEmptyStoppedSession(sessionId);\n    if (deleteAfter) this.softDeleteConversation(sessionId);\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  /**\n   * An unused start: the user never submitted a prompt, and the conversation\n   * cache has no row for this id (empty Codex/Claude often never write a JSONL).\n   * `conversationId === sessionId` is not evidence of history — only the cache\n   * is. promptCount > 0 or a cache hit keeps today's hold path.\n   */\n  private shouldForgetEmptySession(session: ManagedSession): boolean {\n    const stored = this.sessionStore.getManaged(session.id);\n    const promptCount = Math.max(session.promptCount, stored?.promptCount ?? 0);\n    if (promptCount > 0) return false;\n    return !this.hasCachedConversationFor(session, stored);\n  }\n\n  private hasCachedConversationFor(\n    session: ManagedSession,\n    stored: ManagedSession | null,\n  ): boolean {\n    const cache = this.cache;\n    if (!cache) return false;\n    const ids = new Set<string>([session.id]);\n    if (session.boundConversationId) ids.add(session.boundConversationId);\n    if (session.resumedFromConversationId) ids.add(session.resumedFromConversationId);\n    if (stored?.boundConversationId) ids.add(stored.boundConversationId);\n    if (stored?.resumedFromConversationId) ids.add(stored.resumedFromConversationId);\n    for (const id of ids) {\n      if (cache.hasConversation(id)) return true;\n    }\n    return false;\n  }\n\n  /**\n   * `when=idle`: hold now if the session is already settled, otherwise arm the\n   * existing kill-on-idle latch (`armHoldWhenIdle`, the same one mobile's\n   * `hold_session {when: \"waiting_input\"}` uses) to fire on the next natural\n   * idle transition. Unlike `when=now`, this checks watchers UP FRONT and\n   * reports the count rather than silently no-op'ing later — `ignoreWatchers`\n   * skips that check (and the latch's own fire-time check) entirely.\n   */\n  private handleStopSessionWhenIdle(\n    sessionId: string,\n    res: ServerResponse,\n    ignoreWatchers: boolean,\n    deleteAfter: boolean,\n  ): void {\n    if (!ignoreWatchers) {\n      const watcherCount = this.sessionSubscribers.get(sessionId)?.size ?? 0;\n      if (watcherCount > 0) {\n        json(res, 200, { status: \"watchers_present\", watcherCount, sessionId });\n        return;\n      }\n    }\n    // The delete itself happens in server.ts once the latch actually fires\n    // (immediately, for \"held\", or later for \"armed\") — never here, since an\n    // \"armed\" result hasn't killed anything yet.\n    const result = this.deps.armHoldWhenIdle(sessionId, { ignoreWatchers, deleteAfter });\n    if (result === \"no_session\") {\n      json(res, 404, { error: \"Session not found\" });\n      return;\n    }\n    json(res, 200, { status: result === \"held\" ? \"killed\" : \"armed\", sessionId });\n  }\n\n  private forgetEmptyStoppedSession(sessionId: string): void {\n    this.log.info(`[stop] forgetting empty session ${sessionId.slice(0, 8)}`, {\n      event: \"session.forget_empty\",\n      sessionId,\n    });\n    this.deps.forgetSession(sessionId);\n    this.wsHub.broadcast({\n      type: \"session_list\",\n      sessions: this.sessionStore.list(this.deps.ptyAttachedIds()),\n    });\n  }\n\n  /**\n   * Same empty-unused check stop uses (promptCount === 0 and no cache row,\n   * including boundConversationId / resumedFromConversationId). Safe after\n   * putOnHold: the runner may already have dropped the live session.\n   */\n  forgetIfEmptyUnused(sessionId: string): void {\n    const live = this.ptyManager.getSession(sessionId);\n    const stored = this.sessionStore.getManaged(sessionId);\n    const session = live ?? stored;\n    if (!session) return;\n    if (!this.shouldForgetEmptySession(session)) return;\n    this.forgetEmptyStoppedSession(sessionId);\n  }\n\n  /**\n   * Soft-deletes the cached conversation(s) this session maps to — the\n   * session id itself, plus any bound/resumed-from alias `hasCachedConversationFor`\n   * already knows how to chase (a Codex placeholder id vs. its real rollout\n   * id, etc.). Reads from `sessionStore`, not `ptyManager`, so it's safe to\n   * call after `putOnHold` has already dropped the live session.\n   */\n  softDeleteConversation(sessionId: string): void {\n    const cache = this.cache;\n    if (!cache) return;\n    const stored = this.sessionStore.getManaged(sessionId);\n    const ids = new Set<string>([sessionId]);\n    if (stored?.boundConversationId) ids.add(stored.boundConversationId);\n    if (stored?.resumedFromConversationId) ids.add(stored.resumedFromConversationId);\n    for (const id of ids) {\n      if (cache.hasConversation(id)) cache.softDeleteConversation(id);\n    }\n  }\n\n  /**\n   * Take over a Codex conversation from the standalone TUI that holds it.\n   *\n   * The owner is re-probed HERE rather than trusted from the 409 that offered\n   * the action: that pid was observed when resume was refused, possibly minutes\n   * earlier, and pids are reused. Killing a stale pid would stop an unrelated\n   * process, which is the one mistake this path must never make.\n   *\n   * Only a standalone `codex` TUI is eligible. A desktop / VS Code\n   * `codex app-server` hosts unrelated conversations in the same process, and\n   * an unidentified owner offers nothing to prove it does not — both refuse,\n   * and forking stays the recovery path for them.\n   */\n  private async adoptCodexRolloutOwner(\n    sessionId: string,\n    res: ServerResponse,\n    ownedHere: boolean,\n  ): Promise<void> {\n    if (ownedHere) {\n      json(res, 404, { error: \"Discovered session not found\" });\n      return;\n    }\n\n    const target = await this.deps.resolveConversationTarget(sessionId);\n    if (!target.ok || target.provider !== CODEX_CLI_PROVIDER || !target.historyPath) {\n      json(res, 404, { error: \"Discovered session not found\" });\n      return;\n    }\n\n    const availability = classifyResumability(target.projectPath);\n    if (!availability.resumable) {\n      json(res, 400, {\n        error: \"Cannot take over this session: its project directory no longer exists\",\n        code: \"ADOPT_PROJECT_PATH_MISSING\",\n        reason: availability.unavailable_reason,\n      });\n      return;\n    }\n\n    const owner = await findRolloutOwner(target.historyPath);\n    if (!owner) {\n      // Nobody holds it now — the collision that prompted this has cleared, so\n      // there is nothing to take over and a plain resume will succeed.\n      json(res, 409, {\n        error: \"Nothing is holding this conversation now; resume it instead\",\n        code: \"ADOPT_NO_OWNER\",\n      });\n      return;\n    }\n    if (owner.source !== \"terminal\") {\n      this.log.warn(`[adopt] refusing takeover of ${owner.command} (pid ${owner.pid})`, {\n        event: \"adopt.owner_not_terminal\",\n        sessionId,\n        ownerPid: owner.pid,\n        ownerCommand: owner.command,\n      });\n      json(res, 409, {\n        error:\n          \"This conversation is held by a process that may host other conversations; forking is the safe option\",\n        code: \"ADOPT_OWNER_NOT_TERMINAL\",\n        ownerCommand: owner.command,\n      });\n      return;\n    }\n\n    this.log.info(`[adopt] taking over codex rollout from pid ${owner.pid}`, {\n      event: \"session.codex_takeover\",\n      sessionId,\n      historyId: target.historyId,\n      ownerPid: owner.pid,\n    });\n\n    await this.killAndRespawn(res, {\n      sessionId,\n      spawnId: sessionId,\n      pid: owner.pid,\n      provider: CODEX_CLI_PROVIDER,\n      projectPath: target.projectPath,\n      projectName: target.conv?.projectName,\n      // `codex resume` takes the rollout id, which is not necessarily the id\n      // the client navigated to (that may be a local placeholder).\n      resumeId: target.historyId,\n    });\n  }\n\n  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.deps.ptyAttachedIds());\n    if (!discSession || discSession.ptyAttached) {\n      // A Codex collision usually has no discovered row to adopt: discovery\n      // matches on argv, and a Codex process need not carry its rollout uuid\n      // there. The open file handle on the rollout is the signal that does see\n      // it, so fall back to that before refusing.\n      await this.adoptCodexRolloutOwner(sessionId, res, discSession?.ptyAttached === true);\n      return;\n    }\n\n    const { branch } = discSession;\n    let { projectPath, projectName } = 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    // Windows exposes no process CWD (neither CIM nor wmic carries it), so\n    // discovery reports an empty projectPath rather than fabricating one. Fall\n    // back to the conversation's own JSONL — the same authoritative source\n    // handleResume uses, since it is the file Claude looks up by filename when\n    // processing --resume. Every session reaching adopt has a conversation id\n    // (SessionStore drops discovered processes without one), so this resolves\n    // in the normal case on every platform.\n    if (!projectPath) {\n      const jsonlPath = this.deps.findJsonlPath(convId);\n      const jsonlCwd = jsonlPath ? await this.deps.readCwdFromJsonl(jsonlPath) : null;\n      if (jsonlCwd) {\n        projectPath = jsonlCwd;\n        projectName = projectName || basename(jsonlCwd);\n      }\n    }\n\n    // Refuse BEFORE killing anything if we could not resolve where the process\n    // is running. Adopt is destructive-then-restorative, so every reason it\n    // cannot restore has to be checked first: spawning the replacement with an\n    // empty cwd fails outright, and killing first then discovering this would\n    // destroy the user's session with nothing to put back in its place.\n    if (!projectPath) {\n      this.log.warn(\"adopt: refusing, working directory unknown\", {\n        event: \"adopt.no_project_path\",\n        sessionId,\n        pid: discSession.pid,\n      });\n      json(res, 400, {\n        error:\n          \"Cannot take over this session: its working directory could not be determined on this platform\",\n        code: \"ADOPT_NO_PROJECT_PATH\",\n      });\n      return;\n    }\n\n    // Same reasoning one step further: a conversation whose project directory\n    // was deleted (or whose worktree was removed) cannot be respawned there, so\n    // refuse while the external session is still alive rather than killing it\n    // and failing on spawn.\n    const availability = classifyResumability(projectPath);\n    if (!availability.resumable) {\n      this.log.warn(\"adopt: refusing, project directory no longer exists\", {\n        event: \"adopt.project_path_missing\",\n        sessionId,\n        pid: discSession.pid,\n        projectPath,\n        reason: availability.unavailable_reason,\n      });\n      json(res, 400, {\n        error: \"Cannot take over this session: its project directory no longer exists\",\n        code: \"ADOPT_PROJECT_PATH_MISSING\",\n        reason: availability.unavailable_reason,\n      });\n      return;\n    }\n\n    await this.killAndRespawn(res, {\n      sessionId,\n      spawnId: convId,\n      pid: discSession.pid,\n      provider: discSession.provider,\n      projectPath,\n      projectName,\n      branch,\n      // No `resumeId` — a discovered Codex process states its rollout id in\n      // argv (`codex resume <uuid>`), so the conversation id already IS the\n      // provider-side id, which is the case that field exists to cover.\n    });\n  }\n\n  /**\n   * The destructive half of a takeover, shared by every path that reaches it:\n   * stop the process that owns the conversation, prove it is gone, then respawn\n   * the conversation under this streamer.\n   *\n   * Both halves matter. SIGTERM is asynchronous, and spawning a resume before\n   * the old process has actually exited leaves two agents appending to one\n   * transcript — the interleaved state this codebase has no way to repair. A\n   * process that outlives the grace period aborts the takeover rather than\n   * knowingly creating that.\n   */\n  private async killAndRespawn(\n    res: ServerResponse,\n    opts: {\n      /** The conversation the client asked about — for logs. */\n      sessionId: string;\n      /** The id the new session is keyed by. */\n      spawnId: string;\n      pid: number;\n      provider?: ProviderName;\n      projectPath: string;\n      projectName?: string;\n      branch?: string;\n      /** Provider-side id for the resume, when it differs from `spawnId`. */\n      resumeId?: string;\n    },\n  ): Promise<void> {\n    this.ptyManager.killPid(opts.pid);\n    const exited = await waitForProcessExit(opts.pid, ADOPT_KILL_TIMEOUT_MS);\n    if (!exited) {\n      this.log.warn(\"adopt: external process did not exit; refusing to double-write\", {\n        event: \"adopt.kill_timeout\",\n        sessionId: opts.sessionId,\n        pid: opts.pid,\n      });\n      json(res, 409, {\n        error:\n          \"The existing process did not exit; not starting a second agent on this conversation\",\n        code: \"ADOPT_KILL_TIMEOUT\",\n        pid: opts.pid,\n      });\n      return;\n    }\n\n    // The provider has to be carried: `LiveSessionManager.start` defaults to\n    // Claude when it is absent, so adopting a Codex session used to respawn it\n    // as Claude against a Codex rollout id.\n    const session = await this.ptyManager.start(opts.spawnId, {\n      provider: opts.provider,\n      projectPath: opts.projectPath,\n      projectName: opts.projectName,\n      branch: opts.branch,\n      ...(opts.resumeId != null && { resumeId: opts.resumeId }),\n      claudeFlags: this.claudeFlags,\n      claudeExtraArgs: this.claudeExtraArgs,\n      ...this.deps.spawnFlagOverrides(),\n    });\n\n    this.sessionStore.addManaged(session);\n    this.registryBoot.recordSessionSpawn(session);\n    void this.sessionWatchers.watchConversationFile(session.id);\n\n    this.wsHub.broadcast({\n      type: \"session_list\",\n      sessions: this.sessionStore.list(this.deps.ptyAttachedIds()),\n    });\n\n    json(res, 201, { sessionId: session.id });\n  }\n\n  async handleStartSession(req: IncomingMessage, res: ServerResponse): Promise<void> {\n    const ip = req.socket?.remoteAddress ?? \"unknown\";\n    if (!this.deps.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.deps.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 = canonicalizeProviderName(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    // Positional systemPrompt is the opening user turn (Codex, Cursor), not a\n    // system-level instruction. Gate it so a fresh session never gets an\n    // uninvited first message unless opted in.\n    const includeSystemPrompt =\n      capabilitiesFor(provider).systemPrompt !== \"positional\" || this.codexSystemPromptEnabled;\n\n    try {\n      const session = await this.ptyManager.startFresh({\n        provider,\n        projectPath: resolvedPath,\n        projectName: body.projectName,\n        ...(includeSystemPrompt && { systemPrompt: systemPromptParts.join(\"\\n\") }),\n        claudeFlags: this.claudeFlags,\n        claudeExtraArgs: this.claudeExtraArgs,\n        ...this.deps.spawnFlagOverrides(),\n      });\n\n      this.sessionStore.addManaged(session);\n      this.registryBoot.recordSessionSpawn(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 { outcome, session: settled } = await this.deps.waitForStartupOutcome(\n        session.id,\n        START_READY_TIMEOUT_MS,\n      );\n      const current = this.sessionStore.get(session.id, this.deps.ptyAttachedIds());\n\n      if (outcome === \"ready\" && current) {\n        json(res, 200, { session: current });\n      } else if (outcome === \"failed\" && current) {\n        // The diagnosed reason rides on the settled session, not on the store\n        // copy — same as the Codex resume path below. Reading only the store\n        // here is what turned \"the Claude binary is not accessible\" into a\n        // bare \"exited before becoming ready\".\n        json(res, 502, {\n          id: session.id,\n          status: \"idle\",\n          error: settled?.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.sessionWatchers.watchForCodexRollout(session.id, resolvedPath);\n      } else if (provider === CLAUDE_CODE_PROVIDER) {\n        // Wire up JSONL watching once Claude creates the conversation file.\n        this.sessionWatchers.watchForJsonl(session.id, resolvedPath);\n      } else if (provider === CURSOR_PROVIDER) {\n        // Cursor mints its own run id under agent-transcripts/; bind it so\n        // GET /api/conversations/:placeholderId resolves like Codex.\n        this.sessionWatchers.watchForCursorTranscript(session.id, resolvedPath);\n      }\n\n      this.deps.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      const code = (err as Error & { code?: unknown }).code;\n      this.log.error(`[start] failed to start session: ${message}`, {\n        event: \"session.start_failed\",\n        error: message,\n      });\n      json(\n        res,\n        statusCode,\n        typeof code === \"string\" ? { error: message, code } : { error: message },\n      );\n    }\n  }\n\n  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  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","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    if (size <= 0) return ''\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 { 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 listFiles(absolutePath: string): Promise<Array<{ name: string }>> {\n  const entries = await readdir(absolutePath, { withFileTypes: true });\n  return entries\n    .filter((e) => e.isFile())\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 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 { randomUUID } from \"node:crypto\";\nimport {\n  PROMPT_SCHEMA_VERSION,\n  type Prompt,\n  type PromptAnswer,\n  type PromptOption,\n  type PromptQuestion,\n  PromptSchema,\n} from \"../../schemas/prompt.schema\";\n\nexport const PROMPT_TERMINAL_RETENTION_MS = 10 * 60 * 1000;\nexport const PROMPT_MAX_RECORDS_PER_SESSION = 200;\nconst MAX_TIMEOUT_MS = 2_147_483_647;\n\ntype PromptTerminalState = Extract<\n  Prompt[\"state\"],\n  \"resolved\" | \"cancelled\" | \"expired\" | \"unavailable\"\n>;\n\nexport type PromptOptionDraft = Omit<PromptOption, \"optionId\">;\nexport type PromptQuestionDraft = Omit<PromptQuestion, \"questionId\" | \"options\"> & {\n  options: PromptOptionDraft[];\n};\nexport type PromptDraft = Omit<\n  Prompt,\n  \"schemaVersion\" | \"promptId\" | \"revision\" | \"state\" | \"terminalReason\" | \"questions\"\n> & {\n  questions: PromptQuestionDraft[];\n};\n\nexport type PromptAnswerErrorCode =\n  | \"prompt_not_found\"\n  | \"prompt_revision_mismatch\"\n  | \"already_resolved\"\n  | \"prompt_expired\"\n  | \"prompt_cancelled\"\n  | \"prompt_unavailable\"\n  | \"unknown_question\"\n  | \"unknown_option\"\n  | \"incomplete_answer\"\n  | \"unsupported_prompt_shape\"\n  | \"provider_error\";\n\nexport type PromptAnswerOutcome =\n  | { ok: true; prompt: Prompt }\n  | {\n      ok: false;\n      code: PromptAnswerErrorCode;\n      currentRevision?: number;\n    };\n\nexport type PromptAdapterResult =\n  | { ok: true }\n  | {\n      ok: false;\n      code: PromptAnswerErrorCode;\n      terminal?: { state: PromptTerminalState; reason: string };\n    };\n\nexport type PromptAnswerAdapter = (context: {\n  prompt: Prompt;\n  answer: PromptAnswer;\n}) => PromptAdapterResult | Promise<PromptAdapterResult>;\n\nexport interface PromptEvent {\n  type: \"prompt_event\";\n  sessionId: string;\n  sequence: number;\n  prompt: Prompt;\n}\n\n/**\n * Everything the registry still RETAINS for a session, not just what is\n * actionable: terminal records stay for PROMPT_TERMINAL_RETENTION_MS (capped at\n * PROMPT_MAX_RECORDS_PER_SESSION) so a reconnecting client can see how a prompt\n * it was showing ended. A subscriber therefore filters on `state` — presence in\n * a snapshot is not an open prompt.\n *\n * Retention also bounds idempotency: once a record is pruned, an answer retry\n * that would have replayed its recorded outcome gets `prompt_not_found`\n * instead (HTTP 404 on the answer route).\n */\nexport interface PromptSnapshot {\n  type: \"prompt_snapshot\";\n  schemaVersion: typeof PROMPT_SCHEMA_VERSION;\n  sessionId: string;\n  sequence: number;\n  prompts: Prompt[];\n}\n\ninterface RecordedOutcome {\n  at: number;\n  outcome: PromptAnswerOutcome;\n}\n\ninterface PromptEntry {\n  prompt: Prompt;\n  adapter?: PromptAnswerAdapter;\n  terminalAt?: number;\n  expiryTimer?: ReturnType<typeof setTimeout>;\n  queue: Promise<void>;\n  inFlight: Map<string, Promise<PromptAnswerOutcome>>;\n  outcomes: Map<string, RecordedOutcome>;\n  // Set only across a provider write, so a teardown observed during it can be\n  // attributed to that write instead of read as the provider moving on.\n  answering?: boolean;\n  deferredClose?: string;\n}\n\nexport interface PromptRegistryOptions {\n  createId?: () => string;\n  emit?: (event: PromptEvent) => void;\n  onExpire?: (prompt: Prompt) => void;\n  now?: () => number;\n  terminalRetentionMs?: number;\n  maxRecordsPerSession?: number;\n}\n\nfunction copyPrompt(prompt: Prompt): Prompt {\n  return {\n    ...prompt,\n    questions: prompt.questions.map((question) => ({\n      ...question,\n      options: question.options.map((option) => ({ ...option })),\n    })),\n    provenance: { ...prompt.provenance },\n  };\n}\n\nfunction terminalError(state: PromptTerminalState): PromptAnswerErrorCode {\n  switch (state) {\n    case \"resolved\":\n      return \"already_resolved\";\n    case \"expired\":\n      return \"prompt_expired\";\n    case \"cancelled\":\n      return \"prompt_cancelled\";\n    case \"unavailable\":\n      return \"prompt_unavailable\";\n  }\n}\n\nexport class PromptRegistry {\n  private readonly bySession = new Map<string, Map<string, PromptEntry>>();\n  private readonly byId = new Map<string, PromptEntry>();\n  private readonly sequences = new Map<string, number>();\n  private readonly createId: () => string;\n  private readonly emit?: (event: PromptEvent) => void;\n  private readonly onExpire?: (prompt: Prompt) => void;\n  private readonly now: () => number;\n  private readonly terminalRetentionMs: number;\n  private readonly maxRecordsPerSession: number;\n\n  constructor(options: PromptRegistryOptions = {}) {\n    this.createId = options.createId ?? randomUUID;\n    this.emit = options.emit;\n    this.onExpire = options.onExpire;\n    this.now = options.now ?? Date.now;\n    this.terminalRetentionMs = options.terminalRetentionMs ?? PROMPT_TERMINAL_RETENTION_MS;\n    this.maxRecordsPerSession = options.maxRecordsPerSession ?? PROMPT_MAX_RECORDS_PER_SESSION;\n  }\n\n  open(draft: PromptDraft, adapter?: PromptAnswerAdapter, promptId = this.createId()): Prompt {\n    this.sweepExpired(draft.sessionId);\n    const held = this.byId.get(promptId);\n    if (held && (held.prompt.state === \"open\" || held.prompt.state === \"updated\")) {\n      throw new Error(`Prompt id already exists: ${promptId}`);\n    }\n    // A RETAINED TERMINAL record under this id is a producer replay, not a\n    // duplicate. The pty-host keeps one occurrence id for as long as its\n    // detector sees the same content, while a streamer-side clear the host\n    // never saw (gate_closed on the legacy answer route, a cancelled question)\n    // leaves that id terminal here for PROMPT_TERMINAL_RETENTION_MS. The next\n    // repaint of the same gate then arrives with an id we still hold — inside\n    // a detector callback with no catch anywhere above it. Mint a fresh id and\n    // open normally; the retained record stays readable under the old one.\n    const id = held ? this.createId() : promptId;\n    const prompt = PromptSchema.parse({\n      ...draft,\n      schemaVersion: PROMPT_SCHEMA_VERSION,\n      promptId: id,\n      revision: 1,\n      state: \"open\",\n      questions: draft.questions.map((question) => ({\n        ...question,\n        questionId: this.createId(),\n        options: question.options.map((option) => ({ ...option, optionId: this.createId() })),\n      })),\n      provenance: { ...draft.provenance },\n    });\n    const entry: PromptEntry = {\n      prompt,\n      adapter,\n      queue: Promise.resolve(),\n      inFlight: new Map(),\n      outcomes: new Map(),\n    };\n    const session = this.bySession.get(prompt.sessionId) ?? new Map<string, PromptEntry>();\n    session.set(prompt.promptId, entry);\n    this.bySession.set(prompt.sessionId, session);\n    this.byId.set(prompt.promptId, entry);\n    this.publish(entry);\n    this.scheduleExpiration(entry);\n    this.enforceCap(prompt.sessionId);\n    return copyPrompt(prompt);\n  }\n\n  update(promptId: string, draft: PromptDraft, adapter?: PromptAnswerAdapter): Prompt {\n    const entry = this.requireEntry(promptId);\n    this.expireIfDue(entry, this.now());\n    if (entry.prompt.sessionId !== draft.sessionId) throw new Error(\"Prompt session cannot change\");\n    if (entry.prompt.state !== \"open\" && entry.prompt.state !== \"updated\") {\n      throw new Error(`Cannot update terminal prompt ${promptId}`);\n    }\n    if (entry.prompt.questions.length !== draft.questions.length) {\n      throw new Error(\"Prompt question cardinality cannot change during an update\");\n    }\n    const questions = draft.questions.map((question, questionIndex): PromptQuestion => {\n      const prior = entry.prompt.questions[questionIndex];\n      if (prior.options.length !== question.options.length) {\n        throw new Error(\"Prompt option cardinality cannot change during an update\");\n      }\n      return {\n        ...question,\n        questionId: prior.questionId,\n        options: question.options.map((option, optionIndex) => ({\n          ...option,\n          optionId: prior.options[optionIndex].optionId,\n        })),\n      };\n    });\n    const prompt = PromptSchema.parse({\n      ...draft,\n      schemaVersion: PROMPT_SCHEMA_VERSION,\n      promptId,\n      revision: entry.prompt.revision + 1,\n      state: \"updated\",\n      questions,\n      provenance: { ...draft.provenance },\n    });\n    entry.prompt = prompt;\n    if (adapter) entry.adapter = adapter;\n    this.publish(entry);\n    this.scheduleExpiration(entry);\n    return copyPrompt(entry.prompt);\n  }\n\n  transition(promptId: string, state: PromptTerminalState, reason: string): Prompt {\n    const entry = this.requireEntry(promptId);\n    if (entry.prompt.state !== \"open\" && entry.prompt.state !== \"updated\") {\n      throw new Error(`Cannot transition terminal prompt ${promptId}`);\n    }\n    entry.prompt = {\n      ...entry.prompt,\n      revision: entry.prompt.revision + 1,\n      state,\n      terminalReason: reason,\n    };\n    entry.terminalAt = this.now();\n    this.clearExpiration(entry);\n    this.publish(entry);\n    return copyPrompt(entry.prompt);\n  }\n\n  /**\n   * The provider's prompt left the screen.\n   *\n   * Distinct from `transition` because the detector cannot tell a teardown it\n   * observed on its own from one OUR OWN write caused. On a screen-scraped gate\n   * the answer keys remove the box, and pty-manager's sendKeys fires the close\n   * synchronously from inside the write — so the close always arrives before\n   * the answer that caused it has settled, and reporting it as a cancel failed\n   * every answer whose bytes had already landed (#720).\n   *\n   * While an answer is writing, the close is deferred and the answer decides:\n   * `resolved` if the write landed, the deferred close if it did not. Success\n   * is the adapter's own result, never the prompt's absence from the screen —\n   * a prompt that does not vanish when answered (a multi-select form) simply\n   * never reaches here, and is settled by the same adapter result (#721).\n   *\n   * ONLY this path defers. `replaced` from a different gate taking the screen,\n   * `unavailable` from invalidateSession and `expired` all still go through\n   * `transition` and still win, because none of them was caused by our write.\n   */\n  providerClosed(promptId: string, reason: string): Prompt | null {\n    const entry = this.byId.get(promptId);\n    if (!entry) return null;\n    if (entry.prompt.state !== \"open\" && entry.prompt.state !== \"updated\") return null;\n    if (entry.answering) {\n      entry.deferredClose = reason;\n      return null;\n    }\n    return this.transition(promptId, \"cancelled\", reason);\n  }\n\n  /**\n   * Run a provider write that may close the prompt as a side effect, so the\n   * close it causes is deferred rather than applied. For callers outside this\n   * class that write without going through `answer()` — the legacy permission\n   * route. `performAnswer` manages the same two fields directly, because it\n   * needs the deferred value in its own control flow.\n   *\n   * The write's own failure needs no unwinding here: pty-manager fires the\n   * close as the last statement of a successful sendKeys, so a throw means no\n   * close was ever deferred and the record is still open for the caller.\n   */\n  whileAnswering<T>(promptId: string, write: () => T): T {\n    const entry = this.byId.get(promptId);\n    if (!entry) return write();\n    entry.answering = true;\n    try {\n      return write();\n    } finally {\n      entry.answering = false;\n      entry.deferredClose = undefined;\n    }\n  }\n\n  invalidateSession(sessionId: string, reason = \"session_ended\"): Prompt[] {\n    const transitioned: Prompt[] = [];\n    for (const entry of this.bySession.get(sessionId)?.values() ?? []) {\n      if (entry.prompt.state === \"open\" || entry.prompt.state === \"updated\") {\n        transitioned.push(this.transition(entry.prompt.promptId, \"unavailable\", reason));\n      }\n    }\n    return transitioned;\n  }\n\n  get(promptId: string): Prompt | null {\n    const entry = this.byId.get(promptId);\n    if (!entry) return null;\n    this.sweepExpired(entry.prompt.sessionId);\n    return this.byId.has(promptId) ? copyPrompt(entry.prompt) : null;\n  }\n\n  snapshot(sessionId: string): PromptSnapshot {\n    this.sweepExpired(sessionId);\n    return {\n      type: \"prompt_snapshot\",\n      schemaVersion: PROMPT_SCHEMA_VERSION,\n      sessionId,\n      sequence: this.sequences.get(sessionId) ?? 0,\n      prompts: [...(this.bySession.get(sessionId)?.values() ?? [])].map((entry) =>\n        copyPrompt(entry.prompt),\n      ),\n    };\n  }\n\n  dispose(): void {\n    for (const entry of this.byId.values()) this.clearExpiration(entry);\n  }\n\n  answer(sessionId: string, answer: PromptAnswer): Promise<PromptAnswerOutcome> {\n    this.sweepExpired(sessionId);\n    const entry = this.byId.get(answer.promptId);\n    if (!entry || entry.prompt.sessionId !== sessionId) {\n      return Promise.resolve({ ok: false, code: \"prompt_not_found\" });\n    }\n    this.pruneOutcomes(entry);\n    const recorded = entry.outcomes.get(answer.idempotencyKey);\n    if (recorded) return Promise.resolve(recorded.outcome);\n    const pending = entry.inFlight.get(answer.idempotencyKey);\n    if (pending) return pending;\n\n    const task = entry.queue.then(() => this.performAnswer(entry, answer));\n    entry.queue = task.then(\n      () => undefined,\n      () => undefined,\n    );\n    entry.inFlight.set(answer.idempotencyKey, task);\n    void task.then((outcome) => {\n      entry.inFlight.delete(answer.idempotencyKey);\n      entry.outcomes.set(answer.idempotencyKey, { at: this.now(), outcome });\n    });\n    return task;\n  }\n\n  private async performAnswer(\n    entry: PromptEntry,\n    answer: PromptAnswer,\n  ): Promise<PromptAnswerOutcome> {\n    const prompt = entry.prompt;\n    if (prompt.state !== \"open\" && prompt.state !== \"updated\") {\n      return { ok: false, code: terminalError(prompt.state) };\n    }\n    if (this.expireIfDue(entry, this.now())) {\n      return { ok: false, code: \"prompt_expired\" };\n    }\n    if (prompt.revision !== answer.revision) {\n      return {\n        ok: false,\n        code: \"prompt_revision_mismatch\",\n        currentRevision: prompt.revision,\n      };\n    }\n    const responseError = this.validateResponses(prompt, answer);\n    if (responseError) return { ok: false, code: responseError };\n    if (!entry.adapter) return { ok: false, code: \"prompt_unavailable\" };\n\n    // Marked HERE: after every pre-adapter check above has passed (state,\n    // expiry, revision, responses, adapter presence) and immediately before the\n    // write. Never at the top of this method — the marker's lifetime is exactly\n    // the window in which our own write can close the prompt, so it can never\n    // mask a teardown that has nothing to do with us. Validation stays entirely\n    // on the near side of the write: a refused answer writes zero bytes and\n    // leaves the record open at its revision, and nothing below moves a check\n    // across that boundary.\n    entry.answering = true;\n    let adapterResult: PromptAdapterResult;\n    let threw = false;\n    try {\n      adapterResult = await entry.adapter({ prompt: copyPrompt(prompt), answer });\n    } catch {\n      threw = true;\n      adapterResult = { ok: false, code: \"provider_error\" };\n    } finally {\n      entry.answering = false;\n    }\n    // A close the detector observed while we were writing is OUR close, held\n    // back by providerClosed rather than applied. It settles on every exit that\n    // does not resolve, so a refused answer can never leave the record open\n    // with the provider's box already gone.\n    const deferred = entry.deferredClose;\n    entry.deferredClose = undefined;\n    const settle = (outcome: PromptAnswerOutcome): PromptAnswerOutcome => {\n      if (deferred) this.providerClosed(prompt.promptId, deferred);\n      return outcome;\n    };\n\n    if (threw) return settle({ ok: false, code: \"provider_error\" });\n    if (entry.prompt.state !== \"open\" && entry.prompt.state !== \"updated\") {\n      return settle({ ok: false, code: terminalError(entry.prompt.state) });\n    }\n    if (entry.prompt.revision !== answer.revision) {\n      return settle({\n        ok: false,\n        code: \"prompt_revision_mismatch\",\n        currentRevision: entry.prompt.revision,\n      });\n    }\n    if (!adapterResult.ok) {\n      if (adapterResult.terminal) {\n        this.transition(\n          prompt.promptId,\n          adapterResult.terminal.state,\n          adapterResult.terminal.reason,\n        );\n      }\n      return settle({ ok: false, code: adapterResult.code });\n    }\n    return { ok: true, prompt: this.transition(prompt.promptId, \"resolved\", \"answered\") };\n  }\n\n  private validateResponses(prompt: Prompt, answer: PromptAnswer): PromptAnswerErrorCode | null {\n    const questions = new Map(prompt.questions.map((question) => [question.questionId, question]));\n    for (const response of answer.responses) {\n      if (!questions.has(response.questionId)) return \"unknown_question\";\n    }\n    if (answer.responses.length !== prompt.questions.length) return \"incomplete_answer\";\n\n    const responses = new Map(answer.responses.map((response) => [response.questionId, response]));\n    for (const question of prompt.questions) {\n      const response = responses.get(question.questionId);\n      if (!response) return \"incomplete_answer\";\n      if (question.inputMode === \"text\") {\n        if (typeof response.text !== \"string\") return \"incomplete_answer\";\n        continue;\n      }\n      const optionIds = response.optionIds;\n      if (!optionIds) return \"incomplete_answer\";\n      if (question.inputMode === \"single\" && optionIds.length !== 1) {\n        return \"unsupported_prompt_shape\";\n      }\n      const known = new Set(question.options.map((option) => option.optionId));\n      if (optionIds.some((optionId) => !known.has(optionId))) return \"unknown_option\";\n    }\n    return null;\n  }\n\n  private publish(entry: PromptEntry): void {\n    const sessionId = entry.prompt.sessionId;\n    const sequence = (this.sequences.get(sessionId) ?? 0) + 1;\n    this.sequences.set(sessionId, sequence);\n    this.emit?.({\n      type: \"prompt_event\",\n      sessionId,\n      sequence,\n      prompt: copyPrompt(entry.prompt),\n    });\n  }\n\n  private scheduleExpiration(entry: PromptEntry): void {\n    this.clearExpiration(entry);\n    const expiresAt = entry.prompt.expiresAt;\n    if (expiresAt === null) return;\n    const delay = Math.min(MAX_TIMEOUT_MS, Math.max(0, Date.parse(expiresAt) - this.now()));\n    entry.expiryTimer = setTimeout(() => {\n      entry.expiryTimer = undefined;\n      if (!this.expireIfDue(entry, this.now())) this.scheduleExpiration(entry);\n    }, delay);\n    entry.expiryTimer.unref?.();\n  }\n\n  private clearExpiration(entry: PromptEntry): void {\n    if (entry.expiryTimer) clearTimeout(entry.expiryTimer);\n    entry.expiryTimer = undefined;\n  }\n\n  private expireIfDue(entry: PromptEntry, now: number): boolean {\n    if (entry.prompt.state !== \"open\" && entry.prompt.state !== \"updated\") return false;\n    if (entry.prompt.expiresAt === null || now < Date.parse(entry.prompt.expiresAt)) return false;\n    const expired = this.transition(entry.prompt.promptId, \"expired\", \"deadline_elapsed\");\n    this.onExpire?.(expired);\n    return true;\n  }\n\n  private requireEntry(promptId: string): PromptEntry {\n    const entry = this.byId.get(promptId);\n    if (!entry) throw new Error(`Unknown prompt: ${promptId}`);\n    return entry;\n  }\n\n  sweepExpired(sessionId: string): void {\n    const now = this.now();\n    const session = this.bySession.get(sessionId);\n    if (!session) return;\n    for (const [promptId, entry] of session) {\n      this.expireIfDue(entry, now);\n      if (entry.terminalAt !== undefined && now - entry.terminalAt > this.terminalRetentionMs) {\n        this.clearExpiration(entry);\n        session.delete(promptId);\n        this.byId.delete(promptId);\n      }\n    }\n    if (session.size === 0) this.bySession.delete(sessionId);\n  }\n\n  private enforceCap(sessionId: string): void {\n    const session = this.bySession.get(sessionId);\n    if (!session || session.size <= this.maxRecordsPerSession) return;\n    const terminal = [...session.entries()]\n      .filter(([, entry]) => entry.terminalAt !== undefined)\n      .sort((a, b) => (a[1].terminalAt ?? 0) - (b[1].terminalAt ?? 0));\n    while (session.size > this.maxRecordsPerSession && terminal.length > 0) {\n      const [promptId, entry] = terminal.shift() as [string, PromptEntry];\n      this.clearExpiration(entry);\n      session.delete(promptId);\n      this.byId.delete(promptId);\n    }\n  }\n\n  private pruneOutcomes(entry: PromptEntry): void {\n    const now = this.now();\n    for (const [key, recorded] of entry.outcomes) {\n      if (now - recorded.at > this.terminalRetentionMs) entry.outcomes.delete(key);\n    }\n  }\n}\n","import type { AskQuestion } from \"../../types\";\nimport type { PermissionGate } from \"../questions/detectPermissionGate\";\nimport type { PromptDraft } from \"./promptRegistry\";\n\nexport function permissionPromptDraft(sessionId: string, gate: PermissionGate | null): PromptDraft {\n  if (!gate) throw new Error(\"Cannot normalize an absent permission gate\");\n  const message = gate.prompt?.trim() || \"Approval required\";\n  return {\n    sessionId,\n    intent: \"approval\",\n    title: \"Approval\",\n    message,\n    ...(gate.detail?.trim() ? { detail: gate.detail } : {}),\n    questions: [\n      {\n        text: message,\n        header: \"Approval\",\n        inputMode: \"single\",\n        options: gate.options.map((option) => ({ label: option.label })),\n        allowOther: false,\n        secret: \"unknown\",\n      },\n    ],\n    answerRequirement: \"unknown\",\n    expiresAt: null,\n    provenance: { source: \"screen\", confidence: \"inferred\" },\n  };\n}\n\nexport function questionPromptDraft(\n  sessionId: string,\n  questions: AskQuestion[],\n  source: \"screen\" | \"transcript\",\n): PromptDraft {\n  const first = questions[0];\n  if (!first) throw new Error(\"Cannot normalize an empty question list\");\n  return {\n    sessionId,\n    intent: \"question\",\n    ...(first.header.trim() ? { title: first.header } : {}),\n    message: first.question,\n    questions: questions.map((question) => ({\n      text: question.question,\n      ...(question.header.trim() ? { header: question.header } : {}),\n      inputMode: question.multiSelect ? \"multi\" : \"single\",\n      options: question.options.map((option) => ({\n        label: option.label,\n        ...(option.description ? { description: option.description } : {}),\n        ...(option.preview ? { preview: option.preview } : {}),\n      })),\n      allowOther: false,\n      secret: \"unknown\",\n    })),\n    answerRequirement: \"unknown\",\n    expiresAt: null,\n    provenance: {\n      source,\n      confidence: source === \"transcript\" ? \"authoritative\" : \"inferred\",\n    },\n  };\n}\n","// Status-line parsing. Claude Code paints a footer at the bottom of the screen\n// carrying the facts mobile wants to show natively instead of rendering raw\n// terminal text:\n//\n//   \"  Opus 4.8 (1M context) │ ~/dev/ai-tools/tb-streamer  main ✎ │  26.5.0 23:41 │ ⚓4\"\n//   \"  ⏵⏵ accept edits on (shift+tab to cycle) · ← for agents\"\n//   \"                                          ● high · /effort\"\n//\n// Only stable facts are extracted (model, effort, permission mode). The elapsed\n// \"(56s · ↑ 3.4k tokens)\" counter is deliberately NOT parsed: Claude repaints it\n// only when it has other output to draw, so any value we forwarded would be\n// stale the moment it arrived. The client animates elapsed time locally instead.\n//\n// Pure — no I/O. Operates on rendered screen lines (getOutputLines), because the\n// footer is painted with absolute-cursor moves and does not exist as a\n// contiguous run of bytes in the raw PTY stream.\n\nexport interface StatusLineInfo {\n  /** e.g. \"Opus 4.8 (1M context)\" or \"Sonnet 5\". */\n  model?: string;\n  /** e.g. \"high\" — the reasoning-effort tier. */\n  effort?: string;\n  /** e.g. \"accept edits on\" — the active permission mode. */\n  permissionMode?: string;\n}\n\n// Model segment: the footer row is \"│\"-delimited and the model is its first\n// cell. Anchored on a known family name so an arbitrary first cell (a path, a\n// git branch) can't be mistaken for a model.\nconst MODEL_RE = /(Opus|Sonnet|Haiku|Fable)\\s+[\\d.]+(?:\\s*\\([^)]*\\))?/;\n\n// Effort row: \"● high · /effort\". The bullet and the \"/effort\" suffix together\n// make this unambiguous; the tier itself is captured open-endedly so a new tier\n// name doesn't silently parse as undefined.\nconst EFFORT_RE = /●\\s*([A-Za-z]+)\\s*·\\s*\\/effort/;\n\n// Permission-mode row: \"⏵⏵ accept edits on (shift+tab to cycle) · ← for agents\".\n// Stop at the parenthetical so the hint text isn't captured as part of the mode.\nconst PERMISSION_MODE_RE = /⏵⏵\\s*([^(·\\n]+?)\\s*(?:\\(|·|$)/;\n\n/**\n * Extract model / effort / permission mode from rendered screen lines.\n * Returns an empty object when the footer isn't on screen — callers treat every\n * field as optional, so a missed parse degrades to \"not reported\" rather than\n * throwing or inventing a value.\n *\n * Scans from the bottom: the footer is the last thing painted, and older\n * scrollback can contain text that looks like a footer row.\n */\nexport function parseStatusLine(lines: string[]): StatusLineInfo {\n  const info: StatusLineInfo = {};\n\n  for (let i = lines.length - 1; i >= 0; i--) {\n    const line = lines[i];\n    if (!line) continue;\n\n    if (info.effort === undefined) {\n      const m = EFFORT_RE.exec(line);\n      if (m) info.effort = m[1];\n    }\n    if (info.permissionMode === undefined) {\n      const m = PERMISSION_MODE_RE.exec(line);\n      if (m) info.permissionMode = m[1].trim();\n    }\n    if (info.model === undefined) {\n      const m = MODEL_RE.exec(line);\n      if (m) info.model = m[0].replace(/\\s+/g, \" \").trim();\n    }\n\n    if (info.model && info.effort && info.permissionMode) break;\n  }\n\n  return info;\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// A shape this keystroke path cannot answer safely: a multi-question form, a\n// multi-select question, or more than one label for a single-select question.\nexport class UnsupportedPromptShapeError extends Error {\n  constructor(public readonly detail: string) {\n    super(`Unsupported prompt shape: ${detail}`);\n    this.name = \"UnsupportedPromptShapeError\";\n  }\n}\n\nexport class IncompleteAnswerError extends Error {\n  constructor(public readonly question: string) {\n    super(`Missing answer for question \"${question}\"`);\n    this.name = \"IncompleteAnswerError\";\n  }\n}\n\n// Supported shape: exactly one single-select question answered with one label.\n// Cursor starts at index 0; N downs + Enter selects index N.\n//\n// Deliberately nothing more. Multi-question forms used to be answered by\n// concatenating one block per question, blind to whether the TUI advanced or\n// where its cursor landed; a multi-select question was \"answered\" with its\n// first label as if single-select. Both wrote bytes for an answer the user did\n// not give. They now throw BEFORE any keystroke is built, and the route refuses\n// without writing. The TUI shows a multi-question form one picker at a time and\n// the screen detector cards each picker as a single question, so that path is\n// unaffected.\nexport function answersToKeystrokes(\n  questions: AskQuestion[],\n  answers: Record<string, string | string[]>,\n): string {\n  if (questions.length !== 1) {\n    throw new UnsupportedPromptShapeError(`${questions.length} questions`);\n  }\n  const q = questions[0];\n  if (q.multiSelect) throw new UnsupportedPromptShapeError(\"multiSelect\");\n  const raw = answers[q.question];\n  if (raw === undefined || (Array.isArray(raw) && raw.length === 0)) {\n    throw new IncompleteAnswerError(q.question);\n  }\n  if (Array.isArray(raw) && raw.length > 1) {\n    throw new UnsupportedPromptShapeError(`${raw.length} labels for a single-select question`);\n  }\n  const label = Array.isArray(raw) ? raw[0] : raw;\n  const target = q.options.findIndex((o) => o.label === label);\n  if (target < 0) throw new UnknownOptionError(q.question, label);\n  return DOWN.repeat(target) + ENTER;\n}\n","import type { AskQuestion } from \"../../types\";\nimport {\n  answersToKeystrokes,\n  IncompleteAnswerError,\n  UnknownOptionError,\n  UnsupportedPromptShapeError,\n} from \"./answersToKeystrokes\";\n\nexport type AnswerResolution =\n  | { ok: true; keys: string }\n  | {\n      ok: false;\n      reason:\n        | \"no_pending_question\"\n        | \"tool_use_mismatch\"\n        | \"unknown_option\"\n        | \"unsupported_prompt_shape\"\n        | \"incomplete_answer\";\n    };\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    if (e instanceof UnsupportedPromptShapeError) {\n      return { ok: false, reason: \"unsupported_prompt_shape\" };\n    }\n    if (e instanceof IncompleteAnswerError) return { ok: false, reason: \"incomplete_answer\" };\n    throw e;\n  }\n}\n","import { execFile } from \"child_process\";\n\n/**\n * Who is holding a Codex rollout JSONL open right now.\n *\n * Codex enforces a single-writer rule per rollout and only reports it *after*\n * `codex resume` has started (\"already has an active writer (code -32600)\"),\n * by which point the streamer has already spawned a PTY and answered 201. The\n * heuristic pre-flight in conversationBusy.ts cannot see that owner: a Codex\n * process need not carry the rollout UUID in its argv, and a quiet-but-owned\n * rollout has an mtime well outside the busy window (the 2026-08-09 incident:\n * pid 9935 held the file open with an mtime older than 120 s).\n *\n * An open file handle on the exact rollout is direct evidence of the condition\n * Codex itself rejects, so it is the one pre-spawn signal worth paying for.\n *\n * It is an OPTIMISATION, never a proof of the negative: `lsof` is POSIX-only,\n * may be absent, may be denied, and cannot see another user's process. A null\n * result means \"no evidence\", and the caller must still rely on the\n * authoritative post-spawn handshake.\n */\n\n/** Hard cap on the probe. Resume is latency-sensitive — a slow lsof is a miss. */\nexport const ROLLOUT_OWNER_TIMEOUT_MS = 800;\n\n/**\n * How the owning process is best described to a client.\n *\n * Only a process whose command is exactly `codex` is classified, and even then\n * only as \"terminal\". A VS Code / desktop `codex app-server` can host several\n * unrelated threads, so mis-labelling one as a standalone TUI is what would\n * make a destructive recovery action look safe. Everything else stays\n * \"unknown\", and no takeover is ever offered from this signal.\n */\nexport type CodexOwnerSource = \"terminal\" | \"unknown\";\n\nexport interface CodexRolloutOwner {\n  pid: number;\n  command: string;\n  source: CodexOwnerSource;\n}\n\nexport interface FindRolloutOwnerOptions {\n  platform?: NodeJS.Platform;\n  timeoutMs?: number;\n  /** Our own pid — the streamer's handles on the file are not a collision. */\n  selfPid?: number;\n  /** Injection point for tests; defaults to a bounded `lsof` call. */\n  run?: (rolloutPath: string, timeoutMs: number) => Promise<string>;\n}\n\nfunction runLsof(rolloutPath: string, timeoutMs: number): Promise<string> {\n  return new Promise((resolve, reject) => {\n    // -F pc → machine-readable field output, one `p<pid>` / `c<command>` per\n    // line. -w silences warnings that would otherwise land on stdout.\n    // `--` guards a path that starts with a dash.\n    const child = execFile(\n      \"lsof\",\n      [\"-F\", \"pc\", \"-w\", \"--\", rolloutPath],\n      { windowsHide: true },\n      (err, stdout) => {\n        clearTimeout(timer);\n        // lsof exits 1 when nothing matches — that is a normal empty result,\n        // and stdout is what we parse either way.\n        if (err && !stdout) {\n          reject(err);\n          return;\n        }\n        resolve(stdout);\n      },\n    );\n\n    // The deadline is enforced here rather than via execFile's `timeout`\n    // option, which only SIGTERMs. lsof walks every process on the box, and on\n    // a busy machine it both outlives a polite signal and keeps its stdio\n    // handles alive — enough to hold the whole streamer's event loop past\n    // shutdown. Kill it outright, drop the pipes, and stop counting it.\n    const timer = setTimeout(() => {\n      try {\n        child.kill(\"SIGKILL\");\n      } catch {\n        // Already gone.\n      }\n      child.stdout?.destroy();\n      child.stderr?.destroy();\n      reject(new Error(\"lsof timed out\"));\n    }, timeoutMs);\n    timer.unref?.();\n    child.unref();\n  });\n}\n\n/** Parse `lsof -F pc` output into (pid, command) pairs, in file order. */\nexport function parseLsofFieldOutput(stdout: string): Array<{ pid: number; command: string }> {\n  const owners: Array<{ pid: number; command: string }> = [];\n  let pid: number | null = null;\n  for (const line of stdout.split(\"\\n\")) {\n    if (line.startsWith(\"p\")) {\n      const n = Number.parseInt(line.slice(1), 10);\n      pid = Number.isFinite(n) ? n : null;\n    } else if (line.startsWith(\"c\") && pid != null) {\n      owners.push({ pid, command: line.slice(1).trim() });\n      pid = null;\n    }\n  }\n  return owners;\n}\n\n/**\n * First foreign process holding `rolloutPath` open, or null when there is no\n * evidence of one (no match, unsupported platform, missing/denied/slow lsof).\n */\nexport async function findRolloutOwner(\n  rolloutPath: string,\n  options: FindRolloutOwnerOptions = {},\n): Promise<CodexRolloutOwner | null> {\n  const platform = options.platform ?? process.platform;\n  // No portable open-handle enumeration on Windows; `handle.exe` is a separate\n  // Sysinternals download and not something to shell out to on a resume path.\n  if (platform === \"win32\") return null;\n\n  const selfPid = options.selfPid ?? process.pid;\n  const run = options.run ?? runLsof;\n\n  let stdout: string;\n  try {\n    stdout = await run(rolloutPath, options.timeoutMs ?? ROLLOUT_OWNER_TIMEOUT_MS);\n  } catch {\n    // Timed out, not installed, or refused — no evidence either way.\n    return null;\n  }\n\n  for (const { pid, command } of parseLsofFieldOutput(stdout)) {\n    if (pid === selfPid) continue;\n    return { pid, command, source: command === \"codex\" ? \"terminal\" : \"unknown\" };\n  }\n  return null;\n}\n","import { statSync } from \"fs\";\nimport type { DiscoveredProcess } from \"../../types\";\nimport { canonicalizeProjectPath } from \"../../utils/canonicalizeProjectPath\";\n\n// A conversation's JSONL touched within this window is treated as actively\n// owned. This is the PRIMARY collision signal — it is the only one that catches\n// a session launched WITHOUT `--resume` (no process argv to match). Exported so\n// callers and tests share one value; override at runtime via the env var below.\nexport const RESUME_BUSY_WINDOW_MS = 120_000;\n\n// Resolve the busy window, allowing an env override (ms). Falls back to the\n// constant for missing/invalid values.\nexport function resolveResumeBusyWindowMs(env: NodeJS.ProcessEnv = process.env): number {\n  const raw = env.THREADBASE_RESUME_BUSY_WINDOW_MS;\n  if (raw === undefined) return RESUME_BUSY_WINDOW_MS;\n  const n = Number.parseInt(raw, 10);\n  return Number.isFinite(n) && n >= 0 ? n : RESUME_BUSY_WINDOW_MS;\n}\n\n// `file_handle` is produced by the Codex-only pre-flight in\n// codexRolloutOwner.ts, not by conversationBusy() — another process holds the\n// exact rollout JSONL open. Listed here so the wire vocabulary of `detectedBy`\n// stays in one place.\nexport type BusySignal = \"jsonl_mtime\" | \"process_argv\" | \"process_cwd\" | \"file_handle\";\n\n// Allowance for a JSONL write that lands just AFTER we observe our own PTY go\n// idle — Claude flushes the tail of a turn as it exits, so the file's mtime can\n// trail the exit by a moment. Without this the flush would read as foreign.\nconst SELF_ACTIVITY_SKEW_MS = 5_000;\n\nexport interface ConversationBusyInput {\n  conversationId: string;\n  projectPath: string | null;\n  jsonlPath: string | null;\n  discovered: DiscoveredProcess[];\n  now?: number;\n  windowMs?: number;\n  platform?: NodeJS.Platform;\n  /**\n   * When this streamer's own PTY for this conversation last went idle (ms epoch),\n   * or null/undefined if it never owned one in this process's lifetime.\n   *\n   * Without this the probe cannot tell our own echo from a stranger: the normal\n   * background → hold_session → foreground → resume flow releases the PTY (so the\n   * hasSession early-return no longer applies) while leaving a JSONL we wrote\n   * seconds ago — which would 409 the single most common resume in the product.\n   * File activity at or before this timestamp is attributed to us, not a collision.\n   */\n  selfPtyEndedAt?: number | null;\n}\n\nexport interface ConversationBusyResult {\n  busy: boolean;\n  detectedBy: BusySignal[];\n  // Milliseconds since the JSONL was last written (now - mtime), or null when\n  // there is no readable JSONL to measure.\n  lastActivityMs: number | null;\n  likelyOwner: \"external\" | \"unknown\";\n}\n\n// Pre-flight collision probe for a conversation about to be resumed. Pure with\n// respect to process discovery (the caller passes `discovered`); only touches\n// the filesystem to stat the JSONL. No `force` handling here — that is a caller\n// decision (a forced resume never runs this).\nexport function conversationBusy(input: ConversationBusyInput): ConversationBusyResult {\n  const now = input.now ?? Date.now();\n  const windowMs = input.windowMs ?? RESUME_BUSY_WINDOW_MS;\n  const platform = input.platform ?? process.platform;\n  const detectedBy: BusySignal[] = [];\n  let lastActivityMs: number | null = null;\n\n  // Signal 1 (PRIMARY): the JSONL was modified within the busy window. Only the\n  // UPPER bound matters — a just-written file can carry an mtime a few ms in the\n  // FUTURE (filesystem timestamp granularity / clock skew), which still means\n  // \"active right now\". Over-detecting busy is the safe direction (soft 409 with\n  // a force override); under-detecting would miss a real collision.\n  if (input.jsonlPath) {\n    try {\n      const mtimeMs = statSync(input.jsonlPath).mtimeMs;\n      const age = now - mtimeMs;\n      lastActivityMs = Math.max(0, age);\n      // Attribute the write: if the file has not been touched since our own PTY\n      // for this conversation released it, the recency is our own echo (a\n      // hold → resume round trip), not evidence of another owner.\n      const isSelfEcho =\n        input.selfPtyEndedAt != null && mtimeMs <= input.selfPtyEndedAt + SELF_ACTIVITY_SKEW_MS;\n      if (age <= windowMs && !isSelfEcho) detectedBy.push(\"jsonl_mtime\");\n    } catch {\n      // File missing / unreadable — no mtime signal.\n    }\n  }\n\n  // Signal 2: a discovered process is resuming this exact conversation id.\n  const argvMatch = input.discovered.some((p) => p.conversationId === input.conversationId);\n  if (argvMatch) detectedBy.push(\"process_argv\");\n\n  // Signal 3: a discovered process is running in this conversation's project\n  // directory. POSIX only — a process cwd is not available on win32.\n  let cwdMatch = false;\n  if (platform !== \"win32\" && input.projectPath) {\n    const target = canonicalizeProjectPath(input.projectPath);\n    cwdMatch = input.discovered.some(\n      (p) => !!p.projectPath && canonicalizeProjectPath(p.projectPath) === target,\n    );\n    if (cwdMatch) detectedBy.push(\"process_cwd\");\n  }\n\n  return {\n    busy: detectedBy.length > 0,\n    detectedBy,\n    lastActivityMs,\n    // A matched process is a concrete external owner; a lone mtime hit could be\n    // an editor, a crashed process, or a process we could not enumerate.\n    likelyOwner: argvMatch || cwdMatch ? \"external\" : \"unknown\",\n  };\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","/**\n * Idempotency for session input (C4).\n *\n * `POST /api/sessions/:id/input` had no duplicate protection. A retry — a flaky\n * network, a user double-tap, a client that resends on timeout — submitted the\n * same prompt to the agent twice, and nothing downstream could tell the second\n * submission from a deliberate repeat of the same words.\n *\n * The rate limiter does not help: 500 requests/minute is aimed at floods, and a\n * genuine retry is well inside it.\n *\n * Clients send `idempotencyKey` with a write. A repeat of a key we have already\n * accepted replays the original outcome instead of re-submitting.\n *\n * Scoped per session, because a key is only meaningful against the conversation\n * it was minted for — the same key against a different session is a different\n * write, and treating it as a duplicate would silently drop a real prompt.\n */\n\n/** How long a key is remembered. Long enough to cover a retry, not a session. */\nexport const IDEMPOTENCY_TTL_MS = 10 * 60 * 1000;\n\n/**\n * Cap on remembered keys per session. Bounds memory on a long conversation; the\n * oldest entries are evicted first, so a key older than the cap behaves as if it\n * had expired — the request is treated as new.\n */\nexport const IDEMPOTENCY_MAX_KEYS = 200;\n\nexport interface IdempotentResult {\n  status: number;\n  body: unknown;\n}\n\ninterface Entry {\n  key: string;\n  at: number;\n  result: IdempotentResult;\n}\n\n/**\n * Per-session record of recently accepted idempotency keys.\n *\n * Deliberately in-memory: this guards against retries seconds apart, and a\n * streamer restart already ends the PTY those retries would target (see the\n * durable-session-runtime ADR). Persisting it would imply a durability the\n * surrounding runtime does not have.\n */\nexport class IdempotencyStore {\n  private bySession = new Map<string, Entry[]>();\n\n  constructor(\n    private ttlMs: number = IDEMPOTENCY_TTL_MS,\n    private maxKeys: number = IDEMPOTENCY_MAX_KEYS,\n  ) {}\n\n  /**\n   * Previously recorded result for this key, or null if the key is new,\n   * expired, or evicted. A miss always means \"treat as a fresh request\" —\n   * failing open, because dropping a real prompt is far worse than allowing a\n   * rare duplicate.\n   */\n  get(sessionId: string, key: string, now: number = Date.now()): IdempotentResult | null {\n    const entries = this.bySession.get(sessionId);\n    if (!entries) return null;\n\n    const hit = entries.find((e) => e.key === key);\n    if (!hit) return null;\n    if (now - hit.at > this.ttlMs) {\n      this.bySession.set(\n        sessionId,\n        entries.filter((e) => e !== hit),\n      );\n      return null;\n    }\n    return hit.result;\n  }\n\n  /** Record the outcome of an accepted write so a retry can replay it. */\n  set(sessionId: string, key: string, result: IdempotentResult, now: number = Date.now()): void {\n    const entries = this.bySession.get(sessionId) ?? [];\n    const pruned = entries.filter((e) => e.key !== key && now - e.at <= this.ttlMs);\n    pruned.push({ key, at: now, result });\n    // Oldest-first eviction; entries are appended in arrival order.\n    this.bySession.set(sessionId, pruned.slice(-this.maxKeys));\n  }\n\n  /** Drop everything for a session whose PTY is gone. */\n  clear(sessionId: string): void {\n    this.bySession.delete(sessionId);\n  }\n\n  /** Test/diagnostic helper: how many keys are currently held for a session. */\n  size(sessionId: string): number {\n    return this.bySession.get(sessionId)?.length ?? 0;\n  }\n}\n\n/**\n * Extract and validate an idempotency key from a request body.\n *\n * Returns `undefined` when absent (the field is optional, so existing clients\n * keep working) and throws on a present-but-invalid value rather than ignoring\n * it — a client that sends a malformed key believes it has retry protection,\n * and silently proceeding without it would be worse than a clear rejection.\n */\nexport function readIdempotencyKey(body: Record<string, unknown>): string | undefined {\n  const raw = body.idempotencyKey;\n  if (raw === undefined || raw === null) return undefined;\n  if (typeof raw !== \"string\" || raw.length === 0 || raw.length > 200) {\n    throw new Error(\"idempotencyKey must be a non-empty string of at most 200 characters\");\n  }\n  return raw;\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    // Replace spaces and other shell-problematic characters with underscores.\n    // Mobile sends paths as @path references; Claude Code's parser splits on\n    // whitespace, so \"My Photo.jpg\" becomes \"@/path/My\" + \"Photo.jpg\" (broken).\n    .replace(/[\\s@\"'`$\\\\]/g, \"_\");\n  return cleaned;\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 type { ManagedSession, SessionStatus } from \"../../types\";\nimport { currentBootToken } from \"../../utils/bootToken\";\n\n/**\n * Durable registry of managed sessions (C1 Phase 2).\n * See docs/architecture/2026-07-24-durable-session-runtime.md.\n *\n * Distinct from SessionsRepository, which wraps the in-memory SessionStore for\n * the live request path. This one exists so session identity and provenance\n * outlive the streamer process: without it, a restart loses startedAt,\n * promptCount, sessionName, the Codex rollout binding, and failureReason, and\n * the boot reconciler has nothing to reconcile against.\n *\n * Deliberately not stored here: the PTY output ring buffer and xterm screen.\n * Their authoritative copy is the provider's JSONL, and writing 64KiB of ANSI\n * on every chunk to duplicate it would be a write storm for no gain.\n */\n\n/** How a persisted status was obtained — never inferred, always recorded. */\nexport type StatusSource = \"spawn\" | \"transition\" | \"exit\" | \"shutdown\" | \"probe\" | \"reconcile\";\n\n// Canonical definition lives in types.ts — it is a wire type on\n// SessionResponse, not a persistence detail. Re-exported here so reconciler and\n// repository consumers can keep importing it from one place.\nexport type { SessionLifecycle } from \"../../types\";\n\nexport interface ManagedSessionRow {\n  is_subagent?: number;\n  parent_conversation_id?: string | null;\n  session_id: string;\n  provider: string;\n  pid: number | null;\n  cmdline: string | null;\n  project_path: string;\n  project_name: string;\n  branch: string;\n  status: string;\n  status_source: string;\n  status_updated_at: number;\n  started_at: number;\n  completed_at: number | null;\n  last_activity_at: number | null;\n  prompt_count: number;\n  session_name: string | null;\n  project_id: string | null;\n  bound_conversation_id: string | null;\n  resumed_from_conversation_id: string | null;\n  failure_reason: string | null;\n  streamer_instance_id: string;\n  /**\n   * Which machine boot `pid` was recorded during (migration 002). Optional\n   * because rows written before it exists read back as null/absent, which the\n   * reconciler treats exactly like a mismatch — never like a match.\n   */\n  boot_token?: string | null;\n}\n\n/** Most rows the boot reconciler will probe in one pass. */\nexport const PROBE_SET_MAX = 200;\n\n/** Most rows one diagnostics report will carry. */\nexport const DIAGNOSTICS_MAX = 200;\n\n/** How long a finished session stays in the registry as history. */\nexport const TERMINAL_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;\n\nexport interface RecordSpawnInput {\n  session: ManagedSession;\n  pid: number | null;\n  cmdline: string | null;\n  streamerInstanceId: string;\n}\n\nexport class ManagedSessionsRepository {\n  private upsertStmt: Database.Statement;\n  private updateStatusStmt: Database.Statement;\n  private bindStmt: Database.Statement;\n  private getStmt: Database.Statement;\n  private listNonTerminalStmt: Database.Statement;\n  private listAllStmt: Database.Statement;\n  private pruneTerminalStmt: Database.Statement;\n  private listRecoverableStmt: Database.Statement;\n  private deleteStmt: Database.Statement;\n\n  constructor(db: Database.Database) {\n    this.upsertStmt = db.prepare(`\n      INSERT INTO managed_sessions (\n        session_id, provider, pid, cmdline, project_path, project_name, branch,\n        status, status_source, status_updated_at, started_at, completed_at,\n        last_activity_at, prompt_count, session_name, project_id,\n        bound_conversation_id, resumed_from_conversation_id, failure_reason,\n        streamer_instance_id, boot_token, is_subagent, parent_conversation_id\n      ) VALUES (\n        @session_id, @provider, @pid, @cmdline, @project_path, @project_name, @branch,\n        @status, @status_source, @status_updated_at, @started_at, @completed_at,\n        @last_activity_at, @prompt_count, @session_name, @project_id,\n        @bound_conversation_id, @resumed_from_conversation_id, @failure_reason,\n        @streamer_instance_id, @boot_token, @is_subagent, @parent_conversation_id\n      )\n      ON CONFLICT(session_id) DO UPDATE SET\n        pid = excluded.pid,\n        cmdline = excluded.cmdline,\n        project_path = excluded.project_path,\n        project_name = excluded.project_name,\n        branch = excluded.branch,\n        status = excluded.status,\n        status_source = excluded.status_source,\n        status_updated_at = excluded.status_updated_at,\n        completed_at = excluded.completed_at,\n        last_activity_at = excluded.last_activity_at,\n        prompt_count = excluded.prompt_count,\n        session_name = excluded.session_name,\n        project_id = excluded.project_id,\n        bound_conversation_id = excluded.bound_conversation_id,\n        resumed_from_conversation_id = excluded.resumed_from_conversation_id,\n        failure_reason = excluded.failure_reason,\n        streamer_instance_id = excluded.streamer_instance_id,\n        boot_token = excluded.boot_token,\n        is_subagent = excluded.is_subagent,\n        parent_conversation_id = excluded.parent_conversation_id\n    `);\n\n    // Narrow status-only write for the hot transition path, so a\n    // running↔waiting_input flip doesn't rewrite every column.\n    //\n    // session_name rides along under COALESCE, like failure_reason: it is\n    // derived from the session's FIRST user message, which is strictly after\n    // recordSpawn — the only other writer of the column — so without a second\n    // write path the registry could never hold a name at all, and every\n    // recovered session came back unnamed.\n    //\n    // last_activity_at and prompt_count follow the same rule: a caller that\n    // knows only the status (the reconciler) must not zero what it never read.\n    this.updateStatusStmt = db.prepare(`\n      UPDATE managed_sessions\n         SET status = @status,\n             status_source = @status_source,\n             status_updated_at = @status_updated_at,\n             completed_at = @completed_at,\n             last_activity_at = COALESCE(@last_activity_at, last_activity_at),\n             prompt_count = COALESCE(@prompt_count, prompt_count),\n             failure_reason = COALESCE(@failure_reason, failure_reason),\n             session_name = COALESCE(@session_name, session_name)\n       WHERE session_id = @session_id\n    `);\n\n    this.bindStmt = db.prepare(`\n      UPDATE managed_sessions\n         SET bound_conversation_id = @bound_conversation_id\n       WHERE session_id = @session_id\n    `);\n\n    this.getStmt = db.prepare(\"SELECT * FROM managed_sessions WHERE session_id = ?\");\n\n    // The reconciler's boot read. Terminal rows are kept as history but never\n    // re-probed — there is nothing left to discover about them.\n    //\n    // Bounded because every row costs a pid probe, and on Windows a probe is a\n    // CIM query measured in seconds: an unbounded set turns a registry that\n    // accumulated badly into a multi-minute boot. Oldest first, so the rows\n    // most likely to be genuinely stale are the ones examined.\n    this.listNonTerminalStmt = db.prepare(`\n      SELECT * FROM managed_sessions\n       WHERE completed_at IS NULL\n       ORDER BY started_at ASC\n       LIMIT @limit\n    `);\n\n    // The diagnostics read. Unlike the two above it filters nothing — the\n    // question it answers is \"what does the registry actually hold\", and a row\n    // excluded by a WHERE clause is exactly the row someone is looking for.\n    this.listAllStmt = db.prepare(`\n      SELECT * FROM managed_sessions\n       ORDER BY status_updated_at DESC\n       LIMIT @limit\n    `);\n\n    this.pruneTerminalStmt = db.prepare(`\n      DELETE FROM managed_sessions\n       WHERE completed_at IS NOT NULL\n         AND completed_at < @before\n    `);\n\n    // The rehydrator's boot read. Deliberately WIDER than listNonTerminal:\n    // `recordShutdownState` stamps completed_at on every live session as the\n    // streamer stops, so the sessions most worth recovering are exactly the ones\n    // the probe set excludes. `status_source = 'shutdown'` is what distinguishes\n    // \"we stopped it\" from \"the agent finished\" — written since C1 Phase 2, read\n    // by nothing until now.\n    this.listRecoverableStmt = db.prepare(`\n      SELECT * FROM managed_sessions\n       WHERE (completed_at IS NULL OR status_source = 'shutdown')\n         AND (@include_subagents = 1 OR is_subagent = 0)\n         AND status_updated_at >= @since\n       ORDER BY status_updated_at DESC\n       LIMIT @limit\n    `);\n\n    this.deleteStmt = db.prepare(\"DELETE FROM managed_sessions WHERE session_id = ?\");\n  }\n\n  /** Record a session at spawn, or refresh every field of an existing row. */\n  recordSpawn({ session, pid, cmdline, streamerInstanceId }: RecordSpawnInput): void {\n    this.upsertStmt.run({\n      session_id: session.id,\n      is_subagent: Number(session.isSubagent ?? false),\n      parent_conversation_id: session.parentConversationId ?? null,\n      provider: session.provider ?? \"claude-code\",\n      pid,\n      cmdline,\n      project_path: session.projectPath,\n      project_name: session.projectName,\n      branch: session.branch ?? \"\",\n      status: session.status,\n      status_source: \"spawn\" satisfies StatusSource,\n      status_updated_at: Date.now(),\n      started_at: session.startedAt.getTime(),\n      completed_at: session.completedAt?.getTime() ?? null,\n      last_activity_at: session.lastActivityAt?.getTime() ?? null,\n      prompt_count: session.promptCount,\n      session_name: session.sessionName ?? null,\n      project_id: session.projectId ?? null,\n      bound_conversation_id: session.boundConversationId ?? null,\n      resumed_from_conversation_id: session.resumedFromConversationId ?? null,\n      failure_reason: session.failureReason ?? null,\n      streamer_instance_id: streamerInstanceId,\n      // Recorded, never backfilled: the pid above is only probeable while this\n      // token still matches the running machine.\n      boot_token: currentBootToken(),\n    });\n  }\n\n  /**\n   * Persist a status transition. `source` is required rather than defaulted:\n   * a status whose provenance is unknown is the thing this table exists to\n   * prevent, and the reconciler reads it to decide how much to trust the value.\n   */\n  recordStatus(\n    sessionId: string,\n    status: SessionStatus,\n    source: StatusSource,\n    fields: {\n      completedAt?: Date | null;\n      lastActivityAt?: Date | null;\n      promptCount?: number;\n      failureReason?: string | null;\n      /** Null/omitted keeps whatever is stored — it never clears a known name. */\n      sessionName?: string | null;\n    } = {},\n  ): void {\n    this.updateStatusStmt.run({\n      session_id: sessionId,\n      status,\n      status_source: source,\n      status_updated_at: Date.now(),\n      completed_at: fields.completedAt?.getTime() ?? null,\n      last_activity_at: fields.lastActivityAt?.getTime() ?? null,\n      prompt_count: fields.promptCount ?? null,\n      failure_reason: fields.failureReason ?? null,\n      session_name: fields.sessionName ?? null,\n    });\n  }\n\n  /**\n   * Persist the Codex rollout id discovered after spawn.\n   *\n   * Its own statement rather than a `recordSpawn` re-run: the binding arrives\n   * while the session is live, and re-upserting would also rewrite `cmdline`\n   * with an id that is *not* in a fresh Codex process's argv, turning the\n   * reconciler's identity check into a false `orphaned`. Without this write the\n   * binding lives only in memory and dies with the streamer — which is the\n   * whole reason a restarted Codex session could not be resumed (G6).\n   */\n  recordBinding(sessionId: string, boundConversationId: string): void {\n    this.bindStmt.run({\n      session_id: sessionId,\n      bound_conversation_id: boundConversationId,\n    });\n  }\n\n  get(sessionId: string): ManagedSessionRow | null {\n    return (this.getStmt.get(sessionId) as ManagedSessionRow | undefined) ?? null;\n  }\n\n  /**\n   * Rows with no recorded completion — the reconciler's probe set.\n   *\n   * Capped. Callers must compare the result length against the limit and say so\n   * when it clips: a silently truncated probe set reads as \"we checked\n   * everything\" when it did not.\n   */\n  listNonTerminal(limit: number = PROBE_SET_MAX): ManagedSessionRow[] {\n    return this.listNonTerminalStmt.all({ limit }) as ManagedSessionRow[];\n  }\n\n  /** Every row, most recently touched first, for the diagnostics surface. */\n  listAll(limit: number = DIAGNOSTICS_MAX): ManagedSessionRow[] {\n    return this.listAllStmt.all({ limit }) as ManagedSessionRow[];\n  }\n\n  /**\n   * Delete terminal rows older than `olderThanMs`, returning how many went.\n   *\n   * Only rows carrying a `completed_at` are eligible, so nothing the reconciler\n   * or rehydrator might still want is reachable from here — a row without one\n   * is by definition unfinished business, however old it looks.\n   */\n  pruneTerminal(olderThanMs: number = TERMINAL_RETENTION_MS): number {\n    return this.pruneTerminalStmt.run({ before: Date.now() - olderThanMs }).changes;\n  }\n\n  /**\n   * Rows a restart could bring back: still open, or closed by our own shutdown,\n   * and touched no longer ago than `sinceMs`. Newest first, capped — the caller\n   * decides which of these actually deserve rehydrating (`shouldRehydrate`).\n   */\n  listRecoverable({\n    sinceMs,\n    limit,\n    includeSubagents = true,\n  }: {\n    sinceMs: number;\n    limit: number;\n    includeSubagents?: boolean;\n  }): ManagedSessionRow[] {\n    return this.listRecoverableStmt.all({\n      since: sinceMs,\n      limit,\n      include_subagents: Number(includeSubagents),\n    }) as ManagedSessionRow[];\n  }\n\n  delete(sessionId: string): void {\n    this.deleteStmt.run(sessionId);\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  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    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,\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,\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      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","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 Database from \"better-sqlite3\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\nimport { instrumentDatabase } from \"./query-timing\";\nimport { resolveMigrationsDir, runSqliteMigrations } from \"./sqlite-migrate\";\n\n/**\n * Where runtime.db lives: explicit override, then the test hook, then a sibling\n * of server.yaml — deliberately NOT under cache/, for the reasons below.\n *\n * Exported because both StreamerServer and the CLI's `devices` command need to\n * agree on it, and a second hand-written copy of this precedence chain would\n * drift the moment one of the three sources changed.\n */\nexport function resolveRuntimeDbPath(override?: string): string {\n  return (\n    override ??\n    process.env.THREADBASE_RUNTIME_DB ??\n    join(process.env.THREADBASE_CONFIG_DIR ?? join(homedir(), \".threadbase\"), \"runtime.db\")\n  );\n}\n\n/**\n * `~/.threadbase/runtime.db` — the authoritative, non-derived half of the\n * streamer's SQLite state.\n *\n * Deliberately a separate file from `cache/cache.db`, and deliberately not\n * under `cache/`. The conversation cache is rebuildable from the provider\n * JSONLs at any time, which is why \"delete the cache and restart\" is reasonable\n * support advice and why the integrity monitor offers a reset-and-rescan\n * action. The managed-session registry is not rebuildable from anything on\n * disk, so it must not sit one plausible instruction away from deletion — nor\n * share a handle whose failure (a better-sqlite3 ABI mismatch, most commonly)\n * would silently disable session persistence along with the cache.\n *\n * Runs its own migrations from `src/db/runtime-migrations/`, tracked in this\n * file's own `schema_migrations` table.\n */\nexport class RuntimeStore {\n  private constructor(private readonly db: Database.Database) {}\n\n  static open(dbPath: string, migrationsDir?: string): RuntimeStore {\n    const db = instrumentDatabase(new Database(dbPath));\n    db.pragma(\"journal_mode = WAL\");\n    runSqliteMigrations(db, migrationsDir ?? resolveMigrationsDir(\"runtime-migrations\"));\n    return new RuntimeStore(db);\n  }\n\n  getDatabase(): Database.Database {\n    return this.db;\n  }\n\n  /**\n   * One-time move of `managed_sessions` rows out of a pre-split `cache.db`.\n   *\n   * Returns the number of rows copied.\n   */\n  importLegacyManagedSessions(source: Database.Database): number {\n    return this.importLegacyTable(source, \"managed_sessions\");\n  }\n\n  /**\n   * One-time move of `devices` rows out of `cache.db`, where the registry used\n   * to live (migration `011_create_devices.sql`).\n   *\n   * Losing this table invalidates every device token ever issued, and cache.db\n   * is the file `tb-streamer cache clear` deletes and the integrity monitor\n   * rebuilds — see `runtime-migrations/003_create_devices.sql`.\n   *\n   * Unlike `managed_sessions`, this one MOVES rather than copies: the source\n   * rows are deleted once the copy is verified. A `devices` row carries a\n   * user-supplied label (\"Ronen's iPhone\"), and leaving a second copy of that\n   * on disk indefinitely — in the one file the user is told to delete when\n   * something goes wrong — is more retained personal data than the rollback\n   * path is worth. Recovering from a rollback is re-scanning a pairing QR.\n   *\n   * The delete is conditional on the copy being complete: `INSERT OR IGNORE`\n   * can silently skip a row, so the destination count must match what was read\n   * before anything is removed. A mismatch keeps the source and reports\n   * `purged: false` rather than throwing — the import itself still succeeded,\n   * and keeping data is the safe direction to fail in.\n   */\n  importLegacyDevices(source: Database.Database): { copied: number; purged: boolean } {\n    const copied = this.importLegacyTable(source, \"devices\");\n    if (copied === 0) return { copied: 0, purged: false };\n\n    const landed = (this.db.prepare(\"SELECT COUNT(*) AS n FROM devices\").get() as { n: number }).n;\n    if (landed !== copied) return { copied, purged: false };\n\n    source.prepare(\"DELETE FROM devices\").run();\n    // Deliberately no VACUUM. The rows can linger in cache.db's free pages\n    // until reused, but the same data sits unencrypted in runtime.db by design,\n    // so reclaiming those pages buys nothing and a VACUUM locks a database the\n    // scanner is actively writing to.\n    return { copied, purged: true };\n  }\n\n  /**\n   * Copy a whole table out of a pre-split `cache.db` into this file.\n   *\n   * The copy itself is non-destructive — the source is left in place, so an\n   * older streamer rolled back onto the same machine still finds its data.\n   * `importLegacyDevices` deletes the source afterwards for its own reasons;\n   * `managed_sessions` does not. Runs only when this file's table is empty, so\n   * a second boot is a no-op rather than a re-copy that would resurrect rows\n   * deleted since.\n   *\n   * The table name is interpolated into SQL, so it is typed as a closed union\n   * rather than `string` — the set of tables that can ever be lifted is known\n   * at compile time, and that is what keeps a caller from making this a hole.\n   */\n  private importLegacyTable(\n    source: Database.Database,\n    table: \"managed_sessions\" | \"devices\",\n  ): number {\n    const existing = this.db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get() as {\n      n: number;\n    };\n    if (existing.n > 0) return 0;\n\n    const hasTable = source\n      .prepare(\"SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?\")\n      .get(table);\n    if (!hasTable) return 0;\n\n    const rows = source.prepare(`SELECT * FROM ${table}`).all() as Array<Record<string, unknown>>;\n    if (rows.length === 0) return 0;\n\n    // Column names come from SQLite's own schema, never from user input.\n    const columns = Object.keys(rows[0]);\n    const insert = this.db.prepare(\n      `INSERT OR IGNORE INTO ${table} (${columns.join(\", \")})\n       VALUES (${columns.map((c) => `@${c}`).join(\", \")})`,\n    );\n    this.db.transaction((batch: Array<Record<string, unknown>>) => {\n      for (const row of batch) insert.run(row);\n    })(rows);\n\n    return rows.length;\n  }\n\n  close(): void {\n    this.db.close();\n  }\n}\n","// The authenticated payloads inside the pair handshake's two Noise messages.\n//\n// Distinct from `pair-request.ts`, which parses the *outer*, unauthenticated\n// `e2ee` field of the HTTP body. Everything here sits inside the AEAD: message\n// 1's payload decrypted only because the initiator held the right static key\n// and the PSK derived from this QR's pair token, and message 2's payload\n// encrypted so only that same initiator can read it.\n//\n// That is the whole reason these exist as a separate contract (design.md §2.4,\n// GATE 4). The outer JSON is attacker-modifiable — an intermediary can rename a\n// device or widen `readOnly` on the way past — so on the E2EE path the values\n// that reach the device row come from message 1's payload, and every result a\n// new client persists or presents as verified comes from message 2's.\n//\n// Being inside the AEAD makes the bytes authentic, NOT well-formed. A payload\n// that decrypts is one the peer really sent; it is still JSON of an arbitrary\n// shape, so it is parsed with the same suspicion as the outer field.\n\nimport { E2EE_EXCHANGE_VERSION, E2eeRequestError } from \"./pair-request\";\n\n/**\n * The device-registration inputs message 1 authenticates.\n *\n * `deviceName` is the one optional field in the contract, and is normalised to\n * `null` when absent so both pairing paths hand `devicesRepo.register` the same\n * shape. The 100-character bound is the legacy path's, kept identical rather\n * than re-chosen: authenticating a value is not a reason to stop bounding it.\n */\nexport interface E2eePairRegistration {\n  version: number;\n  deviceName: string | null;\n  readOnly: boolean;\n}\n\n/** Matches the legacy path's `body.deviceName.slice(0, 100)`. */\nconst MAX_DEVICE_NAME_CHARS = 100;\n\n/**\n * Parse message 1's decrypted payload.\n *\n * Throws rather than defaulting. A client that completed the handshake and then\n * sent an unreadable payload is a protocol violation, not an older client — an\n * older client cannot reach this function at all, because it sends no `e2ee`\n * field and no handshake is performed. Defaulting a missing `readOnly` to\n * `false` would silently grant the wider capability preset off a payload the\n * device never actually stated, which is the exact substitution this contract\n * exists to prevent, arrived at through a parser instead of an intermediary.\n *\n * `v` is checked against the same version and with the same ordering as\n * `parseE2eeRequest`: a version mismatch is not a malformed payload, and saying\n * so is what lets a future client fall back deliberately.\n */\nexport function parseE2eeMsg1Payload(payload: Buffer): E2eePairRegistration {\n  let raw: unknown;\n  try {\n    raw = JSON.parse(payload.toString(\"utf-8\"));\n  } catch {\n    throw new E2eeRequestError(\"E2EE_MALFORMED\", \"e2ee message 1 payload is not valid JSON\");\n  }\n\n  if (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) {\n    throw new E2eeRequestError(\"E2EE_MALFORMED\", \"e2ee message 1 payload must be an object\");\n  }\n\n  const { v, deviceName, readOnly } = raw as {\n    v?: unknown;\n    deviceName?: unknown;\n    readOnly?: unknown;\n  };\n\n  if (typeof v !== \"number\" || !Number.isInteger(v)) {\n    throw new E2eeRequestError(\"E2EE_MALFORMED\", \"e2ee message 1 payload v must be an integer\");\n  }\n  if (v !== E2EE_EXCHANGE_VERSION) {\n    throw new E2eeRequestError(\n      \"E2EE_VERSION_UNSUPPORTED\",\n      `e2ee message 1 payload v ${v} is not supported; this server speaks ${E2EE_EXCHANGE_VERSION}`,\n    );\n  }\n\n  if (typeof readOnly !== \"boolean\") {\n    throw new E2eeRequestError(\n      \"E2EE_MALFORMED\",\n      \"e2ee message 1 payload readOnly must be a boolean\",\n    );\n  }\n\n  if (deviceName !== undefined && deviceName !== null && typeof deviceName !== \"string\") {\n    throw new E2eeRequestError(\n      \"E2EE_MALFORMED\",\n      \"e2ee message 1 payload deviceName must be a string when present\",\n    );\n  }\n\n  return {\n    version: v,\n    deviceName: typeof deviceName === \"string\" ? deviceName.slice(0, MAX_DEVICE_NAME_CHARS) : null,\n    readOnly,\n  };\n}\n\n/**\n * Every pairing result a new client persists or presents as verified.\n *\n * The outer response still carries compatibility copies of `deviceId`,\n * `deviceToken`, `capabilities`, `publicUrl` and `machineName` for released\n * clients that can read nothing else, and a new client ignores all of them —\n * so this shape is the one that has to be complete. A field missing here is a\n * field the new client would have to take from the unauthenticated outer copy,\n * which is the same as not authenticating it at all.\n *\n * `deviceId` and `deviceToken` are non-nullable on purpose: an E2EE pairing\n * that cannot produce them is a failed pairing, not a success carrying nulls.\n */\nexport interface E2eeMsg2Payload {\n  v: number;\n  deviceId: string;\n  deviceToken: string;\n  capabilities: string[];\n  publicUrl: string | null;\n  machineName: string;\n  serverVersion: string;\n  /**\n   * Always `true`. Completing a handshake is what pins the device, and\n   * design.md §6.3 says nothing a client sends ever clears it — so this is a\n   * literal here rather than an argument, and there is no call site that can\n   * pass `false` by accident.\n   */\n  e2eeRequired: true;\n}\n\nexport function encodeE2eeMsg2Payload(fields: Omit<E2eeMsg2Payload, \"v\" | \"e2eeRequired\">): Buffer {\n  const payload: E2eeMsg2Payload = {\n    v: E2EE_EXCHANGE_VERSION,\n    ...fields,\n    e2eeRequired: true,\n  };\n  return Buffer.from(JSON.stringify(payload), \"utf-8\");\n}\n","import { statSync } from \"fs\";\nimport { ConversationCache } from \"./conversation-cache\";\nimport { getLogger } from \"./logger\";\nimport type { CacheIntegrityMonitor } from \"./services/cache-integrity/cacheIntegrityMonitor\";\nimport type { ConversationWatcher } from \"./services/conversations/conversationWatcher\";\nimport { RESUME_BUSY_WINDOW_MS } from \"./services/sessions/conversationBusy\";\nimport type { SessionActivity, SessionResponse } from \"./types\";\nimport { canonicalizeFilePath } from \"./utils/canonicalizeFilePath\";\nimport type { WSHub } from \"./ws-hub\";\n\n// ─── External (non-PTY) live tails ───────────────────────────────────────────\n// A JSONL that changes in a watched project directory while NOT owned by any\n// PTY session belongs to an external agent (a terminal `claude`, another\n// streamer, an IDE). Tail it explicitly so mobile gets pushed transcript lines\n// for it instead of nothing.\n\n// A directory event only attaches a tail when the file was touched this\n// recently — the same \"actively owned right now\" window the resume collision\n// probe uses, so both features agree on what \"live\" means.\nexport const EXTERNAL_TAIL_RECENCY_MS = RESUME_BUSY_WINDOW_MS;\n\n// Hard cap on concurrent external tails; attaching past it LRU-evicts the\n// least recently active one. Bounds chokidar handles on a machine with a large\n// project tree (an unbounded map would attach one per touched JSONL).\nexport const EXTERNAL_TAIL_MAX = 32;\n\n// An external tail with no appended lines for this long is detached — the\n// external agent finished or moved on, and the directory watcher re-attaches\n// if it starts writing again.\nexport const EXTERNAL_TAIL_IDLE_MS = 300_000; // 5 minutes\n\n// A JSONL that grew within this window reads as \"active_writing\"; older reads as\n// \"quiet\". Deliberately short — this is an inferred hint, not a status.\nexport const EXTERNAL_ACTIVE_WRITING_MS = 30_000;\n\nexport type ExternalTailEntry = { conversationId: string; lastActivityAt: number };\n\n/**\n * Everything ExternalTailManager reads from the server. Nullable collaborators\n * are thunks rather than values because they are opened during listen() and\n * rebound by the integrity monitor's reset-and-rescan — the same reason\n * ApiDeps passes `cache: () => ConversationCache | null`.\n *\n * `tails` and `sessionFileMap` are the server's own Maps, held by reference:\n * the server still owns them as instance fields (both are reached directly\n * elsewhere in server.ts, and the tests read `externalTails` off the server),\n * this class only drives them.\n */\nexport type ExternalTailManagerDeps = {\n  tails: Map<string, ExternalTailEntry>;\n  sessionFileMap: Map<string, string>;\n  fileWatcher: ConversationWatcher;\n  wsHub: WSHub;\n  cache: () => ConversationCache | null;\n  cacheMonitor: () => CacheIntegrityMonitor | null;\n  broadcastConversationLines: (\n    sessionId: string,\n    lines: string[],\n    seqs?: (number | null)[] | null,\n  ) => void;\n};\n\n/**\n * Owns the live tails on JSONLs no PTY session is writing: when one attaches,\n * when it is evicted or swept, the inferred activity it feeds into session\n * responses, and the transcript lines it pushes.\n *\n * Extracted from StreamerServer so external-tail work stops editing the server\n * file, continuing the split in docs/plans/2026-07-12-server-ts-split.md.\n */\nexport class ExternalTailManager {\n  private log = getLogger(\"server\");\n\n  constructor(private deps: ExternalTailManagerDeps) {}\n\n  private get externalTails(): Map<string, ExternalTailEntry> {\n    return this.deps.tails;\n  }\n\n  /** True when a managed (PTY) session owns the tail for this canonical path. */\n  isManagedTailPath(key: string): boolean {\n    for (const watchedPath of this.deps.sessionFileMap.values()) {\n      if (canonicalizeFilePath(watchedPath) === key) return true;\n    }\n    return false;\n  }\n\n  /**\n   * Attach a live tail to a JSONL nobody is tailing yet, when it was touched\n   * recently enough to look actively written by an external agent. Capped at\n   * EXTERNAL_TAIL_MAX with LRU eviction.\n   */\n  maybeAttachExternalTail(filePath: string): void {\n    if (!filePath.endsWith(\".jsonl\")) return;\n    const key = canonicalizeFilePath(filePath);\n    if (this.externalTails.has(key)) return;\n    // A managed session's tail is owned by the PTY path; never shadow it.\n    if (this.isManagedTailPath(key)) return;\n\n    let mtimeMs: number;\n    try {\n      mtimeMs = statSync(filePath).mtimeMs;\n    } catch {\n      return; // unlink event, or unreadable — nothing to tail\n    }\n    const now = Date.now();\n    // Only the UPPER bound matters: a just-written file can carry an mtime a\n    // few ms in the future (timestamp granularity / clock skew).\n    if (now - mtimeMs > EXTERNAL_TAIL_RECENCY_MS) return;\n\n    this.evictExternalTailsIfNeeded();\n    // The real conversation id is resolved from the cache row on the first\n    // broadcast (codex rollout files aren't named after their id); the filename\n    // stem is only a placeholder until then.\n    this.externalTails.set(key, {\n      conversationId: ConversationCache.conversationIdForFile(key),\n      lastActivityAt: now,\n    });\n    this.deps.fileWatcher.watch(filePath);\n    this.log.debug?.(`External tail attached: ${filePath}`, {\n      filePath,\n      tails: this.externalTails.size,\n      event: \"external_tail.attach\",\n    });\n  }\n\n  /** Stop tailing an external file and drop its bookkeeping. */\n  detachExternalTail(key: string): void {\n    if (!this.externalTails.delete(key)) return;\n    this.deps.fileWatcher.unwatch(key);\n    this.log.debug?.(`External tail detached: ${key}`, {\n      filePath: key,\n      event: \"external_tail.detach\",\n    });\n  }\n\n  /**\n   * Shared unlink path for the per-file watcher and the directory watcher.\n   * Detaches any external tail and drops the cache row (unless an integrity\n   * alert is freezing deletes).\n   */\n  handleJsonlDeleted(filePath: string): void {\n    // The file is gone; an external tail on it can never fire again.\n    this.detachExternalTail(canonicalizeFilePath(filePath));\n    // While an alert is pending, freeze: queue the deletion instead of\n    // invalidating the row, so an rm -rf mid-freeze can't drain the cache.\n    if (this.deps.cacheMonitor()?.pending) {\n      this.deps.cacheMonitor()?.deferUnlink(filePath);\n      return;\n    }\n    const id = this.deps.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    // Feed the storm detector — a burst of unlinks re-triggers detection.\n    this.deps.cacheMonitor()?.recordUnlink(filePath);\n  }\n\n  /** Make room for one more tail by evicting the least recently active ones. */\n  evictExternalTailsIfNeeded(): void {\n    while (this.externalTails.size >= EXTERNAL_TAIL_MAX) {\n      let lruKey: string | null = null;\n      let lruAt = Number.POSITIVE_INFINITY;\n      for (const [key, entry] of this.externalTails) {\n        // A path a PTY session has since adopted is no longer ours: release the\n        // bookkeeping WITHOUT closing the watcher the managed path now owns.\n        if (this.isManagedTailPath(key)) {\n          this.externalTails.delete(key);\n          return;\n        }\n        if (entry.lastActivityAt < lruAt) {\n          lruAt = entry.lastActivityAt;\n          lruKey = key;\n        }\n      }\n      if (!lruKey) return;\n      this.detachExternalTail(lruKey);\n    }\n  }\n\n  /**\n   * INFERRED activity for an externally-owned conversation, derived purely from\n   * how recently its JSONL grew (the external tail's bookkeeping). Returns\n   * undefined when we hold no tail for it, so a session we know nothing about\n   * reports no activity rather than a fabricated \"quiet\".\n   *\n   * This can never distinguish a generating agent from one blocked on a\n   * permission gate — gates render on the PTY screen and never reach the JSONL —\n   * which is why it is a separate field and not folded into `status`.\n   */\n  externalActivityFor(conversationId: string, now = Date.now()): SessionActivity | undefined {\n    for (const entry of this.externalTails.values()) {\n      if (entry.conversationId !== conversationId) continue;\n      return {\n        state:\n          now - entry.lastActivityAt <= EXTERNAL_ACTIVE_WRITING_MS ? \"active_writing\" : \"quiet\",\n        lastEventAt: new Date(entry.lastActivityAt).toISOString(),\n        source: \"jsonl\",\n      };\n    }\n    return undefined;\n  }\n\n  /** Attach inferred `activity` to externally-owned sessions in a response set. */\n  withExternalActivity(sessions: readonly SessionResponse[]): readonly SessionResponse[] {\n    if (this.externalTails.size === 0) return sessions;\n    const now = Date.now();\n    return sessions.map((s) => {\n      if (s.ownership !== \"external\") return s;\n      const activity = this.externalActivityFor(s.conversationId ?? s.id, now);\n      return activity ? { ...s, activity } : s;\n    });\n  }\n\n  /** Detach external tails idle past EXTERNAL_TAIL_IDLE_MS. */\n  sweepIdleExternalTails(now = Date.now()): void {\n    for (const [key, entry] of [...this.externalTails]) {\n      if (this.isManagedTailPath(key)) {\n        // Adopted by a PTY session — release bookkeeping, keep the watcher.\n        this.externalTails.delete(key);\n        continue;\n      }\n      if (now - entry.lastActivityAt > EXTERNAL_TAIL_IDLE_MS) this.detachExternalTail(key);\n    }\n  }\n\n  /**\n   * Push appended lines from an externally-owned conversation. Reuses the exact\n   * conversation_events / conversation_event shapes mobile already consumes,\n   * keyed by the conversation UUID — an external session has no PTY, so it must\n   * never produce terminal_output / terminal_replay / session_ready, and never a\n   * session_update whose session.id is a conversation UUID (that would mint a\n   * phantom session row in the mobile cache). Question cards are likewise never\n   * derived here: with no PTY there is nothing that could deliver an answer.\n   */\n  broadcastExternalTailLines(\n    filePath: string,\n    lines: string[],\n    seqs?: (number | null)[] | null,\n  ): void {\n    const key = canonicalizeFilePath(filePath);\n    const entry = this.externalTails.get(key);\n    if (!entry) return;\n    entry.lastActivityAt = Date.now();\n\n    // Resolve the id from the cache row updateFromLines just wrote. Absent means\n    // nothing recordable landed (or the batch was an agent JSONL, whose row is\n    // deleted) — either way there is nothing to push.\n    const conversationId = this.deps.cache()?.getIdByFilePath(key);\n    if (!conversationId) return;\n    entry.conversationId = conversationId;\n\n    // A provider-created child session is hidden everywhere over HTTP when the\n    // subagentSessions flag is off — the list omits it, /api/sessions omits it,\n    // the detail 404s, and subscribe_session refuses it. This path was the one\n    // surface that ignored that: chokidar watches project dirs recursively, so\n    // Claude's <project>/<parentId>/subagents/agent-<id>.jsonl looks exactly\n    // like a JSONL an external agent is writing, and its every line — including\n    // the fork boilerplate the parent never sees — went out to all clients.\n    // Same predicate the HTTP path uses, so the two cannot disagree.\n    if (this.deps.cache()?.isExcludedSubagent(conversationId)) return;\n\n    this.deps.broadcastConversationLines(conversationId, lines, seqs);\n\n    // List-row refresh hint so clients don't have to poll ?refresh=1 to notice\n    // an external conversation advancing.\n    const meta = this.deps.cache()?.getMetaById(conversationId);\n    this.deps.wsHub.broadcast({\n      type: \"conversation_updated\",\n      conversationId,\n      messageCount: meta?.messageCount ?? 0,\n      lastActivity: meta?.lastActivity ?? new Date().toISOString(),\n      ownership: \"external\",\n    });\n  }\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  /**\n   * What `consume` would answer right now, WITHOUT spending the token.\n   *\n   * Exists so a caller can reject a bad token before doing any work, and still\n   * spend the token only once the work has succeeded. A pair token is\n   * single-use and lives 180 seconds, so spending it on a request that then\n   * fails costs the user a whole new QR — and, worse, makes their retry\n   * indistinguishable from an attacker replaying a photographed code, which is\n   * the one signal `design.md` §2.6 designates as replay detection.\n   *\n   * **Named for its relationship to `consume`, deliberately.** It reserves\n   * nothing and takes no lock, so two callers can both be told `{ ok: true }`\n   * for the same token. A name like `verify` would read as a gate that had\n   * decided something, and inviting a caller to act on this alone is precisely\n   * the misuse the ordering it enables exists to prevent. A reader who sees\n   * `wouldConsume` asks where the `consume` is, which is the right question.\n   */\n  wouldConsume(token: string): ConsumeResult {\n    const result = this.check(token);\n    return result.ok ? { ok: true } : result;\n  }\n\n  consume(token: string): ConsumeResult {\n    const result = this.check(token);\n    if (!result.ok) return result;\n    result.record.used = true;\n    return { ok: true };\n  }\n\n  /**\n   * The shared predicate behind `verify` and `consume`.\n   *\n   * One implementation on purpose: two copies of \"is this token usable\" is two\n   * places for the expiry or single-use rule to drift, and a drift in this\n   * direction fails open.\n   */\n  private check(token: string): { ok: true; record: PairTokenRecord } | ConsumeErr {\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    return { ok: true, record };\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 { spawn } from \"node:child_process\";\nimport type { HostTransport } from \"./protocol\";\nimport { connectToHost, hostSocketPath } from \"./socket\";\n\n/**\n * Start a pty-host, or attach to the one already running (plan Phase 6b).\n *\n * The streamer calls this at boot when the `ptyHost` feature flag is enabled.\n */\n\n/** How long to wait for a freshly-spawned host to accept a connection. */\nconst HOST_READY_TIMEOUT_MS = 5_000;\nconst HOST_POLL_INTERVAL_MS = 50;\n\nexport interface SpawnHostOptions {\n  instanceId: string;\n  /** argv[1] for the child — the CLI entry point. Defaults to this process's. */\n  entryPoint?: string;\n  timeoutMs?: number;\n}\n\n/**\n * Connect to the host for this instance, spawning it if none is listening.\n *\n * Attach-first rather than spawn-first: the host outliving the streamer is the\n * feature, so the common case after a restart is that one is already there.\n */\nexport async function connectOrSpawnHost(options: SpawnHostOptions): Promise<HostTransport> {\n  const socketPath = hostSocketPath(options.instanceId);\n\n  try {\n    return await connectToHost(socketPath);\n  } catch {\n    // Nothing listening — ours to start.\n  }\n\n  spawnDetachedHost(socketPath, options.entryPoint);\n\n  // Poll rather than wait on a ready signal: the child's stdio is discarded, so\n  // there is no channel to signal on. The socket accepting a connection is the\n  // only readiness fact that actually matters to the caller.\n  const deadline = Date.now() + (options.timeoutMs ?? HOST_READY_TIMEOUT_MS);\n  let lastError: unknown;\n  while (Date.now() < deadline) {\n    try {\n      return await connectToHost(socketPath);\n    } catch (err) {\n      lastError = err;\n      await new Promise((r) => setTimeout(r, HOST_POLL_INTERVAL_MS));\n    }\n  }\n  throw new Error(\n    `pty-host did not accept a connection on ${socketPath} within ${options.timeoutMs ?? HOST_READY_TIMEOUT_MS}ms` +\n      (lastError instanceof Error ? `: ${lastError.message}` : \"\"),\n  );\n}\n\n/**\n * Spawn the host so it survives this process.\n *\n * Three properties, each load-bearing:\n *\n *  - `detached` puts it in its own process group, so a Ctrl-C or a group kill\n *    aimed at the streamer does not take the agents with it.\n *  - `stdio: \"ignore\"` means it holds none of our descriptors. A child that\n *    inherited our stdout keeps a pipe open that the parent's exit cannot\n *    close, and on POSIX would also receive our terminal's SIGHUP — which is\n *    precisely the hangup this whole feature exists to avoid.\n *  - `unref()` removes it from our event loop, so the streamer can exit while\n *    the host keeps running.\n */\nexport function spawnDetachedHost(socketPath: string, entryPoint?: string): void {\n  const child = spawn(\n    process.execPath,\n    [entryPoint ?? process.argv[1], \"pty-host\", \"--socket\", socketPath],\n    { detached: true, stdio: \"ignore\" },\n  );\n  child.unref();\n}\n","import { createConnection, createServer, type Server, type Socket } from \"node:net\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport {\n  encodeMessage,\n  type HostMessage,\n  type HostTransport,\n  isHostEvent,\n  LineDecoder,\n} from \"./protocol\";\n\n/**\n * Socket plumbing for the pty-host (plan Phase 6b).\n *\n * A unix domain socket on POSIX, a named pipe on Windows. Both are local-only\n * by construction, which is the security property that matters: the host can\n * spawn arbitrary agent processes, so it must never be reachable over a TCP\n * port that something else on the network could find.\n */\n\n/**\n * Where the host listens.\n *\n * Scoped by instance id, so two streamers configured as separate instances on\n * one machine do not fight over one host — the same reason DB-persisted\n * sessions are scoped by it.\n *\n * POSIX puts it under the config dir rather than /tmp: `THREADBASE_CONFIG_DIR`\n * already redirects the whole config tree in tests, and a path under a\n * user-owned directory cannot be pre-created by another user to hijack the\n * bind. Windows named pipes are not filesystem paths at all, so the config dir\n * has nothing to say about them.\n */\nexport function hostSocketPath(instanceId: string): string {\n  if (process.platform === \"win32\") {\n    return `\\\\\\\\.\\\\pipe\\\\threadbase-pty-host-${instanceId}`;\n  }\n  const dir = process.env.THREADBASE_CONFIG_DIR ?? join(homedir(), \".threadbase\");\n  return join(dir, \"run\", `pty-host-${instanceId}.sock`);\n}\n\n/** Wrap a connected socket as the duplex line channel the runner expects. */\nexport function socketTransport(socket: Socket): HostTransport {\n  socket.setEncoding(\"utf8\");\n  return {\n    send(line: string) {\n      // Writes are fire-and-forget: a full send buffer backpressures inside\n      // node, and a dead socket surfaces through onClose rather than here.\n      socket.write(line);\n    },\n    onLine(handler) {\n      socket.on(\"data\", (chunk: string) => handler(chunk));\n    },\n    onClose(handler) {\n      let handled = false;\n      const handleClose = () => {\n        if (handled) return;\n        handled = true;\n        handler();\n      };\n      socket.once(\"close\", handleClose);\n      // A connection error is a close as far as every caller is concerned; the\n      // runner's job is to fail in-flight requests either way.\n      socket.once(\"error\", handleClose);\n    },\n    close() {\n      socket.destroy();\n    },\n  };\n}\n\n/** Connect to a host that is already listening. Rejects if none is. */\nexport function connectToHost(socketPath: string): Promise<HostTransport> {\n  return new Promise((resolve, reject) => {\n    const socket = createConnection(socketPath);\n    socket.once(\"error\", reject);\n    socket.once(\"connect\", () => {\n      socket.removeListener(\"error\", reject);\n      resolve(socketTransport(socket));\n    });\n  });\n}\n\nexport type PtyHostStatusProbe =\n  | { reachable: true; protocolVersion: number; sessionCount: number }\n  | { reachable: false; error: string };\n\n/** Query host status without subscribing to events or adopting its sessions. */\nexport async function probePtyHostStatus(\n  socketPath: string,\n  timeoutMs = 1_000,\n): Promise<PtyHostStatusProbe> {\n  let transport: HostTransport;\n  let connectTimedOut = false;\n  let connectTimer: ReturnType<typeof setTimeout> | undefined;\n  const connecting = connectToHost(socketPath);\n  try {\n    transport = await Promise.race([\n      connecting,\n      new Promise<never>((_, reject) => {\n        connectTimer = setTimeout(() => {\n          connectTimedOut = true;\n          reject(new Error(`status timed out after ${timeoutMs}ms`));\n        }, timeoutMs);\n        connectTimer.unref?.();\n      }),\n    ]);\n  } catch (err) {\n    if (connectTimer) clearTimeout(connectTimer);\n    if (connectTimedOut)\n      void connecting.then(\n        (late) => late.close(),\n        () => {},\n      );\n    return { reachable: false, error: err instanceof Error ? err.message : String(err) };\n  }\n  if (connectTimer) clearTimeout(connectTimer);\n\n  return await new Promise<PtyHostStatusProbe>((resolve) => {\n    const decoder = new LineDecoder();\n    let settled = false;\n    const finish = (result: PtyHostStatusProbe) => {\n      if (settled) return;\n      settled = true;\n      clearTimeout(timer);\n      transport.close();\n      resolve(result);\n    };\n    const timer = setTimeout(\n      () => finish({ reachable: false, error: `status timed out after ${timeoutMs}ms` }),\n      timeoutMs,\n    );\n    timer.unref?.();\n    transport.onClose(() => finish({ reachable: false, error: \"pty-host connection closed\" }));\n    transport.onLine((chunk) => {\n      for (const line of decoder.push(chunk)) {\n        let message: HostMessage;\n        try {\n          message = JSON.parse(line) as HostMessage;\n        } catch {\n          finish({ reachable: false, error: \"pty-host returned an invalid status response\" });\n          return;\n        }\n        if (isHostEvent(message) || message.id !== 1) continue;\n        if (!message.ok) {\n          finish({ reachable: false, error: message.error });\n          return;\n        }\n        const status = message.result as { protocolVersion?: unknown; sessions?: unknown };\n        if (typeof status?.protocolVersion !== \"number\" || !Array.isArray(status.sessions)) {\n          finish({ reachable: false, error: \"pty-host returned an invalid status response\" });\n          return;\n        }\n        finish({\n          reachable: true,\n          protocolVersion: status.protocolVersion,\n          sessionCount: status.sessions.length,\n        });\n      }\n    });\n    try {\n      transport.send(encodeMessage({ id: 1, type: \"status\" }));\n    } catch (err) {\n      finish({ reachable: false, error: err instanceof Error ? err.message : String(err) });\n    }\n  });\n}\n\nexport interface HostServerHandlers {\n  /** One connected streamer. Returns a disposer run when it disconnects. */\n  onConnection: (transport: HostTransport) => () => void;\n}\n\n/**\n * Listen for streamer connections.\n *\n * A stale socket file from a host that died without cleaning up would make\n * `listen` fail with EADDRINUSE forever, so an unconnectable one is removed\n * first — but only after a probe confirms nothing answers on it, because\n * deleting a live host's socket would strand every session it holds.\n */\nexport async function listenForStreamers(\n  socketPath: string,\n  handlers: HostServerHandlers,\n): Promise<Server> {\n  if (process.platform !== \"win32\") {\n    const { mkdirSync, rmSync } = await import(\"node:fs\");\n    const { dirname } = await import(\"node:path\");\n    mkdirSync(dirname(socketPath), { recursive: true });\n    try {\n      const probe = await connectToHost(socketPath);\n      probe.close();\n      throw new Error(`a pty-host is already listening on ${socketPath}`);\n    } catch (err) {\n      if ((err as Error).message?.startsWith(\"a pty-host is already listening\")) throw err;\n      // Nothing answered — the file, if any, is a corpse.\n      rmSync(socketPath, { force: true });\n    }\n  }\n\n  const server = createServer((socket) => {\n    const transport = socketTransport(socket);\n    const dispose = handlers.onConnection(transport);\n    transport.onClose(dispose);\n  });\n\n  return new Promise((resolve, reject) => {\n    server.once(\"error\", reject);\n    server.listen(socketPath, () => {\n      server.removeListener(\"error\", reject);\n      resolve(server);\n    });\n  });\n}\n\n/** Broadcast helper: a message to every currently-connected streamer. */\nexport function broadcast(\n  transports: Iterable<HostTransport>,\n  message: Parameters<typeof encodeMessage>[0],\n): void {\n  const line = encodeMessage(message);\n  for (const t of transports) {\n    try {\n      t.send(line);\n    } catch {\n      // A dead peer is reaped by its own close handler; one bad write must not\n      // stop the others from being told.\n    }\n  }\n}\n","import {\n  type Conversation,\n  type ConversationMeta,\n  ConversationScanner,\n  type FileStatEntry,\n} from \"@threadbase-sh/scanner\";\nimport { statSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\nimport type { ConversationCache } from \"./conversation-cache\";\nimport type { CacheMetadataRepository } from \"./db/repositories/cacheMetadata.repository\";\nimport type { ConversationsRepository } from \"./db/repositories/conversations.repository\";\nimport type { ProjectsRepository } from \"./db/repositories/projects.repository\";\nimport { getLogger } from \"./logger\";\nimport { CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER, CURSOR_PROVIDER } from \"./providers\";\nimport { setCacheMetadata } from \"./services/cache/cacheMetadata\";\nimport type { CacheIntegrityMonitor } from \"./services/cache-integrity/cacheIntegrityMonitor\";\nimport { refreshConversationCache } from \"./services/conversations/refreshConversationCache\";\nimport { shouldRefreshProjectsFromHdd } from \"./services/conversations/shouldRefreshProjectsFromHdd\";\nimport {\n  canonicalizeFilePath,\n  canonicalLivePathSet,\n  joinStatCacheByNativePath,\n  toNativeFilePath,\n} from \"./utils/canonicalizeFilePath\";\nimport { debounce } from \"./utils/debounce\";\nimport { isScannedSnapshotStale } from \"./utils/isScannedSnapshotStale\";\n\n// A refresh that settled within this window is served from the current\n// snapshot instead of re-parsing. Read path only: a post-write refresh is\n// never throttled out (see refreshFileAfterWrite).\nexport const REFRESH_TTL_MS = 2000;\n\n/** What a refresh request actually did — see refreshFileForRead/AfterWrite. */\nexport type RefreshOutcome =\n  /** This request started the parse that covers it. */\n  | \"refreshed\"\n  /** It awaited a parse another request had already started or queued. */\n  | \"joined\"\n  /** Read path only: a parse fulfilled within REFRESH_TTL_MS, so none ran. */\n  | \"skipped\";\n\nexport type RefreshResult = {\n  outcome: RefreshOutcome;\n  /**\n   * The scanner's own answer for the covering pass: fresh metadata, or null\n   * for a file that no longer parses (missing/empty — refreshFile drops it\n   * from the indexes and returns null). Always null for \"skipped\", which is\n   * the outcome to check before reading anything into a null meta.\n   */\n  meta: ConversationMeta | null;\n};\n\ntype RefreshPass = Promise<ConversationMeta | null>;\n\nconst withOutcome = (outcome: RefreshOutcome, pass: RefreshPass): Promise<RefreshResult> =>\n  pass.then((meta) => ({ outcome, meta }));\n\n/**\n * Per-file refresh coordination. This state is the ONLY thing that calls\n * scanner.refreshFile for its path, which is what makes the post-write\n * guarantee hold: the scanner single-flights refreshFile internally, so a\n * caller that reached it directly during an in-flight parse would be handed\n * that parse's result — metadata read before the write it is asking about.\n */\ntype RefreshState = {\n  /**\n   * The scanner the recorded work belongs to. refreshFile indexes into one\n   * instance, so a pass against a scanner that rescanForRefresh has since\n   * swapped out can never satisfy a request against its replacement.\n   */\n  scanner: ConversationScanner;\n  /**\n   * The path as the first caller for this key spelled it. Kept native rather\n   * than reusing the canonical map key, because the scanner is a native-path\n   * consumer (its own canonicalPath() converts TO the platform separator) and\n   * handing it the key would change what every existing caller passes.\n   */\n  filePath: string;\n  /** The parse currently reading the file, if any. */\n  active: RefreshPass | null;\n  /**\n   * When the last parse FULFILLED. A rejection leaves this alone: it observed\n   * nothing about the file, so it must not tell the next read \"recent enough\".\n   */\n  completedAt: number;\n  /**\n   * The follow-up pass shared by every post-write request that arrived while\n   * `active` was running. Started synchronously the moment `active` settles.\n   */\n  queued: { promise: RefreshPass; settle: (pass: RefreshPass) => void } | null;\n};\n\nexport type ConversationReconcileMode = \"files\" | \"full\";\n\nexport type ScanProfile = {\n  id: string;\n  label: string;\n  configDir: string;\n  enabled: boolean;\n  emoji: string;\n};\n\n/**\n * Everything ScannerManager reads from the server. Nullable collaborators are\n * thunks rather than values because they are opened during listen() and\n * rebound by the integrity monitor's reset-and-rescan — the same reason\n * ApiDeps passes `cache: () => ConversationCache | null`.\n */\nexport type ScannerManagerDeps = {\n  scanProfiles: ScanProfile[] | undefined;\n  codexRoots: string[];\n  cursorRoots: string[];\n  directoryDebounceMs: number;\n  persistenceDisabled: boolean;\n  cache: () => ConversationCache | null;\n  cacheMonitor: () => CacheIntegrityMonitor | null;\n  projectsRepo: () => ProjectsRepository | null;\n  conversationsRepo: () => ConversationsRepository | null;\n  cacheMetadataRepo: () => CacheMetadataRepository | null;\n  trackCacheWrite: (task: Promise<unknown>) => void;\n};\n\n/**\n * Owns the conversation scanner's lifecycle and freshness state: which scanner\n * instance is current, whether a scan is in flight, which files went stale, and\n * the cache↔disk reconcile that closes the gap.\n *\n * Extracted from StreamerServer so scanner work stops editing the server file\n * (see docs/plans/2026-07-12-server-ts-split.md, PR 2).\n */\nexport class ScannerManager {\n  private scanner: ConversationScanner | null = null;\n  private scannerReady: Promise<unknown> | null = null;\n  private scannerStale = false;\n  private persistenceDisabled: boolean;\n  private allScanners = new Set<ConversationScanner>();\n  private stalePaths = new Set<string>();\n  private reconcileInFlight: Promise<void> | null = null;\n  private lastAutoFullReconcileAt = 0;\n  // reconcileMode() returns \"full\" on every poll while a permanent drift\n  // condition holds (e.g. an orphan row) — without a cooldown each poll\n  // re-walks the whole corpus back-to-back. refresh=1 and per-file\n  // reconciles go through other paths and are unaffected.\n  private static readonly AUTO_FULL_RECONCILE_COOLDOWN_MS = 60_000;\n  // Keyed canonically (canonicalizeFilePath), because that is the identity the\n  // rest of the process keys refresh work by and the form the scanner reduces\n  // its own in-flight key to. Bounded by the set of files ever refreshed; an\n  // entry is overwritten, never duplicated, per path.\n  private refreshState = new Map<string, RefreshState>();\n  private log = getLogger(\"server\");\n\n  /**\n   * Trailing-debounced \"the directory changed\" signal. Kept public because the\n   * server wires it straight into the watcher callback and cancels it in\n   * close(); `.cancel()` is part of its contract.\n   */\n  readonly markStaleDebounced: ReturnType<typeof debounce>;\n\n  constructor(private deps: ScannerManagerDeps) {\n    this.persistenceDisabled = deps.persistenceDisabled;\n    this.markStaleDebounced = debounce(() => {\n      if (this.scannerReady) {\n        // Only arm when paths remain. takeStaleFiles may have drained them\n        // while this debounce was pending; re-arming then would leave\n        // scannerStale=true with an empty set, and the next \"files\" reconcile\n        // would silently upgrade into a full-tree rescan (#409).\n        if (this.stalePaths.size > 0) this.scannerStale = true;\n      }\n      // No adopted scanner yet, so there are no in-place indexes to refresh —\n      // drop it and let the next get() build one. The recorded paths go with\n      // it; the fresh scan covers them.\n      else {\n        this.scanner = null;\n        this.stalePaths.clear();\n      }\n    }, deps.directoryDebounceMs);\n  }\n\n  // ─── state accessors ──────────────────────────────────────────────\n\n  /** The current scanner without triggering a scan; null before the first one. */\n  get current(): ConversationScanner | null {\n    return this.scanner;\n  }\n\n  /** The in-flight (or settled) scan promise; null when no scanner was ever built. */\n  get ready(): Promise<unknown> | null {\n    return this.scannerReady;\n  }\n\n  get stale(): boolean {\n    return this.scannerStale;\n  }\n\n  set stale(value: boolean) {\n    this.scannerStale = value;\n  }\n\n  /** The stale-path set itself, so callers can add/clear/size it in place. */\n  get staleFiles(): Set<string> {\n    return this.stalePaths;\n  }\n\n  /** Drop the current scanner so the next get() builds a fresh one. */\n  invalidate(): void {\n    this.scanner = null;\n    this.scannerReady = null;\n  }\n\n  /**\n   * A new JSONL bound to a live session: the index must pick it up.\n   *\n   * Arm the stale flag when a scan already exists, so the next get() reconciles\n   * in place. With no scanner yet there is nothing to mark, so drop it instead\n   * and let the next get() build one. Exactly one of the two happens — dropping\n   * the scanner in both cases would throw away a live index on every bind.\n   */\n  markStaleOrDrop(): void {\n    if (this.scannerReady) {\n      this.scannerStale = true;\n    } else {\n      this.scanner = null;\n    }\n  }\n\n  /** Persistent indexes are disabled for the rest of this process's life. */\n  disablePersistence(): void {\n    this.persistenceDisabled = true;\n  }\n\n  /** Track a scanner the server built itself so close() still tears it down. */\n  track(scanner: ConversationScanner): void {\n    this.allScanners.add(scanner);\n  }\n\n  /**\n   * Adopt the warm-up scanner as the live one, but only if nothing else claimed\n   * the slot while the warm-up scan ran.\n   */\n  adoptIfUnclaimed(scanner: ConversationScanner): boolean {\n    if (this.scannerReady || this.scanner) return false;\n    this.scanner = scanner;\n    this.scannerReady = Promise.resolve();\n    return true;\n  }\n\n  // ─── construction helpers ─────────────────────────────────────────\n\n  buildStatCache(\n    previousScanner: ConversationScanner | null,\n  ): Map<string, { stat: FileStatEntry; meta: ConversationMeta }> | undefined {\n    const cache = this.deps.cache();\n    if (!cache) return undefined;\n    // The scanner looks statCache entries up by its own ConversationMeta\n    // .filePath, which is native-separator. Cache rows are canonical. So every\n    // join below compares canonical forms, but the returned map is always keyed\n    // native — otherwise the scanner misses every entry on Windows and silently\n    // re-parses every conversation on each rescan.\n    if (!previousScanner) {\n      const persisted = cache.getScannerStatCache();\n      if (persisted.size === 0) return undefined;\n      const nativeKeyed = new Map<string, { stat: FileStatEntry; meta: ConversationMeta }>();\n      for (const [canonicalPath, entry] of persisted) {\n        nativeKeyed.set(toNativeFilePath(canonicalPath), entry);\n      }\n      return nativeKeyed;\n    }\n    const dbStats = cache.getFileStats();\n    if (dbStats.size === 0) return undefined;\n    const statCache = joinStatCacheByNativePath(\n      previousScanner.getMetadataCache().values(),\n      dbStats,\n    );\n    return statCache.size > 0 ? statCache : undefined;\n  }\n\n  // Returns the provider + roots fragment to spread into every scan()/search() call.\n  // Empty *Roots disables that provider (safe no-op per scanner contract).\n  codexScanOpts() {\n    return {\n      providers: [CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER, CURSOR_PROVIDER],\n      codexRoots: this.deps.codexRoots,\n      cursorRoots: this.deps.cursorRoots,\n    };\n  }\n\n  newScanner(options?: ConstructorParameters<typeof ConversationScanner>[0]): ConversationScanner {\n    return new ConversationScanner(\n      options ?? (this.persistenceDisabled ? { persistent: false } : undefined),\n    );\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  projectsDirs(): string[] {\n    const profiles = this.deps.scanProfiles;\n    if (profiles && profiles.length > 0) {\n      return profiles.filter((p) => p.enabled).map((p) => join(p.configDir, \"projects\"));\n    }\n    return [join(homedir(), \".claude\", \"projects\")];\n  }\n\n  // ─── staleness ────────────────────────────────────────────────────\n\n  // Drain the stale set and disarm the flag together. The caller owns the\n  // returned paths: clearing before the refresh means events that land DURING\n  // it re-arm the flag and get their own pass instead of being swallowed.\n  takeStaleFiles(): string[] {\n    const paths = [...this.stalePaths];\n    this.stalePaths.clear();\n    this.scannerStale = false;\n    return paths;\n  }\n\n  // Reconcile exactly the JSONLs a directory event named. Failures are logged\n  // and swallowed per file: one unreadable transcript must not abort the\n  // others, and the file simply stays on its previous snapshot until the next\n  // event — the same outcome the full rescan gave on a parse failure.\n  async refreshStaleFiles(\n    scanner: ConversationScanner,\n    paths: string[],\n  ): Promise<ConversationMeta[]> {\n    const metas = await Promise.all(\n      paths.map((filePath) =>\n        // Post-write, not read: a directory event IS the notification that the\n        // file changed, so this must never be throttled out or satisfied by a\n        // parse that started before the event. Routing it through the same\n        // coordinator as the other writers is also what keeps the scanner's\n        // internal refreshFile coalescing from quietly serving one of them a\n        // pre-write parse.\n        this.refreshFileAfterWrite(scanner, filePath)\n          .then((result) => result.meta)\n          .catch((err) => {\n            this.log.warn(\"scanner.refreshFile: failed\", {\n              event: \"scanner.refresh_failed\",\n              filePath,\n              trigger: \"directory-event\",\n              err,\n            });\n            return null;\n          }),\n      ),\n    );\n    return metas.filter((m): m is ConversationMeta => m !== null);\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  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  /**\n   * Read-path refresh: throttled and coalesced, for a caller that would rather\n   * serve the current snapshot than pay a parse.\n   *\n   *  - a pass already reading the file → await it (\"joined\");\n   *  - a pass that fulfilled within REFRESH_TTL_MS → skip (\"skipped\"), so N\n   *    stacked detail requests on a live, actively-appended file cost one\n   *    parse per window rather than one each;\n   *  - otherwise parse (\"refreshed\").\n   *\n   * Deliberately NOT content-aware: skipping only when the file is unchanged\n   * would defeat the throttle exactly where it earns its keep, since a live\n   * rollout changes on every append.\n   */\n  refreshFileForRead(scanner: ConversationScanner, filePath: string): Promise<RefreshResult> {\n    const key = canonicalizeFilePath(filePath);\n    const state = this.refreshStateFor(scanner, key, filePath);\n    if (state.active) return withOutcome(\"joined\", state.active);\n    if (state.completedAt > 0 && Date.now() - state.completedAt < REFRESH_TTL_MS) {\n      return Promise.resolve({ outcome: \"skipped\", meta: null });\n    }\n    return withOutcome(\"refreshed\", this.startRefreshPass(state, key));\n  }\n\n  /**\n   * Post-write refresh, for a caller that knows the file just changed (the\n   * end of an agent turn, a user's input, a directory event).\n   *\n   * The guarantee: when the returned promise fulfils, a parse that STARTED\n   * after this call has completed. It cannot be thrown away by the read\n   * throttle, and it cannot be satisfied by an older parse — completion of a\n   * parse that began before the write is no proof it observed the write, and\n   * the scanner would hand exactly that parse to a direct caller.\n   *\n   * So: no pass running → parse now; a pass running → wait for a follow-up\n   * that starts when it settles. Every post-write request arriving during a\n   * pass shares that one follow-up, so a burst costs one extra parse, not one\n   * per caller. Requests that arrive after the follow-up has STARTED are not\n   * covered by it and get the next one.\n   *\n   * What it does not promise: that the writer's bytes have reached disk. This\n   * is \"everything visible in the file when this was called is indexed\", not\n   * a flush protocol — a waiting_input signal is not proof of a flush.\n   */\n  refreshFileAfterWrite(scanner: ConversationScanner, filePath: string): Promise<RefreshResult> {\n    const key = canonicalizeFilePath(filePath);\n    const state = this.refreshStateFor(scanner, key, filePath);\n    if (!state.active) return withOutcome(\"refreshed\", this.startRefreshPass(state, key));\n    if (state.queued) return withOutcome(\"joined\", state.queued.promise);\n    return withOutcome(\"refreshed\", this.queueRefreshPass(state).promise);\n  }\n\n  // The state for this file, rebuilt from scratch when the scanner it recorded\n  // work against is no longer the one being asked about. Any post-write pass\n  // the discarded state still owed is re-armed against the new scanner rather\n  // than left hanging: its callers asked for their write to be indexed, and\n  // the index that matters now is the new instance's.\n  private refreshStateFor(\n    scanner: ConversationScanner,\n    key: string,\n    filePath: string,\n  ): RefreshState {\n    const previous = this.refreshState.get(key);\n    if (previous && previous.scanner === scanner) return previous;\n    const state: RefreshState = {\n      scanner,\n      filePath,\n      active: null,\n      completedAt: 0,\n      queued: null,\n    };\n    this.refreshState.set(key, state);\n    const orphaned = previous?.queued;\n    if (orphaned) {\n      previous.queued = null;\n      orphaned.settle(this.startRefreshPass(state, key));\n    }\n    return state;\n  }\n\n  private startRefreshPass(state: RefreshState, key: string): RefreshPass {\n    const pass: RefreshPass = state.scanner.refreshFile(state.filePath).then(\n      (meta) => {\n        try {\n          if (meta) this.deps.cache()?.upsertFromScannerMeta([meta] as any[]);\n          else this.deps.cache()?.reconcileClassification(state.filePath);\n        } catch (err) {\n          this.log.warn(\"Failed to cache refreshed conversation classification\", { err });\n        }\n        this.finishRefreshPass(state, key, pass, true);\n        return meta;\n      },\n      (err) => {\n        this.finishRefreshPass(state, key, pass, false);\n        throw err;\n      },\n    );\n    state.active = pass;\n    return pass;\n  }\n\n  private queueRefreshPass(state: RefreshState): NonNullable<RefreshState[\"queued\"]> {\n    let settle!: (pass: RefreshPass) => void;\n    // Resolving with a promise adopts it, rejection included, so a queued\n    // caller sees exactly what its covering pass saw.\n    const promise = new Promise<ConversationMeta | null>((resolve) => {\n      settle = resolve;\n    });\n    state.queued = { promise, settle };\n    return state.queued;\n  }\n\n  private finishRefreshPass(\n    state: RefreshState,\n    key: string,\n    pass: RefreshPass,\n    fulfilled: boolean,\n  ): void {\n    // A pass whose state was replaced (or superseded) must not resurrect it or\n    // clobber the newer one's bookkeeping.\n    if (this.refreshState.get(key) !== state || state.active !== pass) return;\n    state.active = null;\n    if (fulfilled) state.completedAt = Date.now();\n    const queued = state.queued;\n    if (!queued) return;\n    state.queued = null;\n    // Started synchronously with the settle — not through a microtask hop —\n    // so nothing can slip into the gap and open a second parallel parse, and\n    // so a rejected pass still gets its follow-up attempted exactly once.\n    queued.settle(this.startRefreshPass(state, key));\n  }\n\n  // Let every outstanding pass finish before the scanners it indexes into are\n  // closed. Rounds, because a queued pass only starts once its predecessor\n  // settles; capped, because a caller appending during shutdown must not be\n  // able to hold close() open indefinitely.\n  private async drainRefreshPasses(): Promise<void> {\n    for (let round = 0; round < 4; round++) {\n      const outstanding: Promise<unknown>[] = [];\n      for (const state of this.refreshState.values()) {\n        if (state.active) outstanding.push(state.active.catch(() => undefined));\n        if (state.queued) outstanding.push(state.queued.promise.catch(() => undefined));\n      }\n      if (outstanding.length === 0) break;\n      await Promise.all(outstanding);\n    }\n    this.refreshState.clear();\n  }\n\n  // ─── scanner acquisition ──────────────────────────────────────────\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  async get(skipStaleRescan = false): Promise<ConversationScanner> {\n    // Detail path (#368): never block on an in-flight scan. The live scanner is\n    // kept readable across full rescans (shadow-and-swap in rescanForRefresh);\n    // mid-rebuild the previous generation is still correct for conversations\n    // that existed before the rescan started.\n    if (skipStaleRescan && this.scanner) {\n      return this.scanner;\n    }\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, reconcile now\n        // rather than serving a stale result — but per file, not by discarding\n        // the instance. refreshFile() re-indexes exactly the named JSONL and\n        // touches only that file's conversationLRU keys, so an append to one\n        // conversation can no longer evict every other conversation's parsed\n        // snapshot. Paths are drained first so events arriving during the\n        // refresh re-arm the flag and get their own pass.\n        if (this.scannerStale) {\n          const paths = this.takeStaleFiles();\n          // Armed with no paths (\"stale, source unknown\"): fall back to the\n          // full rebuild, which is what the flag meant before it carried\n          // identity.\n          if (paths.length === 0) {\n            this.scanner = null;\n            this.scannerReady = null;\n            return this.get();\n          }\n          await this.refreshStaleFiles(this.scanner, paths);\n          return this.scanner ?? this.get();\n        }\n        return this.scanner;\n      }\n    }\n    this.takeStaleFiles();\n    const statCache = this.buildStatCache(this.scanner);\n    // Scanner 0.9.4 reads statCache only in non-persistent scans, and the\n    // streamer never serves from the scanner's persistent index. Letting the\n    // no-statCache case fall through to the persistent default made an empty\n    // cache import rows for files deleted long ago (#876).\n    this.scanner = this.newScanner({ persistent: false });\n    this.allScanners.add(this.scanner);\n    this.scannerReady = this.scanner.scan({\n      ...(this.deps.scanProfiles ? { profiles: this.deps.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.get();\n    return scanner;\n  }\n\n  async getFresh(): Promise<ConversationScanner> {\n    this.scanner = null;\n    this.scannerReady = null;\n    return this.get();\n  }\n\n  // refresh=1's scan: build a SHADOW scanner with fullRescan:true, then swap\n  // it in atomically. The escape hatch bypasses the scanner's dir-mtime\n  // discovery gate (an explicit user pull-to-refresh is exactly the \"don't\n  // trust the gate, check disk for real\" signal).\n  //\n  // Scanning in place would clear the non-persistent scanner's metadataCache\n  // at start, so a concurrent detail fetch that skipped the await would 404 a\n  // conversation that exists — and one that awaited would pay the full scan's\n  // wall clock (#368). Shadow-and-swap keeps this.scanner readable as the\n  // previous generation for the whole rebuild.\n  async rescanForRefresh(\n    onProgress?: (scanned: number, total: number) => void,\n  ): 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, per-file paths included.\n    this.takeStaleFiles();\n    const previous = this.scanner;\n    const statCache = this.buildStatCache(previous);\n    const shadow = this.newScanner({ persistent: false });\n    this.allScanners.add(shadow);\n    this.scannerReady = shadow.scan({\n      ...(this.deps.scanProfiles ? { profiles: this.deps.scanProfiles } : {}),\n      ...this.codexScanOpts(),\n      fullRescan: true,\n      ...(statCache ? { statCache } : {}),\n      ...(onProgress ? { onProgress } : {}),\n    });\n    try {\n      await this.scannerReady;\n    } catch (err) {\n      // Leave this.scanner as the previous generation; clear the failed promise\n      // so the next caller retries rather than replaying the rejection.\n      this.scannerReady = null;\n      throw err;\n    }\n    this.scanner = shadow;\n    return shadow;\n  }\n\n  // ─── cache ↔ disk reconcile ───────────────────────────────────────\n\n  /**\n   * Full-glob scan + cache upsert/delete reconcile. Used by ?refresh=1 and by\n   * the automatic freshness path when the directory watcher marked the scanner\n   * stale or shouldRefreshProjectsFromHdd detected disk drift.\n   */\n  async reconcileFromDisk(onProgress?: (scanned: number, total: number) => void): Promise<void> {\n    const cache = this.deps.cache();\n    if (!cache) return;\n    // No warm-up gate here: the caller decides. The cold-start path wraps this\n    // in withWarmup (nothing to serve, so 503 while building is correct); the\n    // routine background path must NOT gate, or every JSONL append flips the\n    // server into SERVER_WARMING_UP and the client flickers.\n    const scanner = await this.rescanForRefresh(onProgress);\n    const metas = [...scanner.getMetadataCache().values()];\n    try {\n      cache.upsertFromScannerMeta(metas as any[]);\n      // Additions/updates are always safe. But while a cache-integrity alert\n      // is pending, freeze the removal half — reconcileDeletions must not\n      // drop rows until a human resolves the alert.\n      if (!this.deps.cacheMonitor()?.pending) {\n        // Canonical, because reconcileDeletions compares against\n        // conversation_meta.file_path while the scanner's filePath is native.\n        cache.reconcileDeletions(canonicalLivePathSet(metas));\n      }\n      const projectsRepo = this.deps.projectsRepo();\n      const conversationsRepo = this.deps.conversationsRepo();\n      const cacheMetadataRepo = this.deps.cacheMetadataRepo();\n      if (projectsRepo && conversationsRepo && cacheMetadataRepo) {\n        refreshConversationCache({\n          cache,\n          projectsRepo,\n          conversationsRepo,\n          cacheMetadataRepo,\n        });\n      } else if (cacheMetadataRepo) {\n        setCacheMetadata(\n          cacheMetadataRepo,\n          \"conversations_last_indexed_at\",\n          new Date().toISOString(),\n        );\n      }\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  // Reconcile the cache from disk without blocking the caller. Single-flighted\n  // so a burst of list polls during active session writes shares one rescan\n  // rather than queueing a full rescan each; tracked so close() awaits the\n  // in-flight cache write before shutting the DB.\n  startBackgroundReconcile(mode: ConversationReconcileMode = \"full\"): void {\n    if (this.reconcileInFlight) return;\n    if (mode === \"full\") {\n      const now = Date.now();\n      if (now - this.lastAutoFullReconcileAt < ScannerManager.AUTO_FULL_RECONCILE_COOLDOWN_MS) {\n        return;\n      }\n      this.lastAutoFullReconcileAt = now;\n    }\n    const paths = mode === \"files\" ? this.takeStaleFiles() : [];\n    const task = (\n      paths.length > 0 ? this.reconcileStaleFilesFromDisk(paths) : this.reconcileFromDisk()\n    ).finally(() => {\n      this.reconcileInFlight = null;\n    });\n    this.reconcileInFlight = task;\n    this.deps.trackCacheWrite(task);\n  }\n\n  // \"files\": a directory event named specific JSONLs, so refresh only those.\n  // \"full\": disk drifted in ways a per-file refresh can't see (a project dir\n  // appeared, rows vanished), so walk the tree. Order matters — the staleness\n  // check short-circuits first so the HDD freshness probe stays off the hot\n  // poll path, exactly as it did when this returned a boolean.\n  reconcileMode(): ConversationReconcileMode | null {\n    if (!this.deps.cache()) return null;\n    if (this.scannerStale) return \"files\";\n    const conversationsRepo = this.deps.conversationsRepo();\n    const cacheMetadataRepo = this.deps.cacheMetadataRepo();\n    if (!conversationsRepo || !cacheMetadataRepo) return null;\n    return shouldRefreshProjectsFromHdd(conversationsRepo, cacheMetadataRepo, {\n      projectsDirs: this.projectsDirs(),\n    })\n      ? \"full\"\n      : null;\n  }\n\n  // The per-file half of reconcileFromDisk: re-index just the changed JSONLs\n  // and upsert their rows. No reconcileDeletions here — that needs the whole\n  // live-path set, and deletions already have their own path (onFileDeleted ->\n  // invalidateByFilePath). New projects still arrive via the HDD-freshness\n  // \"full\" mode.\n  private async reconcileStaleFilesFromDisk(paths: string[]): Promise<void> {\n    const cache = this.deps.cache();\n    if (!cache) return;\n    const scanner = await this.get(true);\n    const metas = await this.refreshStaleFiles(scanner, paths);\n    if (metas.length === 0) return;\n    try {\n      cache.upsertFromScannerMeta(metas as any[]);\n    } catch (err) {\n      this.log.warn(\n        `stale-file reconcile failed: ${err instanceof Error ? err.message : String(err)}`,\n        { event: \"conversations.reconcile_failed\" },\n      );\n    }\n  }\n\n  // ─── teardown ─────────────────────────────────────────────────────\n\n  async close(): Promise<void> {\n    this.markStaleDebounced.cancel();\n    await this.drainRefreshPasses();\n    await Promise.all([...this.allScanners].map((s) => s.close()));\n    this.allScanners.clear();\n    this.scanner = null;\n  }\n}\n","import type {\n  CacheMetadataKey,\n  CacheMetadataRepository,\n} from \"../../db/repositories/cacheMetadata.repository\";\n\n/**\n * Functional accessors over the cache_metadata table. Repositories give us\n * the SQL surface; these helpers give us the verbs the services use.\n */\nexport function getCacheMetadata(\n  repo: CacheMetadataRepository,\n  key: CacheMetadataKey,\n): string | null {\n  return repo.getCacheMetadata(key);\n}\n\nexport function setCacheMetadata(\n  repo: CacheMetadataRepository,\n  key: CacheMetadataKey,\n  value: string,\n): void {\n  repo.setCacheMetadata(key, value);\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 type { ProjectsRepository } from \"../../db/repositories/projects.repository\";\nimport { canonicalizeProjectPath } from \"../../utils/canonicalizeProjectPath\";\nimport { compareIsoDesc } from \"../../utils/dates\";\n\nexport interface ConversationLikeForProjects {\n  id: string;\n  projectPath: string | null | undefined;\n  createdAt?: string | null;\n  latestMessageAt?: string | null;\n}\n\n/**\n * Group cached conversations by their canonical project path, find the\n * latest conversation per project, then upsert one project row per unique\n * path. Returns a map from canonical path → projectId so callers can\n * backfill conversation/session project_id columns afterwards.\n */\nexport function ensureProjectsForConversations(\n  repo: ProjectsRepository,\n  conversations: ConversationLikeForProjects[],\n): Map<string, string> {\n  const conversationsByPath = new Map<string, ConversationLikeForProjects[]>();\n\n  for (const conversation of conversations) {\n    if (!conversation.projectPath) continue;\n    const canonical = canonicalizeProjectPath(conversation.projectPath);\n    if (!canonical) continue;\n    const existing = conversationsByPath.get(canonical) ?? [];\n    existing.push(conversation);\n    conversationsByPath.set(canonical, existing);\n  }\n\n  const pathToProjectId = new Map<string, string>();\n\n  for (const [path, projectConversations] of conversationsByPath) {\n    const latest = pickLatestConversation(projectConversations);\n\n    const project = repo.upsertProjectByPath(path, {\n      lastConversationId: latest?.id ?? null,\n      lastConversationCreatedAt: latest?.createdAt ?? null,\n      latestMessageAt: latest?.latestMessageAt ?? null,\n    });\n\n    pathToProjectId.set(path, project.id);\n  }\n\n  return pathToProjectId;\n}\n\nfunction pickLatestConversation(\n  conversations: ConversationLikeForProjects[],\n): ConversationLikeForProjects | undefined {\n  if (conversations.length === 0) return undefined;\n  return [...conversations].sort((a, b) => {\n    const cmp = compareIsoDesc(a.latestMessageAt ?? null, b.latestMessageAt ?? null);\n    if (cmp !== 0) return cmp;\n    return compareIsoDesc(a.createdAt ?? null, b.createdAt ?? null);\n  })[0];\n}\n","import type { ConversationCache } from \"../../conversation-cache\";\nimport type { CacheMetadataRepository } from \"../../db/repositories/cacheMetadata.repository\";\nimport type { ConversationsRepository } from \"../../db/repositories/conversations.repository\";\nimport type { ProjectsRepository } from \"../../db/repositories/projects.repository\";\nimport { canonicalizeProjectPath } from \"../../utils/canonicalizeProjectPath\";\nimport { setCacheMetadata } from \"../cache/cacheMetadata\";\nimport { ensureProjectsForConversations } from \"../projects/ensureProjectsForConversations\";\n\nexport interface RefreshConversationCacheDeps {\n  cache: ConversationCache;\n  projectsRepo: ProjectsRepository;\n  conversationsRepo: ConversationsRepository;\n  cacheMetadataRepo: CacheMetadataRepository;\n}\n\nexport interface RefreshConversationCacheResult {\n  projectsTouched: number;\n  conversationsBackfilled: number;\n  latestConversationId: string | null;\n}\n\n/**\n * After a scanner-driven cache rebuild has run, walk the cache to:\n *   1. Upsert one project row per unique canonical project_path.\n *   2. Backfill conversation_meta.project_id for any rows missing it.\n *   3. Update cache_metadata.last_conversation_id so subsequent freshness\n *      checks can short-circuit.\n *\n * The scanner itself runs in `server.ts` today; this function picks up\n * after it has populated `conversation_meta`.\n */\nexport function refreshConversationCache(\n  deps: RefreshConversationCacheDeps,\n): RefreshConversationCacheResult {\n  const { projectsRepo, conversationsRepo, cacheMetadataRepo } = deps;\n\n  const conversations = conversationsRepo.listConversationsForProjectBackfill();\n\n  const pathToProjectId = ensureProjectsForConversations(\n    projectsRepo,\n    conversations.map((c) => ({\n      id: c.id,\n      projectPath: c.projectPath,\n      latestMessageAt: c.lastActivity ?? null,\n      createdAt: c.lastActivity ?? null,\n    })),\n  );\n\n  let conversationsBackfilled = 0;\n  for (const conversation of conversations) {\n    if (!conversation.projectPath) continue;\n    if (conversation.projectId) continue;\n    const projectId = pathToProjectId.get(canonicalizeProjectPath(conversation.projectPath));\n    if (!projectId) continue;\n    conversationsRepo.updateConversationProjectId({\n      conversationId: conversation.id,\n      projectId,\n    });\n    conversationsBackfilled += 1;\n  }\n\n  const latest = conversationsRepo.getLatestConversation();\n  if (latest) {\n    setCacheMetadata(cacheMetadataRepo, \"last_conversation_id\", latest.id);\n    if (latest.lastActivity) {\n      setCacheMetadata(cacheMetadataRepo, \"last_conversation_created_at\", latest.lastActivity);\n    }\n  }\n  setCacheMetadata(cacheMetadataRepo, \"conversations_last_indexed_at\", new Date().toISOString());\n\n  return {\n    projectsTouched: pathToProjectId.size,\n    conversationsBackfilled,\n    latestConversationId: latest?.id ?? null,\n  };\n}\n","import { readdirSync, statSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\nimport type { CacheMetadataRepository } from \"../../db/repositories/cacheMetadata.repository\";\nimport type { ConversationsRepository } from \"../../db/repositories/conversations.repository\";\nimport { getCacheMetadata } from \"../cache/cacheMetadata\";\n\nconst DEFAULT_PROJECTS_DIR = join(homedir(), \".claude\", \"projects\");\n\nexport interface ShouldRefreshOptions {\n  /** Override the disk path checked for drift. Defaults to ~/.claude/projects. */\n  projectsDir?: string;\n  /** Additional project roots (e.g. per-profile `configDir/projects`). */\n  projectsDirs?: string[];\n}\n\n/**\n * Newest mtime across a projects root and its immediate child directories.\n *\n * POSIX directory mtime updates when direct entries are added/removed/renamed,\n * not when a file inside a child is appended. Checking one level of children\n * catches new JSONLs in an existing project without stating every file.\n */\nexport function maxProjectsTreeMtimeMs(projectsDir: string): number | null {\n  let maxMs: number;\n  try {\n    maxMs = statSync(projectsDir).mtimeMs;\n  } catch {\n    return null;\n  }\n\n  try {\n    for (const ent of readdirSync(projectsDir, { withFileTypes: true })) {\n      if (!ent.isDirectory()) continue;\n      try {\n        const childMs = statSync(join(projectsDir, ent.name)).mtimeMs;\n        if (childMs > maxMs) maxMs = childMs;\n      } catch {\n        // race: directory removed between readdir and stat\n      }\n    }\n  } catch {\n    // projectsDir unreadable after the root stat — keep root mtime\n  }\n\n  return maxMs;\n}\n\n/**\n * Decide whether the projects/conversations cache needs a refresh.\n *\n * Returns true when either:\n *   1. The cache has orphan rows — conversations with a project_path but no\n *      project_id. These need a backfill pass to become visible to\n *      /project-chats (which filters out null project_id).\n *   2. A watched projects tree on disk has changed since the last scanner pass —\n *      max(root mtime, child-dir mtimes) is newer than\n *      cache_metadata.conversations_last_indexed_at. This catches new JSONLs\n *      that the file watcher missed (including under existing project dirs).\n *\n * Appends to existing JSONLs still rely on the directory watcher flipping\n * scannerStale; this mtime gate does not see in-place file growth.\n */\nexport function shouldRefreshProjectsFromHdd(\n  conversationsRepo: ConversationsRepository,\n  cacheMetadataRepo: CacheMetadataRepository,\n  opts: ShouldRefreshOptions = {},\n): boolean {\n  if (conversationsRepo.hasOrphanRows()) return true;\n\n  const dirs = new Set<string>();\n  if (opts.projectsDirs) {\n    for (const d of opts.projectsDirs) dirs.add(d);\n  }\n  dirs.add(opts.projectsDir ?? DEFAULT_PROJECTS_DIR);\n\n  let newestMs: number | null = null;\n  for (const dir of dirs) {\n    const ms = maxProjectsTreeMtimeMs(dir);\n    if (ms === null) continue;\n    if (newestMs === null || ms > newestMs) newestMs = ms;\n  }\n  if (newestMs === null) return false;\n\n  const lastIndexedIso = getCacheMetadata(cacheMetadataRepo, \"conversations_last_indexed_at\");\n  if (!lastIndexedIso) return true;\n\n  const lastIndexedMs = Date.parse(lastIndexedIso);\n  if (Number.isNaN(lastIndexedMs)) return true;\n\n  return newestMs > lastIndexedMs;\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","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 type { EventEmitter } from \"events\";\nimport { statSync } from \"fs\";\nimport type { IncomingMessage, ServerResponse } from \"http\";\nimport type { WebSocket } from \"ws\";\nimport type { AgentClient } from \"./agent/agent-client\";\nimport type { AgentConfig } from \"./agent/agent-config\";\nimport type { ConversationWriter } from \"./agent/conversation-writer\";\nimport type { ConversationHandlers } from \"./api/handlers/conversations.handlers\";\nimport type { SessionHandlers } from \"./api/handlers/sessions.handlers\";\nimport type { ApiDeps } from \"./api/types/api-deps\";\nimport { ConversationCache } from \"./conversation-cache\";\nimport type { CacheMetadataRepository } from \"./db/repositories/cacheMetadata.repository\";\nimport type { ConversationsRepository } from \"./db/repositories/conversations.repository\";\nimport type { DevicesRepository } from \"./db/repositories/devices.repository\";\nimport type { ManagedSessionsRepository } from \"./db/repositories/managed-sessions.repository\";\nimport type { ProjectsRepository } from \"./db/repositories/projects.repository\";\nimport type { PushRepository } from \"./db/repositories/push.repository\";\nimport type { SessionsRepository } from \"./db/repositories/sessions.repository\";\nimport type { RuntimeStore } from \"./db/runtime-store\";\nimport type { ExternalTailManager } from \"./external-tails\";\nimport { handleListProjects } from \"./handlers/handleListProjects\";\nimport type { LiveSessionManager } from \"./live-session-manager\";\nimport { getLogger, type Logger } from \"./logger\";\nimport { PTY_COLS, PTY_ROWS, REPLAY_MAX_LINES } from \"./pty-shared\";\nimport type { ScannerManager } from \"./scanner-manager\";\nimport type { Prompt } from \"./schemas/prompt.schema\";\nimport type { CacheIntegrityMonitor } from \"./services/cache-integrity/cacheIntegrityMonitor\";\nimport type {\n  ConversationWatcher,\n  ConversationWatcherEvents,\n} from \"./services/conversations/conversationWatcher\";\nimport type { HostPressureMonitor } from \"./services/host-pressure/hostPressure\";\nimport type { PromptRegistry } from \"./services/prompts/promptRegistry\";\nimport type { LiveActivityNotifier } from \"./services/push/liveActivityNotifier\";\nimport type { WaitingInputNotifier } from \"./services/push/waitingInputNotifier\";\nimport { permissionGateKey } from \"./services/questions/detectPermissionGate\";\nimport { type Capability, hasCapability, type Principal } from \"./services/security/capabilities\";\nimport type { ReconcileVerdict } from \"./services/sessions/reconcileSessions\";\nimport type { SessionStore } from \"./session-store\";\nimport type {\n  AskQuestion,\n  PermissionOption,\n  PTYManagerOptions,\n  ServerWarmupState,\n  SessionResponse,\n} from \"./types\";\nimport type { WSHub } from \"./ws-hub\";\n\n/**\n * Whether a socket's principal may send a given frame.\n *\n * A null principal means authMiddleware never set one, which for `/ws` — a\n * route it classifies as `history:read` — happens only on a bypass path. Today\n * that is `--local-no-auth`, which grants unauthenticated full access to\n * loopback callers by design, so allowing it here keeps the socket telling the\n * same story as the HTTP routes rather than being stricter than them.\n *\n * Deliberately local rather than exported: it is a fail-OPEN helper, correct\n * only where the caller has already established that a null principal means a\n * deliberate bypass.\n */\nfunction wsAllows(principal: Principal | null, required: Capability): boolean {\n  return principal === null || hasCapability(principal, required);\n}\n\n/** The permission gate currently open for a session (scraped via OSC 777). */\nexport type PendingPermission = {\n  prompt?: string;\n  detail?: string;\n  options: PermissionOption[];\n  cursor?: number;\n  /** Server-owned instance id, minted by handlePermissionChange. */\n  gateId: string;\n  promptId?: string;\n  /**\n   * The pty-host's occurrence id for this gate, absent on the in-process PTY\n   * path. Instance identity is compared against THIS, never against promptId:\n   * the two were equal by construction until open() started minting a fresh id\n   * for a replayed occurrence held by a terminal record.\n   */\n  occurrenceId?: string;\n};\n\n/** The AskUserQuestion card currently broadcast for a session. */\nexport type PendingQuestion = {\n  toolUseId: string;\n  questions: AskQuestion[];\n  origin: \"pty\" | \"jsonl\";\n  promptId: string;\n};\n\nexport type ExpiredPendingPromptDeps = {\n  pendingPermission: Map<string, PendingPermission>;\n  pendingPermissionKey: Map<string, string>;\n  pendingQuestions: Map<string, PendingQuestion>;\n  pendingQuestionKey: Map<string, string>;\n  sessionSubscribers: Map<string, Set<WebSocket>>;\n  wsHub: Pick<WSHub, \"broadcastToClients\">;\n};\n\nexport function clearExpiredPendingPrompt(deps: ExpiredPendingPromptDeps, prompt: Prompt): void {\n  const permission = deps.pendingPermission.get(prompt.sessionId);\n  if (permission?.promptId === prompt.promptId) {\n    deps.pendingPermission.delete(prompt.sessionId);\n    deps.pendingPermissionKey.delete(prompt.sessionId);\n    deps.wsHub.broadcastToClients(deps.sessionSubscribers.get(prompt.sessionId) ?? [], {\n      type: \"permission_cancelled\",\n      sessionId: prompt.sessionId,\n    });\n  }\n\n  const question = deps.pendingQuestions.get(prompt.sessionId);\n  if (question?.promptId === prompt.promptId) {\n    deps.pendingQuestions.delete(prompt.sessionId);\n    deps.pendingQuestionKey.delete(prompt.sessionId);\n    deps.wsHub.broadcastToClients(deps.sessionSubscribers.get(prompt.sessionId) ?? [], {\n      type: \"question_cancelled\",\n      sessionId: prompt.sessionId,\n      toolUseId: question.toolUseId,\n    });\n  }\n}\n\n/**\n * Everything the ConversationWatcher callbacks read from the server. Thunks\n * rather than values for anything constructed after the watcher itself\n * (`externalTailManager`, and `fileWatcher` — which IS the watcher these\n * callbacks are handed to) or swapped on the instance by tests (`log`,\n * `broadcastConversationLines`); the same reason ScannerManagerDeps passes\n * `cache: () => ConversationCache | null`.\n *\n * The Maps are the server's own, held by reference: they stay StreamerServer\n * state and are read directly elsewhere in server.ts.\n */\nexport type ConversationWatcherWiringDeps = {\n  sessionFileMap: Map<string, string>;\n  pendingLineSeqs: Map<string, (number | null)[]>;\n  scannerManager: ScannerManager;\n  cache: () => ConversationCache | null;\n  log: () => Logger;\n  fileWatcher: () => ConversationWatcher;\n  externalTailManager: () => ExternalTailManager;\n  trackCacheWrite: (task: Promise<unknown>) => void;\n  processJsonlQuestions: (sessionId: string, lines: string[]) => void;\n  broadcastConversationLines: (\n    sessionId: string,\n    lines: string[],\n    seqs?: (number | null)[] | null,\n  ) => void;\n};\n\n/**\n * The JSONL tail/offset-index/directory callbacks StreamerServer hands to its\n * ConversationWatcher.\n *\n * Extracted from the constructor so watcher work stops editing the server file\n * (see docs/plans/2026-07-12-server-ts-split.md, PR 7). State stays on the\n * server: these callbacks only reach it through `deps`.\n */\nexport function createConversationWatcherEvents(\n  deps: ConversationWatcherWiringDeps,\n): ConversationWatcherEvents {\n  return {\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      const cache = deps.cache();\n      if (!cache) return;\n      // No stale seqs from a prior read may leak into this one's WS stamping.\n      deps.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          deps.trackCacheWrite(\n            cache.backfillIndex(filePath).catch((err) => {\n              deps.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        deps.pendingLineSeqs.set(filePath, seqs);\n      } catch (err) {\n        deps.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      deps.cache()?.updateFromLines(filePath, lines);\n      let managed = false;\n      for (const [sessionId, watchedPath] of deps.sessionFileMap) {\n        if (watchedPath === filePath) {\n          managed = true;\n          deps.processJsonlQuestions(sessionId, lines);\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 = deps.pendingLineSeqs.get(filePath);\n          deps.broadcastConversationLines(sessionId, lines, seqs);\n          break;\n        }\n      }\n      // No PTY owns this file: it's an external tail (or nothing at all, in\n      // which case the call is a no-op). Transcript push only — never the\n      // managed-session events, and never a question card.\n      if (!managed) {\n        deps\n          .externalTailManager()\n          .broadcastExternalTailLines(filePath, lines, deps.pendingLineSeqs.get(filePath));\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      deps.pendingLineSeqs.delete(filePath);\n    },\n    onConversationChanged: (filePath) => {\n      // Directory unlink is the survivor when the per-file watcher's unlink\n      // is dropped (delete inside a write-finish window, or a dead handle).\n      // Detect gone-on-disk here and take the same detach + invalidate path\n      // as onFileDeleted — otherwise an external tail stays attached until\n      // the 5 min idle sweep (#393).\n      try {\n        statSync(filePath);\n      } catch {\n        deps.externalTailManager().handleJsonlDeleted(filePath);\n        return;\n      }\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      const tailed = deps.fileWatcher().poke(filePath);\n      // Nobody is tailing it yet — if an external agent is actively writing\n      // it, attach a tail so its transcript is pushed instead of silently\n      // waiting for the client to poll.\n      if (!tailed) deps.externalTailManager().maybeAttachExternalTail(filePath);\n      deps.externalTailManager().sweepIdleExternalTails();\n      // Upsert-or-leave: a change event NEVER deletes the cache row (skipIfTailed).\n      // This same append also drives the live-tail watcher's updateFromLines\n      // upsert; the two fire with no ordering guarantee, so deleting here would\n      // wipe a row the live tail just wrote (CRITICAL #2) — and a refresh-created\n      // untailed row would vanish on its next append. The debounced rescan below\n      // re-derives metadata, so leaving the row loses nothing.\n      deps.cache()?.invalidateByFilePath(filePath, { skipIfTailed: true });\n      // Debounce the scanner-staleness flip so a burst of directory events\n      // during active sessions collapses into one reconcile trigger after a\n      // quiet period. The debounced callback still checks scannerReady at\n      // fire time, preserving the anti-infinite-loop rule (never null\n      // scannerReady mid-scan). Record the path before debouncing so the\n      // reconcile can refresh exactly this file instead of the whole tree.\n      deps.scannerManager.staleFiles.add(filePath);\n      deps.scannerManager.markStaleDebounced();\n      deps.log().debug?.(`Scanner invalidated by directory event: ${filePath}`, {\n        filePath,\n        event: \"cache.directory_change\",\n      });\n    },\n    onTruncated: (filePath) => {\n      deps.cache()?.reconcileClassification(filePath);\n      // The file shrank below our offset — it is a different generation of\n      // content now, so every byte span we recorded for it is meaningless.\n      // Drop the index (and its parse state); the next read rebuilds from 0.\n      deps.cache()?.deleteFileIndex(filePath, ConversationCache.conversationIdForFile(filePath));\n      deps.cache()?.clearIndexParseState(filePath);\n      deps.log().warn(`JSONL truncated/replaced; offset index dropped: ${filePath}`, {\n        filePath,\n        event: \"tail.truncated\",\n      });\n    },\n    onFileDeleted: (filePath) => deps.externalTailManager().handleJsonlDeleted(filePath),\n    onError: (filePath, err) => {\n      // Was unwired, so every watcher error was dropped on the floor. The one\n      // that matters is ENOSPC from a directory watch: chokidar takes one\n      // OS-level watch handle PER FILE under a watched root (see\n      // watchDirectory's note), so on Linux the conversation corpus is spent\n      // directly against inotify's per-user max_user_watches — a ceiling as\n      // low as 8192 on some distros, shared with every other watcher the user\n      // is running. Past it the watch never attaches: tails go quiet and new\n      // conversations stop being discovered, with nothing in the log tying it\n      // to the fd budget. Name the cause here so it isn't re-derived.\n      const enospc = (err as NodeJS.ErrnoException).code === \"ENOSPC\";\n      deps\n        .log()\n        .error(\n          enospc\n            ? `Watcher hit the OS watch-handle limit on ${filePath} — raise fs.inotify.max_user_watches (Linux) or the process fd limit; conversation discovery and live tails are degraded until then`\n            : `Watcher error on ${filePath}: ${err.message}`,\n          { filePath, err, event: enospc ? \"watcher.limit_exhausted\" : \"watcher.error\" },\n        );\n    },\n  };\n}\n\n/**\n * Everything the LiveSessionManager callbacks read from the server. Same thunk\n * discipline: `sessionHandlers` is constructed after the runner, the repos and\n * notifiers are bound during listen(), and `log` is swapped by tests.\n */\nexport type LiveSessionWiringDeps = {\n  sessionGeometry: Map<string, { cols: number; rows: number }>;\n  sessionStore: SessionStore;\n  wsHub: WSHub;\n  fileWatcher: ConversationWatcher;\n  scannerManager: ScannerManager;\n  sessionStatusBus: EventEmitter;\n  sessionFileMap: Map<string, string>;\n  sessionSubscribers: Map<string, Set<WebSocket>>;\n  lastAgentChunkAt: Map<string, number>;\n  terminalSeq: Map<string, number>;\n  pendingQuestions: Map<string, PendingQuestion>;\n  pendingQuestionKey: Map<string, string>;\n  pendingPermission: Map<string, PendingPermission>;\n  pendingPermissionKey: Map<string, string>;\n  promptRegistry: PromptRegistry;\n  contendedSessions: Set<string>;\n  log: () => Logger;\n  sessionHandlers: () => SessionHandlers;\n  managedSessionsRepo: () => ManagedSessionsRepository | null;\n  liveActivityNotifier: () => LiveActivityNotifier | null;\n  waitingInputNotifier: () => WaitingInputNotifier | null;\n  ptyAttachedIds: () => Set<string>;\n  cancelPendingQuestion: (sessionId: string) => void;\n  rememberSelfPtyEnded: (conversationId: string) => void;\n  maybeFireHoldWhenIdle: (session: { id: string; status: string }) => void;\n};\n\n/**\n * The options StreamerServer hands to its LiveSessionManager: terminal/user\n * output fan-out, gate and question plumbing, and the status funnel that\n * mirrors every transition into SessionStore, the durable registry, the\n * scanner index and the push notifiers.\n *\n * Extracted from the constructor for the same reason as the watcher events\n * above; the server keeps every Map and Set these callbacks mutate.\n */\nexport function createLiveSessionOptions(deps: LiveSessionWiringDeps): PTYManagerOptions {\n  // The agent stopped mid-turn for the user. Fire-and-forget, like the status\n  // notifiers below; the notifier dedupes repaints of one prompt itself.\n  const notifyPrompt = (sessionId: string, kind: \"permission\" | \"question\", open: boolean) => {\n    const notifier = deps.waitingInputNotifier();\n    const session = notifier && deps.sessionStore.get(sessionId, deps.ptyAttachedIds());\n    if (session) void notifier.onPrompt(session, kind, open);\n  };\n  return {\n    logger: getLogger(\"pty\"),\n    onOutput: (sessionId, data) => {\n      // Agent-activity stamp for the idle reaper. Fires for every provider\n      // (both runners call onOutput), so the reaper needs no per-runner\n      // bookkeeping. Distinct from ManagedSession.lastActivityAt, which only\n      // moves on *user* input — an agent grinding through a long task is\n      // active even when nobody has touched it.\n      deps.lastAgentChunkAt.set(sessionId, Date.now());\n      const seq = (deps.terminalSeq.get(sessionId) ?? 0) + 1;\n      deps.terminalSeq.set(sessionId, seq);\n      deps.wsHub.broadcastToClients(deps.sessionSubscribers.get(sessionId) ?? [], {\n        type: \"terminal_output\",\n        sessionId,\n        data,\n        seq,\n      });\n    },\n    onPhaseChange: (sessionId, phase) => {\n      // Mirror into SessionStore so the REST field carries it too. The runner\n      // mutates its own InternalSession and toPublicSession does not forward\n      // subStatus, so without this `GET /api/sessions/:id` reports null for the\n      // whole turn and only a subscribed socket ever sees a phase.\n      deps.sessionStore.updateManaged(sessionId, { subStatus: phase });\n      // Scoped to this session's subscribers and sent as a minimal frame —\n      // NOT routed through onStatusChange's handler, which writes a DB row,\n      // refreshes the scanner index, broadcasts globally and pokes the APNs\n      // and push notifiers on every call. This can fire every scrape tick.\n      deps.wsHub.broadcastToClients(deps.sessionSubscribers.get(sessionId) ?? [], {\n        type: \"session_phase\",\n        sessionId,\n        phase,\n        updatedAt: new Date().toISOString(),\n      });\n    },\n    onPromptSuggestionChange: (sessionId, text) => {\n      // Same shape as onPhaseChange above: mirror into the store so REST carries\n      // it, then a minimal frame to this session's subscribers only.\n      deps.sessionStore.updateManaged(sessionId, { promptSuggestion: text });\n      deps.wsHub.broadcastToClients(deps.sessionSubscribers.get(sessionId) ?? [], {\n        type: \"prompt_suggestion\",\n        sessionId,\n        text,\n        updatedAt: new Date().toISOString(),\n      });\n    },\n    onUserMessage: (sessionId, text, ts) => {\n      deps.wsHub.broadcastToClients(deps.sessionSubscribers.get(sessionId) ?? [], {\n        type: \"user_message\",\n        sessionId,\n        text,\n        ts,\n      });\n    },\n    onPermissionChange: (sessionId, gate, occurrenceId) => {\n      deps.sessionHandlers().handlePermissionChange(sessionId, gate, occurrenceId);\n      notifyPrompt(sessionId, \"permission\", gate !== null);\n    },\n    onLiveQuestion: (sessionId, questions, occurrenceId) => {\n      deps.sessionHandlers().handleLiveQuestion(sessionId, questions, occurrenceId);\n      notifyPrompt(sessionId, \"question\", true);\n    },\n    onLiveQuestionGone: (sessionId) => {\n      notifyPrompt(sessionId, \"question\", false);\n      // The rendered AskUserQuestion menu closed on this streamer's own live\n      // PTY — authoritative regardless of whether pendingQuestions still holds\n      // the screen-synthesized id or the real toolUseId a JSONL flush swapped\n      // in later. Gating on the \"screen:\" prefix (as this used to) assumed the\n      // JSONL id meant handleSendAnswer had already cleared it, which only\n      // holds when the menu closed BECAUSE we answered it — not when it closed\n      // via Esc, an answer typed at the host keyboard, /clear, or the model\n      // giving up, all of which leave pendingQuestions holding the real id.\n      deps.pendingQuestionKey.delete(sessionId);\n      deps.cancelPendingQuestion(sessionId);\n    },\n    onReady: (session) => {\n      const resp = deps.sessionStore.get(session.id, deps.ptyAttachedIds());\n      if (resp) deps.wsHub.broadcast({ type: \"session_ready\", session: resp });\n    },\n    onStatusChange: (session) => {\n      // Captured before the update below overwrites it — the Live Activity\n      // notifier needs the pre-transition status to tell a genuine\n      // waiting_input↔running edge apart from a same-status re-emit.\n      const previousStatus = deps.sessionStore.getManaged(session.id)?.status;\n      deps.sessionStore.updateManaged(session.id, {\n        status: session.status,\n        completedAt: session.completedAt,\n        ...(session.lastActivityAt != null && { lastActivityAt: session.lastActivityAt }),\n        // The runner derives this from the first user message, on its own\n        // copy of the session. Without mirroring it here SessionStore never\n        // learns it, so a fresh live session is served with no sessionName\n        // even though the registry has one — the name only appeared after a\n        // restart rebuilt the row as a stub. Guarded so a runner that has not\n        // derived one yet cannot blank a name set by enrichResumedSessionAsync.\n        ...(session.sessionName != null && { sessionName: session.sessionName }),\n        // Without mirroring this, SessionStore's copy is frozen at whatever\n        // `addManaged` saw at spawn (\"spawn\") forever, because updateManaged\n        // is a partial merge — so managedToResponse can never tell a\n        // grace-timer/idle-reaper hold (statusSource \"shutdown\") apart from a\n        // genuine process exit (\"process-exit\"), and reports both as\n        // `lifecycle: \"completed\"`. See managedToResponse in session-store.ts.\n        ...(session.statusSource != null && { statusSource: session.statusSource }),\n        // Why a session died, not just that it did. Without this the store's\n        // copy has no failureReason, so managedToResponse falls through to\n        // `lifecycle: \"completed\"` (see session-store.ts) and a session that\n        // never started — missing CLI, missing project dir — is reported to\n        // every client as one that finished normally. Guarded like its\n        // neighbours: a later transition must not blank a recorded failure.\n        ...(session.failureReason != null && { failureReason: session.failureReason }),\n        ...(session.failureCode != null && { failureCode: session.failureCode }),\n      });\n      // Mirror the transition into the durable registry. Both runners funnel\n      // every status change through this callback, so this is the one place\n      // that needs to know. `exit` vs `transition` is the distinction the\n      // reconciler cares about: a row with a recorded exit needs no probe.\n      deps\n        .managedSessionsRepo()\n        ?.recordStatus(\n          session.id,\n          session.status,\n          session.completedAt != null ? \"exit\" : \"transition\",\n          {\n            completedAt: session.completedAt,\n            lastActivityAt: session.lastActivityAt ?? null,\n            promptCount: session.promptCount,\n            failureReason: session.failureReason ?? null,\n            // Derived from the first user message, so it does not exist yet at\n            // recordSpawn. The input that produces it also flips\n            // waiting_input→running, which lands here.\n            sessionName: session.sessionName ?? null,\n          },\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 = deps.sessionFileMap.get(session.id);\n        if (filePath) {\n          deps.scannerManager\n            .get()\n            .then((scanner) => deps.scannerManager.refreshFileAfterWrite(scanner, filePath))\n            .then(({ outcome, meta }) => {\n              // \"ok\" is reserved for a parse this request started. A post-write\n              // refresh is never \"skipped\"; \"joined\" means it shared the pass\n              // another writer had already queued, and one line per request\n              // with an explicit outcome is what makes the two countable —\n              // `messageCount` alone never could.\n              deps.log().info(`scanner.refreshFile: ${outcome === \"refreshed\" ? \"ok\" : outcome}`, {\n                event: \"scanner.refresh\",\n                sessionId: session.id,\n                filePath,\n                trigger: session.status,\n                outcome,\n                messageCount: meta?.messageCount,\n              });\n            })\n            .catch((err) => {\n              deps.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 = deps.sessionFileMap.get(session.id);\n        if (filePath) {\n          deps.fileWatcher.unwatch(filePath);\n          deps.sessionFileMap.delete(session.id);\n        }\n        // The next PTY for this conversation spawns at the defaults again, so a\n        // remembered size would be reported for a session that never had it.\n        deps.sessionGeometry.delete(session.id);\n        // Unconditional, like the permission clear below: a screen-detected\n        // question needs no JSONL mapping to exist, and one left pending here\n        // survives the exit as an unanswerable card whose stale\n        // pendingQuestionKey then suppresses the same menu on resume. Before\n        // invalidateSession, so the prompt ends cancelled/provider_closed\n        // rather than unavailable.\n        deps.cancelPendingQuestion(session.id);\n        // cancelPendingQuestion clears the key only while a question is still\n        // pending, but an ANSWERED menu keeps its key with no pending entry\n        // (#724) — and that key would suppress the same menu on resume.\n        deps.pendingQuestionKey.delete(session.id);\n        // A gone PTY can never have an open gate; clear silently.\n        deps.pendingPermission.delete(session.id);\n        deps.pendingPermissionKey.delete(session.id);\n        deps.promptRegistry.invalidateSession(session.id, \"session_ended\");\n        deps.contendedSessions.delete(session.id);\n        // Remember that WE owned this conversation up to now, so a resume that\n        // follows a hold isn't mistaken for a collision with someone else\n        // (see conversationBusy's selfPtyEndedAt).\n        deps.rememberSelfPtyEnded(session.id);\n      }\n      const resp = deps.sessionStore.get(session.id, deps.ptyAttachedIds());\n      if (resp) {\n        deps.wsHub.broadcast({ type: \"session_update\", session: resp });\n      }\n      // Push the transition to any iOS Live Activity watching this session.\n      // Fire-and-forget: the notifier logs its own failures, and a push must\n      // never delay or fail a session transition. No-op when APNs is off.\n      void deps.liveActivityNotifier()?.onStatusChange(session, previousStatus);\n      // And tell the phone its turn is up, if nobody is watching this session\n      // already. Same funnel, same fire-and-forget contract as above.\n      void deps.waitingInputNotifier()?.onStatusChange(session, previousStatus);\n      // The session object rides along: SessionStore's copy is a partial\n      // merge that does not carry failureReason/failureCode, so a startup\n      // handshake reading the store could not tell WHY a session went idle.\n      deps.sessionStatusBus.emit(`status:${session.id}`, session.status, session);\n      // Kill-on-idle latch: hold at the next waiting_input/idle if a client\n      // armed `hold_session` with when:\"waiting_input\". Both runners (and the\n      // pty-host remote runner) funnel status here, so this is the one fire\n      // site. Optional so unit tests that stub LiveSessionWiringDeps stay valid.\n      deps.maybeFireHoldWhenIdle?.(session);\n    },\n  };\n}\n\n/**\n * Everything the ApiDeps assembly reads from the server.\n *\n * Values where the original literal captured a value (`publicUrl`,\n * `browseRoot`, and the collaborators built earlier in the constructor);\n * thunks and arrows everywhere the original used them, which is load-bearing:\n * the stores open during listen(), `apiKey` changes under rotateApiKey(), and\n * tests swap `log`/`startGraceTimer`/`broadcastConversationLines` on the\n * server instance after construction.\n */\nexport type ApiDepsWiring = {\n  sessionGeometry: Map<string, { cols: number; rows: number }>;\n  apiKey: () => string;\n  localNoAuth: boolean;\n  logMenubarRequests: boolean;\n  publicUrl: string | null;\n  browseRoot: string | null;\n  browserCors: string | undefined;\n  ptyGracePeriodMs: number;\n  rotateApiKey: ApiDeps[\"rotateApiKey\"];\n  claudeFlagsConfig: ApiDeps[\"claudeFlagsConfig\"];\n  featureFlagsConfig: ApiDeps[\"featureFlagsConfig\"];\n  setClaudeFlagsConfig: ApiDeps[\"setClaudeFlagsConfig\"];\n  ptyManager: LiveSessionManager;\n  sessionStore: SessionStore;\n  wsHub: WSHub;\n  sessionHandlers: SessionHandlers;\n  conversationHandlers: ConversationHandlers;\n  cache: () => ConversationCache | null;\n  cacheMonitor: () => CacheIntegrityMonitor | null;\n  hostPressureMonitor: () => HostPressureMonitor | null;\n  pushRepo: () => PushRepository | null;\n  liveActivityPushEnabled: () => boolean;\n  expoPushEnabled: () => boolean;\n  expoPushSender: ApiDeps[\"expoPushSender\"];\n  devicesRepo: () => DevicesRepository | null;\n  projectsRepo: () => ProjectsRepository | null;\n  conversationsRepo: () => ConversationsRepository | null;\n  sessionsRepo: () => SessionsRepository | null;\n  cacheMetadataRepo: () => CacheMetadataRepository | null;\n  runtimeStore: () => RuntimeStore | null;\n  managedSessionsRepo: () => ManagedSessionsRepository | null;\n  sessionVerdicts: () => Map<string, ReconcileVerdict>;\n  log: () => Logger;\n  ptyAttachedIds: () => Set<string>;\n  withReconciledLifecycle: (sessions: readonly SessionResponse[]) => readonly SessionResponse[];\n  currentWarmupState: () => ServerWarmupState | null;\n  addSessionSubscriber: (sessionId: string, ws: WebSocket) => void;\n  removeSessionSubscriber: (sessionId: string, ws: WebSocket) => void;\n  startGraceTimer: (sessionId: string, delayMs: number) => void;\n  armHoldWhenIdle: (sessionId: string) => \"held\" | \"armed\" | \"no_session\";\n  handleSessionsCount: (res: ServerResponse) => void;\n  applyLiveSessionSetting: (\n    sessionId: string,\n    req: IncomingMessage,\n    res: ServerResponse,\n    setting: \"model\" | \"effort\",\n  ) => Promise<void>;\n  handlePairStart: (res: ServerResponse) => void;\n  handlePairExchange: (req: IncomingMessage, res: ServerResponse) => Promise<void>;\n  handleBrowse: (url: URL, res: ServerResponse) => Promise<void>;\n  handleMkdir: (req: IncomingMessage, res: ServerResponse) => Promise<void>;\n  clientIdToWs: Map<string, WebSocket>;\n  wsToClientId: Map<WebSocket, string>;\n  sessionSubscribers: Map<string, Set<WebSocket>>;\n  terminalSeq: Map<string, number>;\n  pendingPermission: Map<string, PendingPermission>;\n  pendingQuestions: Map<string, PendingQuestion>;\n  promptRegistry: PromptRegistry;\n  agentClient: AgentClient | null;\n  conversationWriter: ConversationWriter | null;\n  agentConfig: AgentConfig;\n};\n\n/**\n * Assemble the dependency bag the Hono app and the WS routes are built from,\n * including the three WebSocket lifecycle handlers.\n *\n * Extracted from the constructor for the same reason as the two factories\n * above; every key, its order and its target are unchanged.\n */\nexport function createApiDeps(deps: ApiDepsWiring): ApiDeps {\n  return {\n    // ponytail: getter so rotateApiKey() takes effect without restarting the server\n    get apiKey() {\n      return deps.apiKey();\n    },\n    localNoAuth: deps.localNoAuth,\n    logMenubarRequests: deps.logMenubarRequests,\n    rotateApiKey: () => deps.rotateApiKey(),\n    claudeFlagsConfig: () => deps.claudeFlagsConfig(),\n    featureFlagsConfig: () => deps.featureFlagsConfig(),\n    setClaudeFlagsConfig: (values, extraArgs) => deps.setClaudeFlagsConfig(values, extraArgs),\n    publicUrl: deps.publicUrl,\n    browseRoot: deps.browseRoot,\n    browserCors: deps.browserCors,\n    ptyManager: deps.ptyManager,\n    sessionStore: deps.sessionStore,\n    wsHub: deps.wsHub,\n    cache: () => deps.cache(),\n    isExcludedSubagent: (id) => deps.conversationHandlers.isExcludedSubagent(id),\n    cacheMonitor: () => deps.cacheMonitor(),\n    hostPressureMonitor: () => deps.hostPressureMonitor(),\n    pushRepo: () => deps.pushRepo(),\n    liveActivityPushEnabled: () => deps.liveActivityPushEnabled(),\n    expoPushEnabled: () => deps.expoPushEnabled(),\n    expoPushSender: () => deps.expoPushSender(),\n\n    devicesRepo: () => deps.devicesRepo(),\n    projectsRepo: () => deps.projectsRepo(),\n    conversationsRepo: () => deps.conversationsRepo(),\n    sessionsRepo: () => deps.sessionsRepo(),\n    cacheMetadataRepo: () => deps.cacheMetadataRepo(),\n    runtimeStore: () => deps.runtimeStore(),\n    managedSessionsRepo: () => deps.managedSessionsRepo(),\n    sessionVerdicts: () => deps.sessionVerdicts(),\n    ptyAttachedIds: () => deps.ptyAttachedIds(),\n    handleListSessions: (url, res) => deps.sessionHandlers.handleListSessions(url, res),\n    handleSessionsCount: (res) => deps.handleSessionsCount(res),\n    handleGetRecentSessions: (url, res) =>\n      deps.conversationHandlers.handleGetRecentSessions(url, res),\n    handleGetSessionNames: (res) => deps.sessionHandlers.handleGetSessionNames(res),\n    handleGetSession: (id, res) => deps.sessionHandlers.handleGetSession(id, res),\n    handleGetOutput: (id, res) => deps.sessionHandlers.handleGetOutput(id, res),\n    handleSendInput: (id, req, res) => deps.sessionHandlers.handleSendInput(id, req, res),\n    handleRawKey: (id, req, res) => deps.sessionHandlers.handleRawKey(id, req, res),\n    handleSendAnswer: (id, req, res) => deps.sessionHandlers.handleSendAnswer(id, req, res),\n    handlePromptAnswer: (id, req, res) => deps.sessionHandlers.handlePromptAnswer(id, req, res),\n    handlePermissionAnswer: (id, req, res) =>\n      deps.sessionHandlers.handlePermissionAnswer(id, req, res),\n    handleCancel: (id, res) => deps.sessionHandlers.handleCancel(id, res),\n    handleStopSession: (id, res, opts) => deps.sessionHandlers.handleStopSession(id, res, opts),\n    handleKillSession: (id, res, opts) => deps.sessionHandlers.handleKillSession(id, res, opts),\n    handleSetSessionName: (id, req, res) => deps.sessionHandlers.handleSetSessionName(id, req, res),\n    handleSetSessionModel: (id, req, res) => deps.applyLiveSessionSetting(id, req, res, \"model\"),\n    handleSetSessionEffort: (id, req, res) => deps.applyLiveSessionSetting(id, req, res, \"effort\"),\n    handleUploadFile: (id, req, res) => deps.sessionHandlers.handleUploadFile(id, req, res),\n    handleAdopt: (id, res) => deps.sessionHandlers.handleAdopt(id, res),\n    handleFork: (id, req, res) => deps.sessionHandlers.handleFork(id, req, res),\n    handleResume: (req, res) => deps.sessionHandlers.handleResume(req, res),\n    handleStartSession: (req, res) => deps.sessionHandlers.handleStartSession(req, res),\n    handleListConversations: (url, res) =>\n      deps.conversationHandlers.handleListConversations(url, res),\n    handleConversationsCount: (url, res) =>\n      deps.conversationHandlers.handleConversationsCount(url, res),\n    handleGetConversation: (id, url, res, ifNoneMatch) =>\n      deps.conversationHandlers.handleGetConversation(id, url, res, ifNoneMatch),\n    handleSearch: (url, res) => deps.conversationHandlers.handleSearch(url, res),\n    handleSearchTarget: (id, req, res) =>\n      deps.conversationHandlers.handleSearchTarget(id, req, res),\n    handleListProjects: (url, res) => handleListProjects(url, res),\n    handleGetPopularProjects: (url, res) =>\n      deps.conversationHandlers.handleGetPopularProjects(url, res),\n    handleGetProjectSummaries: (url, res) =>\n      deps.conversationHandlers.handleGetProjectSummaries(url, res),\n    handlePairStart: (res) => deps.handlePairStart(res),\n    handlePairExchange: (req, res) => deps.handlePairExchange(req, res),\n    handleBrowse: (url, res) => deps.handleBrowse(url, res),\n    handleMkdir: (req, res) => deps.handleMkdir(req, res),\n    handleWsOpen: (ws, context) => {\n      deps.wsHub.addClient(ws, context);\n      const sessions = deps.withReconciledLifecycle(deps.sessionStore.list(deps.ptyAttachedIds()));\n      deps.wsHub.unicast(ws, { type: \"session_list\", sessions });\n      if (!deps.currentWarmupState()) {\n        deps.wsHub.unicast(ws, { type: \"cache_ready\" });\n      }\n      // Re-surface a pending cache-integrity alert to every connecting client\n      // (covers the startup warm-up window and every reconnect).\n      const alertMsg = deps.cacheMonitor()?.wsMessage();\n      if (alertMsg) deps.wsHub.unicast(ws, alertMsg);\n      const pressureMsg = deps.hostPressureMonitor()?.wsMessage();\n      if (pressureMsg) deps.wsHub.unicast(ws, pressureMsg);\n    },\n    handleWsMessage: async (ws, raw, principal) => {\n      // A refused frame is dropped and logged rather than answered: the\n      // server→client union has no error type (types.ts), and adding one is a\n      // contract change older clients would ignore anyway. Dropping matches how\n      // this handler already treats malformed JSON; the log is what makes the\n      // refusal diagnosable.\n      const deny = (type: string, required: Capability): void => {\n        deps.log().warn(`[ws.capability_denied] ${type} requires ${required}`, {\n          event: \"ws.capability_denied\",\n          type,\n          required,\n          ...(principal?.deviceId ? { deviceId: principal.deviceId } : {}),\n        });\n      };\n      // **The parse catch below is narrowed to PARSING, and nothing else.**\n      //\n      // This used to be one `try { …the whole handler… } catch {}`, which\n      // swallowed every failure in the body as if it were malformed JSON. With\n      // sealing inside its reach a refused frame would have become a silently\n      // dropped one — no close, no code, no log — the opposite of what §9 froze\n      // four distinct codes to achieve.\n      //\n      // `receive` unseals for a sealed socket and passes a legacy client's text\n      // through unchanged, and it never throws: it closes the socket with the\n      // §9 code that says why and answers null. So it sits OUTSIDE both catches\n      // — anything that did escape it would be a fault in this server, and\n      // filing that under \"malformed JSON\" is the very shape being removed.\n      const text = deps.wsHub.receive(ws, raw);\n      if (text === null) return;\n\n      let msg: {\n        type?: string;\n        clientId?: string;\n        sessionId?: string;\n        when?: string;\n        cols?: number;\n        rows?: number;\n      };\n      try {\n        msg = JSON.parse(text);\n      } catch {\n        // malformed JSON, ignore. THIS catch covers the parse and nothing else.\n        return;\n      }\n\n      try {\n        if (msg.type === \"register\" && typeof msg.clientId === \"string\") {\n          const oldClientId = deps.wsToClientId.get(ws);\n          if (oldClientId) deps.clientIdToWs.delete(oldClientId);\n          deps.clientIdToWs.set(msg.clientId, ws);\n          deps.wsToClientId.set(ws, msg.clientId);\n        }\n        // Cache-backed and synchronous on purpose. `handleWsMessage` is async,\n        // so its first `await` defers everything after it by a microtask — and\n        // subscribe_session emits its snapshot BEFORE any await (the ordering\n        // ws-hub-wire-identity, ws-replay-depth, permission-gate-identity and\n        // prompt-contract-scoping all pin). The handler's own\n        // isExcludedSubagent may classify off disk, which belongs on the HTTP\n        // detail path, not on every WS frame.\n        if (typeof msg.sessionId === \"string\") {\n          // A live session carries its own flag in memory, so it is refused\n          // even with no cache; the stored classification is cache-derived and\n          // degrades with it. Cacheless also loses the flag's value, which\n          // falls back to the registry default (off).\n          const subagentCache = deps.cache?.();\n          const managed = deps.sessionStore?.getManaged?.(msg.sessionId);\n          const includeSubagents = subagentCache?.includeSubagentSessions ?? false;\n          if (\n            (managed?.isSubagent === true && !includeSubagents) ||\n            subagentCache?.isExcludedSubagent(managed?.boundConversationId ?? msg.sessionId)\n          )\n            return;\n        }\n        if (msg.type === \"subscribe_session\" && typeof msg.sessionId === \"string\") {\n          // Reading a session's stream is the same authority as reading its\n          // history over HTTP. Checked per frame rather than inherited from the\n          // upgrade, so this is the seam any future per-project scoping hangs\n          // off — it does not add scoping today, and every preset holds\n          // history:read, so no client's behaviour changes.\n          if (!wsAllows(principal, \"history:read\")) {\n            deny(msg.type, \"history:read\");\n            return;\n          }\n          deps.addSessionSubscriber(msg.sessionId, ws);\n          if (deps.promptRegistry) {\n            deps.wsHub.unicast(ws, deps.promptRegistry.snapshot(msg.sessionId));\n          }\n          if (deps.ptyManager.hasSession(msg.sessionId)) {\n            // Replay everything the session's render terminal still holds; it\n            // caps itself (REPLAY_MAX_LINES) and the client keeps its own,\n            // larger, retention cap. The old fixed 200 was under a fifth of\n            // that, so most of a live session's scrollback was unreachable on\n            // the client however far back it could scroll.\n            const lines = await deps.ptyManager.getOutputLines(msg.sessionId, REPLAY_MAX_LINES);\n            const userMessages = deps.ptyManager.getInputHistory(msg.sessionId);\n            // Carried on the replay rather than a separate frame: a client\n            // subscribing to an already-resized session would otherwise decode\n            // the replayed screen at the default size before any later resize\n            // told it better.\n            const geometry = deps.sessionGeometry.get(msg.sessionId) ?? {\n              cols: PTY_COLS,\n              rows: PTY_ROWS,\n            };\n            deps.wsHub.unicast(ws, {\n              type: \"terminal_replay\",\n              sessionId: msg.sessionId,\n              lines,\n              userMessages,\n              seq: deps.terminalSeq.get(msg.sessionId),\n              cols: geometry.cols,\n              rows: geometry.rows,\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 = deps.pendingPermission.get(msg.sessionId);\n          if (pendingGate) {\n            deps.log().info(`[ws.replay_permission] ${msg.sessionId.slice(0, 8)}`, {\n              event: \"ws.replay_permission\",\n              sessionId: msg.sessionId,\n            });\n            deps.wsHub.unicast(ws, {\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              contentKey: permissionGateKey(pendingGate),\n              gateId: pendingGate.gateId,\n            });\n          }\n          const pendingQuestion = deps.pendingQuestions.get(msg.sessionId);\n          if (pendingQuestion) {\n            deps.log().info(`[ws.replay_question] ${msg.sessionId.slice(0, 8)}`, {\n              event: \"ws.replay_question\",\n              sessionId: msg.sessionId,\n            });\n            deps.wsHub.unicast(ws, {\n              type: \"question\",\n              sessionId: msg.sessionId,\n              toolUseId: pendingQuestion.toolUseId,\n              questions: pendingQuestion.questions,\n            });\n          }\n        }\n        if (msg.type === \"unsubscribe_session\" && typeof msg.sessionId === \"string\") {\n          if (!wsAllows(principal, \"history:read\")) {\n            deny(msg.type, \"history:read\");\n            return;\n          }\n          deps.removeSessionSubscriber(msg.sessionId, ws);\n        }\n        if (\n          msg.type === \"resize_session\" &&\n          typeof msg.sessionId === \"string\" &&\n          typeof msg.cols === \"number\" &&\n          typeof msg.rows === \"number\"\n        ) {\n          // Control, not reading, for the same reason hold_session is: this\n          // reshapes the terminal every other subscriber is watching, so a\n          // read-only device must not be able to send it.\n          if (!wsAllows(principal, \"session:control\")) {\n            deny(msg.type, \"session:control\");\n            return;\n          }\n          // Silent on an unknown session and on nonsense dimensions — the\n          // runner guards both. A client dragging a window must not have to\n          // care whether the session is still alive.\n          if (!deps.ptyManager.hasSession(msg.sessionId)) return;\n          deps.ptyManager.resize(msg.sessionId, msg.cols, msg.rows);\n          deps.sessionGeometry.set(msg.sessionId, { cols: msg.cols, rows: msg.rows });\n          // Every other subscriber is decoding this session's stream against a\n          // geometry that just changed. A client that assumes the spawn size —\n          // as every shipped mobile build does — renders absolute cursor moves\n          // against the wrong viewport until it is told otherwise.\n          deps.wsHub.broadcast({\n            type: \"terminal_resize\",\n            sessionId: msg.sessionId,\n            cols: msg.cols,\n            rows: msg.rows,\n          });\n        }\n        if (msg.type === \"hold_session\" && typeof msg.sessionId === \"string\") {\n          // Holding a session SIGINTs the agent and disposes its screen, which\n          // is control, not reading — a read-only device could previously stop\n          // any session by id with a single frame, because the only check this\n          // path ever had was the upgrade's `history:read`.\n          if (!wsAllows(principal, \"session:control\")) {\n            deny(msg.type, \"session:control\");\n            deps.wsHub.unicast(ws, {\n              type: \"hold_session_result\",\n              sessionId: msg.sessionId,\n              ok: false,\n              reason: \"permission_denied\",\n            });\n            return;\n          }\n          // Additive `when` on the existing frame. Omitted / \"grace\" is today's\n          // backgrounding path (ptyGracePeriodMs). \"waiting_input\" latches a\n          // hold at the end of the current turn. Anything else is ignored —\n          // do not fall through to grace, including for read-only (already\n          // returned above).\n          const when = msg.when;\n          if (when === undefined || when === \"grace\") {\n            deps.startGraceTimer(msg.sessionId, deps.ptyGracePeriodMs);\n            deps.wsHub.unicast(ws, {\n              type: \"hold_session_result\",\n              sessionId: msg.sessionId,\n              ok: true,\n              applied: \"grace\",\n            });\n            return;\n          }\n          if (when === \"waiting_input\") {\n            const applied = deps.armHoldWhenIdle(msg.sessionId);\n            deps.wsHub.unicast(ws, {\n              type: \"hold_session_result\",\n              sessionId: msg.sessionId,\n              ok: applied !== \"no_session\",\n              ...(applied === \"no_session\" ? { reason: \"no_session\" } : { applied }),\n            });\n            return;\n          }\n          deps.log().warn(`[pty.hold_when_unknown] hold_session when=${String(when)}`, {\n            event: \"pty.hold_when_unknown\",\n            sessionId: msg.sessionId,\n            when,\n          });\n          deps.wsHub.unicast(ws, {\n            type: \"hold_session_result\",\n            sessionId: msg.sessionId,\n            ok: false,\n            reason: \"unknown_when\",\n          });\n        }\n      } catch (err) {\n        // NOT a parse failure — the frame was well-formed and the handler threw.\n        // Logged rather than swallowed: this handler is `async`, so an escaping\n        // rejection would be an unhandled one, and a silently dropped frame here\n        // is a client waiting forever for a reply that was never attempted.\n        deps.log().error(\"[ws.handler_failed] a well-formed frame threw\", {\n          event: \"ws.handler_failed\",\n          type: msg.type,\n          err,\n        });\n      }\n    },\n    handleWsClose: (ws) => {\n      const clientId = deps.wsToClientId.get(ws);\n      if (clientId) {\n        deps.clientIdToWs.delete(clientId);\n        deps.wsToClientId.delete(ws);\n      }\n      for (const subscribers of deps.sessionSubscribers.values()) {\n        subscribers.delete(ws);\n        // Deliberately does NOT arm a kill timer. A socket closing is not a\n        // request to stop the agent: phones sleep, signal drops, Wi-Fi hands\n        // off to cellular. Killing the PTY because nobody is watching means a\n        // long agent task cannot outlive a backgrounded app — the failure this\n        // runtime exists to prevent (see\n        // docs/architecture/2026-07-24-durable-session-runtime.md).\n        //\n        // Unbounded PTY growth is bounded by the idle reaper instead, which\n        // measures agent inactivity rather than subscriber absence. An\n        // explicit hold_session from the client still terminates immediately\n        // (see the hold_session handler above) — that one IS a user intent.\n      }\n    },\n    agentClient: deps.agentClient,\n    conversationWriter: deps.conversationWriter,\n    agentConfig: deps.agentConfig,\n  };\n}\n","import { closeSync, openSync, readdirSync, readSync, statSync } from \"fs\";\nimport type { ServerResponse } from \"http\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\n\n// How much of a conversation JSONL to read looking for `cwd`. The field is on\n// the first line in practice; a few KB of slack covers a long opening message.\nconst HEAD_BYTES = 64 * 1024;\n// Conversations whose head we'll read before giving up on a directory. A JSONL\n// truncated before its first `cwd` is rare; scanning all of them is not worth\n// the file handles.\nconst MAX_FILES_PROBED = 3;\n\n/**\n * The authoritative project path, read from the `cwd` a conversation in this\n * directory recorded.\n *\n * Claude encodes a project path into a directory name by replacing every '/',\n * '.' and '_' with '-', which is not invertible: `-Users-me-tb-mobile` is\n * equally `/Users/me/tb/mobile` and `/Users/me/tb-mobile`. Any path with a\n * hyphen inside a segment therefore decodes to something that exists nowhere\n * and can never be joined against a conversation's `project_path`.\n */\nfunction readRecordedCwd(dir: string): string | null {\n  let files: string[];\n  try {\n    files = readdirSync(dir).filter((f) => f.endsWith(\".jsonl\"));\n  } catch {\n    return null;\n  }\n\n  for (const file of files.slice(0, MAX_FILES_PROBED)) {\n    let fd: number | undefined;\n    try {\n      fd = openSync(join(dir, file), \"r\");\n      const buf = Buffer.alloc(HEAD_BYTES);\n      const bytes = readSync(fd, buf, 0, HEAD_BYTES, 0);\n      for (const line of buf.subarray(0, bytes).toString(\"utf8\").split(\"\\n\")) {\n        if (!line.includes('\"cwd\"')) continue;\n        try {\n          const cwd = (JSON.parse(line) as { cwd?: unknown }).cwd;\n          if (typeof cwd === \"string\" && cwd.length > 0) return cwd;\n        } catch {\n          // Partial trailing line, or a line that isn't JSON — keep looking.\n        }\n      }\n    } catch {\n      // Unreadable file — try the next one.\n    } finally {\n      if (fd !== undefined) closeSync(fd);\n    }\n  }\n  return null;\n}\n\nfunction decodeProjectPath(dirName: string): string {\n  // Lossy last resort, used only when no conversation in the directory records\n  // a cwd (an empty or freshly-created project dir).\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<{ 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        return { dirName: String(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  // Resolve paths for the requested page only: reading a cwd costs a file\n  // open, and there are hundreds of project directories.\n  const page = entries.slice(offset, offset + limit).map(({ dirName }) => {\n    const path = readRecordedCwd(join(projectsDir, dirName)) ?? decodeProjectPath(String(dirName));\n    const name = path.split(/[\\\\/]/).filter(Boolean).pop() ?? dirName;\n    return { name, path, dirName };\n  });\n\n  res.writeHead(200, { \"Content-Type\": \"application/json\" });\n  res.end(JSON.stringify({ projects: page, total }));\n}\n","// CacheIntegrityMonitor — detects cache/disk drift (missing JSONLs), freezes\n// automatic pruning while a decision is pending, backs up the DB before any\n// destructive resolution, and applies exactly the action a human chose, once,\n// idempotently. See docs/superpowers/specs/2026-07-18-cache-integrity-alert-design.md.\n\nimport { createHash } from \"crypto\";\nimport { existsSync } from \"fs\";\nimport type { ConversationCache, ScannerMeta } from \"../../conversation-cache\";\nimport type { Logger } from \"../../logger\";\nimport type { CacheAlertResolveAction, WSMessage } from \"../../types\";\nimport type { WSHub } from \"../../ws-hub\";\nimport {\n  type AlertState,\n  loadAlertState,\n  type MissingEntry,\n  type PendingAlert,\n  saveAlertState,\n} from \"./alertStore\";\nimport { backupCacheDb } from \"./backup\";\n\nexport type ResolveAction = CacheAlertResolveAction;\n\n/** The `cache_alert` WS variant, narrowed from the WSMessage union. */\nexport type CacheAlertWsMessage = Extract<WSMessage, { type: \"cache_alert\" }>;\n\nexport type ResolveResult =\n  | { ok: true; action: ResolveAction; pruned?: number; backupPath?: string }\n  | { alreadyResolved: true }\n  | { conflict: true; currentFingerprint: string };\n\nconst MAX_MISSING_PERSISTED = 1000;\nconst SAMPLE_SIZE = 20;\nconst STORM_WINDOW_MS = 30_000;\nconst STORM_THRESHOLD = 10;\n\nfunction envInt(name: string, fallback: number): number {\n  const parsed = Number.parseInt(process.env[name] ?? \"\", 10);\n  return Number.isFinite(parsed) ? parsed : fallback;\n}\n\n/** sha256 of the sorted missing-id list — stable identity for a missing-set. */\nfunction fingerprintOf(ids: string[]): string {\n  const sorted = [...ids].sort();\n  return `sha256:${createHash(\"sha256\").update(sorted.join(\"\\n\")).digest(\"hex\")}`;\n}\n\nexport class CacheIntegrityMonitor {\n  private _pending: PendingAlert | null;\n  private ignoredIds: Set<string>;\n  private deferredUnlinks: string[] = [];\n  private unlinkTimes: number[] = [];\n\n  constructor(\n    private readonly cache: ConversationCache,\n    private readonly wsHub: WSHub,\n    private readonly log: Logger,\n    private readonly cacheDir: string,\n    // Injected so reset_rescan can rebuild from disk truth without the monitor\n    // depending on the whole server. Returns the metas a fresh scan surfaced.\n    private readonly rescan?: () => Promise<ScannerMeta[]>,\n    private readonly runDuringReset?: <T>(operation: () => Promise<T>) => Promise<T>,\n  ) {\n    const state = loadAlertState();\n    this._pending = state.pending ?? null;\n    this.ignoredIds = new Set(state.ignoredIds ?? []);\n  }\n\n  get pending(): PendingAlert | null {\n    return this._pending;\n  }\n\n  private persist(): void {\n    const state: AlertState = {};\n    if (this._pending) {\n      state.pending = {\n        ...this._pending,\n        missing: this._pending.missing.slice(0, MAX_MISSING_PERSISTED),\n      };\n    }\n    if (this.ignoredIds.size > 0) state.ignoredIds = [...this.ignoredIds];\n    saveAlertState(state);\n  }\n\n  private classifySeverity(missingCount: number, totalRows: number): \"high\" | \"low\" {\n    const minMissing = envInt(\"THREADBASE_CACHE_ALERT_MIN_MISSING\", 20);\n    const minRatio = Number.parseFloat(process.env.THREADBASE_CACHE_ALERT_MIN_RATIO ?? \"0.20\");\n    const ratio = totalRows > 0 ? missingCount / totalRows : 0;\n    const ratioThreshold = Number.isFinite(minRatio) ? minRatio : 0.2;\n    return missingCount >= minMissing && ratio >= ratioThreshold ? \"high\" : \"low\";\n  }\n\n  private sampleOf(missing: MissingEntry[]): { id: string; title?: string }[] {\n    return missing.slice(0, SAMPLE_SIZE).map((m) => ({\n      id: m.id,\n      ...(m.title != null ? { title: m.title } : {}),\n    }));\n  }\n\n  private buildWsMessage(pending: PendingAlert): CacheAlertWsMessage {\n    return {\n      type: \"cache_alert\",\n      fingerprint: pending.fingerprint,\n      severity: pending.severity,\n      missingCount: pending.missingCount,\n      totalRows: pending.totalRows,\n      detectedAt: pending.detectedAt,\n      sample: this.sampleOf(pending.missing),\n    };\n  }\n\n  wsMessage(): CacheAlertWsMessage | null {\n    return this._pending ? this.buildWsMessage(this._pending) : null;\n  }\n\n  healthzField():\n    | { severity: \"high\" | \"low\"; missingCount: number; fingerprint: string; detectedAt: string }\n    | undefined {\n    if (!this._pending) return undefined;\n    return {\n      severity: this._pending.severity,\n      missingCount: this._pending.missingCount,\n      fingerprint: this._pending.fingerprint,\n      detectedAt: this._pending.detectedAt,\n    };\n  }\n\n  /**\n   * Scan the cache for rows whose file is gone, excluding ids the user chose to\n   * ignore. If none remain, clear any stale pending alert and return (the caller\n   * decides whether to run pruneGhostFiles). Otherwise classify severity, persist\n   * the pending record, back up on high severity, and broadcast the alert.\n   */\n  async runDetection(detectedAt: string = new Date().toISOString()): Promise<void> {\n    const all = this.cache.listMissingFiles(existsSync);\n    const missing = all.filter((m) => !this.ignoredIds.has(m.id));\n\n    if (missing.length === 0) {\n      if (this._pending) {\n        this._pending = null;\n        this.persist();\n      }\n      return;\n    }\n\n    const totalRows = this.cache.countAllRows();\n    const fingerprint = fingerprintOf(missing.map((m) => m.id));\n    const severity = this.classifySeverity(missing.length, totalRows);\n\n    const pending: PendingAlert = {\n      fingerprint,\n      severity,\n      detectedAt,\n      missingCount: missing.length,\n      totalRows,\n      missing,\n    };\n\n    if (severity === \"high\") {\n      try {\n        pending.backupPath = await backupCacheDb(this.cache.getDatabase(), this.cacheDir);\n      } catch (err) {\n        this.log.warn(\"cache-integrity backup failed\", {\n          event: \"cache_integrity.backup_failed\",\n          error: err instanceof Error ? err.message : String(err),\n        });\n      }\n    }\n\n    this._pending = pending;\n    this.persist();\n    this.log.warn(\"cache integrity drift detected\", {\n      event: \"cache_integrity.detected\",\n      severity,\n      missingCount: missing.length,\n      totalRows,\n      fingerprint,\n    });\n    this.wsHub.broadcast(this.buildWsMessage(pending));\n  }\n\n  /** Queue an unlink while an alert is pending — the row is not invalidated. */\n  deferUnlink(filePath: string): void {\n    this.deferredUnlinks.push(filePath);\n  }\n\n  /**\n   * Record a live unlink while NO alert is pending. Crossing the storm threshold\n   * (>= 10 unlinks within 30s) re-triggers detection.\n   */\n  recordUnlink(filePath: string): void {\n    const now = Date.now();\n    this.unlinkTimes.push(now);\n    this.unlinkTimes = this.unlinkTimes.filter((t) => now - t < STORM_WINDOW_MS);\n    if (this.unlinkTimes.length >= STORM_THRESHOLD) {\n      this.unlinkTimes = [];\n      void this.runDetection().catch((err) => {\n        this.log.error(\"cache-integrity storm detection failed\", {\n          event: \"cache_integrity.storm_detection_failed\",\n          error: err instanceof Error ? err.message : String(err),\n          filePath,\n        });\n      });\n    }\n  }\n\n  private async ensureBackup(pending: PendingAlert): Promise<string | undefined> {\n    if (pending.backupPath) return pending.backupPath;\n    try {\n      pending.backupPath = await backupCacheDb(this.cache.getDatabase(), this.cacheDir);\n    } catch (err) {\n      this.log.warn(\"cache-integrity backup failed\", {\n        event: \"cache_integrity.backup_failed\",\n        error: err instanceof Error ? err.message : String(err),\n      });\n    }\n    return pending.backupPath;\n  }\n\n  private clearPending(): void {\n    this._pending = null;\n    this.deferredUnlinks = [];\n    this.persist();\n  }\n\n  private applyDeferredUnlinks(): void {\n    for (const fp of this.deferredUnlinks) this.cache.invalidateByFilePath(fp);\n    this.deferredUnlinks = [];\n  }\n\n  private broadcastResolved(fingerprint: string, action: ResolveAction): void {\n    this.wsHub.broadcast({ type: \"cache_alert_resolved\", fingerprint, action });\n  }\n\n  /**\n   * Apply the human's chosen resolution. Idempotent per fingerprint: no pending\n   * alert → alreadyResolved; a different fingerprint → conflict. See the spec's\n   * four-action semantics.\n   */\n  async resolve(\n    fingerprint: string,\n    action: ResolveAction,\n    ids?: string[],\n  ): Promise<ResolveResult> {\n    const pending = this._pending;\n    if (!pending) return { alreadyResolved: true };\n    if (pending.fingerprint !== fingerprint) {\n      return { conflict: true, currentFingerprint: pending.fingerprint };\n    }\n    // Claim the alert synchronously — before the first `await` below (backup is\n    // async). A concurrent same-fingerprint resolve then sees no pending alert\n    // and no-ops with `alreadyResolved`, upholding the spec's \"first resolver\n    // wins, second harmlessly no-ops\" invariant across the await points.\n    this._pending = null;\n\n    switch (action) {\n      case \"prune_all\": {\n        await this.ensureBackup(pending);\n        const backupPath = pending.backupPath;\n        // Re-verify each id against disk — a file may have reappeared.\n        const stillMissing = pending.missing\n          .filter((m) => !existsSync(m.filePath))\n          .map((m) => m.id);\n        const pruned = this.cache.dropRowsById(stillMissing);\n        this.applyDeferredUnlinks();\n        this.clearPending();\n        this.broadcastResolved(fingerprint, action);\n        return { ok: true, action, pruned, backupPath };\n      }\n\n      case \"prune_selected\": {\n        const requested = new Set(ids ?? []);\n        const pendingIds = new Set(pending.missing.map((m) => m.id));\n        // Intersect: only drop ids that are actually in the pending set.\n        const toDrop = [...requested].filter((id) => pendingIds.has(id));\n        await this.ensureBackup(pending);\n        const backupPath = pending.backupPath;\n        const pruned = this.cache.dropRowsById(toDrop);\n        // Drain only the deferred unlinks for the pruned subset.\n        const prunedPaths = new Set(\n          pending.missing.filter((m) => toDrop.includes(m.id)).map((m) => m.filePath),\n        );\n        this.deferredUnlinks = this.deferredUnlinks.filter((fp) => {\n          if (prunedPaths.has(fp)) {\n            this.cache.invalidateByFilePath(fp);\n            return false;\n          }\n          return true;\n        });\n        // Re-run detection on the remainder; a still-missing set raises a new\n        // alert. (_pending was already cleared at the top of resolve.)\n        this.persist();\n        await this.runDetection();\n        this.broadcastResolved(fingerprint, action);\n        return { ok: true, action, pruned, backupPath };\n      }\n\n      case \"ignore\": {\n        // Persist the individual ids (not the fingerprint) so one more deletion\n        // doesn't resurface these — see spec rationale.\n        for (const m of pending.missing) this.ignoredIds.add(m.id);\n        // Discard the deferred queue: the user chose to keep these rows, so\n        // queued deletions for other paths must not be applied as a side effect.\n        this.deferredUnlinks = [];\n        this.clearPending();\n        this.broadcastResolved(fingerprint, action);\n        return { ok: true, action };\n      }\n\n      case \"reset_rescan\": {\n        const backupPath = await this.ensureBackup(pending);\n        const reset = async () => {\n          this.cache.clearAll();\n          if (this.rescan) {\n            const metas = await this.rescan();\n            this.cache.upsertFromScannerMeta(metas);\n          }\n        };\n        const resetPromise = this.runDuringReset ? this.runDuringReset(reset) : reset();\n        void resetPromise.catch((err) => {\n          this.log.error(\"cache-integrity reset rescan failed\", {\n            event: \"cache_integrity.reset_rescan_failed\",\n            error: err instanceof Error ? err.message : String(err),\n          });\n        });\n        this.clearPending();\n        this.broadcastResolved(fingerprint, action);\n        return { ok: true, action, backupPath };\n      }\n    }\n  }\n}\n","// Persisted cache-integrity alert state. Mirrors codexGateAnswers.ts: one small\n// JSON file at ~/.threadbase/cache-alert.json (respecting THREADBASE_CONFIG_DIR),\n// read/written so a pending alert survives a restart and re-surfaces on every\n// client connect, and so an \"ignore\" decision is durable across restarts.\n\nimport { mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { dirname, join } from \"path\";\n\n/** One missing conversation the alert covers. */\nexport interface MissingEntry {\n  id: string;\n  filePath: string;\n  title: string | null;\n  tailed: boolean;\n}\n\nexport interface PendingAlert {\n  fingerprint: string;\n  severity: \"high\" | \"low\";\n  detectedAt: string;\n  missingCount: number;\n  totalRows: number;\n  backupPath?: string;\n  /** Capped at 1000 entries in the persisted file. */\n  missing: MissingEntry[];\n}\n\nexport interface AlertState {\n  pending?: PendingAlert;\n  ignoredIds?: string[];\n}\n\nfunction alertStatePath(): string {\n  const dir = process.env.THREADBASE_CONFIG_DIR ?? join(homedir(), \".threadbase\");\n  return join(dir, \"cache-alert.json\");\n}\n\nexport function loadAlertState(): AlertState {\n  try {\n    const parsed = JSON.parse(readFileSync(alertStatePath(), \"utf-8\"));\n    return parsed && typeof parsed === \"object\" ? (parsed as AlertState) : {};\n  } catch {\n    return {};\n  }\n}\n\nexport function saveAlertState(state: AlertState): void {\n  const path = alertStatePath();\n  mkdirSync(dirname(path), { recursive: true });\n  writeFileSync(path, `${JSON.stringify(state, null, 2)}\\n`);\n}\n","// WAL-safe backup of the SQLite conversation cache, taken before any\n// destructive cache-integrity resolution. Uses better-sqlite3's native async\n// db.backup() (not a raw cp) — the cache runs in WAL mode, so copying cache.db\n// alone would miss uncheckpointed data in cache.db-wal.\n\nimport type Database from \"better-sqlite3\";\nimport { existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from \"fs\";\nimport { join } from \"path\";\n\nconst DEFAULT_RETAIN = 3;\n\nfunction retainCount(): number {\n  const parsed = Number.parseInt(process.env.THREADBASE_CACHE_BACKUP_RETAIN ?? \"\", 10);\n  return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_RETAIN;\n}\n\n/** `YYYYMMDD-HHMMSS` in local time, matching a hand-taken backup's name. */\nfunction timestamp(d: Date): string {\n  const p = (n: number) => String(n).padStart(2, \"0\");\n  return (\n    `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}` +\n    `-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`\n  );\n}\n\n/**\n * Back up the cache DB to `<cacheDir>/backups/cache-<timestamp>.db`, then prune\n * all but the newest THREADBASE_CACHE_BACKUP_RETAIN backups. Returns the path\n * of the backup just created.\n */\nexport async function backupCacheDb(db: Database.Database, cacheDir: string): Promise<string> {\n  const backupsDir = join(cacheDir, \"backups\");\n  mkdirSync(backupsDir, { recursive: true });\n  const destPath = join(backupsDir, `cache-${timestamp(new Date())}.db`);\n  await db.backup(destPath);\n\n  const retain = retainCount();\n  const backups = readdirSync(backupsDir)\n    .filter((f) => f.startsWith(\"cache-\") && f.endsWith(\".db\"))\n    .map((f) => {\n      const full = join(backupsDir, f);\n      return { full, mtime: statSync(full).mtimeMs };\n    })\n    .sort((a, b) => b.mtime - a.mtime); // newest first\n\n  for (const stale of backups.slice(retain)) {\n    if (existsSync(stale.full)) unlinkSync(stale.full);\n  }\n\n  return destPath;\n}\n","import chokidar, { type FSWatcher } from \"chokidar\";\nimport { statSync } from \"fs\";\nimport { open, stat } from \"fs/promises\";\nimport { canonicalizeFilePath } from \"../../utils/canonicalizeFilePath\";\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  /**\n   * Fires when a tailed file shrank below our read offset (in-place truncation\n   * or replacement by a shorter file). The tail has already reset to byte 0;\n   * the consumer must discard any byte-offset index built for the old content,\n   * which no longer describes this file.\n   */\n  onTruncated?: (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  // The original (un-canonicalized) path passed to watch(). The `files` map is\n  // keyed by the canonical path (so mixed-separator poke/unwatch match), but FS\n  // reads and emitted callback paths use this original so the offset index's\n  // stat identity is unchanged (P1.a).\n  path: string;\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 onTruncated: ConversationWatcherEvents[\"onTruncated\"];\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.onTruncated = events.onTruncated;\n    this.onError = events.onError;\n  }\n\n  watch(filePath: string): void {\n    // Key the map by the canonical (forward-slash) path so a native-separator\n    // path from a directory event (chokidar on Windows) resolves to the entry a\n    // scanner-posix path registered — otherwise poke()/unwatch() miss and the\n    // tail self-heal is dead (P1.a). FS reads and emitted callback paths keep the\n    // original path (`entry.path`); its stat identity is what the offset index\n    // relies on, so it must not be normalized.\n    const key = canonicalizeFilePath(filePath);\n    if (this.files.has(key)) 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      // No awaitWriteFinish: readNewLines already coalesces bursts via the\n      // reading/pending flags, and awaitWriteFinish on Linux has been observed\n      // to drop the unlink when a just-created file is deleted inside the\n      // stability window — leaving an external tail attached until the 5 min\n      // idle sweep (#393).\n    });\n\n    watcher.on(\"change\", () => {\n      void this.readNewLines(key);\n    });\n    watcher.on(\"add\", () => {\n      void this.readNewLines(key);\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(key, { watcher, offset, reading: false, pending: false, path: filePath });\n  }\n\n  unwatch(filePath: string): void {\n    const key = canonicalizeFilePath(filePath);\n    const entry = this.files.get(key);\n    if (!entry) return;\n    void entry.watcher.close();\n    this.files.delete(key);\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    // Canonicalize so a directory event's native-separator path matches the entry\n    // watch() keyed by the (possibly posix) scanner path (P1.a).\n    const key = canonicalizeFilePath(filePath);\n    if (!this.files.has(key)) return false;\n    void this.readNewLines(key);\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   * **This costs one OS watch handle per file under `directory`, not one per\n   * directory.** chokidar recurses the tree and registers a separate fs.watch\n   * per entry, because a directory watch alone does not report writes to files\n   * inside it — and per-file `change` events are exactly what the caller needs\n   * (they drive poke()'s tail self-heal and the external-tail attach). So the\n   * handle count tracks the size of the conversation corpus on disk, not the\n   * number of live sessions, and it does not shrink until transcripts are\n   * deleted. `ignoreInitial` suppresses the startup *events*, not the walk.\n   *\n   * Measured 2026-08-09 on the live macOS instance: 2131 open .jsonl fds\n   * against 2133 files under the watched roots — 1:1, ~88% of all fds on the\n   * process, at 2.0% of that box's 122 880 per-process ceiling. Comfortable\n   * there. **Linux is the tight one**: these are inotify watches billed to the\n   * per-user `max_user_watches`, which can be 8192 and is shared with every\n   * other watcher the user runs. Exhaustion surfaces as ENOSPC on the `error`\n   * event — which is why server.ts wires onError rather than leaving it unset.\n   *\n   * Before trading handles for a bound here, note the regression it invites: a\n   * conversation excluded from the walk (by age or by an LRU cap) is one whose\n   * external appends produce no event at all, and that failure is silent.\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(key: string): Promise<void> {\n    const entry = this.files.get(key);\n    if (!entry) return;\n    // Map is keyed by the canonical path; FS reads and emitted callbacks use the\n    // original path so the offset index's stat identity is unchanged (P1.a).\n    const filePath = entry.path;\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        // Truncated or replaced by a SHORTER file: our offset now points past\n        // EOF. Leaving it there makes the tail silently stall until the file\n        // grows back past the stale offset and then resume mid-line, splicing\n        // the new file's content onto the old conversation. Reset to the start\n        // and tell the caller so it can drop the byte-offset index it built for\n        // the previous generation of this file.\n        if (st.size < entry.offset) {\n          entry.offset = 0;\n          this.onTruncated?.(filePath);\n        }\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(key)) 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(key)) {\n        entry.pending = false;\n        void this.readNewLines(key);\n      }\n    }\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","import { execFile } from \"node:child_process\";\nimport { type CpuInfo, cpus, freemem, loadavg, totalmem } from \"os\";\nimport { monitorEventLoopDelay } from \"perf_hooks\";\nimport type { HostPressureLevel, HostPressureOs, HostPressureReason, WSMessage } from \"../../types\";\nimport type { WSHub } from \"../../ws-hub\";\n\nexport type CpuTimesSnapshot = CpuInfo[\"times\"];\n\n// Derived from the function rather than imported by name: @types/node renamed\n// this interface IntervalHistogram -> ELDHistogram in 26.5.0, and the two\n// versions share neither name, so importing either one pins the repo to one\n// side of that bump. ReturnType tracks whatever the installed types call it.\ntype EventLoopHistogram = ReturnType<typeof monitorEventLoopDelay>;\n\nexport const HOST_PRESSURE_SAMPLE_MS = 5_000;\n\n/**\n * Enter a level at the more extreme bar; leave at the less extreme bar so a\n * value sitting between them does not flicker. The elevated→ok leave bars sit\n * a little past the enter bars so a 7.9% ↔ 8.1% free-mem wiggle stays put.\n *\n * Tune these only together with `__tests__/host-pressure.test.ts`.\n */\nexport const HOST_PRESSURE_BARS = {\n  memFreeRatio: {\n    enterElevated: 0.08,\n    leaveElevated: 0.1,\n    enterCritical: 0.04,\n    leaveCritical: 0.08,\n  },\n  eventLoopP99Ms: {\n    enterElevated: 250,\n    leaveElevated: 200,\n    enterCritical: 500,\n    leaveCritical: 250,\n  },\n  loadPerCpu: {\n    enterElevated: 2.0,\n    leaveElevated: 1.5,\n    enterCritical: 3.0,\n    leaveCritical: 2.0,\n  },\n  // win32 has no loadavg. Busy ratio from os.cpus()[].times deltas is 0–1, so\n  // it cannot reuse loadPerCpu's 3.0 critical bar. Reason on the wire stays `load`.\n  cpuBusy: {\n    enterElevated: 0.95,\n    leaveElevated: 0.85,\n    enterCritical: 0.99,\n    leaveCritical: 0.95,\n  },\n  liveAgentsPair: 8,\n} as const;\n\nexport type HostSample = {\n  liveAgents: number;\n  memFreeRatio?: number;\n  eventLoopP99Ms: number;\n  load1: number;\n  ncpu: number;\n  /** 0–1 CPU busy from consecutive os.cpus() times snapshots. Used on win32 only. */\n  cpuBusyRatio?: number;\n};\n\nexport type HostPressureState = \"ok\" | HostPressureLevel;\n\nexport type HostPressureClassification = {\n  level: HostPressureState;\n  reasons: HostPressureReason[];\n};\n\nexport type HostPressureWsMessage = Extract<WSMessage, { type: \"host_pressure\" }>;\n\nconst REASON_ORDER: readonly HostPressureReason[] = [\"memory\", \"event_loop\", \"load\", \"agents\"];\n\nconst RANK: Record<HostPressureState, number> = { ok: 0, elevated: 1, critical: 2 };\n\nexport function parseMacMemoryPressureFreeRatio(output: string): number | undefined {\n  const match = output.match(/System-wide memory free percentage:\\s*(\\d+(?:\\.\\d+)?)%/);\n  if (!match) return undefined;\n  const percentage = Number(match[1]);\n  return percentage <= 100 ? percentage / 100 : undefined;\n}\n\nfunction runMacMemoryPressure(): Promise<string> {\n  return new Promise((resolve, reject) => {\n    execFile(\n      \"/usr/bin/memory_pressure\",\n      [\"-Q\"],\n      { encoding: \"utf8\", timeout: 1_000 },\n      (error, stdout) => {\n        if (error) {\n          reject(error);\n          return;\n        }\n        resolve(stdout);\n      },\n    );\n  });\n}\n\nexport async function readMacMemoryPressureFreeRatio(\n  run: () => Promise<string> = runMacMemoryPressure,\n): Promise<number | undefined> {\n  try {\n    return parseMacMemoryPressureFreeRatio(await run());\n  } catch {\n    return undefined;\n  }\n}\n\nexport class MacMemoryPressureProbe {\n  private current: number | undefined;\n  private pending = false;\n\n  constructor(private readonly read: () => Promise<number | undefined>) {}\n\n  refresh(): void {\n    if (this.pending) return;\n    this.pending = true;\n    void this.read()\n      .then((ratio) => {\n        this.current = ratio;\n      })\n      .finally(() => {\n        this.pending = false;\n      });\n  }\n\n  value(): number | undefined {\n    return this.current;\n  }\n}\n\nexport function hostMemoryFreeRatio(\n  platform: NodeJS.Platform,\n  totalBytes: number,\n  freeBytes: number,\n  macPressureFreeRatio?: number,\n): number | undefined {\n  if (platform === \"darwin\") return macPressureFreeRatio;\n  return totalBytes > 0 ? freeBytes / totalBytes : 1;\n}\n\nfunction worst(levels: HostPressureState[]): HostPressureState {\n  return levels.reduce<HostPressureState>(\n    (acc, level) => (RANK[level] > RANK[acc] ? level : acc),\n    \"ok\",\n  );\n}\n\nfunction schmittLowIsWorse(\n  value: number,\n  previous: HostPressureState,\n  enterElevated: number,\n  leaveElevated: number,\n  enterCritical: number,\n  leaveCritical: number,\n): HostPressureState {\n  if (previous === \"critical\") {\n    if (value < leaveCritical) return \"critical\";\n    if (value < leaveElevated) return \"elevated\";\n    return \"ok\";\n  }\n  if (previous === \"elevated\") {\n    if (value < enterCritical) return \"critical\";\n    if (value < leaveElevated) return \"elevated\";\n    return \"ok\";\n  }\n  if (value < enterCritical) return \"critical\";\n  if (value < enterElevated) return \"elevated\";\n  return \"ok\";\n}\n\nfunction schmittHighIsWorse(\n  value: number,\n  previous: HostPressureState,\n  enterElevated: number,\n  leaveElevated: number,\n  enterCritical: number,\n  leaveCritical: number,\n): HostPressureState {\n  if (previous === \"critical\") {\n    if (value > leaveCritical) return \"critical\";\n    if (value > leaveElevated) return \"elevated\";\n    return \"ok\";\n  }\n  if (previous === \"elevated\") {\n    if (value > enterCritical) return \"critical\";\n    if (value > leaveElevated) return \"elevated\";\n    return \"ok\";\n  }\n  if (value > enterCritical) return \"critical\";\n  if (value > enterElevated) return \"elevated\";\n  return \"ok\";\n}\n\nfunction timesTotal(times: CpuTimesSnapshot): number {\n  return times.user + times.nice + times.sys + times.idle + times.irq;\n}\n\n/** Node reports win32 for every Windows build. Other platforms are omitted on the wire. */\nexport function hostPressureOs(platform: NodeJS.Platform): HostPressureOs | undefined {\n  if (platform === \"darwin\" || platform === \"linux\" || platform === \"win32\") return platform;\n  return undefined;\n}\n\n/** Fraction of CPU that was busy between two os.cpus() snapshots. 0 when there is no delta. */\nexport function cpuBusyRatio(\n  previous: readonly CpuTimesSnapshot[] | null,\n  next: readonly CpuTimesSnapshot[],\n): number {\n  if (!previous || previous.length === 0 || next.length === 0 || previous.length !== next.length) {\n    return 0;\n  }\n  let idle = 0;\n  let total = 0;\n  for (let i = 0; i < next.length; i++) {\n    const dt = timesTotal(next[i]) - timesTotal(previous[i]);\n    if (dt <= 0) continue;\n    total += dt;\n    idle += Math.max(0, next[i].idle - previous[i].idle);\n  }\n  if (total <= 0) return 0;\n  return 1 - idle / total;\n}\n\nexport function classifyHostPressure(\n  sample: HostSample,\n  previous: HostPressureState,\n  platform: NodeJS.Platform,\n): HostPressureClassification {\n  const mem =\n    sample.memFreeRatio === undefined\n      ? \"ok\"\n      : schmittLowIsWorse(\n          sample.memFreeRatio,\n          previous,\n          HOST_PRESSURE_BARS.memFreeRatio.enterElevated,\n          HOST_PRESSURE_BARS.memFreeRatio.leaveElevated,\n          HOST_PRESSURE_BARS.memFreeRatio.enterCritical,\n          HOST_PRESSURE_BARS.memFreeRatio.leaveCritical,\n        );\n  const eventLoop = schmittHighIsWorse(\n    sample.eventLoopP99Ms,\n    previous,\n    HOST_PRESSURE_BARS.eventLoopP99Ms.enterElevated,\n    HOST_PRESSURE_BARS.eventLoopP99Ms.leaveElevated,\n    HOST_PRESSURE_BARS.eventLoopP99Ms.enterCritical,\n    HOST_PRESSURE_BARS.eventLoopP99Ms.leaveCritical,\n  );\n  const ncpu = sample.ncpu > 0 ? sample.ncpu : 1;\n  // Node reports win32 for every Windows build (ia32, x64, arm64). There is no win64.\n  const windows = platform === \"win32\";\n  const loadValue = windows ? (sample.cpuBusyRatio ?? 0) : sample.load1 / ncpu;\n  const loadBars = windows ? HOST_PRESSURE_BARS.cpuBusy : HOST_PRESSURE_BARS.loadPerCpu;\n  const load = schmittHighIsWorse(\n    loadValue,\n    previous,\n    loadBars.enterElevated,\n    loadBars.leaveElevated,\n    loadBars.enterCritical,\n    loadBars.leaveCritical,\n  );\n\n  const resourceLevel = worst([mem, eventLoop, load]);\n  const agentsPair =\n    sample.liveAgents >= HOST_PRESSURE_BARS.liveAgentsPair && resourceLevel !== \"ok\";\n  const level = resourceLevel;\n\n  const firing: HostPressureReason[] = [];\n  if (mem !== \"ok\") firing.push(\"memory\");\n  if (eventLoop !== \"ok\") firing.push(\"event_loop\");\n  if (load !== \"ok\") firing.push(\"load\");\n  if (agentsPair) firing.push(\"agents\");\n  const reasons = REASON_ORDER.filter((reason) => firing.includes(reason));\n\n  return { level, reasons };\n}\n\nexport type HostPressureMonitorOpts = {\n  wsHub: Pick<WSHub, \"broadcast\">;\n  readSample: () => HostSample;\n  now?: () => Date;\n  platform?: NodeJS.Platform;\n  histogram?: EventLoopHistogram;\n  intervalMs?: number;\n};\n\nexport class HostPressureMonitor {\n  private timer: ReturnType<typeof setInterval> | null = null;\n  private level: HostPressureState = \"ok\";\n  private lastWarning: HostPressureWsMessage | null = null;\n\n  constructor(private readonly opts: HostPressureMonitorOpts) {}\n\n  start(): void {\n    this.opts.histogram?.enable();\n    if (this.timer) return;\n    const intervalMs = this.opts.intervalMs ?? HOST_PRESSURE_SAMPLE_MS;\n    this.timer = setInterval(() => this.tick(), intervalMs);\n    this.timer.unref?.();\n  }\n\n  tick(): void {\n    const sample = this.opts.readSample();\n    const platform = this.opts.platform ?? process.platform;\n    const classified = classifyHostPressure(sample, this.level, platform);\n    if (classified.level === this.level) return;\n\n    this.level = classified.level;\n    const updatedAt = (this.opts.now ?? (() => new Date()))().toISOString();\n    if (classified.level === \"ok\") {\n      this.lastWarning = null;\n      this.opts.wsHub.broadcast({ type: \"host_pressure_cleared\", updatedAt });\n      return;\n    }\n\n    const os = hostPressureOs(platform);\n    const message: HostPressureWsMessage = {\n      type: \"host_pressure\",\n      level: classified.level,\n      reasons: classified.reasons,\n      liveAgents: sample.liveAgents,\n      updatedAt,\n      ...(os ? { os } : {}),\n    };\n    this.lastWarning = message;\n    this.opts.wsHub.broadcast(message);\n  }\n\n  wsMessage(): HostPressureWsMessage | null {\n    return this.lastWarning;\n  }\n\n  dispose(): void {\n    if (this.timer) {\n      clearInterval(this.timer);\n      this.timer = null;\n    }\n    this.opts.histogram?.disable();\n  }\n}\n\nexport function createHostPressureMonitor(\n  wsHub: WSHub,\n  liveAgents: () => number,\n): HostPressureMonitor {\n  const histogram = monitorEventLoopDelay({ resolution: 20 });\n  const platform = process.platform;\n  const windows = platform === \"win32\";\n  const macMemoryPressure =\n    platform === \"darwin\" ? new MacMemoryPressureProbe(readMacMemoryPressureFreeRatio) : null;\n  let prevCpuTimes: CpuTimesSnapshot[] | null = null;\n  macMemoryPressure?.refresh();\n  const monitor = new HostPressureMonitor({\n    wsHub,\n    histogram,\n    readSample: () => {\n      const eventLoopP99Ms = histogram.percentile(99) / 1e6;\n      histogram.reset();\n      const total = platform === \"darwin\" ? 0 : totalmem();\n      const free = platform === \"darwin\" ? 0 : freemem();\n      const cpuList = cpus();\n      const sample: HostSample = {\n        liveAgents: liveAgents(),\n        memFreeRatio: hostMemoryFreeRatio(platform, total, free, macMemoryPressure?.value()),\n        eventLoopP99Ms,\n        load1: loadavg()[0],\n        ncpu: cpuList.length,\n      };\n      macMemoryPressure?.refresh();\n      if (windows) {\n        const cpuTimes = cpuList.map((cpu) => cpu.times);\n        sample.cpuBusyRatio = cpuBusyRatio(prevCpuTimes, cpuTimes);\n        prevCpuTimes = cpuTimes;\n      }\n      return sample;\n    },\n  });\n  monitor.start();\n  return monitor;\n}\n","import {\n  type NotificationPrefs,\n  NotificationPrefsSchema,\n  type QuietHours,\n  type QuietWindow,\n  WEEKDAYS,\n  type Weekday,\n} from \"../../schemas/notification-prefs.schema\";\n\n/** The push kinds a preference can gate. */\nexport type PushEvent = \"waitingInput\" | \"sessionFailed\";\n\n/**\n * Read a stored preference blob.\n *\n * NULL and anything that no longer parses both come back `null`, which the send\n * path treats as \"everything on\". Failing open is deliberate: a corrupt row\n * must not silently mute the one notification the user is waiting for.\n */\nexport function parseStoredPrefs(raw: string | null): NotificationPrefs | null {\n  if (raw == null) return null;\n  try {\n    const parsed = NotificationPrefsSchema.safeParse(JSON.parse(raw));\n    return parsed.success ? parsed.data : null;\n  } catch {\n    return null;\n  }\n}\n\nfunction toMinutes(hhmm: string): number {\n  const [h, m] = hhmm.split(\":\").map(Number);\n  return h * 60 + m;\n}\n\n/** The weekday and minute-of-day `now` falls on in `tz`. */\nfunction localClock(now: Date, tz: string): { day: number; minutes: number } {\n  const parts = new Intl.DateTimeFormat(\"en-US\", {\n    timeZone: tz,\n    weekday: \"short\",\n    hour: \"2-digit\",\n    minute: \"2-digit\",\n    hourCycle: \"h23\",\n  }).formatToParts(now);\n  const get = (type: string) => parts.find((p) => p.type === type)?.value ?? \"\";\n  const key = get(\"weekday\").slice(0, 3).toLowerCase() as Weekday;\n  return { day: WEEKDAYS.indexOf(key), minutes: Number(get(\"hour\")) * 60 + Number(get(\"minute\")) };\n}\n\n/** The window that STARTS on weekday index `day`, or null for none. */\nfunction windowStartingOn(qh: QuietHours, day: number): QuietWindow | null {\n  const override = qh.days?.[WEEKDAYS[day]];\n  // `undefined` = no override for that day; `null` = explicitly no quiet hours.\n  return override === undefined ? qh.default : override;\n}\n\n/**\n * Is `now` inside the user's quiet hours?\n *\n * A window that ends earlier than it starts runs overnight and belongs to the\n * day it starts on: Friday 22:00–08:00 covers Saturday until 08:00 and uses\n * Friday's entry, not Saturday's. `from === to` is an empty window, not 24h —\n * \"always quiet\" is what turning the notification off is for.\n */\nexport function isQuietNow(qh: QuietHours, now: Date): boolean {\n  if (!qh.enabled) return false;\n  const { day, minutes } = localClock(now, qh.tz);\n\n  const today = windowStartingOn(qh, day);\n  if (today) {\n    const from = toMinutes(today.from);\n    const to = toMinutes(today.to);\n    if (from < to && minutes >= from && minutes < to) return true;\n    if (from > to && minutes >= from) return true;\n  }\n\n  // The tail of an overnight window that began yesterday.\n  const yesterday = windowStartingOn(qh, (day + 6) % 7);\n  if (yesterday) {\n    const from = toMinutes(yesterday.from);\n    const to = toMinutes(yesterday.to);\n    if (from > to && minutes < to) return true;\n  }\n  return false;\n}\n\n/** Should a push of this kind reach a device with these preferences right now? */\nexport function allows(prefs: NotificationPrefs | null, event: PushEvent, now: Date): boolean {\n  if (prefs === null) return true;\n  if (!prefs[event]) return false;\n  return !(prefs.quietHours && isQuietNow(prefs.quietHours, now));\n}\n","import type { PushRepository, PushTokenRow } from \"../../db/repositories/push.repository\";\nimport { getLogger } from \"../../logger\";\nimport { allows, type PushEvent, parseStoredPrefs } from \"./notificationPrefs\";\n\n/**\n * Ordinary push notifications, sent through Expo's relay.\n *\n * Deliberately not the APNs path. `ApnsClient` signs with a `.p8` for\n * `${bundleId}.push-type.liveactivity`, and Apple issues those keys per\n * developer team — a self-hosted streamer cannot sign for the published app's\n * bundle id, so anything built on it works only for the maintainer. Expo holds\n * the APNs and FCM credentials for the app, so any streamer can send with a\n * plain POST, no Apple credential and one code path for both platforms.\n *\n * Self-hosting is the primary deployment, which is what makes this the\n * transport for ordinary notifications.\n */\n\nconst log = getLogger(\"expo-push\");\n\nexport const EXPO_PUSH_ENDPOINT = \"https://exp.host/--/api/v2/push/send\";\n\n/** Expo accepts at most 100 messages per request. */\nexport const EXPO_PUSH_BATCH_SIZE = 100;\n\nexport interface ExpoPushMessage {\n  title: string;\n  body: string;\n  /** Delivered to the app as `notification.request.content.data`. */\n  data: Record<string, string>;\n  /** iOS plays nothing unless this is set — \"default\" is the device sound. */\n  sound?: \"default\";\n  /** \"high\" wakes an Android device immediately instead of batching. */\n  priority?: \"default\" | \"normal\" | \"high\";\n  /** iOS: groups one session's notifications into a single stack. */\n  threadId?: string;\n  /** iOS + Android: a newer push with the same id replaces the shown one. */\n  collapseId?: string;\n  /** Android: the same replacement, for an already-displayed notification. */\n  tag?: string;\n}\n\n/** A message fixed for every device, or one built per device's language. */\nexport type ExpoPushContent = ExpoPushMessage | ((locale: string | null) => ExpoPushMessage);\n\nexport interface ExpoPushOutcome {\n  /** Tokens the send was actually attempted for, after preferences were applied. */\n  attempted: number;\n  succeeded: number;\n  /** Tokens Expo rejected as permanently dead, now revoked locally. */\n  retired: number;\n  /** Deliverable tokens skipped because their owner's preferences said not to send. */\n  suppressed: number;\n}\n\n/** One entry of Expo's `data` array, positionally matched to the request. */\ninterface ExpoPushTicket {\n  status?: string;\n  message?: string;\n  details?: { error?: string };\n}\n\n/**\n * The one ticket error that means \"this token will never work again\" — the app\n * was uninstalled, or the token rotated. Every other error (rate limit,\n * message too big, a relay hiccup) is transient and only counts toward the\n * repository's failure streak.\n */\nconst DEAD_TOKEN_ERROR = \"DeviceNotRegistered\";\n\nexport class ExpoPushSender {\n  /**\n   * @param accessToken Expo access token, when the project has enhanced\n   * security enabled. Optional on purpose: a self-hoster does not own the Expo\n   * project and cannot obtain one, so requiring it would break the deployment\n   * this transport exists to serve.\n   */\n  constructor(\n    private readonly repo: PushRepository,\n    private readonly accessToken?: string,\n  ) {}\n\n  /**\n   * Send one message to every deliverable Expo token whose preferences allow it.\n   *\n   * Sends are independent: Expo returns a ticket per token in one response, so\n   * a dead device is recorded against its own row and never silences the other\n   * devices in the batch. The same holds for preferences — they are applied per\n   * token, so one device's quiet hours never mute another phone.\n   *\n   * `event` names what the push is about so the right toggle applies; leaving\n   * it out sends to every deliverable token regardless of preferences.\n   */\n  async send(\n    message: ExpoPushContent,\n    opts: { event?: PushEvent; now?: number } = {},\n  ): Promise<ExpoPushOutcome> {\n    const now = opts.now ?? Date.now();\n    const deliverable = this.repo.listDeliverable();\n    const { event } = opts;\n    const rows = event\n      ? deliverable.filter((row) =>\n          allows(parseStoredPrefs(row.notification_prefs), event, new Date(now)),\n        )\n      : deliverable;\n    return this.deliver(rows, message, now, deliverable.length - rows.length);\n  }\n\n  /**\n   * Send to exactly these tokens, ignoring preferences.\n   *\n   * For a push the user asked for by name (the settings screen's test button):\n   * muting it because their own quiet hours are on would answer \"does delivery\n   * work?\" with silence.\n   */\n  sendTo(\n    rows: PushTokenRow[],\n    message: ExpoPushContent,\n    now: number = Date.now(),\n  ): Promise<ExpoPushOutcome> {\n    return this.deliver(rows, message, now, 0);\n  }\n\n  private async deliver(\n    rows: PushTokenRow[],\n    message: ExpoPushContent,\n    now: number,\n    suppressed: number,\n  ): Promise<ExpoPushOutcome> {\n    const outcome: ExpoPushOutcome = {\n      attempted: rows.length,\n      succeeded: 0,\n      retired: 0,\n      suppressed,\n    };\n    if (rows.length === 0) return outcome;\n\n    for (let i = 0; i < rows.length; i += EXPO_PUSH_BATCH_SIZE) {\n      const chunk = rows.slice(i, i + EXPO_PUSH_BATCH_SIZE);\n      await this.sendChunk(chunk, message, now, outcome);\n    }\n    return outcome;\n  }\n\n  private async sendChunk(\n    rows: PushTokenRow[],\n    content: ExpoPushContent,\n    now: number,\n    outcome: ExpoPushOutcome,\n  ): Promise<void> {\n    let payload: unknown;\n    try {\n      const res = await fetch(EXPO_PUSH_ENDPOINT, {\n        method: \"POST\",\n        headers: {\n          \"content-type\": \"application/json\",\n          accept: \"application/json\",\n          ...(this.accessToken && { authorization: `Bearer ${this.accessToken}` }),\n        },\n        body: JSON.stringify(\n          rows.map((row) => {\n            const message = typeof content === \"function\" ? content(row.locale) : content;\n            return {\n              to: row.token,\n              ...message,\n              // Per token, not per message: the same push token is registered\n              // with every server the phone pairs, and each server stores the id\n              // the app files *it* under. A token with none (an older client)\n              // gets no serverId, so the app uses its default server instead of\n              // failing to resolve a name it has no entry for.\n              data: row.client_server_id\n                ? { ...message.data, serverId: row.client_server_id }\n                : message.data,\n            };\n          }),\n        ),\n      });\n      if (!res.ok) {\n        // A request-level rejection (bad access token, malformed batch) says\n        // nothing about any individual device, so no token is retired here.\n        const code = `HTTP_${res.status}`;\n        for (const row of rows) this.repo.recordFailure(row.token, code, now);\n        log.warn(\"expo_push.request_rejected\", {\n          event: \"expo_push.request_rejected\",\n          status: res.status,\n          tokens: rows.length,\n        });\n        return;\n      }\n      payload = await res.json();\n    } catch (err) {\n      // Never swallowed: without this the user simply stops being told their\n      // turn is up, and there is nothing anywhere to explain why.\n      for (const row of rows) this.repo.recordFailure(row.token, \"SendError\", now);\n      log.error(\"expo_push.send_failed\", {\n        event: \"expo_push.send_failed\",\n        tokens: rows.length,\n        err: String(err),\n      });\n      return;\n    }\n\n    const tickets = (payload as { data?: ExpoPushTicket[] } | null)?.data;\n    rows.forEach((row, index) => {\n      const ticket = Array.isArray(tickets) ? tickets[index] : undefined;\n      if (!ticket) {\n        // Expo returns one ticket per message, positionally. A short array\n        // means we cannot say this device was reached, so it is a failure.\n        this.repo.recordFailure(row.token, \"NoTicket\", now);\n        return;\n      }\n      if (ticket.status === \"ok\") {\n        this.repo.recordSuccess(row.token, now);\n        outcome.succeeded += 1;\n        return;\n      }\n\n      const code = ticket.details?.error ?? \"PushError\";\n      this.repo.recordFailure(row.token, code, now);\n      if (code === DEAD_TOKEN_ERROR) {\n        // Retire rather than retry, mirroring how LiveActivitySender expires a\n        // dead APNs token: this one fails forever, and retrying it makes the\n        // health report read \"failing\" when the device is simply gone.\n        this.repo.revoke(row.token, now);\n        outcome.retired += 1;\n      }\n      log.warn(\"expo_push.send_rejected\", {\n        event: \"expo_push.send_rejected\",\n        code,\n        message: ticket.message,\n        retired: code === DEAD_TOKEN_ERROR,\n      });\n    });\n  }\n}\n","/**\n * The Live Activity content-state contract, shared with tb-mobile.\n *\n * tb-mobile defines the mirror of this shape in `types/live-activity.ts` and\n * decodes it with a Swift `Codable` ActivityAttributes struct. A decode failure\n * in ActivityKit is silent — the Lock Screen surface simply stops updating,\n * with no error anywhere on the server — so a field name or type drifting apart\n * from mobile's struct breaks the feature invisibly. Treat this file as a\n * contract, not an internal type: changing it requires a coordinated tb-mobile\n * change.\n */\n\n/**\n * Statuses the surface renders. Deliberately narrower than the server's\n * `SessionStatus`: `idle` has no Live Activity representation, because a session\n * that is no longer live gets an `end` event rather than an update carrying a\n * third status mobile has no case for.\n */\nexport type LiveActivityStatus = \"running\" | \"waiting_input\";\n\n/** Longest `lastOutput` sent. Mobile truncates to the same bound. */\nexport const LAST_OUTPUT_MAX_LENGTH = 90;\n\nexport interface LiveActivityContentState {\n  sessionId: string;\n  serverId: string;\n  projectName: string;\n  status: LiveActivityStatus;\n  /**\n   * Epoch **milliseconds** of the session's start.\n   *\n   * iOS renders its own ticking elapsed timer from this, so the server must\n   * never send a precomputed elapsed value and never push per-second updates.\n   * Carried unchanged across a renewal — a fresh value makes the user's visible\n   * timer reset to zero.\n   */\n  startedAt: number;\n  lastOutput: string;\n  serverLabel?: string;\n  /**\n   * User-visible session title, derived from the first user message.\n   *\n   * Absent until the first message is submitted (a session between spawn and\n   * that point has no title yet); mobile falls back to `projectName` when unset.\n   */\n  sessionName?: string;\n}\n\n/**\n * Collapse a server session status onto the two the surface renders.\n *\n * Returns null for a status with no representation, which is the signal to send\n * an `end` rather than an update.\n */\nexport function toLiveActivityStatus(status: string): LiveActivityStatus | null {\n  return status === \"running\" || status === \"waiting_input\" ? status : null;\n}\n\n/**\n * Trim terminal output to the field's bound.\n *\n * Terminal output is unbounded and the whole APNs payload must stay under 4 KB,\n * so this is what keeps a chatty session from producing a rejected push.\n * Newlines collapse to spaces because the surface renders a single line.\n */\nexport function truncateLastOutput(raw: string): string {\n  const oneLine = raw.replace(/\\s+/g, \" \").trim();\n  return oneLine.length <= LAST_OUTPUT_MAX_LENGTH\n    ? oneLine\n    : oneLine.slice(0, LAST_OUTPUT_MAX_LENGTH);\n}\n","import { getLogger } from \"../../logger\";\nimport type { ManagedSession } from \"../../types\";\nimport {\n  type LiveActivityContentState,\n  toLiveActivityStatus,\n  truncateLastOutput,\n} from \"./liveActivityContentState\";\nimport type { LiveActivitySender } from \"./liveActivitySender\";\n\n/**\n * Bridge from the session lifecycle to Live Activity pushes.\n *\n * `onStatusChange` in server.ts is the one funnel every status transition passes\n * through, for both the Claude and Codex runners, so hooking here covers every\n * path rather than one runner's. This module exists to keep that callback a\n * single call and to hold the \"which transitions are worth a push\" rule in one\n * testable place.\n *\n * Per-turn, not per-session: a turn opens on `waiting_input → running` (the\n * user sent a prompt) and closes on the matching `running → waiting_input`\n * (the response — including sub-agents, invisible to this signal since they\n * run inside the same `running` span — finished). A session's very first\n * `running`, right after spawn with no prior `waiting_input`, opens nothing —\n * that's what keeps a freshly booted or idling session from showing an\n * activity before the user has asked for anything. See\n * docs/guides/live-activity-push.md for the full contract.\n */\n\nconst log = getLogger(\"live-activity\");\n\n/**\n * Build the content state mobile decodes.\n *\n * `startedAt` comes from the session's own start and is passed through in epoch\n * milliseconds: iOS renders its own ticking timer from it, so the server must\n * never compute an elapsed value.\n */\nexport function contentStateForSession(args: {\n  session: ManagedSession;\n  serverId: string;\n  serverLabel?: string;\n  /** Overrides the session's own start — used by a renewal to preserve continuity. */\n  startedAtOverride?: number | null;\n}): LiveActivityContentState | null {\n  const status = toLiveActivityStatus(args.session.status);\n  // No representation for a non-live status: that case is an `end`, not an\n  // update carrying a status mobile has no case for.\n  if (!status) return null;\n\n  return {\n    sessionId: args.session.id,\n    serverId: args.serverId,\n    projectName: args.session.projectName,\n    status,\n    startedAt: args.startedAtOverride ?? args.session.startedAt.getTime(),\n    lastOutput: truncateLastOutput(args.session.lastOutput ?? \"\"),\n    ...(args.serverLabel != null && { serverLabel: args.serverLabel }),\n    ...(args.session.sessionName != null && { sessionName: args.session.sessionName }),\n  };\n}\n\nexport class LiveActivityNotifier {\n  /**\n   * Sessions with a currently open (pushed) activity.\n   *\n   * An activity opens on a `waiting_input → running` edge (the user sent a\n   * prompt) and closes on the matching `running → waiting_input` edge (the\n   * response, including any sub-agents, finished) — so this set is what makes\n   * the notifier per-turn rather than per-session. A session's very first\n   * `running` (right after spawn, before any user prompt) has no prior\n   * `waiting_input` and therefore no edge, so it never opens an activity —\n   * this is what keeps a fresh/idle session from pushing anything.\n   */\n  private openActivity = new Map<string, { sessionNameSent: boolean }>();\n\n  constructor(\n    private readonly sender: LiveActivitySender,\n    private readonly serverId: string,\n    private readonly serverLabel?: string,\n  ) {}\n\n  /**\n   * React to a session status change.\n   *\n   * Fire-and-forget by design: a push must never delay or fail a session\n   * transition, so this returns a promise the caller may ignore and every error\n   * is logged rather than propagated.\n   */\n  async onStatusChange(session: ManagedSession, previousStatus?: string): Promise<void> {\n    const status = toLiveActivityStatus(session.status);\n\n    try {\n      if (!status) {\n        // Anything not renderable means the session is no longer live. If a\n        // turn was still open (mid-turn PTY death), end it so a renewal can't\n        // resurrect it; otherwise there is nothing to close.\n        if (this.openActivity.has(session.id)) await this.endFor(session);\n        return;\n      }\n\n      if (status === \"running\" && previousStatus === \"waiting_input\") {\n        await this.startTurn(session);\n        return;\n      }\n\n      if (status === \"waiting_input\" && previousStatus === \"running\") {\n        // Only a real turn end if one was actually opened — the session's\n        // very first waiting_input (boot ready, no prior turn) has none.\n        if (this.openActivity.has(session.id)) await this.endFor(session);\n        return;\n      }\n\n      // Any other same-status re-emit: refresh the name if it has just become\n      // available on an already-open turn (the first message races the title\n      // derivation, so the name can land after the turn already started).\n      await this.maybeSendName(session);\n    } catch (err) {\n      // A push failure must not surface as a session-transition failure, but it\n      // must not vanish either.\n      log.error(\"live_activity.notify_failed\", {\n        event: \"live_activity.notify_failed\",\n        sessionId: session.id,\n        status: session.status,\n        err: String(err),\n      });\n    }\n  }\n\n  private async startTurn(session: ManagedSession): Promise<void> {\n    const contentState = contentStateForSession({\n      session,\n      serverId: this.serverId,\n      serverLabel: this.serverLabel,\n    });\n    if (!contentState) return;\n\n    const outcome = await this.sender.send({\n      sessionId: session.id,\n      event: \"update\",\n      contentState,\n    });\n    this.openActivity.set(session.id, { sessionNameSent: session.sessionName != null });\n    if (outcome.attempted > 0) {\n      log.info(\"live_activity.updated\", {\n        event: \"live_activity.updated\",\n        sessionId: session.id,\n        status: contentState.status,\n        ...outcome,\n      });\n    }\n  }\n\n  private async maybeSendName(session: ManagedSession): Promise<void> {\n    const open = this.openActivity.get(session.id);\n    if (!open || open.sessionNameSent || session.sessionName == null) return;\n\n    const contentState = contentStateForSession({\n      session,\n      serverId: this.serverId,\n      serverLabel: this.serverLabel,\n    });\n    if (!contentState) return;\n\n    const outcome = await this.sender.send({\n      sessionId: session.id,\n      event: \"update\",\n      contentState,\n    });\n    open.sessionNameSent = true;\n    if (outcome.attempted > 0) {\n      log.info(\"live_activity.updated\", {\n        event: \"live_activity.updated\",\n        sessionId: session.id,\n        status: contentState.status,\n        ...outcome,\n      });\n    }\n  }\n\n  private async endFor(session: ManagedSession): Promise<void> {\n    this.openActivity.delete(session.id);\n\n    // Carry the last renderable status through the end event: the content state\n    // is required by the payload, and `idle` has no encoding mobile understands.\n    const status = toLiveActivityStatus(session.status);\n    const contentState = contentStateForSession({\n      session: { ...session, status: status ?? \"waiting_input\" },\n      serverId: this.serverId,\n      serverLabel: this.serverLabel,\n    });\n    if (!contentState) return;\n\n    const outcome = await this.sender.end({ sessionId: session.id, contentState });\n    if (outcome.attempted > 0) {\n      log.info(\"live_activity.ended\", {\n        event: \"live_activity.ended\",\n        sessionId: session.id,\n        ...outcome,\n      });\n    }\n  }\n\n  /** Drop cached state for a session, so a resume re-opens on its next turn. */\n  forget(sessionId: string): void {\n    this.openActivity.delete(sessionId);\n  }\n}\n","import type { PushRepository, PushTokenRow } from \"../../db/repositories/push.repository\";\nimport { getLogger } from \"../../logger\";\nimport type { ApnsClient, ApnsSendResult } from \"./apnsClient\";\nimport type { LiveActivityContentState } from \"./liveActivityContentState\";\n\n/**\n * Live Activity push sender.\n *\n * Turns a session lifecycle change into an ActivityKit push. Owns the `aps`\n * envelope and the per-token bookkeeping — which token to send to, and what to\n * do when APNs rejects one — so the session lifecycle callback stays a one-line\n * call.\n */\n\nconst log = getLogger(\"live-activity\");\n\n/** iOS ends a Live Activity ~8h after it starts. */\nexport const ACTIVITY_MAX_LIFETIME_MS = 8 * 60 * 60 * 1000;\n\nexport type LiveActivityEvent = \"update\" | \"end\";\n\nexport interface LiveActivitySendOutcome {\n  attempted: number;\n  succeeded: number;\n  /** Tokens APNs rejected as permanently unusable, now expired locally. */\n  retired: number;\n}\n\n/**\n * The APNs `aps` envelope for an ActivityKit push.\n *\n * Key names are Apple's and are not ours to choose: `content-state`,\n * `stale-date`, and `dismissal-date` are hyphenated, while `timestamp` and\n * `event` are not.\n */\ninterface ActivityKitPayload {\n  aps: {\n    /** Seconds since epoch. iOS discards a push whose timestamp regresses. */\n    timestamp: number;\n    event: LiveActivityEvent;\n    \"content-state\": LiveActivityContentState;\n    /** Seconds since epoch. When iOS should start showing the content as stale. */\n    \"stale-date\"?: number;\n    \"dismissal-date\"?: number;\n  };\n}\n\nexport function buildActivityKitPayload(args: {\n  event: LiveActivityEvent;\n  contentState: LiveActivityContentState;\n  now: number;\n  staleDate?: number | null;\n  dismissalDate?: number | null;\n}): ActivityKitPayload {\n  return {\n    aps: {\n      timestamp: Math.floor(args.now / 1000),\n      event: args.event,\n      \"content-state\": args.contentState,\n      ...(args.staleDate != null && { \"stale-date\": Math.floor(args.staleDate / 1000) }),\n      ...(args.dismissalDate != null && {\n        \"dismissal-date\": Math.floor(args.dismissalDate / 1000),\n      }),\n    },\n  };\n}\n\nexport class LiveActivitySender {\n  constructor(\n    private readonly apns: ApnsClient,\n    private readonly repo: PushRepository,\n  ) {}\n\n  /**\n   * Push to every live activity of a session.\n   *\n   * Sends are independent: one rejected token must not stop the others, because\n   * a single dead device would otherwise silence every other device watching the\n   * same session.\n   */\n  async send(args: {\n    sessionId: string;\n    event: LiveActivityEvent;\n    contentState: LiveActivityContentState;\n    now?: number;\n    priority?: 5 | 10;\n  }): Promise<LiveActivitySendOutcome> {\n    const now = args.now ?? Date.now();\n    return this.sendToTokens({\n      tokens: this.repo.listForSession(\"liveactivity_update\", args.sessionId, now),\n      sessionId: args.sessionId,\n      event: args.event,\n      contentState: args.contentState,\n      now,\n      priority: args.priority,\n    });\n  }\n\n  /**\n   * Push to an explicit token list.\n   *\n   * Renewal needs this: a replacement activity does not exist yet, so it is\n   * started via the app-wide push-to-start token rather than any per-session\n   * lookup. Shares one fan-out body with `send()` so failure handling cannot\n   * drift between the two paths.\n   */\n  async sendToTokens(args: {\n    tokens: PushTokenRow[];\n    sessionId: string;\n    event: LiveActivityEvent;\n    contentState: LiveActivityContentState;\n    now?: number;\n    priority?: 5 | 10;\n    /** Overrides the row's own cap — renewal sets the replacement's window. */\n    staleDate?: number | null;\n  }): Promise<LiveActivitySendOutcome> {\n    const now = args.now ?? Date.now();\n    const tokens = args.tokens;\n    const outcome: LiveActivitySendOutcome = {\n      attempted: tokens.length,\n      succeeded: 0,\n      retired: 0,\n    };\n    if (tokens.length === 0) return outcome;\n\n    const results = await Promise.all(\n      tokens.map((row) =>\n        this.sendToToken(row, args.event, args.contentState, now, args.priority, args.staleDate),\n      ),\n    );\n\n    for (const { row, result, error } of results) {\n      if (error) {\n        // Never swallowed: without this the surface just stops updating and\n        // there is nothing anywhere to explain why.\n        log.error(\"live_activity.send_failed\", {\n          event: \"live_activity.send_failed\",\n          sessionId: args.sessionId,\n          activityId: row.activity_id,\n          apnsEvent: args.event,\n          err: String(error),\n        });\n        this.repo.recordFailure(row.token, \"SendError\", now);\n        continue;\n      }\n      if (!result) continue;\n\n      if (result.ok) {\n        this.repo.recordSuccess(row.token, now);\n        outcome.succeeded += 1;\n        continue;\n      }\n\n      this.repo.recordFailure(row.token, result.reason ?? `HTTP_${result.status}`, now);\n      if (result.tokenDead) {\n        // Retire rather than retry. A dead token fails forever, and retrying it\n        // makes the health report say \"failing\" when the truth is the device or\n        // activity is gone.\n        this.repo.expire(row.token, now);\n        outcome.retired += 1;\n      }\n      log.warn(\"live_activity.send_rejected\", {\n        event: \"live_activity.send_rejected\",\n        sessionId: args.sessionId,\n        activityId: row.activity_id,\n        apnsEvent: args.event,\n        status: result.status,\n        reason: result.reason,\n        tokenDead: result.tokenDead,\n      });\n    }\n\n    return outcome;\n  }\n\n  /**\n   * End every live activity for a session and stop tracking them.\n   *\n   * Expiring locally is what stops the renewal sweep from later resurrecting an\n   * activity for a session that has already finished.\n   */\n  async end(args: {\n    sessionId: string;\n    contentState: LiveActivityContentState;\n    now?: number;\n  }): Promise<LiveActivitySendOutcome> {\n    const now = args.now ?? Date.now();\n    const outcome = await this.send({\n      sessionId: args.sessionId,\n      event: \"end\",\n      contentState: args.contentState,\n      now,\n    });\n    this.repo.expireSessionActivities(args.sessionId, now);\n    return outcome;\n  }\n\n  private async sendToToken(\n    row: PushTokenRow,\n    event: LiveActivityEvent,\n    contentState: LiveActivityContentState,\n    now: number,\n    priority?: 5 | 10,\n    staleDateOverride?: number | null,\n  ): Promise<{ row: PushTokenRow; result?: ApnsSendResult; error?: unknown }> {\n    // A stale-date is only meaningful for an update; an `end` is terminal.\n    const staleDate =\n      event === \"update\"\n        ? (staleDateOverride ?? row.stale_date ?? contentState.startedAt + ACTIVITY_MAX_LIFETIME_MS)\n        : null;\n    try {\n      const result = await this.apns.send({\n        deviceToken: row.token,\n        payload: buildActivityKitPayload({ event, contentState, now, staleDate }),\n        priority,\n      });\n      return { row, result };\n    } catch (error) {\n      return { row, error };\n    }\n  }\n}\n","import type { PushRepository, PushTokenRow } from \"../../db/repositories/push.repository\";\nimport { getLogger } from \"../../logger\";\nimport type { SessionStore } from \"../../session-store\";\nimport { toLiveActivityStatus, truncateLastOutput } from \"./liveActivityContentState\";\nimport { ACTIVITY_MAX_LIFETIME_MS, type LiveActivitySender } from \"./liveActivitySender\";\n\n/**\n * Live Activity renewal scheduler.\n *\n * iOS ends a Live Activity roughly 8 hours after it starts, so a long-running\n * session loses its Lock Screen surface mid-session unless the activity is\n * replaced before the cap. Renewal ends the old activity and starts a\n * replacement.\n *\n * Deadlines live in the DB (`push_tokens.stale_date`) and timers are re-armed on\n * boot. An in-process `setTimeout` alone is not enough: it dies with the process,\n * and a restart inside an 8-hour window would drop every pending renewal\n * silently. Firing is gated by `claimRenewal()` so a re-armed timer cannot\n * double-send.\n *\n * Temporal is deliberately not used here even though `@temporalio/client` is a\n * dependency: it is reachable only under MULTI_AGENT_FLOW, where the PTY path\n * this feature observes does not run. Adding a Temporal dependency to the\n * default path would make Live Activities require a Temporal server.\n */\n\nconst log = getLogger(\"live-activity\");\n\n/**\n * How long before the cap to renew.\n *\n * Far enough out that a missed tick or a slow APNs round trip still lands inside\n * the window, close enough that the replacement activity gets a nearly full\n * 8-hour lifetime of its own.\n */\nexport const RENEWAL_LEAD_MS = 30 * 60 * 1000;\n\n/**\n * Longest a single timer is allowed to sleep.\n *\n * `setTimeout` overflows past ~24.8 days (2^31 ms) and fires immediately, and a\n * long sleep also drifts across a laptop suspend. Long waits are therefore\n * chained in bounded hops, re-reading the deadline each time.\n */\nconst MAX_TIMER_MS = 60 * 60 * 1000;\n\nexport interface LiveActivityRenewalDeps {\n  repo: PushRepository;\n  sender: LiveActivitySender;\n  sessionStore: SessionStore;\n  serverId: string;\n  serverLabel?: string;\n  /** Injected in tests; production uses the real clock. */\n  now?: () => number;\n}\n\n/** When a row should be renewed: its cap, minus the lead time. */\nexport function renewalDueAt(row: PushTokenRow): number | null {\n  return row.stale_date == null ? null : row.stale_date - RENEWAL_LEAD_MS;\n}\n\nexport class LiveActivityRenewalScheduler {\n  private timer: NodeJS.Timeout | null = null;\n  private stopped = false;\n  private readonly now: () => number;\n\n  constructor(private readonly deps: LiveActivityRenewalDeps) {\n    this.now = deps.now ?? (() => Date.now());\n  }\n\n  /**\n   * Arm the scheduler from persisted state.\n   *\n   * Called on boot, which is what makes a renewal survive a restart: the\n   * deadlines were never in memory to begin with.\n   */\n  start(): void {\n    this.stopped = false;\n    void this.tick();\n  }\n\n  stop(): void {\n    this.stopped = true;\n    if (this.timer) {\n      clearTimeout(this.timer);\n      this.timer = null;\n    }\n  }\n\n  /**\n   * Renew everything due, then sleep until the next deadline.\n   *\n   * Re-reads from the DB every tick rather than caching a schedule in memory, so\n   * an activity registered after boot is picked up without re-arming anything.\n   */\n  async tick(): Promise<void> {\n    if (this.stopped) return;\n\n    const now = this.now();\n    try {\n      for (const row of this.deps.repo.listRenewable()) {\n        const dueAt = renewalDueAt(row);\n        if (dueAt == null || dueAt > now) continue;\n        await this.renew(row, now);\n      }\n    } catch (err) {\n      // A failed sweep must not kill the scheduler — the next tick retries.\n      // Silence here would strand every future renewal.\n      log.error(\"live_activity.renewal_sweep_failed\", {\n        event: \"live_activity.renewal_sweep_failed\",\n        err: String(err),\n      });\n    }\n\n    this.scheduleNext();\n  }\n\n  private scheduleNext(): void {\n    if (this.stopped) return;\n\n    const now = this.now();\n    const pending = this.deps.repo\n      .listRenewable()\n      .map(renewalDueAt)\n      .filter((d): d is number => d != null);\n\n    // Nothing pending: still wake up on the hour cap, since an activity may be\n    // registered while we sleep and nothing else re-arms this timer.\n    const nextDue = pending.length > 0 ? Math.min(...pending) : now + MAX_TIMER_MS;\n    const delay = Math.min(Math.max(nextDue - now, 0), MAX_TIMER_MS);\n\n    this.timer = setTimeout(() => void this.tick(), delay);\n    // Never hold the process open for a renewal: shutdown must not wait on it.\n    this.timer.unref?.();\n  }\n\n  /**\n   * Renew one activity.\n   *\n   * Claims first: `claimRenewal()` succeeds exactly once per row, so a timer\n   * re-armed after a restart mid-window cannot send a second time.\n   */\n  private async renew(row: PushTokenRow, now: number): Promise<void> {\n    if (!row.session_id) return;\n\n    // Only renew a session that is genuinely still live. A session that ended\n    // inside the renewal window must not be resurrected — the reason this is\n    // checked here rather than trusted from the row.\n    const session = this.deps.sessionStore.getManaged(row.session_id);\n    const status = session ? toLiveActivityStatus(session.status) : null;\n    if (!session || !status) {\n      // Claim it anyway so the sweep stops reconsidering a row it will never\n      // renew, and expire the token so it is no longer a delivery target.\n      this.deps.repo.claimRenewal(row.token, now);\n      this.deps.repo.expire(row.token, now);\n      log.info(\"live_activity.renewal_skipped\", {\n        event: \"live_activity.renewal_skipped\",\n        sessionId: row.session_id,\n        activityId: row.activity_id,\n        reason: session ? `status_${session.status}` : \"session_gone\",\n      });\n      return;\n    }\n\n    if (!this.deps.repo.claimRenewal(row.token, now)) {\n      // Another tick, or a pre-restart run, already handled this row.\n      return;\n    }\n\n    // THE failure mode this whole path exists to avoid: iOS renders its own\n    // ticking elapsed timer from startedAt, so the replacement must carry the\n    // ORIGINAL start. A fresh value makes the user's visible timer reset to zero.\n    const startedAt = row.started_at ?? session.startedAt.getTime();\n\n    const contentState = {\n      sessionId: session.id,\n      serverId: this.deps.serverId,\n      projectName: session.projectName,\n      status,\n      startedAt,\n      lastOutput: session.lastOutput ?? \"\",\n      ...(this.deps.serverLabel != null && { serverLabel: this.deps.serverLabel }),\n      ...(session.sessionName != null && { sessionName: session.sessionName }),\n    };\n\n    try {\n      // End the old activity, then hand mobile the state to start its\n      // replacement with. The replacement's own update token arrives via\n      // /api/push/register once iOS issues it.\n      await this.deps.sender.send({\n        sessionId: session.id,\n        event: \"end\",\n        contentState: { ...contentState, lastOutput: truncateLastOutput(contentState.lastOutput) },\n        now,\n      });\n      this.deps.repo.expire(row.token, now);\n\n      const started = await this.startReplacement({\n        sessionId: session.id,\n        startedAt,\n        now,\n      });\n\n      log.info(\"live_activity.renewed\", {\n        event: \"live_activity.renewed\",\n        sessionId: session.id,\n        activityId: row.activity_id,\n        // Logged because a regression here is invisible on the server and only\n        // shows up as a reset timer on someone's Lock Screen.\n        startedAt,\n        replacementRequested: started,\n      });\n    } catch (err) {\n      log.error(\"live_activity.renewal_failed\", {\n        event: \"live_activity.renewal_failed\",\n        sessionId: session.id,\n        activityId: row.activity_id,\n        err: String(err),\n      });\n    }\n  }\n\n  /**\n   * Ask the device to start a replacement activity.\n   *\n   * Uses the app-wide push-to-start token, because the replacement does not\n   * exist yet and therefore has no per-activity token. Returns false when the\n   * device never registered one, which is not an error: the app simply cannot be\n   * asked to start an activity remotely, and the next foreground WS update\n   * recreates it.\n   */\n  private async startReplacement(args: {\n    sessionId: string;\n    startedAt: number;\n    now: number;\n  }): Promise<boolean> {\n    const starters = this.deps.repo.listByKind(\"liveactivity_start\", args.now);\n    if (starters.length === 0) return false;\n\n    const session = this.deps.sessionStore.getManaged(args.sessionId);\n    const status = session ? toLiveActivityStatus(session.status) : null;\n    if (!session || !status) return false;\n\n    await this.deps.sender.sendToTokens({\n      tokens: starters,\n      event: \"update\",\n      sessionId: args.sessionId,\n      contentState: {\n        sessionId: session.id,\n        serverId: this.deps.serverId,\n        projectName: session.projectName,\n        status,\n        // Carried through unchanged — the whole point of the renewal.\n        startedAt: args.startedAt,\n        lastOutput: truncateLastOutput(session.lastOutput ?? \"\"),\n        ...(this.deps.serverLabel != null && { serverLabel: this.deps.serverLabel }),\n        ...(session.sessionName != null && { sessionName: session.sessionName }),\n      },\n      now: args.now,\n      staleDate: args.startedAt + ACTIVITY_MAX_LIFETIME_MS,\n    });\n    return true;\n  }\n}\n","import { getLogger } from \"../../logger\";\nimport type { ManagedSession } from \"../../types\";\nimport type { ExpoPushContent, ExpoPushSender } from \"./expoPushSender\";\nimport { type AttentionKind, attentionBody, attentionTitle } from \"./notificationCopy\";\nimport type { PushEvent } from \"./notificationPrefs\";\n\n/**\n * \"Your turn\" notifications.\n *\n * The away-from-desk workflow depends on one moment: the agent finished and is\n * waiting for the user, who put the phone down expecting to be told. This is\n * the module that tells them.\n *\n * Hooked to `onStatusChange` in server.ts for the same reason\n * LiveActivityNotifier is — it is the one funnel every status transition passes\n * through, for both the Claude and Codex runners, so the three `markReady`\n * detectors (prompt-marker, screen-marker, timeout fallback) are all covered\n * without knowing about any of them.\n *\n * Per-turn, not per-status: a turn opens on `waiting_input → running` (the user\n * sent a prompt) and the notification fires on the matching\n * `running → waiting_input`. A session's very first `waiting_input` — boot or\n * resume ready, with no prior turn — opens nothing, so starting a session never\n * notifies the user about the session they just started.\n *\n * The same notifier covers the agent stopping mid-turn for the user — a\n * permission gate or an AskUserQuestion menu. Those keep the turn open (Claude\n * holds its turn signal across them), so without a push of their own an\n * away-from-desk user would never learn the agent was blocked on them. One\n * push per prompt: repaints and cursor moves of the same prompt are not new.\n *\n * A session that dies at birth — the process exits at once, or Codex refuses to\n * start — gets one push of its own, since otherwise a user who started it and\n * walked away sees nothing. The text never carries `failureReason`, which\n * embeds project paths. Codex usage-limit screens are not this: they already\n * arrive as a permission prompt.\n *\n * Every push is gated by the receiving device's own preferences, per token, in\n * ExpoPushSender: \"waitingInput\" for the three kinds that mean the agent needs\n * the user, \"sessionFailed\" for the failure push.\n *\n * Always notifies for a closed turn, including when a WebSocket client is still\n * subscribed. Suppression-while-watched used to skip those pushes as noise when\n * the phone was on the session screen; that also silenced the phone when a\n * desktop browser (or a second device) was subscribed, which is worse than a\n * duplicate banner.\n */\n\nconst log = getLogger(\"expo-push\");\n\n/**\n * The payload, and why it is this thin.\n *\n * It says *which* session wants attention, never *what* the agent said. The\n * published privacy policy states notification payloads exclude prompts,\n * terminal output, credentials and conversation content, so `lastOutput` (raw\n * PTY output) and `sessionName` (derived from the user's first message) are\n * both deliberately absent — carrying them is what made the Live Activity\n * payload diverge from that text (RonenMars/threadbase-mobile#636).\n *\n * `projectName` and `sessionId` stay: mobile needs the session id to route the\n * tap, and a notification that cannot say which project it is about is not\n * actionable.\n *\n * There is no `serverId` here on purpose. Which server the app files this one\n * under is per registered token, so `ExpoPushSender` adds it per recipient.\n * The streamer's own hostname is not a substitute: the app keys servers by a\n * hash of their URL and cannot resolve a hostname.\n */\nexport function waitingInputMessage(\n  session: Pick<ManagedSession, \"id\" | \"projectName\" | \"provider\">,\n  kind: AttentionKind = \"turn_done\",\n): ExpoPushContent {\n  return (locale) => ({\n    title: attentionTitle(kind, session.projectName),\n    body: attentionBody(kind, session.provider, locale),\n    data: { sessionId: session.id, kind },\n    sound: \"default\",\n    priority: \"high\",\n    // One stack per session, and the newest state replaces the older banner:\n    // \"needs your go-ahead\" is stale the moment the turn finishes.\n    threadId: session.id,\n    collapseId: session.id,\n    tag: session.id,\n  });\n}\n\nexport class WaitingInputNotifier {\n  /** Sessions with a turn the user started that has not yet been answered. */\n  private openTurn = new Set<string>();\n  /** Sessions with a prompt (gate or question) already pushed and still open. */\n  private openPrompt = new Set<string>();\n  /** Sessions that have reached a prompt at least once, so they did start. */\n  private readySeen = new Set<string>();\n  /**\n   * Sessions whose going idle has already been looked at, so a repeat emit is\n   * not read as a second death. Cleared when the session is alive again.\n   */\n  private idleHandled = new Set<string>();\n\n  constructor(private readonly sender: ExpoPushSender) {}\n\n  /**\n   * React to a session status change.\n   *\n   * Fire-and-forget by design: a push must never delay or fail a session\n   * transition, so this returns a promise the caller may ignore and every error\n   * is logged rather than propagated.\n   */\n  async onStatusChange(session: ManagedSession, previousStatus?: string): Promise<void> {\n    try {\n      if (session.status === \"running\") {\n        this.idleHandled.delete(session.id);\n        if (previousStatus === \"waiting_input\") this.openTurn.add(session.id);\n        return;\n      }\n      this.openPrompt.delete(session.id);\n      if (session.status !== \"waiting_input\") {\n        // idle: the PTY is gone, so any open turn ended without a prompt.\n        this.openTurn.delete(session.id);\n        // \"Could not start\" means it never got as far as a prompt. A Codex\n        // session that hit a usage limit keeps its failureReason and goes idle\n        // when the user closes it much later — that is not a failed start.\n        const neverReady = !this.readySeen.delete(session.id);\n        const firstIdle = !this.idleHandled.has(session.id);\n        this.idleHandled.add(session.id);\n        if (firstIdle && neverReady && session.failureReason != null) {\n          await this.push(session, \"failed\");\n        }\n        return;\n      }\n      // Alive again (a resume after a failed start), so the next failure counts.\n      this.idleHandled.delete(session.id);\n      this.readySeen.add(session.id);\n\n      // Delete-as-test: no open turn means boot/resume ready, or a repeat\n      // emit of a status we have already notified for. Either way the user is\n      // not owed a second notification for one turn.\n      if (!this.openTurn.delete(session.id)) return;\n\n      await this.push(session, \"turn_done\");\n    } catch (err) {\n      log.error(\"expo_push.notify_failed\", {\n        event: \"expo_push.notify_failed\",\n        sessionId: session.id,\n        status: session.status,\n        err: String(err),\n      });\n    }\n  }\n\n  /**\n   * A permission gate or question opened (`open`) or closed on this session.\n   * Fire-and-forget like onStatusChange.\n   */\n  async onPrompt(\n    session: Pick<ManagedSession, \"id\" | \"projectName\" | \"provider\">,\n    kind: \"permission\" | \"question\",\n    open: boolean,\n  ): Promise<void> {\n    try {\n      if (!open) {\n        this.openPrompt.delete(session.id);\n        return;\n      }\n      if (this.openPrompt.has(session.id)) return;\n      this.openPrompt.add(session.id);\n      await this.push(session, kind);\n    } catch (err) {\n      log.error(\"expo_push.notify_failed\", {\n        event: \"expo_push.notify_failed\",\n        sessionId: session.id,\n        kind,\n        err: String(err),\n      });\n    }\n  }\n\n  private async push(\n    session: Pick<ManagedSession, \"id\" | \"projectName\" | \"provider\">,\n    kind: AttentionKind,\n  ): Promise<void> {\n    const event: PushEvent = kind === \"failed\" ? \"sessionFailed\" : \"waitingInput\";\n    const outcome = await this.sender.send(waitingInputMessage(session, kind), { event });\n    if (outcome.attempted > 0 || outcome.suppressed > 0) {\n      log.info(\"expo_push.waiting_input\", {\n        event: \"expo_push.waiting_input\",\n        sessionId: session.id,\n        kind,\n        ...outcome,\n      });\n    }\n  }\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 { parseCodexJsonlLine } from \"@threadbase-sh/scanner\";\n\n/**\n * Does a Codex rollout still look the way this streamer reads it?\n *\n * Everything built on Codex history — the conversation list, detail, search,\n * resume identity, and a fork's inherited prefix — parses an on-disk format\n * that Codex owns, does not version, and changes without telling anyone. It has\n * already changed once here: `forked_from_ordinal_exclusive` appears in exactly\n * one of the 38 fork rollouts on the machine this was written on, and its\n * absence is why every earlier fork opened as an empty conversation for months.\n *\n * That is the failure mode worth paying for. A format change does not throw —\n * it renders a conversation short, or empty, or a fork with turns it never\n * inherited, and every one of those looks like a bug in this app rather than a\n * moved contract.\n *\n * So this asserts the handful of things the readers actually depend on, and\n * NOTHING ELSE. A canary that fires on a benign addition gets muted, and a\n * muted canary is worse than none — Codex is free to add line types, payload\n * fields and roles, and every one of those must pass silently.\n */\n\n/** What broke, in terms of the assumption it breaks. */\nexport type CodexFormatFindingCode =\n  /** First line is not a `session_meta` — every reader starts there. */\n  | \"first_line_not_session_meta\"\n  /** `session_meta` carries no id, so the file names no conversation. */\n  | \"session_meta_missing_id\"\n  /**\n   * Ordinals are not one-per-line ascending. `readMessagesBeforeOrdinal` treats\n   * a line ordinal as a position, and falls back to the line counter when the\n   * field is absent — both are wrong the moment ordinals skip or repeat, and a\n   * fork's cut then lands in the wrong place with no error.\n   */\n  | \"ordinals_not_sequential\"\n  /**\n   * Lines that are structurally messages exist, but the render rule matched\n   * none of them — a renamed role, type, or content shape. This is the silent\n   * empty-conversation case.\n   */\n  | \"no_messages_parsed\"\n  /**\n   * A fork that names its source but not where the source stops. Its inherited\n   * history cannot be reconstructed, so it opens showing only its own turns.\n   */\n  | \"fork_link_without_cut\"\n  /** Most lines are not JSON at all — the container itself changed. */\n  | \"not_jsonl\";\n\nexport interface CodexFormatFinding {\n  code: CodexFormatFindingCode;\n  detail: string;\n}\n\n/** Lines read before giving up: enough to see the shape, bounded for a 68 MB rollout. */\nexport const CANARY_LINE_BUDGET = 2000;\n\n/** Above this share of unparseable lines, the container is the problem. */\nconst NOT_JSONL_RATIO = 0.5;\n\n/** Message-shaped lines needed before \"none rendered\" means drift, not tag-stripping. */\nconst MIN_MESSAGES_FOR_RENDER_DRIFT = 5;\n\nfunction asRecord(value: unknown): Record<string, unknown> | null {\n  return typeof value === \"object\" && value !== null ? (value as Record<string, unknown>) : null;\n}\n\n/**\n * Audit the opening `lines` of one rollout.\n *\n * Pure and synchronous so the assumptions can be tested against drifted\n * fixtures rather than only against healthy ones — a canary nobody has seen\n * fire is a canary nobody knows is wired up.\n */\nexport function auditCodexRolloutLines(lines: string[]): CodexFormatFinding[] {\n  const findings: CodexFormatFinding[] = [];\n  const present = lines.filter((l) => l.trim().length > 0);\n  if (present.length === 0) return findings;\n\n  const parsed: Array<Record<string, unknown> | null> = present.map((l) => {\n    try {\n      return asRecord(JSON.parse(l));\n    } catch {\n      return null;\n    }\n  });\n\n  const unparseable = parsed.filter((p) => p === null).length;\n  if (unparseable / present.length > NOT_JSONL_RATIO) {\n    return [\n      {\n        code: \"not_jsonl\",\n        detail: `${unparseable} of ${present.length} opening lines are not JSON`,\n      },\n    ];\n  }\n\n  const first = parsed[0];\n  if (first?.type !== \"session_meta\") {\n    findings.push({\n      code: \"first_line_not_session_meta\",\n      detail: `first line has type ${JSON.stringify(first?.type ?? null)}`,\n    });\n  } else {\n    const payload = asRecord(first.payload);\n    const id = payload?.id ?? payload?.session_id;\n    if (typeof id !== \"string\" || id.length === 0) {\n      findings.push({\n        code: \"session_meta_missing_id\",\n        detail: \"session_meta payload carries neither `id` nor `session_id`\",\n      });\n    }\n    // A fork that names its source should say where the source stops — but\n    // only when Codex could have known. Measured on real rollouts: a fork\n    // records `forked_from_ordinal_exclusive` exactly when its SOURCE has\n    // ordinals, and inherits them (its own first line carries the cut as its\n    // ordinal). Fork from a rollout written before ordinals existed and you\n    // get an id and nothing else, today, with a current Codex.\n    //\n    // So a missing cut is only drift when this file HAS an ordinal: that\n    // proves the source had them, which is exactly when a cut was available to\n    // record. Without that qualifier this fires on every fork of an old\n    // conversation — a permanent, known condition, and the fastest way to\n    // train someone to ignore the canary.\n    if (typeof payload?.forked_from_id === \"string\" && typeof first.ordinal === \"number\") {\n      const cut = payload.forked_from_ordinal_exclusive;\n      if (typeof cut !== \"number\" || !Number.isFinite(cut)) {\n        findings.push({\n          code: \"fork_link_without_cut\",\n          detail: `forked_from_id present with ordinals, forked_from_ordinal_exclusive is ${JSON.stringify(cut ?? null)}`,\n        });\n      }\n    }\n  }\n\n  // Ordinals must not go BACKWARDS. That is the whole assumption the readers\n  // make: `readMessagesBeforeOrdinal` walks until `ordinal >= cut` and stops,\n  // so a descending ordinal would end the prefix early and silently drop\n  // inherited turns.\n  //\n  // Not asserted: that they start at zero — a fork continues its source's\n  // numbering, its first line carrying the cut. Nor that they advance by\n  // exactly one. Codex does emit repeats: one file in ~676 on the machine this\n  // was written on carries ordinal 312 twice, and the readers are fine with it\n  // because two lines sharing a position still fall on the same side of a cut.\n  const ordinals = parsed.map((p) => (typeof p?.ordinal === \"number\" ? p.ordinal : null));\n  let lastOrdinal: number | null = null;\n  for (let i = 0; i < ordinals.length; i++) {\n    const cur = ordinals[i];\n    if (cur === null) continue;\n    if (lastOrdinal !== null && cur < lastOrdinal) {\n      findings.push({\n        code: \"ordinals_not_sequential\",\n        detail: `line ${i} has ordinal ${cur} after ${lastOrdinal}`,\n      });\n      break;\n    }\n    lastOrdinal = cur;\n  }\n\n  // The render rule, against the structural shape it reads. Only the\n  // all-or-nothing case is a finding: a file legitimately renders fewer\n  // messages than it has message-shaped lines (developer/system roles, bodies\n  // that are only system tags), and reporting that would fire on every file.\n  let messageShaped = 0;\n  let rendered = 0;\n  for (let i = 0; i < present.length; i++) {\n    const entry = parsed[i];\n    const payload = asRecord(entry?.payload);\n    // Only roles the render rule would ever accept count as \"should have\n    // rendered\". An aborted session whose sole message line is `developer`\n    // sandbox boilerplate is legitimately empty, not drift — four such files\n    // exist in this corpus, and counting them fires the canary on a working\n    // conversation that simply has nothing to say.\n    if (\n      entry?.type === \"response_item\" &&\n      payload?.type === \"message\" &&\n      (payload.role === \"user\" || payload.role === \"assistant\")\n    ) {\n      messageShaped++;\n    }\n    if (parseCodexJsonlLine(present[i])) rendered++;\n  }\n  // A handful of message-shaped lines can legitimately render as nothing: a\n  // body that is entirely `<command-name>` / `<local-command-stdout>` tags is\n  // stripped to empty by design, and two such files exist here. Drift is a\n  // file with real conversation in it rendering as none of it, so this needs a\n  // floor. Five is a judgement call, sized above the observed noise.\n  if (messageShaped >= MIN_MESSAGES_FOR_RENDER_DRIFT && rendered === 0) {\n    findings.push({\n      code: \"no_messages_parsed\",\n      detail: `${messageShaped} message-shaped lines, none matched the render rule`,\n    });\n  }\n\n  return findings;\n}\n\n/**\n * The newest rollouts under `roots`, newest first.\n *\n * Newest is the whole point: an old file proves only what Codex used to write,\n * and the drift worth catching is in what it writes now. Date-partitioned\n * directories (`<root>/YYYY/MM/DD`) mean the newest day's directory is enough —\n * walking every rollout would be a full-tree stat on a machine with thousands.\n */\nexport function findNewestRollouts(\n  roots: string[],\n  limit: number,\n  fs: {\n    readdirSync: (p: string) => string[];\n    statSync: (p: string) => { mtimeMs: number; isDirectory(): boolean };\n    existsSync: (p: string) => boolean;\n  },\n  join: (...parts: string[]) => string,\n): string[] {\n  const candidates: Array<{ path: string; mtimeMs: number }> = [];\n\n  const newestChildren = (dir: string, depth: number): void => {\n    if (!fs.existsSync(dir)) return;\n    let entries: string[];\n    try {\n      entries = fs.readdirSync(dir);\n    } catch {\n      return;\n    }\n    const dirs: Array<{ path: string; mtimeMs: number }> = [];\n    for (const name of entries) {\n      const full = join(dir, name);\n      let stat: { mtimeMs: number; isDirectory(): boolean };\n      try {\n        stat = fs.statSync(full);\n      } catch {\n        continue;\n      }\n      if (stat.isDirectory()) {\n        dirs.push({ path: full, mtimeMs: stat.mtimeMs });\n      } else if (name.startsWith(\"rollout-\") && name.endsWith(\".jsonl\")) {\n        candidates.push({ path: full, mtimeMs: stat.mtimeMs });\n      }\n    }\n    // Descend only into the newest directories, and only as deep as the\n    // YYYY/MM/DD layout goes.\n    if (depth <= 0) return;\n    dirs.sort((a, b) => b.mtimeMs - a.mtimeMs);\n    for (const d of dirs.slice(0, 2)) newestChildren(d.path, depth - 1);\n  };\n\n  for (const root of roots) newestChildren(root, 3);\n  candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);\n  return candidates.slice(0, limit).map((c) => c.path);\n}\n\nexport interface CanaryDeps {\n  readdirSync: (p: string) => string[];\n  statSync: (p: string) => { mtimeMs: number; isDirectory(): boolean };\n  existsSync: (p: string) => boolean;\n  readFileSync: (p: string, enc: \"utf8\") => string;\n  join: (...parts: string[]) => string;\n}\n\nexport interface CanaryReport {\n  audited: number;\n  findings: Array<{ filePath: string } & CodexFormatFinding>;\n}\n\n/** How many of the newest rollouts each sweep looks at. */\nexport const CANARY_SAMPLE_SIZE = 3;\n\n/**\n * One sweep: audit the newest rollouts and report what no longer holds.\n *\n * Returns rather than logs so the caller owns the log vocabulary and a test can\n * assert on findings instead of on log lines.\n */\nexport function sweepCodexFormat(roots: string[], deps: CanaryDeps): CanaryReport {\n  const files = findNewestRollouts(roots, CANARY_SAMPLE_SIZE, deps, deps.join);\n  const findings: CanaryReport[\"findings\"] = [];\n  for (const filePath of files) {\n    let head: string[];\n    try {\n      // Bounded: a rollout can reach tens of MB, and the opening lines carry\n      // every structural assumption this checks.\n      head = deps.readFileSync(filePath, \"utf8\").split(\"\\n\", CANARY_LINE_BUDGET);\n    } catch {\n      continue; // Unreadable is not drift — it may be mid-write.\n    }\n    for (const finding of auditCodexRolloutLines(head)) findings.push({ filePath, ...finding });\n  }\n  return { audited: files.length, findings };\n}\n","import { existsSync } from \"fs\";\nimport type { ConversationCache } from \"./conversation-cache\";\nimport {\n  type ManagedSessionRow,\n  type ManagedSessionsRepository,\n  PROBE_SET_MAX,\n} from \"./db/repositories/managed-sessions.repository\";\nimport type { FeatureFlagValues } from \"./feature-flags\";\nimport { isPidAlive } from \"./lifecycle/process-liveness\";\nimport type { LiveSessionManager } from \"./live-session-manager\";\nimport type { Logger } from \"./logger\";\nimport { getProcessArgs } from \"./process-discovery\";\nimport { CODEX_CLI_PROVIDER } from \"./providers\";\nimport type { ResumeOutcome } from \"./server\";\nimport {\n  AUTO_RESUME_CONCURRENCY,\n  AUTO_RESUME_STAGGER_MS,\n  type AutoResumeSkipReason,\n  autoResumeSkipReason,\n  planAutoResume,\n} from \"./services/sessions/autoResumeOnBoot\";\nimport {\n  PRE_BOOT_REASON,\n  type ReconcileVerdict,\n  reconcileSessions,\n} from \"./services/sessions/reconcileSessions\";\nimport {\n  REHYDRATE_MAX,\n  REHYDRATE_WINDOW_MS,\n  rehydrateSkipReason,\n  rowToStubSession,\n} from \"./services/sessions/rehydrateSessions\";\nimport type { SessionStore } from \"./session-store\";\nimport type { ManagedSession } from \"./types\";\nimport { currentBootToken } from \"./utils/bootToken\";\n\n/**\n * Everything the boot-time registry lifecycle reads from the server. Thunks\n * rather than values for anything bound after construction or replaceable at\n * runtime (`managedSessionsRepo` opens during listen(); tests swap `log` and\n * `resumeSession` on the server instance) — the same reason ScannerManagerDeps\n * passes `cache: () => ConversationCache | null`.\n */\nexport type SessionRegistryBootDeps = {\n  log: () => Logger;\n  ptyManager: () => LiveSessionManager;\n  sessionStore: () => SessionStore;\n  featureFlags: () => FeatureFlagValues;\n  autoResumeOnBoot: () => boolean;\n  managedSessionsRepo: () => ManagedSessionsRepository | null;\n  cache: () => ConversationCache | null;\n  streamerInstanceId: string;\n  sessionVerdicts: Map<string, ReconcileVerdict>;\n  selfPtyEndedAt: Map<string, number>;\n  resumeSession: (opts: {\n    sessionId: string;\n    projectName?: string;\n    branch?: string;\n  }) => Promise<ResumeOutcome>;\n  watchConversationFile: (sessionId: string, historyId?: string) => Promise<void>;\n  broadcastSessionList: () => void;\n  // Boot auto-resume preflights each candidate for a provider history before\n  // spending one of the AUTO_RESUME_MAX slots on it (#483). Typed loosely on the\n  // success side for the same reason SessionHandlersDeps is: the full shape is\n  // declared on StreamerServer and only `ok` / `reason` are read here.\n  resolveConversationTarget: (\n    sessionId: string,\n  ) => Promise<{ ok: false; reason: string } | { ok: true; [key: string]: unknown }>;\n};\n\n/**\n * The durable session registry's boot-and-shutdown lifecycle: classifying what\n * previous runs left behind, seeding the session list from it, optionally\n * resuming, pruning retired rows, and mirroring spawns and shutdown back into\n * the registry.\n *\n * Extracted from StreamerServer so registry work stops editing the server file\n * (see docs/plans/2026-07-12-server-ts-split.md, PR 6). State stays on the\n * server: this class only reads it through `deps`.\n */\nexport class SessionRegistryBoot {\n  constructor(private deps: SessionRegistryBootDeps) {}\n\n  private get log(): Logger {\n    return this.deps.log();\n  }\n\n  private get ptyManager(): LiveSessionManager {\n    return this.deps.ptyManager();\n  }\n\n  private get sessionStore(): SessionStore {\n    return this.deps.sessionStore();\n  }\n\n  private get featureFlags(): FeatureFlagValues {\n    return this.deps.featureFlags();\n  }\n\n  private get autoResumeOnBoot(): boolean {\n    return this.deps.autoResumeOnBoot();\n  }\n\n  private get managedSessionsRepo(): ManagedSessionsRepository | null {\n    return this.deps.managedSessionsRepo();\n  }\n\n  private get streamerInstanceId(): string {\n    return this.deps.streamerInstanceId;\n  }\n\n  private get sessionVerdicts(): Map<string, ReconcileVerdict> {\n    return this.deps.sessionVerdicts;\n  }\n\n  private get selfPtyEndedAt(): Map<string, number> {\n    return this.deps.selfPtyEndedAt;\n  }\n\n  /**\n   * Classify sessions left behind by previous streamer runs (C1 Phase 3a).\n   *\n   * Agents already outlive the streamer today on the crash and dev-takeover\n   * paths, which exit without reaching ptyManager.dispose() — they are just\n   * invisible when they do, because nothing recorded that they existed. This\n   * turns those rows into an explicit verdict per session.\n   *\n   * Read-only with respect to processes: it probes and classifies, and never\n   * signals anything. `orphaned` is a report, not a cleanup trigger.\n   */\n  async reconcilePreviousSessions(): Promise<ReconcileVerdict[]> {\n    if (!this.managedSessionsRepo) return [];\n    let verdicts: ReconcileVerdict[] = [];\n    try {\n      const rows = this.managedSessionsRepo\n        .listNonTerminal()\n        .filter((row) => !this.ptyManager.hasSession(row.session_id));\n      if (rows.length === PROBE_SET_MAX) {\n        // Said out loud rather than absorbed: past the cap this boot classifies\n        // a subset, and the rows it skipped are indistinguishable in the log\n        // from rows that did not exist.\n        this.log.warn(\n          `[reconcile] probe set hit its cap of ${PROBE_SET_MAX} — older rows skipped`,\n          {\n            event: \"registry.probe_truncated\",\n            limit: PROBE_SET_MAX,\n          },\n        );\n      }\n      if (rows.length === 0) return [];\n\n      verdicts = await reconcileSessions(\n        rows,\n        { isPidAlive, getProcessArgs },\n        this.streamerInstanceId,\n        currentBootToken(),\n      );\n\n      for (const v of verdicts) {\n        this.sessionVerdicts.set(v.sessionId, v);\n        // A skipped probe is the one verdict that looks like a decision but is\n        // an abstention — surfaced so \"why is this resumable?\" has an answer.\n        if (v.reason === PRE_BOOT_REASON) {\n          this.log.info(`[reconcile] ${v.sessionId} predates this machine boot — pid not probed`, {\n            event: \"sessions.boot_token_mismatch\",\n            sessionId: v.sessionId,\n          });\n        }\n        // Terminal verdicts leave the probe set so the next boot does no work\n        // for them. Non-terminal ones stay: a `detached` process may still be\n        // running, and we want to re-probe it next time.\n        if (v.lifecycle === \"completed\" || v.lifecycle === \"failed\") {\n          this.managedSessionsRepo.recordStatus(v.sessionId, \"idle\", \"reconcile\", {\n            completedAt: new Date(),\n          });\n        }\n      }\n\n      this.log.info(`[reconcile] classified ${verdicts.length} session(s) from previous runs`, {\n        event: \"sessions.reconciled\",\n        counts: verdicts.reduce<Record<string, number>>((acc, v) => {\n          acc[v.lifecycle] = (acc[v.lifecycle] ?? 0) + 1;\n          return acc;\n        }, {}),\n      });\n    } catch (err) {\n      // Reconciliation is diagnostic. A failure costs post-restart visibility,\n      // and must never stop the server from starting.\n      this.log.warn(\"[reconcile] failed to reconcile previous sessions\", {\n        event: \"sessions.reconcile_failed\",\n        err,\n      });\n    }\n    return verdicts;\n  }\n\n  /**\n   * Drop finished sessions the registry has held long enough (plan Phase 4).\n   *\n   * The registry is authoritative and never rebuilt from the cache, so nothing\n   * else would ever remove a row: without this it grows for the life of the\n   * install, and every boot pays for rows about sessions from months ago.\n   */\n  pruneTerminalSessions(): void {\n    if (!this.managedSessionsRepo) return;\n    try {\n      const pruned = this.managedSessionsRepo.pruneTerminal();\n      if (pruned > 0) {\n        this.log.info(`[registry] pruned ${pruned} terminal session row(s)`, {\n          event: \"registry.pruned\",\n          pruned,\n        });\n      }\n    } catch (err) {\n      // Retention is housekeeping. Failing it costs disk, never correctness.\n      this.log.warn(\"[registry] failed to prune terminal sessions\", {\n        event: \"registry.prune_failed\",\n        err,\n      });\n    }\n  }\n\n  /**\n   * Seed the session list with what previous runs left behind (persistence plan\n   * Phase 1, gaps G1/G2/G8).\n   *\n   * Reconciliation classifies rows and stops there; a verdict is overlaid onto a\n   * SessionResponse that already exists, and after a clean restart none does —\n   * `SessionStore` starts empty. So the user's session did not become\n   * `resumable`, it became *absent*. This is the half that puts it back.\n   *\n   * The seeded stubs hold no PTY and are never handed to `LiveSessionManager`,\n   * so `reapIdleSessions` and `startGraceTimer` — both of which iterate\n   * `ptyManager.listSessions()` — cannot observe them. A later resume calls\n   * `sessionStore.addManaged` with the real session, which overwrites the stub\n   * by id rather than duplicating it.\n   */\n  rehydratePreviousSessions(verdicts: ReconcileVerdict[]): ManagedSessionRow[] {\n    if (!this.managedSessionsRepo) return [];\n    try {\n      const now = Date.now();\n      // One over the cap, so a clipped result set can be reported rather than\n      // silently read as \"that was all of them\".\n      const rows = this.managedSessionsRepo.listRecoverable({\n        sinceMs: now - REHYDRATE_WINDOW_MS,\n        limit: REHYDRATE_MAX + 1,\n        includeSubagents: this.featureFlags.subagentSessions ?? false,\n      });\n      const truncated = rows.length > REHYDRATE_MAX;\n      const candidates = truncated ? rows.slice(0, REHYDRATE_MAX) : rows;\n      // The auto-resume preference is independent of the Phase 1 feature flag,\n      // but both paths must make decisions from this exact, bounded row set.\n      if (!this.featureFlags.sessionRehydration || candidates.length === 0) return candidates;\n\n      const verdictById = new Map(verdicts.map((v) => [v.sessionId, v]));\n      const cache = this.deps.cache();\n      const hasConversation = cache ? (id: string) => cache.hasConversation(id) : undefined;\n      let rehydrated = 0;\n      const skippedBy: Record<string, number> = {};\n      for (const row of candidates) {\n        // Anything already in the store is live and authoritative; a stub must\n        // never overwrite it.\n        if (this.sessionStore.getManaged(row.session_id)) continue;\n\n        const skip = rehydrateSkipReason(row, {\n          now,\n          projectExists: existsSync,\n          hasConversation,\n        });\n        if (skip) {\n          skippedBy[skip] = (skippedBy[skip] ?? 0) + 1;\n          // Per row, not just counted: \"my session did not come back\" is the\n          // report this has to answer, and it is about one specific session.\n          this.log.info(`[rehydrate] skipped ${row.session_id}: ${skip}`, {\n            event: \"sessions.rehydrate_skipped\",\n            sessionId: row.session_id,\n            reason: skip,\n          });\n          // The one skip that deletes. A session killed by a restart rather than\n          // a stop never reached `forgetSession`, and its row is never terminal,\n          // so without this every later boot re-probes it.\n          if (skip === \"never_prompted\") this.managedSessionsRepo.delete(row.session_id);\n          continue;\n        }\n\n        this.sessionStore.addManaged(rowToStubSession(row));\n        // The reconciler knows strictly more when it produced a verdict for this\n        // row (it probed the pid); `resumable` is the fallback for the clean\n        // restart case, which leaves no row in the probe set at all.\n        this.sessionVerdicts.set(\n          row.session_id,\n          verdictById.get(row.session_id) ?? {\n            sessionId: row.session_id,\n            lifecycle: \"resumable\",\n            reason: \"recovered from the registry at boot\",\n          },\n        );\n        // Re-seed the \"our own PTY ended here\" marker, so the very first resume\n        // after a restart doesn't read the JSONL we flushed on the way out as a\n        // FOREIGN owner and answer 409 CONVERSATION_BUSY (plan risk R3).\n        if (row.completed_at != null) this.selfPtyEndedAt.set(row.session_id, row.completed_at);\n        rehydrated++;\n      }\n\n      this.log.info(`[rehydrate] recovered ${rehydrated} session(s) from the registry`, {\n        event: \"sessions.rehydrated\",\n        rehydrated,\n        skipped: candidates.length - rehydrated,\n        skippedBy,\n        truncated,\n      });\n      return candidates;\n    } catch (err) {\n      // Same contract as reconciliation: this costs post-restart visibility, and\n      // must never stop the server from starting.\n      this.log.warn(\"[rehydrate] failed to rehydrate previous sessions\", {\n        event: \"sessions.rehydrate_failed\",\n        err,\n      });\n      return [];\n    }\n  }\n\n  /** Resume only the recent sessions the user explicitly allowed us to start at boot. */\n  async autoResumePreviousSessions(rows: ManagedSessionRow[]): Promise<void> {\n    if (!this.autoResumeOnBoot) return;\n\n    // Preflight each otherwise-eligible row for a provider history before\n    // planning. A session whose JSONL/rollout is gone can never resume, and\n    // spending one of the AUTO_RESUME_MAX slots on it starves a session that\n    // could (#483). Rows already skippable for a cheaper reason are marked\n    // ineligible without a probe, so this costs nothing for them.\n    const now = Date.now();\n    const baseOptions = { now, projectExists: existsSync, historyExists: () => true };\n    const historyExists = new Map<string, boolean>();\n    const preflights = new Set<Promise<void>>();\n    for (const row of rows) {\n      if (autoResumeSkipReason(row, baseOptions) != null) {\n        historyExists.set(row.session_id, false);\n        continue;\n      }\n      while (preflights.size >= AUTO_RESUME_CONCURRENCY) {\n        await Promise.race(preflights);\n      }\n      const preflight = (async () => {\n        try {\n          const target = await this.deps.resolveConversationTarget(row.session_id);\n          historyExists.set(row.session_id, target.ok || target.reason !== \"history_file_missing\");\n        } catch {\n          // Preserve the normal per-attempt failure handling when preflight is unavailable.\n          historyExists.set(row.session_id, true);\n        }\n      })();\n      preflights.add(preflight);\n      void preflight.then(() => preflights.delete(preflight));\n    }\n    await Promise.all(preflights);\n\n    const plan = planAutoResume(rows, {\n      ...baseOptions,\n      historyExists: (row) => historyExists.get(row.session_id) ?? false,\n    });\n    const skippedBy: Partial<Record<AutoResumeSkipReason, number>> = {};\n    for (const { row, reason } of plan.skipped) {\n      skippedBy[reason] = (skippedBy[reason] ?? 0) + 1;\n      this.log.debug(`[auto-resume] skipped ${row.session_id}: ${reason}`, {\n        event: \"sessions.auto_resume_skipped\",\n        sessionId: row.session_id,\n        reason,\n      });\n    }\n    if (plan.skipped.length > 0) {\n      this.log.info(\n        `[auto-resume] left ${plan.skipped.length} ineligible session(s) for manual resume`,\n        {\n          event: \"sessions.auto_resume_skipped\",\n          skipped: plan.skipped.length,\n          skippedBy,\n        },\n      );\n    }\n    for (const row of plan.overflow) {\n      this.log.info(`[auto-resume] left ${row.session_id} for manual resume: ceiling reached`, {\n        event: \"sessions.auto_resume_skipped\",\n        sessionId: row.session_id,\n        reason: \"ceiling_reached\",\n      });\n    }\n\n    let resumed = 0;\n    let failed = 0;\n    const inFlight = new Set<Promise<void>>();\n    let started = 0;\n\n    const resume = async (row: ManagedSessionRow): Promise<void> => {\n      try {\n        const outcome = await this.deps.resumeSession({\n          sessionId: row.session_id,\n          projectName: row.project_name,\n          branch: row.branch,\n        });\n        if (!outcome.ok) {\n          failed++;\n          this.log.info(`[auto-resume] skipped ${row.session_id}: ${outcome.reason}`, {\n            event: \"sessions.auto_resume_skipped\",\n            sessionId: row.session_id,\n            reason: outcome.reason,\n            ...(outcome.reason === \"conversation_busy\" && {\n              detectedBy: outcome.detectedBy,\n              lastActivityMs: outcome.lastActivityMs,\n              likelyOwner: outcome.likelyOwner,\n            }),\n          });\n          return;\n        }\n\n        resumed++;\n        this.log.info(`[auto-resume] resumed ${row.session_id}`, {\n          event: \"sessions.auto_resume_succeeded\",\n          sessionId: row.session_id,\n          alreadyRunning: outcome.alreadyRunning,\n        });\n      } catch (err) {\n        failed++;\n        this.log.warn(`[auto-resume] failed to resume ${row.session_id}`, {\n          event: \"sessions.auto_resume_failed\",\n          sessionId: row.session_id,\n          err,\n        });\n      }\n    };\n\n    for (const row of plan.attempts) {\n      while (inFlight.size >= AUTO_RESUME_CONCURRENCY) {\n        await Promise.race(inFlight);\n      }\n      if (started > 0) {\n        await new Promise<void>((resolve) => setTimeout(resolve, AUTO_RESUME_STAGGER_MS));\n      }\n\n      const task = resume(row);\n      inFlight.add(task);\n      void task.then(() => inFlight.delete(task));\n      started++;\n    }\n    await Promise.all(inFlight);\n\n    if (resumed > 0) this.deps.broadcastSessionList();\n\n    this.log.info(`[auto-resume] completed boot recovery: ${resumed} resumed`, {\n      event: \"sessions.auto_resume_completed\",\n      attempted: plan.attempts.length,\n      resumed,\n      failed,\n      ineligible: plan.skipped.length,\n      overflow: plan.overflow.length,\n    });\n  }\n\n  /**\n   * Pick a token guaranteed to appear in the spawned process's argv, for the\n   * reconciler's pid-reuse guard.\n   *\n   * Claude always passes the session id (`--resume <id>` or `--session-id\n   * <id>`), so it is both present and unique. Codex only does on *resume*\n   * (`codex resume <id>`); a fresh Codex spawn is `codex --cd <path>\n   * --no-alt-screen` with no id at all, because the rollout id does not exist\n   * until the CLI writes it. boundConversationId is what distinguishes the two:\n   * it is set once that rollout has been discovered.\n   */\n  spawnArgvToken(session: ManagedSession): string {\n    if (session.provider !== CODEX_CLI_PROVIDER) return session.id;\n    return session.boundConversationId ?? session.projectPath;\n  }\n\n  /** Restore registry-only metadata after the host mirror has been adopted. */\n  refreshHostedSessionsFromRegistry(): void {\n    for (const session of this.ptyManager.listSessions()) {\n      const row = this.managedSessionsRepo?.get(session.id);\n      const merged: ManagedSession = {\n        ...session,\n        reconciled: true,\n        ...(row?.project_id != null && { projectId: row.project_id }),\n        ...(row?.session_name != null && { sessionName: row.session_name }),\n        ...(row?.bound_conversation_id != null && {\n          boundConversationId: row.bound_conversation_id,\n        }),\n        ...(row?.resumed_from_conversation_id != null && {\n          resumedFromConversationId: row.resumed_from_conversation_id,\n        }),\n      };\n      this.sessionStore.addManaged(merged);\n      void this.deps.watchConversationFile(session.id, merged.boundConversationId ?? session.id);\n    }\n  }\n\n  /**\n   * Mirror a freshly-spawned session into the durable registry (C1 Phase 2).\n   *\n   * Called at each addManaged() site rather than inside SessionStore, because\n   * the store is a pure in-memory structure with no DB dependency and adding\n   * one would drag persistence into every unit test that touches it.\n   *\n   * Best-effort by design: a failed registry write must never break session\n   * start. Losing a row costs post-restart *visibility* for that session, which\n   * is strictly better than refusing to run the agent at all.\n   */\n  recordSessionSpawn(session: ManagedSession): void {\n    if (!this.managedSessionsRepo) return;\n    try {\n      const pid = this.ptyManager.getPid(session.id);\n      this.managedSessionsRepo.recordSpawn({\n        session,\n        pid,\n        // Identity guard against pid reuse: the reconciler requires this token\n        // to appear in the live process's argv before it will claim the pid is\n        // still ours (docs/architecture/2026-07-24-durable-session-runtime.md).\n        //\n        // Reading the real argv here would cost an async `ps` per session start\n        // on a path the user is waiting on, so we record a token we already\n        // know is in it. Claude always carries the session id (`--resume <id>`\n        // on resume, `--session-id <id>` on fresh). A *fresh* Codex spawn does\n        // not — its argv is only `--cd <path> --no-alt-screen`, because the\n        // rollout id doesn't exist yet — so fall back to the project path,\n        // which is present in every spawn path for both providers.\n        //\n        // The fallback is weaker: two sessions in one project share a token, so\n        // it proves \"a process of ours in this project\" rather than \"this exact\n        // session\". It still rejects an unrelated recycled pid, which is the\n        // failure being guarded against.\n        //\n        // Note the Codex id is always *set* (a local placeholder) — it is just\n        // not in the process's argv — so the choice keys off the provider, not\n        // off the id being null.\n        cmdline: pid != null ? this.spawnArgvToken(session) : null,\n        streamerInstanceId: this.streamerInstanceId,\n      });\n    } catch (err) {\n      this.log.warn(\"[registry] failed to record session spawn\", {\n        event: \"registry.spawn_write_failed\",\n        sessionId: session.id,\n        err,\n      });\n    }\n  }\n\n  /**\n   * Stamp every live session as ended-by-shutdown before dispose() kills it.\n   *\n   * PTYManager.dispose() signals each child directly and fires no\n   * onStatusChange, so the registry would otherwise keep rows sitting at\n   * `running` forever and the next boot could not tell a deliberate restart\n   * from a crash. Recording `shutdown` as the status source makes that\n   * distinction explicit rather than inferred.\n   *\n   * Not a `completed_at` write for the agent's own work — the agent did not\n   * finish, we stopped it — but the session is genuinely terminal, so it must\n   * leave the reconciler's probe set.\n   */\n  recordShutdownState(): void {\n    if (!this.managedSessionsRepo) return;\n    const now = new Date();\n    for (const session of this.ptyManager.listSessions()) {\n      try {\n        // No failureReason: a shutdown is not a failure of the session, and\n        // writing one would make a healthy agent look broken on the next boot.\n        // status_source=\"shutdown\" already carries the reason.\n        //\n        // The session's LIVE status, not a flattened `idle`. `completed_at` and\n        // `status_source` are what make the row terminal; overwriting the status\n        // as well erased the one thing a recovered session cannot re-derive —\n        // whether the agent was mid-answer when we stopped it (Phase 5's\n        // `interruptedStatus`). Nothing keys off this value: the reconciler\n        // reads completed_at/pid/boot_token, the rehydrator reads status_source,\n        // and the stub reports `idle` on the wire regardless.\n        this.managedSessionsRepo.recordStatus(session.id, session.status, \"shutdown\", {\n          completedAt: now,\n          lastActivityAt: session.lastActivityAt ?? null,\n          promptCount: session.promptCount,\n        });\n      } catch (err) {\n        // Shutdown must complete regardless. A lost row costs visibility, not\n        // correctness, and throwing here would strand the rest of teardown.\n        this.log.warn(\"[registry] failed to record shutdown state\", {\n          event: \"registry.shutdown_write_failed\",\n          sessionId: session.id,\n          err,\n        });\n      }\n    }\n  }\n}\n","/**\n * POSIX trick: kill(pid, 0) sends no signal but throws ESRCH if no such PID,\n * EPERM if the PID exists but is owned by another user. Either way the\n * process exists; ESRCH alone means dead.\n */\nexport function isPidAlive(pid: number): boolean {\n  if (!Number.isInteger(pid) || pid < 1) return false;\n  try {\n    process.kill(pid, 0);\n    return true;\n  } catch (err) {\n    const code = (err as NodeJS.ErrnoException).code;\n    return code === \"EPERM\"; // exists, just not ours\n  }\n}\n","import type { ManagedSessionRow } from \"../../db/repositories/managed-sessions.repository\";\nimport { resumeIdForRow } from \"./resumeIdentity\";\n\n/** Only a recent restart is eligible for unattended recovery. */\nexport const AUTO_RESUME_WINDOW_MS = 15 * 60 * 1000;\n\n/** Hard ceiling for unattended agent starts during one boot. */\nexport const AUTO_RESUME_MAX = 5;\n\n/** At most two provider processes may be starting at once. */\nexport const AUTO_RESUME_CONCURRENCY = 2;\n\n/** Keep successive starts from landing on the host in the same instant. */\nexport const AUTO_RESUME_STAGGER_MS = 500;\n\nexport type AutoResumeSkipReason =\n  | \"not_shutdown\"\n  | \"not_interrupted\"\n  | \"too_old\"\n  | \"project_missing\"\n  | \"resume_identity_missing\"\n  | \"history_missing\";\n\nexport interface AutoResumeOptions {\n  now: number;\n  projectExists: (projectPath: string) => boolean;\n  historyExists: (row: ManagedSessionRow) => boolean;\n}\n\n/** Why a persisted row must remain a user-driven resume, or null when eligible. */\nexport function autoResumeSkipReason(\n  row: ManagedSessionRow,\n  opts: AutoResumeOptions,\n): AutoResumeSkipReason | null {\n  if (row.status_source !== \"shutdown\") return \"not_shutdown\";\n  if (row.status !== \"running\" && row.status !== \"waiting_input\") return \"not_interrupted\";\n  if (opts.now - row.status_updated_at > AUTO_RESUME_WINDOW_MS) return \"too_old\";\n  if (!opts.projectExists(row.project_path)) return \"project_missing\";\n  if (resumeIdForRow(row) == null) return \"resume_identity_missing\";\n  if (!opts.historyExists(row)) return \"history_missing\";\n  return null;\n}\n\nexport interface AutoResumePlan {\n  attempts: ManagedSessionRow[];\n  skipped: Array<{ row: ManagedSessionRow; reason: AutoResumeSkipReason }>;\n  overflow: ManagedSessionRow[];\n}\n\n/** Preserve repository order while applying eligibility and the per-boot ceiling. */\nexport function planAutoResume(rows: ManagedSessionRow[], opts: AutoResumeOptions): AutoResumePlan {\n  const eligible: ManagedSessionRow[] = [];\n  const skipped: AutoResumePlan[\"skipped\"] = [];\n\n  for (const row of rows) {\n    const reason = autoResumeSkipReason(row, opts);\n    if (reason) skipped.push({ row, reason });\n    else eligible.push(row);\n  }\n\n  return {\n    attempts: eligible.slice(0, AUTO_RESUME_MAX),\n    skipped,\n    overflow: eligible.slice(AUTO_RESUME_MAX),\n  };\n}\n","import type {\n  ManagedSessionRow,\n  SessionLifecycle,\n} from \"../../db/repositories/managed-sessions.repository\";\nimport { resumeIdForRow } from \"./resumeIdentity\";\n\n/**\n * Boot reconciliation (C1 Phase 3a).\n * See docs/architecture/2026-07-24-durable-session-runtime.md.\n *\n * On startup the registry holds rows from previous runs. Some of those\n * processes are gone; some are still alive (the crash and dev-takeover paths\n * exit without reaching ptyManager.dispose(), so agents genuinely outlive the\n * streamer today — they are simply invisible when they do). This module decides\n * which is which.\n *\n * Two rules make it safe:\n *\n *  1. **A stored status is never trusted over a live probe.** A SIGKILLed\n *     streamer never ran its exit writes, so a row can claim `running`\n *     indefinitely. Only the pid probe decides liveness.\n *\n *  2. **Liveness is never treated as identity.** Pids are recycled. A live pid\n *     whose command line does not carry the recorded token is reported\n *     `orphaned` and never signalled — that is the difference between a\n *     durability feature and one that kills an unrelated user process.\n */\n\n/**\n * Reason attached to a row whose pid was never probed because it was recorded\n * under a previous machine boot. Exported so the caller can recognise the\n * abstention and log it, without matching on a free-text string that could\n * drift away from the one produced here.\n */\nexport const PRE_BOOT_REASON = \"recorded before this machine boot\";\n\n/** Result for a single registry row. Pure data — nothing here acts. */\nexport interface ReconcileVerdict {\n  sessionId: string;\n  lifecycle: SessionLifecycle;\n  /** Why this verdict was reached, for logs and the diagnostics surface. */\n  reason: string;\n}\n\nexport interface ReconcileProbe {\n  /** Whether a process currently exists at this pid. */\n  isPidAlive: (pid: number) => boolean;\n  /** Full command line for a live pid, or \"\" when it cannot be read. */\n  getProcessArgs: (pid: number) => Promise<string>;\n  /**\n   * Whether the provider's own history shows this session ended cleanly.\n   * Distinguishes `completed` from `failed` for a process that is gone.\n   */\n  endedCleanly?: (row: ManagedSessionRow) => boolean;\n}\n\n/**\n * Classify one registry row against live process state.\n *\n * Exported separately from reconcileSessions so the decision table can be\n * tested without a database. `currentBootToken` is passed in rather than read\n * here for the same reason — this stays a pure function of its arguments.\n * Omitted/null means the caller has no boot identity, which skips the pre-boot\n * check entirely; the server always supplies one.\n */\nexport async function classifySession(\n  row: ManagedSessionRow,\n  probe: ReconcileProbe,\n  currentInstanceId: string,\n  currentBootToken: string | null = null,\n): Promise<ReconcileVerdict> {\n  const { session_id: sessionId } = row;\n\n  // `resumable` is only an honest verdict when some id can actually resume the\n  // row. A Codex session that died before `watchForCodexRollout` bound its\n  // rollout id has none — `codex resume <placeholder>` fails — so offering it\n  // would spend the user's tap on a guaranteed failure.\n  const resumable = (reason: string): ReconcileVerdict =>\n    resumeIdForRow(row) == null\n      ? {\n          sessionId,\n          lifecycle: \"failed\",\n          reason: \"Codex session ended before its rollout id was known\",\n        }\n      : { sessionId, lifecycle: \"resumable\", reason };\n\n  // A row this run created and completed needs no probe.\n  if (row.completed_at != null) {\n    const clean = probe.endedCleanly?.(row) ?? row.failure_reason == null;\n    return {\n      sessionId,\n      lifecycle: clean ? \"completed\" : \"failed\",\n      reason: `terminal (${row.status_source})`,\n    };\n  }\n\n  // No pid recorded — the spawn write raced the process, or the row predates\n  // pid capture. Nothing to probe, so fall back to whether the provider can\n  // resume it.\n  if (row.pid == null) {\n    return resumable(\"no pid recorded\");\n  }\n\n  // A pid only identifies a process within the boot it was recorded in: pid\n  // assignment restarts at boot, so probing a pre-reboot pid can hit an\n  // unrelated process that inherited the number — and if its argv happens to\n  // contain the recorded token (for a fresh Codex session that is only the\n  // project path) we would claim a live process that is not ours. Equality\n  // only, and a mismatch is always the harmless verdict.\n  if (currentBootToken != null && row.boot_token !== currentBootToken) {\n    return resumable(PRE_BOOT_REASON);\n  }\n\n  if (!probe.isPidAlive(row.pid)) {\n    // The process is gone. No provider answers `endedCleanly` today, so this\n    // used to fall through `?? false` and call every dead session `resumable`,\n    // including ones that died of a diagnosed failure. Decide on the evidence\n    // the registry already holds instead of on a probe nobody supplies — and\n    // still honour the hook if some future provider can answer it.\n    if (probe.endedCleanly?.(row)) {\n      return { sessionId, lifecycle: \"completed\", reason: \"process gone, history ended cleanly\" };\n    }\n    if (row.failure_reason != null) {\n      return { sessionId, lifecycle: \"failed\", reason: \"process gone, failure recorded\" };\n    }\n    // Nothing recorded why — the SIGKILL case, where the row can still say\n    // `running` because no exit write ever ran. Cleanliness is genuinely\n    // unknowable here, and `resumable` is the honest answer.\n    return resumable(\"process gone, resumable from provider history\");\n  }\n\n  // Something is alive at that pid. Identity must be confirmed before we claim\n  // it: an unreadable or non-matching command line means \"cannot confirm\",\n  // never \"confirmed\".\n  const args = await probe.getProcessArgs(row.pid);\n  const token = row.cmdline;\n  if (!token || !args?.includes(token)) {\n    return {\n      sessionId,\n      lifecycle: \"orphaned\",\n      reason: args ? \"pid alive but command line does not match\" : \"pid alive but argv unreadable\",\n    };\n  }\n\n  // Alive and ours. `detached` rather than `attached`: the process survived,\n  // but its PTY master fd died with the streamer that spawned it, so this run\n  // cannot stream its bytes — only observe it through provider history and\n  // offer an explicit resume.\n  const sameRun = row.streamer_instance_id === currentInstanceId;\n  return {\n    sessionId,\n    lifecycle: sameRun ? \"attached\" : \"detached\",\n    reason: sameRun ? \"owned by this run\" : \"survived a previous streamer run\",\n  };\n}\n\n/**\n * Classify every non-terminal row. Returns verdicts only — the caller decides\n * what to persist or broadcast. This function never signals a process.\n */\nexport async function reconcileSessions(\n  rows: ManagedSessionRow[],\n  probe: ReconcileProbe,\n  currentInstanceId: string,\n  currentBootToken: string | null = null,\n): Promise<ReconcileVerdict[]> {\n  return Promise.all(\n    rows.map((row) => classifySession(row, probe, currentInstanceId, currentBootToken)),\n  );\n}\n","import type { Stage } from \"@threadbase-sh/agent-types\";\nimport type { ProgressDedupeLRU } from \"./agent/dedupe\";\nimport type { ClaudeFlagValues, EffortLevel, PermissionMode } from \"./claude-flags\";\nimport type { FeatureFlagValues } from \"./feature-flags\";\nimport type { ProviderName } from \"./providers\";\nimport type { PromptEvent, PromptSnapshot } from \"./services/prompts/promptRegistry\";\n\n// ─── Session Lifecycle ─────────────────────────────────────────────\n\nexport type SessionStatus = \"running\" | \"waiting_input\" | \"idle\";\n\n/**\n * Phase axis *inside* `status === \"running\"` — what the agent is doing during a\n * turn. Deliberately a separate field rather than new SessionStatus members:\n * VALID_STATUSES rejects unknown values and the store drops sessions outside\n * the requested set, so a new status string would make those sessions vanish\n * from already-shipped apps. Additive fields are safe; additive values in a\n * union a shipped client filters on are not.\n *\n * The full set is defined here even though Codex only ever emits `working`\n * (its status bar is binary — Ready/Working, and claiming otherwise would be\n * invention). Defining it up front keeps a two-valued provider from fixing the\n * field's shape before Claude's richer footer lands. Consumers must ignore an\n * unrecognised value rather than coerce it.\n *\n * This union lives in exactly one place. Two independently-maintained copies of\n * a TUI-derived grammar have already drifted once (tb-mobile PR #647).\n */\nexport type AgentPhase = \"thinking\" | \"streaming\" | \"hooks\" | \"acting\" | \"working\";\n\n/**\n * Process-lifetime axis for a managed session (C1 durable session runtime).\n * Orthogonal to SessionStatus — see SessionResponse.lifecycle for why the two\n * are separate, and docs/architecture/2026-07-24-durable-session-runtime.md.\n */\nexport type SessionLifecycle =\n  | \"attached\" // this streamer run owns the PTY and streams its bytes\n  | \"starting\" // registered, spawn in flight; not attached here and no exit observed\n  | \"detached\" // process alive, but this run does not own its fd\n  | \"orphaned\" // something is alive at the recorded pid, identity unconfirmed\n  | \"resumable\" // no live process; provider history supports resume\n  | \"completed\" // terminal, ended cleanly\n  | \"failed\"; // terminal, ended badly\n\n/**\n * How a SessionStatus was derived (C3).\n * See docs/architecture/2026-07-24-session-state-confidence.md.\n *\n * The runners already compute this at every transition — it was written to a log\n * line and discarded, so a status reached by a timer expiring was indistinguishable\n * on the wire from one reached by observing a prompt marker.\n */\nexport type StatusSource =\n  | \"spawn\" // initial state when the process started\n  | \"prompt-marker\" // a provider prompt marker appeared in the stream\n  | \"screen-marker\" // marker found by re-reading the rendered screen\n  | \"user-input\" // we wrote input, so it is running by construction\n  | \"process-exit\" // the process exited; status follows from that\n  | \"timeout-fallback\" // NO marker appeared; a timer elapsed and we assumed\n  | \"quiet-fallback\" // the PTY fell silent during boot; we assumed\n  | \"shutdown\"; // the streamer terminated it\n\n/**\n * How much to trust the status.\n *\n * `observed` — something in the stream or the process told us.\n * `inferred` — a timer expired and we picked the most likely state.\n *\n * Deliberately two buckets rather than a numeric score: a percentage would imply\n * a calibration we have no data to support. The point is that a guess must never\n * be presented as an observation.\n */\nexport type StatusConfidence = \"observed\" | \"inferred\";\n\n/** Confidence implied by each source. Inference is exactly the timer-driven paths. */\nexport function confidenceForSource(source: StatusSource): StatusConfidence {\n  return source === \"timeout-fallback\" || source === \"quiet-fallback\" ? \"inferred\" : \"observed\";\n}\n\nexport interface ManagedSession {\n  isSubagent?: boolean;\n  parentConversationId?: string | null;\n  id: string; // JSONL UUID — the .jsonl filename under ~/.claude/projects/\n  provider?: ProviderName;\n  projectId?: string; // Stable identity into the projects table (added during migration).\n  projectPath: string;\n  projectName: string;\n  branch: string;\n  status: SessionStatus;\n  startedAt: Date;\n  completedAt: Date | null;\n  promptCount: number;\n  lastOutput: string;\n  failureReason?: string;\n  /**\n   * Machine-readable companion to `failureReason`, set only when the runner\n   * recognised the failure. Currently just `codex_active_writer` (Codex's\n   * single-writer lock), which the resume path maps to a structured 409 rather\n   * than reporting a spawn as successful. Absent for every other failure.\n   */\n  failureCode?: string;\n  sessionName?: string;\n  model?: string;\n  /**\n   * Reasoning-effort tier this session is running with.\n   *\n   * Seeded from the resolved spawn config rather than scraped, because\n   * `session_update` is event-driven and has no screen to read. `GET\n   * /api/sessions/:id` still prefers a live status-line scrape, which is\n   * authoritative if the user changed it inside the terminal.\n   */\n  effort?: string;\n  account?: string;\n  messageCount?: number;\n  preview?: string;\n  firstMessageText?: string;\n  firstMessageAt?: Date;\n  lastMessageText?: string;\n  lastMessageAt?: Date;\n  lastActivityAt?: Date;\n  /**\n   * How `status` was derived, and when (C3). Confidence is derived from the\n   * source via confidenceForSource — storing both would let them disagree.\n   */\n  statusSource?: StatusSource;\n  statusUpdatedAt?: Date;\n  /**\n   * Agent phase within a running turn, scraped from the rendered screen.\n   * Optional internally (every existing construction site predates it), but\n   * managedToResponse emits it unconditionally as `?? null` — on the wire\n   * absence must never be a third state, because the client merges session\n   * frames and a merge cannot express a removed key.\n   *\n   * Cleared in markReady() for the running -> waiting_input turn end, which is\n   * the only exit a runner observes on screen. Every other way out of `running`\n   * — handleExit, putOnHold, failStartup — is enforced by SessionStore\n   * .updateManaged() instead: a phase exists only while the status is\n   * `running`, so leaving it clears the field.\n   */\n  subStatus?: AgentPhase | null;\n  /**\n   * Claude Code's dim \"next prompt\" suggestion from the composer row, scraped\n   * from cell attributes (see detectPromptSuggestion). Exists only while the\n   * status is `waiting_input`; SessionStore.updateManaged() clears it on any\n   * other status. Emitted unconditionally as `?? null` for the same reason as\n   * `subStatus`.\n   */\n  promptSuggestion?: string | null;\n  filePath?: string;\n  resumedFromConversationId?: string;\n\n  /**\n   * This session was seeded from the durable registry at boot, not spawned by\n   * this run — there is no PTY behind it and never was one in this process.\n   *\n   * INTERNAL. It never reaches the wire: `managedToResponse` translates it into\n   * the already-defined `ownership: \"historical\"` + `lifecycle: \"resumable\"`\n   * pair, so no client needs to learn a new field to render a recovered session.\n   * Cleared implicitly when a resume overwrites the stub with a real session.\n   */\n  rehydrated?: boolean;\n\n  /**\n   * This live session was re-adopted from the pty-host during boot rather than\n   * spawned by this streamer process. Internal lifecycle provenance only.\n   */\n  reconciled?: boolean;\n\n  /**\n   * What this session was doing when the streamer stopped it, for a stub whose\n   * `status` had to flatten to `idle`. Only ever set from a registry row whose\n   * `status_source` is `shutdown` — the one source that means \"we ended this\",\n   * as opposed to the agent finishing or a crash freezing the row mid-turn.\n   */\n  interruptedStatus?: \"running\" | \"waiting_input\";\n\n  /**\n   * Set once a live session's underlying persisted conversation file is\n   * discovered after the fact (currently: fresh Codex sessions, whose\n   * rollout id isn't known until the CLI creates its own JSONL). Distinct\n   * from `resumedFromConversationId` (resume flow) and `conversationId` on\n   * `SessionResponse` (stable mobile deep-link alias, always === id for the\n   * lifetime of a live PTY) — must never be written into either of those.\n   */\n  boundConversationId?: string;\n\n  /**\n   * Source conversation this session was FORKED from (`codex fork`). Distinct\n   * from `resumedFromConversationId`: a resume continues one conversation, a\n   * fork starts a second one whose history diverges from the source at the fork\n   * point — and the source keeps its own owner, which is the entire point.\n   */\n  forkedFromConversationId?: string;\n\n  /**\n   * Multi-agent mode only. Per-session in-memory LRU of progress event ids\n   * seen by the webhook receiver. Used to drop Temporal-replay duplicates\n   * before they reach the WebSocket. See spec §7.1.\n   */\n  progressDedupeIds?: ProgressDedupeLRU;\n\n  /** Multi-agent: current stage of the active turn (advisory; advisory wire field). */\n  stage?: Stage | string;\n\n  /** Multi-agent: ms since the session last emitted a stage transition. */\n  stalledSinceMs?: number;\n\n  /** Multi-agent: 1 or 2 when stage === \"rework\". */\n  reworkAttempt?: number;\n\n  /**\n   * Multi-agent: id of the in-flight turn, or null when idle. Set when\n   * `POST /api/sessions/:id/input` accepts a request; cleared by the webhook\n   * receiver on stage=done or terminal_failure. Undefined in PTY mode.\n   */\n  currentTurnId?: string | null;\n\n  /**\n   * Multi-agent resume: stable identity of the underlying conversation\n   * (the JSONL filename), distinct from `id` which is per-orchestrator-instance.\n   * Undefined in PTY mode (PTY uses `resumedFromConversationId` instead).\n   */\n  conversationId?: string;\n}\n\nexport interface DiscoveredProcess {\n  pid: number;\n  /**\n   * Which agent this process is running. Discovery used to find only Claude, so\n   * every consumer could assume it; a discovered Codex session must not be\n   * reported — or adopted — as a Claude one.\n   */\n  provider: ProviderName;\n  projectPath: string;\n  projectName: string;\n  branch: string;\n  conversationId: string | null; // JSONL UUID extracted from --resume arg\n  startedAt: Date;\n}\n\n// ─── WebSocket Messages ────────────────────────────────────────────\n\nexport interface AskOption {\n  label: string;\n  description: string;\n  preview?: string;\n}\n\nexport interface AskQuestion {\n  question: string;\n  header: string;\n  multiSelect: boolean;\n  options: AskOption[];\n}\n\n// A permission-gate option scraped from the rendered screen. `index` is the\n// ACTUAL on-screen number (e.g. 2, 3), not a 1-based array index — gates can\n// show \"2. Yes / 3. No\". Mobile answers by sending `${index}\\r` via the\n// existing /input { keys } route.\nexport interface PermissionOption {\n  index: number;\n  label: string;\n  // Literal keystroke bytes that answer this option (e.g. \"y\\r\", \"2\\r\"), set by\n  // the unstructured shell-prompt detector (detectShellPrompt). Authoritative\n  // over `index` when present; absent for OSC-777 gates. Additive — old clients\n  // ignore it and fall back to `${index}\\r`.\n  answerKeys?: string;\n}\n\n// A user message the streamer submitted to the PTY. `ts` is epoch ms recorded\n// at submit time.\nexport interface UserMessage {\n  text: string;\n  ts: number;\n}\n\nexport type WSMessage =\n  // `seq` is a per-session monotonically increasing chunk counter (starts at\n  // 1). Additive — old clients ignore it. Lets a client detect a stale chunk\n  // delivered after a reconnect/replay race (seq not > its last-seen seq for\n  // this session) instead of trusting raw WS arrival order.\n  | { type: \"terminal_output\"; sessionId: string; data: string; seq?: number }\n  | {\n      type: \"session_update\";\n      session?: SessionResponse;\n      sessionId?: string;\n      // Multi-agent additive fields. Existing clients ignore these.\n      turnId?: string;\n      stage?: Stage | string;\n      stalledSinceMs?: number;\n      reworkAttempt?: number;\n    }\n  /**\n   * Agent phase changed within a running turn. Scoped to that session's\n   * subscribers, like terminal_output and user_message.\n   *\n   * A minimal frame rather than a SessionResponse copy, deliberately:\n   * managedToResponse recomputes `elapsedMs` from `new Date()` on every call\n   * for a live session, so a session copy would differ on every tick whether\n   * or not the phase changed — and a client that merges frames would get a\n   * fresh object identity several times a second, re-rendering every consumer\n   * for the whole turn.\n   *\n   * `phase` is always present and is `null` when there is no phase. Absence\n   * must never carry meaning: clients merge session state, and a merge cannot\n   * express a removed key, so an omitted field would keep its previous value\n   * and the indicator would latch on a finished turn.\n   */\n  | {\n      type: \"session_phase\";\n      sessionId: string;\n      phase: AgentPhase | null;\n      updatedAt: string; // ISO 8601\n    }\n  /**\n   * The composer's predicted next prompt changed. Scoped to the session's\n   * subscribers and minimal like `session_phase`. `text` is always present and\n   * `null` means cleared — absence must never carry meaning.\n   */\n  | {\n      type: \"prompt_suggestion\";\n      sessionId: string;\n      text: string | null;\n      updatedAt: string; // ISO 8601\n    }\n  | { type: \"session_list\"; sessions: readonly SessionResponse[] }\n  | { type: \"conversation_event\"; sessionId: string; line: string }\n  // Additive batched variant: one message carries all lines from a single\n  // watcher read. Old clients ignore it and rely on conversation_event.\n  // `seqs`, when present, is parallel to `lines`: seqs[i] is the message_index\n  // (offset-index seq) of lines[i], or null for a non-message line. Additive —\n  // old clients ignore it.\n  | { type: \"conversation_events\"; sessionId: string; lines: string[]; seqs?: (number | null)[] }\n  // A conversation's cached row advanced. Emitted after the cache write for a\n  // live external (non-PTY) tail so list rows can refresh without polling\n  // ?refresh=1. Additive — old clients ignore it.\n  | {\n      type: \"conversation_updated\";\n      conversationId: string;\n      messageCount: number;\n      lastActivity: string;\n      ownership: \"external\" | \"managed\";\n    }\n  // Structured interactive prompt (AskUserQuestion). Old clients ignore it.\n  | { type: \"question\"; sessionId: string; toolUseId: string; questions: AskQuestion[] }\n  | { type: \"question_cancelled\"; sessionId: string; toolUseId: string }\n  // Permission gate (OSC 777). Additive; old clients ignore it. `options`/`cursor`\n  // are scraped from the rendered screen and may be absent if not yet painted.\n  | {\n      type: \"permission\";\n      sessionId: string;\n      prompt?: string;\n      detail?: string;\n      options: PermissionOption[];\n      cursor?: number;\n      /**\n       * Cursor-stripped identity of this gate (permissionGateKey). Echoed\n       * verbatim, as an opaque token, to POST /:id/permission/answer. Always\n       * present when the server has that route — its absence is how a client\n       * detects an older server and falls back to POST /:id/input.\n       */\n      contentKey: string;\n      /**\n       * Server-owned id of THIS gate instance. contentKey is content-derived\n       * and cannot tell two consecutive identical gates apart; gateId can.\n       * Echoed as an opaque token to POST /:id/permission/answer, which refuses\n       * an answer whose gateId is not the pending instance. Additive: clients\n       * that omit it fall back to contentKey-only identity.\n       */\n      gateId: string;\n    }\n  | { type: \"permission_cancelled\"; sessionId: string }\n  // Unicast reply to a client's `hold_session` frame, echoed to the requesting\n  // socket only (not broadcast). `applied` distinguishes an immediate hold\n  // from a latch armed for the next running -> waiting_input edge, so the\n  // client can navigate on `ok` without waiting for the PTY to actually exit\n  // (which may be arbitrarily far off for `armed`). Additive; old clients\n  // ignore it.\n  | {\n      type: \"hold_session_result\";\n      sessionId: string;\n      ok: boolean;\n      applied?: \"held\" | \"armed\" | \"grace\";\n      reason?: \"permission_denied\" | \"unknown_when\" | \"no_session\";\n    }\n  | PromptEvent\n  | PromptSnapshot\n  | { type: \"ping\"; ts: number }\n  // Ground-truth user message: the streamer wrote this text to the PTY, so the\n  // client can positively identify user-owned output instead of parsing the\n  // `❯ <text>` transcript line heuristically. Additive; old clients ignore it.\n  | { type: \"user_message\"; sessionId: string; text: string; ts: number }\n  // `seq` is the sender's last-emitted terminal_output seq at replay time (or\n  // absent if no chunk has been emitted yet), so a resubscribing client can\n  // baseline lastSeq before trusting subsequent terminal_output chunks.\n  | {\n      type: \"terminal_replay\";\n      sessionId: string;\n      lines: string[];\n      userMessages?: UserMessage[];\n      seq?: number;\n      // The geometry these lines were rendered at. A TUI addresses rows\n      // absolutely within the viewport, so a client decoding the replay at a\n      // different size resolves those moves to the wrong rows. Additive: absent\n      // means the spawn defaults, which is what an older client assumed anyway.\n      cols?: number;\n      rows?: number;\n    }\n  // A live session's PTY was resized. Broadcast so every subscriber re-bases\n  // its decoder; only something attached asks for this, so it is rare.\n  | { type: \"terminal_resize\"; sessionId: string; cols: number; rows: number }\n  | { type: \"session_ready\"; session: SessionResponse }\n  // Multi-agent additive variants. Old clients ignore unknown types.\n  | {\n      type: \"agent_output\";\n      sessionId: string;\n      turnId: string;\n      role: \"worker\" | \"reviewer\" | \"signoff\";\n      content: string;\n      partial?: boolean;\n      reviewerOverruled?: boolean;\n      stage?: Stage | string;\n      reworkAttempt?: number;\n    }\n  | {\n      type: \"turn_failure\";\n      sessionId: string;\n      turnId: string;\n      reason: string;\n    }\n  | { type: \"cache_ready\" }\n  | { type: \"scan_progress\"; scanned: number; total: number }\n  // Cache-integrity drift alert (server-level, no sessionId). Broadcast on\n  // raise/severity-change and unicast to a client on WS open while pending.\n  // Old clients ignore unknown types. See cacheIntegrityMonitor.ts.\n  | {\n      type: \"cache_alert\";\n      fingerprint: string;\n      severity: \"high\" | \"low\";\n      missingCount: number;\n      totalRows: number;\n      detectedAt: string;\n      sample: { id: string; title?: string }[]; // first 20\n    }\n  | { type: \"cache_alert_resolved\"; fingerprint: string; action: CacheAlertResolveAction }\n  // Host pressure (server-level, no sessionId). Broadcast on level change and\n  // unicast on WS open while currently warned. Additive; old clients ignore it.\n  | {\n      type: \"host_pressure\";\n      level: HostPressureLevel;\n      reasons: HostPressureReason[];\n      liveAgents: number;\n      updatedAt: string;\n      /** Additive: Node `process.platform` of the host. Old clients ignore it. */\n      os?: HostPressureOs;\n    }\n  | { type: \"host_pressure_cleared\"; updatedAt: string };\n\n/** Coarse host-starvation level pushed on `host_pressure`. Never `ok` on the wire. */\nexport type HostPressureLevel = \"elevated\" | \"critical\";\n\n/** Why the host is starved, worst-first. Enum, not English. */\nexport type HostPressureReason = \"memory\" | \"event_loop\" | \"load\" | \"agents\";\n\n/** Host OS for client advice. win32 covers 32- and 64-bit Windows. */\nexport type HostPressureOs = \"darwin\" | \"linux\" | \"win32\";\n\n/** The four cache-integrity resolution actions (POST /api/cache/alert/resolve). */\nexport type CacheAlertResolveAction = \"prune_all\" | \"prune_selected\" | \"ignore\" | \"reset_rescan\";\n\n// ─── REST Response Shapes ──────────────────────────────────────────\n\nexport type ServerWarmupState = \"startup\" | \"cache_reset\" | \"conversation_refresh\";\n\nexport interface ServerWarmingUpResponse {\n  error: \"Server is warming up\";\n  code: \"SERVER_WARMING_UP\";\n  warmupState: ServerWarmupState;\n}\n\nexport interface SessionResponse {\n  isSubagent?: boolean;\n  parentConversationId?: string | null;\n  id: string; // JSONL UUID\n  conversationId: string; // alias for id — mobile uses this to build deep-link URLs\n  provider?: ProviderName;\n  projectId?: string; // Stable identity into the projects table (added during migration).\n  status: SessionStatus;\n  projectPath: string;\n  projectName: string;\n  branch: string;\n  lastOutput: string;\n  elapsedMs: number;\n  promptCount: number;\n  startedAt: string;\n  completedAt: string | null;\n  ptyAttached: boolean; // true when a live PTY is spawned for this session\n  /**\n   * Process-lifetime axis, orthogonal to `status` (C1).\n   *\n   * `status` answers \"what is the agent doing\" (running / waiting_input /\n   * idle); `lifecycle` answers \"does this process still exist and do we own\n   * it\". They were conflated before: `idle` meant finished, killed-to-save-\n   * resources, and externally-discovered all at once, so a client could not\n   * tell a completed session from one we terminated.\n   *\n   * Additive and optional — `ptyAttached` keeps its meaning (=== \"attached\"),\n   * so a client that ignores this behaves exactly as it did before. `\"starting\"`\n   * is additive in the same way: a session we hold no PTY for and have observed\n   * no exit for reports it instead of the `\"completed\"` it used to, so a client\n   * can tell \"not attached yet\" from \"ended\" (tb-mobile #508).\n   */\n  /**\n   * How `status` was derived and how far to trust it (C3). Additive: `status`\n   * keeps its exact meaning, so a client ignoring these behaves as before.\n   * An `inferred` confidence means a timer expired and we assumed — not that\n   * anything in the stream confirmed the state.\n   */\n  statusSource?: StatusSource;\n  statusConfidence?: StatusConfidence;\n  statusUpdatedAt?: string; // ISO 8601\n  lifecycle?: SessionLifecycle;\n  /** How `lifecycle` was determined, so stale values are visible not implied. */\n  lifecycleSource?: \"spawn\" | \"exit\" | \"probe\" | \"reconcile\";\n  lifecycleUpdatedAt?: string; // ISO 8601\n  /**\n   * ADDITIVE (plan Phase 5). What a recovered session was doing when the\n   * streamer stopped it, so a client can say \"interrupted mid-response\" instead\n   * of the `idle` its `status` is forced to report — a stub holds no PTY, and a\n   * novel SessionStatus value would be dropped by `?status=` filtering on\n   * already-shipped clients. Present only on rows whose `status_source` was\n   * `shutdown`; older clients ignore it.\n   */\n  interruptedStatus?: \"running\" | \"waiting_input\";\n  failureReason?: string;\n  pid?: number;\n  sessionName?: string;\n  model?: string;\n  /**\n   * Reasoning-effort tier scraped from the live PTY status line (e.g. \"high\").\n   * Live sessions only — absent for historical/resumable conversations.\n   */\n  effort?: string;\n  /**\n   * Active permission mode from the live PTY status line (e.g. \"accept edits on\").\n   * Live sessions only.\n   */\n  permissionMode?: string;\n  /**\n   * Agent phase within a running turn, scraped from the rendered PTY screen.\n   *\n   * NOT optional, and always serialised — `null` when there is no phase. A\n   * client that merges session frames (`{...prev, ...next}`) cannot express a\n   * removed key, so an omitted field would keep its previous value and the\n   * indicator would latch on a finished turn. That is the bug tb-mobile PR #647\n   * shipped; absence must never carry meaning here.\n   *\n   * Consequently this must NOT be moved into the `...(x != null && { x })`\n   * guard block in managedToResponse: `!= null` catches null and undefined\n   * alike and would convert an explicit clear back into absence.\n   */\n  subStatus: AgentPhase | null;\n  /**\n   * Claude Code's predicted next prompt, or `null`. Always serialised, never\n   * omitted — a merging client cannot express a removed key, so an absent\n   * field would keep a stale suggestion on screen. Do not move it into a\n   * `...(x != null && { x })` block.\n   */\n  promptSuggestion: string | null;\n  account?: string;\n  messageCount?: number;\n  preview?: string;\n  firstMessageText?: string;\n  firstMessageAt?: string;\n  lastMessageText?: string;\n  lastMessageAt?: string;\n  lastActivityAt?: string;\n  filePath?: string;\n  resumedFromConversationId?: string;\n  /** See `ManagedSession.forkedFromConversationId`. Additive; older clients ignore it. */\n  forkedFromConversationId?: string;\n  /** See `ManagedSession.boundConversationId` — never repurposes `conversationId`. */\n  boundConversationId?: string;\n  /**\n   * Who owns the underlying process. Additive — older clients ignore it, and it\n   * deliberately does NOT introduce a new `status` value: `VALID_STATUSES`\n   * rejects unknown values in `?status=` and the store drops sessions outside\n   * the requested set, so a new status string would make these sessions vanish\n   * from already-shipped apps.\n   *   managed    — this streamer spawned and holds the PTY\n   *   external   — a process we discovered but do not own (read-only)\n   *   historical — a cached conversation, no process known\n   */\n  ownership?: SessionOwnership;\n  /**\n   * Whether the process is believed to be running. Only ever \"alive\" when\n   * discovery actually saw it; never guessed from file activity.\n   */\n  processLiveness?: ProcessLiveness;\n  /**\n   * INFERRED from JSONL writes, never authoritative: \"active_writing\" means the\n   * transcript grew recently, which cannot distinguish a generating agent from\n   * one blocked on a permission gate (gates are screen-only). Absent for\n   * sessions we own — their `status` is the authoritative signal.\n   */\n  activity?: SessionActivity;\n}\n\nexport type SessionOwnership = \"managed\" | \"external\" | \"historical\";\nexport type ProcessLiveness = \"alive\" | \"gone\" | \"unknown\";\n\nexport interface SessionActivity {\n  state: \"active_writing\" | \"quiet\";\n  lastEventAt: string;\n  source: \"jsonl\";\n}\n\nexport interface ConversationListResponse {\n  conversations: unknown[];\n  hasMore: boolean;\n  offset: number;\n  total: number;\n}\n\n// ─── Sessions Pagination ───────────────────────────────────────────\n\nexport type SessionSortKey = \"startedAt\" | \"lastActivityAt\" | \"projectName\" | \"status\";\nexport type SortOrder = \"asc\" | \"desc\";\n\nexport interface SessionListPage {\n  /** Response copies, like `SessionStore.list()` — see the note there. */\n  sessions: readonly Readonly<SessionResponse>[];\n  nextCursor: string | null;\n  total: number;\n}\n\nexport interface SessionListQuery {\n  limit: number;\n  cursor?: string;\n  sortBy: SessionSortKey;\n  order: SortOrder;\n  status?: SessionStatus[];\n}\n\n// Decoded cursor payload. `k` is the value of the chosen sort key for the\n// last item on the previous page; `id` is the tiebreaker.\nexport interface SessionCursor {\n  k: string | number;\n  id: string;\n}\n\n// ─── Configuration ─────────────────────────────────────────────────\n\nexport interface ServerConfig {\n  port: number;\n  host?: string;\n  apiKey?: string;\n  /** 'cli' when --api-key was passed; rotation persists in-memory only and reverts on restart */\n  apiKeySource?: \"config\" | \"cli\";\n  localNoAuth?: boolean;\n  verbose?: boolean;\n  // Also log the menubar app's HEALTHY /healthz polls (default: false — they are\n  // ~17 280 lines/day and carry nothing). A menubar request that is not a\n  // healthy /healthz poll is logged regardless; see src/api/app.ts.\n  logMenubarRequests?: boolean;\n  browseRoot?: string;\n  publicUrl?: string;\n  browserCors?: string;\n  disableDb?: boolean;\n  scanProfiles?: Array<{\n    id: string;\n    label: string;\n    configDir: string;\n    enabled: boolean;\n    emoji: string;\n  }>;\n  codexRoots?: string[]; // paths to codex sessions dirs; empty array disables codex scanning\n  cursorRoots?: string[]; // paths to Cursor project dirs; empty array disables cursor scanning\n  // false forces every ConversationScanner to run in-memory (persistent: false)\n  // instead of the scanner package's shared default SQLite index at\n  // ~/.config/threadbase-scanner/index.db. That index isn't scoped by\n  // scanProfiles, so multiple StreamerServer instances (e.g. test servers)\n  // see each other's — and any real serve instance's — indexed conversations.\n  // Default true (real serve usage keeps the persistent index).\n  scannerPersistent?: boolean;\n  // Skip the startup cache warm-up scan (default false — real servers warm up).\n  //\n  // A test hook, and specifically a flake fix. `listen()` kicks off a full scan\n  // of every conversation on the machine, and `close()` awaits it so a post-scan\n  // cache write can't hit a closed database. On a loaded machine that scan takes\n  // tens of seconds against a developer's real ~/.claude/projects, so any test\n  // that constructs a server pays it twice (listen + close) and can overrun the\n  // vitest timeout — measured at 34s for a single close() during a full-suite\n  // run. Tests that never read conversation data should set this.\n  //\n  // The cache and repositories are still opened; only the scan and its\n  // dependent cache writes are skipped, so /api/conversations serves an empty\n  // cache rather than throwing.\n  skipStartupWarmup?: boolean;\n  ptyGracePeriodMs?: number; // ms to wait after WS disconnect before killing PTY (default 270000, 4.5 minutes); 0 disables the auto-hold timer (explicit hold_session still works)\n  /**\n   * Re-start interrupted sessions at boot instead of listing them for the user\n   * to tap (plan Phase 7). A boolean here rather than the tri-state the loader\n   * returns: by the time a ServerConfig is built the \"never asked\" case has\n   * already resolved to false. Precedence: explicit here → `auto_resume_on_boot`\n   * in server.yaml → false. Never enabled implicitly — it is the one setting\n   * that starts an agent nobody asked for in that moment.\n   */\n  autoResumeOnBoot?: boolean;\n  cacheDir?: string;\n  // Path to the authoritative session registry. Defaults to runtime.db beside\n  // server.yaml (THREADBASE_CONFIG_DIR aware) — never under cacheDir, which a\n  // reset-and-rescan is allowed to delete.\n  runtimeDbPath?: string;\n  tailSize?: number;\n  directoryScanDebounceMs?: number; // trailing debounce before flagging the scanner stale on directory events (default 1000)\n  defaultSystemPrompt?: string; // prepended to every PTY session's --system-prompt; overrides the built-in default\n  // Codex has no --system-prompt flag — the only injection point is the\n  // positional [PROMPT] argument, which Codex treats as the opening turn\n  // rather than a system-level instruction. Off by default so a fresh Codex\n  // session never gets an uninvited first message; enable to send the built\n  // system prompt (+ browse-root boundary + client prompt) that way anyway.\n  //\n  // LEGACY EXPLICIT OVERRIDE, not a second source of truth: this is the same\n  // bit as featureFlags.codexSystemPrompt (see src/feature-flags.ts) and, when\n  // set, outranks every other source. It predates the flag registry and is kept\n  // so embedders and tests that set it directly keep working.\n  codexSystemPromptEnabled?: boolean;\n  // Feature-flag values from the CLI (`--feature <id=bool>`). Outranked by the\n  // matching env var and by codexSystemPromptEnabled above; outranks\n  // feature_flags: in server.yaml. See resolveFeatureFlags().\n  featureFlags?: FeatureFlagValues;\n  defaultPermissionMode?: PermissionMode; // Claude Code --permission-mode for spawned PTY sessions (default \"acceptEdits\")\n  defaultModel?: string; // Claude Code --model for spawned PTY sessions (default \"sonnet\")\n  defaultEffort?: EffortLevel; // Claude Code --effort for spawned PTY sessions (default \"low\")\n  claudeFlags?: ClaudeFlagValues; // allowlisted Claude CLI flags appended to every spawn (see src/claude-flags.ts)\n  claudeExtraArgs?: string; // free-text argv appended after claudeFlags; unvalidated escape hatch\n}\n\n// ─── PTY Manager ───────────────────────────────────────────────────\n\nexport interface PTYManagerOptions {\n  onOutput?: (sessionId: string, data: string) => void;\n  onStatusChange?: (session: ManagedSession) => void;\n  onReady?: (session: ManagedSession) => void;\n  // Fired when a permission gate opens (gate !== null, scraped from the rendered\n  // screen) or closes (gate === null). Detected live from the PTY stream (OSC\n  // 777 + rendered options) — not JSONL. Additive; absent in tests that omit it.\n  onPermissionChange?: (\n    sessionId: string,\n    gate: {\n      prompt?: string;\n      detail?: string;\n      options: PermissionOption[];\n      cursor?: number;\n    } | null,\n    occurrenceId?: string,\n  ) => void;\n  /**\n   * Fired when the agent's phase within a running turn changes, including to\n   * `null` at turn end. Additive; absent in tests that omit it.\n   *\n   * Deliberately NOT routed through onStatusChange, even though that callback\n   * already exists and is already relayed across the pty-host boundary. Its\n   * handler writes a DB row per invocation with no same-status guard, refreshes\n   * the scanner index, broadcasts globally, and pokes the APNs and push\n   * notifiers — machinery built for a handful of transitions per session, not\n   * for a signal that can fire every SCRAPE_THROTTLE_MS.\n   *\n   * The server must broadcast this to that session's subscribers only\n   * (wsHub.broadcastToClients), as a minimal frame rather than a SessionResponse\n   * copy: managedToResponse recomputes elapsedMs on every call, so a session\n   * copy would differ every tick and re-render every client consumer of that\n   * session for the whole turn.\n   */\n  onPhaseChange?: (sessionId: string, phase: AgentPhase | null) => void;\n  // Fired when the composer's dim next-prompt suggestion appears, changes or is\n  // cleared (`null`). Change-guarded like onPhaseChange: the scrape runs per chunk.\n  onPromptSuggestionChange?: (sessionId: string, text: string | null) => void;\n  // Fired when an AskUserQuestion menu is detected on the rendered screen (before\n  // the JSONL tool_use block flushes). The server de-dupes against the JSONL path.\n  onLiveQuestion?: (sessionId: string, questions: AskQuestion[], occurrenceId?: string) => void;\n  // Fired when a previously-detected AskUserQuestion screen menu disappears (the\n  // user answered it and Claude's prompt marker is back). Lets the server clear\n  // the pending question so an answered menu doesn't linger or re-appear.\n  onLiveQuestionGone?: (sessionId: string) => void;\n  // Fired when a user message is actually submitted to the PTY (after the\n  // paste/submit write, including the queued-input flush). Ground truth for\n  // user-owned output — not fired for raw keystrokes (sendKeys).\n  onUserMessage?: (sessionId: string, text: string, ts: number) => void;\n  logger?: import(\"./logger\").Logger;\n}\n\nexport interface StartSessionOptions {\n  projectPath: string;\n  projectName?: string;\n  branch?: string;\n  /**\n   * Provider-side id to resume from, when it differs from `sessionId`. Codex\n   * keys a fresh session by a local placeholder UUID and only learns its real\n   * rollout id once it writes the file, so resuming needs the rollout id in\n   * argv while the session keeps the placeholder the client navigated to.\n   * Absent means \"resume by the session id\", which is what every Claude session\n   * does — PTYManager ignores this field entirely.\n   */\n  resumeId?: string;\n  permissionMode?: PermissionMode;\n  model?: string;\n  effort?: EffortLevel;\n  claudeFlags?: ClaudeFlagValues;\n  claudeExtraArgs?: string;\n}\n\nexport interface StartForkSessionOptions {\n  /**\n   * Provider-side id of the conversation to fork FROM (for Codex, the rollout\n   * id — never a local placeholder). The forked session gets its own id; the\n   * two identities are deliberately kept distinct.\n   */\n  forkFromId: string;\n  projectPath: string;\n  projectName?: string;\n  branch?: string;\n}\n\nexport interface StartFreshSessionOptions {\n  projectPath: string;\n  projectName?: string;\n  systemPrompt?: string;\n  permissionMode?: PermissionMode;\n  model?: string;\n  effort?: EffortLevel;\n  claudeFlags?: ClaudeFlagValues;\n  claudeExtraArgs?: string;\n}\n\n// ─── Session Runner ────────────────────────────────────────────────\n\n// Provider-neutral subset of PTYManager's public surface that\n// LiveSessionManager delegates to. Each provider (claude-code, codex-cli,\n// ...) implements this against its own process-management mechanics.\nexport interface SessionRunner {\n  start(sessionId: string, options: StartSessionOptions): Promise<ManagedSession>;\n  startFresh(options: StartFreshSessionOptions): Promise<ManagedSession>;\n  sendInput(sessionId: string, input: string): number;\n  sendKeys(sessionId: string, keys: string): void;\n  sendRawKeys(sessionId: string, keys: string): void;\n  /**\n   * Resize the session's PTY. A no-op for a session this runner does not own,\n   * so a caller racing a session's exit does not have to guard the call.\n   *\n   * Sessions still SPAWN at the fixed `PTY_COLS`/`PTY_ROWS`: those are the size\n   * every headless consumer (mobile's VirtualTerminal, the replay ring buffer)\n   * assumes, and nothing here changes that default. This exists for an attached\n   * local terminal, which has a real size of its own and is the only caller\n   * that can know it.\n   */\n  resize(sessionId: string, cols: number, rows: number): void;\n  cancel(sessionId: string): void;\n  killPid(pid: number): void;\n  putOnHold(sessionId: string, signal?: NodeJS.Signals): void;\n  getOutput(sessionId: string): string;\n  getOutputLines(sessionId: string, maxLines: number): Promise<string[]>;\n  // Recorded user messages submitted to the PTY, oldest-first. Empty for an\n  // unknown session. Sourced by terminal_replay so a re-subscribing client can\n  // reconcile ground-truth ownership without the live user_message stream.\n  getInputHistory(sessionId: string): UserMessage[];\n  // OS pid of the live agent process, or null if this runner doesn't own the\n  // session. Recorded in the durable session registry so a later streamer run\n  // can probe whether the process outlived it.\n  getPid(sessionId: string): number | null;\n  getSession(sessionId: string): ManagedSession | null;\n  hasSession(sessionId: string): boolean;\n  listSessions(): ManagedSession[];\n  dispose(): void;\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\";\nimport { confidenceForSource } from \"./types\";\n\nexport class SessionStore {\n  constructor(\n    private readonly visibility: (id: string, session?: ManagedSession) => boolean = () => true,\n  ) {}\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    // A phase only exists *within* a running turn, so leaving `running` clears\n    // it. Enforced here rather than at each transition because only markReady\n    // (running -> waiting_input) clears it in the runners: every other exit —\n    // handleExit, putOnHold, failStartup, in both runners — would otherwise\n    // leave a dead session reporting `{status:\"idle\", subStatus:\"working\"}`\n    // forever, and a merging client cannot express a removed key. Keyed off\n    // `updates.status` rather than the merged status so a phase write (which\n    // carries no status) can never be dropped by a store copy that has not yet\n    // seen the running transition.\n    if (updates.status != null && updates.status !== \"running\") session.subStatus = null;\n    // Likewise a suggestion exists only while the composer is waiting on input.\n    if (updates.status != null && updates.status !== \"waiting_input\") {\n      session.promptSuggestion = null;\n    }\n    return session;\n  }\n\n  removeManaged(sessionId: string): boolean {\n    return this.managed.delete(sessionId);\n  }\n\n  /**\n   * The **live** stored record — mutating it mutates the store. Paired with\n   * `get()`, which hands back a throwaway response copy. Prefer\n   * `updateManaged()` for writes; this is for readers that need the internal\n   * shape (Date fields, `rehydrated`, …) rather than the wire shape.\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  /**\n   * The **live** stored records — mutating an element mutates the store. Paired\n   * with `list()`, which hands back throwaway response copies.\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  //\n  // Returns freshly constructed response objects, NOT references into the\n  // store — hence `Readonly`: writing to one changes nothing, so the compiler\n  // refuses it. Persist state with `updateManaged()`; to decorate a response,\n  // build a new object (`{ ...s, … }`) as `withReconciledLifecycle` does.\n  list(ptyAttachedIds: Set<string>): readonly Readonly<SessionResponse>[] {\n    const results: SessionResponse[] = [];\n    const seenIds = new Set<string>();\n\n    for (const s of this.managed.values()) {\n      if (!this.visibility(s.boundConversationId ?? s.id, s)) continue;\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 (!this.visibility(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  // A freshly constructed response object, NOT a reference into the store —\n  // hence `Readonly`, for the same reason as `list()` above. Use `getManaged()`\n  // when you want the live record.\n  get(sessionId: string, ptyAttachedIds: Set<string>): Readonly<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 statusSortValue(s);\n  }\n}\n\n// `sortBy=status` sorts by an explicit rank, not by the status string: the two\n// live statuses (`running`, `waiting_input`) rank ahead of `idle` as one bucket,\n// and inside a bucket the most recently active session comes first — the order a\n// client renders anyway. A lexicographic sort put `idle` first on `asc`.\n//\n// Both halves are folded into ONE ascending string rather than a rank plus a\n// secondary comparator key, because `findCursorBoundary` resumes a page from\n// `{k, id}` alone: a secondary key it cannot see would put the boundary scan and\n// the sort in disagreement. (Pagination over `status` is still racy by nature —\n// the key mutates and the list is re-derived per request — that is unchanged and\n// out of scope here.)\n//\n// `order=desc` therefore reverses the whole thing: `idle` first, oldest first.\nfunction statusSortValue(s: SessionResponse): string {\n  const rank = s.status === \"idle\" ? 1 : 0;\n  const activeAt = Date.parse(s.lastActivityAt ?? s.startedAt);\n  // Complement so newest sorts first under an ascending compare. Fixed width so\n  // the comparison stays lexicographic-safe.\n  const recency = String(1e15 - (Number.isNaN(activeAt) ? 0 : activeAt)).padStart(16, \"0\");\n  return `${rank}:${recency}`;\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\n/** Multi-agent sessions set `currentTurnId` (possibly null); PTY sessions leave it undefined. */\nfunction isLiveMultiAgent(s: ManagedSession): boolean {\n  return s.currentTurnId !== undefined && (s.status === \"running\" || s.status === \"waiting_input\");\n}\n\nfunction managedToResponse(s: ManagedSession, ptyAttached: boolean): SessionResponse {\n  return {\n    isSubagent: s.isSubagent ?? false,\n    parentConversationId: s.parentConversationId ?? null,\n    id: s.id,\n    conversationId: s.id,\n    provider: s.provider ?? CLAUDE_CODE_PROVIDER,\n    status: s.status,\n    // Lifecycle for a session this run knows about. `attached` while we hold\n    // its PTY; once the PTY is gone the session is terminal from this run's\n    // perspective — `failed` when it recorded a reason, else `completed`.\n    // A `rehydrated` stub is the exception: the boot rehydrator seeded it from\n    // the durable registry, so it is a previous run's session with no process\n    // behind it — `resumable`, and `historical` rather than `managed`\n    // (docs/plans/live-sessions-persistence-plan.md §4, Phase 1).\n    // A session this run itself put on hold (grace timer, explicit hold_session,\n    // idle reaper) is the other exception: the PTY is gone the same way an exit\n    // leaves it gone, but the conversation is still resumable, not terminal —\n    // `putOnHold` records that by leaving `statusSource: \"shutdown\"` (the only\n    // place either runner sets it), so it is checked here alongside `rehydrated`.\n    // Multi-agent sessions never have a PTY (`currentTurnId` is defined — null\n    // while idle between turns — only on that path). While their status is\n    // still live they are `attached`, not terminal (#438).\n    // Otherwise, terminal requires evidence of termination — a recorded\n    // `failureReason`, or the `completedAt` every exit path stamps. Without\n    // either, no PTY here means the spawn has not landed, which is `starting`,\n    // not `completed` (tb-mobile #508). Scoped to the PTY path only\n    // (`currentTurnId === undefined`) — multi-agent never stamps `completedAt`\n    // at all, so an idle multi-agent session (no pre-attach race to\n    // disambiguate) stays `completed` regardless.\n    lifecycle:\n      ptyAttached || isLiveMultiAgent(s)\n        ? \"attached\"\n        : s.rehydrated || s.statusSource === \"shutdown\"\n          ? \"resumable\"\n          : s.failureReason != null\n            ? \"failed\"\n            : s.currentTurnId === undefined && s.completedAt == null\n              ? \"starting\"\n              : \"completed\",\n    lifecycleSource:\n      ptyAttached || isLiveMultiAgent(s)\n        ? s.reconciled\n          ? \"reconcile\"\n          : \"spawn\"\n        : s.rehydrated\n          ? \"reconcile\"\n          : s.currentTurnId === undefined && s.completedAt == null\n            ? \"spawn\"\n            : \"exit\",\n    // We own its PTY, so `status` is the authoritative signal — no inferred\n    // `activity` is attached for managed sessions.\n    ownership: s.rehydrated ? \"historical\" : \"managed\",\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    // Unconditional, like completedAt above — do NOT move this into the\n    // `...(x != null && { x })` block below. That guard uses loose `!=`, which\n    // catches null as well as undefined, and would turn an explicit \"no phase\"\n    // back into an absent key. The client merges session frames, so an absent\n    // key keeps the previous value and the indicator latches on a finished\n    // turn — the tb-mobile PR #647 bug, arriving through the serialiser.\n    subStatus: s.subStatus ?? null,\n    // Unconditional for the same reason as subStatus: absence must never be a\n    // third state on the wire.\n    promptSuggestion: s.promptSuggestion ?? 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.effort != null && { effort: s.effort }),\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    // C3: how the status was derived, and how far to trust it. Confidence is\n    // derived from the source rather than stored, so the two cannot disagree.\n    ...(s.statusSource != null && {\n      statusSource: s.statusSource,\n      statusConfidence: confidenceForSource(s.statusSource),\n    }),\n    ...(s.statusUpdatedAt != null && { statusUpdatedAt: s.statusUpdatedAt.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.forkedFromConversationId != null && {\n      forkedFromConversationId: s.forkedFromConversationId,\n    }),\n    ...(s.boundConversationId != null && { boundConversationId: s.boundConversationId }),\n    ...(s.interruptedStatus != null && { interruptedStatus: s.interruptedStatus }),\n  };\n}\n\nfunction discoveredToResponse(d: DiscoveredProcess, conversationId: string): SessionResponse {\n  return {\n    id: conversationId,\n    conversationId,\n    provider: d.provider,\n    // Stays \"idle\" deliberately: we cannot see this process's prompt state, and\n    // reporting `running` would route mobile to the destructive Overtake screen.\n    // Liveness travels in the additive fields below instead.\n    status: \"idle\",\n    ownership: \"external\",\n    // Discovery just enumerated this PID, so it was alive moments ago. We never\n    // report \"gone\" here — a vanished process simply stops being listed.\n    processLiveness: \"alive\",\n    // Alive, but spawned outside this streamer, so we hold no PTY for it. That\n    // is precisely `detached` — and it is strictly more informative than the\n    // `status: \"idle\"` above, which discovery is forced to report because it\n    // cannot see the process's prompt state.\n    lifecycle: \"detached\",\n    lifecycleSource: \"probe\",\n    // No PTY here, so nothing to scrape and no phase to report. Emitted\n    // explicitly rather than omitted, for the same reason as in\n    // managedToResponse: absence must never be a third state on the wire.\n    subStatus: null,\n    promptSuggestion: null,\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 type { WebSocket } from \"ws\";\nimport { contextRegistry, type E2eeContext } from \"./e2ee/context\";\nimport {\n  E2EE_CTX_UNKNOWN,\n  E2EE_DEVICE_REVOKED,\n  E2EE_SEAL_FAILED,\n  E2EE_SEQUENCE_VIOLATION,\n  type E2eeRejectionCode,\n  own,\n} from \"./e2ee/protocol\";\nimport { CHANNEL_WS, RecordError } from \"./e2ee/record\";\nimport { getLogger } from \"./logger\";\nimport type { WSMessage } from \"./types\";\n\n/**\n * The one interval both liveness signals run on. It must stay under\n * `CLIENT_SILENCE_TIMEOUT_MS`.\n *\n * Two pings leave this timer per sweep and they are **not** redundant. The\n * WebSocket PROTOCOL ping proves the TCP connection is alive to the socket\n * layer, and is handled below `onmessage` — React Native's JS layer never sees\n * it. The app-level `{ type: \"ping\" }` frame is the only liveness signal the\n * client's silence timer can observe. Without it an idle-but-alive session\n * redials every `CLIENT_SILENCE_TIMEOUT_MS`: measured on hardware at 3.1\n * context opens per minute against a limit of 5 per device per minute, 62 % of\n * the budget spent while nobody touched the phone (tb-mobile #946).\n *\n * **One timer rather than two.** A single schedule cannot drift against itself,\n * there is one place to change the cadence, and the ordering argument on\n * `sendTo` has to hold for one call site instead of two.\n */\nexport const PING_INTERVAL_MS = 30_000;\n/**\n * The client's silence timer, mirrored here because this file's cadence is only\n * correct relative to it — `WS_SILENCE_TIMEOUT_MS` in tb-mobile\n * `hooks/useTerminalStream.ts`. A client that receives nothing for this long\n * calls `forceReconnect`, and against a pinned server every reconnect is a\n * fresh Noise handshake charged to a 5-per-minute-per-device limit.\n *\n * Nothing reads it at runtime. It is here so a test can assert\n * `PING_INTERVAL_MS` stays under it: raising the cadence past this window\n * silently restores the churn the app-level ping was added to stop, and no\n * other part of the server would notice.\n */\nexport const CLIENT_SILENCE_TIMEOUT_MS = 45_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\nconst log = getLogger(\"ws\");\n\n/**\n * How long a sealed socket has to prove it holds the keys (NONCE-DESIGN §10).\n *\n * The clock starts at the 101 and stops on the FIRST frame that unseals — **any\n * valid sealed inbound frame, never a particular message type.** The client\n * contract says a socket sends `register` promptly, and that is one way for a\n * client to satisfy this; making it the server's condition would tie the server\n * to a message name it does not need and break the day a client legitimately\n * sends something else first.\n *\n * It exists because of the ticket thief. An intermediary that spends the\n * client's ticket first holds a socket bound to a context whose keys it does not\n * have: it gets no plaintext — every frame is sealed to keys it lacks — but it\n * occupies a hub slot and receives sealed broadcasts, and **the existing ping\n * reaper cannot evict it, because answering a pong costs it nothing.** Silence\n * is precisely what it is good at. This is the only clock that runs on a socket\n * that never speaks.\n *\n * **Ten seconds, and 15 s — the client's own connect timeout — is the only\n * permitted relaxation. Never lower**: a real phone on a bad network has to fit\n * an upgrade and one frame inside it.\n */\nexport const WS_FIRST_FRAME_DEADLINE_MS = 10_000;\n\n/**\n * The plaintext of one broadcast, built at most once.\n *\n * A broadcast serialises its message once and then seals it N times. The JSON\n * is shared; the ciphertext never is. Lazy so a hub with no sealed socket at\n * all — every client on the legacy path — allocates nothing extra.\n */\ninterface Memo {\n  plaintext?: Buffer;\n}\n\n/** The §9 code a record-layer throw carries, or a server fault if it carries none. */\nfunction sealCode(err: unknown): E2eeRejectionCode {\n  return err instanceof RecordError ? err.code : E2EE_SEAL_FAILED;\n}\n\n/**\n * The code the SEND path is allowed to report (§9).\n *\n * A sequence violation is a **claim about the peer**. Nothing we fail at while\n * sealing our own frame can be one, so a send-path error carrying that code\n * would tell a client its frames were wrong when the fault was entirely ours —\n * the collapse §9 splits these two codes to prevent, arriving from the other\n * direction. Unreachable today, because `seal` only ever raises\n * `E2EE_SEAL_FAILED`; asserted here so it stays unreachable rather than staying\n * unreachable by luck. The original code is still logged.\n */\nfunction sendCode(err: unknown): E2eeRejectionCode {\n  const code = sealCode(err);\n  return code === E2EE_SEQUENCE_VIOLATION ? E2EE_SEAL_FAILED : code;\n}\n\n/**\n * Whatever `ws` handed us, as bytes.\n *\n * A sealed socket speaks the binary opcode (§12), so anything else — a text\n * frame, a client that forgot `binaryType` — is not a record. It becomes an\n * empty buffer and is refused by the record layer's own length check rather\n * than by a second copy of that check here.\n */\nfunction asBytes(raw: unknown): Buffer {\n  if (Buffer.isBuffer(raw)) return raw;\n  if (raw instanceof ArrayBuffer) return Buffer.from(raw);\n  if (ArrayBuffer.isView(raw)) return Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength);\n  return Buffer.alloc(0);\n}\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   * The E2EE context of every sealed socket. Absent for a legacy `?key=` client.\n   *\n   * PER SOCKET, not per device (NONCE-DESIGN §8). That is what forces N seals\n   * for N sockets below: the three send paths used to `JSON.stringify` once and\n   * hand the same bytes to every client, and the same bytes cannot be sealed to\n   * N different keys under N independent counters.\n   *\n   * A socket absent from this map is a legacy plaintext client and keeps\n   * working exactly as it does today — dual paths are the whole reason a\n   * released app survives this change.\n   */\n  private contexts = new Map<WebSocket, E2eeContext>();\n  /**\n   * Every socket that has EVER held a context.\n   *\n   * `contexts` is cleared the moment a socket is closed for a policy violation\n   * or a revocation, and `close()` is not instantaneous — a frame already in\n   * the receive buffer still arrives. Without this set that frame would find no\n   * context and be handled as a LEGACY PLAINTEXT one: a sealed socket\n   * downgrading itself to cleartext in the window after it was cut off, which\n   * is the one thing a dual-path design must never do.\n   */\n  private everSealed = new WeakSet<WebSocket>();\n  /** Per-socket \"prove you hold the keys\" deadline; cleared on the first unseal. */\n  private unprovenTimers = new Map<WebSocket, ReturnType<typeof setTimeout>>();\n  private readonly firstFrameMs: number;\n\n  /**\n   * `firstFrameMs` is a test seam and nothing else: the deadline it defaults to\n   * is the contract, and a suite cannot spend ten seconds per case proving a\n   * socket went away. A test asserts the DEFAULT is the contract's value, so\n   * lowering the constant is still caught.\n   *\n   * **Read with `own()`, never `??`.** `server.ts` constructs `new WSHub()`, so\n   * `options` is `{}` — an object that carries `Object.prototype`, and `??`\n   * reads straight through it. A single `Object.prototype.firstFrameMs`\n   * anywhere in the process then sets this deadline, in either direction and\n   * both behavioural: lengthened, the ticket-thief defence stops firing\n   * entirely, because the ping reaper cannot evict a socket that answers\n   * pongs; shortened below the floor, every legitimate socket is reaped before\n   * a phone on a bad network can fit an upgrade and one frame inside it.\n   *\n   * `own()` asks the object and nothing above it. `context.ts` reads `now` this\n   * way and `record.ts` reads `initialCounter` this way, for exactly this\n   * reason; this call site is the one that did not.\n   */\n  constructor(options: { firstFrameMs?: number } = {}) {\n    this.firstFrameMs = own(options, \"firstFrameMs\") ?? WS_FIRST_FRAME_DEADLINE_MS;\n  }\n\n  addClient(ws: WebSocket, context?: E2eeContext): void {\n    this.clients.add(ws);\n    if (context) {\n      this.contexts.set(ws, context);\n      // Set HERE, at attach — not on the first successful seal. Whoever holds\n      // this socket has spent a ticket, and everything downstream that asks\n      // \"was this ever sealed?\" must be true from the 101 onwards.\n      this.everSealed.add(ws);\n      log.info(\"[e2ee.upgrade] encrypted websocket\", { event: \"e2ee.upgrade\" });\n      // The clock starts at the 101.\n      this.unprovenTimers.set(\n        ws,\n        setTimeout(() => {\n          this.unprovenTimers.delete(ws);\n          log.warn(\"[e2ee] closing a sealed socket that never proved it holds the keys\", {\n            event: \"e2ee.unproven_socket_reaped\",\n          });\n          // `E2EE_CTX_UNKNOWN` and not a fifth code: by the time this close\n          // lands the context IS destroyed, so the code is literally true, and\n          // §9 already defines it as the recoverable one — a legitimate client\n          // whose first frame was lost to a network stall re-opens, which is\n          // exactly the right recovery. A thief learns nothing it did not\n          // already know.\n          this.closeForE2ee(ws, E2EE_CTX_UNKNOWN, \"unproven\");\n        }, this.firstFrameMs),\n      );\n    }\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      this.forgetContext(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      this.forgetContext(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 memo: Memo = {};\n    const dead: WebSocket[] = [];\n\n    for (const client of this.clients) {\n      if (!this.sendTo(client, data, memo)) dead.push(client);\n    }\n\n    for (const client of dead) {\n      this.clients.delete(client);\n    }\n  }\n\n  // Scoped broadcast for high-frequency per-session messages (terminal_output,\n  // user_message). Sending to every connected client for every PTY output\n  // chunk made broadcast() cost scale with connections x active sessions;\n  // this bounds it to only that session's subscribers.\n  broadcastToClients(clients: Iterable<WebSocket>, message: WSMessage): void {\n    const data = JSON.stringify(message);\n    const memo: Memo = {};\n    for (const client of clients) {\n      if (!this.sendTo(client, data, memo)) this.clients.delete(client);\n    }\n  }\n\n  unicast(ws: WebSocket, message: WSMessage): void {\n    if (!this.sendTo(ws, JSON.stringify(message), {})) this.clients.delete(ws);\n  }\n\n  /**\n   * Seal for THIS socket, then send. One synchronous step, with no `await`\n   * anywhere between the two (NONCE-DESIGN §14).\n   *\n   * An await between `seal` and `ws.send` reorders frames — two sends resumed\n   * in the wrong order put counter 8 on the wire before counter 7 — and the\n   * peer's strict `counter == expected` (§5 R2) then closes the socket. The\n   * `await getOutputLines(...)` on the `terminal_replay` path is upstream of\n   * this method for exactly that reason: it finishes, and only then is anything\n   * sealed.\n   *\n   * Returns false when the caller should drop this client.\n   *\n   * The plaintext Buffer is memoised across a broadcast because it is the same\n   * bytes for every recipient; the SEAL is not, and cannot be — N sockets means\n   * N keys, N counters and N distinct nonces. `seal` does not mutate its\n   * argument, so one Buffer feeds every seal.\n   *\n   * An app-level `{ type: \"ping\" }` frame — emitted by `startPing` — is sealed\n   * here like every other frame and consumes a counter. That is correct and\n   * costs nothing, and it is written down because WebSocket PROTOCOL pings are\n   * invisible to React Native's JS layer — the client's silence timer depends on\n   * the app-level ping continuing to exist, so nobody should optimise it away on\n   * the grounds that the protocol already has one (NONCE-DESIGN §18).\n   */\n  private sendTo(ws: WebSocket, json: string, memo: Memo): boolean {\n    const context = this.contexts.get(ws);\n    // **A socket that consumed a ticket is never on the plaintext send path**\n    // (NONCE-DESIGN §8). Sealed-and-now-contextless is not a legacy client; it\n    // is a fault, and it closes rather than falling through.\n    //\n    // Unreachable as the code stands — the only way to hold `everSealed` with\n    // no context is `forgetContext`, which runs on close or error, and the\n    // `readyState` check a few lines down catches that socket anyway. The guard\n    // is here because UNREACHABILITY was doing the work: detach a context from\n    // a live socket by any future means and the failure is a silent plaintext\n    // `session_list`, which enumerates every session with its project path.\n    if (!context && this.everSealed.has(ws)) {\n      this.closeForE2ee(ws, E2EE_SEAL_FAILED, \"send-without-context\");\n      return false;\n    }\n    if (ws.readyState !== ws.OPEN) {\n      if (context) this.closeForE2ee(ws, E2EE_SEAL_FAILED, \"transport-not-open\");\n      return false;\n    }\n    let frame: string | Buffer = json;\n    if (context) {\n      memo.plaintext ??= Buffer.from(json, \"utf-8\");\n      try {\n        frame = context.sendState(CHANNEL_WS).seal(memo.plaintext);\n      } catch (err) {\n        // A seal failure is a SERVER-side fault and gets its own code (§9).\n        // Closing rather than dropping is deliberate: a dropped frame is a gap,\n        // and under the strict counter the peer would reject everything after\n        // it anyway — with a reason that pointed at the client.\n        this.closeForE2ee(ws, sendCode(err), \"send\", err);\n        return false;\n      }\n    }\n    try {\n      ws.send(frame);\n      return true;\n    } catch (err) {\n      if (context) this.closeForE2ee(ws, E2EE_SEAL_FAILED, \"transport-send\", err);\n      return false;\n    }\n  }\n\n  /**\n   * Decode one client→server frame: unseal it for a sealed socket, pass it\n   * through for a legacy one.\n   *\n   * Returns null when the frame was refused — the socket has already been\n   * closed with the §9 code that says why, and the caller drops the frame. A\n   * refusal is never silent: `E2EE_SEQUENCE_VIOLATION` is a claim about the\n   * peer, `E2EE_SEAL_FAILED` is a fault on this side, and the two must not\n   * arrive as the same \"nothing happened\".\n   */\n  receive(ws: WebSocket, raw: unknown): string | null {\n    const context = this.contexts.get(ws);\n    if (!context) {\n      // A socket that was never sealed is a legacy client; one that WAS is a\n      // socket already closed for cause, and it does not get to speak plaintext\n      // on the way out.\n      if (this.everSealed.has(ws)) return null;\n      return typeof raw === \"string\" ? raw : String(raw);\n    }\n\n    try {\n      const plaintext = context.receiveState(CHANNEL_WS).unseal(asBytes(raw)).toString(\"utf-8\");\n      // Proved. ANY valid sealed frame stops the clock — the unseal succeeding\n      // is the proof, and what the frame says is none of this method's business.\n      this.clearUnproven(ws);\n      return plaintext;\n    } catch (err) {\n      this.closeForE2ee(ws, sealCode(err), \"receive\", err);\n      return null;\n    }\n  }\n\n  /** Close every socket bound to one of these explicit context handles. */\n  closeContexts(ctxIds: Iterable<string>): number {\n    const ids = new Set(ctxIds);\n    if (ids.size === 0) return 0;\n    let closed = 0;\n    for (const [ws, context] of this.contexts) {\n      if (!ids.has(context.ctxId)) continue;\n      closed++;\n      this.closeForE2ee(ws, E2EE_DEVICE_REVOKED, \"revoked\");\n    }\n    return closed;\n  }\n\n  /** Close every sealed socket the hub owns for one device. */\n  closeDevice(deviceId: string): number {\n    let closed = 0;\n    for (const [ws, context] of this.contexts) {\n      if (context.deviceId !== deviceId) continue;\n      closed++;\n      this.closeForE2ee(ws, E2EE_DEVICE_REVOKED, \"revoked\");\n    }\n    return closed;\n  }\n\n  /**\n   * A socket's close destroys ITS OWN context and nothing else.\n   *\n   * Never the device's REST context (§8): the 2 s HTTP replay fallback runs\n   * precisely when the socket is down, so a REST context that died with the\n   * socket would take the fallback with it. A reconnect is a new\n   * `POST /api/e2ee/open`, not a resurrection of this one.\n   */\n  private clearUnproven(ws: WebSocket): void {\n    const t = this.unprovenTimers.get(ws);\n    if (t) {\n      clearTimeout(t);\n      this.unprovenTimers.delete(ws);\n    }\n  }\n\n  private forgetContext(ws: WebSocket): void {\n    // Unconditional, and before the early return: a socket whose context has\n    // already gone must not leave a timer behind to fire at a dead socket.\n    this.clearUnproven(ws);\n    const context = this.contexts.get(ws);\n    if (!context) return;\n    this.contexts.delete(ws);\n    contextRegistry().destroyOwned(context);\n  }\n\n  private closeForE2ee(ws: WebSocket, code: E2eeRejectionCode, phase: string, err?: unknown): void {\n    const violation = code === E2EE_SEQUENCE_VIOLATION;\n    // **The record layer's own words, carried through.** Without them a frame\n    // addressed to ANOTHER live context and a frame naming a context that never\n    // existed logged byte-identical lines — the hub threw away the distinction\n    // the record layer had already made. §9 is explicit that the wording here is\n    // what a human reads at 3am: \"misaddressed\" and \"unknown\" call for different\n    // next steps, and the log was the only place either could be seen.\n    const detail = err instanceof RecordError ? err.message : undefined;\n    const reported = err instanceof RecordError ? err.code : undefined;\n    log.warn(\n      `[e2ee.${violation ? \"sequence_violation\" : \"frame_refused\"}] ${phase}${detail ? `: ${detail}` : \"\"}`,\n      {\n        event: violation ? \"e2ee.sequence_violation\" : \"e2ee.frame_refused\",\n        code,\n        phase,\n        ...(detail ? { detail } : {}),\n        // Differs from `code` only where the send path coerced a peer-claim into\n        // a server fault; logging both keeps that coercion visible rather than\n        // silent.\n        ...(reported && reported !== code ? { reported } : {}),\n      },\n    );\n    this.clients.delete(ws);\n    try {\n      // 1008 policy violation. The REASON is the frozen §9 code, so the client\n      // can tell a policy close from a network drop and from each other.\n      ws.close(1008, code);\n    } catch {\n      // Already gone.\n    }\n    this.forgetContext(ws);\n  }\n\n  get connectionCount(): number {\n    return this.clients.size;\n  }\n\n  /** Sealed sockets. Tests and diagnostics — never a key, never a `ctxId`. */\n  get sealedCount(): number {\n    return this.contexts.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    // Destroy them HERE rather than leaving it to each socket's `close` event.\n    // `terminate()` fires that event on a later tick, by which point this map\n    // is empty and `forgetContext` finds nothing — so the registry would keep\n    // every context of a hub that had already gone away. §8: contexts are\n    // in-memory only and do not outlive the thing they belong to.\n    for (const context of this.contexts.values()) {\n      contextRegistry().destroyOwned(context);\n    }\n    this.contexts.clear();\n    for (const [, t] of this.unprovenTimers) {\n      clearTimeout(t);\n    }\n    this.unprovenTimers.clear();\n  }\n\n  /**\n   * The maintenance sweep: both liveness pings, and the stale-context check.\n   *\n   * **The callback must stay synchronous.** `sendTo` seals and sends in one\n   * synchronous step precisely so no `await` can sit between the two and reorder\n   * counters (see `sendTo`), and that guarantee is what makes a periodic sender\n   * safe at all: a synchronous block runs to completion, so two sends cannot\n   * interleave and counter order is wire order. Making this callback `async`\n   * would create the hazard `sendTo` documents rather than inherit its absence.\n   *\n   * **Ordering inside the loop is load-bearing.** The app-level ping is emitted\n   * only for a socket that has already passed the stale-context and `readyState`\n   * guards. Sealing on a registry-invalidated context throws, so emitting above\n   * the stale-context guard would attempt a send on a socket this sweep has\n   * already decided to close, and report the refusal as `phase: \"send\"` rather\n   * than `phase: \"maintenance\"` — the sweep's own verdict, logged as if the\n   * frame had been at fault. The §9 code is unaffected either way, since\n   * `sendState` on an invalidated context raises `E2EE_CTX_UNKNOWN` itself.\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      // One `ts` and one plaintext Buffer per sweep; the SEAL is still per\n      // socket, because N sockets means N keys and N counters.\n      //\n      // `: WSMessage` is load-bearing, not redundant typing. Do NOT inline this\n      // literal into the `JSON.stringify` below: that function accepts anything,\n      // so an inlined literal is type-checked against nothing and this frame's\n      // shape could drift a field at a time with the build staying green. The\n      // annotation is what makes the contract with tb-mobile's own `WSMessage`\n      // union a build failure (TS2353) rather than a convention everyone has to\n      // remember — the shape is frozen across two repositories (`types.ts` and\n      // NONCE-DESIGN §18), and this is where that freeze is enforced.\n      const appPing: WSMessage = { type: \"ping\", ts: Date.now() };\n      const appPingJson = JSON.stringify(appPing);\n      const memo: Memo = {};\n      for (const client of this.clients) {\n        const context = this.contexts.get(client);\n        if (context && contextRegistry().get(context.ctxId) !== context) {\n          this.closeForE2ee(client, E2EE_CTX_UNKNOWN, \"maintenance\");\n          continue;\n        }\n        if (client.readyState !== client.OPEN) continue;\n        // The app-level liveness frame — the ONLY one the client's silence timer\n        // can see, since the protocol ping below never reaches `onmessage`.\n        // Sealed for a sealed socket and plaintext for a legacy one, by the same\n        // `sendTo` every other frame uses; a legacy client ignores an unknown\n        // type, and its silence timer resets just the same.\n        if (!this.sendTo(client, appPingJson, memo)) {\n          this.clients.delete(client);\n          continue;\n        }\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":["record","resolve","crypto","pino","import_fs","import_path","pool","import_path","import_crypto","import_fs","import_path","import_fs","import_os","import_path","candidates","import_fs","import_os","import_path","run","resolve","import_crypto","import_fs","import_path","OUTPUT_BUFFER_MAX","INPUT_HISTORY_MAX","QUIET_DETECT_MS","SUBMIT_BYTES","toPublicSession","resolve","resolve","import_crypto","import_fs","import_path","COMPOSER_RULE_RE","OPTION_RE","BOX_ONLY_RE","BOX_ONLY_RE","OPTION_RE","BOX_ONLY_RE","ASK_MENU_FOOTER_RE","NUMBERED_RE","ENTER","OUTPUT_BUFFER_MAX","INPUT_HISTORY_MAX","QUIET_DETECT_MS","SUBMIT_BYTES","toPublicSession","resolve","import_child_process","import_os","import_path","resolve","import_crypto","import_fs","import_promises","import_http","import_os","import_path","import_hono","import_crypto","import_crypto","import_crypto","import_crypto","presented","required","log","resolve","seal","body","headers","record","out","import_hono","import_os","import_node_path","resolve","import_hono","ALREADY_HANDLED","import_hono","import_zod","readRawBody","resolve","import_hono","import_zod","readRawBody","resolve","import_hono","ALREADY_HANDLED","alreadyHandled","import_hono","log","import_fs","import_hono","worst","import_node_fs","cached","os","import_hono","import_crypto","import_crypto","import_fs","import_os","import_path","import_node_crypto","import_hono","import_os","import_node_fs","import_node_os","import_node_path","import_zod","parseYaml","json","import_zod","import_node_crypto","log","log","resolve","resolve","readRawBody","platform","readRawBody","log","resolve","import_hono","import_node_fs","import_node_path","import_hono","import_node_os","import_node_path","logger","import_hono","ALREADY_HANDLED","alreadyHandled","import_hono","ALREADY_HANDLED","alreadyHandled","import_hono","import_child_process","cached","resolve","import_hono","ALREADY_HANDLED","alreadyHandled","import_hono","ALREADY_HANDLED","alreadyHandled","import_hono","log","refusal","ALREADY_HANDLED","import_scanner","import_fs","import_path","import_scanner","import_fs","import_promises","import_path","log","import_fs","import_path","import_url","import_meta","getMigrationsDir","import_scanner","import_fs","import_zod","import_scanner","import_scanner","timestamp","isCodexFormatPreamble","spawn","import_fs","cached","import_path","stat","import_crypto","stat","stat","openAsync","yieldToEventLoop","cached","Database","timestamp","run","import_node_fs","import_scanner","cached","resolve","finish","import_scanner","import_fs","import_os","import_path","import_fs","import_path","fsWatch","import_node_crypto","timestamp","import_fs","readBody","resolve","total","adapted","cached","resolve","filePath","availability","messagesPayload","readBody","import_crypto","import_fs","import_path","import_zod","import_crypto","crypto","log","log","import_node_fs","import_node_path","import_promises","import_path","pool","import_node_crypto","DOWN","ENTER","import_child_process","resolve","platform","run","import_fs","platform","import_crypto","import_promises","import_path","heicConvert","resolve","cached","readBody","body","prior","option","import_crypto","import_better_sqlite3","import_os","import_path","Database","import_fs","import_crypto","record","import_node_child_process","import_node_os","import_node_path","resolve","import_scanner","import_fs","import_os","import_path","import_fs","import_os","import_path","resolve","naclUtil","nacl","import_fs","import_fs","import_os","import_path","open","import_crypto","import_fs","import_fs","import_os","import_path","import_fs","import_path","log","import_fs","import_promises","chokidar","import_fs","import_node_child_process","import_os","resolve","run","platform","os","log","log","open","log","log","log","open","normalizeContent","import_scanner","asRecord","join","stat","import_fs","currentBootToken","resolve","log","json","TemporalClient","resolve","ALREADY_HANDLED","readBody","cached"]}