{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server/server.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,OAAO,MAAM,SAAS,CAAC;AAkB9B,OAAO,KAAK,EAAgB,aAAa,EAAE,MAAM,WAAW,CAAC;AAC7D,OAAO,EAKN,qBAAqB,EACrB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,QAAQ,EAA+C,MAAM,gBAAgB,CAAC;AAIvF,OAAO,KAAK,EAA4B,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAG/E,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,OAAO,GAAG;IAClD,mFAAmF;IACnF,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAChC,CAAC;AAEF,MAAM,WAAW,0BAA0B;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,IAAI,CAAC;IACd,QAAQ,EAAE,IAAI,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,sBAAsB;IACtC,IAAI,EAAE,aAAa,CAAC;IACpB,IAAI,EAAE,WAAW,CAAC;IAClB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yEAAuE;IACvE,eAAe,EAAE,MAAM,OAAO,CAAC,0BAA0B,EAAE,CAAC,CAAC;IAC7D,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAClD,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,sGAAsG;IACtG,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2HAAyH;IACzH,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,2GAA2G;IAC3G,YAAY,CAAC,EAAE,qBAAqB,CAAC;IACrC,wDAAwD;IACxD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAGD,eAAO,MAAM,sBAAsB,QAAkB,CAAC;AACtD,eAAO,MAAM,+BAA+B,QAAS,CAAC;AAoFtD,oDAAoD;AACpD,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAQtF;AAQD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,GAAG,kBAAkB,CA+mCzF","sourcesContent":["/**\n * Dashboard HTTP server — Express app wiring auth, the runtime pool, the SSE\n * hub, and the file API into the REST surface the browser client consumes.\n *\n * Bind address discipline: local mode binds 127.0.0.1 only. The\n * caller decides the bind address; `createDashboardServer` never listens by\n * itself. Remote mode still passes every request through DashboardAuth.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport { existsSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { basename, join } from \"node:path\";\nimport type { NextFunction, Request, Response } from \"express\";\nimport express from \"express\";\nimport type {\n\tActiveRuntimeSnapshotDto,\n\tAuthStatusDto,\n\tClientConnectionDiagnosticDto,\n\tDashboardResyncDto,\n\tFleetDto,\n\tImageAttachmentDto,\n\tPairingCodeDto,\n\tRuntimeHydrationDto,\n\tSessionInfoDto,\n\tSessionInventoryDto,\n} from \"../shared/protocol.js\";\nimport {\n\tMAX_CLIENT_DIAGNOSTIC_BYTES,\n\tMAX_PROMPT_BODY_BYTES,\n\tMAX_SESSION_PREVIEW_CHARACTERS,\n} from \"../shared/protocol.js\";\nimport type { AuthDecision, DashboardAuth } from \"./auth.js\";\nimport {\n\tDASHBOARD_IMAGE_ID_PATTERN,\n\tDashboardImageNotFoundError,\n\tDashboardImagePreviewError,\n\ttype DashboardImageScope,\n\tDashboardImageService,\n} from \"./dashboard-images.js\";\nimport { EventHub, formatHeartbeatFrame, type SseWriteMetadata } from \"./event-hub.js\";\nimport { defaultPlaces, FileApi, resolveExistingDirectory } from \"./files.js\";\nimport { ImagePreviewWorker } from \"./image-preview.js\";\nimport { MemoryApi } from \"./memories.js\";\nimport type { DashboardRuntimeSnapshot, RuntimePool } from \"./runtime-pool.js\";\nimport { readSubagentMessages, SubagentSessionLogNotFoundError } from \"./subagent-log.js\";\n\nexport type DashboardServerApp = express.Express & {\n\t/** Close dashboard-owned services. Safe to call more than once during shutdown. */\n\tcloseDashboard(): Promise<void>;\n};\n\nexport interface DashboardSessionInfoSource {\n\tpath: string;\n\tid: string;\n\tcwd: string;\n\tname?: string;\n\tcreated: Date;\n\tmodified: Date;\n\tmessageCount: number;\n\tfirstMessage: string;\n}\n\nexport interface DashboardServerOptions {\n\tauth: DashboardAuth;\n\tpool: RuntimePool;\n\t/** Directory of built client assets; omit to skip static serving (tests). */\n\tstaticDir?: string;\n\t/** Session listing (cross-project) — injected so tests can stub it. */\n\tlistAllSessions: () => Promise<DashboardSessionInfoSource[]>;\n\tdeleteSession: (path: string) => Promise<unknown>;\n\tlogger?: (line: string) => void;\n\t/** Build version of the running server process (for the settings footer / stale-server detection). */\n\tserverVersion?: string;\n\t/** Restart hook — when set, POST /api/server/restart invokes it (typically process exit for a supervisor to respawn). */\n\tonRestart?: () => void;\n\t/** Injectable only to make SSE limits deterministic in integration tests. */\n\teventHub?: EventHub;\n\t/** Injectable bounded image repository/preview service for deterministic tests and lifecycle ownership. */\n\timageService?: DashboardImageService;\n\t/** Named heartbeat interval; defaults to 25 seconds. */\n\theartbeatIntervalMs?: number;\n\t/** Test-only override for the global dreb memory home directory. */\n\tmemoryHomeDir?: string;\n}\n\nconst DEVICE_COOKIE = \"dreb_dashboard_device\";\nexport const MAX_SSE_BUFFERED_BYTES = 4 * 1024 * 1024;\nexport const CLIENT_DIAGNOSTIC_RATE_LIMIT_MS = 30_000;\nconst CLIENT_DIAGNOSTIC_CONNECTION_TTL_MS = 10 * 60_000;\n\nfunction boundedSessionPreview(text: string): string {\n\tlet preview = \"\";\n\tlet characters = 0;\n\tfor (const character of text) {\n\t\tif (characters === MAX_SESSION_PREVIEW_CHARACTERS) break;\n\t\tpreview += character;\n\t\tcharacters++;\n\t}\n\treturn preview;\n}\n\ninterface SessionInfoProjectionSource extends Omit<DashboardSessionInfoSource, \"created\" | \"modified\"> {\n\tcreated: Date | string;\n\tmodified: Date | string;\n}\n\nfunction toIsoString(value: Date | string): string {\n\treturn value instanceof Date ? value.toISOString() : value;\n}\n\nasync function toSessionInfoDto(session: SessionInfoProjectionSource): Promise<SessionInfoDto> {\n\tlet resolvedCwd: string | undefined;\n\ttry {\n\t\tresolvedCwd = await resolveExistingDirectory(session.cwd);\n\t} catch {\n\t\t// Historical metadata remains useful even when its directory disappeared.\n\t}\n\treturn {\n\t\tpath: session.path,\n\t\tid: session.id,\n\t\tcwd: session.cwd,\n\t\tcwdAvailable: resolvedCwd !== undefined,\n\t\t...(resolvedCwd ? { resolvedCwd } : {}),\n\t\tname: session.name,\n\t\tcreated: toIsoString(session.created),\n\t\tmodified: toIsoString(session.modified),\n\t\tmessageCount: session.messageCount,\n\t\tfirstMessage: boundedSessionPreview(session.firstMessage),\n\t};\n}\n\nfunction isClientDiagnostic(value: unknown): value is ClientConnectionDiagnosticDto {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) return false;\n\tconst body = value as Record<string, unknown>;\n\tconst allowed = new Set([\n\t\t\"connectionId\",\n\t\t\"state\",\n\t\t\"previousState\",\n\t\t\"attempt\",\n\t\t\"delayMs\",\n\t\t\"visibility\",\n\t\t\"lastAppliedSeq\",\n\t\t\"heartbeatAgeMs\",\n\t\t\"eventCount\",\n\t\t\"eventRatePerMinute\",\n\t\t\"processingLagTotalMs\",\n\t\t\"processingLagMaxMs\",\n\t]);\n\tif (Object.keys(body).some((key) => !allowed.has(key))) return false;\n\tconst states = new Set([\"connecting\", \"connected\", \"retrying\", \"resyncing\", \"disconnected\", \"auth_failed\"]);\n\tconst nonNegativeNumber = (item: unknown) => typeof item === \"number\" && Number.isFinite(item) && item >= 0;\n\tconst nonNegativeInteger = (item: unknown) => typeof item === \"number\" && Number.isSafeInteger(item) && item >= 0;\n\treturn (\n\t\ttypeof body.connectionId === \"string\" &&\n\t\t/^[0-9a-f-]{36}$/i.test(body.connectionId) &&\n\t\ttypeof body.state === \"string\" &&\n\t\tstates.has(body.state) &&\n\t\t(body.previousState === undefined ||\n\t\t\t(typeof body.previousState === \"string\" && states.has(body.previousState))) &&\n\t\tnonNegativeInteger(body.attempt) &&\n\t\tnonNegativeNumber(body.eventCount) &&\n\t\tnonNegativeNumber(body.eventRatePerMinute) &&\n\t\tnonNegativeNumber(body.processingLagTotalMs) &&\n\t\tnonNegativeNumber(body.processingLagMaxMs) &&\n\t\t(body.delayMs === undefined || nonNegativeNumber(body.delayMs)) &&\n\t\t(body.lastAppliedSeq === undefined || nonNegativeInteger(body.lastAppliedSeq)) &&\n\t\t(body.heartbeatAgeMs === undefined || nonNegativeNumber(body.heartbeatAgeMs)) &&\n\t\t(body.visibility === \"visible\" || body.visibility === \"hidden\")\n\t);\n}\n\n/** Parse the device cookie from a Cookie header. */\nexport function parseDeviceCookie(cookieHeader: string | undefined): string | undefined {\n\tif (!cookieHeader) return undefined;\n\tfor (const part of cookieHeader.split(\";\")) {\n\t\tconst eq = part.indexOf(\"=\");\n\t\tif (eq === -1) continue;\n\t\tif (part.slice(0, eq).trim() === DEVICE_COOKIE) return part.slice(eq + 1).trim();\n\t}\n\treturn undefined;\n}\n\ninterface AuthedRequest extends Request {\n\tauthDecision?: AuthDecision;\n\t/** Per-SSE-request opaque diagnostic correlation id. */\n\tsseConnectionId?: string;\n}\n\nexport function createDashboardServer(options: DashboardServerOptions): DashboardServerApp {\n\tconst { auth, pool } = options;\n\tconst serverStartedAt = new Date().toISOString();\n\tconst diagnosticConnections = new Map<string, { issuedAt: number; lastAt?: number }>();\n\tconst log = options.logger ?? ((line: string) => console.log(`[dashboard] ${line}`));\n\tconst files = new FileApi((op, path, detail) => log(`file ${op}: ${path}${detail ? ` (${detail})` : \"\"}`));\n\tconst memories = new MemoryApi(options.memoryHomeDir ?? homedir(), (op, scopeId, detail) =>\n\t\tlog(`memory ${op}: ${scopeId}${detail ? ` (${detail})` : \"\"}`),\n\t);\n\tconst hub = options.eventHub ?? new EventHub();\n\tconst images = options.imageService ?? new DashboardImageService(new ImagePreviewWorker());\n\thub.setEventProjector((key, event) => (key ? images.projectEvent(event, { runtimeKey: key }) : event));\n\tpool.onEvent((key, event) => {\n\t\tif (event.type === \"dashboard_snapshot_barrier\" && typeof event.snapshotId === \"string\") {\n\t\t\t// This RPC marker has no browser frame: its synchronous sequence capture\n\t\t\t// orders the HTTP snapshot before all later EventHub publications.\n\t\t\tpool.recordDashboardBarrier(key, event.snapshotId, hub.currentSequence);\n\t\t\treturn;\n\t\t}\n\t\thub.publish(key, event);\n\t\tif (event.type === \"runtime_removed\") images.removeRuntime(key);\n\t});\n\tpool.onFleetSnapshot((event) => hub.publish(\"\", { ...event }));\n\n\tconst app = express() as DashboardServerApp;\n\tlet closePromise: Promise<void> | undefined;\n\tapp.closeDashboard = () => {\n\t\tclosePromise ??= images.close();\n\t\treturn closePromise;\n\t};\n\tapp.disable(\"x-powered-by\");\n\n\t// -- auth middleware (every route, fail-closed) ---------------------------\n\tapp.use((req: AuthedRequest, res: Response, next: NextFunction) => {\n\t\tif (req.path === \"/api/events\") req.sseConnectionId = randomUUID();\n\t\tauth\n\t\t\t.authenticate({\n\t\t\t\tremoteAddress: req.socket.remoteAddress,\n\t\t\t\thostHeader: req.headers.host,\n\t\t\t\toriginHeader: req.headers.origin,\n\t\t\t\tdeviceToken: parseDeviceCookie(req.headers.cookie),\n\t\t\t})\n\t\t\t.then((decision) => {\n\t\t\t\treq.authDecision = decision;\n\t\t\t\tif (decision.allowed) return next();\n\t\t\t\tconst canRenderAuthScreen = decision.needsPairing || Boolean(decision.identity);\n\t\t\t\tif (canRenderAuthScreen) {\n\t\t\t\t\t// The auth/pairing endpoints must be reachable by allowed-but-unpaired\n\t\t\t\t\t// identities, and /api/auth must also be reachable by rejected\n\t\t\t\t\t// Tailscale identities so the SPA denial screen can name them.\n\t\t\t\t\tif (req.path === \"/api/auth\" || (decision.needsPairing && req.path === \"/api/pair\")) return next();\n\t\t\t\t\t// Let the SPA shell + static assets load so the client-side pairing or\n\t\t\t\t\t// denial screen can render. No data exposure: every /api/* data route\n\t\t\t\t\t// below stays fail-closed — only non-API GETs (the app shell) are allowed.\n\t\t\t\t\tif (req.method === \"GET\" && !req.path.startsWith(\"/api/\")) return next();\n\t\t\t\t}\n\t\t\t\tif (req.sseConnectionId) {\n\t\t\t\t\tlog(\n\t\t\t\t\t\t`sse ${JSON.stringify({\n\t\t\t\t\t\t\tconnectionId: req.sseConnectionId,\n\t\t\t\t\t\t\tkind: \"auth_denial\",\n\t\t\t\t\t\t\tmethod: req.method,\n\t\t\t\t\t\t\tpath: req.path,\n\t\t\t\t\t\t\tstatus: decision.status,\n\t\t\t\t\t\t})}`,\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\tlog(`denied ${req.method} ${req.path}: ${decision.reason}`);\n\t\t\t\t}\n\t\t\t\tres.status(decision.status).json({\n\t\t\t\t\terror: decision.reason,\n\t\t\t\t\tneedsPairing: decision.needsPairing ?? false,\n\t\t\t\t\tidentity: decision.identity?.loginName,\n\t\t\t\t});\n\t\t\t})\n\t\t\t.catch((err) => {\n\t\t\t\t// authenticate() already catches internally; this is belt-and-suspenders.\n\t\t\t\tlog(`auth middleware error — denying: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t\tres.status(500).json({ error: \"Auth subsystem error — denied\" });\n\t\t\t});\n\t});\n\n\t// Authenticate before consuming request bodies. Diagnostics have their own\n\t// small parser limit; the larger limit exists only for prompt image payloads.\n\tapp.use(\"/api/events/diagnostic\", express.json({ limit: MAX_CLIENT_DIAGNOSTIC_BYTES }));\n\tconst jsonBodyParser = express.json({ limit: MAX_PROMPT_BODY_BYTES });\n\tapp.use((req: Request, res: Response, next: NextFunction) => {\n\t\t// /api/files/upload pipes the raw request stream into the destination\n\t\t// file, so the parser must not run first — it would drain the stream\n\t\t// and the upload would commit 0 bytes. Express 5 matching is\n\t\t// case-insensitive and non-strict, so the route also accepts\n\t\t// case-variant and trailing-slash URLs; the skip must cover all of them.\n\t\tconst uploadPath = req.path.toLowerCase();\n\t\tif (uploadPath === \"/api/files/upload\" || uploadPath === \"/api/files/upload/\") return next();\n\t\tjsonBodyParser(req, res, next);\n\t});\n\tapp.use((err: unknown, _req: Request, res: Response, next: NextFunction) => {\n\t\tif ((err as { type?: string }).type === \"entity.too.large\") {\n\t\t\tres.status(413).json({ error: \"Request body is too large\" });\n\t\t\treturn;\n\t\t}\n\t\tnext(err);\n\t});\n\n\t// -- auth/pairing ----------------------------------------------------------\n\tapp.get(\"/api/auth\", (req: AuthedRequest, res) => {\n\t\tconst decision = req.authDecision!;\n\t\tif (decision.allowed && decision.mode === \"local\") {\n\t\t\tconst status: AuthStatusDto = { mode: \"local\" };\n\t\t\tres.json({ ...status, needsPairing: false });\n\t\t\treturn;\n\t\t}\n\t\tif (decision.allowed) {\n\t\t\tauth\n\t\t\t\t.claimPairingExpiryStatus(decision.pairing.id)\n\t\t\t\t.then((expiry) => {\n\t\t\t\t\tconst status: AuthStatusDto = {\n\t\t\t\t\t\tmode: \"remote\",\n\t\t\t\t\t\tidentity: decision.identity.loginName,\n\t\t\t\t\t\tdevice: decision.identity.device,\n\t\t\t\t\t\t...(expiry.warning ? { pairingExpiryWarning: expiry.warning } : {}),\n\t\t\t\t\t\t...(expiry.nextCheckAt ? { pairingExpiryCheckAt: expiry.nextCheckAt } : {}),\n\t\t\t\t\t};\n\t\t\t\t\tres.json({ ...status, needsPairing: false });\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tlog(`pairing expiry status failed: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t\t\tres.status(500).json({ error: \"Auth subsystem error — denied\", needsPairing: false });\n\t\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tres.status(decision.status).json({\n\t\t\terror: decision.reason,\n\t\t\tneedsPairing: decision.needsPairing ?? false,\n\t\t\tidentity: decision.identity?.loginName,\n\t\t});\n\t});\n\n\tapp.get(\"/api/pairing-code\", (req: AuthedRequest, res) => {\n\t\tconst decision = req.authDecision!;\n\t\tif (!decision.allowed || decision.mode !== \"local\") {\n\t\t\tres.status(403).json({ error: \"Pairing code is only available from the host machine\" });\n\t\t\treturn;\n\t\t}\n\t\tif (!auth.isRemoteEnabled) {\n\t\t\tconst body: PairingCodeDto = { enabled: false };\n\t\t\tres.json(body);\n\t\t\treturn;\n\t\t}\n\t\tconst body: PairingCodeDto = { enabled: true, ...auth.currentPairingCode() };\n\t\tres.json(body);\n\t});\n\n\tapp.post(\"/api/pair\", (req: AuthedRequest, res) => {\n\t\tconst pin = typeof req.body?.pin === \"string\" ? req.body.pin : \"\";\n\t\tauth\n\t\t\t.pair(\n\t\t\t\t{\n\t\t\t\t\tremoteAddress: req.socket.remoteAddress,\n\t\t\t\t\thostHeader: req.headers.host,\n\t\t\t\t\toriginHeader: req.headers.origin,\n\t\t\t\t\tdeviceToken: undefined,\n\t\t\t\t},\n\t\t\t\tpin,\n\t\t\t)\n\t\t\t.then(({ token, device }) => {\n\t\t\t\tlog(`paired device ${device.id} (${device.identity})`);\n\t\t\t\tres.cookie(DEVICE_COOKIE, token, {\n\t\t\t\t\thttpOnly: true,\n\t\t\t\t\tsameSite: \"strict\",\n\t\t\t\t\tsecure: false, // Tailscale already encrypts; the dashboard serves plain HTTP on the tailnet.\n\t\t\t\t\texpires: new Date(device.expiresAt),\n\t\t\t\t}).json({ device });\n\t\t\t})\n\t\t\t.catch((err) => {\n\t\t\t\tconst status = typeof err?.status === \"number\" ? err.status : 500;\n\t\t\t\tlog(`pairing failed: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t\tres.status(status).json({ error: err instanceof Error ? err.message : String(err) });\n\t\t\t});\n\t});\n\n\tapp.get(\"/api/pairing-settings\", (_req, res) => {\n\t\tauth\n\t\t\t.getPairingSettings()\n\t\t\t.then((settings) => res.json(settings))\n\t\t\t.catch((err) => res.status(500).json({ error: String(err?.message ?? err) }));\n\t});\n\n\tapp.put(\"/api/pairing-settings\", (req, res) => {\n\t\tconst pairingTtlDays = req.body?.pairingTtlDays;\n\t\tif (typeof pairingTtlDays !== \"number\") {\n\t\t\tres.status(400).json({ error: \"pairingTtlDays must be a number\" });\n\t\t\treturn;\n\t\t}\n\t\tauth\n\t\t\t.setPairingSettings(pairingTtlDays)\n\t\t\t.then((settings) => res.json(settings))\n\t\t\t.catch((err) => {\n\t\t\t\tconst status = typeof err?.status === \"number\" ? err.status : 500;\n\t\t\t\tres.status(status).json({ error: String(err?.message ?? err) });\n\t\t\t});\n\t});\n\n\tapp.get(\"/api/devices\", (_req, res) => {\n\t\tauth\n\t\t\t.listDevices()\n\t\t\t.then((devices) => res.json({ devices }))\n\t\t\t.catch((err) => res.status(500).json({ error: String(err?.message ?? err) }));\n\t});\n\n\tapp.delete(\"/api/devices/:id\", (req, res) => {\n\t\tauth\n\t\t\t.unpair(req.params.id)\n\t\t\t.then((removed) => {\n\t\t\t\tif (!removed) {\n\t\t\t\t\tres.status(404).json({ error: `No paired device with id ${String(req.params.id)}` });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tlog(`unpaired device ${String(req.params.id)}`);\n\t\t\t\tres.json({ ok: true });\n\t\t\t})\n\t\t\t.catch((err) => res.status(500).json({ error: String(err?.message ?? err) }));\n\t});\n\n\t// -- events (SSE) ----------------------------------------------------------\n\tapp.get(\"/api/events\", (req: AuthedRequest, res) => {\n\t\tconst connectionId = req.sseConnectionId ?? randomUUID();\n\t\tconst diagnostic = (kind: string, metadata: object = {}) =>\n\t\t\tlog(`sse ${JSON.stringify({ connectionId, kind, ...metadata })}`);\n\t\tres.writeHead(200, {\n\t\t\t\"content-type\": \"text/event-stream\",\n\t\t\t\"cache-control\": \"no-cache\",\n\t\t\tconnection: \"keep-alive\",\n\t\t});\n\n\t\tconst guardedWrite = (\n\t\t\tchunk: string,\n\t\t\tmetadata: SseWriteMetadata | { kind: \"handshake\" | \"heartbeat\" | \"connection\" },\n\t\t): boolean => {\n\t\t\tif (res.destroyed || res.writableEnded) {\n\t\t\t\tdiagnostic(\"write_closed\", { writeKind: metadata.kind });\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst accepted = res.write(chunk);\n\t\t\tconst details = {\n\t\t\t\twriteKind: metadata.kind,\n\t\t\t\t...(\"seq\" in metadata\n\t\t\t\t\t? { seq: metadata.seq, type: metadata.type, frameBytes: metadata.frameBytes, reason: metadata.reason }\n\t\t\t\t\t: {}),\n\t\t\t\twritableLength: res.writableLength,\n\t\t\t};\n\t\t\tdiagnostic(\"write\", details);\n\t\t\tif (!accepted && res.writableLength > MAX_SSE_BUFFERED_BYTES) {\n\t\t\t\tdiagnostic(\"backpressure\", details);\n\t\t\t\tres.destroy();\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn true;\n\t\t};\n\n\t\tconst lastIdRaw = req.headers[\"last-event-id\"] ?? req.query.lastEventId;\n\t\tconst lastEventId =\n\t\t\ttypeof lastIdRaw === \"string\" && /^\\d+$/.test(lastIdRaw) ? Number.parseInt(lastIdRaw, 10) : undefined;\n\t\tdiagnostic(\"connect\", { cursor: lastEventId });\n\t\tif (!guardedWrite(\":ok\\n\\n\", { kind: \"handshake\" })) return;\n\t\t// Unnumbered connection metadata lets a browser correlate optional,\n\t\t// payload-free diagnostics without mutating its application SSE cursor.\n\t\tconst issuedAt = Date.now();\n\t\tfor (const [id, record] of diagnosticConnections) {\n\t\t\tif (issuedAt - record.issuedAt > CLIENT_DIAGNOSTIC_CONNECTION_TTL_MS) diagnosticConnections.delete(id);\n\t\t}\n\t\tdiagnosticConnections.set(connectionId, { issuedAt });\n\t\tif (!guardedWrite(`event: connection\\ndata: ${JSON.stringify({ connectionId })}\\n\\n`, { kind: \"connection\" }))\n\t\t\treturn;\n\t\tlet detach = () => {};\n\t\tlet keepAlive: ReturnType<typeof setInterval> | undefined;\n\t\tconst stop = () => {\n\t\t\tif (keepAlive) clearInterval(keepAlive);\n\t\t\tdetach();\n\t\t};\n\t\tlet usable = true;\n\t\tdetach = hub.attach(\n\t\t\t{\n\t\t\t\twrite: (chunk, metadata) => {\n\t\t\t\t\tif (!metadata) return false;\n\t\t\t\t\tusable = guardedWrite(chunk, metadata);\n\t\t\t\t\treturn usable;\n\t\t\t\t},\n\t\t\t},\n\t\t\tlastEventId,\n\t\t\t(replay) => diagnostic(replay.kind, replay),\n\t\t);\n\t\t// A rejected/destroyed replay must not leave a timer or live client behind.\n\t\tif (!usable) return;\n\t\t// Named heartbeats are visible to EventSource but have no id, so they do\n\t\t// not alter the application cursor or consume replay history.\n\t\tkeepAlive = setInterval(() => {\n\t\t\tif (!guardedWrite(formatHeartbeatFrame(), { kind: \"heartbeat\" })) stop();\n\t\t}, options.heartbeatIntervalMs ?? 25_000);\n\t\treq.on(\"close\", () => {\n\t\t\tdiagnostic(\"close\", { writableLength: res.writableLength });\n\t\t\tstop();\n\t\t});\n\t});\n\n\t// -- optional client stream diagnostics -----------------------------------\n\tapp.post(\"/api/events/diagnostic\", (req, res) => {\n\t\tconst declaredLength = Number(req.headers[\"content-length\"] ?? 0);\n\t\tconst encodedBytes = Buffer.byteLength(JSON.stringify(req.body ?? null));\n\t\tif (declaredLength > MAX_CLIENT_DIAGNOSTIC_BYTES || encodedBytes > MAX_CLIENT_DIAGNOSTIC_BYTES) {\n\t\t\tres.status(413).json({ error: \"Diagnostic summary exceeds the 4 KiB limit\" });\n\t\t\treturn;\n\t\t}\n\t\tif (!isClientDiagnostic(req.body)) {\n\t\t\tres.status(400).json({ error: \"Invalid diagnostic summary\" });\n\t\t\treturn;\n\t\t}\n\t\tconst now = Date.now();\n\t\tfor (const [id, record] of diagnosticConnections) {\n\t\t\tif (now - record.issuedAt > CLIENT_DIAGNOSTIC_CONNECTION_TTL_MS) diagnosticConnections.delete(id);\n\t\t}\n\t\tconst record = diagnosticConnections.get(req.body.connectionId);\n\t\tif (!record) {\n\t\t\tres.status(400).json({ error: \"Unknown or expired SSE connection\" });\n\t\t\treturn;\n\t\t}\n\t\tif (record.lastAt !== undefined && now - record.lastAt < CLIENT_DIAGNOSTIC_RATE_LIMIT_MS) {\n\t\t\tres.status(429).json({ error: \"Diagnostic summary rate limited\" });\n\t\t\treturn;\n\t\t}\n\t\trecord.lastAt = now;\n\t\t// Never log the request body wholesale. The schema is intentionally only\n\t\t// connection metadata, and this explicit projection prevents future fields\n\t\t// from accidentally turning diagnostics into a payload side-channel.\n\t\tlog(\n\t\t\t`sse ${JSON.stringify({\n\t\t\t\tconnectionId: req.body.connectionId,\n\t\t\t\tkind: \"client_diagnostic\",\n\t\t\t\tstate: req.body.state,\n\t\t\t\tpreviousState: req.body.previousState,\n\t\t\t\tattempt: req.body.attempt,\n\t\t\t\tdelayMs: req.body.delayMs,\n\t\t\t\tvisibility: req.body.visibility,\n\t\t\t\tlastAppliedSeq: req.body.lastAppliedSeq,\n\t\t\t\theartbeatAgeMs: req.body.heartbeatAgeMs,\n\t\t\t\teventCount: req.body.eventCount,\n\t\t\t\teventRatePerMinute: req.body.eventRatePerMinute,\n\t\t\t\tprocessingLagTotalMs: req.body.processingLagTotalMs,\n\t\t\t\tprocessingLagMaxMs: req.body.processingLagMaxMs,\n\t\t\t})}`,\n\t\t);\n\t\tres.json({ ok: true });\n\t});\n\n\t// -- fleet -----------------------------------------------------------------\n\tconst listDiskSessions = async (): Promise<SessionInfoDto[]> =>\n\t\tPromise.all((await options.listAllSessions()).map(toSessionInfoDto));\n\n\tconst currentCwdInventory = async (): Promise<string[]> => [\n\t\t...pool.list().map((handle) => handle.cwd),\n\t\t...(await listDiskSessions()).flatMap((session) => (session.resolvedCwd ? [session.resolvedCwd] : [])),\n\t];\n\n\tconst handleMemoryError = (res: Response, err: unknown): void => {\n\t\tconst status =\n\t\t\ttypeof (err as { status?: unknown })?.status === \"number\" ? (err as { status: number }).status : 500;\n\t\tres.status(status).json({ error: err instanceof Error ? err.message : String(err) });\n\t};\n\n\tapp.get(\"/api/memories/scopes\", (_req, res) => {\n\t\tcurrentCwdInventory()\n\t\t\t.then((inventory) => memories.scopes(inventory))\n\t\t\t.then((scopes) => res.json({ scopes }))\n\t\t\t.catch((err) => handleMemoryError(res, err));\n\t});\n\n\tapp.get(\"/api/memories/:scopeId\", (req, res) => {\n\t\tcurrentCwdInventory()\n\t\t\t.then((inventory) => memories.listing(req.params.scopeId, inventory))\n\t\t\t.then((listing) => res.json(listing))\n\t\t\t.catch((err) => handleMemoryError(res, err));\n\t});\n\n\tapp.get(\"/api/memories/:scopeId/documents/:file\", (req, res) => {\n\t\tcurrentCwdInventory()\n\t\t\t.then((inventory) => memories.readDocument(req.params.scopeId, req.params.file, inventory))\n\t\t\t.then((document) => res.json(document))\n\t\t\t.catch((err) => handleMemoryError(res, err));\n\t});\n\n\tapp.put(\"/api/memories/:scopeId/documents/:file\", (req, res) => {\n\t\tcurrentCwdInventory()\n\t\t\t.then((inventory) => memories.saveDocument(req.params.scopeId, req.params.file, req.body, inventory))\n\t\t\t.then((result) => res.json(result))\n\t\t\t.catch((err) => handleMemoryError(res, err));\n\t});\n\n\tapp.delete(\"/api/memories/:scopeId/entries/:file\", (req, res) => {\n\t\tcurrentCwdInventory()\n\t\t\t.then((inventory) => memories.deleteEntry(req.params.scopeId, req.params.file, req.body, inventory))\n\t\t\t.then((result) => res.json(result))\n\t\t\t.catch((err) => handleMemoryError(res, err));\n\t});\n\n\tconst getFleet = async (): Promise<FleetDto> => {\n\t\tconst runtimes = await Promise.all(pool.list().map((h) => pool.describe(h)));\n\t\treturn { runtimes, diskSessions: await listDiskSessions() };\n\t};\n\n\t/** Map the one-RPC parent snapshot consistently for recovery and drill-in hydration. */\n\tconst toRuntimeHydration = (snapshot: DashboardRuntimeSnapshot): RuntimeHydrationDto => ({\n\t\tkey: snapshot.key,\n\t\tstate: snapshot.snapshot.state,\n\t\tmessages: images.project(snapshot.snapshot.messages, { runtimeKey: snapshot.key }),\n\t\tbackgroundAgents: snapshot.snapshot.backgroundAgents,\n\t\tpendingExtensionUiRequests: snapshot.snapshot.pendingExtensionUiRequests ?? [],\n\t\tbarrierSeq: snapshot.barrierSeq,\n\t});\n\n\tapp.get(\"/api/fleet\", (_req, res) => {\n\t\tconst startedAt = Date.now();\n\t\tgetFleet()\n\t\t\t.then((fleet) => {\n\t\t\t\t// Serialize once so the diagnostic reports the exact JSON response size\n\t\t\t\t// without retaining or logging any fleet payload fields.\n\t\t\t\tconst body = JSON.stringify(fleet);\n\t\t\t\tconst diagnostic = {\n\t\t\t\t\telapsedMs: Date.now() - startedAt,\n\t\t\t\t\tencodedBytes: Buffer.byteLength(body),\n\t\t\t\t\truntimeCount: fleet.runtimes.length,\n\t\t\t\t\tdiskSessionCount: fleet.diskSessions.length,\n\t\t\t\t};\n\t\t\t\tres.type(\"json\").send(body);\n\t\t\t\tlog(`fleet ${JSON.stringify(diagnostic)}`);\n\t\t\t})\n\t\t\t.catch((err) => res.status(500).json({ error: String(err?.message ?? err) }));\n\t});\n\n\t/** On-disk inventory only; does not query or describe live runtimes. */\n\tapp.get(\"/api/sessions\", (_req, res) => {\n\t\tlistDiskSessions()\n\t\t\t.then((sessions) => {\n\t\t\t\tconst body: SessionInventoryDto = { sessions };\n\t\t\t\tres.json(body);\n\t\t\t})\n\t\t\t.catch((err) => res.status(500).json({ error: String(err?.message ?? err) }));\n\t});\n\n\t/**\n\t * Full recovery snapshot. For an active runtime, its RPC marker captures the\n\t * current EventHub sequence before the response; later publications have a\n\t * higher sequence. This is an ordering contract, not a timing heuristic.\n\t */\n\tapp.get(\"/api/resync\", (req, res) => {\n\t\t(async () => {\n\t\t\tconst activeKey = typeof req.query.key === \"string\" ? req.query.key : undefined;\n\t\t\tconst activeAgentId = typeof req.query.agentId === \"string\" ? req.query.agentId : undefined;\n\t\t\tlet active: DashboardResyncDto[\"active\"];\n\t\t\tlet barrierSeq: number;\n\t\t\tif (activeKey) {\n\t\t\t\tconst handle = pool.get(activeKey);\n\t\t\t\tif (!handle) {\n\t\t\t\t\tconst body: DashboardResyncDto = { fleet: await getFleet(), barrierSeq: hub.currentSequence };\n\t\t\t\t\tres.json(body);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// The disk transcript has its own sequence boundary because it is read\n\t\t\t\t// before the parent RPC snapshot. Relays between these two barriers must\n\t\t\t\t// be reapplied so a subagent delta cannot disappear during recovery.\n\t\t\t\tlet preBarrierSubagent: NonNullable<ActiveRuntimeSnapshotDto[\"subagent\"]> | undefined;\n\t\t\t\tif (activeAgentId) {\n\t\t\t\t\tconst agents = await handle.client.listBackgroundAgents();\n\t\t\t\t\tconst agent = agents.find((candidate) => candidate.agentId === activeAgentId);\n\t\t\t\t\tif (!agent) throw new Error(`No background agent ${activeAgentId} in this runtime`);\n\t\t\t\t\tconst messages = readSubagentMessages(agent);\n\t\t\t\t\tpreBarrierSubagent = {\n\t\t\t\t\t\tagentId: activeAgentId,\n\t\t\t\t\t\tagent,\n\t\t\t\t\t\tmessages: images.project(messages, { runtimeKey: activeKey, agentId: activeAgentId }),\n\t\t\t\t\t\tbarrierSeq: hub.currentSequence,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tconst snapshot = await pool.snapshotDashboard(handle);\n\t\t\t\tbarrierSeq = snapshot.barrierSeq;\n\t\t\t\tactive = {\n\t\t\t\t\t...toRuntimeHydration(snapshot),\n\t\t\t\t\t...(preBarrierSubagent ? { subagent: preBarrierSubagent } : {}),\n\t\t\t\t};\n\t\t\t} else {\n\t\t\t\tbarrierSeq = hub.currentSequence;\n\t\t\t}\n\t\t\tconst body: DashboardResyncDto = { fleet: await getFleet(), ...(active ? { active } : {}), barrierSeq };\n\t\t\tres.json(body);\n\t\t})().catch((err) => res.status(502).json({ error: String(err?.message ?? err) }));\n\t});\n\n\t// -- runtimes ---------------------------------------------------------------\n\tapp.post(\"/api/runtimes\", (req, res) => {\n\t\t(async () => {\n\t\t\tconst requestedCwd = typeof req.body?.cwd === \"string\" ? req.body.cwd : \"\";\n\t\t\tconst cwd = await resolveExistingDirectory(requestedCwd);\n\t\t\tconst sessionPath = typeof req.body?.sessionPath === \"string\" ? req.body.sessionPath : undefined;\n\t\t\tconst handle = await pool.create(cwd, sessionPath);\n\t\t\tlog(`runtime ${handle.key} started in ${cwd}${sessionPath ? ` (resume ${basename(sessionPath)})` : \"\"}`);\n\t\t\tconst firstPrompt = typeof req.body?.firstPrompt === \"string\" ? req.body.firstPrompt : undefined;\n\t\t\tif (firstPrompt) await handle.client.prompt(firstPrompt);\n\t\t\tres.status(201).json(await pool.describe(handle));\n\t\t})().catch((err) => handleMemoryError(res, err));\n\t});\n\n\tapp.delete(\"/api/runtimes/:key\", (req, res) => {\n\t\tpool\n\t\t\t.stop(req.params.key)\n\t\t\t.then((stopped) => {\n\t\t\t\tif (!stopped) {\n\t\t\t\t\tres.status(404).json({ error: `No runtime ${String(req.params.key)}` });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\timages.removeRuntime(String(req.params.key));\n\t\t\t\tlog(`runtime ${String(req.params.key)} stopped`);\n\t\t\t\tres.json({ ok: true });\n\t\t\t})\n\t\t\t.catch((err) => res.status(500).json({ error: String(err?.message ?? err) }));\n\t});\n\n\t/** Helper: run an async op against a pooled runtime with uniform errors. */\n\tfunction withRuntime(\n\t\treq: Request,\n\t\tres: Response,\n\t\tfn: (handle: NonNullable<ReturnType<RuntimePool[\"get\"]>>) => Promise<unknown>,\n\t): void {\n\t\tconst handle = pool.get(String(req.params.key));\n\t\tif (!handle) {\n\t\t\tres.status(404).json({ error: `No runtime ${String(req.params.key)}` });\n\t\t\treturn;\n\t\t}\n\t\tfn(handle)\n\t\t\t.then((data) => res.json(data ?? { ok: true }))\n\t\t\t.catch((err) => {\n\t\t\t\tres.status(502).json({ error: String(err?.message ?? err) });\n\t\t\t});\n\t}\n\n\tapp.get(\"/api/runtimes/:key\", (req, res) => {\n\t\twithRuntime(req, res, (h) => pool.describe(h));\n\t});\n\n\t/**\n\t * Atomic drill-in snapshot. snapshotDashboard performs exactly one RPC and\n\t * consumes its marker barrier, so no independently-read runtime fields can\n\t * describe different moments in a live turn.\n\t */\n\tapp.get(\"/api/runtimes/:key/hydrate\", (req, res) => {\n\t\twithRuntime(req, res, async (h) => toRuntimeHydration(await pool.snapshotDashboard(h)));\n\t});\n\n\tapp.get(\"/api/runtimes/:key/messages\", (req, res) => {\n\t\twithRuntime(req, res, async (h) => ({\n\t\t\tmessages: images.project(await h.client.getMessages(), { runtimeKey: h.key }),\n\t\t}));\n\t});\n\n\tconst sendImage = async (\n\t\treq: Request,\n\t\tres: Response,\n\t\tscope: DashboardImageScope,\n\t\tvariant: \"preview\" | \"original\",\n\t\tloadAuthoritative: () => Promise<unknown>,\n\t): Promise<void> => {\n\t\tconst id = String(req.params.id);\n\t\tif (!DASHBOARD_IMAGE_ID_PATTERN.test(id)) {\n\t\t\tres.status(400).json({ error: \"Invalid dashboard image ID\" });\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tconst image =\n\t\t\t\tvariant === \"preview\"\n\t\t\t\t\t? await images.preview(scope, id, loadAuthoritative)\n\t\t\t\t\t: await images.original(scope, id, loadAuthoritative);\n\t\t\tres.set({\n\t\t\t\t\"Content-Type\": image.mimeType,\n\t\t\t\t\"Content-Length\": String(image.bytes.byteLength),\n\t\t\t\t\"X-Content-Type-Options\": \"nosniff\",\n\t\t\t\t\"Cache-Control\": \"private, max-age=31536000, immutable\",\n\t\t\t});\n\t\t\tres.send(Buffer.from(image.bytes));\n\t\t} catch (error) {\n\t\t\tif (error instanceof DashboardImageNotFoundError) {\n\t\t\t\tres.status(404).json({ error: error.message });\n\t\t\t} else if (error instanceof DashboardImagePreviewError) {\n\t\t\t\tres.status(422).json({ error: error.message });\n\t\t\t} else {\n\t\t\t\tres.status(502).json({\n\t\t\t\t\terror: `Image source unavailable: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t};\n\n\tapp.get(\"/api/runtimes/:key/images/:id/:variant\", (req, res) => {\n\t\tconst key = String(req.params.key);\n\t\tconst variant = String(req.params.variant);\n\t\tif (variant !== \"preview\" && variant !== \"original\") {\n\t\t\tres.status(404).json({ error: \"Unknown dashboard image variant\" });\n\t\t\treturn;\n\t\t}\n\t\tconst handle = pool.get(key);\n\t\tif (!handle) {\n\t\t\tres.status(404).json({ error: `No runtime ${key}` });\n\t\t\treturn;\n\t\t}\n\t\tvoid sendImage(req, res, { runtimeKey: key }, variant, () => handle.client.getMessages());\n\t});\n\n\tapp.get(\"/api/runtimes/:key/subagents/:agentId/images/:id/:variant\", (req, res) => {\n\t\tconst key = String(req.params.key);\n\t\tconst agentId = String(req.params.agentId);\n\t\tconst variant = String(req.params.variant);\n\t\tif (variant !== \"preview\" && variant !== \"original\") {\n\t\t\tres.status(404).json({ error: \"Unknown dashboard image variant\" });\n\t\t\treturn;\n\t\t}\n\t\tconst handle = pool.get(key);\n\t\tif (!handle) {\n\t\t\tres.status(404).json({ error: `No runtime ${key}` });\n\t\t\treturn;\n\t\t}\n\t\tvoid sendImage(req, res, { runtimeKey: key, agentId }, variant, async () => {\n\t\t\tconst agents = await handle.client.listBackgroundAgents();\n\t\t\tconst agent = agents.find((candidate) => candidate.agentId === agentId);\n\t\t\tif (!agent) throw new DashboardImageNotFoundError(`No background agent ${agentId} in this runtime`);\n\t\t\treturn readSubagentMessages(agent);\n\t\t});\n\t});\n\n\tapp.get(\"/api/runtimes/:key/pending\", (req, res) => {\n\t\twithRuntime(req, res, (h) => h.client.getPendingMessages());\n\t});\n\n\tapp.get(\"/api/runtimes/:key/subagents/:agentId/pending\", (req, res) => {\n\t\twithRuntime(req, res, (h) => h.client.getBackgroundAgentPending(String(req.params.agentId)));\n\t});\n\n\tapp.post(\"/api/runtimes/:key/subagents/:agentId/steer\", (req, res) => {\n\t\tconst { message } = req.body ?? {};\n\t\tif (typeof message !== \"string\" || message.length === 0) {\n\t\t\tres.status(400).json({ error: \"message is required\" });\n\t\t\treturn;\n\t\t}\n\t\twithRuntime(req, res, (h) => h.client.steerBackgroundAgent(String(req.params.agentId), message));\n\t});\n\n\tapp.post(\"/api/runtimes/:key/dequeue\", (req, res) => {\n\t\twithRuntime(req, res, (h) => h.client.clearPendingMessages());\n\t});\n\n\tfunction parseImages(body: unknown): ImageAttachmentDto[] | undefined | \"invalid\" {\n\t\tconst images = (body as { images?: unknown } | undefined)?.images;\n\t\tif (images === undefined) return undefined;\n\t\tif (!Array.isArray(images)) return \"invalid\";\n\t\tconst parsed: ImageAttachmentDto[] = [];\n\t\tfor (const image of images) {\n\t\t\tif (\n\t\t\t\t!image ||\n\t\t\t\ttypeof image !== \"object\" ||\n\t\t\t\ttypeof (image as { data?: unknown }).data !== \"string\" ||\n\t\t\t\ttypeof (image as { mimeType?: unknown }).mimeType !== \"string\"\n\t\t\t) {\n\t\t\t\treturn \"invalid\";\n\t\t\t}\n\t\t\tparsed.push({ data: (image as ImageAttachmentDto).data, mimeType: (image as ImageAttachmentDto).mimeType });\n\t\t}\n\t\treturn parsed;\n\t}\n\n\tapp.post(\"/api/runtimes/:key/prompt\", (req, res) => {\n\t\tconst { message, mode } = req.body ?? {};\n\t\tif (typeof message !== \"string\" || message.length === 0) {\n\t\t\tres.status(400).json({ error: \"message is required\" });\n\t\t\treturn;\n\t\t}\n\t\tconst images = parseImages(req.body);\n\t\tif (images === \"invalid\") {\n\t\t\tres.status(400).json({ error: \"images must be an array of {data, mimeType} objects\" });\n\t\t\treturn;\n\t\t}\n\t\tconst rpcImages = images?.map((image) => ({\n\t\t\ttype: \"image\" as const,\n\t\t\tdata: image.data,\n\t\t\tmimeType: image.mimeType,\n\t\t}));\n\t\twithRuntime(req, res, async (h) => {\n\t\t\tif (mode === \"steer\") await h.client.steer(message, rpcImages);\n\t\t\telse if (mode === \"follow_up\") await h.client.followUp(message, rpcImages);\n\t\t\telse await h.client.prompt(message, rpcImages);\n\t\t});\n\t});\n\n\tapp.post(\"/api/runtimes/:key/abort\", (req, res) => {\n\t\twithRuntime(req, res, (h) => h.client.abort());\n\t});\n\n\tapp.post(\"/api/runtimes/:key/abort-compaction\", (req, res) => {\n\t\twithRuntime(req, res, (h) => h.client.abortCompaction());\n\t});\n\n\tapp.post(\"/api/runtimes/:key/abort-retry\", (req, res) => {\n\t\twithRuntime(req, res, (h) => h.client.abortRetry());\n\t});\n\n\tapp.post(\"/api/runtimes/:key/model\", (req, res) => {\n\t\tconst { provider, modelId } = req.body ?? {};\n\t\tif (typeof provider !== \"string\" || typeof modelId !== \"string\") {\n\t\t\tres.status(400).json({ error: \"provider and modelId are required\" });\n\t\t\treturn;\n\t\t}\n\t\twithRuntime(req, res, (h) => pool.setModel(h, provider, modelId));\n\t});\n\n\tapp.get(\"/api/runtimes/:key/models\", (req, res) => {\n\t\twithRuntime(req, res, async (h) => ({ models: await h.client.getAvailableModels() }));\n\t});\n\n\tapp.post(\"/api/runtimes/:key/thinking\", (req, res) => {\n\t\tconst { level } = req.body ?? {};\n\t\tif (typeof level !== \"string\") {\n\t\t\tres.status(400).json({ error: \"level is required\" });\n\t\t\treturn;\n\t\t}\n\t\twithRuntime(req, res, (h) => pool.setThinkingLevel(h, level as never));\n\t});\n\n\tapp.post(\"/api/runtimes/:key/compact\", (req, res) => {\n\t\tconst instructions = typeof req.body?.instructions === \"string\" ? req.body.instructions : undefined;\n\t\twithRuntime(req, res, (h) => h.client.compact(instructions));\n\t});\n\n\tapp.post(\"/api/runtimes/:key/new-session\", (req, res) => {\n\t\twithRuntime(req, res, (h) => h.client.newSession());\n\t});\n\n\tapp.post(\"/api/runtimes/:key/reload\", (req, res) => {\n\t\twithRuntime(req, res, async (h) => {\n\t\t\tawait h.client.reload();\n\t\t\treturn { ok: true };\n\t\t});\n\t});\n\n\tapp.post(\"/api/runtimes/:key/dream\", (req, res) => {\n\t\tconst args = typeof req.body?.args === \"string\" ? req.body.args : undefined;\n\t\twithRuntime(req, res, (h) => h.client.dream(args));\n\t});\n\n\tapp.post(\"/api/runtimes/:key/import\", (req, res) => {\n\t\tconst inputPath = typeof req.body?.inputPath === \"string\" ? req.body.inputPath.trim() : \"\";\n\t\tif (!inputPath) {\n\t\t\tres.status(400).json({ error: \"inputPath is required\" });\n\t\t\treturn;\n\t\t}\n\t\twithRuntime(req, res, (h) => h.client.importJsonl(inputPath));\n\t});\n\n\tapp.post(\"/api/runtimes/:key/name\", (req, res) => {\n\t\tconst { name } = req.body ?? {};\n\t\tif (typeof name !== \"string\" || name.length === 0) {\n\t\t\tres.status(400).json({ error: \"name is required\" });\n\t\t\treturn;\n\t\t}\n\t\twithRuntime(req, res, (h) => h.client.setSessionName(name));\n\t});\n\n\tapp.get(\"/api/runtimes/:key/stats\", (req, res) => {\n\t\twithRuntime(req, res, (h) => h.client.getSessionStats());\n\t});\n\n\tapp.get(\"/api/runtimes/:key/performance\", (req, res) => {\n\t\twithRuntime(req, res, (h) => h.client.getPerformanceStats());\n\t});\n\n\tapp.get(\"/api/runtimes/:key/resources\", (req, res) => {\n\t\twithRuntime(req, res, (h) => h.client.getResources());\n\t});\n\n\tapp.get(\"/api/runtimes/:key/commands\", (req, res) => {\n\t\twithRuntime(req, res, async (h) => ({ commands: await h.client.getCommands() }));\n\t});\n\n\tapp.get(\"/api/runtimes/:key/branch\", (req, res) => {\n\t\twithRuntime(req, res, async (h) => ({ branch: await h.client.getGitBranch() }));\n\t});\n\n\tapp.get(\"/api/runtimes/:key/fork-messages\", (req, res) => {\n\t\twithRuntime(req, res, async (h) => ({ messages: await h.client.getForkMessages() }));\n\t});\n\n\tapp.post(\"/api/runtimes/:key/fork\", (req, res) => {\n\t\tconst { entryId } = req.body ?? {};\n\t\tif (typeof entryId !== \"string\") {\n\t\t\tres.status(400).json({ error: \"entryId is required\" });\n\t\t\treturn;\n\t\t}\n\t\twithRuntime(req, res, (h) => h.client.fork(entryId));\n\t});\n\n\tapp.get(\"/api/runtimes/:key/tree\", (req, res) => {\n\t\twithRuntime(req, res, (h) => h.client.getTree());\n\t});\n\n\tapp.post(\"/api/runtimes/:key/tree\", (req, res) => {\n\t\tconst targetId = typeof req.body?.targetId === \"string\" ? req.body.targetId : \"\";\n\t\tif (!targetId) {\n\t\t\tres.status(400).json({ error: \"targetId is required\" });\n\t\t\treturn;\n\t\t}\n\t\twithRuntime(req, res, (h) => h.client.navigateTree(targetId));\n\t});\n\n\tapp.get(\"/api/runtimes/:key/sessions\", (req, res) => {\n\t\twithRuntime(req, res, async (h) => ({\n\t\t\tsessions: await Promise.all((await h.client.listSessions()).map(toSessionInfoDto)),\n\t\t}));\n\t});\n\n\tapp.post(\"/api/runtimes/:key/resume\", (req, res) => {\n\t\tconst sessionPath = typeof req.body?.sessionPath === \"string\" ? req.body.sessionPath : \"\";\n\t\tif (!sessionPath) {\n\t\t\tres.status(400).json({ error: \"sessionPath is required\" });\n\t\t\treturn;\n\t\t}\n\t\twithRuntime(req, res, (h) => h.client.switchSession(sessionPath));\n\t});\n\n\tapp.get(\"/api/runtimes/:key/export-html\", (req, res) => {\n\t\tconst handle = pool.get(String(req.params.key));\n\t\tif (!handle) {\n\t\t\tres.status(404).json({ error: `No runtime ${String(req.params.key)}` });\n\t\t\treturn;\n\t\t}\n\t\thandle.client\n\t\t\t.exportHtml()\n\t\t\t.then(({ path }) => {\n\t\t\t\tres.download(path);\n\t\t\t})\n\t\t\t.catch((err) => res.status(502).json({ error: String(err?.message ?? err) }));\n\t});\n\n\tapp.get(\"/api/runtimes/:key/background-agents\", (req, res) => {\n\t\twithRuntime(req, res, async (h) => ({ agents: await h.client.listBackgroundAgents() }));\n\t});\n\n\tapp.get(\"/api/runtimes/:key/subagents/:agentId/messages\", (req, res) => {\n\t\tconst agentId = String(req.params.agentId);\n\t\twithRuntime(req, res, async (h) => {\n\t\t\t// The runtime's registry is authoritative for status + log location.\n\t\t\tconst agents = await h.client.listBackgroundAgents();\n\t\t\tconst agent = agents.find((a) => a.agentId === agentId);\n\t\t\tif (!agent) throw new Error(`No background agent ${agentId} in this runtime`);\n\t\t\tlet messages: unknown[];\n\t\t\ttry {\n\t\t\t\tmessages = readSubagentMessages(agent);\n\t\t\t} catch (error) {\n\t\t\t\tconst failedBeforeSpawn =\n\t\t\t\t\tagent.arbitrations !== undefined &&\n\t\t\t\t\tagent.arbitrations.length > 0 &&\n\t\t\t\t\tagent.arbitrations.every((record) => record.status === \"failure\");\n\t\t\t\tif (!(error instanceof SubagentSessionLogNotFoundError) || !failedBeforeSpawn) throw error;\n\t\t\t\tmessages = [];\n\t\t\t}\n\t\t\treturn { agent, messages: images.project(messages, { runtimeKey: h.key, agentId }) };\n\t\t});\n\t});\n\n\tapp.post(\"/api/runtimes/:key/extension-ui-response\", (req, res) => {\n\t\tconst handle = pool.get(String(req.params.key));\n\t\tif (!handle) {\n\t\t\tres.status(404).json({ error: `No runtime ${String(req.params.key)}` });\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\thandle.client.sendExtensionUIResponse(req.body);\n\t\t\tres.json({ ok: true });\n\t\t} catch (err) {\n\t\t\tres.status(502).json({ error: String((err as Error)?.message ?? err) });\n\t\t}\n\t});\n\n\t// -- disk sessions -----------------------------------------------------------\n\tapp.delete(\"/api/sessions\", (req, res) => {\n\t\tconst path = typeof req.body?.path === \"string\" ? req.body.path : \"\";\n\t\tif (!path) {\n\t\t\tres.status(400).json({ error: \"path is required\" });\n\t\t\treturn;\n\t\t}\n\t\toptions\n\t\t\t.deleteSession(path)\n\t\t\t.then((result) => {\n\t\t\t\tlog(`session deleted: ${path}`);\n\t\t\t\thub.publish(\"\", { type: \"disk_sessions_changed\" });\n\t\t\t\tres.json(result ?? { ok: true });\n\t\t\t})\n\t\t\t.catch((err) => res.status(500).json({ error: String(err?.message ?? err) }));\n\t});\n\n\t// -- settings ------------------------------------------------------------------\n\t// Settings are process-global persistent defaults. They route through hidden\n\t// utility runtimes instead of whichever user session happened to open first.\n\t// Agent-definition discovery is cwd-sensitive, so callers may pass an explicit\n\t// project cwd for endpoints that need project-local .dreb/agents.\n\tfunction withAnyRuntime(\n\t\tres: Response,\n\t\tfn: (h: NonNullable<ReturnType<RuntimePool[\"get\"]>>) => Promise<unknown>,\n\t\tcwd?: string,\n\t) {\n\t\tpool\n\t\t\t.ensureUtilityRuntime(cwd)\n\t\t\t.then((handle) => fn(handle))\n\t\t\t.then((data) => res.json(data ?? { ok: true }))\n\t\t\t.catch((err) => {\n\t\t\t\tres.status(502).json({ error: String(err?.message ?? err) });\n\t\t\t});\n\t}\n\n\tfunction optionalSettingsCwd(req: Request, res: Response): string | undefined | null {\n\t\tif (req.query.cwd === undefined) return undefined;\n\t\tif (typeof req.query.cwd !== \"string\" || !req.query.cwd.trim()) {\n\t\t\tres.status(400).json({ error: \"cwd must be a non-empty path\" });\n\t\t\treturn null;\n\t\t}\n\t\tif (!existsSync(req.query.cwd)) {\n\t\t\tres.status(400).json({ error: `cwd does not exist: ${req.query.cwd}` });\n\t\t\treturn null;\n\t\t}\n\t\ttry {\n\t\t\tif (!statSync(req.query.cwd).isDirectory()) {\n\t\t\t\tres.status(400).json({ error: `cwd is not a directory: ${req.query.cwd}` });\n\t\t\t\treturn null;\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tres.status(400).json({ error: `cannot access cwd ${req.query.cwd}: ${(error as Error).message}` });\n\t\t\treturn null;\n\t\t}\n\t\treturn req.query.cwd;\n\t}\n\n\tapp.get(\"/api/settings\", (req, res) => {\n\t\tconst cwd = optionalSettingsCwd(req, res);\n\t\tif (cwd === null) return;\n\t\twithAnyRuntime(res, (h) => h.client.getSettings(), cwd);\n\t});\n\n\tapp.get(\"/api/settings/models\", (req, res) => {\n\t\tconst cwd = optionalSettingsCwd(req, res);\n\t\tif (cwd === null) return;\n\t\twithAnyRuntime(res, async (h) => ({ models: await h.client.getAvailableModels() }), cwd);\n\t});\n\n\tapp.get(\"/api/settings/agent-types\", (req, res) => {\n\t\tconst cwd = typeof req.query.cwd === \"string\" && req.query.cwd.trim() ? req.query.cwd : undefined;\n\t\tif (cwd && !existsSync(cwd)) {\n\t\t\tres.status(400).json({ error: `cwd does not exist: ${cwd}` });\n\t\t\treturn;\n\t\t}\n\t\twithAnyRuntime(res, async (h) => ({ agentTypes: await h.client.listAgentTypes() }), cwd);\n\t});\n\n\tapp.get(\"/api/daily-cost\", (_req, res) => {\n\t\twithAnyRuntime(res, async (h) => ({ cost: await h.client.getDailyCost() }));\n\t});\n\n\tapp.put(\"/api/settings\", (req, res) => {\n\t\tconst cwd = optionalSettingsCwd(req, res);\n\t\tif (cwd === null) return;\n\t\twithAnyRuntime(res, (h) => h.client.setSettings(req.body ?? {}), cwd);\n\t});\n\n\tapp.get(\"/api/version\", (_req, res) => {\n\t\twithAnyRuntime(res, async (h) => ({ version: await h.client.getVersion() }));\n\t});\n\n\tapp.post(\"/api/settings/remove-trusted\", (req, res) => {\n\t\tconst rawPath = typeof req.body?.path === \"string\" ? req.body.path : \"\";\n\t\tif (!rawPath) {\n\t\t\tres.status(400).json({ error: \"path is required\" });\n\t\t\treturn;\n\t\t}\n\t\tpool\n\t\t\t.ensureUtilityRuntime()\n\t\t\t.then(async (handle) => {\n\t\t\t\tconst result = await handle.client.removeTrustedContextFolder(rawPath);\n\t\t\t\tlog(`context trust configured remove: ${rawPath}`);\n\t\t\t\tres.json(result);\n\t\t\t})\n\t\t\t.catch((err) => res.status(err?.status ?? 502).json({ error: String(err?.message ?? err) }));\n\t});\n\n\t// -- server lifecycle ----------------------------------------------------------\n\t// Build/version of the *server* process (distinct from a freshly-spawned RPC\n\t// child's version) so a stale long-running service is visible at a glance.\n\tapp.get(\"/api/server/info\", (_req, res) => {\n\t\tres.json({\n\t\t\tversion: options.serverVersion ?? null,\n\t\t\tstartedAt: serverStartedAt,\n\t\t\t// systemd sets INVOCATION_ID; other supervisors set LISTEN_PID. Best-effort.\n\t\t\tsupervised: Boolean(process.env.INVOCATION_ID || process.env.LISTEN_PID),\n\t\t\trestartable: Boolean(options.onRestart),\n\t\t});\n\t});\n\n\tapp.post(\"/api/server/restart\", (_req, res) => {\n\t\tif (!options.onRestart) {\n\t\t\tres.status(501).json({\n\t\t\t\terror: \"Restart is unavailable — the dashboard is not running under a supervisor that can respawn it\",\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tlog(\"restart requested via API\");\n\t\tres.json({ ok: true, restarting: true });\n\t\t// Defer so the HTTP response flushes before the process exits.\n\t\tsetTimeout(() => options.onRestart?.(), 100);\n\t});\n\n\t// -- files -----------------------------------------------------------------------\n\tapp.get(\"/api/files\", (req, res) => {\n\t\tconst path = typeof req.query.path === \"string\" ? req.query.path : homedir();\n\t\tfiles\n\t\t\t.list(path)\n\t\t\t.then(async (listing) => {\n\t\t\t\tconst handle = await pool.ensureUtilityRuntime();\n\t\t\t\tconst contextTrust = await handle.client.evaluateContextTrust(listing.path);\n\t\t\t\tres.json({ ...listing, contextTrust });\n\t\t\t})\n\t\t\t.catch((err) => res.status(err?.status ?? 502).json({ error: String(err?.message ?? err) }));\n\t});\n\n\tfunction contextTrustMutation(\n\t\treq: Request,\n\t\tres: Response,\n\t\toperation: \"trustContextFolder\" | \"untrustContextFolder\",\n\t): void {\n\t\tconst rawPath = typeof req.body?.path === \"string\" ? req.body.path : \"\";\n\t\tif (!rawPath) {\n\t\t\tres.status(400).json({ error: \"path is required\" });\n\t\t\treturn;\n\t\t}\n\t\tfiles\n\t\t\t.resolveDirectory(rawPath)\n\t\t\t.then(async (path) => {\n\t\t\t\tconst handle = await pool.ensureUtilityRuntime();\n\t\t\t\tconst result = await handle.client[operation](path);\n\t\t\t\tlog(`context trust ${operation === \"trustContextFolder\" ? \"add\" : \"remove\"}: ${path}`);\n\t\t\t\tres.json(result);\n\t\t\t})\n\t\t\t.catch((err) => res.status(err?.status ?? 502).json({ error: String(err?.message ?? err) }));\n\t}\n\n\tapp.post(\"/api/files/trust\", (req, res) => contextTrustMutation(req, res, \"trustContextFolder\"));\n\tapp.post(\"/api/files/untrust\", (req, res) => contextTrustMutation(req, res, \"untrustContextFolder\"));\n\n\tapp.get(\"/api/files/places\", (_req, res) => {\n\t\tconst roots = [...new Set(pool.list().map((h) => h.cwd))];\n\t\tres.json({ places: defaultPlaces(homedir(), roots) });\n\t});\n\n\tapp.get(\"/api/files/download\", (req, res) => {\n\t\tconst path = typeof req.query.path === \"string\" ? req.query.path : \"\";\n\t\tfiles\n\t\t\t.resolveDownload(path)\n\t\t\t.then(({ path: real }) => {\n\t\t\t\t// send's default (dotfiles: \"ignore\") would 404 any dot-prefixed\n\t\t\t\t// component; resolveDownload already canonicalized and validated.\n\t\t\t\t// The explicit filename keeps the Content-Disposition identical to\n\t\t\t\t// the no-filename form (content-disposition basenames internally).\n\t\t\t\tres.download(real, basename(real), { dotfiles: \"allow\" });\n\t\t\t})\n\t\t\t.catch((err) => res.status(err?.status ?? 500).json({ error: String(err?.message ?? err) }));\n\t});\n\n\tapp.post(\"/api/files/upload\", (req, res) => {\n\t\t(async () => {\n\t\t\tconst dir = typeof req.query.dir === \"string\" ? req.query.dir : \"\";\n\t\t\tconst name = typeof req.query.name === \"string\" ? req.query.name : \"\";\n\t\t\tconst overwrite = req.query.overwrite === \"true\";\n\t\t\tconst upload = await files.prepareUpload(dir, name, overwrite);\n\t\t\ttry {\n\t\t\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\t\t\tlet settled = false;\n\t\t\t\t\tconst fail = (err: unknown) => {\n\t\t\t\t\t\tif (settled) return;\n\t\t\t\t\t\tsettled = true;\n\t\t\t\t\t\tupload.stream.destroy();\n\t\t\t\t\t\treject(err);\n\t\t\t\t\t};\n\t\t\t\t\treq.pipe(upload.stream);\n\t\t\t\t\tupload.stream.on(\"finish\", () => {\n\t\t\t\t\t\tif (settled) return;\n\t\t\t\t\t\tsettled = true;\n\t\t\t\t\t\tresolve();\n\t\t\t\t\t});\n\t\t\t\t\tupload.stream.on(\"error\", fail);\n\t\t\t\t\treq.on(\"error\", fail);\n\t\t\t\t\treq.on(\"aborted\", () => fail(Object.assign(new Error(\"Upload aborted\"), { status: 499 })));\n\t\t\t\t});\n\t\t\t\tawait upload.commit();\n\t\t\t\tres.json({ path: upload.path });\n\t\t\t} catch (err) {\n\t\t\t\tawait upload.cleanup();\n\t\t\t\tthrow err;\n\t\t\t}\n\t\t})().catch((err) => {\n\t\t\tif (!res.headersSent) res.status(err?.status ?? 500).json({ error: String(err?.message ?? err) });\n\t\t});\n\t});\n\n\tapp.post(\"/api/files/mkdir\", (req, res) => {\n\t\tconst { dir, name } = req.body ?? {};\n\t\tif (typeof dir !== \"string\" || typeof name !== \"string\") {\n\t\t\tres.status(400).json({ error: \"dir and name are required\" });\n\t\t\treturn;\n\t\t}\n\t\tfiles\n\t\t\t.mkdir(dir, name)\n\t\t\t.then((path) => res.json({ path }))\n\t\t\t.catch((err) => res.status(err?.status ?? 500).json({ error: String(err?.message ?? err) }));\n\t});\n\n\t// -- static client -----------------------------------------------------------------\n\tif (options.staticDir) {\n\t\tapp.use(express.static(options.staticDir));\n\t\t// SPA fallback: serve index.html for non-API GETs (client-side routing).\n\t\tapp.get(/^\\/(?!api\\/).*/, (_req, res) => {\n\t\t\tres.sendFile(join(options.staticDir!, \"index.html\"));\n\t\t});\n\t}\n\n\treturn app;\n}\n"]}