@cabane/companion 0.6.25 → 0.6.26

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
@@ -8377,6 +8377,7 @@ var CompanionSupervisor = class {
8377
8377
  refreshing = false;
8378
8378
  stopped = false;
8379
8379
  draining = false;
8380
+ restartPending = false;
8380
8381
  // CT484: latch so the companion/server version-skew warning is logged once, not
8381
8382
  // on every 30s heartbeat.
8382
8383
  versionSkewWarned = false;
@@ -8487,7 +8488,8 @@ var CompanionSupervisor = class {
8487
8488
  // actually landed (`harnessSignals` non-null) — an absent field means "we
8488
8489
  // didn't look this beat" and leaves the server's stored suggestion alone,
8489
8490
  // the same fail-soft contract `models` keeps.
8490
- ...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {}
8491
+ ...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {},
8492
+ ...this.restartPending ? { restarting: true } : {}
8491
8493
  });
8492
8494
  this.hub.setDevice({ deviceId: res.deviceId });
8493
8495
  this.deviceId = res.deviceId;
@@ -9125,32 +9127,41 @@ var CompanionSupervisor = class {
9125
9127
  );
9126
9128
  }
9127
9129
  async drainForRestart(graceMs) {
9128
- this.draining = true;
9129
- if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
9130
- if (this.pollTimer) clearInterval(this.pollTimer);
9130
+ this.restartPending = true;
9131
9131
  if (this.inFlightHeartbeat) await this.inFlightHeartbeat;
9132
9132
  if (!this.deviceApi) throw new Error("cannot establish deploy drain before pairing");
9133
- await this.deviceApi.beginDrain();
9134
- for (const wr of this.workspaces.values()) wr.sub?.stop();
9135
- const turns = [...this.workspaces.values()].flatMap((wr) => [...wr.chains.values()]);
9133
+ await this.sendHeartbeat();
9134
+ const drainStartedAt = Date.now();
9135
+ const deadline = Date.now() + Math.max(0, graceMs);
9136
9136
  let timedOut = false;
9137
- if (turns.length > 0) {
9137
+ while (true) {
9138
+ const turns = [...this.workspaces.values()].flatMap((wr) => [...wr.chains.values()]);
9139
+ if (turns.length === 0) break;
9140
+ const remainingMs = deadline - Date.now();
9141
+ if (remainingMs <= 0) {
9142
+ timedOut = true;
9143
+ break;
9144
+ }
9138
9145
  let timer;
9139
9146
  await Promise.race([
9140
9147
  Promise.allSettled(turns),
9141
9148
  new Promise((resolve) => {
9142
- timer = setTimeout(
9143
- () => {
9144
- timedOut = true;
9145
- resolve();
9146
- },
9147
- Math.max(0, graceMs)
9148
- );
9149
+ timer = setTimeout(resolve, remainingMs);
9149
9150
  timer.unref?.();
9150
9151
  })
9151
9152
  ]);
9152
9153
  if (timer) clearTimeout(timer);
9153
9154
  }
9155
+ this.log.info(
9156
+ { waitedMs: Date.now() - drainStartedAt, timedOut },
9157
+ timedOut ? "companion: deploy drain grace elapsed; fencing admission for restart" : "companion: deploy drain reached a quiet point; fencing admission for restart"
9158
+ );
9159
+ this.draining = true;
9160
+ if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
9161
+ if (this.pollTimer) clearInterval(this.pollTimer);
9162
+ if (this.inFlightHeartbeat) await this.inFlightHeartbeat;
9163
+ await this.deviceApi.beginDrain();
9164
+ for (const wr of this.workspaces.values()) wr.sub?.stop();
9154
9165
  await Promise.allSettled(
9155
9166
  [...this.workspaces.values()].flatMap(
9156
9167
  (wr) => [...wr.agents.values()].map((agent) => agent.api.drainOutbox())
package/dist/runtime.js CHANGED
@@ -7996,6 +7996,7 @@ var CompanionSupervisor = class {
7996
7996
  refreshing = false;
7997
7997
  stopped = false;
7998
7998
  draining = false;
7999
+ restartPending = false;
7999
8000
  // CT484: latch so the companion/server version-skew warning is logged once, not
8000
8001
  // on every 30s heartbeat.
8001
8002
  versionSkewWarned = false;
@@ -8106,7 +8107,8 @@ var CompanionSupervisor = class {
8106
8107
  // actually landed (`harnessSignals` non-null) — an absent field means "we
8107
8108
  // didn't look this beat" and leaves the server's stored suggestion alone,
8108
8109
  // the same fail-soft contract `models` keeps.
8109
- ...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {}
8110
+ ...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {},
8111
+ ...this.restartPending ? { restarting: true } : {}
8110
8112
  });
8111
8113
  this.hub.setDevice({ deviceId: res.deviceId });
8112
8114
  this.deviceId = res.deviceId;
@@ -8744,32 +8746,41 @@ var CompanionSupervisor = class {
8744
8746
  );
8745
8747
  }
8746
8748
  async drainForRestart(graceMs) {
8747
- this.draining = true;
8748
- if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
8749
- if (this.pollTimer) clearInterval(this.pollTimer);
8749
+ this.restartPending = true;
8750
8750
  if (this.inFlightHeartbeat) await this.inFlightHeartbeat;
8751
8751
  if (!this.deviceApi) throw new Error("cannot establish deploy drain before pairing");
8752
- await this.deviceApi.beginDrain();
8753
- for (const wr of this.workspaces.values()) wr.sub?.stop();
8754
- const turns = [...this.workspaces.values()].flatMap((wr) => [...wr.chains.values()]);
8752
+ await this.sendHeartbeat();
8753
+ const drainStartedAt = Date.now();
8754
+ const deadline = Date.now() + Math.max(0, graceMs);
8755
8755
  let timedOut = false;
8756
- if (turns.length > 0) {
8756
+ while (true) {
8757
+ const turns = [...this.workspaces.values()].flatMap((wr) => [...wr.chains.values()]);
8758
+ if (turns.length === 0) break;
8759
+ const remainingMs = deadline - Date.now();
8760
+ if (remainingMs <= 0) {
8761
+ timedOut = true;
8762
+ break;
8763
+ }
8757
8764
  let timer;
8758
8765
  await Promise.race([
8759
8766
  Promise.allSettled(turns),
8760
8767
  new Promise((resolve) => {
8761
- timer = setTimeout(
8762
- () => {
8763
- timedOut = true;
8764
- resolve();
8765
- },
8766
- Math.max(0, graceMs)
8767
- );
8768
+ timer = setTimeout(resolve, remainingMs);
8768
8769
  timer.unref?.();
8769
8770
  })
8770
8771
  ]);
8771
8772
  if (timer) clearTimeout(timer);
8772
8773
  }
8774
+ this.log.info(
8775
+ { waitedMs: Date.now() - drainStartedAt, timedOut },
8776
+ timedOut ? "companion: deploy drain grace elapsed; fencing admission for restart" : "companion: deploy drain reached a quiet point; fencing admission for restart"
8777
+ );
8778
+ this.draining = true;
8779
+ if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
8780
+ if (this.pollTimer) clearInterval(this.pollTimer);
8781
+ if (this.inFlightHeartbeat) await this.inFlightHeartbeat;
8782
+ await this.deviceApi.beginDrain();
8783
+ for (const wr of this.workspaces.values()) wr.sub?.stop();
8773
8784
  await Promise.allSettled(
8774
8785
  [...this.workspaces.values()].flatMap(
8775
8786
  (wr) => [...wr.agents.values()].map((agent) => agent.api.drainOutbox())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.25",
3
+ "version": "0.6.26",
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",