@cabane/companion 0.6.158 → 0.6.159
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/README.md +2 -2
- package/dist/cli.js +64 -48
- package/dist/runtime.js +54 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -106,7 +106,7 @@ A freshly paired device runs nothing yet: pairing doesn't put any agent on it. I
|
|
|
106
106
|
| `pair [--server <url>]` | Pair this device with a short code entered in Cabane, without starting the Companion. Refuses, like `start`, until an agent is signed in. `start` pairs by itself, so `pair` is rarely needed. |
|
|
107
107
|
| `start [--foreground] [--daemon]` | Offer the signed-in agents, pair if needed, and run in the background; refuse, pairing nothing, when none is usable. `--daemon` is an accepted compatibility alias for the default. `--foreground` stays attached for a terminal or supervisor and never refuses. A non-interactive `start` on a paired device also stays attached. |
|
|
108
108
|
| `connect <claude-code\|codex\|opencode>` | Connect a signed-in agent; with the Companion running, it checks and connects over its control socket, no restart. Refuses a signed-out agent and changes nothing. |
|
|
109
|
-
| `update [--auto on\|off]` | Update to the version Cabane names now
|
|
109
|
+
| `update [--auto on\|off]` | Update to the version Cabane names now; with `--auto`, set whether the Companion keeps itself up to date. |
|
|
110
110
|
| `service install\|uninstall\|status` | Run the Companion as a systemd user service or a LaunchAgent that starts at boot and restarts if it stops; remove it; show its state and how updates are going. |
|
|
111
111
|
| `stop` | Stop a foreground or detached Companion cleanly. Idempotent — "nothing running" is a success. |
|
|
112
112
|
| `status` | Print the version and install, the paired device, whether it's running, each agent's sign-in, log and transcript paths, declared secret names, and local overrides. Works unpaired. |
|
|
@@ -134,7 +134,7 @@ In an interactive terminal, `cabane start` backgrounds itself, returns the promp
|
|
|
134
134
|
|
|
135
135
|
Yes, or `cabane service install` later, runs it as a service: a systemd user unit (`~/.config/systemd/user/cabane-companion.service`) on Linux, a LaunchAgent (`~/Library/LaunchAgents/ai.cabane.companion.plist`) on macOS, with auto-update on. It runs `<node> ~/.cabane/boot-guard.mjs start --foreground` on its own copy of this install under `~/.cabane/versions`, restarts it whatever it exits with, and reads `~/.cabane/service.env`, which holds your login shell's PATH and any headless sign-in (`CLAUDE_CODE_OAUTH_TOKEN=…`). `<node>` is the install script's `~/.cabane/node/bin/node` when it's there, otherwise the Node that ran `service install`; `cabane service status` says if it's gone. On Linux it tells you when lingering is off and a logout would stop it. With a service installed, `cabane start` and `cabane stop` start and stop the service; `cabane service uninstall` removes it.
|
|
136
136
|
|
|
137
|
-
Updates need the service: it's what restarts the Companion into a new version. The Companion follows the version Cabane names on each heartbeat: it installs it under `~/.cabane/versions/<version>`, checks it starts and still has the bundled agents this device uses, points `~/.cabane/current` at it
|
|
137
|
+
Updates need the service: it's what restarts the Companion into a new version. The Companion follows the version Cabane names on each heartbeat: it installs it under `~/.cabane/versions/<version>`, checks it starts and still has the bundled agents this device uses, points `~/.cabane/current` at it, and exits for the service to restart it. That happens between turns, or after ten minutes if the Companion never stops working, in which case the turns it cuts carry on after the restart. A version below the minimum Cabane accepts swaps at once. A version that stops three times before connecting, or can't connect within ten minutes of Cabane answering, is rolled back by the boot guard. `cabane update --auto off|on` stops or resumes updates under the service. Without the service, `cabane update` installs the latest version and the next `cabane start` runs it.
|
|
138
138
|
|
|
139
139
|
### Other supervisors
|
|
140
140
|
|
package/dist/cli.js
CHANGED
|
@@ -15433,6 +15433,8 @@ var SseSubscriber = class {
|
|
|
15433
15433
|
opts;
|
|
15434
15434
|
aborted = false;
|
|
15435
15435
|
controller = null;
|
|
15436
|
+
// Ends the reconnect backoff early, so a stop never waits out a sleep of up to 30 s.
|
|
15437
|
+
wake = null;
|
|
15436
15438
|
lastEventId;
|
|
15437
15439
|
// Set after `start()`; resolved when the subscriber loop exits (either
|
|
15438
15440
|
// because `stop()` was called or because an auth failure tore it down).
|
|
@@ -15443,12 +15445,7 @@ var SseSubscriber = class {
|
|
|
15443
15445
|
stop() {
|
|
15444
15446
|
this.aborted = true;
|
|
15445
15447
|
this.controller?.abort();
|
|
15446
|
-
|
|
15447
|
-
// Connect again after a `stop()`, once its loop has exited.
|
|
15448
|
-
async resume() {
|
|
15449
|
-
await this.finished;
|
|
15450
|
-
this.aborted = false;
|
|
15451
|
-
this.start();
|
|
15448
|
+
this.wake?.();
|
|
15452
15449
|
}
|
|
15453
15450
|
setCursor(eventId) {
|
|
15454
15451
|
this.lastEventId = eventId;
|
|
@@ -15461,7 +15458,7 @@ var SseSubscriber = class {
|
|
|
15461
15458
|
backoff = 500;
|
|
15462
15459
|
if (!this.aborted) {
|
|
15463
15460
|
this.opts.log.warn("Lost the connection to Cabane; reconnecting");
|
|
15464
|
-
await
|
|
15461
|
+
await this.backoff(backoff);
|
|
15465
15462
|
}
|
|
15466
15463
|
} catch (err) {
|
|
15467
15464
|
if (this.aborted) return;
|
|
@@ -15481,11 +15478,21 @@ var SseSubscriber = class {
|
|
|
15481
15478
|
},
|
|
15482
15479
|
"Lost the connection to Cabane; reconnecting"
|
|
15483
15480
|
);
|
|
15484
|
-
await
|
|
15481
|
+
await this.backoff(backoff);
|
|
15485
15482
|
backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
|
|
15486
15483
|
}
|
|
15487
15484
|
}
|
|
15488
15485
|
}
|
|
15486
|
+
backoff(ms) {
|
|
15487
|
+
return new Promise((resolve2) => {
|
|
15488
|
+
const timer = setTimeout(done, ms);
|
|
15489
|
+
this.wake = done;
|
|
15490
|
+
function done() {
|
|
15491
|
+
clearTimeout(timer);
|
|
15492
|
+
resolve2();
|
|
15493
|
+
}
|
|
15494
|
+
});
|
|
15495
|
+
}
|
|
15489
15496
|
async connect() {
|
|
15490
15497
|
const base = this.opts.baseUrl.endsWith("/") ? this.opts.baseUrl.slice(0, -1) : this.opts.baseUrl;
|
|
15491
15498
|
const url = `${base}${this.opts.path}`;
|
|
@@ -15532,9 +15539,6 @@ var SseSubscriber = class {
|
|
|
15532
15539
|
}
|
|
15533
15540
|
}
|
|
15534
15541
|
};
|
|
15535
|
-
function sleep4(ms) {
|
|
15536
|
-
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
15537
|
-
}
|
|
15538
15542
|
|
|
15539
15543
|
// src/supervisor.ts
|
|
15540
15544
|
init_transcript_writer();
|
|
@@ -15717,7 +15721,7 @@ function updateMode(env = process.env) {
|
|
|
15717
15721
|
return env.CABANE_COMPANION_SUPERVISED === "1" ? "supervised" : "attached";
|
|
15718
15722
|
}
|
|
15719
15723
|
var STAGE_RETRY_MS = 60 * 6e4;
|
|
15720
|
-
var
|
|
15724
|
+
var SWAP_WAIT_MS = 10 * 6e4;
|
|
15721
15725
|
var SOURCE_BUILD_VERSION = "0.0.0";
|
|
15722
15726
|
function versionToStage(input) {
|
|
15723
15727
|
const { own, state, now, force } = input;
|
|
@@ -15867,6 +15871,8 @@ var CompanionUpdater = class {
|
|
|
15867
15871
|
deps;
|
|
15868
15872
|
staging = null;
|
|
15869
15873
|
ready = null;
|
|
15874
|
+
// When `ready` verified; `SWAP_WAIT_MS` counts from here.
|
|
15875
|
+
readyAt = 0;
|
|
15870
15876
|
swapping = false;
|
|
15871
15877
|
watchdog = null;
|
|
15872
15878
|
now;
|
|
@@ -15965,7 +15971,7 @@ var CompanionUpdater = class {
|
|
|
15965
15971
|
return { ok: true, message: `Cabane ${this.deps.own} is up to date.` };
|
|
15966
15972
|
if (state.pending?.to === latest) return { ok: true, message: this.waitingMessage(latest) };
|
|
15967
15973
|
if (this.ready === latest) {
|
|
15968
|
-
void this.trySwap();
|
|
15974
|
+
void this.trySwap(true);
|
|
15969
15975
|
return { ok: true, message: this.waitingMessage(latest) };
|
|
15970
15976
|
}
|
|
15971
15977
|
this.maybeStage(true);
|
|
@@ -15981,13 +15987,14 @@ var CompanionUpdater = class {
|
|
|
15981
15987
|
message: failed?.kind === "failed" && failed.to === latest ? `Couldn't update to ${latest}: ${failed.reason ?? "see cabane logs"}.` : `Couldn't update to ${latest}. cabane logs says why.`
|
|
15982
15988
|
};
|
|
15983
15989
|
}
|
|
15984
|
-
void this.trySwap();
|
|
15990
|
+
void this.trySwap(true);
|
|
15985
15991
|
return { ok: true, message: this.waitingMessage(latest) };
|
|
15986
15992
|
}
|
|
15993
|
+
// Only the person's `cabane update` asks, and that swaps at once.
|
|
15987
15994
|
waitingMessage(version) {
|
|
15988
15995
|
if (this.deps.mode === "attached")
|
|
15989
15996
|
return `Cabane ${version} is installed. It runs from the next cabane start.`;
|
|
15990
|
-
return this.deps.isIdle() ? `Updating to ${version} now.` : `Updating to ${version}
|
|
15997
|
+
return this.deps.isIdle() ? `Updating to ${version} now.` : `Updating to ${version} now. The turns running now carry on after the restart.`;
|
|
15991
15998
|
}
|
|
15992
15999
|
maybeStage(force) {
|
|
15993
16000
|
if (this.staging) return;
|
|
@@ -16005,6 +16012,7 @@ var CompanionUpdater = class {
|
|
|
16005
16012
|
const outcome = await stage(version, this.deps.harnesses());
|
|
16006
16013
|
if (outcome.ok) {
|
|
16007
16014
|
this.ready = version;
|
|
16015
|
+
this.readyAt = this.now();
|
|
16008
16016
|
updateUpdateState((s) => {
|
|
16009
16017
|
const { failed: _failed, ...rest } = s;
|
|
16010
16018
|
return rest;
|
|
@@ -16043,8 +16051,10 @@ var CompanionUpdater = class {
|
|
|
16043
16051
|
});
|
|
16044
16052
|
void this.staging.then(() => this.trySwap());
|
|
16045
16053
|
}
|
|
16046
|
-
// Swap into the ready version
|
|
16047
|
-
|
|
16054
|
+
// Swap into the ready version: when nothing is running, when it has waited `SWAP_WAIT_MS` for
|
|
16055
|
+
// that, when this version is below the server's minimum, or `now` (the person asked). Called on
|
|
16056
|
+
// each heartbeat until it does.
|
|
16057
|
+
async trySwap(now = false) {
|
|
16048
16058
|
const to = this.ready;
|
|
16049
16059
|
if (!to || this.swapping) return;
|
|
16050
16060
|
if (readUpdateState().pending) return;
|
|
@@ -16053,18 +16063,9 @@ var CompanionUpdater = class {
|
|
|
16053
16063
|
this.deps.log.info({ version: to }, `Cabane ${to} is installed; it runs from the next start`);
|
|
16054
16064
|
return;
|
|
16055
16065
|
}
|
|
16056
|
-
if (!this.deps.isIdle()) return;
|
|
16066
|
+
if (!now && !this.deps.isIdle() && !this.waitedLongEnough() && !this.belowMinimum()) return;
|
|
16057
16067
|
this.swapping = true;
|
|
16058
16068
|
try {
|
|
16059
|
-
await this.deps.pauseStream();
|
|
16060
|
-
const sleep5 = this.deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
16061
|
-
const deadline = this.now() + SWAP_DRAIN_MS;
|
|
16062
|
-
await sleep5(250);
|
|
16063
|
-
while (!this.deps.isIdle() && this.now() < deadline) await sleep5(250);
|
|
16064
|
-
if (!this.deps.isIdle()) {
|
|
16065
|
-
this.deps.resumeStream();
|
|
16066
|
-
return;
|
|
16067
|
-
}
|
|
16068
16069
|
this.select(to);
|
|
16069
16070
|
this.deps.log.info({ from: this.deps.own, to }, `Restarting into Cabane ${to}`);
|
|
16070
16071
|
await this.deps.restart();
|
|
@@ -16072,6 +16073,14 @@ var CompanionUpdater = class {
|
|
|
16072
16073
|
this.swapping = false;
|
|
16073
16074
|
}
|
|
16074
16075
|
}
|
|
16076
|
+
waitedLongEnough() {
|
|
16077
|
+
return this.now() - this.readyAt >= SWAP_WAIT_MS;
|
|
16078
|
+
}
|
|
16079
|
+
// Cabane refuses this version's turns: waiting would only turn them away.
|
|
16080
|
+
belowMinimum() {
|
|
16081
|
+
const minimum = readUpdateState().server?.minimum;
|
|
16082
|
+
return !!minimum && compareVersions(this.deps.own, minimum) < 0;
|
|
16083
|
+
}
|
|
16075
16084
|
// Record the swap and point `current` at `to`, leaving the guard that counts its starts.
|
|
16076
16085
|
select(to) {
|
|
16077
16086
|
const fromRoot = currentRoot() ?? ownPackageRoot();
|
|
@@ -16096,6 +16105,7 @@ init_version2();
|
|
|
16096
16105
|
init_src();
|
|
16097
16106
|
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
16098
16107
|
var ARCHIVE_PRUNE_MS = 24 * 60 * 6e4;
|
|
16108
|
+
var FORCE_EXIT_MS = 4e3;
|
|
16099
16109
|
var SHUTDOWN_HEARTBEAT_WAIT_MS = 1e3;
|
|
16100
16110
|
function positiveIntEnv(name) {
|
|
16101
16111
|
const raw = process.env[name];
|
|
@@ -16316,14 +16326,7 @@ var CompanionSupervisor = class {
|
|
|
16316
16326
|
autoUpdate: () => this.config.update?.auto === true,
|
|
16317
16327
|
harnesses: () => this.connectedBundledHarnesses(),
|
|
16318
16328
|
isIdle: () => this.idle(),
|
|
16319
|
-
|
|
16320
|
-
const sub = this.deviceSub;
|
|
16321
|
-
if (!sub) return;
|
|
16322
|
-
sub.stop();
|
|
16323
|
-
await sub.finished;
|
|
16324
|
-
},
|
|
16325
|
-
resumeStream: () => void this.deviceSub?.resume(),
|
|
16326
|
-
restart: () => this.restartForUpdate(),
|
|
16329
|
+
restart: () => this.restartBounded(),
|
|
16327
16330
|
log: this.log
|
|
16328
16331
|
});
|
|
16329
16332
|
this.updater.start();
|
|
@@ -17445,8 +17448,22 @@ var CompanionSupervisor = class {
|
|
|
17445
17448
|
);
|
|
17446
17449
|
}
|
|
17447
17450
|
}
|
|
17451
|
+
// The update's restart, held to the same `FORCE_EXIT_MS` a signal's stop gets. Exit 0: the
|
|
17452
|
+
// swap is recorded and `current` names the new version, so the service starts it either way.
|
|
17453
|
+
async restartBounded() {
|
|
17454
|
+
const force = setTimeout(() => {
|
|
17455
|
+
this.log.warn({ ms: FORCE_EXIT_MS }, "Restarting for the update: shutdown took too long");
|
|
17456
|
+
this.exitFn(0);
|
|
17457
|
+
}, FORCE_EXIT_MS);
|
|
17458
|
+
force.unref?.();
|
|
17459
|
+
try {
|
|
17460
|
+
await this.restartForUpdate();
|
|
17461
|
+
} finally {
|
|
17462
|
+
clearTimeout(force);
|
|
17463
|
+
}
|
|
17464
|
+
}
|
|
17448
17465
|
// Nothing running and nothing about to: no dispatch claimed, no turn chained, no recovery
|
|
17449
|
-
// in progress. What a self-update swap waits for
|
|
17466
|
+
// in progress. What a self-update swap waits for, up to `SWAP_WAIT_MS`.
|
|
17450
17467
|
idle() {
|
|
17451
17468
|
if (this.claims.size > 0 || this.recovering) return false;
|
|
17452
17469
|
for (const wr of this.workspaces.values()) if (wr.chains.size > 0) return false;
|
|
@@ -17468,7 +17485,7 @@ var CompanionSupervisor = class {
|
|
|
17468
17485
|
if (auto) this.kickHeartbeat();
|
|
17469
17486
|
return {
|
|
17470
17487
|
ok: true,
|
|
17471
|
-
message: auto ? "Cabane will keep itself up to date. Updates install between turns and roll back if they fail." : "Cabane won't update itself. Run cabane update to update."
|
|
17488
|
+
message: auto ? "Cabane will keep itself up to date. Updates install between turns when they can, and roll back if they fail." : "Cabane won't update itself. Run cabane update to update."
|
|
17472
17489
|
};
|
|
17473
17490
|
}
|
|
17474
17491
|
if (!this.updater) return { ok: false, message: "This computer isn't paired yet." };
|
|
@@ -18178,7 +18195,7 @@ async function startDaemon(opts = {}, deps = {}) {
|
|
|
18178
18195
|
const readState = deps.readState ?? readLiveRuntimeState;
|
|
18179
18196
|
const verify = deps.verify ?? ((s) => verifyRuntime(s));
|
|
18180
18197
|
const spawnDetached = deps.spawnDetached ?? defaultSpawnDetached;
|
|
18181
|
-
const
|
|
18198
|
+
const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
18182
18199
|
const now = deps.now ?? (() => Date.now());
|
|
18183
18200
|
const quiet = opts.report === "failures";
|
|
18184
18201
|
const cfg = requireStartConfig({
|
|
@@ -18226,7 +18243,7 @@ async function startDaemon(opts = {}, deps = {}) {
|
|
|
18226
18243
|
const deadline = now() + STARTUP_TIMEOUT_MS;
|
|
18227
18244
|
let seen = readState();
|
|
18228
18245
|
while (!ready(seen) && now() < deadline) {
|
|
18229
|
-
await
|
|
18246
|
+
await sleep4(POLL_INTERVAL_MS);
|
|
18230
18247
|
seen = readState();
|
|
18231
18248
|
}
|
|
18232
18249
|
return ready(seen) ? seen : null;
|
|
@@ -18299,7 +18316,7 @@ async function stop(deps = {}) {
|
|
|
18299
18316
|
const readState = deps.readState ?? readLiveRuntimeState;
|
|
18300
18317
|
const verify = deps.verify ?? ((s) => verifyRuntime(s));
|
|
18301
18318
|
const kill = deps.kill ?? ((pid2, signal) => process.kill(pid2, signal));
|
|
18302
|
-
const
|
|
18319
|
+
const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
18303
18320
|
const now = deps.now ?? (() => Date.now());
|
|
18304
18321
|
const state = readState();
|
|
18305
18322
|
if (!state) {
|
|
@@ -18327,7 +18344,7 @@ async function stop(deps = {}) {
|
|
|
18327
18344
|
process.stdout.write("companion: stopped.\n");
|
|
18328
18345
|
return;
|
|
18329
18346
|
}
|
|
18330
|
-
await
|
|
18347
|
+
await sleep4(POLL_INTERVAL_MS2);
|
|
18331
18348
|
}
|
|
18332
18349
|
process.stdout.write(
|
|
18333
18350
|
`companion: didn't exit within ${TERM_GRACE_MS / 1e3}s, sending SIGKILL.
|
|
@@ -18353,7 +18370,7 @@ function isAlive(kill, pid) {
|
|
|
18353
18370
|
var STARTUP_WAIT_MS = 2e4;
|
|
18354
18371
|
async function installAndStartService(deps = {}) {
|
|
18355
18372
|
const readState = deps.readState ?? readLiveRuntimeState;
|
|
18356
|
-
const
|
|
18373
|
+
const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
18357
18374
|
const now = deps.now ?? Date.now;
|
|
18358
18375
|
const before = await serviceState(deps);
|
|
18359
18376
|
const running = readState();
|
|
@@ -18369,7 +18386,7 @@ async function installAndStartService(deps = {}) {
|
|
|
18369
18386
|
while (now() < deadline) {
|
|
18370
18387
|
const state = readState();
|
|
18371
18388
|
if (state?.socket) return true;
|
|
18372
|
-
await
|
|
18389
|
+
await sleep4(250);
|
|
18373
18390
|
}
|
|
18374
18391
|
write(
|
|
18375
18392
|
`${INDENT}The service is installed but Cabane hasn't come up yet. ${cabane("service status")} and ${cabane("logs")} say why.`
|
|
@@ -18383,7 +18400,7 @@ async function serviceInstall(deps = {}) {
|
|
|
18383
18400
|
}
|
|
18384
18401
|
write(
|
|
18385
18402
|
INDENT + tick(
|
|
18386
|
-
"Cabane runs as a service. It starts at boot, restarts if it stops, and updates itself between turns."
|
|
18403
|
+
"Cabane runs as a service. It starts at boot, restarts if it stops, and updates itself, usually between turns."
|
|
18387
18404
|
)
|
|
18388
18405
|
);
|
|
18389
18406
|
write(`${INDENT} Status: ${cabane("service status")} Logs: ${cabane("logs")}`);
|
|
@@ -18446,7 +18463,7 @@ function updateRows(state, auto, opts = {}) {
|
|
|
18446
18463
|
rows.push(
|
|
18447
18464
|
row(
|
|
18448
18465
|
"Auto-update",
|
|
18449
|
-
!opts.service ? `off \u2014 runs with the service: ${cabane("service install")}` : auto === true ? "on \u2014 updates install between turns and roll back if they fail" : `off \u2014 ${cabane("update --auto on")} turns it on`
|
|
18466
|
+
!opts.service ? `off \u2014 runs with the service: ${cabane("service install")}` : auto === true ? "on \u2014 updates install between turns when they can, and roll back if they fail" : `off \u2014 ${cabane("update --auto on")} turns it on`
|
|
18450
18467
|
)
|
|
18451
18468
|
);
|
|
18452
18469
|
const last = state.lastResult;
|
|
@@ -18477,7 +18494,6 @@ function setRunsAsService(on) {
|
|
|
18477
18494
|
}
|
|
18478
18495
|
|
|
18479
18496
|
// src/commands/start.ts
|
|
18480
|
-
var FORCE_EXIT_MS = 4e3;
|
|
18481
18497
|
var MAX_CODES = 3;
|
|
18482
18498
|
async function start(opts = {}) {
|
|
18483
18499
|
setLogOverrides(opts);
|
|
@@ -18943,7 +18959,7 @@ async function askHowToRun(deps = {}) {
|
|
|
18943
18959
|
return { asService: false };
|
|
18944
18960
|
blank();
|
|
18945
18961
|
const asService = await ask(
|
|
18946
|
-
"Keep Cabane running and up to date on this computer? It starts at boot, restarts if it stops, and updates itself between turns."
|
|
18962
|
+
"Keep Cabane running and up to date on this computer? It starts at boot, restarts if it stops, and updates itself, usually between turns."
|
|
18947
18963
|
);
|
|
18948
18964
|
if (!asService) saveConfig({ ...cfg, serviceDeclined: true });
|
|
18949
18965
|
blank();
|
|
@@ -19367,7 +19383,7 @@ async function update(opts = {}) {
|
|
|
19367
19383
|
saveConfig({ ...cfg2, update: { auto } });
|
|
19368
19384
|
write(
|
|
19369
19385
|
INDENT + tick(
|
|
19370
|
-
auto ? "Cabane will keep itself up to date. Updates install between turns and roll back if they fail." : "Cabane won't update itself."
|
|
19386
|
+
auto ? "Cabane will keep itself up to date. Updates install between turns when they can, and roll back if they fail." : "Cabane won't update itself."
|
|
19371
19387
|
)
|
|
19372
19388
|
);
|
|
19373
19389
|
return;
|
package/dist/runtime.js
CHANGED
|
@@ -13281,6 +13281,8 @@ var SseSubscriber = class {
|
|
|
13281
13281
|
opts;
|
|
13282
13282
|
aborted = false;
|
|
13283
13283
|
controller = null;
|
|
13284
|
+
// Ends the reconnect backoff early, so a stop never waits out a sleep of up to 30 s.
|
|
13285
|
+
wake = null;
|
|
13284
13286
|
lastEventId;
|
|
13285
13287
|
// Set after `start()`; resolved when the subscriber loop exits (either
|
|
13286
13288
|
// because `stop()` was called or because an auth failure tore it down).
|
|
@@ -13291,12 +13293,7 @@ var SseSubscriber = class {
|
|
|
13291
13293
|
stop() {
|
|
13292
13294
|
this.aborted = true;
|
|
13293
13295
|
this.controller?.abort();
|
|
13294
|
-
|
|
13295
|
-
// Connect again after a `stop()`, once its loop has exited.
|
|
13296
|
-
async resume() {
|
|
13297
|
-
await this.finished;
|
|
13298
|
-
this.aborted = false;
|
|
13299
|
-
this.start();
|
|
13296
|
+
this.wake?.();
|
|
13300
13297
|
}
|
|
13301
13298
|
setCursor(eventId) {
|
|
13302
13299
|
this.lastEventId = eventId;
|
|
@@ -13309,7 +13306,7 @@ var SseSubscriber = class {
|
|
|
13309
13306
|
backoff = 500;
|
|
13310
13307
|
if (!this.aborted) {
|
|
13311
13308
|
this.opts.log.warn("Lost the connection to Cabane; reconnecting");
|
|
13312
|
-
await
|
|
13309
|
+
await this.backoff(backoff);
|
|
13313
13310
|
}
|
|
13314
13311
|
} catch (err) {
|
|
13315
13312
|
if (this.aborted) return;
|
|
@@ -13329,11 +13326,21 @@ var SseSubscriber = class {
|
|
|
13329
13326
|
},
|
|
13330
13327
|
"Lost the connection to Cabane; reconnecting"
|
|
13331
13328
|
);
|
|
13332
|
-
await
|
|
13329
|
+
await this.backoff(backoff);
|
|
13333
13330
|
backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
|
|
13334
13331
|
}
|
|
13335
13332
|
}
|
|
13336
13333
|
}
|
|
13334
|
+
backoff(ms) {
|
|
13335
|
+
return new Promise((resolve2) => {
|
|
13336
|
+
const timer = setTimeout(done, ms);
|
|
13337
|
+
this.wake = done;
|
|
13338
|
+
function done() {
|
|
13339
|
+
clearTimeout(timer);
|
|
13340
|
+
resolve2();
|
|
13341
|
+
}
|
|
13342
|
+
});
|
|
13343
|
+
}
|
|
13337
13344
|
async connect() {
|
|
13338
13345
|
const base = this.opts.baseUrl.endsWith("/") ? this.opts.baseUrl.slice(0, -1) : this.opts.baseUrl;
|
|
13339
13346
|
const url = `${base}${this.opts.path}`;
|
|
@@ -13380,9 +13387,6 @@ var SseSubscriber = class {
|
|
|
13380
13387
|
}
|
|
13381
13388
|
}
|
|
13382
13389
|
};
|
|
13383
|
-
function sleep3(ms) {
|
|
13384
|
-
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
13385
|
-
}
|
|
13386
13390
|
|
|
13387
13391
|
// src/updater.ts
|
|
13388
13392
|
import { spawn as spawn8 } from "child_process";
|
|
@@ -13554,7 +13558,7 @@ function updateMode(env = process.env) {
|
|
|
13554
13558
|
return env.CABANE_COMPANION_SUPERVISED === "1" ? "supervised" : "attached";
|
|
13555
13559
|
}
|
|
13556
13560
|
var STAGE_RETRY_MS = 60 * 6e4;
|
|
13557
|
-
var
|
|
13561
|
+
var SWAP_WAIT_MS = 10 * 6e4;
|
|
13558
13562
|
var SOURCE_BUILD_VERSION = "0.0.0";
|
|
13559
13563
|
function versionToStage(input) {
|
|
13560
13564
|
const { own, state, now, force } = input;
|
|
@@ -13704,6 +13708,8 @@ var CompanionUpdater = class {
|
|
|
13704
13708
|
deps;
|
|
13705
13709
|
staging = null;
|
|
13706
13710
|
ready = null;
|
|
13711
|
+
// When `ready` verified; `SWAP_WAIT_MS` counts from here.
|
|
13712
|
+
readyAt = 0;
|
|
13707
13713
|
swapping = false;
|
|
13708
13714
|
watchdog = null;
|
|
13709
13715
|
now;
|
|
@@ -13802,7 +13808,7 @@ var CompanionUpdater = class {
|
|
|
13802
13808
|
return { ok: true, message: `Cabane ${this.deps.own} is up to date.` };
|
|
13803
13809
|
if (state.pending?.to === latest) return { ok: true, message: this.waitingMessage(latest) };
|
|
13804
13810
|
if (this.ready === latest) {
|
|
13805
|
-
void this.trySwap();
|
|
13811
|
+
void this.trySwap(true);
|
|
13806
13812
|
return { ok: true, message: this.waitingMessage(latest) };
|
|
13807
13813
|
}
|
|
13808
13814
|
this.maybeStage(true);
|
|
@@ -13818,13 +13824,14 @@ var CompanionUpdater = class {
|
|
|
13818
13824
|
message: failed?.kind === "failed" && failed.to === latest ? `Couldn't update to ${latest}: ${failed.reason ?? "see cabane logs"}.` : `Couldn't update to ${latest}. cabane logs says why.`
|
|
13819
13825
|
};
|
|
13820
13826
|
}
|
|
13821
|
-
void this.trySwap();
|
|
13827
|
+
void this.trySwap(true);
|
|
13822
13828
|
return { ok: true, message: this.waitingMessage(latest) };
|
|
13823
13829
|
}
|
|
13830
|
+
// Only the person's `cabane update` asks, and that swaps at once.
|
|
13824
13831
|
waitingMessage(version) {
|
|
13825
13832
|
if (this.deps.mode === "attached")
|
|
13826
13833
|
return `Cabane ${version} is installed. It runs from the next cabane start.`;
|
|
13827
|
-
return this.deps.isIdle() ? `Updating to ${version} now.` : `Updating to ${version}
|
|
13834
|
+
return this.deps.isIdle() ? `Updating to ${version} now.` : `Updating to ${version} now. The turns running now carry on after the restart.`;
|
|
13828
13835
|
}
|
|
13829
13836
|
maybeStage(force) {
|
|
13830
13837
|
if (this.staging) return;
|
|
@@ -13842,6 +13849,7 @@ var CompanionUpdater = class {
|
|
|
13842
13849
|
const outcome = await stage(version, this.deps.harnesses());
|
|
13843
13850
|
if (outcome.ok) {
|
|
13844
13851
|
this.ready = version;
|
|
13852
|
+
this.readyAt = this.now();
|
|
13845
13853
|
updateUpdateState((s) => {
|
|
13846
13854
|
const { failed: _failed, ...rest } = s;
|
|
13847
13855
|
return rest;
|
|
@@ -13880,8 +13888,10 @@ var CompanionUpdater = class {
|
|
|
13880
13888
|
});
|
|
13881
13889
|
void this.staging.then(() => this.trySwap());
|
|
13882
13890
|
}
|
|
13883
|
-
// Swap into the ready version
|
|
13884
|
-
|
|
13891
|
+
// Swap into the ready version: when nothing is running, when it has waited `SWAP_WAIT_MS` for
|
|
13892
|
+
// that, when this version is below the server's minimum, or `now` (the person asked). Called on
|
|
13893
|
+
// each heartbeat until it does.
|
|
13894
|
+
async trySwap(now = false) {
|
|
13885
13895
|
const to = this.ready;
|
|
13886
13896
|
if (!to || this.swapping) return;
|
|
13887
13897
|
if (readUpdateState().pending) return;
|
|
@@ -13890,18 +13900,9 @@ var CompanionUpdater = class {
|
|
|
13890
13900
|
this.deps.log.info({ version: to }, `Cabane ${to} is installed; it runs from the next start`);
|
|
13891
13901
|
return;
|
|
13892
13902
|
}
|
|
13893
|
-
if (!this.deps.isIdle()) return;
|
|
13903
|
+
if (!now && !this.deps.isIdle() && !this.waitedLongEnough() && !this.belowMinimum()) return;
|
|
13894
13904
|
this.swapping = true;
|
|
13895
13905
|
try {
|
|
13896
|
-
await this.deps.pauseStream();
|
|
13897
|
-
const sleep4 = this.deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
13898
|
-
const deadline = this.now() + SWAP_DRAIN_MS;
|
|
13899
|
-
await sleep4(250);
|
|
13900
|
-
while (!this.deps.isIdle() && this.now() < deadline) await sleep4(250);
|
|
13901
|
-
if (!this.deps.isIdle()) {
|
|
13902
|
-
this.deps.resumeStream();
|
|
13903
|
-
return;
|
|
13904
|
-
}
|
|
13905
13906
|
this.select(to);
|
|
13906
13907
|
this.deps.log.info({ from: this.deps.own, to }, `Restarting into Cabane ${to}`);
|
|
13907
13908
|
await this.deps.restart();
|
|
@@ -13909,6 +13910,14 @@ var CompanionUpdater = class {
|
|
|
13909
13910
|
this.swapping = false;
|
|
13910
13911
|
}
|
|
13911
13912
|
}
|
|
13913
|
+
waitedLongEnough() {
|
|
13914
|
+
return this.now() - this.readyAt >= SWAP_WAIT_MS;
|
|
13915
|
+
}
|
|
13916
|
+
// Cabane refuses this version's turns: waiting would only turn them away.
|
|
13917
|
+
belowMinimum() {
|
|
13918
|
+
const minimum = readUpdateState().server?.minimum;
|
|
13919
|
+
return !!minimum && compareVersions(this.deps.own, minimum) < 0;
|
|
13920
|
+
}
|
|
13912
13921
|
// Record the swap and point `current` at `to`, leaving the guard that counts its starts.
|
|
13913
13922
|
select(to) {
|
|
13914
13923
|
const fromRoot = currentRoot() ?? ownPackageRoot();
|
|
@@ -13936,6 +13945,7 @@ var COMPANION_VERSION = pkg.version;
|
|
|
13936
13945
|
// src/supervisor.ts
|
|
13937
13946
|
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
13938
13947
|
var ARCHIVE_PRUNE_MS = 24 * 60 * 6e4;
|
|
13948
|
+
var FORCE_EXIT_MS = 4e3;
|
|
13939
13949
|
var SHUTDOWN_HEARTBEAT_WAIT_MS = 1e3;
|
|
13940
13950
|
function positiveIntEnv(name) {
|
|
13941
13951
|
const raw = process.env[name];
|
|
@@ -14156,14 +14166,7 @@ var CompanionSupervisor = class {
|
|
|
14156
14166
|
autoUpdate: () => this.config.update?.auto === true,
|
|
14157
14167
|
harnesses: () => this.connectedBundledHarnesses(),
|
|
14158
14168
|
isIdle: () => this.idle(),
|
|
14159
|
-
|
|
14160
|
-
const sub = this.deviceSub;
|
|
14161
|
-
if (!sub) return;
|
|
14162
|
-
sub.stop();
|
|
14163
|
-
await sub.finished;
|
|
14164
|
-
},
|
|
14165
|
-
resumeStream: () => void this.deviceSub?.resume(),
|
|
14166
|
-
restart: () => this.restartForUpdate(),
|
|
14169
|
+
restart: () => this.restartBounded(),
|
|
14167
14170
|
log: this.log
|
|
14168
14171
|
});
|
|
14169
14172
|
this.updater.start();
|
|
@@ -15285,8 +15288,22 @@ var CompanionSupervisor = class {
|
|
|
15285
15288
|
);
|
|
15286
15289
|
}
|
|
15287
15290
|
}
|
|
15291
|
+
// The update's restart, held to the same `FORCE_EXIT_MS` a signal's stop gets. Exit 0: the
|
|
15292
|
+
// swap is recorded and `current` names the new version, so the service starts it either way.
|
|
15293
|
+
async restartBounded() {
|
|
15294
|
+
const force = setTimeout(() => {
|
|
15295
|
+
this.log.warn({ ms: FORCE_EXIT_MS }, "Restarting for the update: shutdown took too long");
|
|
15296
|
+
this.exitFn(0);
|
|
15297
|
+
}, FORCE_EXIT_MS);
|
|
15298
|
+
force.unref?.();
|
|
15299
|
+
try {
|
|
15300
|
+
await this.restartForUpdate();
|
|
15301
|
+
} finally {
|
|
15302
|
+
clearTimeout(force);
|
|
15303
|
+
}
|
|
15304
|
+
}
|
|
15288
15305
|
// Nothing running and nothing about to: no dispatch claimed, no turn chained, no recovery
|
|
15289
|
-
// in progress. What a self-update swap waits for
|
|
15306
|
+
// in progress. What a self-update swap waits for, up to `SWAP_WAIT_MS`.
|
|
15290
15307
|
idle() {
|
|
15291
15308
|
if (this.claims.size > 0 || this.recovering) return false;
|
|
15292
15309
|
for (const wr of this.workspaces.values()) if (wr.chains.size > 0) return false;
|
|
@@ -15308,7 +15325,7 @@ var CompanionSupervisor = class {
|
|
|
15308
15325
|
if (auto) this.kickHeartbeat();
|
|
15309
15326
|
return {
|
|
15310
15327
|
ok: true,
|
|
15311
|
-
message: auto ? "Cabane will keep itself up to date. Updates install between turns and roll back if they fail." : "Cabane won't update itself. Run cabane update to update."
|
|
15328
|
+
message: auto ? "Cabane will keep itself up to date. Updates install between turns when they can, and roll back if they fail." : "Cabane won't update itself. Run cabane update to update."
|
|
15312
15329
|
};
|
|
15313
15330
|
}
|
|
15314
15331
|
if (!this.updater) return { ok: false, message: "This computer isn't paired yet." };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.159",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The Cabane Companion (headless): connect an 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",
|