@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 +84 -161
- package/dist/runtime.js +4 -71
- 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: "
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
10403
|
-
|
|
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
|
|
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.
|
|
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
|
|
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: "
|
|
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.
|
|
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.
|
|
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",
|