@cabane/companion 0.6.119 → 0.6.121
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 +166 -342
- package/dist/runtime.js +149 -325
- package/package.json +1 -1
package/dist/runtime.js
CHANGED
|
@@ -2281,6 +2281,7 @@ var bootId = randomUUID();
|
|
|
2281
2281
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
2282
2282
|
|
|
2283
2283
|
// src/api.ts
|
|
2284
|
+
var DEVICE_HEADER = "x-cabane-device";
|
|
2284
2285
|
var RETRY_BACKOFF_MS = [250, 750];
|
|
2285
2286
|
var TURN_CONTEXT_BACKOFF_MS = [250, 750, 2e3, 5e3];
|
|
2286
2287
|
var ACTIVE_RUN_OUTBOX_SEQ = 0;
|
|
@@ -2310,6 +2311,7 @@ var CabaneApi = class {
|
|
|
2310
2311
|
method,
|
|
2311
2312
|
headers: {
|
|
2312
2313
|
Authorization: `Bearer ${this.opts.token}`,
|
|
2314
|
+
...this.opts.deviceToken ? { [DEVICE_HEADER]: this.opts.deviceToken } : {},
|
|
2313
2315
|
...body !== void 0 ? { "content-type": "application/json" } : {},
|
|
2314
2316
|
accept: "application/json"
|
|
2315
2317
|
},
|
|
@@ -2444,27 +2446,6 @@ var CabaneApi = class {
|
|
|
2444
2446
|
outboxSize() {
|
|
2445
2447
|
return this.opts.outbox?.size() ?? 0;
|
|
2446
2448
|
}
|
|
2447
|
-
getConversation(workspaceId, conversationId) {
|
|
2448
|
-
return this.request("GET", `/api/workspaces/${workspaceId}/conversations/${conversationId}`);
|
|
2449
|
-
}
|
|
2450
|
-
// SJ383: companion-only participant ops. All three authenticate with the
|
|
2451
|
-
// workspace's agent-bound PAT (passed as `token` on this client) — never
|
|
2452
|
-
// the user PAT.
|
|
2453
|
-
// Recovery-path read: returns the companion's `agentSessionId` for this
|
|
2454
|
-
// conversation so the dispatcher can pass it as `Options.resume`, plus
|
|
2455
|
-
// the current `activeRunStartedAt` so a freshly-reconnected companion can
|
|
2456
|
-
// see whether a prior run is still flagged in-flight.
|
|
2457
|
-
// SJ383: recovery-path read for a participant row — the `agentSessionId` a
|
|
2458
|
-
// freshly-reconnected companion resumes on. CT262: the per-turn piggybacks this
|
|
2459
|
-
// fetch grew (the ABOUT charters / visionBlocks / channel / members / conversationContext)
|
|
2460
|
-
// are gone — `getTurnContext` composes the whole turn now — so this is back to
|
|
2461
|
-
// the plain recovery read, with no `messageId` param.
|
|
2462
|
-
getParticipantAgent(workspaceId, conversationId, agentId) {
|
|
2463
|
-
return this.request(
|
|
2464
|
-
"GET",
|
|
2465
|
-
`/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}`
|
|
2466
|
-
);
|
|
2467
|
-
}
|
|
2468
2449
|
// CT262: the ONE turn-context fetch. Collapses the companion's old four-fetch
|
|
2469
2450
|
// choreography (getConversation + getMessage + getAgentSelf + getParticipantAgent)
|
|
2470
2451
|
// into a single call: the server composes the whole server portion of the
|
|
@@ -2744,34 +2725,6 @@ var CabaneApi = class {
|
|
|
2744
2725
|
signal
|
|
2745
2726
|
);
|
|
2746
2727
|
}
|
|
2747
|
-
// SJ493: fetch the agent's self-view — identity + operating context. The
|
|
2748
|
-
// companion calls this on each dispatch to get its `systemPrompt` (composed
|
|
2749
|
-
// server-side from the bundled default + the agent's charter) rather than
|
|
2750
|
-
// baking a copy of the prompt into the download. Cabane is the control plane
|
|
2751
|
-
// for the prompt, so changing it (or the per-agent charter) takes effect
|
|
2752
|
-
// without shipping a new companion. Agent-PAT authed; the workspace is implied
|
|
2753
|
-
// by the PAT, so no workspace arg.
|
|
2754
|
-
// CT245: pass the triggering turn's `conversationId` so the server returns the
|
|
2755
|
-
// run-config RESOLVED for this conversation (agent default + that
|
|
2756
|
-
// conversation-agent's per-conversation override), computed with the same
|
|
2757
|
-
// shared resolver the in-app path uses. Omitting it (older call sites) returns
|
|
2758
|
-
// the agent default — graceful degradation, no version coupling.
|
|
2759
|
-
getAgentSelf(conversationId) {
|
|
2760
|
-
const path = conversationId ? `/api/agent/me?conversationId=${encodeURIComponent(conversationId)}` : `/api/agent/me`;
|
|
2761
|
-
return this.request("GET", path);
|
|
2762
|
-
}
|
|
2763
|
-
// The companion fetches the triggering message body by listing the
|
|
2764
|
-
// conversation's messages and finding the one with `id === messageId`.
|
|
2765
|
-
// Cabane has no single-message GET endpoint; for v0 this is fine because
|
|
2766
|
-
// the companion only reaches for the specific row immediately after the
|
|
2767
|
-
// event fires (the thread is small at that point).
|
|
2768
|
-
async getMessage(workspaceId, conversationId, messageId2) {
|
|
2769
|
-
const res = await this.request(
|
|
2770
|
-
"GET",
|
|
2771
|
-
`/api/workspaces/${workspaceId}/conversations/${conversationId}/messages`
|
|
2772
|
-
);
|
|
2773
|
-
return res.messages.find((m) => m.id === messageId2) ?? null;
|
|
2774
|
-
}
|
|
2775
2728
|
};
|
|
2776
2729
|
function isRetryable(err) {
|
|
2777
2730
|
if (err instanceof ApiError) return err.status >= 500;
|
|
@@ -2965,78 +2918,21 @@ function pruneCredentials(keepAgentIds) {
|
|
|
2965
2918
|
return removed;
|
|
2966
2919
|
}
|
|
2967
2920
|
|
|
2968
|
-
// src/cursor.ts
|
|
2969
|
-
import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync6 } from "fs";
|
|
2970
|
-
import { join as join9 } from "path";
|
|
2971
|
-
function pathFor(workspaceId) {
|
|
2972
|
-
return join9(cabaneDir(), "cursors", encodeURIComponent(workspaceId));
|
|
2973
|
-
}
|
|
2974
|
-
function readCursor(workspaceId) {
|
|
2975
|
-
const path = pathFor(workspaceId);
|
|
2976
|
-
if (!existsSync6(path)) return null;
|
|
2977
|
-
const raw = readFileSync4(path, "utf8").trim();
|
|
2978
|
-
return raw.length > 0 ? raw : null;
|
|
2979
|
-
}
|
|
2980
|
-
function writeCursor(workspaceId, eventId) {
|
|
2981
|
-
const path = pathFor(workspaceId);
|
|
2982
|
-
mkdirSync6(join9(cabaneDir(), "cursors"), { recursive: true });
|
|
2983
|
-
writeFileSync4(path, eventId + "\n", "utf8");
|
|
2984
|
-
}
|
|
2985
|
-
|
|
2986
|
-
// src/cursor-tracker.ts
|
|
2987
|
-
var CursorTracker = class {
|
|
2988
|
-
constructor(workspaceId) {
|
|
2989
|
-
this.workspaceId = workspaceId;
|
|
2990
|
-
}
|
|
2991
|
-
workspaceId;
|
|
2992
|
-
// Event ids in arrival order that haven't yet flushed to the persisted
|
|
2993
|
-
// cursor. Holds the in-flight (un-settled) dispatch events plus any later
|
|
2994
|
-
// events that have settled but are blocked behind an earlier in-flight one.
|
|
2995
|
-
pending = [];
|
|
2996
|
-
// Ids observed as settled. An entry is dropped the moment it flushes, so the
|
|
2997
|
-
// set only ever holds settled-but-blocked ids — bounded by concurrent
|
|
2998
|
-
// in-flight turns and the traffic that arrived during them.
|
|
2999
|
-
settled = /* @__PURE__ */ new Set();
|
|
3000
|
-
// Record an event id in arrival order. Must be called synchronously, in
|
|
3001
|
-
// stream order, for every event the supervisor will later `settle` — so the
|
|
3002
|
-
// cumulative prefix reflects the true stream sequence. Idempotent: an id
|
|
3003
|
-
// already tracked (an in-flight event, or a settled-but-blocked one) is
|
|
3004
|
-
// ignored, so a process-restart replay can re-observe the same ids without
|
|
3005
|
-
// double-counting.
|
|
3006
|
-
observe(eventId) {
|
|
3007
|
-
if (this.pending.includes(eventId) || this.settled.has(eventId)) return;
|
|
3008
|
-
this.pending.push(eventId);
|
|
3009
|
-
}
|
|
3010
|
-
// Mark an event settled — for a dispatch turn, only once it has completed;
|
|
3011
|
-
// for everything else, immediately after it's handled. Flushes the persisted
|
|
3012
|
-
// cursor forward through the now-longer contiguous settled prefix.
|
|
3013
|
-
settle(eventId) {
|
|
3014
|
-
this.settled.add(eventId);
|
|
3015
|
-
let advanced = null;
|
|
3016
|
-
while (this.pending.length > 0 && this.settled.has(this.pending[0])) {
|
|
3017
|
-
const id = this.pending.shift();
|
|
3018
|
-
this.settled.delete(id);
|
|
3019
|
-
advanced = id;
|
|
3020
|
-
}
|
|
3021
|
-
if (advanced) writeCursor(this.workspaceId, advanced);
|
|
3022
|
-
}
|
|
3023
|
-
};
|
|
3024
|
-
|
|
3025
2921
|
// src/dispatch-dedupe.ts
|
|
3026
|
-
import { mkdirSync as
|
|
3027
|
-
import { join as
|
|
2922
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync6 } from "fs";
|
|
2923
|
+
import { join as join9 } from "path";
|
|
3028
2924
|
var MAX_IDS = 256;
|
|
3029
2925
|
function dir(log) {
|
|
3030
|
-
return
|
|
2926
|
+
return join9(cabaneDir(), log);
|
|
3031
2927
|
}
|
|
3032
|
-
function
|
|
3033
|
-
return
|
|
2928
|
+
function pathFor(log, workspaceId) {
|
|
2929
|
+
return join9(dir(log), encodeURIComponent(workspaceId));
|
|
3034
2930
|
}
|
|
3035
2931
|
function readIds(log, workspaceId) {
|
|
3036
|
-
const path =
|
|
3037
|
-
if (!
|
|
2932
|
+
const path = pathFor(log, workspaceId);
|
|
2933
|
+
if (!existsSync6(path)) return [];
|
|
3038
2934
|
try {
|
|
3039
|
-
return
|
|
2935
|
+
return readFileSync4(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
3040
2936
|
} catch {
|
|
3041
2937
|
return [];
|
|
3042
2938
|
}
|
|
@@ -3049,8 +2945,8 @@ function mark(log, workspaceId, eventId) {
|
|
|
3049
2945
|
if (ids.includes(eventId)) return;
|
|
3050
2946
|
ids.push(eventId);
|
|
3051
2947
|
const trimmed = ids.length > MAX_IDS ? ids.slice(-MAX_IDS) : ids;
|
|
3052
|
-
|
|
3053
|
-
|
|
2948
|
+
mkdirSync6(dir(log), { recursive: true });
|
|
2949
|
+
writeFileSync4(pathFor(log, workspaceId), trimmed.join("\n") + "\n", "utf8");
|
|
3054
2950
|
}
|
|
3055
2951
|
function hasDispatched(workspaceId, eventId) {
|
|
3056
2952
|
return has("dispatched", workspaceId, eventId);
|
|
@@ -3067,17 +2963,17 @@ function markCompleted(workspaceId, eventId) {
|
|
|
3067
2963
|
}
|
|
3068
2964
|
var MAX_RESUME_ATTEMPTS = 3;
|
|
3069
2965
|
function resumeDir() {
|
|
3070
|
-
return
|
|
2966
|
+
return join9(cabaneDir(), "resume-attempts");
|
|
3071
2967
|
}
|
|
3072
2968
|
function resumePathFor(workspaceId) {
|
|
3073
|
-
return
|
|
2969
|
+
return join9(resumeDir(), encodeURIComponent(workspaceId));
|
|
3074
2970
|
}
|
|
3075
2971
|
function readResumeCounts(workspaceId) {
|
|
3076
2972
|
const out = /* @__PURE__ */ new Map();
|
|
3077
2973
|
const path = resumePathFor(workspaceId);
|
|
3078
|
-
if (!
|
|
2974
|
+
if (!existsSync6(path)) return out;
|
|
3079
2975
|
try {
|
|
3080
|
-
for (const line of
|
|
2976
|
+
for (const line of readFileSync4(path, "utf8").split("\n")) {
|
|
3081
2977
|
const trimmed = line.trim();
|
|
3082
2978
|
if (!trimmed) continue;
|
|
3083
2979
|
const tab = trimmed.lastIndexOf(" ");
|
|
@@ -3097,8 +2993,8 @@ function bumpResumeAttempt(workspaceId, eventId) {
|
|
|
3097
2993
|
counts.set(eventId, next);
|
|
3098
2994
|
const entries = [...counts.entries()];
|
|
3099
2995
|
const trimmed = entries.length > MAX_IDS ? entries.slice(-MAX_IDS) : entries;
|
|
3100
|
-
|
|
3101
|
-
|
|
2996
|
+
mkdirSync6(resumeDir(), { recursive: true });
|
|
2997
|
+
writeFileSync4(
|
|
3102
2998
|
resumePathFor(workspaceId),
|
|
3103
2999
|
trimmed.map(([id, c]) => `${id} ${c}`).join("\n") + "\n",
|
|
3104
3000
|
"utf8"
|
|
@@ -3106,18 +3002,18 @@ function bumpResumeAttempt(workspaceId, eventId) {
|
|
|
3106
3002
|
return next;
|
|
3107
3003
|
}
|
|
3108
3004
|
function turnDir() {
|
|
3109
|
-
return
|
|
3005
|
+
return join9(cabaneDir(), "turns");
|
|
3110
3006
|
}
|
|
3111
3007
|
function turnPathFor(workspaceId) {
|
|
3112
|
-
return
|
|
3008
|
+
return join9(turnDir(), encodeURIComponent(workspaceId));
|
|
3113
3009
|
}
|
|
3114
3010
|
var TURN_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3115
3011
|
function readTurnIds(workspaceId) {
|
|
3116
3012
|
const out = /* @__PURE__ */ new Map();
|
|
3117
3013
|
const path = turnPathFor(workspaceId);
|
|
3118
|
-
if (!
|
|
3014
|
+
if (!existsSync6(path)) return out;
|
|
3119
3015
|
try {
|
|
3120
|
-
for (const line of
|
|
3016
|
+
for (const line of readFileSync4(path, "utf8").split("\n")) {
|
|
3121
3017
|
const trimmed = line.trim();
|
|
3122
3018
|
if (!trimmed) continue;
|
|
3123
3019
|
const tab = trimmed.lastIndexOf(" ");
|
|
@@ -3137,10 +3033,10 @@ function turnIdForEvent(workspaceId, eventId) {
|
|
|
3137
3033
|
var TURN_ID_OVERFLOW_WARN = 8192;
|
|
3138
3034
|
function writeTurnIds(workspaceId, turns) {
|
|
3139
3035
|
const entries = [...turns.entries()];
|
|
3140
|
-
|
|
3036
|
+
mkdirSync6(turnDir(), { recursive: true });
|
|
3141
3037
|
const path = turnPathFor(workspaceId);
|
|
3142
3038
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
3143
|
-
|
|
3039
|
+
writeFileSync4(tmp, entries.map(([id, t]) => `${id} ${t}`).join("\n") + "\n", "utf8");
|
|
3144
3040
|
renameSync3(tmp, path);
|
|
3145
3041
|
return entries.length;
|
|
3146
3042
|
}
|
|
@@ -7915,8 +7811,8 @@ var ConnectorHealthStore = class {
|
|
|
7915
7811
|
};
|
|
7916
7812
|
|
|
7917
7813
|
// src/dispatcher.ts
|
|
7918
|
-
import { existsSync as
|
|
7919
|
-
import { join as
|
|
7814
|
+
import { existsSync as existsSync10, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
7815
|
+
import { join as join14 } from "path";
|
|
7920
7816
|
|
|
7921
7817
|
// src/term.ts
|
|
7922
7818
|
import { createInterface } from "readline";
|
|
@@ -7930,7 +7826,7 @@ function tildePath(path) {
|
|
|
7930
7826
|
|
|
7931
7827
|
// src/turn-execution.ts
|
|
7932
7828
|
import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
|
|
7933
|
-
import { existsSync as
|
|
7829
|
+
import { existsSync as existsSync9 } from "fs";
|
|
7934
7830
|
|
|
7935
7831
|
// src/turn-control-tools.ts
|
|
7936
7832
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
@@ -8166,24 +8062,10 @@ function buildCompanionTurnRequest(params) {
|
|
|
8166
8062
|
session: t.session,
|
|
8167
8063
|
cabane: {
|
|
8168
8064
|
mcpUrl: cabaneMcpUrl(params.baseUrl),
|
|
8169
|
-
|
|
8170
|
-
// when the API didn't mint one (older API / unresolvable delegation).
|
|
8171
|
-
bearer: params.turnToken ?? params.agentPat,
|
|
8065
|
+
bearer: params.turnToken,
|
|
8172
8066
|
activeConversationId: params.activeConversationId,
|
|
8173
8067
|
workspaceId: params.workspaceId,
|
|
8174
|
-
|
|
8175
|
-
// this turn — the surface admits `turn_token` auth exclusively, so a
|
|
8176
|
-
// PAT-fallback bearer would be rejected there. Absent it, external adapters
|
|
8177
|
-
// simply don't mount it that turn.
|
|
8178
|
-
//
|
|
8179
|
-
// CT1074: that fallback is now an OLD-API case only. The server mints a token
|
|
8180
|
-
// for every turn it dispatched, so "no token" no longer means "no human behind
|
|
8181
|
-
// the turn" — it means the API predates this change. The condition is right as
|
|
8182
|
-
// it stands and stays where the failure is honest (a rejected bearer is worse
|
|
8183
|
-
// than an unmounted server); what was wrong was upstream, where an undelegated
|
|
8184
|
-
// turn minted nothing and this silently dropped all five verbs on Codex and
|
|
8185
|
-
// opencode with no refusal anyone could see.
|
|
8186
|
-
...params.turnToken ? { turnControlUrl: turnControlMcpUrl(params.baseUrl) } : {}
|
|
8068
|
+
turnControlUrl: turnControlMcpUrl(params.baseUrl)
|
|
8187
8069
|
},
|
|
8188
8070
|
local: {
|
|
8189
8071
|
...params.cwd ? { cwd: params.cwd } : {},
|
|
@@ -8207,11 +8089,11 @@ function trimSlash2(s) {
|
|
|
8207
8089
|
// src/codex-instructions.ts
|
|
8208
8090
|
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
8209
8091
|
import { tmpdir } from "os";
|
|
8210
|
-
import { join as
|
|
8092
|
+
import { join as join10 } from "path";
|
|
8211
8093
|
var PREFIX = "cabane-codex-instructions-";
|
|
8212
8094
|
async function writeCodexInstructionsFile(contents) {
|
|
8213
|
-
const dir2 = await mkdtemp(
|
|
8214
|
-
const path =
|
|
8095
|
+
const dir2 = await mkdtemp(join10(tmpdir(), PREFIX));
|
|
8096
|
+
const path = join10(dir2, "instructions.md");
|
|
8215
8097
|
await writeFile(path, contents, { encoding: "utf8", mode: 384 });
|
|
8216
8098
|
return {
|
|
8217
8099
|
path,
|
|
@@ -8317,22 +8199,22 @@ function reportFailure(config, body) {
|
|
|
8317
8199
|
}
|
|
8318
8200
|
|
|
8319
8201
|
// src/prepared.ts
|
|
8320
|
-
import { mkdirSync as
|
|
8321
|
-
import { join as
|
|
8202
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync5, rmSync as rmSync5, writeFileSync as writeFileSync5, existsSync as existsSync7 } from "fs";
|
|
8203
|
+
import { join as join11 } from "path";
|
|
8322
8204
|
function dirFor(workspaceId) {
|
|
8323
|
-
return
|
|
8205
|
+
return join11(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
|
|
8324
8206
|
}
|
|
8325
8207
|
function conversationDir(workspaceId, conversationId) {
|
|
8326
|
-
return
|
|
8208
|
+
return join11(dirFor(workspaceId), encodeURIComponent(conversationId));
|
|
8327
8209
|
}
|
|
8328
|
-
function
|
|
8329
|
-
return
|
|
8210
|
+
function pathFor2(workspaceId, conversationId, agentId) {
|
|
8211
|
+
return join11(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
|
|
8330
8212
|
}
|
|
8331
8213
|
function readPrepared(workspaceId, conversationId, agentId) {
|
|
8332
|
-
const path =
|
|
8333
|
-
if (!
|
|
8214
|
+
const path = pathFor2(workspaceId, conversationId, agentId);
|
|
8215
|
+
if (!existsSync7(path)) return null;
|
|
8334
8216
|
try {
|
|
8335
|
-
const parsed = JSON.parse(
|
|
8217
|
+
const parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
8336
8218
|
if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
|
|
8337
8219
|
return {
|
|
8338
8220
|
cwd: parsed.cwd,
|
|
@@ -8345,32 +8227,32 @@ function readPrepared(workspaceId, conversationId, agentId) {
|
|
|
8345
8227
|
}
|
|
8346
8228
|
}
|
|
8347
8229
|
function writePrepared(workspaceId, conversationId, agentId, result) {
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
8230
|
+
mkdirSync7(conversationDir(workspaceId, conversationId), { recursive: true });
|
|
8231
|
+
writeFileSync5(
|
|
8232
|
+
pathFor2(workspaceId, conversationId, agentId),
|
|
8351
8233
|
JSON.stringify(result) + "\n",
|
|
8352
8234
|
"utf8"
|
|
8353
8235
|
);
|
|
8354
8236
|
}
|
|
8355
8237
|
function clearPrepared(workspaceId, conversationId, agentId) {
|
|
8356
|
-
rmSync5(
|
|
8238
|
+
rmSync5(pathFor2(workspaceId, conversationId, agentId), { force: true });
|
|
8357
8239
|
}
|
|
8358
8240
|
|
|
8359
8241
|
// src/secrets.ts
|
|
8360
|
-
import { existsSync as
|
|
8361
|
-
import { join as
|
|
8242
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
|
|
8243
|
+
import { join as join12 } from "path";
|
|
8362
8244
|
import { z as z14 } from "zod";
|
|
8363
8245
|
var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
8364
8246
|
function secretsPath() {
|
|
8365
|
-
return
|
|
8247
|
+
return join12(cabaneDir(), "secrets.json");
|
|
8366
8248
|
}
|
|
8367
8249
|
var secretStoreSchema = z14.record(z14.string(), z14.string());
|
|
8368
8250
|
function loadSecretStore() {
|
|
8369
8251
|
const path = secretsPath();
|
|
8370
|
-
if (!
|
|
8252
|
+
if (!existsSync8(path)) return makeStore({});
|
|
8371
8253
|
let raw;
|
|
8372
8254
|
try {
|
|
8373
|
-
raw =
|
|
8255
|
+
raw = readFileSync6(path, "utf8");
|
|
8374
8256
|
} catch (err) {
|
|
8375
8257
|
throw new ConfigError(
|
|
8376
8258
|
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -8448,10 +8330,10 @@ function resolveMcpSecrets(mcpServers, store) {
|
|
|
8448
8330
|
}
|
|
8449
8331
|
|
|
8450
8332
|
// src/transcript-writer.ts
|
|
8451
|
-
import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as
|
|
8452
|
-
import { basename, dirname as dirname6, join as
|
|
8333
|
+
import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync6 } from "fs";
|
|
8334
|
+
import { basename, dirname as dirname6, join as join13 } from "path";
|
|
8453
8335
|
function transcriptsDir() {
|
|
8454
|
-
return
|
|
8336
|
+
return join13(cabaneDir(), "transcripts");
|
|
8455
8337
|
}
|
|
8456
8338
|
var RETAIN = 200;
|
|
8457
8339
|
var ANOMALY_RETAIN = 50;
|
|
@@ -8461,9 +8343,9 @@ var TranscriptWriter = class {
|
|
|
8461
8343
|
onWarn;
|
|
8462
8344
|
constructor(dir2, meta, onWarn) {
|
|
8463
8345
|
this.onWarn = onWarn;
|
|
8464
|
-
this.path =
|
|
8346
|
+
this.path = join13(dir2, fileName(meta));
|
|
8465
8347
|
try {
|
|
8466
|
-
|
|
8348
|
+
mkdirSync8(dir2, { recursive: true });
|
|
8467
8349
|
try {
|
|
8468
8350
|
chmodSync4(dir2, 448);
|
|
8469
8351
|
} catch {
|
|
@@ -8490,10 +8372,10 @@ var TranscriptWriter = class {
|
|
|
8490
8372
|
preserveAnomaly() {
|
|
8491
8373
|
if (this.broken) return;
|
|
8492
8374
|
try {
|
|
8493
|
-
const dir2 =
|
|
8494
|
-
|
|
8375
|
+
const dir2 = join13(dirname6(this.path), "anomalies");
|
|
8376
|
+
mkdirSync8(dir2, { recursive: true, mode: 448 });
|
|
8495
8377
|
chmodSync4(dir2, 448);
|
|
8496
|
-
const target =
|
|
8378
|
+
const target = join13(dir2, basename(this.path));
|
|
8497
8379
|
copyFileSync(this.path, target);
|
|
8498
8380
|
chmodSync4(target, 384);
|
|
8499
8381
|
pruneOld(dir2, ANOMALY_RETAIN);
|
|
@@ -8539,7 +8421,7 @@ function pruneOld(dir2, retain) {
|
|
|
8539
8421
|
const drop = files.sort().slice(0, files.length - retain);
|
|
8540
8422
|
for (const f of drop) {
|
|
8541
8423
|
try {
|
|
8542
|
-
rmSync6(
|
|
8424
|
+
rmSync6(join13(dir2, f), { force: true });
|
|
8543
8425
|
} catch {
|
|
8544
8426
|
}
|
|
8545
8427
|
}
|
|
@@ -8547,7 +8429,7 @@ function pruneOld(dir2, retain) {
|
|
|
8547
8429
|
|
|
8548
8430
|
// src/turn-containment.ts
|
|
8549
8431
|
import { execFile, execFileSync, spawn as spawn5 } from "child_process";
|
|
8550
|
-
import { readFileSync as
|
|
8432
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
8551
8433
|
import { promisify } from "util";
|
|
8552
8434
|
var execFileAsync = promisify(execFile);
|
|
8553
8435
|
var asContained = (child) => child;
|
|
@@ -8592,7 +8474,7 @@ function currentSystemdUnit(cgroupText) {
|
|
|
8592
8474
|
}
|
|
8593
8475
|
function readCurrentSystemdUnit() {
|
|
8594
8476
|
try {
|
|
8595
|
-
return currentSystemdUnit(
|
|
8477
|
+
return currentSystemdUnit(readFileSync7("/proc/self/cgroup", "utf8"));
|
|
8596
8478
|
} catch {
|
|
8597
8479
|
return null;
|
|
8598
8480
|
}
|
|
@@ -9018,6 +8900,7 @@ function missingSecretReason(missing) {
|
|
|
9018
8900
|
return `${prefix}+${missing.length}`;
|
|
9019
8901
|
}
|
|
9020
8902
|
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:";
|
|
8903
|
+
var NO_TURN_TOKEN_REASON = "no_turn_token: the server issued no turn credential for this turn";
|
|
9021
8904
|
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`;
|
|
9022
8905
|
var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
|
|
9023
8906
|
var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 15 * 6e4;
|
|
@@ -9297,6 +9180,10 @@ var TurnExecution = class {
|
|
|
9297
9180
|
const fetchReason = `fetch_failed: ${describeError(err)}`;
|
|
9298
9181
|
throw this.concluded(fetchReason, fetchReason);
|
|
9299
9182
|
}
|
|
9183
|
+
if (!turnContext.turnToken) {
|
|
9184
|
+
turnLog.warn({ turnId }, "dispatcher: turn context carried no turn token");
|
|
9185
|
+
throw this.concluded(NO_TURN_TOKEN_REASON, NO_TURN_TOKEN_REASON);
|
|
9186
|
+
}
|
|
9300
9187
|
this.turnContext = turnContext;
|
|
9301
9188
|
if (this.resumed) {
|
|
9302
9189
|
const sources = { serverFloor: turnContext.turnSeqFloor, outboxFloor };
|
|
@@ -9344,7 +9231,7 @@ var TurnExecution = class {
|
|
|
9344
9231
|
const prepareHook = this.opts.local.prepareHook;
|
|
9345
9232
|
const cabaneCwd = this.turnContext.cwd;
|
|
9346
9233
|
let effectiveCwd = localCwd ?? cabaneCwd;
|
|
9347
|
-
if (effectiveCwd && !
|
|
9234
|
+
if (effectiveCwd && !existsSync9(effectiveCwd)) {
|
|
9348
9235
|
turnLog.warn(
|
|
9349
9236
|
{ cwd: effectiveCwd },
|
|
9350
9237
|
"The working directory is missing; using the companion's directory instead. Check this agent's working directory setting."
|
|
@@ -9548,10 +9435,8 @@ var TurnExecution = class {
|
|
|
9548
9435
|
this.request = buildCompanionTurnRequest({
|
|
9549
9436
|
turnContext: this.turnContext,
|
|
9550
9437
|
baseUrl: this.opts.baseUrl,
|
|
9551
|
-
|
|
9552
|
-
|
|
9553
|
-
// the companion PAT (`agentPat`) inside `buildCompanionTurnRequest` otherwise.
|
|
9554
|
-
...this.turnContext.turnToken ? { turnToken: this.turnContext.turnToken } : {},
|
|
9438
|
+
// ACC8: always present; `fetchTurnContext` refused a context without one.
|
|
9439
|
+
turnToken: this.turnContext.turnToken,
|
|
9555
9440
|
// SJ524: the hook-resolved cwd overrides the static local cwd.
|
|
9556
9441
|
...this.effectiveCwd ? { cwd: this.effectiveCwd } : {},
|
|
9557
9442
|
// CT1103: the prepare-hook env plus the authoritative host-access token.
|
|
@@ -10063,7 +9948,7 @@ function fingerprintSessionState(state) {
|
|
|
10063
9948
|
// src/dispatcher.ts
|
|
10064
9949
|
function checkoutState(cwd) {
|
|
10065
9950
|
if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
|
|
10066
|
-
if (!
|
|
9951
|
+
if (!existsSync10(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
|
|
10067
9952
|
let entries;
|
|
10068
9953
|
try {
|
|
10069
9954
|
entries = readdirSync2(cwd);
|
|
@@ -10073,15 +9958,15 @@ function checkoutState(cwd) {
|
|
|
10073
9958
|
if (entries.length === 0) {
|
|
10074
9959
|
return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
|
|
10075
9960
|
}
|
|
10076
|
-
const gitPath =
|
|
10077
|
-
if (!
|
|
9961
|
+
const gitPath = join14(cwd, ".git");
|
|
9962
|
+
if (!existsSync10(gitPath)) return { ok: true, reason: "usable" };
|
|
10078
9963
|
let stat;
|
|
10079
9964
|
try {
|
|
10080
9965
|
stat = statSync2(gitPath);
|
|
10081
9966
|
} catch (error) {
|
|
10082
9967
|
return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
|
|
10083
9968
|
}
|
|
10084
|
-
if (stat.isDirectory() && !
|
|
9969
|
+
if (stat.isDirectory() && !existsSync10(join14(gitPath, "HEAD")))
|
|
10085
9970
|
return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
|
|
10086
9971
|
return { ok: true, reason: "usable" };
|
|
10087
9972
|
}
|
|
@@ -10241,15 +10126,15 @@ var DispatchClaims = class {
|
|
|
10241
10126
|
|
|
10242
10127
|
// src/outbox.ts
|
|
10243
10128
|
import {
|
|
10244
|
-
existsSync as
|
|
10245
|
-
mkdirSync as
|
|
10129
|
+
existsSync as existsSync11,
|
|
10130
|
+
mkdirSync as mkdirSync9,
|
|
10246
10131
|
readdirSync as readdirSync3,
|
|
10247
|
-
readFileSync as
|
|
10132
|
+
readFileSync as readFileSync8,
|
|
10248
10133
|
renameSync as renameSync4,
|
|
10249
10134
|
rmSync as rmSync7,
|
|
10250
|
-
writeFileSync as
|
|
10135
|
+
writeFileSync as writeFileSync6
|
|
10251
10136
|
} from "fs";
|
|
10252
|
-
import { join as
|
|
10137
|
+
import { join as join15 } from "path";
|
|
10253
10138
|
var MAX_ENTRIES = 2e3;
|
|
10254
10139
|
var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
10255
10140
|
var Outbox = class {
|
|
@@ -10262,21 +10147,21 @@ var Outbox = class {
|
|
|
10262
10147
|
// Resolved lazily (per call, like cursor.ts) so tests that swap HOME between
|
|
10263
10148
|
// cases route writes at the right tmpdir.
|
|
10264
10149
|
dir() {
|
|
10265
|
-
return
|
|
10150
|
+
return join15(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
|
|
10266
10151
|
}
|
|
10267
10152
|
fileFor(turnId, seq) {
|
|
10268
|
-
return
|
|
10153
|
+
return join15(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
|
|
10269
10154
|
}
|
|
10270
10155
|
// Persist a commit for later draining. Atomic (temp file + rename) so a
|
|
10271
10156
|
// concurrent `list()` never reads a half-written entry, then enforces the
|
|
10272
10157
|
// per-workspace bounds.
|
|
10273
10158
|
persist(entry) {
|
|
10274
10159
|
const dir2 = this.dir();
|
|
10275
|
-
|
|
10160
|
+
mkdirSync9(dir2, { recursive: true });
|
|
10276
10161
|
const target = this.fileFor(entry.turnId, entry.seq);
|
|
10277
10162
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
10278
10163
|
try {
|
|
10279
|
-
|
|
10164
|
+
writeFileSync6(tmp, JSON.stringify(entry) + "\n", "utf8");
|
|
10280
10165
|
renameSync4(tmp, target);
|
|
10281
10166
|
} catch (err) {
|
|
10282
10167
|
try {
|
|
@@ -10297,7 +10182,7 @@ var Outbox = class {
|
|
|
10297
10182
|
// wedging the drain.
|
|
10298
10183
|
list() {
|
|
10299
10184
|
const dir2 = this.dir();
|
|
10300
|
-
if (!
|
|
10185
|
+
if (!existsSync11(dir2)) return [];
|
|
10301
10186
|
let names;
|
|
10302
10187
|
try {
|
|
10303
10188
|
names = readdirSync3(dir2);
|
|
@@ -10307,9 +10192,9 @@ var Outbox = class {
|
|
|
10307
10192
|
const entries = [];
|
|
10308
10193
|
for (const name of names) {
|
|
10309
10194
|
if (!name.endsWith(".json")) continue;
|
|
10310
|
-
const full =
|
|
10195
|
+
const full = join15(dir2, name);
|
|
10311
10196
|
try {
|
|
10312
|
-
const parsed = JSON.parse(
|
|
10197
|
+
const parsed = JSON.parse(readFileSync8(full, "utf8"));
|
|
10313
10198
|
if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
|
|
10314
10199
|
entries.push(parsed);
|
|
10315
10200
|
} else {
|
|
@@ -10343,7 +10228,7 @@ var Outbox = class {
|
|
|
10343
10228
|
// different: it is positive evidence that nothing was ever queued here.
|
|
10344
10229
|
maxSeqForTurn(turnId) {
|
|
10345
10230
|
const dir2 = this.dir();
|
|
10346
|
-
if (!
|
|
10231
|
+
if (!existsSync11(dir2)) return 0;
|
|
10347
10232
|
const names = readdirSync3(dir2);
|
|
10348
10233
|
const prefix = `${encodeURIComponent(turnId)}__`;
|
|
10349
10234
|
let max = 0;
|
|
@@ -10363,7 +10248,7 @@ var Outbox = class {
|
|
|
10363
10248
|
}
|
|
10364
10249
|
size() {
|
|
10365
10250
|
const dir2 = this.dir();
|
|
10366
|
-
if (!
|
|
10251
|
+
if (!existsSync11(dir2)) return 0;
|
|
10367
10252
|
try {
|
|
10368
10253
|
return readdirSync3(dir2).filter((n) => n.endsWith(".json")).length;
|
|
10369
10254
|
} catch {
|
|
@@ -10517,7 +10402,7 @@ var SseSubscriber = class {
|
|
|
10517
10402
|
}
|
|
10518
10403
|
async connect() {
|
|
10519
10404
|
const base = this.opts.baseUrl.endsWith("/") ? this.opts.baseUrl.slice(0, -1) : this.opts.baseUrl;
|
|
10520
|
-
const url =
|
|
10405
|
+
const url = `${base}${this.opts.path}`;
|
|
10521
10406
|
this.controller = new AbortController();
|
|
10522
10407
|
const headers = {
|
|
10523
10408
|
Authorization: `Bearer ${this.opts.token}`,
|
|
@@ -10624,6 +10509,9 @@ var CompanionSupervisor = class {
|
|
|
10624
10509
|
unrunnableRetryDelaysMs;
|
|
10625
10510
|
deviceApi = null;
|
|
10626
10511
|
deviceSub = null;
|
|
10512
|
+
// ACC8: whether the device stream is open, which is what every workspace's
|
|
10513
|
+
// link on the dashboard reports.
|
|
10514
|
+
deviceConnected = false;
|
|
10627
10515
|
recoveryChecked = false;
|
|
10628
10516
|
recovering = false;
|
|
10629
10517
|
heartbeated = false;
|
|
@@ -10690,7 +10578,18 @@ var CompanionSupervisor = class {
|
|
|
10690
10578
|
token: this.config.deviceToken,
|
|
10691
10579
|
log: this.log,
|
|
10692
10580
|
lastEventId: null,
|
|
10693
|
-
onOpen: () =>
|
|
10581
|
+
onOpen: () => {
|
|
10582
|
+
this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ?? hostname2()}`);
|
|
10583
|
+
this.deviceConnected = true;
|
|
10584
|
+
for (const wr of this.workspaces.values()) {
|
|
10585
|
+
this.hub.setConnected(wr.workspaceId, true);
|
|
10586
|
+
for (const a of wr.agents.values()) a.kickDrain();
|
|
10587
|
+
}
|
|
10588
|
+
},
|
|
10589
|
+
onClose: () => {
|
|
10590
|
+
this.deviceConnected = false;
|
|
10591
|
+
for (const wr of this.workspaces.values()) this.hub.setConnected(wr.workspaceId, false);
|
|
10592
|
+
},
|
|
10694
10593
|
onMessage: async (ev) => {
|
|
10695
10594
|
if (ev.event === "assignments_changed") {
|
|
10696
10595
|
if (this.refreshing && this.inFlightRefresh) await this.inFlightRefresh;
|
|
@@ -10722,10 +10621,7 @@ var CompanionSupervisor = class {
|
|
|
10722
10621
|
// at least the loops keeping the event loop alive; the SSE promises let a
|
|
10723
10622
|
// clean teardown resolve.
|
|
10724
10623
|
finishedPromises() {
|
|
10725
|
-
return [
|
|
10726
|
-
...this.deviceSub ? [this.deviceSub.finished] : [],
|
|
10727
|
-
...[...this.workspaces.values()].flatMap((w) => w.sub ? [w.sub.finished] : [])
|
|
10728
|
-
];
|
|
10624
|
+
return [...this.deviceSub ? [this.deviceSub.finished] : []];
|
|
10729
10625
|
}
|
|
10730
10626
|
// ---- device-level loops ----
|
|
10731
10627
|
kickHeartbeat() {
|
|
@@ -10873,11 +10769,8 @@ var CompanionSupervisor = class {
|
|
|
10873
10769
|
workspaceId,
|
|
10874
10770
|
slug: first.workspaceSlug,
|
|
10875
10771
|
name: first.workspaceName,
|
|
10876
|
-
sub: null,
|
|
10877
|
-
sseAgentId: null,
|
|
10878
10772
|
agents: /* @__PURE__ */ new Map(),
|
|
10879
|
-
chains: /* @__PURE__ */ new Map()
|
|
10880
|
-
cursor: new CursorTracker(workspaceId)
|
|
10773
|
+
chains: /* @__PURE__ */ new Map()
|
|
10881
10774
|
};
|
|
10882
10775
|
this.workspaces.set(workspaceId, wr);
|
|
10883
10776
|
this.hub.registerWorkspace({
|
|
@@ -10885,6 +10778,7 @@ var CompanionSupervisor = class {
|
|
|
10885
10778
|
slug: first.workspaceSlug,
|
|
10886
10779
|
name: first.workspaceName
|
|
10887
10780
|
});
|
|
10781
|
+
this.hub.setConnected(workspaceId, this.deviceConnected);
|
|
10888
10782
|
}
|
|
10889
10783
|
const store = loadSecretStoreTolerant();
|
|
10890
10784
|
const exposed = new Set(store.names());
|
|
@@ -10944,7 +10838,7 @@ var CompanionSupervisor = class {
|
|
|
10944
10838
|
);
|
|
10945
10839
|
}
|
|
10946
10840
|
}
|
|
10947
|
-
this.
|
|
10841
|
+
if (wr.agents.size === 0) void this.removeWorkspace(wr.workspaceId);
|
|
10948
10842
|
}
|
|
10949
10843
|
addAgent(wr, it, credential, runConfig) {
|
|
10950
10844
|
const agentLog = this.log.child({ agentName: it.agentDisplayName });
|
|
@@ -10952,6 +10846,7 @@ var CompanionSupervisor = class {
|
|
|
10952
10846
|
const api = new CabaneApi({
|
|
10953
10847
|
baseUrl: this.config.baseUrl,
|
|
10954
10848
|
token: credential,
|
|
10849
|
+
...this.config.deviceToken ? { deviceToken: this.config.deviceToken } : {},
|
|
10955
10850
|
outbox,
|
|
10956
10851
|
log: agentLog
|
|
10957
10852
|
});
|
|
@@ -11153,71 +11048,18 @@ var CompanionSupervisor = class {
|
|
|
11153
11048
|
connectorHealth: this.connectorHealth
|
|
11154
11049
|
});
|
|
11155
11050
|
}
|
|
11156
|
-
// Stand up (or re-point) the workspace's single shared SSE subscription. It is
|
|
11157
|
-
// authed with one assigned agent's PAT; if that agent is unassigned, re-pick
|
|
11158
|
-
// another. With no agents left the workspace runner is torn down.
|
|
11159
|
-
ensureWorkspaceSse(wr) {
|
|
11160
|
-
if (wr.agents.size === 0) {
|
|
11161
|
-
void this.removeWorkspace(wr.workspaceId);
|
|
11162
|
-
return;
|
|
11163
|
-
}
|
|
11164
|
-
if (wr.sub && wr.sseAgentId && wr.agents.has(wr.sseAgentId)) return;
|
|
11165
|
-
const sseAgent = wr.agents.get(wr.sseAgentId ?? "") ?? [...wr.agents.values()][0];
|
|
11166
|
-
if (wr.sub) wr.sub.stop();
|
|
11167
|
-
wr.sseAgentId = sseAgent.agentId;
|
|
11168
|
-
wr.sub = new SseSubscriber({
|
|
11169
|
-
baseUrl: this.config.baseUrl,
|
|
11170
|
-
workspaceId: wr.workspaceId,
|
|
11171
|
-
token: sseAgent.credential,
|
|
11172
|
-
log: this.log,
|
|
11173
|
-
lastEventId: readCursor(wr.workspaceId),
|
|
11174
|
-
onOpen: () => {
|
|
11175
|
-
this.hub.setConnected(wr.workspaceId, true);
|
|
11176
|
-
for (const a of wr.agents.values()) a.kickDrain();
|
|
11177
|
-
},
|
|
11178
|
-
onClose: () => this.hub.setConnected(wr.workspaceId, false),
|
|
11179
|
-
onMessage: (ev) => this.handleWorkspaceMessage(wr, ev),
|
|
11180
|
-
onAuthFailure: (status) => {
|
|
11181
|
-
this.hub.setAuthFailed(wr.workspaceId);
|
|
11182
|
-
this.log.debug(
|
|
11183
|
-
{ workspaceId: wr.workspaceId, status, sseAgentId: wr.sseAgentId },
|
|
11184
|
-
"companion: workspace stream auth failed \u2014 re-pulling assignments"
|
|
11185
|
-
);
|
|
11186
|
-
wr.sseAgentId = null;
|
|
11187
|
-
void this.refreshAssignments();
|
|
11188
|
-
}
|
|
11189
|
-
});
|
|
11190
|
-
wr.sub.start();
|
|
11191
|
-
this.log.debug({ workspaceId: wr.workspaceId }, "companion: subscribed");
|
|
11192
|
-
}
|
|
11193
11051
|
async removeWorkspace(workspaceId) {
|
|
11194
11052
|
const wr = this.workspaces.get(workspaceId);
|
|
11195
11053
|
if (!wr) return;
|
|
11196
11054
|
for (const agentId of [...wr.agents.keys()]) this.removeAgent(wr, agentId);
|
|
11197
|
-
if (wr.sub) {
|
|
11198
|
-
wr.sub.stop();
|
|
11199
|
-
await wr.sub.finished;
|
|
11200
|
-
}
|
|
11201
11055
|
this.workspaces.delete(workspaceId);
|
|
11202
11056
|
this.hub.removeWorkspace(workspaceId);
|
|
11203
11057
|
}
|
|
11204
11058
|
// ---- SSE event routing (per workspace, BY agentId) ----
|
|
11205
|
-
// Fire-and-forget per event (the subscriber doesn't await us). Cancel events
|
|
11206
|
-
// route to the matching agent immediately; dispatch events serialize per
|
|
11207
|
-
// (conversation, agent); everything else just advances the cursor.
|
|
11208
|
-
async handleWorkspaceMessage(wr, ev) {
|
|
11209
|
-
if (ev.event !== "message") return;
|
|
11210
|
-
this.hub.recordEvent(wr.workspaceId);
|
|
11211
|
-
const wire = this.parseFrame(ev);
|
|
11212
|
-
if (!wire) return;
|
|
11213
|
-
if (ev.id) wr.cursor.observe(ev.id);
|
|
11214
|
-
await this.deliverFrame(wr, ev.id, wire);
|
|
11215
|
-
}
|
|
11216
11059
|
// ACC10: the device stream's frames — dispatch and cancel, addressed to this
|
|
11217
|
-
// device and authenticated as it.
|
|
11218
|
-
//
|
|
11219
|
-
//
|
|
11220
|
-
// nothing here observes or settles one.
|
|
11060
|
+
// device and authenticated as it. ACC8: the only stream this companion opens.
|
|
11061
|
+
// It keeps no cursor: it replays what is still offered on every connect
|
|
11062
|
+
// instead of resuming from a `Last-Event-ID`.
|
|
11221
11063
|
async handleDeviceMessage(ev) {
|
|
11222
11064
|
const wire = this.parseFrame(ev);
|
|
11223
11065
|
if (!wire) return;
|
|
@@ -11240,7 +11082,7 @@ var CompanionSupervisor = class {
|
|
|
11240
11082
|
}
|
|
11241
11083
|
}
|
|
11242
11084
|
this.hub.recordEvent(wr.workspaceId);
|
|
11243
|
-
await this.deliverFrame(wr,
|
|
11085
|
+
await this.deliverFrame(wr, wire);
|
|
11244
11086
|
}
|
|
11245
11087
|
parseFrame(ev) {
|
|
11246
11088
|
try {
|
|
@@ -11253,30 +11095,27 @@ var CompanionSupervisor = class {
|
|
|
11253
11095
|
return null;
|
|
11254
11096
|
}
|
|
11255
11097
|
}
|
|
11256
|
-
// ACC10: the dedupe gate
|
|
11257
|
-
//
|
|
11258
|
-
//
|
|
11259
|
-
// stream's cursor. Settling a duplicate earlier than that would advance the
|
|
11260
|
-
// low-water mark past an in-flight turn, which is exactly the CT282 bug.
|
|
11098
|
+
// ACC10: the dedupe gate. A reconnect replays every dispatch still offered,
|
|
11099
|
+
// including one whose turn is already running here; the first copy runs the
|
|
11100
|
+
// turn and a later one waits for it and then does nothing.
|
|
11261
11101
|
//
|
|
11262
11102
|
// A cancel needs no gate: `dispatcher.cancel` on an already-aborted run is a
|
|
11263
11103
|
// no-op that reports `false`, so a second copy is inert.
|
|
11264
|
-
async deliverFrame(wr,
|
|
11104
|
+
async deliverFrame(wr, wire) {
|
|
11265
11105
|
const claim = wire.type === "device:dispatch_requested" && wire.id ? this.claims.claim(wire.id) : null;
|
|
11266
11106
|
if (claim && !claim.first) {
|
|
11267
11107
|
await claim.done;
|
|
11268
|
-
if (cursorId) wr.cursor.settle(cursorId);
|
|
11269
11108
|
return;
|
|
11270
11109
|
}
|
|
11271
11110
|
try {
|
|
11272
|
-
await this.routeFrame(wr,
|
|
11111
|
+
await this.routeFrame(wr, wire);
|
|
11273
11112
|
} finally {
|
|
11274
11113
|
if (claim) this.claims.release(wire.id);
|
|
11275
11114
|
}
|
|
11276
11115
|
}
|
|
11277
11116
|
// Everything below here is the routing as it was: it takes a frame and knows
|
|
11278
11117
|
// nothing about which stream carried it.
|
|
11279
|
-
async routeFrame(wr,
|
|
11118
|
+
async routeFrame(wr, wire) {
|
|
11280
11119
|
if (wire.type === "device:cancel_requested") {
|
|
11281
11120
|
const payload2 = {
|
|
11282
11121
|
type: "device:cancel_requested",
|
|
@@ -11296,12 +11135,10 @@ var CompanionSupervisor = class {
|
|
|
11296
11135
|
);
|
|
11297
11136
|
}
|
|
11298
11137
|
}
|
|
11299
|
-
if (cursorId) wr.cursor.settle(cursorId);
|
|
11300
11138
|
return;
|
|
11301
11139
|
}
|
|
11302
11140
|
const payload = dispatchPayload(wire);
|
|
11303
11141
|
if (!payload) {
|
|
11304
|
-
if (cursorId) wr.cursor.settle(cursorId);
|
|
11305
11142
|
return;
|
|
11306
11143
|
}
|
|
11307
11144
|
if (this.deviceId && payload.deviceId && payload.deviceId !== this.deviceId) {
|
|
@@ -11314,7 +11151,6 @@ var CompanionSupervisor = class {
|
|
|
11314
11151
|
},
|
|
11315
11152
|
"companion: dispatch addressed to another device; ignoring"
|
|
11316
11153
|
);
|
|
11317
|
-
if (cursorId) wr.cursor.settle(cursorId);
|
|
11318
11154
|
return;
|
|
11319
11155
|
}
|
|
11320
11156
|
let agent = wr.agents.get(payload.agentId);
|
|
@@ -11322,16 +11158,14 @@ var CompanionSupervisor = class {
|
|
|
11322
11158
|
agent = await this.recoverRacedAgent(wr, payload);
|
|
11323
11159
|
if (!agent) {
|
|
11324
11160
|
if (this.deviceId && payload.deviceId === this.deviceId) {
|
|
11325
|
-
|
|
11326
|
-
if (!reported) return;
|
|
11161
|
+
await this.reportUnrunnableDispatch(wr.workspaceId, payload);
|
|
11327
11162
|
}
|
|
11328
|
-
if (cursorId) wr.cursor.settle(cursorId);
|
|
11329
11163
|
return;
|
|
11330
11164
|
}
|
|
11331
11165
|
}
|
|
11332
11166
|
const chainKey = `${payload.conversationId}|${payload.agentId}`;
|
|
11333
11167
|
const prev = wr.chains.get(chainKey) ?? Promise.resolve();
|
|
11334
|
-
const tail = prev.then(() => this.runDispatch(wr,
|
|
11168
|
+
const tail = prev.then(() => this.runDispatch(wr, wire.id, payload, agent)).catch((err) => {
|
|
11335
11169
|
this.log.warn(
|
|
11336
11170
|
{
|
|
11337
11171
|
workspaceId: wr.workspaceId,
|
|
@@ -11348,8 +11182,7 @@ var CompanionSupervisor = class {
|
|
|
11348
11182
|
}
|
|
11349
11183
|
// CT1146: tell the server this device cannot run a dispatch it was addressed to.
|
|
11350
11184
|
//
|
|
11351
|
-
// Returns whether the report LANDED
|
|
11352
|
-
// caller may settle the cursor. The server checks the recorded dispatch
|
|
11185
|
+
// Returns whether the report LANDED. The server checks the recorded dispatch
|
|
11353
11186
|
// destination even if the agent's assignment has moved. Reporting promptly
|
|
11354
11187
|
// explains the roster miss; the thirty-minute unanswered-offer deadline is
|
|
11355
11188
|
// only the backstop when no report lands.
|
|
@@ -11443,10 +11276,10 @@ var CompanionSupervisor = class {
|
|
|
11443
11276
|
};
|
|
11444
11277
|
const tail = this.runDispatch(
|
|
11445
11278
|
wr,
|
|
11446
|
-
// No frame delivered this one — it came off `/runs` — so there is
|
|
11447
|
-
//
|
|
11448
|
-
//
|
|
11449
|
-
|
|
11279
|
+
// No frame delivered this one — it came off `/runs` — so there is no
|
|
11280
|
+
// dispatch id to dedupe on. The `recoveredTurns` set above is this
|
|
11281
|
+
// path's own guard.
|
|
11282
|
+
"",
|
|
11450
11283
|
payload,
|
|
11451
11284
|
agent,
|
|
11452
11285
|
run.turnId
|
|
@@ -11468,61 +11301,55 @@ var CompanionSupervisor = class {
|
|
|
11468
11301
|
}
|
|
11469
11302
|
}
|
|
11470
11303
|
// One turn: the SJ516 F7 / CT26 skip-vs-resume dedupe, the dispatch, then
|
|
11471
|
-
//
|
|
11304
|
+
// the console summary. Mirrors the old per-workspace `handleEvent`, now
|
|
11472
11305
|
// scoped to one agent.
|
|
11473
|
-
async runDispatch(wr,
|
|
11306
|
+
async runDispatch(wr, dispatchId, payload, agent, recoveredTurnId) {
|
|
11474
11307
|
const workspaceId = wr.workspaceId;
|
|
11475
|
-
|
|
11476
|
-
if (
|
|
11477
|
-
};
|
|
11478
|
-
if (ids.dispatchId && hasDispatched(workspaceId, ids.dispatchId)) {
|
|
11479
|
-
if (hasCompleted(workspaceId, ids.dispatchId)) {
|
|
11308
|
+
if (dispatchId && hasDispatched(workspaceId, dispatchId)) {
|
|
11309
|
+
if (hasCompleted(workspaceId, dispatchId)) {
|
|
11480
11310
|
this.log.debug(
|
|
11481
|
-
{ workspaceId, eventId:
|
|
11311
|
+
{ workspaceId, eventId: dispatchId },
|
|
11482
11312
|
"companion: skipping already-completed event (resume after restart)"
|
|
11483
11313
|
);
|
|
11484
|
-
settleCursor();
|
|
11485
11314
|
return;
|
|
11486
11315
|
}
|
|
11487
11316
|
if (noResume()) {
|
|
11488
11317
|
this.log.debug(
|
|
11489
|
-
{ workspaceId, eventId:
|
|
11318
|
+
{ workspaceId, eventId: dispatchId },
|
|
11490
11319
|
"companion: skipping interrupted turn (CABANE_COMPANION_NO_RESUME=1) \u2014 resume disabled for this boot"
|
|
11491
11320
|
);
|
|
11492
|
-
markCompleted(workspaceId,
|
|
11493
|
-
settleCursor();
|
|
11321
|
+
markCompleted(workspaceId, dispatchId);
|
|
11494
11322
|
return;
|
|
11495
11323
|
}
|
|
11496
|
-
const attempt = bumpResumeAttempt(workspaceId,
|
|
11324
|
+
const attempt = bumpResumeAttempt(workspaceId, dispatchId);
|
|
11497
11325
|
if (attempt > MAX_RESUME_ATTEMPTS) {
|
|
11498
11326
|
this.log.error(
|
|
11499
|
-
{ workspaceId, eventId:
|
|
11327
|
+
{ workspaceId, eventId: dispatchId, attempt, cap: MAX_RESUME_ATTEMPTS },
|
|
11500
11328
|
"An interrupted turn couldn't finish after several attempts. Reply in Cabane to try again."
|
|
11501
11329
|
);
|
|
11502
|
-
markCompleted(workspaceId,
|
|
11503
|
-
settleCursor();
|
|
11330
|
+
markCompleted(workspaceId, dispatchId);
|
|
11504
11331
|
return;
|
|
11505
11332
|
}
|
|
11506
11333
|
this.log.debug(
|
|
11507
|
-
{ workspaceId, eventId:
|
|
11334
|
+
{ workspaceId, eventId: dispatchId, attempt, cap: MAX_RESUME_ATTEMPTS },
|
|
11508
11335
|
"companion: re-dispatching interrupted turn (resume after restart)"
|
|
11509
11336
|
);
|
|
11510
11337
|
}
|
|
11511
|
-
const resumedTurnId = recoveredTurnId ?? (
|
|
11338
|
+
const resumedTurnId = recoveredTurnId ?? (dispatchId ? turnIdForEvent(workspaceId, dispatchId) : null);
|
|
11512
11339
|
const turnId = resumedTurnId ?? randomUUID4();
|
|
11513
|
-
if (
|
|
11514
|
-
const liveTurnIds = rememberTurnId(workspaceId,
|
|
11340
|
+
if (dispatchId) {
|
|
11341
|
+
const liveTurnIds = rememberTurnId(workspaceId, dispatchId, turnId);
|
|
11515
11342
|
if (liveTurnIds > TURN_ID_OVERFLOW_WARN) {
|
|
11516
11343
|
this.log.debug(
|
|
11517
11344
|
{ workspaceId, liveTurnIds, threshold: TURN_ID_OVERFLOW_WARN },
|
|
11518
11345
|
"companion: in-flight turn-id map is implausibly large \u2014 completion pruning is likely broken. Keeping every mapping; dropping one would silently lose a resumable turn."
|
|
11519
11346
|
);
|
|
11520
11347
|
}
|
|
11521
|
-
markDispatched(workspaceId,
|
|
11348
|
+
markDispatched(workspaceId, dispatchId);
|
|
11522
11349
|
}
|
|
11523
11350
|
if (resumedTurnId) {
|
|
11524
11351
|
this.log.debug(
|
|
11525
|
-
{ workspaceId, eventId:
|
|
11352
|
+
{ workspaceId, eventId: dispatchId, turnId },
|
|
11526
11353
|
"companion: resuming an interrupted turn under its original id"
|
|
11527
11354
|
);
|
|
11528
11355
|
}
|
|
@@ -11537,8 +11364,7 @@ var CompanionSupervisor = class {
|
|
|
11537
11364
|
turnId,
|
|
11538
11365
|
resumed: resumedTurnId !== null
|
|
11539
11366
|
});
|
|
11540
|
-
if (
|
|
11541
|
-
settleCursor();
|
|
11367
|
+
if (dispatchId) markCompleted(workspaceId, dispatchId);
|
|
11542
11368
|
const durationS = (result.durationMs / 1e3).toFixed(1);
|
|
11543
11369
|
const bindings = {
|
|
11544
11370
|
...turnBindings,
|
|
@@ -11787,12 +11613,10 @@ var CompanionSupervisor = class {
|
|
|
11787
11613
|
this.deviceSub?.stop();
|
|
11788
11614
|
for (const wr of this.workspaces.values()) {
|
|
11789
11615
|
for (const a of wr.agents.values()) a.cancelDrain();
|
|
11790
|
-
if (wr.sub) wr.sub.stop();
|
|
11791
11616
|
}
|
|
11792
11617
|
await Promise.all([
|
|
11793
11618
|
...heartbeat ? [waitForBoundedHeartbeat(heartbeat)] : [],
|
|
11794
|
-
...this.deviceSub ? [this.deviceSub.finished] : []
|
|
11795
|
-
...[...this.workspaces.values()].flatMap((wr) => wr.sub ? [wr.sub.finished] : [])
|
|
11619
|
+
...this.deviceSub ? [this.deviceSub.finished] : []
|
|
11796
11620
|
]);
|
|
11797
11621
|
}
|
|
11798
11622
|
async requestStop() {
|
|
@@ -11889,22 +11713,22 @@ function handleUncaught(log, err, origin) {
|
|
|
11889
11713
|
}
|
|
11890
11714
|
|
|
11891
11715
|
// src/crash-marker.ts
|
|
11892
|
-
import { existsSync as
|
|
11893
|
-
import { join as
|
|
11716
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync10, readFileSync as readFileSync9, rmSync as rmSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
11717
|
+
import { join as join16 } from "path";
|
|
11894
11718
|
function crashMarkerPath() {
|
|
11895
|
-
return
|
|
11719
|
+
return join16(cabaneDir(), "last-error.json");
|
|
11896
11720
|
}
|
|
11897
11721
|
function recordCrash(rec) {
|
|
11898
11722
|
try {
|
|
11899
|
-
|
|
11900
|
-
|
|
11723
|
+
mkdirSync10(cabaneDir(), { recursive: true });
|
|
11724
|
+
writeFileSync7(crashMarkerPath(), JSON.stringify(rec, null, 2) + "\n");
|
|
11901
11725
|
} catch {
|
|
11902
11726
|
}
|
|
11903
11727
|
}
|
|
11904
11728
|
function clearCrash() {
|
|
11905
11729
|
try {
|
|
11906
11730
|
const path = crashMarkerPath();
|
|
11907
|
-
if (
|
|
11731
|
+
if (existsSync12(path)) rmSync8(path, { force: true });
|
|
11908
11732
|
} catch {
|
|
11909
11733
|
}
|
|
11910
11734
|
}
|