@cabane/companion 0.6.156 → 0.6.157
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 +7 -109
- package/dist/bin.js +2 -1
- package/dist/boot-guard.js +112 -0
- package/dist/cli.js +1633 -193
- package/dist/launch.js +411 -0
- package/dist/pairing-config.js +9 -0
- package/dist/runtime.js +755 -31
- package/package.json +1 -1
package/dist/runtime.js
CHANGED
|
@@ -616,6 +616,15 @@ var companionConfigSchema = z2.object({
|
|
|
616
616
|
// absent `claudeCode` block means "never offered"; without it, the block's
|
|
617
617
|
// absence marks a config from before connectors were chosen.
|
|
618
618
|
harnessesChosen: z2.boolean().optional(),
|
|
619
|
+
// Whether this companion keeps itself current: installs the version Cabane names, between
|
|
620
|
+
// turns, and rolls back one that doesn't come up (`updater.ts`). Absent means never asked,
|
|
621
|
+
// which `start` offers; off either way until someone says yes.
|
|
622
|
+
update: z2.object({
|
|
623
|
+
auto: z2.boolean()
|
|
624
|
+
}).strict().optional(),
|
|
625
|
+
// The answer to `start`'s "start at boot?" when it was no, so it isn't asked again.
|
|
626
|
+
// `cabane service install` still installs it.
|
|
627
|
+
serviceDeclined: z2.boolean().optional(),
|
|
619
628
|
// OpenCode's decline. Its block is an address, so a decline can't live there
|
|
620
629
|
// the way `claudeCode`/`codex` `enabled: false` does; without it a declined
|
|
621
630
|
// serve would be offered again on every `start`.
|
|
@@ -792,7 +801,7 @@ import { serve } from "@hono/node-server";
|
|
|
792
801
|
import { Hono } from "hono";
|
|
793
802
|
|
|
794
803
|
// src/dashboard/routes.ts
|
|
795
|
-
import { openSync, readSync, closeSync, fstatSync, existsSync as existsSync3 } from "fs";
|
|
804
|
+
import { openSync as openSync2, readSync, closeSync, fstatSync, existsSync as existsSync3 } from "fs";
|
|
796
805
|
import { readFile } from "fs/promises";
|
|
797
806
|
import { extname, join as join6, normalize } from "path";
|
|
798
807
|
import { streamSSE } from "hono/streaming";
|
|
@@ -1264,7 +1273,13 @@ function connectLines(outcome, opts = {}) {
|
|
|
1264
1273
|
}
|
|
1265
1274
|
|
|
1266
1275
|
// src/logger.ts
|
|
1267
|
-
import {
|
|
1276
|
+
import {
|
|
1277
|
+
createWriteStream,
|
|
1278
|
+
mkdirSync as mkdirSync2,
|
|
1279
|
+
openSync,
|
|
1280
|
+
renameSync as renameSync2,
|
|
1281
|
+
statSync as statSync2
|
|
1282
|
+
} from "fs";
|
|
1268
1283
|
import { dirname as dirname3, join as join5 } from "path";
|
|
1269
1284
|
import { stripVTControlCharacters } from "util";
|
|
1270
1285
|
import pino from "pino";
|
|
@@ -1319,6 +1334,53 @@ function formatHumanLine(record) {
|
|
|
1319
1334
|
}
|
|
1320
1335
|
return line + "\n";
|
|
1321
1336
|
}
|
|
1337
|
+
var LOG_ROTATE_BYTES = 50 * 1024 * 1024;
|
|
1338
|
+
var LOG_ROTATIONS_KEPT = 3;
|
|
1339
|
+
var RotatingLog = class {
|
|
1340
|
+
constructor(path, maxBytes = LOG_ROTATE_BYTES, keep = LOG_ROTATIONS_KEPT) {
|
|
1341
|
+
this.path = path;
|
|
1342
|
+
this.maxBytes = maxBytes;
|
|
1343
|
+
this.keep = keep;
|
|
1344
|
+
this.size = sizeOf(path);
|
|
1345
|
+
this.stream = openAppend(path);
|
|
1346
|
+
}
|
|
1347
|
+
path;
|
|
1348
|
+
maxBytes;
|
|
1349
|
+
keep;
|
|
1350
|
+
stream;
|
|
1351
|
+
size;
|
|
1352
|
+
write(chunk) {
|
|
1353
|
+
const ok = this.stream.write(chunk);
|
|
1354
|
+
this.size += Buffer.byteLength(chunk);
|
|
1355
|
+
if (this.size >= this.maxBytes) this.rotate();
|
|
1356
|
+
return ok;
|
|
1357
|
+
}
|
|
1358
|
+
rotate() {
|
|
1359
|
+
this.stream.end();
|
|
1360
|
+
try {
|
|
1361
|
+
for (let i = this.keep - 1; i >= 1; i--) {
|
|
1362
|
+
try {
|
|
1363
|
+
renameSync2(`${this.path}.${i}`, `${this.path}.${i + 1}`);
|
|
1364
|
+
} catch {
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
renameSync2(this.path, `${this.path}.1`);
|
|
1368
|
+
} catch {
|
|
1369
|
+
}
|
|
1370
|
+
this.stream = openAppend(this.path);
|
|
1371
|
+
this.size = sizeOf(this.path);
|
|
1372
|
+
}
|
|
1373
|
+
};
|
|
1374
|
+
function openAppend(path) {
|
|
1375
|
+
return createWriteStream("", { fd: openSync(path, "a") });
|
|
1376
|
+
}
|
|
1377
|
+
function sizeOf(path) {
|
|
1378
|
+
try {
|
|
1379
|
+
return statSync2(path).size;
|
|
1380
|
+
} catch {
|
|
1381
|
+
return 0;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1322
1384
|
var cached = null;
|
|
1323
1385
|
var consoleLogging = true;
|
|
1324
1386
|
var overrides = {};
|
|
@@ -1331,7 +1393,7 @@ function configureLogger(log, config) {
|
|
|
1331
1393
|
function createLogger(destinations = {}, options = {}) {
|
|
1332
1394
|
const path = companionLogPath();
|
|
1333
1395
|
if (!destinations.file) mkdirSync2(dirname3(path), { recursive: true });
|
|
1334
|
-
const file = destinations.file ??
|
|
1396
|
+
const file = destinations.file ?? new RotatingLog(path);
|
|
1335
1397
|
const state = { format: options.logFormat ?? "human" };
|
|
1336
1398
|
const streams = [];
|
|
1337
1399
|
const human = (chunk) => {
|
|
@@ -1755,7 +1817,7 @@ function tailFile(path, lines2) {
|
|
|
1755
1817
|
const MAX_BYTES = 256 * 1024;
|
|
1756
1818
|
let fd;
|
|
1757
1819
|
try {
|
|
1758
|
-
fd =
|
|
1820
|
+
fd = openSync2(path, "r");
|
|
1759
1821
|
const size = fstatSync(fd).size;
|
|
1760
1822
|
const start = Math.max(0, size - MAX_BYTES);
|
|
1761
1823
|
const len = size - start;
|
|
@@ -1931,6 +1993,10 @@ async function serveConnection(socket, handlers) {
|
|
|
1931
1993
|
reply(socket, result);
|
|
1932
1994
|
return;
|
|
1933
1995
|
}
|
|
1996
|
+
if (req.cmd === "update") {
|
|
1997
|
+
reply(socket, await handlers.update(req.auto));
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
1934
2000
|
if (req.cmd === "stop") {
|
|
1935
2001
|
reply(socket, { ok: true });
|
|
1936
2002
|
setTimeout(() => handlers.stop(), 50).unref?.();
|
|
@@ -2053,7 +2119,7 @@ import {
|
|
|
2053
2119
|
rmSync as rmSync3,
|
|
2054
2120
|
writeFileSync as writeFileSync2,
|
|
2055
2121
|
mkdirSync as mkdirSync4,
|
|
2056
|
-
openSync as
|
|
2122
|
+
openSync as openSync3,
|
|
2057
2123
|
closeSync as closeSync2
|
|
2058
2124
|
} from "fs";
|
|
2059
2125
|
import { join as join9 } from "path";
|
|
@@ -2075,7 +2141,7 @@ function acquireRuntimeState(state) {
|
|
|
2075
2141
|
mkdirSync4(cabaneDir(), { recursive: true });
|
|
2076
2142
|
let fd;
|
|
2077
2143
|
try {
|
|
2078
|
-
fd =
|
|
2144
|
+
fd = openSync3(runtimePath(), "wx");
|
|
2079
2145
|
} catch {
|
|
2080
2146
|
return { acquired: false, existing: readLiveRuntimeState() };
|
|
2081
2147
|
}
|
|
@@ -2961,7 +3027,7 @@ import {
|
|
|
2961
3027
|
existsSync as existsSync6,
|
|
2962
3028
|
mkdirSync as mkdirSync5,
|
|
2963
3029
|
readFileSync as readFileSync3,
|
|
2964
|
-
renameSync as
|
|
3030
|
+
renameSync as renameSync3,
|
|
2965
3031
|
rmSync as rmSync4,
|
|
2966
3032
|
writeFileSync as writeFileSync3
|
|
2967
3033
|
} from "fs";
|
|
@@ -3002,7 +3068,7 @@ function save(map) {
|
|
|
3002
3068
|
chmodSync3(tmp, 384);
|
|
3003
3069
|
} catch {
|
|
3004
3070
|
}
|
|
3005
|
-
|
|
3071
|
+
renameSync3(tmp, path);
|
|
3006
3072
|
} catch (err) {
|
|
3007
3073
|
try {
|
|
3008
3074
|
rmSync4(tmp, { force: true });
|
|
@@ -3035,7 +3101,7 @@ function pruneCredentials(keepAgentIds) {
|
|
|
3035
3101
|
}
|
|
3036
3102
|
|
|
3037
3103
|
// src/dispatch-dedupe.ts
|
|
3038
|
-
import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, renameSync as
|
|
3104
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, renameSync as renameSync4, existsSync as existsSync7 } from "fs";
|
|
3039
3105
|
import { join as join11 } from "path";
|
|
3040
3106
|
var MAX_IDS = 256;
|
|
3041
3107
|
function dir(log) {
|
|
@@ -3153,7 +3219,7 @@ function writeTurnIds(workspaceId, turns) {
|
|
|
3153
3219
|
const path = turnPathFor(workspaceId);
|
|
3154
3220
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
3155
3221
|
writeFileSync4(tmp, entries.map(([id, t]) => `${id} ${t}`).join("\n") + "\n", "utf8");
|
|
3156
|
-
|
|
3222
|
+
renameSync4(tmp, path);
|
|
3157
3223
|
return entries.length;
|
|
3158
3224
|
}
|
|
3159
3225
|
function rememberTurnId(workspaceId, eventId, turnId) {
|
|
@@ -8078,7 +8144,7 @@ var ConnectorHealthStore = class {
|
|
|
8078
8144
|
|
|
8079
8145
|
// src/dispatcher.ts
|
|
8080
8146
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
8081
|
-
import { existsSync as existsSync11, readdirSync as readdirSync2, statSync as
|
|
8147
|
+
import { existsSync as existsSync11, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
8082
8148
|
import { join as join17 } from "path";
|
|
8083
8149
|
|
|
8084
8150
|
// src/runner/launch.ts
|
|
@@ -11199,7 +11265,7 @@ function checkoutState(cwd) {
|
|
|
11199
11265
|
if (!existsSync11(gitPath)) return { ok: true, reason: "usable" };
|
|
11200
11266
|
let stat;
|
|
11201
11267
|
try {
|
|
11202
|
-
stat =
|
|
11268
|
+
stat = statSync3(gitPath);
|
|
11203
11269
|
} catch (error) {
|
|
11204
11270
|
return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
|
|
11205
11271
|
}
|
|
@@ -11416,9 +11482,9 @@ import {
|
|
|
11416
11482
|
mkdirSync as mkdirSync10,
|
|
11417
11483
|
readdirSync as readdirSync4,
|
|
11418
11484
|
readFileSync as readFileSync9,
|
|
11419
|
-
renameSync as
|
|
11485
|
+
renameSync as renameSync6,
|
|
11420
11486
|
rmSync as rmSync9,
|
|
11421
|
-
statSync as
|
|
11487
|
+
statSync as statSync4,
|
|
11422
11488
|
writeFileSync as writeFileSync6
|
|
11423
11489
|
} from "fs";
|
|
11424
11490
|
import { join as join20 } from "path";
|
|
@@ -11612,7 +11678,7 @@ import {
|
|
|
11612
11678
|
mkdirSync as mkdirSync9,
|
|
11613
11679
|
readdirSync as readdirSync3,
|
|
11614
11680
|
readFileSync as readFileSync8,
|
|
11615
|
-
renameSync as
|
|
11681
|
+
renameSync as renameSync5,
|
|
11616
11682
|
rmSync as rmSync8
|
|
11617
11683
|
} from "fs";
|
|
11618
11684
|
import { join as join18 } from "path";
|
|
@@ -11648,11 +11714,11 @@ function admitCollectedEntry(raw, turn) {
|
|
|
11648
11714
|
}
|
|
11649
11715
|
var atomicMove = (from, to) => {
|
|
11650
11716
|
try {
|
|
11651
|
-
|
|
11717
|
+
renameSync5(from, to);
|
|
11652
11718
|
} catch {
|
|
11653
11719
|
const partial = `${to}.partial`;
|
|
11654
11720
|
copyFileSync2(from, partial);
|
|
11655
|
-
|
|
11721
|
+
renameSync5(partial, to);
|
|
11656
11722
|
rmSync8(from, { force: true });
|
|
11657
11723
|
}
|
|
11658
11724
|
};
|
|
@@ -11896,6 +11962,25 @@ function pruneOtherAttempts(deviceDir, pair, keep) {
|
|
|
11896
11962
|
}
|
|
11897
11963
|
}
|
|
11898
11964
|
}
|
|
11965
|
+
var IDLE_SESSION_ARCHIVE_MS = 30 * 24 * 60 * 6e4;
|
|
11966
|
+
function pruneIdleSessionArchives(deviceDir, now = Date.now(), maxAgeMs = IDLE_SESSION_ARCHIVE_MS) {
|
|
11967
|
+
const root = join20(deviceDir, "attempts");
|
|
11968
|
+
if (!existsSync14(root)) return [];
|
|
11969
|
+
const removed = [];
|
|
11970
|
+
for (const id of readdirSync4(root)) {
|
|
11971
|
+
if (!UUID_RE.test(id)) continue;
|
|
11972
|
+
let kept;
|
|
11973
|
+
try {
|
|
11974
|
+
kept = statSync4(join20(root, id)).mtimeMs;
|
|
11975
|
+
} catch {
|
|
11976
|
+
continue;
|
|
11977
|
+
}
|
|
11978
|
+
if (now - kept < maxAgeMs) continue;
|
|
11979
|
+
rmSync9(join20(root, id), { recursive: true, force: true });
|
|
11980
|
+
removed.push(id);
|
|
11981
|
+
}
|
|
11982
|
+
return removed;
|
|
11983
|
+
}
|
|
11899
11984
|
var CappedFile = class extends Writable {
|
|
11900
11985
|
constructor(path, cap) {
|
|
11901
11986
|
super();
|
|
@@ -12273,7 +12358,7 @@ function createDockerProvider(opts) {
|
|
|
12273
12358
|
(resolve2) => file.writableFinished ? resolve2() : file.once("finish", resolve2)
|
|
12274
12359
|
);
|
|
12275
12360
|
if (r.code === 0 && !file.over) {
|
|
12276
|
-
|
|
12361
|
+
renameSync6(partial, archive);
|
|
12277
12362
|
writeFileSync6(join20(archive, "..", "pair"), `${attempt.conversationId}/${attempt.agentId}`);
|
|
12278
12363
|
pruneOtherAttempts(opts.deviceDir, attempt, attemptId);
|
|
12279
12364
|
await writeMarker(name, attemptId);
|
|
@@ -12410,7 +12495,7 @@ function createDockerProvider(opts) {
|
|
|
12410
12495
|
);
|
|
12411
12496
|
} else {
|
|
12412
12497
|
log.info(
|
|
12413
|
-
{ container: name, attemptId, bytes:
|
|
12498
|
+
{ container: name, attemptId, bytes: statSync4(archive).size },
|
|
12414
12499
|
"runners: session files restored"
|
|
12415
12500
|
);
|
|
12416
12501
|
}
|
|
@@ -12983,7 +13068,7 @@ import {
|
|
|
12983
13068
|
mkdirSync as mkdirSync12,
|
|
12984
13069
|
readdirSync as readdirSync5,
|
|
12985
13070
|
readFileSync as readFileSync11,
|
|
12986
|
-
renameSync as
|
|
13071
|
+
renameSync as renameSync7,
|
|
12987
13072
|
rmSync as rmSync11,
|
|
12988
13073
|
writeFileSync as writeFileSync8
|
|
12989
13074
|
} from "fs";
|
|
@@ -13015,7 +13100,7 @@ var Outbox = class {
|
|
|
13015
13100
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
13016
13101
|
try {
|
|
13017
13102
|
writeFileSync8(tmp, JSON.stringify(entry) + "\n", "utf8");
|
|
13018
|
-
|
|
13103
|
+
renameSync7(tmp, target);
|
|
13019
13104
|
} catch (err) {
|
|
13020
13105
|
try {
|
|
13021
13106
|
rmSync11(tmp, { force: true });
|
|
@@ -13170,6 +13255,12 @@ var SseSubscriber = class {
|
|
|
13170
13255
|
this.aborted = true;
|
|
13171
13256
|
this.controller?.abort();
|
|
13172
13257
|
}
|
|
13258
|
+
// Connect again after a `stop()`, once its loop has exited.
|
|
13259
|
+
async resume() {
|
|
13260
|
+
await this.finished;
|
|
13261
|
+
this.aborted = false;
|
|
13262
|
+
this.start();
|
|
13263
|
+
}
|
|
13173
13264
|
setCursor(eventId) {
|
|
13174
13265
|
this.lastEventId = eventId;
|
|
13175
13266
|
}
|
|
@@ -13256,6 +13347,550 @@ function sleep3(ms) {
|
|
|
13256
13347
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
13257
13348
|
}
|
|
13258
13349
|
|
|
13350
|
+
// src/updater.ts
|
|
13351
|
+
import { spawn as spawn8 } from "child_process";
|
|
13352
|
+
import { copyFileSync as copyFileSync3, existsSync as existsSync17, mkdirSync as mkdirSync16, renameSync as renameSync11, rmSync as rmSync14 } from "fs";
|
|
13353
|
+
import { dirname as dirname12, join as join26 } from "path";
|
|
13354
|
+
|
|
13355
|
+
// src/boot-rule.ts
|
|
13356
|
+
import {
|
|
13357
|
+
mkdirSync as mkdirSync13,
|
|
13358
|
+
readFileSync as readFileSync12,
|
|
13359
|
+
realpathSync as realpathSync5,
|
|
13360
|
+
renameSync as renameSync8,
|
|
13361
|
+
rmSync as rmSync12,
|
|
13362
|
+
symlinkSync,
|
|
13363
|
+
writeFileSync as writeFileSync9
|
|
13364
|
+
} from "fs";
|
|
13365
|
+
import { dirname as dirname9, join as join23 } from "path";
|
|
13366
|
+
var UNCONFIRMED_WINDOW_MS = 10 * 6e4;
|
|
13367
|
+
var MAX_STAGE_ATTEMPTS = 3;
|
|
13368
|
+
function superseded(pending, ...running) {
|
|
13369
|
+
return !!pending && !running.includes(pending.to);
|
|
13370
|
+
}
|
|
13371
|
+
function rolledBackState(state, pending, reason, now) {
|
|
13372
|
+
const { pending: _done, ...rest } = state;
|
|
13373
|
+
return {
|
|
13374
|
+
...rest,
|
|
13375
|
+
lastResult: {
|
|
13376
|
+
kind: "rolled_back",
|
|
13377
|
+
from: pending.from,
|
|
13378
|
+
to: pending.to,
|
|
13379
|
+
at: now.toISOString(),
|
|
13380
|
+
reason: reason.replace(/\s+/g, " ").trim().slice(0, 400)
|
|
13381
|
+
},
|
|
13382
|
+
failed: { version: pending.to, attempts: MAX_STAGE_ATTEMPTS, at: now.toISOString() }
|
|
13383
|
+
};
|
|
13384
|
+
}
|
|
13385
|
+
|
|
13386
|
+
// src/install-layout.ts
|
|
13387
|
+
import {
|
|
13388
|
+
existsSync as existsSync16,
|
|
13389
|
+
lstatSync,
|
|
13390
|
+
mkdirSync as mkdirSync14,
|
|
13391
|
+
readFileSync as readFileSync13,
|
|
13392
|
+
readdirSync as readdirSync6,
|
|
13393
|
+
realpathSync as realpathSync6,
|
|
13394
|
+
renameSync as renameSync9,
|
|
13395
|
+
rmSync as rmSync13,
|
|
13396
|
+
symlinkSync as symlinkSync2
|
|
13397
|
+
} from "fs";
|
|
13398
|
+
import { dirname as dirname10, join as join24 } from "path";
|
|
13399
|
+
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
13400
|
+
function versionsDir() {
|
|
13401
|
+
return join24(cabaneDir(), "versions");
|
|
13402
|
+
}
|
|
13403
|
+
function currentLink() {
|
|
13404
|
+
return join24(cabaneDir(), "current");
|
|
13405
|
+
}
|
|
13406
|
+
function bootGuardPath() {
|
|
13407
|
+
return join24(cabaneDir(), "boot-guard.mjs");
|
|
13408
|
+
}
|
|
13409
|
+
function versionPackageRoot(version) {
|
|
13410
|
+
return join24(versionsDir(), version, "node_modules", "@cabane", "companion");
|
|
13411
|
+
}
|
|
13412
|
+
function ownPackageRoot(moduleUrl = import.meta.url) {
|
|
13413
|
+
return dirname10(dirname10(fileURLToPath6(moduleUrl)));
|
|
13414
|
+
}
|
|
13415
|
+
function packageVersion(root) {
|
|
13416
|
+
try {
|
|
13417
|
+
const pkg2 = JSON.parse(readFileSync13(join24(root, "package.json"), "utf8"));
|
|
13418
|
+
return typeof pkg2.version === "string" ? pkg2.version : null;
|
|
13419
|
+
} catch {
|
|
13420
|
+
return null;
|
|
13421
|
+
}
|
|
13422
|
+
}
|
|
13423
|
+
function currentRoot() {
|
|
13424
|
+
try {
|
|
13425
|
+
const root = realpathSync6(currentLink());
|
|
13426
|
+
return existsSync16(join24(root, "dist", "cli.js")) ? root : null;
|
|
13427
|
+
} catch {
|
|
13428
|
+
return null;
|
|
13429
|
+
}
|
|
13430
|
+
}
|
|
13431
|
+
function pointCurrentAt(root) {
|
|
13432
|
+
const link = currentLink();
|
|
13433
|
+
mkdirSync14(dirname10(link), { recursive: true });
|
|
13434
|
+
const fresh = `${link}.${process.pid}.${Date.now()}`;
|
|
13435
|
+
symlinkSync2(root, fresh);
|
|
13436
|
+
try {
|
|
13437
|
+
renameSync9(fresh, link);
|
|
13438
|
+
} catch (err) {
|
|
13439
|
+
rmSync13(fresh, { force: true });
|
|
13440
|
+
throw err;
|
|
13441
|
+
}
|
|
13442
|
+
}
|
|
13443
|
+
function pruneVersions(keep) {
|
|
13444
|
+
const dir2 = versionsDir();
|
|
13445
|
+
if (!existsSync16(dir2)) return [];
|
|
13446
|
+
const removed = [];
|
|
13447
|
+
for (const name of readdirSync6(dir2)) {
|
|
13448
|
+
if (keep.includes(name)) continue;
|
|
13449
|
+
rmSync13(join24(dir2, name), { recursive: true, force: true });
|
|
13450
|
+
removed.push(name);
|
|
13451
|
+
}
|
|
13452
|
+
return removed;
|
|
13453
|
+
}
|
|
13454
|
+
function compareVersions(a, b) {
|
|
13455
|
+
const parts = (v) => /^\d+(\.\d+)*$/.test(v) ? v.split(".").map(Number) : null;
|
|
13456
|
+
const pa = parts(a);
|
|
13457
|
+
const pb = parts(b);
|
|
13458
|
+
if (!pa || !pb) return pa ? 1 : pb ? -1 : 0;
|
|
13459
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
13460
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
13461
|
+
if (d !== 0) return d;
|
|
13462
|
+
}
|
|
13463
|
+
return 0;
|
|
13464
|
+
}
|
|
13465
|
+
|
|
13466
|
+
// src/update-state.ts
|
|
13467
|
+
import { mkdirSync as mkdirSync15, readFileSync as readFileSync14, renameSync as renameSync10, writeFileSync as writeFileSync10 } from "fs";
|
|
13468
|
+
import { dirname as dirname11, join as join25 } from "path";
|
|
13469
|
+
import { z as z18 } from "zod";
|
|
13470
|
+
var resultSchema = z18.object({
|
|
13471
|
+
kind: z18.enum(["updated", "rolled_back", "failed"]),
|
|
13472
|
+
from: z18.string(),
|
|
13473
|
+
to: z18.string(),
|
|
13474
|
+
at: z18.string(),
|
|
13475
|
+
reason: z18.string().optional()
|
|
13476
|
+
});
|
|
13477
|
+
var pendingSchema = z18.object({
|
|
13478
|
+
from: z18.string(),
|
|
13479
|
+
// The package root `current` pointed at before the swap, so a rollback can point it back.
|
|
13480
|
+
fromRoot: z18.string(),
|
|
13481
|
+
to: z18.string(),
|
|
13482
|
+
at: z18.string(),
|
|
13483
|
+
boots: z18.number().int().nonnegative()
|
|
13484
|
+
});
|
|
13485
|
+
var stateSchema = z18.object({
|
|
13486
|
+
server: z18.object({ latest: z18.string(), minimum: z18.string(), at: z18.string() }).optional(),
|
|
13487
|
+
pending: pendingSchema.optional(),
|
|
13488
|
+
lastResult: resultSchema.optional(),
|
|
13489
|
+
failed: z18.object({ version: z18.string(), attempts: z18.number().int(), at: z18.string() }).optional()
|
|
13490
|
+
});
|
|
13491
|
+
function updateStatePath(dir2 = cabaneDir()) {
|
|
13492
|
+
return join25(dir2, "update.json");
|
|
13493
|
+
}
|
|
13494
|
+
function readUpdateState(path = updateStatePath()) {
|
|
13495
|
+
try {
|
|
13496
|
+
const parsed = stateSchema.safeParse(JSON.parse(readFileSync14(path, "utf8")));
|
|
13497
|
+
return parsed.success ? parsed.data : {};
|
|
13498
|
+
} catch {
|
|
13499
|
+
return {};
|
|
13500
|
+
}
|
|
13501
|
+
}
|
|
13502
|
+
function writeUpdateState(state, path = updateStatePath()) {
|
|
13503
|
+
mkdirSync15(dirname11(path), { recursive: true });
|
|
13504
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
13505
|
+
writeFileSync10(tmp, `${JSON.stringify(state, null, 2)}
|
|
13506
|
+
`, { mode: 384 });
|
|
13507
|
+
renameSync10(tmp, path);
|
|
13508
|
+
}
|
|
13509
|
+
function updateUpdateState(change, path = updateStatePath()) {
|
|
13510
|
+
const next = change(readUpdateState(path));
|
|
13511
|
+
writeUpdateState(next, path);
|
|
13512
|
+
return next;
|
|
13513
|
+
}
|
|
13514
|
+
|
|
13515
|
+
// src/updater.ts
|
|
13516
|
+
function updateMode(env = process.env) {
|
|
13517
|
+
return env.CABANE_COMPANION_SUPERVISED === "1" ? "supervised" : "attached";
|
|
13518
|
+
}
|
|
13519
|
+
var STAGE_RETRY_MS = 60 * 6e4;
|
|
13520
|
+
var SWAP_DRAIN_MS = 1e4;
|
|
13521
|
+
var SOURCE_BUILD_VERSION = "0.0.0";
|
|
13522
|
+
function versionToStage(input) {
|
|
13523
|
+
const { own, state, now, force } = input;
|
|
13524
|
+
if (!input.auto && !force) return null;
|
|
13525
|
+
if (own === SOURCE_BUILD_VERSION) return null;
|
|
13526
|
+
const latest = state.server?.latest;
|
|
13527
|
+
if (!latest || compareVersions(latest, own) <= 0) return null;
|
|
13528
|
+
if (state.pending) return null;
|
|
13529
|
+
const failed = state.failed;
|
|
13530
|
+
if (!force && failed?.version === latest) {
|
|
13531
|
+
if (failed.attempts >= MAX_STAGE_ATTEMPTS) return null;
|
|
13532
|
+
if (now - Date.parse(failed.at) < STAGE_RETRY_MS) return null;
|
|
13533
|
+
}
|
|
13534
|
+
return latest;
|
|
13535
|
+
}
|
|
13536
|
+
function rollBack(pending, reason, now = /* @__PURE__ */ new Date()) {
|
|
13537
|
+
if (existsSync17(pending.fromRoot)) pointCurrentAt(pending.fromRoot);
|
|
13538
|
+
return updateUpdateState((state) => rolledBackState(state, pending, reason, now));
|
|
13539
|
+
}
|
|
13540
|
+
function writeBootGuard(root = ownPackageRoot()) {
|
|
13541
|
+
const built = join26(root, "dist", "boot-guard.js");
|
|
13542
|
+
if (!existsSync17(built)) return false;
|
|
13543
|
+
const target = bootGuardPath();
|
|
13544
|
+
mkdirSync16(dirname12(target), { recursive: true });
|
|
13545
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
13546
|
+
copyFileSync3(built, tmp);
|
|
13547
|
+
renameSync11(tmp, target);
|
|
13548
|
+
return true;
|
|
13549
|
+
}
|
|
13550
|
+
function oneLine(text) {
|
|
13551
|
+
return text.replace(/\s+/g, " ").trim().slice(0, 400);
|
|
13552
|
+
}
|
|
13553
|
+
var runProcess = (command, args, opts = {}) => new Promise((resolve2) => {
|
|
13554
|
+
let stdout = "";
|
|
13555
|
+
let stderr = "";
|
|
13556
|
+
let child;
|
|
13557
|
+
try {
|
|
13558
|
+
child = spawn8(command, [...args], {
|
|
13559
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
13560
|
+
env: opts.env ?? process.env
|
|
13561
|
+
});
|
|
13562
|
+
} catch (err) {
|
|
13563
|
+
resolve2({ code: null, stdout, stderr: err instanceof Error ? err.message : String(err) });
|
|
13564
|
+
return;
|
|
13565
|
+
}
|
|
13566
|
+
const timer = opts.timeoutMs ? setTimeout(() => child.kill("SIGKILL"), opts.timeoutMs) : null;
|
|
13567
|
+
child.stdout.on("data", (d) => stdout += d.toString());
|
|
13568
|
+
child.stderr.on("data", (d) => stderr += d.toString());
|
|
13569
|
+
child.on("error", (err) => {
|
|
13570
|
+
if (timer) clearTimeout(timer);
|
|
13571
|
+
resolve2({ code: null, stdout, stderr: stderr || err.message });
|
|
13572
|
+
});
|
|
13573
|
+
child.on("close", (code) => {
|
|
13574
|
+
if (timer) clearTimeout(timer);
|
|
13575
|
+
resolve2({ code, stdout, stderr });
|
|
13576
|
+
});
|
|
13577
|
+
});
|
|
13578
|
+
async function stageVersion(version, harnesses, deps = {}) {
|
|
13579
|
+
const run = deps.run ?? runProcess;
|
|
13580
|
+
const node = deps.execPath ?? process.execPath;
|
|
13581
|
+
const target = join26(versionsDir(), version);
|
|
13582
|
+
const placed = versionPackageRoot(version);
|
|
13583
|
+
if (existsSync17(placed)) {
|
|
13584
|
+
if (!await verifyInstall(placed, version, harnesses, { run, execPath: node }))
|
|
13585
|
+
return { ok: true, root: placed };
|
|
13586
|
+
}
|
|
13587
|
+
const npm = join26(dirname12(node), "npm");
|
|
13588
|
+
if (!existsSync17(npm))
|
|
13589
|
+
return { ok: false, reason: `there is no npm beside Node at ${dirname12(node)}` };
|
|
13590
|
+
mkdirSync16(versionsDir(), { recursive: true });
|
|
13591
|
+
const staging = join26(versionsDir(), `.${version}.staging-${process.pid}`);
|
|
13592
|
+
rmSync14(staging, { recursive: true, force: true });
|
|
13593
|
+
const installed = await run(
|
|
13594
|
+
npm,
|
|
13595
|
+
[
|
|
13596
|
+
"install",
|
|
13597
|
+
"--prefix",
|
|
13598
|
+
staging,
|
|
13599
|
+
"--include=optional",
|
|
13600
|
+
"--no-audit",
|
|
13601
|
+
"--no-fund",
|
|
13602
|
+
"--no-save",
|
|
13603
|
+
"--loglevel=error",
|
|
13604
|
+
`@cabane/companion@${version}`
|
|
13605
|
+
],
|
|
13606
|
+
{
|
|
13607
|
+
// npm's install scripts run on this Node, not whichever `node` is first on PATH.
|
|
13608
|
+
env: { ...process.env, PATH: `${dirname12(node)}:${process.env.PATH ?? ""}` },
|
|
13609
|
+
timeoutMs: 15 * 6e4
|
|
13610
|
+
}
|
|
13611
|
+
);
|
|
13612
|
+
if (installed.code !== 0) {
|
|
13613
|
+
rmSync14(staging, { recursive: true, force: true });
|
|
13614
|
+
if (/\bE404\b|\bETARGET\b|No matching version found|is not in this registry/i.test(
|
|
13615
|
+
installed.stderr
|
|
13616
|
+
))
|
|
13617
|
+
return { ok: false, reason: `version ${version} isn't published yet`, unpublished: true };
|
|
13618
|
+
return { ok: false, reason: `npm couldn't install it: ${lastLine(installed.stderr)}` };
|
|
13619
|
+
}
|
|
13620
|
+
const root = join26(staging, "node_modules", "@cabane", "companion");
|
|
13621
|
+
const verdict = await verifyInstall(root, version, harnesses, { run, execPath: node });
|
|
13622
|
+
if (verdict) {
|
|
13623
|
+
rmSync14(staging, { recursive: true, force: true });
|
|
13624
|
+
return { ok: false, reason: verdict };
|
|
13625
|
+
}
|
|
13626
|
+
rmSync14(target, { recursive: true, force: true });
|
|
13627
|
+
renameSync11(staging, target);
|
|
13628
|
+
return { ok: true, root: versionPackageRoot(version) };
|
|
13629
|
+
}
|
|
13630
|
+
async function verifyInstall(root, version, harnesses, deps) {
|
|
13631
|
+
if (packageVersion(root) !== version) return `the installed package isn't version ${version}`;
|
|
13632
|
+
const answered = await deps.run(deps.execPath, [join26(root, "dist", "bin.js"), "--version"], {
|
|
13633
|
+
timeoutMs: 6e4,
|
|
13634
|
+
env: { ...process.env, CABANE_LAUNCH_SELF: "1" }
|
|
13635
|
+
});
|
|
13636
|
+
if (answered.code !== 0 || answered.stdout.trim() !== version) {
|
|
13637
|
+
return `it doesn't start: ${lastLine(answered.stderr) || "no version printed"}`;
|
|
13638
|
+
}
|
|
13639
|
+
if (harnesses.length === 0) return null;
|
|
13640
|
+
const probed = await deps.run(deps.execPath, [join26(root, "dist", "postinstall.js"), "--json"], {
|
|
13641
|
+
timeoutMs: 6e4
|
|
13642
|
+
});
|
|
13643
|
+
let resolutions = {};
|
|
13644
|
+
try {
|
|
13645
|
+
resolutions = JSON.parse(probed.stdout);
|
|
13646
|
+
} catch {
|
|
13647
|
+
return "it couldn't say which bundled agents it has";
|
|
13648
|
+
}
|
|
13649
|
+
const missing = harnesses.filter((h) => resolutions[h]?.status !== "present");
|
|
13650
|
+
if (missing.length > 0) {
|
|
13651
|
+
return `its bundled ${missing.map(harnessName).join(" and ")} ${missing.length > 1 ? "are" : "is"} missing`;
|
|
13652
|
+
}
|
|
13653
|
+
return null;
|
|
13654
|
+
}
|
|
13655
|
+
function harnessName(h) {
|
|
13656
|
+
return h === "claude-code" ? "Claude Code" : "Codex";
|
|
13657
|
+
}
|
|
13658
|
+
function lastLine(text) {
|
|
13659
|
+
const lines2 = text.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
13660
|
+
return lines2.at(-1)?.slice(0, 300) ?? "";
|
|
13661
|
+
}
|
|
13662
|
+
var CompanionUpdater = class {
|
|
13663
|
+
constructor(deps) {
|
|
13664
|
+
this.deps = deps;
|
|
13665
|
+
this.now = deps.now ?? Date.now;
|
|
13666
|
+
}
|
|
13667
|
+
deps;
|
|
13668
|
+
staging = null;
|
|
13669
|
+
ready = null;
|
|
13670
|
+
swapping = false;
|
|
13671
|
+
watchdog = null;
|
|
13672
|
+
now;
|
|
13673
|
+
// At start: a recorded swap to a version that will never start again (someone installed or
|
|
13674
|
+
// chose another since) is dropped, or nothing would ever confirm it and no later update would
|
|
13675
|
+
// be installed.
|
|
13676
|
+
start() {
|
|
13677
|
+
const pending = readUpdateState().pending;
|
|
13678
|
+
const current = currentRoot();
|
|
13679
|
+
const currentVersion = current ? packageVersion(current) : null;
|
|
13680
|
+
if (superseded(pending, this.deps.own, ...currentVersion ? [currentVersion] : [])) {
|
|
13681
|
+
updateUpdateState(({ pending: _gone, ...rest }) => rest);
|
|
13682
|
+
this.deps.log.info(
|
|
13683
|
+
{ to: pending.to, own: this.deps.own },
|
|
13684
|
+
`Dropped the update to ${pending.to}: Cabane ${this.deps.own} was installed since`
|
|
13685
|
+
);
|
|
13686
|
+
}
|
|
13687
|
+
}
|
|
13688
|
+
// The deadline for an update this process is the result of: ten minutes from the first time
|
|
13689
|
+
// Cabane answers it without its stream connecting. Until Cabane answers, there's no verdict on
|
|
13690
|
+
// time: a server or network that is down says nothing about the version.
|
|
13691
|
+
armDeadline() {
|
|
13692
|
+
if (this.watchdog) return;
|
|
13693
|
+
this.watchdog = setTimeout(() => void this.giveUp(), UNCONFIRMED_WINDOW_MS);
|
|
13694
|
+
this.watchdog.unref?.();
|
|
13695
|
+
}
|
|
13696
|
+
stop() {
|
|
13697
|
+
if (this.watchdog) clearTimeout(this.watchdog);
|
|
13698
|
+
this.watchdog = null;
|
|
13699
|
+
}
|
|
13700
|
+
async giveUp() {
|
|
13701
|
+
const pending = readUpdateState().pending;
|
|
13702
|
+
if (!pending || pending.to !== this.deps.own) return;
|
|
13703
|
+
this.deps.log.warn(
|
|
13704
|
+
{ from: pending.from, to: pending.to },
|
|
13705
|
+
`Cabane ${pending.to} didn't connect within ${UNCONFIRMED_WINDOW_MS / 6e4} minutes of Cabane answering; going back to ${pending.from}`
|
|
13706
|
+
);
|
|
13707
|
+
rollBack(
|
|
13708
|
+
pending,
|
|
13709
|
+
`it didn't connect to Cabane within ${UNCONFIRMED_WINDOW_MS / 6e4} minutes`
|
|
13710
|
+
);
|
|
13711
|
+
await this.deps.restart();
|
|
13712
|
+
}
|
|
13713
|
+
// Auto-update is on and something will restart this companion into the new version.
|
|
13714
|
+
updatesItself() {
|
|
13715
|
+
return this.deps.mode === "supervised" && this.deps.autoUpdate();
|
|
13716
|
+
}
|
|
13717
|
+
// What the heartbeat reports.
|
|
13718
|
+
report() {
|
|
13719
|
+
const last = readUpdateState().lastResult;
|
|
13720
|
+
return { auto: this.updatesItself(), ...last ? { lastResult: last } : {} };
|
|
13721
|
+
}
|
|
13722
|
+
// A heartbeat landed. Record what the server named; confirm an update this process is the
|
|
13723
|
+
// result of; then, when there is something newer, install it and swap when idle.
|
|
13724
|
+
async onHeartbeat(server, streamConnected) {
|
|
13725
|
+
const state = updateUpdateState((s) => ({
|
|
13726
|
+
...s,
|
|
13727
|
+
...server ? { server: { ...server, at: new Date(this.now()).toISOString() } } : {}
|
|
13728
|
+
}));
|
|
13729
|
+
if (state.pending?.to === this.deps.own) {
|
|
13730
|
+
if (streamConnected) this.confirm(state.pending);
|
|
13731
|
+
else this.armDeadline();
|
|
13732
|
+
}
|
|
13733
|
+
if (this.ready) {
|
|
13734
|
+
await this.trySwap();
|
|
13735
|
+
return;
|
|
13736
|
+
}
|
|
13737
|
+
this.maybeStage(false);
|
|
13738
|
+
}
|
|
13739
|
+
confirm(pending) {
|
|
13740
|
+
this.stop();
|
|
13741
|
+
updateUpdateState((s) => {
|
|
13742
|
+
const { pending: _done, failed: _failed, ...rest } = s;
|
|
13743
|
+
return {
|
|
13744
|
+
...rest,
|
|
13745
|
+
lastResult: {
|
|
13746
|
+
kind: "updated",
|
|
13747
|
+
from: pending.from,
|
|
13748
|
+
to: pending.to,
|
|
13749
|
+
at: new Date(this.now()).toISOString()
|
|
13750
|
+
}
|
|
13751
|
+
};
|
|
13752
|
+
});
|
|
13753
|
+
const removed = pruneVersions([pending.to, pending.from]);
|
|
13754
|
+
this.deps.log.info(
|
|
13755
|
+
{ from: pending.from, to: pending.to, removed },
|
|
13756
|
+
`Updated Cabane from ${pending.from} to ${pending.to}`
|
|
13757
|
+
);
|
|
13758
|
+
}
|
|
13759
|
+
// `cabane update`, from the person: install the latest now, whatever the setting.
|
|
13760
|
+
async updateNow() {
|
|
13761
|
+
const state = readUpdateState();
|
|
13762
|
+
const latest = state.server?.latest;
|
|
13763
|
+
if (!latest) return { ok: false, message: "Cabane hasn't said which version is latest yet." };
|
|
13764
|
+
if (compareVersions(latest, this.deps.own) <= 0)
|
|
13765
|
+
return { ok: true, message: `Cabane ${this.deps.own} is up to date.` };
|
|
13766
|
+
if (state.pending?.to === latest) return { ok: true, message: this.waitingMessage(latest) };
|
|
13767
|
+
if (this.ready === latest) {
|
|
13768
|
+
void this.trySwap();
|
|
13769
|
+
return { ok: true, message: this.waitingMessage(latest) };
|
|
13770
|
+
}
|
|
13771
|
+
this.maybeStage(true);
|
|
13772
|
+
if (this.staging) await this.staging;
|
|
13773
|
+
if (this.ready !== latest) {
|
|
13774
|
+
const after = readUpdateState();
|
|
13775
|
+
if (after.pending?.to === latest) return { ok: true, message: this.waitingMessage(latest) };
|
|
13776
|
+
if (after.failed?.version === latest && after.failed.attempts === 0)
|
|
13777
|
+
return { ok: false, message: `Cabane ${latest} isn't published yet. Try again later.` };
|
|
13778
|
+
const failed = after.lastResult;
|
|
13779
|
+
return {
|
|
13780
|
+
ok: false,
|
|
13781
|
+
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.`
|
|
13782
|
+
};
|
|
13783
|
+
}
|
|
13784
|
+
void this.trySwap();
|
|
13785
|
+
return { ok: true, message: this.waitingMessage(latest) };
|
|
13786
|
+
}
|
|
13787
|
+
waitingMessage(version) {
|
|
13788
|
+
if (this.deps.mode === "attached")
|
|
13789
|
+
return `Cabane ${version} is installed. It runs from the next cabane start.`;
|
|
13790
|
+
return this.deps.isIdle() ? `Updating to ${version} now.` : `Updating to ${version} once the turns running now finish.`;
|
|
13791
|
+
}
|
|
13792
|
+
maybeStage(force) {
|
|
13793
|
+
if (this.staging) return;
|
|
13794
|
+
const version = versionToStage({
|
|
13795
|
+
auto: this.updatesItself(),
|
|
13796
|
+
own: this.deps.own,
|
|
13797
|
+
state: readUpdateState(),
|
|
13798
|
+
now: this.now(),
|
|
13799
|
+
force
|
|
13800
|
+
});
|
|
13801
|
+
if (!version) return;
|
|
13802
|
+
const stage = this.deps.stage ?? stageVersion;
|
|
13803
|
+
this.staging = (async () => {
|
|
13804
|
+
this.deps.log.info({ version }, `Installing Cabane ${version} beside this one`);
|
|
13805
|
+
const outcome = await stage(version, this.deps.harnesses());
|
|
13806
|
+
if (outcome.ok) {
|
|
13807
|
+
this.ready = version;
|
|
13808
|
+
updateUpdateState((s) => {
|
|
13809
|
+
const { failed: _failed, ...rest } = s;
|
|
13810
|
+
return rest;
|
|
13811
|
+
});
|
|
13812
|
+
return;
|
|
13813
|
+
}
|
|
13814
|
+
if (outcome.unpublished) {
|
|
13815
|
+
this.deps.log.info({ version }, `Cabane ${version} isn't published yet; trying later`);
|
|
13816
|
+
updateUpdateState((s) => ({
|
|
13817
|
+
...s,
|
|
13818
|
+
failed: { version, attempts: 0, at: new Date(this.now()).toISOString() }
|
|
13819
|
+
}));
|
|
13820
|
+
return;
|
|
13821
|
+
}
|
|
13822
|
+
this.deps.log.warn({ version, reason: outcome.reason }, `Couldn't install Cabane ${version}`);
|
|
13823
|
+
updateUpdateState((s) => {
|
|
13824
|
+
const attempts = s.failed?.version === version ? s.failed.attempts + 1 : 1;
|
|
13825
|
+
const at = new Date(this.now()).toISOString();
|
|
13826
|
+
return {
|
|
13827
|
+
...s,
|
|
13828
|
+
failed: { version, attempts, at },
|
|
13829
|
+
// Reported once it has failed for good, so a registry blip isn't an email.
|
|
13830
|
+
...attempts >= MAX_STAGE_ATTEMPTS || force ? {
|
|
13831
|
+
lastResult: {
|
|
13832
|
+
kind: "failed",
|
|
13833
|
+
from: this.deps.own,
|
|
13834
|
+
to: version,
|
|
13835
|
+
at,
|
|
13836
|
+
reason: oneLine(outcome.reason)
|
|
13837
|
+
}
|
|
13838
|
+
} : {}
|
|
13839
|
+
};
|
|
13840
|
+
});
|
|
13841
|
+
})().finally(() => {
|
|
13842
|
+
this.staging = null;
|
|
13843
|
+
});
|
|
13844
|
+
void this.staging.then(() => this.trySwap());
|
|
13845
|
+
}
|
|
13846
|
+
// Swap into the ready version once nothing is running. Called on each heartbeat until it can.
|
|
13847
|
+
async trySwap() {
|
|
13848
|
+
const to = this.ready;
|
|
13849
|
+
if (!to || this.swapping) return;
|
|
13850
|
+
if (readUpdateState().pending) return;
|
|
13851
|
+
if (this.deps.mode === "attached") {
|
|
13852
|
+
this.select(to);
|
|
13853
|
+
this.deps.log.info({ version: to }, `Cabane ${to} is installed; it runs from the next start`);
|
|
13854
|
+
return;
|
|
13855
|
+
}
|
|
13856
|
+
if (!this.deps.isIdle()) return;
|
|
13857
|
+
this.swapping = true;
|
|
13858
|
+
try {
|
|
13859
|
+
await this.deps.pauseStream();
|
|
13860
|
+
const sleep4 = this.deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
13861
|
+
const deadline = this.now() + SWAP_DRAIN_MS;
|
|
13862
|
+
await sleep4(250);
|
|
13863
|
+
while (!this.deps.isIdle() && this.now() < deadline) await sleep4(250);
|
|
13864
|
+
if (!this.deps.isIdle()) {
|
|
13865
|
+
this.deps.resumeStream();
|
|
13866
|
+
return;
|
|
13867
|
+
}
|
|
13868
|
+
this.select(to);
|
|
13869
|
+
this.deps.log.info({ from: this.deps.own, to }, `Restarting into Cabane ${to}`);
|
|
13870
|
+
await this.deps.restart();
|
|
13871
|
+
} finally {
|
|
13872
|
+
this.swapping = false;
|
|
13873
|
+
}
|
|
13874
|
+
}
|
|
13875
|
+
// Record the swap and point `current` at `to`, leaving the guard that counts its starts.
|
|
13876
|
+
select(to) {
|
|
13877
|
+
const fromRoot = currentRoot() ?? ownPackageRoot();
|
|
13878
|
+
writeBootGuard();
|
|
13879
|
+
updateUpdateState((s) => ({
|
|
13880
|
+
...s,
|
|
13881
|
+
pending: {
|
|
13882
|
+
from: this.deps.own,
|
|
13883
|
+
fromRoot,
|
|
13884
|
+
to,
|
|
13885
|
+
at: new Date(this.now()).toISOString(),
|
|
13886
|
+
boots: 0
|
|
13887
|
+
}
|
|
13888
|
+
}));
|
|
13889
|
+
pointCurrentAt(versionPackageRoot(to));
|
|
13890
|
+
this.ready = null;
|
|
13891
|
+
}
|
|
13892
|
+
};
|
|
13893
|
+
|
|
13259
13894
|
// src/version.ts
|
|
13260
13895
|
import { createRequire as createRequire3 } from "module";
|
|
13261
13896
|
var pkg = createRequire3(import.meta.url)("../package.json");
|
|
@@ -13263,6 +13898,7 @@ var COMPANION_VERSION = pkg.version;
|
|
|
13263
13898
|
|
|
13264
13899
|
// src/supervisor.ts
|
|
13265
13900
|
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
13901
|
+
var ARCHIVE_PRUNE_MS = 24 * 60 * 6e4;
|
|
13266
13902
|
var SHUTDOWN_HEARTBEAT_WAIT_MS = 1e3;
|
|
13267
13903
|
function positiveIntEnv(name) {
|
|
13268
13904
|
const raw = process.env[name];
|
|
@@ -13344,6 +13980,10 @@ var CompanionSupervisor = class {
|
|
|
13344
13980
|
// Which dispatches this process is running, so a redelivered frame doesn't
|
|
13345
13981
|
// run the turn a second time.
|
|
13346
13982
|
claims = new DispatchClaims();
|
|
13983
|
+
archivePruneTimer = null;
|
|
13984
|
+
// Keeps this companion current when its person said so; null until paired.
|
|
13985
|
+
updater = null;
|
|
13986
|
+
restartForUpdate;
|
|
13347
13987
|
constructor(opts) {
|
|
13348
13988
|
this.config = opts.config;
|
|
13349
13989
|
this.log = opts.log;
|
|
@@ -13353,6 +13993,10 @@ var CompanionSupervisor = class {
|
|
|
13353
13993
|
this.harnessVersions = opts.harnessVersions ?? emptyHarnessVersions();
|
|
13354
13994
|
this.exitFn = opts.exit ?? ((code) => process.exit(code));
|
|
13355
13995
|
this.reexecFn = opts.reexec ?? defaultReexec;
|
|
13996
|
+
this.restartForUpdate = opts.restartForUpdate ?? (async () => {
|
|
13997
|
+
await this.shutdown();
|
|
13998
|
+
this.exitFn(0);
|
|
13999
|
+
});
|
|
13356
14000
|
this.dispatcherFactory = opts.dispatcherFactory;
|
|
13357
14001
|
if (opts.runnerCommand) this.runnerCommand = opts.runnerCommand;
|
|
13358
14002
|
if (opts.runnerProvider) this.providerForRunners = opts.runnerProvider;
|
|
@@ -13403,6 +14047,9 @@ var CompanionSupervisor = class {
|
|
|
13403
14047
|
if (!this.runnerImage) await invalidateCompanionVolume(docker, image, source);
|
|
13404
14048
|
}
|
|
13405
14049
|
this.latchBootBundledState();
|
|
14050
|
+
this.pruneSessionArchives();
|
|
14051
|
+
this.archivePruneTimer = setInterval(() => this.pruneSessionArchives(), ARCHIVE_PRUNE_MS);
|
|
14052
|
+
this.archivePruneTimer.unref?.();
|
|
13406
14053
|
}
|
|
13407
14054
|
const sandboxRows = agentSandboxRows(this.config);
|
|
13408
14055
|
if (sandboxRows.length > 0 && this.dockerRunners()) {
|
|
@@ -13466,6 +14113,23 @@ var CompanionSupervisor = class {
|
|
|
13466
14113
|
);
|
|
13467
14114
|
}
|
|
13468
14115
|
});
|
|
14116
|
+
this.updater = new CompanionUpdater({
|
|
14117
|
+
own: COMPANION_VERSION,
|
|
14118
|
+
mode: updateMode(),
|
|
14119
|
+
autoUpdate: () => this.config.update?.auto === true,
|
|
14120
|
+
harnesses: () => this.connectedBundledHarnesses(),
|
|
14121
|
+
isIdle: () => this.idle(),
|
|
14122
|
+
pauseStream: async () => {
|
|
14123
|
+
const sub = this.deviceSub;
|
|
14124
|
+
if (!sub) return;
|
|
14125
|
+
sub.stop();
|
|
14126
|
+
await sub.finished;
|
|
14127
|
+
},
|
|
14128
|
+
resumeStream: () => void this.deviceSub?.resume(),
|
|
14129
|
+
restart: () => this.restartForUpdate(),
|
|
14130
|
+
log: this.log
|
|
14131
|
+
});
|
|
14132
|
+
this.updater.start();
|
|
13469
14133
|
await this.refreshAssignments();
|
|
13470
14134
|
this.deviceSub.start();
|
|
13471
14135
|
const firstHeartbeat = startupHarnessRefresh.then(async () => {
|
|
@@ -13526,13 +14190,20 @@ var CompanionSupervisor = class {
|
|
|
13526
14190
|
// Omitted until the first observed failure or heal, so the server's
|
|
13527
14191
|
// status-less manifest synthesis still runs for a fresh companion.
|
|
13528
14192
|
...connectorReports.length > 0 ? { connectors: connectorReports } : {},
|
|
13529
|
-
...opencodeModels !== null ? { models: opencodeModels } : {}
|
|
14193
|
+
...opencodeModels !== null ? { models: opencodeModels } : {},
|
|
14194
|
+
...this.updater ? { update: this.updater.report() } : {}
|
|
13530
14195
|
});
|
|
13531
14196
|
this.hub.setDevice({ deviceId: res.deviceId });
|
|
13532
14197
|
this.deviceId = res.deviceId;
|
|
13533
14198
|
this.checkVersionSkew(res.serverVersion);
|
|
13534
14199
|
this.heartbeated = true;
|
|
13535
14200
|
void this.recoverRuns();
|
|
14201
|
+
void this.updater?.onHeartbeat(res.companion, this.deviceConnected).catch(
|
|
14202
|
+
(err) => this.log.warn(
|
|
14203
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
14204
|
+
"Couldn't check for a Cabane update; trying again on the next check-in."
|
|
14205
|
+
)
|
|
14206
|
+
);
|
|
13536
14207
|
} catch (err) {
|
|
13537
14208
|
this.log.warn(
|
|
13538
14209
|
{ ...apiErrorLogFields(err) },
|
|
@@ -14563,9 +15234,54 @@ var CompanionSupervisor = class {
|
|
|
14563
15234
|
}
|
|
14564
15235
|
}
|
|
14565
15236
|
}
|
|
15237
|
+
// A long-lived device keeps a session archive per conversation and agent it ever ran; those
|
|
15238
|
+
// nobody has used in a month go.
|
|
15239
|
+
pruneSessionArchives() {
|
|
15240
|
+
try {
|
|
15241
|
+
const removed = pruneIdleSessionArchives(cabaneDir());
|
|
15242
|
+
if (removed.length > 0)
|
|
15243
|
+
this.log.info({ removed: removed.length }, "runners: removed idle session archives");
|
|
15244
|
+
} catch (err) {
|
|
15245
|
+
this.log.warn(
|
|
15246
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
15247
|
+
"runners: couldn't remove idle session archives"
|
|
15248
|
+
);
|
|
15249
|
+
}
|
|
15250
|
+
}
|
|
15251
|
+
// Nothing running and nothing about to: no dispatch claimed, no turn chained, no recovery
|
|
15252
|
+
// in progress. What a self-update swap waits for.
|
|
15253
|
+
idle() {
|
|
15254
|
+
if (this.claims.size > 0 || this.recovering) return false;
|
|
15255
|
+
for (const wr of this.workspaces.values()) if (wr.chains.size > 0) return false;
|
|
15256
|
+
return true;
|
|
15257
|
+
}
|
|
15258
|
+
// The bundled harnesses this device has connected, which a new version must still carry.
|
|
15259
|
+
connectedBundledHarnesses() {
|
|
15260
|
+
return [
|
|
15261
|
+
...isClaudeCodeConnected(this.config) ? ["claude-code"] : [],
|
|
15262
|
+
...isCodexEnabled(this.config) ? ["codex"] : []
|
|
15263
|
+
];
|
|
15264
|
+
}
|
|
15265
|
+
// `cabane update`, over the control socket: set whether this companion keeps itself current
|
|
15266
|
+
// (`auto`), or install the latest now.
|
|
15267
|
+
async update(auto) {
|
|
15268
|
+
if (auto !== void 0) {
|
|
15269
|
+
this.config = { ...this.config, update: { auto } };
|
|
15270
|
+
saveConfig(this.config);
|
|
15271
|
+
if (auto) this.kickHeartbeat();
|
|
15272
|
+
return {
|
|
15273
|
+
ok: true,
|
|
15274
|
+
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."
|
|
15275
|
+
};
|
|
15276
|
+
}
|
|
15277
|
+
if (!this.updater) return { ok: false, message: "This computer isn't paired yet." };
|
|
15278
|
+
return this.updater.updateNow();
|
|
15279
|
+
}
|
|
14566
15280
|
// ---- control actions ----
|
|
14567
15281
|
async shutdown() {
|
|
14568
15282
|
this.stopped = true;
|
|
15283
|
+
this.updater?.stop();
|
|
15284
|
+
if (this.archivePruneTimer) clearInterval(this.archivePruneTimer);
|
|
14569
15285
|
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
14570
15286
|
if (this.pollTimer) clearInterval(this.pollTimer);
|
|
14571
15287
|
const heartbeat = this.inFlightHeartbeat;
|
|
@@ -14600,9 +15316,9 @@ async function waitForBoundedHeartbeat(heartbeat) {
|
|
|
14600
15316
|
}
|
|
14601
15317
|
function defaultReexec() {
|
|
14602
15318
|
clearRuntimeState();
|
|
14603
|
-
void import("child_process").then(({ spawn:
|
|
15319
|
+
void import("child_process").then(({ spawn: spawn9 }) => {
|
|
14604
15320
|
try {
|
|
14605
|
-
const child =
|
|
15321
|
+
const child = spawn9(process.execPath, process.argv.slice(1), {
|
|
14606
15322
|
stdio: "inherit",
|
|
14607
15323
|
detached: false
|
|
14608
15324
|
});
|
|
@@ -14672,22 +15388,22 @@ function handleUncaught(log, err, origin) {
|
|
|
14672
15388
|
}
|
|
14673
15389
|
|
|
14674
15390
|
// src/crash-marker.ts
|
|
14675
|
-
import { existsSync as
|
|
14676
|
-
import { join as
|
|
15391
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync17, readFileSync as readFileSync15, rmSync as rmSync15, writeFileSync as writeFileSync11 } from "fs";
|
|
15392
|
+
import { join as join27 } from "path";
|
|
14677
15393
|
function crashMarkerPath() {
|
|
14678
|
-
return
|
|
15394
|
+
return join27(cabaneDir(), "last-error.json");
|
|
14679
15395
|
}
|
|
14680
15396
|
function recordCrash(rec) {
|
|
14681
15397
|
try {
|
|
14682
|
-
|
|
14683
|
-
|
|
15398
|
+
mkdirSync17(cabaneDir(), { recursive: true });
|
|
15399
|
+
writeFileSync11(crashMarkerPath(), JSON.stringify(rec, null, 2) + "\n");
|
|
14684
15400
|
} catch {
|
|
14685
15401
|
}
|
|
14686
15402
|
}
|
|
14687
15403
|
function clearCrash() {
|
|
14688
15404
|
try {
|
|
14689
15405
|
const path = crashMarkerPath();
|
|
14690
|
-
if (
|
|
15406
|
+
if (existsSync18(path)) rmSync15(path, { force: true });
|
|
14691
15407
|
} catch {
|
|
14692
15408
|
}
|
|
14693
15409
|
}
|
|
@@ -14742,11 +15458,14 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
14742
15458
|
// process behind the marker pid is this companion (not a recycled pid).
|
|
14743
15459
|
instanceId
|
|
14744
15460
|
});
|
|
15461
|
+
let restartForUpdate = async () => {
|
|
15462
|
+
};
|
|
14745
15463
|
const supervisor = new CompanionSupervisor({
|
|
14746
15464
|
config: cfg,
|
|
14747
15465
|
log,
|
|
14748
15466
|
hub,
|
|
14749
|
-
...opts.checkHarness ? { checkHarness: opts.checkHarness } : {}
|
|
15467
|
+
...opts.checkHarness ? { checkHarness: opts.checkHarness } : {},
|
|
15468
|
+
restartForUpdate: () => restartForUpdate()
|
|
14750
15469
|
});
|
|
14751
15470
|
await supervisor.start();
|
|
14752
15471
|
const connectHarness = async (runtime, serverUrl) => {
|
|
@@ -14771,6 +15490,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
14771
15490
|
control = await startControlServer({
|
|
14772
15491
|
status: () => hub.statusJson(),
|
|
14773
15492
|
connect: (runtime, serverUrl) => connectHarness(runtime, serverUrl),
|
|
15493
|
+
update: (auto) => supervisor.update(auto),
|
|
14774
15494
|
stop: () => void supervisor.requestStop()
|
|
14775
15495
|
});
|
|
14776
15496
|
if (opts.dashboard) {
|
|
@@ -14823,6 +15543,10 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
14823
15543
|
} catch {
|
|
14824
15544
|
}
|
|
14825
15545
|
};
|
|
15546
|
+
restartForUpdate = async () => {
|
|
15547
|
+
await stop();
|
|
15548
|
+
process.exit(0);
|
|
15549
|
+
};
|
|
14826
15550
|
return {
|
|
14827
15551
|
ok: true,
|
|
14828
15552
|
runtime: {
|