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