@cabane/companion 0.6.38 → 0.6.40

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
@@ -2676,8 +2676,18 @@ var DeviceApi = class {
2676
2676
  getAssignments() {
2677
2677
  return this.request("GET", "/api/companion/assignments");
2678
2678
  }
2679
- beginDrain() {
2680
- return this.request("POST", "/api/companion/drain", {});
2679
+ // CT1146: report that a dispatch addressed to THIS device can't be run, because
2680
+ // the agent isn't one this device runs (and still wasn't after a forced
2681
+ // assignments refresh). The server clears the Working flag, retires the dispatch
2682
+ // and posts an explanatory system message.
2683
+ //
2684
+ // This rides the DEVICE token deliberately: the agent's own credential is
2685
+ // delivered through the device's assignments, so an agent this device doesn't
2686
+ // roster is exactly an agent it holds no credential for. The device token is the
2687
+ // only thing it can present, which is why this lives on `DeviceApi` rather than
2688
+ // the per-agent `CabaneApi`.
2689
+ reportDispatchDrop(body) {
2690
+ return this.request("POST", "/api/companion/dispatch-drop", body);
2681
2691
  }
2682
2692
  // Per-device liveness ping. Reports the companion build version and the env-var
2683
2693
  // names the operator's secret store exposes (never values), so CT30's UI can
@@ -3458,18 +3468,24 @@ function summarizeToolResult(content) {
3458
3468
  }
3459
3469
  if (Array.isArray(content)) {
3460
3470
  const parts = [];
3471
+ let sawContentEntry = false;
3461
3472
  for (const raw of content) {
3462
3473
  if (typeof raw === "string") {
3474
+ sawContentEntry = true;
3463
3475
  parts.push(raw);
3464
3476
  } else if (raw && typeof raw === "object") {
3465
3477
  const block = raw;
3478
+ if (typeof block.type === "string") sawContentEntry = true;
3466
3479
  if (block.type === "text" && typeof block.text === "string") parts.push(block.text);
3467
3480
  }
3468
3481
  }
3469
3482
  const joined = parts.join("\n").trim();
3470
- return joined.length > 0 ? truncate(joined, TOOL_IO_MAX_CHARS) : "";
3483
+ if (joined.length > 0) return truncate(joined, TOOL_IO_MAX_CHARS);
3484
+ if (sawContentEntry || content.length === 0) return "";
3471
3485
  }
3472
- return "";
3486
+ if (typeof content !== "object") return "";
3487
+ if (!Array.isArray(content) && Object.keys(content).length === 0) return "";
3488
+ return summarizeToolIo(content);
3473
3489
  }
3474
3490
  function truncate(s, max) {
3475
3491
  if (s.length <= max) return s;
@@ -8999,8 +9015,6 @@ var CompanionSupervisor = class {
8999
9015
  pollTimer = null;
9000
9016
  refreshing = false;
9001
9017
  stopped = false;
9002
- draining = false;
9003
- restartPending = false;
9004
9018
  // CT484: latch so the companion/server version-skew warning is logged once, not
9005
9019
  // on every 30s heartbeat.
9006
9020
  versionSkewWarned = false;
@@ -9084,7 +9098,7 @@ var CompanionSupervisor = class {
9084
9098
  }
9085
9099
  // ---- device-level loops ----
9086
9100
  kickHeartbeat() {
9087
- if (this.draining || this.inFlightHeartbeat) return;
9101
+ if (this.inFlightHeartbeat) return;
9088
9102
  const pending = this.sendHeartbeat();
9089
9103
  this.inFlightHeartbeat = pending;
9090
9104
  void pending.finally(() => {
@@ -9132,8 +9146,7 @@ var CompanionSupervisor = class {
9132
9146
  // actually landed (`harnessSignals` non-null) — an absent field means "we
9133
9147
  // didn't look this beat" and leaves the server's stored suggestion alone,
9134
9148
  // the same fail-soft contract `models` keeps.
9135
- ...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {},
9136
- ...this.restartPending ? { restarting: true } : {}
9149
+ ...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {}
9137
9150
  });
9138
9151
  this.hub.setDevice({ deviceId: res.deviceId });
9139
9152
  this.deviceId = res.deviceId;
@@ -9489,7 +9502,6 @@ var CompanionSupervisor = class {
9489
9502
  if (ev.id) wr.cursor.settle(ev.id);
9490
9503
  return;
9491
9504
  }
9492
- if (this.draining) return;
9493
9505
  const payload = {
9494
9506
  type: "device:dispatch_requested",
9495
9507
  ...wire.payload
@@ -9498,6 +9510,10 @@ var CompanionSupervisor = class {
9498
9510
  if (!agent) {
9499
9511
  agent = await this.recoverRacedAgent(wr, payload);
9500
9512
  if (!agent) {
9513
+ if (this.deviceId && payload.deviceId === this.deviceId) {
9514
+ const reported = await this.reportUnrunnableDispatch(wr, payload);
9515
+ if (!reported) return;
9516
+ }
9501
9517
  if (ev.id) wr.cursor.settle(ev.id);
9502
9518
  return;
9503
9519
  }
@@ -9519,6 +9535,59 @@ var CompanionSupervisor = class {
9519
9535
  await tail;
9520
9536
  if (wr.chains.get(chainKey) === tail) wr.chains.delete(chainKey);
9521
9537
  }
9538
+ // CT1146: tell the server this device cannot run a dispatch it was addressed to.
9539
+ //
9540
+ // Returns whether the report LANDED, because that answer decides whether the
9541
+ // caller may settle the cursor. Nothing else on either side ends this turn: the
9542
+ // server's sweep resolves the agent's effective connector, which in a roster
9543
+ // desync still points at this device, so it is never classified `no_device` and
9544
+ // only the silent 12h age backstop touches it. An unreported drop is therefore
9545
+ // indistinguishable from the original defect.
9546
+ //
9547
+ // Bounded retry, because the common failure is transient (a 503 mid-deploy, a
9548
+ // dropped socket) and one more attempt a second later usually lands. Bounded
9549
+ // hard — this runs inline on the event loop, and an unbounded wait here would
9550
+ // wedge the workspace's whole stream.
9551
+ async reportUnrunnableDispatch(wr, payload) {
9552
+ if (!this.deviceApi) return false;
9553
+ const backoffMs = [0, 1e3, 3e3];
9554
+ let lastErr;
9555
+ for (const wait of backoffMs) {
9556
+ if (this.stopped) return false;
9557
+ if (wait > 0) await new Promise((r) => setTimeout(r, wait));
9558
+ try {
9559
+ const { cleared } = await this.deviceApi.reportDispatchDrop({
9560
+ workspaceId: wr.workspaceId,
9561
+ conversationId: payload.conversationId,
9562
+ agentId: payload.agentId,
9563
+ messageId: payload.messageId,
9564
+ reason: "agent_not_on_device"
9565
+ });
9566
+ this.log.warn(
9567
+ {
9568
+ workspaceId: wr.workspaceId,
9569
+ conversationId: payload.conversationId,
9570
+ agentId: payload.agentId,
9571
+ cleared
9572
+ },
9573
+ "companion: dispatch addressed to this device is for an agent it does not run \u2014 reported as unrunnable"
9574
+ );
9575
+ return true;
9576
+ } catch (err) {
9577
+ lastErr = err;
9578
+ }
9579
+ }
9580
+ this.log.error(
9581
+ {
9582
+ workspaceId: wr.workspaceId,
9583
+ conversationId: payload.conversationId,
9584
+ agentId: payload.agentId,
9585
+ err: lastErr instanceof Error ? lastErr.message : String(lastErr)
9586
+ },
9587
+ "companion: could not report an unrunnable dispatch \u2014 leaving it unsettled so a reconnect replays it"
9588
+ );
9589
+ return false;
9590
+ }
9522
9591
  // Pull-on-miss for the assignment-change race: a dispatch arrived for an agent
9523
9592
  // we don't yet run. If it's addressed to THIS device, the assignment just
9524
9593
  // changed under us (the poll hasn't caught it) — force a fresh assignments pull
@@ -9794,50 +9863,6 @@ var CompanionSupervisor = class {
9794
9863
  ...[...this.workspaces.values()].flatMap((wr) => wr.sub ? [wr.sub.finished] : [])
9795
9864
  ]);
9796
9865
  }
9797
- async drainForRestart(graceMs) {
9798
- this.restartPending = true;
9799
- if (this.inFlightHeartbeat) await this.inFlightHeartbeat;
9800
- if (!this.deviceApi) throw new Error("cannot establish deploy drain before pairing");
9801
- await this.sendHeartbeat();
9802
- const drainStartedAt = Date.now();
9803
- const deadline = Date.now() + Math.max(0, graceMs);
9804
- let timedOut = false;
9805
- while (true) {
9806
- const turns = [...this.workspaces.values()].flatMap((wr) => [...wr.chains.values()]);
9807
- if (turns.length === 0) break;
9808
- const remainingMs = deadline - Date.now();
9809
- if (remainingMs <= 0) {
9810
- timedOut = true;
9811
- break;
9812
- }
9813
- let timer;
9814
- await Promise.race([
9815
- Promise.allSettled(turns),
9816
- new Promise((resolve) => {
9817
- timer = setTimeout(resolve, remainingMs);
9818
- timer.unref?.();
9819
- })
9820
- ]);
9821
- if (timer) clearTimeout(timer);
9822
- }
9823
- this.log.info(
9824
- { waitedMs: Date.now() - drainStartedAt, timedOut },
9825
- timedOut ? "companion: deploy drain grace elapsed; fencing admission for restart" : "companion: deploy drain reached a quiet point; fencing admission for restart"
9826
- );
9827
- this.draining = true;
9828
- if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
9829
- if (this.pollTimer) clearInterval(this.pollTimer);
9830
- if (this.inFlightHeartbeat) await this.inFlightHeartbeat;
9831
- await this.deviceApi.beginDrain();
9832
- for (const wr of this.workspaces.values()) wr.sub?.stop();
9833
- await Promise.allSettled(
9834
- [...this.workspaces.values()].flatMap(
9835
- (wr) => [...wr.agents.values()].map((agent) => agent.api.drainOutbox())
9836
- )
9837
- );
9838
- await this.shutdown();
9839
- return { drained: !timedOut };
9840
- }
9841
9866
  async requestStop() {
9842
9867
  await this.shutdown();
9843
9868
  this.exitFn(0);
@@ -10069,14 +10094,7 @@ async function createCompanionRuntime(opts = {}) {
10069
10094
  stop: stop2,
10070
10095
  heartbeatNow: () => supervisor.heartbeatNow(),
10071
10096
  harnesses: () => hub.statusJson().harnesses ?? [],
10072
- connectHarness,
10073
- drainForRestart: async (graceMs) => {
10074
- clearRuntimeStateIfOurs(instanceId);
10075
- const result = await supervisor.drainForRestart(graceMs);
10076
- await closeSurfaces(control, dashboard);
10077
- stopped = true;
10078
- return result;
10079
- }
10097
+ connectHarness
10080
10098
  }
10081
10099
  };
10082
10100
  }
@@ -10205,8 +10223,6 @@ function defaultSpawnDetached(args) {
10205
10223
 
10206
10224
  // src/commands/start.ts
10207
10225
  var FORCE_EXIT_MS = 4e3;
10208
- var DEPLOY_REEXEC_EXIT = 75;
10209
- var DEPLOY_GRACE_MS = 6.5 * 60 * 60 * 1e3;
10210
10226
  var MAX_CODES = 3;
10211
10227
  async function start(opts = {}) {
10212
10228
  const interactive = isInteractive();
@@ -10445,31 +10461,6 @@ companion: received ${signal}, shutting down\u2026
10445
10461
  };
10446
10462
  process.on("SIGINT", () => void shutdown("SIGINT"));
10447
10463
  process.on("SIGTERM", () => void shutdown("SIGTERM"));
10448
- process.on("SIGUSR2", () => {
10449
- if (shuttingDown) return;
10450
- shuttingDown = true;
10451
- const configuredGrace = Number.parseInt(
10452
- process.env.CABANE_COMPANION_DEPLOY_GRACE_MS ?? String(DEPLOY_GRACE_MS),
10453
- 10
10454
- );
10455
- const graceMs = Number.isFinite(configuredGrace) ? configuredGrace : DEPLOY_GRACE_MS;
10456
- process.stdout.write(`
10457
- companion: deploy drain requested (${graceMs}ms grace)\u2026
10458
- `);
10459
- void runtime.drainForRestart(graceMs).then(({ drained }) => {
10460
- process.stdout.write(
10461
- drained ? "companion: deploy drain complete; re-execing onto the new dist.\n" : "companion: deploy grace expired; re-execing \u2014 unfinished turns resume after restart.\n"
10462
- );
10463
- resolve();
10464
- process.exit(DEPLOY_REEXEC_EXIT);
10465
- }).catch((err) => {
10466
- process.stderr.write(
10467
- `companion: deploy drain failed: ${err instanceof Error ? err.message : String(err)}
10468
- `
10469
- );
10470
- process.exit(DEPLOY_REEXEC_EXIT);
10471
- });
10472
- });
10473
10464
  });
10474
10465
  }
10475
10466
 
package/dist/runtime.js CHANGED
@@ -2104,8 +2104,18 @@ var DeviceApi = class {
2104
2104
  getAssignments() {
2105
2105
  return this.request("GET", "/api/companion/assignments");
2106
2106
  }
2107
- beginDrain() {
2108
- return this.request("POST", "/api/companion/drain", {});
2107
+ // CT1146: report that a dispatch addressed to THIS device can't be run, because
2108
+ // the agent isn't one this device runs (and still wasn't after a forced
2109
+ // assignments refresh). The server clears the Working flag, retires the dispatch
2110
+ // and posts an explanatory system message.
2111
+ //
2112
+ // This rides the DEVICE token deliberately: the agent's own credential is
2113
+ // delivered through the device's assignments, so an agent this device doesn't
2114
+ // roster is exactly an agent it holds no credential for. The device token is the
2115
+ // only thing it can present, which is why this lives on `DeviceApi` rather than
2116
+ // the per-agent `CabaneApi`.
2117
+ reportDispatchDrop(body) {
2118
+ return this.request("POST", "/api/companion/dispatch-drop", body);
2109
2119
  }
2110
2120
  // Per-device liveness ping. Reports the companion build version and the env-var
2111
2121
  // names the operator's secret store exposes (never values), so CT30's UI can
@@ -2965,18 +2975,24 @@ function summarizeToolResult(content) {
2965
2975
  }
2966
2976
  if (Array.isArray(content)) {
2967
2977
  const parts = [];
2978
+ let sawContentEntry = false;
2968
2979
  for (const raw of content) {
2969
2980
  if (typeof raw === "string") {
2981
+ sawContentEntry = true;
2970
2982
  parts.push(raw);
2971
2983
  } else if (raw && typeof raw === "object") {
2972
2984
  const block = raw;
2985
+ if (typeof block.type === "string") sawContentEntry = true;
2973
2986
  if (block.type === "text" && typeof block.text === "string") parts.push(block.text);
2974
2987
  }
2975
2988
  }
2976
2989
  const joined = parts.join("\n").trim();
2977
- return joined.length > 0 ? truncate(joined, TOOL_IO_MAX_CHARS) : "";
2990
+ if (joined.length > 0) return truncate(joined, TOOL_IO_MAX_CHARS);
2991
+ if (sawContentEntry || content.length === 0) return "";
2978
2992
  }
2979
- return "";
2993
+ if (typeof content !== "object") return "";
2994
+ if (!Array.isArray(content) && Object.keys(content).length === 0) return "";
2995
+ return summarizeToolIo(content);
2980
2996
  }
2981
2997
  function truncate(s, max) {
2982
2998
  if (s.length <= max) return s;
@@ -8506,8 +8522,6 @@ var CompanionSupervisor = class {
8506
8522
  pollTimer = null;
8507
8523
  refreshing = false;
8508
8524
  stopped = false;
8509
- draining = false;
8510
- restartPending = false;
8511
8525
  // CT484: latch so the companion/server version-skew warning is logged once, not
8512
8526
  // on every 30s heartbeat.
8513
8527
  versionSkewWarned = false;
@@ -8591,7 +8605,7 @@ var CompanionSupervisor = class {
8591
8605
  }
8592
8606
  // ---- device-level loops ----
8593
8607
  kickHeartbeat() {
8594
- if (this.draining || this.inFlightHeartbeat) return;
8608
+ if (this.inFlightHeartbeat) return;
8595
8609
  const pending = this.sendHeartbeat();
8596
8610
  this.inFlightHeartbeat = pending;
8597
8611
  void pending.finally(() => {
@@ -8639,8 +8653,7 @@ var CompanionSupervisor = class {
8639
8653
  // actually landed (`harnessSignals` non-null) — an absent field means "we
8640
8654
  // didn't look this beat" and leaves the server's stored suggestion alone,
8641
8655
  // the same fail-soft contract `models` keeps.
8642
- ...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {},
8643
- ...this.restartPending ? { restarting: true } : {}
8656
+ ...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {}
8644
8657
  });
8645
8658
  this.hub.setDevice({ deviceId: res.deviceId });
8646
8659
  this.deviceId = res.deviceId;
@@ -8996,7 +9009,6 @@ var CompanionSupervisor = class {
8996
9009
  if (ev.id) wr.cursor.settle(ev.id);
8997
9010
  return;
8998
9011
  }
8999
- if (this.draining) return;
9000
9012
  const payload = {
9001
9013
  type: "device:dispatch_requested",
9002
9014
  ...wire.payload
@@ -9005,6 +9017,10 @@ var CompanionSupervisor = class {
9005
9017
  if (!agent) {
9006
9018
  agent = await this.recoverRacedAgent(wr, payload);
9007
9019
  if (!agent) {
9020
+ if (this.deviceId && payload.deviceId === this.deviceId) {
9021
+ const reported = await this.reportUnrunnableDispatch(wr, payload);
9022
+ if (!reported) return;
9023
+ }
9008
9024
  if (ev.id) wr.cursor.settle(ev.id);
9009
9025
  return;
9010
9026
  }
@@ -9026,6 +9042,59 @@ var CompanionSupervisor = class {
9026
9042
  await tail;
9027
9043
  if (wr.chains.get(chainKey) === tail) wr.chains.delete(chainKey);
9028
9044
  }
9045
+ // CT1146: tell the server this device cannot run a dispatch it was addressed to.
9046
+ //
9047
+ // Returns whether the report LANDED, because that answer decides whether the
9048
+ // caller may settle the cursor. Nothing else on either side ends this turn: the
9049
+ // server's sweep resolves the agent's effective connector, which in a roster
9050
+ // desync still points at this device, so it is never classified `no_device` and
9051
+ // only the silent 12h age backstop touches it. An unreported drop is therefore
9052
+ // indistinguishable from the original defect.
9053
+ //
9054
+ // Bounded retry, because the common failure is transient (a 503 mid-deploy, a
9055
+ // dropped socket) and one more attempt a second later usually lands. Bounded
9056
+ // hard — this runs inline on the event loop, and an unbounded wait here would
9057
+ // wedge the workspace's whole stream.
9058
+ async reportUnrunnableDispatch(wr, payload) {
9059
+ if (!this.deviceApi) return false;
9060
+ const backoffMs = [0, 1e3, 3e3];
9061
+ let lastErr;
9062
+ for (const wait of backoffMs) {
9063
+ if (this.stopped) return false;
9064
+ if (wait > 0) await new Promise((r) => setTimeout(r, wait));
9065
+ try {
9066
+ const { cleared } = await this.deviceApi.reportDispatchDrop({
9067
+ workspaceId: wr.workspaceId,
9068
+ conversationId: payload.conversationId,
9069
+ agentId: payload.agentId,
9070
+ messageId: payload.messageId,
9071
+ reason: "agent_not_on_device"
9072
+ });
9073
+ this.log.warn(
9074
+ {
9075
+ workspaceId: wr.workspaceId,
9076
+ conversationId: payload.conversationId,
9077
+ agentId: payload.agentId,
9078
+ cleared
9079
+ },
9080
+ "companion: dispatch addressed to this device is for an agent it does not run \u2014 reported as unrunnable"
9081
+ );
9082
+ return true;
9083
+ } catch (err) {
9084
+ lastErr = err;
9085
+ }
9086
+ }
9087
+ this.log.error(
9088
+ {
9089
+ workspaceId: wr.workspaceId,
9090
+ conversationId: payload.conversationId,
9091
+ agentId: payload.agentId,
9092
+ err: lastErr instanceof Error ? lastErr.message : String(lastErr)
9093
+ },
9094
+ "companion: could not report an unrunnable dispatch \u2014 leaving it unsettled so a reconnect replays it"
9095
+ );
9096
+ return false;
9097
+ }
9029
9098
  // Pull-on-miss for the assignment-change race: a dispatch arrived for an agent
9030
9099
  // we don't yet run. If it's addressed to THIS device, the assignment just
9031
9100
  // changed under us (the poll hasn't caught it) — force a fresh assignments pull
@@ -9301,50 +9370,6 @@ var CompanionSupervisor = class {
9301
9370
  ...[...this.workspaces.values()].flatMap((wr) => wr.sub ? [wr.sub.finished] : [])
9302
9371
  ]);
9303
9372
  }
9304
- async drainForRestart(graceMs) {
9305
- this.restartPending = true;
9306
- if (this.inFlightHeartbeat) await this.inFlightHeartbeat;
9307
- if (!this.deviceApi) throw new Error("cannot establish deploy drain before pairing");
9308
- await this.sendHeartbeat();
9309
- const drainStartedAt = Date.now();
9310
- const deadline = Date.now() + Math.max(0, graceMs);
9311
- let timedOut = false;
9312
- while (true) {
9313
- const turns = [...this.workspaces.values()].flatMap((wr) => [...wr.chains.values()]);
9314
- if (turns.length === 0) break;
9315
- const remainingMs = deadline - Date.now();
9316
- if (remainingMs <= 0) {
9317
- timedOut = true;
9318
- break;
9319
- }
9320
- let timer;
9321
- await Promise.race([
9322
- Promise.allSettled(turns),
9323
- new Promise((resolve) => {
9324
- timer = setTimeout(resolve, remainingMs);
9325
- timer.unref?.();
9326
- })
9327
- ]);
9328
- if (timer) clearTimeout(timer);
9329
- }
9330
- this.log.info(
9331
- { waitedMs: Date.now() - drainStartedAt, timedOut },
9332
- timedOut ? "companion: deploy drain grace elapsed; fencing admission for restart" : "companion: deploy drain reached a quiet point; fencing admission for restart"
9333
- );
9334
- this.draining = true;
9335
- if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
9336
- if (this.pollTimer) clearInterval(this.pollTimer);
9337
- if (this.inFlightHeartbeat) await this.inFlightHeartbeat;
9338
- await this.deviceApi.beginDrain();
9339
- for (const wr of this.workspaces.values()) wr.sub?.stop();
9340
- await Promise.allSettled(
9341
- [...this.workspaces.values()].flatMap(
9342
- (wr) => [...wr.agents.values()].map((agent) => agent.api.drainOutbox())
9343
- )
9344
- );
9345
- await this.shutdown();
9346
- return { drained: !timedOut };
9347
- }
9348
9373
  async requestStop() {
9349
9374
  await this.shutdown();
9350
9375
  this.exitFn(0);
@@ -9576,14 +9601,7 @@ async function createCompanionRuntime(opts = {}) {
9576
9601
  stop,
9577
9602
  heartbeatNow: () => supervisor.heartbeatNow(),
9578
9603
  harnesses: () => hub.statusJson().harnesses ?? [],
9579
- connectHarness,
9580
- drainForRestart: async (graceMs) => {
9581
- clearRuntimeStateIfOurs(instanceId);
9582
- const result = await supervisor.drainForRestart(graceMs);
9583
- await closeSurfaces(control, dashboard);
9584
- stopped = true;
9585
- return result;
9586
- }
9604
+ connectHarness
9587
9605
  }
9588
9606
  };
9589
9607
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.38",
3
+ "version": "0.6.40",
4
4
  "type": "module",
5
5
  "description": "The Cabane Companion (headless): connect a coding 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",