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