@dreb/dashboard 2.55.6 → 2.57.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 +5 -4
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +11 -0
- package/dist/server/server.js.map +1 -1
- package/dist/static/assets/index-DKq1kEAm.js +79 -0
- package/dist/static/index.html +1 -1
- package/dist/static/sw.js +1 -1
- package/package.json +1 -1
- package/dist/static/assets/index-CNuWaOpq.js +0 -79
package/README.md
CHANGED
|
@@ -50,10 +50,11 @@ Open `http://127.0.0.1:5343`.
|
|
|
50
50
|
latest-100 median tok/s with sample count and long-term delta), stats/loaded-context/fork modals, steer/follow-up composer
|
|
51
51
|
modes, ■ abort, model/thinking switchers, extension-UI modals, export HTML,
|
|
52
52
|
and live auto-naming.
|
|
53
|
-
- **Subagent drill-in** —
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
- **Subagent drill-in** — transcript of a background agent: live events via
|
|
54
|
+
the relay, hydrated from the agent's on-disk session log so the view survives
|
|
55
|
+
browser reloads. While the child is running, its composer queues user-written
|
|
56
|
+
steering messages directly to that child and shows its effective queue mode;
|
|
57
|
+
completed and rehydrated transcripts remain read-only.
|
|
57
58
|
- **Files** — host-wide browse with places shortcuts, upload (collision
|
|
58
59
|
prompts before overwrite), download, new-folder, "new session here", and the
|
|
59
60
|
effective global nested-context trust for the viewed canonical folder. Trust
|
|
@@ -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;AAkB9B,OAAO,KAAK,EAAgB,aAAa,EAAE,MAAM,WAAW,CAAC;AAC7D,OAAO,EAKN,qBAAqB,EACrB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,QAAQ,EAA+C,MAAM,gBAAgB,CAAC;AAIvF,OAAO,KAAK,EAA4B,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAG/E,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,OAAO,GAAG;IAClD,mFAAmF;IACnF,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAChC,CAAC;AAEF,MAAM,WAAW,0BAA0B;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,IAAI,CAAC;IACd,QAAQ,EAAE,IAAI,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,sBAAsB;IACtC,IAAI,EAAE,aAAa,CAAC;IACpB,IAAI,EAAE,WAAW,CAAC;IAClB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yEAAuE;IACvE,eAAe,EAAE,MAAM,OAAO,CAAC,0BAA0B,EAAE,CAAC,CAAC;IAC7D,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAClD,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,sGAAsG;IACtG,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2HAAyH;IACzH,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,2GAA2G;IAC3G,YAAY,CAAC,EAAE,qBAAqB,CAAC;IACrC,wDAAwD;IACxD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAGD,eAAO,MAAM,sBAAsB,QAAkB,CAAC;AACtD,eAAO,MAAM,+BAA+B,QAAS,CAAC;AAmEtD,oDAAoD;AACpD,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAQtF;AAQD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,GAAG,kBAAkB,CAqlCzF","sourcesContent":["/**\n * Dashboard HTTP server — Express app wiring auth, the runtime pool, the SSE\n * hub, and the file API into the REST surface the browser client consumes.\n *\n * Bind address discipline: local mode binds 127.0.0.1 only. The\n * caller decides the bind address; `createDashboardServer` never listens by\n * itself. Remote mode still passes every request through DashboardAuth.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport { existsSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { basename, join } from \"node:path\";\nimport type { NextFunction, Request, Response } from \"express\";\nimport express from \"express\";\nimport type {\n\tActiveRuntimeSnapshotDto,\n\tAuthStatusDto,\n\tClientConnectionDiagnosticDto,\n\tDashboardResyncDto,\n\tFleetDto,\n\tImageAttachmentDto,\n\tPairingCodeDto,\n\tRuntimeHydrationDto,\n\tSessionInfoDto,\n\tSessionInventoryDto,\n} from \"../shared/protocol.js\";\nimport {\n\tMAX_CLIENT_DIAGNOSTIC_BYTES,\n\tMAX_PROMPT_BODY_BYTES,\n\tMAX_SESSION_PREVIEW_CHARACTERS,\n} from \"../shared/protocol.js\";\nimport type { AuthDecision, DashboardAuth } from \"./auth.js\";\nimport {\n\tDASHBOARD_IMAGE_ID_PATTERN,\n\tDashboardImageNotFoundError,\n\tDashboardImagePreviewError,\n\ttype DashboardImageScope,\n\tDashboardImageService,\n} from \"./dashboard-images.js\";\nimport { EventHub, formatHeartbeatFrame, type SseWriteMetadata } from \"./event-hub.js\";\nimport { defaultPlaces, FileApi } from \"./files.js\";\nimport { ImagePreviewWorker } from \"./image-preview.js\";\nimport { MemoryApi } from \"./memories.js\";\nimport type { DashboardRuntimeSnapshot, RuntimePool } from \"./runtime-pool.js\";\nimport { readSubagentMessages, SubagentSessionLogNotFoundError } from \"./subagent-log.js\";\n\nexport type DashboardServerApp = express.Express & {\n\t/** Close dashboard-owned services. Safe to call more than once during shutdown. */\n\tcloseDashboard(): Promise<void>;\n};\n\nexport interface DashboardSessionInfoSource {\n\tpath: string;\n\tid: string;\n\tcwd: string;\n\tname?: string;\n\tcreated: Date;\n\tmodified: Date;\n\tmessageCount: number;\n\tfirstMessage: string;\n}\n\nexport interface DashboardServerOptions {\n\tauth: DashboardAuth;\n\tpool: RuntimePool;\n\t/** Directory of built client assets; omit to skip static serving (tests). */\n\tstaticDir?: string;\n\t/** Session listing (cross-project) — injected so tests can stub it. */\n\tlistAllSessions: () => Promise<DashboardSessionInfoSource[]>;\n\tdeleteSession: (path: string) => Promise<unknown>;\n\tlogger?: (line: string) => void;\n\t/** Build version of the running server process (for the settings footer / stale-server detection). */\n\tserverVersion?: string;\n\t/** Restart hook — when set, POST /api/server/restart invokes it (typically process exit for a supervisor to respawn). */\n\tonRestart?: () => void;\n\t/** Injectable only to make SSE limits deterministic in integration tests. */\n\teventHub?: EventHub;\n\t/** Injectable bounded image repository/preview service for deterministic tests and lifecycle ownership. */\n\timageService?: DashboardImageService;\n\t/** Named heartbeat interval; defaults to 25 seconds. */\n\theartbeatIntervalMs?: number;\n\t/** Test-only override for the global dreb memory home directory. */\n\tmemoryHomeDir?: string;\n}\n\nconst DEVICE_COOKIE = \"dreb_dashboard_device\";\nexport const MAX_SSE_BUFFERED_BYTES = 4 * 1024 * 1024;\nexport const CLIENT_DIAGNOSTIC_RATE_LIMIT_MS = 30_000;\nconst CLIENT_DIAGNOSTIC_CONNECTION_TTL_MS = 10 * 60_000;\n\nfunction boundedSessionPreview(text: string): string {\n\tlet preview = \"\";\n\tlet characters = 0;\n\tfor (const character of text) {\n\t\tif (characters === MAX_SESSION_PREVIEW_CHARACTERS) break;\n\t\tpreview += character;\n\t\tcharacters++;\n\t}\n\treturn preview;\n}\n\nfunction toSessionInfoDto(session: DashboardSessionInfoSource): SessionInfoDto {\n\treturn {\n\t\tpath: session.path,\n\t\tid: session.id,\n\t\tcwd: session.cwd,\n\t\tname: session.name,\n\t\tcreated: session.created.toISOString(),\n\t\tmodified: session.modified.toISOString(),\n\t\tmessageCount: session.messageCount,\n\t\tfirstMessage: boundedSessionPreview(session.firstMessage),\n\t};\n}\n\nfunction isClientDiagnostic(value: unknown): value is ClientConnectionDiagnosticDto {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) return false;\n\tconst body = value as Record<string, unknown>;\n\tconst allowed = new Set([\n\t\t\"connectionId\",\n\t\t\"state\",\n\t\t\"previousState\",\n\t\t\"attempt\",\n\t\t\"delayMs\",\n\t\t\"visibility\",\n\t\t\"lastAppliedSeq\",\n\t\t\"heartbeatAgeMs\",\n\t\t\"eventCount\",\n\t\t\"eventRatePerMinute\",\n\t\t\"processingLagTotalMs\",\n\t\t\"processingLagMaxMs\",\n\t]);\n\tif (Object.keys(body).some((key) => !allowed.has(key))) return false;\n\tconst states = new Set([\"connecting\", \"connected\", \"retrying\", \"resyncing\", \"disconnected\", \"auth_failed\"]);\n\tconst nonNegativeNumber = (item: unknown) => typeof item === \"number\" && Number.isFinite(item) && item >= 0;\n\tconst nonNegativeInteger = (item: unknown) => typeof item === \"number\" && Number.isSafeInteger(item) && item >= 0;\n\treturn (\n\t\ttypeof body.connectionId === \"string\" &&\n\t\t/^[0-9a-f-]{36}$/i.test(body.connectionId) &&\n\t\ttypeof body.state === \"string\" &&\n\t\tstates.has(body.state) &&\n\t\t(body.previousState === undefined ||\n\t\t\t(typeof body.previousState === \"string\" && states.has(body.previousState))) &&\n\t\tnonNegativeInteger(body.attempt) &&\n\t\tnonNegativeNumber(body.eventCount) &&\n\t\tnonNegativeNumber(body.eventRatePerMinute) &&\n\t\tnonNegativeNumber(body.processingLagTotalMs) &&\n\t\tnonNegativeNumber(body.processingLagMaxMs) &&\n\t\t(body.delayMs === undefined || nonNegativeNumber(body.delayMs)) &&\n\t\t(body.lastAppliedSeq === undefined || nonNegativeInteger(body.lastAppliedSeq)) &&\n\t\t(body.heartbeatAgeMs === undefined || nonNegativeNumber(body.heartbeatAgeMs)) &&\n\t\t(body.visibility === \"visible\" || body.visibility === \"hidden\")\n\t);\n}\n\n/** Parse the device cookie from a Cookie header. */\nexport function parseDeviceCookie(cookieHeader: string | undefined): string | undefined {\n\tif (!cookieHeader) return undefined;\n\tfor (const part of cookieHeader.split(\";\")) {\n\t\tconst eq = part.indexOf(\"=\");\n\t\tif (eq === -1) continue;\n\t\tif (part.slice(0, eq).trim() === DEVICE_COOKIE) return part.slice(eq + 1).trim();\n\t}\n\treturn undefined;\n}\n\ninterface AuthedRequest extends Request {\n\tauthDecision?: AuthDecision;\n\t/** Per-SSE-request opaque diagnostic correlation id. */\n\tsseConnectionId?: string;\n}\n\nexport function createDashboardServer(options: DashboardServerOptions): DashboardServerApp {\n\tconst { auth, pool } = options;\n\tconst serverStartedAt = new Date().toISOString();\n\tconst diagnosticConnections = new Map<string, { issuedAt: number; lastAt?: number }>();\n\tconst log = options.logger ?? ((line: string) => console.log(`[dashboard] ${line}`));\n\tconst files = new FileApi((op, path, detail) => log(`file ${op}: ${path}${detail ? ` (${detail})` : \"\"}`));\n\tconst memories = new MemoryApi(options.memoryHomeDir ?? homedir(), (op, scopeId, detail) =>\n\t\tlog(`memory ${op}: ${scopeId}${detail ? ` (${detail})` : \"\"}`),\n\t);\n\tconst hub = options.eventHub ?? new EventHub();\n\tconst images = options.imageService ?? new DashboardImageService(new ImagePreviewWorker());\n\thub.setEventProjector((key, event) => (key ? images.projectEvent(event, { runtimeKey: key }) : event));\n\tpool.onEvent((key, event) => {\n\t\tif (event.type === \"dashboard_snapshot_barrier\" && typeof event.snapshotId === \"string\") {\n\t\t\t// This RPC marker has no browser frame: its synchronous sequence capture\n\t\t\t// orders the HTTP snapshot before all later EventHub publications.\n\t\t\tpool.recordDashboardBarrier(key, event.snapshotId, hub.currentSequence);\n\t\t\treturn;\n\t\t}\n\t\thub.publish(key, event);\n\t\tif (event.type === \"runtime_removed\") images.removeRuntime(key);\n\t});\n\tpool.onFleetSnapshot((event) => hub.publish(\"\", { ...event }));\n\n\tconst app = express() as DashboardServerApp;\n\tlet closePromise: Promise<void> | undefined;\n\tapp.closeDashboard = () => {\n\t\tclosePromise ??= images.close();\n\t\treturn closePromise;\n\t};\n\tapp.disable(\"x-powered-by\");\n\n\t// -- auth middleware (every route, fail-closed) ---------------------------\n\tapp.use((req: AuthedRequest, res: Response, next: NextFunction) => {\n\t\tif (req.path === \"/api/events\") req.sseConnectionId = randomUUID();\n\t\tauth\n\t\t\t.authenticate({\n\t\t\t\tremoteAddress: req.socket.remoteAddress,\n\t\t\t\thostHeader: req.headers.host,\n\t\t\t\toriginHeader: req.headers.origin,\n\t\t\t\tdeviceToken: parseDeviceCookie(req.headers.cookie),\n\t\t\t})\n\t\t\t.then((decision) => {\n\t\t\t\treq.authDecision = decision;\n\t\t\t\tif (decision.allowed) return next();\n\t\t\t\tconst canRenderAuthScreen = decision.needsPairing || Boolean(decision.identity);\n\t\t\t\tif (canRenderAuthScreen) {\n\t\t\t\t\t// The auth/pairing endpoints must be reachable by allowed-but-unpaired\n\t\t\t\t\t// identities, and /api/auth must also be reachable by rejected\n\t\t\t\t\t// Tailscale identities so the SPA denial screen can name them.\n\t\t\t\t\tif (req.path === \"/api/auth\" || (decision.needsPairing && req.path === \"/api/pair\")) return next();\n\t\t\t\t\t// Let the SPA shell + static assets load so the client-side pairing or\n\t\t\t\t\t// denial screen can render. No data exposure: every /api/* data route\n\t\t\t\t\t// below stays fail-closed — only non-API GETs (the app shell) are allowed.\n\t\t\t\t\tif (req.method === \"GET\" && !req.path.startsWith(\"/api/\")) return next();\n\t\t\t\t}\n\t\t\t\tif (req.sseConnectionId) {\n\t\t\t\t\tlog(\n\t\t\t\t\t\t`sse ${JSON.stringify({\n\t\t\t\t\t\t\tconnectionId: req.sseConnectionId,\n\t\t\t\t\t\t\tkind: \"auth_denial\",\n\t\t\t\t\t\t\tmethod: req.method,\n\t\t\t\t\t\t\tpath: req.path,\n\t\t\t\t\t\t\tstatus: decision.status,\n\t\t\t\t\t\t})}`,\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\tlog(`denied ${req.method} ${req.path}: ${decision.reason}`);\n\t\t\t\t}\n\t\t\t\tres.status(decision.status).json({\n\t\t\t\t\terror: decision.reason,\n\t\t\t\t\tneedsPairing: decision.needsPairing ?? false,\n\t\t\t\t\tidentity: decision.identity?.loginName,\n\t\t\t\t});\n\t\t\t})\n\t\t\t.catch((err) => {\n\t\t\t\t// authenticate() already catches internally; this is belt-and-suspenders.\n\t\t\t\tlog(`auth middleware error — denying: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t\tres.status(500).json({ error: \"Auth subsystem error — denied\" });\n\t\t\t});\n\t});\n\n\t// Authenticate before consuming request bodies. Diagnostics have their own\n\t// small parser limit; the larger limit exists only for prompt image payloads.\n\tapp.use(\"/api/events/diagnostic\", express.json({ limit: MAX_CLIENT_DIAGNOSTIC_BYTES }));\n\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 && decision.mode === \"local\") {\n\t\t\tconst status: AuthStatusDto = { mode: \"local\" };\n\t\t\tres.json({ ...status, needsPairing: false });\n\t\t\treturn;\n\t\t}\n\t\tif (decision.allowed) {\n\t\t\tauth\n\t\t\t\t.claimPairingExpiryStatus(decision.pairing.id)\n\t\t\t\t.then((expiry) => {\n\t\t\t\t\tconst status: AuthStatusDto = {\n\t\t\t\t\t\tmode: \"remote\",\n\t\t\t\t\t\tidentity: decision.identity.loginName,\n\t\t\t\t\t\tdevice: decision.identity.device,\n\t\t\t\t\t\t...(expiry.warning ? { pairingExpiryWarning: expiry.warning } : {}),\n\t\t\t\t\t\t...(expiry.nextCheckAt ? { pairingExpiryCheckAt: expiry.nextCheckAt } : {}),\n\t\t\t\t\t};\n\t\t\t\t\tres.json({ ...status, needsPairing: false });\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tlog(`pairing expiry status failed: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t\t\tres.status(500).json({ error: \"Auth subsystem error — denied\", needsPairing: false });\n\t\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tres.status(decision.status).json({\n\t\t\terror: decision.reason,\n\t\t\tneedsPairing: decision.needsPairing ?? false,\n\t\t\tidentity: decision.identity?.loginName,\n\t\t});\n\t});\n\n\tapp.get(\"/api/pairing-code\", (req: AuthedRequest, res) => {\n\t\tconst decision = req.authDecision!;\n\t\tif (!decision.allowed || decision.mode !== \"local\") {\n\t\t\tres.status(403).json({ error: \"Pairing code is only available from the host machine\" });\n\t\t\treturn;\n\t\t}\n\t\tif (!auth.isRemoteEnabled) {\n\t\t\tconst body: PairingCodeDto = { enabled: false };\n\t\t\tres.json(body);\n\t\t\treturn;\n\t\t}\n\t\tconst body: PairingCodeDto = { enabled: true, ...auth.currentPairingCode() };\n\t\tres.json(body);\n\t});\n\n\tapp.post(\"/api/pair\", (req: AuthedRequest, res) => {\n\t\tconst pin = typeof req.body?.pin === \"string\" ? req.body.pin : \"\";\n\t\tauth\n\t\t\t.pair(\n\t\t\t\t{\n\t\t\t\t\tremoteAddress: req.socket.remoteAddress,\n\t\t\t\t\thostHeader: req.headers.host,\n\t\t\t\t\toriginHeader: req.headers.origin,\n\t\t\t\t\tdeviceToken: undefined,\n\t\t\t\t},\n\t\t\t\tpin,\n\t\t\t)\n\t\t\t.then(({ token, device }) => {\n\t\t\t\tlog(`paired device ${device.id} (${device.identity})`);\n\t\t\t\tres.cookie(DEVICE_COOKIE, token, {\n\t\t\t\t\thttpOnly: true,\n\t\t\t\t\tsameSite: \"strict\",\n\t\t\t\t\tsecure: false, // Tailscale already encrypts; the dashboard serves plain HTTP on the tailnet.\n\t\t\t\t\texpires: new Date(device.expiresAt),\n\t\t\t\t}).json({ device });\n\t\t\t})\n\t\t\t.catch((err) => {\n\t\t\t\tconst status = typeof err?.status === \"number\" ? err.status : 500;\n\t\t\t\tlog(`pairing failed: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t\tres.status(status).json({ error: err instanceof Error ? err.message : String(err) });\n\t\t\t});\n\t});\n\n\tapp.get(\"/api/pairing-settings\", (_req, res) => {\n\t\tauth\n\t\t\t.getPairingSettings()\n\t\t\t.then((settings) => res.json(settings))\n\t\t\t.catch((err) => res.status(500).json({ error: String(err?.message ?? err) }));\n\t});\n\n\tapp.put(\"/api/pairing-settings\", (req, res) => {\n\t\tconst pairingTtlDays = req.body?.pairingTtlDays;\n\t\tif (typeof pairingTtlDays !== \"number\") {\n\t\t\tres.status(400).json({ error: \"pairingTtlDays must be a number\" });\n\t\t\treturn;\n\t\t}\n\t\tauth\n\t\t\t.setPairingSettings(pairingTtlDays)\n\t\t\t.then((settings) => res.json(settings))\n\t\t\t.catch((err) => {\n\t\t\t\tconst status = typeof err?.status === \"number\" ? err.status : 500;\n\t\t\t\tres.status(status).json({ error: String(err?.message ?? err) });\n\t\t\t});\n\t});\n\n\tapp.get(\"/api/devices\", (_req, res) => {\n\t\tauth\n\t\t\t.listDevices()\n\t\t\t.then((devices) => res.json({ devices }))\n\t\t\t.catch((err) => res.status(500).json({ error: String(err?.message ?? err) }));\n\t});\n\n\tapp.delete(\"/api/devices/:id\", (req, res) => {\n\t\tauth\n\t\t\t.unpair(req.params.id)\n\t\t\t.then((removed) => {\n\t\t\t\tif (!removed) {\n\t\t\t\t\tres.status(404).json({ error: `No paired device with id ${String(req.params.id)}` });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tlog(`unpaired device ${String(req.params.id)}`);\n\t\t\t\tres.json({ ok: true });\n\t\t\t})\n\t\t\t.catch((err) => res.status(500).json({ error: String(err?.message ?? err) }));\n\t});\n\n\t// -- events (SSE) ----------------------------------------------------------\n\tapp.get(\"/api/events\", (req: AuthedRequest, res) => {\n\t\tconst connectionId = req.sseConnectionId ?? randomUUID();\n\t\tconst diagnostic = (kind: string, metadata: object = {}) =>\n\t\t\tlog(`sse ${JSON.stringify({ connectionId, kind, ...metadata })}`);\n\t\tres.writeHead(200, {\n\t\t\t\"content-type\": \"text/event-stream\",\n\t\t\t\"cache-control\": \"no-cache\",\n\t\t\tconnection: \"keep-alive\",\n\t\t});\n\n\t\tconst guardedWrite = (\n\t\t\tchunk: string,\n\t\t\tmetadata: SseWriteMetadata | { kind: \"handshake\" | \"heartbeat\" | \"connection\" },\n\t\t): boolean => {\n\t\t\tif (res.destroyed || res.writableEnded) {\n\t\t\t\tdiagnostic(\"write_closed\", { writeKind: metadata.kind });\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst accepted = res.write(chunk);\n\t\t\tconst details = {\n\t\t\t\twriteKind: metadata.kind,\n\t\t\t\t...(\"seq\" in metadata\n\t\t\t\t\t? { seq: metadata.seq, type: metadata.type, frameBytes: metadata.frameBytes, reason: metadata.reason }\n\t\t\t\t\t: {}),\n\t\t\t\twritableLength: res.writableLength,\n\t\t\t};\n\t\t\tdiagnostic(\"write\", details);\n\t\t\tif (!accepted && res.writableLength > MAX_SSE_BUFFERED_BYTES) {\n\t\t\t\tdiagnostic(\"backpressure\", details);\n\t\t\t\tres.destroy();\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn true;\n\t\t};\n\n\t\tconst lastIdRaw = req.headers[\"last-event-id\"] ?? req.query.lastEventId;\n\t\tconst lastEventId =\n\t\t\ttypeof lastIdRaw === \"string\" && /^\\d+$/.test(lastIdRaw) ? Number.parseInt(lastIdRaw, 10) : undefined;\n\t\tdiagnostic(\"connect\", { cursor: lastEventId });\n\t\tif (!guardedWrite(\":ok\\n\\n\", { kind: \"handshake\" })) return;\n\t\t// Unnumbered connection metadata lets a browser correlate optional,\n\t\t// payload-free diagnostics without mutating its application SSE cursor.\n\t\tconst issuedAt = Date.now();\n\t\tfor (const [id, record] of diagnosticConnections) {\n\t\t\tif (issuedAt - record.issuedAt > CLIENT_DIAGNOSTIC_CONNECTION_TTL_MS) diagnosticConnections.delete(id);\n\t\t}\n\t\tdiagnosticConnections.set(connectionId, { issuedAt });\n\t\tif (!guardedWrite(`event: connection\\ndata: ${JSON.stringify({ connectionId })}\\n\\n`, { kind: \"connection\" }))\n\t\t\treturn;\n\t\tlet detach = () => {};\n\t\tlet keepAlive: ReturnType<typeof setInterval> | undefined;\n\t\tconst stop = () => {\n\t\t\tif (keepAlive) clearInterval(keepAlive);\n\t\t\tdetach();\n\t\t};\n\t\tlet usable = true;\n\t\tdetach = hub.attach(\n\t\t\t{\n\t\t\t\twrite: (chunk, metadata) => {\n\t\t\t\t\tif (!metadata) return false;\n\t\t\t\t\tusable = guardedWrite(chunk, metadata);\n\t\t\t\t\treturn usable;\n\t\t\t\t},\n\t\t\t},\n\t\t\tlastEventId,\n\t\t\t(replay) => diagnostic(replay.kind, replay),\n\t\t);\n\t\t// A rejected/destroyed replay must not leave a timer or live client behind.\n\t\tif (!usable) return;\n\t\t// Named heartbeats are visible to EventSource but have no id, so they do\n\t\t// not alter the application cursor or consume replay history.\n\t\tkeepAlive = setInterval(() => {\n\t\t\tif (!guardedWrite(formatHeartbeatFrame(), { kind: \"heartbeat\" })) stop();\n\t\t}, options.heartbeatIntervalMs ?? 25_000);\n\t\treq.on(\"close\", () => {\n\t\t\tdiagnostic(\"close\", { writableLength: res.writableLength });\n\t\t\tstop();\n\t\t});\n\t});\n\n\t// -- optional client stream diagnostics -----------------------------------\n\tapp.post(\"/api/events/diagnostic\", (req, res) => {\n\t\tconst declaredLength = Number(req.headers[\"content-length\"] ?? 0);\n\t\tconst encodedBytes = Buffer.byteLength(JSON.stringify(req.body ?? null));\n\t\tif (declaredLength > MAX_CLIENT_DIAGNOSTIC_BYTES || encodedBytes > MAX_CLIENT_DIAGNOSTIC_BYTES) {\n\t\t\tres.status(413).json({ error: \"Diagnostic summary exceeds the 4 KiB limit\" });\n\t\t\treturn;\n\t\t}\n\t\tif (!isClientDiagnostic(req.body)) {\n\t\t\tres.status(400).json({ error: \"Invalid diagnostic summary\" });\n\t\t\treturn;\n\t\t}\n\t\tconst now = Date.now();\n\t\tfor (const [id, record] of diagnosticConnections) {\n\t\t\tif (now - record.issuedAt > CLIENT_DIAGNOSTIC_CONNECTION_TTL_MS) diagnosticConnections.delete(id);\n\t\t}\n\t\tconst record = diagnosticConnections.get(req.body.connectionId);\n\t\tif (!record) {\n\t\t\tres.status(400).json({ error: \"Unknown or expired SSE connection\" });\n\t\t\treturn;\n\t\t}\n\t\tif (record.lastAt !== undefined && now - record.lastAt < CLIENT_DIAGNOSTIC_RATE_LIMIT_MS) {\n\t\t\tres.status(429).json({ error: \"Diagnostic summary rate limited\" });\n\t\t\treturn;\n\t\t}\n\t\trecord.lastAt = now;\n\t\t// Never log the request body wholesale. The schema is intentionally only\n\t\t// connection metadata, and this explicit projection prevents future fields\n\t\t// from accidentally turning diagnostics into a payload side-channel.\n\t\tlog(\n\t\t\t`sse ${JSON.stringify({\n\t\t\t\tconnectionId: req.body.connectionId,\n\t\t\t\tkind: \"client_diagnostic\",\n\t\t\t\tstate: req.body.state,\n\t\t\t\tpreviousState: req.body.previousState,\n\t\t\t\tattempt: req.body.attempt,\n\t\t\t\tdelayMs: req.body.delayMs,\n\t\t\t\tvisibility: req.body.visibility,\n\t\t\t\tlastAppliedSeq: req.body.lastAppliedSeq,\n\t\t\t\theartbeatAgeMs: req.body.heartbeatAgeMs,\n\t\t\t\teventCount: req.body.eventCount,\n\t\t\t\teventRatePerMinute: req.body.eventRatePerMinute,\n\t\t\t\tprocessingLagTotalMs: req.body.processingLagTotalMs,\n\t\t\t\tprocessingLagMaxMs: req.body.processingLagMaxMs,\n\t\t\t})}`,\n\t\t);\n\t\tres.json({ ok: true });\n\t});\n\n\t// -- fleet -----------------------------------------------------------------\n\tconst listDiskSessions = async (): Promise<SessionInfoDto[]> =>\n\t\t(await options.listAllSessions()).filter((session) => existsSync(session.cwd)).map(toSessionInfoDto);\n\n\tconst currentCwdInventory = async (): Promise<string[]> => [\n\t\t...pool.list().map((handle) => handle.cwd),\n\t\t...(await listDiskSessions()).map((session) => session.cwd),\n\t];\n\n\tconst handleMemoryError = (res: Response, err: unknown): void => {\n\t\tconst status =\n\t\t\ttypeof (err as { status?: unknown })?.status === \"number\" ? (err as { status: number }).status : 500;\n\t\tres.status(status).json({ error: err instanceof Error ? err.message : String(err) });\n\t};\n\n\tapp.get(\"/api/memories/scopes\", (_req, res) => {\n\t\tcurrentCwdInventory()\n\t\t\t.then((inventory) => memories.scopes(inventory))\n\t\t\t.then((scopes) => res.json({ scopes }))\n\t\t\t.catch((err) => handleMemoryError(res, err));\n\t});\n\n\tapp.get(\"/api/memories/:scopeId\", (req, res) => {\n\t\tcurrentCwdInventory()\n\t\t\t.then((inventory) => memories.listing(req.params.scopeId, inventory))\n\t\t\t.then((listing) => res.json(listing))\n\t\t\t.catch((err) => handleMemoryError(res, err));\n\t});\n\n\tapp.get(\"/api/memories/:scopeId/documents/:file\", (req, res) => {\n\t\tcurrentCwdInventory()\n\t\t\t.then((inventory) => memories.readDocument(req.params.scopeId, req.params.file, inventory))\n\t\t\t.then((document) => res.json(document))\n\t\t\t.catch((err) => handleMemoryError(res, err));\n\t});\n\n\tapp.put(\"/api/memories/:scopeId/documents/:file\", (req, res) => {\n\t\tcurrentCwdInventory()\n\t\t\t.then((inventory) => memories.saveDocument(req.params.scopeId, req.params.file, req.body, inventory))\n\t\t\t.then((result) => res.json(result))\n\t\t\t.catch((err) => handleMemoryError(res, err));\n\t});\n\n\tapp.delete(\"/api/memories/:scopeId/entries/:file\", (req, res) => {\n\t\tcurrentCwdInventory()\n\t\t\t.then((inventory) => memories.deleteEntry(req.params.scopeId, req.params.file, req.body, inventory))\n\t\t\t.then((result) => res.json(result))\n\t\t\t.catch((err) => handleMemoryError(res, err));\n\t});\n\n\tconst getFleet = async (): Promise<FleetDto> => {\n\t\tconst runtimes = await Promise.all(pool.list().map((h) => pool.describe(h)));\n\t\treturn { runtimes, diskSessions: await listDiskSessions() };\n\t};\n\n\t/** Map the one-RPC parent snapshot consistently for recovery and drill-in hydration. */\n\tconst toRuntimeHydration = (snapshot: DashboardRuntimeSnapshot): RuntimeHydrationDto => ({\n\t\tkey: snapshot.key,\n\t\tstate: snapshot.snapshot.state,\n\t\tmessages: images.project(snapshot.snapshot.messages, { runtimeKey: snapshot.key }),\n\t\tbackgroundAgents: snapshot.snapshot.backgroundAgents,\n\t\tpendingExtensionUiRequests: snapshot.snapshot.pendingExtensionUiRequests ?? [],\n\t\tbarrierSeq: snapshot.barrierSeq,\n\t});\n\n\tapp.get(\"/api/fleet\", (_req, res) => {\n\t\tconst startedAt = Date.now();\n\t\tgetFleet()\n\t\t\t.then((fleet) => {\n\t\t\t\t// Serialize once so the diagnostic reports the exact JSON response size\n\t\t\t\t// without retaining or logging any fleet payload fields.\n\t\t\t\tconst body = JSON.stringify(fleet);\n\t\t\t\tconst diagnostic = {\n\t\t\t\t\telapsedMs: Date.now() - startedAt,\n\t\t\t\t\tencodedBytes: Buffer.byteLength(body),\n\t\t\t\t\truntimeCount: fleet.runtimes.length,\n\t\t\t\t\tdiskSessionCount: fleet.diskSessions.length,\n\t\t\t\t};\n\t\t\t\tres.type(\"json\").send(body);\n\t\t\t\tlog(`fleet ${JSON.stringify(diagnostic)}`);\n\t\t\t})\n\t\t\t.catch((err) => res.status(500).json({ error: String(err?.message ?? err) }));\n\t});\n\n\t/** On-disk inventory only; does not query or describe live runtimes. */\n\tapp.get(\"/api/sessions\", (_req, res) => {\n\t\tlistDiskSessions()\n\t\t\t.then((sessions) => {\n\t\t\t\tconst body: SessionInventoryDto = { sessions };\n\t\t\t\tres.json(body);\n\t\t\t})\n\t\t\t.catch((err) => res.status(500).json({ error: String(err?.message ?? err) }));\n\t});\n\n\t/**\n\t * Full recovery snapshot. For an active runtime, its RPC marker captures the\n\t * current EventHub sequence before the response; later publications have a\n\t * higher sequence. This is an ordering contract, not a timing heuristic.\n\t */\n\tapp.get(\"/api/resync\", (req, res) => {\n\t\t(async () => {\n\t\t\tconst activeKey = typeof req.query.key === \"string\" ? req.query.key : undefined;\n\t\t\tconst activeAgentId = typeof req.query.agentId === \"string\" ? req.query.agentId : undefined;\n\t\t\tlet active: DashboardResyncDto[\"active\"];\n\t\t\tlet barrierSeq: number;\n\t\t\tif (activeKey) {\n\t\t\t\tconst handle = pool.get(activeKey);\n\t\t\t\tif (!handle) {\n\t\t\t\t\tconst body: DashboardResyncDto = { fleet: await getFleet(), barrierSeq: hub.currentSequence };\n\t\t\t\t\tres.json(body);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// The disk transcript has its own sequence boundary because it is read\n\t\t\t\t// before the parent RPC snapshot. Relays between these two barriers must\n\t\t\t\t// be reapplied so a subagent delta cannot disappear during recovery.\n\t\t\t\tlet preBarrierSubagent: NonNullable<ActiveRuntimeSnapshotDto[\"subagent\"]> | undefined;\n\t\t\t\tif (activeAgentId) {\n\t\t\t\t\tconst agents = await handle.client.listBackgroundAgents();\n\t\t\t\t\tconst agent = agents.find((candidate) => candidate.agentId === activeAgentId);\n\t\t\t\t\tif (!agent) throw new Error(`No background agent ${activeAgentId} in this runtime`);\n\t\t\t\t\tconst messages = readSubagentMessages(agent);\n\t\t\t\t\tpreBarrierSubagent = {\n\t\t\t\t\t\tagentId: activeAgentId,\n\t\t\t\t\t\tagent,\n\t\t\t\t\t\tmessages: images.project(messages, { runtimeKey: activeKey, agentId: activeAgentId }),\n\t\t\t\t\t\tbarrierSeq: hub.currentSequence,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tconst snapshot = await pool.snapshotDashboard(handle);\n\t\t\t\tbarrierSeq = snapshot.barrierSeq;\n\t\t\t\tactive = {\n\t\t\t\t\t...toRuntimeHydration(snapshot),\n\t\t\t\t\t...(preBarrierSubagent ? { subagent: preBarrierSubagent } : {}),\n\t\t\t\t};\n\t\t\t} else {\n\t\t\t\tbarrierSeq = hub.currentSequence;\n\t\t\t}\n\t\t\tconst body: DashboardResyncDto = { fleet: await getFleet(), ...(active ? { active } : {}), barrierSeq };\n\t\t\tres.json(body);\n\t\t})().catch((err) => res.status(502).json({ error: String(err?.message ?? err) }));\n\t});\n\n\t// -- runtimes ---------------------------------------------------------------\n\tapp.post(\"/api/runtimes\", (req, res) => {\n\t\t(async () => {\n\t\t\tconst 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"]}
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server/server.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,OAAO,MAAM,SAAS,CAAC;AAkB9B,OAAO,KAAK,EAAgB,aAAa,EAAE,MAAM,WAAW,CAAC;AAC7D,OAAO,EAKN,qBAAqB,EACrB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,QAAQ,EAA+C,MAAM,gBAAgB,CAAC;AAIvF,OAAO,KAAK,EAA4B,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAG/E,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,OAAO,GAAG;IAClD,mFAAmF;IACnF,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAChC,CAAC;AAEF,MAAM,WAAW,0BAA0B;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,IAAI,CAAC;IACd,QAAQ,EAAE,IAAI,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,sBAAsB;IACtC,IAAI,EAAE,aAAa,CAAC;IACpB,IAAI,EAAE,WAAW,CAAC;IAClB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yEAAuE;IACvE,eAAe,EAAE,MAAM,OAAO,CAAC,0BAA0B,EAAE,CAAC,CAAC;IAC7D,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAClD,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,sGAAsG;IACtG,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2HAAyH;IACzH,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,2GAA2G;IAC3G,YAAY,CAAC,EAAE,qBAAqB,CAAC;IACrC,wDAAwD;IACxD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAGD,eAAO,MAAM,sBAAsB,QAAkB,CAAC;AACtD,eAAO,MAAM,+BAA+B,QAAS,CAAC;AAmEtD,oDAAoD;AACpD,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAQtF;AAQD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,GAAG,kBAAkB,CAkmCzF","sourcesContent":["/**\n * Dashboard HTTP server — Express app wiring auth, the runtime pool, the SSE\n * hub, and the file API into the REST surface the browser client consumes.\n *\n * Bind address discipline: local mode binds 127.0.0.1 only. The\n * caller decides the bind address; `createDashboardServer` never listens by\n * itself. Remote mode still passes every request through DashboardAuth.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport { existsSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { basename, join } from \"node:path\";\nimport type { NextFunction, Request, Response } from \"express\";\nimport express from \"express\";\nimport type {\n\tActiveRuntimeSnapshotDto,\n\tAuthStatusDto,\n\tClientConnectionDiagnosticDto,\n\tDashboardResyncDto,\n\tFleetDto,\n\tImageAttachmentDto,\n\tPairingCodeDto,\n\tRuntimeHydrationDto,\n\tSessionInfoDto,\n\tSessionInventoryDto,\n} from \"../shared/protocol.js\";\nimport {\n\tMAX_CLIENT_DIAGNOSTIC_BYTES,\n\tMAX_PROMPT_BODY_BYTES,\n\tMAX_SESSION_PREVIEW_CHARACTERS,\n} from \"../shared/protocol.js\";\nimport type { AuthDecision, DashboardAuth } from \"./auth.js\";\nimport {\n\tDASHBOARD_IMAGE_ID_PATTERN,\n\tDashboardImageNotFoundError,\n\tDashboardImagePreviewError,\n\ttype DashboardImageScope,\n\tDashboardImageService,\n} from \"./dashboard-images.js\";\nimport { EventHub, formatHeartbeatFrame, type SseWriteMetadata } from \"./event-hub.js\";\nimport { defaultPlaces, FileApi } from \"./files.js\";\nimport { ImagePreviewWorker } from \"./image-preview.js\";\nimport { MemoryApi } from \"./memories.js\";\nimport type { DashboardRuntimeSnapshot, RuntimePool } from \"./runtime-pool.js\";\nimport { readSubagentMessages, SubagentSessionLogNotFoundError } from \"./subagent-log.js\";\n\nexport type DashboardServerApp = express.Express & {\n\t/** Close dashboard-owned services. Safe to call more than once during shutdown. */\n\tcloseDashboard(): Promise<void>;\n};\n\nexport interface DashboardSessionInfoSource {\n\tpath: string;\n\tid: string;\n\tcwd: string;\n\tname?: string;\n\tcreated: Date;\n\tmodified: Date;\n\tmessageCount: number;\n\tfirstMessage: string;\n}\n\nexport interface DashboardServerOptions {\n\tauth: DashboardAuth;\n\tpool: RuntimePool;\n\t/** Directory of built client assets; omit to skip static serving (tests). */\n\tstaticDir?: string;\n\t/** Session listing (cross-project) — injected so tests can stub it. */\n\tlistAllSessions: () => Promise<DashboardSessionInfoSource[]>;\n\tdeleteSession: (path: string) => Promise<unknown>;\n\tlogger?: (line: string) => void;\n\t/** Build version of the running server process (for the settings footer / stale-server detection). */\n\tserverVersion?: string;\n\t/** Restart hook — when set, POST /api/server/restart invokes it (typically process exit for a supervisor to respawn). */\n\tonRestart?: () => void;\n\t/** Injectable only to make SSE limits deterministic in integration tests. */\n\teventHub?: EventHub;\n\t/** Injectable bounded image repository/preview service for deterministic tests and lifecycle ownership. */\n\timageService?: DashboardImageService;\n\t/** Named heartbeat interval; defaults to 25 seconds. */\n\theartbeatIntervalMs?: number;\n\t/** Test-only override for the global dreb memory home directory. */\n\tmemoryHomeDir?: string;\n}\n\nconst DEVICE_COOKIE = \"dreb_dashboard_device\";\nexport const MAX_SSE_BUFFERED_BYTES = 4 * 1024 * 1024;\nexport const CLIENT_DIAGNOSTIC_RATE_LIMIT_MS = 30_000;\nconst CLIENT_DIAGNOSTIC_CONNECTION_TTL_MS = 10 * 60_000;\n\nfunction boundedSessionPreview(text: string): string {\n\tlet preview = \"\";\n\tlet characters = 0;\n\tfor (const character of text) {\n\t\tif (characters === MAX_SESSION_PREVIEW_CHARACTERS) break;\n\t\tpreview += character;\n\t\tcharacters++;\n\t}\n\treturn preview;\n}\n\nfunction toSessionInfoDto(session: DashboardSessionInfoSource): SessionInfoDto {\n\treturn {\n\t\tpath: session.path,\n\t\tid: session.id,\n\t\tcwd: session.cwd,\n\t\tname: session.name,\n\t\tcreated: session.created.toISOString(),\n\t\tmodified: session.modified.toISOString(),\n\t\tmessageCount: session.messageCount,\n\t\tfirstMessage: boundedSessionPreview(session.firstMessage),\n\t};\n}\n\nfunction isClientDiagnostic(value: unknown): value is ClientConnectionDiagnosticDto {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) return false;\n\tconst body = value as Record<string, unknown>;\n\tconst allowed = new Set([\n\t\t\"connectionId\",\n\t\t\"state\",\n\t\t\"previousState\",\n\t\t\"attempt\",\n\t\t\"delayMs\",\n\t\t\"visibility\",\n\t\t\"lastAppliedSeq\",\n\t\t\"heartbeatAgeMs\",\n\t\t\"eventCount\",\n\t\t\"eventRatePerMinute\",\n\t\t\"processingLagTotalMs\",\n\t\t\"processingLagMaxMs\",\n\t]);\n\tif (Object.keys(body).some((key) => !allowed.has(key))) return false;\n\tconst states = new Set([\"connecting\", \"connected\", \"retrying\", \"resyncing\", \"disconnected\", \"auth_failed\"]);\n\tconst nonNegativeNumber = (item: unknown) => typeof item === \"number\" && Number.isFinite(item) && item >= 0;\n\tconst nonNegativeInteger = (item: unknown) => typeof item === \"number\" && Number.isSafeInteger(item) && item >= 0;\n\treturn (\n\t\ttypeof body.connectionId === \"string\" &&\n\t\t/^[0-9a-f-]{36}$/i.test(body.connectionId) &&\n\t\ttypeof body.state === \"string\" &&\n\t\tstates.has(body.state) &&\n\t\t(body.previousState === undefined ||\n\t\t\t(typeof body.previousState === \"string\" && states.has(body.previousState))) &&\n\t\tnonNegativeInteger(body.attempt) &&\n\t\tnonNegativeNumber(body.eventCount) &&\n\t\tnonNegativeNumber(body.eventRatePerMinute) &&\n\t\tnonNegativeNumber(body.processingLagTotalMs) &&\n\t\tnonNegativeNumber(body.processingLagMaxMs) &&\n\t\t(body.delayMs === undefined || nonNegativeNumber(body.delayMs)) &&\n\t\t(body.lastAppliedSeq === undefined || nonNegativeInteger(body.lastAppliedSeq)) &&\n\t\t(body.heartbeatAgeMs === undefined || nonNegativeNumber(body.heartbeatAgeMs)) &&\n\t\t(body.visibility === \"visible\" || body.visibility === \"hidden\")\n\t);\n}\n\n/** Parse the device cookie from a Cookie header. */\nexport function parseDeviceCookie(cookieHeader: string | undefined): string | undefined {\n\tif (!cookieHeader) return undefined;\n\tfor (const part of cookieHeader.split(\";\")) {\n\t\tconst eq = part.indexOf(\"=\");\n\t\tif (eq === -1) continue;\n\t\tif (part.slice(0, eq).trim() === DEVICE_COOKIE) return part.slice(eq + 1).trim();\n\t}\n\treturn undefined;\n}\n\ninterface AuthedRequest extends Request {\n\tauthDecision?: AuthDecision;\n\t/** Per-SSE-request opaque diagnostic correlation id. */\n\tsseConnectionId?: string;\n}\n\nexport function createDashboardServer(options: DashboardServerOptions): DashboardServerApp {\n\tconst { auth, pool } = options;\n\tconst serverStartedAt = new Date().toISOString();\n\tconst diagnosticConnections = new Map<string, { issuedAt: number; lastAt?: number }>();\n\tconst log = options.logger ?? ((line: string) => console.log(`[dashboard] ${line}`));\n\tconst files = new FileApi((op, path, detail) => log(`file ${op}: ${path}${detail ? ` (${detail})` : \"\"}`));\n\tconst memories = new MemoryApi(options.memoryHomeDir ?? homedir(), (op, scopeId, detail) =>\n\t\tlog(`memory ${op}: ${scopeId}${detail ? ` (${detail})` : \"\"}`),\n\t);\n\tconst hub = options.eventHub ?? new EventHub();\n\tconst images = options.imageService ?? new DashboardImageService(new ImagePreviewWorker());\n\thub.setEventProjector((key, event) => (key ? images.projectEvent(event, { runtimeKey: key }) : event));\n\tpool.onEvent((key, event) => {\n\t\tif (event.type === \"dashboard_snapshot_barrier\" && typeof event.snapshotId === \"string\") {\n\t\t\t// This RPC marker has no browser frame: its synchronous sequence capture\n\t\t\t// orders the HTTP snapshot before all later EventHub publications.\n\t\t\tpool.recordDashboardBarrier(key, event.snapshotId, hub.currentSequence);\n\t\t\treturn;\n\t\t}\n\t\thub.publish(key, event);\n\t\tif (event.type === \"runtime_removed\") images.removeRuntime(key);\n\t});\n\tpool.onFleetSnapshot((event) => hub.publish(\"\", { ...event }));\n\n\tconst app = express() as DashboardServerApp;\n\tlet closePromise: Promise<void> | undefined;\n\tapp.closeDashboard = () => {\n\t\tclosePromise ??= images.close();\n\t\treturn closePromise;\n\t};\n\tapp.disable(\"x-powered-by\");\n\n\t// -- auth middleware (every route, fail-closed) ---------------------------\n\tapp.use((req: AuthedRequest, res: Response, next: NextFunction) => {\n\t\tif (req.path === \"/api/events\") req.sseConnectionId = randomUUID();\n\t\tauth\n\t\t\t.authenticate({\n\t\t\t\tremoteAddress: req.socket.remoteAddress,\n\t\t\t\thostHeader: req.headers.host,\n\t\t\t\toriginHeader: req.headers.origin,\n\t\t\t\tdeviceToken: parseDeviceCookie(req.headers.cookie),\n\t\t\t})\n\t\t\t.then((decision) => {\n\t\t\t\treq.authDecision = decision;\n\t\t\t\tif (decision.allowed) return next();\n\t\t\t\tconst canRenderAuthScreen = decision.needsPairing || Boolean(decision.identity);\n\t\t\t\tif (canRenderAuthScreen) {\n\t\t\t\t\t// The auth/pairing endpoints must be reachable by allowed-but-unpaired\n\t\t\t\t\t// identities, and /api/auth must also be reachable by rejected\n\t\t\t\t\t// Tailscale identities so the SPA denial screen can name them.\n\t\t\t\t\tif (req.path === \"/api/auth\" || (decision.needsPairing && req.path === \"/api/pair\")) return next();\n\t\t\t\t\t// Let the SPA shell + static assets load so the client-side pairing or\n\t\t\t\t\t// denial screen can render. No data exposure: every /api/* data route\n\t\t\t\t\t// below stays fail-closed — only non-API GETs (the app shell) are allowed.\n\t\t\t\t\tif (req.method === \"GET\" && !req.path.startsWith(\"/api/\")) return next();\n\t\t\t\t}\n\t\t\t\tif (req.sseConnectionId) {\n\t\t\t\t\tlog(\n\t\t\t\t\t\t`sse ${JSON.stringify({\n\t\t\t\t\t\t\tconnectionId: req.sseConnectionId,\n\t\t\t\t\t\t\tkind: \"auth_denial\",\n\t\t\t\t\t\t\tmethod: req.method,\n\t\t\t\t\t\t\tpath: req.path,\n\t\t\t\t\t\t\tstatus: decision.status,\n\t\t\t\t\t\t})}`,\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\tlog(`denied ${req.method} ${req.path}: ${decision.reason}`);\n\t\t\t\t}\n\t\t\t\tres.status(decision.status).json({\n\t\t\t\t\terror: decision.reason,\n\t\t\t\t\tneedsPairing: decision.needsPairing ?? false,\n\t\t\t\t\tidentity: decision.identity?.loginName,\n\t\t\t\t});\n\t\t\t})\n\t\t\t.catch((err) => {\n\t\t\t\t// authenticate() already catches internally; this is belt-and-suspenders.\n\t\t\t\tlog(`auth middleware error — denying: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t\tres.status(500).json({ error: \"Auth subsystem error — denied\" });\n\t\t\t});\n\t});\n\n\t// Authenticate before consuming request bodies. Diagnostics have their own\n\t// small parser limit; the larger limit exists only for prompt image payloads.\n\tapp.use(\"/api/events/diagnostic\", express.json({ limit: MAX_CLIENT_DIAGNOSTIC_BYTES }));\n\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 && decision.mode === \"local\") {\n\t\t\tconst status: AuthStatusDto = { mode: \"local\" };\n\t\t\tres.json({ ...status, needsPairing: false });\n\t\t\treturn;\n\t\t}\n\t\tif (decision.allowed) {\n\t\t\tauth\n\t\t\t\t.claimPairingExpiryStatus(decision.pairing.id)\n\t\t\t\t.then((expiry) => {\n\t\t\t\t\tconst status: AuthStatusDto = {\n\t\t\t\t\t\tmode: \"remote\",\n\t\t\t\t\t\tidentity: decision.identity.loginName,\n\t\t\t\t\t\tdevice: decision.identity.device,\n\t\t\t\t\t\t...(expiry.warning ? { pairingExpiryWarning: expiry.warning } : {}),\n\t\t\t\t\t\t...(expiry.nextCheckAt ? { pairingExpiryCheckAt: expiry.nextCheckAt } : {}),\n\t\t\t\t\t};\n\t\t\t\t\tres.json({ ...status, needsPairing: false });\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tlog(`pairing expiry status failed: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t\t\tres.status(500).json({ error: \"Auth subsystem error — denied\", needsPairing: false });\n\t\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tres.status(decision.status).json({\n\t\t\terror: decision.reason,\n\t\t\tneedsPairing: decision.needsPairing ?? false,\n\t\t\tidentity: decision.identity?.loginName,\n\t\t});\n\t});\n\n\tapp.get(\"/api/pairing-code\", (req: AuthedRequest, res) => {\n\t\tconst decision = req.authDecision!;\n\t\tif (!decision.allowed || decision.mode !== \"local\") {\n\t\t\tres.status(403).json({ error: \"Pairing code is only available from the host machine\" });\n\t\t\treturn;\n\t\t}\n\t\tif (!auth.isRemoteEnabled) {\n\t\t\tconst body: PairingCodeDto = { enabled: false };\n\t\t\tres.json(body);\n\t\t\treturn;\n\t\t}\n\t\tconst body: PairingCodeDto = { enabled: true, ...auth.currentPairingCode() };\n\t\tres.json(body);\n\t});\n\n\tapp.post(\"/api/pair\", (req: AuthedRequest, res) => {\n\t\tconst pin = typeof req.body?.pin === \"string\" ? req.body.pin : \"\";\n\t\tauth\n\t\t\t.pair(\n\t\t\t\t{\n\t\t\t\t\tremoteAddress: req.socket.remoteAddress,\n\t\t\t\t\thostHeader: req.headers.host,\n\t\t\t\t\toriginHeader: req.headers.origin,\n\t\t\t\t\tdeviceToken: undefined,\n\t\t\t\t},\n\t\t\t\tpin,\n\t\t\t)\n\t\t\t.then(({ token, device }) => {\n\t\t\t\tlog(`paired device ${device.id} (${device.identity})`);\n\t\t\t\tres.cookie(DEVICE_COOKIE, token, {\n\t\t\t\t\thttpOnly: true,\n\t\t\t\t\tsameSite: \"strict\",\n\t\t\t\t\tsecure: false, // Tailscale already encrypts; the dashboard serves plain HTTP on the tailnet.\n\t\t\t\t\texpires: new Date(device.expiresAt),\n\t\t\t\t}).json({ device });\n\t\t\t})\n\t\t\t.catch((err) => {\n\t\t\t\tconst status = typeof err?.status === \"number\" ? err.status : 500;\n\t\t\t\tlog(`pairing failed: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t\tres.status(status).json({ error: err instanceof Error ? err.message : String(err) });\n\t\t\t});\n\t});\n\n\tapp.get(\"/api/pairing-settings\", (_req, res) => {\n\t\tauth\n\t\t\t.getPairingSettings()\n\t\t\t.then((settings) => res.json(settings))\n\t\t\t.catch((err) => res.status(500).json({ error: String(err?.message ?? err) }));\n\t});\n\n\tapp.put(\"/api/pairing-settings\", (req, res) => {\n\t\tconst pairingTtlDays = req.body?.pairingTtlDays;\n\t\tif (typeof pairingTtlDays !== \"number\") {\n\t\t\tres.status(400).json({ error: \"pairingTtlDays must be a number\" });\n\t\t\treturn;\n\t\t}\n\t\tauth\n\t\t\t.setPairingSettings(pairingTtlDays)\n\t\t\t.then((settings) => res.json(settings))\n\t\t\t.catch((err) => {\n\t\t\t\tconst status = typeof err?.status === \"number\" ? err.status : 500;\n\t\t\t\tres.status(status).json({ error: String(err?.message ?? err) });\n\t\t\t});\n\t});\n\n\tapp.get(\"/api/devices\", (_req, res) => {\n\t\tauth\n\t\t\t.listDevices()\n\t\t\t.then((devices) => res.json({ devices }))\n\t\t\t.catch((err) => res.status(500).json({ error: String(err?.message ?? err) }));\n\t});\n\n\tapp.delete(\"/api/devices/:id\", (req, res) => {\n\t\tauth\n\t\t\t.unpair(req.params.id)\n\t\t\t.then((removed) => {\n\t\t\t\tif (!removed) {\n\t\t\t\t\tres.status(404).json({ error: `No paired device with id ${String(req.params.id)}` });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tlog(`unpaired device ${String(req.params.id)}`);\n\t\t\t\tres.json({ ok: true });\n\t\t\t})\n\t\t\t.catch((err) => res.status(500).json({ error: String(err?.message ?? err) }));\n\t});\n\n\t// -- events (SSE) ----------------------------------------------------------\n\tapp.get(\"/api/events\", (req: AuthedRequest, res) => {\n\t\tconst connectionId = req.sseConnectionId ?? randomUUID();\n\t\tconst diagnostic = (kind: string, metadata: object = {}) =>\n\t\t\tlog(`sse ${JSON.stringify({ connectionId, kind, ...metadata })}`);\n\t\tres.writeHead(200, {\n\t\t\t\"content-type\": \"text/event-stream\",\n\t\t\t\"cache-control\": \"no-cache\",\n\t\t\tconnection: \"keep-alive\",\n\t\t});\n\n\t\tconst guardedWrite = (\n\t\t\tchunk: string,\n\t\t\tmetadata: SseWriteMetadata | { kind: \"handshake\" | \"heartbeat\" | \"connection\" },\n\t\t): boolean => {\n\t\t\tif (res.destroyed || res.writableEnded) {\n\t\t\t\tdiagnostic(\"write_closed\", { writeKind: metadata.kind });\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst accepted = res.write(chunk);\n\t\t\tconst details = {\n\t\t\t\twriteKind: metadata.kind,\n\t\t\t\t...(\"seq\" in metadata\n\t\t\t\t\t? { seq: metadata.seq, type: metadata.type, frameBytes: metadata.frameBytes, reason: metadata.reason }\n\t\t\t\t\t: {}),\n\t\t\t\twritableLength: res.writableLength,\n\t\t\t};\n\t\t\tdiagnostic(\"write\", details);\n\t\t\tif (!accepted && res.writableLength > MAX_SSE_BUFFERED_BYTES) {\n\t\t\t\tdiagnostic(\"backpressure\", details);\n\t\t\t\tres.destroy();\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn true;\n\t\t};\n\n\t\tconst lastIdRaw = req.headers[\"last-event-id\"] ?? req.query.lastEventId;\n\t\tconst lastEventId =\n\t\t\ttypeof lastIdRaw === \"string\" && /^\\d+$/.test(lastIdRaw) ? Number.parseInt(lastIdRaw, 10) : undefined;\n\t\tdiagnostic(\"connect\", { cursor: lastEventId });\n\t\tif (!guardedWrite(\":ok\\n\\n\", { kind: \"handshake\" })) return;\n\t\t// Unnumbered connection metadata lets a browser correlate optional,\n\t\t// payload-free diagnostics without mutating its application SSE cursor.\n\t\tconst issuedAt = Date.now();\n\t\tfor (const [id, record] of diagnosticConnections) {\n\t\t\tif (issuedAt - record.issuedAt > CLIENT_DIAGNOSTIC_CONNECTION_TTL_MS) diagnosticConnections.delete(id);\n\t\t}\n\t\tdiagnosticConnections.set(connectionId, { issuedAt });\n\t\tif (!guardedWrite(`event: connection\\ndata: ${JSON.stringify({ connectionId })}\\n\\n`, { kind: \"connection\" }))\n\t\t\treturn;\n\t\tlet detach = () => {};\n\t\tlet keepAlive: ReturnType<typeof setInterval> | undefined;\n\t\tconst stop = () => {\n\t\t\tif (keepAlive) clearInterval(keepAlive);\n\t\t\tdetach();\n\t\t};\n\t\tlet usable = true;\n\t\tdetach = hub.attach(\n\t\t\t{\n\t\t\t\twrite: (chunk, metadata) => {\n\t\t\t\t\tif (!metadata) return false;\n\t\t\t\t\tusable = guardedWrite(chunk, metadata);\n\t\t\t\t\treturn usable;\n\t\t\t\t},\n\t\t\t},\n\t\t\tlastEventId,\n\t\t\t(replay) => diagnostic(replay.kind, replay),\n\t\t);\n\t\t// A rejected/destroyed replay must not leave a timer or live client behind.\n\t\tif (!usable) return;\n\t\t// Named heartbeats are visible to EventSource but have no id, so they do\n\t\t// not alter the application cursor or consume replay history.\n\t\tkeepAlive = setInterval(() => {\n\t\t\tif (!guardedWrite(formatHeartbeatFrame(), { kind: \"heartbeat\" })) stop();\n\t\t}, options.heartbeatIntervalMs ?? 25_000);\n\t\treq.on(\"close\", () => {\n\t\t\tdiagnostic(\"close\", { writableLength: res.writableLength });\n\t\t\tstop();\n\t\t});\n\t});\n\n\t// -- optional client stream diagnostics -----------------------------------\n\tapp.post(\"/api/events/diagnostic\", (req, res) => {\n\t\tconst declaredLength = Number(req.headers[\"content-length\"] ?? 0);\n\t\tconst encodedBytes = Buffer.byteLength(JSON.stringify(req.body ?? null));\n\t\tif (declaredLength > MAX_CLIENT_DIAGNOSTIC_BYTES || encodedBytes > MAX_CLIENT_DIAGNOSTIC_BYTES) {\n\t\t\tres.status(413).json({ error: \"Diagnostic summary exceeds the 4 KiB limit\" });\n\t\t\treturn;\n\t\t}\n\t\tif (!isClientDiagnostic(req.body)) {\n\t\t\tres.status(400).json({ error: \"Invalid diagnostic summary\" });\n\t\t\treturn;\n\t\t}\n\t\tconst now = Date.now();\n\t\tfor (const [id, record] of diagnosticConnections) {\n\t\t\tif (now - record.issuedAt > CLIENT_DIAGNOSTIC_CONNECTION_TTL_MS) diagnosticConnections.delete(id);\n\t\t}\n\t\tconst record = diagnosticConnections.get(req.body.connectionId);\n\t\tif (!record) {\n\t\t\tres.status(400).json({ error: \"Unknown or expired SSE connection\" });\n\t\t\treturn;\n\t\t}\n\t\tif (record.lastAt !== undefined && now - record.lastAt < CLIENT_DIAGNOSTIC_RATE_LIMIT_MS) {\n\t\t\tres.status(429).json({ error: \"Diagnostic summary rate limited\" });\n\t\t\treturn;\n\t\t}\n\t\trecord.lastAt = now;\n\t\t// Never log the request body wholesale. The schema is intentionally only\n\t\t// connection metadata, and this explicit projection prevents future fields\n\t\t// from accidentally turning diagnostics into a payload side-channel.\n\t\tlog(\n\t\t\t`sse ${JSON.stringify({\n\t\t\t\tconnectionId: req.body.connectionId,\n\t\t\t\tkind: \"client_diagnostic\",\n\t\t\t\tstate: req.body.state,\n\t\t\t\tpreviousState: req.body.previousState,\n\t\t\t\tattempt: req.body.attempt,\n\t\t\t\tdelayMs: req.body.delayMs,\n\t\t\t\tvisibility: req.body.visibility,\n\t\t\t\tlastAppliedSeq: req.body.lastAppliedSeq,\n\t\t\t\theartbeatAgeMs: req.body.heartbeatAgeMs,\n\t\t\t\teventCount: req.body.eventCount,\n\t\t\t\teventRatePerMinute: req.body.eventRatePerMinute,\n\t\t\t\tprocessingLagTotalMs: req.body.processingLagTotalMs,\n\t\t\t\tprocessingLagMaxMs: req.body.processingLagMaxMs,\n\t\t\t})}`,\n\t\t);\n\t\tres.json({ ok: true });\n\t});\n\n\t// -- fleet -----------------------------------------------------------------\n\tconst listDiskSessions = async (): Promise<SessionInfoDto[]> =>\n\t\t(await options.listAllSessions()).filter((session) => existsSync(session.cwd)).map(toSessionInfoDto);\n\n\tconst currentCwdInventory = async (): Promise<string[]> => [\n\t\t...pool.list().map((handle) => handle.cwd),\n\t\t...(await listDiskSessions()).map((session) => session.cwd),\n\t];\n\n\tconst handleMemoryError = (res: Response, err: unknown): void => {\n\t\tconst status =\n\t\t\ttypeof (err as { status?: unknown })?.status === \"number\" ? (err as { status: number }).status : 500;\n\t\tres.status(status).json({ error: err instanceof Error ? err.message : String(err) });\n\t};\n\n\tapp.get(\"/api/memories/scopes\", (_req, res) => {\n\t\tcurrentCwdInventory()\n\t\t\t.then((inventory) => memories.scopes(inventory))\n\t\t\t.then((scopes) => res.json({ scopes }))\n\t\t\t.catch((err) => handleMemoryError(res, err));\n\t});\n\n\tapp.get(\"/api/memories/:scopeId\", (req, res) => {\n\t\tcurrentCwdInventory()\n\t\t\t.then((inventory) => memories.listing(req.params.scopeId, inventory))\n\t\t\t.then((listing) => res.json(listing))\n\t\t\t.catch((err) => handleMemoryError(res, err));\n\t});\n\n\tapp.get(\"/api/memories/:scopeId/documents/:file\", (req, res) => {\n\t\tcurrentCwdInventory()\n\t\t\t.then((inventory) => memories.readDocument(req.params.scopeId, req.params.file, inventory))\n\t\t\t.then((document) => res.json(document))\n\t\t\t.catch((err) => handleMemoryError(res, err));\n\t});\n\n\tapp.put(\"/api/memories/:scopeId/documents/:file\", (req, res) => {\n\t\tcurrentCwdInventory()\n\t\t\t.then((inventory) => memories.saveDocument(req.params.scopeId, req.params.file, req.body, inventory))\n\t\t\t.then((result) => res.json(result))\n\t\t\t.catch((err) => handleMemoryError(res, err));\n\t});\n\n\tapp.delete(\"/api/memories/:scopeId/entries/:file\", (req, res) => {\n\t\tcurrentCwdInventory()\n\t\t\t.then((inventory) => memories.deleteEntry(req.params.scopeId, req.params.file, req.body, inventory))\n\t\t\t.then((result) => res.json(result))\n\t\t\t.catch((err) => handleMemoryError(res, err));\n\t});\n\n\tconst getFleet = async (): Promise<FleetDto> => {\n\t\tconst runtimes = await Promise.all(pool.list().map((h) => pool.describe(h)));\n\t\treturn { runtimes, diskSessions: await listDiskSessions() };\n\t};\n\n\t/** Map the one-RPC parent snapshot consistently for recovery and drill-in hydration. */\n\tconst toRuntimeHydration = (snapshot: DashboardRuntimeSnapshot): RuntimeHydrationDto => ({\n\t\tkey: snapshot.key,\n\t\tstate: snapshot.snapshot.state,\n\t\tmessages: images.project(snapshot.snapshot.messages, { runtimeKey: snapshot.key }),\n\t\tbackgroundAgents: snapshot.snapshot.backgroundAgents,\n\t\tpendingExtensionUiRequests: snapshot.snapshot.pendingExtensionUiRequests ?? [],\n\t\tbarrierSeq: snapshot.barrierSeq,\n\t});\n\n\tapp.get(\"/api/fleet\", (_req, res) => {\n\t\tconst startedAt = Date.now();\n\t\tgetFleet()\n\t\t\t.then((fleet) => {\n\t\t\t\t// Serialize once so the diagnostic reports the exact JSON response size\n\t\t\t\t// without retaining or logging any fleet payload fields.\n\t\t\t\tconst body = JSON.stringify(fleet);\n\t\t\t\tconst diagnostic = {\n\t\t\t\t\telapsedMs: Date.now() - startedAt,\n\t\t\t\t\tencodedBytes: Buffer.byteLength(body),\n\t\t\t\t\truntimeCount: fleet.runtimes.length,\n\t\t\t\t\tdiskSessionCount: fleet.diskSessions.length,\n\t\t\t\t};\n\t\t\t\tres.type(\"json\").send(body);\n\t\t\t\tlog(`fleet ${JSON.stringify(diagnostic)}`);\n\t\t\t})\n\t\t\t.catch((err) => res.status(500).json({ error: String(err?.message ?? err) }));\n\t});\n\n\t/** On-disk inventory only; does not query or describe live runtimes. */\n\tapp.get(\"/api/sessions\", (_req, res) => {\n\t\tlistDiskSessions()\n\t\t\t.then((sessions) => {\n\t\t\t\tconst body: SessionInventoryDto = { sessions };\n\t\t\t\tres.json(body);\n\t\t\t})\n\t\t\t.catch((err) => res.status(500).json({ error: String(err?.message ?? err) }));\n\t});\n\n\t/**\n\t * Full recovery snapshot. For an active runtime, its RPC marker captures the\n\t * current EventHub sequence before the response; later publications have a\n\t * higher sequence. This is an ordering contract, not a timing heuristic.\n\t */\n\tapp.get(\"/api/resync\", (req, res) => {\n\t\t(async () => {\n\t\t\tconst activeKey = typeof req.query.key === \"string\" ? req.query.key : undefined;\n\t\t\tconst activeAgentId = typeof req.query.agentId === \"string\" ? req.query.agentId : undefined;\n\t\t\tlet active: DashboardResyncDto[\"active\"];\n\t\t\tlet barrierSeq: number;\n\t\t\tif (activeKey) {\n\t\t\t\tconst handle = pool.get(activeKey);\n\t\t\t\tif (!handle) {\n\t\t\t\t\tconst body: DashboardResyncDto = { fleet: await getFleet(), barrierSeq: hub.currentSequence };\n\t\t\t\t\tres.json(body);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// The disk transcript has its own sequence boundary because it is read\n\t\t\t\t// before the parent RPC snapshot. Relays between these two barriers must\n\t\t\t\t// be reapplied so a subagent delta cannot disappear during recovery.\n\t\t\t\tlet preBarrierSubagent: NonNullable<ActiveRuntimeSnapshotDto[\"subagent\"]> | undefined;\n\t\t\t\tif (activeAgentId) {\n\t\t\t\t\tconst agents = await handle.client.listBackgroundAgents();\n\t\t\t\t\tconst agent = agents.find((candidate) => candidate.agentId === activeAgentId);\n\t\t\t\t\tif (!agent) throw new Error(`No background agent ${activeAgentId} in this runtime`);\n\t\t\t\t\tconst messages = readSubagentMessages(agent);\n\t\t\t\t\tpreBarrierSubagent = {\n\t\t\t\t\t\tagentId: activeAgentId,\n\t\t\t\t\t\tagent,\n\t\t\t\t\t\tmessages: images.project(messages, { runtimeKey: activeKey, agentId: activeAgentId }),\n\t\t\t\t\t\tbarrierSeq: hub.currentSequence,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tconst snapshot = await pool.snapshotDashboard(handle);\n\t\t\t\tbarrierSeq = snapshot.barrierSeq;\n\t\t\t\tactive = {\n\t\t\t\t\t...toRuntimeHydration(snapshot),\n\t\t\t\t\t...(preBarrierSubagent ? { subagent: preBarrierSubagent } : {}),\n\t\t\t\t};\n\t\t\t} else {\n\t\t\t\tbarrierSeq = hub.currentSequence;\n\t\t\t}\n\t\t\tconst body: DashboardResyncDto = { fleet: await getFleet(), ...(active ? { active } : {}), barrierSeq };\n\t\t\tres.json(body);\n\t\t})().catch((err) => res.status(502).json({ error: String(err?.message ?? err) }));\n\t});\n\n\t// -- runtimes ---------------------------------------------------------------\n\tapp.post(\"/api/runtimes\", (req, res) => {\n\t\t(async () => {\n\t\t\tconst 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.get(\"/api/runtimes/:key/subagents/:agentId/pending\", (req, res) => {\n\t\twithRuntime(req, res, (h) => h.client.getBackgroundAgentPending(String(req.params.agentId)));\n\t});\n\n\tapp.post(\"/api/runtimes/:key/subagents/:agentId/steer\", (req, res) => {\n\t\tconst { message } = req.body ?? {};\n\t\tif (typeof message !== \"string\" || message.length === 0) {\n\t\t\tres.status(400).json({ error: \"message is required\" });\n\t\t\treturn;\n\t\t}\n\t\twithRuntime(req, res, (h) => h.client.steerBackgroundAgent(String(req.params.agentId), message));\n\t});\n\n\tapp.post(\"/api/runtimes/:key/dequeue\", (req, res) => {\n\t\twithRuntime(req, res, (h) => h.client.clearPendingMessages());\n\t});\n\n\tfunction parseImages(body: unknown): ImageAttachmentDto[] | undefined | \"invalid\" {\n\t\tconst images = (body as { images?: unknown } | undefined)?.images;\n\t\tif (images === undefined) return undefined;\n\t\tif (!Array.isArray(images)) return \"invalid\";\n\t\tconst parsed: ImageAttachmentDto[] = [];\n\t\tfor (const image of images) {\n\t\t\tif (\n\t\t\t\t!image ||\n\t\t\t\ttypeof image !== \"object\" ||\n\t\t\t\ttypeof (image as { data?: unknown }).data !== \"string\" ||\n\t\t\t\ttypeof (image as { mimeType?: unknown }).mimeType !== \"string\"\n\t\t\t) {\n\t\t\t\treturn \"invalid\";\n\t\t\t}\n\t\t\tparsed.push({ data: (image as ImageAttachmentDto).data, mimeType: (image as ImageAttachmentDto).mimeType });\n\t\t}\n\t\treturn parsed;\n\t}\n\n\tapp.post(\"/api/runtimes/:key/prompt\", (req, res) => {\n\t\tconst { message, mode } = req.body ?? {};\n\t\tif (typeof message !== \"string\" || message.length === 0) {\n\t\t\tres.status(400).json({ error: \"message is required\" });\n\t\t\treturn;\n\t\t}\n\t\tconst images = parseImages(req.body);\n\t\tif (images === \"invalid\") {\n\t\t\tres.status(400).json({ error: \"images must be an array of {data, mimeType} objects\" });\n\t\t\treturn;\n\t\t}\n\t\tconst rpcImages = images?.map((image) => ({\n\t\t\ttype: \"image\" as const,\n\t\t\tdata: image.data,\n\t\t\tmimeType: image.mimeType,\n\t\t}));\n\t\twithRuntime(req, res, async (h) => {\n\t\t\tif (mode === \"steer\") await h.client.steer(message, rpcImages);\n\t\t\telse if (mode === \"follow_up\") await h.client.followUp(message, rpcImages);\n\t\t\telse await h.client.prompt(message, rpcImages);\n\t\t});\n\t});\n\n\tapp.post(\"/api/runtimes/:key/abort\", (req, res) => {\n\t\twithRuntime(req, res, (h) => h.client.abort());\n\t});\n\n\tapp.post(\"/api/runtimes/:key/abort-compaction\", (req, res) => {\n\t\twithRuntime(req, res, (h) => h.client.abortCompaction());\n\t});\n\n\tapp.post(\"/api/runtimes/:key/abort-retry\", (req, res) => {\n\t\twithRuntime(req, res, (h) => h.client.abortRetry());\n\t});\n\n\tapp.post(\"/api/runtimes/:key/model\", (req, res) => {\n\t\tconst { provider, modelId } = req.body ?? {};\n\t\tif (typeof provider !== \"string\" || typeof modelId !== \"string\") {\n\t\t\tres.status(400).json({ error: \"provider and modelId are required\" });\n\t\t\treturn;\n\t\t}\n\t\twithRuntime(req, res, (h) => 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"]}
|
package/dist/server/server.js
CHANGED
|
@@ -680,6 +680,17 @@ export function createDashboardServer(options) {
|
|
|
680
680
|
app.get("/api/runtimes/:key/pending", (req, res) => {
|
|
681
681
|
withRuntime(req, res, (h) => h.client.getPendingMessages());
|
|
682
682
|
});
|
|
683
|
+
app.get("/api/runtimes/:key/subagents/:agentId/pending", (req, res) => {
|
|
684
|
+
withRuntime(req, res, (h) => h.client.getBackgroundAgentPending(String(req.params.agentId)));
|
|
685
|
+
});
|
|
686
|
+
app.post("/api/runtimes/:key/subagents/:agentId/steer", (req, res) => {
|
|
687
|
+
const { message } = req.body ?? {};
|
|
688
|
+
if (typeof message !== "string" || message.length === 0) {
|
|
689
|
+
res.status(400).json({ error: "message is required" });
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
withRuntime(req, res, (h) => h.client.steerBackgroundAgent(String(req.params.agentId), message));
|
|
693
|
+
});
|
|
683
694
|
app.post("/api/runtimes/:key/dequeue", (req, res) => {
|
|
684
695
|
withRuntime(req, res, (h) => h.client.clearPendingMessages());
|
|
685
696
|
});
|