@cabane/companion 0.6.100 → 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 +738 -443
- package/dist/pairing-config.js +4 -1
- package/dist/runtime.js +596 -409
- 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,42 +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
|
-
|
|
8243
8471
|
// src/turn-seq-floor.ts
|
|
8244
8472
|
var SeqFloorUnavailable = class extends Error {
|
|
8245
8473
|
constructor(detail) {
|
|
@@ -8255,7 +8483,7 @@ function resolveSeqFloor(sources, ctx) {
|
|
|
8255
8483
|
throw new SeqFloorUnavailable("server sent no committed floor for a resumed turn");
|
|
8256
8484
|
}
|
|
8257
8485
|
const floor = Math.max(serverFloor, outboxFloor);
|
|
8258
|
-
ctx.log.
|
|
8486
|
+
ctx.log.debug(
|
|
8259
8487
|
{ turnId: ctx.turnId, floor, outboxFloor, serverFloor },
|
|
8260
8488
|
"companion: resumed turn \u2014 seq counter seeded above its committed high-water mark"
|
|
8261
8489
|
);
|
|
@@ -8265,7 +8493,7 @@ function readOutboxFloor(read, turnId, log) {
|
|
|
8265
8493
|
try {
|
|
8266
8494
|
return read(turnId);
|
|
8267
8495
|
} catch (err) {
|
|
8268
|
-
log.
|
|
8496
|
+
log.debug(
|
|
8269
8497
|
{ turnId, err: err instanceof Error ? err.message : String(err) },
|
|
8270
8498
|
"companion: the on-disk outbox floor is unreadable; refusing to resume"
|
|
8271
8499
|
);
|
|
@@ -8508,8 +8736,8 @@ var TurnCommitter = class {
|
|
|
8508
8736
|
this.deps = deps;
|
|
8509
8737
|
this.onError = (err, hook) => {
|
|
8510
8738
|
deps.log.warn(
|
|
8511
|
-
{
|
|
8512
|
-
"
|
|
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`
|
|
8513
8741
|
);
|
|
8514
8742
|
deps.onCommitFailed?.(err);
|
|
8515
8743
|
};
|
|
@@ -8645,8 +8873,8 @@ async function postIncompleteNotice(ctx, harness, resolution) {
|
|
|
8645
8873
|
return true;
|
|
8646
8874
|
} catch (err) {
|
|
8647
8875
|
ctx.log.warn(
|
|
8648
|
-
{
|
|
8649
|
-
"
|
|
8876
|
+
{ ...apiErrorLogFields(err) },
|
|
8877
|
+
"Couldn't save the explanation to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
8650
8878
|
);
|
|
8651
8879
|
return false;
|
|
8652
8880
|
}
|
|
@@ -8656,7 +8884,7 @@ async function checkBundledBinary(ctx) {
|
|
|
8656
8884
|
if (!harness) return null;
|
|
8657
8885
|
const resolution = resolve(ctx, harness);
|
|
8658
8886
|
if (resolution.status === "present") return null;
|
|
8659
|
-
ctx.log.
|
|
8887
|
+
ctx.log.debug(
|
|
8660
8888
|
{
|
|
8661
8889
|
runtime: harness,
|
|
8662
8890
|
status: resolution.status,
|
|
@@ -8676,7 +8904,7 @@ async function reclassifyThrow(ctx, opts) {
|
|
|
8676
8904
|
if (!harness || opts.aborted) return null;
|
|
8677
8905
|
const resolution = resolve(ctx, harness);
|
|
8678
8906
|
if (resolution.status === "present") return null;
|
|
8679
|
-
ctx.log.
|
|
8907
|
+
ctx.log.debug(
|
|
8680
8908
|
{
|
|
8681
8909
|
runtime: harness,
|
|
8682
8910
|
status: resolution.status,
|
|
@@ -8873,13 +9101,13 @@ var TurnExecution = class {
|
|
|
8873
9101
|
);
|
|
8874
9102
|
} catch (err) {
|
|
8875
9103
|
turnLog.warn(
|
|
8876
|
-
{
|
|
8877
|
-
"
|
|
9104
|
+
{ ...apiErrorLogFields(err) },
|
|
9105
|
+
"Couldn't update the turn's status in Cabane. Check the conversation before trying again."
|
|
8878
9106
|
);
|
|
8879
9107
|
}
|
|
8880
9108
|
const durationMs = Date.now() - startedAt;
|
|
8881
9109
|
this.supervisor.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
8882
|
-
return { ok: false, durationMs, reason };
|
|
9110
|
+
return { ok: false, durationMs, reason, runtime: this.turnContext?.runtime };
|
|
8883
9111
|
}
|
|
8884
9112
|
// The post-admission analogue of `concludeBeforeRun` (Codo's stack review,
|
|
8885
9113
|
// blocking finding #2): this turn WAS admitted — it holds the conversation's
|
|
@@ -8908,13 +9136,13 @@ var TurnExecution = class {
|
|
|
8908
9136
|
);
|
|
8909
9137
|
} catch (err) {
|
|
8910
9138
|
turnLog.warn(
|
|
8911
|
-
{
|
|
8912
|
-
"
|
|
9139
|
+
{ ...apiErrorLogFields(err), turnId },
|
|
9140
|
+
"Couldn't save the turn's result to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
8913
9141
|
);
|
|
8914
9142
|
}
|
|
8915
9143
|
const durationMs = Date.now() - startedAt;
|
|
8916
9144
|
this.supervisor.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
8917
|
-
return { ok: false, durationMs, reason };
|
|
9145
|
+
return { ok: false, durationMs, reason, runtime: this.turnContext?.runtime };
|
|
8918
9146
|
}
|
|
8919
9147
|
async fetchContext() {
|
|
8920
9148
|
const { payload, turnId, turnLog } = this;
|
|
@@ -8931,20 +9159,20 @@ var TurnExecution = class {
|
|
|
8931
9159
|
} catch (err) {
|
|
8932
9160
|
const status2 = err instanceof ApiError ? err.status : 0;
|
|
8933
9161
|
if (status2 === 404) {
|
|
8934
|
-
turnLog.
|
|
9162
|
+
turnLog.debug(
|
|
8935
9163
|
{ err: err instanceof Error ? err.message : String(err) },
|
|
8936
9164
|
"dispatcher: turn-context 404 (stale event \u2014 conversation/message/participant gone); skipping"
|
|
8937
9165
|
);
|
|
8938
9166
|
throw this.concluded("turn_context_not_found");
|
|
8939
9167
|
}
|
|
8940
9168
|
if (status2 === 409 && apiErrorCode(err) === "dispatch_not_admitted") {
|
|
8941
|
-
turnLog.
|
|
9169
|
+
turnLog.debug(
|
|
8942
9170
|
"dispatcher: turn-context 409 dispatch_not_admitted (wake already resolved); skipping"
|
|
8943
9171
|
);
|
|
8944
9172
|
throw this.concluded("turn_context_not_admitted");
|
|
8945
9173
|
}
|
|
8946
9174
|
const reason = err instanceof Error ? err.message : String(err);
|
|
8947
|
-
turnLog.
|
|
9175
|
+
turnLog.debug({ err: reason }, "dispatcher: failed to fetch turn context");
|
|
8948
9176
|
const fetchReason = `fetch_failed: ${reason}`;
|
|
8949
9177
|
throw this.concluded(fetchReason, fetchReason);
|
|
8950
9178
|
}
|
|
@@ -8954,7 +9182,7 @@ var TurnExecution = class {
|
|
|
8954
9182
|
this.resumedFromSeq = resolveSeqFloor(sources, { turnId, log: turnLog });
|
|
8955
9183
|
this.seqCounter = Math.max(this.seqCounter, this.resumedFromSeq);
|
|
8956
9184
|
this.resumedSpoke = turnContext.turnSpoke === true || outboxSpoke;
|
|
8957
|
-
if (this.resumedSpoke) turnLog.
|
|
9185
|
+
if (this.resumedSpoke) turnLog.debug({ turnId }, "dispatcher: resumed span already spoke");
|
|
8958
9186
|
}
|
|
8959
9187
|
}
|
|
8960
9188
|
gateTrigger() {
|
|
@@ -8962,7 +9190,7 @@ var TurnExecution = class {
|
|
|
8962
9190
|
const message = this.turnContext.message;
|
|
8963
9191
|
const isDispatchableTrigger = message.role === "user" || message.role === "agent" || message.role === "system";
|
|
8964
9192
|
if (!isDispatchableTrigger) {
|
|
8965
|
-
turnLog.
|
|
9193
|
+
turnLog.debug({ role: message.role }, "dispatcher: trigger role not dispatchable \u2014 skipping");
|
|
8966
9194
|
throw this.concluded("unexpected_role", UNEXPECTED_ROLE_REASON);
|
|
8967
9195
|
}
|
|
8968
9196
|
this.supervisor.notifyStart({
|
|
@@ -8973,7 +9201,9 @@ var TurnExecution = class {
|
|
|
8973
9201
|
}
|
|
8974
9202
|
async resolveSecrets() {
|
|
8975
9203
|
const { payload, workspaceId, turnLog } = this;
|
|
8976
|
-
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
|
+
);
|
|
8977
9207
|
const { servers: resolvedMcpServers, missing } = resolveMcpSecrets(
|
|
8978
9208
|
// CT262: the user MCP DEFINITIONS (placeholder form) come from the turn
|
|
8979
9209
|
// context now, not a separate `getAgentSelf` run-config fetch.
|
|
@@ -8981,7 +9211,7 @@ var TurnExecution = class {
|
|
|
8981
9211
|
secretStore
|
|
8982
9212
|
);
|
|
8983
9213
|
if (missing.length > 0) {
|
|
8984
|
-
turnLog.
|
|
9214
|
+
turnLog.debug({ missing }, "dispatcher: turn needs secrets not declared on this companion");
|
|
8985
9215
|
const reason = missingSecretReason(missing);
|
|
8986
9216
|
throw this.concluded(reason, reason);
|
|
8987
9217
|
}
|
|
@@ -8996,7 +9226,7 @@ var TurnExecution = class {
|
|
|
8996
9226
|
if (effectiveCwd && !existsSync10(effectiveCwd)) {
|
|
8997
9227
|
turnLog.warn(
|
|
8998
9228
|
{ cwd: effectiveCwd },
|
|
8999
|
-
"
|
|
9229
|
+
"The working directory is missing; using the companion's directory instead. Check this agent's working directory setting."
|
|
9000
9230
|
);
|
|
9001
9231
|
effectiveCwd = void 0;
|
|
9002
9232
|
}
|
|
@@ -9004,7 +9234,7 @@ var TurnExecution = class {
|
|
|
9004
9234
|
if (prepareHook) {
|
|
9005
9235
|
let cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
|
|
9006
9236
|
if (cached2 && !checkoutState(cached2.cwd).ok) {
|
|
9007
|
-
turnLog.
|
|
9237
|
+
turnLog.debug(
|
|
9008
9238
|
{ cwd: cached2.cwd, reason: checkoutState(cached2.cwd).reason },
|
|
9009
9239
|
"dispatcher: the prepared checkout is no longer usable \u2014 re-running the prepare hook to restore it"
|
|
9010
9240
|
);
|
|
@@ -9031,7 +9261,7 @@ var TurnExecution = class {
|
|
|
9031
9261
|
});
|
|
9032
9262
|
} catch (err) {
|
|
9033
9263
|
const reason = err instanceof Error ? err.message : String(err);
|
|
9034
|
-
turnLog.
|
|
9264
|
+
turnLog.debug({ err: reason }, "dispatcher: prepare hook rejected a prepared turn");
|
|
9035
9265
|
const failReason = `prepare_failed: ${reason}`;
|
|
9036
9266
|
throw this.concluded(failReason, failReason);
|
|
9037
9267
|
}
|
|
@@ -9051,7 +9281,7 @@ var TurnExecution = class {
|
|
|
9051
9281
|
phase,
|
|
9052
9282
|
seq
|
|
9053
9283
|
}).catch((err) => {
|
|
9054
|
-
turnLog.
|
|
9284
|
+
turnLog.debug(
|
|
9055
9285
|
{ err: err instanceof Error ? err.message : String(err), phase },
|
|
9056
9286
|
"dispatcher: preparing-activity report failed (continuing with the hook)"
|
|
9057
9287
|
);
|
|
@@ -9091,7 +9321,7 @@ var TurnExecution = class {
|
|
|
9091
9321
|
clearTimeout(preparingTimer);
|
|
9092
9322
|
if (preparingStarted) reportPreparing("error");
|
|
9093
9323
|
const reason = err instanceof Error ? err.message : String(err);
|
|
9094
|
-
turnLog.
|
|
9324
|
+
turnLog.debug({ err: reason }, "dispatcher: prepare hook failed");
|
|
9095
9325
|
const failReason = `prepare_failed: ${reason}`;
|
|
9096
9326
|
throw this.concluded(failReason, failReason);
|
|
9097
9327
|
}
|
|
@@ -9139,14 +9369,14 @@ var TurnExecution = class {
|
|
|
9139
9369
|
} catch (err) {
|
|
9140
9370
|
const refusal = leaseRefusal(err);
|
|
9141
9371
|
if (refusal === "dispatch_not_admitted" || refusal === "turn_already_ended") {
|
|
9142
|
-
turnLog.
|
|
9372
|
+
turnLog.debug(
|
|
9143
9373
|
{ refusal, turnId },
|
|
9144
9374
|
"dispatcher: refused a turn lease; not running the model"
|
|
9145
9375
|
);
|
|
9146
9376
|
throw this.concluded(`lease_refused: ${refusal}`);
|
|
9147
9377
|
}
|
|
9148
9378
|
const reason = refusal ? `lease_refused: ${refusal}` : "lease_unconfirmed";
|
|
9149
|
-
turnLog.
|
|
9379
|
+
turnLog.debug(
|
|
9150
9380
|
{ refusal, turnId, err: err instanceof Error ? err.message : String(err) },
|
|
9151
9381
|
"dispatcher: turn lease not confirmed; not running the model"
|
|
9152
9382
|
);
|
|
@@ -9198,7 +9428,7 @@ var TurnExecution = class {
|
|
|
9198
9428
|
async selectAdapter() {
|
|
9199
9429
|
const { payload, workspaceId, turnId, turnLog } = this;
|
|
9200
9430
|
await guardBundledBinary(this.integrityContext(), (r, e) => this.concluded(r, e), this.outcome);
|
|
9201
|
-
const onWarn = (msg, meta) => turnLog.
|
|
9431
|
+
const onWarn = (msg, meta) => turnLog.debug(meta ?? {}, msg);
|
|
9202
9432
|
const adapters = [];
|
|
9203
9433
|
if (this.opts.claudeCodeAvailable?.() ?? true) {
|
|
9204
9434
|
adapters.push(createClaudeCodeAdapter({ queryFn: this.opts.queryFn, onWarn }));
|
|
@@ -9220,7 +9450,7 @@ var TurnExecution = class {
|
|
|
9220
9450
|
this.adapter = selectAdapter(registry, this.turnContext.runtime);
|
|
9221
9451
|
} catch (err) {
|
|
9222
9452
|
if (!(err instanceof RuntimeUnavailableError)) throw err;
|
|
9223
|
-
turnLog.
|
|
9453
|
+
turnLog.debug(
|
|
9224
9454
|
{ runtime: err.runtime, available: err.available },
|
|
9225
9455
|
"dispatcher: turn runtime not available on this device"
|
|
9226
9456
|
);
|
|
@@ -9233,8 +9463,8 @@ var TurnExecution = class {
|
|
|
9233
9463
|
});
|
|
9234
9464
|
} catch (postErr) {
|
|
9235
9465
|
turnLog.warn(
|
|
9236
|
-
{
|
|
9237
|
-
"
|
|
9466
|
+
{ ...apiErrorLogFields(postErr) },
|
|
9467
|
+
"Couldn't save the explanation to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
9238
9468
|
);
|
|
9239
9469
|
const reason = `runtime_unavailable:${err.runtime}`;
|
|
9240
9470
|
throw this.concluded(reason, reason);
|
|
@@ -9262,7 +9492,7 @@ var TurnExecution = class {
|
|
|
9262
9492
|
false,
|
|
9263
9493
|
true
|
|
9264
9494
|
);
|
|
9265
|
-
turnLog.
|
|
9495
|
+
turnLog.debug(
|
|
9266
9496
|
{ reason },
|
|
9267
9497
|
"dispatcher: stored session not resumed \u2014 the turn context was recomposed for a fresh session"
|
|
9268
9498
|
);
|
|
@@ -9273,8 +9503,8 @@ var TurnExecution = class {
|
|
|
9273
9503
|
};
|
|
9274
9504
|
} catch (err) {
|
|
9275
9505
|
turnLog.error(
|
|
9276
|
-
{ reason,
|
|
9277
|
-
"
|
|
9506
|
+
{ reason, ...apiErrorLogFields(err) },
|
|
9507
|
+
"Couldn't load the full conversation; continuing with the recent messages."
|
|
9278
9508
|
);
|
|
9279
9509
|
return null;
|
|
9280
9510
|
}
|
|
@@ -9295,7 +9525,10 @@ var TurnExecution = class {
|
|
|
9295
9525
|
dispatchId: this.dispatchId,
|
|
9296
9526
|
message: this.turnContext.message.body
|
|
9297
9527
|
},
|
|
9298
|
-
(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
|
+
)
|
|
9299
9532
|
) : null;
|
|
9300
9533
|
const turnRuntime = this.turnContext.runtime;
|
|
9301
9534
|
const committer = this.committer = new TurnCommitter({
|
|
@@ -9335,8 +9568,8 @@ var TurnExecution = class {
|
|
|
9335
9568
|
}
|
|
9336
9569
|
} catch (err) {
|
|
9337
9570
|
turnLog.warn(
|
|
9338
|
-
{
|
|
9339
|
-
"
|
|
9571
|
+
{ ...apiErrorLogFields(err) },
|
|
9572
|
+
"Couldn't check whether the agent chose to pass; the turn may appear to have failed."
|
|
9340
9573
|
);
|
|
9341
9574
|
}
|
|
9342
9575
|
};
|
|
@@ -9344,7 +9577,7 @@ var TurnExecution = class {
|
|
|
9344
9577
|
const fireTimeout = (reason) => {
|
|
9345
9578
|
if (abortController.signal.aborted) return;
|
|
9346
9579
|
o.timeoutReason = reason;
|
|
9347
|
-
turnLog.
|
|
9580
|
+
turnLog.debug(
|
|
9348
9581
|
{ reason, idleTimeoutMs, totalTimeoutMs },
|
|
9349
9582
|
"dispatcher: turn timeout \u2014 aborting"
|
|
9350
9583
|
);
|
|
@@ -9374,7 +9607,7 @@ var TurnExecution = class {
|
|
|
9374
9607
|
if (state !== "ended") return;
|
|
9375
9608
|
if (abortController.signal.aborted) return;
|
|
9376
9609
|
o.leaseLost = true;
|
|
9377
|
-
turnLog.
|
|
9610
|
+
turnLog.debug(
|
|
9378
9611
|
{ turnId, trigger },
|
|
9379
9612
|
"dispatcher: this turn is no longer running server-side \u2014 aborting the loop"
|
|
9380
9613
|
);
|
|
@@ -9402,7 +9635,7 @@ var TurnExecution = class {
|
|
|
9402
9635
|
if (isContentBearingEvent(event)) o.contentBearingEvents += 1;
|
|
9403
9636
|
armIdle();
|
|
9404
9637
|
if (abortController.signal.aborted) {
|
|
9405
|
-
turnLog.
|
|
9638
|
+
turnLog.debug("dispatcher: aborted mid-turn");
|
|
9406
9639
|
o.okResult = false;
|
|
9407
9640
|
o.resultReason = o.timeoutReason ?? "cancelled";
|
|
9408
9641
|
break;
|
|
@@ -9425,18 +9658,18 @@ var TurnExecution = class {
|
|
|
9425
9658
|
o.sessionWriteRejected = true;
|
|
9426
9659
|
turnLog.error(
|
|
9427
9660
|
{
|
|
9428
|
-
|
|
9661
|
+
...apiErrorLogFields(err),
|
|
9429
9662
|
status: status2,
|
|
9430
9663
|
responseBody: describeErrorBody(err instanceof ApiError ? err.body : void 0),
|
|
9431
9664
|
stateLength: event.state.length,
|
|
9432
9665
|
runtime: turnRuntime
|
|
9433
9666
|
},
|
|
9434
|
-
"
|
|
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"
|
|
9435
9668
|
);
|
|
9436
9669
|
} else {
|
|
9437
9670
|
turnLog.warn(
|
|
9438
|
-
{
|
|
9439
|
-
"
|
|
9671
|
+
{ ...apiErrorLogFields(err) },
|
|
9672
|
+
"Couldn't save the agent's memory; trying again next turn."
|
|
9440
9673
|
);
|
|
9441
9674
|
}
|
|
9442
9675
|
}
|
|
@@ -9477,7 +9710,7 @@ var TurnExecution = class {
|
|
|
9477
9710
|
await this.applyRecordedTurnControlIntent();
|
|
9478
9711
|
}
|
|
9479
9712
|
if (!abortController.signal.aborted && this.skipState.skipped) {
|
|
9480
|
-
turnLog.
|
|
9713
|
+
turnLog.debug(
|
|
9481
9714
|
{ reason: this.skipState.reason, turnId, ok: o.okResult },
|
|
9482
9715
|
"agent skipped turn (skip_turn)"
|
|
9483
9716
|
);
|
|
@@ -9485,7 +9718,7 @@ var TurnExecution = class {
|
|
|
9485
9718
|
} catch (err) {
|
|
9486
9719
|
o.okResult = false;
|
|
9487
9720
|
o.resultReason = err instanceof Error ? err.message : String(err);
|
|
9488
|
-
turnLog.
|
|
9721
|
+
turnLog.debug({ err: o.resultReason }, "dispatcher: SDK query threw");
|
|
9489
9722
|
o.runtimeIncomplete = await absorbBundledLoss(
|
|
9490
9723
|
this.integrityContext(),
|
|
9491
9724
|
abortController.signal.aborted,
|
|
@@ -9510,7 +9743,7 @@ var TurnExecution = class {
|
|
|
9510
9743
|
kind: "usage_capped",
|
|
9511
9744
|
resetsAt: health.limitedUntil
|
|
9512
9745
|
});
|
|
9513
|
-
turnLog.
|
|
9746
|
+
turnLog.debug(
|
|
9514
9747
|
{ runtime: turnRuntime, limitedUntil: health.limitedUntil },
|
|
9515
9748
|
"dispatcher: idle-timeout on a capped runtime \u2014 reclassified as usage_capped (CT639)"
|
|
9516
9749
|
);
|
|
@@ -9561,7 +9794,7 @@ var TurnExecution = class {
|
|
|
9561
9794
|
if (diagnosticReason && !["usage_capped", "rate_limited", "auth_expired", "cancelled", "skipped"].includes(
|
|
9562
9795
|
diagnosticReason.kind
|
|
9563
9796
|
)) {
|
|
9564
|
-
turnLog.
|
|
9797
|
+
turnLog.debug(
|
|
9565
9798
|
{
|
|
9566
9799
|
workspaceId,
|
|
9567
9800
|
turnId,
|
|
@@ -9589,8 +9822,8 @@ var TurnExecution = class {
|
|
|
9589
9822
|
);
|
|
9590
9823
|
} catch (err) {
|
|
9591
9824
|
turnLog.warn(
|
|
9592
|
-
{
|
|
9593
|
-
"
|
|
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"
|
|
9594
9827
|
);
|
|
9595
9828
|
}
|
|
9596
9829
|
this.supervisor.releaseAbort(turnId, abortController);
|
|
@@ -9607,9 +9840,6 @@ var TurnExecution = class {
|
|
|
9607
9840
|
...o.resultReason ? { reason: o.resultReason } : {},
|
|
9608
9841
|
durationMs
|
|
9609
9842
|
});
|
|
9610
|
-
if (!o.okResult && o.resultReason !== "cancelled") {
|
|
9611
|
-
turnLog.info(`turn failed \u2014 full this.transcript: ${this.transcript.path}`);
|
|
9612
|
-
}
|
|
9613
9843
|
if (o.settledDiagnostics?.resultReason?.kind === "empty_result" || o.settledDiagnostics?.resultReason?.kind === "empty_result_unverified") {
|
|
9614
9844
|
this.transcript.preserveAnomaly();
|
|
9615
9845
|
}
|
|
@@ -9634,7 +9864,9 @@ var TurnExecution = class {
|
|
|
9634
9864
|
return {
|
|
9635
9865
|
ok: false,
|
|
9636
9866
|
durationMs,
|
|
9637
|
-
...o.resultReason ? { reason: o.resultReason } : {}
|
|
9867
|
+
...o.resultReason ? { reason: o.resultReason } : {},
|
|
9868
|
+
runtime: this.turnContext.runtime,
|
|
9869
|
+
...this.transcript ? { transcriptPath: tildePath(this.transcript.path) } : {}
|
|
9638
9870
|
};
|
|
9639
9871
|
}
|
|
9640
9872
|
};
|
|
@@ -9840,7 +10072,7 @@ var Outbox = class {
|
|
|
9840
10072
|
}
|
|
9841
10073
|
this.log?.warn(
|
|
9842
10074
|
{ workspaceId: this.workspaceId, err: err instanceof Error ? err.message : String(err) },
|
|
9843
|
-
"
|
|
10075
|
+
"Couldn't save an update on this machine. Check free disk space and access to ~/.cabane."
|
|
9844
10076
|
);
|
|
9845
10077
|
return;
|
|
9846
10078
|
}
|
|
@@ -9928,7 +10160,7 @@ var Outbox = class {
|
|
|
9928
10160
|
dropCorrupt(full) {
|
|
9929
10161
|
this.log?.warn(
|
|
9930
10162
|
{ workspaceId: this.workspaceId, file: full },
|
|
9931
|
-
"
|
|
10163
|
+
"A saved update is unreadable and couldn't be delivered. Check the conversation for missing replies."
|
|
9932
10164
|
);
|
|
9933
10165
|
try {
|
|
9934
10166
|
rmSync7(full, { force: true });
|
|
@@ -9945,7 +10177,7 @@ var Outbox = class {
|
|
|
9945
10177
|
if (now - e.enqueuedAt > MAX_AGE_MS) {
|
|
9946
10178
|
this.log?.warn(
|
|
9947
10179
|
{ workspaceId: this.workspaceId, turnId: e.turnId, seq: e.seq, kind: e.kind },
|
|
9948
|
-
"
|
|
10180
|
+
"A saved update waited too long and couldn't be delivered. Check the conversation for missing replies."
|
|
9949
10181
|
);
|
|
9950
10182
|
this.remove(e.turnId, e.seq);
|
|
9951
10183
|
} else {
|
|
@@ -9957,7 +10189,7 @@ var Outbox = class {
|
|
|
9957
10189
|
for (const e of survivors.slice(0, overflow)) {
|
|
9958
10190
|
this.log?.warn(
|
|
9959
10191
|
{ workspaceId: this.workspaceId, turnId: e.turnId, seq: e.seq, kind: e.kind },
|
|
9960
|
-
"
|
|
10192
|
+
"Too many updates are waiting; the oldest couldn't be kept. Check the connection to Cabane."
|
|
9961
10193
|
);
|
|
9962
10194
|
this.remove(e.turnId, e.seq);
|
|
9963
10195
|
}
|
|
@@ -10043,11 +10275,14 @@ var SseSubscriber = class {
|
|
|
10043
10275
|
try {
|
|
10044
10276
|
await this.connect();
|
|
10045
10277
|
backoff = 500;
|
|
10046
|
-
if (!this.aborted)
|
|
10278
|
+
if (!this.aborted) {
|
|
10279
|
+
this.opts.log.warn("Lost the connection to Cabane; reconnecting");
|
|
10280
|
+
await sleep3(backoff);
|
|
10281
|
+
}
|
|
10047
10282
|
} catch (err) {
|
|
10048
10283
|
if (this.aborted) return;
|
|
10049
10284
|
if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
|
|
10050
|
-
this.opts.log.
|
|
10285
|
+
this.opts.log.debug(
|
|
10051
10286
|
{ workspaceId: this.opts.workspaceId, status: err.status },
|
|
10052
10287
|
"SSE auth failed \u2014 tearing down this workspace subscriber"
|
|
10053
10288
|
);
|
|
@@ -10060,7 +10295,7 @@ var SseSubscriber = class {
|
|
|
10060
10295
|
err: err instanceof Error ? err.message : String(err),
|
|
10061
10296
|
backoff
|
|
10062
10297
|
},
|
|
10063
|
-
"
|
|
10298
|
+
"Lost the connection to Cabane; reconnecting"
|
|
10064
10299
|
);
|
|
10065
10300
|
await sleep3(backoff);
|
|
10066
10301
|
backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
|
|
@@ -10084,7 +10319,7 @@ var SseSubscriber = class {
|
|
|
10084
10319
|
if (!res.body) {
|
|
10085
10320
|
throw new Error("SSE response has no body");
|
|
10086
10321
|
}
|
|
10087
|
-
this.opts.log.
|
|
10322
|
+
this.opts.log.debug({ workspaceId: this.opts.workspaceId }, "SSE connected");
|
|
10088
10323
|
let opened = true;
|
|
10089
10324
|
this.opts.onOpen?.();
|
|
10090
10325
|
const parser = createParser({
|
|
@@ -10214,15 +10449,12 @@ var CompanionSupervisor = class {
|
|
|
10214
10449
|
async start() {
|
|
10215
10450
|
this.log.info(
|
|
10216
10451
|
{ protocolVersion: TURN_PROTOCOL_VERSION, version: COMPANION_VERSION },
|
|
10217
|
-
|
|
10452
|
+
`Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ?? hostname2()}`
|
|
10218
10453
|
);
|
|
10219
10454
|
const startupHarnessRefresh = this.refreshHarnessStatuses();
|
|
10220
10455
|
this.trackHeartbeat(startupHarnessRefresh);
|
|
10221
10456
|
if (!this.config.deviceToken) {
|
|
10222
|
-
this.log.warn("
|
|
10223
|
-
process.stdout.write(
|
|
10224
|
-
"companion: this device is not paired \u2014 run `cabane-companion pair`, then confirm the short code in Settings \u2192 Connectors.\n"
|
|
10225
|
-
);
|
|
10457
|
+
this.log.warn("This device isn't paired. Run cabane-companion start to pair it.");
|
|
10226
10458
|
return;
|
|
10227
10459
|
}
|
|
10228
10460
|
this.deviceApi = new DeviceApi({
|
|
@@ -10236,6 +10468,7 @@ var CompanionSupervisor = class {
|
|
|
10236
10468
|
token: this.config.deviceToken,
|
|
10237
10469
|
log: this.log,
|
|
10238
10470
|
lastEventId: null,
|
|
10471
|
+
onOpen: () => this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ?? hostname2()}`),
|
|
10239
10472
|
onMessage: async (ev) => {
|
|
10240
10473
|
if (ev.event === "assignments_changed") {
|
|
10241
10474
|
if (this.refreshing && this.inFlightRefresh) await this.inFlightRefresh;
|
|
@@ -10243,11 +10476,13 @@ var CompanionSupervisor = class {
|
|
|
10243
10476
|
}
|
|
10244
10477
|
},
|
|
10245
10478
|
onAuthFailure: () => {
|
|
10246
|
-
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
|
+
);
|
|
10247
10482
|
}
|
|
10248
10483
|
});
|
|
10249
|
-
this.deviceSub.start();
|
|
10250
10484
|
await this.refreshAssignments();
|
|
10485
|
+
this.deviceSub.start();
|
|
10251
10486
|
const firstHeartbeat = startupHarnessRefresh.then(async () => {
|
|
10252
10487
|
if (this.stopped) return;
|
|
10253
10488
|
await this.sendHeartbeat();
|
|
@@ -10283,7 +10518,9 @@ var CompanionSupervisor = class {
|
|
|
10283
10518
|
if (!this.deviceApi) return;
|
|
10284
10519
|
await this.refreshHarnessStatuses();
|
|
10285
10520
|
try {
|
|
10286
|
-
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
|
+
);
|
|
10287
10524
|
const connectorReports = this.connectorHealth.reports();
|
|
10288
10525
|
const opencodeModels = this.config.opencode?.serverUrl ? await enumerateOpencodeModels(this.config.opencode.serverUrl) : null;
|
|
10289
10526
|
const res = await this.deviceApi.heartbeat({
|
|
@@ -10330,8 +10567,8 @@ var CompanionSupervisor = class {
|
|
|
10330
10567
|
void this.recoverRuns();
|
|
10331
10568
|
} catch (err) {
|
|
10332
10569
|
this.log.warn(
|
|
10333
|
-
{
|
|
10334
|
-
"
|
|
10570
|
+
{ ...apiErrorLogFields(err) },
|
|
10571
|
+
"Couldn't check in with Cabane; trying again shortly."
|
|
10335
10572
|
);
|
|
10336
10573
|
}
|
|
10337
10574
|
}
|
|
@@ -10346,7 +10583,7 @@ var CompanionSupervisor = class {
|
|
|
10346
10583
|
this.versionSkewWarned = true;
|
|
10347
10584
|
this.log.warn(
|
|
10348
10585
|
{ companionVersion: COMPANION_VERSION, serverVersion },
|
|
10349
|
-
"
|
|
10586
|
+
"This companion and Cabane are on different versions. If turns fail, update with npm i -g @cabane/companion@latest"
|
|
10350
10587
|
);
|
|
10351
10588
|
}
|
|
10352
10589
|
// Pull assignments and reconcile the live runner set against them. Re-entrancy
|
|
@@ -10368,8 +10605,8 @@ var CompanionSupervisor = class {
|
|
|
10368
10605
|
device = resp.device;
|
|
10369
10606
|
} catch (err) {
|
|
10370
10607
|
this.log.error(
|
|
10371
|
-
{
|
|
10372
|
-
"
|
|
10608
|
+
{ ...apiErrorLogFields(err) },
|
|
10609
|
+
"Couldn't load the assigned agents. Check that this device is active in Cabane; trying again shortly."
|
|
10373
10610
|
);
|
|
10374
10611
|
this.hub.setDeviceError(err instanceof Error ? err.message : String(err));
|
|
10375
10612
|
return;
|
|
@@ -10435,14 +10672,17 @@ var CompanionSupervisor = class {
|
|
|
10435
10672
|
const credential = it.credential ?? getCredential(it.agentId);
|
|
10436
10673
|
const runConfig = parseRunConfig(
|
|
10437
10674
|
it.runConfig,
|
|
10438
|
-
(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
|
+
)
|
|
10439
10679
|
);
|
|
10440
10680
|
const required = requiredSecretNames(runConfig.mcpServers);
|
|
10441
10681
|
const missing = required.filter((n) => !exposed.has(n));
|
|
10442
10682
|
if (!credential) {
|
|
10443
10683
|
this.log.error(
|
|
10444
10684
|
{ workspaceId, agentId: it.agentId, username: it.agentUsername },
|
|
10445
|
-
|
|
10685
|
+
`Cannot run ${it.agentDisplayName}: its sign-in to Cabane is missing. Reassign it to this device in Cabane.`
|
|
10446
10686
|
);
|
|
10447
10687
|
this.removeAgent(wr, it.agentId);
|
|
10448
10688
|
this.hub.setAgent(workspaceId, {
|
|
@@ -10476,19 +10716,20 @@ var CompanionSupervisor = class {
|
|
|
10476
10716
|
if (missing.length > 0) {
|
|
10477
10717
|
this.log.warn(
|
|
10478
10718
|
{ workspaceId, agentId: it.agentId, missing },
|
|
10479
|
-
|
|
10719
|
+
`${it.agentDisplayName} needs secrets this machine does not have: ${missing.join(", ")}. Add them to ~/.cabane/secrets.json.`
|
|
10480
10720
|
);
|
|
10481
10721
|
}
|
|
10482
10722
|
}
|
|
10483
10723
|
this.ensureWorkspaceSse(wr);
|
|
10484
10724
|
}
|
|
10485
10725
|
addAgent(wr, it, credential, runConfig) {
|
|
10486
|
-
const
|
|
10726
|
+
const agentLog = this.log.child({ agentName: it.agentDisplayName });
|
|
10727
|
+
const outbox = new Outbox(it.agentId, agentLog);
|
|
10487
10728
|
const api = new CabaneApi({
|
|
10488
10729
|
baseUrl: this.config.baseUrl,
|
|
10489
10730
|
token: credential,
|
|
10490
10731
|
outbox,
|
|
10491
|
-
log:
|
|
10732
|
+
log: agentLog
|
|
10492
10733
|
});
|
|
10493
10734
|
const aborts = /* @__PURE__ */ new Map();
|
|
10494
10735
|
const dispatcher = this.buildDispatcher({
|
|
@@ -10498,6 +10739,7 @@ var CompanionSupervisor = class {
|
|
|
10498
10739
|
workspaceSlug: it.workspaceSlug,
|
|
10499
10740
|
agentId: it.agentId,
|
|
10500
10741
|
agentUsername: it.agentUsername,
|
|
10742
|
+
agentDisplayName: it.agentDisplayName,
|
|
10501
10743
|
credential,
|
|
10502
10744
|
runConfig,
|
|
10503
10745
|
aborts
|
|
@@ -10518,7 +10760,7 @@ var CompanionSupervisor = class {
|
|
|
10518
10760
|
drain2.kick();
|
|
10519
10761
|
this.log.info(
|
|
10520
10762
|
{ workspaceId: it.workspaceId, agentId: it.agentId, username: it.agentUsername },
|
|
10521
|
-
|
|
10763
|
+
`Running ${it.agentDisplayName}`
|
|
10522
10764
|
);
|
|
10523
10765
|
}
|
|
10524
10766
|
removeAgent(wr, agentId) {
|
|
@@ -10529,7 +10771,7 @@ var CompanionSupervisor = class {
|
|
|
10529
10771
|
this.hub.removeAgent(wr.workspaceId, agentId);
|
|
10530
10772
|
this.log.info(
|
|
10531
10773
|
{ workspaceId: wr.workspaceId, agentId },
|
|
10532
|
-
|
|
10774
|
+
`Stopped running ${runner.displayName} (unassigned)`
|
|
10533
10775
|
);
|
|
10534
10776
|
}
|
|
10535
10777
|
// CT1379: can this machine LAUNCH the harness — is the SDK's own bundled
|
|
@@ -10602,7 +10844,11 @@ var CompanionSupervisor = class {
|
|
|
10602
10844
|
const meta = { runtime, platform: resolution.platform, arch: resolution.arch, status: status2 };
|
|
10603
10845
|
if (status2 === "present") {
|
|
10604
10846
|
const say = intended ? this.log.info : this.log.debug;
|
|
10605
|
-
say.call(
|
|
10847
|
+
say.call(
|
|
10848
|
+
this.log,
|
|
10849
|
+
meta,
|
|
10850
|
+
`${runtime === "codex" ? "Codex" : "Claude Code"} is available again`
|
|
10851
|
+
);
|
|
10606
10852
|
continue;
|
|
10607
10853
|
}
|
|
10608
10854
|
if (intended) this.log.warn(meta, startupWarning(resolution));
|
|
@@ -10648,7 +10894,7 @@ var CompanionSupervisor = class {
|
|
|
10648
10894
|
local,
|
|
10649
10895
|
aborts: ctx.aborts,
|
|
10650
10896
|
runConfig: ctx.runConfig,
|
|
10651
|
-
log: this.log,
|
|
10897
|
+
log: this.log.child({ agentName: ctx.agentDisplayName ?? ctx.agentUsername }),
|
|
10652
10898
|
// CT833: register the claude-code adapter only when this device actually
|
|
10653
10899
|
// offers claude-code — read per turn (not captured here), so a harness
|
|
10654
10900
|
// installed or connected after boot works on the next turn exactly as it
|
|
@@ -10706,7 +10952,7 @@ var CompanionSupervisor = class {
|
|
|
10706
10952
|
onMessage: (ev) => this.handleWorkspaceMessage(wr, ev),
|
|
10707
10953
|
onAuthFailure: (status2) => {
|
|
10708
10954
|
this.hub.setAuthFailed(wr.workspaceId);
|
|
10709
|
-
this.log.
|
|
10955
|
+
this.log.debug(
|
|
10710
10956
|
{ workspaceId: wr.workspaceId, status: status2, sseAgentId: wr.sseAgentId },
|
|
10711
10957
|
"companion: workspace stream auth failed \u2014 re-pulling assignments"
|
|
10712
10958
|
);
|
|
@@ -10715,7 +10961,7 @@ var CompanionSupervisor = class {
|
|
|
10715
10961
|
}
|
|
10716
10962
|
});
|
|
10717
10963
|
wr.sub.start();
|
|
10718
|
-
this.log.
|
|
10964
|
+
this.log.debug({ workspaceId: wr.workspaceId }, "companion: subscribed");
|
|
10719
10965
|
}
|
|
10720
10966
|
async removeWorkspace(workspaceId) {
|
|
10721
10967
|
const wr = this.workspaces.get(workspaceId);
|
|
@@ -10739,7 +10985,7 @@ var CompanionSupervisor = class {
|
|
|
10739
10985
|
try {
|
|
10740
10986
|
wire = JSON.parse(ev.data);
|
|
10741
10987
|
} catch (err) {
|
|
10742
|
-
this.log.
|
|
10988
|
+
this.log.debug(
|
|
10743
10989
|
{ err: err instanceof Error ? err.message : String(err) },
|
|
10744
10990
|
"malformed SSE payload"
|
|
10745
10991
|
);
|
|
@@ -10755,7 +11001,7 @@ var CompanionSupervisor = class {
|
|
|
10755
11001
|
if (agent2) {
|
|
10756
11002
|
const aborted = agent2.dispatcher.cancel(payload2.conversationId, payload2.agentId);
|
|
10757
11003
|
if (aborted) {
|
|
10758
|
-
this.log.
|
|
11004
|
+
this.log.debug(
|
|
10759
11005
|
{
|
|
10760
11006
|
workspaceId: wr.workspaceId,
|
|
10761
11007
|
conversationId: payload2.conversationId,
|
|
@@ -10809,9 +11055,9 @@ var CompanionSupervisor = class {
|
|
|
10809
11055
|
workspaceId: wr.workspaceId,
|
|
10810
11056
|
conversationId: payload.conversationId,
|
|
10811
11057
|
agentId: payload.agentId,
|
|
10812
|
-
|
|
11058
|
+
...apiErrorLogFields(err)
|
|
10813
11059
|
},
|
|
10814
|
-
"
|
|
11060
|
+
"The turn stopped unexpectedly. Reply in Cabane to try again."
|
|
10815
11061
|
);
|
|
10816
11062
|
});
|
|
10817
11063
|
wr.chains.set(chainKey, tail);
|
|
@@ -10844,7 +11090,7 @@ var CompanionSupervisor = class {
|
|
|
10844
11090
|
messageId: payload.messageId,
|
|
10845
11091
|
reason: "agent_not_on_device"
|
|
10846
11092
|
});
|
|
10847
|
-
this.log.
|
|
11093
|
+
this.log.debug(
|
|
10848
11094
|
{
|
|
10849
11095
|
workspaceId: wr.workspaceId,
|
|
10850
11096
|
conversationId: payload.conversationId,
|
|
@@ -10865,7 +11111,7 @@ var CompanionSupervisor = class {
|
|
|
10865
11111
|
agentId: payload.agentId,
|
|
10866
11112
|
err: lastErr instanceof Error ? lastErr.message : String(lastErr)
|
|
10867
11113
|
},
|
|
10868
|
-
"
|
|
11114
|
+
"Couldn't tell Cabane that this agent no longer runs here; trying again when the connection returns."
|
|
10869
11115
|
);
|
|
10870
11116
|
return false;
|
|
10871
11117
|
}
|
|
@@ -10920,7 +11166,7 @@ var CompanionSupervisor = class {
|
|
|
10920
11166
|
agent,
|
|
10921
11167
|
run.turnId
|
|
10922
11168
|
).catch(
|
|
10923
|
-
(err) => this.log.
|
|
11169
|
+
(err) => this.log.debug({ err, turnId: run.turnId }, "companion: restart recovery failed")
|
|
10924
11170
|
).finally(() => {
|
|
10925
11171
|
if (wr.chains.get(key) === tail) wr.chains.delete(key);
|
|
10926
11172
|
});
|
|
@@ -10931,7 +11177,7 @@ var CompanionSupervisor = class {
|
|
|
10931
11177
|
this.recoveryChecked = page.nextCursor === null;
|
|
10932
11178
|
}
|
|
10933
11179
|
} catch (err) {
|
|
10934
|
-
this.log.
|
|
11180
|
+
this.log.debug({ err }, "companion: run recovery read failed (will retry)");
|
|
10935
11181
|
} finally {
|
|
10936
11182
|
this.recovering = false;
|
|
10937
11183
|
}
|
|
@@ -10943,7 +11189,7 @@ var CompanionSupervisor = class {
|
|
|
10943
11189
|
const workspaceId = wr.workspaceId;
|
|
10944
11190
|
if (ev.id && hasDispatched(workspaceId, ev.id)) {
|
|
10945
11191
|
if (hasCompleted(workspaceId, ev.id)) {
|
|
10946
|
-
this.log.
|
|
11192
|
+
this.log.debug(
|
|
10947
11193
|
{ workspaceId, eventId: ev.id },
|
|
10948
11194
|
"companion: skipping already-completed event (resume after restart)"
|
|
10949
11195
|
);
|
|
@@ -10951,7 +11197,7 @@ var CompanionSupervisor = class {
|
|
|
10951
11197
|
return;
|
|
10952
11198
|
}
|
|
10953
11199
|
if (noResume()) {
|
|
10954
|
-
this.log.
|
|
11200
|
+
this.log.debug(
|
|
10955
11201
|
{ workspaceId, eventId: ev.id },
|
|
10956
11202
|
"companion: skipping interrupted turn (CABANE_COMPANION_NO_RESUME=1) \u2014 resume disabled for this boot"
|
|
10957
11203
|
);
|
|
@@ -10963,13 +11209,13 @@ var CompanionSupervisor = class {
|
|
|
10963
11209
|
if (attempt > MAX_RESUME_ATTEMPTS) {
|
|
10964
11210
|
this.log.error(
|
|
10965
11211
|
{ workspaceId, eventId: ev.id, attempt, cap: MAX_RESUME_ATTEMPTS },
|
|
10966
|
-
"
|
|
11212
|
+
"An interrupted turn couldn't finish after several attempts. Reply in Cabane to try again."
|
|
10967
11213
|
);
|
|
10968
11214
|
markCompleted(workspaceId, ev.id);
|
|
10969
11215
|
wr.cursor.settle(ev.id);
|
|
10970
11216
|
return;
|
|
10971
11217
|
}
|
|
10972
|
-
this.log.
|
|
11218
|
+
this.log.debug(
|
|
10973
11219
|
{ workspaceId, eventId: ev.id, attempt, cap: MAX_RESUME_ATTEMPTS },
|
|
10974
11220
|
"companion: re-dispatching interrupted turn (resume after restart)"
|
|
10975
11221
|
);
|
|
@@ -10979,7 +11225,7 @@ var CompanionSupervisor = class {
|
|
|
10979
11225
|
if (ev.id) {
|
|
10980
11226
|
const liveTurnIds = rememberTurnId(workspaceId, ev.id, turnId);
|
|
10981
11227
|
if (liveTurnIds > TURN_ID_OVERFLOW_WARN) {
|
|
10982
|
-
this.log.
|
|
11228
|
+
this.log.debug(
|
|
10983
11229
|
{ workspaceId, liveTurnIds, threshold: TURN_ID_OVERFLOW_WARN },
|
|
10984
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."
|
|
10985
11231
|
);
|
|
@@ -10987,11 +11233,18 @@ var CompanionSupervisor = class {
|
|
|
10987
11233
|
markDispatched(workspaceId, ev.id);
|
|
10988
11234
|
}
|
|
10989
11235
|
if (resumedTurnId) {
|
|
10990
|
-
this.log.
|
|
11236
|
+
this.log.debug(
|
|
10991
11237
|
{ workspaceId, eventId: ev.id, turnId },
|
|
10992
11238
|
"companion: resuming an interrupted turn under its original id"
|
|
10993
11239
|
);
|
|
10994
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");
|
|
10995
11248
|
const result = await agent.dispatcher.handle(payload, {
|
|
10996
11249
|
turnId,
|
|
10997
11250
|
resumed: resumedTurnId !== null
|
|
@@ -11000,16 +11253,23 @@ var CompanionSupervisor = class {
|
|
|
11000
11253
|
if (ev.id) wr.cursor.settle(ev.id);
|
|
11001
11254
|
const durationS = (result.durationMs / 1e3).toFixed(1);
|
|
11002
11255
|
const bindings = {
|
|
11003
|
-
|
|
11004
|
-
|
|
11005
|
-
agentId: agent.agentId
|
|
11256
|
+
...turnBindings,
|
|
11257
|
+
...result.transcriptPath ? { transcriptPath: result.transcriptPath } : {}
|
|
11006
11258
|
};
|
|
11007
11259
|
if (result.ok) {
|
|
11008
|
-
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");
|
|
11009
11269
|
} else {
|
|
11010
|
-
this.log.
|
|
11270
|
+
this.log.info(
|
|
11011
11271
|
bindings,
|
|
11012
|
-
|
|
11272
|
+
`Turn failed after ${durationS}s: ${turnFailureCopy(result.reason, result.runtime)}`
|
|
11013
11273
|
);
|
|
11014
11274
|
}
|
|
11015
11275
|
}
|
|
@@ -11029,7 +11289,7 @@ var CompanionSupervisor = class {
|
|
|
11029
11289
|
}
|
|
11030
11290
|
} catch (err) {
|
|
11031
11291
|
drainDelay = Math.min(drainDelay * 2, DRAIN_MAX_MS);
|
|
11032
|
-
this.log.
|
|
11292
|
+
this.log.debug(
|
|
11033
11293
|
{ agentId, err: err instanceof Error ? err.message : String(err) },
|
|
11034
11294
|
"companion: outbox drain pass threw (will retry with backoff)"
|
|
11035
11295
|
);
|
|
@@ -11064,7 +11324,7 @@ var CompanionSupervisor = class {
|
|
|
11064
11324
|
const next = loadConfig();
|
|
11065
11325
|
if (!next) return;
|
|
11066
11326
|
this.config = next;
|
|
11067
|
-
this.log
|
|
11327
|
+
configureLogger(this.log, next);
|
|
11068
11328
|
this.rebuildDispatchers();
|
|
11069
11329
|
void this.refreshAssignments();
|
|
11070
11330
|
}
|
|
@@ -11080,7 +11340,7 @@ var CompanionSupervisor = class {
|
|
|
11080
11340
|
};
|
|
11081
11341
|
this.config = next;
|
|
11082
11342
|
saveConfig(next);
|
|
11083
|
-
this.log
|
|
11343
|
+
configureLogger(this.log, next);
|
|
11084
11344
|
}
|
|
11085
11345
|
// ---- harnesses (CT586) ----
|
|
11086
11346
|
// Probe all three harnesses, cache the signals + versions, and push the derived
|
|
@@ -11106,7 +11366,7 @@ var CompanionSupervisor = class {
|
|
|
11106
11366
|
};
|
|
11107
11367
|
this.hub.setHarnesses(deriveHarnessSnapshot(signals).harnesses);
|
|
11108
11368
|
} catch (err) {
|
|
11109
|
-
this.log.
|
|
11369
|
+
this.log.debug(
|
|
11110
11370
|
{ err: err instanceof Error ? err.message : String(err) },
|
|
11111
11371
|
"companion: harness probe failed (will retry on next beat)"
|
|
11112
11372
|
);
|
|
@@ -11222,6 +11482,7 @@ var CompanionSupervisor = class {
|
|
|
11222
11482
|
workspaceSlug: wr.slug,
|
|
11223
11483
|
agentId: runner.agentId,
|
|
11224
11484
|
agentUsername: runner.username,
|
|
11485
|
+
agentDisplayName: runner.displayName,
|
|
11225
11486
|
credential: runner.credential,
|
|
11226
11487
|
runConfig: runner.runConfig,
|
|
11227
11488
|
aborts: runner.aborts
|
|
@@ -11327,7 +11588,7 @@ function handleUncaught(log, err, origin) {
|
|
|
11327
11588
|
const message = err instanceof Error ? err.message : String(err);
|
|
11328
11589
|
const code = errorCode(err);
|
|
11329
11590
|
if (isRecoverableSocketError(err)) {
|
|
11330
|
-
log.
|
|
11591
|
+
log.debug(
|
|
11331
11592
|
{ origin, code, err: message },
|
|
11332
11593
|
"companion: recovered from a socket error (kept running \u2014 a broken pipe never kills the companion)"
|
|
11333
11594
|
);
|
|
@@ -11335,7 +11596,7 @@ function handleUncaught(log, err, origin) {
|
|
|
11335
11596
|
}
|
|
11336
11597
|
log.error(
|
|
11337
11598
|
{ origin, code, err: message, stack: err instanceof Error ? err.stack : void 0 },
|
|
11338
|
-
"
|
|
11599
|
+
"An unexpected error occurred; the companion is still running."
|
|
11339
11600
|
);
|
|
11340
11601
|
}
|
|
11341
11602
|
|
|
@@ -11369,7 +11630,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
11369
11630
|
try {
|
|
11370
11631
|
({ cfg, claudeOnPath: claudeCode } = await requireStartConfig({
|
|
11371
11632
|
...opts.probeClaude ? { probeClaude: opts.probeClaude } : {},
|
|
11372
|
-
onMigrated: (migrated, onPath) => log.
|
|
11633
|
+
onMigrated: (migrated, onPath) => log.debug(
|
|
11373
11634
|
{ claudeCode: isClaudeCodeConnected(migrated) },
|
|
11374
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)"
|
|
11375
11636
|
)
|
|
@@ -11390,7 +11651,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
11390
11651
|
});
|
|
11391
11652
|
throw err;
|
|
11392
11653
|
}
|
|
11393
|
-
|
|
11654
|
+
configureLogger(log, cfg);
|
|
11394
11655
|
const harnessVersions = await probeHarnessVersions({
|
|
11395
11656
|
// Connected AND its CLI answered — the two things that make a
|
|
11396
11657
|
// `claude --version` probe worth spawning. Not the offer predicate.
|
|
@@ -11590,7 +11851,12 @@ Connect a harness to the running companion: cabane-companion connect claude-code
|
|
|
11590
11851
|
}
|
|
11591
11852
|
clearRuntimeState();
|
|
11592
11853
|
}
|
|
11593
|
-
const args = [
|
|
11854
|
+
const args = [
|
|
11855
|
+
"start",
|
|
11856
|
+
"--foreground",
|
|
11857
|
+
...opts.logLevel ? ["--log-level", opts.logLevel] : [],
|
|
11858
|
+
...opts.logFormat ? ["--log-format", opts.logFormat] : []
|
|
11859
|
+
];
|
|
11594
11860
|
const launchDetached = () => {
|
|
11595
11861
|
const spawned = spawnDetached(args);
|
|
11596
11862
|
spawned.unref();
|
|
@@ -11620,7 +11886,7 @@ Check ${companionLogPath()} for errors, or run \`cabane-companion status\`.
|
|
|
11620
11886
|
if (quiet) return { started: true, state };
|
|
11621
11887
|
process.stdout.write(
|
|
11622
11888
|
`Cabane Companion started in the background (pid ${state.pid}).
|
|
11623
|
-
Logs: ${companionLogPath()}
|
|
11889
|
+
Logs: cabane-companion logs (${companionLogPath()})
|
|
11624
11890
|
Status: cabane-companion status
|
|
11625
11891
|
Stop: cabane-companion stop
|
|
11626
11892
|
`
|
|
@@ -11646,11 +11912,11 @@ function defaultSpawnDetached(args) {
|
|
|
11646
11912
|
var FORCE_EXIT_MS = 4e3;
|
|
11647
11913
|
var MAX_CODES = 3;
|
|
11648
11914
|
async function start(opts = {}) {
|
|
11915
|
+
setLogOverrides(opts);
|
|
11649
11916
|
const interactive = isInteractive();
|
|
11650
|
-
if (!interactive) return startScript(opts, false);
|
|
11651
11917
|
setConsoleLogging(false);
|
|
11652
11918
|
try {
|
|
11653
|
-
await startScript(opts,
|
|
11919
|
+
await startScript(opts, interactive);
|
|
11654
11920
|
} finally {
|
|
11655
11921
|
setConsoleLogging(true);
|
|
11656
11922
|
}
|
|
@@ -11661,7 +11927,10 @@ async function startScript(opts, interactive) {
|
|
|
11661
11927
|
reportAlreadyRunning(running.pid);
|
|
11662
11928
|
return;
|
|
11663
11929
|
}
|
|
11664
|
-
|
|
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();
|
|
11665
11934
|
const held = !isDevicePaired() ? await collectHarnessChoices(interactive) : [];
|
|
11666
11935
|
let paired = null;
|
|
11667
11936
|
if (!isDevicePaired()) {
|
|
@@ -11670,14 +11939,19 @@ async function startScript(opts, interactive) {
|
|
|
11670
11939
|
return;
|
|
11671
11940
|
}
|
|
11672
11941
|
const { note } = writePairedConfig(paired);
|
|
11673
|
-
|
|
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
|
+
);
|
|
11674
11948
|
}
|
|
11675
11949
|
const justPaired = paired !== null;
|
|
11676
11950
|
const log = getLogger();
|
|
11677
11951
|
const result = await createCompanionRuntime({
|
|
11678
11952
|
// The script says what's connectable in its own words below; a stderr warning
|
|
11679
11953
|
// in the middle of it would be the same news, worse.
|
|
11680
|
-
onReadinessWarning: (message) => log.
|
|
11954
|
+
onReadinessWarning: (message) => log.warn(message)
|
|
11681
11955
|
});
|
|
11682
11956
|
if (!result.ok) {
|
|
11683
11957
|
reportAlreadyRunning(result.existing?.pid);
|
|
@@ -11695,24 +11969,28 @@ async function startScript(opts, interactive) {
|
|
|
11695
11969
|
if (interactive && !opts.foreground) {
|
|
11696
11970
|
await handOffToBackground(runtime, {
|
|
11697
11971
|
justPaired,
|
|
11698
|
-
connected
|
|
11972
|
+
connected,
|
|
11973
|
+
...opts
|
|
11699
11974
|
});
|
|
11700
11975
|
return;
|
|
11701
11976
|
}
|
|
11702
11977
|
if (interactive) {
|
|
11703
11978
|
blank();
|
|
11704
11979
|
write(INDENT + tick("Cabane companion is running in this terminal."));
|
|
11705
|
-
write(
|
|
11980
|
+
write(
|
|
11981
|
+
` Stop: Ctrl-C Logs: cabane-companion logs (${tildePath(companionLogPath())})`
|
|
11982
|
+
);
|
|
11706
11983
|
blank();
|
|
11707
11984
|
write(`${INDENT}Listening for messages\u2026`);
|
|
11708
11985
|
setConsoleLogging(true);
|
|
11709
|
-
} else {
|
|
11986
|
+
} else if (!daemon) {
|
|
11710
11987
|
blank();
|
|
11711
11988
|
write(
|
|
11712
11989
|
`${INDENT}Cabane companion is running. No terminal attached, so it stays in the foreground.`
|
|
11713
11990
|
);
|
|
11714
11991
|
write(`${INDENT}Listening for messages\u2026`);
|
|
11715
11992
|
}
|
|
11993
|
+
setConsoleLogging(true);
|
|
11716
11994
|
await runAttached(runtime);
|
|
11717
11995
|
}
|
|
11718
11996
|
async function liveCompanion() {
|
|
@@ -11788,7 +12066,7 @@ function prePairFoundPhrase(harness) {
|
|
|
11788
12066
|
}
|
|
11789
12067
|
async function pairHere(opts, interactive) {
|
|
11790
12068
|
const baseUrl = resolvePairBaseUrl(opts.server);
|
|
11791
|
-
const label = deviceLabelFromHostname(
|
|
12069
|
+
const label = deviceLabelFromHostname(hostname3());
|
|
11792
12070
|
const aborter = new AbortController();
|
|
11793
12071
|
const onSigint = () => aborter.abort();
|
|
11794
12072
|
process.on("SIGINT", onSigint);
|
|
@@ -11858,7 +12136,7 @@ function connectCommand(runtime) {
|
|
|
11858
12136
|
}
|
|
11859
12137
|
async function handOffToBackground(runtime, ctx) {
|
|
11860
12138
|
await runtime.stop();
|
|
11861
|
-
const outcome = await startDaemon({ report: "failures" });
|
|
12139
|
+
const outcome = await startDaemon({ report: "failures", ...ctx });
|
|
11862
12140
|
if (!outcome.started) {
|
|
11863
12141
|
return;
|
|
11864
12142
|
}
|
|
@@ -11871,7 +12149,9 @@ async function handOffToBackground(runtime, ctx) {
|
|
|
11871
12149
|
}
|
|
11872
12150
|
}
|
|
11873
12151
|
write(INDENT + tick("Cabane companion is running in the background."));
|
|
11874
|
-
write(
|
|
12152
|
+
write(
|
|
12153
|
+
` Stop: cabane-companion stop Logs: cabane-companion logs (${tildePath(companionLogPath())})`
|
|
12154
|
+
);
|
|
11875
12155
|
if (ctx.justPaired) {
|
|
11876
12156
|
blank();
|
|
11877
12157
|
write(`${INDENT}! It won't restart on its own \u2014 after a reboot, or if it ever stops,`);
|
|
@@ -11883,20 +12163,15 @@ async function handOffToBackground(runtime, ctx) {
|
|
|
11883
12163
|
async function runAttached(runtime) {
|
|
11884
12164
|
await new Promise((resolve2) => {
|
|
11885
12165
|
let shuttingDown = false;
|
|
11886
|
-
const shutdown = async (
|
|
12166
|
+
const shutdown = async () => {
|
|
11887
12167
|
if (shuttingDown) {
|
|
11888
|
-
|
|
11889
|
-
companion: second ${signal}, force-quitting.
|
|
11890
|
-
`);
|
|
12168
|
+
getLogger().error("Stopped: forced to quit after a second stop request.");
|
|
11891
12169
|
process.exit(1);
|
|
11892
12170
|
}
|
|
11893
12171
|
shuttingDown = true;
|
|
11894
|
-
|
|
11895
|
-
companion: received ${signal}, shutting down\u2026
|
|
11896
|
-
`);
|
|
12172
|
+
getLogger().info("Stopping the companion");
|
|
11897
12173
|
const forceExit = setTimeout(() => {
|
|
11898
|
-
|
|
11899
|
-
`);
|
|
12174
|
+
getLogger().error("Stopped: shutdown took too long.");
|
|
11900
12175
|
process.exit(1);
|
|
11901
12176
|
}, FORCE_EXIT_MS);
|
|
11902
12177
|
forceExit.unref?.();
|
|
@@ -11905,8 +12180,8 @@ companion: received ${signal}, shutting down\u2026
|
|
|
11905
12180
|
resolve2();
|
|
11906
12181
|
process.exit(0);
|
|
11907
12182
|
};
|
|
11908
|
-
process.on("SIGINT", () => void shutdown(
|
|
11909
|
-
process.on("SIGTERM", () => void shutdown(
|
|
12183
|
+
process.on("SIGINT", () => void shutdown());
|
|
12184
|
+
process.on("SIGTERM", () => void shutdown());
|
|
11910
12185
|
});
|
|
11911
12186
|
}
|
|
11912
12187
|
|
|
@@ -11925,7 +12200,7 @@ async function status() {
|
|
|
11925
12200
|
`);
|
|
11926
12201
|
if (cfg.deviceLabel) process.stdout.write(`device: ${cfg.deviceLabel}
|
|
11927
12202
|
`);
|
|
11928
|
-
process.stdout.write(`log file: ${companionLogPath()}
|
|
12203
|
+
process.stdout.write(`log file: cabane-companion logs (${companionLogPath()})
|
|
11929
12204
|
`);
|
|
11930
12205
|
process.stdout.write(`transcripts: ${transcriptsLine()}
|
|
11931
12206
|
`);
|
|
@@ -11960,9 +12235,9 @@ async function status() {
|
|
|
11960
12235
|
process.stdout.write(`prepare hook: ${cfg.prepareHook.command} (device-level, all agents)
|
|
11961
12236
|
`);
|
|
11962
12237
|
}
|
|
11963
|
-
const
|
|
11964
|
-
if (
|
|
11965
|
-
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(", ")}
|
|
11966
12241
|
`);
|
|
11967
12242
|
}
|
|
11968
12243
|
}
|
|
@@ -12408,8 +12683,21 @@ program.command("write-paired-config", { hidden: true }).description("persist an
|
|
|
12408
12683
|
});
|
|
12409
12684
|
program.command("start").description(
|
|
12410
12685
|
"pair this device if needed, connect a coding agent on it, and run in the background."
|
|
12411
|
-
).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) => {
|
|
12412
12698
|
await start({
|
|
12699
|
+
...opts.logLevel ? { logLevel: opts.logLevel } : {},
|
|
12700
|
+
...opts.logFormat ? { logFormat: opts.logFormat } : {},
|
|
12413
12701
|
...opts.foreground ? { foreground: true } : {},
|
|
12414
12702
|
...opts.server !== void 0 ? { server: opts.server } : {}
|
|
12415
12703
|
});
|
|
@@ -12423,6 +12711,13 @@ program.command("stop").description("stop a running companion (SIGTERM, then for
|
|
|
12423
12711
|
program.command("status").description("print the companion's local state (pairing, secrets, log path).").action(async () => {
|
|
12424
12712
|
await status();
|
|
12425
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
|
+
});
|
|
12426
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) => {
|
|
12427
12722
|
await transcript({
|
|
12428
12723
|
...file !== void 0 ? { target: file } : {},
|
|
@@ -12455,7 +12750,7 @@ program.parseAsync(process.argv).catch((err) => {
|
|
|
12455
12750
|
}
|
|
12456
12751
|
setConsoleLogging(false);
|
|
12457
12752
|
try {
|
|
12458
|
-
getLogger().error({ err },
|
|
12753
|
+
getLogger().error({ err }, `Stopped: ${err instanceof Error ? err.message : String(err)}`);
|
|
12459
12754
|
} catch {
|
|
12460
12755
|
}
|
|
12461
12756
|
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|