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