@cabane/companion 0.6.101 → 0.6.102

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 `warning: ${label} is ${HARNESS_INTENT_WORD[r.runtime]}, but @cabane/companion's bundled ${label} ${missingNoun(r.status)} is missing. It will not be offered to Cabane. ${repairAction()} Then restart the companion.`;
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
- var CONSOLE_IGNORE = [
1087
- "pid",
1088
- "hostname",
1089
- "workspaceId",
1090
- "conversationId",
1091
- "agentId",
1092
- "messageId",
1093
- "sessionId",
1094
- "companionId"
1095
- ].join(",");
1096
- function consoleShortId(log) {
1097
- const id = log.conversationId ?? log.workspaceId;
1098
- return typeof id === "string" && id.length > 0 ? id.slice(0, 8) : null;
1099
- }
1100
- function consoleMessageFormat(log, messageKey) {
1101
- const short = consoleShortId(log);
1102
- const msg = String(log[messageKey] ?? "");
1103
- return short ? `${short} ${msg}` : msg;
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
- function createLogger(destinations = {}) {
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: "info",
1161
+ level: "debug",
1120
1162
  stream: {
1121
1163
  write(chunk) {
1122
- if (consoleLogging) consoleStream.write(chunk);
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: destinations.file ?? createWriteStream(path, { flags: "a" })
1171
+ stream: {
1172
+ write(chunk) {
1173
+ file.write(state.format === "json" ? chunk : human(chunk));
1174
+ }
1175
+ }
1130
1176
  });
1131
- return pino({ level: "debug" }, pino.multistream(streams));
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
- cached = createLogger();
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?.warn(
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, status: err.status },
2075
- "companion outbox: discarding entry on terminal 4xx (will never land)"
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?.warn(
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?.warn(
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 z3 } from "zod";
2829
+ import { z as z4 } from "zod";
2528
2830
  function credentialsPath() {
2529
2831
  return join8(cabaneDir(), "credentials.json");
2530
2832
  }
2531
- var credentialStoreSchema = z3.record(z3.string(), z3.string());
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 z4 } from "zod";
2794
- var hostPolicySchema = z4.object({
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: z4.boolean(),
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: z4.boolean(),
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: z4.boolean(),
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: z4.boolean(),
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: z4.boolean(),
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: z4.literal("never"),
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: z4.literal("never")
3127
+ uiPrompts: z5.literal("never")
2826
3128
  });
2827
3129
 
2828
3130
  // packages/agent-runtime/src/turn-event.ts
2829
- import { z as z5 } from "zod";
2830
- var turnEventSchema = z5.discriminatedUnion("type", [
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
- z5.object({
2851
- type: z5.literal("session"),
2852
- state: z5.string(),
2853
- degraded: z5.boolean().optional()
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
- z5.object({ type: z5.literal("thinking"), text: z5.string() }),
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
- z5.object({ type: z5.literal("text"), body: z5.string(), terminal: z5.boolean() }),
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
- z5.object({ type: z5.literal("runtime_notice"), body: z5.string() }),
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
- z5.object({
2884
- type: z5.literal("tool"),
2885
- id: z5.string(),
2886
- name: z5.string(),
2887
- phase: z5.enum(["start", "done", "error"]),
2888
- summary: z5.string(),
2889
- input: z5.unknown().optional(),
2890
- result: z5.unknown().optional(),
2891
- mcpServer: z5.string().nullable().optional()
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
- z5.object({
2936
- type: z5.literal("result"),
2937
- ok: z5.boolean(),
2938
- reason: z5.string().optional(),
2939
- usage: z5.object({
2940
- inputTokens: z5.number(),
2941
- outputTokens: z5.number(),
2942
- cacheReadTokens: z5.number().optional(),
2943
- cacheCreationTokens: z5.number().optional(),
2944
- contextTokens: z5.number().optional(),
2945
- contextWindow: z5.number().optional()
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: z5.string().optional(),
2948
- resolvedConfig: z5.object({
2949
- effort: z5.string().optional(),
2950
- thinking: z5.string().optional(),
2951
- reasoningEffort: z5.string().optional()
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: z5.object({
2958
- initReceived: z5.boolean(),
2959
- servers: z5.array(z5.object({ name: z5.string(), status: z5.string() })),
2960
- toolCount: z5.number().int().nonnegative()
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 z6 } from "zod";
2967
- var turnResultReasonSchema = z6.discriminatedUnion("kind", [
2968
- z6.object({ kind: z6.literal("usage_capped"), resetsAt: z6.string().optional() }),
2969
- z6.object({ kind: z6.literal("rate_limited") }),
2970
- z6.object({ kind: z6.literal("server_error") }),
2971
- z6.object({ kind: z6.literal("auth_expired") }),
2972
- z6.object({ kind: z6.literal("no_result") }),
2973
- z6.object({ kind: z6.literal("empty_result") }),
2974
- z6.object({ kind: z6.literal("empty_result_unverified") }),
2975
- z6.object({ kind: z6.literal("timeout_idle") }),
2976
- z6.object({ kind: z6.literal("timeout_total") }),
2977
- z6.object({ kind: z6.literal("cancelled") }),
2978
- z6.object({ kind: z6.literal("lease_lost") }),
2979
- z6.object({ kind: z6.literal("skipped") }),
2980
- z6.object({ kind: z6.literal("session_start_failed") }),
2981
- z6.object({ kind: z6.literal("workspace_tools_missing") }),
2982
- z6.object({ kind: z6.literal("runtime_error") })
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 = z6.object({
2995
- outcome: z6.enum(turnOutcomes),
3296
+ var turnDiagnosticsSchema = z7.object({
3297
+ outcome: z7.enum(turnOutcomes),
2996
3298
  resultReason: turnResultReasonSchema.nullable(),
2997
- sessionMode: z6.enum(turnSessionModes),
2998
- sessionFingerprint: z6.string().regex(/^[a-f0-9]{16}$/).nullable(),
2999
- eventCounts: z6.object({
3000
- session: z6.number().int().nonnegative(),
3001
- text: z6.number().int().nonnegative(),
3002
- runtime_notice: z6.number().int().nonnegative(),
3003
- thinking: z6.number().int().nonnegative(),
3004
- tool: z6.number().int().nonnegative(),
3005
- result: z6.number().int().nonnegative()
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: z6.enum(turnRuntimeResultKinds).nullable(),
3008
- finalSource: z6.enum(turnFinalSources),
3009
- mcpInventory: z6.object({
3010
- initReceived: z6.boolean(),
3011
- servers: z6.array(z6.object({ name: z6.string(), status: z6.string() })),
3012
- toolCount: z6.number().int().nonnegative()
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(overrides = {}) {
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
- ...overrides
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(overrides = {}) {
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
- ...overrides
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(overrides = {}) {
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
- ...overrides
6684
+ ...overrides2
6505
6685
  };
6506
6686
  }
6507
6687
  var threadStarted = (threadId) => ({
@@ -7435,6 +7615,16 @@ var ConnectorHealthStore = class {
7435
7615
  import { existsSync as existsSync11, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
7436
7616
  import { join as join15 } from "path";
7437
7617
 
7618
+ // src/term.ts
7619
+ import { createInterface } from "readline";
7620
+ import { homedir as homedir2 } from "os";
7621
+ import pc from "picocolors";
7622
+ function tildePath(path) {
7623
+ const home = homedir2();
7624
+ if (home && path.startsWith(home + "/")) return `~${path.slice(home.length)}`;
7625
+ return path;
7626
+ }
7627
+
7438
7628
  // src/turn-execution.ts
7439
7629
  import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
7440
7630
  import { existsSync as existsSync10 } from "fs";
@@ -7701,53 +7891,6 @@ async function writeCodexInstructionsFile(contents) {
7701
7891
  };
7702
7892
  }
7703
7893
 
7704
- // src/api-error-shape.ts
7705
- var LEASE_REFUSALS = /* @__PURE__ */ new Set([
7706
- "dispatch_not_admitted",
7707
- "turn_already_ended",
7708
- "turn_belongs_elsewhere"
7709
- ]);
7710
- function apiErrorCode(err) {
7711
- if (!(err instanceof ApiError)) return null;
7712
- const body = err.body;
7713
- return typeof body === "object" && body !== null && typeof body.error === "string" ? body.error : null;
7714
- }
7715
- function leaseRefusal(err) {
7716
- const code = apiErrorCode(err);
7717
- if (code && LEASE_REFUSALS.has(code)) return code;
7718
- return null;
7719
- }
7720
- function isWriteFenceRefusal(err) {
7721
- return err instanceof ApiError && err.status === 409 && apiErrorCode(err) === "not_running";
7722
- }
7723
- var ERROR_BODY_LOG_CAP = 2e3;
7724
- function describeErrorBody(body) {
7725
- if (body === void 0 || body === null) return void 0;
7726
- let text;
7727
- if (typeof body === "string") {
7728
- text = body;
7729
- } else {
7730
- try {
7731
- text = JSON.stringify(body);
7732
- } catch {
7733
- text = String(body);
7734
- }
7735
- }
7736
- if (text.length === 0) return void 0;
7737
- return text.length > ERROR_BODY_LOG_CAP ? `${text.slice(0, ERROR_BODY_LOG_CAP)}\u2026` : text;
7738
- }
7739
- function apiErrorLogFields(err) {
7740
- const fields = {
7741
- err: err instanceof Error ? err.message : String(err)
7742
- };
7743
- if (err instanceof ApiError) {
7744
- fields.status = err.status;
7745
- const body = describeErrorBody(err.body);
7746
- if (body !== void 0) fields.responseBody = body;
7747
- }
7748
- return fields;
7749
- }
7750
-
7751
7894
  // src/turn-seq-floor.ts
7752
7895
  var SeqFloorUnavailable = class extends Error {
7753
7896
  constructor(detail) {
@@ -7763,7 +7906,7 @@ function resolveSeqFloor(sources, ctx) {
7763
7906
  throw new SeqFloorUnavailable("server sent no committed floor for a resumed turn");
7764
7907
  }
7765
7908
  const floor = Math.max(serverFloor, outboxFloor);
7766
- ctx.log.info(
7909
+ ctx.log.debug(
7767
7910
  { turnId: ctx.turnId, floor, outboxFloor, serverFloor },
7768
7911
  "companion: resumed turn \u2014 seq counter seeded above its committed high-water mark"
7769
7912
  );
@@ -7773,7 +7916,7 @@ function readOutboxFloor(read, turnId, log) {
7773
7916
  try {
7774
7917
  return read(turnId);
7775
7918
  } catch (err) {
7776
- log.error(
7919
+ log.debug(
7777
7920
  { turnId, err: err instanceof Error ? err.message : String(err) },
7778
7921
  "companion: the on-disk outbox floor is unreadable; refusing to resume"
7779
7922
  );
@@ -8015,7 +8158,10 @@ var TurnCommitter = class {
8015
8158
  constructor(deps) {
8016
8159
  this.deps = deps;
8017
8160
  this.onError = (err, hook) => {
8018
- deps.log.warn({ ...apiErrorLogFields(err), hook }, "dispatcher: transcript callback failed");
8161
+ deps.log.warn(
8162
+ { ...apiErrorLogFields(err), hook },
8163
+ `Couldn't save ${String(deps.log.bindings().agentName ?? "the agent")}'s ${hook === "text" ? "reply" : "reply or activity"} to Cabane. This companion may need an update: npm i -g @cabane/companion@latest`
8164
+ );
8019
8165
  deps.onCommitFailed?.(err);
8020
8166
  };
8021
8167
  const commit = {
@@ -8150,8 +8296,8 @@ async function postIncompleteNotice(ctx, harness, resolution) {
8150
8296
  return true;
8151
8297
  } catch (err) {
8152
8298
  ctx.log.warn(
8153
- { err: err instanceof Error ? err.message : String(err) },
8154
- "dispatcher: runtime-incomplete notice post failed"
8299
+ { ...apiErrorLogFields(err) },
8300
+ "Couldn't save the explanation to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
8155
8301
  );
8156
8302
  return false;
8157
8303
  }
@@ -8161,7 +8307,7 @@ async function checkBundledBinary(ctx) {
8161
8307
  if (!harness) return null;
8162
8308
  const resolution = resolve(ctx, harness);
8163
8309
  if (resolution.status === "present") return null;
8164
- ctx.log.error(
8310
+ ctx.log.debug(
8165
8311
  {
8166
8312
  runtime: harness,
8167
8313
  status: resolution.status,
@@ -8181,7 +8327,7 @@ async function reclassifyThrow(ctx, opts) {
8181
8327
  if (!harness || opts.aborted) return null;
8182
8328
  const resolution = resolve(ctx, harness);
8183
8329
  if (resolution.status === "present") return null;
8184
- ctx.log.error(
8330
+ ctx.log.debug(
8185
8331
  {
8186
8332
  runtime: harness,
8187
8333
  status: resolution.status,
@@ -8378,13 +8524,13 @@ var TurnExecution = class {
8378
8524
  );
8379
8525
  } catch (err) {
8380
8526
  turnLog.warn(
8381
- { err: err instanceof Error ? err.message : String(err) },
8382
- "dispatcher: pre-run active-run clear failed terminally; server age-sweep is the backstop"
8527
+ { ...apiErrorLogFields(err) },
8528
+ "Couldn't update the turn's status in Cabane. Check the conversation before trying again."
8383
8529
  );
8384
8530
  }
8385
8531
  const durationMs = Date.now() - startedAt;
8386
8532
  this.supervisor.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
8387
- return { ok: false, durationMs, reason };
8533
+ return { ok: false, durationMs, reason, runtime: this.turnContext?.runtime };
8388
8534
  }
8389
8535
  // The post-admission analogue of `concludeBeforeRun` (Codo's stack review,
8390
8536
  // blocking finding #2): this turn WAS admitted — it holds the conversation's
@@ -8414,12 +8560,12 @@ var TurnExecution = class {
8414
8560
  } catch (err) {
8415
8561
  turnLog.warn(
8416
8562
  { ...apiErrorLogFields(err), turnId },
8417
- "dispatcher: admitted-run settle failed terminally; server age-sweep is the backstop"
8563
+ "Couldn't save the turn's result to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
8418
8564
  );
8419
8565
  }
8420
8566
  const durationMs = Date.now() - startedAt;
8421
8567
  this.supervisor.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
8422
- return { ok: false, durationMs, reason };
8568
+ return { ok: false, durationMs, reason, runtime: this.turnContext?.runtime };
8423
8569
  }
8424
8570
  async fetchContext() {
8425
8571
  const { payload, turnId, turnLog } = this;
@@ -8436,20 +8582,20 @@ var TurnExecution = class {
8436
8582
  } catch (err) {
8437
8583
  const status = err instanceof ApiError ? err.status : 0;
8438
8584
  if (status === 404) {
8439
- turnLog.warn(
8585
+ turnLog.debug(
8440
8586
  { err: err instanceof Error ? err.message : String(err) },
8441
8587
  "dispatcher: turn-context 404 (stale event \u2014 conversation/message/participant gone); skipping"
8442
8588
  );
8443
8589
  throw this.concluded("turn_context_not_found");
8444
8590
  }
8445
8591
  if (status === 409 && apiErrorCode(err) === "dispatch_not_admitted") {
8446
- turnLog.warn(
8592
+ turnLog.debug(
8447
8593
  "dispatcher: turn-context 409 dispatch_not_admitted (wake already resolved); skipping"
8448
8594
  );
8449
8595
  throw this.concluded("turn_context_not_admitted");
8450
8596
  }
8451
8597
  const reason = err instanceof Error ? err.message : String(err);
8452
- turnLog.error({ err: reason }, "dispatcher: failed to fetch turn context");
8598
+ turnLog.debug({ err: reason }, "dispatcher: failed to fetch turn context");
8453
8599
  const fetchReason = `fetch_failed: ${reason}`;
8454
8600
  throw this.concluded(fetchReason, fetchReason);
8455
8601
  }
@@ -8459,7 +8605,7 @@ var TurnExecution = class {
8459
8605
  this.resumedFromSeq = resolveSeqFloor(sources, { turnId, log: turnLog });
8460
8606
  this.seqCounter = Math.max(this.seqCounter, this.resumedFromSeq);
8461
8607
  this.resumedSpoke = turnContext.turnSpoke === true || outboxSpoke;
8462
- if (this.resumedSpoke) turnLog.info({ turnId }, "dispatcher: resumed span already spoke");
8608
+ if (this.resumedSpoke) turnLog.debug({ turnId }, "dispatcher: resumed span already spoke");
8463
8609
  }
8464
8610
  }
8465
8611
  gateTrigger() {
@@ -8467,7 +8613,7 @@ var TurnExecution = class {
8467
8613
  const message = this.turnContext.message;
8468
8614
  const isDispatchableTrigger = message.role === "user" || message.role === "agent" || message.role === "system";
8469
8615
  if (!isDispatchableTrigger) {
8470
- turnLog.warn({ role: message.role }, "dispatcher: trigger role not dispatchable \u2014 skipping");
8616
+ turnLog.debug({ role: message.role }, "dispatcher: trigger role not dispatchable \u2014 skipping");
8471
8617
  throw this.concluded("unexpected_role", UNEXPECTED_ROLE_REASON);
8472
8618
  }
8473
8619
  this.supervisor.notifyStart({
@@ -8478,7 +8624,9 @@ var TurnExecution = class {
8478
8624
  }
8479
8625
  async resolveSecrets() {
8480
8626
  const { payload, workspaceId, turnLog } = this;
8481
- const secretStore = this.opts.secretStore ?? loadSecretStoreTolerant((m) => turnLog.warn(m));
8627
+ const secretStore = this.opts.secretStore ?? loadSecretStoreTolerant(
8628
+ (m) => turnLog.warn({ err: m }, "Could not read the secrets file. Check ~/.cabane/secrets.json.")
8629
+ );
8482
8630
  const { servers: resolvedMcpServers, missing } = resolveMcpSecrets(
8483
8631
  // CT262: the user MCP DEFINITIONS (placeholder form) come from the turn
8484
8632
  // context now, not a separate `getAgentSelf` run-config fetch.
@@ -8486,7 +8634,7 @@ var TurnExecution = class {
8486
8634
  secretStore
8487
8635
  );
8488
8636
  if (missing.length > 0) {
8489
- turnLog.error({ missing }, "dispatcher: turn needs secrets not declared on this companion");
8637
+ turnLog.debug({ missing }, "dispatcher: turn needs secrets not declared on this companion");
8490
8638
  const reason = missingSecretReason(missing);
8491
8639
  throw this.concluded(reason, reason);
8492
8640
  }
@@ -8501,7 +8649,7 @@ var TurnExecution = class {
8501
8649
  if (effectiveCwd && !existsSync10(effectiveCwd)) {
8502
8650
  turnLog.warn(
8503
8651
  { cwd: effectiveCwd },
8504
- "dispatcher: configured working directory does not exist on this device \u2014 falling back to the process cwd"
8652
+ "The working directory is missing; using the companion's directory instead. Check this agent's working directory setting."
8505
8653
  );
8506
8654
  effectiveCwd = void 0;
8507
8655
  }
@@ -8509,7 +8657,7 @@ var TurnExecution = class {
8509
8657
  if (prepareHook) {
8510
8658
  let cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
8511
8659
  if (cached2 && !checkoutState(cached2.cwd).ok) {
8512
- turnLog.warn(
8660
+ turnLog.debug(
8513
8661
  { cwd: cached2.cwd, reason: checkoutState(cached2.cwd).reason },
8514
8662
  "dispatcher: the prepared checkout is no longer usable \u2014 re-running the prepare hook to restore it"
8515
8663
  );
@@ -8536,7 +8684,7 @@ var TurnExecution = class {
8536
8684
  });
8537
8685
  } catch (err) {
8538
8686
  const reason = err instanceof Error ? err.message : String(err);
8539
- turnLog.error({ err: reason }, "dispatcher: prepare hook rejected a prepared turn");
8687
+ turnLog.debug({ err: reason }, "dispatcher: prepare hook rejected a prepared turn");
8540
8688
  const failReason = `prepare_failed: ${reason}`;
8541
8689
  throw this.concluded(failReason, failReason);
8542
8690
  }
@@ -8556,7 +8704,7 @@ var TurnExecution = class {
8556
8704
  phase,
8557
8705
  seq
8558
8706
  }).catch((err) => {
8559
- turnLog.warn(
8707
+ turnLog.debug(
8560
8708
  { err: err instanceof Error ? err.message : String(err), phase },
8561
8709
  "dispatcher: preparing-activity report failed (continuing with the hook)"
8562
8710
  );
@@ -8596,7 +8744,7 @@ var TurnExecution = class {
8596
8744
  clearTimeout(preparingTimer);
8597
8745
  if (preparingStarted) reportPreparing("error");
8598
8746
  const reason = err instanceof Error ? err.message : String(err);
8599
- turnLog.error({ err: reason }, "dispatcher: prepare hook failed");
8747
+ turnLog.debug({ err: reason }, "dispatcher: prepare hook failed");
8600
8748
  const failReason = `prepare_failed: ${reason}`;
8601
8749
  throw this.concluded(failReason, failReason);
8602
8750
  }
@@ -8644,14 +8792,14 @@ var TurnExecution = class {
8644
8792
  } catch (err) {
8645
8793
  const refusal = leaseRefusal(err);
8646
8794
  if (refusal === "dispatch_not_admitted" || refusal === "turn_already_ended") {
8647
- turnLog.error(
8795
+ turnLog.debug(
8648
8796
  { refusal, turnId },
8649
8797
  "dispatcher: refused a turn lease; not running the model"
8650
8798
  );
8651
8799
  throw this.concluded(`lease_refused: ${refusal}`);
8652
8800
  }
8653
8801
  const reason = refusal ? `lease_refused: ${refusal}` : "lease_unconfirmed";
8654
- turnLog.error(
8802
+ turnLog.debug(
8655
8803
  { refusal, turnId, err: err instanceof Error ? err.message : String(err) },
8656
8804
  "dispatcher: turn lease not confirmed; not running the model"
8657
8805
  );
@@ -8703,7 +8851,7 @@ var TurnExecution = class {
8703
8851
  async selectAdapter() {
8704
8852
  const { payload, workspaceId, turnId, turnLog } = this;
8705
8853
  await guardBundledBinary(this.integrityContext(), (r, e) => this.concluded(r, e), this.outcome);
8706
- const onWarn = (msg, meta) => turnLog.warn(meta ?? {}, msg);
8854
+ const onWarn = (msg, meta) => turnLog.debug(meta ?? {}, msg);
8707
8855
  const adapters = [];
8708
8856
  if (this.opts.claudeCodeAvailable?.() ?? true) {
8709
8857
  adapters.push(createClaudeCodeAdapter({ queryFn: this.opts.queryFn, onWarn }));
@@ -8725,7 +8873,7 @@ var TurnExecution = class {
8725
8873
  this.adapter = selectAdapter(registry, this.turnContext.runtime);
8726
8874
  } catch (err) {
8727
8875
  if (!(err instanceof RuntimeUnavailableError)) throw err;
8728
- turnLog.error(
8876
+ turnLog.debug(
8729
8877
  { runtime: err.runtime, available: err.available },
8730
8878
  "dispatcher: turn runtime not available on this device"
8731
8879
  );
@@ -8738,8 +8886,8 @@ var TurnExecution = class {
8738
8886
  });
8739
8887
  } catch (postErr) {
8740
8888
  turnLog.warn(
8741
- { err: postErr instanceof Error ? postErr.message : String(postErr) },
8742
- "dispatcher: runtime-unavailable notice post failed"
8889
+ { ...apiErrorLogFields(postErr) },
8890
+ "Couldn't save the explanation to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
8743
8891
  );
8744
8892
  const reason = `runtime_unavailable:${err.runtime}`;
8745
8893
  throw this.concluded(reason, reason);
@@ -8767,7 +8915,7 @@ var TurnExecution = class {
8767
8915
  false,
8768
8916
  true
8769
8917
  );
8770
- turnLog.warn(
8918
+ turnLog.debug(
8771
8919
  { reason },
8772
8920
  "dispatcher: stored session not resumed \u2014 the turn context was recomposed for a fresh session"
8773
8921
  );
@@ -8778,8 +8926,8 @@ var TurnExecution = class {
8778
8926
  };
8779
8927
  } catch (err) {
8780
8928
  turnLog.error(
8781
- { reason, err: err instanceof Error ? err.message : String(err) },
8782
- "dispatcher: could not recompose the turn context for a fresh session \u2014 running on the incremental one, degraded"
8929
+ { reason, ...apiErrorLogFields(err) },
8930
+ "Couldn't load the full conversation; continuing with the recent messages."
8783
8931
  );
8784
8932
  return null;
8785
8933
  }
@@ -8800,7 +8948,10 @@ var TurnExecution = class {
8800
8948
  dispatchId: this.dispatchId,
8801
8949
  message: this.turnContext.message.body
8802
8950
  },
8803
- (m) => turnLog.warn(m)
8951
+ (m) => turnLog.warn(
8952
+ { err: m },
8953
+ "Could not save the transcript. Check free disk space and access to ~/.cabane."
8954
+ )
8804
8955
  ) : null;
8805
8956
  const turnRuntime = this.turnContext.runtime;
8806
8957
  const committer = this.committer = new TurnCommitter({
@@ -8840,8 +8991,8 @@ var TurnExecution = class {
8840
8991
  }
8841
8992
  } catch (err) {
8842
8993
  turnLog.warn(
8843
- { err: err instanceof Error ? err.message : String(err) },
8844
- "dispatcher: turn-control intent fetch failed; the skip for this turn is dropped"
8994
+ { ...apiErrorLogFields(err) },
8995
+ "Couldn't check whether the agent chose to pass; the turn may appear to have failed."
8845
8996
  );
8846
8997
  }
8847
8998
  };
@@ -8849,7 +9000,7 @@ var TurnExecution = class {
8849
9000
  const fireTimeout = (reason) => {
8850
9001
  if (abortController.signal.aborted) return;
8851
9002
  o.timeoutReason = reason;
8852
- turnLog.warn(
9003
+ turnLog.debug(
8853
9004
  { reason, idleTimeoutMs, totalTimeoutMs },
8854
9005
  "dispatcher: turn timeout \u2014 aborting"
8855
9006
  );
@@ -8879,7 +9030,7 @@ var TurnExecution = class {
8879
9030
  if (state !== "ended") return;
8880
9031
  if (abortController.signal.aborted) return;
8881
9032
  o.leaseLost = true;
8882
- turnLog.warn(
9033
+ turnLog.debug(
8883
9034
  { turnId, trigger },
8884
9035
  "dispatcher: this turn is no longer running server-side \u2014 aborting the loop"
8885
9036
  );
@@ -8907,7 +9058,7 @@ var TurnExecution = class {
8907
9058
  if (isContentBearingEvent(event)) o.contentBearingEvents += 1;
8908
9059
  armIdle();
8909
9060
  if (abortController.signal.aborted) {
8910
- turnLog.info("dispatcher: aborted mid-turn");
9061
+ turnLog.debug("dispatcher: aborted mid-turn");
8911
9062
  o.okResult = false;
8912
9063
  o.resultReason = o.timeoutReason ?? "cancelled";
8913
9064
  break;
@@ -8930,18 +9081,18 @@ var TurnExecution = class {
8930
9081
  o.sessionWriteRejected = true;
8931
9082
  turnLog.error(
8932
9083
  {
8933
- err: err instanceof Error ? err.message : String(err),
9084
+ ...apiErrorLogFields(err),
8934
9085
  status,
8935
9086
  responseBody: describeErrorBody(err instanceof ApiError ? err.body : void 0),
8936
9087
  stateLength: event.state.length,
8937
9088
  runtime: turnRuntime
8938
9089
  },
8939
- "dispatcher: session-state write REJECTED (4xx) \u2014 this turn is not resumable and the next turn starts with no memory of it"
9090
+ "Couldn't save the agent's memory in Cabane. The next turn will start a new session. This companion may need an update: npm i -g @cabane/companion@latest"
8940
9091
  );
8941
9092
  } else {
8942
9093
  turnLog.warn(
8943
- { err: err instanceof Error ? err.message : String(err) },
8944
- "dispatcher: session-id write failed (will retry next turn)"
9094
+ { ...apiErrorLogFields(err) },
9095
+ "Couldn't save the agent's memory; trying again next turn."
8945
9096
  );
8946
9097
  }
8947
9098
  }
@@ -8982,7 +9133,7 @@ var TurnExecution = class {
8982
9133
  await this.applyRecordedTurnControlIntent();
8983
9134
  }
8984
9135
  if (!abortController.signal.aborted && this.skipState.skipped) {
8985
- turnLog.info(
9136
+ turnLog.debug(
8986
9137
  { reason: this.skipState.reason, turnId, ok: o.okResult },
8987
9138
  "agent skipped turn (skip_turn)"
8988
9139
  );
@@ -8990,7 +9141,7 @@ var TurnExecution = class {
8990
9141
  } catch (err) {
8991
9142
  o.okResult = false;
8992
9143
  o.resultReason = err instanceof Error ? err.message : String(err);
8993
- turnLog.error({ err: o.resultReason }, "dispatcher: SDK query threw");
9144
+ turnLog.debug({ err: o.resultReason }, "dispatcher: SDK query threw");
8994
9145
  o.runtimeIncomplete = await absorbBundledLoss(
8995
9146
  this.integrityContext(),
8996
9147
  abortController.signal.aborted,
@@ -9015,7 +9166,7 @@ var TurnExecution = class {
9015
9166
  kind: "usage_capped",
9016
9167
  resetsAt: health.limitedUntil
9017
9168
  });
9018
- turnLog.info(
9169
+ turnLog.debug(
9019
9170
  { runtime: turnRuntime, limitedUntil: health.limitedUntil },
9020
9171
  "dispatcher: idle-timeout on a capped runtime \u2014 reclassified as usage_capped (CT639)"
9021
9172
  );
@@ -9066,7 +9217,7 @@ var TurnExecution = class {
9066
9217
  if (diagnosticReason && !["usage_capped", "rate_limited", "auth_expired", "cancelled", "skipped"].includes(
9067
9218
  diagnosticReason.kind
9068
9219
  )) {
9069
- turnLog.warn(
9220
+ turnLog.debug(
9070
9221
  {
9071
9222
  workspaceId,
9072
9223
  turnId,
@@ -9094,8 +9245,8 @@ var TurnExecution = class {
9094
9245
  );
9095
9246
  } catch (err) {
9096
9247
  turnLog.warn(
9097
- { err: err instanceof Error ? err.message : String(err) },
9098
- "dispatcher: active-run clear failed terminally; server age-sweep is the backstop"
9248
+ { ...apiErrorLogFields(err) },
9249
+ "Couldn't save the turn's result to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
9099
9250
  );
9100
9251
  }
9101
9252
  this.supervisor.releaseAbort(turnId, abortController);
@@ -9112,9 +9263,6 @@ var TurnExecution = class {
9112
9263
  ...o.resultReason ? { reason: o.resultReason } : {},
9113
9264
  durationMs
9114
9265
  });
9115
- if (!o.okResult && o.resultReason !== "cancelled") {
9116
- turnLog.info(`turn failed \u2014 full this.transcript: ${this.transcript.path}`);
9117
- }
9118
9266
  if (o.settledDiagnostics?.resultReason?.kind === "empty_result" || o.settledDiagnostics?.resultReason?.kind === "empty_result_unverified") {
9119
9267
  this.transcript.preserveAnomaly();
9120
9268
  }
@@ -9139,7 +9287,9 @@ var TurnExecution = class {
9139
9287
  return {
9140
9288
  ok: false,
9141
9289
  durationMs,
9142
- ...o.resultReason ? { reason: o.resultReason } : {}
9290
+ ...o.resultReason ? { reason: o.resultReason } : {},
9291
+ runtime: this.turnContext.runtime,
9292
+ ...this.transcript ? { transcriptPath: tildePath(this.transcript.path) } : {}
9143
9293
  };
9144
9294
  }
9145
9295
  };
@@ -9345,7 +9495,7 @@ var Outbox = class {
9345
9495
  }
9346
9496
  this.log?.warn(
9347
9497
  { workspaceId: this.workspaceId, err: err instanceof Error ? err.message : String(err) },
9348
- "companion outbox: failed to persist entry"
9498
+ "Couldn't save an update on this machine. Check free disk space and access to ~/.cabane."
9349
9499
  );
9350
9500
  return;
9351
9501
  }
@@ -9433,7 +9583,7 @@ var Outbox = class {
9433
9583
  dropCorrupt(full) {
9434
9584
  this.log?.warn(
9435
9585
  { workspaceId: this.workspaceId, file: full },
9436
- "companion outbox: dropping unreadable entry"
9586
+ "A saved update is unreadable and couldn't be delivered. Check the conversation for missing replies."
9437
9587
  );
9438
9588
  try {
9439
9589
  rmSync7(full, { force: true });
@@ -9450,7 +9600,7 @@ var Outbox = class {
9450
9600
  if (now - e.enqueuedAt > MAX_AGE_MS) {
9451
9601
  this.log?.warn(
9452
9602
  { workspaceId: this.workspaceId, turnId: e.turnId, seq: e.seq, kind: e.kind },
9453
- "companion outbox: evicting entry past max age (undeliverable)"
9603
+ "A saved update waited too long and couldn't be delivered. Check the conversation for missing replies."
9454
9604
  );
9455
9605
  this.remove(e.turnId, e.seq);
9456
9606
  } else {
@@ -9462,7 +9612,7 @@ var Outbox = class {
9462
9612
  for (const e of survivors.slice(0, overflow)) {
9463
9613
  this.log?.warn(
9464
9614
  { workspaceId: this.workspaceId, turnId: e.turnId, seq: e.seq, kind: e.kind },
9465
- "companion outbox: evicting oldest entry past max size"
9615
+ "Too many updates are waiting; the oldest couldn't be kept. Check the connection to Cabane."
9466
9616
  );
9467
9617
  this.remove(e.turnId, e.seq);
9468
9618
  }
@@ -9548,11 +9698,14 @@ var SseSubscriber = class {
9548
9698
  try {
9549
9699
  await this.connect();
9550
9700
  backoff = 500;
9551
- if (!this.aborted) await sleep2(backoff);
9701
+ if (!this.aborted) {
9702
+ this.opts.log.warn("Lost the connection to Cabane; reconnecting");
9703
+ await sleep2(backoff);
9704
+ }
9552
9705
  } catch (err) {
9553
9706
  if (this.aborted) return;
9554
9707
  if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
9555
- this.opts.log.error(
9708
+ this.opts.log.debug(
9556
9709
  { workspaceId: this.opts.workspaceId, status: err.status },
9557
9710
  "SSE auth failed \u2014 tearing down this workspace subscriber"
9558
9711
  );
@@ -9565,7 +9718,7 @@ var SseSubscriber = class {
9565
9718
  err: err instanceof Error ? err.message : String(err),
9566
9719
  backoff
9567
9720
  },
9568
- "SSE disconnected; reconnecting"
9721
+ "Lost the connection to Cabane; reconnecting"
9569
9722
  );
9570
9723
  await sleep2(backoff);
9571
9724
  backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
@@ -9589,7 +9742,7 @@ var SseSubscriber = class {
9589
9742
  if (!res.body) {
9590
9743
  throw new Error("SSE response has no body");
9591
9744
  }
9592
- this.opts.log.info({ workspaceId: this.opts.workspaceId }, "SSE connected");
9745
+ this.opts.log.debug({ workspaceId: this.opts.workspaceId }, "SSE connected");
9593
9746
  let opened = true;
9594
9747
  this.opts.onOpen?.();
9595
9748
  const parser = createParser({
@@ -9719,15 +9872,12 @@ var CompanionSupervisor = class {
9719
9872
  async start() {
9720
9873
  this.log.info(
9721
9874
  { protocolVersion: TURN_PROTOCOL_VERSION, version: COMPANION_VERSION },
9722
- "companion: starting"
9875
+ `Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ?? hostname()}`
9723
9876
  );
9724
9877
  const startupHarnessRefresh = this.refreshHarnessStatuses();
9725
9878
  this.trackHeartbeat(startupHarnessRefresh);
9726
9879
  if (!this.config.deviceToken) {
9727
- this.log.warn("companion: not paired (no device token) \u2014 run `cabane-companion pair`");
9728
- process.stdout.write(
9729
- "companion: this device is not paired \u2014 run `cabane-companion pair`, then confirm the short code in Settings \u2192 Connectors.\n"
9730
- );
9880
+ this.log.warn("This device isn't paired. Run cabane-companion start to pair it.");
9731
9881
  return;
9732
9882
  }
9733
9883
  this.deviceApi = new DeviceApi({
@@ -9741,6 +9891,7 @@ var CompanionSupervisor = class {
9741
9891
  token: this.config.deviceToken,
9742
9892
  log: this.log,
9743
9893
  lastEventId: null,
9894
+ onOpen: () => this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ?? hostname()}`),
9744
9895
  onMessage: async (ev) => {
9745
9896
  if (ev.event === "assignments_changed") {
9746
9897
  if (this.refreshing && this.inFlightRefresh) await this.inFlightRefresh;
@@ -9748,11 +9899,13 @@ var CompanionSupervisor = class {
9748
9899
  }
9749
9900
  },
9750
9901
  onAuthFailure: () => {
9751
- this.log.error("companion: device control stream authentication failed");
9902
+ this.log.error(
9903
+ "Cabane no longer accepts this device. Run cabane-companion logout, then cabane-companion start to pair again."
9904
+ );
9752
9905
  }
9753
9906
  });
9754
- this.deviceSub.start();
9755
9907
  await this.refreshAssignments();
9908
+ this.deviceSub.start();
9756
9909
  const firstHeartbeat = startupHarnessRefresh.then(async () => {
9757
9910
  if (this.stopped) return;
9758
9911
  await this.sendHeartbeat();
@@ -9788,7 +9941,9 @@ var CompanionSupervisor = class {
9788
9941
  if (!this.deviceApi) return;
9789
9942
  await this.refreshHarnessStatuses();
9790
9943
  try {
9791
- const store = loadSecretStoreTolerant((m) => this.log.warn({ msg: m }, "companion: secrets"));
9944
+ const store = loadSecretStoreTolerant(
9945
+ (m) => this.log.warn({ err: m }, "Could not read the secrets file. Check ~/.cabane/secrets.json.")
9946
+ );
9792
9947
  const connectorReports = this.connectorHealth.reports();
9793
9948
  const opencodeModels = this.config.opencode?.serverUrl ? await enumerateOpencodeModels(this.config.opencode.serverUrl) : null;
9794
9949
  const res = await this.deviceApi.heartbeat({
@@ -9835,8 +9990,8 @@ var CompanionSupervisor = class {
9835
9990
  void this.recoverRuns();
9836
9991
  } catch (err) {
9837
9992
  this.log.warn(
9838
- { err: err instanceof Error ? err.message : String(err) },
9839
- "companion: device heartbeat failed (will retry on next tick)"
9993
+ { ...apiErrorLogFields(err) },
9994
+ "Couldn't check in with Cabane; trying again shortly."
9840
9995
  );
9841
9996
  }
9842
9997
  }
@@ -9851,7 +10006,7 @@ var CompanionSupervisor = class {
9851
10006
  this.versionSkewWarned = true;
9852
10007
  this.log.warn(
9853
10008
  { companionVersion: COMPANION_VERSION, serverVersion },
9854
- "companion: VERSION SKEW \u2014 this companion and its server were built from different versions. Turns may misbehave. The self-host artifact ships a matching companion; run `cabane update` so the companion matches its server."
10009
+ "This companion and Cabane are on different versions. If turns fail, update with npm i -g @cabane/companion@latest"
9855
10010
  );
9856
10011
  }
9857
10012
  // Pull assignments and reconcile the live runner set against them. Re-entrancy
@@ -9873,8 +10028,8 @@ var CompanionSupervisor = class {
9873
10028
  device = resp.device;
9874
10029
  } catch (err) {
9875
10030
  this.log.error(
9876
- { err: err instanceof Error ? err.message : String(err) },
9877
- "companion: assignments pull failed \u2014 check the device is still active in the cabane app"
10031
+ { ...apiErrorLogFields(err) },
10032
+ "Couldn't load the assigned agents. Check that this device is active in Cabane; trying again shortly."
9878
10033
  );
9879
10034
  this.hub.setDeviceError(err instanceof Error ? err.message : String(err));
9880
10035
  return;
@@ -9940,14 +10095,17 @@ var CompanionSupervisor = class {
9940
10095
  const credential = it.credential ?? getCredential(it.agentId);
9941
10096
  const runConfig = parseRunConfig(
9942
10097
  it.runConfig,
9943
- (m) => this.log.warn({ agentId: it.agentId, msg: m }, "companion: run-config")
10098
+ (m) => this.log.warn(
10099
+ { agentId: it.agentId, agentName: it.agentDisplayName, err: m },
10100
+ "Some agent settings could not be read; using defaults. Check the agent settings in Cabane."
10101
+ )
9944
10102
  );
9945
10103
  const required = requiredSecretNames(runConfig.mcpServers);
9946
10104
  const missing = required.filter((n) => !exposed.has(n));
9947
10105
  if (!credential) {
9948
10106
  this.log.error(
9949
10107
  { workspaceId, agentId: it.agentId, username: it.agentUsername },
9950
- "companion: agent assigned but no credential on this device \u2014 re-assign it in the cabane app"
10108
+ `Cannot run ${it.agentDisplayName}: its sign-in to Cabane is missing. Reassign it to this device in Cabane.`
9951
10109
  );
9952
10110
  this.removeAgent(wr, it.agentId);
9953
10111
  this.hub.setAgent(workspaceId, {
@@ -9981,19 +10139,20 @@ var CompanionSupervisor = class {
9981
10139
  if (missing.length > 0) {
9982
10140
  this.log.warn(
9983
10141
  { workspaceId, agentId: it.agentId, missing },
9984
- "companion: agent needs secrets this device does not expose (turns using them will fail)"
10142
+ `${it.agentDisplayName} needs secrets this machine does not have: ${missing.join(", ")}. Add them to ~/.cabane/secrets.json.`
9985
10143
  );
9986
10144
  }
9987
10145
  }
9988
10146
  this.ensureWorkspaceSse(wr);
9989
10147
  }
9990
10148
  addAgent(wr, it, credential, runConfig) {
9991
- const outbox = new Outbox(it.agentId, this.log);
10149
+ const agentLog = this.log.child({ agentName: it.agentDisplayName });
10150
+ const outbox = new Outbox(it.agentId, agentLog);
9992
10151
  const api = new CabaneApi({
9993
10152
  baseUrl: this.config.baseUrl,
9994
10153
  token: credential,
9995
10154
  outbox,
9996
- log: this.log
10155
+ log: agentLog
9997
10156
  });
9998
10157
  const aborts = /* @__PURE__ */ new Map();
9999
10158
  const dispatcher = this.buildDispatcher({
@@ -10003,6 +10162,7 @@ var CompanionSupervisor = class {
10003
10162
  workspaceSlug: it.workspaceSlug,
10004
10163
  agentId: it.agentId,
10005
10164
  agentUsername: it.agentUsername,
10165
+ agentDisplayName: it.agentDisplayName,
10006
10166
  credential,
10007
10167
  runConfig,
10008
10168
  aborts
@@ -10023,7 +10183,7 @@ var CompanionSupervisor = class {
10023
10183
  drain2.kick();
10024
10184
  this.log.info(
10025
10185
  { workspaceId: it.workspaceId, agentId: it.agentId, username: it.agentUsername },
10026
- "companion: running agent"
10186
+ `Running ${it.agentDisplayName}`
10027
10187
  );
10028
10188
  }
10029
10189
  removeAgent(wr, agentId) {
@@ -10034,7 +10194,7 @@ var CompanionSupervisor = class {
10034
10194
  this.hub.removeAgent(wr.workspaceId, agentId);
10035
10195
  this.log.info(
10036
10196
  { workspaceId: wr.workspaceId, agentId },
10037
- "companion: stopped agent (unassigned)"
10197
+ `Stopped running ${runner.displayName} (unassigned)`
10038
10198
  );
10039
10199
  }
10040
10200
  // CT1379: can this machine LAUNCH the harness — is the SDK's own bundled
@@ -10107,7 +10267,11 @@ var CompanionSupervisor = class {
10107
10267
  const meta = { runtime, platform: resolution.platform, arch: resolution.arch, status };
10108
10268
  if (status === "present") {
10109
10269
  const say = intended ? this.log.info : this.log.debug;
10110
- say.call(this.log, meta, "companion: bundled harness binary is present again");
10270
+ say.call(
10271
+ this.log,
10272
+ meta,
10273
+ `${runtime === "codex" ? "Codex" : "Claude Code"} is available again`
10274
+ );
10111
10275
  continue;
10112
10276
  }
10113
10277
  if (intended) this.log.warn(meta, startupWarning(resolution));
@@ -10153,7 +10317,7 @@ var CompanionSupervisor = class {
10153
10317
  local,
10154
10318
  aborts: ctx.aborts,
10155
10319
  runConfig: ctx.runConfig,
10156
- log: this.log,
10320
+ log: this.log.child({ agentName: ctx.agentDisplayName ?? ctx.agentUsername }),
10157
10321
  // CT833: register the claude-code adapter only when this device actually
10158
10322
  // offers claude-code — read per turn (not captured here), so a harness
10159
10323
  // installed or connected after boot works on the next turn exactly as it
@@ -10211,7 +10375,7 @@ var CompanionSupervisor = class {
10211
10375
  onMessage: (ev) => this.handleWorkspaceMessage(wr, ev),
10212
10376
  onAuthFailure: (status) => {
10213
10377
  this.hub.setAuthFailed(wr.workspaceId);
10214
- this.log.error(
10378
+ this.log.debug(
10215
10379
  { workspaceId: wr.workspaceId, status, sseAgentId: wr.sseAgentId },
10216
10380
  "companion: workspace stream auth failed \u2014 re-pulling assignments"
10217
10381
  );
@@ -10220,7 +10384,7 @@ var CompanionSupervisor = class {
10220
10384
  }
10221
10385
  });
10222
10386
  wr.sub.start();
10223
- this.log.info({ workspaceId: wr.workspaceId }, "companion: subscribed");
10387
+ this.log.debug({ workspaceId: wr.workspaceId }, "companion: subscribed");
10224
10388
  }
10225
10389
  async removeWorkspace(workspaceId) {
10226
10390
  const wr = this.workspaces.get(workspaceId);
@@ -10244,7 +10408,7 @@ var CompanionSupervisor = class {
10244
10408
  try {
10245
10409
  wire = JSON.parse(ev.data);
10246
10410
  } catch (err) {
10247
- this.log.warn(
10411
+ this.log.debug(
10248
10412
  { err: err instanceof Error ? err.message : String(err) },
10249
10413
  "malformed SSE payload"
10250
10414
  );
@@ -10260,7 +10424,7 @@ var CompanionSupervisor = class {
10260
10424
  if (agent2) {
10261
10425
  const aborted = agent2.dispatcher.cancel(payload2.conversationId, payload2.agentId);
10262
10426
  if (aborted) {
10263
- this.log.info(
10427
+ this.log.debug(
10264
10428
  {
10265
10429
  workspaceId: wr.workspaceId,
10266
10430
  conversationId: payload2.conversationId,
@@ -10314,9 +10478,9 @@ var CompanionSupervisor = class {
10314
10478
  workspaceId: wr.workspaceId,
10315
10479
  conversationId: payload.conversationId,
10316
10480
  agentId: payload.agentId,
10317
- err: err instanceof Error ? err.message : String(err)
10481
+ ...apiErrorLogFields(err)
10318
10482
  },
10319
- "companion: conversation turn handler threw"
10483
+ "The turn stopped unexpectedly. Reply in Cabane to try again."
10320
10484
  );
10321
10485
  });
10322
10486
  wr.chains.set(chainKey, tail);
@@ -10349,7 +10513,7 @@ var CompanionSupervisor = class {
10349
10513
  messageId: payload.messageId,
10350
10514
  reason: "agent_not_on_device"
10351
10515
  });
10352
- this.log.warn(
10516
+ this.log.debug(
10353
10517
  {
10354
10518
  workspaceId: wr.workspaceId,
10355
10519
  conversationId: payload.conversationId,
@@ -10370,7 +10534,7 @@ var CompanionSupervisor = class {
10370
10534
  agentId: payload.agentId,
10371
10535
  err: lastErr instanceof Error ? lastErr.message : String(lastErr)
10372
10536
  },
10373
- "companion: could not report an unrunnable dispatch \u2014 leaving it unsettled so a reconnect replays it"
10537
+ "Couldn't tell Cabane that this agent no longer runs here; trying again when the connection returns."
10374
10538
  );
10375
10539
  return false;
10376
10540
  }
@@ -10425,7 +10589,7 @@ var CompanionSupervisor = class {
10425
10589
  agent,
10426
10590
  run.turnId
10427
10591
  ).catch(
10428
- (err) => this.log.warn({ err, turnId: run.turnId }, "companion: restart recovery failed")
10592
+ (err) => this.log.debug({ err, turnId: run.turnId }, "companion: restart recovery failed")
10429
10593
  ).finally(() => {
10430
10594
  if (wr.chains.get(key) === tail) wr.chains.delete(key);
10431
10595
  });
@@ -10436,7 +10600,7 @@ var CompanionSupervisor = class {
10436
10600
  this.recoveryChecked = page.nextCursor === null;
10437
10601
  }
10438
10602
  } catch (err) {
10439
- this.log.warn({ err }, "companion: run recovery read failed (will retry)");
10603
+ this.log.debug({ err }, "companion: run recovery read failed (will retry)");
10440
10604
  } finally {
10441
10605
  this.recovering = false;
10442
10606
  }
@@ -10448,7 +10612,7 @@ var CompanionSupervisor = class {
10448
10612
  const workspaceId = wr.workspaceId;
10449
10613
  if (ev.id && hasDispatched(workspaceId, ev.id)) {
10450
10614
  if (hasCompleted(workspaceId, ev.id)) {
10451
- this.log.info(
10615
+ this.log.debug(
10452
10616
  { workspaceId, eventId: ev.id },
10453
10617
  "companion: skipping already-completed event (resume after restart)"
10454
10618
  );
@@ -10456,7 +10620,7 @@ var CompanionSupervisor = class {
10456
10620
  return;
10457
10621
  }
10458
10622
  if (noResume()) {
10459
- this.log.warn(
10623
+ this.log.debug(
10460
10624
  { workspaceId, eventId: ev.id },
10461
10625
  "companion: skipping interrupted turn (CABANE_COMPANION_NO_RESUME=1) \u2014 resume disabled for this boot"
10462
10626
  );
@@ -10468,13 +10632,13 @@ var CompanionSupervisor = class {
10468
10632
  if (attempt > MAX_RESUME_ATTEMPTS) {
10469
10633
  this.log.error(
10470
10634
  { workspaceId, eventId: ev.id, attempt, cap: MAX_RESUME_ATTEMPTS },
10471
- "companion: giving up on an interrupted turn after too many resume attempts \u2014 retiring it so boot is never wedged (run `cabane companion reset` to clear resume state)"
10635
+ "An interrupted turn couldn't finish after several attempts. Reply in Cabane to try again."
10472
10636
  );
10473
10637
  markCompleted(workspaceId, ev.id);
10474
10638
  wr.cursor.settle(ev.id);
10475
10639
  return;
10476
10640
  }
10477
- this.log.info(
10641
+ this.log.debug(
10478
10642
  { workspaceId, eventId: ev.id, attempt, cap: MAX_RESUME_ATTEMPTS },
10479
10643
  "companion: re-dispatching interrupted turn (resume after restart)"
10480
10644
  );
@@ -10484,7 +10648,7 @@ var CompanionSupervisor = class {
10484
10648
  if (ev.id) {
10485
10649
  const liveTurnIds = rememberTurnId(workspaceId, ev.id, turnId);
10486
10650
  if (liveTurnIds > TURN_ID_OVERFLOW_WARN) {
10487
- this.log.error(
10651
+ this.log.debug(
10488
10652
  { workspaceId, liveTurnIds, threshold: TURN_ID_OVERFLOW_WARN },
10489
10653
  "companion: in-flight turn-id map is implausibly large \u2014 completion pruning is likely broken. Keeping every mapping; dropping one would silently lose a resumable turn."
10490
10654
  );
@@ -10492,11 +10656,18 @@ var CompanionSupervisor = class {
10492
10656
  markDispatched(workspaceId, ev.id);
10493
10657
  }
10494
10658
  if (resumedTurnId) {
10495
- this.log.info(
10659
+ this.log.debug(
10496
10660
  { workspaceId, eventId: ev.id, turnId },
10497
10661
  "companion: resuming an interrupted turn under its original id"
10498
10662
  );
10499
10663
  }
10664
+ const turnBindings = {
10665
+ agentName: agent.displayName,
10666
+ conversationId: payload.conversationId,
10667
+ agentId: agent.agentId,
10668
+ workspaceId
10669
+ };
10670
+ this.log.info(turnBindings, "Turn started");
10500
10671
  const result = await agent.dispatcher.handle(payload, {
10501
10672
  turnId,
10502
10673
  resumed: resumedTurnId !== null
@@ -10505,16 +10676,23 @@ var CompanionSupervisor = class {
10505
10676
  if (ev.id) wr.cursor.settle(ev.id);
10506
10677
  const durationS = (result.durationMs / 1e3).toFixed(1);
10507
10678
  const bindings = {
10508
- workspaceId,
10509
- conversationId: payload.conversationId,
10510
- agentId: agent.agentId
10679
+ ...turnBindings,
10680
+ ...result.transcriptPath ? { transcriptPath: result.transcriptPath } : {}
10511
10681
  };
10512
10682
  if (result.ok) {
10513
- this.log.info(bindings, `${wr.slug}/${agent.username} \u2014 replied (${durationS}s)`);
10683
+ this.log.info(bindings, `Replied (${durationS}s)`);
10684
+ } else if (result.reason === "cancelled") {
10685
+ this.log.info(turnBindings, `Turn stopped (${durationS}s)`);
10686
+ } else if ([
10687
+ "turn_context_not_found",
10688
+ "turn_context_not_admitted",
10689
+ "trigger_role_not_dispatchable"
10690
+ ].includes(result.reason ?? "") || result.reason === "lease_refused: dispatch_not_admitted" || result.reason === "lease_refused: turn_already_ended") {
10691
+ this.log.debug({ ...turnBindings, reason: result.reason }, "Dispatch no longer needed");
10514
10692
  } else {
10515
- this.log.warn(
10693
+ this.log.info(
10516
10694
  bindings,
10517
- `${wr.slug}/${agent.username} \u2014 dispatch failed: ${result.reason ?? "unknown"} (${durationS}s)`
10695
+ `Turn failed after ${durationS}s: ${turnFailureCopy(result.reason, result.runtime)}`
10518
10696
  );
10519
10697
  }
10520
10698
  }
@@ -10534,7 +10712,7 @@ var CompanionSupervisor = class {
10534
10712
  }
10535
10713
  } catch (err) {
10536
10714
  drainDelay = Math.min(drainDelay * 2, DRAIN_MAX_MS);
10537
- this.log.warn(
10715
+ this.log.debug(
10538
10716
  { agentId, err: err instanceof Error ? err.message : String(err) },
10539
10717
  "companion: outbox drain pass threw (will retry with backoff)"
10540
10718
  );
@@ -10569,7 +10747,7 @@ var CompanionSupervisor = class {
10569
10747
  const next = loadConfig();
10570
10748
  if (!next) return;
10571
10749
  this.config = next;
10572
- this.log.level = next.logLevel ?? "debug";
10750
+ configureLogger(this.log, next);
10573
10751
  this.rebuildDispatchers();
10574
10752
  void this.refreshAssignments();
10575
10753
  }
@@ -10585,7 +10763,7 @@ var CompanionSupervisor = class {
10585
10763
  };
10586
10764
  this.config = next;
10587
10765
  saveConfig(next);
10588
- this.log.level = next.logLevel ?? "debug";
10766
+ configureLogger(this.log, next);
10589
10767
  }
10590
10768
  // ---- harnesses (CT586) ----
10591
10769
  // Probe all three harnesses, cache the signals + versions, and push the derived
@@ -10611,7 +10789,7 @@ var CompanionSupervisor = class {
10611
10789
  };
10612
10790
  this.hub.setHarnesses(deriveHarnessSnapshot(signals).harnesses);
10613
10791
  } catch (err) {
10614
- this.log.warn(
10792
+ this.log.debug(
10615
10793
  { err: err instanceof Error ? err.message : String(err) },
10616
10794
  "companion: harness probe failed (will retry on next beat)"
10617
10795
  );
@@ -10727,6 +10905,7 @@ var CompanionSupervisor = class {
10727
10905
  workspaceSlug: wr.slug,
10728
10906
  agentId: runner.agentId,
10729
10907
  agentUsername: runner.username,
10908
+ agentDisplayName: runner.displayName,
10730
10909
  credential: runner.credential,
10731
10910
  runConfig: runner.runConfig,
10732
10911
  aborts: runner.aborts
@@ -10832,7 +11011,7 @@ function handleUncaught(log, err, origin) {
10832
11011
  const message = err instanceof Error ? err.message : String(err);
10833
11012
  const code = errorCode(err);
10834
11013
  if (isRecoverableSocketError(err)) {
10835
- log.warn(
11014
+ log.debug(
10836
11015
  { origin, code, err: message },
10837
11016
  "companion: recovered from a socket error (kept running \u2014 a broken pipe never kills the companion)"
10838
11017
  );
@@ -10840,7 +11019,7 @@ function handleUncaught(log, err, origin) {
10840
11019
  }
10841
11020
  log.error(
10842
11021
  { origin, code, err: message, stack: err instanceof Error ? err.stack : void 0 },
10843
- "companion: uncaught error (kept running \u2014 see the stack above)"
11022
+ "An unexpected error occurred; the companion is still running."
10844
11023
  );
10845
11024
  }
10846
11025
 
@@ -10874,7 +11053,7 @@ async function createCompanionRuntime(opts = {}) {
10874
11053
  try {
10875
11054
  ({ cfg, claudeOnPath: claudeCode } = await requireStartConfig({
10876
11055
  ...opts.probeClaude ? { probeClaude: opts.probeClaude } : {},
10877
- onMigrated: (migrated, onPath) => log.info(
11056
+ onMigrated: (migrated, onPath) => log.debug(
10878
11057
  { claudeCode: isClaudeCodeConnected(migrated) },
10879
11058
  onPath ? "companion: carried Claude Code over as a connected harness on this device (connectors are now chosen, not detected)" : "companion: no Claude Code on PATH, so this device starts with it disconnected (connectors are now chosen, not detected)"
10880
11059
  )
@@ -10895,7 +11074,7 @@ async function createCompanionRuntime(opts = {}) {
10895
11074
  });
10896
11075
  throw err;
10897
11076
  }
10898
- if (cfg.logLevel) log.level = cfg.logLevel;
11077
+ configureLogger(log, cfg);
10899
11078
  const harnessVersions = await probeHarnessVersions({
10900
11079
  // Connected AND its CLI answered — the two things that make a
10901
11080
  // `claude --version` probe worth spawning. Not the offer predicate.