@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/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 compactions = [];
4307
- let lastRequestContextTokens;
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
- const initModel = msg.model;
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
- const compaction = readCompaction(msg);
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
- const failure = classifyPreInitFrame(msg, signals);
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
- const reqContext = readRequestContextTokens(msg);
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 = readSdkUsage(msg);
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
- const resultText = msg.result ?? "";
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 = mcpInventory.initReceived ? classifyErrorText(message2) : classifyFailure(signals, { text: message2 });
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
- const failure = classifyFailure(signals, { text: "" });
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 sleep3(backoff);
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 sleep3(backoff);
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 SWAP_DRAIN_MS = 1e4;
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} once the turns running now finish.`;
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 once nothing is running. Called on each heartbeat until it can.
13847
- async trySwap() {
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
- pauseStream: async () => {
14123
- const sub = this.deviceSub;
14124
- if (!sub) return;
14125
- sub.stop();
14126
- await sub.finished;
14127
- },
14128
- resumeStream: () => void this.deviceSub?.resume(),
14129
- restart: () => this.restartForUpdate(),
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.157",
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",