@polderlabs/bizar-omp 0.6.5 → 0.6.7

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.
Files changed (44) hide show
  1. package/README.md +3 -3
  2. package/dist/cli/agents-ink.d.ts +1 -1
  3. package/dist/cli/agents-ink.d.ts.map +1 -1
  4. package/dist/cli/agents-ink.js +8 -0
  5. package/dist/cli/agents-ink.js.map +1 -1
  6. package/dist/cli/agents.d.ts +1 -1
  7. package/dist/cli/agents.d.ts.map +1 -1
  8. package/dist/cli/agents.js +1 -1
  9. package/dist/cli/agents.js.map +1 -1
  10. package/dist/cli/omb-commands.js +1 -1
  11. package/dist/cli/omb-commands.js.map +1 -1
  12. package/dist/cli/omb-update.d.ts +5 -0
  13. package/dist/cli/omb-update.d.ts.map +1 -1
  14. package/dist/cli/omb-update.js +14 -5
  15. package/dist/cli/omb-update.js.map +1 -1
  16. package/dist/cli/update-ink.d.ts +50 -0
  17. package/dist/cli/update-ink.d.ts.map +1 -0
  18. package/dist/cli/update-ink.js +185 -0
  19. package/dist/cli/update-ink.js.map +1 -0
  20. package/dist/omp/daemon-service.d.ts.map +1 -1
  21. package/dist/omp/daemon-service.js +453 -2
  22. package/dist/omp/daemon-service.js.map +1 -1
  23. package/dist/omp/daemon.d.ts +1 -1
  24. package/dist/omp/daemon.js +1 -1
  25. package/dist/omp/dashboard-html.d.ts.map +1 -1
  26. package/dist/omp/dashboard-html.js +292 -48
  27. package/dist/omp/dashboard-html.js.map +1 -1
  28. package/dist/omp/omb-sessions.d.ts +25 -0
  29. package/dist/omp/omb-sessions.d.ts.map +1 -1
  30. package/dist/omp/omb-sessions.js +100 -1
  31. package/dist/omp/omb-sessions.js.map +1 -1
  32. package/dist/omp/omb-settings.d.ts +13 -0
  33. package/dist/omp/omb-settings.d.ts.map +1 -1
  34. package/dist/omp/omb-settings.js +46 -2
  35. package/dist/omp/omb-settings.js.map +1 -1
  36. package/dist/omp/omp-provider-config.d.ts +26 -0
  37. package/dist/omp/omp-provider-config.d.ts.map +1 -0
  38. package/dist/omp/omp-provider-config.js +160 -0
  39. package/dist/omp/omp-provider-config.js.map +1 -0
  40. package/docs/compatibility/baseline.json +1 -1
  41. package/docs/releases/0.6.6.md +27 -0
  42. package/docs/releases/0.6.7.md +29 -0
  43. package/docs/releases/native-stable.md +1 -1
  44. package/package.json +4 -2
@@ -4,13 +4,14 @@ import { deferred } from "./daemon.js";
4
4
  import { randomBytes } from "node:crypto";
5
5
  import { mkdirSync, statSync } from "node:fs";
6
6
  import { resolve } from "node:path";
7
- import { listOmbSessions, ombSessionDisplayName } from "./omb-sessions.js";
7
+ import { listOmbSessions, ombSessionDisplayName, ombSessionJournalPath, readOmbSessionMessages } from "./omb-sessions.js";
8
8
  import { ombSessionName } from "./tmux.js";
9
9
  import { DAEMON_PROTOCOL_VERSION, DaemonError, daemonStateDir, writeSnapshot } from "./daemon.js";
10
10
  import { captureOmbSessionConsole, createOmbSession, deleteOmbSession, forgetOmbSession, openOmbSessionInTerminal, renameOmbSession, sendOmbSessionInput, setOmbSessionMouse, } from "./tmux.js";
11
11
  import { DASHBOARD_PATH, dashboardPageHtml } from "./dashboard-html.js";
12
12
  import { OMB_APPROVAL_MODES, OMB_THINKING_LEVELS, loadOmbSettings, saveOmbSettings } from "./omb-settings.js";
13
13
  import { startTray, stopTray } from "./tray.js";
14
+ import { deleteCustomOmpProvider, listCustomOmpProviders, OMP_PROVIDER_APIS, saveCustomOmpProvider, } from "./omp-provider-config.js";
14
15
  const SCAN_INTERVAL_MS = 5_000;
15
16
  export const DASHBOARD_MAX_BODY_BYTES = 64 * 1024;
16
17
  export const DASHBOARD_BODY_TIMEOUT_MS = 10_000;
@@ -40,6 +41,224 @@ function isDirectory(path) {
40
41
  return false;
41
42
  }
42
43
  }
44
+ const OMP_OUTPUT_LIMIT = 256 * 1024;
45
+ const OMP_TIMEOUT_MS = 12_000;
46
+ const OMP_PROVIDER_KEYS = new Set(["enabledProviders", "disabledProviders", "modelProviderOrder"]);
47
+ const OMP_AUTH_ACTIONS = new Set(["status", "list"]);
48
+ /** Invoke only fixed OMP subcommands. Output is capped and never sent verbatim. */
49
+ async function runDashboardOmp(env, args) {
50
+ const child = spawn(env.BIZAR_OMP_BIN || "omp", args, { env, stdio: ["ignore", "pipe", "pipe"] });
51
+ let output = "";
52
+ const collect = (chunk) => { if (output.length < OMP_OUTPUT_LIMIT)
53
+ output += String(chunk).slice(0, OMP_OUTPUT_LIMIT - output.length); };
54
+ child.stdout?.on("data", collect);
55
+ // Stderr can contain provider diagnostics and credentials. Drain it so the
56
+ // child cannot block, but do not retain it or expose it to the browser.
57
+ child.stderr?.resume();
58
+ const code = await new Promise((resolveResult, reject) => {
59
+ const timer = setTimeout(() => { child.kill(); reject(new Error("OMP command timed out")); }, OMP_TIMEOUT_MS);
60
+ child.once("error", error => { clearTimeout(timer); reject(error); });
61
+ child.once("close", value => { clearTimeout(timer); resolveResult(value ?? 1); });
62
+ });
63
+ if (code !== 0)
64
+ throw new Error("OMP command failed");
65
+ return output;
66
+ }
67
+ const STREAM_POLL_MS = 1_000;
68
+ const STREAM_KEEPALIVE_MS = 15_000;
69
+ // Per-stream cleanup callbacks. service.close() iterates this to clear
70
+ // poll/keepalive timers immediately, so a streaming client cannot keep the
71
+ // daemon alive past shutdown.
72
+ const STREAM_CLEANUPS = new Map();
73
+ async function streamSessionMessages(res, session, env, log) {
74
+ let active = true;
75
+ let lastStamp = "";
76
+ let lastSnapshot = "";
77
+ let journalPath;
78
+ try {
79
+ journalPath = await ombSessionJournalPath(session, env) ?? undefined;
80
+ }
81
+ catch {
82
+ journalPath = undefined;
83
+ }
84
+ res.writeHead(200, {
85
+ "content-type": "text/event-stream",
86
+ "cache-control": "no-cache, no-transform",
87
+ "x-bizar-daemon-stream": "messages",
88
+ "x-accel-buffering": "no",
89
+ });
90
+ const pollTimer = setInterval(async () => {
91
+ if (!active)
92
+ return;
93
+ let next;
94
+ try {
95
+ if (!journalPath) {
96
+ journalPath = await ombSessionJournalPath(session, env) ?? undefined;
97
+ }
98
+ if (journalPath) {
99
+ try {
100
+ next = JSON.stringify({ mtimeMs: statSync(journalPath).mtimeMs, size: statSync(journalPath).size });
101
+ }
102
+ catch {
103
+ journalPath = undefined;
104
+ }
105
+ }
106
+ if (next && next !== lastStamp) {
107
+ lastStamp = next;
108
+ const messages = journalPath ? await readOmbSessionMessages(session, env) : [];
109
+ const snapshot = JSON.stringify({ kind: "snapshot", capturedAt: Date.now(), messages });
110
+ if (snapshot !== lastSnapshot) {
111
+ lastSnapshot = snapshot;
112
+ send("snapshot", { capturedAt: Date.now(), messages });
113
+ }
114
+ }
115
+ else if (!next) {
116
+ send("idle", { capturedAt: Date.now() });
117
+ }
118
+ }
119
+ catch (error) {
120
+ log(`stream error for ${session.name}: ${error instanceof Error ? error.message : String(error)}`);
121
+ send("error", { capturedAt: Date.now(), error: error instanceof Error ? error.message : String(error) });
122
+ }
123
+ }, STREAM_POLL_MS);
124
+ const keepaliveTimer = setInterval(() => {
125
+ if (!active)
126
+ return;
127
+ res.write(`: keepalive ${Date.now()}\n\n`);
128
+ }, STREAM_KEEPALIVE_MS);
129
+ pollTimer.unref?.();
130
+ keepaliveTimer.unref?.();
131
+ const send = (event, payload) => {
132
+ if (!active)
133
+ return;
134
+ res.write(`event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`);
135
+ };
136
+ const cleanup = () => {
137
+ if (!active)
138
+ return;
139
+ active = false;
140
+ STREAM_CLEANUPS.delete(res);
141
+ clearInterval(pollTimer);
142
+ clearInterval(keepaliveTimer);
143
+ res.end();
144
+ };
145
+ STREAM_CLEANUPS.set(res, cleanup);
146
+ res.on("close", cleanup);
147
+ res.on("error", cleanup);
148
+ }
149
+ function configEntries(value) {
150
+ const raw = Array.isArray(value) ? value : value && typeof value === "object"
151
+ ? (Array.isArray(value.settings)
152
+ ? value.settings
153
+ : Object.entries(value).map(([key, entry]) => entry && typeof entry === "object" ? { key, ...entry } : { key, value: entry }))
154
+ : [];
155
+ return raw.flatMap(entry => {
156
+ if (!entry || typeof entry !== "object")
157
+ return [];
158
+ const item = entry;
159
+ const key = typeof item.key === "string" ? item.key : typeof item.name === "string" ? item.name : "";
160
+ const type = typeof item.type === "string" ? item.type : "string";
161
+ const description = typeof item.description === "string" ? item.description : undefined;
162
+ const sensitive = /(?:token|secret|password|credential|api[-_]?key|bearer|private key)/i;
163
+ const containsSensitiveValue = (candidate, depth = 0) => depth > 4 || Boolean(candidate && typeof candidate === "object" && Object.entries(candidate).some(([nestedKey, nestedValue]) => sensitive.test(nestedKey) || containsSensitiveValue(nestedValue, depth + 1)));
164
+ if (!key || sensitive.test(key) || (description ? sensitive.test(description) : false) || item.redacted === true || containsSensitiveValue(item.value)) {
165
+ return key ? [{ key, type, description: typeof item.description === "string" ? item.description : undefined, redacted: true, writable: false }] : [];
166
+ }
167
+ const options = Array.isArray(item.options) ? item.options.filter(option => typeof option === "string") : undefined;
168
+ return [{ key, type, description: typeof item.description === "string" ? item.description : undefined, options, value: item.value, writable: ["boolean", "number", "string", "enum", "array", "record"].includes(type) }];
169
+ }).sort((left, right) => left.key.localeCompare(right.key));
170
+ }
171
+ async function dashboardConfig(env) {
172
+ return configEntries(JSON.parse(await runDashboardOmp(env, ["config", "list", "--json"])));
173
+ }
174
+ function providerValues(settings, key) {
175
+ const value = settings.find(setting => setting.key === key)?.value;
176
+ return Array.isArray(value) ? value.filter(item => typeof item === "string") : [];
177
+ }
178
+ async function dashboardProviders(env) {
179
+ const [models, settings] = await Promise.all([dashboardModels(env), dashboardConfig(env)]);
180
+ const customProviders = listCustomOmpProviders(env);
181
+ const enabledProviders = providerValues(settings, "enabledProviders");
182
+ const disabledProviders = providerValues(settings, "disabledProviders");
183
+ const modelProviderOrder = providerValues(settings, "modelProviderOrder");
184
+ const ids = [...new Set([...models.map(model => model.provider), ...customProviders.map(provider => provider.id)])].sort();
185
+ const providers = ids.map(id => ({
186
+ id,
187
+ models: models.filter(model => model.provider === id).length,
188
+ enabled: !disabledProviders.includes(id) && (enabledProviders.length === 0 || enabledProviders.includes(id)),
189
+ custom: customProviders.find(provider => provider.id === id),
190
+ }));
191
+ return { providers, enabledProviders, disabledProviders, modelProviderOrder, providerApis: OMP_PROVIDER_APIS };
192
+ }
193
+ /** Retain only small, non-secret status facts from the auth broker's JSON. */
194
+ function safeAuthStatus(value) {
195
+ if (value === null || typeof value === "boolean")
196
+ return value;
197
+ if (typeof value === "number")
198
+ return Number.isFinite(value) ? value : undefined;
199
+ if (typeof value === "string")
200
+ return /(?:token|secret|password|credential|api[-_]?key|bearer)/i.test(value) ? undefined : value.slice(0, 256);
201
+ if (Array.isArray(value))
202
+ return value.map(safeAuthStatus).filter(item => item !== undefined).slice(0, 32);
203
+ if (!value || typeof value !== "object")
204
+ return undefined;
205
+ return Object.fromEntries(Object.entries(value).flatMap(([key, item]) => /(?:token|secret|password|credential|api[-_]?key|bearer)/i.test(key)
206
+ ? [] : [[key.slice(0, 96), safeAuthStatus(item)]]).filter(([, item]) => item !== undefined));
207
+ }
208
+ function collabHost(value) {
209
+ const record = (value && typeof value === "object") ? value : {};
210
+ const model = (record.model && typeof record.model === "object") ? record.model : {};
211
+ return {
212
+ instanceId: typeof record.instanceId === "string" ? record.instanceId : null,
213
+ pid: typeof record.pid === "number" ? record.pid : null,
214
+ sessionId: typeof record.sessionId === "string" ? record.sessionId : null,
215
+ sessionName: typeof record.sessionName === "string" ? record.sessionName : null,
216
+ cwd: typeof record.cwd === "string" ? record.cwd : null,
217
+ model: {
218
+ provider: typeof model.provider === "string" ? model.provider : null,
219
+ id: typeof model.id === "string" ? model.id : null,
220
+ },
221
+ startedAt: typeof record.startedAt === "number" && Number.isFinite(record.startedAt) ? record.startedAt : null,
222
+ participants: typeof record.participants === "number" && Number.isFinite(record.participants) ? record.participants : 0,
223
+ relayConnected: typeof record.relayConnected === "boolean" ? record.relayConnected : false,
224
+ access: typeof record.access === "string" ? record.access : null,
225
+ };
226
+ }
227
+ function configValue(setting, value) {
228
+ if (!setting.writable)
229
+ return undefined;
230
+ if (setting.type === "boolean")
231
+ return typeof value === "boolean" ? String(value) : undefined;
232
+ if (setting.type === "number")
233
+ return typeof value === "number" && Number.isFinite(value) ? String(value) : undefined;
234
+ if (setting.type === "string")
235
+ return typeof value === "string" && value.length <= 16_384 ? value : undefined;
236
+ if (setting.type === "enum")
237
+ return typeof value === "string" && (!setting.options || setting.options.includes(value)) ? value : undefined;
238
+ if (setting.type === "array")
239
+ return Array.isArray(value) && value.every(item => typeof item === "string") ? JSON.stringify(value) : undefined;
240
+ if (setting.type === "record")
241
+ return value && typeof value === "object" && !Array.isArray(value) ? JSON.stringify(value) : undefined;
242
+ return undefined;
243
+ }
244
+ function customProviderInput(body) {
245
+ const id = typeof body.id === "string" ? body.id.trim() : "";
246
+ const baseUrl = typeof body.baseUrl === "string" ? body.baseUrl.trim() : "";
247
+ const api = typeof body.api === "string" ? body.api : undefined;
248
+ const auth = typeof body.auth === "string" ? body.auth : undefined;
249
+ const models = Array.isArray(body.models) ? body.models.filter(model => typeof model === "string") : undefined;
250
+ if (!id || !baseUrl || !api || !auth || !models)
251
+ return undefined;
252
+ return {
253
+ id,
254
+ baseUrl,
255
+ api,
256
+ auth,
257
+ models,
258
+ apiKey: typeof body.apiKey === "string" ? body.apiKey : undefined,
259
+ clearApiKey: body.clearApiKey === true,
260
+ };
261
+ }
43
262
  async function dashboardModels(env) {
44
263
  const child = spawn(env.BIZAR_OMP_BIN || "omp", ["models", "--json"], { env, stdio: ["ignore", "pipe", "ignore"] });
45
264
  let output = "";
@@ -231,6 +450,11 @@ export function startDaemonService(options = {}) {
231
450
  res.end('{"ok":true}\n');
232
451
  return;
233
452
  }
453
+ if (path === "/capabilities" && (req.method === "GET" || req.method === "HEAD")) {
454
+ res.writeHead(200, { "content-type": "application/json", "x-bizar-daemon-protocol": String(DAEMON_PROTOCOL_VERSION) });
455
+ res.end(`${JSON.stringify({ protocolVersion: DAEMON_PROTOCOL_VERSION, capabilities: ["ping", "overview", "messages", "messages-stream", "omp-collab", "settings-watch", "snapshots", "providers", "auth-status"] })}\n`);
456
+ return;
457
+ }
234
458
  if (!authorized(req)) {
235
459
  res.writeHead(401, { "content-type": "application/json" });
236
460
  res.end('{"error":"unauthorized"}\n');
@@ -258,6 +482,190 @@ export function startDaemonService(options = {}) {
258
482
  }
259
483
  return;
260
484
  }
485
+ if (path === "/overview" && req.method === "GET") {
486
+ const projected = sessions.map(dashboardSession);
487
+ const states = projected.reduce((counts, session) => { counts[session.state] = (counts[session.state] ?? 0) + 1; return counts; }, {});
488
+ const workspaces = new Set(projected.map(session => session.cwd ?? session.project).filter(Boolean));
489
+ let models = [];
490
+ let ompAvailable = true;
491
+ try {
492
+ models = await dashboardModels(env);
493
+ }
494
+ catch {
495
+ ompAvailable = false;
496
+ }
497
+ json(res, 200, {
498
+ collectedAt: lastScanAt ?? 0,
499
+ stale: !lastScanAt || Date.now() - lastScanAt > Math.max(intervalMs * 2, SCAN_INTERVAL_MS * 2),
500
+ scanError: lastScanError ?? undefined,
501
+ daemon: { healthy: !lastScanError, ompAvailable },
502
+ sessions: { total: projected.length, states, workspaces: workspaces.size, attention: projected.filter(session => ["waiting", "disconnected", "stopped"].includes(session.state)).slice(0, 12) },
503
+ models: { total: models.length, providers: new Set(models.map(model => model.provider)).size },
504
+ });
505
+ return;
506
+ }
507
+ if (path === "/omp/config" && req.method === "GET") {
508
+ try {
509
+ json(res, 200, { settings: await dashboardConfig(env) });
510
+ }
511
+ catch {
512
+ json(res, 503, { error: "OMP configuration is unavailable" });
513
+ }
514
+ return;
515
+ }
516
+ if (path === "/omp/config" && req.method === "PATCH") {
517
+ const body = await readBody(req);
518
+ const key = typeof body.key === "string" ? body.key : "";
519
+ try {
520
+ const settings = await dashboardConfig(env);
521
+ const setting = settings.find(candidate => candidate.key === key);
522
+ const native = setting ? configValue(setting, body.value) : undefined;
523
+ if (native === undefined || !setting) {
524
+ json(res, 400, { error: "unsupported OMP configuration value" });
525
+ return;
526
+ }
527
+ await runDashboardOmp(env, ["config", "set", setting.key, native]);
528
+ const effective = (await dashboardConfig(env)).find(candidate => candidate.key === setting.key);
529
+ json(res, 200, { setting: effective });
530
+ }
531
+ catch {
532
+ json(res, 503, { error: "OMP configuration update failed" });
533
+ }
534
+ return;
535
+ }
536
+ if (path === "/omp/config/reset" && req.method === "POST") {
537
+ const body = await readBody(req);
538
+ const key = typeof body.key === "string" ? body.key : "";
539
+ try {
540
+ const setting = (await dashboardConfig(env)).find(candidate => candidate.key === key && candidate.writable);
541
+ if (!setting) {
542
+ json(res, 400, { error: "unsupported OMP configuration key" });
543
+ return;
544
+ }
545
+ await runDashboardOmp(env, ["config", "reset", setting.key]);
546
+ json(res, 200, { setting: (await dashboardConfig(env)).find(candidate => candidate.key === setting.key) });
547
+ }
548
+ catch {
549
+ json(res, 503, { error: "OMP configuration reset failed" });
550
+ }
551
+ return;
552
+ }
553
+ if (path === "/omp/providers" && req.method === "GET") {
554
+ try {
555
+ json(res, 200, await dashboardProviders(env));
556
+ }
557
+ catch {
558
+ json(res, 503, { error: "OMP providers are unavailable" });
559
+ }
560
+ return;
561
+ }
562
+ if (path === "/omp/providers" && req.method === "PATCH") {
563
+ const body = await readBody(req);
564
+ const operation = typeof body.operation === "string" ? body.operation : "";
565
+ const provider = typeof body.provider === "string" && /^[a-zA-Z0-9._-]{1,128}$/.test(body.provider) ? body.provider : "";
566
+ try {
567
+ const settings = await dashboardConfig(env);
568
+ const values = (key) => providerValues(settings, key);
569
+ if (operation === "order" && Array.isArray(body.order) && body.order.every(value => typeof value === "string" && /^[a-zA-Z0-9._-]{1,128}$/.test(value))) {
570
+ await runDashboardOmp(env, ["config", "set", "modelProviderOrder", JSON.stringify(body.order)]);
571
+ json(res, 200, await dashboardProviders(env));
572
+ return;
573
+ }
574
+ if (!provider || !["enable", "disable"].includes(operation)) {
575
+ json(res, 400, { error: "unsupported provider operation" });
576
+ return;
577
+ }
578
+ const enabled = values("enabledProviders");
579
+ const disabled = values("disabledProviders");
580
+ const nextDisabled = operation === "enable" ? disabled.filter(value => value !== provider) : [...new Set([...disabled, provider])];
581
+ const nextEnabled = operation === "enable"
582
+ ? (enabled.length ? [...new Set([...enabled, provider])] : enabled)
583
+ : enabled.filter(value => value !== provider);
584
+ for (const [key, next] of [["disabledProviders", nextDisabled], ["enabledProviders", nextEnabled]]) {
585
+ if (!OMP_PROVIDER_KEYS.has(key) || !settings.find(setting => setting.key === key)?.writable) {
586
+ json(res, 400, { error: "provider controls are unavailable" });
587
+ return;
588
+ }
589
+ await runDashboardOmp(env, ["config", "set", key, JSON.stringify(next)]);
590
+ }
591
+ json(res, 200, await dashboardProviders(env));
592
+ }
593
+ catch {
594
+ json(res, 503, { error: "provider update failed" });
595
+ }
596
+ return;
597
+ }
598
+ const customProviderMatch = path.match(/^\/omp\/providers\/([^/]+)$/);
599
+ if (customProviderMatch && req.method === "PUT") {
600
+ const id = decodeURIComponent(customProviderMatch[1]);
601
+ const body = await readBody(req);
602
+ const input = customProviderInput({ ...body, id });
603
+ if (!input) {
604
+ json(res, 400, { error: "provider id, endpoint, API, auth mode, and model ids are required" });
605
+ return;
606
+ }
607
+ try {
608
+ const provider = saveCustomOmpProvider(input, env);
609
+ await dashboardModels(env);
610
+ json(res, 200, { provider });
611
+ }
612
+ catch (error) {
613
+ json(res, 400, { error: error instanceof Error ? error.message : "provider update failed" });
614
+ }
615
+ return;
616
+ }
617
+ if (customProviderMatch && req.method === "DELETE") {
618
+ const id = decodeURIComponent(customProviderMatch[1]);
619
+ try {
620
+ if (!deleteCustomOmpProvider(id, env)) {
621
+ json(res, 404, { error: "custom provider was not found" });
622
+ return;
623
+ }
624
+ json(res, 200, { ok: true });
625
+ }
626
+ catch (error) {
627
+ json(res, 400, { error: error instanceof Error ? error.message : "provider removal failed" });
628
+ }
629
+ return;
630
+ }
631
+ if (path === "/omp/provider-action" && req.method === "POST") {
632
+ const body = await readBody(req);
633
+ const action = typeof body.action === "string" ? body.action : "";
634
+ if (!OMP_AUTH_ACTIONS.has(action)) {
635
+ json(res, 400, { error: "unsupported auth action" });
636
+ return;
637
+ }
638
+ try {
639
+ const output = await runDashboardOmp(env, ["auth-broker", action, "--json"]);
640
+ let result;
641
+ try {
642
+ result = output.trim() ? safeAuthStatus(JSON.parse(output)) : undefined;
643
+ }
644
+ catch {
645
+ result = undefined;
646
+ }
647
+ json(res, 200, { status: "completed", result });
648
+ }
649
+ catch {
650
+ json(res, 503, { error: "auth action failed" });
651
+ }
652
+ return;
653
+ }
654
+ if (path === "/omp/collab" && req.method === "GET") {
655
+ try {
656
+ const raw = await runDashboardOmp(env, ["collab", "list", "--json"]);
657
+ const parsed = JSON.parse(raw);
658
+ if (parsed && Array.isArray(parsed.hosts)) {
659
+ json(res, 200, { hosts: parsed.hosts.map(collabHost) });
660
+ return;
661
+ }
662
+ json(res, 200, { hosts: [] });
663
+ }
664
+ catch {
665
+ json(res, 503, { error: "could not list collab hosts" });
666
+ }
667
+ return;
668
+ }
261
669
  if (path === "/settings" && req.method === "PATCH") {
262
670
  const body = await readBody(req);
263
671
  const previous = loadOmbSettings(env);
@@ -353,6 +761,33 @@ export function startDaemonService(options = {}) {
353
761
  return;
354
762
  }
355
763
  const consoleMatch = path.match(/^\/sessions\/([^/]+)\/console$/);
764
+ const messagesMatch = path.match(/^\/sessions\/([^/]+)\/messages$/);
765
+ const streamMatch = path.match(/^\/sessions\/([^/]+)\/messages\/stream$/);
766
+ if (messagesMatch && req.method === "GET") {
767
+ const name = decodeURIComponent(messagesMatch[1]);
768
+ const current = sessions.find(candidate => candidate.name === name);
769
+ if (!current) {
770
+ json(res, 404, { error: "unknown session" });
771
+ return;
772
+ }
773
+ try {
774
+ json(res, 200, { messages: await readOmbSessionMessages(current, env), capturedAt: Date.now() });
775
+ }
776
+ catch {
777
+ json(res, 503, { error: "session messages are not available" });
778
+ }
779
+ return;
780
+ }
781
+ if (streamMatch && req.method === "GET") {
782
+ const name = decodeURIComponent(streamMatch[1]);
783
+ const current = sessions.find(candidate => candidate.name === name);
784
+ if (!current) {
785
+ json(res, 404, { error: "unknown session" });
786
+ return;
787
+ }
788
+ streamSessionMessages(res, current, env, log);
789
+ return;
790
+ }
356
791
  if (consoleMatch && req.method === "GET") {
357
792
  const name = decodeURIComponent(consoleMatch[1]);
358
793
  const current = sessions.find(candidate => candidate.name === name);
@@ -465,7 +900,23 @@ export function startDaemonService(options = {}) {
465
900
  close: async () => {
466
901
  if (scanTimer)
467
902
  clearInterval(scanTimer);
468
- await new Promise(resolve => server.close(() => resolve()));
903
+ // Run every SSE cleanup so poll/keepalive timers drop the response
904
+ // reference immediately — otherwise the timers themselves keep the
905
+ // socket (and therefore server.close()) alive past shutdown.
906
+ for (const cleanup of [...STREAM_CLEANUPS.values()]) {
907
+ try {
908
+ cleanup();
909
+ }
910
+ catch { /* already gone */ }
911
+ }
912
+ STREAM_CLEANUPS.clear();
913
+ await new Promise(resolve => {
914
+ server.close(() => resolve());
915
+ // server.close() alone would wait on HTTP/1.1 keep-alive and undici's
916
+ // pooled fetch connections. closeAllConnections() tears them down at
917
+ // once so a streaming client cannot hold shutdown open.
918
+ server.closeAllConnections?.();
919
+ });
469
920
  },
470
921
  };
471
922
  server.listen(options.port ?? 0, "127.0.0.1", () => {