@cabane/companion 0.6.39 → 0.6.40

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
@@ -2676,9 +2676,6 @@ var DeviceApi = class {
2676
2676
  getAssignments() {
2677
2677
  return this.request("GET", "/api/companion/assignments");
2678
2678
  }
2679
- beginDrain() {
2680
- return this.request("POST", "/api/companion/drain", {});
2681
- }
2682
2679
  // CT1146: report that a dispatch addressed to THIS device can't be run, because
2683
2680
  // the agent isn't one this device runs (and still wasn't after a forced
2684
2681
  // assignments refresh). The server clears the Working flag, retires the dispatch
@@ -9018,8 +9015,6 @@ var CompanionSupervisor = class {
9018
9015
  pollTimer = null;
9019
9016
  refreshing = false;
9020
9017
  stopped = false;
9021
- draining = false;
9022
- restartPending = false;
9023
9018
  // CT484: latch so the companion/server version-skew warning is logged once, not
9024
9019
  // on every 30s heartbeat.
9025
9020
  versionSkewWarned = false;
@@ -9103,7 +9098,7 @@ var CompanionSupervisor = class {
9103
9098
  }
9104
9099
  // ---- device-level loops ----
9105
9100
  kickHeartbeat() {
9106
- if (this.draining || this.inFlightHeartbeat) return;
9101
+ if (this.inFlightHeartbeat) return;
9107
9102
  const pending = this.sendHeartbeat();
9108
9103
  this.inFlightHeartbeat = pending;
9109
9104
  void pending.finally(() => {
@@ -9151,8 +9146,7 @@ var CompanionSupervisor = class {
9151
9146
  // actually landed (`harnessSignals` non-null) — an absent field means "we
9152
9147
  // didn't look this beat" and leaves the server's stored suggestion alone,
9153
9148
  // the same fail-soft contract `models` keeps.
9154
- ...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {},
9155
- ...this.restartPending ? { restarting: true } : {}
9149
+ ...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {}
9156
9150
  });
9157
9151
  this.hub.setDevice({ deviceId: res.deviceId });
9158
9152
  this.deviceId = res.deviceId;
@@ -9508,7 +9502,6 @@ var CompanionSupervisor = class {
9508
9502
  if (ev.id) wr.cursor.settle(ev.id);
9509
9503
  return;
9510
9504
  }
9511
- if (this.draining) return;
9512
9505
  const payload = {
9513
9506
  type: "device:dispatch_requested",
9514
9507
  ...wire.payload
@@ -9870,50 +9863,6 @@ var CompanionSupervisor = class {
9870
9863
  ...[...this.workspaces.values()].flatMap((wr) => wr.sub ? [wr.sub.finished] : [])
9871
9864
  ]);
9872
9865
  }
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
9866
  async requestStop() {
9918
9867
  await this.shutdown();
9919
9868
  this.exitFn(0);
@@ -10145,14 +10094,7 @@ async function createCompanionRuntime(opts = {}) {
10145
10094
  stop: stop2,
10146
10095
  heartbeatNow: () => supervisor.heartbeatNow(),
10147
10096
  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
- }
10097
+ connectHarness
10156
10098
  }
10157
10099
  };
10158
10100
  }
@@ -10281,8 +10223,6 @@ function defaultSpawnDetached(args) {
10281
10223
 
10282
10224
  // src/commands/start.ts
10283
10225
  var FORCE_EXIT_MS = 4e3;
10284
- var DEPLOY_REEXEC_EXIT = 75;
10285
- var DEPLOY_GRACE_MS = 6.5 * 60 * 60 * 1e3;
10286
10226
  var MAX_CODES = 3;
10287
10227
  async function start(opts = {}) {
10288
10228
  const interactive = isInteractive();
@@ -10521,31 +10461,6 @@ companion: received ${signal}, shutting down\u2026
10521
10461
  };
10522
10462
  process.on("SIGINT", () => void shutdown("SIGINT"));
10523
10463
  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
10464
  });
10550
10465
  }
10551
10466
 
package/dist/runtime.js CHANGED
@@ -2104,9 +2104,6 @@ var DeviceApi = class {
2104
2104
  getAssignments() {
2105
2105
  return this.request("GET", "/api/companion/assignments");
2106
2106
  }
2107
- beginDrain() {
2108
- return this.request("POST", "/api/companion/drain", {});
2109
- }
2110
2107
  // CT1146: report that a dispatch addressed to THIS device can't be run, because
2111
2108
  // the agent isn't one this device runs (and still wasn't after a forced
2112
2109
  // assignments refresh). The server clears the Working flag, retires the dispatch
@@ -8525,8 +8522,6 @@ var CompanionSupervisor = class {
8525
8522
  pollTimer = null;
8526
8523
  refreshing = false;
8527
8524
  stopped = false;
8528
- draining = false;
8529
- restartPending = false;
8530
8525
  // CT484: latch so the companion/server version-skew warning is logged once, not
8531
8526
  // on every 30s heartbeat.
8532
8527
  versionSkewWarned = false;
@@ -8610,7 +8605,7 @@ var CompanionSupervisor = class {
8610
8605
  }
8611
8606
  // ---- device-level loops ----
8612
8607
  kickHeartbeat() {
8613
- if (this.draining || this.inFlightHeartbeat) return;
8608
+ if (this.inFlightHeartbeat) return;
8614
8609
  const pending = this.sendHeartbeat();
8615
8610
  this.inFlightHeartbeat = pending;
8616
8611
  void pending.finally(() => {
@@ -8658,8 +8653,7 @@ var CompanionSupervisor = class {
8658
8653
  // actually landed (`harnessSignals` non-null) — an absent field means "we
8659
8654
  // didn't look this beat" and leaves the server's stored suggestion alone,
8660
8655
  // the same fail-soft contract `models` keeps.
8661
- ...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {},
8662
- ...this.restartPending ? { restarting: true } : {}
8656
+ ...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {}
8663
8657
  });
8664
8658
  this.hub.setDevice({ deviceId: res.deviceId });
8665
8659
  this.deviceId = res.deviceId;
@@ -9015,7 +9009,6 @@ var CompanionSupervisor = class {
9015
9009
  if (ev.id) wr.cursor.settle(ev.id);
9016
9010
  return;
9017
9011
  }
9018
- if (this.draining) return;
9019
9012
  const payload = {
9020
9013
  type: "device:dispatch_requested",
9021
9014
  ...wire.payload
@@ -9377,50 +9370,6 @@ var CompanionSupervisor = class {
9377
9370
  ...[...this.workspaces.values()].flatMap((wr) => wr.sub ? [wr.sub.finished] : [])
9378
9371
  ]);
9379
9372
  }
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
9373
  async requestStop() {
9425
9374
  await this.shutdown();
9426
9375
  this.exitFn(0);
@@ -9652,14 +9601,7 @@ async function createCompanionRuntime(opts = {}) {
9652
9601
  stop,
9653
9602
  heartbeatNow: () => supervisor.heartbeatNow(),
9654
9603
  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
- }
9604
+ connectHarness
9663
9605
  }
9664
9606
  };
9665
9607
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.39",
3
+ "version": "0.6.40",
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",