@cabane/companion 0.6.95 → 0.6.96

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
@@ -1963,7 +1963,7 @@ function getLogger() {
1963
1963
  }
1964
1964
 
1965
1965
  // src/runtime.ts
1966
- import { randomUUID as randomUUID4 } from "crypto";
1966
+ import { randomUUID as randomUUID5 } from "crypto";
1967
1967
 
1968
1968
  // src/dashboard/server.ts
1969
1969
  import { dirname as dirname5, join as join8 } from "path";
@@ -2524,8 +2524,12 @@ async function warnAboutHarnessReadiness(cfg, deps = {}) {
2524
2524
  warn(`No harness is connected on this device yet, so no agent turn can run here. ${guidance}`);
2525
2525
  }
2526
2526
 
2527
+ // src/boot-id.ts
2528
+ import { randomUUID } from "crypto";
2529
+ var bootId = randomUUID();
2530
+
2527
2531
  // src/supervisor.ts
2528
- import { randomUUID as randomUUID3 } from "crypto";
2532
+ import { randomUUID as randomUUID4 } from "crypto";
2529
2533
 
2530
2534
  // src/api.ts
2531
2535
  var RETRY_BACKOFF_MS = [250, 750];
@@ -3043,9 +3047,10 @@ var DeviceApi = class {
3043
3047
  get base() {
3044
3048
  return this.opts.baseUrl.endsWith("/") ? this.opts.baseUrl.slice(0, -1) : this.opts.baseUrl;
3045
3049
  }
3046
- async request(method, path, body) {
3050
+ async request(method, path, body, signal) {
3047
3051
  const res = await fetch(`${this.base}${path}`, {
3048
3052
  method,
3053
+ ...signal ? { signal } : {},
3049
3054
  headers: {
3050
3055
  Authorization: `Bearer ${this.opts.deviceToken}`,
3051
3056
  ...body !== void 0 ? { "content-type": "application/json" } : {},
@@ -3072,6 +3077,16 @@ var DeviceApi = class {
3072
3077
  getAssignments() {
3073
3078
  return this.request("GET", "/api/companion/assignments");
3074
3079
  }
3080
+ getRuns(cursor) {
3081
+ const query = new URLSearchParams({ limit: "100", order: "asc" });
3082
+ if (cursor) query.set("cursor", cursor);
3083
+ return this.request(
3084
+ "GET",
3085
+ `/api/companion/runs?${query}`,
3086
+ void 0,
3087
+ AbortSignal.timeout(5e3)
3088
+ );
3089
+ }
3075
3090
  // CT1146: report that a dispatch addressed to THIS device can't be run, because
3076
3091
  // the agent isn't one this device runs (and still wasn't after a forced
3077
3092
  // assignments refresh). The server clears the Working flag, retires the dispatch
@@ -7889,11 +7904,11 @@ import { existsSync as existsSync11, readdirSync as readdirSync2, statSync as st
7889
7904
  import { join as join15 } from "path";
7890
7905
 
7891
7906
  // src/turn-execution.ts
7892
- import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
7907
+ import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
7893
7908
  import { existsSync as existsSync10 } from "fs";
7894
7909
 
7895
7910
  // src/turn-control-tools.ts
7896
- import { randomUUID } from "crypto";
7911
+ import { randomUUID as randomUUID2 } from "crypto";
7897
7912
 
7898
7913
  // packages/agent-runtime/src/turn-control/descriptions.ts
7899
7914
  var MAX_ASK_QUESTION_BYTES = 400;
@@ -8045,7 +8060,7 @@ function createTurnControlMcpServer(acts, skipState) {
8045
8060
  }
8046
8061
  return performAct(acts, {
8047
8062
  kind: "ask",
8048
- callId: randomUUID(),
8063
+ callId: randomUUID2(),
8049
8064
  // CT1350: the swap rides the ask, so the server commits both halves
8050
8065
  // in one transaction — the person never sees neither or both.
8051
8066
  ...args.replaces ? { replaces: args.replaces } : {},
@@ -8087,7 +8102,7 @@ function createTurnControlMcpServer(acts, skipState) {
8087
8102
  }
8088
8103
  return performAct(acts, {
8089
8104
  kind: "wake",
8090
- callId: randomUUID(),
8105
+ callId: randomUUID2(),
8091
8106
  wake: {
8092
8107
  ...hasAfter ? { afterSeconds: args.afterSeconds } : {},
8093
8108
  ...hasAt ? { at: args.at } : {},
@@ -8111,7 +8126,7 @@ function createTurnControlMcpServer(acts, skipState) {
8111
8126
  },
8112
8127
  async (args) => performAct(acts, {
8113
8128
  kind: "withdraw_ask",
8114
- callId: randomUUID(),
8129
+ callId: randomUUID2(),
8115
8130
  withdraw: { askId: args.askId, reason: args.reason }
8116
8131
  }),
8117
8132
  { annotations: ACT_TOOL, alwaysLoad: true }
@@ -8122,7 +8137,7 @@ function createTurnControlMcpServer(acts, skipState) {
8122
8137
  CANCEL_WAKE_TOOL,
8123
8138
  TURN_CONTROL_TOOL_DESCRIPTIONS.cancel_wake,
8124
8139
  {},
8125
- async () => performAct(acts, { kind: "cancel_wake", callId: randomUUID() }),
8140
+ async () => performAct(acts, { kind: "cancel_wake", callId: randomUUID2() }),
8126
8141
  { annotations: CANCEL_TOOL, alwaysLoad: true }
8127
8142
  )
8128
8143
  ]
@@ -8755,7 +8770,7 @@ var TurnExecution = class {
8755
8770
  agentId: payload.agentId,
8756
8771
  messageId: payload.messageId
8757
8772
  });
8758
- this.turnId = handleOpts.turnId ?? randomUUID2();
8773
+ this.turnId = handleOpts.turnId ?? randomUUID3();
8759
8774
  }
8760
8775
  opts;
8761
8776
  supervisor;
@@ -8969,7 +8984,7 @@ var TurnExecution = class {
8969
8984
  await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
8970
8985
  body: `${MISSING_SECRET_PREFIX} ${list}`,
8971
8986
  kind: "runtime_notice",
8972
- turnId: randomUUID2(),
8987
+ turnId: randomUUID3(),
8973
8988
  // CT113: even a turn that fails before it runs answers a message.
8974
8989
  parentMessageId: payload.messageId
8975
8990
  });
@@ -9110,6 +9125,7 @@ var TurnExecution = class {
9110
9125
  try {
9111
9126
  await this.opts.api.setActiveRun(workspaceId, payload.conversationId, payload.agentId, {
9112
9127
  activeRunStartedAt: (/* @__PURE__ */ new Date()).toISOString(),
9128
+ bootId,
9113
9129
  // CT1380: the durable record that this turn resumed, and from where.
9114
9130
  ...this.resumedFromSeq !== void 0 ? { resumedFromSeq: this.resumedFromSeq } : {},
9115
9131
  // CT33: hand the server this turn's id so the new-run chokepoint's
@@ -10161,6 +10177,11 @@ var CompanionSupervisor = class {
10161
10177
  unrunnableRetryDelaysMs;
10162
10178
  deviceApi = null;
10163
10179
  deviceSub = null;
10180
+ recoveryChecked = false;
10181
+ recovering = false;
10182
+ heartbeated = false;
10183
+ recoveryCursor;
10184
+ recoveredTurns = /* @__PURE__ */ new Set();
10164
10185
  heartbeatTimer = null;
10165
10186
  inFlightHeartbeat = null;
10166
10187
  pollTimer = null;
@@ -10273,6 +10294,7 @@ var CompanionSupervisor = class {
10273
10294
  const connectorReports = this.connectorHealth.reports();
10274
10295
  const opencodeModels = this.config.opencode?.serverUrl ? await enumerateOpencodeModels(this.config.opencode.serverUrl) : null;
10275
10296
  const res = await this.deviceApi.heartbeat({
10297
+ bootId,
10276
10298
  version: COMPANION_VERSION,
10277
10299
  exposedSecretNames: store.names(),
10278
10300
  // Report each runtime only when this device can actually run it:
@@ -10311,6 +10333,8 @@ var CompanionSupervisor = class {
10311
10333
  this.hub.setDevice({ deviceId: res.deviceId });
10312
10334
  this.deviceId = res.deviceId;
10313
10335
  this.checkVersionSkew(res.serverVersion);
10336
+ this.heartbeated = true;
10337
+ void this.recoverRuns();
10314
10338
  } catch (err) {
10315
10339
  this.log.warn(
10316
10340
  { err: err instanceof Error ? err.message : String(err) },
@@ -10374,6 +10398,7 @@ var CompanionSupervisor = class {
10374
10398
  }
10375
10399
  for (const [wsId, list] of byWorkspace) this.reconcileWorkspace(wsId, list);
10376
10400
  pruneCredentials(items.map((i) => i.agentId));
10401
+ void this.recoverRuns();
10377
10402
  } finally {
10378
10403
  this.refreshing = false;
10379
10404
  }
@@ -10864,10 +10889,65 @@ var CompanionSupervisor = class {
10864
10889
  await this.refreshAssignments();
10865
10890
  return this.workspaces.get(wr.workspaceId)?.agents.get(payload.agentId);
10866
10891
  }
10892
+ // The server may still hold a run whose SSE event no longer replays.
10893
+ // Read one bounded page per refresh. Only a stored session earns a reclaim;
10894
+ // without one, leave the old boot untouched for the server's restart sweep.
10895
+ async recoverRuns() {
10896
+ if (!this.deviceApi || !this.heartbeated || this.recovering || this.recoveryChecked || this.stopped || noResume())
10897
+ return;
10898
+ this.recovering = true;
10899
+ try {
10900
+ const page = await this.deviceApi.getRuns(this.recoveryCursor);
10901
+ if (this.stopped) return;
10902
+ let assignmentsMissing = false;
10903
+ for (const run of page.runs) {
10904
+ if (run.bootId === bootId || !run.hasSession || this.recoveredTurns.has(run.turnId))
10905
+ continue;
10906
+ const wr = this.workspaces.get(run.workspaceId);
10907
+ const agent = wr?.agents.get(run.agentId);
10908
+ if (!wr || !agent) {
10909
+ assignmentsMissing = true;
10910
+ continue;
10911
+ }
10912
+ const key = `${run.conversationId}|${run.agentId}`;
10913
+ if (wr.chains.has(key)) continue;
10914
+ this.recoveredTurns.add(run.turnId);
10915
+ if (bumpResumeAttempt(run.workspaceId, `restart:${run.turnId}`) > MAX_RESUME_ATTEMPTS)
10916
+ continue;
10917
+ const payload = {
10918
+ type: "device:dispatch_requested",
10919
+ deviceId: this.deviceId,
10920
+ agentId: run.agentId,
10921
+ conversationId: run.conversationId,
10922
+ messageId: run.messageId
10923
+ };
10924
+ const tail = this.runDispatch(
10925
+ wr,
10926
+ { id: "", event: "recovery", data: "" },
10927
+ payload,
10928
+ agent,
10929
+ run.turnId
10930
+ ).catch(
10931
+ (err) => this.log.warn({ err, turnId: run.turnId }, "companion: restart recovery failed")
10932
+ ).finally(() => {
10933
+ if (wr.chains.get(key) === tail) wr.chains.delete(key);
10934
+ });
10935
+ wr.chains.set(key, tail);
10936
+ }
10937
+ if (!assignmentsMissing) {
10938
+ this.recoveryCursor = page.nextCursor ?? void 0;
10939
+ this.recoveryChecked = page.nextCursor === null;
10940
+ }
10941
+ } catch (err) {
10942
+ this.log.warn({ err }, "companion: run recovery read failed (will retry)");
10943
+ } finally {
10944
+ this.recovering = false;
10945
+ }
10946
+ }
10867
10947
  // One turn: the SJ516 F7 / CT26 skip-vs-resume dedupe, the dispatch, then
10868
10948
  // cursor + console summary. Mirrors the old per-workspace `handleEvent`, now
10869
10949
  // scoped to one agent.
10870
- async runDispatch(wr, ev, payload, agent) {
10950
+ async runDispatch(wr, ev, payload, agent, recoveredTurnId) {
10871
10951
  const workspaceId = wr.workspaceId;
10872
10952
  if (ev.id && hasDispatched(workspaceId, ev.id)) {
10873
10953
  if (hasCompleted(workspaceId, ev.id)) {
@@ -10902,8 +10982,8 @@ var CompanionSupervisor = class {
10902
10982
  "companion: re-dispatching interrupted turn (resume after restart)"
10903
10983
  );
10904
10984
  }
10905
- const resumedTurnId = ev.id ? turnIdForEvent(workspaceId, ev.id) : null;
10906
- const turnId = resumedTurnId ?? randomUUID3();
10985
+ const resumedTurnId = recoveredTurnId ?? (ev.id ? turnIdForEvent(workspaceId, ev.id) : null);
10986
+ const turnId = resumedTurnId ?? randomUUID4();
10907
10987
  if (ev.id) {
10908
10988
  const liveTurnIds = rememberTurnId(workspaceId, ev.id, turnId);
10909
10989
  if (liveTurnIds > TURN_ID_OVERFLOW_WARN) {
@@ -11326,7 +11406,7 @@ async function createCompanionRuntime(opts = {}) {
11326
11406
  opencodeServerUrl: cfg.opencode?.serverUrl,
11327
11407
  codex: isCodexEnabled(cfg)
11328
11408
  });
11329
- const instanceId = randomUUID4();
11409
+ const instanceId = randomUUID5();
11330
11410
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
11331
11411
  const pre = readLiveRuntimeState();
11332
11412
  if (pre && await verifyRuntime(pre) === "stale") clearRuntimeState();
package/dist/runtime.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/runtime.ts
2
- import { randomUUID as randomUUID4 } from "crypto";
2
+ import { randomUUID as randomUUID5 } from "crypto";
3
3
 
4
4
  // src/config.ts
5
5
  import {
@@ -1942,8 +1942,12 @@ async function verifyRuntime(state, requestImpl = controlRequest) {
1942
1942
  return body.instance_id === state.instanceId ? "ours" : "stale";
1943
1943
  }
1944
1944
 
1945
+ // src/boot-id.ts
1946
+ import { randomUUID } from "crypto";
1947
+ var bootId = randomUUID();
1948
+
1945
1949
  // src/supervisor.ts
1946
- import { randomUUID as randomUUID3 } from "crypto";
1950
+ import { randomUUID as randomUUID4 } from "crypto";
1947
1951
 
1948
1952
  // src/api.ts
1949
1953
  var RETRY_BACKOFF_MS = [250, 750];
@@ -2461,9 +2465,10 @@ var DeviceApi = class {
2461
2465
  get base() {
2462
2466
  return this.opts.baseUrl.endsWith("/") ? this.opts.baseUrl.slice(0, -1) : this.opts.baseUrl;
2463
2467
  }
2464
- async request(method, path, body) {
2468
+ async request(method, path, body, signal) {
2465
2469
  const res = await fetch(`${this.base}${path}`, {
2466
2470
  method,
2471
+ ...signal ? { signal } : {},
2467
2472
  headers: {
2468
2473
  Authorization: `Bearer ${this.opts.deviceToken}`,
2469
2474
  ...body !== void 0 ? { "content-type": "application/json" } : {},
@@ -2490,6 +2495,16 @@ var DeviceApi = class {
2490
2495
  getAssignments() {
2491
2496
  return this.request("GET", "/api/companion/assignments");
2492
2497
  }
2498
+ getRuns(cursor) {
2499
+ const query = new URLSearchParams({ limit: "100", order: "asc" });
2500
+ if (cursor) query.set("cursor", cursor);
2501
+ return this.request(
2502
+ "GET",
2503
+ `/api/companion/runs?${query}`,
2504
+ void 0,
2505
+ AbortSignal.timeout(5e3)
2506
+ );
2507
+ }
2493
2508
  // CT1146: report that a dispatch addressed to THIS device can't be run, because
2494
2509
  // the agent isn't one this device runs (and still wasn't after a forced
2495
2510
  // assignments refresh). The server clears the Working flag, retires the dispatch
@@ -7386,11 +7401,11 @@ import { existsSync as existsSync11, readdirSync as readdirSync2, statSync as st
7386
7401
  import { join as join15 } from "path";
7387
7402
 
7388
7403
  // src/turn-execution.ts
7389
- import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
7404
+ import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
7390
7405
  import { existsSync as existsSync10 } from "fs";
7391
7406
 
7392
7407
  // src/turn-control-tools.ts
7393
- import { randomUUID } from "crypto";
7408
+ import { randomUUID as randomUUID2 } from "crypto";
7394
7409
 
7395
7410
  // packages/agent-runtime/src/turn-control/descriptions.ts
7396
7411
  var MAX_ASK_QUESTION_BYTES = 400;
@@ -7542,7 +7557,7 @@ function createTurnControlMcpServer(acts, skipState) {
7542
7557
  }
7543
7558
  return performAct(acts, {
7544
7559
  kind: "ask",
7545
- callId: randomUUID(),
7560
+ callId: randomUUID2(),
7546
7561
  // CT1350: the swap rides the ask, so the server commits both halves
7547
7562
  // in one transaction — the person never sees neither or both.
7548
7563
  ...args.replaces ? { replaces: args.replaces } : {},
@@ -7584,7 +7599,7 @@ function createTurnControlMcpServer(acts, skipState) {
7584
7599
  }
7585
7600
  return performAct(acts, {
7586
7601
  kind: "wake",
7587
- callId: randomUUID(),
7602
+ callId: randomUUID2(),
7588
7603
  wake: {
7589
7604
  ...hasAfter ? { afterSeconds: args.afterSeconds } : {},
7590
7605
  ...hasAt ? { at: args.at } : {},
@@ -7608,7 +7623,7 @@ function createTurnControlMcpServer(acts, skipState) {
7608
7623
  },
7609
7624
  async (args) => performAct(acts, {
7610
7625
  kind: "withdraw_ask",
7611
- callId: randomUUID(),
7626
+ callId: randomUUID2(),
7612
7627
  withdraw: { askId: args.askId, reason: args.reason }
7613
7628
  }),
7614
7629
  { annotations: ACT_TOOL, alwaysLoad: true }
@@ -7619,7 +7634,7 @@ function createTurnControlMcpServer(acts, skipState) {
7619
7634
  CANCEL_WAKE_TOOL,
7620
7635
  TURN_CONTROL_TOOL_DESCRIPTIONS.cancel_wake,
7621
7636
  {},
7622
- async () => performAct(acts, { kind: "cancel_wake", callId: randomUUID() }),
7637
+ async () => performAct(acts, { kind: "cancel_wake", callId: randomUUID2() }),
7623
7638
  { annotations: CANCEL_TOOL, alwaysLoad: true }
7624
7639
  )
7625
7640
  ]
@@ -8252,7 +8267,7 @@ var TurnExecution = class {
8252
8267
  agentId: payload.agentId,
8253
8268
  messageId: payload.messageId
8254
8269
  });
8255
- this.turnId = handleOpts.turnId ?? randomUUID2();
8270
+ this.turnId = handleOpts.turnId ?? randomUUID3();
8256
8271
  }
8257
8272
  opts;
8258
8273
  supervisor;
@@ -8466,7 +8481,7 @@ var TurnExecution = class {
8466
8481
  await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
8467
8482
  body: `${MISSING_SECRET_PREFIX} ${list}`,
8468
8483
  kind: "runtime_notice",
8469
- turnId: randomUUID2(),
8484
+ turnId: randomUUID3(),
8470
8485
  // CT113: even a turn that fails before it runs answers a message.
8471
8486
  parentMessageId: payload.messageId
8472
8487
  });
@@ -8607,6 +8622,7 @@ var TurnExecution = class {
8607
8622
  try {
8608
8623
  await this.opts.api.setActiveRun(workspaceId, payload.conversationId, payload.agentId, {
8609
8624
  activeRunStartedAt: (/* @__PURE__ */ new Date()).toISOString(),
8625
+ bootId,
8610
8626
  // CT1380: the durable record that this turn resumed, and from where.
8611
8627
  ...this.resumedFromSeq !== void 0 ? { resumedFromSeq: this.resumedFromSeq } : {},
8612
8628
  // CT33: hand the server this turn's id so the new-run chokepoint's
@@ -9658,6 +9674,11 @@ var CompanionSupervisor = class {
9658
9674
  unrunnableRetryDelaysMs;
9659
9675
  deviceApi = null;
9660
9676
  deviceSub = null;
9677
+ recoveryChecked = false;
9678
+ recovering = false;
9679
+ heartbeated = false;
9680
+ recoveryCursor;
9681
+ recoveredTurns = /* @__PURE__ */ new Set();
9661
9682
  heartbeatTimer = null;
9662
9683
  inFlightHeartbeat = null;
9663
9684
  pollTimer = null;
@@ -9770,6 +9791,7 @@ var CompanionSupervisor = class {
9770
9791
  const connectorReports = this.connectorHealth.reports();
9771
9792
  const opencodeModels = this.config.opencode?.serverUrl ? await enumerateOpencodeModels(this.config.opencode.serverUrl) : null;
9772
9793
  const res = await this.deviceApi.heartbeat({
9794
+ bootId,
9773
9795
  version: COMPANION_VERSION,
9774
9796
  exposedSecretNames: store.names(),
9775
9797
  // Report each runtime only when this device can actually run it:
@@ -9808,6 +9830,8 @@ var CompanionSupervisor = class {
9808
9830
  this.hub.setDevice({ deviceId: res.deviceId });
9809
9831
  this.deviceId = res.deviceId;
9810
9832
  this.checkVersionSkew(res.serverVersion);
9833
+ this.heartbeated = true;
9834
+ void this.recoverRuns();
9811
9835
  } catch (err) {
9812
9836
  this.log.warn(
9813
9837
  { err: err instanceof Error ? err.message : String(err) },
@@ -9871,6 +9895,7 @@ var CompanionSupervisor = class {
9871
9895
  }
9872
9896
  for (const [wsId, list] of byWorkspace) this.reconcileWorkspace(wsId, list);
9873
9897
  pruneCredentials(items.map((i) => i.agentId));
9898
+ void this.recoverRuns();
9874
9899
  } finally {
9875
9900
  this.refreshing = false;
9876
9901
  }
@@ -10361,10 +10386,65 @@ var CompanionSupervisor = class {
10361
10386
  await this.refreshAssignments();
10362
10387
  return this.workspaces.get(wr.workspaceId)?.agents.get(payload.agentId);
10363
10388
  }
10389
+ // The server may still hold a run whose SSE event no longer replays.
10390
+ // Read one bounded page per refresh. Only a stored session earns a reclaim;
10391
+ // without one, leave the old boot untouched for the server's restart sweep.
10392
+ async recoverRuns() {
10393
+ if (!this.deviceApi || !this.heartbeated || this.recovering || this.recoveryChecked || this.stopped || noResume())
10394
+ return;
10395
+ this.recovering = true;
10396
+ try {
10397
+ const page = await this.deviceApi.getRuns(this.recoveryCursor);
10398
+ if (this.stopped) return;
10399
+ let assignmentsMissing = false;
10400
+ for (const run of page.runs) {
10401
+ if (run.bootId === bootId || !run.hasSession || this.recoveredTurns.has(run.turnId))
10402
+ continue;
10403
+ const wr = this.workspaces.get(run.workspaceId);
10404
+ const agent = wr?.agents.get(run.agentId);
10405
+ if (!wr || !agent) {
10406
+ assignmentsMissing = true;
10407
+ continue;
10408
+ }
10409
+ const key = `${run.conversationId}|${run.agentId}`;
10410
+ if (wr.chains.has(key)) continue;
10411
+ this.recoveredTurns.add(run.turnId);
10412
+ if (bumpResumeAttempt(run.workspaceId, `restart:${run.turnId}`) > MAX_RESUME_ATTEMPTS)
10413
+ continue;
10414
+ const payload = {
10415
+ type: "device:dispatch_requested",
10416
+ deviceId: this.deviceId,
10417
+ agentId: run.agentId,
10418
+ conversationId: run.conversationId,
10419
+ messageId: run.messageId
10420
+ };
10421
+ const tail = this.runDispatch(
10422
+ wr,
10423
+ { id: "", event: "recovery", data: "" },
10424
+ payload,
10425
+ agent,
10426
+ run.turnId
10427
+ ).catch(
10428
+ (err) => this.log.warn({ err, turnId: run.turnId }, "companion: restart recovery failed")
10429
+ ).finally(() => {
10430
+ if (wr.chains.get(key) === tail) wr.chains.delete(key);
10431
+ });
10432
+ wr.chains.set(key, tail);
10433
+ }
10434
+ if (!assignmentsMissing) {
10435
+ this.recoveryCursor = page.nextCursor ?? void 0;
10436
+ this.recoveryChecked = page.nextCursor === null;
10437
+ }
10438
+ } catch (err) {
10439
+ this.log.warn({ err }, "companion: run recovery read failed (will retry)");
10440
+ } finally {
10441
+ this.recovering = false;
10442
+ }
10443
+ }
10364
10444
  // One turn: the SJ516 F7 / CT26 skip-vs-resume dedupe, the dispatch, then
10365
10445
  // cursor + console summary. Mirrors the old per-workspace `handleEvent`, now
10366
10446
  // scoped to one agent.
10367
- async runDispatch(wr, ev, payload, agent) {
10447
+ async runDispatch(wr, ev, payload, agent, recoveredTurnId) {
10368
10448
  const workspaceId = wr.workspaceId;
10369
10449
  if (ev.id && hasDispatched(workspaceId, ev.id)) {
10370
10450
  if (hasCompleted(workspaceId, ev.id)) {
@@ -10399,8 +10479,8 @@ var CompanionSupervisor = class {
10399
10479
  "companion: re-dispatching interrupted turn (resume after restart)"
10400
10480
  );
10401
10481
  }
10402
- const resumedTurnId = ev.id ? turnIdForEvent(workspaceId, ev.id) : null;
10403
- const turnId = resumedTurnId ?? randomUUID3();
10482
+ const resumedTurnId = recoveredTurnId ?? (ev.id ? turnIdForEvent(workspaceId, ev.id) : null);
10483
+ const turnId = resumedTurnId ?? randomUUID4();
10404
10484
  if (ev.id) {
10405
10485
  const liveTurnIds = rememberTurnId(workspaceId, ev.id, turnId);
10406
10486
  if (liveTurnIds > TURN_ID_OVERFLOW_WARN) {
@@ -10823,7 +10903,7 @@ async function createCompanionRuntime(opts = {}) {
10823
10903
  opencodeServerUrl: cfg.opencode?.serverUrl,
10824
10904
  codex: isCodexEnabled(cfg)
10825
10905
  });
10826
- const instanceId = randomUUID4();
10906
+ const instanceId = randomUUID5();
10827
10907
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
10828
10908
  const pre = readLiveRuntimeState();
10829
10909
  if (pre && await verifyRuntime(pre) === "stale") clearRuntimeState();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.95",
3
+ "version": "0.6.96",
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",