@cabane/companion 0.6.215 → 0.6.217

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/dist/runtime.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/runtime.ts
2
2
  import { randomUUID as randomUUID7 } from "crypto";
3
- import { fileURLToPath as fileURLToPath6 } from "url";
3
+ import { fileURLToPath as fileURLToPath5 } from "url";
4
4
 
5
5
  // src/cabane-command.ts
6
6
  import { accessSync, constants, realpathSync as realpathSync2 } from "fs";
@@ -254,9 +254,9 @@ var companionConfigSchema = z2.object({
254
254
  // silent roster: an agent nobody listed gets no hook and no error, and starts
255
255
  // in the operator's `$HOME`.
256
256
  prepareHook: prepareHookSchema.optional(),
257
- // `dashboardPort` and `autoOpen` are IGNORED by the CLI (no port, no browser)
258
- // but stay in the schema so an existing file still parses; the Electron shell
259
- // reads them. `logLevel` is pino's level, live-editable.
257
+ // The CLI ignores both dashboard settings. Desktop uses `dashboardPort`;
258
+ // `autoOpen` remains parseable for older config files but no longer controls
259
+ // startup. `logLevel` is pino's level, live-editable.
260
260
  dashboardPort: z2.number().int().min(1).max(65535).optional(),
261
261
  autoOpen: z2.boolean().optional(),
262
262
  logLevel: z2.enum(["warn", "info", "debug"]).optional(),
@@ -294,7 +294,10 @@ var companionConfigSchema = z2.object({
294
294
  // credential and secrets on its stdin (`runner/launch.ts`, `wrapped`). The README lists
295
295
  // what a wrapper can rely on.
296
296
  runner: z2.object({
297
- command: z2.array(z2.string().min(1)).min(1)
297
+ command: z2.array(z2.string().min(1)).min(1),
298
+ // The external wrapper supplies only broker placeholders and removes
299
+ // credential mounts. Do not hand provider logins to that runner on stdin.
300
+ brokeredCredentials: z2.boolean().optional()
298
301
  }).strict().optional()
299
302
  });
300
303
  function workRoot(cfg) {
@@ -456,31 +459,199 @@ function requireConfig() {
456
459
  return cfg;
457
460
  }
458
461
 
459
- // src/dashboard/server.ts
460
- import { dirname as dirname5, join as join7 } from "path";
461
- import { fileURLToPath as fileURLToPath3 } from "url";
462
- import { serve } from "@hono/node-server";
463
- import { Hono } from "hono";
464
-
465
- // src/dashboard/routes.ts
466
- import { openSync as openSync2, readSync, closeSync, fstatSync, existsSync as existsSync4 } from "fs";
467
- import { readFile } from "fs/promises";
468
- import { extname, join as join6, normalize } from "path";
469
- import { streamSSE } from "hono/streaming";
462
+ // src/control-socket.ts
463
+ import { createHash } from "crypto";
464
+ import { chmodSync as chmodSync2, existsSync as existsSync4, rmSync as rmSync2, mkdirSync as mkdirSync2 } from "fs";
465
+ import { createServer, connect } from "net";
466
+ import { join as join5 } from "path";
467
+ var CONTROL_TIMEOUT_MS = 1e3;
468
+ var MAX_UNIX_SOCKET_PATH_BYTES = 103;
469
+ function controlSocketPath() {
470
+ const dir2 = cabaneDir();
471
+ const key = createHash("sha256").update(dir2).digest("hex").slice(0, 16);
472
+ if (process.platform === "win32") {
473
+ return `\\\\.\\pipe\\cabane-companion-${key}`;
474
+ }
475
+ const besideRuntime = join5(dir2, "companion.sock");
476
+ if (Buffer.byteLength(besideRuntime) <= MAX_UNIX_SOCKET_PATH_BYTES) return besideRuntime;
477
+ const filename = `cabane-companion-${key}.sock`;
478
+ const xdgPath = process.env.XDG_RUNTIME_DIR && existsSync4(process.env.XDG_RUNTIME_DIR) ? join5(process.env.XDG_RUNTIME_DIR, filename) : null;
479
+ return xdgPath && Buffer.byteLength(xdgPath) <= MAX_UNIX_SOCKET_PATH_BYTES ? xdgPath : join5("/tmp", filename);
480
+ }
481
+ async function startControlServer(handlers) {
482
+ const path = controlSocketPath();
483
+ mkdirSync2(cabaneDir(), { recursive: true });
484
+ if (process.platform !== "win32" && existsSync4(path)) {
485
+ const alive = await ping(path);
486
+ if (alive) throw new Error(`another companion is already listening on ${path}`);
487
+ rmSync2(path, { force: true });
488
+ }
489
+ const server = createServer((socket) => {
490
+ void serveConnection(socket, handlers);
491
+ });
492
+ server.unref();
493
+ await new Promise((resolve, reject) => {
494
+ server.once("error", reject);
495
+ server.listen(path, () => {
496
+ server.removeListener("error", reject);
497
+ resolve();
498
+ });
499
+ });
500
+ if (process.platform !== "win32") {
501
+ try {
502
+ chmodSync2(path, 384);
503
+ } catch (err) {
504
+ await new Promise((resolve) => server.close(() => resolve()));
505
+ try {
506
+ rmSync2(path, { force: true });
507
+ } catch {
508
+ }
509
+ throw err;
510
+ }
511
+ }
512
+ server.on("error", () => {
513
+ });
514
+ return {
515
+ path,
516
+ close: () => new Promise((resolve) => {
517
+ server.close(() => {
518
+ if (process.platform !== "win32") rmSync2(path, { force: true });
519
+ resolve();
520
+ });
521
+ })
522
+ };
523
+ }
524
+ async function serveConnection(socket, handlers) {
525
+ socket.on("error", () => socket.destroy());
526
+ const line = await readLine(socket, CONTROL_TIMEOUT_MS * 5);
527
+ if (line === null) {
528
+ socket.destroy();
529
+ return;
530
+ }
531
+ let req;
532
+ try {
533
+ req = JSON.parse(line);
534
+ } catch {
535
+ reply(socket, { error: "malformed request" });
536
+ return;
537
+ }
538
+ try {
539
+ if (req.cmd === "status") {
540
+ reply(socket, handlers.status());
541
+ return;
542
+ }
543
+ if (req.cmd === "detect") {
544
+ const result = { detection: await handlers.detect() };
545
+ reply(socket, result);
546
+ return;
547
+ }
548
+ if (req.cmd === "allow") {
549
+ const result = { detection: await handlers.allow(req.runtime) };
550
+ reply(socket, result);
551
+ return;
552
+ }
553
+ if (req.cmd === "update") {
554
+ reply(socket, await handlers.update(req.auto));
555
+ return;
556
+ }
557
+ if (req.cmd === "stop") {
558
+ reply(socket, { ok: true });
559
+ setTimeout(() => handlers.stop(), 50).unref?.();
560
+ return;
561
+ }
562
+ reply(socket, { error: `unknown command "${String(req.cmd)}"` });
563
+ } catch (err) {
564
+ reply(socket, { error: err instanceof Error ? err.message : String(err) });
565
+ }
566
+ }
567
+ function reply(socket, body) {
568
+ try {
569
+ socket.end(`${JSON.stringify(body)}
570
+ `);
571
+ } catch {
572
+ socket.destroy();
573
+ }
574
+ }
575
+ async function controlRequest(path, req, timeoutMs = CONTROL_TIMEOUT_MS) {
576
+ const socket = connect(path);
577
+ try {
578
+ await new Promise((resolve, reject) => {
579
+ const timer = setTimeout(() => reject(new ControlTimeout()), timeoutMs);
580
+ timer.unref?.();
581
+ socket.once("connect", () => {
582
+ clearTimeout(timer);
583
+ resolve();
584
+ });
585
+ socket.once("error", (err) => {
586
+ clearTimeout(timer);
587
+ reject(err);
588
+ });
589
+ });
590
+ socket.write(`${JSON.stringify(req)}
591
+ `);
592
+ const line = await readLine(socket, timeoutMs);
593
+ if (line === null) throw new ControlTimeout();
594
+ return JSON.parse(line);
595
+ } finally {
596
+ socket.destroy();
597
+ }
598
+ }
599
+ var ControlTimeout = class extends Error {
600
+ constructor() {
601
+ super("the companion did not answer its control socket in time");
602
+ this.name = "ControlTimeout";
603
+ }
604
+ };
605
+ function isNotListening(err) {
606
+ const code = err?.code;
607
+ return code === "ENOENT" || code === "ECONNREFUSED";
608
+ }
609
+ function readLine(socket, timeoutMs) {
610
+ return new Promise((resolve) => {
611
+ let buf = "";
612
+ let settled = false;
613
+ const done = (v) => {
614
+ if (settled) return;
615
+ settled = true;
616
+ clearTimeout(timer);
617
+ socket.removeListener("data", onData);
618
+ resolve(v);
619
+ };
620
+ const timer = setTimeout(() => done(null), timeoutMs);
621
+ timer.unref?.();
622
+ const onData = (chunk) => {
623
+ buf += chunk.toString("utf8");
624
+ const nl = buf.indexOf("\n");
625
+ if (nl >= 0) done(buf.slice(0, nl));
626
+ else if (buf.length > 1e6) done(null);
627
+ };
628
+ socket.on("data", onData);
629
+ socket.once("close", () => done(null));
630
+ socket.once("error", () => done(null));
631
+ });
632
+ }
633
+ async function ping(path) {
634
+ try {
635
+ await controlRequest(path, { cmd: "status" });
636
+ return true;
637
+ } catch (err) {
638
+ return !isNotListening(err);
639
+ }
640
+ }
470
641
 
471
642
  // src/logger.ts
472
643
  import {
473
644
  createWriteStream,
474
- mkdirSync as mkdirSync2,
645
+ mkdirSync as mkdirSync3,
475
646
  openSync,
476
647
  renameSync as renameSync2,
477
648
  statSync
478
649
  } from "fs";
479
- import { dirname as dirname4, join as join5 } from "path";
650
+ import { dirname as dirname4, join as join6 } from "path";
480
651
  import { stripVTControlCharacters } from "util";
481
652
  import pino from "pino";
482
653
  function companionLogPath() {
483
- return join5(cabaneDir(), "companion.log");
654
+ return join6(cabaneDir(), "companion.log");
484
655
  }
485
656
  function humanText(value) {
486
657
  return stripVTControlCharacters(String(value ?? "")).replace(
@@ -588,7 +759,7 @@ function configureLogger(log, config) {
588
759
  }
589
760
  function createLogger(destinations = {}, options = {}) {
590
761
  const path = companionLogPath();
591
- if (!destinations.file) mkdirSync2(dirname4(path), { recursive: true });
762
+ if (!destinations.file) mkdirSync3(dirname4(path), { recursive: true });
592
763
  const file = destinations.file ?? new RotatingLog(path);
593
764
  const state = { format: options.logFormat ?? "human" };
594
765
  const streams = [];
@@ -634,671 +805,46 @@ function getLogger() {
634
805
  return cached;
635
806
  }
636
807
 
637
- // src/state.ts
638
- import { EventEmitter } from "events";
639
- function dispatchToWire(r) {
640
- return {
641
- id: r.id,
642
- timestamp: r.timestamp,
643
- workspace_slug: r.workspaceSlug,
644
- ...r.agentUsername ? { agent_username: r.agentUsername } : {},
645
- message: r.message,
646
- status: r.status,
647
- ...r.durationMs !== void 0 ? { duration_ms: r.durationMs } : {},
648
- ...r.error ? { error: r.error } : {},
649
- ...r.reply ? { reply: r.reply } : {}
650
- };
651
- }
652
- var MAX_DISPATCHES = 50;
653
- function today() {
654
- return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
655
- }
656
- var CompanionStateHub = class {
657
- constructor(opts) {
658
- this.opts = opts;
659
- this.emitter.setMaxListeners(0);
808
+ // src/bundled-binaries.ts
809
+ import { realpathSync as realpathSync3, statSync as statSync2 } from "fs";
810
+ import { createRequire } from "module";
811
+ import { dirname as dirname5, join as join7 } from "path";
812
+ import { fileURLToPath as fileURLToPath3 } from "url";
813
+ function nodeResolveSelf(specifier) {
814
+ const here = fileURLToPath3(import.meta.url);
815
+ if (typeof import.meta.resolve === "function") {
816
+ try {
817
+ return fileURLToPath3(import.meta.resolve(specifier));
818
+ } catch {
819
+ }
660
820
  }
661
- opts;
662
- emitter = new EventEmitter();
663
- workspaces = /* @__PURE__ */ new Map();
664
- dispatches = [];
665
- dashboardUrl = null;
666
- deviceId = null;
667
- deviceLabel = null;
668
- deviceError = null;
669
- // The latest harness snapshot the supervisor probed, or null before the
670
- // first probe (see CompanionStatusJson.harnesses).
671
- harnesses = null;
672
- // ---- subscription (SSE) ----
673
- on(listener) {
674
- this.emitter.on("event", listener);
675
- return () => this.emitter.off("event", listener);
821
+ try {
822
+ return createRequire(here).resolve(specifier);
823
+ } catch {
676
824
  }
677
- emit(type, data) {
678
- this.emitter.emit("event", { type, data });
825
+ return walkNodeModules(specifier, here);
826
+ }
827
+ function walkNodeModules(pkg2, fromPath) {
828
+ let dir2 = dirname5(fromPath);
829
+ for (; ; ) {
830
+ const manifest = join7(dir2, "node_modules", pkg2, "package.json");
831
+ if (nodeIsFile(manifest)) {
832
+ try {
833
+ return realpathSync3(manifest);
834
+ } catch {
835
+ return manifest;
836
+ }
837
+ }
838
+ const parent = dirname5(dir2);
839
+ if (parent === dir2) return null;
840
+ dir2 = parent;
679
841
  }
680
- // ---- device ----
681
- setDevice(info) {
682
- if (info.deviceId !== void 0) this.deviceId = info.deviceId;
683
- if (info.deviceLabel !== void 0) this.deviceLabel = info.deviceLabel;
684
- this.emitStatus();
685
- }
686
- setDeviceError(message) {
687
- if (this.deviceError === message) return;
688
- this.deviceError = message;
689
- this.emitStatus();
690
- }
691
- clearDeviceError() {
692
- if (this.deviceError === null) return;
693
- this.deviceError = null;
694
- this.emitStatus();
695
- }
696
- // ---- harnesses ----
697
- // Replace the harness snapshot and push a status change (so the SSE feed flips
698
- // the Companion's Harnesses surface live). The supervisor calls this on each
699
- // heartbeat-cadence probe and on an on-demand recheck.
700
- setHarnesses(harnesses) {
701
- this.harnesses = harnesses;
702
- this.emitStatus();
703
- }
704
- // ---- workspace lifecycle ----
705
- registerWorkspace(ws) {
706
- const existing = this.workspaces.get(ws.workspaceId);
707
- if (existing) {
708
- existing.slug = ws.slug;
709
- existing.name = ws.name;
710
- return;
711
- }
712
- this.workspaces.set(ws.workspaceId, {
713
- workspaceId: ws.workspaceId,
714
- slug: ws.slug,
715
- name: ws.name,
716
- connected: false,
717
- authFailed: false,
718
- lastEventAt: null,
719
- dispatchCountToday: 0,
720
- countDate: today(),
721
- agents: /* @__PURE__ */ new Map()
722
- });
723
- this.emitStatus();
724
- }
725
- removeWorkspace(workspaceId) {
726
- const ws = this.workspaces.get(workspaceId);
727
- if (!ws) return;
728
- this.workspaces.delete(workspaceId);
729
- this.emit("workspace:disconnected", { slug: ws.slug });
730
- this.emitStatus();
731
- }
732
- setConnected(workspaceId, connected) {
733
- const ws = this.workspaces.get(workspaceId);
734
- if (!ws || ws.connected === connected) return;
735
- ws.connected = connected;
736
- if (connected) ws.authFailed = false;
737
- this.emitStatus();
738
- }
739
- setAuthFailed(workspaceId) {
740
- const ws = this.workspaces.get(workspaceId);
741
- if (!ws) return;
742
- if (ws.authFailed && !ws.connected) return;
743
- ws.authFailed = true;
744
- ws.connected = false;
745
- this.emitStatus();
746
- }
747
- recordEvent(workspaceId) {
748
- const ws = this.workspaces.get(workspaceId);
749
- if (!ws) return;
750
- ws.lastEventAt = (/* @__PURE__ */ new Date()).toISOString();
751
- }
752
- // ---- agents ----
753
- // Upsert one assigned agent's observable state (called on each reconcile).
754
- setAgent(workspaceId, agent) {
755
- const ws = this.workspaces.get(workspaceId);
756
- if (!ws) return;
757
- ws.agents.set(agent.agentId, agent);
758
- this.emitStatus();
759
- }
760
- removeAgent(workspaceId, agentId) {
761
- const ws = this.workspaces.get(workspaceId);
762
- if (!ws) return;
763
- if (ws.agents.delete(agentId)) this.emitStatus();
764
- }
765
- // ---- dispatch feed ----
766
- // A DispatchObserver bound to one (workspace, agent), handed to that agent's
767
- // Dispatcher by the supervisor.
768
- observerFor(workspaceId, agentId, slug) {
769
- const username = () => this.workspaces.get(workspaceId)?.agents.get(agentId)?.username ?? "";
770
- return {
771
- onStart: (info) => this.dispatchStarted(workspaceId, slug, username(), info),
772
- onEnd: (info) => this.dispatchEnded(slug, username(), info)
773
- };
774
- }
775
- bumpCount(workspaceId) {
776
- const ws = this.workspaces.get(workspaceId);
777
- if (!ws) return;
778
- const day = today();
779
- if (ws.countDate !== day) {
780
- ws.countDate = day;
781
- ws.dispatchCountToday = 0;
782
- }
783
- ws.dispatchCountToday += 1;
784
- }
785
- dispatchStarted(workspaceId, slug, agentUsername, info) {
786
- this.bumpCount(workspaceId);
787
- const record = {
788
- id: info.id,
789
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
790
- workspaceSlug: slug,
791
- ...agentUsername ? { agentUsername } : {},
792
- message: info.message,
793
- status: "running"
794
- };
795
- this.upsertDispatch(record);
796
- this.emit("dispatch:started", dispatchToWire(record));
797
- this.emitStatus();
798
- }
799
- dispatchEnded(slug, agentUsername, info) {
800
- const existing = this.dispatches.find((d) => d.id === info.id);
801
- const record = {
802
- id: info.id,
803
- timestamp: existing?.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
804
- workspaceSlug: existing?.workspaceSlug ?? slug,
805
- ...existing?.agentUsername ?? agentUsername ? { agentUsername: existing?.agentUsername ?? agentUsername } : {},
806
- message: existing?.message ?? "",
807
- status: info.ok ? "replied" : "error",
808
- durationMs: info.durationMs,
809
- ...info.reason ? { error: info.reason } : {},
810
- ...info.reply ? { reply: info.reply } : {}
811
- };
812
- this.upsertDispatch(record);
813
- this.emit(info.ok ? "dispatch:completed" : "dispatch:error", dispatchToWire(record));
814
- }
815
- upsertDispatch(record) {
816
- const idx = this.dispatches.findIndex((d) => d.id === record.id);
817
- if (idx >= 0) {
818
- this.dispatches[idx] = record;
819
- return;
820
- }
821
- this.dispatches.unshift(record);
822
- if (this.dispatches.length > MAX_DISPATCHES) this.dispatches.length = MAX_DISPATCHES;
823
- }
824
- // ---- snapshots for the HTTP routes ----
825
- setDashboardUrl(url) {
826
- this.dashboardUrl = url;
827
- }
828
- recentDispatches(limit = 20) {
829
- return this.dispatches.slice(0, Math.max(0, limit));
830
- }
831
- statusJson() {
832
- const list = [...this.workspaces.values()].sort((a, b) => a.name.localeCompare(b.name));
833
- const lastOverall = list.reduce((acc, ws) => {
834
- if (!ws.lastEventAt) return acc;
835
- if (!acc || ws.lastEventAt > acc) return ws.lastEventAt;
836
- return acc;
837
- }, null);
838
- return {
839
- cabane_url: this.opts.baseUrl,
840
- device_id: this.deviceId,
841
- device_label: this.deviceLabel,
842
- device_error: this.deviceError,
843
- connected: list.some((ws) => ws.connected),
844
- workspaces: list.map((ws) => ({
845
- slug: ws.slug,
846
- name: ws.name,
847
- last_event_at: ws.lastEventAt,
848
- dispatch_count_today: ws.dispatchCountToday,
849
- connected: ws.connected,
850
- auth_failed: ws.authFailed,
851
- agents: [...ws.agents.values()].sort((a, b) => a.username.localeCompare(b.username)).map((a) => ({
852
- agent_id: a.agentId,
853
- username: a.username,
854
- display_name: a.displayName,
855
- has_credential: a.hasCredential,
856
- missing_secrets: a.missingSecrets
857
- }))
858
- })),
859
- last_event_at_overall: lastOverall,
860
- dashboard_url: this.dashboardUrl,
861
- companion_version: this.opts.companionVersion,
862
- instance_id: this.opts.instanceId ?? null,
863
- harnesses: this.harnesses
864
- };
865
- }
866
- emitStatus() {
867
- this.emit("status:changed", this.statusJson());
868
- }
869
- };
870
-
871
- // src/dashboard/routes.ts
872
- var CONTENT_TYPES = {
873
- ".html": "text/html; charset=utf-8",
874
- ".css": "text/css; charset=utf-8",
875
- ".js": "text/javascript; charset=utf-8",
876
- ".svg": "image/svg+xml",
877
- ".ico": "image/x-icon"
878
- };
879
- function registerRoutes(app, deps) {
880
- const { supervisor, hub, staticDir } = deps;
881
- app.get("/", async (c) => {
882
- const html = await readFile(join6(staticDir, "index.html"), "utf8");
883
- return c.html(html);
884
- });
885
- app.get("/static/:file", async (c) => {
886
- const file = c.req.param("file");
887
- const safe = normalize(file).replace(/^(\.\.[/\\])+/, "");
888
- const full = join6(staticDir, safe);
889
- if (!full.startsWith(staticDir) || !existsSync4(full)) return c.notFound();
890
- const body = await readFile(full);
891
- const type = CONTENT_TYPES[extname(full).toLowerCase()] ?? "application/octet-stream";
892
- c.header("content-type", type);
893
- return c.body(body);
894
- });
895
- app.get("/api/status", (c) => c.json(hub.statusJson()));
896
- app.get("/api/dispatches", (c) => {
897
- const limit = clampLimit(c.req.query("limit"), 20);
898
- return c.json({ dispatches: hub.recentDispatches(limit).map(dispatchToWire) });
899
- });
900
- app.get("/api/logs", async (c) => {
901
- const lines2 = clampLimit(c.req.query("lines"), 200, 1e3);
902
- return c.json({ lines: tailFile(companionLogPath(), lines2) });
903
- });
904
- app.post("/api/settings", async (c) => {
905
- const body = await readJson(c);
906
- const patch = {};
907
- if (body.logLevel === "warn" || body.logLevel === "info" || body.logLevel === "debug") {
908
- patch.logLevel = body.logLevel;
909
- }
910
- if (typeof body.autoOpen === "boolean") patch.autoOpen = body.autoOpen;
911
- supervisor.applySettings(patch);
912
- return c.json({ ok: true, status: hub.statusJson() });
913
- });
914
- app.post("/api/control", async (c) => {
915
- const body = await readJson(c);
916
- const action = body.action;
917
- if (action === "stop") {
918
- setTimeout(() => void supervisor.requestStop(), 50);
919
- return c.json({ ok: true, action: "stop" });
920
- }
921
- if (action === "restart") {
922
- setTimeout(() => supervisor.requestRestart(), 50);
923
- return c.json({ ok: true, action: "restart" });
924
- }
925
- if (action === "reload-config") {
926
- supervisor.reloadConfig();
927
- return c.json({ ok: true, action: "reload-config" });
928
- }
929
- if (action === "refresh") {
930
- supervisor.refresh();
931
- return c.json({ ok: true, action: "refresh" });
932
- }
933
- return c.json({ error: `unknown action "${String(action)}"` }, 400);
934
- });
935
- app.post("/api/harnesses/recheck", async (c) => {
936
- await supervisor.redetect();
937
- return c.json({ ok: true, status: hub.statusJson() });
938
- });
939
- app.get(
940
- "/api/events",
941
- (c) => streamSSE(c, async (stream) => {
942
- await stream.writeSSE({ event: "status:changed", data: JSON.stringify(hub.statusJson()) });
943
- const queue = [];
944
- let wake = null;
945
- const unsubscribe = hub.on((ev) => {
946
- queue.push(ev);
947
- wake?.();
948
- });
949
- stream.onAbort(() => {
950
- unsubscribe();
951
- wake?.();
952
- });
953
- while (!stream.aborted) {
954
- if (queue.length === 0) {
955
- await Promise.race([
956
- new Promise((resolve) => {
957
- wake = resolve;
958
- }),
959
- stream.sleep(25e3)
960
- ]);
961
- wake = null;
962
- }
963
- if (stream.aborted) break;
964
- if (queue.length === 0) {
965
- await stream.writeSSE({ event: "ping", data: "" });
966
- continue;
967
- }
968
- const ev = queue.shift();
969
- await stream.writeSSE({ event: ev.type, data: JSON.stringify(ev.data) });
970
- }
971
- unsubscribe();
972
- })
973
- );
974
- }
975
- async function readJson(c) {
976
- try {
977
- const parsed = await c.req.json();
978
- return parsed && typeof parsed === "object" ? parsed : {};
979
- } catch {
980
- return {};
981
- }
982
- }
983
- function clampLimit(raw, fallback, max = 200) {
984
- if (!raw) return fallback;
985
- const n = Number(raw);
986
- if (!Number.isFinite(n) || n <= 0) return fallback;
987
- return Math.min(Math.floor(n), max);
988
- }
989
- function tailFile(path, lines2) {
990
- if (!existsSync4(path)) return [];
991
- const MAX_BYTES = 256 * 1024;
992
- let fd;
993
- try {
994
- fd = openSync2(path, "r");
995
- const size = fstatSync(fd).size;
996
- const start = Math.max(0, size - MAX_BYTES);
997
- const len = size - start;
998
- if (len <= 0) return [];
999
- const buf = Buffer.alloc(len);
1000
- readSync(fd, buf, 0, len, start);
1001
- const text = buf.toString("utf8");
1002
- const all = text.split("\n");
1003
- if (start > 0 && all.length > 0) all.shift();
1004
- return all.filter((l) => l.length > 0).slice(-lines2);
1005
- } catch {
1006
- return [];
1007
- } finally {
1008
- if (fd !== void 0) closeSync(fd);
1009
- }
1010
- }
1011
-
1012
- // src/dashboard/server.ts
1013
- var DEFAULT_PORT = 7474;
1014
- var PORT_FALLBACK_SPAN = 10;
1015
- function buildDashboardApp(deps) {
1016
- const app = new Hono();
1017
- app.onError((err, c) => {
1018
- if (err instanceof ApiError) {
1019
- const status = err.status >= 400 && err.status < 600 ? err.status : 502;
1020
- return c.json({ error: err.message }, status);
1021
- }
1022
- if (err instanceof CompanionError) {
1023
- return c.json({ error: err.message }, 400);
1024
- }
1025
- return c.json({ error: err instanceof Error ? err.message : "internal error" }, 500);
1026
- });
1027
- registerRoutes(app, { ...deps, staticDir: resolveStaticDir() });
1028
- return app;
1029
- }
1030
- async function startDashboard(opts) {
1031
- const app = buildDashboardApp({ supervisor: opts.supervisor, hub: opts.hub });
1032
- const preferred = opts.port ?? DEFAULT_PORT;
1033
- let lastErr;
1034
- for (let port = preferred; port < preferred + PORT_FALLBACK_SPAN; port++) {
1035
- try {
1036
- const server = await listen(app, port);
1037
- const url = `http://127.0.0.1:${port}`;
1038
- return {
1039
- url,
1040
- port,
1041
- close: () => new Promise((resolve) => {
1042
- server.close(() => resolve());
1043
- server.closeAllConnections?.();
1044
- })
1045
- };
1046
- } catch (err) {
1047
- if (isAddrInUse(err)) {
1048
- lastErr = err;
1049
- continue;
1050
- }
1051
- throw err;
1052
- }
1053
- }
1054
- throw new CompanionError(
1055
- `couldn't bind the dashboard to any port in ${preferred}\u2013${preferred + PORT_FALLBACK_SPAN - 1} (all in use). Free one up or pass --port. (last error: ${lastErr instanceof Error ? lastErr.message : String(lastErr)})`
1056
- );
1057
- }
1058
- function listen(app, port) {
1059
- return new Promise((resolve, reject) => {
1060
- let settled = false;
1061
- const server = serve({ fetch: app.fetch, hostname: "127.0.0.1", port }, () => {
1062
- if (!settled) {
1063
- settled = true;
1064
- resolve(server);
1065
- }
1066
- });
1067
- server.on("error", (err) => {
1068
- if (!settled) {
1069
- settled = true;
1070
- reject(err);
1071
- }
1072
- });
1073
- });
1074
- }
1075
- function isAddrInUse(err) {
1076
- return Boolean(err && typeof err === "object" && "code" in err && err.code === "EADDRINUSE");
1077
- }
1078
- function resolveStaticDir() {
1079
- return join7(dirname5(fileURLToPath3(import.meta.url)), "static");
1080
- }
1081
-
1082
- // src/control-socket.ts
1083
- import { createHash } from "crypto";
1084
- import { chmodSync as chmodSync2, existsSync as existsSync5, rmSync as rmSync2, mkdirSync as mkdirSync3 } from "fs";
1085
- import { createServer, connect } from "net";
1086
- import { join as join8 } from "path";
1087
- var CONTROL_TIMEOUT_MS = 1e3;
1088
- var MAX_UNIX_SOCKET_PATH_BYTES = 103;
1089
- function controlSocketPath() {
1090
- const dir2 = cabaneDir();
1091
- const key = createHash("sha256").update(dir2).digest("hex").slice(0, 16);
1092
- if (process.platform === "win32") {
1093
- return `\\\\.\\pipe\\cabane-companion-${key}`;
1094
- }
1095
- const besideRuntime = join8(dir2, "companion.sock");
1096
- if (Buffer.byteLength(besideRuntime) <= MAX_UNIX_SOCKET_PATH_BYTES) return besideRuntime;
1097
- const filename = `cabane-companion-${key}.sock`;
1098
- const xdgPath = process.env.XDG_RUNTIME_DIR && existsSync5(process.env.XDG_RUNTIME_DIR) ? join8(process.env.XDG_RUNTIME_DIR, filename) : null;
1099
- return xdgPath && Buffer.byteLength(xdgPath) <= MAX_UNIX_SOCKET_PATH_BYTES ? xdgPath : join8("/tmp", filename);
1100
- }
1101
- async function startControlServer(handlers) {
1102
- const path = controlSocketPath();
1103
- mkdirSync3(cabaneDir(), { recursive: true });
1104
- if (process.platform !== "win32" && existsSync5(path)) {
1105
- const alive = await ping(path);
1106
- if (alive) throw new Error(`another companion is already listening on ${path}`);
1107
- rmSync2(path, { force: true });
1108
- }
1109
- const server = createServer((socket) => {
1110
- void serveConnection(socket, handlers);
1111
- });
1112
- server.unref();
1113
- await new Promise((resolve, reject) => {
1114
- server.once("error", reject);
1115
- server.listen(path, () => {
1116
- server.removeListener("error", reject);
1117
- resolve();
1118
- });
1119
- });
1120
- if (process.platform !== "win32") {
1121
- try {
1122
- chmodSync2(path, 384);
1123
- } catch (err) {
1124
- await new Promise((resolve) => server.close(() => resolve()));
1125
- try {
1126
- rmSync2(path, { force: true });
1127
- } catch {
1128
- }
1129
- throw err;
1130
- }
1131
- }
1132
- server.on("error", () => {
1133
- });
1134
- return {
1135
- path,
1136
- close: () => new Promise((resolve) => {
1137
- server.close(() => {
1138
- if (process.platform !== "win32") rmSync2(path, { force: true });
1139
- resolve();
1140
- });
1141
- })
1142
- };
1143
- }
1144
- async function serveConnection(socket, handlers) {
1145
- socket.on("error", () => socket.destroy());
1146
- const line = await readLine(socket, CONTROL_TIMEOUT_MS * 5);
1147
- if (line === null) {
1148
- socket.destroy();
1149
- return;
1150
- }
1151
- let req;
1152
- try {
1153
- req = JSON.parse(line);
1154
- } catch {
1155
- reply(socket, { error: "malformed request" });
1156
- return;
1157
- }
1158
- try {
1159
- if (req.cmd === "status") {
1160
- reply(socket, handlers.status());
1161
- return;
1162
- }
1163
- if (req.cmd === "detect") {
1164
- const result = { detection: await handlers.detect() };
1165
- reply(socket, result);
1166
- return;
1167
- }
1168
- if (req.cmd === "allow") {
1169
- const result = { detection: await handlers.allow(req.runtime) };
1170
- reply(socket, result);
1171
- return;
1172
- }
1173
- if (req.cmd === "update") {
1174
- reply(socket, await handlers.update(req.auto));
1175
- return;
1176
- }
1177
- if (req.cmd === "stop") {
1178
- reply(socket, { ok: true });
1179
- setTimeout(() => handlers.stop(), 50).unref?.();
1180
- return;
1181
- }
1182
- reply(socket, { error: `unknown command "${String(req.cmd)}"` });
1183
- } catch (err) {
1184
- reply(socket, { error: err instanceof Error ? err.message : String(err) });
1185
- }
1186
- }
1187
- function reply(socket, body) {
1188
- try {
1189
- socket.end(`${JSON.stringify(body)}
1190
- `);
1191
- } catch {
1192
- socket.destroy();
1193
- }
1194
- }
1195
- async function controlRequest(path, req, timeoutMs = CONTROL_TIMEOUT_MS) {
1196
- const socket = connect(path);
1197
- try {
1198
- await new Promise((resolve, reject) => {
1199
- const timer = setTimeout(() => reject(new ControlTimeout()), timeoutMs);
1200
- timer.unref?.();
1201
- socket.once("connect", () => {
1202
- clearTimeout(timer);
1203
- resolve();
1204
- });
1205
- socket.once("error", (err) => {
1206
- clearTimeout(timer);
1207
- reject(err);
1208
- });
1209
- });
1210
- socket.write(`${JSON.stringify(req)}
1211
- `);
1212
- const line = await readLine(socket, timeoutMs);
1213
- if (line === null) throw new ControlTimeout();
1214
- return JSON.parse(line);
1215
- } finally {
1216
- socket.destroy();
1217
- }
1218
- }
1219
- var ControlTimeout = class extends Error {
1220
- constructor() {
1221
- super("the companion did not answer its control socket in time");
1222
- this.name = "ControlTimeout";
1223
- }
1224
- };
1225
- function isNotListening(err) {
1226
- const code = err?.code;
1227
- return code === "ENOENT" || code === "ECONNREFUSED";
1228
- }
1229
- function readLine(socket, timeoutMs) {
1230
- return new Promise((resolve) => {
1231
- let buf = "";
1232
- let settled = false;
1233
- const done = (v) => {
1234
- if (settled) return;
1235
- settled = true;
1236
- clearTimeout(timer);
1237
- socket.removeListener("data", onData);
1238
- resolve(v);
1239
- };
1240
- const timer = setTimeout(() => done(null), timeoutMs);
1241
- timer.unref?.();
1242
- const onData = (chunk) => {
1243
- buf += chunk.toString("utf8");
1244
- const nl = buf.indexOf("\n");
1245
- if (nl >= 0) done(buf.slice(0, nl));
1246
- else if (buf.length > 1e6) done(null);
1247
- };
1248
- socket.on("data", onData);
1249
- socket.once("close", () => done(null));
1250
- socket.once("error", () => done(null));
1251
- });
1252
- }
1253
- async function ping(path) {
1254
- try {
1255
- await controlRequest(path, { cmd: "status" });
1256
- return true;
1257
- } catch (err) {
1258
- return !isNotListening(err);
1259
- }
1260
- }
1261
-
1262
- // src/bundled-binaries.ts
1263
- import { realpathSync as realpathSync3, statSync as statSync2 } from "fs";
1264
- import { createRequire } from "module";
1265
- import { dirname as dirname6, join as join9 } from "path";
1266
- import { fileURLToPath as fileURLToPath4 } from "url";
1267
- function nodeResolveSelf(specifier) {
1268
- const here = fileURLToPath4(import.meta.url);
1269
- if (typeof import.meta.resolve === "function") {
1270
- try {
1271
- return fileURLToPath4(import.meta.resolve(specifier));
1272
- } catch {
1273
- }
1274
- }
1275
- try {
1276
- return createRequire(here).resolve(specifier);
1277
- } catch {
1278
- }
1279
- return walkNodeModules(specifier, here);
1280
- }
1281
- function walkNodeModules(pkg2, fromPath) {
1282
- let dir2 = dirname6(fromPath);
1283
- for (; ; ) {
1284
- const manifest = join9(dir2, "node_modules", pkg2, "package.json");
1285
- if (nodeIsFile(manifest)) {
1286
- try {
1287
- return realpathSync3(manifest);
1288
- } catch {
1289
- return manifest;
1290
- }
1291
- }
1292
- const parent = dirname6(dir2);
1293
- if (parent === dir2) return null;
1294
- dir2 = parent;
1295
- }
1296
- }
1297
- function nodeResolve(specifier, fromPath) {
1298
- try {
1299
- return createRequire(fromPath).resolve(specifier);
1300
- } catch {
1301
- return null;
842
+ }
843
+ function nodeResolve(specifier, fromPath) {
844
+ try {
845
+ return createRequire(fromPath).resolve(specifier);
846
+ } catch {
847
+ return null;
1302
848
  }
1303
849
  }
1304
850
  function nodeIsFile(path) {
@@ -1351,7 +897,7 @@ function resolveClaude(env) {
1351
897
  const manifest = env.resolve(`${pkg2}/package.json`, sdkEntry);
1352
898
  if (!manifest) continue;
1353
899
  sawPackage ??= pkg2;
1354
- const exe = join9(dirname6(manifest), env.platform === "win32" ? "claude.exe" : "claude");
900
+ const exe = join7(dirname5(manifest), env.platform === "win32" ? "claude.exe" : "claude");
1355
901
  if (env.isFile(exe)) {
1356
902
  return { ...base, packageName: pkg2, status: "present", path: exe };
1357
903
  }
@@ -1402,13 +948,13 @@ function resolveCodex(env) {
1402
948
  if (!cliManifest) return { ...base, status: "platform_package_missing", path: null };
1403
949
  const platformManifest = env.resolve(`${pkg2}/package.json`, cliManifest);
1404
950
  if (!platformManifest) return { ...base, status: "platform_package_missing", path: null };
1405
- const packageRoot = join9(dirname6(platformManifest), "vendor", triple);
951
+ const packageRoot = join7(dirname5(platformManifest), "vendor", triple);
1406
952
  const exeName = env.platform === "win32" ? "codex.exe" : "codex";
1407
- const current = join9(packageRoot, "bin", exeName);
1408
- if (env.isFile(current) && env.isFile(join9(packageRoot, "codex-package.json"))) {
953
+ const current = join7(packageRoot, "bin", exeName);
954
+ if (env.isFile(current) && env.isFile(join7(packageRoot, "codex-package.json"))) {
1409
955
  return { ...base, status: "present", path: current };
1410
956
  }
1411
- const legacy = join9(packageRoot, "codex", exeName);
957
+ const legacy = join7(packageRoot, "codex", exeName);
1412
958
  if (env.isFile(legacy)) return { ...base, status: "present", path: legacy };
1413
959
  return { ...base, status: "executable_missing", path: null };
1414
960
  }
@@ -1473,18 +1019,18 @@ function warnAboutHarnessReadiness(cfg, deps = {}) {
1473
1019
 
1474
1020
  // src/runtime-file.ts
1475
1021
  import {
1476
- existsSync as existsSync6,
1022
+ existsSync as existsSync5,
1477
1023
  readFileSync as readFileSync3,
1478
1024
  rmSync as rmSync3,
1479
1025
  writeFileSync as writeFileSync2,
1480
1026
  mkdirSync as mkdirSync4,
1481
- openSync as openSync3,
1482
- closeSync as closeSync2
1027
+ openSync as openSync2,
1028
+ closeSync
1483
1029
  } from "fs";
1484
- import { join as join10 } from "path";
1030
+ import { join as join8 } from "path";
1485
1031
  var PROBE_TIMEOUT_MS = 1e3;
1486
1032
  function runtimePath() {
1487
- return join10(cabaneDir(), "runtime.json");
1033
+ return join8(cabaneDir(), "runtime.json");
1488
1034
  }
1489
1035
  function serialize(state) {
1490
1036
  return JSON.stringify(state, null, 2) + "\n";
@@ -1500,24 +1046,24 @@ function acquireRuntimeState(state) {
1500
1046
  mkdirSync4(cabaneDir(), { recursive: true });
1501
1047
  let fd;
1502
1048
  try {
1503
- fd = openSync3(runtimePath(), "wx");
1049
+ fd = openSync2(runtimePath(), "wx");
1504
1050
  } catch {
1505
1051
  return { acquired: false, existing: readLiveRuntimeState() };
1506
1052
  }
1507
1053
  try {
1508
1054
  writeFileSync2(fd, serialize(state), "utf8");
1509
1055
  } finally {
1510
- closeSync2(fd);
1056
+ closeSync(fd);
1511
1057
  }
1512
1058
  return { acquired: true };
1513
1059
  }
1514
1060
  function clearRuntimeState() {
1515
1061
  const path = runtimePath();
1516
- if (existsSync6(path)) rmSync3(path, { force: true });
1062
+ if (existsSync5(path)) rmSync3(path, { force: true });
1517
1063
  }
1518
1064
  function clearRuntimeStateIfOurs(instanceId) {
1519
1065
  const path = runtimePath();
1520
- if (!existsSync6(path)) return;
1066
+ if (!existsSync5(path)) return;
1521
1067
  try {
1522
1068
  const parsed = JSON.parse(readFileSync3(path, "utf8"));
1523
1069
  if (parsed.instanceId && parsed.instanceId !== instanceId) return;
@@ -1529,7 +1075,7 @@ function readLiveRuntimeState({
1529
1075
  sweep = true
1530
1076
  } = {}) {
1531
1077
  const path = runtimePath();
1532
- if (!existsSync6(path)) return null;
1078
+ if (!existsSync5(path)) return null;
1533
1079
  let parsed;
1534
1080
  try {
1535
1081
  parsed = JSON.parse(readFileSync3(path, "utf8"));
@@ -1543,23 +1089,257 @@ function readLiveRuntimeState({
1543
1089
  if (sweep) clearRuntimeState();
1544
1090
  return null;
1545
1091
  }
1546
- return parsed;
1547
- }
1548
- async function verifyRuntime(state, requestImpl = controlRequest) {
1549
- if (!state.instanceId || !state.socket) return "unknown";
1550
- let body;
1551
- try {
1552
- body = await requestImpl(
1553
- state.socket,
1554
- { cmd: "status" },
1555
- PROBE_TIMEOUT_MS
1556
- );
1557
- } catch (err) {
1558
- return isNotListening(err) ? "stale" : "unknown";
1092
+ return parsed;
1093
+ }
1094
+ async function verifyRuntime(state, requestImpl = controlRequest) {
1095
+ if (!state.instanceId || !state.socket) return "unknown";
1096
+ let body;
1097
+ try {
1098
+ body = await requestImpl(
1099
+ state.socket,
1100
+ { cmd: "status" },
1101
+ PROBE_TIMEOUT_MS
1102
+ );
1103
+ } catch (err) {
1104
+ return isNotListening(err) ? "stale" : "unknown";
1105
+ }
1106
+ if (typeof body?.instance_id !== "string") return "unknown";
1107
+ return body.instance_id === state.instanceId ? "ours" : "stale";
1108
+ }
1109
+
1110
+ // src/state.ts
1111
+ import { EventEmitter } from "events";
1112
+ function dispatchToWire(r) {
1113
+ return {
1114
+ id: r.id,
1115
+ timestamp: r.timestamp,
1116
+ workspace_slug: r.workspaceSlug,
1117
+ ...r.agentUsername ? { agent_username: r.agentUsername } : {},
1118
+ message: r.message,
1119
+ status: r.status,
1120
+ ...r.durationMs !== void 0 ? { duration_ms: r.durationMs } : {},
1121
+ ...r.error ? { error: r.error } : {},
1122
+ ...r.reply ? { reply: r.reply } : {}
1123
+ };
1124
+ }
1125
+ var MAX_DISPATCHES = 50;
1126
+ function today() {
1127
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1128
+ }
1129
+ var CompanionStateHub = class {
1130
+ constructor(opts) {
1131
+ this.opts = opts;
1132
+ this.emitter.setMaxListeners(0);
1133
+ }
1134
+ opts;
1135
+ emitter = new EventEmitter();
1136
+ workspaces = /* @__PURE__ */ new Map();
1137
+ dispatches = [];
1138
+ dashboardUrl = null;
1139
+ deviceId = null;
1140
+ deviceLabel = null;
1141
+ deviceError = null;
1142
+ // The latest harness snapshot the supervisor probed, or null before the
1143
+ // first probe (see CompanionStatusJson.harnesses).
1144
+ harnesses = null;
1145
+ // ---- subscription (SSE) ----
1146
+ on(listener) {
1147
+ this.emitter.on("event", listener);
1148
+ return () => this.emitter.off("event", listener);
1149
+ }
1150
+ emit(type, data) {
1151
+ this.emitter.emit("event", { type, data });
1152
+ }
1153
+ // ---- device ----
1154
+ setDevice(info) {
1155
+ if (info.deviceId !== void 0) this.deviceId = info.deviceId;
1156
+ if (info.deviceLabel !== void 0) this.deviceLabel = info.deviceLabel;
1157
+ this.emitStatus();
1158
+ }
1159
+ setDeviceError(message) {
1160
+ if (this.deviceError === message) return;
1161
+ this.deviceError = message;
1162
+ this.emitStatus();
1163
+ }
1164
+ clearDeviceError() {
1165
+ if (this.deviceError === null) return;
1166
+ this.deviceError = null;
1167
+ this.emitStatus();
1168
+ }
1169
+ // ---- harnesses ----
1170
+ // Replace the harness snapshot and push a status change (so the SSE feed flips
1171
+ // the Companion's Harnesses surface live). The supervisor calls this on each
1172
+ // heartbeat-cadence probe and on an on-demand recheck.
1173
+ setHarnesses(harnesses) {
1174
+ this.harnesses = harnesses;
1175
+ this.emitStatus();
1176
+ }
1177
+ // ---- workspace lifecycle ----
1178
+ registerWorkspace(ws) {
1179
+ const existing = this.workspaces.get(ws.workspaceId);
1180
+ if (existing) {
1181
+ existing.slug = ws.slug;
1182
+ existing.name = ws.name;
1183
+ return;
1184
+ }
1185
+ this.workspaces.set(ws.workspaceId, {
1186
+ workspaceId: ws.workspaceId,
1187
+ slug: ws.slug,
1188
+ name: ws.name,
1189
+ connected: false,
1190
+ authFailed: false,
1191
+ lastEventAt: null,
1192
+ dispatchCountToday: 0,
1193
+ countDate: today(),
1194
+ agents: /* @__PURE__ */ new Map()
1195
+ });
1196
+ this.emitStatus();
1197
+ }
1198
+ removeWorkspace(workspaceId) {
1199
+ const ws = this.workspaces.get(workspaceId);
1200
+ if (!ws) return;
1201
+ this.workspaces.delete(workspaceId);
1202
+ this.emit("workspace:disconnected", { slug: ws.slug });
1203
+ this.emitStatus();
1204
+ }
1205
+ setConnected(workspaceId, connected) {
1206
+ const ws = this.workspaces.get(workspaceId);
1207
+ if (!ws || ws.connected === connected) return;
1208
+ ws.connected = connected;
1209
+ if (connected) ws.authFailed = false;
1210
+ this.emitStatus();
1211
+ }
1212
+ setAuthFailed(workspaceId) {
1213
+ const ws = this.workspaces.get(workspaceId);
1214
+ if (!ws) return;
1215
+ if (ws.authFailed && !ws.connected) return;
1216
+ ws.authFailed = true;
1217
+ ws.connected = false;
1218
+ this.emitStatus();
1219
+ }
1220
+ recordEvent(workspaceId) {
1221
+ const ws = this.workspaces.get(workspaceId);
1222
+ if (!ws) return;
1223
+ ws.lastEventAt = (/* @__PURE__ */ new Date()).toISOString();
1224
+ }
1225
+ // ---- agents ----
1226
+ // Upsert one assigned agent's observable state (called on each reconcile).
1227
+ setAgent(workspaceId, agent) {
1228
+ const ws = this.workspaces.get(workspaceId);
1229
+ if (!ws) return;
1230
+ ws.agents.set(agent.agentId, agent);
1231
+ this.emitStatus();
1232
+ }
1233
+ removeAgent(workspaceId, agentId) {
1234
+ const ws = this.workspaces.get(workspaceId);
1235
+ if (!ws) return;
1236
+ if (ws.agents.delete(agentId)) this.emitStatus();
1237
+ }
1238
+ // ---- dispatch feed ----
1239
+ // A DispatchObserver bound to one (workspace, agent), handed to that agent's
1240
+ // Dispatcher by the supervisor.
1241
+ observerFor(workspaceId, agentId, slug) {
1242
+ const username = () => this.workspaces.get(workspaceId)?.agents.get(agentId)?.username ?? "";
1243
+ return {
1244
+ onStart: (info) => this.dispatchStarted(workspaceId, slug, username(), info),
1245
+ onEnd: (info) => this.dispatchEnded(slug, username(), info)
1246
+ };
1247
+ }
1248
+ bumpCount(workspaceId) {
1249
+ const ws = this.workspaces.get(workspaceId);
1250
+ if (!ws) return;
1251
+ const day = today();
1252
+ if (ws.countDate !== day) {
1253
+ ws.countDate = day;
1254
+ ws.dispatchCountToday = 0;
1255
+ }
1256
+ ws.dispatchCountToday += 1;
1257
+ }
1258
+ dispatchStarted(workspaceId, slug, agentUsername, info) {
1259
+ this.bumpCount(workspaceId);
1260
+ const record = {
1261
+ id: info.id,
1262
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1263
+ workspaceSlug: slug,
1264
+ ...agentUsername ? { agentUsername } : {},
1265
+ message: info.message,
1266
+ status: "running"
1267
+ };
1268
+ this.upsertDispatch(record);
1269
+ this.emit("dispatch:started", dispatchToWire(record));
1270
+ this.emitStatus();
1271
+ }
1272
+ dispatchEnded(slug, agentUsername, info) {
1273
+ const existing = this.dispatches.find((d) => d.id === info.id);
1274
+ const record = {
1275
+ id: info.id,
1276
+ timestamp: existing?.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
1277
+ workspaceSlug: existing?.workspaceSlug ?? slug,
1278
+ ...existing?.agentUsername ?? agentUsername ? { agentUsername: existing?.agentUsername ?? agentUsername } : {},
1279
+ message: existing?.message ?? "",
1280
+ status: info.ok ? "replied" : "error",
1281
+ durationMs: info.durationMs,
1282
+ ...info.reason ? { error: info.reason } : {},
1283
+ ...info.reply ? { reply: info.reply } : {}
1284
+ };
1285
+ this.upsertDispatch(record);
1286
+ this.emit(info.ok ? "dispatch:completed" : "dispatch:error", dispatchToWire(record));
1287
+ }
1288
+ upsertDispatch(record) {
1289
+ const idx = this.dispatches.findIndex((d) => d.id === record.id);
1290
+ if (idx >= 0) {
1291
+ this.dispatches[idx] = record;
1292
+ return;
1293
+ }
1294
+ this.dispatches.unshift(record);
1295
+ if (this.dispatches.length > MAX_DISPATCHES) this.dispatches.length = MAX_DISPATCHES;
1296
+ }
1297
+ // ---- snapshots for the HTTP routes ----
1298
+ setDashboardUrl(url) {
1299
+ this.dashboardUrl = url;
1300
+ }
1301
+ recentDispatches(limit = 20) {
1302
+ return this.dispatches.slice(0, Math.max(0, limit));
1303
+ }
1304
+ statusJson() {
1305
+ const list = [...this.workspaces.values()].sort((a, b) => a.name.localeCompare(b.name));
1306
+ const lastOverall = list.reduce((acc, ws) => {
1307
+ if (!ws.lastEventAt) return acc;
1308
+ if (!acc || ws.lastEventAt > acc) return ws.lastEventAt;
1309
+ return acc;
1310
+ }, null);
1311
+ return {
1312
+ cabane_url: this.opts.baseUrl,
1313
+ device_id: this.deviceId,
1314
+ device_label: this.deviceLabel,
1315
+ device_error: this.deviceError,
1316
+ connected: list.some((ws) => ws.connected),
1317
+ workspaces: list.map((ws) => ({
1318
+ slug: ws.slug,
1319
+ name: ws.name,
1320
+ last_event_at: ws.lastEventAt,
1321
+ dispatch_count_today: ws.dispatchCountToday,
1322
+ connected: ws.connected,
1323
+ auth_failed: ws.authFailed,
1324
+ agents: [...ws.agents.values()].sort((a, b) => a.username.localeCompare(b.username)).map((a) => ({
1325
+ agent_id: a.agentId,
1326
+ username: a.username,
1327
+ display_name: a.displayName,
1328
+ has_credential: a.hasCredential,
1329
+ missing_secrets: a.missingSecrets
1330
+ }))
1331
+ })),
1332
+ last_event_at_overall: lastOverall,
1333
+ dashboard_url: this.dashboardUrl,
1334
+ companion_version: this.opts.companionVersion,
1335
+ instance_id: this.opts.instanceId ?? null,
1336
+ harnesses: this.harnesses
1337
+ };
1338
+ }
1339
+ emitStatus() {
1340
+ this.emit("status:changed", this.statusJson());
1559
1341
  }
1560
- if (typeof body?.instance_id !== "string") return "unknown";
1561
- return body.instance_id === state.instanceId ? "ours" : "stale";
1562
- }
1342
+ };
1563
1343
 
1564
1344
  // src/heartbeat-status.ts
1565
1345
  import { z as z3 } from "zod";
@@ -2516,27 +2296,27 @@ function requestSignal(signal, timeoutMs = 5e3) {
2516
2296
  // src/credentials.ts
2517
2297
  import {
2518
2298
  chmodSync as chmodSync3,
2519
- existsSync as existsSync7,
2299
+ existsSync as existsSync6,
2520
2300
  mkdirSync as mkdirSync5,
2521
2301
  readFileSync as readFileSync4,
2522
2302
  renameSync as renameSync3,
2523
2303
  rmSync as rmSync4,
2524
2304
  writeFileSync as writeFileSync3
2525
2305
  } from "fs";
2526
- import { join as join11 } from "path";
2306
+ import { join as join9 } from "path";
2527
2307
  import { z as z5 } from "zod";
2528
2308
  var credentialMapSchema = z5.record(z5.string(), z5.string());
2529
2309
  function credentialsDir() {
2530
- return join11(cabaneDir(), "credentials");
2310
+ return join9(cabaneDir(), "credentials");
2531
2311
  }
2532
2312
  function legacyPath() {
2533
- return join11(cabaneDir(), "credentials.json");
2313
+ return join9(cabaneDir(), "credentials.json");
2534
2314
  }
2535
2315
  function devicePath(deviceId) {
2536
- return join11(credentialsDir(), `${deviceId.replace(/[^A-Za-z0-9_-]/g, "_")}.json`);
2316
+ return join9(credentialsDir(), `${deviceId.replace(/[^A-Za-z0-9_-]/g, "_")}.json`);
2537
2317
  }
2538
2318
  function read(path) {
2539
- if (!existsSync7(path)) return null;
2319
+ if (!existsSync6(path)) return null;
2540
2320
  let raw;
2541
2321
  try {
2542
2322
  raw = readFileSync4(path, "utf8");
@@ -2601,18 +2381,18 @@ function pruneCredentials(deviceId, keepAgentIds) {
2601
2381
  }
2602
2382
 
2603
2383
  // src/dispatch-dedupe.ts
2604
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync as renameSync4, existsSync as existsSync8 } from "fs";
2605
- import { join as join12 } from "path";
2384
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync as renameSync4, existsSync as existsSync7 } from "fs";
2385
+ import { join as join10 } from "path";
2606
2386
  var MAX_IDS = 256;
2607
2387
  function dir(log) {
2608
- return join12(cabaneDir(), log);
2388
+ return join10(cabaneDir(), log);
2609
2389
  }
2610
2390
  function pathFor(log, workspaceId) {
2611
- return join12(dir(log), encodeURIComponent(workspaceId));
2391
+ return join10(dir(log), encodeURIComponent(workspaceId));
2612
2392
  }
2613
2393
  function readIds(log, workspaceId) {
2614
2394
  const path = pathFor(log, workspaceId);
2615
- if (!existsSync8(path)) return [];
2395
+ if (!existsSync7(path)) return [];
2616
2396
  try {
2617
2397
  return readFileSync5(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
2618
2398
  } catch {
@@ -2645,15 +2425,15 @@ function markCompleted(workspaceId, eventId) {
2645
2425
  }
2646
2426
  var MAX_RESUME_ATTEMPTS = 3;
2647
2427
  function resumeDir() {
2648
- return join12(cabaneDir(), "resume-attempts");
2428
+ return join10(cabaneDir(), "resume-attempts");
2649
2429
  }
2650
2430
  function resumePathFor(workspaceId) {
2651
- return join12(resumeDir(), encodeURIComponent(workspaceId));
2431
+ return join10(resumeDir(), encodeURIComponent(workspaceId));
2652
2432
  }
2653
2433
  function readResumeCounts(workspaceId) {
2654
2434
  const out = /* @__PURE__ */ new Map();
2655
2435
  const path = resumePathFor(workspaceId);
2656
- if (!existsSync8(path)) return out;
2436
+ if (!existsSync7(path)) return out;
2657
2437
  try {
2658
2438
  for (const line of readFileSync5(path, "utf8").split("\n")) {
2659
2439
  const trimmed = line.trim();
@@ -2684,16 +2464,16 @@ function bumpResumeAttempt(workspaceId, eventId) {
2684
2464
  return next;
2685
2465
  }
2686
2466
  function turnDir() {
2687
- return join12(cabaneDir(), "turns");
2467
+ return join10(cabaneDir(), "turns");
2688
2468
  }
2689
2469
  function turnPathFor(workspaceId) {
2690
- return join12(turnDir(), encodeURIComponent(workspaceId));
2470
+ return join10(turnDir(), encodeURIComponent(workspaceId));
2691
2471
  }
2692
2472
  var TURN_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2693
2473
  function readTurnIds(workspaceId) {
2694
2474
  const out = /* @__PURE__ */ new Map();
2695
2475
  const path = turnPathFor(workspaceId);
2696
- if (!existsSync8(path)) return out;
2476
+ if (!existsSync7(path)) return out;
2697
2477
  try {
2698
2478
  for (const line of readFileSync5(path, "utf8").split("\n")) {
2699
2479
  const trimmed = line.trim();
@@ -2875,7 +2655,7 @@ var turnOutcomes = ["success", "failure", "cancelled", "skipped"];
2875
2655
  var turnSessionModes = ["fresh", "resumed", "degraded"];
2876
2656
  var turnRuntimeResultKinds = ["success", "error", "no_terminal"];
2877
2657
  var turnFinalSources = [
2878
- // The agent declared its answer with the `answer` turn act (CT1973).
2658
+ // The agent declared its answer with the `answer` turn act.
2879
2659
  "declared",
2880
2660
  "runtime_text",
2881
2661
  "progress_promotion",
@@ -3016,15 +2796,6 @@ function standingSegments(req) {
3016
2796
  return req.standing ?? { core: req.systemPrompt, conversation: "" };
3017
2797
  }
3018
2798
 
3019
- // packages/agent-runtime/src/conformance.ts
3020
- import { z as z9 } from "zod";
3021
- var conformanceFixtureSchema = z9.object({
3022
- name: z9.string(),
3023
- request: turnRequestSchema,
3024
- nativeStream: z9.array(z9.unknown()),
3025
- expected: z9.array(turnEventSchema)
3026
- });
3027
-
3028
2799
  // packages/agent-runtime/src/transcript.ts
3029
2800
  function createTerminalTextBuffer() {
3030
2801
  return { held: null };
@@ -3292,7 +3063,7 @@ function envResolvedFrom(server, secretEnv) {
3292
3063
  }
3293
3064
 
3294
3065
  // packages/agent-runtime/src/claude-code/policy.ts
3295
- import { z as z10 } from "zod";
3066
+ import { z as z9 } from "zod";
3296
3067
  var SUBAGENT_TOOLS = ["Task", "Agent"];
3297
3068
  var UNSUPPORTED_TOOLS = ["AskUserQuestion"];
3298
3069
  var STRANDING_TOOLS = [
@@ -3319,18 +3090,18 @@ function withThinkingSummaries(thinking) {
3319
3090
  if (thinking.type === "disabled") return thinking;
3320
3091
  return { display: "summarized", ...thinking };
3321
3092
  }
3322
- var claudeCodeDialectSchema = z10.object({
3323
- thinking: z10.discriminatedUnion("type", [
3324
- z10.object({
3325
- type: z10.literal("adaptive"),
3326
- display: z10.enum(["summarized", "omitted"]).optional()
3093
+ var claudeCodeDialectSchema = z9.object({
3094
+ thinking: z9.discriminatedUnion("type", [
3095
+ z9.object({
3096
+ type: z9.literal("adaptive"),
3097
+ display: z9.enum(["summarized", "omitted"]).optional()
3327
3098
  }),
3328
- z10.object({
3329
- type: z10.literal("enabled"),
3330
- budgetTokens: z10.number().int().positive().optional(),
3331
- display: z10.enum(["summarized", "omitted"]).optional()
3099
+ z9.object({
3100
+ type: z9.literal("enabled"),
3101
+ budgetTokens: z9.number().int().positive().optional(),
3102
+ display: z9.enum(["summarized", "omitted"]).optional()
3332
3103
  }),
3333
- z10.object({ type: z10.literal("disabled") })
3104
+ z9.object({ type: z9.literal("disabled") })
3334
3105
  ]).optional()
3335
3106
  }).loose();
3336
3107
  function readThinking(runtimeOptions) {
@@ -3393,7 +3164,7 @@ async function preToolUseHook(input) {
3393
3164
  hookSpecificOutput: {
3394
3165
  hookEventName: "PreToolUse",
3395
3166
  permissionDecision: "allow",
3396
- permissionDecisionReason: "headless never-prompt (CT680)",
3167
+ permissionDecisionReason: "headless never-prompt",
3397
3168
  updatedInput: toolInput
3398
3169
  }
3399
3170
  };
@@ -3918,7 +3689,7 @@ async function* runWithResumeRecovery(queryFn, req, signal, initial, augmentOpti
3918
3689
  if (recoverResume) {
3919
3690
  if (signal.aborted) return;
3920
3691
  onWarn?.(
3921
- `claude-code adapter: stored session ${initial.resume} ended before system/init \u2014 starting one fresh session (CT1293 recovery)`,
3692
+ `claude-code adapter: stored session ${initial.resume} ended before system/init \u2014 starting one fresh session`,
3922
3693
  {
3923
3694
  cwd: req.local.cwd,
3924
3695
  reason: "session_start_failed",
@@ -3947,7 +3718,7 @@ function createClaudeCodeAdapter(deps = {}) {
3947
3718
  let built = buildClaudeCodeOptions(req, deps.augmentOptions);
3948
3719
  if (built.freshReason === "cwd_mismatch") {
3949
3720
  deps.onWarn?.(
3950
- "claude-code adapter: stored session cwd no longer matches the current environment \u2014 starting a fresh session (SJ527 guard)",
3721
+ "claude-code adapter: stored session cwd no longer matches the current environment \u2014 starting a fresh session",
3951
3722
  { cwd: req.local.cwd }
3952
3723
  );
3953
3724
  }
@@ -3979,538 +3750,6 @@ function createClaudeCodeAdapter(deps = {}) {
3979
3750
  }
3980
3751
  var claudeCodeAdapter = createClaudeCodeAdapter();
3981
3752
 
3982
- // packages/agent-runtime/src/claude-code/conformance.ts
3983
- var ABORT_SENTINEL = { __abortHere: true };
3984
- var CWD = "/env/here";
3985
- var HEALTHY_MCP_INVENTORY = {
3986
- initReceived: true,
3987
- servers: [{ name: "cabane", status: "connected" }],
3988
- toolCount: 1
3989
- };
3990
- function makeRequest(overrides2 = {}) {
3991
- return {
3992
- systemPrompt: "system",
3993
- prompt: "hi there",
3994
- content: [{ type: "text", text: "hi there" }],
3995
- // The adapter rejects a null model, so the fixture carries a real bare SDK id.
3996
- config: { model: "claude-opus-4-8" },
3997
- session: null,
3998
- cabane: { mcpUrl: "https://cabane.test/api/mcp", bearer: "tok", activeConversationId: "conv" },
3999
- local: { cwd: CWD },
4000
- extra: { mcpServers: {} },
4001
- ...overrides2
4002
- };
4003
- }
4004
- var init = (sessionId, model) => ({
4005
- type: "system",
4006
- subtype: "init",
4007
- session_id: sessionId,
4008
- mcp_servers: HEALTHY_MCP_INVENTORY.servers,
4009
- tools: ["mcp__cabane__query"],
4010
- ...model ? { model } : {}
4011
- });
4012
- var assistantText = (text) => ({
4013
- type: "assistant",
4014
- message: { content: [{ type: "text", text }] }
4015
- });
4016
- var assistantTextTool = (text, tool2) => ({
4017
- type: "assistant",
4018
- message: {
4019
- content: [
4020
- { type: "text", text },
4021
- { type: "tool_use", id: tool2.id, name: tool2.name, input: tool2.input }
4022
- ]
4023
- }
4024
- });
4025
- var toolResult = (id, content, isError = false) => ({
4026
- type: "user",
4027
- message: { content: [{ type: "tool_result", tool_use_id: id, is_error: isError, content }] }
4028
- });
4029
- var resultSuccess = (sessionId) => ({
4030
- type: "result",
4031
- subtype: "success",
4032
- session_id: sessionId
4033
- });
4034
- var resultSuccessFull = (sessionId, extra) => ({
4035
- ...resultSuccess(sessionId),
4036
- ...extra
4037
- });
4038
- var resultError = (subtype) => ({
4039
- type: "result",
4040
- subtype,
4041
- session_id: "s"
4042
- });
4043
- var resultErrorFull = (subtype, extra = {}) => ({
4044
- type: "result",
4045
- subtype,
4046
- session_id: "s",
4047
- ...extra
4048
- });
4049
- var rateLimitEvent = (status, resetsAt, rateLimitType) => ({
4050
- type: "rate_limit_event",
4051
- rate_limit_info: {
4052
- status,
4053
- ...resetsAt !== void 0 ? { resetsAt } : {},
4054
- ...rateLimitType !== void 0 ? { rateLimitType } : {}
4055
- },
4056
- session_id: "s"
4057
- });
4058
- var authStatus = (error) => ({
4059
- type: "auth_status",
4060
- isAuthenticating: false,
4061
- output: [],
4062
- error,
4063
- session_id: "s"
4064
- });
4065
- var sessionEvent = (sdkSessionId, cwd = CWD, degraded = false) => ({
4066
- type: "session",
4067
- state: encodeSession({ sdkSessionId, cwd }),
4068
- ...degraded ? { degraded: true } : {}
4069
- });
4070
- var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
4071
- {
4072
- // A plain text reply: the held text-only block flushes as the terminal final.
4073
- name: "clean turn",
4074
- request: makeRequest(),
4075
- nativeStream: [init("s1"), assistantText("Hello there."), resultSuccess("s1")],
4076
- expected: [
4077
- sessionEvent("s1"),
4078
- { type: "text", body: "Hello there.", terminal: true },
4079
- { type: "result", ok: true }
4080
- ]
4081
- },
4082
- {
4083
- // Narration + a tool call + result + closing reply.
4084
- name: "tool turn",
4085
- request: makeRequest(),
4086
- nativeStream: [
4087
- init("s1"),
4088
- assistantTextTool("Reading your notes.", {
4089
- id: "tu1",
4090
- name: "mcp__cabane__cabane_read",
4091
- input: { path: "sleep.md" }
4092
- }),
4093
- toolResult("tu1", [{ type: "text", text: "sleep early" }]),
4094
- assistantText("Here's the summary."),
4095
- resultSuccess("s1")
4096
- ],
4097
- expected: [
4098
- sessionEvent("s1"),
4099
- { type: "text", body: "Reading your notes.", terminal: false },
4100
- {
4101
- type: "tool",
4102
- id: "tu1",
4103
- name: "cabane_read",
4104
- phase: "start",
4105
- summary: "sleep.md",
4106
- input: { path: "sleep.md" },
4107
- // Derived from the raw `mcp__cabane__` name.
4108
- mcpServer: "cabane"
4109
- },
4110
- {
4111
- type: "tool",
4112
- id: "tu1",
4113
- name: "cabane_read",
4114
- phase: "done",
4115
- summary: "sleep.md",
4116
- input: { path: "sleep.md" },
4117
- result: [{ type: "text", text: "sleep early" }],
4118
- mcpServer: "cabane"
4119
- },
4120
- { type: "text", body: "Here's the summary.", terminal: true },
4121
- { type: "result", ok: true }
4122
- ]
4123
- },
4124
- {
4125
- // Highest risk: cancel mid-stream. The abort lands after the tool result; the adapter stops
4126
- // before the closing reply — no final text, no `result` event (the server writes the stopped
4127
- // marker).
4128
- name: "cancel mid-stream",
4129
- request: makeRequest(),
4130
- nativeStream: [
4131
- init("s1"),
4132
- assistantTextTool("Working on it.", {
4133
- id: "tu1",
4134
- name: "mcp__cabane__cabane_list",
4135
- input: {}
4136
- }),
4137
- toolResult("tu1"),
4138
- ABORT_SENTINEL,
4139
- assistantText("This reply must never be emitted."),
4140
- resultSuccess("s1")
4141
- ],
4142
- expected: [
4143
- sessionEvent("s1"),
4144
- { type: "text", body: "Working on it.", terminal: false },
4145
- {
4146
- type: "tool",
4147
- id: "tu1",
4148
- name: "cabane_list",
4149
- phase: "start",
4150
- summary: "",
4151
- input: {},
4152
- mcpServer: "cabane"
4153
- },
4154
- {
4155
- type: "tool",
4156
- id: "tu1",
4157
- name: "cabane_list",
4158
- phase: "done",
4159
- summary: "",
4160
- input: {},
4161
- result: void 0,
4162
- mcpServer: "cabane"
4163
- }
4164
- ]
4165
- },
4166
- {
4167
- // An error result: partial narration lands as `progress` (terminal:false),
4168
- // and the `result` event carries the machine reason.
4169
- name: "error result",
4170
- request: makeRequest(),
4171
- nativeStream: [init("s1"), assistantText("Partial work."), resultError("error_max_turns")],
4172
- expected: [
4173
- sessionEvent("s1"),
4174
- { type: "text", body: "Partial work.", terminal: false },
4175
- { type: "result", ok: false, reason: "result_error:error_max_turns" }
4176
- ]
4177
- },
4178
- {
4179
- // The empty-runtime anomaly's captured trace: `system/init` then a successful `result`, nothing
4180
- // in between. Distinct from `empty-final` (a tool ran, no speech); this produced nothing. The
4181
- // adapter reports faithfully — `result{ok:true}` — and the host decides that a success with no
4182
- // content-bearing event is a failed turn.
4183
- name: "zero-content success (empty runtime result)",
4184
- request: makeRequest(),
4185
- nativeStream: [init("s1"), resultSuccess("s1")],
4186
- expected: [sessionEvent("s1"), { type: "result", ok: true }]
4187
- },
4188
- {
4189
- // On resume, a completed background task can emit a notification and its own zero-token result
4190
- // before the prompt's frames. That result is not the turn boundary; decoding continues through
4191
- // the reply and real usage.
4192
- name: "task-notification result does not end a resumed turn",
4193
- request: makeRequest({ session: encodeSession({ sdkSessionId: "s1", cwd: CWD }) }),
4194
- nativeStream: [
4195
- init("s1"),
4196
- {
4197
- type: "system",
4198
- subtype: "task_notification",
4199
- task_id: "task-1",
4200
- status: "completed",
4201
- output_file: "/tmp/task-1.output",
4202
- summary: "Background work completed.",
4203
- session_id: "s1"
4204
- },
4205
- resultSuccessFull("s1", {
4206
- is_error: false,
4207
- origin: { kind: "task-notification" },
4208
- usage: {
4209
- input_tokens: 0,
4210
- output_tokens: 0,
4211
- cache_read_input_tokens: 0,
4212
- cache_creation_input_tokens: 0
4213
- }
4214
- }),
4215
- assistantTextTool("Checking the returned work.", {
4216
- id: "tu1",
4217
- name: "mcp__cabane__cabane_read",
4218
- input: { path: "result.md" }
4219
- }),
4220
- toolResult("tu1", [{ type: "text", text: "ready" }]),
4221
- assistantText("The prompt completed normally."),
4222
- resultSuccessFull("s1", {
4223
- is_error: false,
4224
- usage: {
4225
- input_tokens: 120,
4226
- output_tokens: 30,
4227
- cache_read_input_tokens: 80,
4228
- cache_creation_input_tokens: 10
4229
- }
4230
- })
4231
- ],
4232
- expected: [
4233
- { type: "text", body: "Checking the returned work.", terminal: false },
4234
- {
4235
- type: "tool",
4236
- id: "tu1",
4237
- name: "cabane_read",
4238
- phase: "start",
4239
- summary: "result.md",
4240
- input: { path: "result.md" },
4241
- mcpServer: "cabane"
4242
- },
4243
- {
4244
- type: "tool",
4245
- id: "tu1",
4246
- name: "cabane_read",
4247
- phase: "done",
4248
- summary: "result.md",
4249
- input: { path: "result.md" },
4250
- result: [{ type: "text", text: "ready" }],
4251
- mcpServer: "cabane"
4252
- },
4253
- { type: "text", body: "The prompt completed normally.", terminal: true },
4254
- {
4255
- type: "result",
4256
- ok: true,
4257
- usage: {
4258
- inputTokens: 210,
4259
- outputTokens: 30,
4260
- cacheReadTokens: 80,
4261
- cacheCreationTokens: 10
4262
- }
4263
- }
4264
- ]
4265
- },
4266
- {
4267
- // The filter is deliberately narrow: an origin-less zero-token result still ends the turn, even
4268
- // if more frames follow.
4269
- name: "origin-less result still ends a resumed turn",
4270
- request: makeRequest({ session: encodeSession({ sdkSessionId: "s1", cwd: CWD }) }),
4271
- nativeStream: [
4272
- init("s1"),
4273
- resultSuccessFull("s1", {
4274
- is_error: false,
4275
- usage: {
4276
- input_tokens: 0,
4277
- output_tokens: 0,
4278
- cache_read_input_tokens: 0,
4279
- cache_creation_input_tokens: 0
4280
- }
4281
- }),
4282
- assistantText("This must not be emitted."),
4283
- resultSuccess("s1")
4284
- ],
4285
- expected: [
4286
- {
4287
- type: "result",
4288
- ok: true,
4289
- usage: {
4290
- inputTokens: 0,
4291
- outputTokens: 0,
4292
- cacheReadTokens: 0,
4293
- cacheCreationTokens: 0
4294
- }
4295
- }
4296
- ]
4297
- },
4298
- {
4299
- // Empty-final: a clean turn that ended on a tool call with no closing text.
4300
- // The adapter emits NO final text — empty-final promotion is host/pump
4301
- // territory (this fixture pins that boundary).
4302
- name: "empty-final (no closing text)",
4303
- request: makeRequest(),
4304
- nativeStream: [
4305
- init("s1"),
4306
- assistantTextTool("", {
4307
- id: "tu1",
4308
- name: "mcp__cabane__cabane_write",
4309
- input: { path: "a.md" }
4310
- }),
4311
- toolResult("tu1"),
4312
- resultSuccess("s1")
4313
- ],
4314
- expected: [
4315
- sessionEvent("s1"),
4316
- {
4317
- type: "tool",
4318
- id: "tu1",
4319
- name: "cabane_write",
4320
- phase: "start",
4321
- summary: "a.md",
4322
- input: { path: "a.md" },
4323
- mcpServer: "cabane"
4324
- },
4325
- {
4326
- type: "tool",
4327
- id: "tu1",
4328
- name: "cabane_write",
4329
- phase: "done",
4330
- summary: "a.md",
4331
- input: { path: "a.md" },
4332
- result: void 0,
4333
- mcpServer: "cabane"
4334
- },
4335
- { type: "result", ok: true }
4336
- ]
4337
- },
4338
- {
4339
- // A subscription cap. The SDK emits a rejected `rate_limit_event` with a named subscription
4340
- // window; the terminal error result classifies as `usage_capped`. The held provider line
4341
- // independently classifies to the same kind, so it lands as a runtime notice, not agent
4342
- // progress.
4343
- name: "subscription cap \u2192 usage_capped",
4344
- request: makeRequest(),
4345
- nativeStream: [
4346
- init("s1"),
4347
- assistantText("You've hit your session limit \xB7 resets 8:20pm (UTC)"),
4348
- rateLimitEvent("rejected", void 0, "five_hour"),
4349
- resultError("error_during_execution")
4350
- ],
4351
- expected: [
4352
- sessionEvent("s1"),
4353
- {
4354
- type: "runtime_notice",
4355
- body: "You've hit your session limit \xB7 resets 8:20pm (UTC)"
4356
- },
4357
- { type: "result", ok: false, reason: "usage_capped" }
4358
- ]
4359
- },
4360
- {
4361
- // A failed turn can still contain the agent's own partial narration. No text classification
4362
- // means no authorship rewrite, even though the structured result is the same usage cap.
4363
- name: "partial narration before subscription cap stays progress",
4364
- request: makeRequest(),
4365
- nativeStream: [
4366
- init("s1"),
4367
- assistantText("Partial work."),
4368
- rateLimitEvent("rejected", void 0, "five_hour"),
4369
- resultError("error_during_execution")
4370
- ],
4371
- expected: [
4372
- sessionEvent("s1"),
4373
- { type: "text", body: "Partial work.", terminal: false },
4374
- { type: "result", ok: false, reason: "usage_capped" }
4375
- ]
4376
- },
4377
- {
4378
- // Recognized runtime prose stays authored when its kind disagrees with the structured terminal
4379
- // failure.
4380
- name: "cap notice before server failure stays progress",
4381
- request: makeRequest(),
4382
- nativeStream: [
4383
- init("s1"),
4384
- assistantText("You've hit your session limit \xB7 resets 8:20pm (UTC)"),
4385
- resultErrorFull("error_during_execution", {
4386
- is_error: true,
4387
- result: '529 {"type":"error","error":{"type":"overloaded_error"}}'
4388
- })
4389
- ],
4390
- expected: [
4391
- sessionEvent("s1"),
4392
- {
4393
- type: "text",
4394
- body: "You've hit your session limit \xB7 resets 8:20pm (UTC)",
4395
- terminal: false
4396
- },
4397
- { type: "result", ok: false, reason: "server_error" }
4398
- ]
4399
- },
4400
- {
4401
- // `classifyErrorText` deliberately searches arbitrary error blobs for status tokens. Assistant
4402
- // narration needs the stricter whole-notice classifier, or an incidental count would be
4403
- // de-authored when the turn later fails with the matching provider kind.
4404
- name: "incidental 5xx-shaped narration before server failure stays progress",
4405
- request: makeRequest(),
4406
- nativeStream: [
4407
- init("s1"),
4408
- assistantText("Indexed 500 files. Writing the summary next."),
4409
- resultErrorFull("error_during_execution", {
4410
- is_error: true,
4411
- result: '529 {"type":"error","error":{"type":"overloaded_error"}}'
4412
- })
4413
- ],
4414
- expected: [
4415
- sessionEvent("s1"),
4416
- {
4417
- type: "text",
4418
- body: "Indexed 500 files. Writing the summary next.",
4419
- terminal: false
4420
- },
4421
- { type: "result", ok: false, reason: "server_error" }
4422
- ]
4423
- },
4424
- {
4425
- // A cap signalled only by the result's `terminal_reason:'blocking_limit'` (no rate-limit event)
4426
- // still classifies as `usage_capped`.
4427
- name: "blocking_limit terminal reason \u2192 usage_capped",
4428
- request: makeRequest(),
4429
- nativeStream: [
4430
- init("s1"),
4431
- resultErrorFull("error_during_execution", {
4432
- is_error: true,
4433
- terminal_reason: "blocking_limit",
4434
- result: "stopped"
4435
- })
4436
- ],
4437
- expected: [sessionEvent("s1"), { type: "result", ok: false, reason: "usage_capped" }]
4438
- },
4439
- {
4440
- // An expired login surfaces as an `auth_status` error frame; the failing result classifies as
4441
- // `auth_expired`.
4442
- name: "expired login \u2192 auth_expired",
4443
- request: makeRequest(),
4444
- nativeStream: [
4445
- init("s1"),
4446
- authStatus("OAuth token has expired, please run /login"),
4447
- resultErrorFull("error_during_execution", {
4448
- is_error: true,
4449
- result: "authentication failed"
4450
- })
4451
- ],
4452
- expected: [sessionEvent("s1"), { type: "result", ok: false, reason: "auth_expired" }]
4453
- },
4454
- {
4455
- // A transient provider outage in the terminal result text (no structured event or enum) — a 529
4456
- // overload — classifies as `server_error` via the text fallback. It must not read as the cap or
4457
- // stamp the connector `limited`.
4458
- name: "provider overload text \u2192 server_error",
4459
- request: makeRequest(),
4460
- nativeStream: [
4461
- init("s1"),
4462
- resultErrorFull("error_during_execution", {
4463
- is_error: true,
4464
- result: '529 {"type":"error","error":{"type":"overloaded_error"}}'
4465
- })
4466
- ],
4467
- expected: [sessionEvent("s1"), { type: "result", ok: false, reason: "server_error" }]
4468
- },
4469
- {
4470
- // The resolved model is captured off `system/init.model` and attached to the terminal `result`
4471
- // even on a plain resume (this fixture resumes, so no `session` event fires). What's asserted is
4472
- // that the concrete id the harness reported comes home.
4473
- name: "resolved model captured from system/init",
4474
- request: makeRequest({ session: encodeSession({ sdkSessionId: "s1", cwd: CWD }) }),
4475
- nativeStream: [init("s1", "claude-opus-4-8"), assistantText("Done."), resultSuccess("s1")],
4476
- expected: [
4477
- { type: "text", body: "Done.", terminal: true },
4478
- { type: "result", ok: true, resolvedModel: "claude-opus-4-8" }
4479
- ]
4480
- },
4481
- {
4482
- // Session resume echo: the stored state matches the cwd, so its SDK id is resumed and
4483
- // `system/init` echoes the same id — no session event fires.
4484
- name: "session resume (no re-emit on echo)",
4485
- request: makeRequest({ session: encodeSession({ sdkSessionId: "s1", cwd: CWD }) }),
4486
- nativeStream: [init("s1"), assistantText("Back again."), resultSuccess("s1")],
4487
- expected: [
4488
- { type: "text", body: "Back again.", terminal: true },
4489
- { type: "result", ok: true }
4490
- ]
4491
- },
4492
- {
4493
- // Cwd-mismatch degrade: the stored state was created under another cwd, so the adapter starts
4494
- // fresh — `system/init` reports a new id, and the session event records the current cwd.
4495
- name: "session cwd-mismatch degrades to fresh",
4496
- request: makeRequest({ session: encodeSession({ sdkSessionId: "old", cwd: "/env/gone" }) }),
4497
- nativeStream: [init("fresh1"), assistantText("Fresh start."), resultSuccess("fresh1")],
4498
- expected: [
4499
- // The degrade is flagged on the session event so the host relays it on settle and the server
4500
- // rewinds the catch-up mark (recovering context the discarded session no longer carries).
4501
- sessionEvent("fresh1", CWD, true),
4502
- { type: "text", body: "Fresh start.", terminal: true },
4503
- { type: "result", ok: true }
4504
- ]
4505
- }
4506
- ];
4507
- var CLAUDE_CODE_CONFORMANCE_FIXTURES = BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES.map((fixture) => ({
4508
- ...fixture,
4509
- expected: fixture.expected.map(
4510
- (event) => event.type === "result" ? { ...event, mcpInventory: HEALTHY_MCP_INVENTORY } : event
4511
- )
4512
- }));
4513
-
4514
3753
  // packages/agent-runtime/src/opencode/addendum.ts
4515
3754
  var OPENCODE_ADDENDUM = [
4516
3755
  "Every tool here goes by its plain name: the workspace tools are `query`, `mutate` and",
@@ -4752,7 +3991,7 @@ function sealHeld(held, terminal) {
4752
3991
  }
4753
3992
 
4754
3993
  // packages/agent-runtime/src/opencode/policy.ts
4755
- import { z as z11 } from "zod";
3994
+ import { z as z10 } from "zod";
4756
3995
  var OPENCODE_SCHEDULING_TOOLS = [];
4757
3996
  var OPENCODE_UI_PROMPT_TOOLS = ["question"];
4758
3997
  function opencodeTools() {
@@ -4761,8 +4000,8 @@ function opencodeTools() {
4761
4000
  tools[name] = false;
4762
4001
  return tools;
4763
4002
  }
4764
- var opencodeDialectSchema = z11.object({
4765
- agent: z11.string().min(1).optional()
4003
+ var opencodeDialectSchema = z10.object({
4004
+ agent: z10.string().min(1).optional()
4766
4005
  }).loose();
4767
4006
  function readOpencodeDialect(runtimeOptions) {
4768
4007
  const parsed = opencodeDialectSchema.safeParse(runtimeOptions?.["opencode"] ?? {});
@@ -5146,516 +4385,40 @@ function createOpencodeAdapter(deps = {}) {
5146
4385
  const resume = "resume" in decision ? decision.resume : null;
5147
4386
  if ("fresh" in decision && decision.reason === "directory_mismatch") {
5148
4387
  deps.onWarn?.(
5149
- "opencode adapter: stored session directory no longer matches the current environment \u2014 starting a fresh session (SJ527-analogue guard)",
4388
+ "opencode adapter: stored session directory no longer matches the current environment \u2014 starting a fresh session",
5150
4389
  { directory }
5151
4390
  );
5152
- }
5153
- let degraded = "fresh" in decision && decision.reason !== void 0 && decision.reason !== "no_session";
5154
- if ("fresh" in decision && decision.reason !== void 0 && decision.reason !== "no_session" && hooks?.fresh) {
5155
- const recomposed = await hooks.fresh(decision.reason);
5156
- if (signal.aborted) return;
5157
- if (recomposed) {
5158
- req = { ...req, ...recomposed, session: null };
5159
- degraded = false;
5160
- }
5161
- }
5162
- const spec = buildRunSpec(req, resume);
5163
- const result = await transport.run(spec, signal);
5164
- if (signal.aborted) return;
5165
- if (result.created || result.sessionId !== resume) {
5166
- yield {
5167
- type: "session",
5168
- state: encodeSession2({ sessionId: result.sessionId, directory: result.directory }),
5169
- ...degraded ? { degraded: true } : {}
5170
- };
5171
- }
5172
- yield* decodeOpencodeStream(result.events, {
5173
- signal,
5174
- sessionId: result.sessionId,
5175
- // The keys the turn's servers were installed under: what the decoder
5176
- // splits a raw `<server>_<tool>` id on.
5177
- mcpServers: Object.keys(spec.mcp)
5178
- });
5179
- }
5180
- };
5181
- }
5182
- var opencodeAdapter = createOpencodeAdapter();
5183
-
5184
- // packages/agent-runtime/src/opencode/conformance.ts
5185
- var ABORT_SENTINEL2 = { __abortHere: true };
5186
- var NEW_SESSION_ID = "sess_new";
5187
- var BOUNDED_NOTICE = "You've hit your usage limit \xB7 ".padEnd(200, "x");
5188
- var DIR = "/env/here";
5189
- function makeRequest2(overrides2 = {}) {
5190
- return {
5191
- systemPrompt: "system",
5192
- prompt: "hi there",
5193
- content: [{ type: "text", text: "hi there" }],
5194
- config: { model: "deepseek/deepseek-chat" },
5195
- session: null,
5196
- cabane: { mcpUrl: "https://cabane.test/api/mcp", bearer: "tok", activeConversationId: "conv" },
5197
- local: { cwd: DIR },
5198
- extra: { mcpServers: {} },
5199
- ...overrides2
5200
- };
5201
- }
5202
- var textPart = (id, text) => ({
5203
- type: "message.part.updated",
5204
- properties: { part: { type: "text", id, text } }
5205
- });
5206
- var reasoningPart = (id, text) => ({
5207
- type: "message.part.updated",
5208
- properties: { part: { type: "reasoning", id, text } }
5209
- });
5210
- var toolPart = (id, tool2, state, callID) => ({
5211
- type: "message.part.updated",
5212
- properties: { part: { type: "tool", id, tool: tool2, state, ...callID ? { callID } : {} } }
5213
- });
5214
- var messageUpdated = (role, id) => ({
5215
- type: "message.updated",
5216
- properties: { info: { role, id } }
5217
- });
5218
- var echoedUserPart = (partId, messageID, text) => ({
5219
- type: "message.part.updated",
5220
- properties: { part: { type: "text", id: partId, messageID, text } }
5221
- });
5222
- var idle = () => ({ type: "session.idle", properties: {} });
5223
- var errored = (message) => ({
5224
- type: "session.error",
5225
- properties: { error: { message } }
5226
- });
5227
- var sessionEvent2 = (sessionId, directory = DIR, degraded = false) => ({
5228
- type: "session",
5229
- state: encodeSession2({ sessionId, directory }),
5230
- ...degraded ? { degraded: true } : {}
5231
- });
5232
- var OPENCODE_CONFORMANCE_FIXTURES = [
5233
- {
5234
- // A plain text reply: the held text part seals as the terminal final at idle.
5235
- name: "clean turn",
5236
- request: makeRequest2(),
5237
- nativeStream: [textPart("p1", "Hello there."), idle()],
5238
- expected: [
5239
- sessionEvent2(NEW_SESSION_ID),
5240
- { type: "text", body: "Hello there.", terminal: true },
5241
- { type: "result", ok: true }
5242
- ]
5243
- },
5244
- {
5245
- // Narration + a tool call + result + closing reply. The narration part is
5246
- // sealed as interim (a tool follows it); the trailing text is the final.
5247
- name: "tool turn",
5248
- request: makeRequest2(),
5249
- nativeStream: [
5250
- textPart("p1", "Reading your notes."),
5251
- toolPart("t1", "read", { status: "running", input: { path: "sleep.md" } }, "call_1"),
5252
- toolPart(
5253
- "t1",
5254
- "read",
5255
- { status: "completed", input: { path: "sleep.md" }, output: "sleep early" },
5256
- "call_1"
5257
- ),
5258
- textPart("p2", "Here's the summary."),
5259
- idle()
5260
- ],
5261
- expected: [
5262
- sessionEvent2(NEW_SESSION_ID),
5263
- { type: "text", body: "Reading your notes.", terminal: false },
5264
- {
5265
- type: "tool",
5266
- id: "call_1",
5267
- name: "read",
5268
- phase: "start",
5269
- summary: "sleep.md",
5270
- input: { path: "sleep.md" }
5271
- },
5272
- {
5273
- type: "tool",
5274
- id: "call_1",
5275
- name: "read",
5276
- phase: "done",
5277
- summary: "sleep.md",
5278
- input: { path: "sleep.md" },
5279
- result: "sleep early"
5280
- },
5281
- { type: "text", body: "Here's the summary.", terminal: true },
5282
- { type: "result", ok: true }
5283
- ]
5284
- },
5285
- {
5286
- // A streaming provider's tool input arrives late. opencode re-emits the tool part as a full
5287
- // snapshot as the args parse: DeepSeek sends `input: {}` on the early `pending`/`running` frame
5288
- // and the full args on a later one (captured on opencode 1.17: `pending {}` → `running {full}` →
5289
- // `completed {full}`). `start` fires off the first (empty) frame — the live card — but `done`
5290
- // (and the row it upserts) must carry the latest non-empty input and its summary. Every other
5291
- // tool fixture bakes the full input into the first snapshot, so only this one catches it.
5292
- name: "streaming tool input \u2014 late args refresh the done event (CT301)",
5293
- request: makeRequest2(),
5294
- nativeStream: [
5295
- toolPart("t1", "cabane_list", { status: "pending", input: {} }, "call_1"),
5296
- toolPart(
5297
- "t1",
5298
- "cabane_list",
5299
- { status: "running", input: { path: "/product/tasks" } },
5300
- "call_1"
5301
- ),
5302
- toolPart(
5303
- "t1",
5304
- "cabane_list",
5305
- { status: "completed", input: { path: "/product/tasks" }, output: "CT301.md" },
5306
- "call_1"
5307
- ),
5308
- idle()
5309
- ],
5310
- expected: [
5311
- sessionEvent2(NEW_SESSION_ID),
5312
- // start: fires off the first (empty) snapshot — empty input + summary, by design.
5313
- {
5314
- type: "tool",
5315
- id: "call_1",
5316
- name: "cabane_list",
5317
- phase: "start",
5318
- summary: "",
5319
- input: {},
5320
- mcpServer: "cabane"
5321
- },
5322
- // done: carries the full args captured from the later snapshot, and the
5323
- // summary recomputed from them — NOT the first-seen `{}`.
5324
- {
5325
- type: "tool",
5326
- id: "call_1",
5327
- name: "cabane_list",
5328
- phase: "done",
5329
- summary: "/product/tasks",
5330
- input: { path: "/product/tasks" },
5331
- result: "CT301.md",
5332
- mcpServer: "cabane"
5333
- },
5334
- { type: "result", ok: true }
5335
- ]
5336
- },
5337
- {
5338
- // The turn-control server's `answer` (CT1973). opencode names an MCP tool `<server>_<tool>`,
5339
- // so the installed keys split the raw id: `cabane_companion_answer` is the turn-control
5340
- // server's, never the workspace server's `companion_answer`. The pump binds on that.
5341
- name: "turn-control answer is read as the turn-control server\u2019s, not cabane\u2019s",
5342
- request: makeRequest2({
5343
- cabane: {
5344
- mcpUrl: "https://cabane.test/api/mcp",
5345
- turnControlUrl: "https://cabane.test/api/turn-control",
5346
- bearer: "tok",
5347
- activeConversationId: "conv"
5348
- }
5349
- }),
5350
- nativeStream: [
5351
- textPart("p1", "On it."),
5352
- toolPart("t1", "cabane_companion_answer", { status: "running", input: {} }, "call_1"),
5353
- toolPart(
5354
- "t1",
5355
- "cabane_companion_answer",
5356
- { status: "completed", input: {}, output: '{"ok":true,"kind":"answer"}' },
5357
- "call_1"
5358
- ),
5359
- textPart("p2", "The answer."),
5360
- idle()
5361
- ],
5362
- expected: [
5363
- sessionEvent2(NEW_SESSION_ID),
5364
- { type: "text", body: "On it.", terminal: false },
5365
- {
5366
- type: "tool",
5367
- id: "call_1",
5368
- name: "cabane_companion_answer",
5369
- phase: "start",
5370
- summary: "",
5371
- input: {},
5372
- mcpServer: "cabane_companion"
5373
- },
5374
- {
5375
- type: "tool",
5376
- id: "call_1",
5377
- name: "cabane_companion_answer",
5378
- phase: "done",
5379
- summary: "",
5380
- input: {},
5381
- result: '{"ok":true,"kind":"answer"}',
5382
- mcpServer: "cabane_companion"
5383
- },
5384
- { type: "text", body: "The answer.", terminal: true },
5385
- { type: "result", ok: true }
5386
- ]
5387
- },
5388
- {
5389
- // Highest risk: cancel mid-stream. The abort lands after the tool result; the adapter stops before
5390
- // the closing reply — no final text, no `result` event (the server writes the stopped marker).
5391
- name: "cancel mid-stream",
5392
- request: makeRequest2(),
5393
- nativeStream: [
5394
- textPart("p1", "Working on it."),
5395
- toolPart("t1", "list", { status: "running", input: {} }, "call_1"),
5396
- toolPart("t1", "list", { status: "completed", input: {}, output: "ok" }, "call_1"),
5397
- ABORT_SENTINEL2,
5398
- textPart("p2", "This reply must never be emitted."),
5399
- idle()
5400
- ],
5401
- expected: [
5402
- sessionEvent2(NEW_SESSION_ID),
5403
- { type: "text", body: "Working on it.", terminal: false },
5404
- { type: "tool", id: "call_1", name: "list", phase: "start", summary: "", input: {} },
5405
- {
5406
- type: "tool",
5407
- id: "call_1",
5408
- name: "list",
5409
- phase: "done",
5410
- summary: "",
5411
- input: {},
5412
- result: "ok"
5413
- }
5414
- ]
5415
- },
5416
- {
5417
- // An error result: partial narration lands as `progress` (terminal:false),
5418
- // and the `result` event carries the machine reason.
5419
- name: "error result",
5420
- request: makeRequest2(),
5421
- nativeStream: [textPart("p1", "Partial work."), errored("provider exploded")],
5422
- expected: [
5423
- sessionEvent2(NEW_SESSION_ID),
5424
- { type: "text", body: "Partial work.", terminal: false },
5425
- { type: "runtime_notice", body: "provider exploded" },
5426
- { type: "result", ok: false, reason: "error:provider exploded" }
5427
- ]
5428
- },
5429
- {
5430
- // A provider 429 throttle. The wrapped `RateLimitError` is unwrapped by `readSessionError` and
5431
- // text-classified: a bare "rate limit (429)" with no cap noun is the throttle (`rate_limited`),
5432
- // not the account cap.
5433
- name: "provider 429 \u2192 rate_limited (throttle, no reset)",
5434
- request: makeRequest2(),
5435
- nativeStream: [
5436
- {
5437
- type: "session.error",
5438
- properties: { error: { name: "RateLimitError", message: "rate limit exceeded (429)" } }
5439
- }
5440
- ],
5441
- expected: [
5442
- sessionEvent2(NEW_SESSION_ID),
5443
- { type: "runtime_notice", body: "RateLimitError: rate limit exceeded (429)" },
5444
- { type: "result", ok: false, reason: "rate_limited" }
5445
- ]
5446
- },
5447
- {
5448
- // A subscription cap surfaced as text ("usage limit reached") → `usage_capped`. opencode carries
5449
- // no reset hint, so it's reset-less (the server says "try again later").
5450
- name: "usage cap text \u2192 usage_capped (no reset)",
5451
- request: makeRequest2(),
5452
- nativeStream: [
5453
- {
5454
- type: "session.error",
5455
- properties: { error: { name: "Error", message: "Claude usage limit reached" } }
5456
- }
5457
- ],
5458
- expected: [
5459
- sessionEvent2(NEW_SESSION_ID),
5460
- { type: "runtime_notice", body: "Claude usage limit reached" },
5461
- { type: "result", ok: false, reason: "usage_capped" }
5462
- ]
5463
- },
5464
- {
5465
- // A transient provider outage (5xx / overload) surfaced as text → `server_error`, never the cap.
5466
- name: "provider overload text \u2192 server_error",
5467
- request: makeRequest2(),
5468
- nativeStream: [
5469
- {
5470
- type: "session.error",
5471
- properties: {
5472
- error: { name: "Error", message: "AI_APICallError: overloaded_error (529)" }
5473
- }
5474
- }
5475
- ],
5476
- expected: [
5477
- sessionEvent2(NEW_SESSION_ID),
5478
- { type: "runtime_notice", body: "AI_APICallError: overloaded_error (529)" },
5479
- { type: "result", ok: false, reason: "server_error" }
5480
- ]
5481
- },
5482
- {
5483
- // An expired or invalid credential classifies as `auth_expired`.
5484
- name: "auth failure \u2192 auth_expired",
5485
- request: makeRequest2(),
5486
- nativeStream: [
5487
- {
5488
- type: "session.error",
5489
- properties: { error: { name: "AuthenticationError", message: "HTTP 401 Unauthorized" } }
5490
- }
5491
- ],
5492
- expected: [
5493
- sessionEvent2(NEW_SESSION_ID),
5494
- { type: "runtime_notice", body: "AuthenticationError: HTTP 401 Unauthorized" },
5495
- { type: "result", ok: false, reason: "auth_expired" }
5496
- ]
5497
- },
5498
- {
5499
- // An unclassified failure stays informative: its provider text is visible, and the raw machine
5500
- // reason is unchanged.
5501
- name: "unclassified failure keeps a runtime notice and raw reason",
5502
- request: makeRequest2(),
5503
- nativeStream: [errored("something local broke")],
5504
- expected: [
5505
- sessionEvent2(NEW_SESSION_ID),
5506
- { type: "runtime_notice", body: "something local broke" },
5507
- { type: "result", ok: false, reason: "error:something local broke" }
5508
- ]
5509
- },
5510
- {
5511
- name: "runtime notice is trimmed and bounded to 200 characters",
5512
- request: makeRequest2(),
5513
- nativeStream: [errored(` ${BOUNDED_NOTICE}${"y".repeat(20)} `)],
5514
- expected: [
5515
- sessionEvent2(NEW_SESSION_ID),
5516
- { type: "runtime_notice", body: BOUNDED_NOTICE },
5517
- { type: "result", ok: false, reason: "usage_capped" }
5518
- ]
5519
- },
5520
- {
5521
- // Empty, whitespace-only and bare status-token failures still set the machine reason but add no
5522
- // empty or unhelpful transcript notice.
5523
- name: "empty failure text emits no runtime notice",
5524
- request: makeRequest2(),
5525
- nativeStream: [errored("")],
5526
- expected: [
5527
- sessionEvent2(NEW_SESSION_ID),
5528
- { type: "result", ok: false, reason: "error:unknown" }
5529
- ]
5530
- },
5531
- {
5532
- name: "whitespace-only failure text emits no runtime notice",
5533
- request: makeRequest2(),
5534
- nativeStream: [errored(" ")],
5535
- expected: [sessionEvent2(NEW_SESSION_ID), { type: "result", ok: false, reason: "error: " }]
5536
- },
5537
- {
5538
- name: "bare status token emits no runtime notice",
5539
- request: makeRequest2(),
5540
- nativeStream: [errored("429")],
5541
- expected: [sessionEvent2(NEW_SESSION_ID), { type: "result", ok: false, reason: "rate_limited" }]
5542
- },
5543
- {
5544
- name: "single-word provider sentence emits a runtime notice",
5545
- request: makeRequest2(),
5546
- nativeStream: [errored("Unavailable.")],
5547
- expected: [
5548
- sessionEvent2(NEW_SESSION_ID),
5549
- { type: "runtime_notice", body: "Unavailable." },
5550
- { type: "result", ok: false, reason: "error:Unavailable." }
5551
- ]
5552
- },
5553
- {
5554
- // The zero-content success the claude-code suite pins, in opencode's vocabulary — the session
5555
- // goes idle having emitted no part. No opencode sample has been seen; the fixture exists because
5556
- // the host's classification is runtime-neutral, so a runtime that can produce this envelope must
5557
- // be shown producing it in the shape the classifier reads.
5558
- name: "zero-content success (empty runtime result)",
5559
- request: makeRequest2(),
5560
- nativeStream: [idle()],
5561
- expected: [sessionEvent2(NEW_SESSION_ID), { type: "result", ok: true }]
5562
- },
5563
- {
5564
- // Empty-final: a clean turn that ended on a tool call with no closing text.
5565
- // The adapter emits NO final text — empty-final promotion is host/pump
5566
- // territory (this fixture pins that boundary).
5567
- name: "empty-final (no closing text)",
5568
- request: makeRequest2(),
5569
- nativeStream: [
5570
- toolPart("t1", "write", { status: "running", input: { path: "a.md" } }, "call_1"),
5571
- toolPart("t1", "write", { status: "completed", input: { path: "a.md" } }, "call_1"),
5572
- idle()
5573
- ],
5574
- expected: [
5575
- sessionEvent2(NEW_SESSION_ID),
5576
- {
5577
- type: "tool",
5578
- id: "call_1",
5579
- name: "write",
5580
- phase: "start",
5581
- summary: "a.md",
5582
- input: { path: "a.md" }
5583
- },
5584
- {
5585
- type: "tool",
5586
- id: "call_1",
5587
- name: "write",
5588
- phase: "done",
5589
- summary: "a.md",
5590
- input: { path: "a.md" },
5591
- result: void 0
5592
- },
5593
- { type: "result", ok: true }
5594
- ]
5595
- },
5596
- {
5597
- // Thinking: a reasoning part seals as a transient `thinking` when the reply
5598
- // text supersedes it; the text is the final.
5599
- name: "thinking then reply",
5600
- request: makeRequest2(),
5601
- nativeStream: [
5602
- reasoningPart("r1", "Let me check the notes."),
5603
- textPart("p1", "All set."),
5604
- idle()
5605
- ],
5606
- expected: [
5607
- sessionEvent2(NEW_SESSION_ID),
5608
- { type: "thinking", text: "Let me check the notes." },
5609
- { type: "text", body: "All set.", terminal: true },
5610
- { type: "result", ok: true }
5611
- ]
5612
- },
5613
- {
5614
- // The stored state matches the directory, so the adapter resumes its id and the transport reports
5615
- // `created:false` on the same id — no session event fires. Resume is what carries cross-turn
5616
- // memory.
5617
- name: "session resume (no re-emit on echo)",
5618
- request: makeRequest2({ session: encodeSession2({ sessionId: "s1", directory: DIR }) }),
5619
- nativeStream: [textPart("p1", "Back again."), idle()],
5620
- expected: [
5621
- { type: "text", body: "Back again.", terminal: true },
5622
- { type: "result", ok: true }
5623
- ]
5624
- },
5625
- {
5626
- // On resume, as on a fresh turn, the decoder learns the user-message id from the stream:
5627
- // `message.updated` user frame precedes the echoed part — and drops the echo, so the injected
5628
- // per-turn context can't leak into Steps. Only the reply surfaces; no session event.
5629
- name: "session resume \u2014 prompt echo filtered via stream-derived user id",
5630
- request: makeRequest2({ session: encodeSession2({ sessionId: "s1", directory: DIR }) }),
5631
- nativeStream: [
5632
- messageUpdated("user", "msg_u1"),
5633
- echoedUserPart("up1", "msg_u1", "INJECTED CONTEXT \u2014 must not leak"),
5634
- messageUpdated("assistant", "msg_a1"),
5635
- echoedUserPart("ap1", "msg_a1", "The real reply."),
5636
- idle()
5637
- ],
5638
- expected: [
5639
- { type: "text", body: "The real reply.", terminal: true },
5640
- { type: "result", ok: true }
5641
- ]
5642
- },
5643
- {
5644
- // Directory-mismatch degrade: the stored state was created under another directory, so the
5645
- // adapter starts fresh — the transport creates a new session, and the session event records the
5646
- // current directory.
5647
- name: "session directory-mismatch degrades to fresh",
5648
- request: makeRequest2({ session: encodeSession2({ sessionId: "old", directory: "/env/gone" }) }),
5649
- nativeStream: [textPart("p1", "Fresh start."), idle()],
5650
- expected: [
5651
- // The degrade is flagged on the session event so the host relays it on settle and the server
5652
- // rewinds the catch-up mark, recovering context the discarded session dropped.
5653
- sessionEvent2(NEW_SESSION_ID, DIR, true),
5654
- { type: "text", body: "Fresh start.", terminal: true },
5655
- { type: "result", ok: true }
5656
- ]
5657
- }
5658
- ];
4391
+ }
4392
+ let degraded = "fresh" in decision && decision.reason !== void 0 && decision.reason !== "no_session";
4393
+ if ("fresh" in decision && decision.reason !== void 0 && decision.reason !== "no_session" && hooks?.fresh) {
4394
+ const recomposed = await hooks.fresh(decision.reason);
4395
+ if (signal.aborted) return;
4396
+ if (recomposed) {
4397
+ req = { ...req, ...recomposed, session: null };
4398
+ degraded = false;
4399
+ }
4400
+ }
4401
+ const spec = buildRunSpec(req, resume);
4402
+ const result = await transport.run(spec, signal);
4403
+ if (signal.aborted) return;
4404
+ if (result.created || result.sessionId !== resume) {
4405
+ yield {
4406
+ type: "session",
4407
+ state: encodeSession2({ sessionId: result.sessionId, directory: result.directory }),
4408
+ ...degraded ? { degraded: true } : {}
4409
+ };
4410
+ }
4411
+ yield* decodeOpencodeStream(result.events, {
4412
+ signal,
4413
+ sessionId: result.sessionId,
4414
+ // The keys the turn's servers were installed under: what the decoder
4415
+ // splits a raw `<server>_<tool>` id on.
4416
+ mcpServers: Object.keys(spec.mcp)
4417
+ });
4418
+ }
4419
+ };
4420
+ }
4421
+ var opencodeAdapter = createOpencodeAdapter();
5659
4422
 
5660
4423
  // packages/agent-runtime/src/codex/addendum.ts
5661
4424
  var CODEX_ADDENDUM = [
@@ -6068,13 +4831,13 @@ function sealHeld2(held, terminal) {
6068
4831
  }
6069
4832
 
6070
4833
  // packages/agent-runtime/src/codex/policy.ts
6071
- import { z as z12 } from "zod";
4834
+ import { z as z11 } from "zod";
6072
4835
  function codexSandboxMode(local) {
6073
4836
  return local.codex?.configSetsSandboxMode ? null : "danger-full-access";
6074
4837
  }
6075
4838
  var CODEX_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max", "ultra"];
6076
- var codexDialectSchema = z12.object({
6077
- modelReasoningEffort: z12.enum(CODEX_REASONING_EFFORTS).optional()
4839
+ var codexDialectSchema = z11.object({
4840
+ modelReasoningEffort: z11.enum(CODEX_REASONING_EFFORTS).optional()
6078
4841
  }).loose();
6079
4842
  function readCodexDialect(runtimeOptions) {
6080
4843
  const parsed = codexDialectSchema.safeParse(runtimeOptions?.["codex"] ?? {});
@@ -6377,7 +5140,7 @@ function createCodexAdapter(deps = {}) {
6377
5140
  let threadUsageBaseline = "resume" in decision ? decision.usageBaseline : null;
6378
5141
  if ("fresh" in decision && decision.reason === "cwd_mismatch") {
6379
5142
  deps.onWarn?.(
6380
- "codex adapter: stored session directory no longer matches the current environment \u2014 starting a fresh thread (SJ527-analogue guard)",
5143
+ "codex adapter: stored session directory no longer matches the current environment \u2014 starting a fresh thread",
6381
5144
  { directory }
6382
5145
  );
6383
5146
  }
@@ -6422,7 +5185,7 @@ function createCodexAdapter(deps = {}) {
6422
5185
  if (imageUrls.length > 0) {
6423
5186
  if (!deps.prepareImages) {
6424
5187
  deps.onWarn?.(
6425
- "codex adapter: no image preparer wired on this host \u2014 the turn runs without its images (CT1757)",
5188
+ "codex adapter: no image preparer wired on this host \u2014 the turn runs without its images",
6426
5189
  { images: imageUrls.length }
6427
5190
  );
6428
5191
  } else {
@@ -6431,7 +5194,7 @@ function createCodexAdapter(deps = {}) {
6431
5194
  } catch (err) {
6432
5195
  if (!signal.aborted) {
6433
5196
  deps.onWarn?.(
6434
- "codex adapter: preparing the images failed \u2014 the turn runs without them (CT1757)",
5197
+ "codex adapter: preparing the images failed \u2014 the turn runs without them",
6435
5198
  { err: err instanceof Error ? err.message : String(err) }
6436
5199
  );
6437
5200
  }
@@ -6507,7 +5270,7 @@ function createCodexAdapter(deps = {}) {
6507
5270
  try {
6508
5271
  await images.cleanup();
6509
5272
  } catch (err) {
6510
- deps.onWarn?.("codex adapter: image cleanup failed (CT1757)", {
5273
+ deps.onWarn?.("codex adapter: image cleanup failed", {
6511
5274
  err: err instanceof Error ? err.message : String(err)
6512
5275
  });
6513
5276
  }
@@ -6518,993 +5281,6 @@ function createCodexAdapter(deps = {}) {
6518
5281
  }
6519
5282
  var codexAdapter = createCodexAdapter();
6520
5283
 
6521
- // packages/agent-runtime/src/codex/conformance.ts
6522
- var ABORT_SENTINEL3 = { __abortHere: true };
6523
- var NEW_THREAD_ID = "th_new";
6524
- var BOUNDED_NOTICE2 = "You've hit your usage limit \xB7 ".padEnd(200, "x");
6525
- var DIR2 = "/env/here";
6526
- var PROMPT = "system";
6527
- function makeRequest3(overrides2 = {}) {
6528
- return {
6529
- systemPrompt: PROMPT,
6530
- prompt: "hi there",
6531
- content: [{ type: "text", text: "hi there" }],
6532
- // The resolved model and reasoning effort ride the terminal `result` off the run spec's model.
6533
- // Behavioural fixtures use the model-less token (no `resolvedModel` emitted); a dedicated capture
6534
- // fixture sets a real model and effort.
6535
- config: { model: null },
6536
- session: null,
6537
- cabane: { mcpUrl: "https://cabane.test/api/mcp", bearer: "tok", activeConversationId: "conv" },
6538
- local: { cwd: DIR2 },
6539
- extra: { mcpServers: {} },
6540
- ...overrides2
6541
- };
6542
- }
6543
- var threadStarted = (threadId) => ({
6544
- type: "thread.started",
6545
- thread_id: threadId
6546
- });
6547
- var agentMessage = (id, text) => ({
6548
- type: "item.completed",
6549
- item: { id, type: "agent_message", text }
6550
- });
6551
- var reasoning = (id, text) => ({
6552
- type: "item.completed",
6553
- item: { id, type: "reasoning", text }
6554
- });
6555
- var toolFrame = (evType, item) => ({ type: evType, item });
6556
- var turnCompleted = (usage) => ({
6557
- type: "turn.completed",
6558
- ...usage ? { usage } : {}
6559
- });
6560
- var turnFailed = (message) => ({
6561
- type: "turn.failed",
6562
- error: { message }
6563
- });
6564
- var fatalError = (message) => ({ type: "error", message });
6565
- var errorItem = (id, message) => ({
6566
- type: "item.completed",
6567
- item: { id, type: "error", message }
6568
- });
6569
- var OP_SUCCESS = { ok: true, result: { source: "native" } };
6570
- var OP_FAILURE = {
6571
- ok: false,
6572
- error: {
6573
- kind: "output_too_large",
6574
- code: "output_too_large",
6575
- message: "files.read returned more than the result ceiling"
6576
- }
6577
- };
6578
- var startedSession = (threadId, cwd = DIR2, degraded = false) => ({
6579
- type: "session",
6580
- state: encodeSession3({ threadId, cwd, promptFingerprint: null, usageBaseline: null }),
6581
- ...degraded ? { degraded: true } : {}
6582
- });
6583
- var closingSession = (threadId, usageBaseline = null) => ({
6584
- type: "session",
6585
- state: encodeSession3({
6586
- threadId,
6587
- cwd: DIR2,
6588
- promptFingerprint: fingerprintPrompt(PROMPT),
6589
- usageBaseline,
6590
- compactions: 0
6591
- })
6592
- });
6593
- var CODEX_CONFORMANCE_FIXTURES = [
6594
- {
6595
- // A plain text reply: the held agent message seals as the terminal final at `turn.completed`;
6596
- // usage rides the `result`.
6597
- //
6598
- // The counters nest — `cached_input_tokens` is a slice of `input_tokens`,
6599
- // `reasoning_output_tokens` a slice of `output_tokens` — so input is 100 (not 120) and output 50
6600
- // (not 60), with the cache read broken out beside them.
6601
- name: "clean turn",
6602
- request: makeRequest3(),
6603
- nativeStream: [
6604
- threadStarted(NEW_THREAD_ID),
6605
- agentMessage("m1", "Hello there."),
6606
- turnCompleted({
6607
- input_tokens: 100,
6608
- cached_input_tokens: 20,
6609
- cache_write_input_tokens: 5,
6610
- output_tokens: 50,
6611
- reasoning_output_tokens: 10
6612
- })
6613
- ],
6614
- expected: [
6615
- startedSession(NEW_THREAD_ID),
6616
- { type: "text", body: "Hello there.", terminal: true },
6617
- // The closing session write — the thread's ending totals, recorded as the baseline the next turn
6618
- // subtracts. A fresh thread, so the `result` usage below is the frame verbatim.
6619
- closingSession(NEW_THREAD_ID, {
6620
- inputTokens: 100,
6621
- outputTokens: 50,
6622
- cacheReadTokens: 20,
6623
- cacheCreationTokens: 5
6624
- }),
6625
- {
6626
- type: "result",
6627
- ok: true,
6628
- usage: {
6629
- inputTokens: 100,
6630
- outputTokens: 50,
6631
- cacheReadTokens: 20,
6632
- cacheCreationTokens: 5
6633
- }
6634
- }
6635
- ]
6636
- },
6637
- {
6638
- // Narration + an MCP tool call + result + closing reply. The narration message
6639
- // is sealed as interim (a tool follows it); the trailing message is the final.
6640
- name: "tool turn",
6641
- request: makeRequest3(),
6642
- nativeStream: [
6643
- threadStarted(NEW_THREAD_ID),
6644
- agentMessage("m1", "Reading your notes."),
6645
- toolFrame("item.started", {
6646
- id: "t1",
6647
- type: "mcp_tool_call",
6648
- server: "cabane",
6649
- tool: "read",
6650
- arguments: { path: "sleep.md" },
6651
- status: "in_progress"
6652
- }),
6653
- toolFrame("item.completed", {
6654
- id: "t1",
6655
- type: "mcp_tool_call",
6656
- server: "cabane",
6657
- tool: "read",
6658
- arguments: { path: "sleep.md" },
6659
- status: "completed",
6660
- result: { content: [{ type: "text", text: "sleep early" }] }
6661
- }),
6662
- agentMessage("m2", "Here's the summary."),
6663
- turnCompleted()
6664
- ],
6665
- expected: [
6666
- startedSession(NEW_THREAD_ID),
6667
- { type: "text", body: "Reading your notes.", terminal: false },
6668
- {
6669
- type: "tool",
6670
- id: "t1",
6671
- name: "read",
6672
- mcpServer: "cabane",
6673
- phase: "start",
6674
- summary: "sleep.md",
6675
- input: { path: "sleep.md" }
6676
- },
6677
- {
6678
- type: "tool",
6679
- id: "t1",
6680
- name: "read",
6681
- mcpServer: "cabane",
6682
- phase: "done",
6683
- summary: "sleep.md",
6684
- input: { path: "sleep.md" },
6685
- result: [{ type: "text", text: "sleep early" }]
6686
- },
6687
- { type: "text", body: "Here's the summary.", terminal: true },
6688
- closingSession(NEW_THREAD_ID),
6689
- { type: "result", ok: true }
6690
- ]
6691
- },
6692
- {
6693
- // The recorded MCP success shape: Codex exposes the text copy and the native object together;
6694
- // normalization must keep the object.
6695
- name: "MCP success prefers native structured content",
6696
- request: makeRequest3(),
6697
- nativeStream: [
6698
- threadStarted(NEW_THREAD_ID),
6699
- toolFrame("item.started", {
6700
- id: "query-success",
6701
- type: "mcp_tool_call",
6702
- server: "cabane",
6703
- tool: "query",
6704
- arguments: { op: "files.read", args: { path: "/ABOUT.md" } },
6705
- status: "in_progress"
6706
- }),
6707
- toolFrame("item.completed", {
6708
- id: "query-success",
6709
- type: "mcp_tool_call",
6710
- server: "cabane",
6711
- tool: "query",
6712
- arguments: { op: "files.read", args: { path: "/ABOUT.md" } },
6713
- status: "completed",
6714
- result: {
6715
- content: [{ type: "text", text: JSON.stringify(OP_SUCCESS) }],
6716
- structured_content: OP_SUCCESS
6717
- }
6718
- }),
6719
- turnCompleted()
6720
- ],
6721
- expected: [
6722
- startedSession(NEW_THREAD_ID),
6723
- {
6724
- type: "tool",
6725
- id: "query-success",
6726
- name: "query",
6727
- mcpServer: "cabane",
6728
- phase: "start",
6729
- summary: "",
6730
- input: { op: "files.read", args: { path: "/ABOUT.md" } }
6731
- },
6732
- {
6733
- type: "tool",
6734
- id: "query-success",
6735
- name: "query",
6736
- mcpServer: "cabane",
6737
- phase: "done",
6738
- summary: "",
6739
- input: { op: "files.read", args: { path: "/ABOUT.md" } },
6740
- result: OP_SUCCESS
6741
- },
6742
- closingSession(NEW_THREAD_ID),
6743
- { type: "result", ok: true }
6744
- ]
6745
- },
6746
- {
6747
- // MCP `isError` keeps the failed phase, while the raw camel-case structured payload survives
6748
- // instead of the generic `error` token.
6749
- name: "failed MCP call preserves structured receipt while remaining error phase",
6750
- request: makeRequest3(),
6751
- nativeStream: [
6752
- threadStarted(NEW_THREAD_ID),
6753
- toolFrame("item.started", {
6754
- id: "query-failed",
6755
- type: "mcp_tool_call",
6756
- server: "cabane",
6757
- tool: "query",
6758
- arguments: { op: "files.read", args: { path: "/ABOUT.md" } },
6759
- status: "in_progress"
6760
- }),
6761
- toolFrame("item.completed", {
6762
- id: "query-failed",
6763
- type: "mcp_tool_call",
6764
- server: "cabane",
6765
- tool: "query",
6766
- arguments: { op: "files.read", args: { path: "/ABOUT.md" } },
6767
- status: "failed",
6768
- result: {
6769
- content: [{ type: "text", text: JSON.stringify(OP_FAILURE) }],
6770
- structuredContent: OP_FAILURE,
6771
- isError: true
6772
- },
6773
- error: { message: "error" }
6774
- }),
6775
- turnCompleted()
6776
- ],
6777
- expected: [
6778
- startedSession(NEW_THREAD_ID),
6779
- {
6780
- type: "tool",
6781
- id: "query-failed",
6782
- name: "query",
6783
- mcpServer: "cabane",
6784
- phase: "start",
6785
- summary: "",
6786
- input: { op: "files.read", args: { path: "/ABOUT.md" } }
6787
- },
6788
- {
6789
- type: "tool",
6790
- id: "query-failed",
6791
- name: "query",
6792
- mcpServer: "cabane",
6793
- phase: "error",
6794
- summary: "",
6795
- input: { op: "files.read", args: { path: "/ABOUT.md" } },
6796
- result: OP_FAILURE
6797
- },
6798
- closingSession(NEW_THREAD_ID),
6799
- { type: "result", ok: true }
6800
- ]
6801
- },
6802
- {
6803
- // The turn-control server's `answer` (CT1973): the tool event carries the server Codex names
6804
- // beside the tool, which is what the pump binds the declaration to. A non-cabane server keeps
6805
- // its prefix on the card as before.
6806
- name: "turn-control answer carries its server",
6807
- request: makeRequest3(),
6808
- nativeStream: [
6809
- threadStarted(NEW_THREAD_ID),
6810
- agentMessage("m1", "On it."),
6811
- toolFrame("item.completed", {
6812
- id: "answer-1",
6813
- type: "mcp_tool_call",
6814
- server: "cabane_companion",
6815
- tool: "answer",
6816
- arguments: {},
6817
- status: "completed",
6818
- result: { content: [{ type: "text", text: '{"ok":true,"kind":"answer"}' }] }
6819
- }),
6820
- agentMessage("m2", "The answer."),
6821
- turnCompleted()
6822
- ],
6823
- expected: [
6824
- startedSession(NEW_THREAD_ID),
6825
- { type: "text", body: "On it.", terminal: false },
6826
- {
6827
- type: "tool",
6828
- id: "answer-1",
6829
- name: "cabane_companion.answer",
6830
- mcpServer: "cabane_companion",
6831
- phase: "start",
6832
- summary: "",
6833
- input: {}
6834
- },
6835
- {
6836
- type: "tool",
6837
- id: "answer-1",
6838
- name: "cabane_companion.answer",
6839
- mcpServer: "cabane_companion",
6840
- phase: "done",
6841
- summary: "",
6842
- input: {},
6843
- result: [{ type: "text", text: '{"ok":true,"kind":"answer"}' }]
6844
- },
6845
- { type: "text", body: "The answer.", terminal: true },
6846
- closingSession(NEW_THREAD_ID),
6847
- { type: "result", ok: true }
6848
- ]
6849
- },
6850
- {
6851
- // A transport/tool failure with no MCP result payload still uses the real adapter error message,
6852
- // never `undefined`.
6853
- name: "failed MCP call without result keeps error message fallback",
6854
- request: makeRequest3(),
6855
- nativeStream: [
6856
- threadStarted(NEW_THREAD_ID),
6857
- toolFrame("item.completed", {
6858
- id: "mcp-failed",
6859
- type: "mcp_tool_call",
6860
- server: "other",
6861
- tool: "lookup",
6862
- arguments: { q: "receipt" },
6863
- status: "failed",
6864
- result: { content: [], isError: true },
6865
- error: { message: "connection closed" }
6866
- }),
6867
- turnCompleted()
6868
- ],
6869
- expected: [
6870
- startedSession(NEW_THREAD_ID),
6871
- {
6872
- type: "tool",
6873
- id: "mcp-failed",
6874
- name: "other.lookup",
6875
- mcpServer: "other",
6876
- phase: "start",
6877
- summary: "receipt",
6878
- input: { q: "receipt" }
6879
- },
6880
- {
6881
- type: "tool",
6882
- id: "mcp-failed",
6883
- name: "other.lookup",
6884
- mcpServer: "other",
6885
- phase: "error",
6886
- summary: "receipt",
6887
- input: { q: "receipt" },
6888
- result: "connection closed"
6889
- },
6890
- closingSession(NEW_THREAD_ID),
6891
- { type: "result", ok: true }
6892
- ]
6893
- },
6894
- {
6895
- // A connector's structured metadata beside an image-only MCP response: no image/base64 payload
6896
- // enters activity receipts.
6897
- name: "image-only MCP result remains empty despite structured metadata",
6898
- request: makeRequest3(),
6899
- nativeStream: [
6900
- threadStarted(NEW_THREAD_ID),
6901
- toolFrame("item.completed", {
6902
- id: "mcp-image",
6903
- type: "mcp_tool_call",
6904
- server: "other",
6905
- tool: "render",
6906
- arguments: {},
6907
- status: "completed",
6908
- result: {
6909
- content: [{ type: "image", data: "base64-bytes", mimeType: "image/png" }],
6910
- structured_content: { image: "base64-bytes" }
6911
- }
6912
- }),
6913
- turnCompleted()
6914
- ],
6915
- expected: [
6916
- startedSession(NEW_THREAD_ID),
6917
- {
6918
- type: "tool",
6919
- id: "mcp-image",
6920
- name: "other.render",
6921
- mcpServer: "other",
6922
- phase: "start",
6923
- summary: "",
6924
- input: {}
6925
- },
6926
- {
6927
- type: "tool",
6928
- id: "mcp-image",
6929
- name: "other.render",
6930
- mcpServer: "other",
6931
- phase: "done",
6932
- summary: "",
6933
- input: {},
6934
- result: [{ type: "image", data: "base64-bytes", mimeType: "image/png" }]
6935
- },
6936
- closingSession(NEW_THREAD_ID),
6937
- { type: "result", ok: true }
6938
- ]
6939
- },
6940
- {
6941
- // A streaming tool whose args arrive on a later snapshot: `item.started` carries
6942
- // empty args, `item.updated` fills them, `item.completed` finishes. The `start`
6943
- // fires off the first (empty) frame — that's the live card — but the `done` must
6944
- // carry the LATEST non-empty input + its recomputed summary, not the first `{}`.
6945
- name: "streaming tool input \u2014 late args refresh the done event",
6946
- request: makeRequest3(),
6947
- nativeStream: [
6948
- threadStarted(NEW_THREAD_ID),
6949
- toolFrame("item.started", {
6950
- id: "t1",
6951
- type: "mcp_tool_call",
6952
- server: "cabane",
6953
- tool: "list",
6954
- arguments: {},
6955
- status: "in_progress"
6956
- }),
6957
- toolFrame("item.updated", {
6958
- id: "t1",
6959
- type: "mcp_tool_call",
6960
- server: "cabane",
6961
- tool: "list",
6962
- arguments: { path: "/product/tasks" },
6963
- status: "in_progress"
6964
- }),
6965
- toolFrame("item.completed", {
6966
- id: "t1",
6967
- type: "mcp_tool_call",
6968
- server: "cabane",
6969
- tool: "list",
6970
- arguments: { path: "/product/tasks" },
6971
- status: "completed",
6972
- result: { content: [{ type: "text", text: "CT481.md" }] }
6973
- }),
6974
- turnCompleted()
6975
- ],
6976
- expected: [
6977
- startedSession(NEW_THREAD_ID),
6978
- {
6979
- type: "tool",
6980
- id: "t1",
6981
- name: "list",
6982
- mcpServer: "cabane",
6983
- phase: "start",
6984
- summary: "",
6985
- input: {}
6986
- },
6987
- {
6988
- type: "tool",
6989
- id: "t1",
6990
- name: "list",
6991
- mcpServer: "cabane",
6992
- phase: "done",
6993
- summary: "/product/tasks",
6994
- input: { path: "/product/tasks" },
6995
- result: [{ type: "text", text: "CT481.md" }]
6996
- },
6997
- closingSession(NEW_THREAD_ID),
6998
- { type: "result", ok: true }
6999
- ]
7000
- },
7001
- {
7002
- // web_search lifecycle. A `web_search` item carries no `status` — completion is signalled by the
7003
- // frame type (`item.started` → `item.completed`). The `start` fires off the first frame (empty
7004
- // query); the `done` must resolve off `item.completed` with the populated query, not stay
7005
- // "running".
7006
- name: "web_search lifecycle \u2014 completes off frame type, populated query on done",
7007
- request: makeRequest3(),
7008
- nativeStream: [
7009
- threadStarted(NEW_THREAD_ID),
7010
- toolFrame("item.started", { id: "ws1", type: "web_search", query: "" }),
7011
- toolFrame("item.completed", { id: "ws1", type: "web_search", query: "best pizza in nyc" }),
7012
- turnCompleted()
7013
- ],
7014
- expected: [
7015
- startedSession(NEW_THREAD_ID),
7016
- {
7017
- type: "tool",
7018
- id: "ws1",
7019
- name: "web_search",
7020
- phase: "start",
7021
- summary: "",
7022
- input: { query: "" }
7023
- },
7024
- {
7025
- type: "tool",
7026
- id: "ws1",
7027
- name: "web_search",
7028
- phase: "done",
7029
- summary: "best pizza in nyc",
7030
- input: { query: "best pizza in nyc" }
7031
- },
7032
- closingSession(NEW_THREAD_ID),
7033
- { type: "result", ok: true }
7034
- ]
7035
- },
7036
- {
7037
- // A non-text web action (`action.type: "other"`) legitimately completes with an empty query —
7038
- // Codex's own data. It must still resolve to `done`.
7039
- name: "web_search lifecycle \u2014 empty-query completion still resolves to done",
7040
- request: makeRequest3(),
7041
- nativeStream: [
7042
- threadStarted(NEW_THREAD_ID),
7043
- toolFrame("item.started", { id: "ws2", type: "web_search", query: "" }),
7044
- toolFrame("item.completed", { id: "ws2", type: "web_search", query: "" }),
7045
- turnCompleted()
7046
- ],
7047
- expected: [
7048
- startedSession(NEW_THREAD_ID),
7049
- {
7050
- type: "tool",
7051
- id: "ws2",
7052
- name: "web_search",
7053
- phase: "start",
7054
- summary: "",
7055
- input: { query: "" }
7056
- },
7057
- {
7058
- type: "tool",
7059
- id: "ws2",
7060
- name: "web_search",
7061
- phase: "done",
7062
- summary: "",
7063
- input: { query: "" }
7064
- },
7065
- closingSession(NEW_THREAD_ID),
7066
- { type: "result", ok: true }
7067
- ]
7068
- },
7069
- {
7070
- // Highest risk: cancel mid-stream. The abort lands after the tool result; the adapter stops before
7071
- // the closing reply — no final text, no `result` event (the server writes the stopped marker).
7072
- name: "cancel mid-stream",
7073
- request: makeRequest3(),
7074
- nativeStream: [
7075
- threadStarted(NEW_THREAD_ID),
7076
- agentMessage("m1", "Working on it."),
7077
- toolFrame("item.started", {
7078
- id: "t1",
7079
- type: "command_execution",
7080
- command: "ls",
7081
- status: "in_progress"
7082
- }),
7083
- toolFrame("item.completed", {
7084
- id: "t1",
7085
- type: "command_execution",
7086
- command: "ls",
7087
- aggregated_output: "ok",
7088
- exit_code: 0,
7089
- status: "completed"
7090
- }),
7091
- ABORT_SENTINEL3,
7092
- agentMessage("m2", "This reply must never be emitted."),
7093
- turnCompleted()
7094
- ],
7095
- expected: [
7096
- startedSession(NEW_THREAD_ID),
7097
- { type: "text", body: "Working on it.", terminal: false },
7098
- {
7099
- type: "tool",
7100
- id: "t1",
7101
- name: "shell",
7102
- phase: "start",
7103
- summary: "ls",
7104
- input: { command: "ls" }
7105
- },
7106
- {
7107
- type: "tool",
7108
- id: "t1",
7109
- name: "shell",
7110
- phase: "done",
7111
- summary: "ls",
7112
- input: { command: "ls" },
7113
- result: { output: "ok", exit_code: 0 }
7114
- }
7115
- ]
7116
- },
7117
- {
7118
- // A failed turn: partial narration lands as `progress` (terminal:false), and the
7119
- // `result` event carries the machine reason off `turn.failed`.
7120
- name: "turn.failed result",
7121
- request: makeRequest3(),
7122
- nativeStream: [
7123
- threadStarted(NEW_THREAD_ID),
7124
- agentMessage("m1", "Partial work."),
7125
- turnFailed("provider exploded")
7126
- ],
7127
- expected: [
7128
- startedSession(NEW_THREAD_ID),
7129
- { type: "text", body: "Partial work.", terminal: false },
7130
- { type: "runtime_notice", body: "provider exploded" },
7131
- { type: "result", ok: false, reason: "error:provider exploded" }
7132
- ]
7133
- },
7134
- {
7135
- // A fatal top-level `error` frame (distinct from `turn.failed`): held narration
7136
- // seals as interim, then a failed `result`.
7137
- name: "fatal stream error",
7138
- request: makeRequest3(),
7139
- nativeStream: [
7140
- threadStarted(NEW_THREAD_ID),
7141
- agentMessage("m1", "Starting."),
7142
- fatalError("stream died")
7143
- ],
7144
- expected: [
7145
- startedSession(NEW_THREAD_ID),
7146
- { type: "text", body: "Starting.", terminal: false },
7147
- { type: "runtime_notice", body: "stream died" },
7148
- { type: "result", ok: false, reason: "error:stream died" }
7149
- ]
7150
- },
7151
- {
7152
- // The failure text goes through the shared classifier (Codex is text-only), so a provider 429 /
7153
- // outage / auth / cap failure splits into the honest taxonomy. One fixture per kind:
7154
- name: "turn.failed 429 \u2192 rate_limited (throttle)",
7155
- request: makeRequest3(),
7156
- nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("429 Too Many Requests")],
7157
- expected: [
7158
- startedSession(NEW_THREAD_ID),
7159
- { type: "runtime_notice", body: "429 Too Many Requests" },
7160
- { type: "result", ok: false, reason: "rate_limited" }
7161
- ]
7162
- },
7163
- {
7164
- name: "turn.failed overload \u2192 server_error",
7165
- request: makeRequest3(),
7166
- nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("503 Service Unavailable")],
7167
- expected: [
7168
- startedSession(NEW_THREAD_ID),
7169
- { type: "runtime_notice", body: "503 Service Unavailable" },
7170
- { type: "result", ok: false, reason: "server_error" }
7171
- ]
7172
- },
7173
- {
7174
- name: "turn.failed usage cap \u2192 usage_capped",
7175
- request: makeRequest3(),
7176
- nativeStream: [
7177
- threadStarted(NEW_THREAD_ID),
7178
- turnFailed("You've hit your session limit \xB7 resets 8:20pm (UTC)")
7179
- ],
7180
- expected: [
7181
- startedSession(NEW_THREAD_ID),
7182
- {
7183
- type: "runtime_notice",
7184
- body: "You've hit your session limit \xB7 resets 8:20pm (UTC)"
7185
- },
7186
- { type: "result", ok: false, reason: "usage_capped" }
7187
- ]
7188
- },
7189
- {
7190
- name: "turn.failed auth \u2192 auth_expired",
7191
- request: makeRequest3(),
7192
- nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("401 Unauthorized: invalid api key")],
7193
- expected: [
7194
- startedSession(NEW_THREAD_ID),
7195
- { type: "runtime_notice", body: "401 Unauthorized: invalid api key" },
7196
- { type: "result", ok: false, reason: "auth_expired" }
7197
- ]
7198
- },
7199
- {
7200
- // A failure text the classifier can't place keeps the raw `error:<text>` reason (the `unknown`
7201
- // fallback).
7202
- name: "turn.failed unclassified \u2192 raw error reason (unchanged)",
7203
- request: makeRequest3(),
7204
- nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("something local broke")],
7205
- expected: [
7206
- startedSession(NEW_THREAD_ID),
7207
- { type: "runtime_notice", body: "something local broke" },
7208
- { type: "result", ok: false, reason: "error:something local broke" }
7209
- ]
7210
- },
7211
- {
7212
- name: "runtime notice is trimmed and bounded to 200 characters",
7213
- request: makeRequest3(),
7214
- nativeStream: [
7215
- threadStarted(NEW_THREAD_ID),
7216
- turnFailed(` ${BOUNDED_NOTICE2}${"y".repeat(20)} `)
7217
- ],
7218
- expected: [
7219
- startedSession(NEW_THREAD_ID),
7220
- { type: "runtime_notice", body: BOUNDED_NOTICE2 },
7221
- { type: "result", ok: false, reason: "usage_capped" }
7222
- ]
7223
- },
7224
- {
7225
- // Empty, whitespace-only and bare status-token failures still set the machine reason but add no
7226
- // empty or unhelpful transcript notice.
7227
- name: "empty failure text emits no runtime notice",
7228
- request: makeRequest3(),
7229
- nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("")],
7230
- expected: [
7231
- startedSession(NEW_THREAD_ID),
7232
- { type: "result", ok: false, reason: "error:unknown" }
7233
- ]
7234
- },
7235
- {
7236
- name: "whitespace-only failure text emits no runtime notice",
7237
- request: makeRequest3(),
7238
- nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed(" ")],
7239
- expected: [startedSession(NEW_THREAD_ID), { type: "result", ok: false, reason: "error: " }]
7240
- },
7241
- {
7242
- name: "bare status token emits no runtime notice",
7243
- request: makeRequest3(),
7244
- nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("429")],
7245
- expected: [
7246
- startedSession(NEW_THREAD_ID),
7247
- { type: "result", ok: false, reason: "rate_limited" }
7248
- ]
7249
- },
7250
- {
7251
- name: "single-word provider sentence emits a runtime notice",
7252
- request: makeRequest3(),
7253
- nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("Unavailable.")],
7254
- expected: [
7255
- startedSession(NEW_THREAD_ID),
7256
- { type: "runtime_notice", body: "Unavailable." },
7257
- { type: "result", ok: false, reason: "error:Unavailable." }
7258
- ]
7259
- },
7260
- {
7261
- // Fail loud on unknown model metadata. An unknown `--model` makes Codex emit an `error` item
7262
- // ("… Defaulting to fallback metadata …") and misbehave (the spurious tool call scripted after it
7263
- // must never be emitted). The adapter ends the turn when the signal lands, with a machine reason.
7264
- name: "unknown model metadata \u2192 fail loud (guard)",
7265
- request: makeRequest3(),
7266
- nativeStream: [
7267
- threadStarted(NEW_THREAD_ID),
7268
- errorItem(
7269
- "e0",
7270
- "Model metadata for `gpt-5-codex` not found. Defaulting to fallback metadata; this can degrade performance and cause issues."
7271
- ),
7272
- // Everything after the guard fires must be dropped (the haywire the guard prevents).
7273
- toolFrame("item.started", {
7274
- id: "t1",
7275
- type: "web_search",
7276
- query: "anything",
7277
- status: "in_progress"
7278
- }),
7279
- agentMessage("m1", "This degraded reply must never be emitted."),
7280
- turnCompleted()
7281
- ],
7282
- expected: [
7283
- startedSession(NEW_THREAD_ID),
7284
- {
7285
- type: "result",
7286
- ok: false,
7287
- reason: "model_unavailable:Model metadata for `gpt-5-codex` not found. Defaulting to fallback metadata; this can degrade performance and cause issues."
7288
- }
7289
- ]
7290
- },
7291
- {
7292
- // A NON-metadata error item is non-fatal — it's skipped, and the turn proceeds
7293
- // to its normal reply (guard specificity: only the fallback-metadata phrase trips).
7294
- name: "non-metadata error item is skipped (non-fatal)",
7295
- request: makeRequest3(),
7296
- nativeStream: [
7297
- threadStarted(NEW_THREAD_ID),
7298
- errorItem("e0", "a transient hiccup"),
7299
- agentMessage("m1", "All good now."),
7300
- turnCompleted()
7301
- ],
7302
- expected: [
7303
- startedSession(NEW_THREAD_ID),
7304
- { type: "text", body: "All good now.", terminal: true },
7305
- closingSession(NEW_THREAD_ID),
7306
- { type: "result", ok: true }
7307
- ]
7308
- },
7309
- {
7310
- // The zero-content success the claude-code suite pins, in Codex's vocabulary — the thread
7311
- // starts, the turn completes, nothing in between. No Codex sample has been seen; the fixture
7312
- // exists because the host's classification is runtime-neutral, so a runtime that can produce
7313
- // this envelope must be shown producing it in the shape the classifier reads.
7314
- name: "zero-content success (empty runtime result)",
7315
- request: makeRequest3(),
7316
- nativeStream: [threadStarted(NEW_THREAD_ID), turnCompleted()],
7317
- expected: [
7318
- startedSession(NEW_THREAD_ID),
7319
- closingSession(NEW_THREAD_ID),
7320
- { type: "result", ok: true }
7321
- ]
7322
- },
7323
- {
7324
- // Empty-final: a clean turn that ended on a tool call with no closing message.
7325
- // The adapter emits NO final text — empty-final promotion is host/pump territory
7326
- // (this fixture pins that boundary).
7327
- name: "empty-final (no closing message)",
7328
- request: makeRequest3(),
7329
- nativeStream: [
7330
- threadStarted(NEW_THREAD_ID),
7331
- toolFrame("item.started", {
7332
- id: "t1",
7333
- type: "file_change",
7334
- changes: [{ path: "a.md", kind: "add" }],
7335
- status: "in_progress"
7336
- }),
7337
- toolFrame("item.completed", {
7338
- id: "t1",
7339
- type: "file_change",
7340
- changes: [{ path: "a.md", kind: "add" }],
7341
- status: "completed"
7342
- }),
7343
- turnCompleted()
7344
- ],
7345
- expected: [
7346
- startedSession(NEW_THREAD_ID),
7347
- {
7348
- type: "tool",
7349
- id: "t1",
7350
- name: "apply_patch",
7351
- phase: "start",
7352
- summary: "",
7353
- input: { changes: [{ path: "a.md", kind: "add" }] }
7354
- },
7355
- {
7356
- type: "tool",
7357
- id: "t1",
7358
- name: "apply_patch",
7359
- phase: "done",
7360
- summary: "",
7361
- input: { changes: [{ path: "a.md", kind: "add" }] },
7362
- result: [{ path: "a.md", kind: "add" }]
7363
- },
7364
- closingSession(NEW_THREAD_ID),
7365
- { type: "result", ok: true }
7366
- ]
7367
- },
7368
- {
7369
- // Thinking: a reasoning item seals as a transient `thinking`, then the reply
7370
- // message is the final.
7371
- name: "thinking then reply",
7372
- request: makeRequest3(),
7373
- nativeStream: [
7374
- threadStarted(NEW_THREAD_ID),
7375
- reasoning("r1", "Let me check the notes."),
7376
- agentMessage("m1", "All set."),
7377
- turnCompleted()
7378
- ],
7379
- expected: [
7380
- startedSession(NEW_THREAD_ID),
7381
- { type: "thinking", text: "Let me check the notes." },
7382
- { type: "text", body: "All set.", terminal: true },
7383
- closingSession(NEW_THREAD_ID),
7384
- { type: "result", ok: true }
7385
- ]
7386
- },
7387
- {
7388
- // Codex reports no model in-stream, so the resolved model and reasoning effort come from what the
7389
- // thread was started with (the run spec) and ride the terminal `result`: a real model and a
7390
- // `runtimeOptions.codex.modelReasoningEffort` are asserted through to `resolvedModel` (bare,
7391
- // `openai/`-stripped) and `resolvedConfig.reasoningEffort`.
7392
- name: "resolved model + reasoning effort captured from run spec",
7393
- request: makeRequest3({
7394
- config: {
7395
- model: "openai/gpt-5-codex",
7396
- runtimeOptions: { codex: { modelReasoningEffort: "high" } }
7397
- }
7398
- }),
7399
- nativeStream: [threadStarted(NEW_THREAD_ID), agentMessage("m1", "Done."), turnCompleted()],
7400
- expected: [
7401
- startedSession(NEW_THREAD_ID),
7402
- { type: "text", body: "Done.", terminal: true },
7403
- closingSession(NEW_THREAD_ID),
7404
- {
7405
- type: "result",
7406
- ok: true,
7407
- resolvedModel: "gpt-5-codex",
7408
- resolvedConfig: { reasoningEffort: "high" }
7409
- }
7410
- ]
7411
- },
7412
- {
7413
- // Session resume: the stored state matches the directory, so the thread id is resumed and
7414
- // `thread.started` echoes it — no session event fires. This is what carries cross-turn memory.
7415
- //
7416
- // The stored state also says the thread already holds the composed prompt, so this turn
7417
- // withholds the preamble and the state it would write is byte-identical: the silence proves both
7418
- // an unchanged id and a prompt that needed no re-send.
7419
- name: "session resume (no re-emit on echo)",
7420
- request: makeRequest3({
7421
- session: encodeSession3({
7422
- threadId: "th1",
7423
- cwd: DIR2,
7424
- promptFingerprint: fingerprintPrompt(PROMPT),
7425
- usageBaseline: null,
7426
- compactions: 0
7427
- })
7428
- }),
7429
- nativeStream: [threadStarted("th1"), agentMessage("m1", "Back again."), turnCompleted()],
7430
- expected: [
7431
- { type: "text", body: "Back again.", terminal: true },
7432
- { type: "result", ok: true }
7433
- ]
7434
- },
7435
- {
7436
- // A resumed thread whose stored state carries no `promptFingerprint` field. The adapter reads the
7437
- // absence as "can't say which prompt this thread holds", sends the full prompt, and re-emits the
7438
- // state with the fingerprint — on the same thread id. That re-emit is the upgrade path: without
7439
- // it a long-lived thread would re-send the whole prompt forever.
7440
- //
7441
- // This fixture fails if the standing half is withheld on a thread we have no evidence about.
7442
- name: "session resume with a pre-CT1075 state re-emits with the prompt fingerprint",
7443
- request: makeRequest3({
7444
- session: JSON.stringify({ threadId: "th1", cwd: DIR2 })
7445
- }),
7446
- nativeStream: [threadStarted("th1"), agentMessage("m1", "Back again."), turnCompleted()],
7447
- expected: [
7448
- startedSession("th1"),
7449
- { type: "text", body: "Back again.", terminal: true },
7450
- closingSession("th1"),
7451
- { type: "result", ok: true }
7452
- ]
7453
- },
7454
- {
7455
- // The thread carries a different prompt than this turn composed — a charter edit, a renamed
7456
- // agent, a redeployed prompt body. The fingerprint doesn't match, so the new prompt is sent as on
7457
- // a fresh thread and the state re-recorded.
7458
- //
7459
- // This fixture fails if a stale prompt is treated as good enough; a boolean flag would pass the
7460
- // resume fixture above and withhold every future charter edit.
7461
- name: "session resume with a stale prompt fingerprint re-sends the prompt",
7462
- request: makeRequest3({
7463
- session: encodeSession3({
7464
- threadId: "th1",
7465
- cwd: DIR2,
7466
- promptFingerprint: fingerprintPrompt("an older composed prompt"),
7467
- usageBaseline: null
7468
- })
7469
- }),
7470
- nativeStream: [threadStarted("th1"), agentMessage("m1", "Back again."), turnCompleted()],
7471
- expected: [
7472
- startedSession("th1"),
7473
- { type: "text", body: "Back again.", terminal: true },
7474
- closingSession("th1"),
7475
- { type: "result", ok: true }
7476
- ]
7477
- },
7478
- {
7479
- // Directory-mismatch degrade: the stored state was created under another directory, so the
7480
- // adapter starts fresh — `thread.started` reports a new id, and the session event records the
7481
- // current directory with `degraded: true` so the host rewinds the catch-up mark.
7482
- //
7483
- // The refused resume means the new thread is empty, so the full prompt goes in even though the
7484
- // stored state vouched for a copy in a thread we're no longer in.
7485
- name: "session directory-mismatch degrades to fresh",
7486
- request: makeRequest3({
7487
- session: encodeSession3({
7488
- threadId: "old",
7489
- cwd: "/env/gone",
7490
- promptFingerprint: fingerprintPrompt(PROMPT),
7491
- usageBaseline: null
7492
- })
7493
- }),
7494
- nativeStream: [
7495
- threadStarted(NEW_THREAD_ID),
7496
- agentMessage("m1", "Fresh start."),
7497
- turnCompleted()
7498
- ],
7499
- expected: [
7500
- startedSession(NEW_THREAD_ID, DIR2, true),
7501
- { type: "text", body: "Fresh start.", terminal: true },
7502
- closingSession(NEW_THREAD_ID),
7503
- { type: "result", ok: true }
7504
- ]
7505
- }
7506
- ];
7507
-
7508
5284
  // packages/agent-runtime/src/claude-code/sdk.ts
7509
5285
  import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
7510
5286
 
@@ -7644,47 +5420,47 @@ function classifyStopError(message) {
7644
5420
  }
7645
5421
 
7646
5422
  // src/runner/protocol.ts
7647
- import { z as z14 } from "zod";
5423
+ import { z as z13 } from "zod";
7648
5424
 
7649
5425
  // src/run-config.ts
7650
- import { z as z13 } from "zod";
7651
- var mcpStdioServerSchema = z13.object({
7652
- type: z13.literal("stdio").optional(),
7653
- command: z13.string().min(1),
7654
- args: z13.array(z13.string()).optional(),
7655
- env: z13.record(z13.string(), z13.string()).optional(),
5426
+ import { z as z12 } from "zod";
5427
+ var mcpStdioServerSchema = z12.object({
5428
+ type: z12.literal("stdio").optional(),
5429
+ command: z12.string().min(1),
5430
+ args: z12.array(z12.string()).optional(),
5431
+ env: z12.record(z12.string(), z12.string()).optional(),
7656
5432
  // Variable → the name of a `secretEnv` entry holding its value. Set by the server on a
7657
5433
  // room's shared browser, whose connect URL is a credential.
7658
- envFromEnv: z13.record(z13.string(), z13.string()).optional()
5434
+ envFromEnv: z12.record(z12.string(), z12.string()).optional()
7659
5435
  });
7660
- var mcpHttpServerSchema = z13.object({
7661
- type: z13.literal("http"),
7662
- url: z13.string().url(),
7663
- headers: z13.record(z13.string(), z13.string()).optional(),
5436
+ var mcpHttpServerSchema = z12.object({
5437
+ type: z12.literal("http"),
5438
+ url: z12.string().url(),
5439
+ headers: z12.record(z12.string(), z12.string()).optional(),
7664
5440
  // Header → the name of a `secretEnv` entry holding its value. Set by
7665
5441
  // the server on a room connection's entry, and by `resolveMcpSecrets` on a
7666
5442
  // header whose `${VAR}` it resolved.
7667
- headersFromEnv: z13.record(z13.string(), z13.string()).optional()
5443
+ headersFromEnv: z12.record(z12.string(), z12.string()).optional()
7668
5444
  });
7669
- var mcpSseServerSchema = z13.object({
7670
- type: z13.literal("sse"),
7671
- url: z13.string().url(),
7672
- headers: z13.record(z13.string(), z13.string()).optional(),
5445
+ var mcpSseServerSchema = z12.object({
5446
+ type: z12.literal("sse"),
5447
+ url: z12.string().url(),
5448
+ headers: z12.record(z12.string(), z12.string()).optional(),
7673
5449
  // Header → the name of a `secretEnv` entry holding its value. Set by
7674
5450
  // the server on a room connection's entry, and by `resolveMcpSecrets` on a
7675
5451
  // header whose `${VAR}` it resolved.
7676
- headersFromEnv: z13.record(z13.string(), z13.string()).optional()
5452
+ headersFromEnv: z12.record(z12.string(), z12.string()).optional()
7677
5453
  });
7678
- var mcpServerDefSchema = z13.union([mcpHttpServerSchema, mcpSseServerSchema, mcpStdioServerSchema]);
7679
- var thinkingConfigSchema = z13.discriminatedUnion("type", [
7680
- z13.object({ type: z13.literal("adaptive") }),
7681
- z13.object({ type: z13.literal("enabled"), budgetTokens: z13.number().int().positive().optional() }),
7682
- z13.object({ type: z13.literal("disabled") })
5454
+ var mcpServerDefSchema = z12.union([mcpHttpServerSchema, mcpSseServerSchema, mcpStdioServerSchema]);
5455
+ var thinkingConfigSchema = z12.discriminatedUnion("type", [
5456
+ z12.object({ type: z12.literal("adaptive") }),
5457
+ z12.object({ type: z12.literal("enabled"), budgetTokens: z12.number().int().positive().optional() }),
5458
+ z12.object({ type: z12.literal("disabled") })
7683
5459
  ]);
7684
- var effortSchema = z13.enum(["low", "medium", "high", "xhigh", "max"]);
7685
- var runConfigSchema = z13.object({
7686
- mcpServers: z13.record(z13.string(), mcpServerDefSchema).optional(),
7687
- model: z13.string().min(1).optional(),
5460
+ var effortSchema = z12.enum(["low", "medium", "high", "xhigh", "max"]);
5461
+ var runConfigSchema = z12.object({
5462
+ mcpServers: z12.record(z12.string(), mcpServerDefSchema).optional(),
5463
+ model: z12.string().min(1).optional(),
7688
5464
  thinking: thinkingConfigSchema.optional(),
7689
5465
  effort: effortSchema.optional()
7690
5466
  });
@@ -7712,61 +5488,61 @@ var RUNNER_ENV = {
7712
5488
  // Everything else the turn is constructed from, as JSON: see `RunnerSpec`.
7713
5489
  spec: "CABANE_RUNNER_SPEC"
7714
5490
  };
7715
- var runtimeHealthSchema = z14.object({
7716
- quotaState: z14.enum(["ok", "limited", "auth_expired", "unknown"]),
7717
- limitedUntil: z14.string().nullable(),
7718
- lastError: z14.string().nullable()
5491
+ var runtimeHealthSchema = z13.object({
5492
+ quotaState: z13.enum(["ok", "limited", "auth_expired", "unknown"]),
5493
+ limitedUntil: z13.string().nullable(),
5494
+ lastError: z13.string().nullable()
7719
5495
  });
7720
- var runnerSpecSchema = z14.object({
7721
- deviceId: z14.string().optional(),
5496
+ var runnerSpecSchema = z13.object({
5497
+ deviceId: z13.string().optional(),
7722
5498
  // The launcher's boot id: the one its heartbeat carries, so a runner's lease
7723
5499
  // matches the device that holds it.
7724
- bootId: z14.string(),
7725
- workspaceId: z14.string(),
7726
- workspaceSlug: z14.string(),
7727
- agentId: z14.string(),
7728
- agentUsername: z14.string(),
7729
- agentName: z14.string(),
5500
+ bootId: z13.string(),
5501
+ workspaceId: z13.string(),
5502
+ workspaceSlug: z13.string(),
5503
+ agentId: z13.string(),
5504
+ agentUsername: z13.string(),
5505
+ agentName: z13.string(),
7730
5506
  local: localAgentConfigSchema,
7731
5507
  runConfig: runConfigSchema,
7732
5508
  // The device's `root`, resolved by the launcher: the pair directory the turn
7733
5509
  // stands in when nothing else names one lives under it (`work-dir.ts`).
7734
- workRoot: z14.string(),
7735
- opencodeServerUrl: z14.string().optional(),
7736
- claudeCodeAvailable: z14.boolean(),
7737
- codexAvailable: z14.boolean(),
7738
- idleTimeoutMs: z14.number().int().positive().optional(),
7739
- totalTimeoutMs: z14.number().int().positive().optional(),
7740
- leaseRenewalMs: z14.number().int().positive().optional(),
7741
- preparingRowDelayMs: z14.number().int().nonnegative().optional(),
5510
+ workRoot: z13.string(),
5511
+ opencodeServerUrl: z13.string().optional(),
5512
+ claudeCodeAvailable: z13.boolean(),
5513
+ codexAvailable: z13.boolean(),
5514
+ idleTimeoutMs: z13.number().int().positive().optional(),
5515
+ totalTimeoutMs: z13.number().int().positive().optional(),
5516
+ leaseRenewalMs: z13.number().int().positive().optional(),
5517
+ preparingRowDelayMs: z13.number().int().nonnegative().optional(),
7742
5518
  // Whether the turn writes a transcript, into the device's own directory
7743
5519
  // (`transcriptsDir()`).
7744
- transcripts: z14.boolean(),
7745
- logLevel: z14.string(),
5520
+ transcripts: z13.boolean(),
5521
+ logLevel: z13.string(),
7746
5522
  // The launcher's connector-health observations, by runtime, as of the start.
7747
- connectorHealth: z14.record(z14.string(), runtimeHealthSchema),
5523
+ connectorHealth: z13.record(z13.string(), runtimeHealthSchema),
7748
5524
  // True when this (conversation, agent) pair was already told its session
7749
5525
  // could not be saved: the notice is once per pair, not once per process.
7750
- sessionWriteNoticeClaimed: z14.boolean(),
5526
+ sessionWriteNoticeClaimed: z13.boolean(),
7751
5527
  // The runner's identity, minted with its credential. The start
7752
5528
  // PATCH carries it so the lease knows which runner holds the span.
7753
- runnerId: z14.string(),
5529
+ runnerId: z13.string(),
7754
5530
  // Whether the server took gzip request bodies at the latest heartbeat, as of
7755
5531
  // the start. The launcher writes `gzip_bodies` when that changes mid-turn.
7756
- gzipBodies: z14.boolean().optional()
5532
+ gzipBodies: z13.boolean().optional()
7757
5533
  });
7758
- var dispatchSchema = z14.object({
7759
- type: z14.literal("device:dispatch_requested"),
7760
- agentId: z14.string(),
7761
- deviceId: z14.string(),
7762
- conversationId: z14.string(),
7763
- messageId: z14.string(),
7764
- episode: z14.number().int().optional()
5534
+ var dispatchSchema = z13.object({
5535
+ type: z13.literal("device:dispatch_requested"),
5536
+ agentId: z13.string(),
5537
+ deviceId: z13.string(),
5538
+ conversationId: z13.string(),
5539
+ messageId: z13.string(),
5540
+ episode: z13.number().int().optional()
7765
5541
  });
7766
- var credentialsSchema = z14.object({
7767
- credential: z14.string().startsWith("cabrn_"),
7768
- secrets: z14.record(z14.string(), z14.string()).optional(),
7769
- grants: z14.record(z14.string(), z14.record(z14.string(), z14.string())).optional()
5542
+ var credentialsSchema = z13.object({
5543
+ credential: z13.string().startsWith("cabrn_"),
5544
+ secrets: z13.record(z13.string(), z13.string()).optional(),
5545
+ grants: z13.record(z13.string(), z13.record(z13.string(), z13.string())).optional()
7770
5546
  });
7771
5547
  function runnerEnv(input) {
7772
5548
  return {
@@ -7801,6 +5577,30 @@ function decodeRunnerEvent(line) {
7801
5577
  return value;
7802
5578
  }
7803
5579
 
5580
+ // src/credential-broker/client-config.ts
5581
+ import { parse, stringify } from "smol-toml";
5582
+ var BROKERED_CREDENTIAL_NAMES = /* @__PURE__ */ new Set([
5583
+ "ANTHROPIC_API_KEY",
5584
+ "ANTHROPIC_AUTH_TOKEN",
5585
+ "CLAUDE_CODE_OAUTH_TOKEN",
5586
+ "CLAUDE_CODE_OAUTH_REFRESH_TOKEN",
5587
+ "CLAUDE_CODE_SESSION_ACCESS_TOKEN",
5588
+ "CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR",
5589
+ "CLAUDE_CODE_HOST_CREDS_FILE",
5590
+ "CODEX_API_KEY",
5591
+ "OPENAI_API_KEY",
5592
+ "OPENAI_ACCESS_TOKEN",
5593
+ "GH_TOKEN",
5594
+ "GITHUB_TOKEN",
5595
+ "GH_ENTERPRISE_TOKEN",
5596
+ "GITHUB_ENTERPRISE_TOKEN"
5597
+ ]);
5598
+ function withoutBrokeredCredentials(values) {
5599
+ return Object.fromEntries(
5600
+ Object.entries(values).filter(([name]) => !BROKERED_CREDENTIAL_NAMES.has(name))
5601
+ );
5602
+ }
5603
+
7804
5604
  // src/runner/launch.ts
7805
5605
  function gzipBodiesSignal() {
7806
5606
  let on = false;
@@ -8064,12 +5864,22 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
8064
5864
  }
8065
5865
  try {
8066
5866
  const wrapped = launch.wrapped === true;
8067
- const env = runnerExecEnv(launch.env ?? process.env, wrapped);
8068
- const grants = minted.deviceGrants.length > 0 ? launch.grants?.(minted.deviceGrants) ?? {} : {};
5867
+ if (launch.brokeredCredentials && !wrapped)
5868
+ throw new Error("credential broker requires a wrapped runner");
5869
+ const inherited = runnerExecEnv(launch.env ?? process.env, wrapped);
5870
+ const env = launch.brokeredCredentials ? withoutBrokeredCredentials(inherited) : inherited;
5871
+ const directGrants = minted.deviceGrants.length > 0 ? launch.grants?.(minted.deviceGrants) ?? {} : {};
5872
+ const grants = launch.brokeredCredentials ? Object.fromEntries(
5873
+ Object.entries(directGrants).map(([key, values]) => [
5874
+ key,
5875
+ withoutBrokeredCredentials(values)
5876
+ ])
5877
+ ) : directGrants;
5878
+ const secrets = wrapped ? launch.secrets?.() ?? {} : void 0;
8069
5879
  const input = {
8070
5880
  baseUrl: opts.baseUrl,
8071
5881
  credential: minted.credential,
8072
- ...wrapped ? { secrets: launch.secrets?.() ?? {} } : {},
5882
+ ...secrets ? { secrets: launch.brokeredCredentials ? withoutBrokeredCredentials(secrets) : secrets } : {},
8073
5883
  ...Object.keys(grants).length > 0 ? { grants } : {},
8074
5884
  dispatch: payload,
8075
5885
  turnId,
@@ -8236,12 +6046,12 @@ import { randomUUID as randomUUID2 } from "crypto";
8236
6046
 
8237
6047
  // src/turn-execution.ts
8238
6048
  import { createHash as createHash2, randomUUID as randomUUID4 } from "crypto";
8239
- import { existsSync as existsSync12, mkdirSync as mkdirSync9 } from "fs";
6049
+ import { existsSync as existsSync11, mkdirSync as mkdirSync9 } from "fs";
8240
6050
 
8241
6051
  // src/grants.ts
8242
- import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
8243
- import { join as join13 } from "path";
8244
- import { z as z15 } from "zod";
6052
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
6053
+ import { join as join11 } from "path";
6054
+ import { z as z14 } from "zod";
8245
6055
  var VARIABLE_NAME_RE = /^[A-Z_][A-Z0-9_]*$/;
8246
6056
  function allowedGrantVariable(name) {
8247
6057
  return name.length <= 100 && VARIABLE_NAME_RE.test(name) && !name.startsWith("CABANE_");
@@ -8250,9 +6060,9 @@ function environmentSafe(value) {
8250
6060
  return !value.includes("\0");
8251
6061
  }
8252
6062
  function grantsPath() {
8253
- return join13(cabaneDir(), "grants.json");
6063
+ return join11(cabaneDir(), "grants.json");
8254
6064
  }
8255
- var grantsFileSchema = z15.record(z15.string(), z15.record(z15.string(), z15.string()));
6065
+ var grantsFileSchema = z14.record(z14.string(), z14.record(z14.string(), z14.string()));
8256
6066
  function readGrants(keys, report) {
8257
6067
  if (keys.length === 0) return {};
8258
6068
  const file = loadGrantsFile(report);
@@ -8270,7 +6080,7 @@ function readGrants(keys, report) {
8270
6080
  }
8271
6081
  function loadGrantsFile(report) {
8272
6082
  const path = grantsPath();
8273
- if (!existsSync9(path)) return {};
6083
+ if (!existsSync8(path)) return {};
8274
6084
  let raw;
8275
6085
  try {
8276
6086
  raw = readFileSync7(path, "utf8");
@@ -8299,11 +6109,11 @@ function loadGrantsFile(report) {
8299
6109
 
8300
6110
  // src/work-dir.ts
8301
6111
  import { statSync as statSync3 } from "fs";
8302
- import { join as join14 } from "path";
6112
+ import { join as join12 } from "path";
8303
6113
 
8304
6114
  // src/turn-control-tools.ts
8305
6115
  import { randomUUID as randomUUID3 } from "crypto";
8306
- import { z as z16 } from "zod";
6116
+ import { z as z15 } from "zod";
8307
6117
  var COMPANION_LOCAL_MCP_SERVER = TURN_CONTROL_MCP_SERVER;
8308
6118
  var COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
8309
6119
  var ASK_TOOL = "ask";
@@ -8316,39 +6126,39 @@ var CANCEL_WAKE_TOOL = "cancel_wake";
8316
6126
  var CANCEL_WAKE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${CANCEL_WAKE_TOOL}`;
8317
6127
 
8318
6128
  // src/codex-config.ts
8319
- import { readFile as readFile2 } from "fs/promises";
6129
+ import { readFile } from "fs/promises";
8320
6130
  import { homedir as homedir6 } from "os";
8321
- import { join as join15 } from "path";
8322
- import { parse } from "smol-toml";
6131
+ import { join as join13 } from "path";
6132
+ import { parse as parse2 } from "smol-toml";
8323
6133
 
8324
6134
  // src/codex-images.ts
8325
6135
  import { mkdtemp, rm, writeFile } from "fs/promises";
8326
6136
  import { tmpdir } from "os";
8327
- import { join as join16 } from "path";
6137
+ import { join as join14 } from "path";
8328
6138
 
8329
6139
  // src/codex-compactions.ts
8330
6140
  import { createReadStream } from "fs";
8331
6141
  import { readdir } from "fs/promises";
8332
6142
  import { homedir as homedir7 } from "os";
8333
- import { join as join17 } from "path";
6143
+ import { join as join15 } from "path";
8334
6144
  import { createInterface as createInterface2 } from "readline";
8335
6145
 
8336
6146
  // src/prepared.ts
8337
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync8, rmSync as rmSync5, writeFileSync as writeFileSync5, existsSync as existsSync10 } from "fs";
8338
- import { join as join18 } from "path";
6147
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync8, rmSync as rmSync5, writeFileSync as writeFileSync5, existsSync as existsSync9 } from "fs";
6148
+ import { join as join16 } from "path";
8339
6149
 
8340
6150
  // src/secrets.ts
8341
- import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
8342
- import { join as join19 } from "path";
8343
- import { z as z17 } from "zod";
6151
+ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
6152
+ import { join as join17 } from "path";
6153
+ import { z as z16 } from "zod";
8344
6154
  var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
8345
6155
  function secretsPath() {
8346
- return join19(cabaneDir(), "secrets.json");
6156
+ return join17(cabaneDir(), "secrets.json");
8347
6157
  }
8348
- var secretStoreSchema = z17.record(z17.string(), z17.string());
6158
+ var secretStoreSchema = z16.record(z16.string(), z16.string());
8349
6159
  function loadSecretValues() {
8350
6160
  const path = secretsPath();
8351
- if (!existsSync11(path)) return {};
6161
+ if (!existsSync10(path)) return {};
8352
6162
  let raw;
8353
6163
  try {
8354
6164
  raw = readFileSync9(path, "utf8");
@@ -8533,9 +6343,9 @@ function runBounded(path, args, env) {
8533
6343
 
8534
6344
  // src/transcript-writer.ts
8535
6345
  import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync6 } from "fs";
8536
- import { basename as basename3, dirname as dirname7, join as join20 } from "path";
6346
+ import { basename as basename3, dirname as dirname6, join as join18 } from "path";
8537
6347
  function transcriptsDir() {
8538
- return join20(cabaneDir(), "transcripts");
6348
+ return join18(cabaneDir(), "transcripts");
8539
6349
  }
8540
6350
 
8541
6351
  // src/turn-execution.ts
@@ -8645,7 +6455,7 @@ var Dispatcher = class {
8645
6455
  };
8646
6456
 
8647
6457
  // src/host-report.ts
8648
- import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
6458
+ import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
8649
6459
  import { userInfo as userInfo2 } from "os";
8650
6460
  function inContainer(signals) {
8651
6461
  if (signals.dockerenv) return true;
@@ -8660,7 +6470,7 @@ function readContainerSignals() {
8660
6470
  } catch {
8661
6471
  cgroup = null;
8662
6472
  }
8663
- return { dockerenv: existsSync13("/.dockerenv"), cgroup, env: process.env };
6473
+ return { dockerenv: existsSync12("/.dockerenv"), cgroup, env: process.env };
8664
6474
  }
8665
6475
  function osUser() {
8666
6476
  try {
@@ -8897,7 +6707,7 @@ function checkReason(check) {
8897
6707
 
8898
6708
  // src/harness-copy.ts
8899
6709
  import { homedir as homedir8 } from "os";
8900
- import { join as join21, win32 } from "path";
6710
+ import { join as join19, win32 } from "path";
8901
6711
  var HARNESS_LABELS = {
8902
6712
  "claude-code": "Claude Code",
8903
6713
  codex: "Codex",
@@ -8911,7 +6721,7 @@ function failureText(runtime, reason) {
8911
6721
  case "spawn":
8912
6722
  return `Couldn't check whether ${label} is signed in: Cabane's bundled ${label} wouldn't start. Repair it with ${repairSteps()} and try again.${spaced(repairCost())}`;
8913
6723
  case "config":
8914
- return `Couldn't check whether ${label} is signed in: its configuration file didn't load${runtime === "codex" ? ` (${tildePath(join21(homedir8(), ".codex", "config.toml"))})` : ""}. Fix it, then try again.`;
6724
+ return `Couldn't check whether ${label} is signed in: its configuration file didn't load${runtime === "codex" ? ` (${tildePath(join19(homedir8(), ".codex", "config.toml"))})` : ""}. Fix it, then try again.`;
8915
6725
  case "unreadable":
8916
6726
  return `Couldn't check whether ${label} is signed in: it answered in a way Cabane couldn't read. Try again, and if it keeps happening, update Cabane.`;
8917
6727
  }
@@ -8962,12 +6772,12 @@ async function detectHarnesses(cfg, deps = {}) {
8962
6772
  );
8963
6773
  return version !== null ? { state: "usable", via: "serve", version, url } : { state: "not_running", url };
8964
6774
  };
8965
- const [claude, codex, serve2] = await Promise.all([
6775
+ const [claude, codex, serve] = await Promise.all([
8966
6776
  bundled("claude-code"),
8967
6777
  bundled("codex"),
8968
6778
  opencode()
8969
6779
  ]);
8970
- return { "claude-code": claude, codex, opencode: serve2 };
6780
+ return { "claude-code": claude, codex, opencode: serve };
8971
6781
  }
8972
6782
  function usableRuntimes(detection) {
8973
6783
  return HARNESS_RUNTIMES.filter((runtime) => detection[runtime].state === "usable");
@@ -9168,7 +6978,7 @@ var DispatchClaims = class {
9168
6978
 
9169
6979
  // src/outbox.ts
9170
6980
  import {
9171
- existsSync as existsSync14,
6981
+ existsSync as existsSync13,
9172
6982
  mkdirSync as mkdirSync10,
9173
6983
  readdirSync as readdirSync2,
9174
6984
  readFileSync as readFileSync11,
@@ -9176,7 +6986,7 @@ import {
9176
6986
  rmSync as rmSync7,
9177
6987
  writeFileSync as writeFileSync6
9178
6988
  } from "fs";
9179
- import { join as join22 } from "path";
6989
+ import { join as join20 } from "path";
9180
6990
  var MAX_ENTRIES = 2e3;
9181
6991
  var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
9182
6992
  var Outbox = class {
@@ -9189,10 +6999,10 @@ var Outbox = class {
9189
6999
  // Resolved lazily (per call) so tests that swap HOME between
9190
7000
  // cases route writes at the right tmpdir.
9191
7001
  dir() {
9192
- return join22(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
7002
+ return join20(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
9193
7003
  }
9194
7004
  fileFor(turnId, seq) {
9195
- return join22(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
7005
+ return join20(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
9196
7006
  }
9197
7007
  // Persist a commit for later draining. Atomic (temp file + rename) so a
9198
7008
  // concurrent `list()` never reads a half-written entry, then enforces the
@@ -9224,7 +7034,7 @@ var Outbox = class {
9224
7034
  // wedging the drain.
9225
7035
  list() {
9226
7036
  const dir2 = this.dir();
9227
- if (!existsSync14(dir2)) return [];
7037
+ if (!existsSync13(dir2)) return [];
9228
7038
  let names;
9229
7039
  try {
9230
7040
  names = readdirSync2(dir2);
@@ -9234,7 +7044,7 @@ var Outbox = class {
9234
7044
  const entries = [];
9235
7045
  for (const name of names) {
9236
7046
  if (!name.endsWith(".json")) continue;
9237
- const full = join22(dir2, name);
7047
+ const full = join20(dir2, name);
9238
7048
  try {
9239
7049
  const parsed = JSON.parse(readFileSync11(full, "utf8"));
9240
7050
  if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
@@ -9270,7 +7080,7 @@ var Outbox = class {
9270
7080
  // different: it is positive evidence that nothing was ever queued here.
9271
7081
  maxSeqForTurn(turnId) {
9272
7082
  const dir2 = this.dir();
9273
- if (!existsSync14(dir2)) return 0;
7083
+ if (!existsSync13(dir2)) return 0;
9274
7084
  const names = readdirSync2(dir2);
9275
7085
  const prefix = `${encodeURIComponent(turnId)}__`;
9276
7086
  let max = 0;
@@ -9290,7 +7100,7 @@ var Outbox = class {
9290
7100
  }
9291
7101
  size() {
9292
7102
  const dir2 = this.dir();
9293
- if (!existsSync14(dir2)) return 0;
7103
+ if (!existsSync13(dir2)) return 0;
9294
7104
  try {
9295
7105
  return readdirSync2(dir2).filter((n) => n.endsWith(".json")).length;
9296
7106
  } catch {
@@ -9457,8 +7267,8 @@ var SseSubscriber = class {
9457
7267
 
9458
7268
  // src/updater.ts
9459
7269
  import { spawn as spawn5 } from "child_process";
9460
- import { copyFileSync as copyFileSync2, existsSync as existsSync16, mkdirSync as mkdirSync14, renameSync as renameSync9, rmSync as rmSync10 } from "fs";
9461
- import { dirname as dirname11, join as join26 } from "path";
7270
+ import { copyFileSync as copyFileSync2, existsSync as existsSync15, mkdirSync as mkdirSync14, renameSync as renameSync9, rmSync as rmSync10 } from "fs";
7271
+ import { dirname as dirname10, join as join24 } from "path";
9462
7272
 
9463
7273
  // src/boot-rule.ts
9464
7274
  import {
@@ -9470,7 +7280,7 @@ import {
9470
7280
  symlinkSync,
9471
7281
  writeFileSync as writeFileSync7
9472
7282
  } from "fs";
9473
- import { dirname as dirname8, join as join23 } from "path";
7283
+ import { dirname as dirname7, join as join21 } from "path";
9474
7284
  var UNCONFIRMED_WINDOW_MS = 10 * 6e4;
9475
7285
  var MAX_STAGE_ATTEMPTS = 3;
9476
7286
  function superseded(pending, ...running) {
@@ -9493,7 +7303,7 @@ function rolledBackState(state, pending, reason, now) {
9493
7303
 
9494
7304
  // src/install-layout.ts
9495
7305
  import {
9496
- existsSync as existsSync15,
7306
+ existsSync as existsSync14,
9497
7307
  lstatSync,
9498
7308
  mkdirSync as mkdirSync12,
9499
7309
  readFileSync as readFileSync13,
@@ -9503,26 +7313,26 @@ import {
9503
7313
  rmSync as rmSync9,
9504
7314
  symlinkSync as symlinkSync2
9505
7315
  } from "fs";
9506
- import { dirname as dirname9, join as join24 } from "path";
9507
- import { fileURLToPath as fileURLToPath5 } from "url";
7316
+ import { dirname as dirname8, join as join22 } from "path";
7317
+ import { fileURLToPath as fileURLToPath4 } from "url";
9508
7318
  function versionsDir() {
9509
- return join24(cabaneDir(), "versions");
7319
+ return join22(cabaneDir(), "versions");
9510
7320
  }
9511
7321
  function currentLink() {
9512
- return join24(cabaneDir(), "current");
7322
+ return join22(cabaneDir(), "current");
9513
7323
  }
9514
7324
  function bootGuardPath() {
9515
- return join24(cabaneDir(), "boot-guard.mjs");
7325
+ return join22(cabaneDir(), "boot-guard.mjs");
9516
7326
  }
9517
7327
  function versionPackageRoot(version) {
9518
- return join24(versionsDir(), version, "node_modules", "@cabane", "companion");
7328
+ return join22(versionsDir(), version, "node_modules", "@cabane", "companion");
9519
7329
  }
9520
7330
  function ownPackageRoot(moduleUrl = import.meta.url) {
9521
- return dirname9(dirname9(fileURLToPath5(moduleUrl)));
7331
+ return dirname8(dirname8(fileURLToPath4(moduleUrl)));
9522
7332
  }
9523
7333
  function packageVersion(root) {
9524
7334
  try {
9525
- const pkg2 = JSON.parse(readFileSync13(join24(root, "package.json"), "utf8"));
7335
+ const pkg2 = JSON.parse(readFileSync13(join22(root, "package.json"), "utf8"));
9526
7336
  return typeof pkg2.version === "string" ? pkg2.version : null;
9527
7337
  } catch {
9528
7338
  return null;
@@ -9531,14 +7341,14 @@ function packageVersion(root) {
9531
7341
  function currentRoot() {
9532
7342
  try {
9533
7343
  const root = realpathSync5(currentLink());
9534
- return existsSync15(join24(root, "dist", "cli.js")) ? root : null;
7344
+ return existsSync14(join22(root, "dist", "cli.js")) ? root : null;
9535
7345
  } catch {
9536
7346
  return null;
9537
7347
  }
9538
7348
  }
9539
7349
  function pointCurrentAt(root) {
9540
7350
  const link = currentLink();
9541
- mkdirSync12(dirname9(link), { recursive: true });
7351
+ mkdirSync12(dirname8(link), { recursive: true });
9542
7352
  const fresh = `${link}.${process.pid}.${Date.now()}`;
9543
7353
  symlinkSync2(root, fresh);
9544
7354
  try {
@@ -9550,11 +7360,11 @@ function pointCurrentAt(root) {
9550
7360
  }
9551
7361
  function pruneVersions(keep) {
9552
7362
  const dir2 = versionsDir();
9553
- if (!existsSync15(dir2)) return [];
7363
+ if (!existsSync14(dir2)) return [];
9554
7364
  const removed = [];
9555
7365
  for (const name of readdirSync3(dir2)) {
9556
7366
  if (keep.includes(name)) continue;
9557
- rmSync9(join24(dir2, name), { recursive: true, force: true });
7367
+ rmSync9(join22(dir2, name), { recursive: true, force: true });
9558
7368
  removed.push(name);
9559
7369
  }
9560
7370
  return removed;
@@ -9573,31 +7383,31 @@ function compareVersions(a, b) {
9573
7383
 
9574
7384
  // src/update-state.ts
9575
7385
  import { mkdirSync as mkdirSync13, readFileSync as readFileSync14, renameSync as renameSync8, writeFileSync as writeFileSync8 } from "fs";
9576
- import { dirname as dirname10, join as join25 } from "path";
9577
- import { z as z18 } from "zod";
9578
- var resultSchema = z18.object({
9579
- kind: z18.enum(["updated", "rolled_back", "failed"]),
9580
- from: z18.string(),
9581
- to: z18.string(),
9582
- at: z18.string(),
9583
- reason: z18.string().optional()
7386
+ import { dirname as dirname9, join as join23 } from "path";
7387
+ import { z as z17 } from "zod";
7388
+ var resultSchema = z17.object({
7389
+ kind: z17.enum(["updated", "rolled_back", "failed"]),
7390
+ from: z17.string(),
7391
+ to: z17.string(),
7392
+ at: z17.string(),
7393
+ reason: z17.string().optional()
9584
7394
  });
9585
- var pendingSchema = z18.object({
9586
- from: z18.string(),
7395
+ var pendingSchema = z17.object({
7396
+ from: z17.string(),
9587
7397
  // The package root `current` pointed at before the swap, so a rollback can point it back.
9588
- fromRoot: z18.string(),
9589
- to: z18.string(),
9590
- at: z18.string(),
9591
- boots: z18.number().int().nonnegative()
7398
+ fromRoot: z17.string(),
7399
+ to: z17.string(),
7400
+ at: z17.string(),
7401
+ boots: z17.number().int().nonnegative()
9592
7402
  });
9593
- var stateSchema = z18.object({
9594
- server: z18.object({ latest: z18.string(), minimum: z18.string(), at: z18.string() }).optional(),
7403
+ var stateSchema = z17.object({
7404
+ server: z17.object({ latest: z17.string(), minimum: z17.string(), at: z17.string() }).optional(),
9595
7405
  pending: pendingSchema.optional(),
9596
7406
  lastResult: resultSchema.optional(),
9597
- failed: z18.object({ version: z18.string(), attempts: z18.number().int(), at: z18.string() }).optional()
7407
+ failed: z17.object({ version: z17.string(), attempts: z17.number().int(), at: z17.string() }).optional()
9598
7408
  });
9599
7409
  function updateStatePath(dir2 = cabaneDir()) {
9600
- return join25(dir2, "update.json");
7410
+ return join23(dir2, "update.json");
9601
7411
  }
9602
7412
  function readUpdateState(path = updateStatePath()) {
9603
7413
  try {
@@ -9608,7 +7418,7 @@ function readUpdateState(path = updateStatePath()) {
9608
7418
  }
9609
7419
  }
9610
7420
  function writeUpdateState(state, path = updateStatePath()) {
9611
- mkdirSync13(dirname10(path), { recursive: true });
7421
+ mkdirSync13(dirname9(path), { recursive: true });
9612
7422
  const tmp = `${path}.${process.pid}.tmp`;
9613
7423
  writeFileSync8(tmp, `${JSON.stringify(state, null, 2)}
9614
7424
  `, { mode: 384 });
@@ -9642,14 +7452,14 @@ function versionToStage(input) {
9642
7452
  return latest;
9643
7453
  }
9644
7454
  function rollBack(pending, reason, now = /* @__PURE__ */ new Date()) {
9645
- if (existsSync16(pending.fromRoot)) pointCurrentAt(pending.fromRoot);
7455
+ if (existsSync15(pending.fromRoot)) pointCurrentAt(pending.fromRoot);
9646
7456
  return updateUpdateState((state) => rolledBackState(state, pending, reason, now));
9647
7457
  }
9648
7458
  function writeBootGuard(root = ownPackageRoot()) {
9649
- const built = join26(root, "dist", "boot-guard.js");
9650
- if (!existsSync16(built)) return false;
7459
+ const built = join24(root, "dist", "boot-guard.js");
7460
+ if (!existsSync15(built)) return false;
9651
7461
  const target = bootGuardPath();
9652
- mkdirSync14(dirname11(target), { recursive: true });
7462
+ mkdirSync14(dirname10(target), { recursive: true });
9653
7463
  const tmp = `${target}.${process.pid}.tmp`;
9654
7464
  copyFileSync2(built, tmp);
9655
7465
  renameSync9(tmp, target);
@@ -9686,17 +7496,17 @@ var runProcess = (command, args, opts = {}) => new Promise((resolve) => {
9686
7496
  async function stageVersion(version, harnesses, deps = {}) {
9687
7497
  const run = deps.run ?? runProcess;
9688
7498
  const node = deps.execPath ?? process.execPath;
9689
- const target = join26(versionsDir(), version);
7499
+ const target = join24(versionsDir(), version);
9690
7500
  const placed = versionPackageRoot(version);
9691
- if (existsSync16(placed)) {
7501
+ if (existsSync15(placed)) {
9692
7502
  if (!await verifyInstall(placed, version, harnesses, { run, execPath: node }))
9693
7503
  return { ok: true, root: placed };
9694
7504
  }
9695
- const npm = join26(dirname11(node), "npm");
9696
- if (!existsSync16(npm))
9697
- return { ok: false, reason: `there is no npm beside Node at ${dirname11(node)}` };
7505
+ const npm = join24(dirname10(node), "npm");
7506
+ if (!existsSync15(npm))
7507
+ return { ok: false, reason: `there is no npm beside Node at ${dirname10(node)}` };
9698
7508
  mkdirSync14(versionsDir(), { recursive: true });
9699
- const staging = join26(versionsDir(), `.${version}.staging-${process.pid}`);
7509
+ const staging = join24(versionsDir(), `.${version}.staging-${process.pid}`);
9700
7510
  rmSync10(staging, { recursive: true, force: true });
9701
7511
  const installed = await run(
9702
7512
  npm,
@@ -9713,7 +7523,7 @@ async function stageVersion(version, harnesses, deps = {}) {
9713
7523
  ],
9714
7524
  {
9715
7525
  // npm's install scripts run on this Node, not whichever `node` is first on PATH.
9716
- env: { ...process.env, PATH: `${dirname11(node)}:${process.env.PATH ?? ""}` },
7526
+ env: { ...process.env, PATH: `${dirname10(node)}:${process.env.PATH ?? ""}` },
9717
7527
  timeoutMs: 15 * 6e4
9718
7528
  }
9719
7529
  );
@@ -9725,7 +7535,7 @@ async function stageVersion(version, harnesses, deps = {}) {
9725
7535
  return { ok: false, reason: `version ${version} isn't published yet`, unpublished: true };
9726
7536
  return { ok: false, reason: `npm couldn't install it: ${lastLine(installed.stderr)}` };
9727
7537
  }
9728
- const root = join26(staging, "node_modules", "@cabane", "companion");
7538
+ const root = join24(staging, "node_modules", "@cabane", "companion");
9729
7539
  const verdict = await verifyInstall(root, version, harnesses, { run, execPath: node });
9730
7540
  if (verdict) {
9731
7541
  rmSync10(staging, { recursive: true, force: true });
@@ -9737,7 +7547,7 @@ async function stageVersion(version, harnesses, deps = {}) {
9737
7547
  }
9738
7548
  async function verifyInstall(root, version, harnesses, deps) {
9739
7549
  if (packageVersion(root) !== version) return `the installed package isn't version ${version}`;
9740
- const answered = await deps.run(deps.execPath, [join26(root, "dist", "bin.js"), "--version"], {
7550
+ const answered = await deps.run(deps.execPath, [join24(root, "dist", "bin.js"), "--version"], {
9741
7551
  timeoutMs: 6e4,
9742
7552
  env: { ...process.env, CABANE_LAUNCH_SELF: "1" }
9743
7553
  });
@@ -9745,7 +7555,7 @@ async function verifyInstall(root, version, harnesses, deps) {
9745
7555
  return `it doesn't start: ${lastLine(answered.stderr) || "no version printed"}`;
9746
7556
  }
9747
7557
  if (harnesses.length === 0) return null;
9748
- const probed = await deps.run(deps.execPath, [join26(root, "dist", "postinstall.js"), "--json"], {
7558
+ const probed = await deps.run(deps.execPath, [join24(root, "dist", "postinstall.js"), "--json"], {
9749
7559
  timeoutMs: 6e4
9750
7560
  });
9751
7561
  let resolutions = {};
@@ -10761,6 +8571,7 @@ var CompanionSupervisor = class {
10761
8571
  ),
10762
8572
  ...this.config.runner?.command ? {
10763
8573
  wrapped: true,
8574
+ brokeredCredentials: this.config.runner.brokeredCredentials,
10764
8575
  secrets: () => loadSecretValuesTolerant(
10765
8576
  (m) => this.log.warn({ err: m }, "Could not read the secrets file for a wrapped turn.")
10766
8577
  )
@@ -11517,10 +9328,10 @@ function handleUncaught(log, err, origin) {
11517
9328
  }
11518
9329
 
11519
9330
  // src/crash-marker.ts
11520
- import { existsSync as existsSync17, mkdirSync as mkdirSync15, readFileSync as readFileSync15, rmSync as rmSync11, writeFileSync as writeFileSync9 } from "fs";
11521
- import { join as join27 } from "path";
9331
+ import { existsSync as existsSync16, mkdirSync as mkdirSync15, readFileSync as readFileSync15, rmSync as rmSync11, writeFileSync as writeFileSync9 } from "fs";
9332
+ import { join as join25 } from "path";
11522
9333
  function crashMarkerPath() {
11523
- return join27(cabaneDir(), "last-error.json");
9334
+ return join25(cabaneDir(), "last-error.json");
11524
9335
  }
11525
9336
  function recordCrash(rec) {
11526
9337
  try {
@@ -11532,7 +9343,7 @@ function recordCrash(rec) {
11532
9343
  function clearCrash() {
11533
9344
  try {
11534
9345
  const path = crashMarkerPath();
11535
- if (existsSync17(path)) rmSync11(path, { force: true });
9346
+ if (existsSync16(path)) rmSync11(path, { force: true });
11536
9347
  } catch {
11537
9348
  }
11538
9349
  }
@@ -11540,7 +9351,7 @@ function clearCrash() {
11540
9351
  // src/stray-companion.ts
11541
9352
  import { spawnSync } from "child_process";
11542
9353
  import { readFileSync as readFileSync16 } from "fs";
11543
- import { join as join28 } from "path";
9354
+ import { join as join26 } from "path";
11544
9355
  var COMPANION_ENTRY = /@cabane\/companion\/|cabane-companion|(^|[\s/])cabane\s|\.cabane\/(versions|current|boot-guard)/;
11545
9356
  var START = /\sstart(\s|$)/;
11546
9357
  function companionStarts(psOutput) {
@@ -11557,7 +9368,7 @@ function companionStarts(psOutput) {
11557
9368
  }
11558
9369
  function stateDirOf(env) {
11559
9370
  const home = env.CABANE_COMPANION_HOME || env.HOME;
11560
- return home ? join28(home, ".cabane") : null;
9371
+ return home ? join26(home, ".cabane") : null;
11561
9372
  }
11562
9373
  function envFromPsE(output, command) {
11563
9374
  const line = output.replace(/\n$/, "");
@@ -11629,7 +9440,7 @@ function strayCompanionLines(strays, retry) {
11629
9440
  // src/runtime.ts
11630
9441
  function embeddedRunner(host) {
11631
9442
  if (!host.electron) return null;
11632
- const cli = fileURLToPath6(new URL("./cli.js", host.moduleUrl));
9443
+ const cli = fileURLToPath5(new URL("./cli.js", host.moduleUrl));
11633
9444
  return {
11634
9445
  runnerCommand: [host.execPath, cli, "run-turn"],
11635
9446
  runnerEnv: { ...host.env, ELECTRON_RUN_AS_NODE: "1" }
@@ -11724,11 +9535,12 @@ async function createCompanionRuntime(opts = {}) {
11724
9535
  update: (auto) => active2.update(auto),
11725
9536
  stop: () => void stop().then(() => active2.requestStop())
11726
9537
  });
11727
- if (opts.dashboard) {
11728
- const preferredPort = opts.port ?? cfg.dashboardPort;
11729
- dashboard = await startDashboard({
9538
+ if (opts.startDashboard) {
9539
+ const preferredPort = cfg.dashboardPort;
9540
+ dashboard = await opts.startDashboard({
11730
9541
  supervisor,
11731
9542
  hub,
9543
+ logPath: companionLogPath(),
11732
9544
  ...preferredPort !== void 0 ? { port: preferredPort } : {}
11733
9545
  });
11734
9546
  hub.setDashboardUrl(dashboard.url);
@@ -11788,7 +9600,10 @@ async function createCompanionRuntime(opts = {}) {
11788
9600
  };
11789
9601
  }
11790
9602
  export {
9603
+ ApiError,
9604
+ CompanionError,
11791
9605
  createCompanionRuntime,
9606
+ dispatchToWire,
11792
9607
  embeddedRunner,
11793
9608
  strayCompanionLines
11794
9609
  };