@cabane/companion 0.6.187 → 0.6.189

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
@@ -788,7 +788,15 @@ var init_config = __esm({
788
788
  // and an agent with no folder of its own and no prepare hook works there
789
789
  // (`turn-execution.ts`, `prepareEnvironment`). Default `~/.cabane/work`: hidden, so pairing a
790
790
  // laptop creates no new visible folder; set it to make the work visible.
791
- root: z2.string().min(1).refine(isRootPath, { message: "must be an absolute path, or start with ~/" }).optional()
791
+ root: z2.string().min(1).refine(isRootPath, { message: "must be an absolute path, or start with ~/" }).optional(),
792
+ // An argv prefix the operator puts in front of every turn's runner: the companion starts
793
+ // `<prefix…> <its own run-turn command…>` instead of the runner bare. What the prefix does
794
+ // is the operator's (a box of their own making, ENG225); the companion knows nothing of it
795
+ // beyond handing the runner over, with the turn's inputs in its environment and the
796
+ // credential and secrets on its stdin (`runner/launch.ts`, `boxed`).
797
+ runner: z2.object({
798
+ command: z2.array(z2.string().min(1)).min(1)
799
+ }).strict().optional()
792
800
  });
793
801
  legacyOfferSchema = z2.object({
794
802
  claudeCode: z2.object({ enabled: z2.boolean().optional() }).passthrough().optional(),
@@ -1014,6 +1022,142 @@ var init_bundled_binaries = __esm({
1014
1022
  }
1015
1023
  });
1016
1024
 
1025
+ // src/harness-versions.ts
1026
+ function parseVersionToken(raw) {
1027
+ if (!raw) return null;
1028
+ const m = raw.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
1029
+ return m ? m[0] : null;
1030
+ }
1031
+ async function probeOpencodeVersion(serverUrl, fetchImpl = fetch) {
1032
+ try {
1033
+ const base = serverUrl.endsWith("/") ? serverUrl.slice(0, -1) : serverUrl;
1034
+ const res = await fetchImpl(`${base}/global/health`, {
1035
+ headers: { accept: "application/json" }
1036
+ });
1037
+ if (!res.ok) return null;
1038
+ return extractOpencodeVersion(await res.json());
1039
+ } catch {
1040
+ return null;
1041
+ }
1042
+ }
1043
+ function extractOpencodeVersion(json) {
1044
+ if (!json || typeof json !== "object") return null;
1045
+ const obj = json;
1046
+ const candidates = [obj.version];
1047
+ for (const v of Object.values(obj)) {
1048
+ if (v && typeof v === "object") candidates.push(v.version);
1049
+ }
1050
+ for (const c of candidates) {
1051
+ if (typeof c === "string") {
1052
+ const parsed = parseVersionToken(c);
1053
+ if (parsed) return parsed;
1054
+ }
1055
+ }
1056
+ return null;
1057
+ }
1058
+ function emptyHarnessVersions() {
1059
+ return { ...EMPTY };
1060
+ }
1061
+ var EMPTY;
1062
+ var init_harness_versions = __esm({
1063
+ "src/harness-versions.ts"() {
1064
+ "use strict";
1065
+ EMPTY = { claudeCode: null, opencode: null, codex: null };
1066
+ }
1067
+ });
1068
+
1069
+ // src/harness-usable.ts
1070
+ import { spawn as spawn2 } from "child_process";
1071
+ function apiKeyVariable(runtime, env = process.env) {
1072
+ return API_KEY_VARIABLES[runtime].find((name) => !!env[name]?.trim()) ?? null;
1073
+ }
1074
+ async function checkHarnessUsable(runtime, deps = {}) {
1075
+ const env = deps.env ?? process.env;
1076
+ const resolveBundled = deps.resolveBundled ?? ((r) => resolveBundledBinary(r));
1077
+ const run = deps.run ?? runBounded;
1078
+ const resolution = resolveBundled(runtime);
1079
+ if (resolution.status !== "present" || !resolution.path) {
1080
+ return {
1081
+ status: "bundle_missing",
1082
+ bundled: resolution.status === "present" ? "executable_missing" : resolution.status
1083
+ };
1084
+ }
1085
+ const path = resolution.path;
1086
+ const version = run(path, ["--version"], env).then(
1087
+ (r) => r.code === 0 ? parseVersionToken(r.output) : null
1088
+ );
1089
+ const variable = apiKeyVariable(runtime, env);
1090
+ if (variable) return { status: "api_key", variable, version: await version };
1091
+ const statusArgs = runtime === "codex" ? ["login", "status"] : ["auth", "status", "--json"];
1092
+ const [answer, v] = await Promise.all([run(path, statusArgs, env), version]);
1093
+ if (answer.error) return { status: "check_failed", reason: answer.error };
1094
+ const signedIn = runtime === "codex" ? readCodexStatus(answer) : readClaudeStatus(answer);
1095
+ if (typeof signedIn === "string") return { status: "check_failed", reason: signedIn };
1096
+ return signedIn ? { status: "signed_in", version: v } : { status: "signed_out", version: v };
1097
+ }
1098
+ function readClaudeStatus(answer) {
1099
+ const start2 = answer.output.indexOf("{");
1100
+ const end = answer.output.lastIndexOf("}");
1101
+ if (start2 < 0 || end < start2) return "unreadable";
1102
+ try {
1103
+ const parsed = JSON.parse(answer.output.slice(start2, end + 1));
1104
+ const loggedIn = parsed?.loggedIn;
1105
+ return typeof loggedIn === "boolean" ? loggedIn : "unreadable";
1106
+ } catch {
1107
+ return "unreadable";
1108
+ }
1109
+ }
1110
+ function readCodexStatus(answer) {
1111
+ if (answer.code === 0) return true;
1112
+ if (/not logged in/i.test(answer.output)) return false;
1113
+ if (/config/i.test(answer.output)) return "config";
1114
+ return "unreadable";
1115
+ }
1116
+ function runBounded(path, args, env) {
1117
+ return new Promise((resolve2) => {
1118
+ let settled = false;
1119
+ let out = "";
1120
+ const done = (result) => {
1121
+ if (settled) return;
1122
+ settled = true;
1123
+ clearTimeout(timer);
1124
+ resolve2(result);
1125
+ };
1126
+ let child;
1127
+ try {
1128
+ child = spawn2(path, args, { stdio: ["ignore", "pipe", "pipe"], env });
1129
+ } catch {
1130
+ resolve2({ code: null, output: "", error: "spawn" });
1131
+ return;
1132
+ }
1133
+ const capture = (chunk) => {
1134
+ if (out.length < 8192) out += chunk.toString();
1135
+ };
1136
+ child.stdout?.on("data", capture);
1137
+ child.stderr?.on("data", capture);
1138
+ const timer = setTimeout(() => {
1139
+ child.kill("SIGKILL");
1140
+ done({ code: null, output: out, error: "timeout" });
1141
+ }, CHECK_TIMEOUT_MS);
1142
+ timer.unref?.();
1143
+ child.once("error", () => done({ code: null, output: out, error: "spawn" }));
1144
+ child.once("close", (code) => done({ code, output: out }));
1145
+ });
1146
+ }
1147
+ var API_KEY_VARIABLES, CHECK_TIMEOUT_MS;
1148
+ var init_harness_usable = __esm({
1149
+ "src/harness-usable.ts"() {
1150
+ "use strict";
1151
+ init_bundled_binaries();
1152
+ init_harness_versions();
1153
+ API_KEY_VARIABLES = {
1154
+ "claude-code": ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
1155
+ codex: ["CODEX_API_KEY"]
1156
+ };
1157
+ CHECK_TIMEOUT_MS = 5e3;
1158
+ }
1159
+ });
1160
+
1017
1161
  // packages/agent-runtime/src/failure.ts
1018
1162
  import { z as z4 } from "zod";
1019
1163
  function encodeFailureReason(failure) {
@@ -7365,6 +7509,14 @@ function required(env, name) {
7365
7509
  if (!value) throw new Error(`runner: ${name} is not set`);
7366
7510
  return value;
7367
7511
  }
7512
+ function runnerPlace(env) {
7513
+ const workRoot2 = env[PLACE_ENV.workRoot];
7514
+ const transcriptDir = env[PLACE_ENV.transcriptDir];
7515
+ return {
7516
+ ...workRoot2 ? { workRoot: workRoot2 } : {},
7517
+ ...transcriptDir ? { transcriptDir } : {}
7518
+ };
7519
+ }
7368
7520
  function parseRunnerInput(env, stdin) {
7369
7521
  const dispatch = dispatchSchema.parse(JSON.parse(required(env, RUNNER_ENV.dispatch)));
7370
7522
  const spec = runnerSpecSchema.parse(JSON.parse(required(env, RUNNER_ENV.spec)));
@@ -7372,10 +7524,12 @@ function parseRunnerInput(env, stdin) {
7372
7524
  return {
7373
7525
  baseUrl: required(env, RUNNER_ENV.url),
7374
7526
  credential: credentials.credential,
7527
+ ...credentials.secrets ? { secrets: credentials.secrets } : {},
7375
7528
  dispatch,
7376
7529
  turnId: required(env, RUNNER_ENV.turnId),
7377
7530
  resumed: env[RUNNER_ENV.resumed] === "1",
7378
- spec
7531
+ spec,
7532
+ place: runnerPlace(env)
7379
7533
  };
7380
7534
  }
7381
7535
  function runnerEnv(input) {
@@ -7388,7 +7542,10 @@ function runnerEnv(input) {
7388
7542
  };
7389
7543
  }
7390
7544
  function runnerStdin(input) {
7391
- return `${JSON.stringify({ credential: input.credential })}
7545
+ return `${JSON.stringify({
7546
+ credential: input.credential,
7547
+ ...input.secrets ? { secrets: input.secrets } : {}
7548
+ })}
7392
7549
  `;
7393
7550
  }
7394
7551
  function encodeLauncherMessage(message2) {
@@ -7418,7 +7575,7 @@ function decodeRunnerEvent(line) {
7418
7575
  if (!value || typeof value !== "object" || !("cabaneRunner" in value)) return null;
7419
7576
  return value;
7420
7577
  }
7421
- var RUNNER_ENV, runtimeHealthSchema, runnerSpecSchema, dispatchSchema, credentialsSchema;
7578
+ var RUNNER_ENV, PLACE_ENV, runtimeHealthSchema, runnerSpecSchema, dispatchSchema, credentialsSchema;
7422
7579
  var init_protocol = __esm({
7423
7580
  "src/runner/protocol.ts"() {
7424
7581
  "use strict";
@@ -7437,6 +7594,10 @@ var init_protocol = __esm({
7437
7594
  // Everything else the turn is constructed from, as JSON: see `RunnerSpec`.
7438
7595
  spec: "CABANE_RUNNER_SPEC"
7439
7596
  };
7597
+ PLACE_ENV = {
7598
+ workRoot: "CABANE_WORK_ROOT",
7599
+ transcriptDir: "CABANE_TRANSCRIPT_DIR"
7600
+ };
7440
7601
  runtimeHealthSchema = z14.object({
7441
7602
  quotaState: z14.enum(["ok", "limited", "auth_expired", "unknown"]),
7442
7603
  limitedUntil: z14.string().nullable(),
@@ -7486,7 +7647,8 @@ var init_protocol = __esm({
7486
7647
  episode: z14.number().int().optional()
7487
7648
  });
7488
7649
  credentialsSchema = z14.object({
7489
- credential: z14.string().startsWith("cabrn_")
7650
+ credential: z14.string().startsWith("cabrn_"),
7651
+ secrets: z14.record(z14.string(), z14.string()).optional()
7490
7652
  });
7491
7653
  }
7492
7654
  });
@@ -7498,6 +7660,19 @@ function selfRunnerCommand() {
7498
7660
  if (!script) throw new Error("runner: cannot tell which script started this companion");
7499
7661
  return [process.execPath, ...process.execArgv, script, "run-turn"];
7500
7662
  }
7663
+ function wrappedRunnerCommand(prefix, own) {
7664
+ return [...prefix ?? [], ...own];
7665
+ }
7666
+ function runnerExecEnv(base, boxed) {
7667
+ const env = {};
7668
+ for (const [k, v] of Object.entries(base)) {
7669
+ if (v === void 0) continue;
7670
+ if (boxed && (BOXED_ENV_DROPPED.includes(k) || k.startsWith(BOXED_ENV_DROPPED_PREFIX)))
7671
+ continue;
7672
+ env[k] = v;
7673
+ }
7674
+ return env;
7675
+ }
7501
7676
  function healthSnapshot(store) {
7502
7677
  const out = {};
7503
7678
  for (const r of store?.reports() ?? []) {
@@ -7694,13 +7869,12 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7694
7869
  return { ok: false, durationMs: durationMs2, reason: reason2 };
7695
7870
  }
7696
7871
  try {
7697
- const env = {};
7698
- for (const [k, v] of Object.entries(launch.env ?? process.env)) {
7699
- if (v !== void 0) env[k] = v;
7700
- }
7872
+ const boxed = launch.boxed === true;
7873
+ const env = runnerExecEnv(launch.env ?? process.env, boxed);
7701
7874
  const input = {
7702
7875
  baseUrl: opts.baseUrl,
7703
7876
  credential: minted.credential,
7877
+ ...boxed ? { secrets: launch.secrets?.() ?? {} } : {},
7704
7878
  dispatch: payload,
7705
7879
  turnId,
7706
7880
  resumed: handleOpts.resumed,
@@ -7772,7 +7946,7 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7772
7946
  locks?.release(lockOwner(event.id));
7773
7947
  return;
7774
7948
  case "process_group":
7775
- groups.push(event.pgid);
7949
+ if (!boxed) groups.push(event.pgid);
7776
7950
  return;
7777
7951
  case "remote_session":
7778
7952
  remote.push({ serverUrl: event.serverUrl, sessionId: event.sessionId });
@@ -7793,7 +7967,8 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7793
7967
  }
7794
7968
  if (result) return result;
7795
7969
  const durationMs = Date.now() - startedAt;
7796
- const reason = thrown !== void 0 ? `runner_failed: ${thrown}` : `runner_exited: ${code}`;
7970
+ const stopped = controller.signal.aborted && thrown === void 0;
7971
+ const reason = stopped ? "cancelled" : thrown !== void 0 ? `runner_failed: ${thrown}` : `runner_exited: ${code}`;
7797
7972
  await child?.reap(groups);
7798
7973
  const reaped = await (launch.reapTurn ?? reapTurnScopes)(turnId).catch((err) => ({
7799
7974
  failures: [err instanceof Error ? err.message : String(err)]
@@ -7808,8 +7983,7 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7808
7983
  await opts.api.setActiveRun(opts.workspaceId, payload.conversationId, payload.agentId, {
7809
7984
  activeRunStartedAt: null,
7810
7985
  turnId,
7811
- outcome: "failed",
7812
- errorReason: reason.slice(0, ERROR_REASON_MAX),
7986
+ ...stopped ? { outcome: "interrupted" } : { outcome: "failed", errorReason: reason.slice(0, ERROR_REASON_MAX) },
7813
7987
  readThrough: payload.messageId
7814
7988
  });
7815
7989
  } catch (err) {
@@ -7827,11 +8001,12 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7827
8001
  reason: "exited"
7828
8002
  });
7829
8003
  if (thrown !== void 0) throw new Error(thrown);
7830
- log2.warn({ turnId, code }, "runner: exited without reporting a result");
8004
+ if (stopped) log2.info({ turnId, code }, "runner: stopped before it could report");
8005
+ else log2.warn({ turnId, code }, "runner: exited without reporting a result");
7831
8006
  duties.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
7832
8007
  return { ok: false, durationMs, reason };
7833
8008
  }
7834
- var PINO_LEVELS;
8009
+ var BOXED_ENV_DROPPED, BOXED_ENV_DROPPED_PREFIX, PINO_LEVELS;
7835
8010
  var init_launch = __esm({
7836
8011
  "src/runner/launch.ts"() {
7837
8012
  "use strict";
@@ -7842,6 +8017,16 @@ var init_launch = __esm({
7842
8017
  init_boot_id();
7843
8018
  init_turn_containment();
7844
8019
  init_protocol();
8020
+ BOXED_ENV_DROPPED = [
8021
+ "ANTHROPIC_API_KEY",
8022
+ "CLAUDE_CODE_OAUTH_TOKEN",
8023
+ "CODEX_API_KEY",
8024
+ "GH_TOKEN",
8025
+ "GITHUB_TOKEN",
8026
+ "DBUS_SESSION_BUS_ADDRESS",
8027
+ "XDG_RUNTIME_DIR"
8028
+ ];
8029
+ BOXED_ENV_DROPPED_PREFIX = "CABANE_GATE_";
7845
8030
  PINO_LEVELS = {
7846
8031
  10: "trace",
7847
8032
  20: "debug",
@@ -8456,9 +8641,9 @@ import { z as z16 } from "zod";
8456
8641
  function secretsPath() {
8457
8642
  return join19(cabaneDir(), "secrets.json");
8458
8643
  }
8459
- function loadSecretStore() {
8644
+ function loadSecretValues() {
8460
8645
  const path = secretsPath();
8461
- if (!existsSync10(path)) return makeStore({});
8646
+ if (!existsSync10(path)) return {};
8462
8647
  let raw;
8463
8648
  try {
8464
8649
  raw = readFileSync8(path, "utf8");
@@ -8467,7 +8652,7 @@ function loadSecretStore() {
8467
8652
  `couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
8468
8653
  );
8469
8654
  }
8470
- if (raw.trim().length === 0) return makeStore({});
8655
+ if (raw.trim().length === 0) return {};
8471
8656
  let parsed;
8472
8657
  try {
8473
8658
  parsed = JSON.parse(raw);
@@ -8482,14 +8667,17 @@ function loadSecretStore() {
8482
8667
  `${path} must be a flat object mapping secret names to string values (e.g. { "GITHUB_TOKEN": "ghp_\u2026" }).`
8483
8668
  );
8484
8669
  }
8485
- return makeStore(result.data);
8670
+ return result.data;
8486
8671
  }
8487
8672
  function loadSecretStoreTolerant(onWarn) {
8673
+ return makeStore(loadSecretValuesTolerant(onWarn));
8674
+ }
8675
+ function loadSecretValuesTolerant(onWarn) {
8488
8676
  try {
8489
- return loadSecretStore();
8677
+ return loadSecretValues();
8490
8678
  } catch (err) {
8491
8679
  onWarn?.(err instanceof Error ? err.message : String(err));
8492
- return makeStore({});
8680
+ return {};
8493
8681
  }
8494
8682
  }
8495
8683
  function makeStore(map) {
@@ -8581,6 +8769,31 @@ var init_secrets = __esm({
8581
8769
  }
8582
8770
  });
8583
8771
 
8772
+ // src/turn-env.ts
8773
+ function harnessLoginEnv(store, runtime) {
8774
+ const names = [
8775
+ ...runtime in API_KEY_VARIABLES ? API_KEY_VARIABLES[runtime] : [],
8776
+ GITHUB_TOKEN_NAME
8777
+ ];
8778
+ const env = {};
8779
+ for (const name of names) {
8780
+ const value = store.get(name);
8781
+ if (value) env[name] = value;
8782
+ }
8783
+ return env;
8784
+ }
8785
+ function turnIdentityEnv(ids) {
8786
+ return { CABANE_CONVERSATION_ID: ids.conversationId, CABANE_AGENT_ID: ids.agentId };
8787
+ }
8788
+ var GITHUB_TOKEN_NAME;
8789
+ var init_turn_env = __esm({
8790
+ "src/turn-env.ts"() {
8791
+ "use strict";
8792
+ init_harness_usable();
8793
+ GITHUB_TOKEN_NAME = "GH_TOKEN";
8794
+ }
8795
+ });
8796
+
8584
8797
  // src/transcript-writer.ts
8585
8798
  import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync6 } from "fs";
8586
8799
  import { basename as basename3, dirname as dirname7, join as join20 } from "path";
@@ -8971,6 +9184,7 @@ var init_turn_execution = __esm({
8971
9184
  init_prepare_failure();
8972
9185
  init_prepared();
8973
9186
  init_secrets();
9187
+ init_turn_env();
8974
9188
  init_turn_control_tools();
8975
9189
  init_transcript_writer();
8976
9190
  init_turn_containment();
@@ -9038,6 +9252,7 @@ var init_turn_execution = __esm({
9038
9252
  // The harness's secret environment for this turn. Only ever handed to the
9039
9253
  // adapter; never logged, never merged into `process.env`.
9040
9254
  secretEnv = {};
9255
+ secretStore;
9041
9256
  effectiveCwd;
9042
9257
  hookEnv;
9043
9258
  turnEnv;
@@ -9326,7 +9541,8 @@ var init_turn_execution = __esm({
9326
9541
  throw this.concluded(reason, reason);
9327
9542
  }
9328
9543
  this.resolvedMcpServers = resolvedMcpServers;
9329
- this.secretEnv = secretEnv;
9544
+ this.secretEnv = { ...secretEnv, ...harnessLoginEnv(secretStore, this.turnContext.runtime) };
9545
+ this.secretStore = secretStore;
9330
9546
  }
9331
9547
  async prepareEnvironment() {
9332
9548
  const { payload, workspaceId, turnId, turnLog } = this;
@@ -9477,6 +9693,10 @@ var init_turn_execution = __esm({
9477
9693
  }
9478
9694
  const turnEnv = {
9479
9695
  ...hookEnv,
9696
+ ...turnIdentityEnv({
9697
+ conversationId: this.payload.conversationId,
9698
+ agentId: this.payload.agentId
9699
+ }),
9480
9700
  CABANE_COMPANION_HOME: agentCompanionHome()
9481
9701
  };
9482
9702
  this.codexLocal = this.turnContext.runtime === "codex" ? { configSetsSandboxMode: await readCodexConfigSetsSandboxMode() } : void 0;
@@ -10551,8 +10771,14 @@ function runnerTurnOptions(input, log2, emit, extras = {}) {
10551
10771
  agentUsername: spec.agentUsername,
10552
10772
  credential: input.credential,
10553
10773
  local: spec.local,
10554
- workRoot: spec.workRoot,
10774
+ // The place the runner stands in names its own root and transcript
10775
+ // directory when it has them (`PLACE_ENV`); the launcher's host-computed
10776
+ // values are what a runner on the person's own machine gets.
10777
+ workRoot: input.place?.workRoot ?? spec.workRoot,
10555
10778
  runConfig: spec.runConfig,
10779
+ // The launcher's copy of the device's secrets, when it sent one: a runner
10780
+ // in a box has no `secrets.json` to read.
10781
+ ...input.secrets ? { secretStore: makeStore(input.secrets) } : {},
10556
10782
  log: log2,
10557
10783
  ...queryFn ? { queryFn } : {},
10558
10784
  ...spec.opencodeServerUrl ? { opencodeServerUrl: spec.opencodeServerUrl } : {},
@@ -10562,7 +10788,7 @@ function runnerTurnOptions(input, log2, emit, extras = {}) {
10562
10788
  ...spec.totalTimeoutMs !== void 0 ? { totalTimeoutMs: spec.totalTimeoutMs } : {},
10563
10789
  ...spec.leaseRenewalMs !== void 0 ? { leaseRenewalMs: spec.leaseRenewalMs } : {},
10564
10790
  ...spec.preparingRowDelayMs !== void 0 ? { preparingRowDelayMs: spec.preparingRowDelayMs } : {},
10565
- ...spec.transcripts ? { transcriptDir: transcriptsDir() } : {},
10791
+ ...spec.transcripts ? { transcriptDir: input.place?.transcriptDir ?? transcriptsDir() } : {},
10566
10792
  connectorHealth,
10567
10793
  inRunner: {
10568
10794
  bootId: spec.bootId,
@@ -10697,6 +10923,7 @@ var init_run_turn = __esm({
10697
10923
  init_exec_shim();
10698
10924
  init_launcher_channel();
10699
10925
  init_opencode_lock();
10926
+ init_secrets();
10700
10927
  init_transcript_writer();
10701
10928
  init_turn_execution();
10702
10929
  init_protocol();
@@ -11134,133 +11361,8 @@ function spaced(sentence) {
11134
11361
 
11135
11362
  // src/harness-detect.ts
11136
11363
  init_config();
11137
-
11138
- // src/harness-usable.ts
11139
- init_bundled_binaries();
11140
- import { spawn as spawn2 } from "child_process";
11141
-
11142
- // src/harness-versions.ts
11143
- var EMPTY = { claudeCode: null, opencode: null, codex: null };
11144
- function parseVersionToken(raw) {
11145
- if (!raw) return null;
11146
- const m = raw.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
11147
- return m ? m[0] : null;
11148
- }
11149
- async function probeOpencodeVersion(serverUrl, fetchImpl = fetch) {
11150
- try {
11151
- const base = serverUrl.endsWith("/") ? serverUrl.slice(0, -1) : serverUrl;
11152
- const res = await fetchImpl(`${base}/global/health`, {
11153
- headers: { accept: "application/json" }
11154
- });
11155
- if (!res.ok) return null;
11156
- return extractOpencodeVersion(await res.json());
11157
- } catch {
11158
- return null;
11159
- }
11160
- }
11161
- function extractOpencodeVersion(json) {
11162
- if (!json || typeof json !== "object") return null;
11163
- const obj = json;
11164
- const candidates = [obj.version];
11165
- for (const v of Object.values(obj)) {
11166
- if (v && typeof v === "object") candidates.push(v.version);
11167
- }
11168
- for (const c of candidates) {
11169
- if (typeof c === "string") {
11170
- const parsed = parseVersionToken(c);
11171
- if (parsed) return parsed;
11172
- }
11173
- }
11174
- return null;
11175
- }
11176
- function emptyHarnessVersions() {
11177
- return { ...EMPTY };
11178
- }
11179
-
11180
- // src/harness-usable.ts
11181
- var API_KEY_VARIABLES = {
11182
- "claude-code": ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
11183
- codex: ["CODEX_API_KEY"]
11184
- };
11185
- function apiKeyVariable(runtime, env = process.env) {
11186
- return API_KEY_VARIABLES[runtime].find((name) => !!env[name]?.trim()) ?? null;
11187
- }
11188
- var CHECK_TIMEOUT_MS = 5e3;
11189
- async function checkHarnessUsable(runtime, deps = {}) {
11190
- const env = deps.env ?? process.env;
11191
- const resolveBundled = deps.resolveBundled ?? ((r) => resolveBundledBinary(r));
11192
- const run = deps.run ?? runBounded;
11193
- const resolution = resolveBundled(runtime);
11194
- if (resolution.status !== "present" || !resolution.path) {
11195
- return {
11196
- status: "bundle_missing",
11197
- bundled: resolution.status === "present" ? "executable_missing" : resolution.status
11198
- };
11199
- }
11200
- const path = resolution.path;
11201
- const version = run(path, ["--version"], env).then(
11202
- (r) => r.code === 0 ? parseVersionToken(r.output) : null
11203
- );
11204
- const variable = apiKeyVariable(runtime, env);
11205
- if (variable) return { status: "api_key", variable, version: await version };
11206
- const statusArgs = runtime === "codex" ? ["login", "status"] : ["auth", "status", "--json"];
11207
- const [answer, v] = await Promise.all([run(path, statusArgs, env), version]);
11208
- if (answer.error) return { status: "check_failed", reason: answer.error };
11209
- const signedIn = runtime === "codex" ? readCodexStatus(answer) : readClaudeStatus(answer);
11210
- if (typeof signedIn === "string") return { status: "check_failed", reason: signedIn };
11211
- return signedIn ? { status: "signed_in", version: v } : { status: "signed_out", version: v };
11212
- }
11213
- function readClaudeStatus(answer) {
11214
- const start2 = answer.output.indexOf("{");
11215
- const end = answer.output.lastIndexOf("}");
11216
- if (start2 < 0 || end < start2) return "unreadable";
11217
- try {
11218
- const parsed = JSON.parse(answer.output.slice(start2, end + 1));
11219
- const loggedIn = parsed?.loggedIn;
11220
- return typeof loggedIn === "boolean" ? loggedIn : "unreadable";
11221
- } catch {
11222
- return "unreadable";
11223
- }
11224
- }
11225
- function readCodexStatus(answer) {
11226
- if (answer.code === 0) return true;
11227
- if (/not logged in/i.test(answer.output)) return false;
11228
- if (/config/i.test(answer.output)) return "config";
11229
- return "unreadable";
11230
- }
11231
- function runBounded(path, args, env) {
11232
- return new Promise((resolve2) => {
11233
- let settled = false;
11234
- let out = "";
11235
- const done = (result) => {
11236
- if (settled) return;
11237
- settled = true;
11238
- clearTimeout(timer);
11239
- resolve2(result);
11240
- };
11241
- let child;
11242
- try {
11243
- child = spawn2(path, args, { stdio: ["ignore", "pipe", "pipe"], env });
11244
- } catch {
11245
- resolve2({ code: null, output: "", error: "spawn" });
11246
- return;
11247
- }
11248
- const capture = (chunk) => {
11249
- if (out.length < 8192) out += chunk.toString();
11250
- };
11251
- child.stdout?.on("data", capture);
11252
- child.stderr?.on("data", capture);
11253
- const timer = setTimeout(() => {
11254
- child.kill("SIGKILL");
11255
- done({ code: null, output: out, error: "timeout" });
11256
- }, CHECK_TIMEOUT_MS);
11257
- timer.unref?.();
11258
- child.once("error", () => done({ code: null, output: out, error: "spawn" }));
11259
- child.once("close", (code) => done({ code, output: out }));
11260
- });
11261
- }
11262
-
11263
- // src/harness-detect.ts
11364
+ init_harness_usable();
11365
+ init_harness_versions();
11264
11366
  var DEFAULT_OPENCODE_SERVER_URL = "http://127.0.0.1:4096";
11265
11367
  var DEFAULT_OPENCODE_PROBE_MS = 1e3;
11266
11368
  var CONFIGURED_OPENCODE_PROBE_MS = 4e3;
@@ -12420,14 +12522,19 @@ var DeviceApi = class {
12420
12522
  return parsed;
12421
12523
  }
12422
12524
  // Pull the agents this device should run, across all the owner's workspaces.
12423
- getAssignments() {
12424
- return this.request("GET", "/api/companion/assignments");
12525
+ getAssignments(signal) {
12526
+ return this.request("GET", "/api/companion/assignments", void 0, requestSignal(signal));
12425
12527
  }
12426
12528
  // Ask again for the credential of an agent assigned to this device, when the saved copy is
12427
12529
  // gone. The previous credential stops working; any agent not assigned here is a 404, and a
12428
12530
  // second ask inside a minute a 429.
12429
- reissueAgentCredential(agentId) {
12430
- return this.request("POST", `/api/companion/agents/${encodeURIComponent(agentId)}/credential`);
12531
+ reissueAgentCredential(agentId, signal) {
12532
+ return this.request(
12533
+ "POST",
12534
+ `/api/companion/agents/${encodeURIComponent(agentId)}/credential`,
12535
+ void 0,
12536
+ requestSignal(signal, 3e4)
12537
+ );
12431
12538
  }
12432
12539
  getRuns(cursor) {
12433
12540
  const query = new URLSearchParams({ limit: "100", order: "asc" });
@@ -12467,6 +12574,10 @@ function errorMessage2(status2, body) {
12467
12574
  if (typeof body === "string" && body.length > 0) return `${status2} ${body.slice(0, 200)}`;
12468
12575
  return `${status2} error`;
12469
12576
  }
12577
+ function requestSignal(signal, timeoutMs = 5e3) {
12578
+ const deadline = AbortSignal.timeout(timeoutMs);
12579
+ return signal ? AbortSignal.any([signal, deadline]) : deadline;
12580
+ }
12470
12581
 
12471
12582
  // src/supervisor.ts
12472
12583
  init_config();
@@ -12976,6 +13087,10 @@ function runtimeIssuesFor(gates) {
12976
13087
  return issues;
12977
13088
  }
12978
13089
 
13090
+ // src/supervisor.ts
13091
+ init_harness_versions();
13092
+ init_harness_usable();
13093
+
12979
13094
  // src/opencode-models.ts
12980
13095
  var OPENCODE_RUNTIME = "opencode";
12981
13096
  function mapOpencodeProviders(json) {
@@ -13842,6 +13957,7 @@ var CompanionSupervisor = class {
13842
13957
  pollTimer = null;
13843
13958
  refreshing = false;
13844
13959
  stopped = false;
13960
+ reconciliationAbort = new AbortController();
13845
13961
  // The version-skew warning is logged once, not every heartbeat.
13846
13962
  versionSkewWarned = false;
13847
13963
  // This device's id, captured from the heartbeat / assignments response. The SSE
@@ -13887,6 +14003,7 @@ var CompanionSupervisor = class {
13887
14003
  // heartbeat + poll loops. A companion with no device token (logged out) does
13888
14004
  // nothing but say so.
13889
14005
  async start() {
14006
+ if (this.stopped) return;
13890
14007
  this.log.info(
13891
14008
  { protocolVersion: TURN_PROTOCOL_VERSION, version: COMPANION_VERSION },
13892
14009
  `Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ?? hostname2()}`
@@ -13971,6 +14088,7 @@ var CompanionSupervisor = class {
13971
14088
  });
13972
14089
  this.updater.start();
13973
14090
  await this.refreshAssignments();
14091
+ if (this.stopped) return;
13974
14092
  this.deviceSub.start();
13975
14093
  this.heartbeatTimer = setInterval(() => this.kickHeartbeat(), HEARTBEAT_INTERVAL_MS);
13976
14094
  this.heartbeatTimer.unref?.();
@@ -14083,10 +14201,12 @@ var CompanionSupervisor = class {
14083
14201
  let items;
14084
14202
  let device;
14085
14203
  try {
14086
- const resp = await this.deviceApi.getAssignments();
14204
+ const resp = await this.deviceApi.getAssignments(this.reconciliationAbort.signal);
14205
+ if (this.stopped) return;
14087
14206
  items = resp.assignments;
14088
14207
  device = resp.device;
14089
14208
  } catch (err) {
14209
+ if (this.stopped) return;
14090
14210
  this.log.error(
14091
14211
  { ...apiErrorLogFields(err) },
14092
14212
  "Couldn't load the assigned agents. Check that this device is active in Cabane; trying again shortly."
@@ -14105,6 +14225,7 @@ var CompanionSupervisor = class {
14105
14225
  if (it.credential) setCredential(device.id, it.agentId, it.credential);
14106
14226
  }
14107
14227
  items = await this.restoreMissingCredentials(device.id, items);
14228
+ if (this.stopped) return;
14108
14229
  this.uncredentialed = new Set(
14109
14230
  items.filter((it) => !it.credential && !getCredential(device.id, it.agentId)).map((it) => `${it.workspaceId}|${it.agentId}`)
14110
14231
  );
@@ -14117,6 +14238,7 @@ var CompanionSupervisor = class {
14117
14238
  for (const wsId of [...this.workspaces.keys()]) {
14118
14239
  if (!byWorkspace.has(wsId)) await this.removeWorkspace(wsId);
14119
14240
  }
14241
+ if (this.stopped) return;
14120
14242
  for (const [wsId, list] of byWorkspace) this.reconcileWorkspace(device.id, wsId, list);
14121
14243
  const pruned = pruneCredentials(
14122
14244
  device.id,
@@ -14148,6 +14270,7 @@ var CompanionSupervisor = class {
14148
14270
  async restoreMissingCredentials(deviceId, items) {
14149
14271
  const restored = [];
14150
14272
  for (const it of items) {
14273
+ if (this.stopped) return restored;
14151
14274
  if (it.credential || getCredential(deviceId, it.agentId)) {
14152
14275
  restored.push(it);
14153
14276
  continue;
@@ -14159,7 +14282,11 @@ var CompanionSupervisor = class {
14159
14282
  continue;
14160
14283
  }
14161
14284
  try {
14162
- const { credential } = await this.deviceApi.reissueAgentCredential(it.agentId);
14285
+ const { credential } = await this.deviceApi.reissueAgentCredential(
14286
+ it.agentId,
14287
+ this.reconciliationAbort.signal
14288
+ );
14289
+ if (this.stopped) return restored;
14163
14290
  setCredential(deviceId, it.agentId, credential);
14164
14291
  this.log.info(
14165
14292
  { agentId: it.agentId, agentName: it.agentDisplayName },
@@ -14452,7 +14579,16 @@ var CompanionSupervisor = class {
14452
14579
  },
14453
14580
  runner: {
14454
14581
  provider: this.runnerProvider,
14455
- command: this.runnerCommand ?? selfRunnerCommand(),
14582
+ command: wrappedRunnerCommand(
14583
+ this.config.runner?.command,
14584
+ this.runnerCommand ?? selfRunnerCommand()
14585
+ ),
14586
+ ...this.config.runner?.command ? {
14587
+ boxed: true,
14588
+ secrets: () => loadSecretValuesTolerant(
14589
+ (m) => this.log.warn({ err: m }, "Could not read the secrets file for a boxed turn.")
14590
+ )
14591
+ } : {},
14456
14592
  ...this.runnerEnv ? { env: this.runnerEnv } : {},
14457
14593
  reportRunnerGone: (report) => this.reportRunnerGone(report),
14458
14594
  connectorHealth: this.connectorHealth,
@@ -15034,6 +15170,7 @@ var CompanionSupervisor = class {
15034
15170
  // ---- control actions ----
15035
15171
  async shutdown() {
15036
15172
  this.stopped = true;
15173
+ this.reconciliationAbort.abort();
15037
15174
  this.updater?.stop();
15038
15175
  if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
15039
15176
  if (this.detectTimer) clearInterval(this.detectTimer);
@@ -15311,33 +15448,46 @@ async function createCompanionRuntime(opts = {}) {
15311
15448
  });
15312
15449
  let restartForUpdate = async () => {
15313
15450
  };
15314
- const supervisor = new CompanionSupervisor({
15315
- config: cfg,
15316
- log: log2,
15317
- hub,
15318
- ...opts.checkHarness ? { checkHarness: opts.checkHarness } : {},
15319
- ...embeddedRunner({
15320
- electron: Boolean(process.versions.electron),
15321
- execPath: process.execPath,
15322
- moduleUrl: import.meta.url,
15323
- env: process.env
15324
- }) ?? {},
15325
- restartForUpdate: () => restartForUpdate()
15326
- });
15327
- await supervisor.start();
15451
+ let supervisor;
15328
15452
  let control = null;
15329
15453
  let dashboard = null;
15454
+ let stopped = false;
15455
+ const stop2 = async () => {
15456
+ if (stopped) return;
15457
+ stopped = true;
15458
+ clearRuntimeStateIfOurs(instanceId);
15459
+ process.removeListener("exit", clearRuntimeOnExit);
15460
+ await Promise.allSettled([
15461
+ ...supervisor ? [supervisor.shutdown()] : [],
15462
+ ...control ? [control.close()] : [],
15463
+ ...dashboard ? [dashboard.close()] : []
15464
+ ]);
15465
+ };
15330
15466
  try {
15467
+ supervisor = new CompanionSupervisor({
15468
+ config: cfg,
15469
+ log: log2,
15470
+ hub,
15471
+ ...opts.checkHarness ? { checkHarness: opts.checkHarness } : {},
15472
+ ...embeddedRunner({
15473
+ electron: Boolean(process.versions.electron),
15474
+ execPath: process.execPath,
15475
+ moduleUrl: import.meta.url,
15476
+ env: process.env
15477
+ }) ?? {},
15478
+ restartForUpdate: () => restartForUpdate()
15479
+ });
15480
+ const active2 = supervisor;
15331
15481
  control = await startControlServer({
15332
15482
  status: () => ({
15333
15483
  ...hub.statusJson(),
15334
- ...supervisor.lastHeartbeat(),
15335
- detection: supervisor.currentDetection()
15484
+ ...active2.lastHeartbeat(),
15485
+ detection: active2.currentDetection()
15336
15486
  }),
15337
- detect: () => supervisor.redetect(),
15338
- allow: (runtime) => supervisor.allowHarness(runtime),
15339
- update: (auto) => supervisor.update(auto),
15340
- stop: () => void supervisor.requestStop()
15487
+ detect: () => active2.redetect(),
15488
+ allow: (runtime) => active2.allowHarness(runtime),
15489
+ update: (auto) => active2.update(auto),
15490
+ stop: () => void stop2().then(() => active2.requestStop())
15341
15491
  });
15342
15492
  if (opts.dashboard) {
15343
15493
  const preferredPort = opts.port ?? cfg.dashboardPort;
@@ -15348,6 +15498,7 @@ async function createCompanionRuntime(opts = {}) {
15348
15498
  });
15349
15499
  hub.setDashboardUrl(dashboard.url);
15350
15500
  }
15501
+ if (stopped) throw new Error("The companion was stopped during startup.");
15351
15502
  writeRuntimeState({
15352
15503
  pid: process.pid,
15353
15504
  socket: control.path,
@@ -15365,7 +15516,7 @@ async function createCompanionRuntime(opts = {}) {
15365
15516
  clearRuntimeStateIfOurs(instanceId);
15366
15517
  process.removeListener("exit", clearRuntimeOnExit);
15367
15518
  await Promise.allSettled([
15368
- supervisor.shutdown(),
15519
+ ...supervisor ? [supervisor.shutdown()] : [],
15369
15520
  ...control ? [control.close()] : [],
15370
15521
  ...dashboard ? [dashboard.close()] : []
15371
15522
  ]);
@@ -15377,22 +15528,17 @@ async function createCompanionRuntime(opts = {}) {
15377
15528
  });
15378
15529
  throw err;
15379
15530
  }
15380
- let stopped = false;
15381
- const stop2 = async () => {
15382
- if (stopped) return;
15383
- stopped = true;
15384
- clearRuntimeStateIfOurs(instanceId);
15385
- process.removeListener("exit", clearRuntimeOnExit);
15386
- try {
15387
- await supervisor.shutdown();
15388
- await closeSurfaces(control, dashboard);
15389
- } catch {
15390
- }
15391
- };
15531
+ const active = supervisor;
15392
15532
  restartForUpdate = async () => {
15393
15533
  await stop2();
15394
15534
  process.exit(0);
15395
15535
  };
15536
+ void active.start().catch(async (err) => {
15537
+ log2.error({ err }, "Couldn't initialize the companion.");
15538
+ recordCrash({ reason: String(err), origin: "startup", at: (/* @__PURE__ */ new Date()).toISOString() });
15539
+ await stop2();
15540
+ process.exitCode = 1;
15541
+ });
15396
15542
  return {
15397
15543
  ok: true,
15398
15544
  runtime: {
@@ -15400,15 +15546,38 @@ async function createCompanionRuntime(opts = {}) {
15400
15546
  socketPath: control.path,
15401
15547
  config: cfg,
15402
15548
  stop: stop2,
15403
- heartbeatNow: () => supervisor.heartbeatNow(),
15549
+ heartbeatNow: () => active.heartbeatNow(),
15404
15550
  harnesses: () => hub.statusJson().harnesses ?? [],
15405
- detect: () => supervisor.redetect()
15551
+ detect: () => active.redetect()
15406
15552
  }
15407
15553
  };
15408
15554
  }
15409
- async function closeSurfaces(control, dashboard) {
15410
- await control.close();
15411
- if (dashboard) await dashboard.close();
15555
+
15556
+ // src/start-observation.ts
15557
+ init_cabane_command();
15558
+ async function observeStart(deps = {}) {
15559
+ const read2 = deps.readState ?? readLiveRuntimeState;
15560
+ const verify = deps.verify ?? verifyRuntime;
15561
+ const now = deps.now ?? Date.now;
15562
+ const sleep3 = deps.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
15563
+ const deadline = now() + 8e3;
15564
+ for (; ; ) {
15565
+ const state = await read2();
15566
+ if (state?.socket && await verify(state) === "ours") return state;
15567
+ if (now() >= deadline) return null;
15568
+ await sleep3(Math.min(150, Math.max(0, deadline - now())));
15569
+ }
15570
+ }
15571
+ function markLaunchRequester() {
15572
+ process.title = "cabane-launcher";
15573
+ }
15574
+ function unconfirmedStartLines(service2) {
15575
+ return [
15576
+ "Cabane's start was requested, but readiness isn't confirmed yet. It may still finish starting in the background.",
15577
+ `Check: ${cabane("status")}`,
15578
+ ...service2 ? [`Service: ${cabane("service status")}`] : [],
15579
+ `Log: ${cabane("logs")}`
15580
+ ];
15412
15581
  }
15413
15582
 
15414
15583
  // src/commands/start.ts
@@ -15817,15 +15986,11 @@ init_config();
15817
15986
  import { spawn as spawn7 } from "child_process";
15818
15987
  import { closeSync as closeSync3, mkdirSync as mkdirSync17, openSync as openSync4 } from "fs";
15819
15988
  init_config();
15820
- var STARTUP_TIMEOUT_MS = 8e3;
15821
- var POLL_INTERVAL_MS = 150;
15822
15989
  async function startDaemon(opts = {}, deps = {}) {
15823
15990
  const ensureRuntime = deps.ensureRuntime ?? warnAboutHarnessReadiness;
15824
15991
  const readState = deps.readState ?? readLiveRuntimeState;
15825
15992
  const verify = deps.verify ?? ((s) => verifyRuntime(s));
15826
15993
  const spawnDetached = deps.spawnDetached ?? defaultSpawnDetached;
15827
- const sleep3 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
15828
- const now = deps.now ?? (() => Date.now());
15829
15994
  const quiet = opts.report === "failures" || opts.report === "silent";
15830
15995
  const cfg = (deps.requireCfg ?? requireConfig)();
15831
15996
  ensureRuntime(cfg, {
@@ -15837,7 +16002,18 @@ async function startDaemon(opts = {}, deps = {}) {
15837
16002
  });
15838
16003
  const existing = readState();
15839
16004
  if (existing) {
15840
- if (await verify(existing) !== "stale") {
16005
+ const verdict = await verify(existing);
16006
+ if (verdict !== "stale") {
16007
+ if (verdict !== "ours") {
16008
+ const state2 = await observeStart({ ...deps, readState, verify });
16009
+ if (!state2) {
16010
+ if (opts.report !== "silent")
16011
+ process.stdout.write(unconfirmedStartLines(false).join("\n") + "\n");
16012
+ process.exitCode = 1;
16013
+ return { started: false, state: null, unconfirmed: true };
16014
+ }
16015
+ return { started: false, state: state2 };
16016
+ }
15841
16017
  if (opts.report !== "silent")
15842
16018
  process.stdout.write(alreadyRunningLines(existing.pid).join("\n") + "\n");
15843
16019
  return { started: false, state: existing };
@@ -15855,28 +16031,21 @@ async function startDaemon(opts = {}, deps = {}) {
15855
16031
  spawned.unref();
15856
16032
  return spawned;
15857
16033
  };
15858
- const ready = (s) => !!s && !!s.socket;
15859
- const waitForReady = async () => {
15860
- const deadline = now() + STARTUP_TIMEOUT_MS;
15861
- let seen = readState();
15862
- while (!ready(seen) && now() < deadline) {
15863
- await sleep3(POLL_INTERVAL_MS);
15864
- seen = readState();
15865
- }
15866
- return ready(seen) ? seen : null;
15867
- };
16034
+ markLaunchRequester();
15868
16035
  const child = launchDetached();
15869
- const state = await waitForReady();
16036
+ const state = await observeStart({ ...deps, readState, verify });
15870
16037
  if (!state) {
15871
- const error = `The background process (pid ${child?.pid ?? "?"}) did not report ready within ${STARTUP_TIMEOUT_MS / 1e3}s.`;
16038
+ const error = child.failure?.();
16039
+ if (error) {
16040
+ if (opts.report !== "silent") process.stdout.write(`Cabane couldn't start: ${error}
16041
+ `);
16042
+ process.exitCode = 1;
16043
+ return { started: false, state: null, error };
16044
+ }
15872
16045
  if (opts.report !== "silent")
15873
- process.stdout.write(
15874
- `Cabane Companion was launched (pid ${child?.pid ?? "?"}) but didn't report ready within ${STARTUP_TIMEOUT_MS / 1e3}s.
15875
- Check ${companionLogPath()} for errors, or run \`${cabane("status")}\`.
15876
- `
15877
- );
16046
+ process.stdout.write(unconfirmedStartLines(false).join("\n") + "\n");
15878
16047
  process.exitCode = 1;
15879
- return { started: false, state: null, error };
16048
+ return { started: false, state: null, unconfirmed: true };
15880
16049
  }
15881
16050
  if (quiet) return { started: true, state };
15882
16051
  process.stdout.write(
@@ -15896,11 +16065,19 @@ function defaultSpawnDetached(args) {
15896
16065
  mkdirSync17(cabaneDir(), { recursive: true });
15897
16066
  const logFd = openSync4(companionLogPath(), "a");
15898
16067
  try {
15899
- return spawn7(process.execPath, [cliPath, ...args], {
16068
+ const child = spawn7(process.execPath, [cliPath, ...args], {
15900
16069
  detached: true,
15901
16070
  stdio: ["ignore", logFd, logFd],
15902
16071
  env: { ...process.env, CABANE_COMPANION_DAEMON: "1" }
15903
16072
  });
16073
+ let failure;
16074
+ child.once("error", (err) => {
16075
+ failure = err.message;
16076
+ });
16077
+ child.once("exit", (code, signal) => {
16078
+ failure = `The background process exited (${signal ?? code}). Check cabane logs.`;
16079
+ });
16080
+ return { pid: child.pid, unref: () => child.unref(), failure: () => failure };
15904
16081
  } finally {
15905
16082
  closeSync3(logFd);
15906
16083
  }
@@ -16020,8 +16197,10 @@ async function start(opts = {}, deps = {}) {
16020
16197
  try {
16021
16198
  return await startScript(opts, deps);
16022
16199
  } catch (err) {
16023
- for (const line of startFailure(err instanceof Error ? err.message : String(err), reloadLine()))
16024
- write(line);
16200
+ const reason = err instanceof Error ? err.message : String(err);
16201
+ if (process.env.CABANE_COMPANION_SUPERVISED === "1")
16202
+ getLogger().error({ err }, `Cabane couldn't start: ${reason}`);
16203
+ for (const line of startFailure(reason, reloadLine())) write(line);
16025
16204
  process.exitCode = 1;
16026
16205
  return null;
16027
16206
  } finally {
@@ -16057,23 +16236,41 @@ async function startScript(opts, deps) {
16057
16236
  mode = "attached";
16058
16237
  }
16059
16238
  } else if (installed) {
16060
- const result = await startService();
16061
- if (!result.ok) throw new Error(result.lines.join(" "));
16062
- const deadline = Date.now() + 8e3;
16063
- while (!running?.socket && Date.now() < deadline) {
16064
- running = await (deps.live ?? liveCompanionState)();
16065
- if (!running?.socket) await new Promise((resolve2) => setTimeout(resolve2, 150));
16239
+ markLaunchRequester();
16240
+ const before = await serviceState();
16241
+ if (!serviceLaunchPending(before)) {
16242
+ const result = await startService();
16243
+ if (!result.ok) {
16244
+ running = await observeStart({ ...deps, readState: deps.live });
16245
+ const current2 = await serviceState();
16246
+ if (!running && !serviceLaunchPending(current2)) throw new Error(result.lines.join(" "));
16247
+ }
16066
16248
  }
16067
- if (!running?.socket) throw new Error("The service did not report ready within 8s.");
16068
- mode = "service";
16249
+ running ??= await observeStart({ ...deps, readState: deps.live });
16250
+ if (!running) {
16251
+ const current2 = await serviceState();
16252
+ if (!current2.nodeOk || current2.state === "failed")
16253
+ throw new Error(
16254
+ !current2.nodeOk ? "The service Node executable is missing." : "The service failed. Check cabane logs."
16255
+ );
16256
+ return reportUnconfirmed(true);
16257
+ }
16258
+ const current = await serviceState();
16259
+ mode = current.state === "running" && current.pid === running.pid ? "service" : "existing";
16069
16260
  } else {
16070
16261
  const result = await startDaemon({ ...opts, report: "silent" });
16262
+ if (result.unconfirmed) return reportUnconfirmed(false);
16071
16263
  if (!result.state)
16072
16264
  throw new Error(result.error ?? "The background process did not report ready.");
16073
16265
  running = result.state;
16074
16266
  mode = result.started ? "background" : "existing";
16075
16267
  }
16076
16268
  }
16269
+ if (!runtime && mode === "existing") {
16270
+ running = await observeStart({ ...deps, readState: deps.live });
16271
+ if (!running) return reportUnconfirmed(installed);
16272
+ }
16273
+ const attached = runtime ? runAttached(runtime) : null;
16077
16274
  const socket = runtime?.socketPath ?? running?.socket;
16078
16275
  let status2 = await (deps.waitAck ?? waitForAcknowledgement)(socket);
16079
16276
  let detection = status2.detection ?? null;
@@ -16087,6 +16284,26 @@ async function startScript(opts, deps) {
16087
16284
  const latest = await readTerminalStatus(socket);
16088
16285
  if (latest.lastHeartbeatAckAt || latest.lastHeartbeatRefused) status2 = latest;
16089
16286
  }
16287
+ if (!runtime) {
16288
+ const current = await (deps.live ?? liveCompanionState)();
16289
+ if (!current || await (deps.verify ?? verifyRuntime)(current) !== "ours") {
16290
+ if (installed) {
16291
+ const service2 = await serviceState();
16292
+ if (!service2.pid && ["not loaded", "dead", "inactive", "failed"].includes(service2.state))
16293
+ throw new Error("The service stopped before startup completed.");
16294
+ }
16295
+ return reportUnconfirmed(installed);
16296
+ }
16297
+ if (current.pid !== running?.pid || current.instanceId !== running?.instanceId) {
16298
+ status2 = await readTerminalStatus(current.socket);
16299
+ detection = status2.detection ?? null;
16300
+ }
16301
+ running = current;
16302
+ if (mode === "service") {
16303
+ const service2 = await serviceState();
16304
+ if (service2.state !== "running" || service2.pid !== current.pid) mode = "existing";
16305
+ }
16306
+ }
16090
16307
  const personalUrl = `${cfg.baseUrl.replace(/\/$/, "")}/settings/devices`;
16091
16308
  const address = opts.pairing ? opts.pairing.devicesUrl : personalUrl;
16092
16309
  if (!daemon)
@@ -16103,10 +16320,18 @@ async function startScript(opts, deps) {
16103
16320
  write(line);
16104
16321
  if (runtime) {
16105
16322
  setConsoleLogging(true);
16106
- await runAttached(runtime);
16323
+ await attached;
16107
16324
  }
16108
16325
  return detection;
16109
16326
  }
16327
+ function serviceLaunchPending(state) {
16328
+ return !!state.pid || ["running", "activating", "auto-restart"].includes(state.state) || state.platform === "launchd" && state.installed && !["not loaded", "unknown", "failed"].includes(state.state);
16329
+ }
16330
+ function reportUnconfirmed(service2) {
16331
+ for (const line of unconfirmedStartLines(service2)) write(line);
16332
+ process.exitCode = 1;
16333
+ return null;
16334
+ }
16110
16335
  async function detectOverSocket(socket) {
16111
16336
  const body = await controlRequest(socket, { cmd: "detect" }, 15e3);
16112
16337
  return body.detection;
@@ -16480,7 +16705,7 @@ init_term();
16480
16705
 
16481
16706
  // src/commands/stop.ts
16482
16707
  var TERM_GRACE_MS = 6e3;
16483
- var POLL_INTERVAL_MS2 = 150;
16708
+ var POLL_INTERVAL_MS = 150;
16484
16709
  async function stop(deps = {}) {
16485
16710
  if ((deps.serviceInstalled ?? serviceInstalled)()) {
16486
16711
  const outcome = await (deps.stopService ?? stopService)();
@@ -16526,7 +16751,7 @@ async function stop(deps = {}) {
16526
16751
  process.stdout.write("companion: stopped.\n");
16527
16752
  return;
16528
16753
  }
16529
- await sleep3(POLL_INTERVAL_MS2);
16754
+ await sleep3(POLL_INTERVAL_MS);
16530
16755
  }
16531
16756
  process.stdout.write(
16532
16757
  `companion: didn't exit within ${TERM_GRACE_MS / 1e3}s, sending SIGKILL.
@@ -16683,7 +16908,10 @@ function row2(label, value) {
16683
16908
  async function status(deps = {}) {
16684
16909
  const out = (line) => void process.stdout.write(`${line}
16685
16910
  `);
16686
- const running = await liveState();
16911
+ const candidate = readLiveRuntimeState({ sweep: false });
16912
+ const verdict = candidate ? await verifyRuntime(candidate) : null;
16913
+ const running = verdict === "stale" ? null : candidate;
16914
+ const service2 = await serviceState();
16687
16915
  for (const line of pathLines(reloadLine())) out(line);
16688
16916
  const strays = strayStatusLines(running, deps.strays);
16689
16917
  out(` ${versionLine()}`);
@@ -16705,7 +16933,7 @@ async function status(deps = {}) {
16705
16933
  out(row2("Paired with", `${cfg.baseUrl}${cfg.deviceLabel ? ` as ${cfg.deviceLabel}` : ""}`));
16706
16934
  let detection = null;
16707
16935
  if (running) {
16708
- const mode = running.daemon ? "in the background" : "in a terminal";
16936
+ const mode = verdict !== "ours" ? "identity unconfirmed" : runningMode(running, service2);
16709
16937
  const uptime = formatUptime(running.startedAt);
16710
16938
  out(row2("Running", `${mode} (pid ${running.pid}${uptime ? `, up ${uptime}` : ""})`));
16711
16939
  detection = await runningDetection(running);
@@ -16723,7 +16951,6 @@ async function status(deps = {}) {
16723
16951
  )
16724
16952
  );
16725
16953
  for (const line of strays) out(line);
16726
- const service2 = await serviceState();
16727
16954
  for (const line of serviceRows(service2)) out(line);
16728
16955
  for (const line of updateRows(readUpdateState(), cfg.update?.auto, {
16729
16956
  version: false,
@@ -16793,10 +17020,13 @@ function otherInstall(install, own) {
16793
17020
  const ownAlias = own ? real(join30(dirname13(own), "cabane-companion")) : null;
16794
17021
  return real(found) === ownAlias ? null : dirname13(found);
16795
17022
  }
16796
- async function liveState() {
16797
- const running = readLiveRuntimeState({ sweep: false });
16798
- if (running && await verifyRuntime(running) === "stale") return null;
16799
- return running;
17023
+ function runningMode(running, service2) {
17024
+ if (service2.installed) {
17025
+ if (service2.state === "running" && service2.pid === running.pid) return "as a service";
17026
+ if (service2.state === "unknown" || service2.pid === running.pid || service2.state === "running" && !service2.pid)
17027
+ return "run mode unconfirmed";
17028
+ }
17029
+ return running.daemon ? "in the background" : "in a terminal";
16800
17030
  }
16801
17031
  async function runningDetection(running) {
16802
17032
  if (!running.socket) return null;
@@ -16994,7 +17224,7 @@ init_errors();
16994
17224
  init_src();
16995
17225
  init_transcript_writer();
16996
17226
  import { existsSync as existsSync19, readFileSync as readFileSync17, readdirSync as readdirSync6 } from "fs";
16997
- import { isAbsolute as isAbsolute2, join as join31 } from "path";
17227
+ import { basename as basename4, isAbsolute as isAbsolute2, join as join31 } from "path";
16998
17228
  async function transcript(opts = {}) {
16999
17229
  const dir2 = transcriptsDir();
17000
17230
  if (opts.follow) {
@@ -17117,13 +17347,28 @@ async function followTranscripts(dir2) {
17117
17347
  follower.tick();
17118
17348
  });
17119
17349
  }
17120
- function listFiles(dir2) {
17350
+ function listTranscriptFiles(dir2) {
17351
+ let entries;
17121
17352
  try {
17122
- return readdirSync6(dir2).filter((f) => f.endsWith(".jsonl")).sort().reverse();
17353
+ entries = readdirSync6(dir2, { withFileTypes: true });
17123
17354
  } catch {
17124
17355
  return [];
17125
17356
  }
17357
+ const files = [];
17358
+ for (const entry of entries) {
17359
+ if (entry.isFile() && entry.name.endsWith(".jsonl")) files.push(entry.name);
17360
+ if (!entry.isDirectory() || entry.name === "anomalies") continue;
17361
+ let inner;
17362
+ try {
17363
+ inner = readdirSync6(join31(dir2, entry.name));
17364
+ } catch {
17365
+ continue;
17366
+ }
17367
+ for (const name of inner) if (name.endsWith(".jsonl")) files.push(`${entry.name}/${name}`);
17368
+ }
17369
+ return files.sort((a, b) => basename4(a) < basename4(b) ? 1 : basename4(a) > basename4(b) ? -1 : 0);
17126
17370
  }
17371
+ var listFiles = listTranscriptFiles;
17127
17372
  function printList(dir2) {
17128
17373
  const files = listFiles(dir2);
17129
17374
  if (files.length === 0) {
@@ -17170,6 +17415,7 @@ function peek(path) {
17170
17415
  function resolveTarget(dir2, target) {
17171
17416
  if (isAbsolute2(target) || target.includes("/")) {
17172
17417
  if (existsSync19(target)) return target;
17418
+ if (!isAbsolute2(target) && existsSync19(join31(dir2, target))) return join31(dir2, target);
17173
17419
  throw new CompanionError(`no transcript at ${target}.`);
17174
17420
  }
17175
17421
  const exact = join31(dir2, target);