@cabane/companion 0.6.100 → 0.6.102
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -3
- package/dist/cli.js +738 -443
- package/dist/pairing-config.js +4 -1
- package/dist/runtime.js +596 -409
- package/package.json +1 -1
package/dist/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,42 +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
|
-
|
|
7740
7894
|
// src/turn-seq-floor.ts
|
|
7741
7895
|
var SeqFloorUnavailable = class extends Error {
|
|
7742
7896
|
constructor(detail) {
|
|
@@ -7752,7 +7906,7 @@ function resolveSeqFloor(sources, ctx) {
|
|
|
7752
7906
|
throw new SeqFloorUnavailable("server sent no committed floor for a resumed turn");
|
|
7753
7907
|
}
|
|
7754
7908
|
const floor = Math.max(serverFloor, outboxFloor);
|
|
7755
|
-
ctx.log.
|
|
7909
|
+
ctx.log.debug(
|
|
7756
7910
|
{ turnId: ctx.turnId, floor, outboxFloor, serverFloor },
|
|
7757
7911
|
"companion: resumed turn \u2014 seq counter seeded above its committed high-water mark"
|
|
7758
7912
|
);
|
|
@@ -7762,7 +7916,7 @@ function readOutboxFloor(read, turnId, log) {
|
|
|
7762
7916
|
try {
|
|
7763
7917
|
return read(turnId);
|
|
7764
7918
|
} catch (err) {
|
|
7765
|
-
log.
|
|
7919
|
+
log.debug(
|
|
7766
7920
|
{ turnId, err: err instanceof Error ? err.message : String(err) },
|
|
7767
7921
|
"companion: the on-disk outbox floor is unreadable; refusing to resume"
|
|
7768
7922
|
);
|
|
@@ -8005,8 +8159,8 @@ var TurnCommitter = class {
|
|
|
8005
8159
|
this.deps = deps;
|
|
8006
8160
|
this.onError = (err, hook) => {
|
|
8007
8161
|
deps.log.warn(
|
|
8008
|
-
{
|
|
8009
|
-
"
|
|
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`
|
|
8010
8164
|
);
|
|
8011
8165
|
deps.onCommitFailed?.(err);
|
|
8012
8166
|
};
|
|
@@ -8142,8 +8296,8 @@ async function postIncompleteNotice(ctx, harness, resolution) {
|
|
|
8142
8296
|
return true;
|
|
8143
8297
|
} catch (err) {
|
|
8144
8298
|
ctx.log.warn(
|
|
8145
|
-
{
|
|
8146
|
-
"
|
|
8299
|
+
{ ...apiErrorLogFields(err) },
|
|
8300
|
+
"Couldn't save the explanation to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
8147
8301
|
);
|
|
8148
8302
|
return false;
|
|
8149
8303
|
}
|
|
@@ -8153,7 +8307,7 @@ async function checkBundledBinary(ctx) {
|
|
|
8153
8307
|
if (!harness) return null;
|
|
8154
8308
|
const resolution = resolve(ctx, harness);
|
|
8155
8309
|
if (resolution.status === "present") return null;
|
|
8156
|
-
ctx.log.
|
|
8310
|
+
ctx.log.debug(
|
|
8157
8311
|
{
|
|
8158
8312
|
runtime: harness,
|
|
8159
8313
|
status: resolution.status,
|
|
@@ -8173,7 +8327,7 @@ async function reclassifyThrow(ctx, opts) {
|
|
|
8173
8327
|
if (!harness || opts.aborted) return null;
|
|
8174
8328
|
const resolution = resolve(ctx, harness);
|
|
8175
8329
|
if (resolution.status === "present") return null;
|
|
8176
|
-
ctx.log.
|
|
8330
|
+
ctx.log.debug(
|
|
8177
8331
|
{
|
|
8178
8332
|
runtime: harness,
|
|
8179
8333
|
status: resolution.status,
|
|
@@ -8370,13 +8524,13 @@ var TurnExecution = class {
|
|
|
8370
8524
|
);
|
|
8371
8525
|
} catch (err) {
|
|
8372
8526
|
turnLog.warn(
|
|
8373
|
-
{
|
|
8374
|
-
"
|
|
8527
|
+
{ ...apiErrorLogFields(err) },
|
|
8528
|
+
"Couldn't update the turn's status in Cabane. Check the conversation before trying again."
|
|
8375
8529
|
);
|
|
8376
8530
|
}
|
|
8377
8531
|
const durationMs = Date.now() - startedAt;
|
|
8378
8532
|
this.supervisor.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
8379
|
-
return { ok: false, durationMs, reason };
|
|
8533
|
+
return { ok: false, durationMs, reason, runtime: this.turnContext?.runtime };
|
|
8380
8534
|
}
|
|
8381
8535
|
// The post-admission analogue of `concludeBeforeRun` (Codo's stack review,
|
|
8382
8536
|
// blocking finding #2): this turn WAS admitted — it holds the conversation's
|
|
@@ -8405,13 +8559,13 @@ var TurnExecution = class {
|
|
|
8405
8559
|
);
|
|
8406
8560
|
} catch (err) {
|
|
8407
8561
|
turnLog.warn(
|
|
8408
|
-
{
|
|
8409
|
-
"
|
|
8562
|
+
{ ...apiErrorLogFields(err), turnId },
|
|
8563
|
+
"Couldn't save the turn's result to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
8410
8564
|
);
|
|
8411
8565
|
}
|
|
8412
8566
|
const durationMs = Date.now() - startedAt;
|
|
8413
8567
|
this.supervisor.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
8414
|
-
return { ok: false, durationMs, reason };
|
|
8568
|
+
return { ok: false, durationMs, reason, runtime: this.turnContext?.runtime };
|
|
8415
8569
|
}
|
|
8416
8570
|
async fetchContext() {
|
|
8417
8571
|
const { payload, turnId, turnLog } = this;
|
|
@@ -8428,20 +8582,20 @@ var TurnExecution = class {
|
|
|
8428
8582
|
} catch (err) {
|
|
8429
8583
|
const status = err instanceof ApiError ? err.status : 0;
|
|
8430
8584
|
if (status === 404) {
|
|
8431
|
-
turnLog.
|
|
8585
|
+
turnLog.debug(
|
|
8432
8586
|
{ err: err instanceof Error ? err.message : String(err) },
|
|
8433
8587
|
"dispatcher: turn-context 404 (stale event \u2014 conversation/message/participant gone); skipping"
|
|
8434
8588
|
);
|
|
8435
8589
|
throw this.concluded("turn_context_not_found");
|
|
8436
8590
|
}
|
|
8437
8591
|
if (status === 409 && apiErrorCode(err) === "dispatch_not_admitted") {
|
|
8438
|
-
turnLog.
|
|
8592
|
+
turnLog.debug(
|
|
8439
8593
|
"dispatcher: turn-context 409 dispatch_not_admitted (wake already resolved); skipping"
|
|
8440
8594
|
);
|
|
8441
8595
|
throw this.concluded("turn_context_not_admitted");
|
|
8442
8596
|
}
|
|
8443
8597
|
const reason = err instanceof Error ? err.message : String(err);
|
|
8444
|
-
turnLog.
|
|
8598
|
+
turnLog.debug({ err: reason }, "dispatcher: failed to fetch turn context");
|
|
8445
8599
|
const fetchReason = `fetch_failed: ${reason}`;
|
|
8446
8600
|
throw this.concluded(fetchReason, fetchReason);
|
|
8447
8601
|
}
|
|
@@ -8451,7 +8605,7 @@ var TurnExecution = class {
|
|
|
8451
8605
|
this.resumedFromSeq = resolveSeqFloor(sources, { turnId, log: turnLog });
|
|
8452
8606
|
this.seqCounter = Math.max(this.seqCounter, this.resumedFromSeq);
|
|
8453
8607
|
this.resumedSpoke = turnContext.turnSpoke === true || outboxSpoke;
|
|
8454
|
-
if (this.resumedSpoke) turnLog.
|
|
8608
|
+
if (this.resumedSpoke) turnLog.debug({ turnId }, "dispatcher: resumed span already spoke");
|
|
8455
8609
|
}
|
|
8456
8610
|
}
|
|
8457
8611
|
gateTrigger() {
|
|
@@ -8459,7 +8613,7 @@ var TurnExecution = class {
|
|
|
8459
8613
|
const message = this.turnContext.message;
|
|
8460
8614
|
const isDispatchableTrigger = message.role === "user" || message.role === "agent" || message.role === "system";
|
|
8461
8615
|
if (!isDispatchableTrigger) {
|
|
8462
|
-
turnLog.
|
|
8616
|
+
turnLog.debug({ role: message.role }, "dispatcher: trigger role not dispatchable \u2014 skipping");
|
|
8463
8617
|
throw this.concluded("unexpected_role", UNEXPECTED_ROLE_REASON);
|
|
8464
8618
|
}
|
|
8465
8619
|
this.supervisor.notifyStart({
|
|
@@ -8470,7 +8624,9 @@ var TurnExecution = class {
|
|
|
8470
8624
|
}
|
|
8471
8625
|
async resolveSecrets() {
|
|
8472
8626
|
const { payload, workspaceId, turnLog } = this;
|
|
8473
|
-
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
|
+
);
|
|
8474
8630
|
const { servers: resolvedMcpServers, missing } = resolveMcpSecrets(
|
|
8475
8631
|
// CT262: the user MCP DEFINITIONS (placeholder form) come from the turn
|
|
8476
8632
|
// context now, not a separate `getAgentSelf` run-config fetch.
|
|
@@ -8478,7 +8634,7 @@ var TurnExecution = class {
|
|
|
8478
8634
|
secretStore
|
|
8479
8635
|
);
|
|
8480
8636
|
if (missing.length > 0) {
|
|
8481
|
-
turnLog.
|
|
8637
|
+
turnLog.debug({ missing }, "dispatcher: turn needs secrets not declared on this companion");
|
|
8482
8638
|
const reason = missingSecretReason(missing);
|
|
8483
8639
|
throw this.concluded(reason, reason);
|
|
8484
8640
|
}
|
|
@@ -8493,7 +8649,7 @@ var TurnExecution = class {
|
|
|
8493
8649
|
if (effectiveCwd && !existsSync10(effectiveCwd)) {
|
|
8494
8650
|
turnLog.warn(
|
|
8495
8651
|
{ cwd: effectiveCwd },
|
|
8496
|
-
"
|
|
8652
|
+
"The working directory is missing; using the companion's directory instead. Check this agent's working directory setting."
|
|
8497
8653
|
);
|
|
8498
8654
|
effectiveCwd = void 0;
|
|
8499
8655
|
}
|
|
@@ -8501,7 +8657,7 @@ var TurnExecution = class {
|
|
|
8501
8657
|
if (prepareHook) {
|
|
8502
8658
|
let cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
|
|
8503
8659
|
if (cached2 && !checkoutState(cached2.cwd).ok) {
|
|
8504
|
-
turnLog.
|
|
8660
|
+
turnLog.debug(
|
|
8505
8661
|
{ cwd: cached2.cwd, reason: checkoutState(cached2.cwd).reason },
|
|
8506
8662
|
"dispatcher: the prepared checkout is no longer usable \u2014 re-running the prepare hook to restore it"
|
|
8507
8663
|
);
|
|
@@ -8528,7 +8684,7 @@ var TurnExecution = class {
|
|
|
8528
8684
|
});
|
|
8529
8685
|
} catch (err) {
|
|
8530
8686
|
const reason = err instanceof Error ? err.message : String(err);
|
|
8531
|
-
turnLog.
|
|
8687
|
+
turnLog.debug({ err: reason }, "dispatcher: prepare hook rejected a prepared turn");
|
|
8532
8688
|
const failReason = `prepare_failed: ${reason}`;
|
|
8533
8689
|
throw this.concluded(failReason, failReason);
|
|
8534
8690
|
}
|
|
@@ -8548,7 +8704,7 @@ var TurnExecution = class {
|
|
|
8548
8704
|
phase,
|
|
8549
8705
|
seq
|
|
8550
8706
|
}).catch((err) => {
|
|
8551
|
-
turnLog.
|
|
8707
|
+
turnLog.debug(
|
|
8552
8708
|
{ err: err instanceof Error ? err.message : String(err), phase },
|
|
8553
8709
|
"dispatcher: preparing-activity report failed (continuing with the hook)"
|
|
8554
8710
|
);
|
|
@@ -8588,7 +8744,7 @@ var TurnExecution = class {
|
|
|
8588
8744
|
clearTimeout(preparingTimer);
|
|
8589
8745
|
if (preparingStarted) reportPreparing("error");
|
|
8590
8746
|
const reason = err instanceof Error ? err.message : String(err);
|
|
8591
|
-
turnLog.
|
|
8747
|
+
turnLog.debug({ err: reason }, "dispatcher: prepare hook failed");
|
|
8592
8748
|
const failReason = `prepare_failed: ${reason}`;
|
|
8593
8749
|
throw this.concluded(failReason, failReason);
|
|
8594
8750
|
}
|
|
@@ -8636,14 +8792,14 @@ var TurnExecution = class {
|
|
|
8636
8792
|
} catch (err) {
|
|
8637
8793
|
const refusal = leaseRefusal(err);
|
|
8638
8794
|
if (refusal === "dispatch_not_admitted" || refusal === "turn_already_ended") {
|
|
8639
|
-
turnLog.
|
|
8795
|
+
turnLog.debug(
|
|
8640
8796
|
{ refusal, turnId },
|
|
8641
8797
|
"dispatcher: refused a turn lease; not running the model"
|
|
8642
8798
|
);
|
|
8643
8799
|
throw this.concluded(`lease_refused: ${refusal}`);
|
|
8644
8800
|
}
|
|
8645
8801
|
const reason = refusal ? `lease_refused: ${refusal}` : "lease_unconfirmed";
|
|
8646
|
-
turnLog.
|
|
8802
|
+
turnLog.debug(
|
|
8647
8803
|
{ refusal, turnId, err: err instanceof Error ? err.message : String(err) },
|
|
8648
8804
|
"dispatcher: turn lease not confirmed; not running the model"
|
|
8649
8805
|
);
|
|
@@ -8695,7 +8851,7 @@ var TurnExecution = class {
|
|
|
8695
8851
|
async selectAdapter() {
|
|
8696
8852
|
const { payload, workspaceId, turnId, turnLog } = this;
|
|
8697
8853
|
await guardBundledBinary(this.integrityContext(), (r, e) => this.concluded(r, e), this.outcome);
|
|
8698
|
-
const onWarn = (msg, meta) => turnLog.
|
|
8854
|
+
const onWarn = (msg, meta) => turnLog.debug(meta ?? {}, msg);
|
|
8699
8855
|
const adapters = [];
|
|
8700
8856
|
if (this.opts.claudeCodeAvailable?.() ?? true) {
|
|
8701
8857
|
adapters.push(createClaudeCodeAdapter({ queryFn: this.opts.queryFn, onWarn }));
|
|
@@ -8717,7 +8873,7 @@ var TurnExecution = class {
|
|
|
8717
8873
|
this.adapter = selectAdapter(registry, this.turnContext.runtime);
|
|
8718
8874
|
} catch (err) {
|
|
8719
8875
|
if (!(err instanceof RuntimeUnavailableError)) throw err;
|
|
8720
|
-
turnLog.
|
|
8876
|
+
turnLog.debug(
|
|
8721
8877
|
{ runtime: err.runtime, available: err.available },
|
|
8722
8878
|
"dispatcher: turn runtime not available on this device"
|
|
8723
8879
|
);
|
|
@@ -8730,8 +8886,8 @@ var TurnExecution = class {
|
|
|
8730
8886
|
});
|
|
8731
8887
|
} catch (postErr) {
|
|
8732
8888
|
turnLog.warn(
|
|
8733
|
-
{
|
|
8734
|
-
"
|
|
8889
|
+
{ ...apiErrorLogFields(postErr) },
|
|
8890
|
+
"Couldn't save the explanation to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
8735
8891
|
);
|
|
8736
8892
|
const reason = `runtime_unavailable:${err.runtime}`;
|
|
8737
8893
|
throw this.concluded(reason, reason);
|
|
@@ -8759,7 +8915,7 @@ var TurnExecution = class {
|
|
|
8759
8915
|
false,
|
|
8760
8916
|
true
|
|
8761
8917
|
);
|
|
8762
|
-
turnLog.
|
|
8918
|
+
turnLog.debug(
|
|
8763
8919
|
{ reason },
|
|
8764
8920
|
"dispatcher: stored session not resumed \u2014 the turn context was recomposed for a fresh session"
|
|
8765
8921
|
);
|
|
@@ -8770,8 +8926,8 @@ var TurnExecution = class {
|
|
|
8770
8926
|
};
|
|
8771
8927
|
} catch (err) {
|
|
8772
8928
|
turnLog.error(
|
|
8773
|
-
{ reason,
|
|
8774
|
-
"
|
|
8929
|
+
{ reason, ...apiErrorLogFields(err) },
|
|
8930
|
+
"Couldn't load the full conversation; continuing with the recent messages."
|
|
8775
8931
|
);
|
|
8776
8932
|
return null;
|
|
8777
8933
|
}
|
|
@@ -8792,7 +8948,10 @@ var TurnExecution = class {
|
|
|
8792
8948
|
dispatchId: this.dispatchId,
|
|
8793
8949
|
message: this.turnContext.message.body
|
|
8794
8950
|
},
|
|
8795
|
-
(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
|
+
)
|
|
8796
8955
|
) : null;
|
|
8797
8956
|
const turnRuntime = this.turnContext.runtime;
|
|
8798
8957
|
const committer = this.committer = new TurnCommitter({
|
|
@@ -8832,8 +8991,8 @@ var TurnExecution = class {
|
|
|
8832
8991
|
}
|
|
8833
8992
|
} catch (err) {
|
|
8834
8993
|
turnLog.warn(
|
|
8835
|
-
{
|
|
8836
|
-
"
|
|
8994
|
+
{ ...apiErrorLogFields(err) },
|
|
8995
|
+
"Couldn't check whether the agent chose to pass; the turn may appear to have failed."
|
|
8837
8996
|
);
|
|
8838
8997
|
}
|
|
8839
8998
|
};
|
|
@@ -8841,7 +9000,7 @@ var TurnExecution = class {
|
|
|
8841
9000
|
const fireTimeout = (reason) => {
|
|
8842
9001
|
if (abortController.signal.aborted) return;
|
|
8843
9002
|
o.timeoutReason = reason;
|
|
8844
|
-
turnLog.
|
|
9003
|
+
turnLog.debug(
|
|
8845
9004
|
{ reason, idleTimeoutMs, totalTimeoutMs },
|
|
8846
9005
|
"dispatcher: turn timeout \u2014 aborting"
|
|
8847
9006
|
);
|
|
@@ -8871,7 +9030,7 @@ var TurnExecution = class {
|
|
|
8871
9030
|
if (state !== "ended") return;
|
|
8872
9031
|
if (abortController.signal.aborted) return;
|
|
8873
9032
|
o.leaseLost = true;
|
|
8874
|
-
turnLog.
|
|
9033
|
+
turnLog.debug(
|
|
8875
9034
|
{ turnId, trigger },
|
|
8876
9035
|
"dispatcher: this turn is no longer running server-side \u2014 aborting the loop"
|
|
8877
9036
|
);
|
|
@@ -8899,7 +9058,7 @@ var TurnExecution = class {
|
|
|
8899
9058
|
if (isContentBearingEvent(event)) o.contentBearingEvents += 1;
|
|
8900
9059
|
armIdle();
|
|
8901
9060
|
if (abortController.signal.aborted) {
|
|
8902
|
-
turnLog.
|
|
9061
|
+
turnLog.debug("dispatcher: aborted mid-turn");
|
|
8903
9062
|
o.okResult = false;
|
|
8904
9063
|
o.resultReason = o.timeoutReason ?? "cancelled";
|
|
8905
9064
|
break;
|
|
@@ -8922,18 +9081,18 @@ var TurnExecution = class {
|
|
|
8922
9081
|
o.sessionWriteRejected = true;
|
|
8923
9082
|
turnLog.error(
|
|
8924
9083
|
{
|
|
8925
|
-
|
|
9084
|
+
...apiErrorLogFields(err),
|
|
8926
9085
|
status,
|
|
8927
9086
|
responseBody: describeErrorBody(err instanceof ApiError ? err.body : void 0),
|
|
8928
9087
|
stateLength: event.state.length,
|
|
8929
9088
|
runtime: turnRuntime
|
|
8930
9089
|
},
|
|
8931
|
-
"
|
|
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"
|
|
8932
9091
|
);
|
|
8933
9092
|
} else {
|
|
8934
9093
|
turnLog.warn(
|
|
8935
|
-
{
|
|
8936
|
-
"
|
|
9094
|
+
{ ...apiErrorLogFields(err) },
|
|
9095
|
+
"Couldn't save the agent's memory; trying again next turn."
|
|
8937
9096
|
);
|
|
8938
9097
|
}
|
|
8939
9098
|
}
|
|
@@ -8974,7 +9133,7 @@ var TurnExecution = class {
|
|
|
8974
9133
|
await this.applyRecordedTurnControlIntent();
|
|
8975
9134
|
}
|
|
8976
9135
|
if (!abortController.signal.aborted && this.skipState.skipped) {
|
|
8977
|
-
turnLog.
|
|
9136
|
+
turnLog.debug(
|
|
8978
9137
|
{ reason: this.skipState.reason, turnId, ok: o.okResult },
|
|
8979
9138
|
"agent skipped turn (skip_turn)"
|
|
8980
9139
|
);
|
|
@@ -8982,7 +9141,7 @@ var TurnExecution = class {
|
|
|
8982
9141
|
} catch (err) {
|
|
8983
9142
|
o.okResult = false;
|
|
8984
9143
|
o.resultReason = err instanceof Error ? err.message : String(err);
|
|
8985
|
-
turnLog.
|
|
9144
|
+
turnLog.debug({ err: o.resultReason }, "dispatcher: SDK query threw");
|
|
8986
9145
|
o.runtimeIncomplete = await absorbBundledLoss(
|
|
8987
9146
|
this.integrityContext(),
|
|
8988
9147
|
abortController.signal.aborted,
|
|
@@ -9007,7 +9166,7 @@ var TurnExecution = class {
|
|
|
9007
9166
|
kind: "usage_capped",
|
|
9008
9167
|
resetsAt: health.limitedUntil
|
|
9009
9168
|
});
|
|
9010
|
-
turnLog.
|
|
9169
|
+
turnLog.debug(
|
|
9011
9170
|
{ runtime: turnRuntime, limitedUntil: health.limitedUntil },
|
|
9012
9171
|
"dispatcher: idle-timeout on a capped runtime \u2014 reclassified as usage_capped (CT639)"
|
|
9013
9172
|
);
|
|
@@ -9058,7 +9217,7 @@ var TurnExecution = class {
|
|
|
9058
9217
|
if (diagnosticReason && !["usage_capped", "rate_limited", "auth_expired", "cancelled", "skipped"].includes(
|
|
9059
9218
|
diagnosticReason.kind
|
|
9060
9219
|
)) {
|
|
9061
|
-
turnLog.
|
|
9220
|
+
turnLog.debug(
|
|
9062
9221
|
{
|
|
9063
9222
|
workspaceId,
|
|
9064
9223
|
turnId,
|
|
@@ -9086,8 +9245,8 @@ var TurnExecution = class {
|
|
|
9086
9245
|
);
|
|
9087
9246
|
} catch (err) {
|
|
9088
9247
|
turnLog.warn(
|
|
9089
|
-
{
|
|
9090
|
-
"
|
|
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"
|
|
9091
9250
|
);
|
|
9092
9251
|
}
|
|
9093
9252
|
this.supervisor.releaseAbort(turnId, abortController);
|
|
@@ -9104,9 +9263,6 @@ var TurnExecution = class {
|
|
|
9104
9263
|
...o.resultReason ? { reason: o.resultReason } : {},
|
|
9105
9264
|
durationMs
|
|
9106
9265
|
});
|
|
9107
|
-
if (!o.okResult && o.resultReason !== "cancelled") {
|
|
9108
|
-
turnLog.info(`turn failed \u2014 full this.transcript: ${this.transcript.path}`);
|
|
9109
|
-
}
|
|
9110
9266
|
if (o.settledDiagnostics?.resultReason?.kind === "empty_result" || o.settledDiagnostics?.resultReason?.kind === "empty_result_unverified") {
|
|
9111
9267
|
this.transcript.preserveAnomaly();
|
|
9112
9268
|
}
|
|
@@ -9131,7 +9287,9 @@ var TurnExecution = class {
|
|
|
9131
9287
|
return {
|
|
9132
9288
|
ok: false,
|
|
9133
9289
|
durationMs,
|
|
9134
|
-
...o.resultReason ? { reason: o.resultReason } : {}
|
|
9290
|
+
...o.resultReason ? { reason: o.resultReason } : {},
|
|
9291
|
+
runtime: this.turnContext.runtime,
|
|
9292
|
+
...this.transcript ? { transcriptPath: tildePath(this.transcript.path) } : {}
|
|
9135
9293
|
};
|
|
9136
9294
|
}
|
|
9137
9295
|
};
|
|
@@ -9337,7 +9495,7 @@ var Outbox = class {
|
|
|
9337
9495
|
}
|
|
9338
9496
|
this.log?.warn(
|
|
9339
9497
|
{ workspaceId: this.workspaceId, err: err instanceof Error ? err.message : String(err) },
|
|
9340
|
-
"
|
|
9498
|
+
"Couldn't save an update on this machine. Check free disk space and access to ~/.cabane."
|
|
9341
9499
|
);
|
|
9342
9500
|
return;
|
|
9343
9501
|
}
|
|
@@ -9425,7 +9583,7 @@ var Outbox = class {
|
|
|
9425
9583
|
dropCorrupt(full) {
|
|
9426
9584
|
this.log?.warn(
|
|
9427
9585
|
{ workspaceId: this.workspaceId, file: full },
|
|
9428
|
-
"
|
|
9586
|
+
"A saved update is unreadable and couldn't be delivered. Check the conversation for missing replies."
|
|
9429
9587
|
);
|
|
9430
9588
|
try {
|
|
9431
9589
|
rmSync7(full, { force: true });
|
|
@@ -9442,7 +9600,7 @@ var Outbox = class {
|
|
|
9442
9600
|
if (now - e.enqueuedAt > MAX_AGE_MS) {
|
|
9443
9601
|
this.log?.warn(
|
|
9444
9602
|
{ workspaceId: this.workspaceId, turnId: e.turnId, seq: e.seq, kind: e.kind },
|
|
9445
|
-
"
|
|
9603
|
+
"A saved update waited too long and couldn't be delivered. Check the conversation for missing replies."
|
|
9446
9604
|
);
|
|
9447
9605
|
this.remove(e.turnId, e.seq);
|
|
9448
9606
|
} else {
|
|
@@ -9454,7 +9612,7 @@ var Outbox = class {
|
|
|
9454
9612
|
for (const e of survivors.slice(0, overflow)) {
|
|
9455
9613
|
this.log?.warn(
|
|
9456
9614
|
{ workspaceId: this.workspaceId, turnId: e.turnId, seq: e.seq, kind: e.kind },
|
|
9457
|
-
"
|
|
9615
|
+
"Too many updates are waiting; the oldest couldn't be kept. Check the connection to Cabane."
|
|
9458
9616
|
);
|
|
9459
9617
|
this.remove(e.turnId, e.seq);
|
|
9460
9618
|
}
|
|
@@ -9540,11 +9698,14 @@ var SseSubscriber = class {
|
|
|
9540
9698
|
try {
|
|
9541
9699
|
await this.connect();
|
|
9542
9700
|
backoff = 500;
|
|
9543
|
-
if (!this.aborted)
|
|
9701
|
+
if (!this.aborted) {
|
|
9702
|
+
this.opts.log.warn("Lost the connection to Cabane; reconnecting");
|
|
9703
|
+
await sleep2(backoff);
|
|
9704
|
+
}
|
|
9544
9705
|
} catch (err) {
|
|
9545
9706
|
if (this.aborted) return;
|
|
9546
9707
|
if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
|
|
9547
|
-
this.opts.log.
|
|
9708
|
+
this.opts.log.debug(
|
|
9548
9709
|
{ workspaceId: this.opts.workspaceId, status: err.status },
|
|
9549
9710
|
"SSE auth failed \u2014 tearing down this workspace subscriber"
|
|
9550
9711
|
);
|
|
@@ -9557,7 +9718,7 @@ var SseSubscriber = class {
|
|
|
9557
9718
|
err: err instanceof Error ? err.message : String(err),
|
|
9558
9719
|
backoff
|
|
9559
9720
|
},
|
|
9560
|
-
"
|
|
9721
|
+
"Lost the connection to Cabane; reconnecting"
|
|
9561
9722
|
);
|
|
9562
9723
|
await sleep2(backoff);
|
|
9563
9724
|
backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
|
|
@@ -9581,7 +9742,7 @@ var SseSubscriber = class {
|
|
|
9581
9742
|
if (!res.body) {
|
|
9582
9743
|
throw new Error("SSE response has no body");
|
|
9583
9744
|
}
|
|
9584
|
-
this.opts.log.
|
|
9745
|
+
this.opts.log.debug({ workspaceId: this.opts.workspaceId }, "SSE connected");
|
|
9585
9746
|
let opened = true;
|
|
9586
9747
|
this.opts.onOpen?.();
|
|
9587
9748
|
const parser = createParser({
|
|
@@ -9711,15 +9872,12 @@ var CompanionSupervisor = class {
|
|
|
9711
9872
|
async start() {
|
|
9712
9873
|
this.log.info(
|
|
9713
9874
|
{ protocolVersion: TURN_PROTOCOL_VERSION, version: COMPANION_VERSION },
|
|
9714
|
-
|
|
9875
|
+
`Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ?? hostname()}`
|
|
9715
9876
|
);
|
|
9716
9877
|
const startupHarnessRefresh = this.refreshHarnessStatuses();
|
|
9717
9878
|
this.trackHeartbeat(startupHarnessRefresh);
|
|
9718
9879
|
if (!this.config.deviceToken) {
|
|
9719
|
-
this.log.warn("
|
|
9720
|
-
process.stdout.write(
|
|
9721
|
-
"companion: this device is not paired \u2014 run `cabane-companion pair`, then confirm the short code in Settings \u2192 Connectors.\n"
|
|
9722
|
-
);
|
|
9880
|
+
this.log.warn("This device isn't paired. Run cabane-companion start to pair it.");
|
|
9723
9881
|
return;
|
|
9724
9882
|
}
|
|
9725
9883
|
this.deviceApi = new DeviceApi({
|
|
@@ -9733,6 +9891,7 @@ var CompanionSupervisor = class {
|
|
|
9733
9891
|
token: this.config.deviceToken,
|
|
9734
9892
|
log: this.log,
|
|
9735
9893
|
lastEventId: null,
|
|
9894
|
+
onOpen: () => this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ?? hostname()}`),
|
|
9736
9895
|
onMessage: async (ev) => {
|
|
9737
9896
|
if (ev.event === "assignments_changed") {
|
|
9738
9897
|
if (this.refreshing && this.inFlightRefresh) await this.inFlightRefresh;
|
|
@@ -9740,11 +9899,13 @@ var CompanionSupervisor = class {
|
|
|
9740
9899
|
}
|
|
9741
9900
|
},
|
|
9742
9901
|
onAuthFailure: () => {
|
|
9743
|
-
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
|
+
);
|
|
9744
9905
|
}
|
|
9745
9906
|
});
|
|
9746
|
-
this.deviceSub.start();
|
|
9747
9907
|
await this.refreshAssignments();
|
|
9908
|
+
this.deviceSub.start();
|
|
9748
9909
|
const firstHeartbeat = startupHarnessRefresh.then(async () => {
|
|
9749
9910
|
if (this.stopped) return;
|
|
9750
9911
|
await this.sendHeartbeat();
|
|
@@ -9780,7 +9941,9 @@ var CompanionSupervisor = class {
|
|
|
9780
9941
|
if (!this.deviceApi) return;
|
|
9781
9942
|
await this.refreshHarnessStatuses();
|
|
9782
9943
|
try {
|
|
9783
|
-
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
|
+
);
|
|
9784
9947
|
const connectorReports = this.connectorHealth.reports();
|
|
9785
9948
|
const opencodeModels = this.config.opencode?.serverUrl ? await enumerateOpencodeModels(this.config.opencode.serverUrl) : null;
|
|
9786
9949
|
const res = await this.deviceApi.heartbeat({
|
|
@@ -9827,8 +9990,8 @@ var CompanionSupervisor = class {
|
|
|
9827
9990
|
void this.recoverRuns();
|
|
9828
9991
|
} catch (err) {
|
|
9829
9992
|
this.log.warn(
|
|
9830
|
-
{
|
|
9831
|
-
"
|
|
9993
|
+
{ ...apiErrorLogFields(err) },
|
|
9994
|
+
"Couldn't check in with Cabane; trying again shortly."
|
|
9832
9995
|
);
|
|
9833
9996
|
}
|
|
9834
9997
|
}
|
|
@@ -9843,7 +10006,7 @@ var CompanionSupervisor = class {
|
|
|
9843
10006
|
this.versionSkewWarned = true;
|
|
9844
10007
|
this.log.warn(
|
|
9845
10008
|
{ companionVersion: COMPANION_VERSION, serverVersion },
|
|
9846
|
-
"
|
|
10009
|
+
"This companion and Cabane are on different versions. If turns fail, update with npm i -g @cabane/companion@latest"
|
|
9847
10010
|
);
|
|
9848
10011
|
}
|
|
9849
10012
|
// Pull assignments and reconcile the live runner set against them. Re-entrancy
|
|
@@ -9865,8 +10028,8 @@ var CompanionSupervisor = class {
|
|
|
9865
10028
|
device = resp.device;
|
|
9866
10029
|
} catch (err) {
|
|
9867
10030
|
this.log.error(
|
|
9868
|
-
{
|
|
9869
|
-
"
|
|
10031
|
+
{ ...apiErrorLogFields(err) },
|
|
10032
|
+
"Couldn't load the assigned agents. Check that this device is active in Cabane; trying again shortly."
|
|
9870
10033
|
);
|
|
9871
10034
|
this.hub.setDeviceError(err instanceof Error ? err.message : String(err));
|
|
9872
10035
|
return;
|
|
@@ -9932,14 +10095,17 @@ var CompanionSupervisor = class {
|
|
|
9932
10095
|
const credential = it.credential ?? getCredential(it.agentId);
|
|
9933
10096
|
const runConfig = parseRunConfig(
|
|
9934
10097
|
it.runConfig,
|
|
9935
|
-
(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
|
+
)
|
|
9936
10102
|
);
|
|
9937
10103
|
const required = requiredSecretNames(runConfig.mcpServers);
|
|
9938
10104
|
const missing = required.filter((n) => !exposed.has(n));
|
|
9939
10105
|
if (!credential) {
|
|
9940
10106
|
this.log.error(
|
|
9941
10107
|
{ workspaceId, agentId: it.agentId, username: it.agentUsername },
|
|
9942
|
-
|
|
10108
|
+
`Cannot run ${it.agentDisplayName}: its sign-in to Cabane is missing. Reassign it to this device in Cabane.`
|
|
9943
10109
|
);
|
|
9944
10110
|
this.removeAgent(wr, it.agentId);
|
|
9945
10111
|
this.hub.setAgent(workspaceId, {
|
|
@@ -9973,19 +10139,20 @@ var CompanionSupervisor = class {
|
|
|
9973
10139
|
if (missing.length > 0) {
|
|
9974
10140
|
this.log.warn(
|
|
9975
10141
|
{ workspaceId, agentId: it.agentId, missing },
|
|
9976
|
-
|
|
10142
|
+
`${it.agentDisplayName} needs secrets this machine does not have: ${missing.join(", ")}. Add them to ~/.cabane/secrets.json.`
|
|
9977
10143
|
);
|
|
9978
10144
|
}
|
|
9979
10145
|
}
|
|
9980
10146
|
this.ensureWorkspaceSse(wr);
|
|
9981
10147
|
}
|
|
9982
10148
|
addAgent(wr, it, credential, runConfig) {
|
|
9983
|
-
const
|
|
10149
|
+
const agentLog = this.log.child({ agentName: it.agentDisplayName });
|
|
10150
|
+
const outbox = new Outbox(it.agentId, agentLog);
|
|
9984
10151
|
const api = new CabaneApi({
|
|
9985
10152
|
baseUrl: this.config.baseUrl,
|
|
9986
10153
|
token: credential,
|
|
9987
10154
|
outbox,
|
|
9988
|
-
log:
|
|
10155
|
+
log: agentLog
|
|
9989
10156
|
});
|
|
9990
10157
|
const aborts = /* @__PURE__ */ new Map();
|
|
9991
10158
|
const dispatcher = this.buildDispatcher({
|
|
@@ -9995,6 +10162,7 @@ var CompanionSupervisor = class {
|
|
|
9995
10162
|
workspaceSlug: it.workspaceSlug,
|
|
9996
10163
|
agentId: it.agentId,
|
|
9997
10164
|
agentUsername: it.agentUsername,
|
|
10165
|
+
agentDisplayName: it.agentDisplayName,
|
|
9998
10166
|
credential,
|
|
9999
10167
|
runConfig,
|
|
10000
10168
|
aborts
|
|
@@ -10015,7 +10183,7 @@ var CompanionSupervisor = class {
|
|
|
10015
10183
|
drain2.kick();
|
|
10016
10184
|
this.log.info(
|
|
10017
10185
|
{ workspaceId: it.workspaceId, agentId: it.agentId, username: it.agentUsername },
|
|
10018
|
-
|
|
10186
|
+
`Running ${it.agentDisplayName}`
|
|
10019
10187
|
);
|
|
10020
10188
|
}
|
|
10021
10189
|
removeAgent(wr, agentId) {
|
|
@@ -10026,7 +10194,7 @@ var CompanionSupervisor = class {
|
|
|
10026
10194
|
this.hub.removeAgent(wr.workspaceId, agentId);
|
|
10027
10195
|
this.log.info(
|
|
10028
10196
|
{ workspaceId: wr.workspaceId, agentId },
|
|
10029
|
-
|
|
10197
|
+
`Stopped running ${runner.displayName} (unassigned)`
|
|
10030
10198
|
);
|
|
10031
10199
|
}
|
|
10032
10200
|
// CT1379: can this machine LAUNCH the harness — is the SDK's own bundled
|
|
@@ -10099,7 +10267,11 @@ var CompanionSupervisor = class {
|
|
|
10099
10267
|
const meta = { runtime, platform: resolution.platform, arch: resolution.arch, status };
|
|
10100
10268
|
if (status === "present") {
|
|
10101
10269
|
const say = intended ? this.log.info : this.log.debug;
|
|
10102
|
-
say.call(
|
|
10270
|
+
say.call(
|
|
10271
|
+
this.log,
|
|
10272
|
+
meta,
|
|
10273
|
+
`${runtime === "codex" ? "Codex" : "Claude Code"} is available again`
|
|
10274
|
+
);
|
|
10103
10275
|
continue;
|
|
10104
10276
|
}
|
|
10105
10277
|
if (intended) this.log.warn(meta, startupWarning(resolution));
|
|
@@ -10145,7 +10317,7 @@ var CompanionSupervisor = class {
|
|
|
10145
10317
|
local,
|
|
10146
10318
|
aborts: ctx.aborts,
|
|
10147
10319
|
runConfig: ctx.runConfig,
|
|
10148
|
-
log: this.log,
|
|
10320
|
+
log: this.log.child({ agentName: ctx.agentDisplayName ?? ctx.agentUsername }),
|
|
10149
10321
|
// CT833: register the claude-code adapter only when this device actually
|
|
10150
10322
|
// offers claude-code — read per turn (not captured here), so a harness
|
|
10151
10323
|
// installed or connected after boot works on the next turn exactly as it
|
|
@@ -10203,7 +10375,7 @@ var CompanionSupervisor = class {
|
|
|
10203
10375
|
onMessage: (ev) => this.handleWorkspaceMessage(wr, ev),
|
|
10204
10376
|
onAuthFailure: (status) => {
|
|
10205
10377
|
this.hub.setAuthFailed(wr.workspaceId);
|
|
10206
|
-
this.log.
|
|
10378
|
+
this.log.debug(
|
|
10207
10379
|
{ workspaceId: wr.workspaceId, status, sseAgentId: wr.sseAgentId },
|
|
10208
10380
|
"companion: workspace stream auth failed \u2014 re-pulling assignments"
|
|
10209
10381
|
);
|
|
@@ -10212,7 +10384,7 @@ var CompanionSupervisor = class {
|
|
|
10212
10384
|
}
|
|
10213
10385
|
});
|
|
10214
10386
|
wr.sub.start();
|
|
10215
|
-
this.log.
|
|
10387
|
+
this.log.debug({ workspaceId: wr.workspaceId }, "companion: subscribed");
|
|
10216
10388
|
}
|
|
10217
10389
|
async removeWorkspace(workspaceId) {
|
|
10218
10390
|
const wr = this.workspaces.get(workspaceId);
|
|
@@ -10236,7 +10408,7 @@ var CompanionSupervisor = class {
|
|
|
10236
10408
|
try {
|
|
10237
10409
|
wire = JSON.parse(ev.data);
|
|
10238
10410
|
} catch (err) {
|
|
10239
|
-
this.log.
|
|
10411
|
+
this.log.debug(
|
|
10240
10412
|
{ err: err instanceof Error ? err.message : String(err) },
|
|
10241
10413
|
"malformed SSE payload"
|
|
10242
10414
|
);
|
|
@@ -10252,7 +10424,7 @@ var CompanionSupervisor = class {
|
|
|
10252
10424
|
if (agent2) {
|
|
10253
10425
|
const aborted = agent2.dispatcher.cancel(payload2.conversationId, payload2.agentId);
|
|
10254
10426
|
if (aborted) {
|
|
10255
|
-
this.log.
|
|
10427
|
+
this.log.debug(
|
|
10256
10428
|
{
|
|
10257
10429
|
workspaceId: wr.workspaceId,
|
|
10258
10430
|
conversationId: payload2.conversationId,
|
|
@@ -10306,9 +10478,9 @@ var CompanionSupervisor = class {
|
|
|
10306
10478
|
workspaceId: wr.workspaceId,
|
|
10307
10479
|
conversationId: payload.conversationId,
|
|
10308
10480
|
agentId: payload.agentId,
|
|
10309
|
-
|
|
10481
|
+
...apiErrorLogFields(err)
|
|
10310
10482
|
},
|
|
10311
|
-
"
|
|
10483
|
+
"The turn stopped unexpectedly. Reply in Cabane to try again."
|
|
10312
10484
|
);
|
|
10313
10485
|
});
|
|
10314
10486
|
wr.chains.set(chainKey, tail);
|
|
@@ -10341,7 +10513,7 @@ var CompanionSupervisor = class {
|
|
|
10341
10513
|
messageId: payload.messageId,
|
|
10342
10514
|
reason: "agent_not_on_device"
|
|
10343
10515
|
});
|
|
10344
|
-
this.log.
|
|
10516
|
+
this.log.debug(
|
|
10345
10517
|
{
|
|
10346
10518
|
workspaceId: wr.workspaceId,
|
|
10347
10519
|
conversationId: payload.conversationId,
|
|
@@ -10362,7 +10534,7 @@ var CompanionSupervisor = class {
|
|
|
10362
10534
|
agentId: payload.agentId,
|
|
10363
10535
|
err: lastErr instanceof Error ? lastErr.message : String(lastErr)
|
|
10364
10536
|
},
|
|
10365
|
-
"
|
|
10537
|
+
"Couldn't tell Cabane that this agent no longer runs here; trying again when the connection returns."
|
|
10366
10538
|
);
|
|
10367
10539
|
return false;
|
|
10368
10540
|
}
|
|
@@ -10417,7 +10589,7 @@ var CompanionSupervisor = class {
|
|
|
10417
10589
|
agent,
|
|
10418
10590
|
run.turnId
|
|
10419
10591
|
).catch(
|
|
10420
|
-
(err) => this.log.
|
|
10592
|
+
(err) => this.log.debug({ err, turnId: run.turnId }, "companion: restart recovery failed")
|
|
10421
10593
|
).finally(() => {
|
|
10422
10594
|
if (wr.chains.get(key) === tail) wr.chains.delete(key);
|
|
10423
10595
|
});
|
|
@@ -10428,7 +10600,7 @@ var CompanionSupervisor = class {
|
|
|
10428
10600
|
this.recoveryChecked = page.nextCursor === null;
|
|
10429
10601
|
}
|
|
10430
10602
|
} catch (err) {
|
|
10431
|
-
this.log.
|
|
10603
|
+
this.log.debug({ err }, "companion: run recovery read failed (will retry)");
|
|
10432
10604
|
} finally {
|
|
10433
10605
|
this.recovering = false;
|
|
10434
10606
|
}
|
|
@@ -10440,7 +10612,7 @@ var CompanionSupervisor = class {
|
|
|
10440
10612
|
const workspaceId = wr.workspaceId;
|
|
10441
10613
|
if (ev.id && hasDispatched(workspaceId, ev.id)) {
|
|
10442
10614
|
if (hasCompleted(workspaceId, ev.id)) {
|
|
10443
|
-
this.log.
|
|
10615
|
+
this.log.debug(
|
|
10444
10616
|
{ workspaceId, eventId: ev.id },
|
|
10445
10617
|
"companion: skipping already-completed event (resume after restart)"
|
|
10446
10618
|
);
|
|
@@ -10448,7 +10620,7 @@ var CompanionSupervisor = class {
|
|
|
10448
10620
|
return;
|
|
10449
10621
|
}
|
|
10450
10622
|
if (noResume()) {
|
|
10451
|
-
this.log.
|
|
10623
|
+
this.log.debug(
|
|
10452
10624
|
{ workspaceId, eventId: ev.id },
|
|
10453
10625
|
"companion: skipping interrupted turn (CABANE_COMPANION_NO_RESUME=1) \u2014 resume disabled for this boot"
|
|
10454
10626
|
);
|
|
@@ -10460,13 +10632,13 @@ var CompanionSupervisor = class {
|
|
|
10460
10632
|
if (attempt > MAX_RESUME_ATTEMPTS) {
|
|
10461
10633
|
this.log.error(
|
|
10462
10634
|
{ workspaceId, eventId: ev.id, attempt, cap: MAX_RESUME_ATTEMPTS },
|
|
10463
|
-
"
|
|
10635
|
+
"An interrupted turn couldn't finish after several attempts. Reply in Cabane to try again."
|
|
10464
10636
|
);
|
|
10465
10637
|
markCompleted(workspaceId, ev.id);
|
|
10466
10638
|
wr.cursor.settle(ev.id);
|
|
10467
10639
|
return;
|
|
10468
10640
|
}
|
|
10469
|
-
this.log.
|
|
10641
|
+
this.log.debug(
|
|
10470
10642
|
{ workspaceId, eventId: ev.id, attempt, cap: MAX_RESUME_ATTEMPTS },
|
|
10471
10643
|
"companion: re-dispatching interrupted turn (resume after restart)"
|
|
10472
10644
|
);
|
|
@@ -10476,7 +10648,7 @@ var CompanionSupervisor = class {
|
|
|
10476
10648
|
if (ev.id) {
|
|
10477
10649
|
const liveTurnIds = rememberTurnId(workspaceId, ev.id, turnId);
|
|
10478
10650
|
if (liveTurnIds > TURN_ID_OVERFLOW_WARN) {
|
|
10479
|
-
this.log.
|
|
10651
|
+
this.log.debug(
|
|
10480
10652
|
{ workspaceId, liveTurnIds, threshold: TURN_ID_OVERFLOW_WARN },
|
|
10481
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."
|
|
10482
10654
|
);
|
|
@@ -10484,11 +10656,18 @@ var CompanionSupervisor = class {
|
|
|
10484
10656
|
markDispatched(workspaceId, ev.id);
|
|
10485
10657
|
}
|
|
10486
10658
|
if (resumedTurnId) {
|
|
10487
|
-
this.log.
|
|
10659
|
+
this.log.debug(
|
|
10488
10660
|
{ workspaceId, eventId: ev.id, turnId },
|
|
10489
10661
|
"companion: resuming an interrupted turn under its original id"
|
|
10490
10662
|
);
|
|
10491
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");
|
|
10492
10671
|
const result = await agent.dispatcher.handle(payload, {
|
|
10493
10672
|
turnId,
|
|
10494
10673
|
resumed: resumedTurnId !== null
|
|
@@ -10497,16 +10676,23 @@ var CompanionSupervisor = class {
|
|
|
10497
10676
|
if (ev.id) wr.cursor.settle(ev.id);
|
|
10498
10677
|
const durationS = (result.durationMs / 1e3).toFixed(1);
|
|
10499
10678
|
const bindings = {
|
|
10500
|
-
|
|
10501
|
-
|
|
10502
|
-
agentId: agent.agentId
|
|
10679
|
+
...turnBindings,
|
|
10680
|
+
...result.transcriptPath ? { transcriptPath: result.transcriptPath } : {}
|
|
10503
10681
|
};
|
|
10504
10682
|
if (result.ok) {
|
|
10505
|
-
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");
|
|
10506
10692
|
} else {
|
|
10507
|
-
this.log.
|
|
10693
|
+
this.log.info(
|
|
10508
10694
|
bindings,
|
|
10509
|
-
|
|
10695
|
+
`Turn failed after ${durationS}s: ${turnFailureCopy(result.reason, result.runtime)}`
|
|
10510
10696
|
);
|
|
10511
10697
|
}
|
|
10512
10698
|
}
|
|
@@ -10526,7 +10712,7 @@ var CompanionSupervisor = class {
|
|
|
10526
10712
|
}
|
|
10527
10713
|
} catch (err) {
|
|
10528
10714
|
drainDelay = Math.min(drainDelay * 2, DRAIN_MAX_MS);
|
|
10529
|
-
this.log.
|
|
10715
|
+
this.log.debug(
|
|
10530
10716
|
{ agentId, err: err instanceof Error ? err.message : String(err) },
|
|
10531
10717
|
"companion: outbox drain pass threw (will retry with backoff)"
|
|
10532
10718
|
);
|
|
@@ -10561,7 +10747,7 @@ var CompanionSupervisor = class {
|
|
|
10561
10747
|
const next = loadConfig();
|
|
10562
10748
|
if (!next) return;
|
|
10563
10749
|
this.config = next;
|
|
10564
|
-
this.log
|
|
10750
|
+
configureLogger(this.log, next);
|
|
10565
10751
|
this.rebuildDispatchers();
|
|
10566
10752
|
void this.refreshAssignments();
|
|
10567
10753
|
}
|
|
@@ -10577,7 +10763,7 @@ var CompanionSupervisor = class {
|
|
|
10577
10763
|
};
|
|
10578
10764
|
this.config = next;
|
|
10579
10765
|
saveConfig(next);
|
|
10580
|
-
this.log
|
|
10766
|
+
configureLogger(this.log, next);
|
|
10581
10767
|
}
|
|
10582
10768
|
// ---- harnesses (CT586) ----
|
|
10583
10769
|
// Probe all three harnesses, cache the signals + versions, and push the derived
|
|
@@ -10603,7 +10789,7 @@ var CompanionSupervisor = class {
|
|
|
10603
10789
|
};
|
|
10604
10790
|
this.hub.setHarnesses(deriveHarnessSnapshot(signals).harnesses);
|
|
10605
10791
|
} catch (err) {
|
|
10606
|
-
this.log.
|
|
10792
|
+
this.log.debug(
|
|
10607
10793
|
{ err: err instanceof Error ? err.message : String(err) },
|
|
10608
10794
|
"companion: harness probe failed (will retry on next beat)"
|
|
10609
10795
|
);
|
|
@@ -10719,6 +10905,7 @@ var CompanionSupervisor = class {
|
|
|
10719
10905
|
workspaceSlug: wr.slug,
|
|
10720
10906
|
agentId: runner.agentId,
|
|
10721
10907
|
agentUsername: runner.username,
|
|
10908
|
+
agentDisplayName: runner.displayName,
|
|
10722
10909
|
credential: runner.credential,
|
|
10723
10910
|
runConfig: runner.runConfig,
|
|
10724
10911
|
aborts: runner.aborts
|
|
@@ -10824,7 +11011,7 @@ function handleUncaught(log, err, origin) {
|
|
|
10824
11011
|
const message = err instanceof Error ? err.message : String(err);
|
|
10825
11012
|
const code = errorCode(err);
|
|
10826
11013
|
if (isRecoverableSocketError(err)) {
|
|
10827
|
-
log.
|
|
11014
|
+
log.debug(
|
|
10828
11015
|
{ origin, code, err: message },
|
|
10829
11016
|
"companion: recovered from a socket error (kept running \u2014 a broken pipe never kills the companion)"
|
|
10830
11017
|
);
|
|
@@ -10832,7 +11019,7 @@ function handleUncaught(log, err, origin) {
|
|
|
10832
11019
|
}
|
|
10833
11020
|
log.error(
|
|
10834
11021
|
{ origin, code, err: message, stack: err instanceof Error ? err.stack : void 0 },
|
|
10835
|
-
"
|
|
11022
|
+
"An unexpected error occurred; the companion is still running."
|
|
10836
11023
|
);
|
|
10837
11024
|
}
|
|
10838
11025
|
|
|
@@ -10866,7 +11053,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
10866
11053
|
try {
|
|
10867
11054
|
({ cfg, claudeOnPath: claudeCode } = await requireStartConfig({
|
|
10868
11055
|
...opts.probeClaude ? { probeClaude: opts.probeClaude } : {},
|
|
10869
|
-
onMigrated: (migrated, onPath) => log.
|
|
11056
|
+
onMigrated: (migrated, onPath) => log.debug(
|
|
10870
11057
|
{ claudeCode: isClaudeCodeConnected(migrated) },
|
|
10871
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)"
|
|
10872
11059
|
)
|
|
@@ -10887,7 +11074,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
10887
11074
|
});
|
|
10888
11075
|
throw err;
|
|
10889
11076
|
}
|
|
10890
|
-
|
|
11077
|
+
configureLogger(log, cfg);
|
|
10891
11078
|
const harnessVersions = await probeHarnessVersions({
|
|
10892
11079
|
// Connected AND its CLI answered — the two things that make a
|
|
10893
11080
|
// `claude --version` probe worth spawning. Not the offer predicate.
|