@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/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 { createWriteStream, mkdirSync as mkdirSync2 } from "fs";
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 ?? createWriteStream(path, { flags: "a" });
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 = openSync(path, "r");
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 openSync2,
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 = openSync2(runtimePath(), "wx");
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 renameSync2,
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
- renameSync2(tmp, path);
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 renameSync3, existsSync as existsSync7 } from "fs";
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
- renameSync3(tmp, path);
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 statSync2 } from "fs";
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 = statSync2(gitPath);
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 renameSync5,
11485
+ renameSync as renameSync6,
11420
11486
  rmSync as rmSync9,
11421
- statSync as statSync3,
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 renameSync4,
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
- renameSync4(from, to);
11717
+ renameSync5(from, to);
11652
11718
  } catch {
11653
11719
  const partial = `${to}.partial`;
11654
11720
  copyFileSync2(from, partial);
11655
- renameSync4(partial, to);
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
- renameSync5(partial, archive);
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: statSync3(archive).size },
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 renameSync6,
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
- renameSync6(tmp, target);
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: spawn8 }) => {
15319
+ void import("child_process").then(({ spawn: spawn9 }) => {
14604
15320
  try {
14605
- const child = spawn8(process.execPath, process.argv.slice(1), {
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 existsSync16, mkdirSync as mkdirSync13, readFileSync as readFileSync12, rmSync as rmSync12, writeFileSync as writeFileSync9 } from "fs";
14676
- import { join as join23 } from "path";
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 join23(cabaneDir(), "last-error.json");
15394
+ return join27(cabaneDir(), "last-error.json");
14679
15395
  }
14680
15396
  function recordCrash(rec) {
14681
15397
  try {
14682
- mkdirSync13(cabaneDir(), { recursive: true });
14683
- writeFileSync9(crashMarkerPath(), JSON.stringify(rec, null, 2) + "\n");
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 (existsSync16(path)) rmSync12(path, { force: true });
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: {