@cabane/companion 0.6.101 → 0.6.102
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -3
- package/dist/cli.js +739 -452
- package/dist/pairing-config.js +4 -1
- package/dist/runtime.js +597 -418
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { Command, Option } from "commander";
|
|
4
|
+
import { Command, Option, InvalidArgumentError } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/control-socket.ts
|
|
7
7
|
import { createHash } from "crypto";
|
|
@@ -307,6 +307,7 @@ var companionConfigSchema = z2.object({
|
|
|
307
307
|
dashboardPort: z2.number().int().min(1).max(65535).optional(),
|
|
308
308
|
autoOpen: z2.boolean().optional(),
|
|
309
309
|
logLevel: z2.enum(["warn", "info", "debug"]).optional(),
|
|
310
|
+
logFormat: z2.enum(["human", "json"]).optional(),
|
|
310
311
|
// CT270: the opencode runtime, when the operator runs one on this machine. The
|
|
311
312
|
// operator installs opencode, starts `opencode serve` (auth via opencode's own
|
|
312
313
|
// `/connect` — Cabane never sees provider keys), and points the companion at it
|
|
@@ -427,7 +428,7 @@ function loadConfigTolerant() {
|
|
|
427
428
|
}
|
|
428
429
|
const strict = companionConfigSchema.safeParse(parsed);
|
|
429
430
|
if (strict.success) {
|
|
430
|
-
const { agents: agents2, prepareHook: prepareHook2, dashboardPort, autoOpen, logLevel, claudeCode: claudeCode2 } = strict.data;
|
|
431
|
+
const { agents: agents2, prepareHook: prepareHook2, dashboardPort, autoOpen, logLevel, logFormat, claudeCode: claudeCode2 } = strict.data;
|
|
431
432
|
return {
|
|
432
433
|
local: {
|
|
433
434
|
...agents2 !== void 0 ? { agents: agents2 } : {},
|
|
@@ -435,6 +436,7 @@ function loadConfigTolerant() {
|
|
|
435
436
|
...dashboardPort !== void 0 ? { dashboardPort } : {},
|
|
436
437
|
...autoOpen !== void 0 ? { autoOpen } : {},
|
|
437
438
|
...logLevel !== void 0 ? { logLevel } : {},
|
|
439
|
+
...logFormat !== void 0 ? { logFormat } : {},
|
|
438
440
|
...claudeCode2 !== void 0 ? { claudeCode: claudeCode2 } : {}
|
|
439
441
|
},
|
|
440
442
|
note: null,
|
|
@@ -449,6 +451,7 @@ function loadConfigTolerant() {
|
|
|
449
451
|
if (prepareHook.success) local.prepareHook = prepareHook.data;
|
|
450
452
|
if (typeof obj.dashboardPort === "number") local.dashboardPort = obj.dashboardPort;
|
|
451
453
|
if (typeof obj.autoOpen === "boolean") local.autoOpen = obj.autoOpen;
|
|
454
|
+
if (obj.logFormat === "human" || obj.logFormat === "json") local.logFormat = obj.logFormat;
|
|
452
455
|
if (obj.logLevel === "warn" || obj.logLevel === "info" || obj.logLevel === "debug") {
|
|
453
456
|
local.logLevel = obj.logLevel;
|
|
454
457
|
}
|
|
@@ -954,7 +957,7 @@ function repairAction() {
|
|
|
954
957
|
}
|
|
955
958
|
function startupWarning(r) {
|
|
956
959
|
const label = HARNESS_LABEL[r.runtime];
|
|
957
|
-
return
|
|
960
|
+
return `${label} is ${HARNESS_INTENT_WORD[r.runtime]}, but its installation is incomplete (${missingNoun(r.status)} is missing). It cannot run turns until repaired. ${repairAction()} Then restart the companion.`;
|
|
958
961
|
}
|
|
959
962
|
function harnessIssueNote(runtime, status2) {
|
|
960
963
|
const intent = HARNESS_INTENT_WORD[runtime];
|
|
@@ -1766,8 +1769,8 @@ function requestEnrollmentCode(baseUrl, opts = {}) {
|
|
|
1766
1769
|
...opts.label ? { label: opts.label } : {}
|
|
1767
1770
|
});
|
|
1768
1771
|
}
|
|
1769
|
-
function deviceLabelFromHostname(
|
|
1770
|
-
const trimmed =
|
|
1772
|
+
function deviceLabelFromHostname(hostname4) {
|
|
1773
|
+
const trimmed = hostname4.trim().replace(/\.local$/i, "");
|
|
1771
1774
|
if (trimmed.length === 0) return void 0;
|
|
1772
1775
|
return trimmed.slice(0, 120);
|
|
1773
1776
|
}
|
|
@@ -1895,73 +1898,207 @@ async function pair(opts = {}) {
|
|
|
1895
1898
|
// src/cli.ts
|
|
1896
1899
|
import { readFileSync as readFileSync11 } from "fs";
|
|
1897
1900
|
|
|
1898
|
-
// src/commands/
|
|
1899
|
-
import {
|
|
1901
|
+
// src/commands/logs.ts
|
|
1902
|
+
import { once } from "events";
|
|
1903
|
+
import { open } from "fs/promises";
|
|
1904
|
+
import { setTimeout as delay } from "timers/promises";
|
|
1900
1905
|
|
|
1901
1906
|
// src/logger.ts
|
|
1902
1907
|
import { createWriteStream, mkdirSync as mkdirSync5 } from "fs";
|
|
1903
1908
|
import { dirname as dirname4, join as join6 } from "path";
|
|
1909
|
+
import { stripVTControlCharacters } from "util";
|
|
1904
1910
|
import pino from "pino";
|
|
1905
|
-
import pretty from "pino-pretty";
|
|
1906
1911
|
function companionLogPath() {
|
|
1907
1912
|
return join6(cabaneDir(), "companion.log");
|
|
1908
1913
|
}
|
|
1909
|
-
|
|
1910
|
-
"
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
"
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
const
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
const
|
|
1925
|
-
|
|
1926
|
-
|
|
1914
|
+
function humanText(value) {
|
|
1915
|
+
return stripVTControlCharacters(String(value ?? "")).replace(
|
|
1916
|
+
/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi,
|
|
1917
|
+
(id) => id.slice(0, 8)
|
|
1918
|
+
).replace(/\s+/g, " ").trim();
|
|
1919
|
+
}
|
|
1920
|
+
function formatHumanLine(record) {
|
|
1921
|
+
const date = new Date(
|
|
1922
|
+
typeof record.time === "number" || typeof record.time === "string" ? record.time : 0
|
|
1923
|
+
);
|
|
1924
|
+
const time = [date.getHours(), date.getMinutes(), date.getSeconds()].map((n) => String(n).padStart(2, "0")).join(":");
|
|
1925
|
+
const level = typeof record.level === "number" ? record.level : 30;
|
|
1926
|
+
const word = level >= 50 ? "error" : level >= 40 ? "warn" : level < 30 ? "debug" : "";
|
|
1927
|
+
const conversation = typeof record.conversationId === "string" ? record.conversationId.slice(0, 8) : "";
|
|
1928
|
+
const agent = humanText(record.agentName);
|
|
1929
|
+
const context = [agent, conversation].filter(Boolean).join(" \xB7 ");
|
|
1930
|
+
let line = `${time} ${word ? `${word} ` : ""}${context ? `${context} ` : ""}${humanText(record.msg)}`;
|
|
1931
|
+
const err = record.err;
|
|
1932
|
+
const errorRecord = err !== null && typeof err === "object" ? err : null;
|
|
1933
|
+
const message = humanText(errorRecord && "message" in errorRecord ? errorRecord.message : err);
|
|
1934
|
+
const detail = typeof record.status === "number" ? `HTTP ${record.status}: ${humanText(record.responseBody) || message}` : message;
|
|
1935
|
+
if (detail && !line.includes(detail)) line += ` (${detail})`;
|
|
1936
|
+
if (level < 30) {
|
|
1937
|
+
const hidden = /* @__PURE__ */ new Set([
|
|
1938
|
+
"time",
|
|
1939
|
+
"level",
|
|
1940
|
+
"pid",
|
|
1941
|
+
"hostname",
|
|
1942
|
+
"msg",
|
|
1943
|
+
"err",
|
|
1944
|
+
"stack",
|
|
1945
|
+
"agentName",
|
|
1946
|
+
"conversationId",
|
|
1947
|
+
"transcriptPath"
|
|
1948
|
+
]);
|
|
1949
|
+
const details = Object.fromEntries(Object.entries(record).filter(([key]) => !hidden.has(key)));
|
|
1950
|
+
if (Object.keys(details).length)
|
|
1951
|
+
line += ` ${humanText(JSON.stringify(details)).slice(0, 2e3)}`;
|
|
1952
|
+
}
|
|
1953
|
+
if (record.transcriptPath)
|
|
1954
|
+
line += `
|
|
1955
|
+
Transcript: ${humanText(record.transcriptPath)}`;
|
|
1956
|
+
const stack = errorRecord && "stack" in errorRecord ? errorRecord.stack : record.stack;
|
|
1957
|
+
if ((level >= 50 || level < 30) && typeof stack === "string") {
|
|
1958
|
+
line += "\n" + stack.split("\n").map((part) => ` ${humanText(part)}`).join("\n");
|
|
1959
|
+
}
|
|
1960
|
+
return line + "\n";
|
|
1927
1961
|
}
|
|
1928
1962
|
var cached = null;
|
|
1929
1963
|
var consoleLogging = true;
|
|
1964
|
+
var overrides = {};
|
|
1965
|
+
var formats = /* @__PURE__ */ new WeakMap();
|
|
1930
1966
|
function setConsoleLogging(enabled) {
|
|
1931
1967
|
consoleLogging = enabled;
|
|
1932
1968
|
}
|
|
1933
|
-
function
|
|
1969
|
+
function setLogOverrides(options) {
|
|
1970
|
+
overrides = options;
|
|
1971
|
+
}
|
|
1972
|
+
function configureLogger(log, config) {
|
|
1973
|
+
log.level = overrides.logLevel ?? config.logLevel ?? "info";
|
|
1974
|
+
const state = formats.get(log);
|
|
1975
|
+
if (state) state.format = overrides.logFormat ?? config.logFormat ?? "human";
|
|
1976
|
+
}
|
|
1977
|
+
function createLogger(destinations = {}, options = {}) {
|
|
1934
1978
|
const path = companionLogPath();
|
|
1935
1979
|
if (!destinations.file) mkdirSync5(dirname4(path), { recursive: true });
|
|
1980
|
+
const file = destinations.file ?? createWriteStream(path, { flags: "a" });
|
|
1981
|
+
const state = { format: options.logFormat ?? "human" };
|
|
1936
1982
|
const streams = [];
|
|
1983
|
+
const human = (chunk) => {
|
|
1984
|
+
const record = JSON.parse(chunk);
|
|
1985
|
+
let text = formatHumanLine(record);
|
|
1986
|
+
const err = record.err;
|
|
1987
|
+
if (log.isLevelEnabled("debug") && record.level !== 20 && typeof record.level === "number" && record.level < 50 && err && typeof err === "object" && "stack" in err) {
|
|
1988
|
+
text += formatHumanLine({ ...record, level: 20, msg: "Error details" });
|
|
1989
|
+
}
|
|
1990
|
+
return text;
|
|
1991
|
+
};
|
|
1937
1992
|
if (process.env.CABANE_COMPANION_DAEMON !== "1") {
|
|
1938
|
-
const consoleStream = pretty({
|
|
1939
|
-
colorize: true,
|
|
1940
|
-
ignore: CONSOLE_IGNORE,
|
|
1941
|
-
messageFormat: (log, messageKey) => consoleMessageFormat(log, messageKey),
|
|
1942
|
-
...destinations.console ? { destination: destinations.console } : {}
|
|
1943
|
-
});
|
|
1944
1993
|
streams.push({
|
|
1945
|
-
level: "
|
|
1994
|
+
level: "debug",
|
|
1946
1995
|
stream: {
|
|
1947
1996
|
write(chunk) {
|
|
1948
|
-
if (consoleLogging)
|
|
1997
|
+
if (consoleLogging) (destinations.console ?? process.stdout).write(human(chunk));
|
|
1949
1998
|
}
|
|
1950
1999
|
}
|
|
1951
2000
|
});
|
|
1952
2001
|
}
|
|
1953
2002
|
streams.push({
|
|
1954
2003
|
level: "debug",
|
|
1955
|
-
stream:
|
|
2004
|
+
stream: {
|
|
2005
|
+
write(chunk) {
|
|
2006
|
+
file.write(state.format === "json" ? chunk : human(chunk));
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
1956
2009
|
});
|
|
1957
|
-
|
|
2010
|
+
const log = pino({ level: options.logLevel ?? "info" }, pino.multistream(streams));
|
|
2011
|
+
formats.set(log, state);
|
|
2012
|
+
return log;
|
|
1958
2013
|
}
|
|
1959
2014
|
function getLogger() {
|
|
1960
2015
|
if (cached) return cached;
|
|
1961
|
-
|
|
2016
|
+
let config = {};
|
|
2017
|
+
try {
|
|
2018
|
+
config = loadConfig() ?? {};
|
|
2019
|
+
} catch {
|
|
2020
|
+
}
|
|
2021
|
+
cached = createLogger({}, { ...config, ...overrides });
|
|
1962
2022
|
return cached;
|
|
1963
2023
|
}
|
|
1964
2024
|
|
|
2025
|
+
// src/commands/logs.ts
|
|
2026
|
+
var BLOCK_BYTES = 64 * 1024;
|
|
2027
|
+
var TAIL_BYTES = 1024 * 1024;
|
|
2028
|
+
async function readLogTail(file, lines) {
|
|
2029
|
+
const { size } = await file.stat();
|
|
2030
|
+
let offset = size;
|
|
2031
|
+
let bytes = 0;
|
|
2032
|
+
let newlines = 0;
|
|
2033
|
+
const chunks = [];
|
|
2034
|
+
while (offset > 0 && newlines <= lines && bytes < TAIL_BYTES) {
|
|
2035
|
+
const length = Math.min(BLOCK_BYTES, offset, TAIL_BYTES - bytes);
|
|
2036
|
+
offset -= length;
|
|
2037
|
+
const buffer = Buffer.alloc(length);
|
|
2038
|
+
const { bytesRead } = await file.read(buffer, 0, length, offset);
|
|
2039
|
+
const chunk = buffer.subarray(0, bytesRead);
|
|
2040
|
+
chunks.unshift(chunk);
|
|
2041
|
+
bytes += bytesRead;
|
|
2042
|
+
for (const byte of chunk) if (byte === 10) newlines++;
|
|
2043
|
+
}
|
|
2044
|
+
let text = Buffer.concat(chunks).toString("utf8");
|
|
2045
|
+
if (offset > 0) text = text.slice(text.indexOf("\n") + 1);
|
|
2046
|
+
const trailing = text.endsWith("\n");
|
|
2047
|
+
const rows = (trailing ? text.slice(0, -1) : text).split("\n");
|
|
2048
|
+
text = lines === 0 ? "" : rows.slice(-lines).join("\n") + (trailing ? "\n" : "");
|
|
2049
|
+
return { text, position: size };
|
|
2050
|
+
}
|
|
2051
|
+
async function logs(opts = {}) {
|
|
2052
|
+
let file;
|
|
2053
|
+
try {
|
|
2054
|
+
file = await open(companionLogPath(), "r");
|
|
2055
|
+
} catch (err) {
|
|
2056
|
+
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
|
|
2057
|
+
process.stdout.write("No log yet. Start the companion with cabane-companion start.\n");
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
2060
|
+
throw err;
|
|
2061
|
+
}
|
|
2062
|
+
const aborter = new AbortController();
|
|
2063
|
+
const stop2 = () => aborter.abort();
|
|
2064
|
+
if (opts.follow) {
|
|
2065
|
+
process.on("SIGINT", stop2);
|
|
2066
|
+
process.on("SIGTERM", stop2);
|
|
2067
|
+
}
|
|
2068
|
+
try {
|
|
2069
|
+
const tail = await readLogTail(file, opts.lines ?? 50);
|
|
2070
|
+
const write2 = async (chunk) => {
|
|
2071
|
+
if (!process.stdout.write(chunk)) {
|
|
2072
|
+
await once(process.stdout, "drain", { signal: aborter.signal }).catch((err) => {
|
|
2073
|
+
if (!aborter.signal.aborted) throw err;
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
};
|
|
2077
|
+
await write2(tail.text);
|
|
2078
|
+
let position = tail.position;
|
|
2079
|
+
const buffer = Buffer.alloc(BLOCK_BYTES);
|
|
2080
|
+
while (opts.follow && !aborter.signal.aborted) {
|
|
2081
|
+
if ((await file.stat()).size < position) position = 0;
|
|
2082
|
+
const { bytesRead } = await file.read(buffer, 0, buffer.length, position);
|
|
2083
|
+
if (bytesRead) {
|
|
2084
|
+
position += bytesRead;
|
|
2085
|
+
await write2(Buffer.from(buffer.subarray(0, bytesRead)));
|
|
2086
|
+
} else {
|
|
2087
|
+
await delay(200, void 0, { signal: aborter.signal }).catch((err) => {
|
|
2088
|
+
if (!aborter.signal.aborted) throw err;
|
|
2089
|
+
});
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
} finally {
|
|
2093
|
+
process.removeListener("SIGINT", stop2);
|
|
2094
|
+
process.removeListener("SIGTERM", stop2);
|
|
2095
|
+
await file.close();
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
// src/commands/start.ts
|
|
2100
|
+
import { hostname as hostname3 } from "os";
|
|
2101
|
+
|
|
1965
2102
|
// src/runtime.ts
|
|
1966
2103
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
1967
2104
|
|
|
@@ -2524,6 +2661,246 @@ async function warnAboutHarnessReadiness(cfg, deps = {}) {
|
|
|
2524
2661
|
warn(`No harness is connected on this device yet, so no agent turn can run here. ${guidance}`);
|
|
2525
2662
|
}
|
|
2526
2663
|
|
|
2664
|
+
// src/supervisor.ts
|
|
2665
|
+
import { hostname as hostname2 } from "os";
|
|
2666
|
+
|
|
2667
|
+
// packages/agent-runtime/src/failure.ts
|
|
2668
|
+
import { z as z4 } from "zod";
|
|
2669
|
+
var turnFailureSchema = z4.discriminatedUnion("kind", [
|
|
2670
|
+
z4.object({ kind: z4.literal("usage_capped"), resetsAt: z4.string().optional() }),
|
|
2671
|
+
z4.object({ kind: z4.literal("rate_limited") }),
|
|
2672
|
+
z4.object({ kind: z4.literal("server_error") }),
|
|
2673
|
+
z4.object({ kind: z4.literal("auth_expired") })
|
|
2674
|
+
]);
|
|
2675
|
+
var USAGE_CAPPED = "usage_capped";
|
|
2676
|
+
var RATE_LIMITED = "rate_limited";
|
|
2677
|
+
var SERVER_ERROR = "server_error";
|
|
2678
|
+
var AUTH_EXPIRED = "auth_expired";
|
|
2679
|
+
function encodeFailureReason(failure) {
|
|
2680
|
+
switch (failure.kind) {
|
|
2681
|
+
case "auth_expired":
|
|
2682
|
+
return AUTH_EXPIRED;
|
|
2683
|
+
case "server_error":
|
|
2684
|
+
return SERVER_ERROR;
|
|
2685
|
+
case "rate_limited":
|
|
2686
|
+
return RATE_LIMITED;
|
|
2687
|
+
case "usage_capped":
|
|
2688
|
+
return failure.resetsAt ? `${USAGE_CAPPED}:${failure.resetsAt}` : USAGE_CAPPED;
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
function decodeFailureReason(reason) {
|
|
2692
|
+
if (!reason) return null;
|
|
2693
|
+
if (reason === AUTH_EXPIRED) return { kind: "auth_expired" };
|
|
2694
|
+
if (reason === SERVER_ERROR) return { kind: "server_error" };
|
|
2695
|
+
if (reason === RATE_LIMITED) return { kind: "rate_limited" };
|
|
2696
|
+
if (reason === USAGE_CAPPED) return { kind: "usage_capped" };
|
|
2697
|
+
if (reason.startsWith(`${USAGE_CAPPED}:`)) {
|
|
2698
|
+
const iso = reason.slice(USAGE_CAPPED.length + 1);
|
|
2699
|
+
return isValidIso(iso) ? { kind: "usage_capped", resetsAt: iso } : { kind: "usage_capped" };
|
|
2700
|
+
}
|
|
2701
|
+
return null;
|
|
2702
|
+
}
|
|
2703
|
+
function isValidIso(value) {
|
|
2704
|
+
if (!value) return false;
|
|
2705
|
+
const ms = Date.parse(value);
|
|
2706
|
+
return Number.isFinite(ms);
|
|
2707
|
+
}
|
|
2708
|
+
function quotaForFailure(failure) {
|
|
2709
|
+
if (failure.kind === "auth_expired") return { quotaState: "auth_expired", limitedUntil: null };
|
|
2710
|
+
if (failure.kind === "usage_capped")
|
|
2711
|
+
return { quotaState: "limited", limitedUntil: failure.resetsAt ?? null };
|
|
2712
|
+
return null;
|
|
2713
|
+
}
|
|
2714
|
+
function resetsAtToIso(resetsAt) {
|
|
2715
|
+
if (typeof resetsAt !== "number" || !Number.isFinite(resetsAt) || resetsAt <= 0) return void 0;
|
|
2716
|
+
const ms = resetsAt < 1e12 ? resetsAt * 1e3 : resetsAt;
|
|
2717
|
+
const date = new Date(ms);
|
|
2718
|
+
const time = date.getTime();
|
|
2719
|
+
if (Number.isNaN(time)) return void 0;
|
|
2720
|
+
const YEAR_2000 = 9466848e5;
|
|
2721
|
+
const now = Date.now();
|
|
2722
|
+
if (time < YEAR_2000 || time > now + 366 * 24 * 60 * 60 * 1e3) return void 0;
|
|
2723
|
+
return date.toISOString();
|
|
2724
|
+
}
|
|
2725
|
+
function classifyAssistantError(error) {
|
|
2726
|
+
switch (error) {
|
|
2727
|
+
case "rate_limit":
|
|
2728
|
+
return { kind: "rate_limited" };
|
|
2729
|
+
case "overloaded":
|
|
2730
|
+
case "server_error":
|
|
2731
|
+
return { kind: "server_error" };
|
|
2732
|
+
case "authentication_failed":
|
|
2733
|
+
case "oauth_org_not_allowed":
|
|
2734
|
+
return { kind: "auth_expired" };
|
|
2735
|
+
default:
|
|
2736
|
+
return null;
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
function classifyErrorText(text) {
|
|
2740
|
+
if (!text) return null;
|
|
2741
|
+
const t = text.toLowerCase();
|
|
2742
|
+
if (AUTH_PATTERNS.some((re) => re.test(t))) return { kind: "auth_expired" };
|
|
2743
|
+
if (SERVER_PATTERNS.some((re) => re.test(t))) return { kind: "server_error" };
|
|
2744
|
+
const withoutNegatedCap = t.replace(NEGATED_CAP, "");
|
|
2745
|
+
const capNoun = CAP_PATTERNS.some((re) => re.test(withoutNegatedCap));
|
|
2746
|
+
const rateToken = RATE_PATTERNS.some((re) => re.test(t));
|
|
2747
|
+
if (capNoun) return { kind: "usage_capped" };
|
|
2748
|
+
if (rateToken) return { kind: "rate_limited" };
|
|
2749
|
+
if (BARE_LIMIT.test(t)) return { kind: "usage_capped" };
|
|
2750
|
+
return null;
|
|
2751
|
+
}
|
|
2752
|
+
function classifyRuntimeNoticeText(text) {
|
|
2753
|
+
if (!text) return null;
|
|
2754
|
+
const normalized = text.trim().replace(/\s+/g, " ");
|
|
2755
|
+
if (!normalized) return null;
|
|
2756
|
+
if (/^you(?:'|’)ve hit your (?:session|weekly|usage) limit(?:\s*[·—-]\s*resets?\s+.+)?$/i.test(
|
|
2757
|
+
normalized
|
|
2758
|
+
)) {
|
|
2759
|
+
return { kind: "usage_capped" };
|
|
2760
|
+
}
|
|
2761
|
+
return null;
|
|
2762
|
+
}
|
|
2763
|
+
var AUTH_PATTERNS = [
|
|
2764
|
+
/authentication[_ ]error/,
|
|
2765
|
+
/invalid[_ ]?(x-)?api[_ ]?key/,
|
|
2766
|
+
/invalid bearer token/,
|
|
2767
|
+
/\bunauthorized\b/,
|
|
2768
|
+
/\b401\b/,
|
|
2769
|
+
/oauth token.{0,20}expired/,
|
|
2770
|
+
/(login|token|credential|session).{0,20}(has )?expired/,
|
|
2771
|
+
/please run\s+\/login/,
|
|
2772
|
+
/run `?\/login`?/,
|
|
2773
|
+
/not authenticated/
|
|
2774
|
+
];
|
|
2775
|
+
var SERVER_PATTERNS = [
|
|
2776
|
+
/\b5\d\d\b/,
|
|
2777
|
+
// any 5xx status token (500 / 502 / 503 / 529 …)
|
|
2778
|
+
/overloaded(_error)?/,
|
|
2779
|
+
/\beconnreset\b/,
|
|
2780
|
+
/\betimedout\b/,
|
|
2781
|
+
/socket hang up/,
|
|
2782
|
+
/fetch failed/
|
|
2783
|
+
];
|
|
2784
|
+
var CAP_PATTERNS = [/usage limit/, /weekly limit/, /session limit/, /\bquota\b/];
|
|
2785
|
+
var NEGATED_CAP = /not (your|a) usage limit/g;
|
|
2786
|
+
var RATE_PATTERNS = [/rate[_ ]?limit/, /\b429\b/, /too many requests/];
|
|
2787
|
+
var BARE_LIMIT = /\blimit (reached|exceeded)\b/;
|
|
2788
|
+
|
|
2789
|
+
// src/log-copy.ts
|
|
2790
|
+
function turnFailureCopy(reason, runtime) {
|
|
2791
|
+
const failure = decodeFailureReason(reason);
|
|
2792
|
+
if (failure?.kind === "auth_expired") {
|
|
2793
|
+
if (runtime === "claude-code")
|
|
2794
|
+
return "Claude Code sign-in has expired on this machine. Open Claude Code, run /login, then reply in Cabane to try again.";
|
|
2795
|
+
if (runtime === "codex")
|
|
2796
|
+
return "Codex sign-in has expired on this machine. Run codex login, then reply in Cabane to try again.";
|
|
2797
|
+
return "Sign-in has expired on this machine. Sign in to the coding agent, then reply in Cabane to try again.";
|
|
2798
|
+
}
|
|
2799
|
+
if (failure?.kind === "usage_capped")
|
|
2800
|
+
return failure.resetsAt ? `The usage limit is reached; it resets around ${new Date(failure.resetsAt).toLocaleTimeString()}. Reply in Cabane after it resets.` : "The usage limit is reached. Try again later in Cabane.";
|
|
2801
|
+
if (failure?.kind === "rate_limited")
|
|
2802
|
+
return "The provider is limiting requests. Reply in Cabane to try again.";
|
|
2803
|
+
if (failure?.kind === "server_error")
|
|
2804
|
+
return "The provider had a temporary error. Reply in Cabane to try again.";
|
|
2805
|
+
if (reason?.startsWith("missing_secret:"))
|
|
2806
|
+
return `A required secret is missing: ${humanText(reason.slice(15))}. Add it to ~/.cabane/secrets.json, then reply in Cabane to try again.`;
|
|
2807
|
+
if (reason?.startsWith("runtime_incomplete:"))
|
|
2808
|
+
return "The coding agent installation is incomplete. Reinstall with npm i -g @cabane/companion@latest, then restart the companion.";
|
|
2809
|
+
if (/^(runtime|codex|opencode)_unavailable:/.test(reason ?? ""))
|
|
2810
|
+
return "The coding agent is unavailable on this machine. Enable it here or choose another connector in Cabane.";
|
|
2811
|
+
if (reason?.startsWith("model_unavailable:"))
|
|
2812
|
+
return "The selected model is unavailable. Choose a different model in the agent settings in Cabane.";
|
|
2813
|
+
if (reason?.startsWith("prepare_failed:"))
|
|
2814
|
+
return `Could not prepare the working environment (${humanText(reason.slice(15))}). Check the prepare command in ~/.cabane/config.json.`;
|
|
2815
|
+
if (reason?.startsWith("fetch_failed:"))
|
|
2816
|
+
return `Could not load the conversation (${humanText(reason.slice(13))}). Reply in Cabane to try again.`;
|
|
2817
|
+
if (reason === "timeout_idle")
|
|
2818
|
+
return "The coding agent stopped responding. Reply in Cabane to try again.";
|
|
2819
|
+
if (reason === "timeout_total")
|
|
2820
|
+
return "The turn ran longer than expected and was stopped. Reply in Cabane to continue.";
|
|
2821
|
+
if (reason === "result_error:error_max_turns")
|
|
2822
|
+
return "The coding agent reached its step limit. Reply in Cabane to continue.";
|
|
2823
|
+
if (reason === "no_result" || reason === "empty_result" || reason === "empty_result_unverified")
|
|
2824
|
+
return "The coding agent finished without a response. Reply in Cabane to try again.";
|
|
2825
|
+
if (reason === "session_start_failed")
|
|
2826
|
+
return "The coding agent could not start a session. Restart the companion, then reply in Cabane to try again.";
|
|
2827
|
+
if (reason === "no_terminal")
|
|
2828
|
+
return "The coding agent stopped before confirming the turn was complete. Reply in Cabane to continue.";
|
|
2829
|
+
if (reason === "lease_unconfirmed")
|
|
2830
|
+
return "Could not confirm with Cabane that the turn could start. Check this machine\u2019s connection, then reply in Cabane to try again.";
|
|
2831
|
+
if (reason === "workspace_tools_missing")
|
|
2832
|
+
return "The Cabane workspace tools did not load. Reply in Cabane to try again.";
|
|
2833
|
+
if (reason?.startsWith("seq_floor_unavailable:"))
|
|
2834
|
+
return "The interrupted turn could not safely continue. Reply in Cabane to continue.";
|
|
2835
|
+
if (reason === "result_error:error_max_budget_usd")
|
|
2836
|
+
return "The coding agent reached its spending limit. Check its budget settings before trying again in Cabane.";
|
|
2837
|
+
if (reason === "result_error:error_max_structured_output_retries")
|
|
2838
|
+
return "The coding agent could not produce the requested response format. Check the requested format, then reply in Cabane to try again.";
|
|
2839
|
+
if (reason?.startsWith("result_error:"))
|
|
2840
|
+
return "The coding agent stopped with an error while running. Check its transcript, then reply in Cabane to try again.";
|
|
2841
|
+
if (reason === "setup_failed")
|
|
2842
|
+
return "The turn could not be set up. Check the coding agent setup on this machine, then reply in Cabane to try again.";
|
|
2843
|
+
if (reason === "unexpected_role")
|
|
2844
|
+
return "The message could not start an agent response. Send a new message in Cabane to try again.";
|
|
2845
|
+
if (reason === "lease_lost" || reason?.startsWith("lease_refused:"))
|
|
2846
|
+
return "The turn could not keep its connection to Cabane. Reply in Cabane to try again.";
|
|
2847
|
+
const detail = humanText(reason?.replace(/^error:/, "") ?? "").slice(0, 400);
|
|
2848
|
+
if (/\b(EACCES|EPERM)\b|permission denied/i.test(detail)) {
|
|
2849
|
+
const readable = detail.replace(/\b(EACCES|EPERM)\b/g, "permission denied");
|
|
2850
|
+
return `The coding agent was denied access: ${readable}. Check access permissions for the reported file or command, then reply in Cabane to try again.`;
|
|
2851
|
+
}
|
|
2852
|
+
if (detail)
|
|
2853
|
+
return `The coding agent reported: ${detail}. Check the coding agent setup on this machine, then reply in Cabane to try again.`;
|
|
2854
|
+
return "The coding agent could not finish this turn. Reply in Cabane to try again.";
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
// src/api-error-shape.ts
|
|
2858
|
+
var LEASE_REFUSALS = /* @__PURE__ */ new Set([
|
|
2859
|
+
"dispatch_not_admitted",
|
|
2860
|
+
"turn_already_ended",
|
|
2861
|
+
"turn_belongs_elsewhere"
|
|
2862
|
+
]);
|
|
2863
|
+
function apiErrorCode(err) {
|
|
2864
|
+
if (!(err instanceof ApiError)) return null;
|
|
2865
|
+
const body = err.body;
|
|
2866
|
+
return typeof body === "object" && body !== null && typeof body.error === "string" ? body.error : null;
|
|
2867
|
+
}
|
|
2868
|
+
function leaseRefusal(err) {
|
|
2869
|
+
const code = apiErrorCode(err);
|
|
2870
|
+
if (code && LEASE_REFUSALS.has(code)) return code;
|
|
2871
|
+
return null;
|
|
2872
|
+
}
|
|
2873
|
+
function isWriteFenceRefusal(err) {
|
|
2874
|
+
return err instanceof ApiError && err.status === 409 && apiErrorCode(err) === "not_running";
|
|
2875
|
+
}
|
|
2876
|
+
var ERROR_BODY_LOG_CAP = 2e3;
|
|
2877
|
+
function describeErrorBody(body) {
|
|
2878
|
+
if (body === void 0 || body === null) return void 0;
|
|
2879
|
+
let text;
|
|
2880
|
+
if (typeof body === "string") {
|
|
2881
|
+
text = body;
|
|
2882
|
+
} else {
|
|
2883
|
+
try {
|
|
2884
|
+
text = JSON.stringify(body);
|
|
2885
|
+
} catch {
|
|
2886
|
+
text = String(body);
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
if (text.length === 0) return void 0;
|
|
2890
|
+
return text.length > ERROR_BODY_LOG_CAP ? `${text.slice(0, ERROR_BODY_LOG_CAP)}\u2026` : text;
|
|
2891
|
+
}
|
|
2892
|
+
function apiErrorLogFields(err) {
|
|
2893
|
+
const fields = {
|
|
2894
|
+
err: err instanceof Error ? err.message : String(err)
|
|
2895
|
+
};
|
|
2896
|
+
if (err instanceof ApiError) {
|
|
2897
|
+
fields.status = err.status;
|
|
2898
|
+
const body = describeErrorBody(err.body);
|
|
2899
|
+
if (body !== void 0) fields.responseBody = body;
|
|
2900
|
+
}
|
|
2901
|
+
return fields;
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2527
2904
|
// src/boot-id.ts
|
|
2528
2905
|
import { randomUUID } from "crypto";
|
|
2529
2906
|
var bootId = randomUUID();
|
|
@@ -2548,6 +2925,7 @@ var CabaneApi = class {
|
|
|
2548
2925
|
// this guard two overlapping drains could each pick up the same queued entry
|
|
2549
2926
|
// and double-send it (the server dedupes, but the wasted POSTs aren't free).
|
|
2550
2927
|
draining = false;
|
|
2928
|
+
lastDeliveryWarning = 0;
|
|
2551
2929
|
get base() {
|
|
2552
2930
|
return this.opts.baseUrl.endsWith("/") ? this.opts.baseUrl.slice(0, -1) : this.opts.baseUrl;
|
|
2553
2931
|
}
|
|
@@ -2625,7 +3003,7 @@ var CabaneApi = class {
|
|
|
2625
3003
|
if (signal?.aborted || isAbortError(err)) throw err;
|
|
2626
3004
|
if (!isRetryable(err)) throw err;
|
|
2627
3005
|
outbox.persist({ enqueuedAt: Date.now(), turnId, seq, method: "POST", path, body, kind });
|
|
2628
|
-
this.opts.log?.
|
|
3006
|
+
this.opts.log?.debug(
|
|
2629
3007
|
{ kind, turnId, seq, err: err instanceof Error ? err.message : String(err) },
|
|
2630
3008
|
"companion: commit queued to outbox after transient failure (will drain when the API returns)"
|
|
2631
3009
|
);
|
|
@@ -2653,13 +3031,21 @@ var CabaneApi = class {
|
|
|
2653
3031
|
} catch (err) {
|
|
2654
3032
|
if (err instanceof ApiError && err.status >= 400 && err.status < 500) {
|
|
2655
3033
|
this.opts.log?.warn(
|
|
2656
|
-
{ kind: entry.kind, turnId: entry.turnId, seq: entry.seq,
|
|
2657
|
-
"companion
|
|
3034
|
+
{ kind: entry.kind, turnId: entry.turnId, seq: entry.seq, ...apiErrorLogFields(err) },
|
|
3035
|
+
"Cabane rejected a saved update; it could not be delivered. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
2658
3036
|
);
|
|
2659
3037
|
outbox.remove(entry.turnId, entry.seq);
|
|
2660
3038
|
progressed = true;
|
|
2661
3039
|
continue;
|
|
2662
3040
|
}
|
|
3041
|
+
const now = Date.now();
|
|
3042
|
+
if (now - entry.enqueuedAt >= 12e4 && now - this.lastDeliveryWarning >= 12e4) {
|
|
3043
|
+
this.lastDeliveryWarning = now;
|
|
3044
|
+
this.opts.log?.warn(
|
|
3045
|
+
{ ...apiErrorLogFields(err) },
|
|
3046
|
+
"Updates have been waiting to reach Cabane for at least two minutes; still trying. Check the connection to Cabane."
|
|
3047
|
+
);
|
|
3048
|
+
}
|
|
2663
3049
|
break;
|
|
2664
3050
|
}
|
|
2665
3051
|
}
|
|
@@ -2861,7 +3247,7 @@ var CabaneApi = class {
|
|
|
2861
3247
|
body,
|
|
2862
3248
|
kind: "active-run"
|
|
2863
3249
|
});
|
|
2864
|
-
this.opts.log?.
|
|
3250
|
+
this.opts.log?.debug(
|
|
2865
3251
|
{ conversationId, agentId, turnId },
|
|
2866
3252
|
"companion: settle queued behind this turn's undelivered commits (drains in order)"
|
|
2867
3253
|
);
|
|
@@ -2885,7 +3271,7 @@ var CabaneApi = class {
|
|
|
2885
3271
|
body,
|
|
2886
3272
|
kind: "active-run"
|
|
2887
3273
|
});
|
|
2888
|
-
this.opts.log?.
|
|
3274
|
+
this.opts.log?.debug(
|
|
2889
3275
|
{ conversationId, agentId, err: err instanceof Error ? err.message : String(err) },
|
|
2890
3276
|
"companion: active-run write queued to outbox after transient failure (will drain when the API returns)"
|
|
2891
3277
|
);
|
|
@@ -3293,44 +3679,44 @@ function noResume() {
|
|
|
3293
3679
|
var TURN_PROTOCOL_VERSION = 1;
|
|
3294
3680
|
|
|
3295
3681
|
// packages/agent-runtime/src/host-policy.ts
|
|
3296
|
-
import { z as
|
|
3297
|
-
var hostPolicySchema =
|
|
3682
|
+
import { z as z5 } from "zod";
|
|
3683
|
+
var hostPolicySchema = z5.object({
|
|
3298
3684
|
// Host filesystem + shell: `Bash`/`Read`/`Write`/`Edit`/`Glob`/`Grep`, the
|
|
3299
3685
|
// notebook read/write pair, git-worktree tools, and the `REPL` host code-exec
|
|
3300
3686
|
// tool. Off under the locked assistant surface (today's `DISALLOWED_TOOLS`);
|
|
3301
3687
|
// on under `coding` mode.
|
|
3302
|
-
hostFs:
|
|
3688
|
+
hostFs: z5.boolean(),
|
|
3303
3689
|
// Public web (today's `WebSearch` / `WebFetch`, the `DEFAULT_WEB_TOOLS`). Public
|
|
3304
3690
|
// web, not host reach — granted by default today, but expressible as a grant.
|
|
3305
|
-
web:
|
|
3691
|
+
web: z5.boolean(),
|
|
3306
3692
|
// Browser automation (the Playwright MCP surface). Varies by host: a companion has
|
|
3307
3693
|
// it, the house executor does not (CT230).
|
|
3308
|
-
browser:
|
|
3694
|
+
browser: z5.boolean(),
|
|
3309
3695
|
// User-configured MCP servers permitted. False for the house executor
|
|
3310
3696
|
// (CT227: Cabane agents run no user MCP servers), true for a personal companion.
|
|
3311
|
-
userMcp:
|
|
3697
|
+
userMcp: z5.boolean(),
|
|
3312
3698
|
// Subagent / sub-dispatch (`Task` / `Agent`). A per-host GRANT (see the CT261
|
|
3313
3699
|
// amendment above): `false` on the locked assistant/house surface (banned via
|
|
3314
3700
|
// `DISALLOWED_TOOLS`), `true` under coding / custom where the fan-out is left to
|
|
3315
3701
|
// the coding allow-hook (which reaches sub-agent tool calls, CT680) or the user
|
|
3316
3702
|
// allowlist. The subagent completes within the turn, so
|
|
3317
3703
|
// it's not the turn-model invariant `scheduling` is.
|
|
3318
|
-
subagents:
|
|
3704
|
+
subagents: z5.boolean(),
|
|
3319
3705
|
// ── Hard platform invariants — always denied, never granted ────────────────
|
|
3320
3706
|
// Deferred re-invocation / scheduling (`ScheduleWakeup`, the `Cron*` / `Task*`
|
|
3321
3707
|
// families, `Monitor`, `Workflow`, …). A turn is one query that resolves when
|
|
3322
3708
|
// `result` fires; a scheduled callback fires after the reply window has closed
|
|
3323
3709
|
// and strands the agent (the CT155/CT156 rule).
|
|
3324
|
-
scheduling:
|
|
3710
|
+
scheduling: z5.literal("never"),
|
|
3325
3711
|
// Human-facing UI prompts (`AskUserQuestion`). A Cabane conversation has no
|
|
3326
3712
|
// handler to answer a structured prompt, so the call hangs the turn
|
|
3327
3713
|
// (`UNSUPPORTED_TOOLS`). The agent asks in its reply instead.
|
|
3328
|
-
uiPrompts:
|
|
3714
|
+
uiPrompts: z5.literal("never")
|
|
3329
3715
|
});
|
|
3330
3716
|
|
|
3331
3717
|
// packages/agent-runtime/src/turn-event.ts
|
|
3332
|
-
import { z as
|
|
3333
|
-
var turnEventSchema =
|
|
3718
|
+
import { z as z6 } from "zod";
|
|
3719
|
+
var turnEventSchema = z6.discriminatedUnion("type", [
|
|
3334
3720
|
// The runtime's opaque session state, emitted when the adapter learns it (e.g.
|
|
3335
3721
|
// the SDK `system/init` frame). The platform stores `state` verbatim per
|
|
3336
3722
|
// (conversation, agent) and hands it back on the next turn; only the adapter
|
|
@@ -3350,25 +3736,25 @@ var turnEventSchema = z5.discriminatedUnion("type", [
|
|
|
3350
3736
|
// on the companion, after the server committed the manifest). Runtime-neutral: a
|
|
3351
3737
|
// plain boolean, not a runtime-specific reason string (that stays in the
|
|
3352
3738
|
// adapter's `onWarn` log). Additive + optional — an old receiver ignores it.
|
|
3353
|
-
|
|
3354
|
-
type:
|
|
3355
|
-
state:
|
|
3356
|
-
degraded:
|
|
3739
|
+
z6.object({
|
|
3740
|
+
type: z6.literal("session"),
|
|
3741
|
+
state: z6.string(),
|
|
3742
|
+
degraded: z6.boolean().optional()
|
|
3357
3743
|
}),
|
|
3358
3744
|
// One readable thinking summary. Maps `onThinking({ text })`. Transient —
|
|
3359
3745
|
// surfaced live, never persisted as durable content.
|
|
3360
|
-
|
|
3746
|
+
z6.object({ type: z6.literal("thinking"), text: z6.string() }),
|
|
3361
3747
|
// Assistant text. Maps `onAssistantText({ text, final })` — `text`→`body`,
|
|
3362
3748
|
// `final`→`terminal`. `terminal: false` is interim narration (commits as a
|
|
3363
3749
|
// `progress` row); `terminal: true` is the turn's closing reply (commits as
|
|
3364
3750
|
// the `final` row).
|
|
3365
|
-
|
|
3751
|
+
z6.object({ type: z6.literal("text"), body: z6.string(), terminal: z6.boolean() }),
|
|
3366
3752
|
// Runtime/provider-authored prose surfaced alongside a failed turn. Unlike
|
|
3367
3753
|
// `text`, this is not the agent's narration or reply: the pump persists it as
|
|
3368
3754
|
// `runtime_notice`, and the transcript renders it in the shared SystemNote
|
|
3369
3755
|
// voice. Adapters emit it only from positive runtime evidence; the web never
|
|
3370
3756
|
// classifies English strings.
|
|
3371
|
-
|
|
3757
|
+
z6.object({ type: z6.literal("runtime_notice"), body: z6.string() }),
|
|
3372
3758
|
// A tool-activity transition. Maps `onToolActivity` — `toolUseId`→`id`,
|
|
3373
3759
|
// `toolName`→`name` (already prefix-stripped: `cabane_read`, not
|
|
3374
3760
|
// `mcp__cabane__cabane_read`), `summary` is the short card label. `phase`
|
|
@@ -3383,15 +3769,15 @@ var turnEventSchema = z5.discriminatedUnion("type", [
|
|
|
3383
3769
|
// dropped the prefix; null for a host / built-in tool. The client tags Cabane
|
|
3384
3770
|
// MCP calls (`=== 'cabane'`) with a subtle glyph. Nullable + optional so a
|
|
3385
3771
|
// pre-CT496 producer that never sets it is unaffected (treated as null).
|
|
3386
|
-
|
|
3387
|
-
type:
|
|
3388
|
-
id:
|
|
3389
|
-
name:
|
|
3390
|
-
phase:
|
|
3391
|
-
summary:
|
|
3392
|
-
input:
|
|
3393
|
-
result:
|
|
3394
|
-
mcpServer:
|
|
3772
|
+
z6.object({
|
|
3773
|
+
type: z6.literal("tool"),
|
|
3774
|
+
id: z6.string(),
|
|
3775
|
+
name: z6.string(),
|
|
3776
|
+
phase: z6.enum(["start", "done", "error"]),
|
|
3777
|
+
summary: z6.string(),
|
|
3778
|
+
input: z6.unknown().optional(),
|
|
3779
|
+
result: z6.unknown().optional(),
|
|
3780
|
+
mcpServer: z6.string().nullable().optional()
|
|
3395
3781
|
}),
|
|
3396
3782
|
// The turn's terminal outcome. Maps the `{ ok, reason }` both loops return
|
|
3397
3783
|
// inline. `ok:false` carries a machine reason (`no_session`, an error code);
|
|
@@ -3435,54 +3821,54 @@ var turnEventSchema = z5.discriminatedUnion("type", [
|
|
|
3435
3821
|
// `message_turn_metadata.resolved_model` / `resolved_config`. Optional ⇒
|
|
3436
3822
|
// backward-compatible: an old adapter/companion omits them, a cancel has no result
|
|
3437
3823
|
// event at all, and the columns stay null → the UI shows `—`.
|
|
3438
|
-
|
|
3439
|
-
type:
|
|
3440
|
-
ok:
|
|
3441
|
-
reason:
|
|
3442
|
-
usage:
|
|
3443
|
-
inputTokens:
|
|
3444
|
-
outputTokens:
|
|
3445
|
-
cacheReadTokens:
|
|
3446
|
-
cacheCreationTokens:
|
|
3447
|
-
contextTokens:
|
|
3448
|
-
contextWindow:
|
|
3824
|
+
z6.object({
|
|
3825
|
+
type: z6.literal("result"),
|
|
3826
|
+
ok: z6.boolean(),
|
|
3827
|
+
reason: z6.string().optional(),
|
|
3828
|
+
usage: z6.object({
|
|
3829
|
+
inputTokens: z6.number(),
|
|
3830
|
+
outputTokens: z6.number(),
|
|
3831
|
+
cacheReadTokens: z6.number().optional(),
|
|
3832
|
+
cacheCreationTokens: z6.number().optional(),
|
|
3833
|
+
contextTokens: z6.number().optional(),
|
|
3834
|
+
contextWindow: z6.number().optional()
|
|
3449
3835
|
}).optional(),
|
|
3450
|
-
resolvedModel:
|
|
3451
|
-
resolvedConfig:
|
|
3452
|
-
effort:
|
|
3453
|
-
thinking:
|
|
3454
|
-
reasoningEffort:
|
|
3836
|
+
resolvedModel: z6.string().optional(),
|
|
3837
|
+
resolvedConfig: z6.object({
|
|
3838
|
+
effort: z6.string().optional(),
|
|
3839
|
+
thinking: z6.string().optional(),
|
|
3840
|
+
reasoningEffort: z6.string().optional()
|
|
3455
3841
|
}).optional(),
|
|
3456
3842
|
// CT1275: the harness-reported MCP inventory from this turn's init frame.
|
|
3457
3843
|
// This is deliberately diagnostic-only: names, statuses and a count, never
|
|
3458
3844
|
// server definitions, credentials or session ids. `initReceived:false`
|
|
3459
3845
|
// distinguishes a missing init frame from a real empty inventory.
|
|
3460
|
-
mcpInventory:
|
|
3461
|
-
initReceived:
|
|
3462
|
-
servers:
|
|
3463
|
-
toolCount:
|
|
3846
|
+
mcpInventory: z6.object({
|
|
3847
|
+
initReceived: z6.boolean(),
|
|
3848
|
+
servers: z6.array(z6.object({ name: z6.string(), status: z6.string() })),
|
|
3849
|
+
toolCount: z6.number().int().nonnegative()
|
|
3464
3850
|
}).optional()
|
|
3465
3851
|
})
|
|
3466
3852
|
]);
|
|
3467
3853
|
|
|
3468
3854
|
// packages/agent-runtime/src/turn-diagnostics.ts
|
|
3469
|
-
import { z as
|
|
3470
|
-
var turnResultReasonSchema =
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3855
|
+
import { z as z7 } from "zod";
|
|
3856
|
+
var turnResultReasonSchema = z7.discriminatedUnion("kind", [
|
|
3857
|
+
z7.object({ kind: z7.literal("usage_capped"), resetsAt: z7.string().optional() }),
|
|
3858
|
+
z7.object({ kind: z7.literal("rate_limited") }),
|
|
3859
|
+
z7.object({ kind: z7.literal("server_error") }),
|
|
3860
|
+
z7.object({ kind: z7.literal("auth_expired") }),
|
|
3861
|
+
z7.object({ kind: z7.literal("no_result") }),
|
|
3862
|
+
z7.object({ kind: z7.literal("empty_result") }),
|
|
3863
|
+
z7.object({ kind: z7.literal("empty_result_unverified") }),
|
|
3864
|
+
z7.object({ kind: z7.literal("timeout_idle") }),
|
|
3865
|
+
z7.object({ kind: z7.literal("timeout_total") }),
|
|
3866
|
+
z7.object({ kind: z7.literal("cancelled") }),
|
|
3867
|
+
z7.object({ kind: z7.literal("lease_lost") }),
|
|
3868
|
+
z7.object({ kind: z7.literal("skipped") }),
|
|
3869
|
+
z7.object({ kind: z7.literal("session_start_failed") }),
|
|
3870
|
+
z7.object({ kind: z7.literal("workspace_tools_missing") }),
|
|
3871
|
+
z7.object({ kind: z7.literal("runtime_error") })
|
|
3486
3872
|
]);
|
|
3487
3873
|
var turnOutcomes = ["success", "failure", "cancelled", "skipped"];
|
|
3488
3874
|
var turnSessionModes = ["fresh", "resumed", "degraded"];
|
|
@@ -3494,150 +3880,28 @@ var turnFinalSources = [
|
|
|
3494
3880
|
"marker",
|
|
3495
3881
|
"none"
|
|
3496
3882
|
];
|
|
3497
|
-
var turnDiagnosticsSchema =
|
|
3498
|
-
outcome:
|
|
3883
|
+
var turnDiagnosticsSchema = z7.object({
|
|
3884
|
+
outcome: z7.enum(turnOutcomes),
|
|
3499
3885
|
resultReason: turnResultReasonSchema.nullable(),
|
|
3500
|
-
sessionMode:
|
|
3501
|
-
sessionFingerprint:
|
|
3502
|
-
eventCounts:
|
|
3503
|
-
session:
|
|
3504
|
-
text:
|
|
3505
|
-
runtime_notice:
|
|
3506
|
-
thinking:
|
|
3507
|
-
tool:
|
|
3508
|
-
result:
|
|
3886
|
+
sessionMode: z7.enum(turnSessionModes),
|
|
3887
|
+
sessionFingerprint: z7.string().regex(/^[a-f0-9]{16}$/).nullable(),
|
|
3888
|
+
eventCounts: z7.object({
|
|
3889
|
+
session: z7.number().int().nonnegative(),
|
|
3890
|
+
text: z7.number().int().nonnegative(),
|
|
3891
|
+
runtime_notice: z7.number().int().nonnegative(),
|
|
3892
|
+
thinking: z7.number().int().nonnegative(),
|
|
3893
|
+
tool: z7.number().int().nonnegative(),
|
|
3894
|
+
result: z7.number().int().nonnegative()
|
|
3509
3895
|
}),
|
|
3510
|
-
runtimeResultKind:
|
|
3511
|
-
finalSource:
|
|
3512
|
-
mcpInventory:
|
|
3513
|
-
initReceived:
|
|
3514
|
-
servers:
|
|
3515
|
-
toolCount:
|
|
3896
|
+
runtimeResultKind: z7.enum(turnRuntimeResultKinds).nullable(),
|
|
3897
|
+
finalSource: z7.enum(turnFinalSources),
|
|
3898
|
+
mcpInventory: z7.object({
|
|
3899
|
+
initReceived: z7.boolean(),
|
|
3900
|
+
servers: z7.array(z7.object({ name: z7.string(), status: z7.string() })),
|
|
3901
|
+
toolCount: z7.number().int().nonnegative()
|
|
3516
3902
|
}).optional()
|
|
3517
3903
|
});
|
|
3518
3904
|
|
|
3519
|
-
// packages/agent-runtime/src/failure.ts
|
|
3520
|
-
import { z as z7 } from "zod";
|
|
3521
|
-
var turnFailureSchema = z7.discriminatedUnion("kind", [
|
|
3522
|
-
z7.object({ kind: z7.literal("usage_capped"), resetsAt: z7.string().optional() }),
|
|
3523
|
-
z7.object({ kind: z7.literal("rate_limited") }),
|
|
3524
|
-
z7.object({ kind: z7.literal("server_error") }),
|
|
3525
|
-
z7.object({ kind: z7.literal("auth_expired") })
|
|
3526
|
-
]);
|
|
3527
|
-
var USAGE_CAPPED = "usage_capped";
|
|
3528
|
-
var RATE_LIMITED = "rate_limited";
|
|
3529
|
-
var SERVER_ERROR = "server_error";
|
|
3530
|
-
var AUTH_EXPIRED = "auth_expired";
|
|
3531
|
-
function encodeFailureReason(failure) {
|
|
3532
|
-
switch (failure.kind) {
|
|
3533
|
-
case "auth_expired":
|
|
3534
|
-
return AUTH_EXPIRED;
|
|
3535
|
-
case "server_error":
|
|
3536
|
-
return SERVER_ERROR;
|
|
3537
|
-
case "rate_limited":
|
|
3538
|
-
return RATE_LIMITED;
|
|
3539
|
-
case "usage_capped":
|
|
3540
|
-
return failure.resetsAt ? `${USAGE_CAPPED}:${failure.resetsAt}` : USAGE_CAPPED;
|
|
3541
|
-
}
|
|
3542
|
-
}
|
|
3543
|
-
function decodeFailureReason(reason) {
|
|
3544
|
-
if (!reason) return null;
|
|
3545
|
-
if (reason === AUTH_EXPIRED) return { kind: "auth_expired" };
|
|
3546
|
-
if (reason === SERVER_ERROR) return { kind: "server_error" };
|
|
3547
|
-
if (reason === RATE_LIMITED) return { kind: "rate_limited" };
|
|
3548
|
-
if (reason === USAGE_CAPPED) return { kind: "usage_capped" };
|
|
3549
|
-
if (reason.startsWith(`${USAGE_CAPPED}:`)) {
|
|
3550
|
-
const iso = reason.slice(USAGE_CAPPED.length + 1);
|
|
3551
|
-
return isValidIso(iso) ? { kind: "usage_capped", resetsAt: iso } : { kind: "usage_capped" };
|
|
3552
|
-
}
|
|
3553
|
-
return null;
|
|
3554
|
-
}
|
|
3555
|
-
function isValidIso(value) {
|
|
3556
|
-
if (!value) return false;
|
|
3557
|
-
const ms = Date.parse(value);
|
|
3558
|
-
return Number.isFinite(ms);
|
|
3559
|
-
}
|
|
3560
|
-
function quotaForFailure(failure) {
|
|
3561
|
-
if (failure.kind === "auth_expired") return { quotaState: "auth_expired", limitedUntil: null };
|
|
3562
|
-
if (failure.kind === "usage_capped")
|
|
3563
|
-
return { quotaState: "limited", limitedUntil: failure.resetsAt ?? null };
|
|
3564
|
-
return null;
|
|
3565
|
-
}
|
|
3566
|
-
function resetsAtToIso(resetsAt) {
|
|
3567
|
-
if (typeof resetsAt !== "number" || !Number.isFinite(resetsAt) || resetsAt <= 0) return void 0;
|
|
3568
|
-
const ms = resetsAt < 1e12 ? resetsAt * 1e3 : resetsAt;
|
|
3569
|
-
const date = new Date(ms);
|
|
3570
|
-
const time = date.getTime();
|
|
3571
|
-
if (Number.isNaN(time)) return void 0;
|
|
3572
|
-
const YEAR_2000 = 9466848e5;
|
|
3573
|
-
const now = Date.now();
|
|
3574
|
-
if (time < YEAR_2000 || time > now + 366 * 24 * 60 * 60 * 1e3) return void 0;
|
|
3575
|
-
return date.toISOString();
|
|
3576
|
-
}
|
|
3577
|
-
function classifyAssistantError(error) {
|
|
3578
|
-
switch (error) {
|
|
3579
|
-
case "rate_limit":
|
|
3580
|
-
return { kind: "rate_limited" };
|
|
3581
|
-
case "overloaded":
|
|
3582
|
-
case "server_error":
|
|
3583
|
-
return { kind: "server_error" };
|
|
3584
|
-
case "authentication_failed":
|
|
3585
|
-
case "oauth_org_not_allowed":
|
|
3586
|
-
return { kind: "auth_expired" };
|
|
3587
|
-
default:
|
|
3588
|
-
return null;
|
|
3589
|
-
}
|
|
3590
|
-
}
|
|
3591
|
-
function classifyErrorText(text) {
|
|
3592
|
-
if (!text) return null;
|
|
3593
|
-
const t = text.toLowerCase();
|
|
3594
|
-
if (AUTH_PATTERNS.some((re) => re.test(t))) return { kind: "auth_expired" };
|
|
3595
|
-
if (SERVER_PATTERNS.some((re) => re.test(t))) return { kind: "server_error" };
|
|
3596
|
-
const withoutNegatedCap = t.replace(NEGATED_CAP, "");
|
|
3597
|
-
const capNoun = CAP_PATTERNS.some((re) => re.test(withoutNegatedCap));
|
|
3598
|
-
const rateToken = RATE_PATTERNS.some((re) => re.test(t));
|
|
3599
|
-
if (capNoun) return { kind: "usage_capped" };
|
|
3600
|
-
if (rateToken) return { kind: "rate_limited" };
|
|
3601
|
-
if (BARE_LIMIT.test(t)) return { kind: "usage_capped" };
|
|
3602
|
-
return null;
|
|
3603
|
-
}
|
|
3604
|
-
function classifyRuntimeNoticeText(text) {
|
|
3605
|
-
if (!text) return null;
|
|
3606
|
-
const normalized = text.trim().replace(/\s+/g, " ");
|
|
3607
|
-
if (!normalized) return null;
|
|
3608
|
-
if (/^you(?:'|’)ve hit your (?:session|weekly|usage) limit(?:\s*[·—-]\s*resets?\s+.+)?$/i.test(
|
|
3609
|
-
normalized
|
|
3610
|
-
)) {
|
|
3611
|
-
return { kind: "usage_capped" };
|
|
3612
|
-
}
|
|
3613
|
-
return null;
|
|
3614
|
-
}
|
|
3615
|
-
var AUTH_PATTERNS = [
|
|
3616
|
-
/authentication[_ ]error/,
|
|
3617
|
-
/invalid[_ ]?(x-)?api[_ ]?key/,
|
|
3618
|
-
/invalid bearer token/,
|
|
3619
|
-
/\bunauthorized\b/,
|
|
3620
|
-
/\b401\b/,
|
|
3621
|
-
/oauth token.{0,20}expired/,
|
|
3622
|
-
/(login|token|credential|session).{0,20}(has )?expired/,
|
|
3623
|
-
/please run\s+\/login/,
|
|
3624
|
-
/run `?\/login`?/,
|
|
3625
|
-
/not authenticated/
|
|
3626
|
-
];
|
|
3627
|
-
var SERVER_PATTERNS = [
|
|
3628
|
-
/\b5\d\d\b/,
|
|
3629
|
-
// any 5xx status token (500 / 502 / 503 / 529 …)
|
|
3630
|
-
/overloaded(_error)?/,
|
|
3631
|
-
/\beconnreset\b/,
|
|
3632
|
-
/\betimedout\b/,
|
|
3633
|
-
/socket hang up/,
|
|
3634
|
-
/fetch failed/
|
|
3635
|
-
];
|
|
3636
|
-
var CAP_PATTERNS = [/usage limit/, /weekly limit/, /session limit/, /\bquota\b/];
|
|
3637
|
-
var NEGATED_CAP = /not (your|a) usage limit/g;
|
|
3638
|
-
var RATE_PATTERNS = [/rate[_ ]?limit/, /\b429\b/, /too many requests/];
|
|
3639
|
-
var BARE_LIMIT = /\blimit (reached|exceeded)\b/;
|
|
3640
|
-
|
|
3641
3905
|
// packages/agent-runtime/src/turn-diagnostics-normalize.ts
|
|
3642
3906
|
function normalizeTurnResultReason(reason) {
|
|
3643
3907
|
const classified = decodeFailureReason(reason);
|
|
@@ -4355,11 +4619,6 @@ function buildClaudeCodeOptions(req, augment) {
|
|
|
4355
4619
|
const freshReason = "fresh" in resumeDecision ? resumeDecision.reason : void 0;
|
|
4356
4620
|
const devControlsAutoMemory = req.local.claudeCode?.autoMemory === true;
|
|
4357
4621
|
const model = parseClaudeCodeModel(config.model);
|
|
4358
|
-
if (model === null) {
|
|
4359
|
-
console.warn(
|
|
4360
|
-
`[agent-runtime/claude-code] CT823: dispatching a subscription turn with NO concrete model (config.model=${JSON.stringify(config.model)}) \u2014 the SDK will fall back to its bundled default. This should be unreachable post-CT823; the model resolver should have supplied a concrete house default.`
|
|
4361
|
-
);
|
|
4362
|
-
}
|
|
4363
4622
|
const base = {
|
|
4364
4623
|
// model / effort: `model` is pinned only when the config names a real one —
|
|
4365
4624
|
// omitted for "let it choose" (see `parseClaudeCodeModel`), so the SDK picks
|
|
@@ -4678,6 +4937,11 @@ function createClaudeCodeAdapter(deps = {}) {
|
|
|
4678
4937
|
degraded = fresh.degraded;
|
|
4679
4938
|
built = buildClaudeCodeOptions(req, deps.augmentOptions);
|
|
4680
4939
|
}
|
|
4940
|
+
if (built.options.model === void 0) {
|
|
4941
|
+
deps.onWarn?.("Claude Code is using its default model because no model was resolved", {
|
|
4942
|
+
model: req.config.model
|
|
4943
|
+
});
|
|
4944
|
+
}
|
|
4681
4945
|
yield* runWithResumeRecovery(
|
|
4682
4946
|
queryFn,
|
|
4683
4947
|
req,
|
|
@@ -4710,7 +4974,7 @@ var HEALTHY_MCP_INVENTORY = {
|
|
|
4710
4974
|
servers: [{ name: "cabane", status: "connected" }],
|
|
4711
4975
|
toolCount: 1
|
|
4712
4976
|
};
|
|
4713
|
-
function makeRequest(
|
|
4977
|
+
function makeRequest(overrides2 = {}) {
|
|
4714
4978
|
return {
|
|
4715
4979
|
systemPrompt: "system",
|
|
4716
4980
|
prompt: "hi there",
|
|
@@ -4723,7 +4987,7 @@ function makeRequest(overrides = {}) {
|
|
|
4723
4987
|
cabane: { mcpUrl: "https://cabane.test/api/mcp", bearer: "tok", activeConversationId: "conv" },
|
|
4724
4988
|
local: { cwd: CWD },
|
|
4725
4989
|
extra: { mcpServers: {} },
|
|
4726
|
-
...
|
|
4990
|
+
...overrides2
|
|
4727
4991
|
};
|
|
4728
4992
|
}
|
|
4729
4993
|
var init = (sessionId, model) => ({
|
|
@@ -5911,7 +6175,7 @@ var COMPANION_POLICY = {
|
|
|
5911
6175
|
uiPrompts: "never"
|
|
5912
6176
|
};
|
|
5913
6177
|
var DIR = "/env/here";
|
|
5914
|
-
function makeRequest2(
|
|
6178
|
+
function makeRequest2(overrides2 = {}) {
|
|
5915
6179
|
return {
|
|
5916
6180
|
systemPrompt: "system",
|
|
5917
6181
|
prompt: "hi there",
|
|
@@ -5922,7 +6186,7 @@ function makeRequest2(overrides = {}) {
|
|
|
5922
6186
|
cabane: { mcpUrl: "https://cabane.test/api/mcp", bearer: "tok", activeConversationId: "conv" },
|
|
5923
6187
|
local: { cwd: DIR },
|
|
5924
6188
|
extra: { mcpServers: {} },
|
|
5925
|
-
...
|
|
6189
|
+
...overrides2
|
|
5926
6190
|
};
|
|
5927
6191
|
}
|
|
5928
6192
|
var textPart = (id, text) => ({
|
|
@@ -6736,11 +7000,6 @@ function buildRunSpec2(req, resumeThreadId, instructionsFile = null, threadPromp
|
|
|
6736
7000
|
const dialect = readCodexDialect(config.runtimeOptions);
|
|
6737
7001
|
const baseInstructionsFile = !policy.hostFs && instructionsFile ? instructionsFile : null;
|
|
6738
7002
|
const model = config.model ? parseCodexModel(config.model) : null;
|
|
6739
|
-
if (model === null) {
|
|
6740
|
-
console.warn(
|
|
6741
|
-
`[agent-runtime/codex] CT823: dispatching a subscription turn with NO concrete model (config.model=${JSON.stringify(config.model)}) \u2014 Codex will fall back to its own default. This should be unreachable post-CT823; the model resolver should have supplied a concrete house default.`
|
|
6742
|
-
);
|
|
6743
|
-
}
|
|
6744
7003
|
const promptFingerprint = fingerprintPrompt(req.systemPrompt);
|
|
6745
7004
|
const threadHasThisPrompt = resumeThreadId !== null && threadPromptFingerprint !== null && threadPromptFingerprint === promptFingerprint;
|
|
6746
7005
|
const promptRidesInput = baseInstructionsFile === null && !threadHasThisPrompt;
|
|
@@ -6937,6 +7196,11 @@ function createCodexAdapter(deps = {}) {
|
|
|
6937
7196
|
instructions?.path ?? null,
|
|
6938
7197
|
threadPromptFingerprint
|
|
6939
7198
|
);
|
|
7199
|
+
if (spec.model === null) {
|
|
7200
|
+
deps.onWarn?.("Codex is using its default model because no model was resolved", {
|
|
7201
|
+
model: req.config.model
|
|
7202
|
+
});
|
|
7203
|
+
}
|
|
6940
7204
|
const result = await transport.run(spec, signal);
|
|
6941
7205
|
if (signal.aborted) return;
|
|
6942
7206
|
yield* decodeCodexStream(result.events, {
|
|
@@ -6988,7 +7252,7 @@ var COMPANION_POLICY2 = {
|
|
|
6988
7252
|
};
|
|
6989
7253
|
var DIR2 = "/env/here";
|
|
6990
7254
|
var PROMPT = "system";
|
|
6991
|
-
function makeRequest3(
|
|
7255
|
+
function makeRequest3(overrides2 = {}) {
|
|
6992
7256
|
return {
|
|
6993
7257
|
systemPrompt: PROMPT,
|
|
6994
7258
|
prompt: "hi there",
|
|
@@ -7004,7 +7268,7 @@ function makeRequest3(overrides = {}) {
|
|
|
7004
7268
|
cabane: { mcpUrl: "https://cabane.test/api/mcp", bearer: "tok", activeConversationId: "conv" },
|
|
7005
7269
|
local: { cwd: DIR2 },
|
|
7006
7270
|
extra: { mcpServers: {} },
|
|
7007
|
-
...
|
|
7271
|
+
...overrides2
|
|
7008
7272
|
};
|
|
7009
7273
|
}
|
|
7010
7274
|
var threadStarted = (threadId) => ({
|
|
@@ -8204,53 +8468,6 @@ async function writeCodexInstructionsFile(contents) {
|
|
|
8204
8468
|
};
|
|
8205
8469
|
}
|
|
8206
8470
|
|
|
8207
|
-
// src/api-error-shape.ts
|
|
8208
|
-
var LEASE_REFUSALS = /* @__PURE__ */ new Set([
|
|
8209
|
-
"dispatch_not_admitted",
|
|
8210
|
-
"turn_already_ended",
|
|
8211
|
-
"turn_belongs_elsewhere"
|
|
8212
|
-
]);
|
|
8213
|
-
function apiErrorCode(err) {
|
|
8214
|
-
if (!(err instanceof ApiError)) return null;
|
|
8215
|
-
const body = err.body;
|
|
8216
|
-
return typeof body === "object" && body !== null && typeof body.error === "string" ? body.error : null;
|
|
8217
|
-
}
|
|
8218
|
-
function leaseRefusal(err) {
|
|
8219
|
-
const code = apiErrorCode(err);
|
|
8220
|
-
if (code && LEASE_REFUSALS.has(code)) return code;
|
|
8221
|
-
return null;
|
|
8222
|
-
}
|
|
8223
|
-
function isWriteFenceRefusal(err) {
|
|
8224
|
-
return err instanceof ApiError && err.status === 409 && apiErrorCode(err) === "not_running";
|
|
8225
|
-
}
|
|
8226
|
-
var ERROR_BODY_LOG_CAP = 2e3;
|
|
8227
|
-
function describeErrorBody(body) {
|
|
8228
|
-
if (body === void 0 || body === null) return void 0;
|
|
8229
|
-
let text;
|
|
8230
|
-
if (typeof body === "string") {
|
|
8231
|
-
text = body;
|
|
8232
|
-
} else {
|
|
8233
|
-
try {
|
|
8234
|
-
text = JSON.stringify(body);
|
|
8235
|
-
} catch {
|
|
8236
|
-
text = String(body);
|
|
8237
|
-
}
|
|
8238
|
-
}
|
|
8239
|
-
if (text.length === 0) return void 0;
|
|
8240
|
-
return text.length > ERROR_BODY_LOG_CAP ? `${text.slice(0, ERROR_BODY_LOG_CAP)}\u2026` : text;
|
|
8241
|
-
}
|
|
8242
|
-
function apiErrorLogFields(err) {
|
|
8243
|
-
const fields = {
|
|
8244
|
-
err: err instanceof Error ? err.message : String(err)
|
|
8245
|
-
};
|
|
8246
|
-
if (err instanceof ApiError) {
|
|
8247
|
-
fields.status = err.status;
|
|
8248
|
-
const body = describeErrorBody(err.body);
|
|
8249
|
-
if (body !== void 0) fields.responseBody = body;
|
|
8250
|
-
}
|
|
8251
|
-
return fields;
|
|
8252
|
-
}
|
|
8253
|
-
|
|
8254
8471
|
// src/turn-seq-floor.ts
|
|
8255
8472
|
var SeqFloorUnavailable = class extends Error {
|
|
8256
8473
|
constructor(detail) {
|
|
@@ -8266,7 +8483,7 @@ function resolveSeqFloor(sources, ctx) {
|
|
|
8266
8483
|
throw new SeqFloorUnavailable("server sent no committed floor for a resumed turn");
|
|
8267
8484
|
}
|
|
8268
8485
|
const floor = Math.max(serverFloor, outboxFloor);
|
|
8269
|
-
ctx.log.
|
|
8486
|
+
ctx.log.debug(
|
|
8270
8487
|
{ turnId: ctx.turnId, floor, outboxFloor, serverFloor },
|
|
8271
8488
|
"companion: resumed turn \u2014 seq counter seeded above its committed high-water mark"
|
|
8272
8489
|
);
|
|
@@ -8276,7 +8493,7 @@ function readOutboxFloor(read, turnId, log) {
|
|
|
8276
8493
|
try {
|
|
8277
8494
|
return read(turnId);
|
|
8278
8495
|
} catch (err) {
|
|
8279
|
-
log.
|
|
8496
|
+
log.debug(
|
|
8280
8497
|
{ turnId, err: err instanceof Error ? err.message : String(err) },
|
|
8281
8498
|
"companion: the on-disk outbox floor is unreadable; refusing to resume"
|
|
8282
8499
|
);
|
|
@@ -8518,7 +8735,10 @@ var TurnCommitter = class {
|
|
|
8518
8735
|
constructor(deps) {
|
|
8519
8736
|
this.deps = deps;
|
|
8520
8737
|
this.onError = (err, hook) => {
|
|
8521
|
-
deps.log.warn(
|
|
8738
|
+
deps.log.warn(
|
|
8739
|
+
{ ...apiErrorLogFields(err), hook },
|
|
8740
|
+
`Couldn't save ${String(deps.log.bindings().agentName ?? "the agent")}'s ${hook === "text" ? "reply" : "reply or activity"} to Cabane. This companion may need an update: npm i -g @cabane/companion@latest`
|
|
8741
|
+
);
|
|
8522
8742
|
deps.onCommitFailed?.(err);
|
|
8523
8743
|
};
|
|
8524
8744
|
const commit = {
|
|
@@ -8653,8 +8873,8 @@ async function postIncompleteNotice(ctx, harness, resolution) {
|
|
|
8653
8873
|
return true;
|
|
8654
8874
|
} catch (err) {
|
|
8655
8875
|
ctx.log.warn(
|
|
8656
|
-
{
|
|
8657
|
-
"
|
|
8876
|
+
{ ...apiErrorLogFields(err) },
|
|
8877
|
+
"Couldn't save the explanation to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
8658
8878
|
);
|
|
8659
8879
|
return false;
|
|
8660
8880
|
}
|
|
@@ -8664,7 +8884,7 @@ async function checkBundledBinary(ctx) {
|
|
|
8664
8884
|
if (!harness) return null;
|
|
8665
8885
|
const resolution = resolve(ctx, harness);
|
|
8666
8886
|
if (resolution.status === "present") return null;
|
|
8667
|
-
ctx.log.
|
|
8887
|
+
ctx.log.debug(
|
|
8668
8888
|
{
|
|
8669
8889
|
runtime: harness,
|
|
8670
8890
|
status: resolution.status,
|
|
@@ -8684,7 +8904,7 @@ async function reclassifyThrow(ctx, opts) {
|
|
|
8684
8904
|
if (!harness || opts.aborted) return null;
|
|
8685
8905
|
const resolution = resolve(ctx, harness);
|
|
8686
8906
|
if (resolution.status === "present") return null;
|
|
8687
|
-
ctx.log.
|
|
8907
|
+
ctx.log.debug(
|
|
8688
8908
|
{
|
|
8689
8909
|
runtime: harness,
|
|
8690
8910
|
status: resolution.status,
|
|
@@ -8881,13 +9101,13 @@ var TurnExecution = class {
|
|
|
8881
9101
|
);
|
|
8882
9102
|
} catch (err) {
|
|
8883
9103
|
turnLog.warn(
|
|
8884
|
-
{
|
|
8885
|
-
"
|
|
9104
|
+
{ ...apiErrorLogFields(err) },
|
|
9105
|
+
"Couldn't update the turn's status in Cabane. Check the conversation before trying again."
|
|
8886
9106
|
);
|
|
8887
9107
|
}
|
|
8888
9108
|
const durationMs = Date.now() - startedAt;
|
|
8889
9109
|
this.supervisor.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
8890
|
-
return { ok: false, durationMs, reason };
|
|
9110
|
+
return { ok: false, durationMs, reason, runtime: this.turnContext?.runtime };
|
|
8891
9111
|
}
|
|
8892
9112
|
// The post-admission analogue of `concludeBeforeRun` (Codo's stack review,
|
|
8893
9113
|
// blocking finding #2): this turn WAS admitted — it holds the conversation's
|
|
@@ -8917,12 +9137,12 @@ var TurnExecution = class {
|
|
|
8917
9137
|
} catch (err) {
|
|
8918
9138
|
turnLog.warn(
|
|
8919
9139
|
{ ...apiErrorLogFields(err), turnId },
|
|
8920
|
-
"
|
|
9140
|
+
"Couldn't save the turn's result to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
8921
9141
|
);
|
|
8922
9142
|
}
|
|
8923
9143
|
const durationMs = Date.now() - startedAt;
|
|
8924
9144
|
this.supervisor.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
8925
|
-
return { ok: false, durationMs, reason };
|
|
9145
|
+
return { ok: false, durationMs, reason, runtime: this.turnContext?.runtime };
|
|
8926
9146
|
}
|
|
8927
9147
|
async fetchContext() {
|
|
8928
9148
|
const { payload, turnId, turnLog } = this;
|
|
@@ -8939,20 +9159,20 @@ var TurnExecution = class {
|
|
|
8939
9159
|
} catch (err) {
|
|
8940
9160
|
const status2 = err instanceof ApiError ? err.status : 0;
|
|
8941
9161
|
if (status2 === 404) {
|
|
8942
|
-
turnLog.
|
|
9162
|
+
turnLog.debug(
|
|
8943
9163
|
{ err: err instanceof Error ? err.message : String(err) },
|
|
8944
9164
|
"dispatcher: turn-context 404 (stale event \u2014 conversation/message/participant gone); skipping"
|
|
8945
9165
|
);
|
|
8946
9166
|
throw this.concluded("turn_context_not_found");
|
|
8947
9167
|
}
|
|
8948
9168
|
if (status2 === 409 && apiErrorCode(err) === "dispatch_not_admitted") {
|
|
8949
|
-
turnLog.
|
|
9169
|
+
turnLog.debug(
|
|
8950
9170
|
"dispatcher: turn-context 409 dispatch_not_admitted (wake already resolved); skipping"
|
|
8951
9171
|
);
|
|
8952
9172
|
throw this.concluded("turn_context_not_admitted");
|
|
8953
9173
|
}
|
|
8954
9174
|
const reason = err instanceof Error ? err.message : String(err);
|
|
8955
|
-
turnLog.
|
|
9175
|
+
turnLog.debug({ err: reason }, "dispatcher: failed to fetch turn context");
|
|
8956
9176
|
const fetchReason = `fetch_failed: ${reason}`;
|
|
8957
9177
|
throw this.concluded(fetchReason, fetchReason);
|
|
8958
9178
|
}
|
|
@@ -8962,7 +9182,7 @@ var TurnExecution = class {
|
|
|
8962
9182
|
this.resumedFromSeq = resolveSeqFloor(sources, { turnId, log: turnLog });
|
|
8963
9183
|
this.seqCounter = Math.max(this.seqCounter, this.resumedFromSeq);
|
|
8964
9184
|
this.resumedSpoke = turnContext.turnSpoke === true || outboxSpoke;
|
|
8965
|
-
if (this.resumedSpoke) turnLog.
|
|
9185
|
+
if (this.resumedSpoke) turnLog.debug({ turnId }, "dispatcher: resumed span already spoke");
|
|
8966
9186
|
}
|
|
8967
9187
|
}
|
|
8968
9188
|
gateTrigger() {
|
|
@@ -8970,7 +9190,7 @@ var TurnExecution = class {
|
|
|
8970
9190
|
const message = this.turnContext.message;
|
|
8971
9191
|
const isDispatchableTrigger = message.role === "user" || message.role === "agent" || message.role === "system";
|
|
8972
9192
|
if (!isDispatchableTrigger) {
|
|
8973
|
-
turnLog.
|
|
9193
|
+
turnLog.debug({ role: message.role }, "dispatcher: trigger role not dispatchable \u2014 skipping");
|
|
8974
9194
|
throw this.concluded("unexpected_role", UNEXPECTED_ROLE_REASON);
|
|
8975
9195
|
}
|
|
8976
9196
|
this.supervisor.notifyStart({
|
|
@@ -8981,7 +9201,9 @@ var TurnExecution = class {
|
|
|
8981
9201
|
}
|
|
8982
9202
|
async resolveSecrets() {
|
|
8983
9203
|
const { payload, workspaceId, turnLog } = this;
|
|
8984
|
-
const secretStore = this.opts.secretStore ?? loadSecretStoreTolerant(
|
|
9204
|
+
const secretStore = this.opts.secretStore ?? loadSecretStoreTolerant(
|
|
9205
|
+
(m) => turnLog.warn({ err: m }, "Could not read the secrets file. Check ~/.cabane/secrets.json.")
|
|
9206
|
+
);
|
|
8985
9207
|
const { servers: resolvedMcpServers, missing } = resolveMcpSecrets(
|
|
8986
9208
|
// CT262: the user MCP DEFINITIONS (placeholder form) come from the turn
|
|
8987
9209
|
// context now, not a separate `getAgentSelf` run-config fetch.
|
|
@@ -8989,7 +9211,7 @@ var TurnExecution = class {
|
|
|
8989
9211
|
secretStore
|
|
8990
9212
|
);
|
|
8991
9213
|
if (missing.length > 0) {
|
|
8992
|
-
turnLog.
|
|
9214
|
+
turnLog.debug({ missing }, "dispatcher: turn needs secrets not declared on this companion");
|
|
8993
9215
|
const reason = missingSecretReason(missing);
|
|
8994
9216
|
throw this.concluded(reason, reason);
|
|
8995
9217
|
}
|
|
@@ -9004,7 +9226,7 @@ var TurnExecution = class {
|
|
|
9004
9226
|
if (effectiveCwd && !existsSync10(effectiveCwd)) {
|
|
9005
9227
|
turnLog.warn(
|
|
9006
9228
|
{ cwd: effectiveCwd },
|
|
9007
|
-
"
|
|
9229
|
+
"The working directory is missing; using the companion's directory instead. Check this agent's working directory setting."
|
|
9008
9230
|
);
|
|
9009
9231
|
effectiveCwd = void 0;
|
|
9010
9232
|
}
|
|
@@ -9012,7 +9234,7 @@ var TurnExecution = class {
|
|
|
9012
9234
|
if (prepareHook) {
|
|
9013
9235
|
let cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
|
|
9014
9236
|
if (cached2 && !checkoutState(cached2.cwd).ok) {
|
|
9015
|
-
turnLog.
|
|
9237
|
+
turnLog.debug(
|
|
9016
9238
|
{ cwd: cached2.cwd, reason: checkoutState(cached2.cwd).reason },
|
|
9017
9239
|
"dispatcher: the prepared checkout is no longer usable \u2014 re-running the prepare hook to restore it"
|
|
9018
9240
|
);
|
|
@@ -9039,7 +9261,7 @@ var TurnExecution = class {
|
|
|
9039
9261
|
});
|
|
9040
9262
|
} catch (err) {
|
|
9041
9263
|
const reason = err instanceof Error ? err.message : String(err);
|
|
9042
|
-
turnLog.
|
|
9264
|
+
turnLog.debug({ err: reason }, "dispatcher: prepare hook rejected a prepared turn");
|
|
9043
9265
|
const failReason = `prepare_failed: ${reason}`;
|
|
9044
9266
|
throw this.concluded(failReason, failReason);
|
|
9045
9267
|
}
|
|
@@ -9059,7 +9281,7 @@ var TurnExecution = class {
|
|
|
9059
9281
|
phase,
|
|
9060
9282
|
seq
|
|
9061
9283
|
}).catch((err) => {
|
|
9062
|
-
turnLog.
|
|
9284
|
+
turnLog.debug(
|
|
9063
9285
|
{ err: err instanceof Error ? err.message : String(err), phase },
|
|
9064
9286
|
"dispatcher: preparing-activity report failed (continuing with the hook)"
|
|
9065
9287
|
);
|
|
@@ -9099,7 +9321,7 @@ var TurnExecution = class {
|
|
|
9099
9321
|
clearTimeout(preparingTimer);
|
|
9100
9322
|
if (preparingStarted) reportPreparing("error");
|
|
9101
9323
|
const reason = err instanceof Error ? err.message : String(err);
|
|
9102
|
-
turnLog.
|
|
9324
|
+
turnLog.debug({ err: reason }, "dispatcher: prepare hook failed");
|
|
9103
9325
|
const failReason = `prepare_failed: ${reason}`;
|
|
9104
9326
|
throw this.concluded(failReason, failReason);
|
|
9105
9327
|
}
|
|
@@ -9147,14 +9369,14 @@ var TurnExecution = class {
|
|
|
9147
9369
|
} catch (err) {
|
|
9148
9370
|
const refusal = leaseRefusal(err);
|
|
9149
9371
|
if (refusal === "dispatch_not_admitted" || refusal === "turn_already_ended") {
|
|
9150
|
-
turnLog.
|
|
9372
|
+
turnLog.debug(
|
|
9151
9373
|
{ refusal, turnId },
|
|
9152
9374
|
"dispatcher: refused a turn lease; not running the model"
|
|
9153
9375
|
);
|
|
9154
9376
|
throw this.concluded(`lease_refused: ${refusal}`);
|
|
9155
9377
|
}
|
|
9156
9378
|
const reason = refusal ? `lease_refused: ${refusal}` : "lease_unconfirmed";
|
|
9157
|
-
turnLog.
|
|
9379
|
+
turnLog.debug(
|
|
9158
9380
|
{ refusal, turnId, err: err instanceof Error ? err.message : String(err) },
|
|
9159
9381
|
"dispatcher: turn lease not confirmed; not running the model"
|
|
9160
9382
|
);
|
|
@@ -9206,7 +9428,7 @@ var TurnExecution = class {
|
|
|
9206
9428
|
async selectAdapter() {
|
|
9207
9429
|
const { payload, workspaceId, turnId, turnLog } = this;
|
|
9208
9430
|
await guardBundledBinary(this.integrityContext(), (r, e) => this.concluded(r, e), this.outcome);
|
|
9209
|
-
const onWarn = (msg, meta) => turnLog.
|
|
9431
|
+
const onWarn = (msg, meta) => turnLog.debug(meta ?? {}, msg);
|
|
9210
9432
|
const adapters = [];
|
|
9211
9433
|
if (this.opts.claudeCodeAvailable?.() ?? true) {
|
|
9212
9434
|
adapters.push(createClaudeCodeAdapter({ queryFn: this.opts.queryFn, onWarn }));
|
|
@@ -9228,7 +9450,7 @@ var TurnExecution = class {
|
|
|
9228
9450
|
this.adapter = selectAdapter(registry, this.turnContext.runtime);
|
|
9229
9451
|
} catch (err) {
|
|
9230
9452
|
if (!(err instanceof RuntimeUnavailableError)) throw err;
|
|
9231
|
-
turnLog.
|
|
9453
|
+
turnLog.debug(
|
|
9232
9454
|
{ runtime: err.runtime, available: err.available },
|
|
9233
9455
|
"dispatcher: turn runtime not available on this device"
|
|
9234
9456
|
);
|
|
@@ -9241,8 +9463,8 @@ var TurnExecution = class {
|
|
|
9241
9463
|
});
|
|
9242
9464
|
} catch (postErr) {
|
|
9243
9465
|
turnLog.warn(
|
|
9244
|
-
{
|
|
9245
|
-
"
|
|
9466
|
+
{ ...apiErrorLogFields(postErr) },
|
|
9467
|
+
"Couldn't save the explanation to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
9246
9468
|
);
|
|
9247
9469
|
const reason = `runtime_unavailable:${err.runtime}`;
|
|
9248
9470
|
throw this.concluded(reason, reason);
|
|
@@ -9270,7 +9492,7 @@ var TurnExecution = class {
|
|
|
9270
9492
|
false,
|
|
9271
9493
|
true
|
|
9272
9494
|
);
|
|
9273
|
-
turnLog.
|
|
9495
|
+
turnLog.debug(
|
|
9274
9496
|
{ reason },
|
|
9275
9497
|
"dispatcher: stored session not resumed \u2014 the turn context was recomposed for a fresh session"
|
|
9276
9498
|
);
|
|
@@ -9281,8 +9503,8 @@ var TurnExecution = class {
|
|
|
9281
9503
|
};
|
|
9282
9504
|
} catch (err) {
|
|
9283
9505
|
turnLog.error(
|
|
9284
|
-
{ reason,
|
|
9285
|
-
"
|
|
9506
|
+
{ reason, ...apiErrorLogFields(err) },
|
|
9507
|
+
"Couldn't load the full conversation; continuing with the recent messages."
|
|
9286
9508
|
);
|
|
9287
9509
|
return null;
|
|
9288
9510
|
}
|
|
@@ -9303,7 +9525,10 @@ var TurnExecution = class {
|
|
|
9303
9525
|
dispatchId: this.dispatchId,
|
|
9304
9526
|
message: this.turnContext.message.body
|
|
9305
9527
|
},
|
|
9306
|
-
(m) => turnLog.warn(
|
|
9528
|
+
(m) => turnLog.warn(
|
|
9529
|
+
{ err: m },
|
|
9530
|
+
"Could not save the transcript. Check free disk space and access to ~/.cabane."
|
|
9531
|
+
)
|
|
9307
9532
|
) : null;
|
|
9308
9533
|
const turnRuntime = this.turnContext.runtime;
|
|
9309
9534
|
const committer = this.committer = new TurnCommitter({
|
|
@@ -9343,8 +9568,8 @@ var TurnExecution = class {
|
|
|
9343
9568
|
}
|
|
9344
9569
|
} catch (err) {
|
|
9345
9570
|
turnLog.warn(
|
|
9346
|
-
{
|
|
9347
|
-
"
|
|
9571
|
+
{ ...apiErrorLogFields(err) },
|
|
9572
|
+
"Couldn't check whether the agent chose to pass; the turn may appear to have failed."
|
|
9348
9573
|
);
|
|
9349
9574
|
}
|
|
9350
9575
|
};
|
|
@@ -9352,7 +9577,7 @@ var TurnExecution = class {
|
|
|
9352
9577
|
const fireTimeout = (reason) => {
|
|
9353
9578
|
if (abortController.signal.aborted) return;
|
|
9354
9579
|
o.timeoutReason = reason;
|
|
9355
|
-
turnLog.
|
|
9580
|
+
turnLog.debug(
|
|
9356
9581
|
{ reason, idleTimeoutMs, totalTimeoutMs },
|
|
9357
9582
|
"dispatcher: turn timeout \u2014 aborting"
|
|
9358
9583
|
);
|
|
@@ -9382,7 +9607,7 @@ var TurnExecution = class {
|
|
|
9382
9607
|
if (state !== "ended") return;
|
|
9383
9608
|
if (abortController.signal.aborted) return;
|
|
9384
9609
|
o.leaseLost = true;
|
|
9385
|
-
turnLog.
|
|
9610
|
+
turnLog.debug(
|
|
9386
9611
|
{ turnId, trigger },
|
|
9387
9612
|
"dispatcher: this turn is no longer running server-side \u2014 aborting the loop"
|
|
9388
9613
|
);
|
|
@@ -9410,7 +9635,7 @@ var TurnExecution = class {
|
|
|
9410
9635
|
if (isContentBearingEvent(event)) o.contentBearingEvents += 1;
|
|
9411
9636
|
armIdle();
|
|
9412
9637
|
if (abortController.signal.aborted) {
|
|
9413
|
-
turnLog.
|
|
9638
|
+
turnLog.debug("dispatcher: aborted mid-turn");
|
|
9414
9639
|
o.okResult = false;
|
|
9415
9640
|
o.resultReason = o.timeoutReason ?? "cancelled";
|
|
9416
9641
|
break;
|
|
@@ -9433,18 +9658,18 @@ var TurnExecution = class {
|
|
|
9433
9658
|
o.sessionWriteRejected = true;
|
|
9434
9659
|
turnLog.error(
|
|
9435
9660
|
{
|
|
9436
|
-
|
|
9661
|
+
...apiErrorLogFields(err),
|
|
9437
9662
|
status: status2,
|
|
9438
9663
|
responseBody: describeErrorBody(err instanceof ApiError ? err.body : void 0),
|
|
9439
9664
|
stateLength: event.state.length,
|
|
9440
9665
|
runtime: turnRuntime
|
|
9441
9666
|
},
|
|
9442
|
-
"
|
|
9667
|
+
"Couldn't save the agent's memory in Cabane. The next turn will start a new session. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
9443
9668
|
);
|
|
9444
9669
|
} else {
|
|
9445
9670
|
turnLog.warn(
|
|
9446
|
-
{
|
|
9447
|
-
"
|
|
9671
|
+
{ ...apiErrorLogFields(err) },
|
|
9672
|
+
"Couldn't save the agent's memory; trying again next turn."
|
|
9448
9673
|
);
|
|
9449
9674
|
}
|
|
9450
9675
|
}
|
|
@@ -9485,7 +9710,7 @@ var TurnExecution = class {
|
|
|
9485
9710
|
await this.applyRecordedTurnControlIntent();
|
|
9486
9711
|
}
|
|
9487
9712
|
if (!abortController.signal.aborted && this.skipState.skipped) {
|
|
9488
|
-
turnLog.
|
|
9713
|
+
turnLog.debug(
|
|
9489
9714
|
{ reason: this.skipState.reason, turnId, ok: o.okResult },
|
|
9490
9715
|
"agent skipped turn (skip_turn)"
|
|
9491
9716
|
);
|
|
@@ -9493,7 +9718,7 @@ var TurnExecution = class {
|
|
|
9493
9718
|
} catch (err) {
|
|
9494
9719
|
o.okResult = false;
|
|
9495
9720
|
o.resultReason = err instanceof Error ? err.message : String(err);
|
|
9496
|
-
turnLog.
|
|
9721
|
+
turnLog.debug({ err: o.resultReason }, "dispatcher: SDK query threw");
|
|
9497
9722
|
o.runtimeIncomplete = await absorbBundledLoss(
|
|
9498
9723
|
this.integrityContext(),
|
|
9499
9724
|
abortController.signal.aborted,
|
|
@@ -9518,7 +9743,7 @@ var TurnExecution = class {
|
|
|
9518
9743
|
kind: "usage_capped",
|
|
9519
9744
|
resetsAt: health.limitedUntil
|
|
9520
9745
|
});
|
|
9521
|
-
turnLog.
|
|
9746
|
+
turnLog.debug(
|
|
9522
9747
|
{ runtime: turnRuntime, limitedUntil: health.limitedUntil },
|
|
9523
9748
|
"dispatcher: idle-timeout on a capped runtime \u2014 reclassified as usage_capped (CT639)"
|
|
9524
9749
|
);
|
|
@@ -9569,7 +9794,7 @@ var TurnExecution = class {
|
|
|
9569
9794
|
if (diagnosticReason && !["usage_capped", "rate_limited", "auth_expired", "cancelled", "skipped"].includes(
|
|
9570
9795
|
diagnosticReason.kind
|
|
9571
9796
|
)) {
|
|
9572
|
-
turnLog.
|
|
9797
|
+
turnLog.debug(
|
|
9573
9798
|
{
|
|
9574
9799
|
workspaceId,
|
|
9575
9800
|
turnId,
|
|
@@ -9597,8 +9822,8 @@ var TurnExecution = class {
|
|
|
9597
9822
|
);
|
|
9598
9823
|
} catch (err) {
|
|
9599
9824
|
turnLog.warn(
|
|
9600
|
-
{
|
|
9601
|
-
"
|
|
9825
|
+
{ ...apiErrorLogFields(err) },
|
|
9826
|
+
"Couldn't save the turn's result to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
9602
9827
|
);
|
|
9603
9828
|
}
|
|
9604
9829
|
this.supervisor.releaseAbort(turnId, abortController);
|
|
@@ -9615,9 +9840,6 @@ var TurnExecution = class {
|
|
|
9615
9840
|
...o.resultReason ? { reason: o.resultReason } : {},
|
|
9616
9841
|
durationMs
|
|
9617
9842
|
});
|
|
9618
|
-
if (!o.okResult && o.resultReason !== "cancelled") {
|
|
9619
|
-
turnLog.info(`turn failed \u2014 full this.transcript: ${this.transcript.path}`);
|
|
9620
|
-
}
|
|
9621
9843
|
if (o.settledDiagnostics?.resultReason?.kind === "empty_result" || o.settledDiagnostics?.resultReason?.kind === "empty_result_unverified") {
|
|
9622
9844
|
this.transcript.preserveAnomaly();
|
|
9623
9845
|
}
|
|
@@ -9642,7 +9864,9 @@ var TurnExecution = class {
|
|
|
9642
9864
|
return {
|
|
9643
9865
|
ok: false,
|
|
9644
9866
|
durationMs,
|
|
9645
|
-
...o.resultReason ? { reason: o.resultReason } : {}
|
|
9867
|
+
...o.resultReason ? { reason: o.resultReason } : {},
|
|
9868
|
+
runtime: this.turnContext.runtime,
|
|
9869
|
+
...this.transcript ? { transcriptPath: tildePath(this.transcript.path) } : {}
|
|
9646
9870
|
};
|
|
9647
9871
|
}
|
|
9648
9872
|
};
|
|
@@ -9848,7 +10072,7 @@ var Outbox = class {
|
|
|
9848
10072
|
}
|
|
9849
10073
|
this.log?.warn(
|
|
9850
10074
|
{ workspaceId: this.workspaceId, err: err instanceof Error ? err.message : String(err) },
|
|
9851
|
-
"
|
|
10075
|
+
"Couldn't save an update on this machine. Check free disk space and access to ~/.cabane."
|
|
9852
10076
|
);
|
|
9853
10077
|
return;
|
|
9854
10078
|
}
|
|
@@ -9936,7 +10160,7 @@ var Outbox = class {
|
|
|
9936
10160
|
dropCorrupt(full) {
|
|
9937
10161
|
this.log?.warn(
|
|
9938
10162
|
{ workspaceId: this.workspaceId, file: full },
|
|
9939
|
-
"
|
|
10163
|
+
"A saved update is unreadable and couldn't be delivered. Check the conversation for missing replies."
|
|
9940
10164
|
);
|
|
9941
10165
|
try {
|
|
9942
10166
|
rmSync7(full, { force: true });
|
|
@@ -9953,7 +10177,7 @@ var Outbox = class {
|
|
|
9953
10177
|
if (now - e.enqueuedAt > MAX_AGE_MS) {
|
|
9954
10178
|
this.log?.warn(
|
|
9955
10179
|
{ workspaceId: this.workspaceId, turnId: e.turnId, seq: e.seq, kind: e.kind },
|
|
9956
|
-
"
|
|
10180
|
+
"A saved update waited too long and couldn't be delivered. Check the conversation for missing replies."
|
|
9957
10181
|
);
|
|
9958
10182
|
this.remove(e.turnId, e.seq);
|
|
9959
10183
|
} else {
|
|
@@ -9965,7 +10189,7 @@ var Outbox = class {
|
|
|
9965
10189
|
for (const e of survivors.slice(0, overflow)) {
|
|
9966
10190
|
this.log?.warn(
|
|
9967
10191
|
{ workspaceId: this.workspaceId, turnId: e.turnId, seq: e.seq, kind: e.kind },
|
|
9968
|
-
"
|
|
10192
|
+
"Too many updates are waiting; the oldest couldn't be kept. Check the connection to Cabane."
|
|
9969
10193
|
);
|
|
9970
10194
|
this.remove(e.turnId, e.seq);
|
|
9971
10195
|
}
|
|
@@ -10051,11 +10275,14 @@ var SseSubscriber = class {
|
|
|
10051
10275
|
try {
|
|
10052
10276
|
await this.connect();
|
|
10053
10277
|
backoff = 500;
|
|
10054
|
-
if (!this.aborted)
|
|
10278
|
+
if (!this.aborted) {
|
|
10279
|
+
this.opts.log.warn("Lost the connection to Cabane; reconnecting");
|
|
10280
|
+
await sleep3(backoff);
|
|
10281
|
+
}
|
|
10055
10282
|
} catch (err) {
|
|
10056
10283
|
if (this.aborted) return;
|
|
10057
10284
|
if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
|
|
10058
|
-
this.opts.log.
|
|
10285
|
+
this.opts.log.debug(
|
|
10059
10286
|
{ workspaceId: this.opts.workspaceId, status: err.status },
|
|
10060
10287
|
"SSE auth failed \u2014 tearing down this workspace subscriber"
|
|
10061
10288
|
);
|
|
@@ -10068,7 +10295,7 @@ var SseSubscriber = class {
|
|
|
10068
10295
|
err: err instanceof Error ? err.message : String(err),
|
|
10069
10296
|
backoff
|
|
10070
10297
|
},
|
|
10071
|
-
"
|
|
10298
|
+
"Lost the connection to Cabane; reconnecting"
|
|
10072
10299
|
);
|
|
10073
10300
|
await sleep3(backoff);
|
|
10074
10301
|
backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
|
|
@@ -10092,7 +10319,7 @@ var SseSubscriber = class {
|
|
|
10092
10319
|
if (!res.body) {
|
|
10093
10320
|
throw new Error("SSE response has no body");
|
|
10094
10321
|
}
|
|
10095
|
-
this.opts.log.
|
|
10322
|
+
this.opts.log.debug({ workspaceId: this.opts.workspaceId }, "SSE connected");
|
|
10096
10323
|
let opened = true;
|
|
10097
10324
|
this.opts.onOpen?.();
|
|
10098
10325
|
const parser = createParser({
|
|
@@ -10222,15 +10449,12 @@ var CompanionSupervisor = class {
|
|
|
10222
10449
|
async start() {
|
|
10223
10450
|
this.log.info(
|
|
10224
10451
|
{ protocolVersion: TURN_PROTOCOL_VERSION, version: COMPANION_VERSION },
|
|
10225
|
-
|
|
10452
|
+
`Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ?? hostname2()}`
|
|
10226
10453
|
);
|
|
10227
10454
|
const startupHarnessRefresh = this.refreshHarnessStatuses();
|
|
10228
10455
|
this.trackHeartbeat(startupHarnessRefresh);
|
|
10229
10456
|
if (!this.config.deviceToken) {
|
|
10230
|
-
this.log.warn("
|
|
10231
|
-
process.stdout.write(
|
|
10232
|
-
"companion: this device is not paired \u2014 run `cabane-companion pair`, then confirm the short code in Settings \u2192 Connectors.\n"
|
|
10233
|
-
);
|
|
10457
|
+
this.log.warn("This device isn't paired. Run cabane-companion start to pair it.");
|
|
10234
10458
|
return;
|
|
10235
10459
|
}
|
|
10236
10460
|
this.deviceApi = new DeviceApi({
|
|
@@ -10244,6 +10468,7 @@ var CompanionSupervisor = class {
|
|
|
10244
10468
|
token: this.config.deviceToken,
|
|
10245
10469
|
log: this.log,
|
|
10246
10470
|
lastEventId: null,
|
|
10471
|
+
onOpen: () => this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ?? hostname2()}`),
|
|
10247
10472
|
onMessage: async (ev) => {
|
|
10248
10473
|
if (ev.event === "assignments_changed") {
|
|
10249
10474
|
if (this.refreshing && this.inFlightRefresh) await this.inFlightRefresh;
|
|
@@ -10251,11 +10476,13 @@ var CompanionSupervisor = class {
|
|
|
10251
10476
|
}
|
|
10252
10477
|
},
|
|
10253
10478
|
onAuthFailure: () => {
|
|
10254
|
-
this.log.error(
|
|
10479
|
+
this.log.error(
|
|
10480
|
+
"Cabane no longer accepts this device. Run cabane-companion logout, then cabane-companion start to pair again."
|
|
10481
|
+
);
|
|
10255
10482
|
}
|
|
10256
10483
|
});
|
|
10257
|
-
this.deviceSub.start();
|
|
10258
10484
|
await this.refreshAssignments();
|
|
10485
|
+
this.deviceSub.start();
|
|
10259
10486
|
const firstHeartbeat = startupHarnessRefresh.then(async () => {
|
|
10260
10487
|
if (this.stopped) return;
|
|
10261
10488
|
await this.sendHeartbeat();
|
|
@@ -10291,7 +10518,9 @@ var CompanionSupervisor = class {
|
|
|
10291
10518
|
if (!this.deviceApi) return;
|
|
10292
10519
|
await this.refreshHarnessStatuses();
|
|
10293
10520
|
try {
|
|
10294
|
-
const store = loadSecretStoreTolerant(
|
|
10521
|
+
const store = loadSecretStoreTolerant(
|
|
10522
|
+
(m) => this.log.warn({ err: m }, "Could not read the secrets file. Check ~/.cabane/secrets.json.")
|
|
10523
|
+
);
|
|
10295
10524
|
const connectorReports = this.connectorHealth.reports();
|
|
10296
10525
|
const opencodeModels = this.config.opencode?.serverUrl ? await enumerateOpencodeModels(this.config.opencode.serverUrl) : null;
|
|
10297
10526
|
const res = await this.deviceApi.heartbeat({
|
|
@@ -10338,8 +10567,8 @@ var CompanionSupervisor = class {
|
|
|
10338
10567
|
void this.recoverRuns();
|
|
10339
10568
|
} catch (err) {
|
|
10340
10569
|
this.log.warn(
|
|
10341
|
-
{
|
|
10342
|
-
"
|
|
10570
|
+
{ ...apiErrorLogFields(err) },
|
|
10571
|
+
"Couldn't check in with Cabane; trying again shortly."
|
|
10343
10572
|
);
|
|
10344
10573
|
}
|
|
10345
10574
|
}
|
|
@@ -10354,7 +10583,7 @@ var CompanionSupervisor = class {
|
|
|
10354
10583
|
this.versionSkewWarned = true;
|
|
10355
10584
|
this.log.warn(
|
|
10356
10585
|
{ companionVersion: COMPANION_VERSION, serverVersion },
|
|
10357
|
-
"
|
|
10586
|
+
"This companion and Cabane are on different versions. If turns fail, update with npm i -g @cabane/companion@latest"
|
|
10358
10587
|
);
|
|
10359
10588
|
}
|
|
10360
10589
|
// Pull assignments and reconcile the live runner set against them. Re-entrancy
|
|
@@ -10376,8 +10605,8 @@ var CompanionSupervisor = class {
|
|
|
10376
10605
|
device = resp.device;
|
|
10377
10606
|
} catch (err) {
|
|
10378
10607
|
this.log.error(
|
|
10379
|
-
{
|
|
10380
|
-
"
|
|
10608
|
+
{ ...apiErrorLogFields(err) },
|
|
10609
|
+
"Couldn't load the assigned agents. Check that this device is active in Cabane; trying again shortly."
|
|
10381
10610
|
);
|
|
10382
10611
|
this.hub.setDeviceError(err instanceof Error ? err.message : String(err));
|
|
10383
10612
|
return;
|
|
@@ -10443,14 +10672,17 @@ var CompanionSupervisor = class {
|
|
|
10443
10672
|
const credential = it.credential ?? getCredential(it.agentId);
|
|
10444
10673
|
const runConfig = parseRunConfig(
|
|
10445
10674
|
it.runConfig,
|
|
10446
|
-
(m) => this.log.warn(
|
|
10675
|
+
(m) => this.log.warn(
|
|
10676
|
+
{ agentId: it.agentId, agentName: it.agentDisplayName, err: m },
|
|
10677
|
+
"Some agent settings could not be read; using defaults. Check the agent settings in Cabane."
|
|
10678
|
+
)
|
|
10447
10679
|
);
|
|
10448
10680
|
const required = requiredSecretNames(runConfig.mcpServers);
|
|
10449
10681
|
const missing = required.filter((n) => !exposed.has(n));
|
|
10450
10682
|
if (!credential) {
|
|
10451
10683
|
this.log.error(
|
|
10452
10684
|
{ workspaceId, agentId: it.agentId, username: it.agentUsername },
|
|
10453
|
-
|
|
10685
|
+
`Cannot run ${it.agentDisplayName}: its sign-in to Cabane is missing. Reassign it to this device in Cabane.`
|
|
10454
10686
|
);
|
|
10455
10687
|
this.removeAgent(wr, it.agentId);
|
|
10456
10688
|
this.hub.setAgent(workspaceId, {
|
|
@@ -10484,19 +10716,20 @@ var CompanionSupervisor = class {
|
|
|
10484
10716
|
if (missing.length > 0) {
|
|
10485
10717
|
this.log.warn(
|
|
10486
10718
|
{ workspaceId, agentId: it.agentId, missing },
|
|
10487
|
-
|
|
10719
|
+
`${it.agentDisplayName} needs secrets this machine does not have: ${missing.join(", ")}. Add them to ~/.cabane/secrets.json.`
|
|
10488
10720
|
);
|
|
10489
10721
|
}
|
|
10490
10722
|
}
|
|
10491
10723
|
this.ensureWorkspaceSse(wr);
|
|
10492
10724
|
}
|
|
10493
10725
|
addAgent(wr, it, credential, runConfig) {
|
|
10494
|
-
const
|
|
10726
|
+
const agentLog = this.log.child({ agentName: it.agentDisplayName });
|
|
10727
|
+
const outbox = new Outbox(it.agentId, agentLog);
|
|
10495
10728
|
const api = new CabaneApi({
|
|
10496
10729
|
baseUrl: this.config.baseUrl,
|
|
10497
10730
|
token: credential,
|
|
10498
10731
|
outbox,
|
|
10499
|
-
log:
|
|
10732
|
+
log: agentLog
|
|
10500
10733
|
});
|
|
10501
10734
|
const aborts = /* @__PURE__ */ new Map();
|
|
10502
10735
|
const dispatcher = this.buildDispatcher({
|
|
@@ -10506,6 +10739,7 @@ var CompanionSupervisor = class {
|
|
|
10506
10739
|
workspaceSlug: it.workspaceSlug,
|
|
10507
10740
|
agentId: it.agentId,
|
|
10508
10741
|
agentUsername: it.agentUsername,
|
|
10742
|
+
agentDisplayName: it.agentDisplayName,
|
|
10509
10743
|
credential,
|
|
10510
10744
|
runConfig,
|
|
10511
10745
|
aborts
|
|
@@ -10526,7 +10760,7 @@ var CompanionSupervisor = class {
|
|
|
10526
10760
|
drain2.kick();
|
|
10527
10761
|
this.log.info(
|
|
10528
10762
|
{ workspaceId: it.workspaceId, agentId: it.agentId, username: it.agentUsername },
|
|
10529
|
-
|
|
10763
|
+
`Running ${it.agentDisplayName}`
|
|
10530
10764
|
);
|
|
10531
10765
|
}
|
|
10532
10766
|
removeAgent(wr, agentId) {
|
|
@@ -10537,7 +10771,7 @@ var CompanionSupervisor = class {
|
|
|
10537
10771
|
this.hub.removeAgent(wr.workspaceId, agentId);
|
|
10538
10772
|
this.log.info(
|
|
10539
10773
|
{ workspaceId: wr.workspaceId, agentId },
|
|
10540
|
-
|
|
10774
|
+
`Stopped running ${runner.displayName} (unassigned)`
|
|
10541
10775
|
);
|
|
10542
10776
|
}
|
|
10543
10777
|
// CT1379: can this machine LAUNCH the harness — is the SDK's own bundled
|
|
@@ -10610,7 +10844,11 @@ var CompanionSupervisor = class {
|
|
|
10610
10844
|
const meta = { runtime, platform: resolution.platform, arch: resolution.arch, status: status2 };
|
|
10611
10845
|
if (status2 === "present") {
|
|
10612
10846
|
const say = intended ? this.log.info : this.log.debug;
|
|
10613
|
-
say.call(
|
|
10847
|
+
say.call(
|
|
10848
|
+
this.log,
|
|
10849
|
+
meta,
|
|
10850
|
+
`${runtime === "codex" ? "Codex" : "Claude Code"} is available again`
|
|
10851
|
+
);
|
|
10614
10852
|
continue;
|
|
10615
10853
|
}
|
|
10616
10854
|
if (intended) this.log.warn(meta, startupWarning(resolution));
|
|
@@ -10656,7 +10894,7 @@ var CompanionSupervisor = class {
|
|
|
10656
10894
|
local,
|
|
10657
10895
|
aborts: ctx.aborts,
|
|
10658
10896
|
runConfig: ctx.runConfig,
|
|
10659
|
-
log: this.log,
|
|
10897
|
+
log: this.log.child({ agentName: ctx.agentDisplayName ?? ctx.agentUsername }),
|
|
10660
10898
|
// CT833: register the claude-code adapter only when this device actually
|
|
10661
10899
|
// offers claude-code — read per turn (not captured here), so a harness
|
|
10662
10900
|
// installed or connected after boot works on the next turn exactly as it
|
|
@@ -10714,7 +10952,7 @@ var CompanionSupervisor = class {
|
|
|
10714
10952
|
onMessage: (ev) => this.handleWorkspaceMessage(wr, ev),
|
|
10715
10953
|
onAuthFailure: (status2) => {
|
|
10716
10954
|
this.hub.setAuthFailed(wr.workspaceId);
|
|
10717
|
-
this.log.
|
|
10955
|
+
this.log.debug(
|
|
10718
10956
|
{ workspaceId: wr.workspaceId, status: status2, sseAgentId: wr.sseAgentId },
|
|
10719
10957
|
"companion: workspace stream auth failed \u2014 re-pulling assignments"
|
|
10720
10958
|
);
|
|
@@ -10723,7 +10961,7 @@ var CompanionSupervisor = class {
|
|
|
10723
10961
|
}
|
|
10724
10962
|
});
|
|
10725
10963
|
wr.sub.start();
|
|
10726
|
-
this.log.
|
|
10964
|
+
this.log.debug({ workspaceId: wr.workspaceId }, "companion: subscribed");
|
|
10727
10965
|
}
|
|
10728
10966
|
async removeWorkspace(workspaceId) {
|
|
10729
10967
|
const wr = this.workspaces.get(workspaceId);
|
|
@@ -10747,7 +10985,7 @@ var CompanionSupervisor = class {
|
|
|
10747
10985
|
try {
|
|
10748
10986
|
wire = JSON.parse(ev.data);
|
|
10749
10987
|
} catch (err) {
|
|
10750
|
-
this.log.
|
|
10988
|
+
this.log.debug(
|
|
10751
10989
|
{ err: err instanceof Error ? err.message : String(err) },
|
|
10752
10990
|
"malformed SSE payload"
|
|
10753
10991
|
);
|
|
@@ -10763,7 +11001,7 @@ var CompanionSupervisor = class {
|
|
|
10763
11001
|
if (agent2) {
|
|
10764
11002
|
const aborted = agent2.dispatcher.cancel(payload2.conversationId, payload2.agentId);
|
|
10765
11003
|
if (aborted) {
|
|
10766
|
-
this.log.
|
|
11004
|
+
this.log.debug(
|
|
10767
11005
|
{
|
|
10768
11006
|
workspaceId: wr.workspaceId,
|
|
10769
11007
|
conversationId: payload2.conversationId,
|
|
@@ -10817,9 +11055,9 @@ var CompanionSupervisor = class {
|
|
|
10817
11055
|
workspaceId: wr.workspaceId,
|
|
10818
11056
|
conversationId: payload.conversationId,
|
|
10819
11057
|
agentId: payload.agentId,
|
|
10820
|
-
|
|
11058
|
+
...apiErrorLogFields(err)
|
|
10821
11059
|
},
|
|
10822
|
-
"
|
|
11060
|
+
"The turn stopped unexpectedly. Reply in Cabane to try again."
|
|
10823
11061
|
);
|
|
10824
11062
|
});
|
|
10825
11063
|
wr.chains.set(chainKey, tail);
|
|
@@ -10852,7 +11090,7 @@ var CompanionSupervisor = class {
|
|
|
10852
11090
|
messageId: payload.messageId,
|
|
10853
11091
|
reason: "agent_not_on_device"
|
|
10854
11092
|
});
|
|
10855
|
-
this.log.
|
|
11093
|
+
this.log.debug(
|
|
10856
11094
|
{
|
|
10857
11095
|
workspaceId: wr.workspaceId,
|
|
10858
11096
|
conversationId: payload.conversationId,
|
|
@@ -10873,7 +11111,7 @@ var CompanionSupervisor = class {
|
|
|
10873
11111
|
agentId: payload.agentId,
|
|
10874
11112
|
err: lastErr instanceof Error ? lastErr.message : String(lastErr)
|
|
10875
11113
|
},
|
|
10876
|
-
"
|
|
11114
|
+
"Couldn't tell Cabane that this agent no longer runs here; trying again when the connection returns."
|
|
10877
11115
|
);
|
|
10878
11116
|
return false;
|
|
10879
11117
|
}
|
|
@@ -10928,7 +11166,7 @@ var CompanionSupervisor = class {
|
|
|
10928
11166
|
agent,
|
|
10929
11167
|
run.turnId
|
|
10930
11168
|
).catch(
|
|
10931
|
-
(err) => this.log.
|
|
11169
|
+
(err) => this.log.debug({ err, turnId: run.turnId }, "companion: restart recovery failed")
|
|
10932
11170
|
).finally(() => {
|
|
10933
11171
|
if (wr.chains.get(key) === tail) wr.chains.delete(key);
|
|
10934
11172
|
});
|
|
@@ -10939,7 +11177,7 @@ var CompanionSupervisor = class {
|
|
|
10939
11177
|
this.recoveryChecked = page.nextCursor === null;
|
|
10940
11178
|
}
|
|
10941
11179
|
} catch (err) {
|
|
10942
|
-
this.log.
|
|
11180
|
+
this.log.debug({ err }, "companion: run recovery read failed (will retry)");
|
|
10943
11181
|
} finally {
|
|
10944
11182
|
this.recovering = false;
|
|
10945
11183
|
}
|
|
@@ -10951,7 +11189,7 @@ var CompanionSupervisor = class {
|
|
|
10951
11189
|
const workspaceId = wr.workspaceId;
|
|
10952
11190
|
if (ev.id && hasDispatched(workspaceId, ev.id)) {
|
|
10953
11191
|
if (hasCompleted(workspaceId, ev.id)) {
|
|
10954
|
-
this.log.
|
|
11192
|
+
this.log.debug(
|
|
10955
11193
|
{ workspaceId, eventId: ev.id },
|
|
10956
11194
|
"companion: skipping already-completed event (resume after restart)"
|
|
10957
11195
|
);
|
|
@@ -10959,7 +11197,7 @@ var CompanionSupervisor = class {
|
|
|
10959
11197
|
return;
|
|
10960
11198
|
}
|
|
10961
11199
|
if (noResume()) {
|
|
10962
|
-
this.log.
|
|
11200
|
+
this.log.debug(
|
|
10963
11201
|
{ workspaceId, eventId: ev.id },
|
|
10964
11202
|
"companion: skipping interrupted turn (CABANE_COMPANION_NO_RESUME=1) \u2014 resume disabled for this boot"
|
|
10965
11203
|
);
|
|
@@ -10971,13 +11209,13 @@ var CompanionSupervisor = class {
|
|
|
10971
11209
|
if (attempt > MAX_RESUME_ATTEMPTS) {
|
|
10972
11210
|
this.log.error(
|
|
10973
11211
|
{ workspaceId, eventId: ev.id, attempt, cap: MAX_RESUME_ATTEMPTS },
|
|
10974
|
-
"
|
|
11212
|
+
"An interrupted turn couldn't finish after several attempts. Reply in Cabane to try again."
|
|
10975
11213
|
);
|
|
10976
11214
|
markCompleted(workspaceId, ev.id);
|
|
10977
11215
|
wr.cursor.settle(ev.id);
|
|
10978
11216
|
return;
|
|
10979
11217
|
}
|
|
10980
|
-
this.log.
|
|
11218
|
+
this.log.debug(
|
|
10981
11219
|
{ workspaceId, eventId: ev.id, attempt, cap: MAX_RESUME_ATTEMPTS },
|
|
10982
11220
|
"companion: re-dispatching interrupted turn (resume after restart)"
|
|
10983
11221
|
);
|
|
@@ -10987,7 +11225,7 @@ var CompanionSupervisor = class {
|
|
|
10987
11225
|
if (ev.id) {
|
|
10988
11226
|
const liveTurnIds = rememberTurnId(workspaceId, ev.id, turnId);
|
|
10989
11227
|
if (liveTurnIds > TURN_ID_OVERFLOW_WARN) {
|
|
10990
|
-
this.log.
|
|
11228
|
+
this.log.debug(
|
|
10991
11229
|
{ workspaceId, liveTurnIds, threshold: TURN_ID_OVERFLOW_WARN },
|
|
10992
11230
|
"companion: in-flight turn-id map is implausibly large \u2014 completion pruning is likely broken. Keeping every mapping; dropping one would silently lose a resumable turn."
|
|
10993
11231
|
);
|
|
@@ -10995,11 +11233,18 @@ var CompanionSupervisor = class {
|
|
|
10995
11233
|
markDispatched(workspaceId, ev.id);
|
|
10996
11234
|
}
|
|
10997
11235
|
if (resumedTurnId) {
|
|
10998
|
-
this.log.
|
|
11236
|
+
this.log.debug(
|
|
10999
11237
|
{ workspaceId, eventId: ev.id, turnId },
|
|
11000
11238
|
"companion: resuming an interrupted turn under its original id"
|
|
11001
11239
|
);
|
|
11002
11240
|
}
|
|
11241
|
+
const turnBindings = {
|
|
11242
|
+
agentName: agent.displayName,
|
|
11243
|
+
conversationId: payload.conversationId,
|
|
11244
|
+
agentId: agent.agentId,
|
|
11245
|
+
workspaceId
|
|
11246
|
+
};
|
|
11247
|
+
this.log.info(turnBindings, "Turn started");
|
|
11003
11248
|
const result = await agent.dispatcher.handle(payload, {
|
|
11004
11249
|
turnId,
|
|
11005
11250
|
resumed: resumedTurnId !== null
|
|
@@ -11008,16 +11253,23 @@ var CompanionSupervisor = class {
|
|
|
11008
11253
|
if (ev.id) wr.cursor.settle(ev.id);
|
|
11009
11254
|
const durationS = (result.durationMs / 1e3).toFixed(1);
|
|
11010
11255
|
const bindings = {
|
|
11011
|
-
|
|
11012
|
-
|
|
11013
|
-
agentId: agent.agentId
|
|
11256
|
+
...turnBindings,
|
|
11257
|
+
...result.transcriptPath ? { transcriptPath: result.transcriptPath } : {}
|
|
11014
11258
|
};
|
|
11015
11259
|
if (result.ok) {
|
|
11016
|
-
this.log.info(bindings,
|
|
11260
|
+
this.log.info(bindings, `Replied (${durationS}s)`);
|
|
11261
|
+
} else if (result.reason === "cancelled") {
|
|
11262
|
+
this.log.info(turnBindings, `Turn stopped (${durationS}s)`);
|
|
11263
|
+
} else if ([
|
|
11264
|
+
"turn_context_not_found",
|
|
11265
|
+
"turn_context_not_admitted",
|
|
11266
|
+
"trigger_role_not_dispatchable"
|
|
11267
|
+
].includes(result.reason ?? "") || result.reason === "lease_refused: dispatch_not_admitted" || result.reason === "lease_refused: turn_already_ended") {
|
|
11268
|
+
this.log.debug({ ...turnBindings, reason: result.reason }, "Dispatch no longer needed");
|
|
11017
11269
|
} else {
|
|
11018
|
-
this.log.
|
|
11270
|
+
this.log.info(
|
|
11019
11271
|
bindings,
|
|
11020
|
-
|
|
11272
|
+
`Turn failed after ${durationS}s: ${turnFailureCopy(result.reason, result.runtime)}`
|
|
11021
11273
|
);
|
|
11022
11274
|
}
|
|
11023
11275
|
}
|
|
@@ -11037,7 +11289,7 @@ var CompanionSupervisor = class {
|
|
|
11037
11289
|
}
|
|
11038
11290
|
} catch (err) {
|
|
11039
11291
|
drainDelay = Math.min(drainDelay * 2, DRAIN_MAX_MS);
|
|
11040
|
-
this.log.
|
|
11292
|
+
this.log.debug(
|
|
11041
11293
|
{ agentId, err: err instanceof Error ? err.message : String(err) },
|
|
11042
11294
|
"companion: outbox drain pass threw (will retry with backoff)"
|
|
11043
11295
|
);
|
|
@@ -11072,7 +11324,7 @@ var CompanionSupervisor = class {
|
|
|
11072
11324
|
const next = loadConfig();
|
|
11073
11325
|
if (!next) return;
|
|
11074
11326
|
this.config = next;
|
|
11075
|
-
this.log
|
|
11327
|
+
configureLogger(this.log, next);
|
|
11076
11328
|
this.rebuildDispatchers();
|
|
11077
11329
|
void this.refreshAssignments();
|
|
11078
11330
|
}
|
|
@@ -11088,7 +11340,7 @@ var CompanionSupervisor = class {
|
|
|
11088
11340
|
};
|
|
11089
11341
|
this.config = next;
|
|
11090
11342
|
saveConfig(next);
|
|
11091
|
-
this.log
|
|
11343
|
+
configureLogger(this.log, next);
|
|
11092
11344
|
}
|
|
11093
11345
|
// ---- harnesses (CT586) ----
|
|
11094
11346
|
// Probe all three harnesses, cache the signals + versions, and push the derived
|
|
@@ -11114,7 +11366,7 @@ var CompanionSupervisor = class {
|
|
|
11114
11366
|
};
|
|
11115
11367
|
this.hub.setHarnesses(deriveHarnessSnapshot(signals).harnesses);
|
|
11116
11368
|
} catch (err) {
|
|
11117
|
-
this.log.
|
|
11369
|
+
this.log.debug(
|
|
11118
11370
|
{ err: err instanceof Error ? err.message : String(err) },
|
|
11119
11371
|
"companion: harness probe failed (will retry on next beat)"
|
|
11120
11372
|
);
|
|
@@ -11230,6 +11482,7 @@ var CompanionSupervisor = class {
|
|
|
11230
11482
|
workspaceSlug: wr.slug,
|
|
11231
11483
|
agentId: runner.agentId,
|
|
11232
11484
|
agentUsername: runner.username,
|
|
11485
|
+
agentDisplayName: runner.displayName,
|
|
11233
11486
|
credential: runner.credential,
|
|
11234
11487
|
runConfig: runner.runConfig,
|
|
11235
11488
|
aborts: runner.aborts
|
|
@@ -11335,7 +11588,7 @@ function handleUncaught(log, err, origin) {
|
|
|
11335
11588
|
const message = err instanceof Error ? err.message : String(err);
|
|
11336
11589
|
const code = errorCode(err);
|
|
11337
11590
|
if (isRecoverableSocketError(err)) {
|
|
11338
|
-
log.
|
|
11591
|
+
log.debug(
|
|
11339
11592
|
{ origin, code, err: message },
|
|
11340
11593
|
"companion: recovered from a socket error (kept running \u2014 a broken pipe never kills the companion)"
|
|
11341
11594
|
);
|
|
@@ -11343,7 +11596,7 @@ function handleUncaught(log, err, origin) {
|
|
|
11343
11596
|
}
|
|
11344
11597
|
log.error(
|
|
11345
11598
|
{ origin, code, err: message, stack: err instanceof Error ? err.stack : void 0 },
|
|
11346
|
-
"
|
|
11599
|
+
"An unexpected error occurred; the companion is still running."
|
|
11347
11600
|
);
|
|
11348
11601
|
}
|
|
11349
11602
|
|
|
@@ -11377,7 +11630,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
11377
11630
|
try {
|
|
11378
11631
|
({ cfg, claudeOnPath: claudeCode } = await requireStartConfig({
|
|
11379
11632
|
...opts.probeClaude ? { probeClaude: opts.probeClaude } : {},
|
|
11380
|
-
onMigrated: (migrated, onPath) => log.
|
|
11633
|
+
onMigrated: (migrated, onPath) => log.debug(
|
|
11381
11634
|
{ claudeCode: isClaudeCodeConnected(migrated) },
|
|
11382
11635
|
onPath ? "companion: carried Claude Code over as a connected harness on this device (connectors are now chosen, not detected)" : "companion: no Claude Code on PATH, so this device starts with it disconnected (connectors are now chosen, not detected)"
|
|
11383
11636
|
)
|
|
@@ -11398,7 +11651,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
11398
11651
|
});
|
|
11399
11652
|
throw err;
|
|
11400
11653
|
}
|
|
11401
|
-
|
|
11654
|
+
configureLogger(log, cfg);
|
|
11402
11655
|
const harnessVersions = await probeHarnessVersions({
|
|
11403
11656
|
// Connected AND its CLI answered — the two things that make a
|
|
11404
11657
|
// `claude --version` probe worth spawning. Not the offer predicate.
|
|
@@ -11598,7 +11851,12 @@ Connect a harness to the running companion: cabane-companion connect claude-code
|
|
|
11598
11851
|
}
|
|
11599
11852
|
clearRuntimeState();
|
|
11600
11853
|
}
|
|
11601
|
-
const args = [
|
|
11854
|
+
const args = [
|
|
11855
|
+
"start",
|
|
11856
|
+
"--foreground",
|
|
11857
|
+
...opts.logLevel ? ["--log-level", opts.logLevel] : [],
|
|
11858
|
+
...opts.logFormat ? ["--log-format", opts.logFormat] : []
|
|
11859
|
+
];
|
|
11602
11860
|
const launchDetached = () => {
|
|
11603
11861
|
const spawned = spawnDetached(args);
|
|
11604
11862
|
spawned.unref();
|
|
@@ -11628,7 +11886,7 @@ Check ${companionLogPath()} for errors, or run \`cabane-companion status\`.
|
|
|
11628
11886
|
if (quiet) return { started: true, state };
|
|
11629
11887
|
process.stdout.write(
|
|
11630
11888
|
`Cabane Companion started in the background (pid ${state.pid}).
|
|
11631
|
-
Logs: ${companionLogPath()}
|
|
11889
|
+
Logs: cabane-companion logs (${companionLogPath()})
|
|
11632
11890
|
Status: cabane-companion status
|
|
11633
11891
|
Stop: cabane-companion stop
|
|
11634
11892
|
`
|
|
@@ -11654,11 +11912,11 @@ function defaultSpawnDetached(args) {
|
|
|
11654
11912
|
var FORCE_EXIT_MS = 4e3;
|
|
11655
11913
|
var MAX_CODES = 3;
|
|
11656
11914
|
async function start(opts = {}) {
|
|
11915
|
+
setLogOverrides(opts);
|
|
11657
11916
|
const interactive = isInteractive();
|
|
11658
|
-
if (!interactive) return startScript(opts, false);
|
|
11659
11917
|
setConsoleLogging(false);
|
|
11660
11918
|
try {
|
|
11661
|
-
await startScript(opts,
|
|
11919
|
+
await startScript(opts, interactive);
|
|
11662
11920
|
} finally {
|
|
11663
11921
|
setConsoleLogging(true);
|
|
11664
11922
|
}
|
|
@@ -11669,7 +11927,10 @@ async function startScript(opts, interactive) {
|
|
|
11669
11927
|
reportAlreadyRunning(running.pid);
|
|
11670
11928
|
return;
|
|
11671
11929
|
}
|
|
11672
|
-
|
|
11930
|
+
const daemon = process.env.CABANE_COMPANION_DAEMON === "1";
|
|
11931
|
+
if (daemon)
|
|
11932
|
+
write(`Running in the background (pid ${process.pid}). Stop with cabane-companion stop.`);
|
|
11933
|
+
else blank();
|
|
11673
11934
|
const held = !isDevicePaired() ? await collectHarnessChoices(interactive) : [];
|
|
11674
11935
|
let paired = null;
|
|
11675
11936
|
if (!isDevicePaired()) {
|
|
@@ -11678,14 +11939,19 @@ async function startScript(opts, interactive) {
|
|
|
11678
11939
|
return;
|
|
11679
11940
|
}
|
|
11680
11941
|
const { note } = writePairedConfig(paired);
|
|
11681
|
-
|
|
11942
|
+
getLogger().info(`Paired with Cabane as ${paired.deviceLabel}`);
|
|
11943
|
+
if (note)
|
|
11944
|
+
getLogger().warn(
|
|
11945
|
+
{ note },
|
|
11946
|
+
"An older configuration was repaired during pairing. Check ~/.cabane/config.json."
|
|
11947
|
+
);
|
|
11682
11948
|
}
|
|
11683
11949
|
const justPaired = paired !== null;
|
|
11684
11950
|
const log = getLogger();
|
|
11685
11951
|
const result = await createCompanionRuntime({
|
|
11686
11952
|
// The script says what's connectable in its own words below; a stderr warning
|
|
11687
11953
|
// in the middle of it would be the same news, worse.
|
|
11688
|
-
onReadinessWarning: (message) => log.
|
|
11954
|
+
onReadinessWarning: (message) => log.warn(message)
|
|
11689
11955
|
});
|
|
11690
11956
|
if (!result.ok) {
|
|
11691
11957
|
reportAlreadyRunning(result.existing?.pid);
|
|
@@ -11703,24 +11969,28 @@ async function startScript(opts, interactive) {
|
|
|
11703
11969
|
if (interactive && !opts.foreground) {
|
|
11704
11970
|
await handOffToBackground(runtime, {
|
|
11705
11971
|
justPaired,
|
|
11706
|
-
connected
|
|
11972
|
+
connected,
|
|
11973
|
+
...opts
|
|
11707
11974
|
});
|
|
11708
11975
|
return;
|
|
11709
11976
|
}
|
|
11710
11977
|
if (interactive) {
|
|
11711
11978
|
blank();
|
|
11712
11979
|
write(INDENT + tick("Cabane companion is running in this terminal."));
|
|
11713
|
-
write(
|
|
11980
|
+
write(
|
|
11981
|
+
` Stop: Ctrl-C Logs: cabane-companion logs (${tildePath(companionLogPath())})`
|
|
11982
|
+
);
|
|
11714
11983
|
blank();
|
|
11715
11984
|
write(`${INDENT}Listening for messages\u2026`);
|
|
11716
11985
|
setConsoleLogging(true);
|
|
11717
|
-
} else {
|
|
11986
|
+
} else if (!daemon) {
|
|
11718
11987
|
blank();
|
|
11719
11988
|
write(
|
|
11720
11989
|
`${INDENT}Cabane companion is running. No terminal attached, so it stays in the foreground.`
|
|
11721
11990
|
);
|
|
11722
11991
|
write(`${INDENT}Listening for messages\u2026`);
|
|
11723
11992
|
}
|
|
11993
|
+
setConsoleLogging(true);
|
|
11724
11994
|
await runAttached(runtime);
|
|
11725
11995
|
}
|
|
11726
11996
|
async function liveCompanion() {
|
|
@@ -11796,7 +12066,7 @@ function prePairFoundPhrase(harness) {
|
|
|
11796
12066
|
}
|
|
11797
12067
|
async function pairHere(opts, interactive) {
|
|
11798
12068
|
const baseUrl = resolvePairBaseUrl(opts.server);
|
|
11799
|
-
const label = deviceLabelFromHostname(
|
|
12069
|
+
const label = deviceLabelFromHostname(hostname3());
|
|
11800
12070
|
const aborter = new AbortController();
|
|
11801
12071
|
const onSigint = () => aborter.abort();
|
|
11802
12072
|
process.on("SIGINT", onSigint);
|
|
@@ -11866,7 +12136,7 @@ function connectCommand(runtime) {
|
|
|
11866
12136
|
}
|
|
11867
12137
|
async function handOffToBackground(runtime, ctx) {
|
|
11868
12138
|
await runtime.stop();
|
|
11869
|
-
const outcome = await startDaemon({ report: "failures" });
|
|
12139
|
+
const outcome = await startDaemon({ report: "failures", ...ctx });
|
|
11870
12140
|
if (!outcome.started) {
|
|
11871
12141
|
return;
|
|
11872
12142
|
}
|
|
@@ -11879,7 +12149,9 @@ async function handOffToBackground(runtime, ctx) {
|
|
|
11879
12149
|
}
|
|
11880
12150
|
}
|
|
11881
12151
|
write(INDENT + tick("Cabane companion is running in the background."));
|
|
11882
|
-
write(
|
|
12152
|
+
write(
|
|
12153
|
+
` Stop: cabane-companion stop Logs: cabane-companion logs (${tildePath(companionLogPath())})`
|
|
12154
|
+
);
|
|
11883
12155
|
if (ctx.justPaired) {
|
|
11884
12156
|
blank();
|
|
11885
12157
|
write(`${INDENT}! It won't restart on its own \u2014 after a reboot, or if it ever stops,`);
|
|
@@ -11891,20 +12163,15 @@ async function handOffToBackground(runtime, ctx) {
|
|
|
11891
12163
|
async function runAttached(runtime) {
|
|
11892
12164
|
await new Promise((resolve2) => {
|
|
11893
12165
|
let shuttingDown = false;
|
|
11894
|
-
const shutdown = async (
|
|
12166
|
+
const shutdown = async () => {
|
|
11895
12167
|
if (shuttingDown) {
|
|
11896
|
-
|
|
11897
|
-
companion: second ${signal}, force-quitting.
|
|
11898
|
-
`);
|
|
12168
|
+
getLogger().error("Stopped: forced to quit after a second stop request.");
|
|
11899
12169
|
process.exit(1);
|
|
11900
12170
|
}
|
|
11901
12171
|
shuttingDown = true;
|
|
11902
|
-
|
|
11903
|
-
companion: received ${signal}, shutting down\u2026
|
|
11904
|
-
`);
|
|
12172
|
+
getLogger().info("Stopping the companion");
|
|
11905
12173
|
const forceExit = setTimeout(() => {
|
|
11906
|
-
|
|
11907
|
-
`);
|
|
12174
|
+
getLogger().error("Stopped: shutdown took too long.");
|
|
11908
12175
|
process.exit(1);
|
|
11909
12176
|
}, FORCE_EXIT_MS);
|
|
11910
12177
|
forceExit.unref?.();
|
|
@@ -11913,8 +12180,8 @@ companion: received ${signal}, shutting down\u2026
|
|
|
11913
12180
|
resolve2();
|
|
11914
12181
|
process.exit(0);
|
|
11915
12182
|
};
|
|
11916
|
-
process.on("SIGINT", () => void shutdown(
|
|
11917
|
-
process.on("SIGTERM", () => void shutdown(
|
|
12183
|
+
process.on("SIGINT", () => void shutdown());
|
|
12184
|
+
process.on("SIGTERM", () => void shutdown());
|
|
11918
12185
|
});
|
|
11919
12186
|
}
|
|
11920
12187
|
|
|
@@ -11933,7 +12200,7 @@ async function status() {
|
|
|
11933
12200
|
`);
|
|
11934
12201
|
if (cfg.deviceLabel) process.stdout.write(`device: ${cfg.deviceLabel}
|
|
11935
12202
|
`);
|
|
11936
|
-
process.stdout.write(`log file: ${companionLogPath()}
|
|
12203
|
+
process.stdout.write(`log file: cabane-companion logs (${companionLogPath()})
|
|
11937
12204
|
`);
|
|
11938
12205
|
process.stdout.write(`transcripts: ${transcriptsLine()}
|
|
11939
12206
|
`);
|
|
@@ -11968,9 +12235,9 @@ async function status() {
|
|
|
11968
12235
|
process.stdout.write(`prepare hook: ${cfg.prepareHook.command} (device-level, all agents)
|
|
11969
12236
|
`);
|
|
11970
12237
|
}
|
|
11971
|
-
const
|
|
11972
|
-
if (
|
|
11973
|
-
process.stdout.write(`local overrides for: ${
|
|
12238
|
+
const overrides2 = Object.keys(cfg.agents ?? {});
|
|
12239
|
+
if (overrides2.length > 0) {
|
|
12240
|
+
process.stdout.write(`local overrides for: ${overrides2.join(", ")}
|
|
11974
12241
|
`);
|
|
11975
12242
|
}
|
|
11976
12243
|
}
|
|
@@ -12416,8 +12683,21 @@ program.command("write-paired-config", { hidden: true }).description("persist an
|
|
|
12416
12683
|
});
|
|
12417
12684
|
program.command("start").description(
|
|
12418
12685
|
"pair this device if needed, connect a coding agent on it, and run in the background."
|
|
12419
|
-
).option("--foreground", "run attached in this terminal (for terminals and supervisors)").option("--daemon", "run in the background (the default; kept for compatibility)").option("--server <url>", "the cabane instance to pair with, if this device isn\u2019t paired yet").addOption(
|
|
12686
|
+
).option("--foreground", "run attached in this terminal (for terminals and supervisors)").option("--daemon", "run in the background (the default; kept for compatibility)").option("--server <url>", "the cabane instance to pair with, if this device isn\u2019t paired yet").addOption(
|
|
12687
|
+
new Option("--log-level <level>", "info (default), warn, or debug").choices([
|
|
12688
|
+
"info",
|
|
12689
|
+
"warn",
|
|
12690
|
+
"debug"
|
|
12691
|
+
])
|
|
12692
|
+
).addOption(
|
|
12693
|
+
new Option("--log-format <format>", "human (default) or json, for the log file").choices([
|
|
12694
|
+
"human",
|
|
12695
|
+
"json"
|
|
12696
|
+
])
|
|
12697
|
+
).addOption(new Option("--open").hideHelp()).addOption(new Option("--no-open").hideHelp()).addOption(new Option("--port <port>").hideHelp()).action(async (opts) => {
|
|
12420
12698
|
await start({
|
|
12699
|
+
...opts.logLevel ? { logLevel: opts.logLevel } : {},
|
|
12700
|
+
...opts.logFormat ? { logFormat: opts.logFormat } : {},
|
|
12421
12701
|
...opts.foreground ? { foreground: true } : {},
|
|
12422
12702
|
...opts.server !== void 0 ? { server: opts.server } : {}
|
|
12423
12703
|
});
|
|
@@ -12431,6 +12711,13 @@ program.command("stop").description("stop a running companion (SIGTERM, then for
|
|
|
12431
12711
|
program.command("status").description("print the companion's local state (pairing, secrets, log path).").action(async () => {
|
|
12432
12712
|
await status();
|
|
12433
12713
|
});
|
|
12714
|
+
program.command("logs").description("show the companion log (last 50 lines; up to 1 MiB)").option("-n, --lines <N>", "number of recent lines", (value) => {
|
|
12715
|
+
if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)))
|
|
12716
|
+
throw new InvalidArgumentError("N must be a non-negative integer.");
|
|
12717
|
+
return Number(value);
|
|
12718
|
+
}).option("-f, --follow", "follow new log lines (Ctrl-C to stop)").action(async (opts) => {
|
|
12719
|
+
await logs(opts);
|
|
12720
|
+
});
|
|
12434
12721
|
program.command("transcript").description("show the full agent transcript for a recent dispatch (the agent's whole turn).").argument("[file]", "a transcript filename or substring; omit to list recent transcripts").option("--last", "render the most recent transcript").option("-f, --follow", "watch for new turns and live-render them as they land (Ctrl-C to stop)").action(async (file, opts) => {
|
|
12435
12722
|
await transcript({
|
|
12436
12723
|
...file !== void 0 ? { target: file } : {},
|
|
@@ -12463,7 +12750,7 @@ program.parseAsync(process.argv).catch((err) => {
|
|
|
12463
12750
|
}
|
|
12464
12751
|
setConsoleLogging(false);
|
|
12465
12752
|
try {
|
|
12466
|
-
getLogger().error({ err },
|
|
12753
|
+
getLogger().error({ err }, `Stopped: ${err instanceof Error ? err.message : String(err)}`);
|
|
12467
12754
|
} catch {
|
|
12468
12755
|
}
|
|
12469
12756
|
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|