@cabane/companion 0.6.157 → 0.6.159
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 +2 -2
- package/dist/cli.js +281 -214
- package/dist/runtime.js +256 -202
- package/package.json +1 -1
package/dist/runtime.js
CHANGED
|
@@ -4288,6 +4288,196 @@ function dedupe(xs) {
|
|
|
4288
4288
|
return [...new Set(xs)];
|
|
4289
4289
|
}
|
|
4290
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
|
+
|
|
4291
4481
|
// packages/agent-runtime/src/claude-code/loop.ts
|
|
4292
4482
|
async function* decodeSdkStream(iter, ctx2) {
|
|
4293
4483
|
const pending = /* @__PURE__ */ new Map();
|
|
@@ -4303,17 +4493,15 @@ async function* decodeSdkStream(iter, ctx2) {
|
|
|
4303
4493
|
let resultReason;
|
|
4304
4494
|
let sawResult = false;
|
|
4305
4495
|
let usage;
|
|
4306
|
-
const
|
|
4307
|
-
|
|
4308
|
-
let resolvedModel;
|
|
4309
|
-
const signals = { sawRejectedLimit: false };
|
|
4496
|
+
const contextUsage = createContextUsage();
|
|
4497
|
+
const failureEvidence = createFailureEvidence();
|
|
4310
4498
|
let preInitNotice = null;
|
|
4311
4499
|
try {
|
|
4312
4500
|
for await (const msg of iter) {
|
|
4313
4501
|
if (ctx2.signal.aborted) return;
|
|
4502
|
+
failureEvidence.observe(msg);
|
|
4314
4503
|
if (msg.type === "system" && msg.subtype === "init") {
|
|
4315
|
-
|
|
4316
|
-
if (typeof initModel === "string" && initModel.length > 0) resolvedModel = initModel;
|
|
4504
|
+
contextUsage.recordInit(msg);
|
|
4317
4505
|
mcpInventory = readMcpInventory(msg);
|
|
4318
4506
|
workspaceProven = provesWorkspaceTools(msg);
|
|
4319
4507
|
if (!workspaceProven) {
|
|
@@ -4330,63 +4518,35 @@ async function* decodeSdkStream(iter, ctx2) {
|
|
|
4330
4518
|
};
|
|
4331
4519
|
}
|
|
4332
4520
|
} else if (msg.type === "system" && msg.subtype === "compact_boundary") {
|
|
4333
|
-
|
|
4334
|
-
if (compaction) compactions.push(compaction);
|
|
4521
|
+
contextUsage.recordCompaction(msg);
|
|
4335
4522
|
} else if (!workspaceProven && (msg.type === "assistant" || msg.type === "user" || msg.type === "result")) {
|
|
4336
4523
|
if (preInitToolActivity(msg) && ctx2.startEvidence) ctx2.startEvidence.toolActivity = true;
|
|
4337
|
-
|
|
4338
|
-
resultReason = failure ? encodeFailureReason(failure) : "session_start_failed";
|
|
4524
|
+
resultReason = failureEvidence.preInitReason(msg);
|
|
4339
4525
|
if (msg.type === "assistant" && isHarnessAuthored(msg))
|
|
4340
4526
|
preInitNotice = harnessNoticeText(msg);
|
|
4341
4527
|
break;
|
|
4342
4528
|
} else if (msg.type === "assistant") {
|
|
4343
|
-
const assistantErr = msg.error;
|
|
4344
|
-
if (typeof assistantErr === "string" && assistantErr.length > 0) {
|
|
4345
|
-
signals.lastAssistantError = assistantErr;
|
|
4346
|
-
}
|
|
4347
4529
|
if (isHarnessAuthored(msg)) {
|
|
4348
4530
|
await emitHarnessMessage(msg, emit, pending, buffer);
|
|
4349
4531
|
yield* drain(out);
|
|
4350
4532
|
continue;
|
|
4351
4533
|
}
|
|
4352
|
-
|
|
4353
|
-
if (reqContext !== void 0) lastRequestContextTokens = reqContext;
|
|
4534
|
+
contextUsage.recordRequest(msg);
|
|
4354
4535
|
await processAssistantMessage(msg, emit, pending, buffer);
|
|
4355
4536
|
yield* drain(out);
|
|
4356
4537
|
} else if (msg.type === "user") {
|
|
4357
4538
|
await emitToolResults(msg, emit, pending);
|
|
4358
4539
|
yield* drain(out);
|
|
4359
|
-
} else if (msg.type === "rate_limit_event") {
|
|
4360
|
-
const info = msg.rate_limit_info;
|
|
4361
|
-
if (info?.status === "rejected") {
|
|
4362
|
-
signals.sawRejectedLimit = true;
|
|
4363
|
-
signals.rateLimitResetIso = resetsAtToIso(info.resetsAt) ?? signals.rateLimitResetIso;
|
|
4364
|
-
if (typeof info.rateLimitType === "string") signals.rateLimitType = info.rateLimitType;
|
|
4365
|
-
}
|
|
4366
|
-
} else if (msg.type === "auth_status") {
|
|
4367
|
-
const err = msg.error;
|
|
4368
|
-
if (typeof err === "string" && err.length > 0) signals.authError = err;
|
|
4369
4540
|
} else if (msg.type === "result") {
|
|
4370
4541
|
const origin = msg.origin;
|
|
4371
4542
|
if (origin?.kind === "task-notification") continue;
|
|
4372
4543
|
sawResult = true;
|
|
4373
|
-
usage =
|
|
4374
|
-
if (usage) {
|
|
4375
|
-
if (lastRequestContextTokens !== void 0)
|
|
4376
|
-
usage.contextTokens = lastRequestContextTokens;
|
|
4377
|
-
const window = readContextWindow(msg, resolvedModel);
|
|
4378
|
-
if (window !== void 0) usage.contextWindow = window;
|
|
4379
|
-
if (compactions.length > 0) usage.contextCompactions = compactions;
|
|
4380
|
-
const reading = await readHarnessContext(ctx2.readContextUsage);
|
|
4381
|
-
if (reading) Object.assign(usage, reading);
|
|
4382
|
-
}
|
|
4544
|
+
usage = await contextUsage.resultUsage(msg, ctx2.readContextUsage);
|
|
4383
4545
|
const isError = msg.is_error === true;
|
|
4384
4546
|
if (msg.subtype === "success" && !isError) {
|
|
4385
4547
|
ok = true;
|
|
4386
4548
|
} else {
|
|
4387
|
-
|
|
4388
|
-
const failure = classifyResultFailure(msg, signals);
|
|
4389
|
-
resultReason = failure ? encodeFailureReason(failure) : isError ? `error:${resultText.slice(0, 200) || "unknown"}` : `result_error:${msg.subtype}`;
|
|
4549
|
+
resultReason = failureEvidence.failedResultReason(msg);
|
|
4390
4550
|
ok = false;
|
|
4391
4551
|
}
|
|
4392
4552
|
break;
|
|
@@ -4395,7 +4555,7 @@ async function* decodeSdkStream(iter, ctx2) {
|
|
|
4395
4555
|
} catch (err) {
|
|
4396
4556
|
if (ctx2.signal.aborted) throw err;
|
|
4397
4557
|
const message2 = err instanceof Error ? err.message : String(err);
|
|
4398
|
-
const failure =
|
|
4558
|
+
const failure = failureEvidence.thrownFailure(message2, mcpInventory.initReceived);
|
|
4399
4559
|
if (!failure && (mcpInventory.initReceived || ctx2.resumedSessionId === null)) throw err;
|
|
4400
4560
|
ok = false;
|
|
4401
4561
|
if (failure) {
|
|
@@ -4410,8 +4570,7 @@ async function* decodeSdkStream(iter, ctx2) {
|
|
|
4410
4570
|
if (!workspaceProven) {
|
|
4411
4571
|
ok = false;
|
|
4412
4572
|
if (resultReason === void 0) {
|
|
4413
|
-
|
|
4414
|
-
resultReason = failure ? encodeFailureReason(failure) : "session_start_failed";
|
|
4573
|
+
resultReason = failureEvidence.preInitReason();
|
|
4415
4574
|
}
|
|
4416
4575
|
if (preInitNotice) yield { type: "runtime_notice", body: preInitNotice };
|
|
4417
4576
|
}
|
|
@@ -4429,7 +4588,7 @@ async function* decodeSdkStream(iter, ctx2) {
|
|
|
4429
4588
|
ok,
|
|
4430
4589
|
...resultReason ? { reason: resultReason } : {},
|
|
4431
4590
|
...usage ? { usage } : {},
|
|
4432
|
-
...resolvedModel ? { resolvedModel } : {},
|
|
4591
|
+
...contextUsage.resolvedModel ? { resolvedModel: contextUsage.resolvedModel } : {},
|
|
4433
4592
|
mcpInventory
|
|
4434
4593
|
};
|
|
4435
4594
|
}
|
|
@@ -4448,38 +4607,6 @@ function provesWorkspaceTools(msg) {
|
|
|
4448
4607
|
const tools = msg.tools;
|
|
4449
4608
|
return Array.isArray(tools) && tools.includes("mcp__cabane__query");
|
|
4450
4609
|
}
|
|
4451
|
-
function classifyFailure(s, frame) {
|
|
4452
|
-
const rejectedIsCap = s.sawRejectedLimit && (s.rateLimitResetIso !== void 0 || isSubscriptionWindow(s.rateLimitType));
|
|
4453
|
-
if (rejectedIsCap || frame.terminalReason === "blocking_limit")
|
|
4454
|
-
return {
|
|
4455
|
-
kind: "usage_capped",
|
|
4456
|
-
...s.rateLimitResetIso ? { resetsAt: s.rateLimitResetIso } : {}
|
|
4457
|
-
};
|
|
4458
|
-
return classifyAssistantError(s.lastAssistantError) ?? classifyErrorText([s.authError, frame.text].filter(Boolean).join(" "));
|
|
4459
|
-
}
|
|
4460
|
-
function classifyResultFailure(msg, s) {
|
|
4461
|
-
const resultText = msg.result ?? "";
|
|
4462
|
-
const terminalReason = msg.terminal_reason;
|
|
4463
|
-
const errors = msg.errors;
|
|
4464
|
-
const text = [resultText, ...Array.isArray(errors) ? errors.map(String) : []].join(" ");
|
|
4465
|
-
return classifyFailure(s, { terminalReason, text });
|
|
4466
|
-
}
|
|
4467
|
-
function classifyPreInitFrame(msg, s) {
|
|
4468
|
-
if (msg.type === "result") {
|
|
4469
|
-
const isError = msg.is_error === true;
|
|
4470
|
-
if (msg.subtype === "success" && !isError) return classifyFailure(s, { text: "" });
|
|
4471
|
-
return classifyResultFailure(msg, s);
|
|
4472
|
-
}
|
|
4473
|
-
if (msg.type !== "assistant") return classifyFailure(s, { text: "" });
|
|
4474
|
-
const error = msg.error;
|
|
4475
|
-
const signals = typeof error === "string" && error.length > 0 ? { ...s, lastAssistantError: error } : s;
|
|
4476
|
-
const text = contentBlocks(msg).flatMap((b) => b.type === "text" && typeof b.text === "string" ? [b.text] : []).join(" ");
|
|
4477
|
-
return classifyFailure(signals, { text });
|
|
4478
|
-
}
|
|
4479
|
-
function preInitToolActivity(msg) {
|
|
4480
|
-
const kind = msg.type === "assistant" ? "tool_use" : msg.type === "user" ? "tool_result" : null;
|
|
4481
|
-
return kind !== null && contentBlocks(msg).some((b) => b.type === kind);
|
|
4482
|
-
}
|
|
4483
4610
|
function isHarnessAuthored(msg) {
|
|
4484
4611
|
const error = msg.error;
|
|
4485
4612
|
if (typeof error === "string" && error.length > 0) return true;
|
|
@@ -4504,99 +4631,9 @@ async function emitHarnessMessage(msg, emit, pending, buffer) {
|
|
|
4504
4631
|
);
|
|
4505
4632
|
}
|
|
4506
4633
|
}
|
|
4507
|
-
function contentBlocks(msg) {
|
|
4508
|
-
const content = msg.message?.content;
|
|
4509
|
-
return Array.isArray(content) ? content : [];
|
|
4510
|
-
}
|
|
4511
|
-
function isSubscriptionWindow(value) {
|
|
4512
|
-
return value === "five_hour" || value === "seven_day" || value === "seven_day_opus" || value === "seven_day_sonnet" || value === "overage";
|
|
4513
|
-
}
|
|
4514
4634
|
function* drain(out) {
|
|
4515
4635
|
while (out.length > 0) yield out.shift();
|
|
4516
4636
|
}
|
|
4517
|
-
function readSdkUsage(msg) {
|
|
4518
|
-
const usage = msg.usage;
|
|
4519
|
-
if (!usage || typeof usage !== "object") return void 0;
|
|
4520
|
-
const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
4521
|
-
const cacheReadTokens = num(usage.cache_read_input_tokens);
|
|
4522
|
-
const cacheCreationTokens = num(usage.cache_creation_input_tokens);
|
|
4523
|
-
const inputTokens = num(usage.input_tokens) + cacheCreationTokens + cacheReadTokens;
|
|
4524
|
-
const outputTokens = num(usage.output_tokens);
|
|
4525
|
-
return { inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens };
|
|
4526
|
-
}
|
|
4527
|
-
function readRequestContextTokens(msg) {
|
|
4528
|
-
const usage = msg.message?.usage;
|
|
4529
|
-
if (!usage || typeof usage !== "object") return void 0;
|
|
4530
|
-
const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
4531
|
-
return num(usage.input_tokens) + num(usage.cache_read_input_tokens) + num(usage.cache_creation_input_tokens);
|
|
4532
|
-
}
|
|
4533
|
-
var CONTEXT_READING_TIMEOUT_MS = 2e3;
|
|
4534
|
-
async function readHarnessContext(read) {
|
|
4535
|
-
if (!read) return void 0;
|
|
4536
|
-
let timer;
|
|
4537
|
-
const timeout = new Promise((resolve2) => {
|
|
4538
|
-
timer = setTimeout(() => resolve2(void 0), CONTEXT_READING_TIMEOUT_MS);
|
|
4539
|
-
});
|
|
4540
|
-
try {
|
|
4541
|
-
return parseContextUsage(await Promise.race([read(), timeout]));
|
|
4542
|
-
} catch {
|
|
4543
|
-
return void 0;
|
|
4544
|
-
} finally {
|
|
4545
|
-
clearTimeout(timer);
|
|
4546
|
-
}
|
|
4547
|
-
}
|
|
4548
|
-
var CATEGORY_KINDS = /* @__PURE__ */ new Set(["used", "free", "buffer", "deferred"]);
|
|
4549
|
-
function parseContextUsage(response) {
|
|
4550
|
-
const r = response;
|
|
4551
|
-
if (!r || typeof r !== "object") return void 0;
|
|
4552
|
-
const count = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? Math.round(v) : void 0;
|
|
4553
|
-
const total = count(r.totalTokens);
|
|
4554
|
-
const window = count(r.maxTokens);
|
|
4555
|
-
if (total === void 0 || window === void 0 || window === 0) return void 0;
|
|
4556
|
-
const categories = [];
|
|
4557
|
-
for (const c of Array.isArray(r.categories) ? r.categories : []) {
|
|
4558
|
-
const row = c;
|
|
4559
|
-
const tokens = count(row?.tokens);
|
|
4560
|
-
if (typeof row?.name === "string" && tokens !== void 0 && typeof row.kind === "string" && CATEGORY_KINDS.has(row.kind)) {
|
|
4561
|
-
categories.push({ name: row.name, tokens, kind: row.kind });
|
|
4562
|
-
}
|
|
4563
|
-
}
|
|
4564
|
-
const buffer = categories.filter((c) => c.kind === "buffer").reduce((sum, c) => sum + c.tokens, 0);
|
|
4565
|
-
return {
|
|
4566
|
-
contextTokens: total,
|
|
4567
|
-
contextWindow: window,
|
|
4568
|
-
...buffer > 0 && buffer < window ? { contextCompactAt: window - buffer } : {},
|
|
4569
|
-
...categories.length > 0 ? { contextCategories: categories } : {}
|
|
4570
|
-
};
|
|
4571
|
-
}
|
|
4572
|
-
function readCompaction(msg) {
|
|
4573
|
-
const meta = msg.compact_metadata;
|
|
4574
|
-
const count = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? Math.round(v) : void 0;
|
|
4575
|
-
const preTokens = count(meta?.pre_tokens);
|
|
4576
|
-
if (!meta || preTokens === void 0) return void 0;
|
|
4577
|
-
if (meta.trigger !== "manual" && meta.trigger !== "auto") return void 0;
|
|
4578
|
-
const postTokens = count(meta.post_tokens);
|
|
4579
|
-
return {
|
|
4580
|
-
trigger: meta.trigger,
|
|
4581
|
-
preTokens,
|
|
4582
|
-
...postTokens !== void 0 ? { postTokens } : {}
|
|
4583
|
-
};
|
|
4584
|
-
}
|
|
4585
|
-
function readContextWindow(msg, resolvedModel) {
|
|
4586
|
-
const modelUsage = msg.modelUsage;
|
|
4587
|
-
if (!modelUsage || typeof modelUsage !== "object") return void 0;
|
|
4588
|
-
const pos = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : void 0;
|
|
4589
|
-
if (resolvedModel) {
|
|
4590
|
-
const direct = pos(modelUsage[resolvedModel]?.contextWindow);
|
|
4591
|
-
if (direct !== void 0) return direct;
|
|
4592
|
-
}
|
|
4593
|
-
let max;
|
|
4594
|
-
for (const entry of Object.values(modelUsage)) {
|
|
4595
|
-
const w = pos(entry?.contextWindow);
|
|
4596
|
-
if (w !== void 0 && (max === void 0 || w > max)) max = w;
|
|
4597
|
-
}
|
|
4598
|
-
return max;
|
|
4599
|
-
}
|
|
4600
4637
|
|
|
4601
4638
|
// packages/agent-runtime/src/claude-code/prompt-input.ts
|
|
4602
4639
|
function buildQueryPrompt(req, openUntil) {
|
|
@@ -13244,6 +13281,8 @@ var SseSubscriber = class {
|
|
|
13244
13281
|
opts;
|
|
13245
13282
|
aborted = false;
|
|
13246
13283
|
controller = null;
|
|
13284
|
+
// Ends the reconnect backoff early, so a stop never waits out a sleep of up to 30 s.
|
|
13285
|
+
wake = null;
|
|
13247
13286
|
lastEventId;
|
|
13248
13287
|
// Set after `start()`; resolved when the subscriber loop exits (either
|
|
13249
13288
|
// because `stop()` was called or because an auth failure tore it down).
|
|
@@ -13254,12 +13293,7 @@ var SseSubscriber = class {
|
|
|
13254
13293
|
stop() {
|
|
13255
13294
|
this.aborted = true;
|
|
13256
13295
|
this.controller?.abort();
|
|
13257
|
-
|
|
13258
|
-
// Connect again after a `stop()`, once its loop has exited.
|
|
13259
|
-
async resume() {
|
|
13260
|
-
await this.finished;
|
|
13261
|
-
this.aborted = false;
|
|
13262
|
-
this.start();
|
|
13296
|
+
this.wake?.();
|
|
13263
13297
|
}
|
|
13264
13298
|
setCursor(eventId) {
|
|
13265
13299
|
this.lastEventId = eventId;
|
|
@@ -13272,7 +13306,7 @@ var SseSubscriber = class {
|
|
|
13272
13306
|
backoff = 500;
|
|
13273
13307
|
if (!this.aborted) {
|
|
13274
13308
|
this.opts.log.warn("Lost the connection to Cabane; reconnecting");
|
|
13275
|
-
await
|
|
13309
|
+
await this.backoff(backoff);
|
|
13276
13310
|
}
|
|
13277
13311
|
} catch (err) {
|
|
13278
13312
|
if (this.aborted) return;
|
|
@@ -13292,11 +13326,21 @@ var SseSubscriber = class {
|
|
|
13292
13326
|
},
|
|
13293
13327
|
"Lost the connection to Cabane; reconnecting"
|
|
13294
13328
|
);
|
|
13295
|
-
await
|
|
13329
|
+
await this.backoff(backoff);
|
|
13296
13330
|
backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
|
|
13297
13331
|
}
|
|
13298
13332
|
}
|
|
13299
13333
|
}
|
|
13334
|
+
backoff(ms) {
|
|
13335
|
+
return new Promise((resolve2) => {
|
|
13336
|
+
const timer = setTimeout(done, ms);
|
|
13337
|
+
this.wake = done;
|
|
13338
|
+
function done() {
|
|
13339
|
+
clearTimeout(timer);
|
|
13340
|
+
resolve2();
|
|
13341
|
+
}
|
|
13342
|
+
});
|
|
13343
|
+
}
|
|
13300
13344
|
async connect() {
|
|
13301
13345
|
const base = this.opts.baseUrl.endsWith("/") ? this.opts.baseUrl.slice(0, -1) : this.opts.baseUrl;
|
|
13302
13346
|
const url = `${base}${this.opts.path}`;
|
|
@@ -13343,9 +13387,6 @@ var SseSubscriber = class {
|
|
|
13343
13387
|
}
|
|
13344
13388
|
}
|
|
13345
13389
|
};
|
|
13346
|
-
function sleep3(ms) {
|
|
13347
|
-
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
13348
|
-
}
|
|
13349
13390
|
|
|
13350
13391
|
// src/updater.ts
|
|
13351
13392
|
import { spawn as spawn8 } from "child_process";
|
|
@@ -13517,7 +13558,7 @@ function updateMode(env = process.env) {
|
|
|
13517
13558
|
return env.CABANE_COMPANION_SUPERVISED === "1" ? "supervised" : "attached";
|
|
13518
13559
|
}
|
|
13519
13560
|
var STAGE_RETRY_MS = 60 * 6e4;
|
|
13520
|
-
var
|
|
13561
|
+
var SWAP_WAIT_MS = 10 * 6e4;
|
|
13521
13562
|
var SOURCE_BUILD_VERSION = "0.0.0";
|
|
13522
13563
|
function versionToStage(input) {
|
|
13523
13564
|
const { own, state, now, force } = input;
|
|
@@ -13667,6 +13708,8 @@ var CompanionUpdater = class {
|
|
|
13667
13708
|
deps;
|
|
13668
13709
|
staging = null;
|
|
13669
13710
|
ready = null;
|
|
13711
|
+
// When `ready` verified; `SWAP_WAIT_MS` counts from here.
|
|
13712
|
+
readyAt = 0;
|
|
13670
13713
|
swapping = false;
|
|
13671
13714
|
watchdog = null;
|
|
13672
13715
|
now;
|
|
@@ -13765,7 +13808,7 @@ var CompanionUpdater = class {
|
|
|
13765
13808
|
return { ok: true, message: `Cabane ${this.deps.own} is up to date.` };
|
|
13766
13809
|
if (state.pending?.to === latest) return { ok: true, message: this.waitingMessage(latest) };
|
|
13767
13810
|
if (this.ready === latest) {
|
|
13768
|
-
void this.trySwap();
|
|
13811
|
+
void this.trySwap(true);
|
|
13769
13812
|
return { ok: true, message: this.waitingMessage(latest) };
|
|
13770
13813
|
}
|
|
13771
13814
|
this.maybeStage(true);
|
|
@@ -13781,13 +13824,14 @@ var CompanionUpdater = class {
|
|
|
13781
13824
|
message: failed?.kind === "failed" && failed.to === latest ? `Couldn't update to ${latest}: ${failed.reason ?? "see cabane logs"}.` : `Couldn't update to ${latest}. cabane logs says why.`
|
|
13782
13825
|
};
|
|
13783
13826
|
}
|
|
13784
|
-
void this.trySwap();
|
|
13827
|
+
void this.trySwap(true);
|
|
13785
13828
|
return { ok: true, message: this.waitingMessage(latest) };
|
|
13786
13829
|
}
|
|
13830
|
+
// Only the person's `cabane update` asks, and that swaps at once.
|
|
13787
13831
|
waitingMessage(version) {
|
|
13788
13832
|
if (this.deps.mode === "attached")
|
|
13789
13833
|
return `Cabane ${version} is installed. It runs from the next cabane start.`;
|
|
13790
|
-
return this.deps.isIdle() ? `Updating to ${version} now.` : `Updating to ${version}
|
|
13834
|
+
return this.deps.isIdle() ? `Updating to ${version} now.` : `Updating to ${version} now. The turns running now carry on after the restart.`;
|
|
13791
13835
|
}
|
|
13792
13836
|
maybeStage(force) {
|
|
13793
13837
|
if (this.staging) return;
|
|
@@ -13805,6 +13849,7 @@ var CompanionUpdater = class {
|
|
|
13805
13849
|
const outcome = await stage(version, this.deps.harnesses());
|
|
13806
13850
|
if (outcome.ok) {
|
|
13807
13851
|
this.ready = version;
|
|
13852
|
+
this.readyAt = this.now();
|
|
13808
13853
|
updateUpdateState((s) => {
|
|
13809
13854
|
const { failed: _failed, ...rest } = s;
|
|
13810
13855
|
return rest;
|
|
@@ -13843,8 +13888,10 @@ var CompanionUpdater = class {
|
|
|
13843
13888
|
});
|
|
13844
13889
|
void this.staging.then(() => this.trySwap());
|
|
13845
13890
|
}
|
|
13846
|
-
// Swap into the ready version
|
|
13847
|
-
|
|
13891
|
+
// Swap into the ready version: when nothing is running, when it has waited `SWAP_WAIT_MS` for
|
|
13892
|
+
// that, when this version is below the server's minimum, or `now` (the person asked). Called on
|
|
13893
|
+
// each heartbeat until it does.
|
|
13894
|
+
async trySwap(now = false) {
|
|
13848
13895
|
const to = this.ready;
|
|
13849
13896
|
if (!to || this.swapping) return;
|
|
13850
13897
|
if (readUpdateState().pending) return;
|
|
@@ -13853,18 +13900,9 @@ var CompanionUpdater = class {
|
|
|
13853
13900
|
this.deps.log.info({ version: to }, `Cabane ${to} is installed; it runs from the next start`);
|
|
13854
13901
|
return;
|
|
13855
13902
|
}
|
|
13856
|
-
if (!this.deps.isIdle()) return;
|
|
13903
|
+
if (!now && !this.deps.isIdle() && !this.waitedLongEnough() && !this.belowMinimum()) return;
|
|
13857
13904
|
this.swapping = true;
|
|
13858
13905
|
try {
|
|
13859
|
-
await this.deps.pauseStream();
|
|
13860
|
-
const sleep4 = this.deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
13861
|
-
const deadline = this.now() + SWAP_DRAIN_MS;
|
|
13862
|
-
await sleep4(250);
|
|
13863
|
-
while (!this.deps.isIdle() && this.now() < deadline) await sleep4(250);
|
|
13864
|
-
if (!this.deps.isIdle()) {
|
|
13865
|
-
this.deps.resumeStream();
|
|
13866
|
-
return;
|
|
13867
|
-
}
|
|
13868
13906
|
this.select(to);
|
|
13869
13907
|
this.deps.log.info({ from: this.deps.own, to }, `Restarting into Cabane ${to}`);
|
|
13870
13908
|
await this.deps.restart();
|
|
@@ -13872,6 +13910,14 @@ var CompanionUpdater = class {
|
|
|
13872
13910
|
this.swapping = false;
|
|
13873
13911
|
}
|
|
13874
13912
|
}
|
|
13913
|
+
waitedLongEnough() {
|
|
13914
|
+
return this.now() - this.readyAt >= SWAP_WAIT_MS;
|
|
13915
|
+
}
|
|
13916
|
+
// Cabane refuses this version's turns: waiting would only turn them away.
|
|
13917
|
+
belowMinimum() {
|
|
13918
|
+
const minimum = readUpdateState().server?.minimum;
|
|
13919
|
+
return !!minimum && compareVersions(this.deps.own, minimum) < 0;
|
|
13920
|
+
}
|
|
13875
13921
|
// Record the swap and point `current` at `to`, leaving the guard that counts its starts.
|
|
13876
13922
|
select(to) {
|
|
13877
13923
|
const fromRoot = currentRoot() ?? ownPackageRoot();
|
|
@@ -13899,6 +13945,7 @@ var COMPANION_VERSION = pkg.version;
|
|
|
13899
13945
|
// src/supervisor.ts
|
|
13900
13946
|
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
13901
13947
|
var ARCHIVE_PRUNE_MS = 24 * 60 * 6e4;
|
|
13948
|
+
var FORCE_EXIT_MS = 4e3;
|
|
13902
13949
|
var SHUTDOWN_HEARTBEAT_WAIT_MS = 1e3;
|
|
13903
13950
|
function positiveIntEnv(name) {
|
|
13904
13951
|
const raw = process.env[name];
|
|
@@ -14119,14 +14166,7 @@ var CompanionSupervisor = class {
|
|
|
14119
14166
|
autoUpdate: () => this.config.update?.auto === true,
|
|
14120
14167
|
harnesses: () => this.connectedBundledHarnesses(),
|
|
14121
14168
|
isIdle: () => this.idle(),
|
|
14122
|
-
|
|
14123
|
-
const sub = this.deviceSub;
|
|
14124
|
-
if (!sub) return;
|
|
14125
|
-
sub.stop();
|
|
14126
|
-
await sub.finished;
|
|
14127
|
-
},
|
|
14128
|
-
resumeStream: () => void this.deviceSub?.resume(),
|
|
14129
|
-
restart: () => this.restartForUpdate(),
|
|
14169
|
+
restart: () => this.restartBounded(),
|
|
14130
14170
|
log: this.log
|
|
14131
14171
|
});
|
|
14132
14172
|
this.updater.start();
|
|
@@ -15248,8 +15288,22 @@ var CompanionSupervisor = class {
|
|
|
15248
15288
|
);
|
|
15249
15289
|
}
|
|
15250
15290
|
}
|
|
15291
|
+
// The update's restart, held to the same `FORCE_EXIT_MS` a signal's stop gets. Exit 0: the
|
|
15292
|
+
// swap is recorded and `current` names the new version, so the service starts it either way.
|
|
15293
|
+
async restartBounded() {
|
|
15294
|
+
const force = setTimeout(() => {
|
|
15295
|
+
this.log.warn({ ms: FORCE_EXIT_MS }, "Restarting for the update: shutdown took too long");
|
|
15296
|
+
this.exitFn(0);
|
|
15297
|
+
}, FORCE_EXIT_MS);
|
|
15298
|
+
force.unref?.();
|
|
15299
|
+
try {
|
|
15300
|
+
await this.restartForUpdate();
|
|
15301
|
+
} finally {
|
|
15302
|
+
clearTimeout(force);
|
|
15303
|
+
}
|
|
15304
|
+
}
|
|
15251
15305
|
// Nothing running and nothing about to: no dispatch claimed, no turn chained, no recovery
|
|
15252
|
-
// in progress. What a self-update swap waits for
|
|
15306
|
+
// in progress. What a self-update swap waits for, up to `SWAP_WAIT_MS`.
|
|
15253
15307
|
idle() {
|
|
15254
15308
|
if (this.claims.size > 0 || this.recovering) return false;
|
|
15255
15309
|
for (const wr of this.workspaces.values()) if (wr.chains.size > 0) return false;
|
|
@@ -15271,7 +15325,7 @@ var CompanionSupervisor = class {
|
|
|
15271
15325
|
if (auto) this.kickHeartbeat();
|
|
15272
15326
|
return {
|
|
15273
15327
|
ok: true,
|
|
15274
|
-
message: auto ? "Cabane will keep itself up to date. Updates install between turns and roll back if they fail." : "Cabane won't update itself. Run cabane update to update."
|
|
15328
|
+
message: auto ? "Cabane will keep itself up to date. Updates install between turns when they can, and roll back if they fail." : "Cabane won't update itself. Run cabane update to update."
|
|
15275
15329
|
};
|
|
15276
15330
|
}
|
|
15277
15331
|
if (!this.updater) return { ok: false, message: "This computer isn't paired yet." };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.159",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The Cabane Companion (headless): connect an agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.",
|
|
6
6
|
"license": "UNLICENSED",
|