@dreb/dashboard 2.66.3 → 2.68.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 +24 -13
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -2
- package/dist/index.js.map +1 -1
- package/dist/server/runtime-pool.d.ts.map +1 -1
- package/dist/server/runtime-pool.js +5 -1
- package/dist/server/runtime-pool.js.map +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +13 -3
- package/dist/server/server.js.map +1 -1
- package/dist/shared/protocol.d.ts +4 -0
- package/dist/shared/protocol.d.ts.map +1 -1
- package/dist/shared/protocol.js.map +1 -1
- package/dist/static/assets/{index-CJxIzSRV.js → index-yAj_RMi4.js} +5 -5
- package/dist/static/index.html +1 -1
- package/dist/static/sw.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,6 +28,12 @@ dreb dashboard --remote --allow you@example.com \
|
|
|
28
28
|
|
|
29
29
|
Open `http://127.0.0.1:5343`.
|
|
30
30
|
|
|
31
|
+
## Session inventory roots
|
|
32
|
+
|
|
33
|
+
Dashboard snapshots main-session storage when the server starts. It reads only the global `sessionDir` from `<agent-dir>/settings.json` (normally `~/.dreb/agent/settings.json`, or the directory selected by `DREB_CODING_AGENT_DIR`); a project `.dreb/settings.json` in the launch directory cannot redirect host-wide inventory. When configured, that directory is treated as a flat JSONL store for Fleet, `/api/sessions`, resync, resume, and delete. Without it, Dashboard scans the built-in nested all-project tree.
|
|
34
|
+
|
|
35
|
+
Absolute paths remain absolute, `~` expands from the home directory, and a relative global `sessionDir` resolves against Dashboard's startup cwd (an absolute global path is recommended). Restart Dashboard after changing the setting. The separate global-only `subagentSessionDir` controls new child logs in coding-agent runtimes; RPC recovery can also discover matching logs in the legacy root. Neither setting migrates files or changes credential/general configuration paths.
|
|
36
|
+
|
|
31
37
|
## Screens
|
|
32
38
|
|
|
33
39
|
- **Fleet** (home) — live-first: every live session in one grid at the top
|
|
@@ -208,19 +214,24 @@ remain terminal without duplicate status entries.
|
|
|
208
214
|
|
|
209
215
|
## Fleet transport and freshness
|
|
210
216
|
|
|
211
|
-
A normal dashboard load
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
217
|
+
A normal dashboard load is live-first: `GET /api/fleet/live` returns runtime
|
|
218
|
+
cards without scanning historical logs, the SSE connection opens, and
|
|
219
|
+
`GET /api/sessions` fills the on-disk inventory in the background. The combined
|
|
220
|
+
`GET /api/fleet` remains available for diagnostics; exceptional recovery includes
|
|
221
|
+
the fleet in its ordered `/api/resync` snapshot. After that, live runtime cards
|
|
222
|
+
are updated by global, event-derived `fleet_snapshot` SSE frames, debounced by
|
|
223
|
+
200 ms. Those frames are built from the pool's in-memory runtime state, so they
|
|
224
|
+
do not trigger child RPC calls or a disk inventory scan.
|
|
225
|
+
|
|
226
|
+
Disk inventory is separate from live-runtime state. Its lightweight metadata is
|
|
227
|
+
cached by session-file mtime and size, so refreshes stat every log but reread and
|
|
228
|
+
parse only new or changed files. Before fleet, inventory, or resync serialization,
|
|
229
|
+
the server projects each on-disk session to the declared browser DTO, reports
|
|
230
|
+
whether its historical CWD resolves to a directory (plus the canonical candidate
|
|
231
|
+
when it does), and bounds its first-message preview to 256 Unicode characters.
|
|
232
|
+
Missing historical paths remain display metadata but are not used as memory/project
|
|
233
|
+
roots or recent runtime choices. Internal parent paths and complete searchable
|
|
234
|
+
transcript text never cross this boundary. The client narrowly refreshes inventory with `GET /api/sessions` after
|
|
224
235
|
create, resume, stop, or delete, rather than reloading the whole fleet. While the
|
|
225
236
|
Fleet screen or a session/subagent fleet sidebar is visible, it refreshes
|
|
226
237
|
per-runtime stats on a shared 30-second cadence; the refresh is
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* dreb-dashboard [--port 5343] [--remote --allow me@example.com [--allow ...]]
|
|
12
12
|
*/
|
|
13
13
|
import { watch } from "node:fs";
|
|
14
|
+
import { SettingsManager } from "@dreb/coding-agent";
|
|
14
15
|
export { DashboardAuth, TailscaleStatusResolver, TailscaleWhoisResolver } from "./server/auth.js";
|
|
15
16
|
export { DashboardImageService } from "./server/dashboard-images.js";
|
|
16
17
|
export { EventHub } from "./server/event-hub.js";
|
|
@@ -20,6 +21,14 @@ export { FilePairingStorage, loadOrCreateDashboardSecret } from "./server/pairin
|
|
|
20
21
|
export { RuntimePool, resolveDrebCliPath } from "./server/runtime-pool.js";
|
|
21
22
|
export { createDashboardServer, parseDeviceCookie } from "./server/server.js";
|
|
22
23
|
export type * from "./shared/protocol.js";
|
|
24
|
+
/** Select Dashboard's host-wide main-session root from global settings only. */
|
|
25
|
+
export declare function resolveDashboardSessionInventoryRoot(settingsManager: Pick<SettingsManager, "getGlobalSettings">, startupCwd: string): string | undefined;
|
|
26
|
+
interface DashboardSessionInventory<T> {
|
|
27
|
+
listAllMetadata(): Promise<T[]>;
|
|
28
|
+
listAllMetadataFromDir(dir: string): Promise<T[]>;
|
|
29
|
+
}
|
|
30
|
+
/** Build the startup-snapshotted inventory reader used by the Dashboard server. */
|
|
31
|
+
export declare function createDashboardSessionLister<T>(customDir: string | undefined, inventory: DashboardSessionInventory<T>): () => Promise<T[]>;
|
|
23
32
|
interface CliArgs {
|
|
24
33
|
port: number;
|
|
25
34
|
remote: boolean;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n/**\n * dreb-dashboard — launch the dreb web dashboard server.\n *\n * Modes (exactly two, no LAN mode):\n * default loopback bind (127.0.0.1), no auth, no Tailscale needed\n * --remote requires Tailscale; binds all interfaces but every request\n * passes identity allowlist + pairing code + device cookies\n *\n * Usage:\n * dreb-dashboard [--port 5343] [--remote --allow me@example.com [--allow ...]]\n */\n\nimport { existsSync, readFileSync, watch } from \"node:fs\";\nimport { createServer as createHttpServer, type Server as HttpServer } from \"node:http\";\nimport { createServer as createHttpsServer, type Server as HttpsServer } from \"node:https\";\nimport { homedir } from \"node:os\";\nimport { basename, dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { DashboardAuth } from \"./server/auth.js\";\nimport { DashboardImageService } from \"./server/dashboard-images.js\";\nimport { ImagePreviewWorker } from \"./server/image-preview.js\";\nimport { FilePairingStorage, loadOrCreateDashboardSecret } from \"./server/pairing-storage.js\";\nimport { RuntimePool } from \"./server/runtime-pool.js\";\nimport { createDashboardServer } from \"./server/server.js\";\n\nexport { DashboardAuth, TailscaleStatusResolver, TailscaleWhoisResolver } from \"./server/auth.js\";\nexport { DashboardImageService } from \"./server/dashboard-images.js\";\nexport { EventHub } from \"./server/event-hub.js\";\nexport { canonicalizePath, FileApi, resolveExistingDirectory } from \"./server/files.js\";\nexport { ImagePreviewWorker } from \"./server/image-preview.js\";\nexport { FilePairingStorage, loadOrCreateDashboardSecret } from \"./server/pairing-storage.js\";\nexport { RuntimePool, resolveDrebCliPath } from \"./server/runtime-pool.js\";\nexport { createDashboardServer, parseDeviceCookie } from \"./server/server.js\";\nexport type * from \"./shared/protocol.js\";\n\nconst DEFAULT_PORT = 5343;\n\ninterface CliArgs {\n\tport: number;\n\tremote: boolean;\n\tallow: string[];\n\thelp: boolean;\n\thttps: boolean;\n\tcert?: string;\n\tkey?: string;\n}\n\nexport function parseArgs(argv: string[]): CliArgs {\n\tconst args: CliArgs = { port: DEFAULT_PORT, remote: false, allow: [], help: false, https: false };\n\tfor (let i = 0; i < argv.length; i++) {\n\t\tconst arg = argv[i];\n\t\tif (arg === \"--port\") {\n\t\t\tconst value = argv[++i];\n\t\t\tconst port = Number.parseInt(value ?? \"\", 10);\n\t\t\tif (!Number.isInteger(port) || port < 1 || port > 65535) {\n\t\t\t\tthrow new Error(`Invalid --port value: ${value}`);\n\t\t\t}\n\t\t\targs.port = port;\n\t\t} else if (arg === \"--remote\") {\n\t\t\targs.remote = true;\n\t\t} else if (arg === \"--allow\") {\n\t\t\tconst value = argv[++i];\n\t\t\tif (!value) throw new Error(\"--allow requires an identity (Tailscale login name)\");\n\t\t\targs.allow.push(value);\n\t\t} else if (arg === \"--https\") {\n\t\t\targs.https = true;\n\t\t} else if (arg === \"--cert\") {\n\t\t\tconst value = argv[++i];\n\t\t\tif (!value) throw new Error(\"--cert requires a path to a PEM certificate file\");\n\t\t\targs.cert = value;\n\t\t} else if (arg === \"--key\") {\n\t\t\tconst value = argv[++i];\n\t\t\tif (!value) throw new Error(\"--key requires a path to a PEM private key file\");\n\t\t\targs.key = value;\n\t\t} else if (arg === \"--help\" || arg === \"-h\") {\n\t\t\targs.help = true;\n\t\t} else {\n\t\t\tthrow new Error(`Unknown argument: ${arg}`);\n\t\t}\n\t}\n\tif (args.remote && args.allow.length === 0) {\n\t\tthrow new Error(\"--remote requires at least one --allow <tailscale-login> (empty allowlist denies everyone)\");\n\t}\n\t// Native TLS. `https` is opt-in; it requires cert AND key (no silent\n\t// fallback to plain HTTP). The dashboard terminates TLS itself — no reverse\n\t// proxy, no auth-model change. `req.socket.remoteAddress` stays the real\n\t// tailnet IP, so identity resolution + allowlist + pairing keep working.\n\tif (args.https) {\n\t\tif (!args.cert) throw new Error(\"--https requires --cert <path>\");\n\t\tif (!args.key) throw new Error(\"--https requires --key <path>\");\n\t}\n\treturn args;\n}\n\n/**\n * Warning shown at startup when `--remote` binds the tailnet over plain HTTP\n * (no `--https`). Browsers treat plain HTTP over a non-loopback host as an\n * INSECURE context, so service workers and the Notification API are\n * unavailable — PWA install and mobile notifications (iOS exposes no\n * Notification API at all there) silently never work. Loopback (127.0.0.1) is\n * exempt (it counts as secure), so pure-local mode returns no warning.\n *\n * Returns the multi-line warning string, or `undefined` when TLS is enabled or\n * the server is loopback-only. Kept pure (no console) so it is unit-testable.\n */\nexport function insecureRemoteWarning(args: Pick<CliArgs, \"remote\" | \"https\">): string | undefined {\n\tif (!args.remote || args.https) return undefined;\n\treturn (\n\t\t\"⚠ warning: --remote without --https serves plain HTTP over the tailnet, which browsers treat as an\\n\" +\n\t\t\" INSECURE context — service workers, PWA install, and mobile notifications (especially iOS) will NOT work.\\n\" +\n\t\t\" Enable TLS for the tailnet hostname:\\n\" +\n\t\t\" tailscale cert <host>.<tailnet>.ts.net\\n\" +\n\t\t\" …then relaunch with --https --cert <host>.<tailnet>.ts.net.crt --key <host>.<tailnet>.ts.net.key\\n\" +\n\t\t\" and open the dashboard via https://<host>.<tailnet>.ts.net (the hostname, not the tailnet IP).\"\n\t);\n}\n\ntype ShutdownWatcher = Pick<ReturnType<typeof watch>, \"close\">;\n\ninterface ShutdownDeps {\n\tlog: (message: string) => void;\n\tclearReloadTimer: () => void;\n\twatchers: Iterable<ShutdownWatcher>;\n\tcloseServer: () => void;\n\tstopAll: () => Promise<unknown>;\n\texit: (code: number) => void;\n}\n\nexport function createShutdown(deps: ShutdownDeps): (message: string, exitCode: number) => void {\n\tlet shuttingDown = false;\n\treturn (message: string, exitCode: number) => {\n\t\tif (shuttingDown) return;\n\t\tshuttingDown = true;\n\t\tdeps.log(message);\n\t\tdeps.clearReloadTimer();\n\t\tfor (const watcher of deps.watchers) watcher.close();\n\t\tdeps.closeServer();\n\t\tvoid deps\n\t\t\t.stopAll()\n\t\t\t.catch((err) => {\n\t\t\t\tdeps.log(`shutdown teardown failed: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t})\n\t\t\t.finally(() => deps.exit(exitCode));\n\t};\n}\n\ntype TlsOptions = { cert: string; key: string };\ntype TlsWatchFilename = string | Buffer | null;\ntype TlsWatchListener = (event: string, filename: TlsWatchFilename) => void;\ntype TlsFileWatcher = ShutdownWatcher & {\n\ton(event: \"error\", listener: (err: unknown) => void): unknown;\n};\n\ninterface TlsReloaderDeps {\n\treadTlsOptions: () => TlsOptions;\n\tsetSecureContext: (options: TlsOptions) => void;\n\tlog: (message: string) => void;\n\twarn: (message: string) => void;\n}\n\nexport function createTlsReloader(deps: TlsReloaderDeps): () => void {\n\treturn () => {\n\t\ttry {\n\t\t\tdeps.setSecureContext(deps.readTlsOptions());\n\t\t\tdeps.log(\"tls certificate reloaded\");\n\t\t} catch (err) {\n\t\t\tdeps.warn(`tls reload failed (keeping old cert): ${err instanceof Error ? err.message : String(err)}`);\n\t\t}\n\t};\n}\n\nexport function shouldReloadForFile(filename: TlsWatchFilename, certBase: string, keyBase: string): boolean {\n\tif (filename == null) return true;\n\tif (typeof filename !== \"string\") return false;\n\treturn filename === certBase || filename === keyBase;\n}\n\ninterface TlsWatchDeps {\n\tcertPath: string;\n\tkeyPath: string;\n\twatchDirectory: (directory: string, listener: TlsWatchListener) => TlsFileWatcher;\n\treloadTls: () => void;\n\twarn: (message: string) => void;\n\tdebounceMs?: number;\n}\n\ninterface TlsWatchController {\n\twatchers: TlsFileWatcher[];\n\tclearReloadTimer: () => void;\n}\n\nexport function createTlsWatchers(deps: TlsWatchDeps): TlsWatchController {\n\tlet reloadTimer: ReturnType<typeof setTimeout> | undefined;\n\tconst watchers: TlsFileWatcher[] = [];\n\tconst debounceMs = deps.debounceMs ?? 500;\n\tconst certBase = basename(deps.certPath);\n\tconst keyBase = basename(deps.keyPath);\n\tconst clearReloadTimer = () => {\n\t\tif (!reloadTimer) return;\n\t\tclearTimeout(reloadTimer);\n\t\treloadTimer = undefined;\n\t};\n\tconst scheduleReload = () => {\n\t\tclearReloadTimer();\n\t\treloadTimer = setTimeout(deps.reloadTls, debounceMs);\n\t};\n\n\tfor (const dir of new Set([dirname(deps.certPath), dirname(deps.keyPath)])) {\n\t\ttry {\n\t\t\tconst watcher = deps.watchDirectory(dir, (_event, filename) => {\n\t\t\t\t// `fs.watch` directory events on macOS (FSEvents) frequently fire\n\t\t\t\t// with `filename === null`; fall through and reload anyway.\n\t\t\t\t// On Linux (inotify) the basename is carried reliably and filters\n\t\t\t\t// out unrelated files in the parent directory.\n\t\t\t\tif (!shouldReloadForFile(filename, certBase, keyBase)) return;\n\t\t\t\tscheduleReload();\n\t\t\t});\n\t\t\twatcher.on(\"error\", (err) => {\n\t\t\t\tdeps.warn(`tls cert watch error: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t});\n\t\t\twatchers.push(watcher);\n\t\t} catch (err) {\n\t\t\tdeps.warn(`tls cert watch setup failed for ${dir}: ${err instanceof Error ? err.message : String(err)}`);\n\t\t}\n\t}\n\n\treturn { watchers, clearReloadTimer };\n}\n\nconst HELP = `dreb-dashboard — dreb web dashboard server\n\nUsage: dreb-dashboard [options]\n\nOptions:\n --port <n> Port to listen on (default ${DEFAULT_PORT})\n --remote Enable remote mode (requires Tailscale). Without this\n flag the server binds 127.0.0.1 only — no LAN access.\n Plain HTTP over the tailnet is an INSECURE context: add\n --https (below) or mobile PWA install + notifications\n (especially iOS) will not work.\n --allow <identity> Tailscale login name allowed to pair (repeatable;\n required with --remote)\n --https Terminate TLS on the dashboard itself (native TLS). No\n reverse proxy, no auth-model change. Requires --cert\n and --key. Mainly for --remote (loopback is already a\n secure context): use 'tailscale cert' files for a\n tailnet hostname so mobile PWAs + notifications work.\n NOTE: with --https the server speaks TLS only, so the\n host's plain-http local tab (http://127.0.0.1) stops\n working — use the tailnet hostname (https://...) there.\n --cert <path> PEM certificate file (required with --https)\n --key <path> PEM private key file (required with --https)\n --help Show this help\n`;\n\nasync function main(): Promise<void> {\n\tlet args: CliArgs;\n\ttry {\n\t\targs = parseArgs(process.argv.slice(2));\n\t} catch (err) {\n\t\tconsole.error(`error: ${err instanceof Error ? err.message : String(err)}`);\n\t\tconsole.error(HELP);\n\t\tprocess.exit(1);\n\t}\n\tif (args.help) {\n\t\tconsole.log(HELP);\n\t\treturn;\n\t}\n\n\tconst agentDir = join(homedir(), \".dreb\", \"agent\");\n\tconst auth = new DashboardAuth({\n\t\tremoteEnabled: args.remote,\n\t\tallowedIdentities: args.allow,\n\t\tstorage: new FilePairingStorage(join(agentDir, \"dashboard-pairings.json\")),\n\t\tsecret: loadOrCreateDashboardSecret(join(agentDir, \"dashboard-auth-secret\")),\n\t\tlogger: (line) => console.warn(`[dashboard-auth] ${line}`),\n\t});\n\tconst pool = new RuntimePool();\n\tconst imageService = new DashboardImageService(new ImagePreviewWorker());\n\n\t// Static client assets live next to the compiled server (dist/static).\n\tconst staticDir = join(dirname(fileURLToPath(import.meta.url)), \"static\");\n\n\t// The server's own build version (dist/index.js → ../package.json) — surfaced\n\t// in the settings footer so a stale long-running service is spottable.\n\tlet serverVersion: string | undefined;\n\ttry {\n\t\tconst pkgPath = join(dirname(fileURLToPath(import.meta.url)), \"..\", \"package.json\");\n\t\tserverVersion = JSON.parse(readFileSync(pkgPath, \"utf8\")).version;\n\t} catch {\n\t\tserverVersion = undefined;\n\t}\n\n\t// Restart hook: exit non-zero so a supervisor (systemd Restart=on-failure,\n\t// etc.) respawns the process with the freshly-built dist. Assigned the http\n\t// server below via a mutable holder so the closure can close it first.\n\t// The TLS hot-reload debounce timer and directory watchers are lifted here\n\t// too, so restart and signal shutdown can release them (otherwise they leak\n\t// across restart/exit).\n\tlet httpServer: HttpServer | HttpsServer | undefined;\n\tlet clearTlsReloadTimer = () => {};\n\tlet closeDashboard = () => imageService.close();\n\tconst tlsWatchers: TlsFileWatcher[] = [];\n\tconst beginShutdown = createShutdown({\n\t\tlog: (message) => console.log(message),\n\t\tclearReloadTimer: () => clearTlsReloadTimer(),\n\t\twatchers: tlsWatchers,\n\t\tcloseServer: () => httpServer?.close(),\n\t\tstopAll: () => Promise.all([pool.stopAll(), closeDashboard()]),\n\t\texit: (code) => process.exit(code),\n\t});\n\tconst onRestart = () => beginShutdown(\"restart requested — exiting for supervisor to respawn\", 1);\n\n\tconst { SessionManager } = await import(\"@dreb/coding-agent\");\n\tconst app = createDashboardServer({\n\t\tauth,\n\t\tpool,\n\t\timageService,\n\t\tstaticDir: existsSync(staticDir) ? staticDir : undefined,\n\t\tserverVersion,\n\t\tonRestart,\n\t\tlistAllSessions: () => SessionManager.listAll(),\n\t\tdeleteSession: async (path: string) => {\n\t\t\tconst result = await SessionManager.deleteSession(path, {});\n\t\t\tif (!result.ok) throw new Error(result.error ?? \"Unknown deletion error\");\n\t\t\treturn { method: result.method };\n\t\t},\n\t});\n\tcloseDashboard = () => app.closeDashboard();\n\n\tconst host = args.remote ? \"0.0.0.0\" : \"127.0.0.1\";\n\tconst scheme = args.https ? \"https\" : \"http\";\n\n\t// Native TLS: the dashboard terminates TLS itself (no reverse proxy). The\n\t// auth model is unchanged — `req.socket.remoteAddress` is still the real\n\t// tailnet IP, so Tailscale identity resolution + allowlist + pairing keep\n\t// working. Local mode never sets --https (loopback is already a secure\n\t// context), but it's allowed if the operator wants it.\n\tlet server: HttpServer | HttpsServer;\n\tif (args.https && args.cert && args.key) {\n\t\tconst readTlsOptions = () => ({\n\t\t\tcert: readFileSync(args.cert!, \"utf-8\"),\n\t\t\tkey: readFileSync(args.key!, \"utf-8\"),\n\t\t});\n\t\tlet tlsOptions: TlsOptions;\n\t\ttry {\n\t\t\ttlsOptions = readTlsOptions();\n\t\t} catch (err) {\n\t\t\tconsole.error(`error: failed to read TLS cert/key: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\tprocess.exit(1);\n\t\t}\n\t\tconst httpsServer = createHttpsServer(tlsOptions, app);\n\t\t// Hot-reload the cert on file change (zero-downtime renewal — e.g. a\n\t\t// systemd timer rewrites the `tailscale cert` files daily). New\n\t\t// connections pick up the new cert; existing ones finish on the old.\n\t\tconst reloadTls = createTlsReloader({\n\t\t\treadTlsOptions,\n\t\t\tsetSecureContext: (options) => httpsServer.setSecureContext(options),\n\t\t\tlog: (message) => console.log(message),\n\t\t\twarn: (message) => console.warn(message),\n\t\t});\n\t\t// Debounce: cert + key are often rewritten in quick succession. Watch\n\t\t// the PARENT DIRECTORY (not each file) and filter by filename: `fs.watch`\n\t\t// follows the inode, so an atomic renewal (write temp + rename(2))\n\t\t// replaces the inode and a file-level watcher goes silent on the new\n\t\t// file after the first reload. A directory watcher survives renames and\n\t\t// reports the changed basename via the `filename` callback argument.\n\t\tconst tlsWatchController = createTlsWatchers({\n\t\t\tcertPath: args.cert,\n\t\t\tkeyPath: args.key,\n\t\t\twatchDirectory: (dir, listener) => watch(dir, listener),\n\t\t\treloadTls,\n\t\t\twarn: (message) => console.warn(message),\n\t\t});\n\t\tclearTlsReloadTimer = tlsWatchController.clearReloadTimer;\n\t\ttlsWatchers.push(...tlsWatchController.watchers);\n\t\tserver = httpsServer;\n\t} else {\n\t\tserver = createHttpServer(app);\n\t}\n\tserver.listen(args.port, host, () => {\n\t\tconsole.log(\n\t\t\t`dreb dashboard listening on ${scheme}://${host === \"0.0.0.0\" ? \"<tailscale-ip>\" : host}:${args.port}`,\n\t\t);\n\t\tif (args.remote) {\n\t\t\tconsole.log(`remote mode: allowed identities = ${args.allow.join(\", \")}`);\n\t\t\tconst { code, expiresInMs } = auth.currentPairingCode();\n\t\t\tconsole.log(\n\t\t\t\t`pairing code: ${code} (rotates every 30s; current code rolls in ${Math.ceil(expiresInMs / 1000)}s)`,\n\t\t\t);\n\t\t\tconsole.log(\"new devices enter this code; see it live in dashboard Settings on the host machine\");\n\t\t} else {\n\t\t\tconsole.log(\"local mode: loopback only — use --remote for Tailscale access\");\n\t\t}\n\t\tif (args.https) {\n\t\t\tconsole.log(\"tls: native HTTPS enabled (cert + key hot-reload on file change)\");\n\t\t} else {\n\t\t\tconst insecureWarning = insecureRemoteWarning(args);\n\t\t\tif (insecureWarning) console.warn(insecureWarning);\n\t\t}\n\t});\n\thttpServer = server;\n\n\tconst shutdown = () => beginShutdown(\"shutting down…\", 0);\n\tprocess.on(\"SIGINT\", shutdown);\n\tprocess.on(\"SIGTERM\", shutdown);\n}\n\n// Only run when executed directly (not when imported for the library exports).\nconst entryPath = process.argv[1];\nif (entryPath && import.meta.url === new URL(`file://${entryPath}`).href) {\n\tmain().catch((err) => {\n\t\tconsole.error(`fatal: ${err instanceof Error ? err.message : String(err)}`);\n\t\tprocess.exit(1);\n\t});\n}\n"]}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n/**\n * dreb-dashboard — launch the dreb web dashboard server.\n *\n * Modes (exactly two, no LAN mode):\n * default loopback bind (127.0.0.1), no auth, no Tailscale needed\n * --remote requires Tailscale; binds all interfaces but every request\n * passes identity allowlist + pairing code + device cookies\n *\n * Usage:\n * dreb-dashboard [--port 5343] [--remote --allow me@example.com [--allow ...]]\n */\n\nimport { existsSync, readFileSync, watch } from \"node:fs\";\nimport { createServer as createHttpServer, type Server as HttpServer } from \"node:http\";\nimport { createServer as createHttpsServer, type Server as HttpsServer } from \"node:https\";\nimport { homedir } from \"node:os\";\nimport { basename, dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { resolveConfiguredDirectory, SessionManager, SettingsManager } from \"@dreb/coding-agent\";\nimport { DashboardAuth } from \"./server/auth.js\";\nimport { DashboardImageService } from \"./server/dashboard-images.js\";\nimport { ImagePreviewWorker } from \"./server/image-preview.js\";\nimport { FilePairingStorage, loadOrCreateDashboardSecret } from \"./server/pairing-storage.js\";\nimport { RuntimePool } from \"./server/runtime-pool.js\";\nimport { createDashboardServer } from \"./server/server.js\";\n\nexport { DashboardAuth, TailscaleStatusResolver, TailscaleWhoisResolver } from \"./server/auth.js\";\nexport { DashboardImageService } from \"./server/dashboard-images.js\";\nexport { EventHub } from \"./server/event-hub.js\";\nexport { canonicalizePath, FileApi, resolveExistingDirectory } from \"./server/files.js\";\nexport { ImagePreviewWorker } from \"./server/image-preview.js\";\nexport { FilePairingStorage, loadOrCreateDashboardSecret } from \"./server/pairing-storage.js\";\nexport { RuntimePool, resolveDrebCliPath } from \"./server/runtime-pool.js\";\nexport { createDashboardServer, parseDeviceCookie } from \"./server/server.js\";\nexport type * from \"./shared/protocol.js\";\n\nconst DEFAULT_PORT = 5343;\n\n/** Select Dashboard's host-wide main-session root from global settings only. */\nexport function resolveDashboardSessionInventoryRoot(\n\tsettingsManager: Pick<SettingsManager, \"getGlobalSettings\">,\n\tstartupCwd: string,\n): string | undefined {\n\treturn resolveConfiguredDirectory(settingsManager.getGlobalSettings().sessionDir, startupCwd);\n}\n\ninterface DashboardSessionInventory<T> {\n\tlistAllMetadata(): Promise<T[]>;\n\tlistAllMetadataFromDir(dir: string): Promise<T[]>;\n}\n\n/** Build the startup-snapshotted inventory reader used by the Dashboard server. */\nexport function createDashboardSessionLister<T>(\n\tcustomDir: string | undefined,\n\tinventory: DashboardSessionInventory<T>,\n): () => Promise<T[]> {\n\treturn customDir !== undefined\n\t\t? () => inventory.listAllMetadataFromDir(customDir)\n\t\t: () => inventory.listAllMetadata();\n}\n\ninterface CliArgs {\n\tport: number;\n\tremote: boolean;\n\tallow: string[];\n\thelp: boolean;\n\thttps: boolean;\n\tcert?: string;\n\tkey?: string;\n}\n\nexport function parseArgs(argv: string[]): CliArgs {\n\tconst args: CliArgs = { port: DEFAULT_PORT, remote: false, allow: [], help: false, https: false };\n\tfor (let i = 0; i < argv.length; i++) {\n\t\tconst arg = argv[i];\n\t\tif (arg === \"--port\") {\n\t\t\tconst value = argv[++i];\n\t\t\tconst port = Number.parseInt(value ?? \"\", 10);\n\t\t\tif (!Number.isInteger(port) || port < 1 || port > 65535) {\n\t\t\t\tthrow new Error(`Invalid --port value: ${value}`);\n\t\t\t}\n\t\t\targs.port = port;\n\t\t} else if (arg === \"--remote\") {\n\t\t\targs.remote = true;\n\t\t} else if (arg === \"--allow\") {\n\t\t\tconst value = argv[++i];\n\t\t\tif (!value) throw new Error(\"--allow requires an identity (Tailscale login name)\");\n\t\t\targs.allow.push(value);\n\t\t} else if (arg === \"--https\") {\n\t\t\targs.https = true;\n\t\t} else if (arg === \"--cert\") {\n\t\t\tconst value = argv[++i];\n\t\t\tif (!value) throw new Error(\"--cert requires a path to a PEM certificate file\");\n\t\t\targs.cert = value;\n\t\t} else if (arg === \"--key\") {\n\t\t\tconst value = argv[++i];\n\t\t\tif (!value) throw new Error(\"--key requires a path to a PEM private key file\");\n\t\t\targs.key = value;\n\t\t} else if (arg === \"--help\" || arg === \"-h\") {\n\t\t\targs.help = true;\n\t\t} else {\n\t\t\tthrow new Error(`Unknown argument: ${arg}`);\n\t\t}\n\t}\n\tif (args.remote && args.allow.length === 0) {\n\t\tthrow new Error(\"--remote requires at least one --allow <tailscale-login> (empty allowlist denies everyone)\");\n\t}\n\t// Native TLS. `https` is opt-in; it requires cert AND key (no silent\n\t// fallback to plain HTTP). The dashboard terminates TLS itself — no reverse\n\t// proxy, no auth-model change. `req.socket.remoteAddress` stays the real\n\t// tailnet IP, so identity resolution + allowlist + pairing keep working.\n\tif (args.https) {\n\t\tif (!args.cert) throw new Error(\"--https requires --cert <path>\");\n\t\tif (!args.key) throw new Error(\"--https requires --key <path>\");\n\t}\n\treturn args;\n}\n\n/**\n * Warning shown at startup when `--remote` binds the tailnet over plain HTTP\n * (no `--https`). Browsers treat plain HTTP over a non-loopback host as an\n * INSECURE context, so service workers and the Notification API are\n * unavailable — PWA install and mobile notifications (iOS exposes no\n * Notification API at all there) silently never work. Loopback (127.0.0.1) is\n * exempt (it counts as secure), so pure-local mode returns no warning.\n *\n * Returns the multi-line warning string, or `undefined` when TLS is enabled or\n * the server is loopback-only. Kept pure (no console) so it is unit-testable.\n */\nexport function insecureRemoteWarning(args: Pick<CliArgs, \"remote\" | \"https\">): string | undefined {\n\tif (!args.remote || args.https) return undefined;\n\treturn (\n\t\t\"⚠ warning: --remote without --https serves plain HTTP over the tailnet, which browsers treat as an\\n\" +\n\t\t\" INSECURE context — service workers, PWA install, and mobile notifications (especially iOS) will NOT work.\\n\" +\n\t\t\" Enable TLS for the tailnet hostname:\\n\" +\n\t\t\" tailscale cert <host>.<tailnet>.ts.net\\n\" +\n\t\t\" …then relaunch with --https --cert <host>.<tailnet>.ts.net.crt --key <host>.<tailnet>.ts.net.key\\n\" +\n\t\t\" and open the dashboard via https://<host>.<tailnet>.ts.net (the hostname, not the tailnet IP).\"\n\t);\n}\n\ntype ShutdownWatcher = Pick<ReturnType<typeof watch>, \"close\">;\n\ninterface ShutdownDeps {\n\tlog: (message: string) => void;\n\tclearReloadTimer: () => void;\n\twatchers: Iterable<ShutdownWatcher>;\n\tcloseServer: () => void;\n\tstopAll: () => Promise<unknown>;\n\texit: (code: number) => void;\n}\n\nexport function createShutdown(deps: ShutdownDeps): (message: string, exitCode: number) => void {\n\tlet shuttingDown = false;\n\treturn (message: string, exitCode: number) => {\n\t\tif (shuttingDown) return;\n\t\tshuttingDown = true;\n\t\tdeps.log(message);\n\t\tdeps.clearReloadTimer();\n\t\tfor (const watcher of deps.watchers) watcher.close();\n\t\tdeps.closeServer();\n\t\tvoid deps\n\t\t\t.stopAll()\n\t\t\t.catch((err) => {\n\t\t\t\tdeps.log(`shutdown teardown failed: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t})\n\t\t\t.finally(() => deps.exit(exitCode));\n\t};\n}\n\ntype TlsOptions = { cert: string; key: string };\ntype TlsWatchFilename = string | Buffer | null;\ntype TlsWatchListener = (event: string, filename: TlsWatchFilename) => void;\ntype TlsFileWatcher = ShutdownWatcher & {\n\ton(event: \"error\", listener: (err: unknown) => void): unknown;\n};\n\ninterface TlsReloaderDeps {\n\treadTlsOptions: () => TlsOptions;\n\tsetSecureContext: (options: TlsOptions) => void;\n\tlog: (message: string) => void;\n\twarn: (message: string) => void;\n}\n\nexport function createTlsReloader(deps: TlsReloaderDeps): () => void {\n\treturn () => {\n\t\ttry {\n\t\t\tdeps.setSecureContext(deps.readTlsOptions());\n\t\t\tdeps.log(\"tls certificate reloaded\");\n\t\t} catch (err) {\n\t\t\tdeps.warn(`tls reload failed (keeping old cert): ${err instanceof Error ? err.message : String(err)}`);\n\t\t}\n\t};\n}\n\nexport function shouldReloadForFile(filename: TlsWatchFilename, certBase: string, keyBase: string): boolean {\n\tif (filename == null) return true;\n\tif (typeof filename !== \"string\") return false;\n\treturn filename === certBase || filename === keyBase;\n}\n\ninterface TlsWatchDeps {\n\tcertPath: string;\n\tkeyPath: string;\n\twatchDirectory: (directory: string, listener: TlsWatchListener) => TlsFileWatcher;\n\treloadTls: () => void;\n\twarn: (message: string) => void;\n\tdebounceMs?: number;\n}\n\ninterface TlsWatchController {\n\twatchers: TlsFileWatcher[];\n\tclearReloadTimer: () => void;\n}\n\nexport function createTlsWatchers(deps: TlsWatchDeps): TlsWatchController {\n\tlet reloadTimer: ReturnType<typeof setTimeout> | undefined;\n\tconst watchers: TlsFileWatcher[] = [];\n\tconst debounceMs = deps.debounceMs ?? 500;\n\tconst certBase = basename(deps.certPath);\n\tconst keyBase = basename(deps.keyPath);\n\tconst clearReloadTimer = () => {\n\t\tif (!reloadTimer) return;\n\t\tclearTimeout(reloadTimer);\n\t\treloadTimer = undefined;\n\t};\n\tconst scheduleReload = () => {\n\t\tclearReloadTimer();\n\t\treloadTimer = setTimeout(deps.reloadTls, debounceMs);\n\t};\n\n\tfor (const dir of new Set([dirname(deps.certPath), dirname(deps.keyPath)])) {\n\t\ttry {\n\t\t\tconst watcher = deps.watchDirectory(dir, (_event, filename) => {\n\t\t\t\t// `fs.watch` directory events on macOS (FSEvents) frequently fire\n\t\t\t\t// with `filename === null`; fall through and reload anyway.\n\t\t\t\t// On Linux (inotify) the basename is carried reliably and filters\n\t\t\t\t// out unrelated files in the parent directory.\n\t\t\t\tif (!shouldReloadForFile(filename, certBase, keyBase)) return;\n\t\t\t\tscheduleReload();\n\t\t\t});\n\t\t\twatcher.on(\"error\", (err) => {\n\t\t\t\tdeps.warn(`tls cert watch error: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t});\n\t\t\twatchers.push(watcher);\n\t\t} catch (err) {\n\t\t\tdeps.warn(`tls cert watch setup failed for ${dir}: ${err instanceof Error ? err.message : String(err)}`);\n\t\t}\n\t}\n\n\treturn { watchers, clearReloadTimer };\n}\n\nconst HELP = `dreb-dashboard — dreb web dashboard server\n\nUsage: dreb-dashboard [options]\n\nOptions:\n --port <n> Port to listen on (default ${DEFAULT_PORT})\n --remote Enable remote mode (requires Tailscale). Without this\n flag the server binds 127.0.0.1 only — no LAN access.\n Plain HTTP over the tailnet is an INSECURE context: add\n --https (below) or mobile PWA install + notifications\n (especially iOS) will not work.\n --allow <identity> Tailscale login name allowed to pair (repeatable;\n required with --remote)\n --https Terminate TLS on the dashboard itself (native TLS). No\n reverse proxy, no auth-model change. Requires --cert\n and --key. Mainly for --remote (loopback is already a\n secure context): use 'tailscale cert' files for a\n tailnet hostname so mobile PWAs + notifications work.\n NOTE: with --https the server speaks TLS only, so the\n host's plain-http local tab (http://127.0.0.1) stops\n working — use the tailnet hostname (https://...) there.\n --cert <path> PEM certificate file (required with --https)\n --key <path> PEM private key file (required with --https)\n --help Show this help\n`;\n\nasync function main(): Promise<void> {\n\tlet args: CliArgs;\n\ttry {\n\t\targs = parseArgs(process.argv.slice(2));\n\t} catch (err) {\n\t\tconsole.error(`error: ${err instanceof Error ? err.message : String(err)}`);\n\t\tconsole.error(HELP);\n\t\tprocess.exit(1);\n\t}\n\tif (args.help) {\n\t\tconsole.log(HELP);\n\t\treturn;\n\t}\n\n\tconst startupCwd = process.cwd();\n\tconst settingsManager = SettingsManager.create(startupCwd);\n\tfor (const { scope, error } of settingsManager.drainErrors()) {\n\t\tif (scope === \"global\") console.warn(`[dashboard] Could not read global settings: ${error.message}`);\n\t}\n\tconst customSessionInventoryRoot = resolveDashboardSessionInventoryRoot(settingsManager, startupCwd);\n\tconst listAllSessions = createDashboardSessionLister(customSessionInventoryRoot, SessionManager);\n\n\tconst agentDir = join(homedir(), \".dreb\", \"agent\");\n\tconst auth = new DashboardAuth({\n\t\tremoteEnabled: args.remote,\n\t\tallowedIdentities: args.allow,\n\t\tstorage: new FilePairingStorage(join(agentDir, \"dashboard-pairings.json\")),\n\t\tsecret: loadOrCreateDashboardSecret(join(agentDir, \"dashboard-auth-secret\")),\n\t\tlogger: (line) => console.warn(`[dashboard-auth] ${line}`),\n\t});\n\tconst pool = new RuntimePool();\n\tconst imageService = new DashboardImageService(new ImagePreviewWorker());\n\n\t// Static client assets live next to the compiled server (dist/static).\n\tconst staticDir = join(dirname(fileURLToPath(import.meta.url)), \"static\");\n\n\t// The server's own build version (dist/index.js → ../package.json) — surfaced\n\t// in the settings footer so a stale long-running service is spottable.\n\tlet serverVersion: string | undefined;\n\ttry {\n\t\tconst pkgPath = join(dirname(fileURLToPath(import.meta.url)), \"..\", \"package.json\");\n\t\tserverVersion = JSON.parse(readFileSync(pkgPath, \"utf8\")).version;\n\t} catch {\n\t\tserverVersion = undefined;\n\t}\n\n\t// Restart hook: exit non-zero so a supervisor (systemd Restart=on-failure,\n\t// etc.) respawns the process with the freshly-built dist. Assigned the http\n\t// server below via a mutable holder so the closure can close it first.\n\t// The TLS hot-reload debounce timer and directory watchers are lifted here\n\t// too, so restart and signal shutdown can release them (otherwise they leak\n\t// across restart/exit).\n\tlet httpServer: HttpServer | HttpsServer | undefined;\n\tlet clearTlsReloadTimer = () => {};\n\tlet closeDashboard = () => imageService.close();\n\tconst tlsWatchers: TlsFileWatcher[] = [];\n\tconst beginShutdown = createShutdown({\n\t\tlog: (message) => console.log(message),\n\t\tclearReloadTimer: () => clearTlsReloadTimer(),\n\t\twatchers: tlsWatchers,\n\t\tcloseServer: () => httpServer?.close(),\n\t\tstopAll: () => Promise.all([pool.stopAll(), closeDashboard()]),\n\t\texit: (code) => process.exit(code),\n\t});\n\tconst onRestart = () => beginShutdown(\"restart requested — exiting for supervisor to respawn\", 1);\n\n\tconst app = createDashboardServer({\n\t\tauth,\n\t\tpool,\n\t\timageService,\n\t\tstaticDir: existsSync(staticDir) ? staticDir : undefined,\n\t\tserverVersion,\n\t\tonRestart,\n\t\tlistAllSessions,\n\t\tdeleteSession: async (path: string) => {\n\t\t\tconst result = await SessionManager.deleteSession(path, {});\n\t\t\tif (!result.ok) throw new Error(result.error ?? \"Unknown deletion error\");\n\t\t\treturn { method: result.method };\n\t\t},\n\t});\n\tcloseDashboard = () => app.closeDashboard();\n\n\tconst host = args.remote ? \"0.0.0.0\" : \"127.0.0.1\";\n\tconst scheme = args.https ? \"https\" : \"http\";\n\n\t// Native TLS: the dashboard terminates TLS itself (no reverse proxy). The\n\t// auth model is unchanged — `req.socket.remoteAddress` is still the real\n\t// tailnet IP, so Tailscale identity resolution + allowlist + pairing keep\n\t// working. Local mode never sets --https (loopback is already a secure\n\t// context), but it's allowed if the operator wants it.\n\tlet server: HttpServer | HttpsServer;\n\tif (args.https && args.cert && args.key) {\n\t\tconst readTlsOptions = () => ({\n\t\t\tcert: readFileSync(args.cert!, \"utf-8\"),\n\t\t\tkey: readFileSync(args.key!, \"utf-8\"),\n\t\t});\n\t\tlet tlsOptions: TlsOptions;\n\t\ttry {\n\t\t\ttlsOptions = readTlsOptions();\n\t\t} catch (err) {\n\t\t\tconsole.error(`error: failed to read TLS cert/key: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\tprocess.exit(1);\n\t\t}\n\t\tconst httpsServer = createHttpsServer(tlsOptions, app);\n\t\t// Hot-reload the cert on file change (zero-downtime renewal — e.g. a\n\t\t// systemd timer rewrites the `tailscale cert` files daily). New\n\t\t// connections pick up the new cert; existing ones finish on the old.\n\t\tconst reloadTls = createTlsReloader({\n\t\t\treadTlsOptions,\n\t\t\tsetSecureContext: (options) => httpsServer.setSecureContext(options),\n\t\t\tlog: (message) => console.log(message),\n\t\t\twarn: (message) => console.warn(message),\n\t\t});\n\t\t// Debounce: cert + key are often rewritten in quick succession. Watch\n\t\t// the PARENT DIRECTORY (not each file) and filter by filename: `fs.watch`\n\t\t// follows the inode, so an atomic renewal (write temp + rename(2))\n\t\t// replaces the inode and a file-level watcher goes silent on the new\n\t\t// file after the first reload. A directory watcher survives renames and\n\t\t// reports the changed basename via the `filename` callback argument.\n\t\tconst tlsWatchController = createTlsWatchers({\n\t\t\tcertPath: args.cert,\n\t\t\tkeyPath: args.key,\n\t\t\twatchDirectory: (dir, listener) => watch(dir, listener),\n\t\t\treloadTls,\n\t\t\twarn: (message) => console.warn(message),\n\t\t});\n\t\tclearTlsReloadTimer = tlsWatchController.clearReloadTimer;\n\t\ttlsWatchers.push(...tlsWatchController.watchers);\n\t\tserver = httpsServer;\n\t} else {\n\t\tserver = createHttpServer(app);\n\t}\n\tserver.listen(args.port, host, () => {\n\t\tconsole.log(\n\t\t\t`dreb dashboard listening on ${scheme}://${host === \"0.0.0.0\" ? \"<tailscale-ip>\" : host}:${args.port}`,\n\t\t);\n\t\tif (args.remote) {\n\t\t\tconsole.log(`remote mode: allowed identities = ${args.allow.join(\", \")}`);\n\t\t\tconst { code, expiresInMs } = auth.currentPairingCode();\n\t\t\tconsole.log(\n\t\t\t\t`pairing code: ${code} (rotates every 30s; current code rolls in ${Math.ceil(expiresInMs / 1000)}s)`,\n\t\t\t);\n\t\t\tconsole.log(\"new devices enter this code; see it live in dashboard Settings on the host machine\");\n\t\t} else {\n\t\t\tconsole.log(\"local mode: loopback only — use --remote for Tailscale access\");\n\t\t}\n\t\tif (args.https) {\n\t\t\tconsole.log(\"tls: native HTTPS enabled (cert + key hot-reload on file change)\");\n\t\t} else {\n\t\t\tconst insecureWarning = insecureRemoteWarning(args);\n\t\t\tif (insecureWarning) console.warn(insecureWarning);\n\t\t}\n\t});\n\thttpServer = server;\n\n\tconst shutdown = () => beginShutdown(\"shutting down…\", 0);\n\tprocess.on(\"SIGINT\", shutdown);\n\tprocess.on(\"SIGTERM\", shutdown);\n}\n\n// Only run when executed directly (not when imported for the library exports).\nconst entryPath = process.argv[1];\nif (entryPath && import.meta.url === new URL(`file://${entryPath}`).href) {\n\tmain().catch((err) => {\n\t\tconsole.error(`fatal: ${err instanceof Error ? err.message : String(err)}`);\n\t\tprocess.exit(1);\n\t});\n}\n"]}
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { createServer as createHttpsServer } from "node:https";
|
|
|
16
16
|
import { homedir } from "node:os";
|
|
17
17
|
import { basename, dirname, join } from "node:path";
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { resolveConfiguredDirectory, SessionManager, SettingsManager } from "@dreb/coding-agent";
|
|
19
20
|
import { DashboardAuth } from "./server/auth.js";
|
|
20
21
|
import { DashboardImageService } from "./server/dashboard-images.js";
|
|
21
22
|
import { ImagePreviewWorker } from "./server/image-preview.js";
|
|
@@ -31,6 +32,16 @@ export { FilePairingStorage, loadOrCreateDashboardSecret } from "./server/pairin
|
|
|
31
32
|
export { RuntimePool, resolveDrebCliPath } from "./server/runtime-pool.js";
|
|
32
33
|
export { createDashboardServer, parseDeviceCookie } from "./server/server.js";
|
|
33
34
|
const DEFAULT_PORT = 5343;
|
|
35
|
+
/** Select Dashboard's host-wide main-session root from global settings only. */
|
|
36
|
+
export function resolveDashboardSessionInventoryRoot(settingsManager, startupCwd) {
|
|
37
|
+
return resolveConfiguredDirectory(settingsManager.getGlobalSettings().sessionDir, startupCwd);
|
|
38
|
+
}
|
|
39
|
+
/** Build the startup-snapshotted inventory reader used by the Dashboard server. */
|
|
40
|
+
export function createDashboardSessionLister(customDir, inventory) {
|
|
41
|
+
return customDir !== undefined
|
|
42
|
+
? () => inventory.listAllMetadataFromDir(customDir)
|
|
43
|
+
: () => inventory.listAllMetadata();
|
|
44
|
+
}
|
|
34
45
|
export function parseArgs(argv) {
|
|
35
46
|
const args = { port: DEFAULT_PORT, remote: false, allow: [], help: false, https: false };
|
|
36
47
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -224,6 +235,14 @@ async function main() {
|
|
|
224
235
|
console.log(HELP);
|
|
225
236
|
return;
|
|
226
237
|
}
|
|
238
|
+
const startupCwd = process.cwd();
|
|
239
|
+
const settingsManager = SettingsManager.create(startupCwd);
|
|
240
|
+
for (const { scope, error } of settingsManager.drainErrors()) {
|
|
241
|
+
if (scope === "global")
|
|
242
|
+
console.warn(`[dashboard] Could not read global settings: ${error.message}`);
|
|
243
|
+
}
|
|
244
|
+
const customSessionInventoryRoot = resolveDashboardSessionInventoryRoot(settingsManager, startupCwd);
|
|
245
|
+
const listAllSessions = createDashboardSessionLister(customSessionInventoryRoot, SessionManager);
|
|
227
246
|
const agentDir = join(homedir(), ".dreb", "agent");
|
|
228
247
|
const auth = new DashboardAuth({
|
|
229
248
|
remoteEnabled: args.remote,
|
|
@@ -265,7 +284,6 @@ async function main() {
|
|
|
265
284
|
exit: (code) => process.exit(code),
|
|
266
285
|
});
|
|
267
286
|
const onRestart = () => beginShutdown("restart requested — exiting for supervisor to respawn", 1);
|
|
268
|
-
const { SessionManager } = await import("@dreb/coding-agent");
|
|
269
287
|
const app = createDashboardServer({
|
|
270
288
|
auth,
|
|
271
289
|
pool,
|
|
@@ -273,7 +291,7 @@ async function main() {
|
|
|
273
291
|
staticDir: existsSync(staticDir) ? staticDir : undefined,
|
|
274
292
|
serverVersion,
|
|
275
293
|
onRestart,
|
|
276
|
-
listAllSessions
|
|
294
|
+
listAllSessions,
|
|
277
295
|
deleteSession: async (path) => {
|
|
278
296
|
const result = await SessionManager.deleteSession(path, {});
|
|
279
297
|
if (!result.ok)
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n/**\n * dreb-dashboard — launch the dreb web dashboard server.\n *\n * Modes (exactly two, no LAN mode):\n * default loopback bind (127.0.0.1), no auth, no Tailscale needed\n * --remote requires Tailscale; binds all interfaces but every request\n * passes identity allowlist + pairing code + device cookies\n *\n * Usage:\n * dreb-dashboard [--port 5343] [--remote --allow me@example.com [--allow ...]]\n */\n\nimport { existsSync, readFileSync, watch } from \"node:fs\";\nimport { createServer as createHttpServer, type Server as HttpServer } from \"node:http\";\nimport { createServer as createHttpsServer, type Server as HttpsServer } from \"node:https\";\nimport { homedir } from \"node:os\";\nimport { basename, dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { DashboardAuth } from \"./server/auth.js\";\nimport { DashboardImageService } from \"./server/dashboard-images.js\";\nimport { ImagePreviewWorker } from \"./server/image-preview.js\";\nimport { FilePairingStorage, loadOrCreateDashboardSecret } from \"./server/pairing-storage.js\";\nimport { RuntimePool } from \"./server/runtime-pool.js\";\nimport { createDashboardServer } from \"./server/server.js\";\n\nexport { DashboardAuth, TailscaleStatusResolver, TailscaleWhoisResolver } from \"./server/auth.js\";\nexport { DashboardImageService } from \"./server/dashboard-images.js\";\nexport { EventHub } from \"./server/event-hub.js\";\nexport { canonicalizePath, FileApi, resolveExistingDirectory } from \"./server/files.js\";\nexport { ImagePreviewWorker } from \"./server/image-preview.js\";\nexport { FilePairingStorage, loadOrCreateDashboardSecret } from \"./server/pairing-storage.js\";\nexport { RuntimePool, resolveDrebCliPath } from \"./server/runtime-pool.js\";\nexport { createDashboardServer, parseDeviceCookie } from \"./server/server.js\";\nexport type * from \"./shared/protocol.js\";\n\nconst DEFAULT_PORT = 5343;\n\ninterface CliArgs {\n\tport: number;\n\tremote: boolean;\n\tallow: string[];\n\thelp: boolean;\n\thttps: boolean;\n\tcert?: string;\n\tkey?: string;\n}\n\nexport function parseArgs(argv: string[]): CliArgs {\n\tconst args: CliArgs = { port: DEFAULT_PORT, remote: false, allow: [], help: false, https: false };\n\tfor (let i = 0; i < argv.length; i++) {\n\t\tconst arg = argv[i];\n\t\tif (arg === \"--port\") {\n\t\t\tconst value = argv[++i];\n\t\t\tconst port = Number.parseInt(value ?? \"\", 10);\n\t\t\tif (!Number.isInteger(port) || port < 1 || port > 65535) {\n\t\t\t\tthrow new Error(`Invalid --port value: ${value}`);\n\t\t\t}\n\t\t\targs.port = port;\n\t\t} else if (arg === \"--remote\") {\n\t\t\targs.remote = true;\n\t\t} else if (arg === \"--allow\") {\n\t\t\tconst value = argv[++i];\n\t\t\tif (!value) throw new Error(\"--allow requires an identity (Tailscale login name)\");\n\t\t\targs.allow.push(value);\n\t\t} else if (arg === \"--https\") {\n\t\t\targs.https = true;\n\t\t} else if (arg === \"--cert\") {\n\t\t\tconst value = argv[++i];\n\t\t\tif (!value) throw new Error(\"--cert requires a path to a PEM certificate file\");\n\t\t\targs.cert = value;\n\t\t} else if (arg === \"--key\") {\n\t\t\tconst value = argv[++i];\n\t\t\tif (!value) throw new Error(\"--key requires a path to a PEM private key file\");\n\t\t\targs.key = value;\n\t\t} else if (arg === \"--help\" || arg === \"-h\") {\n\t\t\targs.help = true;\n\t\t} else {\n\t\t\tthrow new Error(`Unknown argument: ${arg}`);\n\t\t}\n\t}\n\tif (args.remote && args.allow.length === 0) {\n\t\tthrow new Error(\"--remote requires at least one --allow <tailscale-login> (empty allowlist denies everyone)\");\n\t}\n\t// Native TLS. `https` is opt-in; it requires cert AND key (no silent\n\t// fallback to plain HTTP). The dashboard terminates TLS itself — no reverse\n\t// proxy, no auth-model change. `req.socket.remoteAddress` stays the real\n\t// tailnet IP, so identity resolution + allowlist + pairing keep working.\n\tif (args.https) {\n\t\tif (!args.cert) throw new Error(\"--https requires --cert <path>\");\n\t\tif (!args.key) throw new Error(\"--https requires --key <path>\");\n\t}\n\treturn args;\n}\n\n/**\n * Warning shown at startup when `--remote` binds the tailnet over plain HTTP\n * (no `--https`). Browsers treat plain HTTP over a non-loopback host as an\n * INSECURE context, so service workers and the Notification API are\n * unavailable — PWA install and mobile notifications (iOS exposes no\n * Notification API at all there) silently never work. Loopback (127.0.0.1) is\n * exempt (it counts as secure), so pure-local mode returns no warning.\n *\n * Returns the multi-line warning string, or `undefined` when TLS is enabled or\n * the server is loopback-only. Kept pure (no console) so it is unit-testable.\n */\nexport function insecureRemoteWarning(args: Pick<CliArgs, \"remote\" | \"https\">): string | undefined {\n\tif (!args.remote || args.https) return undefined;\n\treturn (\n\t\t\"⚠ warning: --remote without --https serves plain HTTP over the tailnet, which browsers treat as an\\n\" +\n\t\t\" INSECURE context — service workers, PWA install, and mobile notifications (especially iOS) will NOT work.\\n\" +\n\t\t\" Enable TLS for the tailnet hostname:\\n\" +\n\t\t\" tailscale cert <host>.<tailnet>.ts.net\\n\" +\n\t\t\" …then relaunch with --https --cert <host>.<tailnet>.ts.net.crt --key <host>.<tailnet>.ts.net.key\\n\" +\n\t\t\" and open the dashboard via https://<host>.<tailnet>.ts.net (the hostname, not the tailnet IP).\"\n\t);\n}\n\ntype ShutdownWatcher = Pick<ReturnType<typeof watch>, \"close\">;\n\ninterface ShutdownDeps {\n\tlog: (message: string) => void;\n\tclearReloadTimer: () => void;\n\twatchers: Iterable<ShutdownWatcher>;\n\tcloseServer: () => void;\n\tstopAll: () => Promise<unknown>;\n\texit: (code: number) => void;\n}\n\nexport function createShutdown(deps: ShutdownDeps): (message: string, exitCode: number) => void {\n\tlet shuttingDown = false;\n\treturn (message: string, exitCode: number) => {\n\t\tif (shuttingDown) return;\n\t\tshuttingDown = true;\n\t\tdeps.log(message);\n\t\tdeps.clearReloadTimer();\n\t\tfor (const watcher of deps.watchers) watcher.close();\n\t\tdeps.closeServer();\n\t\tvoid deps\n\t\t\t.stopAll()\n\t\t\t.catch((err) => {\n\t\t\t\tdeps.log(`shutdown teardown failed: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t})\n\t\t\t.finally(() => deps.exit(exitCode));\n\t};\n}\n\ntype TlsOptions = { cert: string; key: string };\ntype TlsWatchFilename = string | Buffer | null;\ntype TlsWatchListener = (event: string, filename: TlsWatchFilename) => void;\ntype TlsFileWatcher = ShutdownWatcher & {\n\ton(event: \"error\", listener: (err: unknown) => void): unknown;\n};\n\ninterface TlsReloaderDeps {\n\treadTlsOptions: () => TlsOptions;\n\tsetSecureContext: (options: TlsOptions) => void;\n\tlog: (message: string) => void;\n\twarn: (message: string) => void;\n}\n\nexport function createTlsReloader(deps: TlsReloaderDeps): () => void {\n\treturn () => {\n\t\ttry {\n\t\t\tdeps.setSecureContext(deps.readTlsOptions());\n\t\t\tdeps.log(\"tls certificate reloaded\");\n\t\t} catch (err) {\n\t\t\tdeps.warn(`tls reload failed (keeping old cert): ${err instanceof Error ? err.message : String(err)}`);\n\t\t}\n\t};\n}\n\nexport function shouldReloadForFile(filename: TlsWatchFilename, certBase: string, keyBase: string): boolean {\n\tif (filename == null) return true;\n\tif (typeof filename !== \"string\") return false;\n\treturn filename === certBase || filename === keyBase;\n}\n\ninterface TlsWatchDeps {\n\tcertPath: string;\n\tkeyPath: string;\n\twatchDirectory: (directory: string, listener: TlsWatchListener) => TlsFileWatcher;\n\treloadTls: () => void;\n\twarn: (message: string) => void;\n\tdebounceMs?: number;\n}\n\ninterface TlsWatchController {\n\twatchers: TlsFileWatcher[];\n\tclearReloadTimer: () => void;\n}\n\nexport function createTlsWatchers(deps: TlsWatchDeps): TlsWatchController {\n\tlet reloadTimer: ReturnType<typeof setTimeout> | undefined;\n\tconst watchers: TlsFileWatcher[] = [];\n\tconst debounceMs = deps.debounceMs ?? 500;\n\tconst certBase = basename(deps.certPath);\n\tconst keyBase = basename(deps.keyPath);\n\tconst clearReloadTimer = () => {\n\t\tif (!reloadTimer) return;\n\t\tclearTimeout(reloadTimer);\n\t\treloadTimer = undefined;\n\t};\n\tconst scheduleReload = () => {\n\t\tclearReloadTimer();\n\t\treloadTimer = setTimeout(deps.reloadTls, debounceMs);\n\t};\n\n\tfor (const dir of new Set([dirname(deps.certPath), dirname(deps.keyPath)])) {\n\t\ttry {\n\t\t\tconst watcher = deps.watchDirectory(dir, (_event, filename) => {\n\t\t\t\t// `fs.watch` directory events on macOS (FSEvents) frequently fire\n\t\t\t\t// with `filename === null`; fall through and reload anyway.\n\t\t\t\t// On Linux (inotify) the basename is carried reliably and filters\n\t\t\t\t// out unrelated files in the parent directory.\n\t\t\t\tif (!shouldReloadForFile(filename, certBase, keyBase)) return;\n\t\t\t\tscheduleReload();\n\t\t\t});\n\t\t\twatcher.on(\"error\", (err) => {\n\t\t\t\tdeps.warn(`tls cert watch error: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t});\n\t\t\twatchers.push(watcher);\n\t\t} catch (err) {\n\t\t\tdeps.warn(`tls cert watch setup failed for ${dir}: ${err instanceof Error ? err.message : String(err)}`);\n\t\t}\n\t}\n\n\treturn { watchers, clearReloadTimer };\n}\n\nconst HELP = `dreb-dashboard — dreb web dashboard server\n\nUsage: dreb-dashboard [options]\n\nOptions:\n --port <n> Port to listen on (default ${DEFAULT_PORT})\n --remote Enable remote mode (requires Tailscale). Without this\n flag the server binds 127.0.0.1 only — no LAN access.\n Plain HTTP over the tailnet is an INSECURE context: add\n --https (below) or mobile PWA install + notifications\n (especially iOS) will not work.\n --allow <identity> Tailscale login name allowed to pair (repeatable;\n required with --remote)\n --https Terminate TLS on the dashboard itself (native TLS). No\n reverse proxy, no auth-model change. Requires --cert\n and --key. Mainly for --remote (loopback is already a\n secure context): use 'tailscale cert' files for a\n tailnet hostname so mobile PWAs + notifications work.\n NOTE: with --https the server speaks TLS only, so the\n host's plain-http local tab (http://127.0.0.1) stops\n working — use the tailnet hostname (https://...) there.\n --cert <path> PEM certificate file (required with --https)\n --key <path> PEM private key file (required with --https)\n --help Show this help\n`;\n\nasync function main(): Promise<void> {\n\tlet args: CliArgs;\n\ttry {\n\t\targs = parseArgs(process.argv.slice(2));\n\t} catch (err) {\n\t\tconsole.error(`error: ${err instanceof Error ? err.message : String(err)}`);\n\t\tconsole.error(HELP);\n\t\tprocess.exit(1);\n\t}\n\tif (args.help) {\n\t\tconsole.log(HELP);\n\t\treturn;\n\t}\n\n\tconst agentDir = join(homedir(), \".dreb\", \"agent\");\n\tconst auth = new DashboardAuth({\n\t\tremoteEnabled: args.remote,\n\t\tallowedIdentities: args.allow,\n\t\tstorage: new FilePairingStorage(join(agentDir, \"dashboard-pairings.json\")),\n\t\tsecret: loadOrCreateDashboardSecret(join(agentDir, \"dashboard-auth-secret\")),\n\t\tlogger: (line) => console.warn(`[dashboard-auth] ${line}`),\n\t});\n\tconst pool = new RuntimePool();\n\tconst imageService = new DashboardImageService(new ImagePreviewWorker());\n\n\t// Static client assets live next to the compiled server (dist/static).\n\tconst staticDir = join(dirname(fileURLToPath(import.meta.url)), \"static\");\n\n\t// The server's own build version (dist/index.js → ../package.json) — surfaced\n\t// in the settings footer so a stale long-running service is spottable.\n\tlet serverVersion: string | undefined;\n\ttry {\n\t\tconst pkgPath = join(dirname(fileURLToPath(import.meta.url)), \"..\", \"package.json\");\n\t\tserverVersion = JSON.parse(readFileSync(pkgPath, \"utf8\")).version;\n\t} catch {\n\t\tserverVersion = undefined;\n\t}\n\n\t// Restart hook: exit non-zero so a supervisor (systemd Restart=on-failure,\n\t// etc.) respawns the process with the freshly-built dist. Assigned the http\n\t// server below via a mutable holder so the closure can close it first.\n\t// The TLS hot-reload debounce timer and directory watchers are lifted here\n\t// too, so restart and signal shutdown can release them (otherwise they leak\n\t// across restart/exit).\n\tlet httpServer: HttpServer | HttpsServer | undefined;\n\tlet clearTlsReloadTimer = () => {};\n\tlet closeDashboard = () => imageService.close();\n\tconst tlsWatchers: TlsFileWatcher[] = [];\n\tconst beginShutdown = createShutdown({\n\t\tlog: (message) => console.log(message),\n\t\tclearReloadTimer: () => clearTlsReloadTimer(),\n\t\twatchers: tlsWatchers,\n\t\tcloseServer: () => httpServer?.close(),\n\t\tstopAll: () => Promise.all([pool.stopAll(), closeDashboard()]),\n\t\texit: (code) => process.exit(code),\n\t});\n\tconst onRestart = () => beginShutdown(\"restart requested — exiting for supervisor to respawn\", 1);\n\n\tconst { SessionManager } = await import(\"@dreb/coding-agent\");\n\tconst app = createDashboardServer({\n\t\tauth,\n\t\tpool,\n\t\timageService,\n\t\tstaticDir: existsSync(staticDir) ? staticDir : undefined,\n\t\tserverVersion,\n\t\tonRestart,\n\t\tlistAllSessions: () => SessionManager.listAll(),\n\t\tdeleteSession: async (path: string) => {\n\t\t\tconst result = await SessionManager.deleteSession(path, {});\n\t\t\tif (!result.ok) throw new Error(result.error ?? \"Unknown deletion error\");\n\t\t\treturn { method: result.method };\n\t\t},\n\t});\n\tcloseDashboard = () => app.closeDashboard();\n\n\tconst host = args.remote ? \"0.0.0.0\" : \"127.0.0.1\";\n\tconst scheme = args.https ? \"https\" : \"http\";\n\n\t// Native TLS: the dashboard terminates TLS itself (no reverse proxy). The\n\t// auth model is unchanged — `req.socket.remoteAddress` is still the real\n\t// tailnet IP, so Tailscale identity resolution + allowlist + pairing keep\n\t// working. Local mode never sets --https (loopback is already a secure\n\t// context), but it's allowed if the operator wants it.\n\tlet server: HttpServer | HttpsServer;\n\tif (args.https && args.cert && args.key) {\n\t\tconst readTlsOptions = () => ({\n\t\t\tcert: readFileSync(args.cert!, \"utf-8\"),\n\t\t\tkey: readFileSync(args.key!, \"utf-8\"),\n\t\t});\n\t\tlet tlsOptions: TlsOptions;\n\t\ttry {\n\t\t\ttlsOptions = readTlsOptions();\n\t\t} catch (err) {\n\t\t\tconsole.error(`error: failed to read TLS cert/key: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\tprocess.exit(1);\n\t\t}\n\t\tconst httpsServer = createHttpsServer(tlsOptions, app);\n\t\t// Hot-reload the cert on file change (zero-downtime renewal — e.g. a\n\t\t// systemd timer rewrites the `tailscale cert` files daily). New\n\t\t// connections pick up the new cert; existing ones finish on the old.\n\t\tconst reloadTls = createTlsReloader({\n\t\t\treadTlsOptions,\n\t\t\tsetSecureContext: (options) => httpsServer.setSecureContext(options),\n\t\t\tlog: (message) => console.log(message),\n\t\t\twarn: (message) => console.warn(message),\n\t\t});\n\t\t// Debounce: cert + key are often rewritten in quick succession. Watch\n\t\t// the PARENT DIRECTORY (not each file) and filter by filename: `fs.watch`\n\t\t// follows the inode, so an atomic renewal (write temp + rename(2))\n\t\t// replaces the inode and a file-level watcher goes silent on the new\n\t\t// file after the first reload. A directory watcher survives renames and\n\t\t// reports the changed basename via the `filename` callback argument.\n\t\tconst tlsWatchController = createTlsWatchers({\n\t\t\tcertPath: args.cert,\n\t\t\tkeyPath: args.key,\n\t\t\twatchDirectory: (dir, listener) => watch(dir, listener),\n\t\t\treloadTls,\n\t\t\twarn: (message) => console.warn(message),\n\t\t});\n\t\tclearTlsReloadTimer = tlsWatchController.clearReloadTimer;\n\t\ttlsWatchers.push(...tlsWatchController.watchers);\n\t\tserver = httpsServer;\n\t} else {\n\t\tserver = createHttpServer(app);\n\t}\n\tserver.listen(args.port, host, () => {\n\t\tconsole.log(\n\t\t\t`dreb dashboard listening on ${scheme}://${host === \"0.0.0.0\" ? \"<tailscale-ip>\" : host}:${args.port}`,\n\t\t);\n\t\tif (args.remote) {\n\t\t\tconsole.log(`remote mode: allowed identities = ${args.allow.join(\", \")}`);\n\t\t\tconst { code, expiresInMs } = auth.currentPairingCode();\n\t\t\tconsole.log(\n\t\t\t\t`pairing code: ${code} (rotates every 30s; current code rolls in ${Math.ceil(expiresInMs / 1000)}s)`,\n\t\t\t);\n\t\t\tconsole.log(\"new devices enter this code; see it live in dashboard Settings on the host machine\");\n\t\t} else {\n\t\t\tconsole.log(\"local mode: loopback only — use --remote for Tailscale access\");\n\t\t}\n\t\tif (args.https) {\n\t\t\tconsole.log(\"tls: native HTTPS enabled (cert + key hot-reload on file change)\");\n\t\t} else {\n\t\t\tconst insecureWarning = insecureRemoteWarning(args);\n\t\t\tif (insecureWarning) console.warn(insecureWarning);\n\t\t}\n\t});\n\thttpServer = server;\n\n\tconst shutdown = () => beginShutdown(\"shutting down…\", 0);\n\tprocess.on(\"SIGINT\", shutdown);\n\tprocess.on(\"SIGTERM\", shutdown);\n}\n\n// Only run when executed directly (not when imported for the library exports).\nconst entryPath = process.argv[1];\nif (entryPath && import.meta.url === new URL(`file://${entryPath}`).href) {\n\tmain().catch((err) => {\n\t\tconsole.error(`fatal: ${err instanceof Error ? err.message : String(err)}`);\n\t\tprocess.exit(1);\n\t});\n}\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n/**\n * dreb-dashboard — launch the dreb web dashboard server.\n *\n * Modes (exactly two, no LAN mode):\n * default loopback bind (127.0.0.1), no auth, no Tailscale needed\n * --remote requires Tailscale; binds all interfaces but every request\n * passes identity allowlist + pairing code + device cookies\n *\n * Usage:\n * dreb-dashboard [--port 5343] [--remote --allow me@example.com [--allow ...]]\n */\n\nimport { existsSync, readFileSync, watch } from \"node:fs\";\nimport { createServer as createHttpServer, type Server as HttpServer } from \"node:http\";\nimport { createServer as createHttpsServer, type Server as HttpsServer } from \"node:https\";\nimport { homedir } from \"node:os\";\nimport { basename, dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { resolveConfiguredDirectory, SessionManager, SettingsManager } from \"@dreb/coding-agent\";\nimport { DashboardAuth } from \"./server/auth.js\";\nimport { DashboardImageService } from \"./server/dashboard-images.js\";\nimport { ImagePreviewWorker } from \"./server/image-preview.js\";\nimport { FilePairingStorage, loadOrCreateDashboardSecret } from \"./server/pairing-storage.js\";\nimport { RuntimePool } from \"./server/runtime-pool.js\";\nimport { createDashboardServer } from \"./server/server.js\";\n\nexport { DashboardAuth, TailscaleStatusResolver, TailscaleWhoisResolver } from \"./server/auth.js\";\nexport { DashboardImageService } from \"./server/dashboard-images.js\";\nexport { EventHub } from \"./server/event-hub.js\";\nexport { canonicalizePath, FileApi, resolveExistingDirectory } from \"./server/files.js\";\nexport { ImagePreviewWorker } from \"./server/image-preview.js\";\nexport { FilePairingStorage, loadOrCreateDashboardSecret } from \"./server/pairing-storage.js\";\nexport { RuntimePool, resolveDrebCliPath } from \"./server/runtime-pool.js\";\nexport { createDashboardServer, parseDeviceCookie } from \"./server/server.js\";\nexport type * from \"./shared/protocol.js\";\n\nconst DEFAULT_PORT = 5343;\n\n/** Select Dashboard's host-wide main-session root from global settings only. */\nexport function resolveDashboardSessionInventoryRoot(\n\tsettingsManager: Pick<SettingsManager, \"getGlobalSettings\">,\n\tstartupCwd: string,\n): string | undefined {\n\treturn resolveConfiguredDirectory(settingsManager.getGlobalSettings().sessionDir, startupCwd);\n}\n\ninterface DashboardSessionInventory<T> {\n\tlistAllMetadata(): Promise<T[]>;\n\tlistAllMetadataFromDir(dir: string): Promise<T[]>;\n}\n\n/** Build the startup-snapshotted inventory reader used by the Dashboard server. */\nexport function createDashboardSessionLister<T>(\n\tcustomDir: string | undefined,\n\tinventory: DashboardSessionInventory<T>,\n): () => Promise<T[]> {\n\treturn customDir !== undefined\n\t\t? () => inventory.listAllMetadataFromDir(customDir)\n\t\t: () => inventory.listAllMetadata();\n}\n\ninterface CliArgs {\n\tport: number;\n\tremote: boolean;\n\tallow: string[];\n\thelp: boolean;\n\thttps: boolean;\n\tcert?: string;\n\tkey?: string;\n}\n\nexport function parseArgs(argv: string[]): CliArgs {\n\tconst args: CliArgs = { port: DEFAULT_PORT, remote: false, allow: [], help: false, https: false };\n\tfor (let i = 0; i < argv.length; i++) {\n\t\tconst arg = argv[i];\n\t\tif (arg === \"--port\") {\n\t\t\tconst value = argv[++i];\n\t\t\tconst port = Number.parseInt(value ?? \"\", 10);\n\t\t\tif (!Number.isInteger(port) || port < 1 || port > 65535) {\n\t\t\t\tthrow new Error(`Invalid --port value: ${value}`);\n\t\t\t}\n\t\t\targs.port = port;\n\t\t} else if (arg === \"--remote\") {\n\t\t\targs.remote = true;\n\t\t} else if (arg === \"--allow\") {\n\t\t\tconst value = argv[++i];\n\t\t\tif (!value) throw new Error(\"--allow requires an identity (Tailscale login name)\");\n\t\t\targs.allow.push(value);\n\t\t} else if (arg === \"--https\") {\n\t\t\targs.https = true;\n\t\t} else if (arg === \"--cert\") {\n\t\t\tconst value = argv[++i];\n\t\t\tif (!value) throw new Error(\"--cert requires a path to a PEM certificate file\");\n\t\t\targs.cert = value;\n\t\t} else if (arg === \"--key\") {\n\t\t\tconst value = argv[++i];\n\t\t\tif (!value) throw new Error(\"--key requires a path to a PEM private key file\");\n\t\t\targs.key = value;\n\t\t} else if (arg === \"--help\" || arg === \"-h\") {\n\t\t\targs.help = true;\n\t\t} else {\n\t\t\tthrow new Error(`Unknown argument: ${arg}`);\n\t\t}\n\t}\n\tif (args.remote && args.allow.length === 0) {\n\t\tthrow new Error(\"--remote requires at least one --allow <tailscale-login> (empty allowlist denies everyone)\");\n\t}\n\t// Native TLS. `https` is opt-in; it requires cert AND key (no silent\n\t// fallback to plain HTTP). The dashboard terminates TLS itself — no reverse\n\t// proxy, no auth-model change. `req.socket.remoteAddress` stays the real\n\t// tailnet IP, so identity resolution + allowlist + pairing keep working.\n\tif (args.https) {\n\t\tif (!args.cert) throw new Error(\"--https requires --cert <path>\");\n\t\tif (!args.key) throw new Error(\"--https requires --key <path>\");\n\t}\n\treturn args;\n}\n\n/**\n * Warning shown at startup when `--remote` binds the tailnet over plain HTTP\n * (no `--https`). Browsers treat plain HTTP over a non-loopback host as an\n * INSECURE context, so service workers and the Notification API are\n * unavailable — PWA install and mobile notifications (iOS exposes no\n * Notification API at all there) silently never work. Loopback (127.0.0.1) is\n * exempt (it counts as secure), so pure-local mode returns no warning.\n *\n * Returns the multi-line warning string, or `undefined` when TLS is enabled or\n * the server is loopback-only. Kept pure (no console) so it is unit-testable.\n */\nexport function insecureRemoteWarning(args: Pick<CliArgs, \"remote\" | \"https\">): string | undefined {\n\tif (!args.remote || args.https) return undefined;\n\treturn (\n\t\t\"⚠ warning: --remote without --https serves plain HTTP over the tailnet, which browsers treat as an\\n\" +\n\t\t\" INSECURE context — service workers, PWA install, and mobile notifications (especially iOS) will NOT work.\\n\" +\n\t\t\" Enable TLS for the tailnet hostname:\\n\" +\n\t\t\" tailscale cert <host>.<tailnet>.ts.net\\n\" +\n\t\t\" …then relaunch with --https --cert <host>.<tailnet>.ts.net.crt --key <host>.<tailnet>.ts.net.key\\n\" +\n\t\t\" and open the dashboard via https://<host>.<tailnet>.ts.net (the hostname, not the tailnet IP).\"\n\t);\n}\n\ntype ShutdownWatcher = Pick<ReturnType<typeof watch>, \"close\">;\n\ninterface ShutdownDeps {\n\tlog: (message: string) => void;\n\tclearReloadTimer: () => void;\n\twatchers: Iterable<ShutdownWatcher>;\n\tcloseServer: () => void;\n\tstopAll: () => Promise<unknown>;\n\texit: (code: number) => void;\n}\n\nexport function createShutdown(deps: ShutdownDeps): (message: string, exitCode: number) => void {\n\tlet shuttingDown = false;\n\treturn (message: string, exitCode: number) => {\n\t\tif (shuttingDown) return;\n\t\tshuttingDown = true;\n\t\tdeps.log(message);\n\t\tdeps.clearReloadTimer();\n\t\tfor (const watcher of deps.watchers) watcher.close();\n\t\tdeps.closeServer();\n\t\tvoid deps\n\t\t\t.stopAll()\n\t\t\t.catch((err) => {\n\t\t\t\tdeps.log(`shutdown teardown failed: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t})\n\t\t\t.finally(() => deps.exit(exitCode));\n\t};\n}\n\ntype TlsOptions = { cert: string; key: string };\ntype TlsWatchFilename = string | Buffer | null;\ntype TlsWatchListener = (event: string, filename: TlsWatchFilename) => void;\ntype TlsFileWatcher = ShutdownWatcher & {\n\ton(event: \"error\", listener: (err: unknown) => void): unknown;\n};\n\ninterface TlsReloaderDeps {\n\treadTlsOptions: () => TlsOptions;\n\tsetSecureContext: (options: TlsOptions) => void;\n\tlog: (message: string) => void;\n\twarn: (message: string) => void;\n}\n\nexport function createTlsReloader(deps: TlsReloaderDeps): () => void {\n\treturn () => {\n\t\ttry {\n\t\t\tdeps.setSecureContext(deps.readTlsOptions());\n\t\t\tdeps.log(\"tls certificate reloaded\");\n\t\t} catch (err) {\n\t\t\tdeps.warn(`tls reload failed (keeping old cert): ${err instanceof Error ? err.message : String(err)}`);\n\t\t}\n\t};\n}\n\nexport function shouldReloadForFile(filename: TlsWatchFilename, certBase: string, keyBase: string): boolean {\n\tif (filename == null) return true;\n\tif (typeof filename !== \"string\") return false;\n\treturn filename === certBase || filename === keyBase;\n}\n\ninterface TlsWatchDeps {\n\tcertPath: string;\n\tkeyPath: string;\n\twatchDirectory: (directory: string, listener: TlsWatchListener) => TlsFileWatcher;\n\treloadTls: () => void;\n\twarn: (message: string) => void;\n\tdebounceMs?: number;\n}\n\ninterface TlsWatchController {\n\twatchers: TlsFileWatcher[];\n\tclearReloadTimer: () => void;\n}\n\nexport function createTlsWatchers(deps: TlsWatchDeps): TlsWatchController {\n\tlet reloadTimer: ReturnType<typeof setTimeout> | undefined;\n\tconst watchers: TlsFileWatcher[] = [];\n\tconst debounceMs = deps.debounceMs ?? 500;\n\tconst certBase = basename(deps.certPath);\n\tconst keyBase = basename(deps.keyPath);\n\tconst clearReloadTimer = () => {\n\t\tif (!reloadTimer) return;\n\t\tclearTimeout(reloadTimer);\n\t\treloadTimer = undefined;\n\t};\n\tconst scheduleReload = () => {\n\t\tclearReloadTimer();\n\t\treloadTimer = setTimeout(deps.reloadTls, debounceMs);\n\t};\n\n\tfor (const dir of new Set([dirname(deps.certPath), dirname(deps.keyPath)])) {\n\t\ttry {\n\t\t\tconst watcher = deps.watchDirectory(dir, (_event, filename) => {\n\t\t\t\t// `fs.watch` directory events on macOS (FSEvents) frequently fire\n\t\t\t\t// with `filename === null`; fall through and reload anyway.\n\t\t\t\t// On Linux (inotify) the basename is carried reliably and filters\n\t\t\t\t// out unrelated files in the parent directory.\n\t\t\t\tif (!shouldReloadForFile(filename, certBase, keyBase)) return;\n\t\t\t\tscheduleReload();\n\t\t\t});\n\t\t\twatcher.on(\"error\", (err) => {\n\t\t\t\tdeps.warn(`tls cert watch error: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t});\n\t\t\twatchers.push(watcher);\n\t\t} catch (err) {\n\t\t\tdeps.warn(`tls cert watch setup failed for ${dir}: ${err instanceof Error ? err.message : String(err)}`);\n\t\t}\n\t}\n\n\treturn { watchers, clearReloadTimer };\n}\n\nconst HELP = `dreb-dashboard — dreb web dashboard server\n\nUsage: dreb-dashboard [options]\n\nOptions:\n --port <n> Port to listen on (default ${DEFAULT_PORT})\n --remote Enable remote mode (requires Tailscale). Without this\n flag the server binds 127.0.0.1 only — no LAN access.\n Plain HTTP over the tailnet is an INSECURE context: add\n --https (below) or mobile PWA install + notifications\n (especially iOS) will not work.\n --allow <identity> Tailscale login name allowed to pair (repeatable;\n required with --remote)\n --https Terminate TLS on the dashboard itself (native TLS). No\n reverse proxy, no auth-model change. Requires --cert\n and --key. Mainly for --remote (loopback is already a\n secure context): use 'tailscale cert' files for a\n tailnet hostname so mobile PWAs + notifications work.\n NOTE: with --https the server speaks TLS only, so the\n host's plain-http local tab (http://127.0.0.1) stops\n working — use the tailnet hostname (https://...) there.\n --cert <path> PEM certificate file (required with --https)\n --key <path> PEM private key file (required with --https)\n --help Show this help\n`;\n\nasync function main(): Promise<void> {\n\tlet args: CliArgs;\n\ttry {\n\t\targs = parseArgs(process.argv.slice(2));\n\t} catch (err) {\n\t\tconsole.error(`error: ${err instanceof Error ? err.message : String(err)}`);\n\t\tconsole.error(HELP);\n\t\tprocess.exit(1);\n\t}\n\tif (args.help) {\n\t\tconsole.log(HELP);\n\t\treturn;\n\t}\n\n\tconst startupCwd = process.cwd();\n\tconst settingsManager = SettingsManager.create(startupCwd);\n\tfor (const { scope, error } of settingsManager.drainErrors()) {\n\t\tif (scope === \"global\") console.warn(`[dashboard] Could not read global settings: ${error.message}`);\n\t}\n\tconst customSessionInventoryRoot = resolveDashboardSessionInventoryRoot(settingsManager, startupCwd);\n\tconst listAllSessions = createDashboardSessionLister(customSessionInventoryRoot, SessionManager);\n\n\tconst agentDir = join(homedir(), \".dreb\", \"agent\");\n\tconst auth = new DashboardAuth({\n\t\tremoteEnabled: args.remote,\n\t\tallowedIdentities: args.allow,\n\t\tstorage: new FilePairingStorage(join(agentDir, \"dashboard-pairings.json\")),\n\t\tsecret: loadOrCreateDashboardSecret(join(agentDir, \"dashboard-auth-secret\")),\n\t\tlogger: (line) => console.warn(`[dashboard-auth] ${line}`),\n\t});\n\tconst pool = new RuntimePool();\n\tconst imageService = new DashboardImageService(new ImagePreviewWorker());\n\n\t// Static client assets live next to the compiled server (dist/static).\n\tconst staticDir = join(dirname(fileURLToPath(import.meta.url)), \"static\");\n\n\t// The server's own build version (dist/index.js → ../package.json) — surfaced\n\t// in the settings footer so a stale long-running service is spottable.\n\tlet serverVersion: string | undefined;\n\ttry {\n\t\tconst pkgPath = join(dirname(fileURLToPath(import.meta.url)), \"..\", \"package.json\");\n\t\tserverVersion = JSON.parse(readFileSync(pkgPath, \"utf8\")).version;\n\t} catch {\n\t\tserverVersion = undefined;\n\t}\n\n\t// Restart hook: exit non-zero so a supervisor (systemd Restart=on-failure,\n\t// etc.) respawns the process with the freshly-built dist. Assigned the http\n\t// server below via a mutable holder so the closure can close it first.\n\t// The TLS hot-reload debounce timer and directory watchers are lifted here\n\t// too, so restart and signal shutdown can release them (otherwise they leak\n\t// across restart/exit).\n\tlet httpServer: HttpServer | HttpsServer | undefined;\n\tlet clearTlsReloadTimer = () => {};\n\tlet closeDashboard = () => imageService.close();\n\tconst tlsWatchers: TlsFileWatcher[] = [];\n\tconst beginShutdown = createShutdown({\n\t\tlog: (message) => console.log(message),\n\t\tclearReloadTimer: () => clearTlsReloadTimer(),\n\t\twatchers: tlsWatchers,\n\t\tcloseServer: () => httpServer?.close(),\n\t\tstopAll: () => Promise.all([pool.stopAll(), closeDashboard()]),\n\t\texit: (code) => process.exit(code),\n\t});\n\tconst onRestart = () => beginShutdown(\"restart requested — exiting for supervisor to respawn\", 1);\n\n\tconst app = createDashboardServer({\n\t\tauth,\n\t\tpool,\n\t\timageService,\n\t\tstaticDir: existsSync(staticDir) ? staticDir : undefined,\n\t\tserverVersion,\n\t\tonRestart,\n\t\tlistAllSessions,\n\t\tdeleteSession: async (path: string) => {\n\t\t\tconst result = await SessionManager.deleteSession(path, {});\n\t\t\tif (!result.ok) throw new Error(result.error ?? \"Unknown deletion error\");\n\t\t\treturn { method: result.method };\n\t\t},\n\t});\n\tcloseDashboard = () => app.closeDashboard();\n\n\tconst host = args.remote ? \"0.0.0.0\" : \"127.0.0.1\";\n\tconst scheme = args.https ? \"https\" : \"http\";\n\n\t// Native TLS: the dashboard terminates TLS itself (no reverse proxy). The\n\t// auth model is unchanged — `req.socket.remoteAddress` is still the real\n\t// tailnet IP, so Tailscale identity resolution + allowlist + pairing keep\n\t// working. Local mode never sets --https (loopback is already a secure\n\t// context), but it's allowed if the operator wants it.\n\tlet server: HttpServer | HttpsServer;\n\tif (args.https && args.cert && args.key) {\n\t\tconst readTlsOptions = () => ({\n\t\t\tcert: readFileSync(args.cert!, \"utf-8\"),\n\t\t\tkey: readFileSync(args.key!, \"utf-8\"),\n\t\t});\n\t\tlet tlsOptions: TlsOptions;\n\t\ttry {\n\t\t\ttlsOptions = readTlsOptions();\n\t\t} catch (err) {\n\t\t\tconsole.error(`error: failed to read TLS cert/key: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\tprocess.exit(1);\n\t\t}\n\t\tconst httpsServer = createHttpsServer(tlsOptions, app);\n\t\t// Hot-reload the cert on file change (zero-downtime renewal — e.g. a\n\t\t// systemd timer rewrites the `tailscale cert` files daily). New\n\t\t// connections pick up the new cert; existing ones finish on the old.\n\t\tconst reloadTls = createTlsReloader({\n\t\t\treadTlsOptions,\n\t\t\tsetSecureContext: (options) => httpsServer.setSecureContext(options),\n\t\t\tlog: (message) => console.log(message),\n\t\t\twarn: (message) => console.warn(message),\n\t\t});\n\t\t// Debounce: cert + key are often rewritten in quick succession. Watch\n\t\t// the PARENT DIRECTORY (not each file) and filter by filename: `fs.watch`\n\t\t// follows the inode, so an atomic renewal (write temp + rename(2))\n\t\t// replaces the inode and a file-level watcher goes silent on the new\n\t\t// file after the first reload. A directory watcher survives renames and\n\t\t// reports the changed basename via the `filename` callback argument.\n\t\tconst tlsWatchController = createTlsWatchers({\n\t\t\tcertPath: args.cert,\n\t\t\tkeyPath: args.key,\n\t\t\twatchDirectory: (dir, listener) => watch(dir, listener),\n\t\t\treloadTls,\n\t\t\twarn: (message) => console.warn(message),\n\t\t});\n\t\tclearTlsReloadTimer = tlsWatchController.clearReloadTimer;\n\t\ttlsWatchers.push(...tlsWatchController.watchers);\n\t\tserver = httpsServer;\n\t} else {\n\t\tserver = createHttpServer(app);\n\t}\n\tserver.listen(args.port, host, () => {\n\t\tconsole.log(\n\t\t\t`dreb dashboard listening on ${scheme}://${host === \"0.0.0.0\" ? \"<tailscale-ip>\" : host}:${args.port}`,\n\t\t);\n\t\tif (args.remote) {\n\t\t\tconsole.log(`remote mode: allowed identities = ${args.allow.join(\", \")}`);\n\t\t\tconst { code, expiresInMs } = auth.currentPairingCode();\n\t\t\tconsole.log(\n\t\t\t\t`pairing code: ${code} (rotates every 30s; current code rolls in ${Math.ceil(expiresInMs / 1000)}s)`,\n\t\t\t);\n\t\t\tconsole.log(\"new devices enter this code; see it live in dashboard Settings on the host machine\");\n\t\t} else {\n\t\t\tconsole.log(\"local mode: loopback only — use --remote for Tailscale access\");\n\t\t}\n\t\tif (args.https) {\n\t\t\tconsole.log(\"tls: native HTTPS enabled (cert + key hot-reload on file change)\");\n\t\t} else {\n\t\t\tconst insecureWarning = insecureRemoteWarning(args);\n\t\t\tif (insecureWarning) console.warn(insecureWarning);\n\t\t}\n\t});\n\thttpServer = server;\n\n\tconst shutdown = () => beginShutdown(\"shutting down…\", 0);\n\tprocess.on(\"SIGINT\", shutdown);\n\tprocess.on(\"SIGTERM\", shutdown);\n}\n\n// Only run when executed directly (not when imported for the library exports).\nconst entryPath = process.argv[1];\nif (entryPath && import.meta.url === new URL(`file://${entryPath}`).href) {\n\tmain().catch((err) => {\n\t\tconsole.error(`fatal: ${err instanceof Error ? err.message : String(err)}`);\n\t\tprocess.exit(1);\n\t});\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-pool.d.ts","sourceRoot":"","sources":["../../src/server/runtime-pool.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,OAAO,EAAE,SAAS,EAAE,KAAK,oBAAoB,EAAoB,MAAM,wBAAwB,CAAC;AAChG,OAAO,EACN,KAAK,kBAAkB,EACvB,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,EAC1B,+BAA+B,EAC/B,KAAK,cAAc,EAEnB,KAAK,eAAe,EAEpB,MAAM,uBAAuB,CAAC;AAE/B,6FAA6F;AAC7F,wBAAgB,kBAAkB,IAAI,MAAM,CAG3C;AAcD,MAAM,MAAM,oBAAoB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;AAEzF,mEAAmE;AACnE,MAAM,MAAM,qBAAqB,GAAG,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,CAAC;AAE3E,OAAO,EAAE,+BAA+B,EAAE,CAAC;AAI3C,MAAM,WAAW,wBAAwB;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,oBAAoB,CAAC;CAC/B;AAED,MAAM,WAAW,aAAa;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,SAAS,CAAC;IAClB,0FAAwF;IACxF,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,sFAAsF;IACtF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kGAAkG;IAClG,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4FAA4F;IAC5F,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,iEAAiE;IACjE,gBAAgB,EAAE,MAAM,CAAC;IACzB,oFAAoF;IACpF,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,mEAAiE;IACjE,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;CAClD;AAED,eAAO,MAAM,gCAAgC,QAAa,CAAC;AAC3D,eAAO,MAAM,+BAA+B,OAAO,CAAC;AACpD,eAAO,MAAM,kCAAkC,MAAM,CAAC;AAwBtD,MAAM,WAAW,kBAAkB;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,4CAA4C;IAC5C,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,EAAE,CAAA;KAAE,KAAK,SAAS,CAAC;IACzF,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,sDAAsD;IACtD,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,mEAAmE;IACnE,uBAAuB,CAAC,EAAE,MAAM,CAAC;CACjC;AAOD,qBAAa,WAAW;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoC;IAC7D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA8B;IACxD,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAA+B;IACtE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAW;IACpC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA2E;IACzG,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAyB;IAChD;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAoC;IAC9D,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA6C;IAC7E,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4B;IACrD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA+B;IAC/D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgC;IAC9D,8EAA8E;IAC9E,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAuC;IACzE,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAS;IAC/C,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAS;IAC/C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAS;IACjD,OAAO,CAAC,0BAA0B,CAA4C;IAC9E,OAAO,CAAC,kBAAkB,CAA4C;IACtE,OAAO,CAAC,OAAO,CAAS;IAExB,YAAY,OAAO,GAAE,kBAAuB,EAU3C;IAED,2EAA2E;IAC3E,OAAO,CAAC,QAAQ,EAAE,oBAAoB,GAAG,MAAM,IAAI,CAMlD;IAED,yDAAyD;IACzD,eAAe,CAAC,QAAQ,EAAE,qBAAqB,GAAG,MAAM,IAAI,CAM3D;IAED;;;OAGG;IACH,aAAa,IAAI,uBAAuB,EAAE,CAEzC;IAED,IAAI,IAAI,aAAa,EAAE,CAEtB;IAED,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAE1C;IAED,gFAAgF;IAC1E,QAAQ,CACb,MAAM,EAAE,aAAa,EACrB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACb,OAAO,CAAC;QACV,KAAK,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,EAAE,EAAE,MAAM,CAAA;SAAE,CAAC;QACxC,aAAa,EAAE,MAAM,CAAC;QACtB,uBAAuB,EAAE,MAAM,EAAE,CAAC;QAClC,gBAAgB,EAAE,MAAM,CAAC;KACzB,CAAC,CAiBD;IAED,mFAAmF;IAC7E,gBAAgB,CACrB,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,UAAU,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC,GACjD,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAE,CAAC,CAMjD;IAED;;;;OAIG;IACH,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAIhF;IAED;;;;OAIG;IACG,iBAAiB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAehF;IAED,OAAO,CAAC,mBAAmB;IAI3B,OAAO,CAAC,sBAAsB;IAa9B,OAAO,CAAC,6BAA6B;IAgBrC,OAAO,CAAC,qBAAqB;IAkB7B,iFAAiF;IAC3E,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA2BtE;YAEa,mBAAmB;IAsBjC,kDAAkD;IAC5C,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAQxC;IAED;;;;;OAKG;IACG,oBAAoB,CAAC,GAAG,SAAY,GAAG,OAAO,CAAC,aAAa,CAAC,CAgClE;YAEa,mBAAmB;IAgB3B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAmB7B;IAED,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,YAAY;IAQpB,OAAO,CAAC,WAAW;IAqHnB;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAkD5B,OAAO,CAAC,kBAAkB;IAM1B,OAAO,CAAC,mBAAmB;IAK3B,OAAO,CAAC,kBAAkB;IAS1B,OAAO,CAAC,8BAA8B;IAetC,OAAO,CAAC,aAAa;IAqBrB,OAAO,CAAC,oBAAoB;YAed,oBAAoB;IAclC,iDAAiD;IAC3C,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC,CAsE7D;CACD","sourcesContent":["/**\n * RPC runtime pool — one `dreb --mode rpc` child process per live session.\n *\n * dreb's RPC mode is strictly one-session-per-process (switch_session repoints\n * the same process; it never multiplexes), so the pool spawns N children keyed\n * by an opaque runtime key. The telegram bridge is the in-repo precedent.\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { isDeepStrictEqual } from \"node:util\";\nimport { RpcClient, type RpcDashboardSnapshot, type RpcExitInfo } from \"@dreb/coding-agent/rpc\";\nimport {\n\ttype BackgroundAgentDto,\n\ttype FleetRuntimeSnapshotDto,\n\ttype FleetSnapshotEventDto,\n\tMAX_COMPLETED_BACKGROUND_AGENTS,\n\ttype RuntimeInfoDto,\n\ttype RuntimeStatsSummaryDto,\n\ttype SessionStateDto,\n\ttype SubagentArbitrationDto,\n} from \"../shared/protocol.js\";\n\n/** Resolve the absolute path to the dreb CLI (RpcClient defaults to a cwd-relative path). */\nexport function resolveDrebCliPath(): string {\n\tconst resolved = import.meta.resolve(\"@dreb/coding-agent\");\n\treturn join(dirname(fileURLToPath(resolved)), \"cli.js\");\n}\n\nfunction formatRpcExit(info: RpcExitInfo): string {\n\tif (info.error) return `RPC process failed: ${info.error.message}`;\n\tconst tail = info.stderrTail?.trim();\n\t// When the child aborted on its own (e.g. the stdout backpressure guard),\n\t// its stderr tail carries the actual reason — surface it instead of a bare\n\t// exit code. The tail may be multi-line; the existing UI already renders\n\t// multi-line error messages (e.g. timeouts embed full stderr).\n\treturn tail\n\t\t? `RPC process exited (code ${info.code}, signal ${info.signal}). Stderr tail: ${tail}`\n\t\t: `RPC process exited (code ${info.code}, signal ${info.signal})`;\n}\n\nexport type RuntimeEventListener = (key: string, event: Record<string, unknown>) => void;\n\n/** Listener for coalesced, synchronous fleet runtime snapshots. */\nexport type FleetSnapshotListener = (event: FleetSnapshotEventDto) => void;\n\nexport { MAX_COMPLETED_BACKGROUND_AGENTS };\n\ntype DashboardSnapshotClient = RpcClient & { getDashboardSnapshot(): Promise<RpcDashboardSnapshot> };\n\nexport interface DashboardRuntimeSnapshot {\n\tkey: string;\n\tbarrierSeq: number;\n\tsnapshot: RpcDashboardSnapshot;\n}\n\nexport interface RuntimeHandle {\n\tkey: string;\n\tcwd: string;\n\tclient: RpcClient;\n\t/** Session start time (ms epoch) — stable tiebreak for deterministic fleet ordering. */\n\tcreatedAt: number;\n\tlastActivity: number;\n\t/** Needs-attention sources, keyed so they can be cleared independently. */\n\tattention: Map<string, string>;\n\t/** Last runtime-level error, persisted server-side so fleet refreshes stay honest. */\n\terror?: string;\n\t/** Provider error currently responsible for error/attention; retry may clear only this source. */\n\tproviderError?: string;\n\t/** Last authoritative state, patched only with event-derivable fields between RPC reads. */\n\tlastState?: SessionStateDto;\n\t/** Monotonic revision for confirmed model/thinking mutations. */\n\tsettingsRevision: number;\n\t/** Resume-path fallback; events must never invent or overwrite session identity. */\n\tsessionFileFallback?: string;\n\t/** Background agents seen via events (agentId → latest info). */\n\tbackgroundAgents: Map<string, BackgroundAgentDto>;\n}\n\nexport const DEFAULT_DASHBOARD_BARRIER_TTL_MS = 5 * 60_000;\nexport const DEFAULT_DASHBOARD_BARRIER_LIMIT = 1000;\nexport const DEFAULT_FLEET_SNAPSHOT_DEBOUNCE_MS = 200;\n\n/** Events that mutate a field carried by the lightweight fleet snapshot. */\nconst FLEET_SNAPSHOT_EVENT_TYPES = new Set([\n\t\"agent_start\",\n\t\"agent_end\",\n\t\"auto_compaction_start\",\n\t\"auto_compaction_end\",\n\t\"auto_retry_start\",\n\t\"auto_retry_end\",\n\t\"background_agent_start\",\n\t\"background_agent_end\",\n\t\"subagent_arbitration\",\n\t\"extension_ui_request\",\n\t\"extension_ui_response_handled\",\n\t\"message_end\",\n\t\"message_start\",\n\t\"parent_paused_for_background_agents\",\n\t\"runtime_removed\",\n\t\"session_name_changed\",\n\t\"suggest_next\",\n\t\"tasks_update\",\n]);\n\nexport interface RuntimePoolOptions {\n\tcliPath?: string;\n\t/** Extra args for every runtime (e.g. --provider). */\n\tbaseArgs?: string[];\n\t/** RpcClient factory override for tests. */\n\tclientFactory?: (options: { cliPath: string; cwd: string; args: string[] }) => RpcClient;\n\tlogger?: (line: string) => void;\n\t/** Bounds unclaimed RPC snapshot ordering records. */\n\tdashboardBarrierTtlMs?: number;\n\tdashboardBarrierLimit?: number;\n\t/** Injectable clock for deterministic barrier-expiry tests. */\n\tnow?: () => number;\n\t/** Coalescing delay for event-derived fleet snapshot emissions. */\n\tfleetSnapshotDebounceMs?: number;\n}\n\ninterface DashboardBarrier {\n\tseq: number;\n\trecordedAt: number;\n}\n\nexport class RuntimePool {\n\tprivate readonly runtimes = new Map<string, RuntimeHandle>();\n\tprivate readonly listeners: RuntimeEventListener[] = [];\n\tprivate readonly fleetSnapshotListeners: FleetSnapshotListener[] = [];\n\tprivate readonly cliPath: string;\n\tprivate readonly baseArgs: string[];\n\tprivate readonly clientFactory: (options: { cliPath: string; cwd: string; args: string[] }) => RpcClient;\n\tprivate readonly logger: (line: string) => void;\n\t/**\n\t * A single lazily-spawned utility runtime used to service settings/model/\n\t * agent-type endpoints when no user session is live. Kept out of `runtimes`\n\t * (and therefore out of the fleet) so it never shows as a session card.\n\t */\n\tprivate readonly utilities = new Map<string, RuntimeHandle>();\n\tprivate readonly utilityPromises = new Map<string, Promise<RuntimeHandle>>();\n\tprivate readonly starting = new Set<RuntimeHandle>();\n\tprivate readonly startupPromises = new Set<Promise<unknown>>();\n\tprivate readonly exitedHandles = new WeakSet<RuntimeHandle>();\n\t/** Snapshot ordering records observed synchronously from RpcClient stdout. */\n\tprivate readonly dashboardBarriers = new Map<string, DashboardBarrier>();\n\tprivate readonly dashboardBarrierTtlMs: number;\n\tprivate readonly dashboardBarrierLimit: number;\n\tprivate readonly now: () => number;\n\tprivate readonly fleetSnapshotDebounceMs: number;\n\tprivate dashboardBarrierPruneTimer: ReturnType<typeof setTimeout> | undefined;\n\tprivate fleetSnapshotTimer: ReturnType<typeof setTimeout> | undefined;\n\tprivate closing = false;\n\n\tconstructor(options: RuntimePoolOptions = {}) {\n\t\tthis.cliPath = options.cliPath ?? resolveDrebCliPath();\n\t\tthis.baseArgs = options.baseArgs ?? [];\n\t\tthis.clientFactory =\n\t\t\toptions.clientFactory ?? ((o) => new RpcClient({ cliPath: o.cliPath, cwd: o.cwd, args: o.args }));\n\t\tthis.logger = options.logger ?? ((line) => console.warn(`[dashboard] ${line}`));\n\t\tthis.dashboardBarrierTtlMs = options.dashboardBarrierTtlMs ?? DEFAULT_DASHBOARD_BARRIER_TTL_MS;\n\t\tthis.dashboardBarrierLimit = options.dashboardBarrierLimit ?? DEFAULT_DASHBOARD_BARRIER_LIMIT;\n\t\tthis.now = options.now ?? Date.now;\n\t\tthis.fleetSnapshotDebounceMs = options.fleetSnapshotDebounceMs ?? DEFAULT_FLEET_SNAPSHOT_DEBOUNCE_MS;\n\t}\n\n\t/** Subscribe to events from every runtime, tagged with the runtime key. */\n\tonEvent(listener: RuntimeEventListener): () => void {\n\t\tthis.listeners.push(listener);\n\t\treturn () => {\n\t\t\tconst i = this.listeners.indexOf(listener);\n\t\t\tif (i !== -1) this.listeners.splice(i, 1);\n\t\t};\n\t}\n\n\t/** Subscribe to debounced, in-memory fleet snapshots. */\n\tonFleetSnapshot(listener: FleetSnapshotListener): () => void {\n\t\tthis.fleetSnapshotListeners.push(listener);\n\t\treturn () => {\n\t\t\tconst i = this.fleetSnapshotListeners.indexOf(listener);\n\t\t\tif (i !== -1) this.fleetSnapshotListeners.splice(i, 1);\n\t\t};\n\t}\n\n\t/**\n\t * Build the fleet's live-runtime view without RPC or disk access. Map\n\t * insertion order is retained intentionally; the UI owns presentation order.\n\t */\n\tfleetSnapshot(): FleetRuntimeSnapshotDto[] {\n\t\treturn [...this.runtimes.values()].map((handle) => this.describeFleetRuntime(handle));\n\t}\n\n\tlist(): RuntimeHandle[] {\n\t\treturn [...this.runtimes.values()];\n\t}\n\n\tget(key: string): RuntimeHandle | undefined {\n\t\treturn this.runtimes.get(key);\n\t}\n\n\t/** Apply a confirmed model mutation to both the child and the pool snapshot. */\n\tasync setModel(\n\t\thandle: RuntimeHandle,\n\t\tprovider: string,\n\t\tmodelId: string,\n\t): Promise<{\n\t\tmodel: { provider: string; id: string };\n\t\tthinkingLevel: string;\n\t\tavailableThinkingLevels: string[];\n\t\tsettingsRevision: number;\n\t}> {\n\t\tconst model = await handle.client.setModel(provider, modelId);\n\t\tconst state = await handle.client.getState();\n\t\thandle.lastState = {\n\t\t\t...this.fallbackState(handle),\n\t\t\tmodel,\n\t\t\tthinkingLevel: state.thinkingLevel,\n\t\t\tavailableThinkingLevels: state.availableThinkingLevels,\n\t\t};\n\t\thandle.settingsRevision += 1;\n\t\tif (this.runtimes.get(handle.key) === handle) this.scheduleFleetSnapshot();\n\t\treturn {\n\t\t\tmodel,\n\t\t\tthinkingLevel: state.thinkingLevel,\n\t\t\tavailableThinkingLevels: state.availableThinkingLevels,\n\t\t\tsettingsRevision: handle.settingsRevision,\n\t\t};\n\t}\n\n\t/** Apply a confirmed thinking mutation to both the child and the pool snapshot. */\n\tasync setThinkingLevel(\n\t\thandle: RuntimeHandle,\n\t\tlevel: Parameters<RpcClient[\"setThinkingLevel\"]>[0],\n\t): Promise<{ ok: true; settingsRevision: number }> {\n\t\tawait handle.client.setThinkingLevel(level);\n\t\thandle.lastState = { ...this.fallbackState(handle), thinkingLevel: level };\n\t\thandle.settingsRevision += 1;\n\t\tif (this.runtimes.get(handle.key) === handle) this.scheduleFleetSnapshot();\n\t\treturn { ok: true, settingsRevision: handle.settingsRevision };\n\t}\n\n\t/**\n\t * Record the EventHub sequence synchronously when the RPC snapshot marker\n\t * arrives. The marker line precedes its response on stdout, so this runs\n\t * before the RpcClient response continuation even across separate chunks.\n\t */\n\trecordDashboardBarrier(runtimeKey: string, snapshotId: string, seq: number): void {\n\t\tthis.pruneDashboardBarriers();\n\t\tthis.dashboardBarriers.set(this.dashboardBarrierKey(runtimeKey, snapshotId), { seq, recordedAt: this.now() });\n\t\tthis.pruneDashboardBarriers();\n\t}\n\n\t/**\n\t * Capture a parent-session recovery snapshot and pair it with the sequence\n\t * captured at its RPC marker. This deliberately does not infer ordering from\n\t * await: later EventHub publications naturally have higher sequence numbers.\n\t */\n\tasync snapshotDashboard(handle: RuntimeHandle): Promise<DashboardRuntimeSnapshot> {\n\t\tconst snapshot = await (handle.client as DashboardSnapshotClient).getDashboardSnapshot();\n\t\tthis.pruneDashboardBarriers();\n\t\tconst barrierKey = this.dashboardBarrierKey(handle.key, snapshot.snapshotId);\n\t\tconst barrier = this.dashboardBarriers.get(barrierKey);\n\t\tthis.dashboardBarriers.delete(barrierKey);\n\t\tthis.scheduleDashboardBarrierPrune();\n\t\tif (!barrier) {\n\t\t\tthrow new Error(`Dashboard snapshot ${snapshot.snapshotId} arrived without its ordering barrier`);\n\t\t}\n\t\t// This is an authoritative RPC baseline, so it may legitimately lower the\n\t\t// count after a fork/rewind instead of retaining an event-derived maximum.\n\t\thandle.lastState = snapshot.state;\n\t\tthis.scheduleFleetSnapshot();\n\t\treturn { key: handle.key, barrierSeq: barrier.seq, snapshot };\n\t}\n\n\tprivate dashboardBarrierKey(runtimeKey: string, snapshotId: string): string {\n\t\treturn `${runtimeKey}\\0${snapshotId}`;\n\t}\n\n\tprivate pruneDashboardBarriers(): void {\n\t\tconst oldestAllowed = this.now() - this.dashboardBarrierTtlMs;\n\t\tfor (const [snapshotId, barrier] of this.dashboardBarriers) {\n\t\t\tif (barrier.recordedAt < oldestAllowed) this.dashboardBarriers.delete(snapshotId);\n\t\t}\n\t\twhile (this.dashboardBarriers.size > this.dashboardBarrierLimit) {\n\t\t\tconst oldest = this.dashboardBarriers.keys().next().value;\n\t\t\tif (oldest === undefined) break;\n\t\t\tthis.dashboardBarriers.delete(oldest);\n\t\t}\n\t\tthis.scheduleDashboardBarrierPrune();\n\t}\n\n\tprivate scheduleDashboardBarrierPrune(): void {\n\t\tif (this.dashboardBarrierPruneTimer) clearTimeout(this.dashboardBarrierPruneTimer);\n\t\tthis.dashboardBarrierPruneTimer = undefined;\n\t\tlet oldest: DashboardBarrier | undefined;\n\t\tfor (const barrier of this.dashboardBarriers.values()) {\n\t\t\tif (!oldest || barrier.recordedAt < oldest.recordedAt) oldest = barrier;\n\t\t}\n\t\tif (!oldest) return;\n\t\tconst delay = Math.max(1, oldest.recordedAt + this.dashboardBarrierTtlMs - this.now() + 1);\n\t\tthis.dashboardBarrierPruneTimer = setTimeout(() => {\n\t\t\tthis.dashboardBarrierPruneTimer = undefined;\n\t\t\tthis.pruneDashboardBarriers();\n\t\t}, delay);\n\t\tthis.dashboardBarrierPruneTimer.unref?.();\n\t}\n\n\tprivate scheduleFleetSnapshot(): void {\n\t\tif (this.closing) return;\n\t\tif (this.fleetSnapshotTimer) clearTimeout(this.fleetSnapshotTimer);\n\t\tthis.fleetSnapshotTimer = setTimeout(() => {\n\t\t\tthis.fleetSnapshotTimer = undefined;\n\t\t\tif (this.closing) return;\n\t\t\tconst event: FleetSnapshotEventDto = { type: \"fleet_snapshot\", runtimes: this.fleetSnapshot() };\n\t\t\tfor (const listener of this.fleetSnapshotListeners) {\n\t\t\t\ttry {\n\t\t\t\t\tlistener(event);\n\t\t\t\t} catch {\n\t\t\t\t\t// An SSE bridge subscriber must not break the pool's event loop.\n\t\t\t\t}\n\t\t\t}\n\t\t}, this.fleetSnapshotDebounceMs);\n\t\tthis.fleetSnapshotTimer.unref?.();\n\t}\n\n\t/** Spawn a new runtime in `cwd`, optionally opening an existing session file. */\n\tasync create(cwd: string, sessionPath?: string): Promise<RuntimeHandle> {\n\t\tif (this.closing) throw new Error(\"Runtime pool is closing\");\n\t\tconst key = randomBytes(6).toString(\"hex\");\n\t\tconst args = [\"--ui\", \"dashboard\", ...this.baseArgs];\n\t\tif (sessionPath) args.push(\"--session\", sessionPath);\n\t\tconst client = this.clientFactory({ cliPath: this.cliPath, cwd, args });\n\t\tconst handle: RuntimeHandle = {\n\t\t\tkey,\n\t\t\tcwd,\n\t\t\tclient,\n\t\t\tcreatedAt: this.now(),\n\t\t\tlastActivity: this.now(),\n\t\t\tattention: new Map(),\n\t\t\tsettingsRevision: 0,\n\t\t\tsessionFileFallback: sessionPath,\n\t\t\tbackgroundAgents: new Map(),\n\t\t};\n\t\tclient.onEvent((event) => this.handleEvent(handle, event as unknown as Record<string, unknown>));\n\t\tclient.onExit((info) => this.handleRuntimeExit(handle, info));\n\n\t\tconst startup = this.startSessionRuntime(handle);\n\t\tthis.startupPromises.add(startup);\n\t\ttry {\n\t\t\treturn await startup;\n\t\t} finally {\n\t\t\tthis.startupPromises.delete(startup);\n\t\t}\n\t}\n\n\tprivate async startSessionRuntime(handle: RuntimeHandle): Promise<RuntimeHandle> {\n\t\tthis.starting.add(handle);\n\t\ttry {\n\t\t\tif (this.closing) throw new Error(\"Runtime pool is closing\");\n\t\t\tawait handle.client.start();\n\t\t\tif (this.closing) {\n\t\t\t\tawait handle.client.stop();\n\t\t\t\tthrow new Error(\"Runtime pool is closing\");\n\t\t\t}\n\t\t\tawait this.seedBackgroundAgents(handle);\n\t\t\tif (this.closing) {\n\t\t\t\tawait handle.client.stop();\n\t\t\t\tthrow new Error(\"Runtime pool is closing\");\n\t\t\t}\n\t\t\tthis.runtimes.set(handle.key, handle);\n\t\t\tthis.scheduleFleetSnapshot();\n\t\t\treturn handle;\n\t\t} finally {\n\t\t\tthis.starting.delete(handle);\n\t\t}\n\t}\n\n\t/** Stop a runtime and remove it from the pool. */\n\tasync stop(key: string): Promise<boolean> {\n\t\tconst handle = this.runtimes.get(key);\n\t\tif (!handle) return false;\n\t\tthis.handleEvent(handle, { type: \"runtime_removed\" });\n\t\tthis.runtimes.delete(key);\n\t\tthis.scheduleFleetSnapshot();\n\t\tawait handle.client.stop();\n\t\treturn true;\n\t}\n\n\t/**\n\t * Return any live runtime suitable for process-global settings work, spawning\n\t * a hidden utility runtime (in the home directory) if no user session exists.\n\t * This is what lets the settings page — models, agent types, defaults — work\n\t * with zero sessions open, instead of 503-ing.\n\t */\n\tasync ensureUtilityRuntime(cwd = homedir()): Promise<RuntimeHandle> {\n\t\tif (this.closing) throw new Error(\"Runtime pool is closing\");\n\t\tconst existing = this.utilities.get(cwd);\n\t\tif (existing) return existing;\n\t\tlet promise = this.utilityPromises.get(cwd);\n\t\tif (!promise) {\n\t\t\tconst args = [\"--ui\", \"dashboard\", ...this.baseArgs];\n\t\t\tconst client = this.clientFactory({ cliPath: this.cliPath, cwd, args });\n\t\t\tconst handle: RuntimeHandle = {\n\t\t\t\tkey: `utility:${cwd}`,\n\t\t\t\tcwd,\n\t\t\t\tclient,\n\t\t\t\tcreatedAt: this.now(),\n\t\t\t\tlastActivity: this.now(),\n\t\t\t\tattention: new Map(),\n\t\t\t\tsettingsRevision: 0,\n\t\t\t\tbackgroundAgents: new Map(),\n\t\t\t};\n\t\t\tconst startup = this.startUtilityRuntime(cwd, handle);\n\t\t\tthis.startupPromises.add(startup);\n\t\t\tpromise = startup\n\t\t\t\t.catch((err) => {\n\t\t\t\t\t// Allow a retry on the next request instead of caching the failure.\n\t\t\t\t\tthis.utilityPromises.delete(cwd);\n\t\t\t\t\tthrow err;\n\t\t\t\t})\n\t\t\t\t.finally(() => {\n\t\t\t\t\tthis.startupPromises.delete(startup);\n\t\t\t\t});\n\t\t\tthis.utilityPromises.set(cwd, promise);\n\t\t}\n\t\treturn promise;\n\t}\n\n\tprivate async startUtilityRuntime(cwd: string, handle: RuntimeHandle): Promise<RuntimeHandle> {\n\t\tthis.starting.add(handle);\n\t\ttry {\n\t\t\tif (this.closing) throw new Error(\"Runtime pool is closing\");\n\t\t\tawait handle.client.start();\n\t\t\tif (this.closing) {\n\t\t\t\tawait handle.client.stop();\n\t\t\t\tthrow new Error(\"Runtime pool is closing\");\n\t\t\t}\n\t\t\tthis.utilities.set(cwd, handle);\n\t\t\treturn handle;\n\t\t} finally {\n\t\t\tthis.starting.delete(handle);\n\t\t}\n\t}\n\n\tasync stopAll(): Promise<void> {\n\t\tthis.closing = true;\n\t\tif (this.dashboardBarrierPruneTimer) clearTimeout(this.dashboardBarrierPruneTimer);\n\t\tthis.dashboardBarrierPruneTimer = undefined;\n\t\tif (this.fleetSnapshotTimer) clearTimeout(this.fleetSnapshotTimer);\n\t\tthis.fleetSnapshotTimer = undefined;\n\t\tthis.fleetSnapshotListeners.length = 0;\n\t\tthis.dashboardBarriers.clear();\n\t\tconst handles = new Set<RuntimeHandle>([\n\t\t\t...this.runtimes.values(),\n\t\t\t...this.utilities.values(),\n\t\t\t...this.starting.values(),\n\t\t]);\n\t\tconst startupPromises = new Set<Promise<unknown>>([...this.startupPromises, ...this.utilityPromises.values()]);\n\t\tthis.runtimes.clear();\n\t\tthis.utilities.clear();\n\t\tthis.utilityPromises.clear();\n\t\tawait Promise.allSettled([...handles].map((handle) => handle.client.stop()));\n\t\tawait Promise.allSettled([...startupPromises]);\n\t}\n\n\tprivate handleRuntimeExit(handle: RuntimeHandle, info: RpcExitInfo): void {\n\t\tif (this.closing || this.exitedHandles.has(handle) || !this.isLiveHandle(handle)) return;\n\t\tthis.exitedHandles.add(handle);\n\t\tconst message = formatRpcExit(info);\n\t\tthis.recordRuntimeError(handle, message);\n\t\tthis.logger(`runtime ${handle.key} ${message}`);\n\t\tthis.handleEvent(handle, { type: \"agent_end\", messages: [], aborted: true, errorMessage: message });\n\t}\n\n\tprivate isLiveHandle(handle: RuntimeHandle): boolean {\n\t\treturn (\n\t\t\tthis.runtimes.get(handle.key) === handle ||\n\t\t\tthis.utilities.get(handle.cwd) === handle ||\n\t\t\tthis.starting.has(handle)\n\t\t);\n\t}\n\n\tprivate handleEvent(handle: RuntimeHandle, event: Record<string, unknown>): void {\n\t\tconst type = event.type as string;\n\t\tif (type !== \"dashboard_snapshot_barrier\") {\n\t\t\thandle.lastActivity = this.now();\n\t\t\tthis.updateStateFromEvent(handle, event, type);\n\t\t}\n\n\t\t// Track needs-attention sources: extension UI requests, parent\n\t\t// paused, error states.\n\t\tif (type === \"extension_ui_request\") {\n\t\t\tconst method = event.method as string;\n\t\t\tif (\n\t\t\t\tmethod === \"select\" ||\n\t\t\t\tmethod === \"confirm\" ||\n\t\t\t\tmethod === \"input\" ||\n\t\t\t\tmethod === \"editor\" ||\n\t\t\t\tmethod === \"ask\"\n\t\t\t) {\n\t\t\t\thandle.attention.set(`ui:${event.id}`, `extension ${method} awaiting response`);\n\t\t\t}\n\t\t}\n\t\tif (type === \"extension_ui_response_handled\") {\n\t\t\thandle.attention.delete(`ui:${event.id}`);\n\t\t}\n\t\tif (type === \"agent_start\") {\n\t\t\t// A new turn clears prior UI-request attention (requests were resolved or timed out).\n\t\t\tfor (const k of [...handle.attention.keys()]) {\n\t\t\t\tif (k.startsWith(\"ui:\")) handle.attention.delete(k);\n\t\t\t}\n\t\t\thandle.attention.delete(\"paused\");\n\t\t\thandle.attention.delete(\"suggest\");\n\t\t\thandle.attention.delete(\"error\");\n\t\t\thandle.error = undefined;\n\t\t\thandle.providerError = undefined;\n\t\t}\n\t\tif (type === \"suggest_next\") {\n\t\t\t// suggest_next as the ending action = \"your move\": mark needs-attention\n\t\t\t// so the fleet card doesn't read idle. Cleared on the next agent_start.\n\t\t\thandle.attention.set(\"suggest\", \"suggested command awaiting\");\n\t\t}\n\t\tif (type === \"parent_paused_for_background_agents\") {\n\t\t\thandle.attention.set(\"paused\", `paused — ${event.runningAgentCount} background agents running`);\n\t\t}\n\t\tif (type === \"agent_end\") {\n\t\t\thandle.attention.delete(\"paused\");\n\t\t\tif (event.errorMessage) this.recordRuntimeError(handle, String(event.errorMessage));\n\t\t}\n\t\tif (type === \"message_end\") {\n\t\t\tconst message = event.message;\n\t\t\tif (message && typeof message === \"object\" && !Array.isArray(message)) {\n\t\t\t\tconst assistant = message as Record<string, unknown>;\n\t\t\t\tif (assistant.role === \"assistant\" && assistant.stopReason === \"error\") {\n\t\t\t\t\tconst rawError = assistant.errorMessage;\n\t\t\t\t\tconst error = typeof rawError === \"string\" && rawError.trim().length > 0 ? rawError : \"Unknown error\";\n\t\t\t\t\tthis.recordProviderError(handle, error);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (type === \"auto_retry_start\") this.clearProviderError(handle);\n\t\tif (type === \"auto_compaction_start\" && event.reason === \"overflow\") this.clearProviderError(handle);\n\t\tif (type === \"auto_retry_end\" && event.success === false && event.finalError) {\n\t\t\tthis.recordProviderError(handle, String(event.finalError));\n\t\t} else if (type === \"auto_retry_end\" && event.success === true) {\n\t\t\tthis.clearProviderError(handle);\n\t\t}\n\t\tif (type === \"auto_compaction_end\" && event.errorMessage) {\n\t\t\tthis.recordRuntimeError(handle, String(event.errorMessage));\n\t\t}\n\n\t\t// Track background agents from lifecycle events.\n\t\tif (type === \"background_agent_start\") {\n\t\t\thandle.backgroundAgents.set(event.agentId as string, {\n\t\t\t\tagentId: event.agentId as string,\n\t\t\t\tagentType: event.agentType as string,\n\t\t\t\ttaskSummary: event.taskSummary as string,\n\t\t\t\tstartedAt: new Date().toISOString(),\n\t\t\t\tstatus: \"running\",\n\t\t\t\tsessionDir: event.sessionDir as string | undefined,\n\t\t\t});\n\t\t}\n\t\tif (type === \"subagent_arbitration\") {\n\t\t\tconst existing = handle.backgroundAgents.get(event.agentId as string);\n\t\t\tif (existing) {\n\t\t\t\tconst record: SubagentArbitrationDto = {\n\t\t\t\t\tstatus: event.status as SubagentArbitrationDto[\"status\"],\n\t\t\t\t\tproposed: event.proposed as SubagentArbitrationDto[\"proposed\"],\n\t\t\t\t\tfinal: (event.final as SubagentArbitrationDto[\"final\"]) ?? null,\n\t\t\t\t\tchanged: (event.changed as SubagentArbitrationDto[\"changed\"]) ?? [],\n\t\t\t\t\tstep: event.step as number | undefined,\n\t\t\t\t\terrorCode: event.errorCode as string | undefined,\n\t\t\t\t\terrorMessage: event.errorMessage as string | undefined,\n\t\t\t\t};\n\t\t\t\texisting.arbitrations = [...(existing.arbitrations ?? []), record];\n\t\t\t\tif (record.status === \"success\" && record.final) existing.agentType = record.final.agent;\n\t\t\t}\n\t\t}\n\t\tif (type === \"background_agent_end\") {\n\t\t\tconst existing = handle.backgroundAgents.get(event.agentId as string);\n\t\t\tif (existing) {\n\t\t\t\texisting.status = event.success ? \"completed\" : \"failed\";\n\t\t\t\texisting.sessionFile = (event.sessionFile as string | undefined) ?? existing.sessionFile;\n\t\t\t\tthis.pruneCompletedBackgroundAgents(handle);\n\t\t\t}\n\t\t}\n\n\t\tfor (const listener of this.listeners) {\n\t\t\ttry {\n\t\t\t\tlistener(handle.key, event);\n\t\t\t} catch {\n\t\t\t\t// A broken SSE subscriber must not break event distribution.\n\t\t\t}\n\t\t}\n\t\tif (this.runtimes.get(handle.key) === handle && FLEET_SNAPSHOT_EVENT_TYPES.has(type)) {\n\t\t\tthis.scheduleFleetSnapshot();\n\t\t}\n\t}\n\n\t/**\n\t * Events intentionally patch only fields they can prove. Session identity,\n\t * session file, configuration, and context usage remain from the last RPC\n\t * baseline (or the stable creation fallback) until a later reconciliation.\n\t */\n\tprivate updateStateFromEvent(handle: RuntimeHandle, event: Record<string, unknown>, type: string): void {\n\t\tconst state = this.fallbackState(handle);\n\t\tswitch (type) {\n\t\t\tcase \"agent_start\": {\n\t\t\t\tconst model = event.model;\n\t\t\t\tif (\n\t\t\t\t\tmodel &&\n\t\t\t\t\ttypeof model === \"object\" &&\n\t\t\t\t\ttypeof (model as Record<string, unknown>).provider === \"string\" &&\n\t\t\t\t\ttypeof (model as Record<string, unknown>).id === \"string\"\n\t\t\t\t) {\n\t\t\t\t\tconst nextModel = model as { provider: string; id: string };\n\t\t\t\t\tstate.model =\n\t\t\t\t\t\tstate.model?.provider === nextModel.provider && state.model.id === nextModel.id\n\t\t\t\t\t\t\t? { ...state.model, ...nextModel }\n\t\t\t\t\t\t\t: nextModel;\n\t\t\t\t}\n\t\t\t\tstate.isStreaming = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"agent_end\":\n\t\t\t\tstate.isStreaming = false;\n\t\t\t\tbreak;\n\t\t\tcase \"auto_compaction_start\":\n\t\t\t\tstate.isCompacting = true;\n\t\t\t\tbreak;\n\t\t\tcase \"auto_compaction_end\":\n\t\t\t\tstate.isCompacting = false;\n\t\t\t\tbreak;\n\t\t\tcase \"auto_retry_start\":\n\t\t\t\tstate.isRetrying = true;\n\t\t\t\tstate.retryAttempt = typeof event.attempt === \"number\" ? event.attempt : state.retryAttempt;\n\t\t\t\tbreak;\n\t\t\tcase \"auto_retry_end\":\n\t\t\t\tstate.isRetrying = false;\n\t\t\t\tstate.retryAttempt = 0;\n\t\t\t\tbreak;\n\t\t\tcase \"tasks_update\":\n\t\t\t\tif (Array.isArray(event.tasks)) state.tasks = [...event.tasks] as SessionStateDto[\"tasks\"];\n\t\t\t\tbreak;\n\t\t\tcase \"session_name_changed\":\n\t\t\t\tif (typeof event.name === \"string\") state.sessionName = event.name;\n\t\t\t\tbreak;\n\t\t\tcase \"message_start\":\n\t\t\t\tstate.messageCount += 1;\n\t\t\t\tbreak;\n\t\t}\n\t\thandle.lastState = state;\n\t}\n\n\tprivate recordRuntimeError(handle: RuntimeHandle, message: string): void {\n\t\thandle.error = message;\n\t\thandle.attention.set(\"error\", message);\n\t\tif (this.runtimes.get(handle.key) === handle) this.scheduleFleetSnapshot();\n\t}\n\n\tprivate recordProviderError(handle: RuntimeHandle, message: string): void {\n\t\thandle.providerError = message;\n\t\tthis.recordRuntimeError(handle, message);\n\t}\n\n\tprivate clearProviderError(handle: RuntimeHandle): void {\n\t\tconst providerError = handle.providerError;\n\t\tif (providerError === undefined) return;\n\t\thandle.providerError = undefined;\n\t\tif (handle.error === providerError) handle.error = undefined;\n\t\tif (handle.attention.get(\"error\") === providerError) handle.attention.delete(\"error\");\n\t\tif (this.runtimes.get(handle.key) === handle) this.scheduleFleetSnapshot();\n\t}\n\n\tprivate pruneCompletedBackgroundAgents(handle: RuntimeHandle): void {\n\t\tconst evictable = [...handle.backgroundAgents.values()]\n\t\t\t.map((agent, index) => {\n\t\t\t\tconst startedAtMs = Date.parse(agent.startedAt);\n\t\t\t\treturn { agent, index, startedAtMs: Number.isFinite(startedAtMs) ? startedAtMs : 0 };\n\t\t\t})\n\t\t\t.filter(({ agent }) => agent.status !== \"running\")\n\t\t\t.sort((a, b) => a.startedAtMs - b.startedAtMs || a.index - b.index);\n\t\tconst excess = evictable.length - MAX_COMPLETED_BACKGROUND_AGENTS;\n\t\tif (excess <= 0) return;\n\t\tfor (const { agent } of evictable.slice(0, excess)) {\n\t\t\thandle.backgroundAgents.delete(agent.agentId);\n\t\t}\n\t}\n\n\tprivate fallbackState(handle: RuntimeHandle): SessionStateDto {\n\t\tconst previous = handle.lastState;\n\t\treturn {\n\t\t\t...previous,\n\t\t\tsessionId: previous?.sessionId ?? handle.key,\n\t\t\ttasks: previous?.tasks ? [...previous.tasks] : [],\n\t\t\tthinkingLevel: previous?.thinkingLevel ?? \"off\",\n\t\t\tavailableThinkingLevels: previous?.availableThinkingLevels ? [...previous.availableThinkingLevels] : [\"off\"],\n\t\t\tisStreaming: previous?.isStreaming ?? false,\n\t\t\tisRetrying: previous?.isRetrying ?? false,\n\t\t\tretryAttempt: previous?.retryAttempt ?? 0,\n\t\t\tisCompacting: previous?.isCompacting ?? false,\n\t\t\tsteeringMode: previous?.steeringMode ?? \"all\",\n\t\t\tfollowUpMode: previous?.followUpMode ?? \"all\",\n\t\t\tsessionFile: previous?.sessionFile ?? handle.sessionFileFallback,\n\t\t\tautoCompactionEnabled: previous?.autoCompactionEnabled ?? false,\n\t\t\tmessageCount: previous?.messageCount ?? 0,\n\t\t\tpendingMessageCount: previous?.pendingMessageCount ?? 0,\n\t\t};\n\t}\n\n\tprivate describeFleetRuntime(handle: RuntimeHandle): FleetRuntimeSnapshotDto {\n\t\tconst state = this.fallbackState(handle);\n\t\treturn {\n\t\t\tkey: handle.key,\n\t\t\tcwd: handle.cwd,\n\t\t\tstate,\n\t\t\tsettingsRevision: handle.settingsRevision,\n\t\t\tbackgroundAgents: [...handle.backgroundAgents.values()].map((agent) => ({ ...agent })),\n\t\t\tneedsAttention: handle.attention.size > 0,\n\t\t\terror: handle.error,\n\t\t\tcreatedAt: new Date(handle.createdAt).toISOString(),\n\t\t\tlastActivity: new Date(handle.lastActivity).toISOString(),\n\t\t};\n\t}\n\n\tprivate async seedBackgroundAgents(handle: RuntimeHandle): Promise<void> {\n\t\ttry {\n\t\t\tconst agents = (await handle.client.listBackgroundAgents()) as unknown as BackgroundAgentDto[];\n\t\t\tfor (const agent of agents) {\n\t\t\t\thandle.backgroundAgents.set(agent.agentId, agent);\n\t\t\t}\n\t\t\tthis.pruneCompletedBackgroundAgents(handle);\n\t\t} catch (err) {\n\t\t\tthis.logger(\n\t\t\t\t`runtime ${handle.key} background-agent registry unavailable: ${err instanceof Error ? err.message : String(err)}`,\n\t\t\t);\n\t\t}\n\t}\n\n\t/** Snapshot a runtime for the fleet endpoint. */\n\tasync describe(handle: RuntimeHandle): Promise<RuntimeInfoDto> {\n\t\tconst previousFleetRuntime =\n\t\t\tthis.runtimes.get(handle.key) === handle ? this.describeFleetRuntime(handle) : undefined;\n\t\tlet fleetRuntimeEnriched = false;\n\t\tlet state: SessionStateDto;\n\t\ttry {\n\t\t\tconst authoritative = (await handle.client.getState()) as unknown as SessionStateDto;\n\t\t\tconst fallback = this.fallbackState(handle);\n\t\t\tstate = {\n\t\t\t\t...fallback,\n\t\t\t\t...authoritative,\n\t\t\t\tsessionId: authoritative.sessionId ?? fallback.sessionId,\n\t\t\t\tsessionFile: authoritative.sessionFile ?? fallback.sessionFile,\n\t\t\t\ttasks: authoritative.tasks ?? fallback.tasks,\n\t\t\t};\n\t\t\thandle.lastState = state;\n\t\t\tfleetRuntimeEnriched = true;\n\t\t\tif (handle.error?.startsWith(\"RPC process\")) {\n\t\t\t\thandle.error = undefined;\n\t\t\t\thandle.attention.delete(\"error\");\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\tthis.recordRuntimeError(handle, message);\n\t\t\tthis.logger(`runtime ${handle.key} state unavailable for fleet card: ${message}`);\n\t\t\tstate = this.fallbackState(handle);\n\t\t}\n\t\tlet stats: RuntimeStatsSummaryDto | undefined;\n\t\ttry {\n\t\t\tconst sessionStats = await handle.client.getSessionStats();\n\t\t\tstats = { tokensTotal: sessionStats.tokens.total, cost: sessionStats.cost };\n\t\t\tif (sessionStats.contextUsage) {\n\t\t\t\thandle.lastState = { ...this.fallbackState(handle), contextUsage: sessionStats.contextUsage };\n\t\t\t\tfleetRuntimeEnriched = true;\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tthis.logger(\n\t\t\t\t`runtime ${handle.key} stats unavailable for fleet card: ${err instanceof Error ? err.message : String(err)}`,\n\t\t\t);\n\t\t}\n\t\tlet lastAssistantText: string | undefined;\n\t\ttry {\n\t\t\tconst text = await handle.client.getLastAssistantText();\n\t\t\tlastAssistantText = text ? text.slice(0, 200) : undefined;\n\t\t} catch (err) {\n\t\t\tthis.logger(\n\t\t\t\t`runtime ${handle.key} last assistant text unavailable for fleet card: ${err instanceof Error ? err.message : String(err)}`,\n\t\t\t);\n\t\t}\n\t\tif (\n\t\t\tfleetRuntimeEnriched &&\n\t\t\tpreviousFleetRuntime &&\n\t\t\tthis.runtimes.get(handle.key) === handle &&\n\t\t\t!isDeepStrictEqual(previousFleetRuntime, this.describeFleetRuntime(handle))\n\t\t) {\n\t\t\tthis.scheduleFleetSnapshot();\n\t\t}\n\t\treturn {\n\t\t\tkey: handle.key,\n\t\t\tcwd: handle.cwd,\n\t\t\tstate,\n\t\t\tsettingsRevision: handle.settingsRevision,\n\t\t\tstats,\n\t\t\tbackgroundAgents: [...handle.backgroundAgents.values()],\n\t\t\tneedsAttention: handle.attention.size > 0,\n\t\t\terror: handle.error,\n\t\t\tlastAssistantText,\n\t\t\tcreatedAt: new Date(handle.createdAt).toISOString(),\n\t\t\tlastActivity: new Date(handle.lastActivity).toISOString(),\n\t\t};\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"runtime-pool.d.ts","sourceRoot":"","sources":["../../src/server/runtime-pool.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * RPC runtime pool — one `dreb --mode rpc` child process per live session.\n *\n * dreb's RPC mode is strictly one-session-per-process (switch_session repoints\n * the same process; it never multiplexes), so the pool spawns N children keyed\n * by an opaque runtime key. The telegram bridge is the in-repo precedent.\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { isDeepStrictEqual } from \"node:util\";\nimport { RpcClient, type RpcDashboardSnapshot, type RpcExitInfo } from \"@dreb/coding-agent/rpc\";\nimport {\n\ttype BackgroundAgentDto,\n\ttype FleetRuntimeSnapshotDto,\n\ttype FleetSnapshotEventDto,\n\tMAX_COMPLETED_BACKGROUND_AGENTS,\n\ttype RuntimeInfoDto,\n\ttype RuntimeStatsSummaryDto,\n\ttype SessionStateDto,\n\ttype SubagentArbitrationDto,\n} from \"../shared/protocol.js\";\n\n/** Resolve the absolute path to the dreb CLI (RpcClient defaults to a cwd-relative path). */\nexport function resolveDrebCliPath(): string {\n\tconst resolved = import.meta.resolve(\"@dreb/coding-agent\");\n\treturn join(dirname(fileURLToPath(resolved)), \"cli.js\");\n}\n\nfunction formatRpcExit(info: RpcExitInfo): string {\n\tif (info.error) return `RPC process failed: ${info.error.message}`;\n\tconst tail = info.stderrTail?.trim();\n\t// When the child aborted on its own (e.g. the stdout backpressure guard),\n\t// its stderr tail carries the actual reason — surface it instead of a bare\n\t// exit code. The tail may be multi-line; the existing UI already renders\n\t// multi-line error messages (e.g. timeouts embed full stderr).\n\treturn tail\n\t\t? `RPC process exited (code ${info.code}, signal ${info.signal}). Stderr tail: ${tail}`\n\t\t: `RPC process exited (code ${info.code}, signal ${info.signal})`;\n}\n\nexport type RuntimeEventListener = (key: string, event: Record<string, unknown>) => void;\n\n/** Listener for coalesced, synchronous fleet runtime snapshots. */\nexport type FleetSnapshotListener = (event: FleetSnapshotEventDto) => void;\n\nexport { MAX_COMPLETED_BACKGROUND_AGENTS };\n\ntype DashboardSnapshotClient = RpcClient & { getDashboardSnapshot(): Promise<RpcDashboardSnapshot> };\n\nexport interface DashboardRuntimeSnapshot {\n\tkey: string;\n\tbarrierSeq: number;\n\tsnapshot: RpcDashboardSnapshot;\n}\n\nexport interface RuntimeHandle {\n\tkey: string;\n\tcwd: string;\n\tclient: RpcClient;\n\t/** Session start time (ms epoch) — stable tiebreak for deterministic fleet ordering. */\n\tcreatedAt: number;\n\tlastActivity: number;\n\t/** Needs-attention sources, keyed so they can be cleared independently. */\n\tattention: Map<string, string>;\n\t/** Last runtime-level error, persisted server-side so fleet refreshes stay honest. */\n\terror?: string;\n\t/** Provider error currently responsible for error/attention; retry may clear only this source. */\n\tproviderError?: string;\n\t/** Last authoritative state, patched only with event-derivable fields between RPC reads. */\n\tlastState?: SessionStateDto;\n\t/** Monotonic revision for confirmed model/thinking mutations. */\n\tsettingsRevision: number;\n\t/** Resume-path fallback; events must never invent or overwrite session identity. */\n\tsessionFileFallback?: string;\n\t/** Background agents seen via events (agentId → latest info). */\n\tbackgroundAgents: Map<string, BackgroundAgentDto>;\n}\n\nexport const DEFAULT_DASHBOARD_BARRIER_TTL_MS = 5 * 60_000;\nexport const DEFAULT_DASHBOARD_BARRIER_LIMIT = 1000;\nexport const DEFAULT_FLEET_SNAPSHOT_DEBOUNCE_MS = 200;\n\n/** Events that mutate a field carried by the lightweight fleet snapshot. */\nconst FLEET_SNAPSHOT_EVENT_TYPES = new Set([\n\t\"agent_start\",\n\t\"agent_end\",\n\t\"auto_compaction_start\",\n\t\"auto_compaction_end\",\n\t\"auto_retry_start\",\n\t\"auto_retry_end\",\n\t\"background_agent_start\",\n\t\"background_agent_end\",\n\t\"subagent_arbitration\",\n\t\"extension_ui_request\",\n\t\"extension_ui_response_handled\",\n\t\"message_end\",\n\t\"message_start\",\n\t\"parent_paused_for_background_agents\",\n\t\"runtime_removed\",\n\t\"session_name_changed\",\n\t\"suggest_next\",\n\t\"tasks_update\",\n]);\n\nexport interface RuntimePoolOptions {\n\tcliPath?: string;\n\t/** Extra args for every runtime (e.g. --provider). */\n\tbaseArgs?: string[];\n\t/** RpcClient factory override for tests. */\n\tclientFactory?: (options: { cliPath: string; cwd: string; args: string[] }) => RpcClient;\n\tlogger?: (line: string) => void;\n\t/** Bounds unclaimed RPC snapshot ordering records. */\n\tdashboardBarrierTtlMs?: number;\n\tdashboardBarrierLimit?: number;\n\t/** Injectable clock for deterministic barrier-expiry tests. */\n\tnow?: () => number;\n\t/** Coalescing delay for event-derived fleet snapshot emissions. */\n\tfleetSnapshotDebounceMs?: number;\n}\n\ninterface DashboardBarrier {\n\tseq: number;\n\trecordedAt: number;\n}\n\nexport class RuntimePool {\n\tprivate readonly runtimes = new Map<string, RuntimeHandle>();\n\tprivate readonly listeners: RuntimeEventListener[] = [];\n\tprivate readonly fleetSnapshotListeners: FleetSnapshotListener[] = [];\n\tprivate readonly cliPath: string;\n\tprivate readonly baseArgs: string[];\n\tprivate readonly clientFactory: (options: { cliPath: string; cwd: string; args: string[] }) => RpcClient;\n\tprivate readonly logger: (line: string) => void;\n\t/**\n\t * A single lazily-spawned utility runtime used to service settings/model/\n\t * agent-type endpoints when no user session is live. Kept out of `runtimes`\n\t * (and therefore out of the fleet) so it never shows as a session card.\n\t */\n\tprivate readonly utilities = new Map<string, RuntimeHandle>();\n\tprivate readonly utilityPromises = new Map<string, Promise<RuntimeHandle>>();\n\tprivate readonly starting = new Set<RuntimeHandle>();\n\tprivate readonly startupPromises = new Set<Promise<unknown>>();\n\tprivate readonly exitedHandles = new WeakSet<RuntimeHandle>();\n\t/** Snapshot ordering records observed synchronously from RpcClient stdout. */\n\tprivate readonly dashboardBarriers = new Map<string, DashboardBarrier>();\n\tprivate readonly dashboardBarrierTtlMs: number;\n\tprivate readonly dashboardBarrierLimit: number;\n\tprivate readonly now: () => number;\n\tprivate readonly fleetSnapshotDebounceMs: number;\n\tprivate dashboardBarrierPruneTimer: ReturnType<typeof setTimeout> | undefined;\n\tprivate fleetSnapshotTimer: ReturnType<typeof setTimeout> | undefined;\n\tprivate closing = false;\n\n\tconstructor(options: RuntimePoolOptions = {}) {\n\t\tthis.cliPath = options.cliPath ?? resolveDrebCliPath();\n\t\tthis.baseArgs = options.baseArgs ?? [];\n\t\tthis.clientFactory =\n\t\t\toptions.clientFactory ?? ((o) => new RpcClient({ cliPath: o.cliPath, cwd: o.cwd, args: o.args }));\n\t\tthis.logger = options.logger ?? ((line) => console.warn(`[dashboard] ${line}`));\n\t\tthis.dashboardBarrierTtlMs = options.dashboardBarrierTtlMs ?? DEFAULT_DASHBOARD_BARRIER_TTL_MS;\n\t\tthis.dashboardBarrierLimit = options.dashboardBarrierLimit ?? DEFAULT_DASHBOARD_BARRIER_LIMIT;\n\t\tthis.now = options.now ?? Date.now;\n\t\tthis.fleetSnapshotDebounceMs = options.fleetSnapshotDebounceMs ?? DEFAULT_FLEET_SNAPSHOT_DEBOUNCE_MS;\n\t}\n\n\t/** Subscribe to events from every runtime, tagged with the runtime key. */\n\tonEvent(listener: RuntimeEventListener): () => void {\n\t\tthis.listeners.push(listener);\n\t\treturn () => {\n\t\t\tconst i = this.listeners.indexOf(listener);\n\t\t\tif (i !== -1) this.listeners.splice(i, 1);\n\t\t};\n\t}\n\n\t/** Subscribe to debounced, in-memory fleet snapshots. */\n\tonFleetSnapshot(listener: FleetSnapshotListener): () => void {\n\t\tthis.fleetSnapshotListeners.push(listener);\n\t\treturn () => {\n\t\t\tconst i = this.fleetSnapshotListeners.indexOf(listener);\n\t\t\tif (i !== -1) this.fleetSnapshotListeners.splice(i, 1);\n\t\t};\n\t}\n\n\t/**\n\t * Build the fleet's live-runtime view without RPC or disk access. Map\n\t * insertion order is retained intentionally; the UI owns presentation order.\n\t */\n\tfleetSnapshot(): FleetRuntimeSnapshotDto[] {\n\t\treturn [...this.runtimes.values()].map((handle) => this.describeFleetRuntime(handle));\n\t}\n\n\tlist(): RuntimeHandle[] {\n\t\treturn [...this.runtimes.values()];\n\t}\n\n\tget(key: string): RuntimeHandle | undefined {\n\t\treturn this.runtimes.get(key);\n\t}\n\n\t/** Apply a confirmed model mutation to both the child and the pool snapshot. */\n\tasync setModel(\n\t\thandle: RuntimeHandle,\n\t\tprovider: string,\n\t\tmodelId: string,\n\t): Promise<{\n\t\tmodel: { provider: string; id: string };\n\t\tthinkingLevel: string;\n\t\tavailableThinkingLevels: string[];\n\t\tsettingsRevision: number;\n\t}> {\n\t\tconst model = await handle.client.setModel(provider, modelId);\n\t\tconst state = await handle.client.getState();\n\t\thandle.lastState = {\n\t\t\t...this.fallbackState(handle),\n\t\t\tmodel,\n\t\t\tthinkingLevel: state.thinkingLevel,\n\t\t\tavailableThinkingLevels: state.availableThinkingLevels,\n\t\t};\n\t\thandle.settingsRevision += 1;\n\t\tif (this.runtimes.get(handle.key) === handle) this.scheduleFleetSnapshot();\n\t\treturn {\n\t\t\tmodel,\n\t\t\tthinkingLevel: state.thinkingLevel,\n\t\t\tavailableThinkingLevels: state.availableThinkingLevels,\n\t\t\tsettingsRevision: handle.settingsRevision,\n\t\t};\n\t}\n\n\t/** Apply a confirmed thinking mutation to both the child and the pool snapshot. */\n\tasync setThinkingLevel(\n\t\thandle: RuntimeHandle,\n\t\tlevel: Parameters<RpcClient[\"setThinkingLevel\"]>[0],\n\t): Promise<{ ok: true; settingsRevision: number }> {\n\t\tawait handle.client.setThinkingLevel(level);\n\t\thandle.lastState = { ...this.fallbackState(handle), thinkingLevel: level };\n\t\thandle.settingsRevision += 1;\n\t\tif (this.runtimes.get(handle.key) === handle) this.scheduleFleetSnapshot();\n\t\treturn { ok: true, settingsRevision: handle.settingsRevision };\n\t}\n\n\t/**\n\t * Record the EventHub sequence synchronously when the RPC snapshot marker\n\t * arrives. The marker line precedes its response on stdout, so this runs\n\t * before the RpcClient response continuation even across separate chunks.\n\t */\n\trecordDashboardBarrier(runtimeKey: string, snapshotId: string, seq: number): void {\n\t\tthis.pruneDashboardBarriers();\n\t\tthis.dashboardBarriers.set(this.dashboardBarrierKey(runtimeKey, snapshotId), { seq, recordedAt: this.now() });\n\t\tthis.pruneDashboardBarriers();\n\t}\n\n\t/**\n\t * Capture a parent-session recovery snapshot and pair it with the sequence\n\t * captured at its RPC marker. This deliberately does not infer ordering from\n\t * await: later EventHub publications naturally have higher sequence numbers.\n\t */\n\tasync snapshotDashboard(handle: RuntimeHandle): Promise<DashboardRuntimeSnapshot> {\n\t\tconst snapshot = await (handle.client as DashboardSnapshotClient).getDashboardSnapshot();\n\t\tthis.pruneDashboardBarriers();\n\t\tconst barrierKey = this.dashboardBarrierKey(handle.key, snapshot.snapshotId);\n\t\tconst barrier = this.dashboardBarriers.get(barrierKey);\n\t\tthis.dashboardBarriers.delete(barrierKey);\n\t\tthis.scheduleDashboardBarrierPrune();\n\t\tif (!barrier) {\n\t\t\tthrow new Error(`Dashboard snapshot ${snapshot.snapshotId} arrived without its ordering barrier`);\n\t\t}\n\t\t// This is an authoritative RPC baseline, so it may legitimately lower the\n\t\t// count after a fork/rewind instead of retaining an event-derived maximum.\n\t\thandle.lastState = snapshot.state;\n\t\tthis.scheduleFleetSnapshot();\n\t\treturn { key: handle.key, barrierSeq: barrier.seq, snapshot };\n\t}\n\n\tprivate dashboardBarrierKey(runtimeKey: string, snapshotId: string): string {\n\t\treturn `${runtimeKey}\\0${snapshotId}`;\n\t}\n\n\tprivate pruneDashboardBarriers(): void {\n\t\tconst oldestAllowed = this.now() - this.dashboardBarrierTtlMs;\n\t\tfor (const [snapshotId, barrier] of this.dashboardBarriers) {\n\t\t\tif (barrier.recordedAt < oldestAllowed) this.dashboardBarriers.delete(snapshotId);\n\t\t}\n\t\twhile (this.dashboardBarriers.size > this.dashboardBarrierLimit) {\n\t\t\tconst oldest = this.dashboardBarriers.keys().next().value;\n\t\t\tif (oldest === undefined) break;\n\t\t\tthis.dashboardBarriers.delete(oldest);\n\t\t}\n\t\tthis.scheduleDashboardBarrierPrune();\n\t}\n\n\tprivate scheduleDashboardBarrierPrune(): void {\n\t\tif (this.dashboardBarrierPruneTimer) clearTimeout(this.dashboardBarrierPruneTimer);\n\t\tthis.dashboardBarrierPruneTimer = undefined;\n\t\tlet oldest: DashboardBarrier | undefined;\n\t\tfor (const barrier of this.dashboardBarriers.values()) {\n\t\t\tif (!oldest || barrier.recordedAt < oldest.recordedAt) oldest = barrier;\n\t\t}\n\t\tif (!oldest) return;\n\t\tconst delay = Math.max(1, oldest.recordedAt + this.dashboardBarrierTtlMs - this.now() + 1);\n\t\tthis.dashboardBarrierPruneTimer = setTimeout(() => {\n\t\t\tthis.dashboardBarrierPruneTimer = undefined;\n\t\t\tthis.pruneDashboardBarriers();\n\t\t}, delay);\n\t\tthis.dashboardBarrierPruneTimer.unref?.();\n\t}\n\n\tprivate scheduleFleetSnapshot(): void {\n\t\tif (this.closing) return;\n\t\tif (this.fleetSnapshotTimer) clearTimeout(this.fleetSnapshotTimer);\n\t\tthis.fleetSnapshotTimer = setTimeout(() => {\n\t\t\tthis.fleetSnapshotTimer = undefined;\n\t\t\tif (this.closing) return;\n\t\t\tconst event: FleetSnapshotEventDto = { type: \"fleet_snapshot\", runtimes: this.fleetSnapshot() };\n\t\t\tfor (const listener of this.fleetSnapshotListeners) {\n\t\t\t\ttry {\n\t\t\t\t\tlistener(event);\n\t\t\t\t} catch {\n\t\t\t\t\t// An SSE bridge subscriber must not break the pool's event loop.\n\t\t\t\t}\n\t\t\t}\n\t\t}, this.fleetSnapshotDebounceMs);\n\t\tthis.fleetSnapshotTimer.unref?.();\n\t}\n\n\t/** Spawn a new runtime in `cwd`, optionally opening an existing session file. */\n\tasync create(cwd: string, sessionPath?: string): Promise<RuntimeHandle> {\n\t\tif (this.closing) throw new Error(\"Runtime pool is closing\");\n\t\tconst key = randomBytes(6).toString(\"hex\");\n\t\tconst args = [\"--ui\", \"dashboard\", ...this.baseArgs];\n\t\tif (sessionPath) args.push(\"--session\", sessionPath);\n\t\tconst client = this.clientFactory({ cliPath: this.cliPath, cwd, args });\n\t\tconst handle: RuntimeHandle = {\n\t\t\tkey,\n\t\t\tcwd,\n\t\t\tclient,\n\t\t\tcreatedAt: this.now(),\n\t\t\tlastActivity: this.now(),\n\t\t\tattention: new Map(),\n\t\t\tsettingsRevision: 0,\n\t\t\tsessionFileFallback: sessionPath,\n\t\t\tbackgroundAgents: new Map(),\n\t\t};\n\t\tclient.onEvent((event) => this.handleEvent(handle, event as unknown as Record<string, unknown>));\n\t\tclient.onExit((info) => this.handleRuntimeExit(handle, info));\n\n\t\tconst startup = this.startSessionRuntime(handle);\n\t\tthis.startupPromises.add(startup);\n\t\ttry {\n\t\t\treturn await startup;\n\t\t} finally {\n\t\t\tthis.startupPromises.delete(startup);\n\t\t}\n\t}\n\n\tprivate async startSessionRuntime(handle: RuntimeHandle): Promise<RuntimeHandle> {\n\t\tthis.starting.add(handle);\n\t\ttry {\n\t\t\tif (this.closing) throw new Error(\"Runtime pool is closing\");\n\t\t\tawait handle.client.start();\n\t\t\tif (this.closing) {\n\t\t\t\tawait handle.client.stop();\n\t\t\t\tthrow new Error(\"Runtime pool is closing\");\n\t\t\t}\n\t\t\tawait this.seedBackgroundAgents(handle);\n\t\t\tif (this.closing) {\n\t\t\t\tawait handle.client.stop();\n\t\t\t\tthrow new Error(\"Runtime pool is closing\");\n\t\t\t}\n\t\t\tthis.runtimes.set(handle.key, handle);\n\t\t\tthis.scheduleFleetSnapshot();\n\t\t\treturn handle;\n\t\t} finally {\n\t\t\tthis.starting.delete(handle);\n\t\t}\n\t}\n\n\t/** Stop a runtime and remove it from the pool. */\n\tasync stop(key: string): Promise<boolean> {\n\t\tconst handle = this.runtimes.get(key);\n\t\tif (!handle) return false;\n\t\tthis.handleEvent(handle, { type: \"runtime_removed\" });\n\t\tthis.runtimes.delete(key);\n\t\tthis.scheduleFleetSnapshot();\n\t\tawait handle.client.stop();\n\t\treturn true;\n\t}\n\n\t/**\n\t * Return any live runtime suitable for process-global settings work, spawning\n\t * a hidden utility runtime (in the home directory) if no user session exists.\n\t * This is what lets the settings page — models, agent types, defaults — work\n\t * with zero sessions open, instead of 503-ing.\n\t */\n\tasync ensureUtilityRuntime(cwd = homedir()): Promise<RuntimeHandle> {\n\t\tif (this.closing) throw new Error(\"Runtime pool is closing\");\n\t\tconst existing = this.utilities.get(cwd);\n\t\tif (existing) return existing;\n\t\tlet promise = this.utilityPromises.get(cwd);\n\t\tif (!promise) {\n\t\t\tconst args = [\"--ui\", \"dashboard\", ...this.baseArgs];\n\t\t\tconst client = this.clientFactory({ cliPath: this.cliPath, cwd, args });\n\t\t\tconst handle: RuntimeHandle = {\n\t\t\t\tkey: `utility:${cwd}`,\n\t\t\t\tcwd,\n\t\t\t\tclient,\n\t\t\t\tcreatedAt: this.now(),\n\t\t\t\tlastActivity: this.now(),\n\t\t\t\tattention: new Map(),\n\t\t\t\tsettingsRevision: 0,\n\t\t\t\tbackgroundAgents: new Map(),\n\t\t\t};\n\t\t\tconst startup = this.startUtilityRuntime(cwd, handle);\n\t\t\tthis.startupPromises.add(startup);\n\t\t\tpromise = startup\n\t\t\t\t.catch((err) => {\n\t\t\t\t\t// Allow a retry on the next request instead of caching the failure.\n\t\t\t\t\tthis.utilityPromises.delete(cwd);\n\t\t\t\t\tthrow err;\n\t\t\t\t})\n\t\t\t\t.finally(() => {\n\t\t\t\t\tthis.startupPromises.delete(startup);\n\t\t\t\t});\n\t\t\tthis.utilityPromises.set(cwd, promise);\n\t\t}\n\t\treturn promise;\n\t}\n\n\tprivate async startUtilityRuntime(cwd: string, handle: RuntimeHandle): Promise<RuntimeHandle> {\n\t\tthis.starting.add(handle);\n\t\ttry {\n\t\t\tif (this.closing) throw new Error(\"Runtime pool is closing\");\n\t\t\tawait handle.client.start();\n\t\t\tif (this.closing) {\n\t\t\t\tawait handle.client.stop();\n\t\t\t\tthrow new Error(\"Runtime pool is closing\");\n\t\t\t}\n\t\t\tthis.utilities.set(cwd, handle);\n\t\t\treturn handle;\n\t\t} finally {\n\t\t\tthis.starting.delete(handle);\n\t\t}\n\t}\n\n\tasync stopAll(): Promise<void> {\n\t\tthis.closing = true;\n\t\tif (this.dashboardBarrierPruneTimer) clearTimeout(this.dashboardBarrierPruneTimer);\n\t\tthis.dashboardBarrierPruneTimer = undefined;\n\t\tif (this.fleetSnapshotTimer) clearTimeout(this.fleetSnapshotTimer);\n\t\tthis.fleetSnapshotTimer = undefined;\n\t\tthis.fleetSnapshotListeners.length = 0;\n\t\tthis.dashboardBarriers.clear();\n\t\tconst handles = new Set<RuntimeHandle>([\n\t\t\t...this.runtimes.values(),\n\t\t\t...this.utilities.values(),\n\t\t\t...this.starting.values(),\n\t\t]);\n\t\tconst startupPromises = new Set<Promise<unknown>>([...this.startupPromises, ...this.utilityPromises.values()]);\n\t\tthis.runtimes.clear();\n\t\tthis.utilities.clear();\n\t\tthis.utilityPromises.clear();\n\t\tawait Promise.allSettled([...handles].map((handle) => handle.client.stop()));\n\t\tawait Promise.allSettled([...startupPromises]);\n\t}\n\n\tprivate handleRuntimeExit(handle: RuntimeHandle, info: RpcExitInfo): void {\n\t\tif (this.closing || this.exitedHandles.has(handle) || !this.isLiveHandle(handle)) return;\n\t\tthis.exitedHandles.add(handle);\n\t\tconst message = formatRpcExit(info);\n\t\tthis.recordRuntimeError(handle, message);\n\t\tthis.logger(`runtime ${handle.key} ${message}`);\n\t\tthis.handleEvent(handle, { type: \"agent_end\", messages: [], aborted: true, errorMessage: message });\n\t}\n\n\tprivate isLiveHandle(handle: RuntimeHandle): boolean {\n\t\treturn (\n\t\t\tthis.runtimes.get(handle.key) === handle ||\n\t\t\tthis.utilities.get(handle.cwd) === handle ||\n\t\t\tthis.starting.has(handle)\n\t\t);\n\t}\n\n\tprivate handleEvent(handle: RuntimeHandle, event: Record<string, unknown>): void {\n\t\tconst type = event.type as string;\n\t\tif (type !== \"dashboard_snapshot_barrier\") {\n\t\t\thandle.lastActivity = this.now();\n\t\t\tthis.updateStateFromEvent(handle, event, type);\n\t\t}\n\n\t\t// Track needs-attention sources: extension UI requests, parent\n\t\t// paused, error states.\n\t\tif (type === \"extension_ui_request\") {\n\t\t\tconst method = event.method as string;\n\t\t\tif (\n\t\t\t\tmethod === \"select\" ||\n\t\t\t\tmethod === \"confirm\" ||\n\t\t\t\tmethod === \"input\" ||\n\t\t\t\tmethod === \"editor\" ||\n\t\t\t\tmethod === \"ask\"\n\t\t\t) {\n\t\t\t\thandle.attention.set(`ui:${event.id}`, `extension ${method} awaiting response`);\n\t\t\t}\n\t\t}\n\t\tif (type === \"extension_ui_response_handled\") {\n\t\t\thandle.attention.delete(`ui:${event.id}`);\n\t\t}\n\t\tif (type === \"agent_start\") {\n\t\t\t// A new turn clears prior UI-request attention (requests were resolved or timed out).\n\t\t\tfor (const k of [...handle.attention.keys()]) {\n\t\t\t\tif (k.startsWith(\"ui:\")) handle.attention.delete(k);\n\t\t\t}\n\t\t\thandle.attention.delete(\"paused\");\n\t\t\thandle.attention.delete(\"suggest\");\n\t\t\thandle.attention.delete(\"error\");\n\t\t\thandle.error = undefined;\n\t\t\thandle.providerError = undefined;\n\t\t}\n\t\tif (type === \"suggest_next\") {\n\t\t\t// suggest_next as the ending action = \"your move\": mark needs-attention\n\t\t\t// so the fleet card doesn't read idle. Cleared on the next agent_start.\n\t\t\thandle.attention.set(\"suggest\", \"suggested command awaiting\");\n\t\t}\n\t\tif (type === \"parent_paused_for_background_agents\") {\n\t\t\thandle.attention.set(\"paused\", `paused — ${event.runningAgentCount} background agents running`);\n\t\t}\n\t\tif (type === \"agent_end\") {\n\t\t\thandle.attention.delete(\"paused\");\n\t\t\tif (event.errorMessage) this.recordRuntimeError(handle, String(event.errorMessage));\n\t\t}\n\t\tif (type === \"message_end\") {\n\t\t\tconst message = event.message;\n\t\t\tif (message && typeof message === \"object\" && !Array.isArray(message)) {\n\t\t\t\tconst assistant = message as Record<string, unknown>;\n\t\t\t\tif (assistant.role === \"assistant\" && assistant.stopReason === \"error\") {\n\t\t\t\t\tconst rawError = assistant.errorMessage;\n\t\t\t\t\tconst error = typeof rawError === \"string\" && rawError.trim().length > 0 ? rawError : \"Unknown error\";\n\t\t\t\t\tthis.recordProviderError(handle, error);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (type === \"auto_retry_start\") this.clearProviderError(handle);\n\t\tif (type === \"auto_compaction_start\" && event.reason === \"overflow\") this.clearProviderError(handle);\n\t\tif (type === \"auto_retry_end\" && event.success === false && event.finalError) {\n\t\t\tthis.recordProviderError(handle, String(event.finalError));\n\t\t} else if (type === \"auto_retry_end\" && event.success === true) {\n\t\t\tthis.clearProviderError(handle);\n\t\t}\n\t\tif (type === \"auto_compaction_end\" && event.errorMessage) {\n\t\t\tthis.recordRuntimeError(handle, String(event.errorMessage));\n\t\t}\n\n\t\t// Track background agents from lifecycle events.\n\t\tif (type === \"background_agent_start\") {\n\t\t\thandle.backgroundAgents.set(event.agentId as string, {\n\t\t\t\tagentId: event.agentId as string,\n\t\t\t\tagentType: event.agentType as string,\n\t\t\t\ttaskSummary: event.taskSummary as string,\n\t\t\t\tstartedAt: new Date().toISOString(),\n\t\t\t\tstatus: \"running\",\n\t\t\t\tsessionDir: event.sessionDir as string | undefined,\n\t\t\t});\n\t\t}\n\t\tif (type === \"subagent_arbitration\") {\n\t\t\tconst existing = handle.backgroundAgents.get(event.agentId as string);\n\t\t\tif (existing) {\n\t\t\t\tconst record: SubagentArbitrationDto = {\n\t\t\t\t\tstatus: event.status as SubagentArbitrationDto[\"status\"],\n\t\t\t\t\tproposed: event.proposed as SubagentArbitrationDto[\"proposed\"],\n\t\t\t\t\tfinal: (event.final as SubagentArbitrationDto[\"final\"]) ?? null,\n\t\t\t\t\tchanged: (event.changed as SubagentArbitrationDto[\"changed\"]) ?? [],\n\t\t\t\t\tstep: event.step as number | undefined,\n\t\t\t\t\terrorCode: event.errorCode as string | undefined,\n\t\t\t\t\terrorMessage: event.errorMessage as string | undefined,\n\t\t\t\t};\n\t\t\t\texisting.arbitrations = [...(existing.arbitrations ?? []), record];\n\t\t\t\tif (record.status === \"success\" && record.final) existing.agentType = record.final.agent;\n\t\t\t}\n\t\t}\n\t\tif (type === \"background_agent_end\") {\n\t\t\tconst existing = handle.backgroundAgents.get(event.agentId as string);\n\t\t\tif (existing) {\n\t\t\t\texisting.status = event.success ? \"completed\" : \"failed\";\n\t\t\t\texisting.sessionFile = (event.sessionFile as string | undefined) ?? existing.sessionFile;\n\t\t\t\tthis.pruneCompletedBackgroundAgents(handle);\n\t\t\t}\n\t\t}\n\n\t\tfor (const listener of this.listeners) {\n\t\t\ttry {\n\t\t\t\tlistener(handle.key, event);\n\t\t\t} catch {\n\t\t\t\t// A broken SSE subscriber must not break event distribution.\n\t\t\t}\n\t\t}\n\t\tif (this.runtimes.get(handle.key) === handle && FLEET_SNAPSHOT_EVENT_TYPES.has(type)) {\n\t\t\tthis.scheduleFleetSnapshot();\n\t\t}\n\t}\n\n\t/**\n\t * Events intentionally patch only fields they can prove. Session identity,\n\t * session file, configuration, and context usage remain from the last RPC\n\t * baseline (or the stable creation fallback) until a later reconciliation.\n\t */\n\tprivate updateStateFromEvent(handle: RuntimeHandle, event: Record<string, unknown>, type: string): void {\n\t\tconst state = this.fallbackState(handle);\n\t\tswitch (type) {\n\t\t\tcase \"agent_start\": {\n\t\t\t\tconst model = event.model;\n\t\t\t\tif (\n\t\t\t\t\tmodel &&\n\t\t\t\t\ttypeof model === \"object\" &&\n\t\t\t\t\ttypeof (model as Record<string, unknown>).provider === \"string\" &&\n\t\t\t\t\ttypeof (model as Record<string, unknown>).id === \"string\"\n\t\t\t\t) {\n\t\t\t\t\tconst nextModel = model as { provider: string; id: string };\n\t\t\t\t\tstate.model =\n\t\t\t\t\t\tstate.model?.provider === nextModel.provider && state.model.id === nextModel.id\n\t\t\t\t\t\t\t? { ...state.model, ...nextModel }\n\t\t\t\t\t\t\t: nextModel;\n\t\t\t\t}\n\t\t\t\tstate.isStreaming = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"agent_end\":\n\t\t\t\tstate.isStreaming = false;\n\t\t\t\tbreak;\n\t\t\tcase \"auto_compaction_start\":\n\t\t\t\tstate.isCompacting = true;\n\t\t\t\tbreak;\n\t\t\tcase \"auto_compaction_end\":\n\t\t\t\tstate.isCompacting = false;\n\t\t\t\tbreak;\n\t\t\tcase \"auto_retry_start\":\n\t\t\t\tstate.isRetrying = true;\n\t\t\t\tstate.retryAttempt = typeof event.attempt === \"number\" ? event.attempt : state.retryAttempt;\n\t\t\t\tbreak;\n\t\t\tcase \"auto_retry_end\":\n\t\t\t\tstate.isRetrying = false;\n\t\t\t\tstate.retryAttempt = 0;\n\t\t\t\tbreak;\n\t\t\tcase \"tasks_update\":\n\t\t\t\tif (Array.isArray(event.tasks)) state.tasks = [...event.tasks] as SessionStateDto[\"tasks\"];\n\t\t\t\tbreak;\n\t\t\tcase \"session_name_changed\":\n\t\t\t\tif (typeof event.name === \"string\") state.sessionName = event.name;\n\t\t\t\tbreak;\n\t\t\tcase \"message_start\":\n\t\t\t\tstate.messageCount += 1;\n\t\t\t\tbreak;\n\t\t}\n\t\thandle.lastState = state;\n\t}\n\n\tprivate recordRuntimeError(handle: RuntimeHandle, message: string): void {\n\t\thandle.error = message;\n\t\thandle.attention.set(\"error\", message);\n\t\tif (this.runtimes.get(handle.key) === handle) this.scheduleFleetSnapshot();\n\t}\n\n\tprivate recordProviderError(handle: RuntimeHandle, message: string): void {\n\t\thandle.providerError = message;\n\t\tthis.recordRuntimeError(handle, message);\n\t}\n\n\tprivate clearProviderError(handle: RuntimeHandle): void {\n\t\tconst providerError = handle.providerError;\n\t\tif (providerError === undefined) return;\n\t\thandle.providerError = undefined;\n\t\tif (handle.error === providerError) handle.error = undefined;\n\t\tif (handle.attention.get(\"error\") === providerError) handle.attention.delete(\"error\");\n\t\tif (this.runtimes.get(handle.key) === handle) this.scheduleFleetSnapshot();\n\t}\n\n\tprivate pruneCompletedBackgroundAgents(handle: RuntimeHandle): void {\n\t\tconst evictable = [...handle.backgroundAgents.values()]\n\t\t\t.map((agent, index) => {\n\t\t\t\tconst startedAtMs = Date.parse(agent.startedAt);\n\t\t\t\treturn { agent, index, startedAtMs: Number.isFinite(startedAtMs) ? startedAtMs : 0 };\n\t\t\t})\n\t\t\t.filter(({ agent }) => agent.status !== \"running\")\n\t\t\t.sort((a, b) => a.startedAtMs - b.startedAtMs || a.index - b.index);\n\t\tconst excess = evictable.length - MAX_COMPLETED_BACKGROUND_AGENTS;\n\t\tif (excess <= 0) return;\n\t\tfor (const { agent } of evictable.slice(0, excess)) {\n\t\t\thandle.backgroundAgents.delete(agent.agentId);\n\t\t}\n\t}\n\n\tprivate fallbackState(handle: RuntimeHandle): SessionStateDto {\n\t\tconst previous = handle.lastState;\n\t\treturn {\n\t\t\t...previous,\n\t\t\tsessionId: previous?.sessionId ?? handle.key,\n\t\t\ttasks: previous?.tasks ? [...previous.tasks] : [],\n\t\t\tthinkingLevel: previous?.thinkingLevel ?? \"off\",\n\t\t\tavailableThinkingLevels: previous?.availableThinkingLevels ? [...previous.availableThinkingLevels] : [\"off\"],\n\t\t\tisStreaming: previous?.isStreaming ?? false,\n\t\t\tisRetrying: previous?.isRetrying ?? false,\n\t\t\tretryAttempt: previous?.retryAttempt ?? 0,\n\t\t\tisCompacting: previous?.isCompacting ?? false,\n\t\t\tsteeringMode: previous?.steeringMode ?? \"all\",\n\t\t\tfollowUpMode: previous?.followUpMode ?? \"all\",\n\t\t\tsessionFile: previous?.sessionFile ?? handle.sessionFileFallback,\n\t\t\tautoCompactionEnabled: previous?.autoCompactionEnabled ?? false,\n\t\t\tmessageCount: previous?.messageCount ?? 0,\n\t\t\tpendingMessageCount: previous?.pendingMessageCount ?? 0,\n\t\t};\n\t}\n\n\tprivate describeFleetRuntime(handle: RuntimeHandle): FleetRuntimeSnapshotDto {\n\t\tconst state = this.fallbackState(handle);\n\t\treturn {\n\t\t\tkey: handle.key,\n\t\t\tcwd: handle.cwd,\n\t\t\tstate,\n\t\t\tsettingsRevision: handle.settingsRevision,\n\t\t\tbackgroundAgents: [...handle.backgroundAgents.values()].map((agent) => ({ ...agent })),\n\t\t\tneedsAttention: handle.attention.size > 0,\n\t\t\terror: handle.error,\n\t\t\tcreatedAt: new Date(handle.createdAt).toISOString(),\n\t\t\tlastActivity: new Date(handle.lastActivity).toISOString(),\n\t\t};\n\t}\n\n\tprivate async seedBackgroundAgents(handle: RuntimeHandle): Promise<void> {\n\t\ttry {\n\t\t\tconst agents = (await handle.client.listBackgroundAgents()) as unknown as BackgroundAgentDto[];\n\t\t\tfor (const agent of agents) {\n\t\t\t\thandle.backgroundAgents.set(agent.agentId, agent);\n\t\t\t}\n\t\t\tthis.pruneCompletedBackgroundAgents(handle);\n\t\t} catch (err) {\n\t\t\tthis.logger(\n\t\t\t\t`runtime ${handle.key} background-agent registry unavailable: ${err instanceof Error ? err.message : String(err)}`,\n\t\t\t);\n\t\t}\n\t}\n\n\t/** Snapshot a runtime for the fleet endpoint. */\n\tasync describe(handle: RuntimeHandle): Promise<RuntimeInfoDto> {\n\t\tconst previousFleetRuntime =\n\t\t\tthis.runtimes.get(handle.key) === handle ? this.describeFleetRuntime(handle) : undefined;\n\t\tlet fleetRuntimeEnriched = false;\n\t\tlet state: SessionStateDto;\n\t\ttry {\n\t\t\tconst authoritative = (await handle.client.getState()) as unknown as SessionStateDto;\n\t\t\tconst fallback = this.fallbackState(handle);\n\t\t\tstate = {\n\t\t\t\t...fallback,\n\t\t\t\t...authoritative,\n\t\t\t\tsessionId: authoritative.sessionId ?? fallback.sessionId,\n\t\t\t\tsessionFile: authoritative.sessionFile ?? fallback.sessionFile,\n\t\t\t\ttasks: authoritative.tasks ?? fallback.tasks,\n\t\t\t};\n\t\t\thandle.lastState = state;\n\t\t\tfleetRuntimeEnriched = true;\n\t\t\tif (handle.error?.startsWith(\"RPC process\")) {\n\t\t\t\thandle.error = undefined;\n\t\t\t\thandle.attention.delete(\"error\");\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\tthis.recordRuntimeError(handle, message);\n\t\t\tthis.logger(`runtime ${handle.key} state unavailable for fleet card: ${message}`);\n\t\t\tstate = this.fallbackState(handle);\n\t\t}\n\t\tlet stats: RuntimeStatsSummaryDto | undefined;\n\t\ttry {\n\t\t\tconst sessionStats = await handle.client.getSessionStats();\n\t\t\tstats = {\n\t\t\t\ttokensTotal: sessionStats.tokens.total,\n\t\t\t\tcost: sessionStats.cost,\n\t\t\t\tsubagentCost: sessionStats.subagentCost,\n\t\t\t};\n\t\t\tif (sessionStats.contextUsage) {\n\t\t\t\thandle.lastState = { ...this.fallbackState(handle), contextUsage: sessionStats.contextUsage };\n\t\t\t\tfleetRuntimeEnriched = true;\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tthis.logger(\n\t\t\t\t`runtime ${handle.key} stats unavailable for fleet card: ${err instanceof Error ? err.message : String(err)}`,\n\t\t\t);\n\t\t}\n\t\tlet lastAssistantText: string | undefined;\n\t\ttry {\n\t\t\tconst text = await handle.client.getLastAssistantText();\n\t\t\tlastAssistantText = text ? text.slice(0, 200) : undefined;\n\t\t} catch (err) {\n\t\t\tthis.logger(\n\t\t\t\t`runtime ${handle.key} last assistant text unavailable for fleet card: ${err instanceof Error ? err.message : String(err)}`,\n\t\t\t);\n\t\t}\n\t\tif (\n\t\t\tfleetRuntimeEnriched &&\n\t\t\tpreviousFleetRuntime &&\n\t\t\tthis.runtimes.get(handle.key) === handle &&\n\t\t\t!isDeepStrictEqual(previousFleetRuntime, this.describeFleetRuntime(handle))\n\t\t) {\n\t\t\tthis.scheduleFleetSnapshot();\n\t\t}\n\t\treturn {\n\t\t\tkey: handle.key,\n\t\t\tcwd: handle.cwd,\n\t\t\tstate,\n\t\t\tsettingsRevision: handle.settingsRevision,\n\t\t\tstats,\n\t\t\tbackgroundAgents: [...handle.backgroundAgents.values()],\n\t\t\tneedsAttention: handle.attention.size > 0,\n\t\t\terror: handle.error,\n\t\t\tlastAssistantText,\n\t\t\tcreatedAt: new Date(handle.createdAt).toISOString(),\n\t\t\tlastActivity: new Date(handle.lastActivity).toISOString(),\n\t\t};\n\t}\n}\n"]}
|
|
@@ -684,7 +684,11 @@ export class RuntimePool {
|
|
|
684
684
|
let stats;
|
|
685
685
|
try {
|
|
686
686
|
const sessionStats = await handle.client.getSessionStats();
|
|
687
|
-
stats = {
|
|
687
|
+
stats = {
|
|
688
|
+
tokensTotal: sessionStats.tokens.total,
|
|
689
|
+
cost: sessionStats.cost,
|
|
690
|
+
subagentCost: sessionStats.subagentCost,
|
|
691
|
+
};
|
|
688
692
|
if (sessionStats.contextUsage) {
|
|
689
693
|
handle.lastState = { ...this.fallbackState(handle), contextUsage: sessionStats.contextUsage };
|
|
690
694
|
fleetRuntimeEnriched = true;
|