@cabane/companion 0.6.39 → 0.6.41

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
@@ -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)}`;
@@ -2676,9 +2671,6 @@ var DeviceApi = class {
2676
2671
  getAssignments() {
2677
2672
  return this.request("GET", "/api/companion/assignments");
2678
2673
  }
2679
- beginDrain() {
2680
- return this.request("POST", "/api/companion/drain", {});
2681
- }
2682
2674
  // CT1146: report that a dispatch addressed to THIS device can't be run, because
2683
2675
  // the agent isn't one this device runs (and still wasn't after a forced
2684
2676
  // assignments refresh). The server clears the Working flag, retires the dispatch
@@ -9018,8 +9010,6 @@ var CompanionSupervisor = class {
9018
9010
  pollTimer = null;
9019
9011
  refreshing = false;
9020
9012
  stopped = false;
9021
- draining = false;
9022
- restartPending = false;
9023
9013
  // CT484: latch so the companion/server version-skew warning is logged once, not
9024
9014
  // on every 30s heartbeat.
9025
9015
  versionSkewWarned = false;
@@ -9103,7 +9093,7 @@ var CompanionSupervisor = class {
9103
9093
  }
9104
9094
  // ---- device-level loops ----
9105
9095
  kickHeartbeat() {
9106
- if (this.draining || this.inFlightHeartbeat) return;
9096
+ if (this.inFlightHeartbeat) return;
9107
9097
  const pending = this.sendHeartbeat();
9108
9098
  this.inFlightHeartbeat = pending;
9109
9099
  void pending.finally(() => {
@@ -9145,14 +9135,7 @@ var CompanionSupervisor = class {
9145
9135
  // CT584: include enumerated models only when the probe SUCCEEDED (non-null).
9146
9136
  // A null (probe failed / no opencode) omits the field, and the server then
9147
9137
  // leaves this device's stored availability untouched.
9148
- ...opencodeModels !== null ? { models: opencodeModels } : {},
9149
- // CT1082: what this machine has that the user hasn't connected, so the web
9150
- // UI can offer it without a companion round-trip. Sent only once a probe has
9151
- // actually landed (`harnessSignals` non-null) — an absent field means "we
9152
- // didn't look this beat" and leaves the server's stored suggestion alone,
9153
- // the same fail-soft contract `models` keeps.
9154
- ...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {},
9155
- ...this.restartPending ? { restarting: true } : {}
9138
+ ...opencodeModels !== null ? { models: opencodeModels } : {}
9156
9139
  });
9157
9140
  this.hub.setDevice({ deviceId: res.deviceId });
9158
9141
  this.deviceId = res.deviceId;
@@ -9508,7 +9491,6 @@ var CompanionSupervisor = class {
9508
9491
  if (ev.id) wr.cursor.settle(ev.id);
9509
9492
  return;
9510
9493
  }
9511
- if (this.draining) return;
9512
9494
  const payload = {
9513
9495
  type: "device:dispatch_requested",
9514
9496
  ...wire.payload
@@ -9870,50 +9852,6 @@ var CompanionSupervisor = class {
9870
9852
  ...[...this.workspaces.values()].flatMap((wr) => wr.sub ? [wr.sub.finished] : [])
9871
9853
  ]);
9872
9854
  }
9873
- async drainForRestart(graceMs) {
9874
- this.restartPending = true;
9875
- if (this.inFlightHeartbeat) await this.inFlightHeartbeat;
9876
- if (!this.deviceApi) throw new Error("cannot establish deploy drain before pairing");
9877
- await this.sendHeartbeat();
9878
- const drainStartedAt = Date.now();
9879
- const deadline = Date.now() + Math.max(0, graceMs);
9880
- let timedOut = false;
9881
- while (true) {
9882
- const turns = [...this.workspaces.values()].flatMap((wr) => [...wr.chains.values()]);
9883
- if (turns.length === 0) break;
9884
- const remainingMs = deadline - Date.now();
9885
- if (remainingMs <= 0) {
9886
- timedOut = true;
9887
- break;
9888
- }
9889
- let timer;
9890
- await Promise.race([
9891
- Promise.allSettled(turns),
9892
- new Promise((resolve) => {
9893
- timer = setTimeout(resolve, remainingMs);
9894
- timer.unref?.();
9895
- })
9896
- ]);
9897
- if (timer) clearTimeout(timer);
9898
- }
9899
- this.log.info(
9900
- { waitedMs: Date.now() - drainStartedAt, timedOut },
9901
- timedOut ? "companion: deploy drain grace elapsed; fencing admission for restart" : "companion: deploy drain reached a quiet point; fencing admission for restart"
9902
- );
9903
- this.draining = true;
9904
- if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
9905
- if (this.pollTimer) clearInterval(this.pollTimer);
9906
- if (this.inFlightHeartbeat) await this.inFlightHeartbeat;
9907
- await this.deviceApi.beginDrain();
9908
- for (const wr of this.workspaces.values()) wr.sub?.stop();
9909
- await Promise.allSettled(
9910
- [...this.workspaces.values()].flatMap(
9911
- (wr) => [...wr.agents.values()].map((agent) => agent.api.drainOutbox())
9912
- )
9913
- );
9914
- await this.shutdown();
9915
- return { drained: !timedOut };
9916
- }
9917
9855
  async requestStop() {
9918
9856
  await this.shutdown();
9919
9857
  this.exitFn(0);
@@ -10145,14 +10083,7 @@ async function createCompanionRuntime(opts = {}) {
10145
10083
  stop: stop2,
10146
10084
  heartbeatNow: () => supervisor.heartbeatNow(),
10147
10085
  harnesses: () => hub.statusJson().harnesses ?? [],
10148
- connectHarness,
10149
- drainForRestart: async (graceMs) => {
10150
- clearRuntimeStateIfOurs(instanceId);
10151
- const result = await supervisor.drainForRestart(graceMs);
10152
- await closeSurfaces(control, dashboard);
10153
- stopped = true;
10154
- return result;
10155
- }
10086
+ connectHarness
10156
10087
  }
10157
10088
  };
10158
10089
  }
@@ -10281,8 +10212,6 @@ function defaultSpawnDetached(args) {
10281
10212
 
10282
10213
  // src/commands/start.ts
10283
10214
  var FORCE_EXIT_MS = 4e3;
10284
- var DEPLOY_REEXEC_EXIT = 75;
10285
- var DEPLOY_GRACE_MS = 6.5 * 60 * 60 * 1e3;
10286
10215
  var MAX_CODES = 3;
10287
10216
  async function start(opts = {}) {
10288
10217
  const interactive = isInteractive();
@@ -10301,6 +10230,7 @@ async function startScript(opts, interactive) {
10301
10230
  return;
10302
10231
  }
10303
10232
  blank();
10233
+ const held = !isDevicePaired() ? await collectHarnessChoices(interactive) : [];
10304
10234
  let paired = null;
10305
10235
  if (!isDevicePaired()) {
10306
10236
  paired = await pairHere(opts, interactive);
@@ -10322,12 +10252,18 @@ async function startScript(opts, interactive) {
10322
10252
  return;
10323
10253
  }
10324
10254
  const runtime = result.runtime;
10255
+ const connected = [];
10256
+ for (const choice of held) {
10257
+ if (!choice.accepted) continue;
10258
+ const outcome = await runtime.connectHarness(choice.runtime, choice.serverUrl);
10259
+ if (outcome.ok) connected.push(HARNESS_LABELS[choice.runtime]);
10260
+ else write(`${INDENT}${outcome.error}`);
10261
+ }
10325
10262
  await runtime.heartbeatNow();
10326
- const offered = await runConnectorOffer(runtime, { interactive, justPaired });
10327
10263
  if (interactive && !opts.foreground) {
10328
10264
  await handOffToBackground(runtime, {
10329
10265
  justPaired,
10330
- spaceAbove: justPaired || offered.printedSomething
10266
+ connected
10331
10267
  });
10332
10268
  return;
10333
10269
  }
@@ -10339,7 +10275,7 @@ async function startScript(opts, interactive) {
10339
10275
  write(`${INDENT}Listening for messages\u2026`);
10340
10276
  setConsoleLogging(true);
10341
10277
  } else {
10342
- if (!offered.printedSomething) blank();
10278
+ blank();
10343
10279
  write(
10344
10280
  `${INDENT}Cabane companion is running. No terminal attached, so it stays in the foreground.`
10345
10281
  );
@@ -10361,11 +10297,60 @@ function reportAlreadyRunning(pid) {
10361
10297
  write("Stop it first with `cabane-companion stop` if you want to relaunch.");
10362
10298
  write("Connect a harness to the running companion: cabane-companion connect claude-code");
10363
10299
  }
10300
+ async function collectHarnessChoices(interactive) {
10301
+ const opencodeUrl = "http://127.0.0.1:4096";
10302
+ const [claudePresent, claudeVersion, codexVersion, opencodeVersion] = await Promise.all([
10303
+ claudeOnPath().catch(() => false),
10304
+ probeCliVersion("claude").catch(() => null),
10305
+ probeCliVersion("codex").catch(() => null),
10306
+ probeOpencodeVersion(opencodeUrl).catch(() => null)
10307
+ ]);
10308
+ const found = [
10309
+ ...claudePresent ? [{ runtime: "claude-code", version: claudeVersion }] : [],
10310
+ ...codexVersion ? [{ runtime: "codex", version: codexVersion }] : [],
10311
+ ...opencodeVersion ? [{ runtime: "opencode", version: opencodeVersion, serverUrl: opencodeUrl }] : []
10312
+ ].sort((a, b) => OFFER_ORDER.indexOf(a.runtime) - OFFER_ORDER.indexOf(b.runtime));
10313
+ if (found.length === 0) {
10314
+ write(
10315
+ `${INDENT}No coding agent found on this machine yet \u2014 you can pair now and add one later.`
10316
+ );
10317
+ blank();
10318
+ return [];
10319
+ }
10320
+ write(`${INDENT}Welcome to Cabane. Let's set up this machine \u2014 two steps, both right here:`);
10321
+ blank();
10322
+ write(`${DATA_INDENT}1. Choose which coding agents Cabane can use on this machine`);
10323
+ write(`${DATA_INDENT}2. Pair this machine with your Cabane account`);
10324
+ blank();
10325
+ const choices = [];
10326
+ for (const harness of found) {
10327
+ const phrase = prePairFoundPhrase(harness);
10328
+ if (!interactive) {
10329
+ write(`${INDENT}${phrase}. Add it with: ${connectCommand(harness.runtime)}`);
10330
+ choices.push({ ...harness, accepted: false });
10331
+ continue;
10332
+ }
10333
+ const accepted = await confirm(`${phrase}. Make it available in Cabane?`);
10334
+ choices.push({ ...harness, accepted });
10335
+ if (accepted) {
10336
+ write(
10337
+ INDENT + tick(`${HARNESS_LABELS[harness.runtime]} \u2014 available once this device is paired.`)
10338
+ );
10339
+ } else {
10340
+ write(`${INDENT}Skipped. Add it any time with: ${connectCommand(harness.runtime)}`);
10341
+ }
10342
+ blank();
10343
+ }
10344
+ return choices;
10345
+ }
10346
+ function prePairFoundPhrase(harness) {
10347
+ if (harness.runtime === "opencode") return `We found OpenCode at ${harness.serverUrl}`;
10348
+ const label = HARNESS_LABELS[harness.runtime];
10349
+ return harness.version ? `We found ${label} on this machine (${harness.version})` : `We found ${label} on this machine`;
10350
+ }
10364
10351
  async function pairHere(opts, interactive) {
10365
10352
  const baseUrl = resolvePairBaseUrl(opts.server);
10366
10353
  const label = deviceLabelFromHostname(hostname2());
10367
- write(`${INDENT}Not paired yet.`);
10368
- blank();
10369
10354
  const aborter = new AbortController();
10370
10355
  const onSigint = () => aborter.abort();
10371
10356
  process.on("SIGINT", onSigint);
@@ -10378,7 +10363,7 @@ async function pairHere(opts, interactive) {
10378
10363
  if (attempt === 1) printFirstCode(code);
10379
10364
  else printFreshCode(code);
10380
10365
  spinner.start(
10381
- `Waiting for you to confirm it in Cabane\u2026 (the code is good for ${minutes} minutes)`
10366
+ `Waiting for you to confirm in Cabane\u2026 (the code is good for ${minutes} minutes)`
10382
10367
  );
10383
10368
  try {
10384
10369
  const device = await awaitEnrollment(baseUrl, code, { signal: aborter.signal });
@@ -10388,7 +10373,7 @@ async function pairHere(opts, interactive) {
10388
10373
  if (err instanceof EnrollmentCancelledError) {
10389
10374
  spinner.clear();
10390
10375
  blank();
10391
- write(`${INDENT}Pairing cancelled. Run \`cabane-companion start\` when you're ready.`);
10376
+ write(`${INDENT}Pairing cancelled. Run cabane-companion start when you're ready.`);
10392
10377
  return null;
10393
10378
  }
10394
10379
  if (!(err instanceof EnrollmentExpiredError)) throw err;
@@ -10399,8 +10384,9 @@ async function pairHere(opts, interactive) {
10399
10384
  }
10400
10385
  spinner.freeze();
10401
10386
  blank();
10402
- write(`${INDENT}Still not confirmed after ${minutesWaited} minutes \u2014 stopping here.`);
10403
- write(`${INDENT}Run \`cabane-companion start\` again when you're ready.`);
10387
+ write(
10388
+ `${INDENT}Still not confirmed after ${minutesWaited} minutes \u2014 stopping here. Run cabane-companion start again when you're ready.`
10389
+ );
10404
10390
  return null;
10405
10391
  }
10406
10392
  }
@@ -10410,11 +10396,11 @@ async function pairHere(opts, interactive) {
10410
10396
  }
10411
10397
  }
10412
10398
  function printFirstCode(code) {
10413
- write(`${INDENT}Enter this code in Cabane:`);
10399
+ write(`${INDENT}Now let's pair this machine. Enter this code in Cabane:`);
10414
10400
  blank();
10415
10401
  write(`${DATA_INDENT}${code.userCode}`);
10416
10402
  blank();
10417
- write(`${INDENT}or open this link:`);
10403
+ write(`${INDENT}or, if Cabane isn\u2019t open:`);
10418
10404
  blank();
10419
10405
  write(`${DATA_INDENT}${code.verificationUriComplete}`);
10420
10406
  blank();
@@ -10429,54 +10415,6 @@ function printFreshCode(code) {
10429
10415
  function possessive(ownerName) {
10430
10416
  return ownerName ? `${ownerName}'s` : "your Cabane";
10431
10417
  }
10432
- async function runConnectorOffer(runtime, ctx) {
10433
- const snapshot = runtime.harnesses();
10434
- const detected = OFFER_ORDER.map((r) => snapshot.find((h) => h.runtime === r)).filter(
10435
- (h) => !!h && h.state === "detected_not_exposed"
10436
- );
10437
- if (detected.length === 0) {
10438
- if (!ctx.justPaired) return { printedSomething: false };
10439
- blank();
10440
- write(
10441
- INDENT + bang(
10442
- "No coding agent found on this machine yet \u2014 install Claude Code, Codex or opencode and sign in,"
10443
- )
10444
- );
10445
- write(`${INDENT} then: cabane-companion connect claude-code (or codex, or opencode)`);
10446
- return { printedSomething: true };
10447
- }
10448
- blank();
10449
- if (!ctx.interactive) {
10450
- for (const h of detected) {
10451
- write(
10452
- `${INDENT}${foundPhrase(h, runtime.config)} but it isn't connected. Connect it: ${connectCommand(h.runtime)}`
10453
- );
10454
- }
10455
- return { printedSomething: true };
10456
- }
10457
- for (const h of detected) {
10458
- const yes = await confirm(`${foundPhrase(h, runtime.config)}. Connect it to Cabane?`);
10459
- if (!yes) {
10460
- write(`${INDENT}Skipped. Connect it later with: ${connectCommand(h.runtime)}`);
10461
- continue;
10462
- }
10463
- const outcome = await runtime.connectHarness(h.runtime);
10464
- if (!outcome.ok) {
10465
- write(`${INDENT}${outcome.error}`);
10466
- continue;
10467
- }
10468
- write(INDENT + tick(outcome.message));
10469
- }
10470
- return { printedSomething: true };
10471
- }
10472
- function foundPhrase(h, cfg) {
10473
- const label = HARNESS_LABELS[h.runtime];
10474
- if (h.runtime === "opencode") {
10475
- const url = cfg.opencode?.serverUrl;
10476
- return url ? `We found ${label} at ${url}` : `We found ${label} on this machine`;
10477
- }
10478
- return h.version ? `We found ${label} on this machine (${h.version})` : `We found ${label} on this machine`;
10479
- }
10480
10418
  function connectCommand(runtime) {
10481
10419
  return `cabane-companion connect ${runtime}`;
10482
10420
  }
@@ -10486,12 +10424,22 @@ async function handOffToBackground(runtime, ctx) {
10486
10424
  if (!outcome.started) {
10487
10425
  return;
10488
10426
  }
10489
- if (ctx.spaceAbove) blank();
10427
+ if (ctx.justPaired) {
10428
+ for (const label of ctx.connected) write(INDENT + tick(`${label} connected.`));
10429
+ if (ctx.connected.length === 0) {
10430
+ write(
10431
+ `${INDENT}Add a coding agent any time: cabane-companion connect claude-code (or codex, or opencode)`
10432
+ );
10433
+ }
10434
+ }
10490
10435
  write(INDENT + tick("Cabane companion is running in the background."));
10491
10436
  write(` Stop: cabane-companion stop Logs: ${tildePath(companionLogPath())}`);
10492
10437
  if (ctx.justPaired) {
10493
10438
  blank();
10494
- write(INDENT + arrow("Head back to Cabane to finish up."));
10439
+ write(`${INDENT}! It won't restart on its own \u2014 after a reboot, or if it ever stops,`);
10440
+ write(`${INDENT} just run cabane-companion start again.`);
10441
+ blank();
10442
+ write(`${INDENT}All set on this side. Finish up in Cabane.`);
10495
10443
  }
10496
10444
  }
10497
10445
  async function runAttached(runtime) {
@@ -10521,31 +10469,6 @@ companion: received ${signal}, shutting down\u2026
10521
10469
  };
10522
10470
  process.on("SIGINT", () => void shutdown("SIGINT"));
10523
10471
  process.on("SIGTERM", () => void shutdown("SIGTERM"));
10524
- process.on("SIGUSR2", () => {
10525
- if (shuttingDown) return;
10526
- shuttingDown = true;
10527
- const configuredGrace = Number.parseInt(
10528
- process.env.CABANE_COMPANION_DEPLOY_GRACE_MS ?? String(DEPLOY_GRACE_MS),
10529
- 10
10530
- );
10531
- const graceMs = Number.isFinite(configuredGrace) ? configuredGrace : DEPLOY_GRACE_MS;
10532
- process.stdout.write(`
10533
- companion: deploy drain requested (${graceMs}ms grace)\u2026
10534
- `);
10535
- void runtime.drainForRestart(graceMs).then(({ drained }) => {
10536
- process.stdout.write(
10537
- drained ? "companion: deploy drain complete; re-execing onto the new dist.\n" : "companion: deploy grace expired; re-execing \u2014 unfinished turns resume after restart.\n"
10538
- );
10539
- resolve();
10540
- process.exit(DEPLOY_REEXEC_EXIT);
10541
- }).catch((err) => {
10542
- process.stderr.write(
10543
- `companion: deploy drain failed: ${err instanceof Error ? err.message : String(err)}
10544
- `
10545
- );
10546
- process.exit(DEPLOY_REEXEC_EXIT);
10547
- });
10548
- });
10549
10472
  });
10550
10473
  }
10551
10474
 
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;
@@ -2104,9 +2101,6 @@ var DeviceApi = class {
2104
2101
  getAssignments() {
2105
2102
  return this.request("GET", "/api/companion/assignments");
2106
2103
  }
2107
- beginDrain() {
2108
- return this.request("POST", "/api/companion/drain", {});
2109
- }
2110
2104
  // CT1146: report that a dispatch addressed to THIS device can't be run, because
2111
2105
  // the agent isn't one this device runs (and still wasn't after a forced
2112
2106
  // assignments refresh). The server clears the Working flag, retires the dispatch
@@ -8525,8 +8519,6 @@ var CompanionSupervisor = class {
8525
8519
  pollTimer = null;
8526
8520
  refreshing = false;
8527
8521
  stopped = false;
8528
- draining = false;
8529
- restartPending = false;
8530
8522
  // CT484: latch so the companion/server version-skew warning is logged once, not
8531
8523
  // on every 30s heartbeat.
8532
8524
  versionSkewWarned = false;
@@ -8610,7 +8602,7 @@ var CompanionSupervisor = class {
8610
8602
  }
8611
8603
  // ---- device-level loops ----
8612
8604
  kickHeartbeat() {
8613
- if (this.draining || this.inFlightHeartbeat) return;
8605
+ if (this.inFlightHeartbeat) return;
8614
8606
  const pending = this.sendHeartbeat();
8615
8607
  this.inFlightHeartbeat = pending;
8616
8608
  void pending.finally(() => {
@@ -8652,14 +8644,7 @@ var CompanionSupervisor = class {
8652
8644
  // CT584: include enumerated models only when the probe SUCCEEDED (non-null).
8653
8645
  // A null (probe failed / no opencode) omits the field, and the server then
8654
8646
  // leaves this device's stored availability untouched.
8655
- ...opencodeModels !== null ? { models: opencodeModels } : {},
8656
- // CT1082: what this machine has that the user hasn't connected, so the web
8657
- // UI can offer it without a companion round-trip. Sent only once a probe has
8658
- // actually landed (`harnessSignals` non-null) — an absent field means "we
8659
- // didn't look this beat" and leaves the server's stored suggestion alone,
8660
- // the same fail-soft contract `models` keeps.
8661
- ...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {},
8662
- ...this.restartPending ? { restarting: true } : {}
8647
+ ...opencodeModels !== null ? { models: opencodeModels } : {}
8663
8648
  });
8664
8649
  this.hub.setDevice({ deviceId: res.deviceId });
8665
8650
  this.deviceId = res.deviceId;
@@ -9015,7 +9000,6 @@ var CompanionSupervisor = class {
9015
9000
  if (ev.id) wr.cursor.settle(ev.id);
9016
9001
  return;
9017
9002
  }
9018
- if (this.draining) return;
9019
9003
  const payload = {
9020
9004
  type: "device:dispatch_requested",
9021
9005
  ...wire.payload
@@ -9377,50 +9361,6 @@ var CompanionSupervisor = class {
9377
9361
  ...[...this.workspaces.values()].flatMap((wr) => wr.sub ? [wr.sub.finished] : [])
9378
9362
  ]);
9379
9363
  }
9380
- async drainForRestart(graceMs) {
9381
- this.restartPending = true;
9382
- if (this.inFlightHeartbeat) await this.inFlightHeartbeat;
9383
- if (!this.deviceApi) throw new Error("cannot establish deploy drain before pairing");
9384
- await this.sendHeartbeat();
9385
- const drainStartedAt = Date.now();
9386
- const deadline = Date.now() + Math.max(0, graceMs);
9387
- let timedOut = false;
9388
- while (true) {
9389
- const turns = [...this.workspaces.values()].flatMap((wr) => [...wr.chains.values()]);
9390
- if (turns.length === 0) break;
9391
- const remainingMs = deadline - Date.now();
9392
- if (remainingMs <= 0) {
9393
- timedOut = true;
9394
- break;
9395
- }
9396
- let timer;
9397
- await Promise.race([
9398
- Promise.allSettled(turns),
9399
- new Promise((resolve) => {
9400
- timer = setTimeout(resolve, remainingMs);
9401
- timer.unref?.();
9402
- })
9403
- ]);
9404
- if (timer) clearTimeout(timer);
9405
- }
9406
- this.log.info(
9407
- { waitedMs: Date.now() - drainStartedAt, timedOut },
9408
- timedOut ? "companion: deploy drain grace elapsed; fencing admission for restart" : "companion: deploy drain reached a quiet point; fencing admission for restart"
9409
- );
9410
- this.draining = true;
9411
- if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
9412
- if (this.pollTimer) clearInterval(this.pollTimer);
9413
- if (this.inFlightHeartbeat) await this.inFlightHeartbeat;
9414
- await this.deviceApi.beginDrain();
9415
- for (const wr of this.workspaces.values()) wr.sub?.stop();
9416
- await Promise.allSettled(
9417
- [...this.workspaces.values()].flatMap(
9418
- (wr) => [...wr.agents.values()].map((agent) => agent.api.drainOutbox())
9419
- )
9420
- );
9421
- await this.shutdown();
9422
- return { drained: !timedOut };
9423
- }
9424
9364
  async requestStop() {
9425
9365
  await this.shutdown();
9426
9366
  this.exitFn(0);
@@ -9652,14 +9592,7 @@ async function createCompanionRuntime(opts = {}) {
9652
9592
  stop,
9653
9593
  heartbeatNow: () => supervisor.heartbeatNow(),
9654
9594
  harnesses: () => hub.statusJson().harnesses ?? [],
9655
- connectHarness,
9656
- drainForRestart: async (graceMs) => {
9657
- clearRuntimeStateIfOurs(instanceId);
9658
- const result = await supervisor.drainForRestart(graceMs);
9659
- await closeSurfaces(control, dashboard);
9660
- stopped = true;
9661
- return result;
9662
- }
9595
+ connectHarness
9663
9596
  }
9664
9597
  };
9665
9598
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.39",
3
+ "version": "0.6.41",
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",