@cabane/companion 0.6.101 → 0.6.103
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -3
- package/dist/cli.js +739 -452
- package/dist/pairing-config.js +4 -1
- package/dist/runtime.js +597 -418
- package/package.json +1 -2
package/dist/runtime.js
CHANGED
|
@@ -299,6 +299,7 @@ var companionConfigSchema = z2.object({
|
|
|
299
299
|
dashboardPort: z2.number().int().min(1).max(65535).optional(),
|
|
300
300
|
autoOpen: z2.boolean().optional(),
|
|
301
301
|
logLevel: z2.enum(["warn", "info", "debug"]).optional(),
|
|
302
|
+
logFormat: z2.enum(["human", "json"]).optional(),
|
|
302
303
|
// CT270: the opencode runtime, when the operator runs one on this machine. The
|
|
303
304
|
// operator installs opencode, starts `opencode serve` (auth via opencode's own
|
|
304
305
|
// `/connect` — Cabane never sees provider keys), and points the companion at it
|
|
@@ -728,7 +729,7 @@ function repairAction() {
|
|
|
728
729
|
}
|
|
729
730
|
function startupWarning(r) {
|
|
730
731
|
const label = HARNESS_LABEL[r.runtime];
|
|
731
|
-
return
|
|
732
|
+
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.`;
|
|
732
733
|
}
|
|
733
734
|
function harnessIssueNote(runtime, status) {
|
|
734
735
|
const intent = HARNESS_INTENT_WORD[runtime];
|
|
@@ -1078,61 +1079,113 @@ function runBounded(command, args) {
|
|
|
1078
1079
|
// src/logger.ts
|
|
1079
1080
|
import { createWriteStream, mkdirSync as mkdirSync2 } from "fs";
|
|
1080
1081
|
import { dirname as dirname3, join as join3 } from "path";
|
|
1082
|
+
import { stripVTControlCharacters } from "util";
|
|
1081
1083
|
import pino from "pino";
|
|
1082
|
-
import pretty from "pino-pretty";
|
|
1083
1084
|
function companionLogPath() {
|
|
1084
1085
|
return join3(cabaneDir(), "companion.log");
|
|
1085
1086
|
}
|
|
1086
|
-
|
|
1087
|
-
"
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
"
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
const
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
const
|
|
1102
|
-
|
|
1103
|
-
|
|
1087
|
+
function humanText(value) {
|
|
1088
|
+
return stripVTControlCharacters(String(value ?? "")).replace(
|
|
1089
|
+
/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi,
|
|
1090
|
+
(id) => id.slice(0, 8)
|
|
1091
|
+
).replace(/\s+/g, " ").trim();
|
|
1092
|
+
}
|
|
1093
|
+
function formatHumanLine(record) {
|
|
1094
|
+
const date = new Date(
|
|
1095
|
+
typeof record.time === "number" || typeof record.time === "string" ? record.time : 0
|
|
1096
|
+
);
|
|
1097
|
+
const time = [date.getHours(), date.getMinutes(), date.getSeconds()].map((n) => String(n).padStart(2, "0")).join(":");
|
|
1098
|
+
const level = typeof record.level === "number" ? record.level : 30;
|
|
1099
|
+
const word = level >= 50 ? "error" : level >= 40 ? "warn" : level < 30 ? "debug" : "";
|
|
1100
|
+
const conversation = typeof record.conversationId === "string" ? record.conversationId.slice(0, 8) : "";
|
|
1101
|
+
const agent = humanText(record.agentName);
|
|
1102
|
+
const context = [agent, conversation].filter(Boolean).join(" \xB7 ");
|
|
1103
|
+
let line = `${time} ${word ? `${word} ` : ""}${context ? `${context} ` : ""}${humanText(record.msg)}`;
|
|
1104
|
+
const err = record.err;
|
|
1105
|
+
const errorRecord = err !== null && typeof err === "object" ? err : null;
|
|
1106
|
+
const message = humanText(errorRecord && "message" in errorRecord ? errorRecord.message : err);
|
|
1107
|
+
const detail = typeof record.status === "number" ? `HTTP ${record.status}: ${humanText(record.responseBody) || message}` : message;
|
|
1108
|
+
if (detail && !line.includes(detail)) line += ` (${detail})`;
|
|
1109
|
+
if (level < 30) {
|
|
1110
|
+
const hidden = /* @__PURE__ */ new Set([
|
|
1111
|
+
"time",
|
|
1112
|
+
"level",
|
|
1113
|
+
"pid",
|
|
1114
|
+
"hostname",
|
|
1115
|
+
"msg",
|
|
1116
|
+
"err",
|
|
1117
|
+
"stack",
|
|
1118
|
+
"agentName",
|
|
1119
|
+
"conversationId",
|
|
1120
|
+
"transcriptPath"
|
|
1121
|
+
]);
|
|
1122
|
+
const details = Object.fromEntries(Object.entries(record).filter(([key]) => !hidden.has(key)));
|
|
1123
|
+
if (Object.keys(details).length)
|
|
1124
|
+
line += ` ${humanText(JSON.stringify(details)).slice(0, 2e3)}`;
|
|
1125
|
+
}
|
|
1126
|
+
if (record.transcriptPath)
|
|
1127
|
+
line += `
|
|
1128
|
+
Transcript: ${humanText(record.transcriptPath)}`;
|
|
1129
|
+
const stack = errorRecord && "stack" in errorRecord ? errorRecord.stack : record.stack;
|
|
1130
|
+
if ((level >= 50 || level < 30) && typeof stack === "string") {
|
|
1131
|
+
line += "\n" + stack.split("\n").map((part) => ` ${humanText(part)}`).join("\n");
|
|
1132
|
+
}
|
|
1133
|
+
return line + "\n";
|
|
1104
1134
|
}
|
|
1105
1135
|
var cached = null;
|
|
1106
1136
|
var consoleLogging = true;
|
|
1107
|
-
|
|
1137
|
+
var overrides = {};
|
|
1138
|
+
var formats = /* @__PURE__ */ new WeakMap();
|
|
1139
|
+
function configureLogger(log, config) {
|
|
1140
|
+
log.level = overrides.logLevel ?? config.logLevel ?? "info";
|
|
1141
|
+
const state = formats.get(log);
|
|
1142
|
+
if (state) state.format = overrides.logFormat ?? config.logFormat ?? "human";
|
|
1143
|
+
}
|
|
1144
|
+
function createLogger(destinations = {}, options = {}) {
|
|
1108
1145
|
const path = companionLogPath();
|
|
1109
1146
|
if (!destinations.file) mkdirSync2(dirname3(path), { recursive: true });
|
|
1147
|
+
const file = destinations.file ?? createWriteStream(path, { flags: "a" });
|
|
1148
|
+
const state = { format: options.logFormat ?? "human" };
|
|
1110
1149
|
const streams = [];
|
|
1150
|
+
const human = (chunk) => {
|
|
1151
|
+
const record = JSON.parse(chunk);
|
|
1152
|
+
let text = formatHumanLine(record);
|
|
1153
|
+
const err = record.err;
|
|
1154
|
+
if (log.isLevelEnabled("debug") && record.level !== 20 && typeof record.level === "number" && record.level < 50 && err && typeof err === "object" && "stack" in err) {
|
|
1155
|
+
text += formatHumanLine({ ...record, level: 20, msg: "Error details" });
|
|
1156
|
+
}
|
|
1157
|
+
return text;
|
|
1158
|
+
};
|
|
1111
1159
|
if (process.env.CABANE_COMPANION_DAEMON !== "1") {
|
|
1112
|
-
const consoleStream = pretty({
|
|
1113
|
-
colorize: true,
|
|
1114
|
-
ignore: CONSOLE_IGNORE,
|
|
1115
|
-
messageFormat: (log, messageKey) => consoleMessageFormat(log, messageKey),
|
|
1116
|
-
...destinations.console ? { destination: destinations.console } : {}
|
|
1117
|
-
});
|
|
1118
1160
|
streams.push({
|
|
1119
|
-
level: "
|
|
1161
|
+
level: "debug",
|
|
1120
1162
|
stream: {
|
|
1121
1163
|
write(chunk) {
|
|
1122
|
-
if (consoleLogging)
|
|
1164
|
+
if (consoleLogging) (destinations.console ?? process.stdout).write(human(chunk));
|
|
1123
1165
|
}
|
|
1124
1166
|
}
|
|
1125
1167
|
});
|
|
1126
1168
|
}
|
|
1127
1169
|
streams.push({
|
|
1128
1170
|
level: "debug",
|
|
1129
|
-
stream:
|
|
1171
|
+
stream: {
|
|
1172
|
+
write(chunk) {
|
|
1173
|
+
file.write(state.format === "json" ? chunk : human(chunk));
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1130
1176
|
});
|
|
1131
|
-
|
|
1177
|
+
const log = pino({ level: options.logLevel ?? "info" }, pino.multistream(streams));
|
|
1178
|
+
formats.set(log, state);
|
|
1179
|
+
return log;
|
|
1132
1180
|
}
|
|
1133
1181
|
function getLogger() {
|
|
1134
1182
|
if (cached) return cached;
|
|
1135
|
-
|
|
1183
|
+
let config = {};
|
|
1184
|
+
try {
|
|
1185
|
+
config = loadConfig() ?? {};
|
|
1186
|
+
} catch {
|
|
1187
|
+
}
|
|
1188
|
+
cached = createLogger({}, { ...config, ...overrides });
|
|
1136
1189
|
return cached;
|
|
1137
1190
|
}
|
|
1138
1191
|
|
|
@@ -1942,6 +1995,246 @@ async function verifyRuntime(state, requestImpl = controlRequest) {
|
|
|
1942
1995
|
return body.instance_id === state.instanceId ? "ours" : "stale";
|
|
1943
1996
|
}
|
|
1944
1997
|
|
|
1998
|
+
// src/supervisor.ts
|
|
1999
|
+
import { hostname } from "os";
|
|
2000
|
+
|
|
2001
|
+
// packages/agent-runtime/src/failure.ts
|
|
2002
|
+
import { z as z3 } from "zod";
|
|
2003
|
+
var turnFailureSchema = z3.discriminatedUnion("kind", [
|
|
2004
|
+
z3.object({ kind: z3.literal("usage_capped"), resetsAt: z3.string().optional() }),
|
|
2005
|
+
z3.object({ kind: z3.literal("rate_limited") }),
|
|
2006
|
+
z3.object({ kind: z3.literal("server_error") }),
|
|
2007
|
+
z3.object({ kind: z3.literal("auth_expired") })
|
|
2008
|
+
]);
|
|
2009
|
+
var USAGE_CAPPED = "usage_capped";
|
|
2010
|
+
var RATE_LIMITED = "rate_limited";
|
|
2011
|
+
var SERVER_ERROR = "server_error";
|
|
2012
|
+
var AUTH_EXPIRED = "auth_expired";
|
|
2013
|
+
function encodeFailureReason(failure) {
|
|
2014
|
+
switch (failure.kind) {
|
|
2015
|
+
case "auth_expired":
|
|
2016
|
+
return AUTH_EXPIRED;
|
|
2017
|
+
case "server_error":
|
|
2018
|
+
return SERVER_ERROR;
|
|
2019
|
+
case "rate_limited":
|
|
2020
|
+
return RATE_LIMITED;
|
|
2021
|
+
case "usage_capped":
|
|
2022
|
+
return failure.resetsAt ? `${USAGE_CAPPED}:${failure.resetsAt}` : USAGE_CAPPED;
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
function decodeFailureReason(reason) {
|
|
2026
|
+
if (!reason) return null;
|
|
2027
|
+
if (reason === AUTH_EXPIRED) return { kind: "auth_expired" };
|
|
2028
|
+
if (reason === SERVER_ERROR) return { kind: "server_error" };
|
|
2029
|
+
if (reason === RATE_LIMITED) return { kind: "rate_limited" };
|
|
2030
|
+
if (reason === USAGE_CAPPED) return { kind: "usage_capped" };
|
|
2031
|
+
if (reason.startsWith(`${USAGE_CAPPED}:`)) {
|
|
2032
|
+
const iso = reason.slice(USAGE_CAPPED.length + 1);
|
|
2033
|
+
return isValidIso(iso) ? { kind: "usage_capped", resetsAt: iso } : { kind: "usage_capped" };
|
|
2034
|
+
}
|
|
2035
|
+
return null;
|
|
2036
|
+
}
|
|
2037
|
+
function isValidIso(value) {
|
|
2038
|
+
if (!value) return false;
|
|
2039
|
+
const ms = Date.parse(value);
|
|
2040
|
+
return Number.isFinite(ms);
|
|
2041
|
+
}
|
|
2042
|
+
function quotaForFailure(failure) {
|
|
2043
|
+
if (failure.kind === "auth_expired") return { quotaState: "auth_expired", limitedUntil: null };
|
|
2044
|
+
if (failure.kind === "usage_capped")
|
|
2045
|
+
return { quotaState: "limited", limitedUntil: failure.resetsAt ?? null };
|
|
2046
|
+
return null;
|
|
2047
|
+
}
|
|
2048
|
+
function resetsAtToIso(resetsAt) {
|
|
2049
|
+
if (typeof resetsAt !== "number" || !Number.isFinite(resetsAt) || resetsAt <= 0) return void 0;
|
|
2050
|
+
const ms = resetsAt < 1e12 ? resetsAt * 1e3 : resetsAt;
|
|
2051
|
+
const date = new Date(ms);
|
|
2052
|
+
const time = date.getTime();
|
|
2053
|
+
if (Number.isNaN(time)) return void 0;
|
|
2054
|
+
const YEAR_2000 = 9466848e5;
|
|
2055
|
+
const now = Date.now();
|
|
2056
|
+
if (time < YEAR_2000 || time > now + 366 * 24 * 60 * 60 * 1e3) return void 0;
|
|
2057
|
+
return date.toISOString();
|
|
2058
|
+
}
|
|
2059
|
+
function classifyAssistantError(error) {
|
|
2060
|
+
switch (error) {
|
|
2061
|
+
case "rate_limit":
|
|
2062
|
+
return { kind: "rate_limited" };
|
|
2063
|
+
case "overloaded":
|
|
2064
|
+
case "server_error":
|
|
2065
|
+
return { kind: "server_error" };
|
|
2066
|
+
case "authentication_failed":
|
|
2067
|
+
case "oauth_org_not_allowed":
|
|
2068
|
+
return { kind: "auth_expired" };
|
|
2069
|
+
default:
|
|
2070
|
+
return null;
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
function classifyErrorText(text) {
|
|
2074
|
+
if (!text) return null;
|
|
2075
|
+
const t = text.toLowerCase();
|
|
2076
|
+
if (AUTH_PATTERNS.some((re) => re.test(t))) return { kind: "auth_expired" };
|
|
2077
|
+
if (SERVER_PATTERNS.some((re) => re.test(t))) return { kind: "server_error" };
|
|
2078
|
+
const withoutNegatedCap = t.replace(NEGATED_CAP, "");
|
|
2079
|
+
const capNoun = CAP_PATTERNS.some((re) => re.test(withoutNegatedCap));
|
|
2080
|
+
const rateToken = RATE_PATTERNS.some((re) => re.test(t));
|
|
2081
|
+
if (capNoun) return { kind: "usage_capped" };
|
|
2082
|
+
if (rateToken) return { kind: "rate_limited" };
|
|
2083
|
+
if (BARE_LIMIT.test(t)) return { kind: "usage_capped" };
|
|
2084
|
+
return null;
|
|
2085
|
+
}
|
|
2086
|
+
function classifyRuntimeNoticeText(text) {
|
|
2087
|
+
if (!text) return null;
|
|
2088
|
+
const normalized = text.trim().replace(/\s+/g, " ");
|
|
2089
|
+
if (!normalized) return null;
|
|
2090
|
+
if (/^you(?:'|’)ve hit your (?:session|weekly|usage) limit(?:\s*[·—-]\s*resets?\s+.+)?$/i.test(
|
|
2091
|
+
normalized
|
|
2092
|
+
)) {
|
|
2093
|
+
return { kind: "usage_capped" };
|
|
2094
|
+
}
|
|
2095
|
+
return null;
|
|
2096
|
+
}
|
|
2097
|
+
var AUTH_PATTERNS = [
|
|
2098
|
+
/authentication[_ ]error/,
|
|
2099
|
+
/invalid[_ ]?(x-)?api[_ ]?key/,
|
|
2100
|
+
/invalid bearer token/,
|
|
2101
|
+
/\bunauthorized\b/,
|
|
2102
|
+
/\b401\b/,
|
|
2103
|
+
/oauth token.{0,20}expired/,
|
|
2104
|
+
/(login|token|credential|session).{0,20}(has )?expired/,
|
|
2105
|
+
/please run\s+\/login/,
|
|
2106
|
+
/run `?\/login`?/,
|
|
2107
|
+
/not authenticated/
|
|
2108
|
+
];
|
|
2109
|
+
var SERVER_PATTERNS = [
|
|
2110
|
+
/\b5\d\d\b/,
|
|
2111
|
+
// any 5xx status token (500 / 502 / 503 / 529 …)
|
|
2112
|
+
/overloaded(_error)?/,
|
|
2113
|
+
/\beconnreset\b/,
|
|
2114
|
+
/\betimedout\b/,
|
|
2115
|
+
/socket hang up/,
|
|
2116
|
+
/fetch failed/
|
|
2117
|
+
];
|
|
2118
|
+
var CAP_PATTERNS = [/usage limit/, /weekly limit/, /session limit/, /\bquota\b/];
|
|
2119
|
+
var NEGATED_CAP = /not (your|a) usage limit/g;
|
|
2120
|
+
var RATE_PATTERNS = [/rate[_ ]?limit/, /\b429\b/, /too many requests/];
|
|
2121
|
+
var BARE_LIMIT = /\blimit (reached|exceeded)\b/;
|
|
2122
|
+
|
|
2123
|
+
// src/log-copy.ts
|
|
2124
|
+
function turnFailureCopy(reason, runtime) {
|
|
2125
|
+
const failure = decodeFailureReason(reason);
|
|
2126
|
+
if (failure?.kind === "auth_expired") {
|
|
2127
|
+
if (runtime === "claude-code")
|
|
2128
|
+
return "Claude Code sign-in has expired on this machine. Open Claude Code, run /login, then reply in Cabane to try again.";
|
|
2129
|
+
if (runtime === "codex")
|
|
2130
|
+
return "Codex sign-in has expired on this machine. Run codex login, then reply in Cabane to try again.";
|
|
2131
|
+
return "Sign-in has expired on this machine. Sign in to the coding agent, then reply in Cabane to try again.";
|
|
2132
|
+
}
|
|
2133
|
+
if (failure?.kind === "usage_capped")
|
|
2134
|
+
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.";
|
|
2135
|
+
if (failure?.kind === "rate_limited")
|
|
2136
|
+
return "The provider is limiting requests. Reply in Cabane to try again.";
|
|
2137
|
+
if (failure?.kind === "server_error")
|
|
2138
|
+
return "The provider had a temporary error. Reply in Cabane to try again.";
|
|
2139
|
+
if (reason?.startsWith("missing_secret:"))
|
|
2140
|
+
return `A required secret is missing: ${humanText(reason.slice(15))}. Add it to ~/.cabane/secrets.json, then reply in Cabane to try again.`;
|
|
2141
|
+
if (reason?.startsWith("runtime_incomplete:"))
|
|
2142
|
+
return "The coding agent installation is incomplete. Reinstall with npm i -g @cabane/companion@latest, then restart the companion.";
|
|
2143
|
+
if (/^(runtime|codex|opencode)_unavailable:/.test(reason ?? ""))
|
|
2144
|
+
return "The coding agent is unavailable on this machine. Enable it here or choose another connector in Cabane.";
|
|
2145
|
+
if (reason?.startsWith("model_unavailable:"))
|
|
2146
|
+
return "The selected model is unavailable. Choose a different model in the agent settings in Cabane.";
|
|
2147
|
+
if (reason?.startsWith("prepare_failed:"))
|
|
2148
|
+
return `Could not prepare the working environment (${humanText(reason.slice(15))}). Check the prepare command in ~/.cabane/config.json.`;
|
|
2149
|
+
if (reason?.startsWith("fetch_failed:"))
|
|
2150
|
+
return `Could not load the conversation (${humanText(reason.slice(13))}). Reply in Cabane to try again.`;
|
|
2151
|
+
if (reason === "timeout_idle")
|
|
2152
|
+
return "The coding agent stopped responding. Reply in Cabane to try again.";
|
|
2153
|
+
if (reason === "timeout_total")
|
|
2154
|
+
return "The turn ran longer than expected and was stopped. Reply in Cabane to continue.";
|
|
2155
|
+
if (reason === "result_error:error_max_turns")
|
|
2156
|
+
return "The coding agent reached its step limit. Reply in Cabane to continue.";
|
|
2157
|
+
if (reason === "no_result" || reason === "empty_result" || reason === "empty_result_unverified")
|
|
2158
|
+
return "The coding agent finished without a response. Reply in Cabane to try again.";
|
|
2159
|
+
if (reason === "session_start_failed")
|
|
2160
|
+
return "The coding agent could not start a session. Restart the companion, then reply in Cabane to try again.";
|
|
2161
|
+
if (reason === "no_terminal")
|
|
2162
|
+
return "The coding agent stopped before confirming the turn was complete. Reply in Cabane to continue.";
|
|
2163
|
+
if (reason === "lease_unconfirmed")
|
|
2164
|
+
return "Could not confirm with Cabane that the turn could start. Check this machine\u2019s connection, then reply in Cabane to try again.";
|
|
2165
|
+
if (reason === "workspace_tools_missing")
|
|
2166
|
+
return "The Cabane workspace tools did not load. Reply in Cabane to try again.";
|
|
2167
|
+
if (reason?.startsWith("seq_floor_unavailable:"))
|
|
2168
|
+
return "The interrupted turn could not safely continue. Reply in Cabane to continue.";
|
|
2169
|
+
if (reason === "result_error:error_max_budget_usd")
|
|
2170
|
+
return "The coding agent reached its spending limit. Check its budget settings before trying again in Cabane.";
|
|
2171
|
+
if (reason === "result_error:error_max_structured_output_retries")
|
|
2172
|
+
return "The coding agent could not produce the requested response format. Check the requested format, then reply in Cabane to try again.";
|
|
2173
|
+
if (reason?.startsWith("result_error:"))
|
|
2174
|
+
return "The coding agent stopped with an error while running. Check its transcript, then reply in Cabane to try again.";
|
|
2175
|
+
if (reason === "setup_failed")
|
|
2176
|
+
return "The turn could not be set up. Check the coding agent setup on this machine, then reply in Cabane to try again.";
|
|
2177
|
+
if (reason === "unexpected_role")
|
|
2178
|
+
return "The message could not start an agent response. Send a new message in Cabane to try again.";
|
|
2179
|
+
if (reason === "lease_lost" || reason?.startsWith("lease_refused:"))
|
|
2180
|
+
return "The turn could not keep its connection to Cabane. Reply in Cabane to try again.";
|
|
2181
|
+
const detail = humanText(reason?.replace(/^error:/, "") ?? "").slice(0, 400);
|
|
2182
|
+
if (/\b(EACCES|EPERM)\b|permission denied/i.test(detail)) {
|
|
2183
|
+
const readable = detail.replace(/\b(EACCES|EPERM)\b/g, "permission denied");
|
|
2184
|
+
return `The coding agent was denied access: ${readable}. Check access permissions for the reported file or command, then reply in Cabane to try again.`;
|
|
2185
|
+
}
|
|
2186
|
+
if (detail)
|
|
2187
|
+
return `The coding agent reported: ${detail}. Check the coding agent setup on this machine, then reply in Cabane to try again.`;
|
|
2188
|
+
return "The coding agent could not finish this turn. Reply in Cabane to try again.";
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
// src/api-error-shape.ts
|
|
2192
|
+
var LEASE_REFUSALS = /* @__PURE__ */ new Set([
|
|
2193
|
+
"dispatch_not_admitted",
|
|
2194
|
+
"turn_already_ended",
|
|
2195
|
+
"turn_belongs_elsewhere"
|
|
2196
|
+
]);
|
|
2197
|
+
function apiErrorCode(err) {
|
|
2198
|
+
if (!(err instanceof ApiError)) return null;
|
|
2199
|
+
const body = err.body;
|
|
2200
|
+
return typeof body === "object" && body !== null && typeof body.error === "string" ? body.error : null;
|
|
2201
|
+
}
|
|
2202
|
+
function leaseRefusal(err) {
|
|
2203
|
+
const code = apiErrorCode(err);
|
|
2204
|
+
if (code && LEASE_REFUSALS.has(code)) return code;
|
|
2205
|
+
return null;
|
|
2206
|
+
}
|
|
2207
|
+
function isWriteFenceRefusal(err) {
|
|
2208
|
+
return err instanceof ApiError && err.status === 409 && apiErrorCode(err) === "not_running";
|
|
2209
|
+
}
|
|
2210
|
+
var ERROR_BODY_LOG_CAP = 2e3;
|
|
2211
|
+
function describeErrorBody(body) {
|
|
2212
|
+
if (body === void 0 || body === null) return void 0;
|
|
2213
|
+
let text;
|
|
2214
|
+
if (typeof body === "string") {
|
|
2215
|
+
text = body;
|
|
2216
|
+
} else {
|
|
2217
|
+
try {
|
|
2218
|
+
text = JSON.stringify(body);
|
|
2219
|
+
} catch {
|
|
2220
|
+
text = String(body);
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
if (text.length === 0) return void 0;
|
|
2224
|
+
return text.length > ERROR_BODY_LOG_CAP ? `${text.slice(0, ERROR_BODY_LOG_CAP)}\u2026` : text;
|
|
2225
|
+
}
|
|
2226
|
+
function apiErrorLogFields(err) {
|
|
2227
|
+
const fields = {
|
|
2228
|
+
err: err instanceof Error ? err.message : String(err)
|
|
2229
|
+
};
|
|
2230
|
+
if (err instanceof ApiError) {
|
|
2231
|
+
fields.status = err.status;
|
|
2232
|
+
const body = describeErrorBody(err.body);
|
|
2233
|
+
if (body !== void 0) fields.responseBody = body;
|
|
2234
|
+
}
|
|
2235
|
+
return fields;
|
|
2236
|
+
}
|
|
2237
|
+
|
|
1945
2238
|
// src/boot-id.ts
|
|
1946
2239
|
import { randomUUID } from "crypto";
|
|
1947
2240
|
var bootId = randomUUID();
|
|
@@ -1966,6 +2259,7 @@ var CabaneApi = class {
|
|
|
1966
2259
|
// this guard two overlapping drains could each pick up the same queued entry
|
|
1967
2260
|
// and double-send it (the server dedupes, but the wasted POSTs aren't free).
|
|
1968
2261
|
draining = false;
|
|
2262
|
+
lastDeliveryWarning = 0;
|
|
1969
2263
|
get base() {
|
|
1970
2264
|
return this.opts.baseUrl.endsWith("/") ? this.opts.baseUrl.slice(0, -1) : this.opts.baseUrl;
|
|
1971
2265
|
}
|
|
@@ -2043,7 +2337,7 @@ var CabaneApi = class {
|
|
|
2043
2337
|
if (signal?.aborted || isAbortError(err)) throw err;
|
|
2044
2338
|
if (!isRetryable(err)) throw err;
|
|
2045
2339
|
outbox.persist({ enqueuedAt: Date.now(), turnId, seq, method: "POST", path, body, kind });
|
|
2046
|
-
this.opts.log?.
|
|
2340
|
+
this.opts.log?.debug(
|
|
2047
2341
|
{ kind, turnId, seq, err: err instanceof Error ? err.message : String(err) },
|
|
2048
2342
|
"companion: commit queued to outbox after transient failure (will drain when the API returns)"
|
|
2049
2343
|
);
|
|
@@ -2071,13 +2365,21 @@ var CabaneApi = class {
|
|
|
2071
2365
|
} catch (err) {
|
|
2072
2366
|
if (err instanceof ApiError && err.status >= 400 && err.status < 500) {
|
|
2073
2367
|
this.opts.log?.warn(
|
|
2074
|
-
{ kind: entry.kind, turnId: entry.turnId, seq: entry.seq,
|
|
2075
|
-
"companion
|
|
2368
|
+
{ kind: entry.kind, turnId: entry.turnId, seq: entry.seq, ...apiErrorLogFields(err) },
|
|
2369
|
+
"Cabane rejected a saved update; it could not be delivered. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
2076
2370
|
);
|
|
2077
2371
|
outbox.remove(entry.turnId, entry.seq);
|
|
2078
2372
|
progressed = true;
|
|
2079
2373
|
continue;
|
|
2080
2374
|
}
|
|
2375
|
+
const now = Date.now();
|
|
2376
|
+
if (now - entry.enqueuedAt >= 12e4 && now - this.lastDeliveryWarning >= 12e4) {
|
|
2377
|
+
this.lastDeliveryWarning = now;
|
|
2378
|
+
this.opts.log?.warn(
|
|
2379
|
+
{ ...apiErrorLogFields(err) },
|
|
2380
|
+
"Updates have been waiting to reach Cabane for at least two minutes; still trying. Check the connection to Cabane."
|
|
2381
|
+
);
|
|
2382
|
+
}
|
|
2081
2383
|
break;
|
|
2082
2384
|
}
|
|
2083
2385
|
}
|
|
@@ -2279,7 +2581,7 @@ var CabaneApi = class {
|
|
|
2279
2581
|
body,
|
|
2280
2582
|
kind: "active-run"
|
|
2281
2583
|
});
|
|
2282
|
-
this.opts.log?.
|
|
2584
|
+
this.opts.log?.debug(
|
|
2283
2585
|
{ conversationId, agentId, turnId },
|
|
2284
2586
|
"companion: settle queued behind this turn's undelivered commits (drains in order)"
|
|
2285
2587
|
);
|
|
@@ -2303,7 +2605,7 @@ var CabaneApi = class {
|
|
|
2303
2605
|
body,
|
|
2304
2606
|
kind: "active-run"
|
|
2305
2607
|
});
|
|
2306
|
-
this.opts.log?.
|
|
2608
|
+
this.opts.log?.debug(
|
|
2307
2609
|
{ conversationId, agentId, err: err instanceof Error ? err.message : String(err) },
|
|
2308
2610
|
"companion: active-run write queued to outbox after transient failure (will drain when the API returns)"
|
|
2309
2611
|
);
|
|
@@ -2524,11 +2826,11 @@ import {
|
|
|
2524
2826
|
writeFileSync as writeFileSync3
|
|
2525
2827
|
} from "fs";
|
|
2526
2828
|
import { dirname as dirname5, join as join8 } from "path";
|
|
2527
|
-
import { z as
|
|
2829
|
+
import { z as z4 } from "zod";
|
|
2528
2830
|
function credentialsPath() {
|
|
2529
2831
|
return join8(cabaneDir(), "credentials.json");
|
|
2530
2832
|
}
|
|
2531
|
-
var credentialStoreSchema =
|
|
2833
|
+
var credentialStoreSchema = z4.record(z4.string(), z4.string());
|
|
2532
2834
|
function load() {
|
|
2533
2835
|
const path = credentialsPath();
|
|
2534
2836
|
if (!existsSync5(path)) return {};
|
|
@@ -2790,44 +3092,44 @@ function noResume() {
|
|
|
2790
3092
|
var TURN_PROTOCOL_VERSION = 1;
|
|
2791
3093
|
|
|
2792
3094
|
// packages/agent-runtime/src/host-policy.ts
|
|
2793
|
-
import { z as
|
|
2794
|
-
var hostPolicySchema =
|
|
3095
|
+
import { z as z5 } from "zod";
|
|
3096
|
+
var hostPolicySchema = z5.object({
|
|
2795
3097
|
// Host filesystem + shell: `Bash`/`Read`/`Write`/`Edit`/`Glob`/`Grep`, the
|
|
2796
3098
|
// notebook read/write pair, git-worktree tools, and the `REPL` host code-exec
|
|
2797
3099
|
// tool. Off under the locked assistant surface (today's `DISALLOWED_TOOLS`);
|
|
2798
3100
|
// on under `coding` mode.
|
|
2799
|
-
hostFs:
|
|
3101
|
+
hostFs: z5.boolean(),
|
|
2800
3102
|
// Public web (today's `WebSearch` / `WebFetch`, the `DEFAULT_WEB_TOOLS`). Public
|
|
2801
3103
|
// web, not host reach — granted by default today, but expressible as a grant.
|
|
2802
|
-
web:
|
|
3104
|
+
web: z5.boolean(),
|
|
2803
3105
|
// Browser automation (the Playwright MCP surface). Varies by host: a companion has
|
|
2804
3106
|
// it, the house executor does not (CT230).
|
|
2805
|
-
browser:
|
|
3107
|
+
browser: z5.boolean(),
|
|
2806
3108
|
// User-configured MCP servers permitted. False for the house executor
|
|
2807
3109
|
// (CT227: Cabane agents run no user MCP servers), true for a personal companion.
|
|
2808
|
-
userMcp:
|
|
3110
|
+
userMcp: z5.boolean(),
|
|
2809
3111
|
// Subagent / sub-dispatch (`Task` / `Agent`). A per-host GRANT (see the CT261
|
|
2810
3112
|
// amendment above): `false` on the locked assistant/house surface (banned via
|
|
2811
3113
|
// `DISALLOWED_TOOLS`), `true` under coding / custom where the fan-out is left to
|
|
2812
3114
|
// the coding allow-hook (which reaches sub-agent tool calls, CT680) or the user
|
|
2813
3115
|
// allowlist. The subagent completes within the turn, so
|
|
2814
3116
|
// it's not the turn-model invariant `scheduling` is.
|
|
2815
|
-
subagents:
|
|
3117
|
+
subagents: z5.boolean(),
|
|
2816
3118
|
// ── Hard platform invariants — always denied, never granted ────────────────
|
|
2817
3119
|
// Deferred re-invocation / scheduling (`ScheduleWakeup`, the `Cron*` / `Task*`
|
|
2818
3120
|
// families, `Monitor`, `Workflow`, …). A turn is one query that resolves when
|
|
2819
3121
|
// `result` fires; a scheduled callback fires after the reply window has closed
|
|
2820
3122
|
// and strands the agent (the CT155/CT156 rule).
|
|
2821
|
-
scheduling:
|
|
3123
|
+
scheduling: z5.literal("never"),
|
|
2822
3124
|
// Human-facing UI prompts (`AskUserQuestion`). A Cabane conversation has no
|
|
2823
3125
|
// handler to answer a structured prompt, so the call hangs the turn
|
|
2824
3126
|
// (`UNSUPPORTED_TOOLS`). The agent asks in its reply instead.
|
|
2825
|
-
uiPrompts:
|
|
3127
|
+
uiPrompts: z5.literal("never")
|
|
2826
3128
|
});
|
|
2827
3129
|
|
|
2828
3130
|
// packages/agent-runtime/src/turn-event.ts
|
|
2829
|
-
import { z as
|
|
2830
|
-
var turnEventSchema =
|
|
3131
|
+
import { z as z6 } from "zod";
|
|
3132
|
+
var turnEventSchema = z6.discriminatedUnion("type", [
|
|
2831
3133
|
// The runtime's opaque session state, emitted when the adapter learns it (e.g.
|
|
2832
3134
|
// the SDK `system/init` frame). The platform stores `state` verbatim per
|
|
2833
3135
|
// (conversation, agent) and hands it back on the next turn; only the adapter
|
|
@@ -2847,25 +3149,25 @@ var turnEventSchema = z5.discriminatedUnion("type", [
|
|
|
2847
3149
|
// on the companion, after the server committed the manifest). Runtime-neutral: a
|
|
2848
3150
|
// plain boolean, not a runtime-specific reason string (that stays in the
|
|
2849
3151
|
// adapter's `onWarn` log). Additive + optional — an old receiver ignores it.
|
|
2850
|
-
|
|
2851
|
-
type:
|
|
2852
|
-
state:
|
|
2853
|
-
degraded:
|
|
3152
|
+
z6.object({
|
|
3153
|
+
type: z6.literal("session"),
|
|
3154
|
+
state: z6.string(),
|
|
3155
|
+
degraded: z6.boolean().optional()
|
|
2854
3156
|
}),
|
|
2855
3157
|
// One readable thinking summary. Maps `onThinking({ text })`. Transient —
|
|
2856
3158
|
// surfaced live, never persisted as durable content.
|
|
2857
|
-
|
|
3159
|
+
z6.object({ type: z6.literal("thinking"), text: z6.string() }),
|
|
2858
3160
|
// Assistant text. Maps `onAssistantText({ text, final })` — `text`→`body`,
|
|
2859
3161
|
// `final`→`terminal`. `terminal: false` is interim narration (commits as a
|
|
2860
3162
|
// `progress` row); `terminal: true` is the turn's closing reply (commits as
|
|
2861
3163
|
// the `final` row).
|
|
2862
|
-
|
|
3164
|
+
z6.object({ type: z6.literal("text"), body: z6.string(), terminal: z6.boolean() }),
|
|
2863
3165
|
// Runtime/provider-authored prose surfaced alongside a failed turn. Unlike
|
|
2864
3166
|
// `text`, this is not the agent's narration or reply: the pump persists it as
|
|
2865
3167
|
// `runtime_notice`, and the transcript renders it in the shared SystemNote
|
|
2866
3168
|
// voice. Adapters emit it only from positive runtime evidence; the web never
|
|
2867
3169
|
// classifies English strings.
|
|
2868
|
-
|
|
3170
|
+
z6.object({ type: z6.literal("runtime_notice"), body: z6.string() }),
|
|
2869
3171
|
// A tool-activity transition. Maps `onToolActivity` — `toolUseId`→`id`,
|
|
2870
3172
|
// `toolName`→`name` (already prefix-stripped: `cabane_read`, not
|
|
2871
3173
|
// `mcp__cabane__cabane_read`), `summary` is the short card label. `phase`
|
|
@@ -2880,15 +3182,15 @@ var turnEventSchema = z5.discriminatedUnion("type", [
|
|
|
2880
3182
|
// dropped the prefix; null for a host / built-in tool. The client tags Cabane
|
|
2881
3183
|
// MCP calls (`=== 'cabane'`) with a subtle glyph. Nullable + optional so a
|
|
2882
3184
|
// pre-CT496 producer that never sets it is unaffected (treated as null).
|
|
2883
|
-
|
|
2884
|
-
type:
|
|
2885
|
-
id:
|
|
2886
|
-
name:
|
|
2887
|
-
phase:
|
|
2888
|
-
summary:
|
|
2889
|
-
input:
|
|
2890
|
-
result:
|
|
2891
|
-
mcpServer:
|
|
3185
|
+
z6.object({
|
|
3186
|
+
type: z6.literal("tool"),
|
|
3187
|
+
id: z6.string(),
|
|
3188
|
+
name: z6.string(),
|
|
3189
|
+
phase: z6.enum(["start", "done", "error"]),
|
|
3190
|
+
summary: z6.string(),
|
|
3191
|
+
input: z6.unknown().optional(),
|
|
3192
|
+
result: z6.unknown().optional(),
|
|
3193
|
+
mcpServer: z6.string().nullable().optional()
|
|
2892
3194
|
}),
|
|
2893
3195
|
// The turn's terminal outcome. Maps the `{ ok, reason }` both loops return
|
|
2894
3196
|
// inline. `ok:false` carries a machine reason (`no_session`, an error code);
|
|
@@ -2932,54 +3234,54 @@ var turnEventSchema = z5.discriminatedUnion("type", [
|
|
|
2932
3234
|
// `message_turn_metadata.resolved_model` / `resolved_config`. Optional ⇒
|
|
2933
3235
|
// backward-compatible: an old adapter/companion omits them, a cancel has no result
|
|
2934
3236
|
// event at all, and the columns stay null → the UI shows `—`.
|
|
2935
|
-
|
|
2936
|
-
type:
|
|
2937
|
-
ok:
|
|
2938
|
-
reason:
|
|
2939
|
-
usage:
|
|
2940
|
-
inputTokens:
|
|
2941
|
-
outputTokens:
|
|
2942
|
-
cacheReadTokens:
|
|
2943
|
-
cacheCreationTokens:
|
|
2944
|
-
contextTokens:
|
|
2945
|
-
contextWindow:
|
|
3237
|
+
z6.object({
|
|
3238
|
+
type: z6.literal("result"),
|
|
3239
|
+
ok: z6.boolean(),
|
|
3240
|
+
reason: z6.string().optional(),
|
|
3241
|
+
usage: z6.object({
|
|
3242
|
+
inputTokens: z6.number(),
|
|
3243
|
+
outputTokens: z6.number(),
|
|
3244
|
+
cacheReadTokens: z6.number().optional(),
|
|
3245
|
+
cacheCreationTokens: z6.number().optional(),
|
|
3246
|
+
contextTokens: z6.number().optional(),
|
|
3247
|
+
contextWindow: z6.number().optional()
|
|
2946
3248
|
}).optional(),
|
|
2947
|
-
resolvedModel:
|
|
2948
|
-
resolvedConfig:
|
|
2949
|
-
effort:
|
|
2950
|
-
thinking:
|
|
2951
|
-
reasoningEffort:
|
|
3249
|
+
resolvedModel: z6.string().optional(),
|
|
3250
|
+
resolvedConfig: z6.object({
|
|
3251
|
+
effort: z6.string().optional(),
|
|
3252
|
+
thinking: z6.string().optional(),
|
|
3253
|
+
reasoningEffort: z6.string().optional()
|
|
2952
3254
|
}).optional(),
|
|
2953
3255
|
// CT1275: the harness-reported MCP inventory from this turn's init frame.
|
|
2954
3256
|
// This is deliberately diagnostic-only: names, statuses and a count, never
|
|
2955
3257
|
// server definitions, credentials or session ids. `initReceived:false`
|
|
2956
3258
|
// distinguishes a missing init frame from a real empty inventory.
|
|
2957
|
-
mcpInventory:
|
|
2958
|
-
initReceived:
|
|
2959
|
-
servers:
|
|
2960
|
-
toolCount:
|
|
3259
|
+
mcpInventory: z6.object({
|
|
3260
|
+
initReceived: z6.boolean(),
|
|
3261
|
+
servers: z6.array(z6.object({ name: z6.string(), status: z6.string() })),
|
|
3262
|
+
toolCount: z6.number().int().nonnegative()
|
|
2961
3263
|
}).optional()
|
|
2962
3264
|
})
|
|
2963
3265
|
]);
|
|
2964
3266
|
|
|
2965
3267
|
// packages/agent-runtime/src/turn-diagnostics.ts
|
|
2966
|
-
import { z as
|
|
2967
|
-
var turnResultReasonSchema =
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
3268
|
+
import { z as z7 } from "zod";
|
|
3269
|
+
var turnResultReasonSchema = z7.discriminatedUnion("kind", [
|
|
3270
|
+
z7.object({ kind: z7.literal("usage_capped"), resetsAt: z7.string().optional() }),
|
|
3271
|
+
z7.object({ kind: z7.literal("rate_limited") }),
|
|
3272
|
+
z7.object({ kind: z7.literal("server_error") }),
|
|
3273
|
+
z7.object({ kind: z7.literal("auth_expired") }),
|
|
3274
|
+
z7.object({ kind: z7.literal("no_result") }),
|
|
3275
|
+
z7.object({ kind: z7.literal("empty_result") }),
|
|
3276
|
+
z7.object({ kind: z7.literal("empty_result_unverified") }),
|
|
3277
|
+
z7.object({ kind: z7.literal("timeout_idle") }),
|
|
3278
|
+
z7.object({ kind: z7.literal("timeout_total") }),
|
|
3279
|
+
z7.object({ kind: z7.literal("cancelled") }),
|
|
3280
|
+
z7.object({ kind: z7.literal("lease_lost") }),
|
|
3281
|
+
z7.object({ kind: z7.literal("skipped") }),
|
|
3282
|
+
z7.object({ kind: z7.literal("session_start_failed") }),
|
|
3283
|
+
z7.object({ kind: z7.literal("workspace_tools_missing") }),
|
|
3284
|
+
z7.object({ kind: z7.literal("runtime_error") })
|
|
2983
3285
|
]);
|
|
2984
3286
|
var turnOutcomes = ["success", "failure", "cancelled", "skipped"];
|
|
2985
3287
|
var turnSessionModes = ["fresh", "resumed", "degraded"];
|
|
@@ -2991,150 +3293,28 @@ var turnFinalSources = [
|
|
|
2991
3293
|
"marker",
|
|
2992
3294
|
"none"
|
|
2993
3295
|
];
|
|
2994
|
-
var turnDiagnosticsSchema =
|
|
2995
|
-
outcome:
|
|
3296
|
+
var turnDiagnosticsSchema = z7.object({
|
|
3297
|
+
outcome: z7.enum(turnOutcomes),
|
|
2996
3298
|
resultReason: turnResultReasonSchema.nullable(),
|
|
2997
|
-
sessionMode:
|
|
2998
|
-
sessionFingerprint:
|
|
2999
|
-
eventCounts:
|
|
3000
|
-
session:
|
|
3001
|
-
text:
|
|
3002
|
-
runtime_notice:
|
|
3003
|
-
thinking:
|
|
3004
|
-
tool:
|
|
3005
|
-
result:
|
|
3299
|
+
sessionMode: z7.enum(turnSessionModes),
|
|
3300
|
+
sessionFingerprint: z7.string().regex(/^[a-f0-9]{16}$/).nullable(),
|
|
3301
|
+
eventCounts: z7.object({
|
|
3302
|
+
session: z7.number().int().nonnegative(),
|
|
3303
|
+
text: z7.number().int().nonnegative(),
|
|
3304
|
+
runtime_notice: z7.number().int().nonnegative(),
|
|
3305
|
+
thinking: z7.number().int().nonnegative(),
|
|
3306
|
+
tool: z7.number().int().nonnegative(),
|
|
3307
|
+
result: z7.number().int().nonnegative()
|
|
3006
3308
|
}),
|
|
3007
|
-
runtimeResultKind:
|
|
3008
|
-
finalSource:
|
|
3009
|
-
mcpInventory:
|
|
3010
|
-
initReceived:
|
|
3011
|
-
servers:
|
|
3012
|
-
toolCount:
|
|
3309
|
+
runtimeResultKind: z7.enum(turnRuntimeResultKinds).nullable(),
|
|
3310
|
+
finalSource: z7.enum(turnFinalSources),
|
|
3311
|
+
mcpInventory: z7.object({
|
|
3312
|
+
initReceived: z7.boolean(),
|
|
3313
|
+
servers: z7.array(z7.object({ name: z7.string(), status: z7.string() })),
|
|
3314
|
+
toolCount: z7.number().int().nonnegative()
|
|
3013
3315
|
}).optional()
|
|
3014
3316
|
});
|
|
3015
3317
|
|
|
3016
|
-
// packages/agent-runtime/src/failure.ts
|
|
3017
|
-
import { z as z7 } from "zod";
|
|
3018
|
-
var turnFailureSchema = z7.discriminatedUnion("kind", [
|
|
3019
|
-
z7.object({ kind: z7.literal("usage_capped"), resetsAt: z7.string().optional() }),
|
|
3020
|
-
z7.object({ kind: z7.literal("rate_limited") }),
|
|
3021
|
-
z7.object({ kind: z7.literal("server_error") }),
|
|
3022
|
-
z7.object({ kind: z7.literal("auth_expired") })
|
|
3023
|
-
]);
|
|
3024
|
-
var USAGE_CAPPED = "usage_capped";
|
|
3025
|
-
var RATE_LIMITED = "rate_limited";
|
|
3026
|
-
var SERVER_ERROR = "server_error";
|
|
3027
|
-
var AUTH_EXPIRED = "auth_expired";
|
|
3028
|
-
function encodeFailureReason(failure) {
|
|
3029
|
-
switch (failure.kind) {
|
|
3030
|
-
case "auth_expired":
|
|
3031
|
-
return AUTH_EXPIRED;
|
|
3032
|
-
case "server_error":
|
|
3033
|
-
return SERVER_ERROR;
|
|
3034
|
-
case "rate_limited":
|
|
3035
|
-
return RATE_LIMITED;
|
|
3036
|
-
case "usage_capped":
|
|
3037
|
-
return failure.resetsAt ? `${USAGE_CAPPED}:${failure.resetsAt}` : USAGE_CAPPED;
|
|
3038
|
-
}
|
|
3039
|
-
}
|
|
3040
|
-
function decodeFailureReason(reason) {
|
|
3041
|
-
if (!reason) return null;
|
|
3042
|
-
if (reason === AUTH_EXPIRED) return { kind: "auth_expired" };
|
|
3043
|
-
if (reason === SERVER_ERROR) return { kind: "server_error" };
|
|
3044
|
-
if (reason === RATE_LIMITED) return { kind: "rate_limited" };
|
|
3045
|
-
if (reason === USAGE_CAPPED) return { kind: "usage_capped" };
|
|
3046
|
-
if (reason.startsWith(`${USAGE_CAPPED}:`)) {
|
|
3047
|
-
const iso = reason.slice(USAGE_CAPPED.length + 1);
|
|
3048
|
-
return isValidIso(iso) ? { kind: "usage_capped", resetsAt: iso } : { kind: "usage_capped" };
|
|
3049
|
-
}
|
|
3050
|
-
return null;
|
|
3051
|
-
}
|
|
3052
|
-
function isValidIso(value) {
|
|
3053
|
-
if (!value) return false;
|
|
3054
|
-
const ms = Date.parse(value);
|
|
3055
|
-
return Number.isFinite(ms);
|
|
3056
|
-
}
|
|
3057
|
-
function quotaForFailure(failure) {
|
|
3058
|
-
if (failure.kind === "auth_expired") return { quotaState: "auth_expired", limitedUntil: null };
|
|
3059
|
-
if (failure.kind === "usage_capped")
|
|
3060
|
-
return { quotaState: "limited", limitedUntil: failure.resetsAt ?? null };
|
|
3061
|
-
return null;
|
|
3062
|
-
}
|
|
3063
|
-
function resetsAtToIso(resetsAt) {
|
|
3064
|
-
if (typeof resetsAt !== "number" || !Number.isFinite(resetsAt) || resetsAt <= 0) return void 0;
|
|
3065
|
-
const ms = resetsAt < 1e12 ? resetsAt * 1e3 : resetsAt;
|
|
3066
|
-
const date = new Date(ms);
|
|
3067
|
-
const time = date.getTime();
|
|
3068
|
-
if (Number.isNaN(time)) return void 0;
|
|
3069
|
-
const YEAR_2000 = 9466848e5;
|
|
3070
|
-
const now = Date.now();
|
|
3071
|
-
if (time < YEAR_2000 || time > now + 366 * 24 * 60 * 60 * 1e3) return void 0;
|
|
3072
|
-
return date.toISOString();
|
|
3073
|
-
}
|
|
3074
|
-
function classifyAssistantError(error) {
|
|
3075
|
-
switch (error) {
|
|
3076
|
-
case "rate_limit":
|
|
3077
|
-
return { kind: "rate_limited" };
|
|
3078
|
-
case "overloaded":
|
|
3079
|
-
case "server_error":
|
|
3080
|
-
return { kind: "server_error" };
|
|
3081
|
-
case "authentication_failed":
|
|
3082
|
-
case "oauth_org_not_allowed":
|
|
3083
|
-
return { kind: "auth_expired" };
|
|
3084
|
-
default:
|
|
3085
|
-
return null;
|
|
3086
|
-
}
|
|
3087
|
-
}
|
|
3088
|
-
function classifyErrorText(text) {
|
|
3089
|
-
if (!text) return null;
|
|
3090
|
-
const t = text.toLowerCase();
|
|
3091
|
-
if (AUTH_PATTERNS.some((re) => re.test(t))) return { kind: "auth_expired" };
|
|
3092
|
-
if (SERVER_PATTERNS.some((re) => re.test(t))) return { kind: "server_error" };
|
|
3093
|
-
const withoutNegatedCap = t.replace(NEGATED_CAP, "");
|
|
3094
|
-
const capNoun = CAP_PATTERNS.some((re) => re.test(withoutNegatedCap));
|
|
3095
|
-
const rateToken = RATE_PATTERNS.some((re) => re.test(t));
|
|
3096
|
-
if (capNoun) return { kind: "usage_capped" };
|
|
3097
|
-
if (rateToken) return { kind: "rate_limited" };
|
|
3098
|
-
if (BARE_LIMIT.test(t)) return { kind: "usage_capped" };
|
|
3099
|
-
return null;
|
|
3100
|
-
}
|
|
3101
|
-
function classifyRuntimeNoticeText(text) {
|
|
3102
|
-
if (!text) return null;
|
|
3103
|
-
const normalized = text.trim().replace(/\s+/g, " ");
|
|
3104
|
-
if (!normalized) return null;
|
|
3105
|
-
if (/^you(?:'|’)ve hit your (?:session|weekly|usage) limit(?:\s*[·—-]\s*resets?\s+.+)?$/i.test(
|
|
3106
|
-
normalized
|
|
3107
|
-
)) {
|
|
3108
|
-
return { kind: "usage_capped" };
|
|
3109
|
-
}
|
|
3110
|
-
return null;
|
|
3111
|
-
}
|
|
3112
|
-
var AUTH_PATTERNS = [
|
|
3113
|
-
/authentication[_ ]error/,
|
|
3114
|
-
/invalid[_ ]?(x-)?api[_ ]?key/,
|
|
3115
|
-
/invalid bearer token/,
|
|
3116
|
-
/\bunauthorized\b/,
|
|
3117
|
-
/\b401\b/,
|
|
3118
|
-
/oauth token.{0,20}expired/,
|
|
3119
|
-
/(login|token|credential|session).{0,20}(has )?expired/,
|
|
3120
|
-
/please run\s+\/login/,
|
|
3121
|
-
/run `?\/login`?/,
|
|
3122
|
-
/not authenticated/
|
|
3123
|
-
];
|
|
3124
|
-
var SERVER_PATTERNS = [
|
|
3125
|
-
/\b5\d\d\b/,
|
|
3126
|
-
// any 5xx status token (500 / 502 / 503 / 529 …)
|
|
3127
|
-
/overloaded(_error)?/,
|
|
3128
|
-
/\beconnreset\b/,
|
|
3129
|
-
/\betimedout\b/,
|
|
3130
|
-
/socket hang up/,
|
|
3131
|
-
/fetch failed/
|
|
3132
|
-
];
|
|
3133
|
-
var CAP_PATTERNS = [/usage limit/, /weekly limit/, /session limit/, /\bquota\b/];
|
|
3134
|
-
var NEGATED_CAP = /not (your|a) usage limit/g;
|
|
3135
|
-
var RATE_PATTERNS = [/rate[_ ]?limit/, /\b429\b/, /too many requests/];
|
|
3136
|
-
var BARE_LIMIT = /\blimit (reached|exceeded)\b/;
|
|
3137
|
-
|
|
3138
3318
|
// packages/agent-runtime/src/turn-diagnostics-normalize.ts
|
|
3139
3319
|
function normalizeTurnResultReason(reason) {
|
|
3140
3320
|
const classified = decodeFailureReason(reason);
|
|
@@ -3852,11 +4032,6 @@ function buildClaudeCodeOptions(req, augment) {
|
|
|
3852
4032
|
const freshReason = "fresh" in resumeDecision ? resumeDecision.reason : void 0;
|
|
3853
4033
|
const devControlsAutoMemory = req.local.claudeCode?.autoMemory === true;
|
|
3854
4034
|
const model = parseClaudeCodeModel(config.model);
|
|
3855
|
-
if (model === null) {
|
|
3856
|
-
console.warn(
|
|
3857
|
-
`[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.`
|
|
3858
|
-
);
|
|
3859
|
-
}
|
|
3860
4035
|
const base = {
|
|
3861
4036
|
// model / effort: `model` is pinned only when the config names a real one —
|
|
3862
4037
|
// omitted for "let it choose" (see `parseClaudeCodeModel`), so the SDK picks
|
|
@@ -4175,6 +4350,11 @@ function createClaudeCodeAdapter(deps = {}) {
|
|
|
4175
4350
|
degraded = fresh.degraded;
|
|
4176
4351
|
built = buildClaudeCodeOptions(req, deps.augmentOptions);
|
|
4177
4352
|
}
|
|
4353
|
+
if (built.options.model === void 0) {
|
|
4354
|
+
deps.onWarn?.("Claude Code is using its default model because no model was resolved", {
|
|
4355
|
+
model: req.config.model
|
|
4356
|
+
});
|
|
4357
|
+
}
|
|
4178
4358
|
yield* runWithResumeRecovery(
|
|
4179
4359
|
queryFn,
|
|
4180
4360
|
req,
|
|
@@ -4207,7 +4387,7 @@ var HEALTHY_MCP_INVENTORY = {
|
|
|
4207
4387
|
servers: [{ name: "cabane", status: "connected" }],
|
|
4208
4388
|
toolCount: 1
|
|
4209
4389
|
};
|
|
4210
|
-
function makeRequest(
|
|
4390
|
+
function makeRequest(overrides2 = {}) {
|
|
4211
4391
|
return {
|
|
4212
4392
|
systemPrompt: "system",
|
|
4213
4393
|
prompt: "hi there",
|
|
@@ -4220,7 +4400,7 @@ function makeRequest(overrides = {}) {
|
|
|
4220
4400
|
cabane: { mcpUrl: "https://cabane.test/api/mcp", bearer: "tok", activeConversationId: "conv" },
|
|
4221
4401
|
local: { cwd: CWD },
|
|
4222
4402
|
extra: { mcpServers: {} },
|
|
4223
|
-
...
|
|
4403
|
+
...overrides2
|
|
4224
4404
|
};
|
|
4225
4405
|
}
|
|
4226
4406
|
var init = (sessionId, model) => ({
|
|
@@ -5408,7 +5588,7 @@ var COMPANION_POLICY = {
|
|
|
5408
5588
|
uiPrompts: "never"
|
|
5409
5589
|
};
|
|
5410
5590
|
var DIR = "/env/here";
|
|
5411
|
-
function makeRequest2(
|
|
5591
|
+
function makeRequest2(overrides2 = {}) {
|
|
5412
5592
|
return {
|
|
5413
5593
|
systemPrompt: "system",
|
|
5414
5594
|
prompt: "hi there",
|
|
@@ -5419,7 +5599,7 @@ function makeRequest2(overrides = {}) {
|
|
|
5419
5599
|
cabane: { mcpUrl: "https://cabane.test/api/mcp", bearer: "tok", activeConversationId: "conv" },
|
|
5420
5600
|
local: { cwd: DIR },
|
|
5421
5601
|
extra: { mcpServers: {} },
|
|
5422
|
-
...
|
|
5602
|
+
...overrides2
|
|
5423
5603
|
};
|
|
5424
5604
|
}
|
|
5425
5605
|
var textPart = (id, text) => ({
|
|
@@ -6233,11 +6413,6 @@ function buildRunSpec2(req, resumeThreadId, instructionsFile = null, threadPromp
|
|
|
6233
6413
|
const dialect = readCodexDialect(config.runtimeOptions);
|
|
6234
6414
|
const baseInstructionsFile = !policy.hostFs && instructionsFile ? instructionsFile : null;
|
|
6235
6415
|
const model = config.model ? parseCodexModel(config.model) : null;
|
|
6236
|
-
if (model === null) {
|
|
6237
|
-
console.warn(
|
|
6238
|
-
`[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.`
|
|
6239
|
-
);
|
|
6240
|
-
}
|
|
6241
6416
|
const promptFingerprint = fingerprintPrompt(req.systemPrompt);
|
|
6242
6417
|
const threadHasThisPrompt = resumeThreadId !== null && threadPromptFingerprint !== null && threadPromptFingerprint === promptFingerprint;
|
|
6243
6418
|
const promptRidesInput = baseInstructionsFile === null && !threadHasThisPrompt;
|
|
@@ -6434,6 +6609,11 @@ function createCodexAdapter(deps = {}) {
|
|
|
6434
6609
|
instructions?.path ?? null,
|
|
6435
6610
|
threadPromptFingerprint
|
|
6436
6611
|
);
|
|
6612
|
+
if (spec.model === null) {
|
|
6613
|
+
deps.onWarn?.("Codex is using its default model because no model was resolved", {
|
|
6614
|
+
model: req.config.model
|
|
6615
|
+
});
|
|
6616
|
+
}
|
|
6437
6617
|
const result = await transport.run(spec, signal);
|
|
6438
6618
|
if (signal.aborted) return;
|
|
6439
6619
|
yield* decodeCodexStream(result.events, {
|
|
@@ -6485,7 +6665,7 @@ var COMPANION_POLICY2 = {
|
|
|
6485
6665
|
};
|
|
6486
6666
|
var DIR2 = "/env/here";
|
|
6487
6667
|
var PROMPT = "system";
|
|
6488
|
-
function makeRequest3(
|
|
6668
|
+
function makeRequest3(overrides2 = {}) {
|
|
6489
6669
|
return {
|
|
6490
6670
|
systemPrompt: PROMPT,
|
|
6491
6671
|
prompt: "hi there",
|
|
@@ -6501,7 +6681,7 @@ function makeRequest3(overrides = {}) {
|
|
|
6501
6681
|
cabane: { mcpUrl: "https://cabane.test/api/mcp", bearer: "tok", activeConversationId: "conv" },
|
|
6502
6682
|
local: { cwd: DIR2 },
|
|
6503
6683
|
extra: { mcpServers: {} },
|
|
6504
|
-
...
|
|
6684
|
+
...overrides2
|
|
6505
6685
|
};
|
|
6506
6686
|
}
|
|
6507
6687
|
var threadStarted = (threadId) => ({
|
|
@@ -7435,6 +7615,16 @@ var ConnectorHealthStore = class {
|
|
|
7435
7615
|
import { existsSync as existsSync11, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
7436
7616
|
import { join as join15 } from "path";
|
|
7437
7617
|
|
|
7618
|
+
// src/term.ts
|
|
7619
|
+
import { createInterface } from "readline";
|
|
7620
|
+
import { homedir as homedir2 } from "os";
|
|
7621
|
+
import pc from "picocolors";
|
|
7622
|
+
function tildePath(path) {
|
|
7623
|
+
const home = homedir2();
|
|
7624
|
+
if (home && path.startsWith(home + "/")) return `~${path.slice(home.length)}`;
|
|
7625
|
+
return path;
|
|
7626
|
+
}
|
|
7627
|
+
|
|
7438
7628
|
// src/turn-execution.ts
|
|
7439
7629
|
import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
|
|
7440
7630
|
import { existsSync as existsSync10 } from "fs";
|
|
@@ -7701,53 +7891,6 @@ async function writeCodexInstructionsFile(contents) {
|
|
|
7701
7891
|
};
|
|
7702
7892
|
}
|
|
7703
7893
|
|
|
7704
|
-
// src/api-error-shape.ts
|
|
7705
|
-
var LEASE_REFUSALS = /* @__PURE__ */ new Set([
|
|
7706
|
-
"dispatch_not_admitted",
|
|
7707
|
-
"turn_already_ended",
|
|
7708
|
-
"turn_belongs_elsewhere"
|
|
7709
|
-
]);
|
|
7710
|
-
function apiErrorCode(err) {
|
|
7711
|
-
if (!(err instanceof ApiError)) return null;
|
|
7712
|
-
const body = err.body;
|
|
7713
|
-
return typeof body === "object" && body !== null && typeof body.error === "string" ? body.error : null;
|
|
7714
|
-
}
|
|
7715
|
-
function leaseRefusal(err) {
|
|
7716
|
-
const code = apiErrorCode(err);
|
|
7717
|
-
if (code && LEASE_REFUSALS.has(code)) return code;
|
|
7718
|
-
return null;
|
|
7719
|
-
}
|
|
7720
|
-
function isWriteFenceRefusal(err) {
|
|
7721
|
-
return err instanceof ApiError && err.status === 409 && apiErrorCode(err) === "not_running";
|
|
7722
|
-
}
|
|
7723
|
-
var ERROR_BODY_LOG_CAP = 2e3;
|
|
7724
|
-
function describeErrorBody(body) {
|
|
7725
|
-
if (body === void 0 || body === null) return void 0;
|
|
7726
|
-
let text;
|
|
7727
|
-
if (typeof body === "string") {
|
|
7728
|
-
text = body;
|
|
7729
|
-
} else {
|
|
7730
|
-
try {
|
|
7731
|
-
text = JSON.stringify(body);
|
|
7732
|
-
} catch {
|
|
7733
|
-
text = String(body);
|
|
7734
|
-
}
|
|
7735
|
-
}
|
|
7736
|
-
if (text.length === 0) return void 0;
|
|
7737
|
-
return text.length > ERROR_BODY_LOG_CAP ? `${text.slice(0, ERROR_BODY_LOG_CAP)}\u2026` : text;
|
|
7738
|
-
}
|
|
7739
|
-
function apiErrorLogFields(err) {
|
|
7740
|
-
const fields = {
|
|
7741
|
-
err: err instanceof Error ? err.message : String(err)
|
|
7742
|
-
};
|
|
7743
|
-
if (err instanceof ApiError) {
|
|
7744
|
-
fields.status = err.status;
|
|
7745
|
-
const body = describeErrorBody(err.body);
|
|
7746
|
-
if (body !== void 0) fields.responseBody = body;
|
|
7747
|
-
}
|
|
7748
|
-
return fields;
|
|
7749
|
-
}
|
|
7750
|
-
|
|
7751
7894
|
// src/turn-seq-floor.ts
|
|
7752
7895
|
var SeqFloorUnavailable = class extends Error {
|
|
7753
7896
|
constructor(detail) {
|
|
@@ -7763,7 +7906,7 @@ function resolveSeqFloor(sources, ctx) {
|
|
|
7763
7906
|
throw new SeqFloorUnavailable("server sent no committed floor for a resumed turn");
|
|
7764
7907
|
}
|
|
7765
7908
|
const floor = Math.max(serverFloor, outboxFloor);
|
|
7766
|
-
ctx.log.
|
|
7909
|
+
ctx.log.debug(
|
|
7767
7910
|
{ turnId: ctx.turnId, floor, outboxFloor, serverFloor },
|
|
7768
7911
|
"companion: resumed turn \u2014 seq counter seeded above its committed high-water mark"
|
|
7769
7912
|
);
|
|
@@ -7773,7 +7916,7 @@ function readOutboxFloor(read, turnId, log) {
|
|
|
7773
7916
|
try {
|
|
7774
7917
|
return read(turnId);
|
|
7775
7918
|
} catch (err) {
|
|
7776
|
-
log.
|
|
7919
|
+
log.debug(
|
|
7777
7920
|
{ turnId, err: err instanceof Error ? err.message : String(err) },
|
|
7778
7921
|
"companion: the on-disk outbox floor is unreadable; refusing to resume"
|
|
7779
7922
|
);
|
|
@@ -8015,7 +8158,10 @@ var TurnCommitter = class {
|
|
|
8015
8158
|
constructor(deps) {
|
|
8016
8159
|
this.deps = deps;
|
|
8017
8160
|
this.onError = (err, hook) => {
|
|
8018
|
-
deps.log.warn(
|
|
8161
|
+
deps.log.warn(
|
|
8162
|
+
{ ...apiErrorLogFields(err), hook },
|
|
8163
|
+
`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`
|
|
8164
|
+
);
|
|
8019
8165
|
deps.onCommitFailed?.(err);
|
|
8020
8166
|
};
|
|
8021
8167
|
const commit = {
|
|
@@ -8150,8 +8296,8 @@ async function postIncompleteNotice(ctx, harness, resolution) {
|
|
|
8150
8296
|
return true;
|
|
8151
8297
|
} catch (err) {
|
|
8152
8298
|
ctx.log.warn(
|
|
8153
|
-
{
|
|
8154
|
-
"
|
|
8299
|
+
{ ...apiErrorLogFields(err) },
|
|
8300
|
+
"Couldn't save the explanation to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
8155
8301
|
);
|
|
8156
8302
|
return false;
|
|
8157
8303
|
}
|
|
@@ -8161,7 +8307,7 @@ async function checkBundledBinary(ctx) {
|
|
|
8161
8307
|
if (!harness) return null;
|
|
8162
8308
|
const resolution = resolve(ctx, harness);
|
|
8163
8309
|
if (resolution.status === "present") return null;
|
|
8164
|
-
ctx.log.
|
|
8310
|
+
ctx.log.debug(
|
|
8165
8311
|
{
|
|
8166
8312
|
runtime: harness,
|
|
8167
8313
|
status: resolution.status,
|
|
@@ -8181,7 +8327,7 @@ async function reclassifyThrow(ctx, opts) {
|
|
|
8181
8327
|
if (!harness || opts.aborted) return null;
|
|
8182
8328
|
const resolution = resolve(ctx, harness);
|
|
8183
8329
|
if (resolution.status === "present") return null;
|
|
8184
|
-
ctx.log.
|
|
8330
|
+
ctx.log.debug(
|
|
8185
8331
|
{
|
|
8186
8332
|
runtime: harness,
|
|
8187
8333
|
status: resolution.status,
|
|
@@ -8378,13 +8524,13 @@ var TurnExecution = class {
|
|
|
8378
8524
|
);
|
|
8379
8525
|
} catch (err) {
|
|
8380
8526
|
turnLog.warn(
|
|
8381
|
-
{
|
|
8382
|
-
"
|
|
8527
|
+
{ ...apiErrorLogFields(err) },
|
|
8528
|
+
"Couldn't update the turn's status in Cabane. Check the conversation before trying again."
|
|
8383
8529
|
);
|
|
8384
8530
|
}
|
|
8385
8531
|
const durationMs = Date.now() - startedAt;
|
|
8386
8532
|
this.supervisor.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
8387
|
-
return { ok: false, durationMs, reason };
|
|
8533
|
+
return { ok: false, durationMs, reason, runtime: this.turnContext?.runtime };
|
|
8388
8534
|
}
|
|
8389
8535
|
// The post-admission analogue of `concludeBeforeRun` (Codo's stack review,
|
|
8390
8536
|
// blocking finding #2): this turn WAS admitted — it holds the conversation's
|
|
@@ -8414,12 +8560,12 @@ var TurnExecution = class {
|
|
|
8414
8560
|
} catch (err) {
|
|
8415
8561
|
turnLog.warn(
|
|
8416
8562
|
{ ...apiErrorLogFields(err), turnId },
|
|
8417
|
-
"
|
|
8563
|
+
"Couldn't save the turn's result to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
8418
8564
|
);
|
|
8419
8565
|
}
|
|
8420
8566
|
const durationMs = Date.now() - startedAt;
|
|
8421
8567
|
this.supervisor.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
8422
|
-
return { ok: false, durationMs, reason };
|
|
8568
|
+
return { ok: false, durationMs, reason, runtime: this.turnContext?.runtime };
|
|
8423
8569
|
}
|
|
8424
8570
|
async fetchContext() {
|
|
8425
8571
|
const { payload, turnId, turnLog } = this;
|
|
@@ -8436,20 +8582,20 @@ var TurnExecution = class {
|
|
|
8436
8582
|
} catch (err) {
|
|
8437
8583
|
const status = err instanceof ApiError ? err.status : 0;
|
|
8438
8584
|
if (status === 404) {
|
|
8439
|
-
turnLog.
|
|
8585
|
+
turnLog.debug(
|
|
8440
8586
|
{ err: err instanceof Error ? err.message : String(err) },
|
|
8441
8587
|
"dispatcher: turn-context 404 (stale event \u2014 conversation/message/participant gone); skipping"
|
|
8442
8588
|
);
|
|
8443
8589
|
throw this.concluded("turn_context_not_found");
|
|
8444
8590
|
}
|
|
8445
8591
|
if (status === 409 && apiErrorCode(err) === "dispatch_not_admitted") {
|
|
8446
|
-
turnLog.
|
|
8592
|
+
turnLog.debug(
|
|
8447
8593
|
"dispatcher: turn-context 409 dispatch_not_admitted (wake already resolved); skipping"
|
|
8448
8594
|
);
|
|
8449
8595
|
throw this.concluded("turn_context_not_admitted");
|
|
8450
8596
|
}
|
|
8451
8597
|
const reason = err instanceof Error ? err.message : String(err);
|
|
8452
|
-
turnLog.
|
|
8598
|
+
turnLog.debug({ err: reason }, "dispatcher: failed to fetch turn context");
|
|
8453
8599
|
const fetchReason = `fetch_failed: ${reason}`;
|
|
8454
8600
|
throw this.concluded(fetchReason, fetchReason);
|
|
8455
8601
|
}
|
|
@@ -8459,7 +8605,7 @@ var TurnExecution = class {
|
|
|
8459
8605
|
this.resumedFromSeq = resolveSeqFloor(sources, { turnId, log: turnLog });
|
|
8460
8606
|
this.seqCounter = Math.max(this.seqCounter, this.resumedFromSeq);
|
|
8461
8607
|
this.resumedSpoke = turnContext.turnSpoke === true || outboxSpoke;
|
|
8462
|
-
if (this.resumedSpoke) turnLog.
|
|
8608
|
+
if (this.resumedSpoke) turnLog.debug({ turnId }, "dispatcher: resumed span already spoke");
|
|
8463
8609
|
}
|
|
8464
8610
|
}
|
|
8465
8611
|
gateTrigger() {
|
|
@@ -8467,7 +8613,7 @@ var TurnExecution = class {
|
|
|
8467
8613
|
const message = this.turnContext.message;
|
|
8468
8614
|
const isDispatchableTrigger = message.role === "user" || message.role === "agent" || message.role === "system";
|
|
8469
8615
|
if (!isDispatchableTrigger) {
|
|
8470
|
-
turnLog.
|
|
8616
|
+
turnLog.debug({ role: message.role }, "dispatcher: trigger role not dispatchable \u2014 skipping");
|
|
8471
8617
|
throw this.concluded("unexpected_role", UNEXPECTED_ROLE_REASON);
|
|
8472
8618
|
}
|
|
8473
8619
|
this.supervisor.notifyStart({
|
|
@@ -8478,7 +8624,9 @@ var TurnExecution = class {
|
|
|
8478
8624
|
}
|
|
8479
8625
|
async resolveSecrets() {
|
|
8480
8626
|
const { payload, workspaceId, turnLog } = this;
|
|
8481
|
-
const secretStore = this.opts.secretStore ?? loadSecretStoreTolerant(
|
|
8627
|
+
const secretStore = this.opts.secretStore ?? loadSecretStoreTolerant(
|
|
8628
|
+
(m) => turnLog.warn({ err: m }, "Could not read the secrets file. Check ~/.cabane/secrets.json.")
|
|
8629
|
+
);
|
|
8482
8630
|
const { servers: resolvedMcpServers, missing } = resolveMcpSecrets(
|
|
8483
8631
|
// CT262: the user MCP DEFINITIONS (placeholder form) come from the turn
|
|
8484
8632
|
// context now, not a separate `getAgentSelf` run-config fetch.
|
|
@@ -8486,7 +8634,7 @@ var TurnExecution = class {
|
|
|
8486
8634
|
secretStore
|
|
8487
8635
|
);
|
|
8488
8636
|
if (missing.length > 0) {
|
|
8489
|
-
turnLog.
|
|
8637
|
+
turnLog.debug({ missing }, "dispatcher: turn needs secrets not declared on this companion");
|
|
8490
8638
|
const reason = missingSecretReason(missing);
|
|
8491
8639
|
throw this.concluded(reason, reason);
|
|
8492
8640
|
}
|
|
@@ -8501,7 +8649,7 @@ var TurnExecution = class {
|
|
|
8501
8649
|
if (effectiveCwd && !existsSync10(effectiveCwd)) {
|
|
8502
8650
|
turnLog.warn(
|
|
8503
8651
|
{ cwd: effectiveCwd },
|
|
8504
|
-
"
|
|
8652
|
+
"The working directory is missing; using the companion's directory instead. Check this agent's working directory setting."
|
|
8505
8653
|
);
|
|
8506
8654
|
effectiveCwd = void 0;
|
|
8507
8655
|
}
|
|
@@ -8509,7 +8657,7 @@ var TurnExecution = class {
|
|
|
8509
8657
|
if (prepareHook) {
|
|
8510
8658
|
let cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
|
|
8511
8659
|
if (cached2 && !checkoutState(cached2.cwd).ok) {
|
|
8512
|
-
turnLog.
|
|
8660
|
+
turnLog.debug(
|
|
8513
8661
|
{ cwd: cached2.cwd, reason: checkoutState(cached2.cwd).reason },
|
|
8514
8662
|
"dispatcher: the prepared checkout is no longer usable \u2014 re-running the prepare hook to restore it"
|
|
8515
8663
|
);
|
|
@@ -8536,7 +8684,7 @@ var TurnExecution = class {
|
|
|
8536
8684
|
});
|
|
8537
8685
|
} catch (err) {
|
|
8538
8686
|
const reason = err instanceof Error ? err.message : String(err);
|
|
8539
|
-
turnLog.
|
|
8687
|
+
turnLog.debug({ err: reason }, "dispatcher: prepare hook rejected a prepared turn");
|
|
8540
8688
|
const failReason = `prepare_failed: ${reason}`;
|
|
8541
8689
|
throw this.concluded(failReason, failReason);
|
|
8542
8690
|
}
|
|
@@ -8556,7 +8704,7 @@ var TurnExecution = class {
|
|
|
8556
8704
|
phase,
|
|
8557
8705
|
seq
|
|
8558
8706
|
}).catch((err) => {
|
|
8559
|
-
turnLog.
|
|
8707
|
+
turnLog.debug(
|
|
8560
8708
|
{ err: err instanceof Error ? err.message : String(err), phase },
|
|
8561
8709
|
"dispatcher: preparing-activity report failed (continuing with the hook)"
|
|
8562
8710
|
);
|
|
@@ -8596,7 +8744,7 @@ var TurnExecution = class {
|
|
|
8596
8744
|
clearTimeout(preparingTimer);
|
|
8597
8745
|
if (preparingStarted) reportPreparing("error");
|
|
8598
8746
|
const reason = err instanceof Error ? err.message : String(err);
|
|
8599
|
-
turnLog.
|
|
8747
|
+
turnLog.debug({ err: reason }, "dispatcher: prepare hook failed");
|
|
8600
8748
|
const failReason = `prepare_failed: ${reason}`;
|
|
8601
8749
|
throw this.concluded(failReason, failReason);
|
|
8602
8750
|
}
|
|
@@ -8644,14 +8792,14 @@ var TurnExecution = class {
|
|
|
8644
8792
|
} catch (err) {
|
|
8645
8793
|
const refusal = leaseRefusal(err);
|
|
8646
8794
|
if (refusal === "dispatch_not_admitted" || refusal === "turn_already_ended") {
|
|
8647
|
-
turnLog.
|
|
8795
|
+
turnLog.debug(
|
|
8648
8796
|
{ refusal, turnId },
|
|
8649
8797
|
"dispatcher: refused a turn lease; not running the model"
|
|
8650
8798
|
);
|
|
8651
8799
|
throw this.concluded(`lease_refused: ${refusal}`);
|
|
8652
8800
|
}
|
|
8653
8801
|
const reason = refusal ? `lease_refused: ${refusal}` : "lease_unconfirmed";
|
|
8654
|
-
turnLog.
|
|
8802
|
+
turnLog.debug(
|
|
8655
8803
|
{ refusal, turnId, err: err instanceof Error ? err.message : String(err) },
|
|
8656
8804
|
"dispatcher: turn lease not confirmed; not running the model"
|
|
8657
8805
|
);
|
|
@@ -8703,7 +8851,7 @@ var TurnExecution = class {
|
|
|
8703
8851
|
async selectAdapter() {
|
|
8704
8852
|
const { payload, workspaceId, turnId, turnLog } = this;
|
|
8705
8853
|
await guardBundledBinary(this.integrityContext(), (r, e) => this.concluded(r, e), this.outcome);
|
|
8706
|
-
const onWarn = (msg, meta) => turnLog.
|
|
8854
|
+
const onWarn = (msg, meta) => turnLog.debug(meta ?? {}, msg);
|
|
8707
8855
|
const adapters = [];
|
|
8708
8856
|
if (this.opts.claudeCodeAvailable?.() ?? true) {
|
|
8709
8857
|
adapters.push(createClaudeCodeAdapter({ queryFn: this.opts.queryFn, onWarn }));
|
|
@@ -8725,7 +8873,7 @@ var TurnExecution = class {
|
|
|
8725
8873
|
this.adapter = selectAdapter(registry, this.turnContext.runtime);
|
|
8726
8874
|
} catch (err) {
|
|
8727
8875
|
if (!(err instanceof RuntimeUnavailableError)) throw err;
|
|
8728
|
-
turnLog.
|
|
8876
|
+
turnLog.debug(
|
|
8729
8877
|
{ runtime: err.runtime, available: err.available },
|
|
8730
8878
|
"dispatcher: turn runtime not available on this device"
|
|
8731
8879
|
);
|
|
@@ -8738,8 +8886,8 @@ var TurnExecution = class {
|
|
|
8738
8886
|
});
|
|
8739
8887
|
} catch (postErr) {
|
|
8740
8888
|
turnLog.warn(
|
|
8741
|
-
{
|
|
8742
|
-
"
|
|
8889
|
+
{ ...apiErrorLogFields(postErr) },
|
|
8890
|
+
"Couldn't save the explanation to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
8743
8891
|
);
|
|
8744
8892
|
const reason = `runtime_unavailable:${err.runtime}`;
|
|
8745
8893
|
throw this.concluded(reason, reason);
|
|
@@ -8767,7 +8915,7 @@ var TurnExecution = class {
|
|
|
8767
8915
|
false,
|
|
8768
8916
|
true
|
|
8769
8917
|
);
|
|
8770
|
-
turnLog.
|
|
8918
|
+
turnLog.debug(
|
|
8771
8919
|
{ reason },
|
|
8772
8920
|
"dispatcher: stored session not resumed \u2014 the turn context was recomposed for a fresh session"
|
|
8773
8921
|
);
|
|
@@ -8778,8 +8926,8 @@ var TurnExecution = class {
|
|
|
8778
8926
|
};
|
|
8779
8927
|
} catch (err) {
|
|
8780
8928
|
turnLog.error(
|
|
8781
|
-
{ reason,
|
|
8782
|
-
"
|
|
8929
|
+
{ reason, ...apiErrorLogFields(err) },
|
|
8930
|
+
"Couldn't load the full conversation; continuing with the recent messages."
|
|
8783
8931
|
);
|
|
8784
8932
|
return null;
|
|
8785
8933
|
}
|
|
@@ -8800,7 +8948,10 @@ var TurnExecution = class {
|
|
|
8800
8948
|
dispatchId: this.dispatchId,
|
|
8801
8949
|
message: this.turnContext.message.body
|
|
8802
8950
|
},
|
|
8803
|
-
(m) => turnLog.warn(
|
|
8951
|
+
(m) => turnLog.warn(
|
|
8952
|
+
{ err: m },
|
|
8953
|
+
"Could not save the transcript. Check free disk space and access to ~/.cabane."
|
|
8954
|
+
)
|
|
8804
8955
|
) : null;
|
|
8805
8956
|
const turnRuntime = this.turnContext.runtime;
|
|
8806
8957
|
const committer = this.committer = new TurnCommitter({
|
|
@@ -8840,8 +8991,8 @@ var TurnExecution = class {
|
|
|
8840
8991
|
}
|
|
8841
8992
|
} catch (err) {
|
|
8842
8993
|
turnLog.warn(
|
|
8843
|
-
{
|
|
8844
|
-
"
|
|
8994
|
+
{ ...apiErrorLogFields(err) },
|
|
8995
|
+
"Couldn't check whether the agent chose to pass; the turn may appear to have failed."
|
|
8845
8996
|
);
|
|
8846
8997
|
}
|
|
8847
8998
|
};
|
|
@@ -8849,7 +9000,7 @@ var TurnExecution = class {
|
|
|
8849
9000
|
const fireTimeout = (reason) => {
|
|
8850
9001
|
if (abortController.signal.aborted) return;
|
|
8851
9002
|
o.timeoutReason = reason;
|
|
8852
|
-
turnLog.
|
|
9003
|
+
turnLog.debug(
|
|
8853
9004
|
{ reason, idleTimeoutMs, totalTimeoutMs },
|
|
8854
9005
|
"dispatcher: turn timeout \u2014 aborting"
|
|
8855
9006
|
);
|
|
@@ -8879,7 +9030,7 @@ var TurnExecution = class {
|
|
|
8879
9030
|
if (state !== "ended") return;
|
|
8880
9031
|
if (abortController.signal.aborted) return;
|
|
8881
9032
|
o.leaseLost = true;
|
|
8882
|
-
turnLog.
|
|
9033
|
+
turnLog.debug(
|
|
8883
9034
|
{ turnId, trigger },
|
|
8884
9035
|
"dispatcher: this turn is no longer running server-side \u2014 aborting the loop"
|
|
8885
9036
|
);
|
|
@@ -8907,7 +9058,7 @@ var TurnExecution = class {
|
|
|
8907
9058
|
if (isContentBearingEvent(event)) o.contentBearingEvents += 1;
|
|
8908
9059
|
armIdle();
|
|
8909
9060
|
if (abortController.signal.aborted) {
|
|
8910
|
-
turnLog.
|
|
9061
|
+
turnLog.debug("dispatcher: aborted mid-turn");
|
|
8911
9062
|
o.okResult = false;
|
|
8912
9063
|
o.resultReason = o.timeoutReason ?? "cancelled";
|
|
8913
9064
|
break;
|
|
@@ -8930,18 +9081,18 @@ var TurnExecution = class {
|
|
|
8930
9081
|
o.sessionWriteRejected = true;
|
|
8931
9082
|
turnLog.error(
|
|
8932
9083
|
{
|
|
8933
|
-
|
|
9084
|
+
...apiErrorLogFields(err),
|
|
8934
9085
|
status,
|
|
8935
9086
|
responseBody: describeErrorBody(err instanceof ApiError ? err.body : void 0),
|
|
8936
9087
|
stateLength: event.state.length,
|
|
8937
9088
|
runtime: turnRuntime
|
|
8938
9089
|
},
|
|
8939
|
-
"
|
|
9090
|
+
"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"
|
|
8940
9091
|
);
|
|
8941
9092
|
} else {
|
|
8942
9093
|
turnLog.warn(
|
|
8943
|
-
{
|
|
8944
|
-
"
|
|
9094
|
+
{ ...apiErrorLogFields(err) },
|
|
9095
|
+
"Couldn't save the agent's memory; trying again next turn."
|
|
8945
9096
|
);
|
|
8946
9097
|
}
|
|
8947
9098
|
}
|
|
@@ -8982,7 +9133,7 @@ var TurnExecution = class {
|
|
|
8982
9133
|
await this.applyRecordedTurnControlIntent();
|
|
8983
9134
|
}
|
|
8984
9135
|
if (!abortController.signal.aborted && this.skipState.skipped) {
|
|
8985
|
-
turnLog.
|
|
9136
|
+
turnLog.debug(
|
|
8986
9137
|
{ reason: this.skipState.reason, turnId, ok: o.okResult },
|
|
8987
9138
|
"agent skipped turn (skip_turn)"
|
|
8988
9139
|
);
|
|
@@ -8990,7 +9141,7 @@ var TurnExecution = class {
|
|
|
8990
9141
|
} catch (err) {
|
|
8991
9142
|
o.okResult = false;
|
|
8992
9143
|
o.resultReason = err instanceof Error ? err.message : String(err);
|
|
8993
|
-
turnLog.
|
|
9144
|
+
turnLog.debug({ err: o.resultReason }, "dispatcher: SDK query threw");
|
|
8994
9145
|
o.runtimeIncomplete = await absorbBundledLoss(
|
|
8995
9146
|
this.integrityContext(),
|
|
8996
9147
|
abortController.signal.aborted,
|
|
@@ -9015,7 +9166,7 @@ var TurnExecution = class {
|
|
|
9015
9166
|
kind: "usage_capped",
|
|
9016
9167
|
resetsAt: health.limitedUntil
|
|
9017
9168
|
});
|
|
9018
|
-
turnLog.
|
|
9169
|
+
turnLog.debug(
|
|
9019
9170
|
{ runtime: turnRuntime, limitedUntil: health.limitedUntil },
|
|
9020
9171
|
"dispatcher: idle-timeout on a capped runtime \u2014 reclassified as usage_capped (CT639)"
|
|
9021
9172
|
);
|
|
@@ -9066,7 +9217,7 @@ var TurnExecution = class {
|
|
|
9066
9217
|
if (diagnosticReason && !["usage_capped", "rate_limited", "auth_expired", "cancelled", "skipped"].includes(
|
|
9067
9218
|
diagnosticReason.kind
|
|
9068
9219
|
)) {
|
|
9069
|
-
turnLog.
|
|
9220
|
+
turnLog.debug(
|
|
9070
9221
|
{
|
|
9071
9222
|
workspaceId,
|
|
9072
9223
|
turnId,
|
|
@@ -9094,8 +9245,8 @@ var TurnExecution = class {
|
|
|
9094
9245
|
);
|
|
9095
9246
|
} catch (err) {
|
|
9096
9247
|
turnLog.warn(
|
|
9097
|
-
{
|
|
9098
|
-
"
|
|
9248
|
+
{ ...apiErrorLogFields(err) },
|
|
9249
|
+
"Couldn't save the turn's result to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
9099
9250
|
);
|
|
9100
9251
|
}
|
|
9101
9252
|
this.supervisor.releaseAbort(turnId, abortController);
|
|
@@ -9112,9 +9263,6 @@ var TurnExecution = class {
|
|
|
9112
9263
|
...o.resultReason ? { reason: o.resultReason } : {},
|
|
9113
9264
|
durationMs
|
|
9114
9265
|
});
|
|
9115
|
-
if (!o.okResult && o.resultReason !== "cancelled") {
|
|
9116
|
-
turnLog.info(`turn failed \u2014 full this.transcript: ${this.transcript.path}`);
|
|
9117
|
-
}
|
|
9118
9266
|
if (o.settledDiagnostics?.resultReason?.kind === "empty_result" || o.settledDiagnostics?.resultReason?.kind === "empty_result_unverified") {
|
|
9119
9267
|
this.transcript.preserveAnomaly();
|
|
9120
9268
|
}
|
|
@@ -9139,7 +9287,9 @@ var TurnExecution = class {
|
|
|
9139
9287
|
return {
|
|
9140
9288
|
ok: false,
|
|
9141
9289
|
durationMs,
|
|
9142
|
-
...o.resultReason ? { reason: o.resultReason } : {}
|
|
9290
|
+
...o.resultReason ? { reason: o.resultReason } : {},
|
|
9291
|
+
runtime: this.turnContext.runtime,
|
|
9292
|
+
...this.transcript ? { transcriptPath: tildePath(this.transcript.path) } : {}
|
|
9143
9293
|
};
|
|
9144
9294
|
}
|
|
9145
9295
|
};
|
|
@@ -9345,7 +9495,7 @@ var Outbox = class {
|
|
|
9345
9495
|
}
|
|
9346
9496
|
this.log?.warn(
|
|
9347
9497
|
{ workspaceId: this.workspaceId, err: err instanceof Error ? err.message : String(err) },
|
|
9348
|
-
"
|
|
9498
|
+
"Couldn't save an update on this machine. Check free disk space and access to ~/.cabane."
|
|
9349
9499
|
);
|
|
9350
9500
|
return;
|
|
9351
9501
|
}
|
|
@@ -9433,7 +9583,7 @@ var Outbox = class {
|
|
|
9433
9583
|
dropCorrupt(full) {
|
|
9434
9584
|
this.log?.warn(
|
|
9435
9585
|
{ workspaceId: this.workspaceId, file: full },
|
|
9436
|
-
"
|
|
9586
|
+
"A saved update is unreadable and couldn't be delivered. Check the conversation for missing replies."
|
|
9437
9587
|
);
|
|
9438
9588
|
try {
|
|
9439
9589
|
rmSync7(full, { force: true });
|
|
@@ -9450,7 +9600,7 @@ var Outbox = class {
|
|
|
9450
9600
|
if (now - e.enqueuedAt > MAX_AGE_MS) {
|
|
9451
9601
|
this.log?.warn(
|
|
9452
9602
|
{ workspaceId: this.workspaceId, turnId: e.turnId, seq: e.seq, kind: e.kind },
|
|
9453
|
-
"
|
|
9603
|
+
"A saved update waited too long and couldn't be delivered. Check the conversation for missing replies."
|
|
9454
9604
|
);
|
|
9455
9605
|
this.remove(e.turnId, e.seq);
|
|
9456
9606
|
} else {
|
|
@@ -9462,7 +9612,7 @@ var Outbox = class {
|
|
|
9462
9612
|
for (const e of survivors.slice(0, overflow)) {
|
|
9463
9613
|
this.log?.warn(
|
|
9464
9614
|
{ workspaceId: this.workspaceId, turnId: e.turnId, seq: e.seq, kind: e.kind },
|
|
9465
|
-
"
|
|
9615
|
+
"Too many updates are waiting; the oldest couldn't be kept. Check the connection to Cabane."
|
|
9466
9616
|
);
|
|
9467
9617
|
this.remove(e.turnId, e.seq);
|
|
9468
9618
|
}
|
|
@@ -9548,11 +9698,14 @@ var SseSubscriber = class {
|
|
|
9548
9698
|
try {
|
|
9549
9699
|
await this.connect();
|
|
9550
9700
|
backoff = 500;
|
|
9551
|
-
if (!this.aborted)
|
|
9701
|
+
if (!this.aborted) {
|
|
9702
|
+
this.opts.log.warn("Lost the connection to Cabane; reconnecting");
|
|
9703
|
+
await sleep2(backoff);
|
|
9704
|
+
}
|
|
9552
9705
|
} catch (err) {
|
|
9553
9706
|
if (this.aborted) return;
|
|
9554
9707
|
if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
|
|
9555
|
-
this.opts.log.
|
|
9708
|
+
this.opts.log.debug(
|
|
9556
9709
|
{ workspaceId: this.opts.workspaceId, status: err.status },
|
|
9557
9710
|
"SSE auth failed \u2014 tearing down this workspace subscriber"
|
|
9558
9711
|
);
|
|
@@ -9565,7 +9718,7 @@ var SseSubscriber = class {
|
|
|
9565
9718
|
err: err instanceof Error ? err.message : String(err),
|
|
9566
9719
|
backoff
|
|
9567
9720
|
},
|
|
9568
|
-
"
|
|
9721
|
+
"Lost the connection to Cabane; reconnecting"
|
|
9569
9722
|
);
|
|
9570
9723
|
await sleep2(backoff);
|
|
9571
9724
|
backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
|
|
@@ -9589,7 +9742,7 @@ var SseSubscriber = class {
|
|
|
9589
9742
|
if (!res.body) {
|
|
9590
9743
|
throw new Error("SSE response has no body");
|
|
9591
9744
|
}
|
|
9592
|
-
this.opts.log.
|
|
9745
|
+
this.opts.log.debug({ workspaceId: this.opts.workspaceId }, "SSE connected");
|
|
9593
9746
|
let opened = true;
|
|
9594
9747
|
this.opts.onOpen?.();
|
|
9595
9748
|
const parser = createParser({
|
|
@@ -9719,15 +9872,12 @@ var CompanionSupervisor = class {
|
|
|
9719
9872
|
async start() {
|
|
9720
9873
|
this.log.info(
|
|
9721
9874
|
{ protocolVersion: TURN_PROTOCOL_VERSION, version: COMPANION_VERSION },
|
|
9722
|
-
|
|
9875
|
+
`Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ?? hostname()}`
|
|
9723
9876
|
);
|
|
9724
9877
|
const startupHarnessRefresh = this.refreshHarnessStatuses();
|
|
9725
9878
|
this.trackHeartbeat(startupHarnessRefresh);
|
|
9726
9879
|
if (!this.config.deviceToken) {
|
|
9727
|
-
this.log.warn("
|
|
9728
|
-
process.stdout.write(
|
|
9729
|
-
"companion: this device is not paired \u2014 run `cabane-companion pair`, then confirm the short code in Settings \u2192 Connectors.\n"
|
|
9730
|
-
);
|
|
9880
|
+
this.log.warn("This device isn't paired. Run cabane-companion start to pair it.");
|
|
9731
9881
|
return;
|
|
9732
9882
|
}
|
|
9733
9883
|
this.deviceApi = new DeviceApi({
|
|
@@ -9741,6 +9891,7 @@ var CompanionSupervisor = class {
|
|
|
9741
9891
|
token: this.config.deviceToken,
|
|
9742
9892
|
log: this.log,
|
|
9743
9893
|
lastEventId: null,
|
|
9894
|
+
onOpen: () => this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ?? hostname()}`),
|
|
9744
9895
|
onMessage: async (ev) => {
|
|
9745
9896
|
if (ev.event === "assignments_changed") {
|
|
9746
9897
|
if (this.refreshing && this.inFlightRefresh) await this.inFlightRefresh;
|
|
@@ -9748,11 +9899,13 @@ var CompanionSupervisor = class {
|
|
|
9748
9899
|
}
|
|
9749
9900
|
},
|
|
9750
9901
|
onAuthFailure: () => {
|
|
9751
|
-
this.log.error(
|
|
9902
|
+
this.log.error(
|
|
9903
|
+
"Cabane no longer accepts this device. Run cabane-companion logout, then cabane-companion start to pair again."
|
|
9904
|
+
);
|
|
9752
9905
|
}
|
|
9753
9906
|
});
|
|
9754
|
-
this.deviceSub.start();
|
|
9755
9907
|
await this.refreshAssignments();
|
|
9908
|
+
this.deviceSub.start();
|
|
9756
9909
|
const firstHeartbeat = startupHarnessRefresh.then(async () => {
|
|
9757
9910
|
if (this.stopped) return;
|
|
9758
9911
|
await this.sendHeartbeat();
|
|
@@ -9788,7 +9941,9 @@ var CompanionSupervisor = class {
|
|
|
9788
9941
|
if (!this.deviceApi) return;
|
|
9789
9942
|
await this.refreshHarnessStatuses();
|
|
9790
9943
|
try {
|
|
9791
|
-
const store = loadSecretStoreTolerant(
|
|
9944
|
+
const store = loadSecretStoreTolerant(
|
|
9945
|
+
(m) => this.log.warn({ err: m }, "Could not read the secrets file. Check ~/.cabane/secrets.json.")
|
|
9946
|
+
);
|
|
9792
9947
|
const connectorReports = this.connectorHealth.reports();
|
|
9793
9948
|
const opencodeModels = this.config.opencode?.serverUrl ? await enumerateOpencodeModels(this.config.opencode.serverUrl) : null;
|
|
9794
9949
|
const res = await this.deviceApi.heartbeat({
|
|
@@ -9835,8 +9990,8 @@ var CompanionSupervisor = class {
|
|
|
9835
9990
|
void this.recoverRuns();
|
|
9836
9991
|
} catch (err) {
|
|
9837
9992
|
this.log.warn(
|
|
9838
|
-
{
|
|
9839
|
-
"
|
|
9993
|
+
{ ...apiErrorLogFields(err) },
|
|
9994
|
+
"Couldn't check in with Cabane; trying again shortly."
|
|
9840
9995
|
);
|
|
9841
9996
|
}
|
|
9842
9997
|
}
|
|
@@ -9851,7 +10006,7 @@ var CompanionSupervisor = class {
|
|
|
9851
10006
|
this.versionSkewWarned = true;
|
|
9852
10007
|
this.log.warn(
|
|
9853
10008
|
{ companionVersion: COMPANION_VERSION, serverVersion },
|
|
9854
|
-
"
|
|
10009
|
+
"This companion and Cabane are on different versions. If turns fail, update with npm i -g @cabane/companion@latest"
|
|
9855
10010
|
);
|
|
9856
10011
|
}
|
|
9857
10012
|
// Pull assignments and reconcile the live runner set against them. Re-entrancy
|
|
@@ -9873,8 +10028,8 @@ var CompanionSupervisor = class {
|
|
|
9873
10028
|
device = resp.device;
|
|
9874
10029
|
} catch (err) {
|
|
9875
10030
|
this.log.error(
|
|
9876
|
-
{
|
|
9877
|
-
"
|
|
10031
|
+
{ ...apiErrorLogFields(err) },
|
|
10032
|
+
"Couldn't load the assigned agents. Check that this device is active in Cabane; trying again shortly."
|
|
9878
10033
|
);
|
|
9879
10034
|
this.hub.setDeviceError(err instanceof Error ? err.message : String(err));
|
|
9880
10035
|
return;
|
|
@@ -9940,14 +10095,17 @@ var CompanionSupervisor = class {
|
|
|
9940
10095
|
const credential = it.credential ?? getCredential(it.agentId);
|
|
9941
10096
|
const runConfig = parseRunConfig(
|
|
9942
10097
|
it.runConfig,
|
|
9943
|
-
(m) => this.log.warn(
|
|
10098
|
+
(m) => this.log.warn(
|
|
10099
|
+
{ agentId: it.agentId, agentName: it.agentDisplayName, err: m },
|
|
10100
|
+
"Some agent settings could not be read; using defaults. Check the agent settings in Cabane."
|
|
10101
|
+
)
|
|
9944
10102
|
);
|
|
9945
10103
|
const required = requiredSecretNames(runConfig.mcpServers);
|
|
9946
10104
|
const missing = required.filter((n) => !exposed.has(n));
|
|
9947
10105
|
if (!credential) {
|
|
9948
10106
|
this.log.error(
|
|
9949
10107
|
{ workspaceId, agentId: it.agentId, username: it.agentUsername },
|
|
9950
|
-
|
|
10108
|
+
`Cannot run ${it.agentDisplayName}: its sign-in to Cabane is missing. Reassign it to this device in Cabane.`
|
|
9951
10109
|
);
|
|
9952
10110
|
this.removeAgent(wr, it.agentId);
|
|
9953
10111
|
this.hub.setAgent(workspaceId, {
|
|
@@ -9981,19 +10139,20 @@ var CompanionSupervisor = class {
|
|
|
9981
10139
|
if (missing.length > 0) {
|
|
9982
10140
|
this.log.warn(
|
|
9983
10141
|
{ workspaceId, agentId: it.agentId, missing },
|
|
9984
|
-
|
|
10142
|
+
`${it.agentDisplayName} needs secrets this machine does not have: ${missing.join(", ")}. Add them to ~/.cabane/secrets.json.`
|
|
9985
10143
|
);
|
|
9986
10144
|
}
|
|
9987
10145
|
}
|
|
9988
10146
|
this.ensureWorkspaceSse(wr);
|
|
9989
10147
|
}
|
|
9990
10148
|
addAgent(wr, it, credential, runConfig) {
|
|
9991
|
-
const
|
|
10149
|
+
const agentLog = this.log.child({ agentName: it.agentDisplayName });
|
|
10150
|
+
const outbox = new Outbox(it.agentId, agentLog);
|
|
9992
10151
|
const api = new CabaneApi({
|
|
9993
10152
|
baseUrl: this.config.baseUrl,
|
|
9994
10153
|
token: credential,
|
|
9995
10154
|
outbox,
|
|
9996
|
-
log:
|
|
10155
|
+
log: agentLog
|
|
9997
10156
|
});
|
|
9998
10157
|
const aborts = /* @__PURE__ */ new Map();
|
|
9999
10158
|
const dispatcher = this.buildDispatcher({
|
|
@@ -10003,6 +10162,7 @@ var CompanionSupervisor = class {
|
|
|
10003
10162
|
workspaceSlug: it.workspaceSlug,
|
|
10004
10163
|
agentId: it.agentId,
|
|
10005
10164
|
agentUsername: it.agentUsername,
|
|
10165
|
+
agentDisplayName: it.agentDisplayName,
|
|
10006
10166
|
credential,
|
|
10007
10167
|
runConfig,
|
|
10008
10168
|
aborts
|
|
@@ -10023,7 +10183,7 @@ var CompanionSupervisor = class {
|
|
|
10023
10183
|
drain2.kick();
|
|
10024
10184
|
this.log.info(
|
|
10025
10185
|
{ workspaceId: it.workspaceId, agentId: it.agentId, username: it.agentUsername },
|
|
10026
|
-
|
|
10186
|
+
`Running ${it.agentDisplayName}`
|
|
10027
10187
|
);
|
|
10028
10188
|
}
|
|
10029
10189
|
removeAgent(wr, agentId) {
|
|
@@ -10034,7 +10194,7 @@ var CompanionSupervisor = class {
|
|
|
10034
10194
|
this.hub.removeAgent(wr.workspaceId, agentId);
|
|
10035
10195
|
this.log.info(
|
|
10036
10196
|
{ workspaceId: wr.workspaceId, agentId },
|
|
10037
|
-
|
|
10197
|
+
`Stopped running ${runner.displayName} (unassigned)`
|
|
10038
10198
|
);
|
|
10039
10199
|
}
|
|
10040
10200
|
// CT1379: can this machine LAUNCH the harness — is the SDK's own bundled
|
|
@@ -10107,7 +10267,11 @@ var CompanionSupervisor = class {
|
|
|
10107
10267
|
const meta = { runtime, platform: resolution.platform, arch: resolution.arch, status };
|
|
10108
10268
|
if (status === "present") {
|
|
10109
10269
|
const say = intended ? this.log.info : this.log.debug;
|
|
10110
|
-
say.call(
|
|
10270
|
+
say.call(
|
|
10271
|
+
this.log,
|
|
10272
|
+
meta,
|
|
10273
|
+
`${runtime === "codex" ? "Codex" : "Claude Code"} is available again`
|
|
10274
|
+
);
|
|
10111
10275
|
continue;
|
|
10112
10276
|
}
|
|
10113
10277
|
if (intended) this.log.warn(meta, startupWarning(resolution));
|
|
@@ -10153,7 +10317,7 @@ var CompanionSupervisor = class {
|
|
|
10153
10317
|
local,
|
|
10154
10318
|
aborts: ctx.aborts,
|
|
10155
10319
|
runConfig: ctx.runConfig,
|
|
10156
|
-
log: this.log,
|
|
10320
|
+
log: this.log.child({ agentName: ctx.agentDisplayName ?? ctx.agentUsername }),
|
|
10157
10321
|
// CT833: register the claude-code adapter only when this device actually
|
|
10158
10322
|
// offers claude-code — read per turn (not captured here), so a harness
|
|
10159
10323
|
// installed or connected after boot works on the next turn exactly as it
|
|
@@ -10211,7 +10375,7 @@ var CompanionSupervisor = class {
|
|
|
10211
10375
|
onMessage: (ev) => this.handleWorkspaceMessage(wr, ev),
|
|
10212
10376
|
onAuthFailure: (status) => {
|
|
10213
10377
|
this.hub.setAuthFailed(wr.workspaceId);
|
|
10214
|
-
this.log.
|
|
10378
|
+
this.log.debug(
|
|
10215
10379
|
{ workspaceId: wr.workspaceId, status, sseAgentId: wr.sseAgentId },
|
|
10216
10380
|
"companion: workspace stream auth failed \u2014 re-pulling assignments"
|
|
10217
10381
|
);
|
|
@@ -10220,7 +10384,7 @@ var CompanionSupervisor = class {
|
|
|
10220
10384
|
}
|
|
10221
10385
|
});
|
|
10222
10386
|
wr.sub.start();
|
|
10223
|
-
this.log.
|
|
10387
|
+
this.log.debug({ workspaceId: wr.workspaceId }, "companion: subscribed");
|
|
10224
10388
|
}
|
|
10225
10389
|
async removeWorkspace(workspaceId) {
|
|
10226
10390
|
const wr = this.workspaces.get(workspaceId);
|
|
@@ -10244,7 +10408,7 @@ var CompanionSupervisor = class {
|
|
|
10244
10408
|
try {
|
|
10245
10409
|
wire = JSON.parse(ev.data);
|
|
10246
10410
|
} catch (err) {
|
|
10247
|
-
this.log.
|
|
10411
|
+
this.log.debug(
|
|
10248
10412
|
{ err: err instanceof Error ? err.message : String(err) },
|
|
10249
10413
|
"malformed SSE payload"
|
|
10250
10414
|
);
|
|
@@ -10260,7 +10424,7 @@ var CompanionSupervisor = class {
|
|
|
10260
10424
|
if (agent2) {
|
|
10261
10425
|
const aborted = agent2.dispatcher.cancel(payload2.conversationId, payload2.agentId);
|
|
10262
10426
|
if (aborted) {
|
|
10263
|
-
this.log.
|
|
10427
|
+
this.log.debug(
|
|
10264
10428
|
{
|
|
10265
10429
|
workspaceId: wr.workspaceId,
|
|
10266
10430
|
conversationId: payload2.conversationId,
|
|
@@ -10314,9 +10478,9 @@ var CompanionSupervisor = class {
|
|
|
10314
10478
|
workspaceId: wr.workspaceId,
|
|
10315
10479
|
conversationId: payload.conversationId,
|
|
10316
10480
|
agentId: payload.agentId,
|
|
10317
|
-
|
|
10481
|
+
...apiErrorLogFields(err)
|
|
10318
10482
|
},
|
|
10319
|
-
"
|
|
10483
|
+
"The turn stopped unexpectedly. Reply in Cabane to try again."
|
|
10320
10484
|
);
|
|
10321
10485
|
});
|
|
10322
10486
|
wr.chains.set(chainKey, tail);
|
|
@@ -10349,7 +10513,7 @@ var CompanionSupervisor = class {
|
|
|
10349
10513
|
messageId: payload.messageId,
|
|
10350
10514
|
reason: "agent_not_on_device"
|
|
10351
10515
|
});
|
|
10352
|
-
this.log.
|
|
10516
|
+
this.log.debug(
|
|
10353
10517
|
{
|
|
10354
10518
|
workspaceId: wr.workspaceId,
|
|
10355
10519
|
conversationId: payload.conversationId,
|
|
@@ -10370,7 +10534,7 @@ var CompanionSupervisor = class {
|
|
|
10370
10534
|
agentId: payload.agentId,
|
|
10371
10535
|
err: lastErr instanceof Error ? lastErr.message : String(lastErr)
|
|
10372
10536
|
},
|
|
10373
|
-
"
|
|
10537
|
+
"Couldn't tell Cabane that this agent no longer runs here; trying again when the connection returns."
|
|
10374
10538
|
);
|
|
10375
10539
|
return false;
|
|
10376
10540
|
}
|
|
@@ -10425,7 +10589,7 @@ var CompanionSupervisor = class {
|
|
|
10425
10589
|
agent,
|
|
10426
10590
|
run.turnId
|
|
10427
10591
|
).catch(
|
|
10428
|
-
(err) => this.log.
|
|
10592
|
+
(err) => this.log.debug({ err, turnId: run.turnId }, "companion: restart recovery failed")
|
|
10429
10593
|
).finally(() => {
|
|
10430
10594
|
if (wr.chains.get(key) === tail) wr.chains.delete(key);
|
|
10431
10595
|
});
|
|
@@ -10436,7 +10600,7 @@ var CompanionSupervisor = class {
|
|
|
10436
10600
|
this.recoveryChecked = page.nextCursor === null;
|
|
10437
10601
|
}
|
|
10438
10602
|
} catch (err) {
|
|
10439
|
-
this.log.
|
|
10603
|
+
this.log.debug({ err }, "companion: run recovery read failed (will retry)");
|
|
10440
10604
|
} finally {
|
|
10441
10605
|
this.recovering = false;
|
|
10442
10606
|
}
|
|
@@ -10448,7 +10612,7 @@ var CompanionSupervisor = class {
|
|
|
10448
10612
|
const workspaceId = wr.workspaceId;
|
|
10449
10613
|
if (ev.id && hasDispatched(workspaceId, ev.id)) {
|
|
10450
10614
|
if (hasCompleted(workspaceId, ev.id)) {
|
|
10451
|
-
this.log.
|
|
10615
|
+
this.log.debug(
|
|
10452
10616
|
{ workspaceId, eventId: ev.id },
|
|
10453
10617
|
"companion: skipping already-completed event (resume after restart)"
|
|
10454
10618
|
);
|
|
@@ -10456,7 +10620,7 @@ var CompanionSupervisor = class {
|
|
|
10456
10620
|
return;
|
|
10457
10621
|
}
|
|
10458
10622
|
if (noResume()) {
|
|
10459
|
-
this.log.
|
|
10623
|
+
this.log.debug(
|
|
10460
10624
|
{ workspaceId, eventId: ev.id },
|
|
10461
10625
|
"companion: skipping interrupted turn (CABANE_COMPANION_NO_RESUME=1) \u2014 resume disabled for this boot"
|
|
10462
10626
|
);
|
|
@@ -10468,13 +10632,13 @@ var CompanionSupervisor = class {
|
|
|
10468
10632
|
if (attempt > MAX_RESUME_ATTEMPTS) {
|
|
10469
10633
|
this.log.error(
|
|
10470
10634
|
{ workspaceId, eventId: ev.id, attempt, cap: MAX_RESUME_ATTEMPTS },
|
|
10471
|
-
"
|
|
10635
|
+
"An interrupted turn couldn't finish after several attempts. Reply in Cabane to try again."
|
|
10472
10636
|
);
|
|
10473
10637
|
markCompleted(workspaceId, ev.id);
|
|
10474
10638
|
wr.cursor.settle(ev.id);
|
|
10475
10639
|
return;
|
|
10476
10640
|
}
|
|
10477
|
-
this.log.
|
|
10641
|
+
this.log.debug(
|
|
10478
10642
|
{ workspaceId, eventId: ev.id, attempt, cap: MAX_RESUME_ATTEMPTS },
|
|
10479
10643
|
"companion: re-dispatching interrupted turn (resume after restart)"
|
|
10480
10644
|
);
|
|
@@ -10484,7 +10648,7 @@ var CompanionSupervisor = class {
|
|
|
10484
10648
|
if (ev.id) {
|
|
10485
10649
|
const liveTurnIds = rememberTurnId(workspaceId, ev.id, turnId);
|
|
10486
10650
|
if (liveTurnIds > TURN_ID_OVERFLOW_WARN) {
|
|
10487
|
-
this.log.
|
|
10651
|
+
this.log.debug(
|
|
10488
10652
|
{ workspaceId, liveTurnIds, threshold: TURN_ID_OVERFLOW_WARN },
|
|
10489
10653
|
"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."
|
|
10490
10654
|
);
|
|
@@ -10492,11 +10656,18 @@ var CompanionSupervisor = class {
|
|
|
10492
10656
|
markDispatched(workspaceId, ev.id);
|
|
10493
10657
|
}
|
|
10494
10658
|
if (resumedTurnId) {
|
|
10495
|
-
this.log.
|
|
10659
|
+
this.log.debug(
|
|
10496
10660
|
{ workspaceId, eventId: ev.id, turnId },
|
|
10497
10661
|
"companion: resuming an interrupted turn under its original id"
|
|
10498
10662
|
);
|
|
10499
10663
|
}
|
|
10664
|
+
const turnBindings = {
|
|
10665
|
+
agentName: agent.displayName,
|
|
10666
|
+
conversationId: payload.conversationId,
|
|
10667
|
+
agentId: agent.agentId,
|
|
10668
|
+
workspaceId
|
|
10669
|
+
};
|
|
10670
|
+
this.log.info(turnBindings, "Turn started");
|
|
10500
10671
|
const result = await agent.dispatcher.handle(payload, {
|
|
10501
10672
|
turnId,
|
|
10502
10673
|
resumed: resumedTurnId !== null
|
|
@@ -10505,16 +10676,23 @@ var CompanionSupervisor = class {
|
|
|
10505
10676
|
if (ev.id) wr.cursor.settle(ev.id);
|
|
10506
10677
|
const durationS = (result.durationMs / 1e3).toFixed(1);
|
|
10507
10678
|
const bindings = {
|
|
10508
|
-
|
|
10509
|
-
|
|
10510
|
-
agentId: agent.agentId
|
|
10679
|
+
...turnBindings,
|
|
10680
|
+
...result.transcriptPath ? { transcriptPath: result.transcriptPath } : {}
|
|
10511
10681
|
};
|
|
10512
10682
|
if (result.ok) {
|
|
10513
|
-
this.log.info(bindings,
|
|
10683
|
+
this.log.info(bindings, `Replied (${durationS}s)`);
|
|
10684
|
+
} else if (result.reason === "cancelled") {
|
|
10685
|
+
this.log.info(turnBindings, `Turn stopped (${durationS}s)`);
|
|
10686
|
+
} else if ([
|
|
10687
|
+
"turn_context_not_found",
|
|
10688
|
+
"turn_context_not_admitted",
|
|
10689
|
+
"trigger_role_not_dispatchable"
|
|
10690
|
+
].includes(result.reason ?? "") || result.reason === "lease_refused: dispatch_not_admitted" || result.reason === "lease_refused: turn_already_ended") {
|
|
10691
|
+
this.log.debug({ ...turnBindings, reason: result.reason }, "Dispatch no longer needed");
|
|
10514
10692
|
} else {
|
|
10515
|
-
this.log.
|
|
10693
|
+
this.log.info(
|
|
10516
10694
|
bindings,
|
|
10517
|
-
|
|
10695
|
+
`Turn failed after ${durationS}s: ${turnFailureCopy(result.reason, result.runtime)}`
|
|
10518
10696
|
);
|
|
10519
10697
|
}
|
|
10520
10698
|
}
|
|
@@ -10534,7 +10712,7 @@ var CompanionSupervisor = class {
|
|
|
10534
10712
|
}
|
|
10535
10713
|
} catch (err) {
|
|
10536
10714
|
drainDelay = Math.min(drainDelay * 2, DRAIN_MAX_MS);
|
|
10537
|
-
this.log.
|
|
10715
|
+
this.log.debug(
|
|
10538
10716
|
{ agentId, err: err instanceof Error ? err.message : String(err) },
|
|
10539
10717
|
"companion: outbox drain pass threw (will retry with backoff)"
|
|
10540
10718
|
);
|
|
@@ -10569,7 +10747,7 @@ var CompanionSupervisor = class {
|
|
|
10569
10747
|
const next = loadConfig();
|
|
10570
10748
|
if (!next) return;
|
|
10571
10749
|
this.config = next;
|
|
10572
|
-
this.log
|
|
10750
|
+
configureLogger(this.log, next);
|
|
10573
10751
|
this.rebuildDispatchers();
|
|
10574
10752
|
void this.refreshAssignments();
|
|
10575
10753
|
}
|
|
@@ -10585,7 +10763,7 @@ var CompanionSupervisor = class {
|
|
|
10585
10763
|
};
|
|
10586
10764
|
this.config = next;
|
|
10587
10765
|
saveConfig(next);
|
|
10588
|
-
this.log
|
|
10766
|
+
configureLogger(this.log, next);
|
|
10589
10767
|
}
|
|
10590
10768
|
// ---- harnesses (CT586) ----
|
|
10591
10769
|
// Probe all three harnesses, cache the signals + versions, and push the derived
|
|
@@ -10611,7 +10789,7 @@ var CompanionSupervisor = class {
|
|
|
10611
10789
|
};
|
|
10612
10790
|
this.hub.setHarnesses(deriveHarnessSnapshot(signals).harnesses);
|
|
10613
10791
|
} catch (err) {
|
|
10614
|
-
this.log.
|
|
10792
|
+
this.log.debug(
|
|
10615
10793
|
{ err: err instanceof Error ? err.message : String(err) },
|
|
10616
10794
|
"companion: harness probe failed (will retry on next beat)"
|
|
10617
10795
|
);
|
|
@@ -10727,6 +10905,7 @@ var CompanionSupervisor = class {
|
|
|
10727
10905
|
workspaceSlug: wr.slug,
|
|
10728
10906
|
agentId: runner.agentId,
|
|
10729
10907
|
agentUsername: runner.username,
|
|
10908
|
+
agentDisplayName: runner.displayName,
|
|
10730
10909
|
credential: runner.credential,
|
|
10731
10910
|
runConfig: runner.runConfig,
|
|
10732
10911
|
aborts: runner.aborts
|
|
@@ -10832,7 +11011,7 @@ function handleUncaught(log, err, origin) {
|
|
|
10832
11011
|
const message = err instanceof Error ? err.message : String(err);
|
|
10833
11012
|
const code = errorCode(err);
|
|
10834
11013
|
if (isRecoverableSocketError(err)) {
|
|
10835
|
-
log.
|
|
11014
|
+
log.debug(
|
|
10836
11015
|
{ origin, code, err: message },
|
|
10837
11016
|
"companion: recovered from a socket error (kept running \u2014 a broken pipe never kills the companion)"
|
|
10838
11017
|
);
|
|
@@ -10840,7 +11019,7 @@ function handleUncaught(log, err, origin) {
|
|
|
10840
11019
|
}
|
|
10841
11020
|
log.error(
|
|
10842
11021
|
{ origin, code, err: message, stack: err instanceof Error ? err.stack : void 0 },
|
|
10843
|
-
"
|
|
11022
|
+
"An unexpected error occurred; the companion is still running."
|
|
10844
11023
|
);
|
|
10845
11024
|
}
|
|
10846
11025
|
|
|
@@ -10874,7 +11053,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
10874
11053
|
try {
|
|
10875
11054
|
({ cfg, claudeOnPath: claudeCode } = await requireStartConfig({
|
|
10876
11055
|
...opts.probeClaude ? { probeClaude: opts.probeClaude } : {},
|
|
10877
|
-
onMigrated: (migrated, onPath) => log.
|
|
11056
|
+
onMigrated: (migrated, onPath) => log.debug(
|
|
10878
11057
|
{ claudeCode: isClaudeCodeConnected(migrated) },
|
|
10879
11058
|
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)"
|
|
10880
11059
|
)
|
|
@@ -10895,7 +11074,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
10895
11074
|
});
|
|
10896
11075
|
throw err;
|
|
10897
11076
|
}
|
|
10898
|
-
|
|
11077
|
+
configureLogger(log, cfg);
|
|
10899
11078
|
const harnessVersions = await probeHarnessVersions({
|
|
10900
11079
|
// Connected AND its CLI answered — the two things that make a
|
|
10901
11080
|
// `claude --version` probe worth spawning. Not the offer predicate.
|