@cabane/companion 0.6.180 → 0.6.182

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/cli.js CHANGED
@@ -9367,7 +9367,7 @@ function fingerprintSessionState(state) {
9367
9367
  }
9368
9368
  return createHash2("sha256").update(opaqueId).digest("hex").slice(0, 16);
9369
9369
  }
9370
- var PREPARING_TOOL_NAME, RUNTIME_UNAVAILABLE_PREFIX, NO_TURN_TOKEN_REASON, UNEXPECTED_ROLE_REASON, DEFAULT_PREPARING_ROW_DELAY_MS, DEFAULT_AGENT_IDLE_TIMEOUT_MS, DEFAULT_AGENT_TOTAL_TIMEOUT_MS, DEFAULT_LEASE_RENEWAL_MS, TurnConcluded, TurnExecution;
9370
+ var PREPARING_TOOL_NAME, HARNESS_UNAVAILABLE_NOTICE, NO_TURN_TOKEN_REASON, UNEXPECTED_ROLE_REASON, DEFAULT_PREPARING_ROW_DELAY_MS, DEFAULT_AGENT_IDLE_TIMEOUT_MS, DEFAULT_AGENT_TOTAL_TIMEOUT_MS, DEFAULT_LEASE_RENEWAL_MS, TurnConcluded, TurnExecution;
9371
9371
  var init_turn_execution = __esm({
9372
9372
  "src/turn-execution.ts"() {
9373
9373
  "use strict";
@@ -9396,7 +9396,7 @@ var init_turn_execution = __esm({
9396
9396
  init_dispatcher();
9397
9397
  init_sandbox_policy();
9398
9398
  PREPARING_TOOL_NAME = "preparing";
9399
- RUNTIME_UNAVAILABLE_PREFIX = "**This agent's runtime isn't available on this companion.** The model this agent uses needs a runtime this device isn't running, so I can't run this turn here. Details:";
9399
+ HARNESS_UNAVAILABLE_NOTICE = "**This harness isn't available on the companion.** Check the harness on the device, or choose another harness in the agent's settings. Then reply to try again.";
9400
9400
  NO_TURN_TOKEN_REASON = "no_turn_token: the server issued no turn credential for this turn";
9401
9401
  UNEXPECTED_ROLE_REASON = `refused the wake trigger (role "system" isn't dispatchable on this companion) \u2014 the companion is likely running outdated code; refresh it, then re-address the agent`;
9402
9402
  DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
@@ -10082,7 +10082,7 @@ var init_turn_execution = __esm({
10082
10082
  );
10083
10083
  try {
10084
10084
  await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
10085
- body: `${RUNTIME_UNAVAILABLE_PREFIX} ${err.message}`,
10085
+ body: HARNESS_UNAVAILABLE_NOTICE,
10086
10086
  kind: "runtime_notice",
10087
10087
  turnId,
10088
10088
  parentMessageId: payload.messageId,
@@ -12554,11 +12554,24 @@ function parseHarnessRuntime(raw) {
12554
12554
  if (key === "opencode") return "opencode";
12555
12555
  return null;
12556
12556
  }
12557
- function advertisedRuntimes(detection) {
12558
- return usableRuntimes(detection);
12557
+ function stillRunnable(agent) {
12558
+ return agent.state === "usable" || agent.state === "signed_out" || agent.state === "problem" && agent.check.status === "check_failed";
12559
+ }
12560
+ function rememberUsed(used, detection) {
12561
+ const next = /* @__PURE__ */ new Set();
12562
+ for (const runtime of ["claude-code", "codex"]) {
12563
+ const agent = detection[runtime];
12564
+ if (agent.state === "usable" || used.has(runtime) && stillRunnable(agent)) next.add(runtime);
12565
+ }
12566
+ return next;
12567
+ }
12568
+ function advertisedRuntimes(detection, used) {
12569
+ return HARNESS_RUNTIMES.filter(
12570
+ (runtime) => detection[runtime].state === "usable" || used.has(runtime) && stillRunnable(detection[runtime])
12571
+ );
12559
12572
  }
12560
12573
  function deriveHarnessSnapshot(detection) {
12561
- const advertised = new Set(advertisedRuntimes(detection));
12574
+ const advertised = new Set(usableRuntimes(detection));
12562
12575
  const harnesses = HARNESS_RUNTIMES.map(
12563
12576
  (runtime) => derive(runtime, detection[runtime], advertised.has(runtime))
12564
12577
  );
@@ -13533,7 +13546,7 @@ function turnFailureCopy(reason, runtime) {
13533
13546
  if (reason?.startsWith("runtime_incomplete:"))
13534
13547
  return `The agent installation is incomplete. ${repairAction()} Then restart the companion.`;
13535
13548
  if (/^(runtime|codex|opencode)_unavailable:/.test(reason ?? ""))
13536
- return "The harness is off on this device. Enable it here, or choose another harness or device in Cabane.";
13549
+ return "This harness isn't available on the companion. Check the harness on this device, or choose another harness in the agent's settings in Cabane. Then reply in Cabane to try again.";
13537
13550
  if (reason?.startsWith("model_unavailable:"))
13538
13551
  return "The selected model is unavailable. Choose a different model in the agent settings in Cabane.";
13539
13552
  if (reason?.startsWith("prepare_failed:"))
@@ -16163,7 +16176,13 @@ var CompanionSupervisor = class {
16163
16176
  // control socket, so none can disagree with what is advertised. Null until
16164
16177
  // the first detection; a heartbeat spawns nothing and reads this.
16165
16178
  detection = null;
16179
+ // The bundled harnesses a detection in this run found usable, which stay offered
16180
+ // through a lapsed sign-in (`rememberUsed`). Empty until the first detection.
16181
+ used = /* @__PURE__ */ new Set();
16166
16182
  detecting = null;
16183
+ // Liveness may be acknowledged before discovery. Execution waits on this
16184
+ // first look only, never on a periodic refresh of an established inventory.
16185
+ initialHarnessReady = null;
16167
16186
  detectTimer = null;
16168
16187
  runHarnessCheck;
16169
16188
  probeOpencode;
@@ -16315,8 +16334,17 @@ var CompanionSupervisor = class {
16315
16334
  this.trackHeartbeat(this.sendHeartbeat(true));
16316
16335
  }
16317
16336
  const firstHeartbeat = this.inFlightHeartbeat;
16318
- const startupHarnessRefresh = this.detect();
16319
- void startupHarnessRefresh.then(async () => {
16337
+ this.initialHarnessReady = this.detect().then(
16338
+ () => {
16339
+ },
16340
+ (err) => {
16341
+ this.log.warn(
16342
+ { err: err instanceof Error ? err.message : String(err) },
16343
+ "Couldn't check harnesses at startup. Turns may fail until the next check."
16344
+ );
16345
+ }
16346
+ );
16347
+ void this.initialHarnessReady.then(async () => {
16320
16348
  await firstHeartbeat;
16321
16349
  if (this.stopped) return;
16322
16350
  await this.heartbeatNow();
@@ -16395,8 +16423,7 @@ var CompanionSupervisor = class {
16395
16423
  async sendHeartbeat(first = false) {
16396
16424
  if (!this.deviceApi) return;
16397
16425
  try {
16398
- if (!first && this.detection && advertisedRuntimes(this.detection).length === 0)
16399
- await this.detect();
16426
+ if (!first && this.detection && this.advertised().length === 0) await this.detect();
16400
16427
  const store = loadSecretStoreTolerant(
16401
16428
  (m) => this.log.warn({ err: m }, "Could not read the secrets file. Check ~/.cabane/secrets.json.")
16402
16429
  );
@@ -16730,11 +16757,15 @@ var CompanionSupervisor = class {
16730
16757
  bundledResolution(runtime) {
16731
16758
  return this.resolveBundled(runtime);
16732
16759
  }
16733
- // Does this device OFFER a runtime — found usable by the last detection? The
16734
- // ONE signal the heartbeat manifest, the adapter registry and the dispatcher
16735
- // read, so what the device advertises and what it can select can't disagree.
16760
+ // Does this device OFFER a runtime — usable now, or used in this run and still
16761
+ // runnable? The ONE signal the heartbeat manifest, the adapter registry and the
16762
+ // dispatcher read, so what the device advertises and what it can select can't
16763
+ // disagree.
16736
16764
  offered(runtime) {
16737
- return this.detection !== null && advertisedRuntimes(this.detection).includes(runtime);
16765
+ return this.advertised().includes(runtime);
16766
+ }
16767
+ advertised() {
16768
+ return this.detection ? advertisedRuntimes(this.detection, this.used) : [];
16738
16769
  }
16739
16770
  // The address an OpenCode turn runs against: the serve detection found, else
16740
16771
  // the configured one, for the model list a beat still asks for.
@@ -17173,6 +17204,8 @@ var CompanionSupervisor = class {
17173
17204
  return;
17174
17205
  this.recovering = true;
17175
17206
  try {
17207
+ await this.initialHarnessReady;
17208
+ if (this.stopped) return;
17176
17209
  const page = await this.deviceApi.getRuns(this.recoveryCursor);
17177
17210
  if (this.stopped) return;
17178
17211
  let assignmentsMissing = false;
@@ -17224,6 +17257,8 @@ var CompanionSupervisor = class {
17224
17257
  }
17225
17258
  // One turn: the skip-vs-resume dedupe, the dispatch, then the console summary.
17226
17259
  async runDispatch(wr, dispatchId, payload, agent, recoveredTurnId) {
17260
+ await this.initialHarnessReady;
17261
+ if (this.stopped) return;
17227
17262
  const workspaceId = wr.workspaceId;
17228
17263
  if (dispatchId && hasDispatched(workspaceId, dispatchId)) {
17229
17264
  if (hasCompleted(workspaceId, dispatchId)) {
@@ -17383,19 +17418,20 @@ var CompanionSupervisor = class {
17383
17418
  async detect() {
17384
17419
  if (this.detecting) return this.detecting;
17385
17420
  this.detecting = (async () => {
17386
- const previous = this.detection;
17421
+ const previousOpencodeUrl = this.opencodeUrl();
17387
17422
  const detection = await detectHarnesses(this.config, {
17388
17423
  check: this.runHarnessCheck,
17389
17424
  ...this.probeOpencode ? { probeOpencode: this.probeOpencode } : {}
17390
17425
  });
17391
17426
  this.detection = detection;
17427
+ this.used = rememberUsed(this.used, detection);
17392
17428
  this.logBundledTransitions();
17393
17429
  this.harnessVersions = {
17394
17430
  claudeCode: detectedVersion(detection["claude-code"]),
17395
17431
  opencode: detectedVersion(detection.opencode),
17396
17432
  codex: detectedVersion(detection.codex)
17397
17433
  };
17398
- if (previous && opencodeAddress(previous) !== opencodeAddress(detection)) {
17434
+ if (previousOpencodeUrl !== this.opencodeUrl()) {
17399
17435
  this.rebuildDispatchers();
17400
17436
  }
17401
17437
  this.hub.setHarnesses(deriveHarnessSnapshot(detection).harnesses);
@@ -17443,7 +17479,7 @@ var CompanionSupervisor = class {
17443
17479
  const health = this.connectorHealth.reports();
17444
17480
  if (health.length === 0 || !this.detection) return health;
17445
17481
  const named = new Set(health.map((r) => r.runtime));
17446
- const bare = advertisedRuntimes(this.detection).filter((runtime) => !named.has(runtime)).map((runtime) => ({ runtime }));
17482
+ const bare = this.advertised().filter((runtime) => !named.has(runtime)).map((runtime) => ({ runtime }));
17447
17483
  return [...health, ...bare];
17448
17484
  }
17449
17485
  // Beat NOW and wait for it to land. `start` calls this before asking the
@@ -17587,10 +17623,6 @@ function defaultReexec() {
17587
17623
  }
17588
17624
  });
17589
17625
  }
17590
- function opencodeAddress(detection) {
17591
- const found = detection.opencode;
17592
- return found.state === "usable" ? found.url ?? DEFAULT_OPENCODE_SERVER_URL : null;
17593
- }
17594
17626
 
17595
17627
  // src/runtime.ts
17596
17628
  init_version2();
package/dist/runtime.js CHANGED
@@ -1893,7 +1893,7 @@ function turnFailureCopy(reason, runtime) {
1893
1893
  if (reason?.startsWith("runtime_incomplete:"))
1894
1894
  return `The agent installation is incomplete. ${repairAction()} Then restart the companion.`;
1895
1895
  if (/^(runtime|codex|opencode)_unavailable:/.test(reason ?? ""))
1896
- return "The harness is off on this device. Enable it here, or choose another harness or device in Cabane.";
1896
+ return "This harness isn't available on the companion. Check the harness on this device, or choose another harness in the agent's settings in Cabane. Then reply in Cabane to try again.";
1897
1897
  if (reason?.startsWith("model_unavailable:"))
1898
1898
  return "The selected model is unavailable. Choose a different model in the agent settings in Cabane.";
1899
1899
  if (reason?.startsWith("prepare_failed:"))
@@ -9850,7 +9850,7 @@ async function absorbBundledLoss(ctx, aborted, outcome) {
9850
9850
 
9851
9851
  // src/turn-execution.ts
9852
9852
  var PREPARING_TOOL_NAME = "preparing";
9853
- var RUNTIME_UNAVAILABLE_PREFIX = "**This agent's runtime isn't available on this companion.** The model this agent uses needs a runtime this device isn't running, so I can't run this turn here. Details:";
9853
+ var HARNESS_UNAVAILABLE_NOTICE = "**This harness isn't available on the companion.** Check the harness on the device, or choose another harness in the agent's settings. Then reply to try again.";
9854
9854
  var NO_TURN_TOKEN_REASON = "no_turn_token: the server issued no turn credential for this turn";
9855
9855
  var UNEXPECTED_ROLE_REASON = `refused the wake trigger (role "system" isn't dispatchable on this companion) \u2014 the companion is likely running outdated code; refresh it, then re-address the agent`;
9856
9856
  var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
@@ -10561,7 +10561,7 @@ var TurnExecution = class {
10561
10561
  );
10562
10562
  try {
10563
10563
  await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
10564
- body: `${RUNTIME_UNAVAILABLE_PREFIX} ${err.message}`,
10564
+ body: HARNESS_UNAVAILABLE_NOTICE,
10565
10565
  kind: "runtime_notice",
10566
10566
  turnId,
10567
10567
  parentMessageId: payload.messageId,
@@ -13042,11 +13042,24 @@ function withTimeout(promise, fallback, timeoutMs) {
13042
13042
  }
13043
13043
 
13044
13044
  // src/harness-status.ts
13045
- function advertisedRuntimes(detection) {
13046
- return usableRuntimes(detection);
13045
+ function stillRunnable(agent) {
13046
+ return agent.state === "usable" || agent.state === "signed_out" || agent.state === "problem" && agent.check.status === "check_failed";
13047
+ }
13048
+ function rememberUsed(used, detection) {
13049
+ const next = /* @__PURE__ */ new Set();
13050
+ for (const runtime of ["claude-code", "codex"]) {
13051
+ const agent = detection[runtime];
13052
+ if (agent.state === "usable" || used.has(runtime) && stillRunnable(agent)) next.add(runtime);
13053
+ }
13054
+ return next;
13055
+ }
13056
+ function advertisedRuntimes(detection, used) {
13057
+ return HARNESS_RUNTIMES.filter(
13058
+ (runtime) => detection[runtime].state === "usable" || used.has(runtime) && stillRunnable(detection[runtime])
13059
+ );
13047
13060
  }
13048
13061
  function deriveHarnessSnapshot(detection) {
13049
- const advertised = new Set(advertisedRuntimes(detection));
13062
+ const advertised = new Set(usableRuntimes(detection));
13050
13063
  const harnesses = HARNESS_RUNTIMES.map(
13051
13064
  (runtime) => derive(runtime, detection[runtime], advertised.has(runtime))
13052
13065
  );
@@ -14095,7 +14108,13 @@ var CompanionSupervisor = class {
14095
14108
  // control socket, so none can disagree with what is advertised. Null until
14096
14109
  // the first detection; a heartbeat spawns nothing and reads this.
14097
14110
  detection = null;
14111
+ // The bundled harnesses a detection in this run found usable, which stay offered
14112
+ // through a lapsed sign-in (`rememberUsed`). Empty until the first detection.
14113
+ used = /* @__PURE__ */ new Set();
14098
14114
  detecting = null;
14115
+ // Liveness may be acknowledged before discovery. Execution waits on this
14116
+ // first look only, never on a periodic refresh of an established inventory.
14117
+ initialHarnessReady = null;
14099
14118
  detectTimer = null;
14100
14119
  runHarnessCheck;
14101
14120
  probeOpencode;
@@ -14247,8 +14266,17 @@ var CompanionSupervisor = class {
14247
14266
  this.trackHeartbeat(this.sendHeartbeat(true));
14248
14267
  }
14249
14268
  const firstHeartbeat = this.inFlightHeartbeat;
14250
- const startupHarnessRefresh = this.detect();
14251
- void startupHarnessRefresh.then(async () => {
14269
+ this.initialHarnessReady = this.detect().then(
14270
+ () => {
14271
+ },
14272
+ (err) => {
14273
+ this.log.warn(
14274
+ { err: err instanceof Error ? err.message : String(err) },
14275
+ "Couldn't check harnesses at startup. Turns may fail until the next check."
14276
+ );
14277
+ }
14278
+ );
14279
+ void this.initialHarnessReady.then(async () => {
14252
14280
  await firstHeartbeat;
14253
14281
  if (this.stopped) return;
14254
14282
  await this.heartbeatNow();
@@ -14327,8 +14355,7 @@ var CompanionSupervisor = class {
14327
14355
  async sendHeartbeat(first = false) {
14328
14356
  if (!this.deviceApi) return;
14329
14357
  try {
14330
- if (!first && this.detection && advertisedRuntimes(this.detection).length === 0)
14331
- await this.detect();
14358
+ if (!first && this.detection && this.advertised().length === 0) await this.detect();
14332
14359
  const store = loadSecretStoreTolerant(
14333
14360
  (m) => this.log.warn({ err: m }, "Could not read the secrets file. Check ~/.cabane/secrets.json.")
14334
14361
  );
@@ -14662,11 +14689,15 @@ var CompanionSupervisor = class {
14662
14689
  bundledResolution(runtime) {
14663
14690
  return this.resolveBundled(runtime);
14664
14691
  }
14665
- // Does this device OFFER a runtime — found usable by the last detection? The
14666
- // ONE signal the heartbeat manifest, the adapter registry and the dispatcher
14667
- // read, so what the device advertises and what it can select can't disagree.
14692
+ // Does this device OFFER a runtime — usable now, or used in this run and still
14693
+ // runnable? The ONE signal the heartbeat manifest, the adapter registry and the
14694
+ // dispatcher read, so what the device advertises and what it can select can't
14695
+ // disagree.
14668
14696
  offered(runtime) {
14669
- return this.detection !== null && advertisedRuntimes(this.detection).includes(runtime);
14697
+ return this.advertised().includes(runtime);
14698
+ }
14699
+ advertised() {
14700
+ return this.detection ? advertisedRuntimes(this.detection, this.used) : [];
14670
14701
  }
14671
14702
  // The address an OpenCode turn runs against: the serve detection found, else
14672
14703
  // the configured one, for the model list a beat still asks for.
@@ -15105,6 +15136,8 @@ var CompanionSupervisor = class {
15105
15136
  return;
15106
15137
  this.recovering = true;
15107
15138
  try {
15139
+ await this.initialHarnessReady;
15140
+ if (this.stopped) return;
15108
15141
  const page = await this.deviceApi.getRuns(this.recoveryCursor);
15109
15142
  if (this.stopped) return;
15110
15143
  let assignmentsMissing = false;
@@ -15156,6 +15189,8 @@ var CompanionSupervisor = class {
15156
15189
  }
15157
15190
  // One turn: the skip-vs-resume dedupe, the dispatch, then the console summary.
15158
15191
  async runDispatch(wr, dispatchId, payload, agent, recoveredTurnId) {
15192
+ await this.initialHarnessReady;
15193
+ if (this.stopped) return;
15159
15194
  const workspaceId = wr.workspaceId;
15160
15195
  if (dispatchId && hasDispatched(workspaceId, dispatchId)) {
15161
15196
  if (hasCompleted(workspaceId, dispatchId)) {
@@ -15315,19 +15350,20 @@ var CompanionSupervisor = class {
15315
15350
  async detect() {
15316
15351
  if (this.detecting) return this.detecting;
15317
15352
  this.detecting = (async () => {
15318
- const previous = this.detection;
15353
+ const previousOpencodeUrl = this.opencodeUrl();
15319
15354
  const detection = await detectHarnesses(this.config, {
15320
15355
  check: this.runHarnessCheck,
15321
15356
  ...this.probeOpencode ? { probeOpencode: this.probeOpencode } : {}
15322
15357
  });
15323
15358
  this.detection = detection;
15359
+ this.used = rememberUsed(this.used, detection);
15324
15360
  this.logBundledTransitions();
15325
15361
  this.harnessVersions = {
15326
15362
  claudeCode: detectedVersion(detection["claude-code"]),
15327
15363
  opencode: detectedVersion(detection.opencode),
15328
15364
  codex: detectedVersion(detection.codex)
15329
15365
  };
15330
- if (previous && opencodeAddress(previous) !== opencodeAddress(detection)) {
15366
+ if (previousOpencodeUrl !== this.opencodeUrl()) {
15331
15367
  this.rebuildDispatchers();
15332
15368
  }
15333
15369
  this.hub.setHarnesses(deriveHarnessSnapshot(detection).harnesses);
@@ -15375,7 +15411,7 @@ var CompanionSupervisor = class {
15375
15411
  const health = this.connectorHealth.reports();
15376
15412
  if (health.length === 0 || !this.detection) return health;
15377
15413
  const named = new Set(health.map((r) => r.runtime));
15378
- const bare = advertisedRuntimes(this.detection).filter((runtime) => !named.has(runtime)).map((runtime) => ({ runtime }));
15414
+ const bare = this.advertised().filter((runtime) => !named.has(runtime)).map((runtime) => ({ runtime }));
15379
15415
  return [...health, ...bare];
15380
15416
  }
15381
15417
  // Beat NOW and wait for it to land. `start` calls this before asking the
@@ -15519,10 +15555,6 @@ function defaultReexec() {
15519
15555
  }
15520
15556
  });
15521
15557
  }
15522
- function opencodeAddress(detection) {
15523
- const found = detection.opencode;
15524
- return found.state === "usable" ? found.url ?? DEFAULT_OPENCODE_SERVER_URL : null;
15525
- }
15526
15558
 
15527
15559
  // src/safety.ts
15528
15560
  var RECOVERABLE_CODES = /* @__PURE__ */ new Set([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.180",
3
+ "version": "0.6.182",
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",