@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/README.md +7 -109
- package/dist/bin.js +2 -1
- package/dist/boot-guard.js +112 -0
- package/dist/cli.js +1848 -357
- package/dist/launch.js +411 -0
- package/dist/pairing-config.js +9 -0
- package/dist/runtime.js +957 -196
- package/package.json +1 -1
package/dist/runtime.js
CHANGED
|
@@ -616,6 +616,15 @@ var companionConfigSchema = z2.object({
|
|
|
616
616
|
// absent `claudeCode` block means "never offered"; without it, the block's
|
|
617
617
|
// absence marks a config from before connectors were chosen.
|
|
618
618
|
harnessesChosen: z2.boolean().optional(),
|
|
619
|
+
// Whether this companion keeps itself current: installs the version Cabane names, between
|
|
620
|
+
// turns, and rolls back one that doesn't come up (`updater.ts`). Absent means never asked,
|
|
621
|
+
// which `start` offers; off either way until someone says yes.
|
|
622
|
+
update: z2.object({
|
|
623
|
+
auto: z2.boolean()
|
|
624
|
+
}).strict().optional(),
|
|
625
|
+
// The answer to `start`'s "start at boot?" when it was no, so it isn't asked again.
|
|
626
|
+
// `cabane service install` still installs it.
|
|
627
|
+
serviceDeclined: z2.boolean().optional(),
|
|
619
628
|
// OpenCode's decline. Its block is an address, so a decline can't live there
|
|
620
629
|
// the way `claudeCode`/`codex` `enabled: false` does; without it a declined
|
|
621
630
|
// serve would be offered again on every `start`.
|
|
@@ -792,7 +801,7 @@ import { serve } from "@hono/node-server";
|
|
|
792
801
|
import { Hono } from "hono";
|
|
793
802
|
|
|
794
803
|
// src/dashboard/routes.ts
|
|
795
|
-
import { openSync, readSync, closeSync, fstatSync, existsSync as existsSync3 } from "fs";
|
|
804
|
+
import { openSync as openSync2, readSync, closeSync, fstatSync, existsSync as existsSync3 } from "fs";
|
|
796
805
|
import { readFile } from "fs/promises";
|
|
797
806
|
import { extname, join as join6, normalize } from "path";
|
|
798
807
|
import { streamSSE } from "hono/streaming";
|
|
@@ -1264,7 +1273,13 @@ function connectLines(outcome, opts = {}) {
|
|
|
1264
1273
|
}
|
|
1265
1274
|
|
|
1266
1275
|
// src/logger.ts
|
|
1267
|
-
import {
|
|
1276
|
+
import {
|
|
1277
|
+
createWriteStream,
|
|
1278
|
+
mkdirSync as mkdirSync2,
|
|
1279
|
+
openSync,
|
|
1280
|
+
renameSync as renameSync2,
|
|
1281
|
+
statSync as statSync2
|
|
1282
|
+
} from "fs";
|
|
1268
1283
|
import { dirname as dirname3, join as join5 } from "path";
|
|
1269
1284
|
import { stripVTControlCharacters } from "util";
|
|
1270
1285
|
import pino from "pino";
|
|
@@ -1319,6 +1334,53 @@ function formatHumanLine(record) {
|
|
|
1319
1334
|
}
|
|
1320
1335
|
return line + "\n";
|
|
1321
1336
|
}
|
|
1337
|
+
var LOG_ROTATE_BYTES = 50 * 1024 * 1024;
|
|
1338
|
+
var LOG_ROTATIONS_KEPT = 3;
|
|
1339
|
+
var RotatingLog = class {
|
|
1340
|
+
constructor(path, maxBytes = LOG_ROTATE_BYTES, keep = LOG_ROTATIONS_KEPT) {
|
|
1341
|
+
this.path = path;
|
|
1342
|
+
this.maxBytes = maxBytes;
|
|
1343
|
+
this.keep = keep;
|
|
1344
|
+
this.size = sizeOf(path);
|
|
1345
|
+
this.stream = openAppend(path);
|
|
1346
|
+
}
|
|
1347
|
+
path;
|
|
1348
|
+
maxBytes;
|
|
1349
|
+
keep;
|
|
1350
|
+
stream;
|
|
1351
|
+
size;
|
|
1352
|
+
write(chunk) {
|
|
1353
|
+
const ok = this.stream.write(chunk);
|
|
1354
|
+
this.size += Buffer.byteLength(chunk);
|
|
1355
|
+
if (this.size >= this.maxBytes) this.rotate();
|
|
1356
|
+
return ok;
|
|
1357
|
+
}
|
|
1358
|
+
rotate() {
|
|
1359
|
+
this.stream.end();
|
|
1360
|
+
try {
|
|
1361
|
+
for (let i = this.keep - 1; i >= 1; i--) {
|
|
1362
|
+
try {
|
|
1363
|
+
renameSync2(`${this.path}.${i}`, `${this.path}.${i + 1}`);
|
|
1364
|
+
} catch {
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
renameSync2(this.path, `${this.path}.1`);
|
|
1368
|
+
} catch {
|
|
1369
|
+
}
|
|
1370
|
+
this.stream = openAppend(this.path);
|
|
1371
|
+
this.size = sizeOf(this.path);
|
|
1372
|
+
}
|
|
1373
|
+
};
|
|
1374
|
+
function openAppend(path) {
|
|
1375
|
+
return createWriteStream("", { fd: openSync(path, "a") });
|
|
1376
|
+
}
|
|
1377
|
+
function sizeOf(path) {
|
|
1378
|
+
try {
|
|
1379
|
+
return statSync2(path).size;
|
|
1380
|
+
} catch {
|
|
1381
|
+
return 0;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1322
1384
|
var cached = null;
|
|
1323
1385
|
var consoleLogging = true;
|
|
1324
1386
|
var overrides = {};
|
|
@@ -1331,7 +1393,7 @@ function configureLogger(log, config) {
|
|
|
1331
1393
|
function createLogger(destinations = {}, options = {}) {
|
|
1332
1394
|
const path = companionLogPath();
|
|
1333
1395
|
if (!destinations.file) mkdirSync2(dirname3(path), { recursive: true });
|
|
1334
|
-
const file = destinations.file ??
|
|
1396
|
+
const file = destinations.file ?? new RotatingLog(path);
|
|
1335
1397
|
const state = { format: options.logFormat ?? "human" };
|
|
1336
1398
|
const streams = [];
|
|
1337
1399
|
const human = (chunk) => {
|
|
@@ -1755,7 +1817,7 @@ function tailFile(path, lines2) {
|
|
|
1755
1817
|
const MAX_BYTES = 256 * 1024;
|
|
1756
1818
|
let fd;
|
|
1757
1819
|
try {
|
|
1758
|
-
fd =
|
|
1820
|
+
fd = openSync2(path, "r");
|
|
1759
1821
|
const size = fstatSync(fd).size;
|
|
1760
1822
|
const start = Math.max(0, size - MAX_BYTES);
|
|
1761
1823
|
const len = size - start;
|
|
@@ -1931,6 +1993,10 @@ async function serveConnection(socket, handlers) {
|
|
|
1931
1993
|
reply(socket, result);
|
|
1932
1994
|
return;
|
|
1933
1995
|
}
|
|
1996
|
+
if (req.cmd === "update") {
|
|
1997
|
+
reply(socket, await handlers.update(req.auto));
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
1934
2000
|
if (req.cmd === "stop") {
|
|
1935
2001
|
reply(socket, { ok: true });
|
|
1936
2002
|
setTimeout(() => handlers.stop(), 50).unref?.();
|
|
@@ -2053,7 +2119,7 @@ import {
|
|
|
2053
2119
|
rmSync as rmSync3,
|
|
2054
2120
|
writeFileSync as writeFileSync2,
|
|
2055
2121
|
mkdirSync as mkdirSync4,
|
|
2056
|
-
openSync as
|
|
2122
|
+
openSync as openSync3,
|
|
2057
2123
|
closeSync as closeSync2
|
|
2058
2124
|
} from "fs";
|
|
2059
2125
|
import { join as join9 } from "path";
|
|
@@ -2075,7 +2141,7 @@ function acquireRuntimeState(state) {
|
|
|
2075
2141
|
mkdirSync4(cabaneDir(), { recursive: true });
|
|
2076
2142
|
let fd;
|
|
2077
2143
|
try {
|
|
2078
|
-
fd =
|
|
2144
|
+
fd = openSync3(runtimePath(), "wx");
|
|
2079
2145
|
} catch {
|
|
2080
2146
|
return { acquired: false, existing: readLiveRuntimeState() };
|
|
2081
2147
|
}
|
|
@@ -2961,7 +3027,7 @@ import {
|
|
|
2961
3027
|
existsSync as existsSync6,
|
|
2962
3028
|
mkdirSync as mkdirSync5,
|
|
2963
3029
|
readFileSync as readFileSync3,
|
|
2964
|
-
renameSync as
|
|
3030
|
+
renameSync as renameSync3,
|
|
2965
3031
|
rmSync as rmSync4,
|
|
2966
3032
|
writeFileSync as writeFileSync3
|
|
2967
3033
|
} from "fs";
|
|
@@ -3002,7 +3068,7 @@ function save(map) {
|
|
|
3002
3068
|
chmodSync3(tmp, 384);
|
|
3003
3069
|
} catch {
|
|
3004
3070
|
}
|
|
3005
|
-
|
|
3071
|
+
renameSync3(tmp, path);
|
|
3006
3072
|
} catch (err) {
|
|
3007
3073
|
try {
|
|
3008
3074
|
rmSync4(tmp, { force: true });
|
|
@@ -3035,7 +3101,7 @@ function pruneCredentials(keepAgentIds) {
|
|
|
3035
3101
|
}
|
|
3036
3102
|
|
|
3037
3103
|
// src/dispatch-dedupe.ts
|
|
3038
|
-
import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, renameSync as
|
|
3104
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, renameSync as renameSync4, existsSync as existsSync7 } from "fs";
|
|
3039
3105
|
import { join as join11 } from "path";
|
|
3040
3106
|
var MAX_IDS = 256;
|
|
3041
3107
|
function dir(log) {
|
|
@@ -3153,7 +3219,7 @@ function writeTurnIds(workspaceId, turns) {
|
|
|
3153
3219
|
const path = turnPathFor(workspaceId);
|
|
3154
3220
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
3155
3221
|
writeFileSync4(tmp, entries.map(([id, t]) => `${id} ${t}`).join("\n") + "\n", "utf8");
|
|
3156
|
-
|
|
3222
|
+
renameSync4(tmp, path);
|
|
3157
3223
|
return entries.length;
|
|
3158
3224
|
}
|
|
3159
3225
|
function rememberTurnId(workspaceId, eventId, turnId) {
|
|
@@ -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
|
|
4241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
11522
|
+
renameSync as renameSync6,
|
|
11420
11523
|
rmSync as rmSync9,
|
|
11421
|
-
statSync as
|
|
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
|
|
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
|
-
|
|
11754
|
+
renameSync5(from, to);
|
|
11652
11755
|
} catch {
|
|
11653
11756
|
const partial = `${to}.partial`;
|
|
11654
11757
|
copyFileSync2(from, partial);
|
|
11655
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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:
|
|
15356
|
+
void import("child_process").then(({ spawn: spawn9 }) => {
|
|
14604
15357
|
try {
|
|
14605
|
-
const child =
|
|
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
|
|
14676
|
-
import { join as
|
|
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
|
|
15431
|
+
return join27(cabaneDir(), "last-error.json");
|
|
14679
15432
|
}
|
|
14680
15433
|
function recordCrash(rec) {
|
|
14681
15434
|
try {
|
|
14682
|
-
|
|
14683
|
-
|
|
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 (
|
|
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: {
|