@cabane/companion 0.6.40 → 0.6.42

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.
Files changed (3) hide show
  1. package/dist/cli.js +106 -89
  2. package/dist/runtime.js +26 -26
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -819,7 +819,7 @@ async function warnAboutHarnessReadiness(cfg, deps = {}) {
819
819
  var HARNESS_LABELS = {
820
820
  "claude-code": "Claude Code",
821
821
  codex: "Codex",
822
- opencode: "opencode"
822
+ opencode: "OpenCode"
823
823
  };
824
824
  var LABELS = HARNESS_LABELS;
825
825
  var OFFER_ORDER = ["claude-code", "codex", "opencode"];
@@ -949,9 +949,6 @@ function deriveOpencode(signals, manifestHas) {
949
949
  enable: "opencode"
950
950
  };
951
951
  }
952
- function detectedRuntimesFor(snapshot) {
953
- return snapshot.harnesses.filter((h) => h.state === "detected_not_exposed").map((h) => ({ runtime: h.runtime, version: h.version }));
954
- }
955
952
  var PROBE_TIMEOUT_MS = 4e3;
956
953
  async function probeHarnessSignals(cfg, deps = {}) {
957
954
  const probeClaudePresence = deps.probeClaudePresence ?? claudeOnPath;
@@ -1198,8 +1195,6 @@ function blank() {
1198
1195
  process.stdout.write("\n");
1199
1196
  }
1200
1197
  var tick = (rest) => `${pc.green("\u2713")} ${rest}`;
1201
- var arrow = (rest) => `${pc.yellow("\u2192")} ${rest}`;
1202
- var bang = (rest) => `${pc.yellow("!")} ${rest}`;
1203
1198
  function tildePath(path) {
1204
1199
  const home = homedir2();
1205
1200
  if (home && path.startsWith(home + "/")) return `~${path.slice(home.length)}`;
@@ -7449,16 +7444,22 @@ async function proveWorkspaceTools(req, runtime, opts = {}) {
7449
7444
  "x-cabane-active-conversation": req.cabane.activeConversationId
7450
7445
  };
7451
7446
  try {
7452
- const initialized = await initializeWithRetry(fetchImpl, req.cabane.mcpUrl, headers, {
7453
- jsonrpc: "2.0",
7454
- id: 1,
7455
- method: "initialize",
7456
- params: {
7457
- protocolVersion: "2025-03-26",
7458
- capabilities: {},
7459
- clientInfo: { name: "cabane-companion-readiness", version: "1" }
7460
- }
7461
- });
7447
+ const initialized = await initializeWithRetry(
7448
+ fetchImpl,
7449
+ req.cabane.mcpUrl,
7450
+ headers,
7451
+ {
7452
+ jsonrpc: "2.0",
7453
+ id: 1,
7454
+ method: "initialize",
7455
+ params: {
7456
+ protocolVersion: "2025-03-26",
7457
+ capabilities: {},
7458
+ clientInfo: { name: "cabane-companion-readiness", version: "1" }
7459
+ }
7460
+ },
7461
+ opts.retryDelaysMs ?? INITIALIZE_RETRY_DELAYS_MS
7462
+ );
7462
7463
  if (initialized.status === 401 || initialized.status === 403)
7463
7464
  return fail(base, "authentication_failed", `initialize returned HTTP ${initialized.status}`);
7464
7465
  if (initialized.transient)
@@ -7502,10 +7503,10 @@ async function proveWorkspaceTools(req, runtime, opts = {}) {
7502
7503
  );
7503
7504
  }
7504
7505
  }
7505
- async function initializeWithRetry(fetchImpl, url, headers, body) {
7506
+ async function initializeWithRetry(fetchImpl, url, headers, body, retryDelaysMs) {
7506
7507
  let lastFailure = null;
7507
7508
  const deadline = Date.now() + INITIALIZE_RETRY_BUDGET_MS;
7508
- for (let attempt = 0; attempt <= INITIALIZE_RETRY_DELAYS_MS.length; attempt += 1) {
7509
+ for (let attempt = 0; attempt <= retryDelaysMs.length; attempt += 1) {
7509
7510
  try {
7510
7511
  const remainingMs = deadline - Date.now();
7511
7512
  if (remainingMs <= 0) break;
@@ -7529,7 +7530,7 @@ async function initializeWithRetry(fetchImpl, url, headers, body) {
7529
7530
  transient: true
7530
7531
  };
7531
7532
  }
7532
- const retryDelayMs = INITIALIZE_RETRY_DELAYS_MS[attempt];
7533
+ const retryDelayMs = retryDelaysMs[attempt];
7533
7534
  if (retryDelayMs === void 0 || Date.now() + retryDelayMs >= deadline) break;
7534
7535
  await delay(retryDelayMs);
7535
7536
  }
@@ -8165,6 +8166,7 @@ ${reason}`,
8165
8166
  }
8166
8167
  const proof = await proveWorkspaceTools(request, adapter.name, {
8167
8168
  ...this.opts.workspaceProofFetch ? { fetchImpl: this.opts.workspaceProofFetch } : {},
8169
+ ...this.opts.workspaceProofRetryDelaysMs ? { retryDelaysMs: this.opts.workspaceProofRetryDelaysMs } : {},
8168
8170
  harnessFingerprint: turnContext.runtime
8169
8171
  });
8170
8172
  turnLog[proof.ok ? "info" : "error"](
@@ -8982,6 +8984,7 @@ var ASSIGNMENTS_POLL_MS = 6e4;
8982
8984
  var DRAIN_BASE_MS = 1e3;
8983
8985
  var DRAIN_MAX_MS = 3e4;
8984
8986
  var DRAIN_IDLE_MS = 15e3;
8987
+ var DEFAULT_UNRUNNABLE_RETRY_DELAYS_MS = [0, 1e3, 3e3];
8985
8988
  var CompanionSupervisor = class {
8986
8989
  workspaces = /* @__PURE__ */ new Map();
8987
8990
  config;
@@ -9008,6 +9011,7 @@ var CompanionSupervisor = class {
9008
9011
  exitFn;
9009
9012
  reexecFn;
9010
9013
  dispatcherFactory;
9014
+ unrunnableRetryDelaysMs;
9011
9015
  deviceApi = null;
9012
9016
  deviceSub = null;
9013
9017
  heartbeatTimer = null;
@@ -9040,6 +9044,7 @@ var CompanionSupervisor = class {
9040
9044
  this.exitFn = opts.exit ?? ((code) => process.exit(code));
9041
9045
  this.reexecFn = opts.reexec ?? defaultReexec;
9042
9046
  this.dispatcherFactory = opts.dispatcherFactory;
9047
+ this.unrunnableRetryDelaysMs = opts.unrunnableRetryDelaysMs ?? DEFAULT_UNRUNNABLE_RETRY_DELAYS_MS;
9043
9048
  }
9044
9049
  // Stand up the data plane: pair check, initial assignments pull, then the
9045
9050
  // heartbeat + poll loops. A companion with no device token (logged out) does
@@ -9140,13 +9145,7 @@ var CompanionSupervisor = class {
9140
9145
  // CT584: include enumerated models only when the probe SUCCEEDED (non-null).
9141
9146
  // A null (probe failed / no opencode) omits the field, and the server then
9142
9147
  // leaves this device's stored availability untouched.
9143
- ...opencodeModels !== null ? { models: opencodeModels } : {},
9144
- // CT1082: what this machine has that the user hasn't connected, so the web
9145
- // UI can offer it without a companion round-trip. Sent only once a probe has
9146
- // actually landed (`harnessSignals` non-null) — an absent field means "we
9147
- // didn't look this beat" and leaves the server's stored suggestion alone,
9148
- // the same fail-soft contract `models` keeps.
9149
- ...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {}
9148
+ ...opencodeModels !== null ? { models: opencodeModels } : {}
9150
9149
  });
9151
9150
  this.hub.setDevice({ deviceId: res.deviceId });
9152
9151
  this.deviceId = res.deviceId;
@@ -9550,9 +9549,8 @@ var CompanionSupervisor = class {
9550
9549
  // wedge the workspace's whole stream.
9551
9550
  async reportUnrunnableDispatch(wr, payload) {
9552
9551
  if (!this.deviceApi) return false;
9553
- const backoffMs = [0, 1e3, 3e3];
9554
9552
  let lastErr;
9555
- for (const wait of backoffMs) {
9553
+ for (const wait of this.unrunnableRetryDelaysMs) {
9556
9554
  if (this.stopped) return false;
9557
9555
  if (wait > 0) await new Promise((r) => setTimeout(r, wait));
9558
9556
  try {
@@ -10241,6 +10239,7 @@ async function startScript(opts, interactive) {
10241
10239
  return;
10242
10240
  }
10243
10241
  blank();
10242
+ const held = !isDevicePaired() ? await collectHarnessChoices(interactive) : [];
10244
10243
  let paired = null;
10245
10244
  if (!isDevicePaired()) {
10246
10245
  paired = await pairHere(opts, interactive);
@@ -10262,12 +10261,18 @@ async function startScript(opts, interactive) {
10262
10261
  return;
10263
10262
  }
10264
10263
  const runtime = result.runtime;
10264
+ const connected = [];
10265
+ for (const choice of held) {
10266
+ if (!choice.accepted) continue;
10267
+ const outcome = await runtime.connectHarness(choice.runtime, choice.serverUrl);
10268
+ if (outcome.ok) connected.push(HARNESS_LABELS[choice.runtime]);
10269
+ else write(`${INDENT}${outcome.error}`);
10270
+ }
10265
10271
  await runtime.heartbeatNow();
10266
- const offered = await runConnectorOffer(runtime, { interactive, justPaired });
10267
10272
  if (interactive && !opts.foreground) {
10268
10273
  await handOffToBackground(runtime, {
10269
10274
  justPaired,
10270
- spaceAbove: justPaired || offered.printedSomething
10275
+ connected
10271
10276
  });
10272
10277
  return;
10273
10278
  }
@@ -10279,7 +10284,7 @@ async function startScript(opts, interactive) {
10279
10284
  write(`${INDENT}Listening for messages\u2026`);
10280
10285
  setConsoleLogging(true);
10281
10286
  } else {
10282
- if (!offered.printedSomething) blank();
10287
+ blank();
10283
10288
  write(
10284
10289
  `${INDENT}Cabane companion is running. No terminal attached, so it stays in the foreground.`
10285
10290
  );
@@ -10301,11 +10306,60 @@ function reportAlreadyRunning(pid) {
10301
10306
  write("Stop it first with `cabane-companion stop` if you want to relaunch.");
10302
10307
  write("Connect a harness to the running companion: cabane-companion connect claude-code");
10303
10308
  }
10309
+ async function collectHarnessChoices(interactive) {
10310
+ const opencodeUrl = "http://127.0.0.1:4096";
10311
+ const [claudePresent, claudeVersion, codexVersion, opencodeVersion] = await Promise.all([
10312
+ claudeOnPath().catch(() => false),
10313
+ probeCliVersion("claude").catch(() => null),
10314
+ probeCliVersion("codex").catch(() => null),
10315
+ probeOpencodeVersion(opencodeUrl).catch(() => null)
10316
+ ]);
10317
+ const found = [
10318
+ ...claudePresent ? [{ runtime: "claude-code", version: claudeVersion }] : [],
10319
+ ...codexVersion ? [{ runtime: "codex", version: codexVersion }] : [],
10320
+ ...opencodeVersion ? [{ runtime: "opencode", version: opencodeVersion, serverUrl: opencodeUrl }] : []
10321
+ ].sort((a, b) => OFFER_ORDER.indexOf(a.runtime) - OFFER_ORDER.indexOf(b.runtime));
10322
+ if (found.length === 0) {
10323
+ write(
10324
+ `${INDENT}No coding agent found on this machine yet \u2014 you can pair now and add one later.`
10325
+ );
10326
+ blank();
10327
+ return [];
10328
+ }
10329
+ write(`${INDENT}Welcome to Cabane. Let's set up this machine \u2014 two steps, both right here:`);
10330
+ blank();
10331
+ write(`${DATA_INDENT}1. Choose which coding agents Cabane can use on this machine`);
10332
+ write(`${DATA_INDENT}2. Pair this machine with your Cabane account`);
10333
+ blank();
10334
+ const choices = [];
10335
+ for (const harness of found) {
10336
+ const phrase = prePairFoundPhrase(harness);
10337
+ if (!interactive) {
10338
+ write(`${INDENT}${phrase}. Add it with: ${connectCommand(harness.runtime)}`);
10339
+ choices.push({ ...harness, accepted: false });
10340
+ continue;
10341
+ }
10342
+ const accepted = await confirm(`${phrase}. Make it available in Cabane?`);
10343
+ choices.push({ ...harness, accepted });
10344
+ if (accepted) {
10345
+ write(
10346
+ INDENT + tick(`${HARNESS_LABELS[harness.runtime]} \u2014 available once this device is paired.`)
10347
+ );
10348
+ } else {
10349
+ write(`${INDENT}Skipped. Add it any time with: ${connectCommand(harness.runtime)}`);
10350
+ }
10351
+ blank();
10352
+ }
10353
+ return choices;
10354
+ }
10355
+ function prePairFoundPhrase(harness) {
10356
+ if (harness.runtime === "opencode") return `We found OpenCode at ${harness.serverUrl}`;
10357
+ const label = HARNESS_LABELS[harness.runtime];
10358
+ return harness.version ? `We found ${label} on this machine (${harness.version})` : `We found ${label} on this machine`;
10359
+ }
10304
10360
  async function pairHere(opts, interactive) {
10305
10361
  const baseUrl = resolvePairBaseUrl(opts.server);
10306
10362
  const label = deviceLabelFromHostname(hostname2());
10307
- write(`${INDENT}Not paired yet.`);
10308
- blank();
10309
10363
  const aborter = new AbortController();
10310
10364
  const onSigint = () => aborter.abort();
10311
10365
  process.on("SIGINT", onSigint);
@@ -10318,7 +10372,7 @@ async function pairHere(opts, interactive) {
10318
10372
  if (attempt === 1) printFirstCode(code);
10319
10373
  else printFreshCode(code);
10320
10374
  spinner.start(
10321
- `Waiting for you to confirm it in Cabane\u2026 (the code is good for ${minutes} minutes)`
10375
+ `Waiting for you to confirm in Cabane\u2026 (the code is good for ${minutes} minutes)`
10322
10376
  );
10323
10377
  try {
10324
10378
  const device = await awaitEnrollment(baseUrl, code, { signal: aborter.signal });
@@ -10328,7 +10382,7 @@ async function pairHere(opts, interactive) {
10328
10382
  if (err instanceof EnrollmentCancelledError) {
10329
10383
  spinner.clear();
10330
10384
  blank();
10331
- write(`${INDENT}Pairing cancelled. Run \`cabane-companion start\` when you're ready.`);
10385
+ write(`${INDENT}Pairing cancelled. Run cabane-companion start when you're ready.`);
10332
10386
  return null;
10333
10387
  }
10334
10388
  if (!(err instanceof EnrollmentExpiredError)) throw err;
@@ -10339,8 +10393,9 @@ async function pairHere(opts, interactive) {
10339
10393
  }
10340
10394
  spinner.freeze();
10341
10395
  blank();
10342
- write(`${INDENT}Still not confirmed after ${minutesWaited} minutes \u2014 stopping here.`);
10343
- write(`${INDENT}Run \`cabane-companion start\` again when you're ready.`);
10396
+ write(
10397
+ `${INDENT}Still not confirmed after ${minutesWaited} minutes \u2014 stopping here. Run cabane-companion start again when you're ready.`
10398
+ );
10344
10399
  return null;
10345
10400
  }
10346
10401
  }
@@ -10350,11 +10405,11 @@ async function pairHere(opts, interactive) {
10350
10405
  }
10351
10406
  }
10352
10407
  function printFirstCode(code) {
10353
- write(`${INDENT}Enter this code in Cabane:`);
10408
+ write(`${INDENT}Now let's pair this machine. Enter this code in Cabane:`);
10354
10409
  blank();
10355
10410
  write(`${DATA_INDENT}${code.userCode}`);
10356
10411
  blank();
10357
- write(`${INDENT}or open this link:`);
10412
+ write(`${INDENT}or, if Cabane isn\u2019t open:`);
10358
10413
  blank();
10359
10414
  write(`${DATA_INDENT}${code.verificationUriComplete}`);
10360
10415
  blank();
@@ -10369,54 +10424,6 @@ function printFreshCode(code) {
10369
10424
  function possessive(ownerName) {
10370
10425
  return ownerName ? `${ownerName}'s` : "your Cabane";
10371
10426
  }
10372
- async function runConnectorOffer(runtime, ctx) {
10373
- const snapshot = runtime.harnesses();
10374
- const detected = OFFER_ORDER.map((r) => snapshot.find((h) => h.runtime === r)).filter(
10375
- (h) => !!h && h.state === "detected_not_exposed"
10376
- );
10377
- if (detected.length === 0) {
10378
- if (!ctx.justPaired) return { printedSomething: false };
10379
- blank();
10380
- write(
10381
- INDENT + bang(
10382
- "No coding agent found on this machine yet \u2014 install Claude Code, Codex or opencode and sign in,"
10383
- )
10384
- );
10385
- write(`${INDENT} then: cabane-companion connect claude-code (or codex, or opencode)`);
10386
- return { printedSomething: true };
10387
- }
10388
- blank();
10389
- if (!ctx.interactive) {
10390
- for (const h of detected) {
10391
- write(
10392
- `${INDENT}${foundPhrase(h, runtime.config)} but it isn't connected. Connect it: ${connectCommand(h.runtime)}`
10393
- );
10394
- }
10395
- return { printedSomething: true };
10396
- }
10397
- for (const h of detected) {
10398
- const yes = await confirm(`${foundPhrase(h, runtime.config)}. Connect it to Cabane?`);
10399
- if (!yes) {
10400
- write(`${INDENT}Skipped. Connect it later with: ${connectCommand(h.runtime)}`);
10401
- continue;
10402
- }
10403
- const outcome = await runtime.connectHarness(h.runtime);
10404
- if (!outcome.ok) {
10405
- write(`${INDENT}${outcome.error}`);
10406
- continue;
10407
- }
10408
- write(INDENT + tick(outcome.message));
10409
- }
10410
- return { printedSomething: true };
10411
- }
10412
- function foundPhrase(h, cfg) {
10413
- const label = HARNESS_LABELS[h.runtime];
10414
- if (h.runtime === "opencode") {
10415
- const url = cfg.opencode?.serverUrl;
10416
- return url ? `We found ${label} at ${url}` : `We found ${label} on this machine`;
10417
- }
10418
- return h.version ? `We found ${label} on this machine (${h.version})` : `We found ${label} on this machine`;
10419
- }
10420
10427
  function connectCommand(runtime) {
10421
10428
  return `cabane-companion connect ${runtime}`;
10422
10429
  }
@@ -10426,12 +10433,22 @@ async function handOffToBackground(runtime, ctx) {
10426
10433
  if (!outcome.started) {
10427
10434
  return;
10428
10435
  }
10429
- if (ctx.spaceAbove) blank();
10436
+ if (ctx.justPaired) {
10437
+ for (const label of ctx.connected) write(INDENT + tick(`${label} connected.`));
10438
+ if (ctx.connected.length === 0) {
10439
+ write(
10440
+ `${INDENT}Add a coding agent any time: cabane-companion connect claude-code (or codex, or opencode)`
10441
+ );
10442
+ }
10443
+ }
10430
10444
  write(INDENT + tick("Cabane companion is running in the background."));
10431
10445
  write(` Stop: cabane-companion stop Logs: ${tildePath(companionLogPath())}`);
10432
10446
  if (ctx.justPaired) {
10433
10447
  blank();
10434
- write(INDENT + arrow("Head back to Cabane to finish up."));
10448
+ write(`${INDENT}! It won't restart on its own \u2014 after a reboot, or if it ever stops,`);
10449
+ write(`${INDENT} just run cabane-companion start again.`);
10450
+ blank();
10451
+ write(`${INDENT}All set on this side. Finish up in Cabane.`);
10435
10452
  }
10436
10453
  }
10437
10454
  async function runAttached(runtime) {
package/dist/runtime.js CHANGED
@@ -1297,7 +1297,7 @@ async function warnAboutHarnessReadiness(cfg, deps = {}) {
1297
1297
  var HARNESS_LABELS = {
1298
1298
  "claude-code": "Claude Code",
1299
1299
  codex: "Codex",
1300
- opencode: "opencode"
1300
+ opencode: "OpenCode"
1301
1301
  };
1302
1302
  var LABELS = HARNESS_LABELS;
1303
1303
  function deriveHarnessSnapshot(signals) {
@@ -1419,9 +1419,6 @@ function deriveOpencode(signals, manifestHas) {
1419
1419
  enable: "opencode"
1420
1420
  };
1421
1421
  }
1422
- function detectedRuntimesFor(snapshot) {
1423
- return snapshot.harnesses.filter((h) => h.state === "detected_not_exposed").map((h) => ({ runtime: h.runtime, version: h.version }));
1424
- }
1425
1422
  var PROBE_TIMEOUT_MS = 4e3;
1426
1423
  async function probeHarnessSignals(cfg, deps = {}) {
1427
1424
  const probeClaudePresence = deps.probeClaudePresence ?? claudeOnPath;
@@ -6956,16 +6953,22 @@ async function proveWorkspaceTools(req, runtime, opts = {}) {
6956
6953
  "x-cabane-active-conversation": req.cabane.activeConversationId
6957
6954
  };
6958
6955
  try {
6959
- const initialized = await initializeWithRetry(fetchImpl, req.cabane.mcpUrl, headers, {
6960
- jsonrpc: "2.0",
6961
- id: 1,
6962
- method: "initialize",
6963
- params: {
6964
- protocolVersion: "2025-03-26",
6965
- capabilities: {},
6966
- clientInfo: { name: "cabane-companion-readiness", version: "1" }
6967
- }
6968
- });
6956
+ const initialized = await initializeWithRetry(
6957
+ fetchImpl,
6958
+ req.cabane.mcpUrl,
6959
+ headers,
6960
+ {
6961
+ jsonrpc: "2.0",
6962
+ id: 1,
6963
+ method: "initialize",
6964
+ params: {
6965
+ protocolVersion: "2025-03-26",
6966
+ capabilities: {},
6967
+ clientInfo: { name: "cabane-companion-readiness", version: "1" }
6968
+ }
6969
+ },
6970
+ opts.retryDelaysMs ?? INITIALIZE_RETRY_DELAYS_MS
6971
+ );
6969
6972
  if (initialized.status === 401 || initialized.status === 403)
6970
6973
  return fail(base, "authentication_failed", `initialize returned HTTP ${initialized.status}`);
6971
6974
  if (initialized.transient)
@@ -7009,10 +7012,10 @@ async function proveWorkspaceTools(req, runtime, opts = {}) {
7009
7012
  );
7010
7013
  }
7011
7014
  }
7012
- async function initializeWithRetry(fetchImpl, url, headers, body) {
7015
+ async function initializeWithRetry(fetchImpl, url, headers, body, retryDelaysMs) {
7013
7016
  let lastFailure = null;
7014
7017
  const deadline = Date.now() + INITIALIZE_RETRY_BUDGET_MS;
7015
- for (let attempt = 0; attempt <= INITIALIZE_RETRY_DELAYS_MS.length; attempt += 1) {
7018
+ for (let attempt = 0; attempt <= retryDelaysMs.length; attempt += 1) {
7016
7019
  try {
7017
7020
  const remainingMs = deadline - Date.now();
7018
7021
  if (remainingMs <= 0) break;
@@ -7036,7 +7039,7 @@ async function initializeWithRetry(fetchImpl, url, headers, body) {
7036
7039
  transient: true
7037
7040
  };
7038
7041
  }
7039
- const retryDelayMs = INITIALIZE_RETRY_DELAYS_MS[attempt];
7042
+ const retryDelayMs = retryDelaysMs[attempt];
7040
7043
  if (retryDelayMs === void 0 || Date.now() + retryDelayMs >= deadline) break;
7041
7044
  await delay(retryDelayMs);
7042
7045
  }
@@ -7672,6 +7675,7 @@ ${reason}`,
7672
7675
  }
7673
7676
  const proof = await proveWorkspaceTools(request, adapter.name, {
7674
7677
  ...this.opts.workspaceProofFetch ? { fetchImpl: this.opts.workspaceProofFetch } : {},
7678
+ ...this.opts.workspaceProofRetryDelaysMs ? { retryDelaysMs: this.opts.workspaceProofRetryDelaysMs } : {},
7675
7679
  harnessFingerprint: turnContext.runtime
7676
7680
  });
7677
7681
  turnLog[proof.ok ? "info" : "error"](
@@ -8489,6 +8493,7 @@ var ASSIGNMENTS_POLL_MS = 6e4;
8489
8493
  var DRAIN_BASE_MS = 1e3;
8490
8494
  var DRAIN_MAX_MS = 3e4;
8491
8495
  var DRAIN_IDLE_MS = 15e3;
8496
+ var DEFAULT_UNRUNNABLE_RETRY_DELAYS_MS = [0, 1e3, 3e3];
8492
8497
  var CompanionSupervisor = class {
8493
8498
  workspaces = /* @__PURE__ */ new Map();
8494
8499
  config;
@@ -8515,6 +8520,7 @@ var CompanionSupervisor = class {
8515
8520
  exitFn;
8516
8521
  reexecFn;
8517
8522
  dispatcherFactory;
8523
+ unrunnableRetryDelaysMs;
8518
8524
  deviceApi = null;
8519
8525
  deviceSub = null;
8520
8526
  heartbeatTimer = null;
@@ -8547,6 +8553,7 @@ var CompanionSupervisor = class {
8547
8553
  this.exitFn = opts.exit ?? ((code) => process.exit(code));
8548
8554
  this.reexecFn = opts.reexec ?? defaultReexec;
8549
8555
  this.dispatcherFactory = opts.dispatcherFactory;
8556
+ this.unrunnableRetryDelaysMs = opts.unrunnableRetryDelaysMs ?? DEFAULT_UNRUNNABLE_RETRY_DELAYS_MS;
8550
8557
  }
8551
8558
  // Stand up the data plane: pair check, initial assignments pull, then the
8552
8559
  // heartbeat + poll loops. A companion with no device token (logged out) does
@@ -8647,13 +8654,7 @@ var CompanionSupervisor = class {
8647
8654
  // CT584: include enumerated models only when the probe SUCCEEDED (non-null).
8648
8655
  // A null (probe failed / no opencode) omits the field, and the server then
8649
8656
  // leaves this device's stored availability untouched.
8650
- ...opencodeModels !== null ? { models: opencodeModels } : {},
8651
- // CT1082: what this machine has that the user hasn't connected, so the web
8652
- // UI can offer it without a companion round-trip. Sent only once a probe has
8653
- // actually landed (`harnessSignals` non-null) — an absent field means "we
8654
- // didn't look this beat" and leaves the server's stored suggestion alone,
8655
- // the same fail-soft contract `models` keeps.
8656
- ...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {}
8657
+ ...opencodeModels !== null ? { models: opencodeModels } : {}
8657
8658
  });
8658
8659
  this.hub.setDevice({ deviceId: res.deviceId });
8659
8660
  this.deviceId = res.deviceId;
@@ -9057,9 +9058,8 @@ var CompanionSupervisor = class {
9057
9058
  // wedge the workspace's whole stream.
9058
9059
  async reportUnrunnableDispatch(wr, payload) {
9059
9060
  if (!this.deviceApi) return false;
9060
- const backoffMs = [0, 1e3, 3e3];
9061
9061
  let lastErr;
9062
- for (const wait of backoffMs) {
9062
+ for (const wait of this.unrunnableRetryDelaysMs) {
9063
9063
  if (this.stopped) return false;
9064
9064
  if (wait > 0) await new Promise((r) => setTimeout(r, wait));
9065
9065
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.40",
3
+ "version": "0.6.42",
4
4
  "type": "module",
5
5
  "description": "The Cabane Companion (headless): connect a coding agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.",
6
6
  "license": "UNLICENSED",