@dreb/dashboard 2.53.1 → 2.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -44,7 +44,7 @@ Open `http://127.0.0.1:5343`.
44
44
  per-message copy, tasks panel, a bounded scrollable panel listing every retained
45
45
  subagent newest-first (collapsed by default on mobile), suggest-next chip,
46
46
  generic built-in slash-command discovery and fail-closed execution (including
47
- settings/model, import/export, session tree, fork, new/compact/dream,
47
+ settings/model/scoped-models, import/export, session tree, fork, new/compact/dream,
48
48
  resume/reload, and quit), image attach/paste with sent-image previews retained in user transcript entries,
49
49
  queued-message restore, persistent session-header live indicator, footer-parity info bar (branch, tokens, cost, ctx%,
50
50
  median tok/s), stats/loaded-context/fork modals, steer/follow-up composer
@@ -61,8 +61,8 @@ Open `http://127.0.0.1:5343`.
61
61
  its inherited descendants).
62
62
  - **Settings** — persistent defaults (provider-grouped model dropdown,
63
63
  thinking, queue modes, image handling, skill commands, transport,
64
- hide-thinking, compaction/retry), per-agent model fallback editor, and the
65
- global-only nested-context policy: an auditable trusted-roots list with
64
+ hide-thinking, compaction/retry), a scoped-models editor, per-agent model
65
+ fallback editor, and the global-only nested-context policy: an auditable trusted-roots list with
66
66
  revoke and simple add-by-path controls, plus a prominent expert trust-all
67
67
  warning. The Files view remains the primary trust-grant flow. Most defaults
68
68
  seed new sessions; opening Settings flushes pending writes and reloads durable
@@ -77,6 +77,12 @@ Open `http://127.0.0.1:5343`.
77
77
  and paired-devices management.
78
78
  - **Pairing** — remote first-login rotating-code flow.
79
79
 
80
+ ### Scoped models
81
+
82
+ The Settings scoped-models editor manages the persistent model-cycling scope. Search is grouped by provider, with individual model, provider, and all-model toggles; non-empty partial scopes have accessible up/down ordering controls plus save/reset, and controls remain usable on mobile. An absent `enabledModels` means implicit all available registry models in registry order, including future additions, so that view cannot be reordered. A saved partial scope is an ordered list of exact canonical `provider/model` references; editing legacy glob, fuzzy, or thinking-suffix values normalizes them to exact references.
83
+
84
+ The selected project context reads effective global + project settings, but saves always write the global setting and warn if `.dreb/settings.json` shadows it. Changes seed new sessions only and never modify a running session. Running `/scoped-models` in a dashboard session opens this editor with that session's cwd selected. For persisted-setting and RPC details, see [Model Cycling](../coding-agent/docs/settings.md#model-cycling) and [`get_settings` / `set_settings`](../coding-agent/docs/rpc.md#settings).
85
+
80
86
  ### Transcript images
81
87
 
82
88
  Image blocks returned by any tool and images uploaded with a user turn render
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server/server.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,OAAO,MAAM,SAAS,CAAC;AAc9B,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;AAGvF,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,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,OAAO,EAAE,CAAC,CAAC;IAC1C,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;CAC7B;AAGD,eAAO,MAAM,sBAAsB,QAAkB,CAAC;AACtD,eAAO,MAAM,+BAA+B,QAAS,CAAC;AA2CtD,oDAAoD;AACpD,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAQtF;AAQD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,GAAG,kBAAkB,CAk+BzF","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 } 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 { MAX_CLIENT_DIAGNOSTIC_BYTES, MAX_PROMPT_BODY_BYTES } 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 } from \"./files.js\";\nimport { ImagePreviewWorker } from \"./image-preview.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 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<unknown[]>;\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}\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 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 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\tapp.use(express.json({ limit: MAX_PROMPT_BODY_BYTES }));\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) {\n\t\t\tconst status: AuthStatusDto =\n\t\t\t\tdecision.mode === \"local\"\n\t\t\t\t\t? { mode: \"local\" }\n\t\t\t\t\t: { mode: \"remote\", identity: decision.identity.loginName, device: decision.identity.device };\n\t\t\tres.json({ ...status, needsPairing: false });\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/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\t((await options.listAllSessions()) as SessionInfoDto[]).filter((session) => existsSync(session.cwd));\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 cwd = typeof req.body?.cwd === \"string\" ? req.body.cwd : \"\";\n\t\t\tif (!cwd || !existsSync(cwd)) {\n\t\t\t\tres.status(400).json({ error: `Working directory does not exist: ${cwd || \"(empty)\"}` });\n\t\t\t\treturn;\n\t\t\t}\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) => res.status(500).json({ error: String(err?.message ?? 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.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) => h.client.setModel(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) => h.client.setThinkingLevel(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) => ({ sessions: await h.client.listSessions() }));\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\tapp.get(\"/api/settings\", (_req, res) => {\n\t\twithAnyRuntime(res, (h) => h.client.getSettings());\n\t});\n\n\tapp.get(\"/api/settings/models\", (_req, res) => {\n\t\twithAnyRuntime(res, async (h) => ({ models: await h.client.getAvailableModels() }));\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\twithAnyRuntime(res, (h) => h.client.setSettings(req.body ?? {}));\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\tres.download(real);\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"]}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server/server.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,OAAO,MAAM,SAAS,CAAC;AAc9B,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;AAGvF,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,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,OAAO,EAAE,CAAC,CAAC;IAC1C,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;CAC7B;AAGD,eAAO,MAAM,sBAAsB,QAAkB,CAAC;AACtD,eAAO,MAAM,+BAA+B,QAAS,CAAC;AA2CtD,oDAAoD;AACpD,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAQtF;AAQD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,GAAG,kBAAkB,CA8/BzF","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 { MAX_CLIENT_DIAGNOSTIC_BYTES, MAX_PROMPT_BODY_BYTES } 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 } from \"./files.js\";\nimport { ImagePreviewWorker } from \"./image-preview.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 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<unknown[]>;\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}\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 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 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\tapp.use(express.json({ limit: MAX_PROMPT_BODY_BYTES }));\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) {\n\t\t\tconst status: AuthStatusDto =\n\t\t\t\tdecision.mode === \"local\"\n\t\t\t\t\t? { mode: \"local\" }\n\t\t\t\t\t: { mode: \"remote\", identity: decision.identity.loginName, device: decision.identity.device };\n\t\t\tres.json({ ...status, needsPairing: false });\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/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\t((await options.listAllSessions()) as SessionInfoDto[]).filter((session) => existsSync(session.cwd));\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 cwd = typeof req.body?.cwd === \"string\" ? req.body.cwd : \"\";\n\t\t\tif (!cwd || !existsSync(cwd)) {\n\t\t\t\tres.status(400).json({ error: `Working directory does not exist: ${cwd || \"(empty)\"}` });\n\t\t\t\treturn;\n\t\t\t}\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) => res.status(500).json({ error: String(err?.message ?? 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.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) => h.client.setModel(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) => h.client.setThinkingLevel(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) => ({ sessions: await h.client.listSessions() }));\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\tres.download(real);\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"]}
@@ -7,7 +7,7 @@
7
7
  * itself. Remote mode still passes every request through DashboardAuth.
8
8
  */
9
9
  import { randomUUID } from "node:crypto";
10
- import { existsSync } from "node:fs";
10
+ import { existsSync, statSync } from "node:fs";
11
11
  import { homedir } from "node:os";
12
12
  import { basename, join } from "node:path";
13
13
  import express from "express";
@@ -818,11 +818,40 @@ export function createDashboardServer(options) {
818
818
  res.status(502).json({ error: String(err?.message ?? err) });
819
819
  });
820
820
  }
821
- app.get("/api/settings", (_req, res) => {
822
- withAnyRuntime(res, (h) => h.client.getSettings());
821
+ function optionalSettingsCwd(req, res) {
822
+ if (req.query.cwd === undefined)
823
+ return undefined;
824
+ if (typeof req.query.cwd !== "string" || !req.query.cwd.trim()) {
825
+ res.status(400).json({ error: "cwd must be a non-empty path" });
826
+ return null;
827
+ }
828
+ if (!existsSync(req.query.cwd)) {
829
+ res.status(400).json({ error: `cwd does not exist: ${req.query.cwd}` });
830
+ return null;
831
+ }
832
+ try {
833
+ if (!statSync(req.query.cwd).isDirectory()) {
834
+ res.status(400).json({ error: `cwd is not a directory: ${req.query.cwd}` });
835
+ return null;
836
+ }
837
+ }
838
+ catch (error) {
839
+ res.status(400).json({ error: `cannot access cwd ${req.query.cwd}: ${error.message}` });
840
+ return null;
841
+ }
842
+ return req.query.cwd;
843
+ }
844
+ app.get("/api/settings", (req, res) => {
845
+ const cwd = optionalSettingsCwd(req, res);
846
+ if (cwd === null)
847
+ return;
848
+ withAnyRuntime(res, (h) => h.client.getSettings(), cwd);
823
849
  });
824
- app.get("/api/settings/models", (_req, res) => {
825
- withAnyRuntime(res, async (h) => ({ models: await h.client.getAvailableModels() }));
850
+ app.get("/api/settings/models", (req, res) => {
851
+ const cwd = optionalSettingsCwd(req, res);
852
+ if (cwd === null)
853
+ return;
854
+ withAnyRuntime(res, async (h) => ({ models: await h.client.getAvailableModels() }), cwd);
826
855
  });
827
856
  app.get("/api/settings/agent-types", (req, res) => {
828
857
  const cwd = typeof req.query.cwd === "string" && req.query.cwd.trim() ? req.query.cwd : undefined;
@@ -836,7 +865,10 @@ export function createDashboardServer(options) {
836
865
  withAnyRuntime(res, async (h) => ({ cost: await h.client.getDailyCost() }));
837
866
  });
838
867
  app.put("/api/settings", (req, res) => {
839
- withAnyRuntime(res, (h) => h.client.setSettings(req.body ?? {}));
868
+ const cwd = optionalSettingsCwd(req, res);
869
+ if (cwd === null)
870
+ return;
871
+ withAnyRuntime(res, (h) => h.client.setSettings(req.body ?? {}), cwd);
840
872
  });
841
873
  app.get("/api/version", (_req, res) => {
842
874
  withAnyRuntime(res, async (h) => ({ version: await h.client.getVersion() }));