@cabane/companion 0.6.100 → 0.6.102

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