@cabane/companion 0.6.156 → 0.6.158

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) {
@@ -4222,6 +4288,196 @@ function dedupe(xs) {
4222
4288
  return [...new Set(xs)];
4223
4289
  }
4224
4290
 
4291
+ // packages/agent-runtime/src/claude-code/failure-evidence.ts
4292
+ function createFailureEvidence() {
4293
+ const signals = { sawRejectedLimit: false };
4294
+ return {
4295
+ observe(msg) {
4296
+ if (msg.type === "assistant" || msg.type === "auth_status") {
4297
+ const error = msg.error;
4298
+ if (typeof error === "string" && error.length > 0) {
4299
+ if (msg.type === "assistant") signals.lastAssistantError = error;
4300
+ else signals.authError = error;
4301
+ }
4302
+ } else if (msg.type === "rate_limit_event") {
4303
+ const info = msg.rate_limit_info;
4304
+ if (info?.status === "rejected") {
4305
+ signals.sawRejectedLimit = true;
4306
+ signals.rateLimitResetIso = resetsAtToIso(info.resetsAt) ?? signals.rateLimitResetIso;
4307
+ if (typeof info.rateLimitType === "string") signals.rateLimitType = info.rateLimitType;
4308
+ }
4309
+ }
4310
+ },
4311
+ preInitReason(msg) {
4312
+ const failure = msg ? classifyPreInitFrame(msg, signals) : classifyFailure(signals, { text: "" });
4313
+ return failure ? encodeFailureReason(failure) : "session_start_failed";
4314
+ },
4315
+ failedResultReason(msg) {
4316
+ const failure = classifyResultFailure(msg, signals);
4317
+ const resultText = msg.result ?? "";
4318
+ const isError = msg.is_error === true;
4319
+ return failure ? encodeFailureReason(failure) : isError ? `error:${resultText.slice(0, 200) || "unknown"}` : `result_error:${msg.subtype}`;
4320
+ },
4321
+ thrownFailure(message2, initReceived) {
4322
+ return initReceived ? classifyErrorText(message2) : classifyFailure(signals, { text: message2 });
4323
+ }
4324
+ };
4325
+ }
4326
+ function classifyFailure(s, frame) {
4327
+ const rejectedIsCap = s.sawRejectedLimit && (s.rateLimitResetIso !== void 0 || isSubscriptionWindow(s.rateLimitType));
4328
+ if (rejectedIsCap || frame.terminalReason === "blocking_limit")
4329
+ return {
4330
+ kind: "usage_capped",
4331
+ ...s.rateLimitResetIso ? { resetsAt: s.rateLimitResetIso } : {}
4332
+ };
4333
+ return classifyAssistantError(s.lastAssistantError) ?? classifyErrorText([s.authError, frame.text].filter(Boolean).join(" "));
4334
+ }
4335
+ function classifyResultFailure(msg, s) {
4336
+ const resultText = msg.result ?? "";
4337
+ const terminalReason = msg.terminal_reason;
4338
+ const errors = msg.errors;
4339
+ const text = [resultText, ...Array.isArray(errors) ? errors.map(String) : []].join(" ");
4340
+ return classifyFailure(s, { terminalReason, text });
4341
+ }
4342
+ function classifyPreInitFrame(msg, s) {
4343
+ if (msg.type === "result") {
4344
+ const isError = msg.is_error === true;
4345
+ if (msg.subtype === "success" && !isError) return classifyFailure(s, { text: "" });
4346
+ return classifyResultFailure(msg, s);
4347
+ }
4348
+ if (msg.type !== "assistant") return classifyFailure(s, { text: "" });
4349
+ const text = contentBlocks(msg).flatMap((b) => b.type === "text" && typeof b.text === "string" ? [b.text] : []).join(" ");
4350
+ return classifyFailure(s, { text });
4351
+ }
4352
+ function preInitToolActivity(msg) {
4353
+ const kind = msg.type === "assistant" ? "tool_use" : msg.type === "user" ? "tool_result" : null;
4354
+ return kind !== null && contentBlocks(msg).some((b) => b.type === kind);
4355
+ }
4356
+ function contentBlocks(msg) {
4357
+ const content = msg.message?.content;
4358
+ return Array.isArray(content) ? content : [];
4359
+ }
4360
+ function isSubscriptionWindow(value) {
4361
+ return value === "five_hour" || value === "seven_day" || value === "seven_day_opus" || value === "seven_day_sonnet" || value === "overage";
4362
+ }
4363
+
4364
+ // packages/agent-runtime/src/claude-code/context-usage.ts
4365
+ function createContextUsage() {
4366
+ let resolvedModel;
4367
+ let lastRequestContextTokens;
4368
+ const compactions = [];
4369
+ return {
4370
+ get resolvedModel() {
4371
+ return resolvedModel;
4372
+ },
4373
+ recordInit(msg) {
4374
+ const model = msg.model;
4375
+ if (typeof model === "string" && model.length > 0) resolvedModel = model;
4376
+ },
4377
+ recordCompaction(msg) {
4378
+ const compaction = readCompaction(msg);
4379
+ if (compaction) compactions.push(compaction);
4380
+ },
4381
+ recordRequest(msg) {
4382
+ const tokens = readRequestContextTokens(msg);
4383
+ if (tokens !== void 0) lastRequestContextTokens = tokens;
4384
+ },
4385
+ async resultUsage(msg, readContextUsage) {
4386
+ const usage = readSdkUsage(msg.usage);
4387
+ if (usage) {
4388
+ if (lastRequestContextTokens !== void 0) usage.contextTokens = lastRequestContextTokens;
4389
+ const window = readContextWindow(msg, resolvedModel);
4390
+ if (window !== void 0) usage.contextWindow = window;
4391
+ if (compactions.length > 0) usage.contextCompactions = compactions;
4392
+ const reading = await readHarnessContext(readContextUsage);
4393
+ if (reading) Object.assign(usage, reading);
4394
+ }
4395
+ return usage;
4396
+ }
4397
+ };
4398
+ }
4399
+ function readSdkUsage(usage) {
4400
+ if (!usage || typeof usage !== "object") return void 0;
4401
+ const fields = usage;
4402
+ const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
4403
+ const cacheReadTokens = num(fields.cache_read_input_tokens);
4404
+ const cacheCreationTokens = num(fields.cache_creation_input_tokens);
4405
+ const inputTokens = num(fields.input_tokens) + cacheCreationTokens + cacheReadTokens;
4406
+ const outputTokens = num(fields.output_tokens);
4407
+ return { inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens };
4408
+ }
4409
+ function readRequestContextTokens(msg) {
4410
+ return readSdkUsage(msg.message?.usage)?.inputTokens;
4411
+ }
4412
+ var CONTEXT_READING_TIMEOUT_MS = 2e3;
4413
+ async function readHarnessContext(read) {
4414
+ if (!read) return void 0;
4415
+ let timer;
4416
+ const timeout = new Promise((resolve2) => {
4417
+ timer = setTimeout(() => resolve2(void 0), CONTEXT_READING_TIMEOUT_MS);
4418
+ });
4419
+ try {
4420
+ return parseContextUsage(await Promise.race([read(), timeout]));
4421
+ } catch {
4422
+ return void 0;
4423
+ } finally {
4424
+ clearTimeout(timer);
4425
+ }
4426
+ }
4427
+ var CATEGORY_KINDS = /* @__PURE__ */ new Set(["used", "free", "buffer", "deferred"]);
4428
+ function parseContextUsage(response) {
4429
+ const r = response;
4430
+ if (!r || typeof r !== "object") return void 0;
4431
+ const total = readContextCount(r.totalTokens);
4432
+ const window = readContextCount(r.maxTokens);
4433
+ if (total === void 0 || window === void 0 || window === 0) return void 0;
4434
+ const categories = [];
4435
+ for (const c of Array.isArray(r.categories) ? r.categories : []) {
4436
+ const row = c;
4437
+ const tokens = readContextCount(row?.tokens);
4438
+ if (typeof row?.name === "string" && tokens !== void 0 && typeof row.kind === "string" && CATEGORY_KINDS.has(row.kind)) {
4439
+ categories.push({ name: row.name, tokens, kind: row.kind });
4440
+ }
4441
+ }
4442
+ const buffer = categories.filter((c) => c.kind === "buffer").reduce((sum, c) => sum + c.tokens, 0);
4443
+ return {
4444
+ contextTokens: total,
4445
+ contextWindow: window,
4446
+ ...buffer > 0 && buffer < window ? { contextCompactAt: window - buffer } : {},
4447
+ ...categories.length > 0 ? { contextCategories: categories } : {}
4448
+ };
4449
+ }
4450
+ function readCompaction(msg) {
4451
+ const meta = msg.compact_metadata;
4452
+ const preTokens = readContextCount(meta?.pre_tokens);
4453
+ if (!meta || preTokens === void 0) return void 0;
4454
+ if (meta.trigger !== "manual" && meta.trigger !== "auto") return void 0;
4455
+ const postTokens = readContextCount(meta.post_tokens);
4456
+ return {
4457
+ trigger: meta.trigger,
4458
+ preTokens,
4459
+ ...postTokens !== void 0 ? { postTokens } : {}
4460
+ };
4461
+ }
4462
+ function readContextWindow(msg, resolvedModel) {
4463
+ const modelUsage = msg.modelUsage;
4464
+ if (!modelUsage || typeof modelUsage !== "object") return void 0;
4465
+ const pos = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : void 0;
4466
+ if (resolvedModel) {
4467
+ const direct = pos(modelUsage[resolvedModel]?.contextWindow);
4468
+ if (direct !== void 0) return direct;
4469
+ }
4470
+ let max;
4471
+ for (const entry of Object.values(modelUsage)) {
4472
+ const w = pos(entry?.contextWindow);
4473
+ if (w !== void 0 && (max === void 0 || w > max)) max = w;
4474
+ }
4475
+ return max;
4476
+ }
4477
+ function readContextCount(v) {
4478
+ return typeof v === "number" && Number.isFinite(v) && v >= 0 ? Math.round(v) : void 0;
4479
+ }
4480
+
4225
4481
  // packages/agent-runtime/src/claude-code/loop.ts
4226
4482
  async function* decodeSdkStream(iter, ctx2) {
4227
4483
  const pending = /* @__PURE__ */ new Map();
@@ -4237,17 +4493,15 @@ async function* decodeSdkStream(iter, ctx2) {
4237
4493
  let resultReason;
4238
4494
  let sawResult = false;
4239
4495
  let usage;
4240
- const compactions = [];
4241
- let lastRequestContextTokens;
4242
- let resolvedModel;
4243
- const signals = { sawRejectedLimit: false };
4496
+ const contextUsage = createContextUsage();
4497
+ const failureEvidence = createFailureEvidence();
4244
4498
  let preInitNotice = null;
4245
4499
  try {
4246
4500
  for await (const msg of iter) {
4247
4501
  if (ctx2.signal.aborted) return;
4502
+ failureEvidence.observe(msg);
4248
4503
  if (msg.type === "system" && msg.subtype === "init") {
4249
- const initModel = msg.model;
4250
- if (typeof initModel === "string" && initModel.length > 0) resolvedModel = initModel;
4504
+ contextUsage.recordInit(msg);
4251
4505
  mcpInventory = readMcpInventory(msg);
4252
4506
  workspaceProven = provesWorkspaceTools(msg);
4253
4507
  if (!workspaceProven) {
@@ -4264,63 +4518,35 @@ async function* decodeSdkStream(iter, ctx2) {
4264
4518
  };
4265
4519
  }
4266
4520
  } else if (msg.type === "system" && msg.subtype === "compact_boundary") {
4267
- const compaction = readCompaction(msg);
4268
- if (compaction) compactions.push(compaction);
4521
+ contextUsage.recordCompaction(msg);
4269
4522
  } else if (!workspaceProven && (msg.type === "assistant" || msg.type === "user" || msg.type === "result")) {
4270
4523
  if (preInitToolActivity(msg) && ctx2.startEvidence) ctx2.startEvidence.toolActivity = true;
4271
- const failure = classifyPreInitFrame(msg, signals);
4272
- resultReason = failure ? encodeFailureReason(failure) : "session_start_failed";
4524
+ resultReason = failureEvidence.preInitReason(msg);
4273
4525
  if (msg.type === "assistant" && isHarnessAuthored(msg))
4274
4526
  preInitNotice = harnessNoticeText(msg);
4275
4527
  break;
4276
4528
  } else if (msg.type === "assistant") {
4277
- const assistantErr = msg.error;
4278
- if (typeof assistantErr === "string" && assistantErr.length > 0) {
4279
- signals.lastAssistantError = assistantErr;
4280
- }
4281
4529
  if (isHarnessAuthored(msg)) {
4282
4530
  await emitHarnessMessage(msg, emit, pending, buffer);
4283
4531
  yield* drain(out);
4284
4532
  continue;
4285
4533
  }
4286
- const reqContext = readRequestContextTokens(msg);
4287
- if (reqContext !== void 0) lastRequestContextTokens = reqContext;
4534
+ contextUsage.recordRequest(msg);
4288
4535
  await processAssistantMessage(msg, emit, pending, buffer);
4289
4536
  yield* drain(out);
4290
4537
  } else if (msg.type === "user") {
4291
4538
  await emitToolResults(msg, emit, pending);
4292
4539
  yield* drain(out);
4293
- } else if (msg.type === "rate_limit_event") {
4294
- const info = msg.rate_limit_info;
4295
- if (info?.status === "rejected") {
4296
- signals.sawRejectedLimit = true;
4297
- signals.rateLimitResetIso = resetsAtToIso(info.resetsAt) ?? signals.rateLimitResetIso;
4298
- if (typeof info.rateLimitType === "string") signals.rateLimitType = info.rateLimitType;
4299
- }
4300
- } else if (msg.type === "auth_status") {
4301
- const err = msg.error;
4302
- if (typeof err === "string" && err.length > 0) signals.authError = err;
4303
4540
  } else if (msg.type === "result") {
4304
4541
  const origin = msg.origin;
4305
4542
  if (origin?.kind === "task-notification") continue;
4306
4543
  sawResult = true;
4307
- usage = readSdkUsage(msg);
4308
- if (usage) {
4309
- if (lastRequestContextTokens !== void 0)
4310
- usage.contextTokens = lastRequestContextTokens;
4311
- const window = readContextWindow(msg, resolvedModel);
4312
- if (window !== void 0) usage.contextWindow = window;
4313
- if (compactions.length > 0) usage.contextCompactions = compactions;
4314
- const reading = await readHarnessContext(ctx2.readContextUsage);
4315
- if (reading) Object.assign(usage, reading);
4316
- }
4544
+ usage = await contextUsage.resultUsage(msg, ctx2.readContextUsage);
4317
4545
  const isError = msg.is_error === true;
4318
4546
  if (msg.subtype === "success" && !isError) {
4319
4547
  ok = true;
4320
4548
  } else {
4321
- const resultText = msg.result ?? "";
4322
- const failure = classifyResultFailure(msg, signals);
4323
- resultReason = failure ? encodeFailureReason(failure) : isError ? `error:${resultText.slice(0, 200) || "unknown"}` : `result_error:${msg.subtype}`;
4549
+ resultReason = failureEvidence.failedResultReason(msg);
4324
4550
  ok = false;
4325
4551
  }
4326
4552
  break;
@@ -4329,7 +4555,7 @@ async function* decodeSdkStream(iter, ctx2) {
4329
4555
  } catch (err) {
4330
4556
  if (ctx2.signal.aborted) throw err;
4331
4557
  const message2 = err instanceof Error ? err.message : String(err);
4332
- const failure = mcpInventory.initReceived ? classifyErrorText(message2) : classifyFailure(signals, { text: message2 });
4558
+ const failure = failureEvidence.thrownFailure(message2, mcpInventory.initReceived);
4333
4559
  if (!failure && (mcpInventory.initReceived || ctx2.resumedSessionId === null)) throw err;
4334
4560
  ok = false;
4335
4561
  if (failure) {
@@ -4344,8 +4570,7 @@ async function* decodeSdkStream(iter, ctx2) {
4344
4570
  if (!workspaceProven) {
4345
4571
  ok = false;
4346
4572
  if (resultReason === void 0) {
4347
- const failure = classifyFailure(signals, { text: "" });
4348
- resultReason = failure ? encodeFailureReason(failure) : "session_start_failed";
4573
+ resultReason = failureEvidence.preInitReason();
4349
4574
  }
4350
4575
  if (preInitNotice) yield { type: "runtime_notice", body: preInitNotice };
4351
4576
  }
@@ -4363,7 +4588,7 @@ async function* decodeSdkStream(iter, ctx2) {
4363
4588
  ok,
4364
4589
  ...resultReason ? { reason: resultReason } : {},
4365
4590
  ...usage ? { usage } : {},
4366
- ...resolvedModel ? { resolvedModel } : {},
4591
+ ...contextUsage.resolvedModel ? { resolvedModel: contextUsage.resolvedModel } : {},
4367
4592
  mcpInventory
4368
4593
  };
4369
4594
  }
@@ -4382,38 +4607,6 @@ function provesWorkspaceTools(msg) {
4382
4607
  const tools = msg.tools;
4383
4608
  return Array.isArray(tools) && tools.includes("mcp__cabane__query");
4384
4609
  }
4385
- function classifyFailure(s, frame) {
4386
- const rejectedIsCap = s.sawRejectedLimit && (s.rateLimitResetIso !== void 0 || isSubscriptionWindow(s.rateLimitType));
4387
- if (rejectedIsCap || frame.terminalReason === "blocking_limit")
4388
- return {
4389
- kind: "usage_capped",
4390
- ...s.rateLimitResetIso ? { resetsAt: s.rateLimitResetIso } : {}
4391
- };
4392
- return classifyAssistantError(s.lastAssistantError) ?? classifyErrorText([s.authError, frame.text].filter(Boolean).join(" "));
4393
- }
4394
- function classifyResultFailure(msg, s) {
4395
- const resultText = msg.result ?? "";
4396
- const terminalReason = msg.terminal_reason;
4397
- const errors = msg.errors;
4398
- const text = [resultText, ...Array.isArray(errors) ? errors.map(String) : []].join(" ");
4399
- return classifyFailure(s, { terminalReason, text });
4400
- }
4401
- function classifyPreInitFrame(msg, s) {
4402
- if (msg.type === "result") {
4403
- const isError = msg.is_error === true;
4404
- if (msg.subtype === "success" && !isError) return classifyFailure(s, { text: "" });
4405
- return classifyResultFailure(msg, s);
4406
- }
4407
- if (msg.type !== "assistant") return classifyFailure(s, { text: "" });
4408
- const error = msg.error;
4409
- const signals = typeof error === "string" && error.length > 0 ? { ...s, lastAssistantError: error } : s;
4410
- const text = contentBlocks(msg).flatMap((b) => b.type === "text" && typeof b.text === "string" ? [b.text] : []).join(" ");
4411
- return classifyFailure(signals, { text });
4412
- }
4413
- function preInitToolActivity(msg) {
4414
- const kind = msg.type === "assistant" ? "tool_use" : msg.type === "user" ? "tool_result" : null;
4415
- return kind !== null && contentBlocks(msg).some((b) => b.type === kind);
4416
- }
4417
4610
  function isHarnessAuthored(msg) {
4418
4611
  const error = msg.error;
4419
4612
  if (typeof error === "string" && error.length > 0) return true;
@@ -4438,99 +4631,9 @@ async function emitHarnessMessage(msg, emit, pending, buffer) {
4438
4631
  );
4439
4632
  }
4440
4633
  }
4441
- function contentBlocks(msg) {
4442
- const content = msg.message?.content;
4443
- return Array.isArray(content) ? content : [];
4444
- }
4445
- function isSubscriptionWindow(value) {
4446
- return value === "five_hour" || value === "seven_day" || value === "seven_day_opus" || value === "seven_day_sonnet" || value === "overage";
4447
- }
4448
4634
  function* drain(out) {
4449
4635
  while (out.length > 0) yield out.shift();
4450
4636
  }
4451
- function readSdkUsage(msg) {
4452
- const usage = msg.usage;
4453
- if (!usage || typeof usage !== "object") return void 0;
4454
- const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
4455
- const cacheReadTokens = num(usage.cache_read_input_tokens);
4456
- const cacheCreationTokens = num(usage.cache_creation_input_tokens);
4457
- const inputTokens = num(usage.input_tokens) + cacheCreationTokens + cacheReadTokens;
4458
- const outputTokens = num(usage.output_tokens);
4459
- return { inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens };
4460
- }
4461
- function readRequestContextTokens(msg) {
4462
- const usage = msg.message?.usage;
4463
- if (!usage || typeof usage !== "object") return void 0;
4464
- const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
4465
- return num(usage.input_tokens) + num(usage.cache_read_input_tokens) + num(usage.cache_creation_input_tokens);
4466
- }
4467
- var CONTEXT_READING_TIMEOUT_MS = 2e3;
4468
- async function readHarnessContext(read) {
4469
- if (!read) return void 0;
4470
- let timer;
4471
- const timeout = new Promise((resolve2) => {
4472
- timer = setTimeout(() => resolve2(void 0), CONTEXT_READING_TIMEOUT_MS);
4473
- });
4474
- try {
4475
- return parseContextUsage(await Promise.race([read(), timeout]));
4476
- } catch {
4477
- return void 0;
4478
- } finally {
4479
- clearTimeout(timer);
4480
- }
4481
- }
4482
- var CATEGORY_KINDS = /* @__PURE__ */ new Set(["used", "free", "buffer", "deferred"]);
4483
- function parseContextUsage(response) {
4484
- const r = response;
4485
- if (!r || typeof r !== "object") return void 0;
4486
- const count = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? Math.round(v) : void 0;
4487
- const total = count(r.totalTokens);
4488
- const window = count(r.maxTokens);
4489
- if (total === void 0 || window === void 0 || window === 0) return void 0;
4490
- const categories = [];
4491
- for (const c of Array.isArray(r.categories) ? r.categories : []) {
4492
- const row = c;
4493
- const tokens = count(row?.tokens);
4494
- if (typeof row?.name === "string" && tokens !== void 0 && typeof row.kind === "string" && CATEGORY_KINDS.has(row.kind)) {
4495
- categories.push({ name: row.name, tokens, kind: row.kind });
4496
- }
4497
- }
4498
- const buffer = categories.filter((c) => c.kind === "buffer").reduce((sum, c) => sum + c.tokens, 0);
4499
- return {
4500
- contextTokens: total,
4501
- contextWindow: window,
4502
- ...buffer > 0 && buffer < window ? { contextCompactAt: window - buffer } : {},
4503
- ...categories.length > 0 ? { contextCategories: categories } : {}
4504
- };
4505
- }
4506
- function readCompaction(msg) {
4507
- const meta = msg.compact_metadata;
4508
- const count = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? Math.round(v) : void 0;
4509
- const preTokens = count(meta?.pre_tokens);
4510
- if (!meta || preTokens === void 0) return void 0;
4511
- if (meta.trigger !== "manual" && meta.trigger !== "auto") return void 0;
4512
- const postTokens = count(meta.post_tokens);
4513
- return {
4514
- trigger: meta.trigger,
4515
- preTokens,
4516
- ...postTokens !== void 0 ? { postTokens } : {}
4517
- };
4518
- }
4519
- function readContextWindow(msg, resolvedModel) {
4520
- const modelUsage = msg.modelUsage;
4521
- if (!modelUsage || typeof modelUsage !== "object") return void 0;
4522
- const pos = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : void 0;
4523
- if (resolvedModel) {
4524
- const direct = pos(modelUsage[resolvedModel]?.contextWindow);
4525
- if (direct !== void 0) return direct;
4526
- }
4527
- let max;
4528
- for (const entry of Object.values(modelUsage)) {
4529
- const w = pos(entry?.contextWindow);
4530
- if (w !== void 0 && (max === void 0 || w > max)) max = w;
4531
- }
4532
- return max;
4533
- }
4534
4637
 
4535
4638
  // packages/agent-runtime/src/claude-code/prompt-input.ts
4536
4639
  function buildQueryPrompt(req, openUntil) {
@@ -8078,7 +8181,7 @@ var ConnectorHealthStore = class {
8078
8181
 
8079
8182
  // src/dispatcher.ts
8080
8183
  import { randomUUID as randomUUID4 } from "crypto";
8081
- import { existsSync as existsSync11, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
8184
+ import { existsSync as existsSync11, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
8082
8185
  import { join as join17 } from "path";
8083
8186
 
8084
8187
  // src/runner/launch.ts
@@ -11199,7 +11302,7 @@ function checkoutState(cwd) {
11199
11302
  if (!existsSync11(gitPath)) return { ok: true, reason: "usable" };
11200
11303
  let stat;
11201
11304
  try {
11202
- stat = statSync2(gitPath);
11305
+ stat = statSync3(gitPath);
11203
11306
  } catch (error) {
11204
11307
  return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
11205
11308
  }
@@ -11416,9 +11519,9 @@ import {
11416
11519
  mkdirSync as mkdirSync10,
11417
11520
  readdirSync as readdirSync4,
11418
11521
  readFileSync as readFileSync9,
11419
- renameSync as renameSync5,
11522
+ renameSync as renameSync6,
11420
11523
  rmSync as rmSync9,
11421
- statSync as statSync3,
11524
+ statSync as statSync4,
11422
11525
  writeFileSync as writeFileSync6
11423
11526
  } from "fs";
11424
11527
  import { join as join20 } from "path";
@@ -11612,7 +11715,7 @@ import {
11612
11715
  mkdirSync as mkdirSync9,
11613
11716
  readdirSync as readdirSync3,
11614
11717
  readFileSync as readFileSync8,
11615
- renameSync as renameSync4,
11718
+ renameSync as renameSync5,
11616
11719
  rmSync as rmSync8
11617
11720
  } from "fs";
11618
11721
  import { join as join18 } from "path";
@@ -11648,11 +11751,11 @@ function admitCollectedEntry(raw, turn) {
11648
11751
  }
11649
11752
  var atomicMove = (from, to) => {
11650
11753
  try {
11651
- renameSync4(from, to);
11754
+ renameSync5(from, to);
11652
11755
  } catch {
11653
11756
  const partial = `${to}.partial`;
11654
11757
  copyFileSync2(from, partial);
11655
- renameSync4(partial, to);
11758
+ renameSync5(partial, to);
11656
11759
  rmSync8(from, { force: true });
11657
11760
  }
11658
11761
  };
@@ -11896,6 +11999,25 @@ function pruneOtherAttempts(deviceDir, pair, keep) {
11896
11999
  }
11897
12000
  }
11898
12001
  }
12002
+ var IDLE_SESSION_ARCHIVE_MS = 30 * 24 * 60 * 6e4;
12003
+ function pruneIdleSessionArchives(deviceDir, now = Date.now(), maxAgeMs = IDLE_SESSION_ARCHIVE_MS) {
12004
+ const root = join20(deviceDir, "attempts");
12005
+ if (!existsSync14(root)) return [];
12006
+ const removed = [];
12007
+ for (const id of readdirSync4(root)) {
12008
+ if (!UUID_RE.test(id)) continue;
12009
+ let kept;
12010
+ try {
12011
+ kept = statSync4(join20(root, id)).mtimeMs;
12012
+ } catch {
12013
+ continue;
12014
+ }
12015
+ if (now - kept < maxAgeMs) continue;
12016
+ rmSync9(join20(root, id), { recursive: true, force: true });
12017
+ removed.push(id);
12018
+ }
12019
+ return removed;
12020
+ }
11899
12021
  var CappedFile = class extends Writable {
11900
12022
  constructor(path, cap) {
11901
12023
  super();
@@ -12273,7 +12395,7 @@ function createDockerProvider(opts) {
12273
12395
  (resolve2) => file.writableFinished ? resolve2() : file.once("finish", resolve2)
12274
12396
  );
12275
12397
  if (r.code === 0 && !file.over) {
12276
- renameSync5(partial, archive);
12398
+ renameSync6(partial, archive);
12277
12399
  writeFileSync6(join20(archive, "..", "pair"), `${attempt.conversationId}/${attempt.agentId}`);
12278
12400
  pruneOtherAttempts(opts.deviceDir, attempt, attemptId);
12279
12401
  await writeMarker(name, attemptId);
@@ -12410,7 +12532,7 @@ function createDockerProvider(opts) {
12410
12532
  );
12411
12533
  } else {
12412
12534
  log.info(
12413
- { container: name, attemptId, bytes: statSync3(archive).size },
12535
+ { container: name, attemptId, bytes: statSync4(archive).size },
12414
12536
  "runners: session files restored"
12415
12537
  );
12416
12538
  }
@@ -12983,7 +13105,7 @@ import {
12983
13105
  mkdirSync as mkdirSync12,
12984
13106
  readdirSync as readdirSync5,
12985
13107
  readFileSync as readFileSync11,
12986
- renameSync as renameSync6,
13108
+ renameSync as renameSync7,
12987
13109
  rmSync as rmSync11,
12988
13110
  writeFileSync as writeFileSync8
12989
13111
  } from "fs";
@@ -13015,7 +13137,7 @@ var Outbox = class {
13015
13137
  const tmp = `${target}.${process.pid}.tmp`;
13016
13138
  try {
13017
13139
  writeFileSync8(tmp, JSON.stringify(entry) + "\n", "utf8");
13018
- renameSync6(tmp, target);
13140
+ renameSync7(tmp, target);
13019
13141
  } catch (err) {
13020
13142
  try {
13021
13143
  rmSync11(tmp, { force: true });
@@ -13170,6 +13292,12 @@ var SseSubscriber = class {
13170
13292
  this.aborted = true;
13171
13293
  this.controller?.abort();
13172
13294
  }
13295
+ // Connect again after a `stop()`, once its loop has exited.
13296
+ async resume() {
13297
+ await this.finished;
13298
+ this.aborted = false;
13299
+ this.start();
13300
+ }
13173
13301
  setCursor(eventId) {
13174
13302
  this.lastEventId = eventId;
13175
13303
  }
@@ -13256,6 +13384,550 @@ function sleep3(ms) {
13256
13384
  return new Promise((resolve2) => setTimeout(resolve2, ms));
13257
13385
  }
13258
13386
 
13387
+ // src/updater.ts
13388
+ import { spawn as spawn8 } from "child_process";
13389
+ import { copyFileSync as copyFileSync3, existsSync as existsSync17, mkdirSync as mkdirSync16, renameSync as renameSync11, rmSync as rmSync14 } from "fs";
13390
+ import { dirname as dirname12, join as join26 } from "path";
13391
+
13392
+ // src/boot-rule.ts
13393
+ import {
13394
+ mkdirSync as mkdirSync13,
13395
+ readFileSync as readFileSync12,
13396
+ realpathSync as realpathSync5,
13397
+ renameSync as renameSync8,
13398
+ rmSync as rmSync12,
13399
+ symlinkSync,
13400
+ writeFileSync as writeFileSync9
13401
+ } from "fs";
13402
+ import { dirname as dirname9, join as join23 } from "path";
13403
+ var UNCONFIRMED_WINDOW_MS = 10 * 6e4;
13404
+ var MAX_STAGE_ATTEMPTS = 3;
13405
+ function superseded(pending, ...running) {
13406
+ return !!pending && !running.includes(pending.to);
13407
+ }
13408
+ function rolledBackState(state, pending, reason, now) {
13409
+ const { pending: _done, ...rest } = state;
13410
+ return {
13411
+ ...rest,
13412
+ lastResult: {
13413
+ kind: "rolled_back",
13414
+ from: pending.from,
13415
+ to: pending.to,
13416
+ at: now.toISOString(),
13417
+ reason: reason.replace(/\s+/g, " ").trim().slice(0, 400)
13418
+ },
13419
+ failed: { version: pending.to, attempts: MAX_STAGE_ATTEMPTS, at: now.toISOString() }
13420
+ };
13421
+ }
13422
+
13423
+ // src/install-layout.ts
13424
+ import {
13425
+ existsSync as existsSync16,
13426
+ lstatSync,
13427
+ mkdirSync as mkdirSync14,
13428
+ readFileSync as readFileSync13,
13429
+ readdirSync as readdirSync6,
13430
+ realpathSync as realpathSync6,
13431
+ renameSync as renameSync9,
13432
+ rmSync as rmSync13,
13433
+ symlinkSync as symlinkSync2
13434
+ } from "fs";
13435
+ import { dirname as dirname10, join as join24 } from "path";
13436
+ import { fileURLToPath as fileURLToPath6 } from "url";
13437
+ function versionsDir() {
13438
+ return join24(cabaneDir(), "versions");
13439
+ }
13440
+ function currentLink() {
13441
+ return join24(cabaneDir(), "current");
13442
+ }
13443
+ function bootGuardPath() {
13444
+ return join24(cabaneDir(), "boot-guard.mjs");
13445
+ }
13446
+ function versionPackageRoot(version) {
13447
+ return join24(versionsDir(), version, "node_modules", "@cabane", "companion");
13448
+ }
13449
+ function ownPackageRoot(moduleUrl = import.meta.url) {
13450
+ return dirname10(dirname10(fileURLToPath6(moduleUrl)));
13451
+ }
13452
+ function packageVersion(root) {
13453
+ try {
13454
+ const pkg2 = JSON.parse(readFileSync13(join24(root, "package.json"), "utf8"));
13455
+ return typeof pkg2.version === "string" ? pkg2.version : null;
13456
+ } catch {
13457
+ return null;
13458
+ }
13459
+ }
13460
+ function currentRoot() {
13461
+ try {
13462
+ const root = realpathSync6(currentLink());
13463
+ return existsSync16(join24(root, "dist", "cli.js")) ? root : null;
13464
+ } catch {
13465
+ return null;
13466
+ }
13467
+ }
13468
+ function pointCurrentAt(root) {
13469
+ const link = currentLink();
13470
+ mkdirSync14(dirname10(link), { recursive: true });
13471
+ const fresh = `${link}.${process.pid}.${Date.now()}`;
13472
+ symlinkSync2(root, fresh);
13473
+ try {
13474
+ renameSync9(fresh, link);
13475
+ } catch (err) {
13476
+ rmSync13(fresh, { force: true });
13477
+ throw err;
13478
+ }
13479
+ }
13480
+ function pruneVersions(keep) {
13481
+ const dir2 = versionsDir();
13482
+ if (!existsSync16(dir2)) return [];
13483
+ const removed = [];
13484
+ for (const name of readdirSync6(dir2)) {
13485
+ if (keep.includes(name)) continue;
13486
+ rmSync13(join24(dir2, name), { recursive: true, force: true });
13487
+ removed.push(name);
13488
+ }
13489
+ return removed;
13490
+ }
13491
+ function compareVersions(a, b) {
13492
+ const parts = (v) => /^\d+(\.\d+)*$/.test(v) ? v.split(".").map(Number) : null;
13493
+ const pa = parts(a);
13494
+ const pb = parts(b);
13495
+ if (!pa || !pb) return pa ? 1 : pb ? -1 : 0;
13496
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
13497
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
13498
+ if (d !== 0) return d;
13499
+ }
13500
+ return 0;
13501
+ }
13502
+
13503
+ // src/update-state.ts
13504
+ import { mkdirSync as mkdirSync15, readFileSync as readFileSync14, renameSync as renameSync10, writeFileSync as writeFileSync10 } from "fs";
13505
+ import { dirname as dirname11, join as join25 } from "path";
13506
+ import { z as z18 } from "zod";
13507
+ var resultSchema = z18.object({
13508
+ kind: z18.enum(["updated", "rolled_back", "failed"]),
13509
+ from: z18.string(),
13510
+ to: z18.string(),
13511
+ at: z18.string(),
13512
+ reason: z18.string().optional()
13513
+ });
13514
+ var pendingSchema = z18.object({
13515
+ from: z18.string(),
13516
+ // The package root `current` pointed at before the swap, so a rollback can point it back.
13517
+ fromRoot: z18.string(),
13518
+ to: z18.string(),
13519
+ at: z18.string(),
13520
+ boots: z18.number().int().nonnegative()
13521
+ });
13522
+ var stateSchema = z18.object({
13523
+ server: z18.object({ latest: z18.string(), minimum: z18.string(), at: z18.string() }).optional(),
13524
+ pending: pendingSchema.optional(),
13525
+ lastResult: resultSchema.optional(),
13526
+ failed: z18.object({ version: z18.string(), attempts: z18.number().int(), at: z18.string() }).optional()
13527
+ });
13528
+ function updateStatePath(dir2 = cabaneDir()) {
13529
+ return join25(dir2, "update.json");
13530
+ }
13531
+ function readUpdateState(path = updateStatePath()) {
13532
+ try {
13533
+ const parsed = stateSchema.safeParse(JSON.parse(readFileSync14(path, "utf8")));
13534
+ return parsed.success ? parsed.data : {};
13535
+ } catch {
13536
+ return {};
13537
+ }
13538
+ }
13539
+ function writeUpdateState(state, path = updateStatePath()) {
13540
+ mkdirSync15(dirname11(path), { recursive: true });
13541
+ const tmp = `${path}.${process.pid}.tmp`;
13542
+ writeFileSync10(tmp, `${JSON.stringify(state, null, 2)}
13543
+ `, { mode: 384 });
13544
+ renameSync10(tmp, path);
13545
+ }
13546
+ function updateUpdateState(change, path = updateStatePath()) {
13547
+ const next = change(readUpdateState(path));
13548
+ writeUpdateState(next, path);
13549
+ return next;
13550
+ }
13551
+
13552
+ // src/updater.ts
13553
+ function updateMode(env = process.env) {
13554
+ return env.CABANE_COMPANION_SUPERVISED === "1" ? "supervised" : "attached";
13555
+ }
13556
+ var STAGE_RETRY_MS = 60 * 6e4;
13557
+ var SWAP_DRAIN_MS = 1e4;
13558
+ var SOURCE_BUILD_VERSION = "0.0.0";
13559
+ function versionToStage(input) {
13560
+ const { own, state, now, force } = input;
13561
+ if (!input.auto && !force) return null;
13562
+ if (own === SOURCE_BUILD_VERSION) return null;
13563
+ const latest = state.server?.latest;
13564
+ if (!latest || compareVersions(latest, own) <= 0) return null;
13565
+ if (state.pending) return null;
13566
+ const failed = state.failed;
13567
+ if (!force && failed?.version === latest) {
13568
+ if (failed.attempts >= MAX_STAGE_ATTEMPTS) return null;
13569
+ if (now - Date.parse(failed.at) < STAGE_RETRY_MS) return null;
13570
+ }
13571
+ return latest;
13572
+ }
13573
+ function rollBack(pending, reason, now = /* @__PURE__ */ new Date()) {
13574
+ if (existsSync17(pending.fromRoot)) pointCurrentAt(pending.fromRoot);
13575
+ return updateUpdateState((state) => rolledBackState(state, pending, reason, now));
13576
+ }
13577
+ function writeBootGuard(root = ownPackageRoot()) {
13578
+ const built = join26(root, "dist", "boot-guard.js");
13579
+ if (!existsSync17(built)) return false;
13580
+ const target = bootGuardPath();
13581
+ mkdirSync16(dirname12(target), { recursive: true });
13582
+ const tmp = `${target}.${process.pid}.tmp`;
13583
+ copyFileSync3(built, tmp);
13584
+ renameSync11(tmp, target);
13585
+ return true;
13586
+ }
13587
+ function oneLine(text) {
13588
+ return text.replace(/\s+/g, " ").trim().slice(0, 400);
13589
+ }
13590
+ var runProcess = (command, args, opts = {}) => new Promise((resolve2) => {
13591
+ let stdout = "";
13592
+ let stderr = "";
13593
+ let child;
13594
+ try {
13595
+ child = spawn8(command, [...args], {
13596
+ stdio: ["ignore", "pipe", "pipe"],
13597
+ env: opts.env ?? process.env
13598
+ });
13599
+ } catch (err) {
13600
+ resolve2({ code: null, stdout, stderr: err instanceof Error ? err.message : String(err) });
13601
+ return;
13602
+ }
13603
+ const timer = opts.timeoutMs ? setTimeout(() => child.kill("SIGKILL"), opts.timeoutMs) : null;
13604
+ child.stdout.on("data", (d) => stdout += d.toString());
13605
+ child.stderr.on("data", (d) => stderr += d.toString());
13606
+ child.on("error", (err) => {
13607
+ if (timer) clearTimeout(timer);
13608
+ resolve2({ code: null, stdout, stderr: stderr || err.message });
13609
+ });
13610
+ child.on("close", (code) => {
13611
+ if (timer) clearTimeout(timer);
13612
+ resolve2({ code, stdout, stderr });
13613
+ });
13614
+ });
13615
+ async function stageVersion(version, harnesses, deps = {}) {
13616
+ const run = deps.run ?? runProcess;
13617
+ const node = deps.execPath ?? process.execPath;
13618
+ const target = join26(versionsDir(), version);
13619
+ const placed = versionPackageRoot(version);
13620
+ if (existsSync17(placed)) {
13621
+ if (!await verifyInstall(placed, version, harnesses, { run, execPath: node }))
13622
+ return { ok: true, root: placed };
13623
+ }
13624
+ const npm = join26(dirname12(node), "npm");
13625
+ if (!existsSync17(npm))
13626
+ return { ok: false, reason: `there is no npm beside Node at ${dirname12(node)}` };
13627
+ mkdirSync16(versionsDir(), { recursive: true });
13628
+ const staging = join26(versionsDir(), `.${version}.staging-${process.pid}`);
13629
+ rmSync14(staging, { recursive: true, force: true });
13630
+ const installed = await run(
13631
+ npm,
13632
+ [
13633
+ "install",
13634
+ "--prefix",
13635
+ staging,
13636
+ "--include=optional",
13637
+ "--no-audit",
13638
+ "--no-fund",
13639
+ "--no-save",
13640
+ "--loglevel=error",
13641
+ `@cabane/companion@${version}`
13642
+ ],
13643
+ {
13644
+ // npm's install scripts run on this Node, not whichever `node` is first on PATH.
13645
+ env: { ...process.env, PATH: `${dirname12(node)}:${process.env.PATH ?? ""}` },
13646
+ timeoutMs: 15 * 6e4
13647
+ }
13648
+ );
13649
+ if (installed.code !== 0) {
13650
+ rmSync14(staging, { recursive: true, force: true });
13651
+ if (/\bE404\b|\bETARGET\b|No matching version found|is not in this registry/i.test(
13652
+ installed.stderr
13653
+ ))
13654
+ return { ok: false, reason: `version ${version} isn't published yet`, unpublished: true };
13655
+ return { ok: false, reason: `npm couldn't install it: ${lastLine(installed.stderr)}` };
13656
+ }
13657
+ const root = join26(staging, "node_modules", "@cabane", "companion");
13658
+ const verdict = await verifyInstall(root, version, harnesses, { run, execPath: node });
13659
+ if (verdict) {
13660
+ rmSync14(staging, { recursive: true, force: true });
13661
+ return { ok: false, reason: verdict };
13662
+ }
13663
+ rmSync14(target, { recursive: true, force: true });
13664
+ renameSync11(staging, target);
13665
+ return { ok: true, root: versionPackageRoot(version) };
13666
+ }
13667
+ async function verifyInstall(root, version, harnesses, deps) {
13668
+ if (packageVersion(root) !== version) return `the installed package isn't version ${version}`;
13669
+ const answered = await deps.run(deps.execPath, [join26(root, "dist", "bin.js"), "--version"], {
13670
+ timeoutMs: 6e4,
13671
+ env: { ...process.env, CABANE_LAUNCH_SELF: "1" }
13672
+ });
13673
+ if (answered.code !== 0 || answered.stdout.trim() !== version) {
13674
+ return `it doesn't start: ${lastLine(answered.stderr) || "no version printed"}`;
13675
+ }
13676
+ if (harnesses.length === 0) return null;
13677
+ const probed = await deps.run(deps.execPath, [join26(root, "dist", "postinstall.js"), "--json"], {
13678
+ timeoutMs: 6e4
13679
+ });
13680
+ let resolutions = {};
13681
+ try {
13682
+ resolutions = JSON.parse(probed.stdout);
13683
+ } catch {
13684
+ return "it couldn't say which bundled agents it has";
13685
+ }
13686
+ const missing = harnesses.filter((h) => resolutions[h]?.status !== "present");
13687
+ if (missing.length > 0) {
13688
+ return `its bundled ${missing.map(harnessName).join(" and ")} ${missing.length > 1 ? "are" : "is"} missing`;
13689
+ }
13690
+ return null;
13691
+ }
13692
+ function harnessName(h) {
13693
+ return h === "claude-code" ? "Claude Code" : "Codex";
13694
+ }
13695
+ function lastLine(text) {
13696
+ const lines2 = text.split("\n").map((l) => l.trim()).filter(Boolean);
13697
+ return lines2.at(-1)?.slice(0, 300) ?? "";
13698
+ }
13699
+ var CompanionUpdater = class {
13700
+ constructor(deps) {
13701
+ this.deps = deps;
13702
+ this.now = deps.now ?? Date.now;
13703
+ }
13704
+ deps;
13705
+ staging = null;
13706
+ ready = null;
13707
+ swapping = false;
13708
+ watchdog = null;
13709
+ now;
13710
+ // At start: a recorded swap to a version that will never start again (someone installed or
13711
+ // chose another since) is dropped, or nothing would ever confirm it and no later update would
13712
+ // be installed.
13713
+ start() {
13714
+ const pending = readUpdateState().pending;
13715
+ const current = currentRoot();
13716
+ const currentVersion = current ? packageVersion(current) : null;
13717
+ if (superseded(pending, this.deps.own, ...currentVersion ? [currentVersion] : [])) {
13718
+ updateUpdateState(({ pending: _gone, ...rest }) => rest);
13719
+ this.deps.log.info(
13720
+ { to: pending.to, own: this.deps.own },
13721
+ `Dropped the update to ${pending.to}: Cabane ${this.deps.own} was installed since`
13722
+ );
13723
+ }
13724
+ }
13725
+ // The deadline for an update this process is the result of: ten minutes from the first time
13726
+ // Cabane answers it without its stream connecting. Until Cabane answers, there's no verdict on
13727
+ // time: a server or network that is down says nothing about the version.
13728
+ armDeadline() {
13729
+ if (this.watchdog) return;
13730
+ this.watchdog = setTimeout(() => void this.giveUp(), UNCONFIRMED_WINDOW_MS);
13731
+ this.watchdog.unref?.();
13732
+ }
13733
+ stop() {
13734
+ if (this.watchdog) clearTimeout(this.watchdog);
13735
+ this.watchdog = null;
13736
+ }
13737
+ async giveUp() {
13738
+ const pending = readUpdateState().pending;
13739
+ if (!pending || pending.to !== this.deps.own) return;
13740
+ this.deps.log.warn(
13741
+ { from: pending.from, to: pending.to },
13742
+ `Cabane ${pending.to} didn't connect within ${UNCONFIRMED_WINDOW_MS / 6e4} minutes of Cabane answering; going back to ${pending.from}`
13743
+ );
13744
+ rollBack(
13745
+ pending,
13746
+ `it didn't connect to Cabane within ${UNCONFIRMED_WINDOW_MS / 6e4} minutes`
13747
+ );
13748
+ await this.deps.restart();
13749
+ }
13750
+ // Auto-update is on and something will restart this companion into the new version.
13751
+ updatesItself() {
13752
+ return this.deps.mode === "supervised" && this.deps.autoUpdate();
13753
+ }
13754
+ // What the heartbeat reports.
13755
+ report() {
13756
+ const last = readUpdateState().lastResult;
13757
+ return { auto: this.updatesItself(), ...last ? { lastResult: last } : {} };
13758
+ }
13759
+ // A heartbeat landed. Record what the server named; confirm an update this process is the
13760
+ // result of; then, when there is something newer, install it and swap when idle.
13761
+ async onHeartbeat(server, streamConnected) {
13762
+ const state = updateUpdateState((s) => ({
13763
+ ...s,
13764
+ ...server ? { server: { ...server, at: new Date(this.now()).toISOString() } } : {}
13765
+ }));
13766
+ if (state.pending?.to === this.deps.own) {
13767
+ if (streamConnected) this.confirm(state.pending);
13768
+ else this.armDeadline();
13769
+ }
13770
+ if (this.ready) {
13771
+ await this.trySwap();
13772
+ return;
13773
+ }
13774
+ this.maybeStage(false);
13775
+ }
13776
+ confirm(pending) {
13777
+ this.stop();
13778
+ updateUpdateState((s) => {
13779
+ const { pending: _done, failed: _failed, ...rest } = s;
13780
+ return {
13781
+ ...rest,
13782
+ lastResult: {
13783
+ kind: "updated",
13784
+ from: pending.from,
13785
+ to: pending.to,
13786
+ at: new Date(this.now()).toISOString()
13787
+ }
13788
+ };
13789
+ });
13790
+ const removed = pruneVersions([pending.to, pending.from]);
13791
+ this.deps.log.info(
13792
+ { from: pending.from, to: pending.to, removed },
13793
+ `Updated Cabane from ${pending.from} to ${pending.to}`
13794
+ );
13795
+ }
13796
+ // `cabane update`, from the person: install the latest now, whatever the setting.
13797
+ async updateNow() {
13798
+ const state = readUpdateState();
13799
+ const latest = state.server?.latest;
13800
+ if (!latest) return { ok: false, message: "Cabane hasn't said which version is latest yet." };
13801
+ if (compareVersions(latest, this.deps.own) <= 0)
13802
+ return { ok: true, message: `Cabane ${this.deps.own} is up to date.` };
13803
+ if (state.pending?.to === latest) return { ok: true, message: this.waitingMessage(latest) };
13804
+ if (this.ready === latest) {
13805
+ void this.trySwap();
13806
+ return { ok: true, message: this.waitingMessage(latest) };
13807
+ }
13808
+ this.maybeStage(true);
13809
+ if (this.staging) await this.staging;
13810
+ if (this.ready !== latest) {
13811
+ const after = readUpdateState();
13812
+ if (after.pending?.to === latest) return { ok: true, message: this.waitingMessage(latest) };
13813
+ if (after.failed?.version === latest && after.failed.attempts === 0)
13814
+ return { ok: false, message: `Cabane ${latest} isn't published yet. Try again later.` };
13815
+ const failed = after.lastResult;
13816
+ return {
13817
+ ok: false,
13818
+ message: failed?.kind === "failed" && failed.to === latest ? `Couldn't update to ${latest}: ${failed.reason ?? "see cabane logs"}.` : `Couldn't update to ${latest}. cabane logs says why.`
13819
+ };
13820
+ }
13821
+ void this.trySwap();
13822
+ return { ok: true, message: this.waitingMessage(latest) };
13823
+ }
13824
+ waitingMessage(version) {
13825
+ if (this.deps.mode === "attached")
13826
+ return `Cabane ${version} is installed. It runs from the next cabane start.`;
13827
+ return this.deps.isIdle() ? `Updating to ${version} now.` : `Updating to ${version} once the turns running now finish.`;
13828
+ }
13829
+ maybeStage(force) {
13830
+ if (this.staging) return;
13831
+ const version = versionToStage({
13832
+ auto: this.updatesItself(),
13833
+ own: this.deps.own,
13834
+ state: readUpdateState(),
13835
+ now: this.now(),
13836
+ force
13837
+ });
13838
+ if (!version) return;
13839
+ const stage = this.deps.stage ?? stageVersion;
13840
+ this.staging = (async () => {
13841
+ this.deps.log.info({ version }, `Installing Cabane ${version} beside this one`);
13842
+ const outcome = await stage(version, this.deps.harnesses());
13843
+ if (outcome.ok) {
13844
+ this.ready = version;
13845
+ updateUpdateState((s) => {
13846
+ const { failed: _failed, ...rest } = s;
13847
+ return rest;
13848
+ });
13849
+ return;
13850
+ }
13851
+ if (outcome.unpublished) {
13852
+ this.deps.log.info({ version }, `Cabane ${version} isn't published yet; trying later`);
13853
+ updateUpdateState((s) => ({
13854
+ ...s,
13855
+ failed: { version, attempts: 0, at: new Date(this.now()).toISOString() }
13856
+ }));
13857
+ return;
13858
+ }
13859
+ this.deps.log.warn({ version, reason: outcome.reason }, `Couldn't install Cabane ${version}`);
13860
+ updateUpdateState((s) => {
13861
+ const attempts = s.failed?.version === version ? s.failed.attempts + 1 : 1;
13862
+ const at = new Date(this.now()).toISOString();
13863
+ return {
13864
+ ...s,
13865
+ failed: { version, attempts, at },
13866
+ // Reported once it has failed for good, so a registry blip isn't an email.
13867
+ ...attempts >= MAX_STAGE_ATTEMPTS || force ? {
13868
+ lastResult: {
13869
+ kind: "failed",
13870
+ from: this.deps.own,
13871
+ to: version,
13872
+ at,
13873
+ reason: oneLine(outcome.reason)
13874
+ }
13875
+ } : {}
13876
+ };
13877
+ });
13878
+ })().finally(() => {
13879
+ this.staging = null;
13880
+ });
13881
+ void this.staging.then(() => this.trySwap());
13882
+ }
13883
+ // Swap into the ready version once nothing is running. Called on each heartbeat until it can.
13884
+ async trySwap() {
13885
+ const to = this.ready;
13886
+ if (!to || this.swapping) return;
13887
+ if (readUpdateState().pending) return;
13888
+ if (this.deps.mode === "attached") {
13889
+ this.select(to);
13890
+ this.deps.log.info({ version: to }, `Cabane ${to} is installed; it runs from the next start`);
13891
+ return;
13892
+ }
13893
+ if (!this.deps.isIdle()) return;
13894
+ this.swapping = true;
13895
+ try {
13896
+ await this.deps.pauseStream();
13897
+ const sleep4 = this.deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
13898
+ const deadline = this.now() + SWAP_DRAIN_MS;
13899
+ await sleep4(250);
13900
+ while (!this.deps.isIdle() && this.now() < deadline) await sleep4(250);
13901
+ if (!this.deps.isIdle()) {
13902
+ this.deps.resumeStream();
13903
+ return;
13904
+ }
13905
+ this.select(to);
13906
+ this.deps.log.info({ from: this.deps.own, to }, `Restarting into Cabane ${to}`);
13907
+ await this.deps.restart();
13908
+ } finally {
13909
+ this.swapping = false;
13910
+ }
13911
+ }
13912
+ // Record the swap and point `current` at `to`, leaving the guard that counts its starts.
13913
+ select(to) {
13914
+ const fromRoot = currentRoot() ?? ownPackageRoot();
13915
+ writeBootGuard();
13916
+ updateUpdateState((s) => ({
13917
+ ...s,
13918
+ pending: {
13919
+ from: this.deps.own,
13920
+ fromRoot,
13921
+ to,
13922
+ at: new Date(this.now()).toISOString(),
13923
+ boots: 0
13924
+ }
13925
+ }));
13926
+ pointCurrentAt(versionPackageRoot(to));
13927
+ this.ready = null;
13928
+ }
13929
+ };
13930
+
13259
13931
  // src/version.ts
13260
13932
  import { createRequire as createRequire3 } from "module";
13261
13933
  var pkg = createRequire3(import.meta.url)("../package.json");
@@ -13263,6 +13935,7 @@ var COMPANION_VERSION = pkg.version;
13263
13935
 
13264
13936
  // src/supervisor.ts
13265
13937
  var HEARTBEAT_INTERVAL_MS = 3e4;
13938
+ var ARCHIVE_PRUNE_MS = 24 * 60 * 6e4;
13266
13939
  var SHUTDOWN_HEARTBEAT_WAIT_MS = 1e3;
13267
13940
  function positiveIntEnv(name) {
13268
13941
  const raw = process.env[name];
@@ -13344,6 +14017,10 @@ var CompanionSupervisor = class {
13344
14017
  // Which dispatches this process is running, so a redelivered frame doesn't
13345
14018
  // run the turn a second time.
13346
14019
  claims = new DispatchClaims();
14020
+ archivePruneTimer = null;
14021
+ // Keeps this companion current when its person said so; null until paired.
14022
+ updater = null;
14023
+ restartForUpdate;
13347
14024
  constructor(opts) {
13348
14025
  this.config = opts.config;
13349
14026
  this.log = opts.log;
@@ -13353,6 +14030,10 @@ var CompanionSupervisor = class {
13353
14030
  this.harnessVersions = opts.harnessVersions ?? emptyHarnessVersions();
13354
14031
  this.exitFn = opts.exit ?? ((code) => process.exit(code));
13355
14032
  this.reexecFn = opts.reexec ?? defaultReexec;
14033
+ this.restartForUpdate = opts.restartForUpdate ?? (async () => {
14034
+ await this.shutdown();
14035
+ this.exitFn(0);
14036
+ });
13356
14037
  this.dispatcherFactory = opts.dispatcherFactory;
13357
14038
  if (opts.runnerCommand) this.runnerCommand = opts.runnerCommand;
13358
14039
  if (opts.runnerProvider) this.providerForRunners = opts.runnerProvider;
@@ -13403,6 +14084,9 @@ var CompanionSupervisor = class {
13403
14084
  if (!this.runnerImage) await invalidateCompanionVolume(docker, image, source);
13404
14085
  }
13405
14086
  this.latchBootBundledState();
14087
+ this.pruneSessionArchives();
14088
+ this.archivePruneTimer = setInterval(() => this.pruneSessionArchives(), ARCHIVE_PRUNE_MS);
14089
+ this.archivePruneTimer.unref?.();
13406
14090
  }
13407
14091
  const sandboxRows = agentSandboxRows(this.config);
13408
14092
  if (sandboxRows.length > 0 && this.dockerRunners()) {
@@ -13466,6 +14150,23 @@ var CompanionSupervisor = class {
13466
14150
  );
13467
14151
  }
13468
14152
  });
14153
+ this.updater = new CompanionUpdater({
14154
+ own: COMPANION_VERSION,
14155
+ mode: updateMode(),
14156
+ autoUpdate: () => this.config.update?.auto === true,
14157
+ harnesses: () => this.connectedBundledHarnesses(),
14158
+ isIdle: () => this.idle(),
14159
+ pauseStream: async () => {
14160
+ const sub = this.deviceSub;
14161
+ if (!sub) return;
14162
+ sub.stop();
14163
+ await sub.finished;
14164
+ },
14165
+ resumeStream: () => void this.deviceSub?.resume(),
14166
+ restart: () => this.restartForUpdate(),
14167
+ log: this.log
14168
+ });
14169
+ this.updater.start();
13469
14170
  await this.refreshAssignments();
13470
14171
  this.deviceSub.start();
13471
14172
  const firstHeartbeat = startupHarnessRefresh.then(async () => {
@@ -13526,13 +14227,20 @@ var CompanionSupervisor = class {
13526
14227
  // Omitted until the first observed failure or heal, so the server's
13527
14228
  // status-less manifest synthesis still runs for a fresh companion.
13528
14229
  ...connectorReports.length > 0 ? { connectors: connectorReports } : {},
13529
- ...opencodeModels !== null ? { models: opencodeModels } : {}
14230
+ ...opencodeModels !== null ? { models: opencodeModels } : {},
14231
+ ...this.updater ? { update: this.updater.report() } : {}
13530
14232
  });
13531
14233
  this.hub.setDevice({ deviceId: res.deviceId });
13532
14234
  this.deviceId = res.deviceId;
13533
14235
  this.checkVersionSkew(res.serverVersion);
13534
14236
  this.heartbeated = true;
13535
14237
  void this.recoverRuns();
14238
+ void this.updater?.onHeartbeat(res.companion, this.deviceConnected).catch(
14239
+ (err) => this.log.warn(
14240
+ { err: err instanceof Error ? err.message : String(err) },
14241
+ "Couldn't check for a Cabane update; trying again on the next check-in."
14242
+ )
14243
+ );
13536
14244
  } catch (err) {
13537
14245
  this.log.warn(
13538
14246
  { ...apiErrorLogFields(err) },
@@ -14563,9 +15271,54 @@ var CompanionSupervisor = class {
14563
15271
  }
14564
15272
  }
14565
15273
  }
15274
+ // A long-lived device keeps a session archive per conversation and agent it ever ran; those
15275
+ // nobody has used in a month go.
15276
+ pruneSessionArchives() {
15277
+ try {
15278
+ const removed = pruneIdleSessionArchives(cabaneDir());
15279
+ if (removed.length > 0)
15280
+ this.log.info({ removed: removed.length }, "runners: removed idle session archives");
15281
+ } catch (err) {
15282
+ this.log.warn(
15283
+ { err: err instanceof Error ? err.message : String(err) },
15284
+ "runners: couldn't remove idle session archives"
15285
+ );
15286
+ }
15287
+ }
15288
+ // Nothing running and nothing about to: no dispatch claimed, no turn chained, no recovery
15289
+ // in progress. What a self-update swap waits for.
15290
+ idle() {
15291
+ if (this.claims.size > 0 || this.recovering) return false;
15292
+ for (const wr of this.workspaces.values()) if (wr.chains.size > 0) return false;
15293
+ return true;
15294
+ }
15295
+ // The bundled harnesses this device has connected, which a new version must still carry.
15296
+ connectedBundledHarnesses() {
15297
+ return [
15298
+ ...isClaudeCodeConnected(this.config) ? ["claude-code"] : [],
15299
+ ...isCodexEnabled(this.config) ? ["codex"] : []
15300
+ ];
15301
+ }
15302
+ // `cabane update`, over the control socket: set whether this companion keeps itself current
15303
+ // (`auto`), or install the latest now.
15304
+ async update(auto) {
15305
+ if (auto !== void 0) {
15306
+ this.config = { ...this.config, update: { auto } };
15307
+ saveConfig(this.config);
15308
+ if (auto) this.kickHeartbeat();
15309
+ return {
15310
+ ok: true,
15311
+ message: auto ? "Cabane will keep itself up to date. Updates install between turns and roll back if they fail." : "Cabane won't update itself. Run cabane update to update."
15312
+ };
15313
+ }
15314
+ if (!this.updater) return { ok: false, message: "This computer isn't paired yet." };
15315
+ return this.updater.updateNow();
15316
+ }
14566
15317
  // ---- control actions ----
14567
15318
  async shutdown() {
14568
15319
  this.stopped = true;
15320
+ this.updater?.stop();
15321
+ if (this.archivePruneTimer) clearInterval(this.archivePruneTimer);
14569
15322
  if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
14570
15323
  if (this.pollTimer) clearInterval(this.pollTimer);
14571
15324
  const heartbeat = this.inFlightHeartbeat;
@@ -14600,9 +15353,9 @@ async function waitForBoundedHeartbeat(heartbeat) {
14600
15353
  }
14601
15354
  function defaultReexec() {
14602
15355
  clearRuntimeState();
14603
- void import("child_process").then(({ spawn: spawn8 }) => {
15356
+ void import("child_process").then(({ spawn: spawn9 }) => {
14604
15357
  try {
14605
- const child = spawn8(process.execPath, process.argv.slice(1), {
15358
+ const child = spawn9(process.execPath, process.argv.slice(1), {
14606
15359
  stdio: "inherit",
14607
15360
  detached: false
14608
15361
  });
@@ -14672,22 +15425,22 @@ function handleUncaught(log, err, origin) {
14672
15425
  }
14673
15426
 
14674
15427
  // 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";
15428
+ import { existsSync as existsSync18, mkdirSync as mkdirSync17, readFileSync as readFileSync15, rmSync as rmSync15, writeFileSync as writeFileSync11 } from "fs";
15429
+ import { join as join27 } from "path";
14677
15430
  function crashMarkerPath() {
14678
- return join23(cabaneDir(), "last-error.json");
15431
+ return join27(cabaneDir(), "last-error.json");
14679
15432
  }
14680
15433
  function recordCrash(rec) {
14681
15434
  try {
14682
- mkdirSync13(cabaneDir(), { recursive: true });
14683
- writeFileSync9(crashMarkerPath(), JSON.stringify(rec, null, 2) + "\n");
15435
+ mkdirSync17(cabaneDir(), { recursive: true });
15436
+ writeFileSync11(crashMarkerPath(), JSON.stringify(rec, null, 2) + "\n");
14684
15437
  } catch {
14685
15438
  }
14686
15439
  }
14687
15440
  function clearCrash() {
14688
15441
  try {
14689
15442
  const path = crashMarkerPath();
14690
- if (existsSync16(path)) rmSync12(path, { force: true });
15443
+ if (existsSync18(path)) rmSync15(path, { force: true });
14691
15444
  } catch {
14692
15445
  }
14693
15446
  }
@@ -14742,11 +15495,14 @@ async function createCompanionRuntime(opts = {}) {
14742
15495
  // process behind the marker pid is this companion (not a recycled pid).
14743
15496
  instanceId
14744
15497
  });
15498
+ let restartForUpdate = async () => {
15499
+ };
14745
15500
  const supervisor = new CompanionSupervisor({
14746
15501
  config: cfg,
14747
15502
  log,
14748
15503
  hub,
14749
- ...opts.checkHarness ? { checkHarness: opts.checkHarness } : {}
15504
+ ...opts.checkHarness ? { checkHarness: opts.checkHarness } : {},
15505
+ restartForUpdate: () => restartForUpdate()
14750
15506
  });
14751
15507
  await supervisor.start();
14752
15508
  const connectHarness = async (runtime, serverUrl) => {
@@ -14771,6 +15527,7 @@ async function createCompanionRuntime(opts = {}) {
14771
15527
  control = await startControlServer({
14772
15528
  status: () => hub.statusJson(),
14773
15529
  connect: (runtime, serverUrl) => connectHarness(runtime, serverUrl),
15530
+ update: (auto) => supervisor.update(auto),
14774
15531
  stop: () => void supervisor.requestStop()
14775
15532
  });
14776
15533
  if (opts.dashboard) {
@@ -14823,6 +15580,10 @@ async function createCompanionRuntime(opts = {}) {
14823
15580
  } catch {
14824
15581
  }
14825
15582
  };
15583
+ restartForUpdate = async () => {
15584
+ await stop();
15585
+ process.exit(0);
15586
+ };
14826
15587
  return {
14827
15588
  ok: true,
14828
15589
  runtime: {