@cabane/companion 0.6.95 → 0.6.97
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 +99 -20
- package/dist/runtime.js +99 -20
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1963,7 +1963,7 @@ function getLogger() {
|
|
|
1963
1963
|
}
|
|
1964
1964
|
|
|
1965
1965
|
// src/runtime.ts
|
|
1966
|
-
import { randomUUID as
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 ??
|
|
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:
|
|
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
|
}
|
|
@@ -10803,11 +10828,10 @@ var CompanionSupervisor = class {
|
|
|
10803
10828
|
// CT1146: tell the server this device cannot run a dispatch it was addressed to.
|
|
10804
10829
|
//
|
|
10805
10830
|
// Returns whether the report LANDED, because that answer decides whether the
|
|
10806
|
-
// caller may settle the cursor.
|
|
10807
|
-
//
|
|
10808
|
-
//
|
|
10809
|
-
// only the
|
|
10810
|
-
// indistinguishable from the original defect.
|
|
10831
|
+
// caller may settle the cursor. The server checks the recorded dispatch
|
|
10832
|
+
// destination even if the agent's assignment has moved. Reporting promptly
|
|
10833
|
+
// explains the roster miss; the thirty-minute unanswered-offer deadline is
|
|
10834
|
+
// only the backstop when no report lands.
|
|
10811
10835
|
//
|
|
10812
10836
|
// Bounded retry, because the common failure is transient (a 503 mid-deploy, a
|
|
10813
10837
|
// dropped socket) and one more attempt a second later usually lands. Bounded
|
|
@@ -10864,10 +10888,65 @@ var CompanionSupervisor = class {
|
|
|
10864
10888
|
await this.refreshAssignments();
|
|
10865
10889
|
return this.workspaces.get(wr.workspaceId)?.agents.get(payload.agentId);
|
|
10866
10890
|
}
|
|
10891
|
+
// The server may still hold a run whose SSE event no longer replays.
|
|
10892
|
+
// Read one bounded page per refresh. Only a stored session earns a reclaim;
|
|
10893
|
+
// without one, leave the old boot untouched for the server's restart sweep.
|
|
10894
|
+
async recoverRuns() {
|
|
10895
|
+
if (!this.deviceApi || !this.heartbeated || this.recovering || this.recoveryChecked || this.stopped || noResume())
|
|
10896
|
+
return;
|
|
10897
|
+
this.recovering = true;
|
|
10898
|
+
try {
|
|
10899
|
+
const page = await this.deviceApi.getRuns(this.recoveryCursor);
|
|
10900
|
+
if (this.stopped) return;
|
|
10901
|
+
let assignmentsMissing = false;
|
|
10902
|
+
for (const run of page.runs) {
|
|
10903
|
+
if (run.bootId === bootId || !run.hasSession || this.recoveredTurns.has(run.turnId))
|
|
10904
|
+
continue;
|
|
10905
|
+
const wr = this.workspaces.get(run.workspaceId);
|
|
10906
|
+
const agent = wr?.agents.get(run.agentId);
|
|
10907
|
+
if (!wr || !agent) {
|
|
10908
|
+
assignmentsMissing = true;
|
|
10909
|
+
continue;
|
|
10910
|
+
}
|
|
10911
|
+
const key = `${run.conversationId}|${run.agentId}`;
|
|
10912
|
+
if (wr.chains.has(key)) continue;
|
|
10913
|
+
this.recoveredTurns.add(run.turnId);
|
|
10914
|
+
if (bumpResumeAttempt(run.workspaceId, `restart:${run.turnId}`) > MAX_RESUME_ATTEMPTS)
|
|
10915
|
+
continue;
|
|
10916
|
+
const payload = {
|
|
10917
|
+
type: "device:dispatch_requested",
|
|
10918
|
+
deviceId: this.deviceId,
|
|
10919
|
+
agentId: run.agentId,
|
|
10920
|
+
conversationId: run.conversationId,
|
|
10921
|
+
messageId: run.messageId
|
|
10922
|
+
};
|
|
10923
|
+
const tail = this.runDispatch(
|
|
10924
|
+
wr,
|
|
10925
|
+
{ id: "", event: "recovery", data: "" },
|
|
10926
|
+
payload,
|
|
10927
|
+
agent,
|
|
10928
|
+
run.turnId
|
|
10929
|
+
).catch(
|
|
10930
|
+
(err) => this.log.warn({ err, turnId: run.turnId }, "companion: restart recovery failed")
|
|
10931
|
+
).finally(() => {
|
|
10932
|
+
if (wr.chains.get(key) === tail) wr.chains.delete(key);
|
|
10933
|
+
});
|
|
10934
|
+
wr.chains.set(key, tail);
|
|
10935
|
+
}
|
|
10936
|
+
if (!assignmentsMissing) {
|
|
10937
|
+
this.recoveryCursor = page.nextCursor ?? void 0;
|
|
10938
|
+
this.recoveryChecked = page.nextCursor === null;
|
|
10939
|
+
}
|
|
10940
|
+
} catch (err) {
|
|
10941
|
+
this.log.warn({ err }, "companion: run recovery read failed (will retry)");
|
|
10942
|
+
} finally {
|
|
10943
|
+
this.recovering = false;
|
|
10944
|
+
}
|
|
10945
|
+
}
|
|
10867
10946
|
// One turn: the SJ516 F7 / CT26 skip-vs-resume dedupe, the dispatch, then
|
|
10868
10947
|
// cursor + console summary. Mirrors the old per-workspace `handleEvent`, now
|
|
10869
10948
|
// scoped to one agent.
|
|
10870
|
-
async runDispatch(wr, ev, payload, agent) {
|
|
10949
|
+
async runDispatch(wr, ev, payload, agent, recoveredTurnId) {
|
|
10871
10950
|
const workspaceId = wr.workspaceId;
|
|
10872
10951
|
if (ev.id && hasDispatched(workspaceId, ev.id)) {
|
|
10873
10952
|
if (hasCompleted(workspaceId, ev.id)) {
|
|
@@ -10902,8 +10981,8 @@ var CompanionSupervisor = class {
|
|
|
10902
10981
|
"companion: re-dispatching interrupted turn (resume after restart)"
|
|
10903
10982
|
);
|
|
10904
10983
|
}
|
|
10905
|
-
const resumedTurnId = ev.id ? turnIdForEvent(workspaceId, ev.id) : null;
|
|
10906
|
-
const turnId = resumedTurnId ??
|
|
10984
|
+
const resumedTurnId = recoveredTurnId ?? (ev.id ? turnIdForEvent(workspaceId, ev.id) : null);
|
|
10985
|
+
const turnId = resumedTurnId ?? randomUUID4();
|
|
10907
10986
|
if (ev.id) {
|
|
10908
10987
|
const liveTurnIds = rememberTurnId(workspaceId, ev.id, turnId);
|
|
10909
10988
|
if (liveTurnIds > TURN_ID_OVERFLOW_WARN) {
|
|
@@ -11326,7 +11405,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
11326
11405
|
opencodeServerUrl: cfg.opencode?.serverUrl,
|
|
11327
11406
|
codex: isCodexEnabled(cfg)
|
|
11328
11407
|
});
|
|
11329
|
-
const instanceId =
|
|
11408
|
+
const instanceId = randomUUID5();
|
|
11330
11409
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11331
11410
|
const pre = readLiveRuntimeState();
|
|
11332
11411
|
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
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 ??
|
|
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:
|
|
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
|
}
|
|
@@ -10300,11 +10325,10 @@ var CompanionSupervisor = class {
|
|
|
10300
10325
|
// CT1146: tell the server this device cannot run a dispatch it was addressed to.
|
|
10301
10326
|
//
|
|
10302
10327
|
// Returns whether the report LANDED, because that answer decides whether the
|
|
10303
|
-
// caller may settle the cursor.
|
|
10304
|
-
//
|
|
10305
|
-
//
|
|
10306
|
-
// only the
|
|
10307
|
-
// indistinguishable from the original defect.
|
|
10328
|
+
// caller may settle the cursor. The server checks the recorded dispatch
|
|
10329
|
+
// destination even if the agent's assignment has moved. Reporting promptly
|
|
10330
|
+
// explains the roster miss; the thirty-minute unanswered-offer deadline is
|
|
10331
|
+
// only the backstop when no report lands.
|
|
10308
10332
|
//
|
|
10309
10333
|
// Bounded retry, because the common failure is transient (a 503 mid-deploy, a
|
|
10310
10334
|
// dropped socket) and one more attempt a second later usually lands. Bounded
|
|
@@ -10361,10 +10385,65 @@ var CompanionSupervisor = class {
|
|
|
10361
10385
|
await this.refreshAssignments();
|
|
10362
10386
|
return this.workspaces.get(wr.workspaceId)?.agents.get(payload.agentId);
|
|
10363
10387
|
}
|
|
10388
|
+
// The server may still hold a run whose SSE event no longer replays.
|
|
10389
|
+
// Read one bounded page per refresh. Only a stored session earns a reclaim;
|
|
10390
|
+
// without one, leave the old boot untouched for the server's restart sweep.
|
|
10391
|
+
async recoverRuns() {
|
|
10392
|
+
if (!this.deviceApi || !this.heartbeated || this.recovering || this.recoveryChecked || this.stopped || noResume())
|
|
10393
|
+
return;
|
|
10394
|
+
this.recovering = true;
|
|
10395
|
+
try {
|
|
10396
|
+
const page = await this.deviceApi.getRuns(this.recoveryCursor);
|
|
10397
|
+
if (this.stopped) return;
|
|
10398
|
+
let assignmentsMissing = false;
|
|
10399
|
+
for (const run of page.runs) {
|
|
10400
|
+
if (run.bootId === bootId || !run.hasSession || this.recoveredTurns.has(run.turnId))
|
|
10401
|
+
continue;
|
|
10402
|
+
const wr = this.workspaces.get(run.workspaceId);
|
|
10403
|
+
const agent = wr?.agents.get(run.agentId);
|
|
10404
|
+
if (!wr || !agent) {
|
|
10405
|
+
assignmentsMissing = true;
|
|
10406
|
+
continue;
|
|
10407
|
+
}
|
|
10408
|
+
const key = `${run.conversationId}|${run.agentId}`;
|
|
10409
|
+
if (wr.chains.has(key)) continue;
|
|
10410
|
+
this.recoveredTurns.add(run.turnId);
|
|
10411
|
+
if (bumpResumeAttempt(run.workspaceId, `restart:${run.turnId}`) > MAX_RESUME_ATTEMPTS)
|
|
10412
|
+
continue;
|
|
10413
|
+
const payload = {
|
|
10414
|
+
type: "device:dispatch_requested",
|
|
10415
|
+
deviceId: this.deviceId,
|
|
10416
|
+
agentId: run.agentId,
|
|
10417
|
+
conversationId: run.conversationId,
|
|
10418
|
+
messageId: run.messageId
|
|
10419
|
+
};
|
|
10420
|
+
const tail = this.runDispatch(
|
|
10421
|
+
wr,
|
|
10422
|
+
{ id: "", event: "recovery", data: "" },
|
|
10423
|
+
payload,
|
|
10424
|
+
agent,
|
|
10425
|
+
run.turnId
|
|
10426
|
+
).catch(
|
|
10427
|
+
(err) => this.log.warn({ err, turnId: run.turnId }, "companion: restart recovery failed")
|
|
10428
|
+
).finally(() => {
|
|
10429
|
+
if (wr.chains.get(key) === tail) wr.chains.delete(key);
|
|
10430
|
+
});
|
|
10431
|
+
wr.chains.set(key, tail);
|
|
10432
|
+
}
|
|
10433
|
+
if (!assignmentsMissing) {
|
|
10434
|
+
this.recoveryCursor = page.nextCursor ?? void 0;
|
|
10435
|
+
this.recoveryChecked = page.nextCursor === null;
|
|
10436
|
+
}
|
|
10437
|
+
} catch (err) {
|
|
10438
|
+
this.log.warn({ err }, "companion: run recovery read failed (will retry)");
|
|
10439
|
+
} finally {
|
|
10440
|
+
this.recovering = false;
|
|
10441
|
+
}
|
|
10442
|
+
}
|
|
10364
10443
|
// One turn: the SJ516 F7 / CT26 skip-vs-resume dedupe, the dispatch, then
|
|
10365
10444
|
// cursor + console summary. Mirrors the old per-workspace `handleEvent`, now
|
|
10366
10445
|
// scoped to one agent.
|
|
10367
|
-
async runDispatch(wr, ev, payload, agent) {
|
|
10446
|
+
async runDispatch(wr, ev, payload, agent, recoveredTurnId) {
|
|
10368
10447
|
const workspaceId = wr.workspaceId;
|
|
10369
10448
|
if (ev.id && hasDispatched(workspaceId, ev.id)) {
|
|
10370
10449
|
if (hasCompleted(workspaceId, ev.id)) {
|
|
@@ -10399,8 +10478,8 @@ var CompanionSupervisor = class {
|
|
|
10399
10478
|
"companion: re-dispatching interrupted turn (resume after restart)"
|
|
10400
10479
|
);
|
|
10401
10480
|
}
|
|
10402
|
-
const resumedTurnId = ev.id ? turnIdForEvent(workspaceId, ev.id) : null;
|
|
10403
|
-
const turnId = resumedTurnId ??
|
|
10481
|
+
const resumedTurnId = recoveredTurnId ?? (ev.id ? turnIdForEvent(workspaceId, ev.id) : null);
|
|
10482
|
+
const turnId = resumedTurnId ?? randomUUID4();
|
|
10404
10483
|
if (ev.id) {
|
|
10405
10484
|
const liveTurnIds = rememberTurnId(workspaceId, ev.id, turnId);
|
|
10406
10485
|
if (liveTurnIds > TURN_ID_OVERFLOW_WARN) {
|
|
@@ -10823,7 +10902,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
10823
10902
|
opencodeServerUrl: cfg.opencode?.serverUrl,
|
|
10824
10903
|
codex: isCodexEnabled(cfg)
|
|
10825
10904
|
});
|
|
10826
|
-
const instanceId =
|
|
10905
|
+
const instanceId = randomUUID5();
|
|
10827
10906
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10828
10907
|
const pre = readLiveRuntimeState();
|
|
10829
10908
|
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.
|
|
3
|
+
"version": "0.6.97",
|
|
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",
|