@cabane/companion 0.6.120 → 0.6.122
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 +11821 -10160
- package/dist/pairing-config.js +10 -0
- package/dist/runtime.js +1437 -925
- package/package.json +2 -1
package/dist/runtime.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/runtime.ts
|
|
2
|
-
import { randomUUID as
|
|
2
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
3
3
|
|
|
4
4
|
// src/config.ts
|
|
5
5
|
import {
|
|
@@ -352,8 +352,24 @@ var companionConfigSchema = z2.object({
|
|
|
352
352
|
// half-installed platform package could not start.
|
|
353
353
|
codex: z2.object({
|
|
354
354
|
enabled: z2.boolean().optional()
|
|
355
|
+
}).strict().optional(),
|
|
356
|
+
// CT1679: run each turn in a child runner process instead of in this one.
|
|
357
|
+
// Absent, or `enabled: false`, is the in-process path, untouched. `sandbox`
|
|
358
|
+
// wraps the runner in an OS sandbox where the host supports one; `'none'`
|
|
359
|
+
// (the default) runs it as a plain child beside this process, a supported
|
|
360
|
+
// configuration in its own right rather than a degraded one.
|
|
361
|
+
runners: z2.object({
|
|
362
|
+
enabled: z2.boolean().optional(),
|
|
363
|
+
provider: z2.literal("local-process").optional(),
|
|
364
|
+
sandbox: z2.enum(["os", "none"]).optional()
|
|
355
365
|
}).strict().optional()
|
|
356
366
|
});
|
|
367
|
+
function runnersEnabled(cfg) {
|
|
368
|
+
return cfg.runners?.enabled === true;
|
|
369
|
+
}
|
|
370
|
+
function runnerStateDir() {
|
|
371
|
+
return process.env.CABANE_RUNNER_STATE_DIR || cabaneDir();
|
|
372
|
+
}
|
|
357
373
|
function isCodexEnabled(cfg) {
|
|
358
374
|
return !!cfg.codex && cfg.codex.enabled !== false;
|
|
359
375
|
}
|
|
@@ -1462,8 +1478,8 @@ function registerRoutes(app, deps) {
|
|
|
1462
1478
|
return c.json({ dispatches: hub.recentDispatches(limit).map(dispatchToWire) });
|
|
1463
1479
|
});
|
|
1464
1480
|
app.get("/api/logs", async (c) => {
|
|
1465
|
-
const
|
|
1466
|
-
return c.json({ lines: tailFile(companionLogPath(),
|
|
1481
|
+
const lines2 = clampLimit(c.req.query("lines"), 200, 1e3);
|
|
1482
|
+
return c.json({ lines: tailFile(companionLogPath(), lines2) });
|
|
1467
1483
|
});
|
|
1468
1484
|
app.post("/api/settings", async (c) => {
|
|
1469
1485
|
const body = await readJson(c);
|
|
@@ -1582,7 +1598,7 @@ function clampLimit(raw, fallback, max = 200) {
|
|
|
1582
1598
|
if (!Number.isFinite(n) || n <= 0) return fallback;
|
|
1583
1599
|
return Math.min(Math.floor(n), max);
|
|
1584
1600
|
}
|
|
1585
|
-
function tailFile(path,
|
|
1601
|
+
function tailFile(path, lines2) {
|
|
1586
1602
|
if (!existsSync2(path)) return [];
|
|
1587
1603
|
const MAX_BYTES = 256 * 1024;
|
|
1588
1604
|
let fd;
|
|
@@ -1597,7 +1613,7 @@ function tailFile(path, lines) {
|
|
|
1597
1613
|
const text = buf.toString("utf8");
|
|
1598
1614
|
const all = text.split("\n");
|
|
1599
1615
|
if (start > 0 && all.length > 0) all.shift();
|
|
1600
|
-
return all.filter((l) => l.length > 0).slice(-
|
|
1616
|
+
return all.filter((l) => l.length > 0).slice(-lines2);
|
|
1601
1617
|
} catch {
|
|
1602
1618
|
return [];
|
|
1603
1619
|
} finally {
|
|
@@ -2278,7 +2294,7 @@ import { randomUUID } from "crypto";
|
|
|
2278
2294
|
var bootId = randomUUID();
|
|
2279
2295
|
|
|
2280
2296
|
// src/supervisor.ts
|
|
2281
|
-
import { randomUUID as
|
|
2297
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
2282
2298
|
|
|
2283
2299
|
// src/api.ts
|
|
2284
2300
|
var DEVICE_HEADER = "x-cabane-device";
|
|
@@ -2446,27 +2462,6 @@ var CabaneApi = class {
|
|
|
2446
2462
|
outboxSize() {
|
|
2447
2463
|
return this.opts.outbox?.size() ?? 0;
|
|
2448
2464
|
}
|
|
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
2465
|
// CT262: the ONE turn-context fetch. Collapses the companion's old four-fetch
|
|
2471
2466
|
// choreography (getConversation + getMessage + getAgentSelf + getParticipantAgent)
|
|
2472
2467
|
// into a single call: the server composes the whole server portion of the
|
|
@@ -2746,34 +2741,6 @@ var CabaneApi = class {
|
|
|
2746
2741
|
signal
|
|
2747
2742
|
);
|
|
2748
2743
|
}
|
|
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
2744
|
};
|
|
2778
2745
|
function isRetryable(err) {
|
|
2779
2746
|
if (err instanceof ApiError) return err.status >= 500;
|
|
@@ -2967,78 +2934,21 @@ function pruneCredentials(keepAgentIds) {
|
|
|
2967
2934
|
return removed;
|
|
2968
2935
|
}
|
|
2969
2936
|
|
|
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
2937
|
// src/dispatch-dedupe.ts
|
|
3028
|
-
import { mkdirSync as
|
|
3029
|
-
import { join as
|
|
2938
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync6 } from "fs";
|
|
2939
|
+
import { join as join9 } from "path";
|
|
3030
2940
|
var MAX_IDS = 256;
|
|
3031
2941
|
function dir(log) {
|
|
3032
|
-
return
|
|
2942
|
+
return join9(cabaneDir(), log);
|
|
3033
2943
|
}
|
|
3034
|
-
function
|
|
3035
|
-
return
|
|
2944
|
+
function pathFor(log, workspaceId) {
|
|
2945
|
+
return join9(dir(log), encodeURIComponent(workspaceId));
|
|
3036
2946
|
}
|
|
3037
2947
|
function readIds(log, workspaceId) {
|
|
3038
|
-
const path =
|
|
3039
|
-
if (!
|
|
2948
|
+
const path = pathFor(log, workspaceId);
|
|
2949
|
+
if (!existsSync6(path)) return [];
|
|
3040
2950
|
try {
|
|
3041
|
-
return
|
|
2951
|
+
return readFileSync4(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
3042
2952
|
} catch {
|
|
3043
2953
|
return [];
|
|
3044
2954
|
}
|
|
@@ -3051,8 +2961,8 @@ function mark(log, workspaceId, eventId) {
|
|
|
3051
2961
|
if (ids.includes(eventId)) return;
|
|
3052
2962
|
ids.push(eventId);
|
|
3053
2963
|
const trimmed = ids.length > MAX_IDS ? ids.slice(-MAX_IDS) : ids;
|
|
3054
|
-
|
|
3055
|
-
|
|
2964
|
+
mkdirSync6(dir(log), { recursive: true });
|
|
2965
|
+
writeFileSync4(pathFor(log, workspaceId), trimmed.join("\n") + "\n", "utf8");
|
|
3056
2966
|
}
|
|
3057
2967
|
function hasDispatched(workspaceId, eventId) {
|
|
3058
2968
|
return has("dispatched", workspaceId, eventId);
|
|
@@ -3069,17 +2979,17 @@ function markCompleted(workspaceId, eventId) {
|
|
|
3069
2979
|
}
|
|
3070
2980
|
var MAX_RESUME_ATTEMPTS = 3;
|
|
3071
2981
|
function resumeDir() {
|
|
3072
|
-
return
|
|
2982
|
+
return join9(cabaneDir(), "resume-attempts");
|
|
3073
2983
|
}
|
|
3074
2984
|
function resumePathFor(workspaceId) {
|
|
3075
|
-
return
|
|
2985
|
+
return join9(resumeDir(), encodeURIComponent(workspaceId));
|
|
3076
2986
|
}
|
|
3077
2987
|
function readResumeCounts(workspaceId) {
|
|
3078
2988
|
const out = /* @__PURE__ */ new Map();
|
|
3079
2989
|
const path = resumePathFor(workspaceId);
|
|
3080
|
-
if (!
|
|
2990
|
+
if (!existsSync6(path)) return out;
|
|
3081
2991
|
try {
|
|
3082
|
-
for (const line of
|
|
2992
|
+
for (const line of readFileSync4(path, "utf8").split("\n")) {
|
|
3083
2993
|
const trimmed = line.trim();
|
|
3084
2994
|
if (!trimmed) continue;
|
|
3085
2995
|
const tab = trimmed.lastIndexOf(" ");
|
|
@@ -3099,8 +3009,8 @@ function bumpResumeAttempt(workspaceId, eventId) {
|
|
|
3099
3009
|
counts.set(eventId, next);
|
|
3100
3010
|
const entries = [...counts.entries()];
|
|
3101
3011
|
const trimmed = entries.length > MAX_IDS ? entries.slice(-MAX_IDS) : entries;
|
|
3102
|
-
|
|
3103
|
-
|
|
3012
|
+
mkdirSync6(resumeDir(), { recursive: true });
|
|
3013
|
+
writeFileSync4(
|
|
3104
3014
|
resumePathFor(workspaceId),
|
|
3105
3015
|
trimmed.map(([id, c]) => `${id} ${c}`).join("\n") + "\n",
|
|
3106
3016
|
"utf8"
|
|
@@ -3108,18 +3018,18 @@ function bumpResumeAttempt(workspaceId, eventId) {
|
|
|
3108
3018
|
return next;
|
|
3109
3019
|
}
|
|
3110
3020
|
function turnDir() {
|
|
3111
|
-
return
|
|
3021
|
+
return join9(cabaneDir(), "turns");
|
|
3112
3022
|
}
|
|
3113
3023
|
function turnPathFor(workspaceId) {
|
|
3114
|
-
return
|
|
3024
|
+
return join9(turnDir(), encodeURIComponent(workspaceId));
|
|
3115
3025
|
}
|
|
3116
3026
|
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
3027
|
function readTurnIds(workspaceId) {
|
|
3118
3028
|
const out = /* @__PURE__ */ new Map();
|
|
3119
3029
|
const path = turnPathFor(workspaceId);
|
|
3120
|
-
if (!
|
|
3030
|
+
if (!existsSync6(path)) return out;
|
|
3121
3031
|
try {
|
|
3122
|
-
for (const line of
|
|
3032
|
+
for (const line of readFileSync4(path, "utf8").split("\n")) {
|
|
3123
3033
|
const trimmed = line.trim();
|
|
3124
3034
|
if (!trimmed) continue;
|
|
3125
3035
|
const tab = trimmed.lastIndexOf(" ");
|
|
@@ -3139,10 +3049,10 @@ function turnIdForEvent(workspaceId, eventId) {
|
|
|
3139
3049
|
var TURN_ID_OVERFLOW_WARN = 8192;
|
|
3140
3050
|
function writeTurnIds(workspaceId, turns) {
|
|
3141
3051
|
const entries = [...turns.entries()];
|
|
3142
|
-
|
|
3052
|
+
mkdirSync6(turnDir(), { recursive: true });
|
|
3143
3053
|
const path = turnPathFor(workspaceId);
|
|
3144
3054
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
3145
|
-
|
|
3055
|
+
writeFileSync4(tmp, entries.map(([id, t]) => `${id} ${t}`).join("\n") + "\n", "utf8");
|
|
3146
3056
|
renameSync3(tmp, path);
|
|
3147
3057
|
return entries.length;
|
|
3148
3058
|
}
|
|
@@ -7917,140 +7827,872 @@ var ConnectorHealthStore = class {
|
|
|
7917
7827
|
};
|
|
7918
7828
|
|
|
7919
7829
|
// src/dispatcher.ts
|
|
7920
|
-
import {
|
|
7921
|
-
import {
|
|
7830
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
7831
|
+
import { existsSync as existsSync10, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
7832
|
+
import { join as join14 } from "path";
|
|
7922
7833
|
|
|
7923
|
-
// src/
|
|
7834
|
+
// src/runner/launch.ts
|
|
7924
7835
|
import { createInterface } from "readline";
|
|
7925
|
-
import { homedir as homedir2 } from "os";
|
|
7926
|
-
import pc from "picocolors";
|
|
7927
|
-
function tildePath(path) {
|
|
7928
|
-
const home = homedir2();
|
|
7929
|
-
if (home && path.startsWith(home + "/")) return `~${path.slice(home.length)}`;
|
|
7930
|
-
return path;
|
|
7931
|
-
}
|
|
7932
7836
|
|
|
7933
|
-
// src/
|
|
7934
|
-
|
|
7935
|
-
|
|
7837
|
+
// src/runner/opencode-lock.ts
|
|
7838
|
+
function opencodeLockKey(serverUrl) {
|
|
7839
|
+
return `opencode:${serverUrl}`;
|
|
7840
|
+
}
|
|
7841
|
+
async function stopRemoteOpencodeSession(serverUrl, sessionId, opts = {}) {
|
|
7842
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
7843
|
+
const base = serverUrl.replace(/\/+$/, "");
|
|
7844
|
+
const id = encodeURIComponent(sessionId);
|
|
7845
|
+
try {
|
|
7846
|
+
await doFetch(`${base}/session/${id}/abort`, {
|
|
7847
|
+
method: "POST",
|
|
7848
|
+
signal: AbortSignal.timeout(5e3)
|
|
7849
|
+
});
|
|
7850
|
+
} catch {
|
|
7851
|
+
}
|
|
7852
|
+
const attempts = opts.attempts ?? 20;
|
|
7853
|
+
for (let i = 0; i < attempts; i++) {
|
|
7854
|
+
try {
|
|
7855
|
+
const res = await doFetch(`${base}/session/status`, { signal: AbortSignal.timeout(5e3) });
|
|
7856
|
+
if (res.ok) {
|
|
7857
|
+
const status = await res.json();
|
|
7858
|
+
const state = status[sessionId];
|
|
7859
|
+
if (!state || state.type === "idle") return true;
|
|
7860
|
+
}
|
|
7861
|
+
} catch {
|
|
7862
|
+
}
|
|
7863
|
+
await new Promise((resolve2) => setTimeout(resolve2, opts.pollMs ?? 500));
|
|
7864
|
+
}
|
|
7865
|
+
return false;
|
|
7866
|
+
}
|
|
7936
7867
|
|
|
7937
|
-
// src/turn-
|
|
7938
|
-
import {
|
|
7939
|
-
import {
|
|
7940
|
-
|
|
7941
|
-
var
|
|
7942
|
-
var
|
|
7943
|
-
var
|
|
7944
|
-
var
|
|
7945
|
-
var
|
|
7946
|
-
|
|
7947
|
-
|
|
7948
|
-
|
|
7949
|
-
var CANCEL_WAKE_TOOL = "cancel_wake";
|
|
7950
|
-
var CANCEL_WAKE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${CANCEL_WAKE_TOOL}`;
|
|
7951
|
-
var TURN_ACT_TIMEOUT_MS = 45e3;
|
|
7952
|
-
function createSkipState() {
|
|
7953
|
-
return { skipped: false, reason: null };
|
|
7868
|
+
// src/turn-containment.ts
|
|
7869
|
+
import { execFile, execFileSync, spawn as spawn4 } from "child_process";
|
|
7870
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
7871
|
+
import { promisify } from "util";
|
|
7872
|
+
var execFileAsync = promisify(execFile);
|
|
7873
|
+
var asContained = (child) => child;
|
|
7874
|
+
var CONTAINMENT_STOP_GRACE_SEC = 5;
|
|
7875
|
+
var FALLBACK_GRACE_MS = CONTAINMENT_STOP_GRACE_SEC * 1e3;
|
|
7876
|
+
var CAPABILITY_PROBE_TIMEOUT_MS = 4e3;
|
|
7877
|
+
function turnScopeUnit(turnId, attempt) {
|
|
7878
|
+
const safe3 = turnId.replace(/[^A-Za-z0-9_.-]/g, "-");
|
|
7879
|
+
return `cabane-turn-${safe3}-${attempt}`;
|
|
7954
7880
|
}
|
|
7955
|
-
function
|
|
7956
|
-
|
|
7881
|
+
async function reapTurnScopes(turnId) {
|
|
7882
|
+
if (process.platform !== "linux") return { failures: [] };
|
|
7883
|
+
const pattern = `${turnScopeUnit(turnId, 0).replace(/-0$/, "")}-*.scope`;
|
|
7884
|
+
try {
|
|
7885
|
+
await execFileAsync("systemctl", ["--user", "stop", pattern], {
|
|
7886
|
+
timeout: (CONTAINMENT_STOP_GRACE_SEC + 10) * 1e3
|
|
7887
|
+
});
|
|
7888
|
+
return { failures: [] };
|
|
7889
|
+
} catch (err) {
|
|
7890
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7891
|
+
return classifyStopError(message) === "absent" ? { failures: [] } : { failures: [message.slice(0, 200)] };
|
|
7892
|
+
}
|
|
7957
7893
|
}
|
|
7958
|
-
|
|
7959
|
-
|
|
7894
|
+
function scopeCapabilityUsable(env, timeoutMs = CAPABILITY_PROBE_TIMEOUT_MS) {
|
|
7895
|
+
if (process.platform !== "linux") return false;
|
|
7960
7896
|
try {
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
|
|
7965
|
-
|
|
7966
|
-
|
|
7897
|
+
execFileSync("systemd-run", ["--user", "--scope", "--quiet", "--collect", "--", "/bin/true"], {
|
|
7898
|
+
env,
|
|
7899
|
+
timeout: timeoutMs,
|
|
7900
|
+
stdio: "ignore"
|
|
7901
|
+
});
|
|
7902
|
+
return true;
|
|
7903
|
+
} catch {
|
|
7904
|
+
return false;
|
|
7905
|
+
}
|
|
7906
|
+
}
|
|
7907
|
+
function classifyStopError(message) {
|
|
7908
|
+
if (/failed to connect to bus|failed to get d-?bus connection|connection refused|refusing to operate|spawn systemctl|systemctl.*ENOENT|ENOENT.*systemctl/i.test(
|
|
7909
|
+
message
|
|
7910
|
+
)) {
|
|
7911
|
+
return "failure";
|
|
7912
|
+
}
|
|
7913
|
+
if (/not loaded|could not be found|no such unit|unit .* not found/i.test(message))
|
|
7914
|
+
return "absent";
|
|
7915
|
+
return "failure";
|
|
7916
|
+
}
|
|
7917
|
+
function currentSystemdUnit(cgroupText) {
|
|
7918
|
+
for (const line of cgroupText.split("\n")) {
|
|
7919
|
+
const path = line.trim().split(":").pop();
|
|
7920
|
+
if (!path?.startsWith("/")) continue;
|
|
7921
|
+
const leaf = path.split("/").filter(Boolean).pop();
|
|
7922
|
+
if (leaf && (leaf.endsWith(".service") || leaf.endsWith(".scope"))) return leaf;
|
|
7923
|
+
}
|
|
7924
|
+
return null;
|
|
7925
|
+
}
|
|
7926
|
+
function readCurrentSystemdUnit() {
|
|
7927
|
+
try {
|
|
7928
|
+
return currentSystemdUnit(readFileSync5("/proc/self/cgroup", "utf8"));
|
|
7929
|
+
} catch {
|
|
7930
|
+
return null;
|
|
7931
|
+
}
|
|
7932
|
+
}
|
|
7933
|
+
function processGroupMembers(pgid) {
|
|
7934
|
+
try {
|
|
7935
|
+
const out = execFileSync("ps", ["-eo", "pid=,pgid="], { timeout: 5e3 }).toString();
|
|
7936
|
+
return {
|
|
7937
|
+
ok: true,
|
|
7938
|
+
members: out.split("\n").map((line) => /^\s*(\d+)\s+(\d+)\s*$/.exec(line)).filter((m) => m !== null).filter((m) => Number(m[2]) === pgid && Number(m[1]) !== process.pid).map((m) => Number(m[1]))
|
|
7939
|
+
};
|
|
7967
7940
|
} catch (err) {
|
|
7968
|
-
|
|
7969
|
-
|
|
7941
|
+
return { ok: false, error: (err instanceof Error ? err.message : String(err)).slice(0, 150) };
|
|
7942
|
+
}
|
|
7943
|
+
}
|
|
7944
|
+
var sleep2 = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
7945
|
+
var MANAGER_ENV_KEYS = ["DBUS_SESSION_BUS_ADDRESS", "XDG_RUNTIME_DIR", "PATH"];
|
|
7946
|
+
var managerEnvKey = (env) => MANAGER_ENV_KEYS.map((k) => `${k}=${env[k] ?? ""}`).join("\0");
|
|
7947
|
+
function createTurnContainment(turnId, deps = {}) {
|
|
7948
|
+
const probe = deps.capabilityProbe ?? scopeCapabilityUsable;
|
|
7949
|
+
const fallbackGraceMs = deps.fallbackGraceMs ?? FALLBACK_GRACE_MS;
|
|
7950
|
+
const ownUnit = (deps.ownSystemdUnit ?? readCurrentSystemdUnit)();
|
|
7951
|
+
const scopeUnits = [];
|
|
7952
|
+
const groups = [];
|
|
7953
|
+
let kind = null;
|
|
7954
|
+
let probedEnvKey = null;
|
|
7955
|
+
let attempt = 0;
|
|
7956
|
+
function scopeArgs(unit) {
|
|
7957
|
+
return [
|
|
7958
|
+
"--user",
|
|
7959
|
+
"--scope",
|
|
7960
|
+
"--quiet",
|
|
7961
|
+
"--collect",
|
|
7962
|
+
`--unit=${unit}`,
|
|
7963
|
+
`--property=TimeoutStopSec=${CONTAINMENT_STOP_GRACE_SEC}s`,
|
|
7964
|
+
// Bind the scope's life to the companion's own unit, so stopping or
|
|
7965
|
+
// restarting the companion still takes its in-flight turns with it —
|
|
7966
|
+
// which is what happens today, and what the scope would otherwise undo.
|
|
7967
|
+
// Omitted outside a unit, where there is nothing to bind to.
|
|
7968
|
+
...ownUnit ? [`--property=PartOf=${ownUnit}`] : [],
|
|
7969
|
+
"--"
|
|
7970
|
+
];
|
|
7971
|
+
}
|
|
7972
|
+
function resolveKind(env) {
|
|
7973
|
+
const envKey = managerEnvKey(env);
|
|
7974
|
+
if (kind === null || envKey !== probedEnvKey) {
|
|
7975
|
+
kind = probe(env) ? "systemd-scope" : "process-group";
|
|
7976
|
+
probedEnvKey = envKey;
|
|
7977
|
+
deps.log?.("containment: resolved strategy for this turn", {
|
|
7978
|
+
kind,
|
|
7979
|
+
platform: process.platform
|
|
7980
|
+
});
|
|
7970
7981
|
}
|
|
7971
|
-
|
|
7972
|
-
|
|
7973
|
-
|
|
7974
|
-
|
|
7982
|
+
return kind;
|
|
7983
|
+
}
|
|
7984
|
+
function spawnInScope(options) {
|
|
7985
|
+
const unit = turnScopeUnit(turnId, attempt++);
|
|
7986
|
+
scopeUnits.push({ unit, env: options.env });
|
|
7987
|
+
deps.log?.("containment: spawning the turn CLI into a systemd scope", {
|
|
7988
|
+
unit,
|
|
7989
|
+
partOf: ownUnit,
|
|
7990
|
+
command: options.command
|
|
7991
|
+
});
|
|
7992
|
+
return asContained(
|
|
7993
|
+
spawn4("systemd-run", [...scopeArgs(unit), options.command, ...options.args], {
|
|
7994
|
+
...options.cwd ? { cwd: options.cwd } : {},
|
|
7995
|
+
env: options.env,
|
|
7996
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
7997
|
+
})
|
|
7998
|
+
);
|
|
7999
|
+
}
|
|
8000
|
+
function spawnInGroup(options) {
|
|
8001
|
+
const child = spawn4(options.command, options.args, {
|
|
8002
|
+
...options.cwd ? { cwd: options.cwd } : {},
|
|
8003
|
+
env: options.env,
|
|
8004
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
8005
|
+
detached: process.platform !== "win32"
|
|
8006
|
+
});
|
|
8007
|
+
if (child.pid !== void 0) {
|
|
8008
|
+
groups.push({ pgid: child.pid, child });
|
|
8009
|
+
deps.onProcessGroup?.(child.pid);
|
|
8010
|
+
}
|
|
8011
|
+
deps.log?.("containment: no host-owned containment here \u2014 best effort only", {
|
|
8012
|
+
platform: process.platform,
|
|
8013
|
+
pid: child.pid
|
|
8014
|
+
});
|
|
8015
|
+
return asContained(child);
|
|
8016
|
+
}
|
|
8017
|
+
async function reapScopes(result) {
|
|
8018
|
+
for (const { unit, env } of scopeUnits) {
|
|
8019
|
+
try {
|
|
8020
|
+
await execFileAsync("systemctl", ["--user", "stop", `${unit}.scope`], {
|
|
8021
|
+
env,
|
|
8022
|
+
timeout: (CONTAINMENT_STOP_GRACE_SEC + 10) * 1e3
|
|
8023
|
+
});
|
|
8024
|
+
} catch (err) {
|
|
8025
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
8026
|
+
if (classifyStopError(message) === "failure") {
|
|
8027
|
+
result.failures.push({ target: unit, error: message.slice(0, 200) });
|
|
8028
|
+
continue;
|
|
8029
|
+
}
|
|
8030
|
+
result.reaped.push(unit);
|
|
8031
|
+
continue;
|
|
8032
|
+
}
|
|
8033
|
+
try {
|
|
8034
|
+
const { stdout } = await execFileAsync(
|
|
8035
|
+
"systemctl",
|
|
8036
|
+
["--user", "show", `${unit}.scope`, "--property=ActiveState", "--value"],
|
|
8037
|
+
{ env, timeout: 1e4 }
|
|
7975
8038
|
);
|
|
8039
|
+
const state = stdout.trim();
|
|
8040
|
+
if (state === "" || state === "inactive" || state === "failed") result.reaped.push(unit);
|
|
8041
|
+
else result.failures.push({ target: unit, error: `scope still ${state} after stop` });
|
|
8042
|
+
} catch (err) {
|
|
8043
|
+
result.failures.push({
|
|
8044
|
+
target: unit,
|
|
8045
|
+
error: `could not confirm the scope stopped: ${(err instanceof Error ? err.message : String(err)).slice(0, 150)}`
|
|
8046
|
+
});
|
|
7976
8047
|
}
|
|
7977
|
-
return toolError(`The act was refused (${err.status}): ${apiErrorMessage(err)}`);
|
|
7978
8048
|
}
|
|
7979
|
-
return uncertainOutcome(err instanceof Error ? err.message : String(err));
|
|
7980
8049
|
}
|
|
7981
|
-
|
|
7982
|
-
|
|
7983
|
-
|
|
7984
|
-
|
|
7985
|
-
|
|
7986
|
-
|
|
7987
|
-
|
|
7988
|
-
}
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
|
|
8005
|
-
|
|
8006
|
-
|
|
8007
|
-
|
|
8008
|
-
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
{
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8050
|
+
async function reapGroups(result) {
|
|
8051
|
+
for (const { pgid, child } of groups) {
|
|
8052
|
+
const target = String(pgid);
|
|
8053
|
+
if (process.platform === "win32") {
|
|
8054
|
+
try {
|
|
8055
|
+
if (child.exitCode === null) child.kill();
|
|
8056
|
+
result.unverified.push(target);
|
|
8057
|
+
} catch (err) {
|
|
8058
|
+
result.failures.push({
|
|
8059
|
+
target,
|
|
8060
|
+
error: (err instanceof Error ? err.message : String(err)).slice(0, 200)
|
|
8061
|
+
});
|
|
8062
|
+
}
|
|
8063
|
+
continue;
|
|
8064
|
+
}
|
|
8065
|
+
const first = processGroupMembers(pgid);
|
|
8066
|
+
if (!first.ok) {
|
|
8067
|
+
result.failures.push({
|
|
8068
|
+
target,
|
|
8069
|
+
error: `could not enumerate the process group: ${first.error}`
|
|
8070
|
+
});
|
|
8071
|
+
continue;
|
|
8072
|
+
}
|
|
8073
|
+
if (first.members.length === 0) {
|
|
8074
|
+
result.reaped.push(target);
|
|
8075
|
+
continue;
|
|
8076
|
+
}
|
|
8077
|
+
for (const pid of first.members) {
|
|
8078
|
+
try {
|
|
8079
|
+
process.kill(pid, "SIGTERM");
|
|
8080
|
+
} catch {
|
|
8081
|
+
}
|
|
8082
|
+
}
|
|
8083
|
+
await sleep2(fallbackGraceMs);
|
|
8084
|
+
const afterTerm = processGroupMembers(pgid);
|
|
8085
|
+
if (!afterTerm.ok) {
|
|
8086
|
+
result.failures.push({
|
|
8087
|
+
target,
|
|
8088
|
+
error: `could not re-enumerate the process group after SIGTERM: ${afterTerm.error}`
|
|
8089
|
+
});
|
|
8090
|
+
continue;
|
|
8091
|
+
}
|
|
8092
|
+
for (const pid of afterTerm.members) {
|
|
8093
|
+
try {
|
|
8094
|
+
process.kill(pid, "SIGKILL");
|
|
8095
|
+
} catch {
|
|
8096
|
+
}
|
|
8097
|
+
}
|
|
8098
|
+
const afterKill = processGroupMembers(pgid);
|
|
8099
|
+
if (!afterKill.ok) {
|
|
8100
|
+
result.failures.push({
|
|
8101
|
+
target,
|
|
8102
|
+
error: `could not confirm the process group is empty after SIGKILL: ${afterKill.error}`
|
|
8103
|
+
});
|
|
8104
|
+
continue;
|
|
8105
|
+
}
|
|
8106
|
+
if (afterKill.members.length)
|
|
8107
|
+
result.failures.push({
|
|
8108
|
+
target,
|
|
8109
|
+
error: `${afterKill.members.length} process(es) still in the group after SIGKILL`
|
|
8110
|
+
});
|
|
8111
|
+
else result.reaped.push(target);
|
|
8112
|
+
}
|
|
8113
|
+
}
|
|
8114
|
+
return {
|
|
8115
|
+
get kind() {
|
|
8116
|
+
return kind;
|
|
8117
|
+
},
|
|
8118
|
+
spawn(options) {
|
|
8119
|
+
return resolveKind(options.env) === "systemd-scope" ? spawnInScope(options) : spawnInGroup(options);
|
|
8120
|
+
},
|
|
8121
|
+
scopePrefix(env) {
|
|
8122
|
+
if (resolveKind(env) !== "systemd-scope") return null;
|
|
8123
|
+
const unit = turnScopeUnit(turnId, attempt++);
|
|
8124
|
+
scopeUnits.push({ unit, env });
|
|
8125
|
+
deps.log?.("containment: a harness will start in a systemd scope", { unit });
|
|
8126
|
+
return ["systemd-run", ...scopeArgs(unit)];
|
|
8127
|
+
},
|
|
8128
|
+
async reap() {
|
|
8129
|
+
const result = {
|
|
8130
|
+
kind: kind ?? "unused",
|
|
8131
|
+
reaped: [],
|
|
8132
|
+
failures: [],
|
|
8133
|
+
unverified: []
|
|
8134
|
+
};
|
|
8135
|
+
await reapScopes(result);
|
|
8136
|
+
await reapGroups(result);
|
|
8137
|
+
deps.log?.("containment: reap complete", {
|
|
8138
|
+
kind: result.kind,
|
|
8139
|
+
reaped: result.reaped,
|
|
8140
|
+
failures: result.failures
|
|
8141
|
+
});
|
|
8142
|
+
return result;
|
|
8143
|
+
}
|
|
8144
|
+
};
|
|
8145
|
+
}
|
|
8146
|
+
|
|
8147
|
+
// src/runner/protocol.ts
|
|
8148
|
+
import { z as z14 } from "zod";
|
|
8149
|
+
|
|
8150
|
+
// src/run-config.ts
|
|
8151
|
+
import { z as z13 } from "zod";
|
|
8152
|
+
var mcpStdioServerSchema = z13.object({
|
|
8153
|
+
type: z13.literal("stdio").optional(),
|
|
8154
|
+
command: z13.string().min(1),
|
|
8155
|
+
args: z13.array(z13.string()).optional(),
|
|
8156
|
+
env: z13.record(z13.string(), z13.string()).optional()
|
|
8157
|
+
});
|
|
8158
|
+
var mcpHttpServerSchema = z13.object({
|
|
8159
|
+
type: z13.literal("http"),
|
|
8160
|
+
url: z13.string().url(),
|
|
8161
|
+
headers: z13.record(z13.string(), z13.string()).optional()
|
|
8162
|
+
});
|
|
8163
|
+
var mcpSseServerSchema = z13.object({
|
|
8164
|
+
type: z13.literal("sse"),
|
|
8165
|
+
url: z13.string().url(),
|
|
8166
|
+
headers: z13.record(z13.string(), z13.string()).optional()
|
|
8167
|
+
});
|
|
8168
|
+
var mcpServerDefSchema = z13.union([mcpHttpServerSchema, mcpSseServerSchema, mcpStdioServerSchema]);
|
|
8169
|
+
var thinkingConfigSchema = z13.discriminatedUnion("type", [
|
|
8170
|
+
z13.object({ type: z13.literal("adaptive") }),
|
|
8171
|
+
z13.object({ type: z13.literal("enabled"), budgetTokens: z13.number().int().positive().optional() }),
|
|
8172
|
+
z13.object({ type: z13.literal("disabled") })
|
|
8173
|
+
]);
|
|
8174
|
+
var effortSchema = z13.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
8175
|
+
var runConfigSchema = z13.object({
|
|
8176
|
+
// CT788: the host-access binary replaced the `assistant`/`coding`/`custom` mode
|
|
8177
|
+
// trio + its custom tool lists — `true` grants the host filesystem/shell, absent
|
|
8178
|
+
// is the locked surface. Kept in lockstep with `@cabane/shared`'s
|
|
8179
|
+
// `agentRunConfigSchema` (independent zod, forward-compatible: unknown keys are
|
|
8180
|
+
// stripped, so an older companion riding a newer server never rejects the config).
|
|
8181
|
+
hostAccess: z13.boolean().optional(),
|
|
8182
|
+
mcpServers: z13.record(z13.string(), mcpServerDefSchema).optional(),
|
|
8183
|
+
model: z13.string().min(1).optional(),
|
|
8184
|
+
thinking: thinkingConfigSchema.optional(),
|
|
8185
|
+
effort: effortSchema.optional()
|
|
8186
|
+
});
|
|
8187
|
+
var EMPTY_RUN_CONFIG = {};
|
|
8188
|
+
function parseRunConfig(raw, onWarn) {
|
|
8189
|
+
const result = runConfigSchema.safeParse(raw ?? {});
|
|
8190
|
+
if (result.success) return result.data;
|
|
8191
|
+
onWarn?.(
|
|
8192
|
+
`run-config from Cabane didn't parse (${result.error.issues.map((i) => i.path.join(".") || "(root)").join(", ")}) \u2014 falling back to the locked assistant default`
|
|
8193
|
+
);
|
|
8194
|
+
return EMPTY_RUN_CONFIG;
|
|
8195
|
+
}
|
|
8196
|
+
|
|
8197
|
+
// src/runner/protocol.ts
|
|
8198
|
+
var RUNNER_ENV = {
|
|
8199
|
+
// The wire the runner speaks.
|
|
8200
|
+
url: "CABANE_URL",
|
|
8201
|
+
// The dispatch as the device stream delivered it.
|
|
8202
|
+
dispatch: "CABANE_DISPATCH",
|
|
8203
|
+
// The turn id the launcher resolved, and whether this is a replay of a turn
|
|
8204
|
+
// an earlier process started. The seq floor for a replay is derived inside
|
|
8205
|
+
// the turn from the outbox and the server, as it always was.
|
|
8206
|
+
turnId: "CABANE_TURN_ID",
|
|
8207
|
+
resumed: "CABANE_RESUMED",
|
|
8208
|
+
// Everything else the turn is constructed from, as JSON: see `RunnerSpec`.
|
|
8209
|
+
spec: "CABANE_RUNNER_SPEC"
|
|
8210
|
+
};
|
|
8211
|
+
var runtimeHealthSchema = z14.object({
|
|
8212
|
+
quotaState: z14.enum(["ok", "limited", "auth_expired", "unknown"]),
|
|
8213
|
+
limitedUntil: z14.string().nullable(),
|
|
8214
|
+
lastError: z14.string().nullable()
|
|
8215
|
+
});
|
|
8216
|
+
var runnerSpecSchema = z14.object({
|
|
8217
|
+
deviceId: z14.string().optional(),
|
|
8218
|
+
// The launcher's boot id: the one its heartbeat carries, so a runner's lease
|
|
8219
|
+
// matches the device that holds it.
|
|
8220
|
+
bootId: z14.string(),
|
|
8221
|
+
workspaceId: z14.string(),
|
|
8222
|
+
workspaceSlug: z14.string(),
|
|
8223
|
+
agentId: z14.string(),
|
|
8224
|
+
agentUsername: z14.string(),
|
|
8225
|
+
agentName: z14.string(),
|
|
8226
|
+
local: localAgentConfigSchema,
|
|
8227
|
+
runConfig: runConfigSchema,
|
|
8228
|
+
opencodeServerUrl: z14.string().optional(),
|
|
8229
|
+
claudeCodeAvailable: z14.boolean(),
|
|
8230
|
+
codexAvailable: z14.boolean(),
|
|
8231
|
+
idleTimeoutMs: z14.number().int().positive().optional(),
|
|
8232
|
+
totalTimeoutMs: z14.number().int().positive().optional(),
|
|
8233
|
+
leaseRenewalMs: z14.number().int().positive().optional(),
|
|
8234
|
+
preparingRowDelayMs: z14.number().int().nonnegative().optional(),
|
|
8235
|
+
// Whether the turn writes a transcript. The directory is the runner's own
|
|
8236
|
+
// (`transcriptsDir()`), so `CABANE_RUNNER_STATE_DIR` moves it with the outbox.
|
|
8237
|
+
transcripts: z14.boolean(),
|
|
8238
|
+
// Whether the harness starts under the OS sandbox. The launcher sends 'os'
|
|
8239
|
+
// only when this host can make one.
|
|
8240
|
+
sandbox: z14.enum(["os", "none"]),
|
|
8241
|
+
logLevel: z14.string(),
|
|
8242
|
+
// The launcher's connector-health observations, by runtime, as of the start.
|
|
8243
|
+
connectorHealth: z14.record(z14.string(), runtimeHealthSchema),
|
|
8244
|
+
// True when this (conversation, agent) pair was already told its session
|
|
8245
|
+
// could not be saved: the notice is once per pair, not once per process.
|
|
8246
|
+
sessionWriteNoticeClaimed: z14.boolean()
|
|
8247
|
+
});
|
|
8248
|
+
var dispatchSchema = z14.object({
|
|
8249
|
+
type: z14.literal("device:dispatch_requested"),
|
|
8250
|
+
agentId: z14.string(),
|
|
8251
|
+
deviceId: z14.string(),
|
|
8252
|
+
conversationId: z14.string(),
|
|
8253
|
+
messageId: z14.string(),
|
|
8254
|
+
episode: z14.number().int().optional()
|
|
8255
|
+
});
|
|
8256
|
+
var credentialsSchema = z14.object({
|
|
8257
|
+
agentToken: z14.string().min(1),
|
|
8258
|
+
deviceToken: z14.string().min(1).optional()
|
|
8259
|
+
});
|
|
8260
|
+
function runnerEnv(input) {
|
|
8261
|
+
return {
|
|
8262
|
+
[RUNNER_ENV.url]: input.baseUrl,
|
|
8263
|
+
[RUNNER_ENV.dispatch]: JSON.stringify(input.dispatch),
|
|
8264
|
+
[RUNNER_ENV.turnId]: input.turnId,
|
|
8265
|
+
...input.resumed ? { [RUNNER_ENV.resumed]: "1" } : {},
|
|
8266
|
+
[RUNNER_ENV.spec]: JSON.stringify(input.spec)
|
|
8267
|
+
};
|
|
8268
|
+
}
|
|
8269
|
+
function runnerStdin(input) {
|
|
8270
|
+
return `${JSON.stringify({
|
|
8271
|
+
// Phase 1 (CT1680) replaces the agent PAT with a runner credential bound to
|
|
8272
|
+
// this one dispatch; until then the child holds what the in-process turn
|
|
8273
|
+
// held, on the same machine.
|
|
8274
|
+
agentToken: input.agentToken,
|
|
8275
|
+
...input.deviceToken ? { deviceToken: input.deviceToken } : {}
|
|
8276
|
+
})}
|
|
8277
|
+
`;
|
|
8278
|
+
}
|
|
8279
|
+
function encodeLauncherMessage(message) {
|
|
8280
|
+
return `${JSON.stringify(message)}
|
|
8281
|
+
`;
|
|
8282
|
+
}
|
|
8283
|
+
function decodeRunnerEvent(line) {
|
|
8284
|
+
if (!line.startsWith("{")) return null;
|
|
8285
|
+
let value;
|
|
8286
|
+
try {
|
|
8287
|
+
value = JSON.parse(line);
|
|
8288
|
+
} catch {
|
|
8289
|
+
return null;
|
|
8290
|
+
}
|
|
8291
|
+
if (!value || typeof value !== "object" || !("cabaneRunner" in value)) return null;
|
|
8292
|
+
return value;
|
|
8293
|
+
}
|
|
8294
|
+
|
|
8295
|
+
// src/runner/launch.ts
|
|
8296
|
+
function selfRunnerCommand() {
|
|
8297
|
+
const script = process.argv[1];
|
|
8298
|
+
if (!script) throw new Error("runners: cannot tell which script started this companion");
|
|
8299
|
+
return [process.execPath, ...process.execArgv, script, "run-turn"];
|
|
8300
|
+
}
|
|
8301
|
+
function healthSnapshot(store) {
|
|
8302
|
+
const out = {};
|
|
8303
|
+
for (const r of store?.reports() ?? []) {
|
|
8304
|
+
out[r.runtime] = {
|
|
8305
|
+
quotaState: r.quotaState ?? "unknown",
|
|
8306
|
+
limitedUntil: r.limitedUntil ?? null,
|
|
8307
|
+
lastError: r.lastError ?? null
|
|
8308
|
+
};
|
|
8309
|
+
}
|
|
8310
|
+
return out;
|
|
8311
|
+
}
|
|
8312
|
+
function runnerSpec(opts, launch, duties) {
|
|
8313
|
+
return {
|
|
8314
|
+
...opts.deviceId ? { deviceId: opts.deviceId } : {},
|
|
8315
|
+
bootId,
|
|
8316
|
+
workspaceId: opts.workspaceId,
|
|
8317
|
+
workspaceSlug: opts.workspaceSlug,
|
|
8318
|
+
agentId: opts.agentId,
|
|
8319
|
+
agentUsername: opts.agentUsername,
|
|
8320
|
+
agentName: String(opts.log.bindings().agentName ?? opts.agentUsername),
|
|
8321
|
+
local: opts.local,
|
|
8322
|
+
runConfig: opts.runConfig,
|
|
8323
|
+
...opts.opencodeServerUrl ? { opencodeServerUrl: opts.opencodeServerUrl } : {},
|
|
8324
|
+
// Read now, per turn, as the in-process path reads them at adapter selection.
|
|
8325
|
+
claudeCodeAvailable: opts.claudeCodeAvailable?.() ?? true,
|
|
8326
|
+
codexAvailable: opts.codexAvailable?.() ?? false,
|
|
8327
|
+
...opts.idleTimeoutMs !== void 0 ? { idleTimeoutMs: opts.idleTimeoutMs } : {},
|
|
8328
|
+
...opts.totalTimeoutMs !== void 0 ? { totalTimeoutMs: opts.totalTimeoutMs } : {},
|
|
8329
|
+
...opts.leaseRenewalMs !== void 0 ? { leaseRenewalMs: opts.leaseRenewalMs } : {},
|
|
8330
|
+
...opts.preparingRowDelayMs !== void 0 ? { preparingRowDelayMs: opts.preparingRowDelayMs } : {},
|
|
8331
|
+
transcripts: opts.transcriptDir !== void 0,
|
|
8332
|
+
sandbox: launch.sandbox ?? "none",
|
|
8333
|
+
logLevel: opts.log.level,
|
|
8334
|
+
connectorHealth: healthSnapshot(launch.connectorHealth),
|
|
8335
|
+
sessionWriteNoticeClaimed: duties.sessionWriteNoticeClaimed()
|
|
8336
|
+
};
|
|
8337
|
+
}
|
|
8338
|
+
var PINO_LEVELS = {
|
|
8339
|
+
10: "trace",
|
|
8340
|
+
20: "debug",
|
|
8341
|
+
30: "info",
|
|
8342
|
+
40: "warn",
|
|
8343
|
+
50: "error",
|
|
8344
|
+
60: "fatal"
|
|
8345
|
+
};
|
|
8346
|
+
function relayRunnerLog(line, log) {
|
|
8347
|
+
if (!line.trim()) return;
|
|
8348
|
+
let record = null;
|
|
8349
|
+
try {
|
|
8350
|
+
const parsed = JSON.parse(line);
|
|
8351
|
+
if (parsed && typeof parsed === "object") record = parsed;
|
|
8352
|
+
} catch {
|
|
8353
|
+
record = null;
|
|
8354
|
+
}
|
|
8355
|
+
if (!record || typeof record.level !== "number") {
|
|
8356
|
+
log.debug({ runnerStderr: line }, "runner: output");
|
|
8357
|
+
return;
|
|
8358
|
+
}
|
|
8359
|
+
const {
|
|
8360
|
+
level,
|
|
8361
|
+
msg,
|
|
8362
|
+
time: _time,
|
|
8363
|
+
pid: _pid,
|
|
8364
|
+
hostname: _hostname,
|
|
8365
|
+
agentName: _agentName,
|
|
8366
|
+
...fields
|
|
8367
|
+
} = record;
|
|
8368
|
+
const method = PINO_LEVELS[level] ?? "info";
|
|
8369
|
+
log[method](fields, typeof msg === "string" ? msg : "");
|
|
8370
|
+
}
|
|
8371
|
+
function lines(stream, onLine) {
|
|
8372
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
8373
|
+
rl.on("line", onLine);
|
|
8374
|
+
return new Promise((resolve2) => rl.once("close", resolve2));
|
|
8375
|
+
}
|
|
8376
|
+
function giveBackLocks(locks, prefix, remote, launch, log) {
|
|
8377
|
+
locks.withdraw(prefix);
|
|
8378
|
+
const stop = launch.stopRemoteSession ?? ((url, id) => stopRemoteOpencodeSession(url, id));
|
|
8379
|
+
for (const { key, owner } of locks.heldBy(prefix)) {
|
|
8380
|
+
const left = remote.filter((r) => opencodeLockKey(r.serverUrl) === key);
|
|
8381
|
+
if (!left.length) {
|
|
8382
|
+
locks.release(owner);
|
|
8383
|
+
continue;
|
|
8384
|
+
}
|
|
8385
|
+
void (async () => {
|
|
8386
|
+
for (let attempt = 1; ; attempt++) {
|
|
8387
|
+
const stopped = await Promise.all(left.map((r) => stop(r.serverUrl, r.sessionId))).catch(
|
|
8388
|
+
() => [false]
|
|
8389
|
+
);
|
|
8390
|
+
if (stopped.every(Boolean)) break;
|
|
8391
|
+
if (attempt === 1)
|
|
8392
|
+
log.warn(
|
|
8393
|
+
{ key, sessions: left.map((r) => r.sessionId) },
|
|
8394
|
+
"runner: a dead runner left work on a shared server that could not be confirmed stopped; holding the server until it is"
|
|
8395
|
+
);
|
|
8396
|
+
await new Promise((resolve2) => setTimeout(resolve2, launch.reconcileRetryMs ?? 5e3));
|
|
8397
|
+
}
|
|
8398
|
+
locks.release(owner);
|
|
8399
|
+
})();
|
|
8400
|
+
}
|
|
8401
|
+
}
|
|
8402
|
+
async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
8403
|
+
const startedAt = Date.now();
|
|
8404
|
+
const { turnId } = handleOpts;
|
|
8405
|
+
const log = opts.log.child({
|
|
8406
|
+
workspaceId: opts.workspaceId,
|
|
8407
|
+
conversationId: payload.conversationId,
|
|
8408
|
+
agentId: payload.agentId,
|
|
8409
|
+
messageId: payload.messageId
|
|
8410
|
+
});
|
|
8411
|
+
const controller = new AbortController();
|
|
8412
|
+
duties.registerAbort(turnId, controller);
|
|
8413
|
+
const stopRunner = new AbortController();
|
|
8414
|
+
let ready = false;
|
|
8415
|
+
const forwardStop = () => {
|
|
8416
|
+
if (ready) stopRunner.abort();
|
|
8417
|
+
};
|
|
8418
|
+
controller.signal.addEventListener("abort", forwardStop, { once: true });
|
|
8419
|
+
const attempt = {
|
|
8420
|
+
workspaceId: opts.workspaceId,
|
|
8421
|
+
conversationId: payload.conversationId,
|
|
8422
|
+
agentId: payload.agentId,
|
|
8423
|
+
turnId
|
|
8424
|
+
};
|
|
8425
|
+
let result;
|
|
8426
|
+
let thrown;
|
|
8427
|
+
let code;
|
|
8428
|
+
let handle;
|
|
8429
|
+
let child;
|
|
8430
|
+
const groups = [];
|
|
8431
|
+
const remote = [];
|
|
8432
|
+
const lockOwner = (id) => `${turnId}#${id}`;
|
|
8433
|
+
const locks = launch.locks;
|
|
8434
|
+
try {
|
|
8435
|
+
handle = await launch.provider.acquire(attempt);
|
|
8436
|
+
const env = {};
|
|
8437
|
+
for (const [k, v] of Object.entries(launch.env ?? process.env)) {
|
|
8438
|
+
if (v !== void 0) env[k] = v;
|
|
8439
|
+
}
|
|
8440
|
+
const input = {
|
|
8441
|
+
baseUrl: opts.baseUrl,
|
|
8442
|
+
agentToken: opts.credential,
|
|
8443
|
+
...launch.deviceToken ? { deviceToken: launch.deviceToken } : {},
|
|
8444
|
+
dispatch: payload,
|
|
8445
|
+
turnId,
|
|
8446
|
+
resumed: handleOpts.resumed,
|
|
8447
|
+
spec: runnerSpec(opts, launch, duties)
|
|
8448
|
+
};
|
|
8449
|
+
Object.assign(env, runnerEnv(input));
|
|
8450
|
+
const running = launch.provider.exec(handle, launch.command, env, {
|
|
8451
|
+
signal: stopRunner.signal,
|
|
8452
|
+
stdin: runnerStdin(input)
|
|
8453
|
+
});
|
|
8454
|
+
child = running;
|
|
8455
|
+
log.debug(
|
|
8456
|
+
{ turnId, runnerPid: running.pid, provider: launch.provider.name },
|
|
8457
|
+
"runner: started"
|
|
8458
|
+
);
|
|
8459
|
+
const stdoutDone = lines(running.stdout, (line) => {
|
|
8460
|
+
const event = decodeRunnerEvent(line);
|
|
8461
|
+
if (!event) {
|
|
8462
|
+
log.debug({ runnerStdout: line }, "runner: output");
|
|
8463
|
+
return;
|
|
8464
|
+
}
|
|
8465
|
+
switch (event.cabaneRunner) {
|
|
8466
|
+
case "ready":
|
|
8467
|
+
ready = true;
|
|
8468
|
+
if (controller.signal.aborted) stopRunner.abort();
|
|
8469
|
+
return;
|
|
8470
|
+
case "start":
|
|
8471
|
+
duties.notifyStart({
|
|
8472
|
+
id: event.id,
|
|
8473
|
+
conversationId: event.conversationId,
|
|
8474
|
+
message: event.message
|
|
8475
|
+
});
|
|
8476
|
+
return;
|
|
8477
|
+
case "end": {
|
|
8478
|
+
const { cabaneRunner: _kind, ...info } = event;
|
|
8479
|
+
duties.notifyEnd(info);
|
|
8480
|
+
return;
|
|
8481
|
+
}
|
|
8482
|
+
case "session_write_notice":
|
|
8483
|
+
duties.markSessionWriteNotice();
|
|
8484
|
+
return;
|
|
8485
|
+
case "connector_health":
|
|
8486
|
+
launch.connectorHealth?.recordSettle(event.runtime, {
|
|
8487
|
+
ok: event.ok,
|
|
8488
|
+
errorReason: event.errorReason
|
|
8489
|
+
});
|
|
8490
|
+
return;
|
|
8491
|
+
case "result":
|
|
8492
|
+
result = event.result;
|
|
8493
|
+
return;
|
|
8494
|
+
case "error":
|
|
8495
|
+
thrown = event.message;
|
|
8496
|
+
return;
|
|
8497
|
+
case "lock":
|
|
8498
|
+
if (!locks) {
|
|
8499
|
+
log.warn(
|
|
8500
|
+
{ key: event.key },
|
|
8501
|
+
"runner: asked for a device lock this launcher has none of"
|
|
8502
|
+
);
|
|
8503
|
+
return;
|
|
8504
|
+
}
|
|
8505
|
+
locks.request(
|
|
8506
|
+
event.key,
|
|
8507
|
+
lockOwner(event.id),
|
|
8508
|
+
() => running.stdin.write(encodeLauncherMessage({ cabaneLauncher: "granted", id: event.id }))
|
|
8509
|
+
);
|
|
8510
|
+
return;
|
|
8511
|
+
case "unlock":
|
|
8512
|
+
locks?.release(lockOwner(event.id));
|
|
8513
|
+
return;
|
|
8514
|
+
case "process_group":
|
|
8515
|
+
groups.push(event.pgid);
|
|
8516
|
+
return;
|
|
8517
|
+
case "remote_session":
|
|
8518
|
+
remote.push({ serverUrl: event.serverUrl, sessionId: event.sessionId });
|
|
8519
|
+
return;
|
|
8520
|
+
}
|
|
8521
|
+
});
|
|
8522
|
+
const stderrDone = lines(running.stderr, (line) => relayRunnerLog(line, opts.log));
|
|
8523
|
+
code = await running.exit;
|
|
8524
|
+
await Promise.all([stdoutDone, stderrDone]);
|
|
8525
|
+
} finally {
|
|
8526
|
+
if (locks) giveBackLocks(locks, `${turnId}#`, remote, launch, log);
|
|
8527
|
+
child?.stdin.end();
|
|
8528
|
+
controller.signal.removeEventListener("abort", forwardStop);
|
|
8529
|
+
duties.releaseAbort(turnId, controller);
|
|
8530
|
+
if (handle) await launch.provider.release(handle, { destroy: false });
|
|
8531
|
+
}
|
|
8532
|
+
if (result) return result;
|
|
8533
|
+
const durationMs = Date.now() - startedAt;
|
|
8534
|
+
const reason = thrown !== void 0 ? `runner_failed: ${thrown}` : `runner_exited: ${code}`;
|
|
8535
|
+
await child?.reap(groups);
|
|
8536
|
+
const reaped = await (launch.reapTurn ?? reapTurnScopes)(turnId).catch((err) => ({
|
|
8537
|
+
failures: [err instanceof Error ? err.message : String(err)]
|
|
8538
|
+
}));
|
|
8539
|
+
if (reaped.failures.length) {
|
|
8540
|
+
log.warn(
|
|
8541
|
+
{ turnId, failures: reaped.failures },
|
|
8542
|
+
"runner: couldn't stop what the dead runner's turn left running"
|
|
8543
|
+
);
|
|
8544
|
+
}
|
|
8545
|
+
try {
|
|
8546
|
+
await opts.api.setActiveRun(opts.workspaceId, payload.conversationId, payload.agentId, {
|
|
8547
|
+
activeRunStartedAt: null,
|
|
8548
|
+
turnId,
|
|
8549
|
+
outcome: "failed",
|
|
8550
|
+
errorReason: reason.slice(0, 200),
|
|
8551
|
+
readThrough: payload.messageId
|
|
8552
|
+
});
|
|
8553
|
+
} catch (err) {
|
|
8554
|
+
log.warn(
|
|
8555
|
+
{ ...apiErrorLogFields(err), turnId },
|
|
8556
|
+
"Couldn't update the turn's status in Cabane. Check the conversation before trying again."
|
|
8557
|
+
);
|
|
8558
|
+
}
|
|
8559
|
+
if (thrown !== void 0) throw new Error(thrown);
|
|
8560
|
+
log.warn({ turnId, code }, "runner: exited without reporting a result");
|
|
8561
|
+
duties.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
8562
|
+
return { ok: false, durationMs, reason };
|
|
8563
|
+
}
|
|
8564
|
+
|
|
8565
|
+
// src/term.ts
|
|
8566
|
+
import { createInterface as createInterface2 } from "readline";
|
|
8567
|
+
import { homedir as homedir2 } from "os";
|
|
8568
|
+
import pc from "picocolors";
|
|
8569
|
+
function tildePath(path) {
|
|
8570
|
+
const home = homedir2();
|
|
8571
|
+
if (home && path.startsWith(home + "/")) return `~${path.slice(home.length)}`;
|
|
8572
|
+
return path;
|
|
8573
|
+
}
|
|
8574
|
+
|
|
8575
|
+
// src/turn-execution.ts
|
|
8576
|
+
import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
|
|
8577
|
+
import { existsSync as existsSync9 } from "fs";
|
|
8578
|
+
|
|
8579
|
+
// src/turn-control-tools.ts
|
|
8580
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
8581
|
+
import { z as z15 } from "zod";
|
|
8582
|
+
var COMPANION_LOCAL_MCP_SERVER = "cabane_companion";
|
|
8583
|
+
var COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
|
|
8584
|
+
var SKIP_TURN_TOOL = "skip_turn";
|
|
8585
|
+
var ASK_TOOL = "ask";
|
|
8586
|
+
var ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${ASK_TOOL}`;
|
|
8587
|
+
var WITHDRAW_ASK_TOOL = "withdraw_ask";
|
|
8588
|
+
var WITHDRAW_ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WITHDRAW_ASK_TOOL}`;
|
|
8589
|
+
var WAKE_ME_TOOL = "wake_me";
|
|
8590
|
+
var WAKE_ME_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WAKE_ME_TOOL}`;
|
|
8591
|
+
var CANCEL_WAKE_TOOL = "cancel_wake";
|
|
8592
|
+
var CANCEL_WAKE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${CANCEL_WAKE_TOOL}`;
|
|
8593
|
+
var TURN_ACT_TIMEOUT_MS = 45e3;
|
|
8594
|
+
function createSkipState() {
|
|
8595
|
+
return { skipped: false, reason: null };
|
|
8596
|
+
}
|
|
8597
|
+
function toolError(text) {
|
|
8598
|
+
return { isError: true, content: [{ type: "text", text }] };
|
|
8599
|
+
}
|
|
8600
|
+
async function performAct(acts, act) {
|
|
8601
|
+
let result;
|
|
8602
|
+
try {
|
|
8603
|
+
result = await acts.api.turnAct(
|
|
8604
|
+
acts.workspaceId,
|
|
8605
|
+
acts.turnId,
|
|
8606
|
+
act,
|
|
8607
|
+
AbortSignal.timeout(TURN_ACT_TIMEOUT_MS)
|
|
8608
|
+
);
|
|
8609
|
+
} catch (err) {
|
|
8610
|
+
if (err instanceof ApiError && err.status >= 500) {
|
|
8611
|
+
return uncertainOutcome(`${err.status}: ${apiErrorMessage(err)}`);
|
|
8612
|
+
}
|
|
8613
|
+
if (err instanceof ApiError) {
|
|
8614
|
+
if (err.status === 409 && apiErrorCode(err) === "no_active_turn") {
|
|
8615
|
+
return toolError(
|
|
8616
|
+
"No active turn: this turn has already settled, so the act was not performed."
|
|
8617
|
+
);
|
|
8618
|
+
}
|
|
8619
|
+
return toolError(`The act was refused (${err.status}): ${apiErrorMessage(err)}`);
|
|
8620
|
+
}
|
|
8621
|
+
return uncertainOutcome(err instanceof Error ? err.message : String(err));
|
|
8622
|
+
}
|
|
8623
|
+
if (!result.ok) return toolError(JSON.stringify(result));
|
|
8624
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
8625
|
+
}
|
|
8626
|
+
function uncertainOutcome(cause) {
|
|
8627
|
+
return toolError(
|
|
8628
|
+
`The outcome of this act could not be confirmed (${cause}). It may already have been performed, with only the response lost. Do NOT simply repeat the call: a repeat is a second, separate act, not a retry. Carry on, and say in your reply that you could not confirm it.`
|
|
8629
|
+
);
|
|
8630
|
+
}
|
|
8631
|
+
function createTurnControlMcpServer(acts, skipState) {
|
|
8632
|
+
const ACT_TOOL = { readOnlyHint: false, destructiveHint: false, openWorldHint: false };
|
|
8633
|
+
const CANCEL_TOOL = { ...ACT_TOOL, idempotentHint: true };
|
|
8634
|
+
const RECORDER_TOOL = { readOnlyHint: true, openWorldHint: false };
|
|
8635
|
+
return createSdkMcpServer({
|
|
8636
|
+
name: COMPANION_LOCAL_MCP_SERVER,
|
|
8637
|
+
version: "0.0.0",
|
|
8638
|
+
tools: [
|
|
8639
|
+
...skipState ? [
|
|
8640
|
+
tool(
|
|
8641
|
+
SKIP_TURN_TOOL,
|
|
8642
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.skip_turn,
|
|
8643
|
+
{
|
|
8644
|
+
reason: z15.string().min(1).max(MAX_SKIP_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.skip_turn.reason)
|
|
8645
|
+
},
|
|
8646
|
+
async (args) => {
|
|
8647
|
+
skipState.skipped = true;
|
|
8648
|
+
skipState.reason = args.reason;
|
|
8649
|
+
return {
|
|
8650
|
+
content: [{ type: "text", text: JSON.stringify({ skipped: true }) }]
|
|
8651
|
+
};
|
|
8652
|
+
},
|
|
8653
|
+
{ annotations: RECORDER_TOOL, alwaysLoad: true }
|
|
8654
|
+
)
|
|
8655
|
+
] : [],
|
|
8656
|
+
tool(
|
|
8657
|
+
ASK_TOOL,
|
|
8658
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.ask,
|
|
8659
|
+
{
|
|
8660
|
+
targetUserId: z15.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
|
|
8661
|
+
question: z15.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.question),
|
|
8662
|
+
headline: z15.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.headline),
|
|
8663
|
+
options: z15.array(
|
|
8664
|
+
z15.union([
|
|
8665
|
+
z15.string().min(1).max(MAX_ASK_OPTION_BYTES),
|
|
8666
|
+
z15.object({
|
|
8667
|
+
label: z15.string().min(1).max(MAX_ASK_OPTION_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.label"]),
|
|
8668
|
+
move: z15.object({
|
|
8669
|
+
task: z15.string().min(1).max(200).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move.task"]),
|
|
8670
|
+
status: z15.enum(TASK_STAGES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move.status"])
|
|
8029
8671
|
}).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move"])
|
|
8030
8672
|
})
|
|
8031
8673
|
])
|
|
8032
8674
|
).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.options),
|
|
8033
|
-
questions:
|
|
8034
|
-
|
|
8035
|
-
headline:
|
|
8036
|
-
body:
|
|
8037
|
-
options:
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
label:
|
|
8042
|
-
move:
|
|
8043
|
-
task:
|
|
8044
|
-
status:
|
|
8675
|
+
questions: z15.array(
|
|
8676
|
+
z15.object({
|
|
8677
|
+
headline: z15.string().min(1).max(MAX_ASK_HEADLINE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.headline"]),
|
|
8678
|
+
body: z15.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.body"]),
|
|
8679
|
+
options: z15.array(
|
|
8680
|
+
z15.union([
|
|
8681
|
+
z15.string().min(1).max(MAX_ASK_OPTION_BYTES),
|
|
8682
|
+
z15.object({
|
|
8683
|
+
label: z15.string().min(1).max(MAX_ASK_OPTION_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.label"]),
|
|
8684
|
+
move: z15.object({
|
|
8685
|
+
task: z15.string().min(1).max(200).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move.task"]),
|
|
8686
|
+
status: z15.enum(TASK_STAGES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move.status"])
|
|
8045
8687
|
}).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move"])
|
|
8046
8688
|
})
|
|
8047
8689
|
])
|
|
8048
8690
|
).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options"])
|
|
8049
8691
|
})
|
|
8050
8692
|
).min(1).max(MAX_ASK_ITEMS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.questions),
|
|
8051
|
-
replaces:
|
|
8052
|
-
askId:
|
|
8053
|
-
reason:
|
|
8693
|
+
replaces: z15.object({
|
|
8694
|
+
askId: z15.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.askId"]),
|
|
8695
|
+
reason: z15.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.reason"])
|
|
8054
8696
|
}).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.replaces)
|
|
8055
8697
|
},
|
|
8056
8698
|
async (args) => {
|
|
@@ -8091,9 +8733,9 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
8091
8733
|
WAKE_ME_TOOL,
|
|
8092
8734
|
TURN_CONTROL_TOOL_DESCRIPTIONS.wake_me,
|
|
8093
8735
|
{
|
|
8094
|
-
afterSeconds:
|
|
8095
|
-
at:
|
|
8096
|
-
note:
|
|
8736
|
+
afterSeconds: z15.number().int().positive().optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.afterSeconds),
|
|
8737
|
+
at: z15.string().datetime({ offset: true }).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.at),
|
|
8738
|
+
note: z15.string().min(1).max(MAX_WAKE_NOTE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.note)
|
|
8097
8739
|
},
|
|
8098
8740
|
async (args) => {
|
|
8099
8741
|
const hasAfter = args.afterSeconds !== void 0;
|
|
@@ -8127,8 +8769,8 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
8127
8769
|
WITHDRAW_ASK_TOOL,
|
|
8128
8770
|
TURN_CONTROL_TOOL_DESCRIPTIONS.withdraw_ask,
|
|
8129
8771
|
{
|
|
8130
|
-
askId:
|
|
8131
|
-
reason:
|
|
8772
|
+
askId: z15.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.askId),
|
|
8773
|
+
reason: z15.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.reason)
|
|
8132
8774
|
},
|
|
8133
8775
|
async (args) => performAct(acts, {
|
|
8134
8776
|
kind: "withdraw_ask",
|
|
@@ -8168,24 +8810,10 @@ function buildCompanionTurnRequest(params) {
|
|
|
8168
8810
|
session: t.session,
|
|
8169
8811
|
cabane: {
|
|
8170
8812
|
mcpUrl: cabaneMcpUrl(params.baseUrl),
|
|
8171
|
-
|
|
8172
|
-
// when the API didn't mint one (older API / unresolvable delegation).
|
|
8173
|
-
bearer: params.turnToken ?? params.agentPat,
|
|
8813
|
+
bearer: params.turnToken,
|
|
8174
8814
|
activeConversationId: params.activeConversationId,
|
|
8175
8815
|
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) } : {}
|
|
8816
|
+
turnControlUrl: turnControlMcpUrl(params.baseUrl)
|
|
8189
8817
|
},
|
|
8190
8818
|
local: {
|
|
8191
8819
|
...params.cwd ? { cwd: params.cwd } : {},
|
|
@@ -8209,11 +8837,11 @@ function trimSlash2(s) {
|
|
|
8209
8837
|
// src/codex-instructions.ts
|
|
8210
8838
|
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
8211
8839
|
import { tmpdir } from "os";
|
|
8212
|
-
import { join as
|
|
8840
|
+
import { join as join10 } from "path";
|
|
8213
8841
|
var PREFIX = "cabane-codex-instructions-";
|
|
8214
8842
|
async function writeCodexInstructionsFile(contents) {
|
|
8215
|
-
const dir2 = await mkdtemp(
|
|
8216
|
-
const path =
|
|
8843
|
+
const dir2 = await mkdtemp(join10(tmpdir(), PREFIX));
|
|
8844
|
+
const path = join10(dir2, "instructions.md");
|
|
8217
8845
|
await writeFile(path, contents, { encoding: "utf8", mode: 384 });
|
|
8218
8846
|
return {
|
|
8219
8847
|
path,
|
|
@@ -8257,7 +8885,7 @@ function readOutboxFloor(read, turnId, log) {
|
|
|
8257
8885
|
}
|
|
8258
8886
|
|
|
8259
8887
|
// src/prepare-failure.ts
|
|
8260
|
-
import { spawn as
|
|
8888
|
+
import { spawn as spawn5 } from "child_process";
|
|
8261
8889
|
import { hostname } from "os";
|
|
8262
8890
|
function prepareFailureCause(error) {
|
|
8263
8891
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -8295,7 +8923,7 @@ async function runPrepareWithReporting(runner, hook, input, context, reportError
|
|
|
8295
8923
|
}
|
|
8296
8924
|
function reportFailure(config, body) {
|
|
8297
8925
|
return new Promise((resolve2, reject) => {
|
|
8298
|
-
const child =
|
|
8926
|
+
const child = spawn5(config.command, config.args ?? [], {
|
|
8299
8927
|
stdio: ["pipe", "ignore", "ignore"],
|
|
8300
8928
|
env: { ...process.env, ...config.env }
|
|
8301
8929
|
});
|
|
@@ -8319,488 +8947,232 @@ function reportFailure(config, body) {
|
|
|
8319
8947
|
}
|
|
8320
8948
|
|
|
8321
8949
|
// src/prepared.ts
|
|
8322
|
-
import { mkdirSync as
|
|
8323
|
-
import { join as
|
|
8950
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync6, rmSync as rmSync5, writeFileSync as writeFileSync5, existsSync as existsSync7 } from "fs";
|
|
8951
|
+
import { join as join11 } from "path";
|
|
8324
8952
|
function dirFor(workspaceId) {
|
|
8325
|
-
return
|
|
8326
|
-
}
|
|
8327
|
-
function conversationDir(workspaceId, conversationId) {
|
|
8328
|
-
return join12(dirFor(workspaceId), encodeURIComponent(conversationId));
|
|
8329
|
-
}
|
|
8330
|
-
function pathFor3(workspaceId, conversationId, agentId) {
|
|
8331
|
-
return join12(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
|
|
8332
|
-
}
|
|
8333
|
-
function readPrepared(workspaceId, conversationId, agentId) {
|
|
8334
|
-
const path = pathFor3(workspaceId, conversationId, agentId);
|
|
8335
|
-
if (!existsSync8(path)) return null;
|
|
8336
|
-
try {
|
|
8337
|
-
const parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
8338
|
-
if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
|
|
8339
|
-
return {
|
|
8340
|
-
cwd: parsed.cwd,
|
|
8341
|
-
...parsed.env && typeof parsed.env === "object" ? { env: parsed.env } : {}
|
|
8342
|
-
};
|
|
8343
|
-
}
|
|
8344
|
-
return null;
|
|
8345
|
-
} catch {
|
|
8346
|
-
return null;
|
|
8347
|
-
}
|
|
8348
|
-
}
|
|
8349
|
-
function writePrepared(workspaceId, conversationId, agentId, result) {
|
|
8350
|
-
mkdirSync8(conversationDir(workspaceId, conversationId), { recursive: true });
|
|
8351
|
-
writeFileSync6(
|
|
8352
|
-
pathFor3(workspaceId, conversationId, agentId),
|
|
8353
|
-
JSON.stringify(result) + "\n",
|
|
8354
|
-
"utf8"
|
|
8355
|
-
);
|
|
8356
|
-
}
|
|
8357
|
-
function clearPrepared(workspaceId, conversationId, agentId) {
|
|
8358
|
-
rmSync5(pathFor3(workspaceId, conversationId, agentId), { force: true });
|
|
8359
|
-
}
|
|
8360
|
-
|
|
8361
|
-
// src/secrets.ts
|
|
8362
|
-
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
|
|
8363
|
-
import { join as join13 } from "path";
|
|
8364
|
-
import { z as z14 } from "zod";
|
|
8365
|
-
var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
8366
|
-
function secretsPath() {
|
|
8367
|
-
return join13(cabaneDir(), "secrets.json");
|
|
8368
|
-
}
|
|
8369
|
-
var secretStoreSchema = z14.record(z14.string(), z14.string());
|
|
8370
|
-
function loadSecretStore() {
|
|
8371
|
-
const path = secretsPath();
|
|
8372
|
-
if (!existsSync9(path)) return makeStore({});
|
|
8373
|
-
let raw;
|
|
8374
|
-
try {
|
|
8375
|
-
raw = readFileSync7(path, "utf8");
|
|
8376
|
-
} catch (err) {
|
|
8377
|
-
throw new ConfigError(
|
|
8378
|
-
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
8379
|
-
);
|
|
8380
|
-
}
|
|
8381
|
-
if (raw.trim().length === 0) return makeStore({});
|
|
8382
|
-
let parsed;
|
|
8383
|
-
try {
|
|
8384
|
-
parsed = JSON.parse(raw);
|
|
8385
|
-
} catch (err) {
|
|
8386
|
-
throw new ConfigError(
|
|
8387
|
-
`${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}. It must be a flat object of "NAME": "value" secret pairs.`
|
|
8388
|
-
);
|
|
8389
|
-
}
|
|
8390
|
-
const result = secretStoreSchema.safeParse(parsed);
|
|
8391
|
-
if (!result.success) {
|
|
8392
|
-
throw new ConfigError(
|
|
8393
|
-
`${path} must be a flat object mapping secret names to string values (e.g. { "GITHUB_TOKEN": "ghp_\u2026" }).`
|
|
8394
|
-
);
|
|
8395
|
-
}
|
|
8396
|
-
return makeStore(result.data);
|
|
8397
|
-
}
|
|
8398
|
-
function loadSecretStoreTolerant(onWarn) {
|
|
8399
|
-
try {
|
|
8400
|
-
return loadSecretStore();
|
|
8401
|
-
} catch (err) {
|
|
8402
|
-
onWarn?.(err instanceof Error ? err.message : String(err));
|
|
8403
|
-
return makeStore({});
|
|
8404
|
-
}
|
|
8405
|
-
}
|
|
8406
|
-
function makeStore(map) {
|
|
8407
|
-
return {
|
|
8408
|
-
get: (name) => map[name],
|
|
8409
|
-
names: () => Object.keys(map).sort()
|
|
8410
|
-
};
|
|
8411
|
-
}
|
|
8412
|
-
function requiredSecretNames(mcpServers) {
|
|
8413
|
-
const names = /* @__PURE__ */ new Set();
|
|
8414
|
-
for (const server of Object.values(mcpServers ?? {})) {
|
|
8415
|
-
const bags = [
|
|
8416
|
-
"env" in server ? server.env : void 0,
|
|
8417
|
-
"headers" in server ? server.headers : void 0
|
|
8418
|
-
];
|
|
8419
|
-
for (const bag of bags) {
|
|
8420
|
-
for (const value of Object.values(bag ?? {})) {
|
|
8421
|
-
for (const [, name] of value.matchAll(PLACEHOLDER_RE)) names.add(name);
|
|
8422
|
-
}
|
|
8423
|
-
}
|
|
8424
|
-
}
|
|
8425
|
-
return [...names].sort();
|
|
8426
|
-
}
|
|
8427
|
-
function resolveMcpSecrets(mcpServers, store) {
|
|
8428
|
-
const missing = /* @__PURE__ */ new Set();
|
|
8429
|
-
const interpolate = (value) => value.replace(PLACEHOLDER_RE, (_match, name) => {
|
|
8430
|
-
const resolved = store.get(name);
|
|
8431
|
-
if (resolved === void 0) {
|
|
8432
|
-
missing.add(name);
|
|
8433
|
-
return _match;
|
|
8434
|
-
}
|
|
8435
|
-
return resolved;
|
|
8436
|
-
});
|
|
8437
|
-
const mapBag = (bag) => {
|
|
8438
|
-
const out = {};
|
|
8439
|
-
for (const [k, v] of Object.entries(bag)) out[k] = interpolate(v);
|
|
8440
|
-
return out;
|
|
8441
|
-
};
|
|
8442
|
-
const servers = {};
|
|
8443
|
-
for (const [name, server] of Object.entries(mcpServers)) {
|
|
8444
|
-
const next = { ...server };
|
|
8445
|
-
if ("env" in next && next.env) next.env = mapBag(next.env);
|
|
8446
|
-
if ("headers" in next && next.headers) next.headers = mapBag(next.headers);
|
|
8447
|
-
servers[name] = next;
|
|
8448
|
-
}
|
|
8449
|
-
return { servers, missing: [...missing].sort() };
|
|
8450
|
-
}
|
|
8451
|
-
|
|
8452
|
-
// src/transcript-writer.ts
|
|
8453
|
-
import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync9, readdirSync, rmSync as rmSync6 } from "fs";
|
|
8454
|
-
import { basename, dirname as dirname6, join as join14 } from "path";
|
|
8455
|
-
function transcriptsDir() {
|
|
8456
|
-
return join14(cabaneDir(), "transcripts");
|
|
8457
|
-
}
|
|
8458
|
-
var RETAIN = 200;
|
|
8459
|
-
var ANOMALY_RETAIN = 50;
|
|
8460
|
-
var TranscriptWriter = class {
|
|
8461
|
-
path;
|
|
8462
|
-
broken = false;
|
|
8463
|
-
onWarn;
|
|
8464
|
-
constructor(dir2, meta, onWarn) {
|
|
8465
|
-
this.onWarn = onWarn;
|
|
8466
|
-
this.path = join14(dir2, fileName(meta));
|
|
8467
|
-
try {
|
|
8468
|
-
mkdirSync9(dir2, { recursive: true });
|
|
8469
|
-
try {
|
|
8470
|
-
chmodSync4(dir2, 448);
|
|
8471
|
-
} catch {
|
|
8472
|
-
}
|
|
8473
|
-
pruneOld(dir2, RETAIN);
|
|
8474
|
-
} catch (err) {
|
|
8475
|
-
this.fail(err);
|
|
8476
|
-
}
|
|
8477
|
-
this.line({ type: "_meta", ...meta });
|
|
8478
|
-
}
|
|
8479
|
-
// Append a raw SDK message verbatim.
|
|
8480
|
-
write(msg) {
|
|
8481
|
-
this.line(msg);
|
|
8482
|
-
}
|
|
8483
|
-
// Append the outcome footer.
|
|
8484
|
-
close(outcome) {
|
|
8485
|
-
this.line({ type: "_outcome", ...outcome });
|
|
8486
|
-
}
|
|
8487
|
-
// CT1145: keep an independent copy of an empty successful envelope after the
|
|
8488
|
-
// footer has landed. The ordinary 200-file FIFO keeps rotating as before;
|
|
8489
|
-
// this bucket retains the newest 50 anomalies regardless of ordinary traffic.
|
|
8490
|
-
// Best-effort like every other transcript operation: observability can never
|
|
8491
|
-
// fail the turn.
|
|
8492
|
-
preserveAnomaly() {
|
|
8493
|
-
if (this.broken) return;
|
|
8494
|
-
try {
|
|
8495
|
-
const dir2 = join14(dirname6(this.path), "anomalies");
|
|
8496
|
-
mkdirSync9(dir2, { recursive: true, mode: 448 });
|
|
8497
|
-
chmodSync4(dir2, 448);
|
|
8498
|
-
const target = join14(dir2, basename(this.path));
|
|
8499
|
-
copyFileSync(this.path, target);
|
|
8500
|
-
chmodSync4(target, 384);
|
|
8501
|
-
pruneOld(dir2, ANOMALY_RETAIN);
|
|
8502
|
-
} catch (err) {
|
|
8503
|
-
this.fail(err);
|
|
8504
|
-
}
|
|
8505
|
-
}
|
|
8506
|
-
line(obj) {
|
|
8507
|
-
if (this.broken) return;
|
|
8508
|
-
try {
|
|
8509
|
-
appendFileSync(this.path, JSON.stringify(obj) + "\n", { mode: 384 });
|
|
8510
|
-
} catch (err) {
|
|
8511
|
-
this.fail(err);
|
|
8512
|
-
}
|
|
8513
|
-
}
|
|
8514
|
-
fail(err) {
|
|
8515
|
-
if (this.broken) return;
|
|
8516
|
-
this.broken = true;
|
|
8517
|
-
this.onWarn?.(`transcript write failed (${err instanceof Error ? err.message : String(err)})`);
|
|
8518
|
-
}
|
|
8519
|
-
};
|
|
8520
|
-
function fileName(meta) {
|
|
8521
|
-
const d = new Date(meta.ts);
|
|
8522
|
-
const stamp = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
8523
|
-
const slug = safe2(meta.workspaceSlug) || "ws";
|
|
8524
|
-
const short = meta.dispatchId.replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "turn";
|
|
8525
|
-
return `${stamp}-${slug}-${short}.jsonl`;
|
|
8953
|
+
return join11(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
|
|
8526
8954
|
}
|
|
8527
|
-
function
|
|
8528
|
-
return
|
|
8955
|
+
function conversationDir(workspaceId, conversationId) {
|
|
8956
|
+
return join11(dirFor(workspaceId), encodeURIComponent(conversationId));
|
|
8529
8957
|
}
|
|
8530
|
-
function
|
|
8531
|
-
return
|
|
8958
|
+
function pathFor2(workspaceId, conversationId, agentId) {
|
|
8959
|
+
return join11(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
|
|
8532
8960
|
}
|
|
8533
|
-
function
|
|
8534
|
-
|
|
8961
|
+
function readPrepared(workspaceId, conversationId, agentId) {
|
|
8962
|
+
const path = pathFor2(workspaceId, conversationId, agentId);
|
|
8963
|
+
if (!existsSync7(path)) return null;
|
|
8535
8964
|
try {
|
|
8536
|
-
|
|
8537
|
-
|
|
8538
|
-
|
|
8539
|
-
|
|
8540
|
-
|
|
8541
|
-
|
|
8542
|
-
for (const f of drop) {
|
|
8543
|
-
try {
|
|
8544
|
-
rmSync6(join14(dir2, f), { force: true });
|
|
8545
|
-
} catch {
|
|
8965
|
+
const parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
8966
|
+
if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
|
|
8967
|
+
return {
|
|
8968
|
+
cwd: parsed.cwd,
|
|
8969
|
+
...parsed.env && typeof parsed.env === "object" ? { env: parsed.env } : {}
|
|
8970
|
+
};
|
|
8546
8971
|
}
|
|
8547
|
-
|
|
8548
|
-
}
|
|
8549
|
-
|
|
8550
|
-
// src/turn-containment.ts
|
|
8551
|
-
import { execFile, execFileSync, spawn as spawn5 } from "child_process";
|
|
8552
|
-
import { readFileSync as readFileSync8 } from "fs";
|
|
8553
|
-
import { promisify } from "util";
|
|
8554
|
-
var execFileAsync = promisify(execFile);
|
|
8555
|
-
var asContained = (child) => child;
|
|
8556
|
-
var CONTAINMENT_STOP_GRACE_SEC = 5;
|
|
8557
|
-
var FALLBACK_GRACE_MS = CONTAINMENT_STOP_GRACE_SEC * 1e3;
|
|
8558
|
-
var CAPABILITY_PROBE_TIMEOUT_MS = 4e3;
|
|
8559
|
-
function turnScopeUnit(turnId, attempt) {
|
|
8560
|
-
const safe3 = turnId.replace(/[^A-Za-z0-9_.-]/g, "-");
|
|
8561
|
-
return `cabane-turn-${safe3}-${attempt}`;
|
|
8562
|
-
}
|
|
8563
|
-
function scopeCapabilityUsable(env, timeoutMs = CAPABILITY_PROBE_TIMEOUT_MS) {
|
|
8564
|
-
if (process.platform !== "linux") return false;
|
|
8565
|
-
try {
|
|
8566
|
-
execFileSync("systemd-run", ["--user", "--scope", "--quiet", "--collect", "--", "/bin/true"], {
|
|
8567
|
-
env,
|
|
8568
|
-
timeout: timeoutMs,
|
|
8569
|
-
stdio: "ignore"
|
|
8570
|
-
});
|
|
8571
|
-
return true;
|
|
8972
|
+
return null;
|
|
8572
8973
|
} catch {
|
|
8573
|
-
return
|
|
8974
|
+
return null;
|
|
8574
8975
|
}
|
|
8575
8976
|
}
|
|
8576
|
-
function
|
|
8577
|
-
|
|
8578
|
-
|
|
8579
|
-
|
|
8580
|
-
|
|
8581
|
-
|
|
8582
|
-
|
|
8583
|
-
return "absent";
|
|
8584
|
-
return "failure";
|
|
8977
|
+
function writePrepared(workspaceId, conversationId, agentId, result) {
|
|
8978
|
+
mkdirSync7(conversationDir(workspaceId, conversationId), { recursive: true });
|
|
8979
|
+
writeFileSync5(
|
|
8980
|
+
pathFor2(workspaceId, conversationId, agentId),
|
|
8981
|
+
JSON.stringify(result) + "\n",
|
|
8982
|
+
"utf8"
|
|
8983
|
+
);
|
|
8585
8984
|
}
|
|
8586
|
-
function
|
|
8587
|
-
|
|
8588
|
-
const path = line.trim().split(":").pop();
|
|
8589
|
-
if (!path?.startsWith("/")) continue;
|
|
8590
|
-
const leaf = path.split("/").filter(Boolean).pop();
|
|
8591
|
-
if (leaf && (leaf.endsWith(".service") || leaf.endsWith(".scope"))) return leaf;
|
|
8592
|
-
}
|
|
8593
|
-
return null;
|
|
8985
|
+
function clearPrepared(workspaceId, conversationId, agentId) {
|
|
8986
|
+
rmSync5(pathFor2(workspaceId, conversationId, agentId), { force: true });
|
|
8594
8987
|
}
|
|
8595
|
-
|
|
8596
|
-
|
|
8597
|
-
|
|
8598
|
-
|
|
8599
|
-
|
|
8600
|
-
|
|
8988
|
+
|
|
8989
|
+
// src/secrets.ts
|
|
8990
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
8991
|
+
import { join as join12 } from "path";
|
|
8992
|
+
import { z as z16 } from "zod";
|
|
8993
|
+
var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
8994
|
+
function secretsPath() {
|
|
8995
|
+
return join12(cabaneDir(), "secrets.json");
|
|
8601
8996
|
}
|
|
8602
|
-
|
|
8997
|
+
var secretStoreSchema = z16.record(z16.string(), z16.string());
|
|
8998
|
+
function loadSecretStore() {
|
|
8999
|
+
const path = secretsPath();
|
|
9000
|
+
if (!existsSync8(path)) return makeStore({});
|
|
9001
|
+
let raw;
|
|
8603
9002
|
try {
|
|
8604
|
-
|
|
8605
|
-
return {
|
|
8606
|
-
ok: true,
|
|
8607
|
-
members: out.split("\n").map((line) => /^\s*(\d+)\s+(\d+)\s*$/.exec(line)).filter((m) => m !== null).filter((m) => Number(m[2]) === pgid && Number(m[1]) !== process.pid).map((m) => Number(m[1]))
|
|
8608
|
-
};
|
|
9003
|
+
raw = readFileSync7(path, "utf8");
|
|
8609
9004
|
} catch (err) {
|
|
8610
|
-
|
|
9005
|
+
throw new ConfigError(
|
|
9006
|
+
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
9007
|
+
);
|
|
8611
9008
|
}
|
|
8612
|
-
}
|
|
8613
|
-
|
|
8614
|
-
|
|
8615
|
-
|
|
8616
|
-
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
const ownUnit = (deps.ownSystemdUnit ?? readCurrentSystemdUnit)();
|
|
8620
|
-
const scopeUnits = [];
|
|
8621
|
-
const groups = [];
|
|
8622
|
-
let kind = null;
|
|
8623
|
-
let probedEnvKey = null;
|
|
8624
|
-
let attempt = 0;
|
|
8625
|
-
function spawnInScope(options) {
|
|
8626
|
-
const unit = turnScopeUnit(turnId, attempt++);
|
|
8627
|
-
scopeUnits.push({ unit, env: options.env });
|
|
8628
|
-
deps.log?.("containment: spawning the turn CLI into a systemd scope", {
|
|
8629
|
-
unit,
|
|
8630
|
-
partOf: ownUnit,
|
|
8631
|
-
command: options.command
|
|
8632
|
-
});
|
|
8633
|
-
return asContained(
|
|
8634
|
-
spawn5(
|
|
8635
|
-
"systemd-run",
|
|
8636
|
-
[
|
|
8637
|
-
"--user",
|
|
8638
|
-
"--scope",
|
|
8639
|
-
"--quiet",
|
|
8640
|
-
"--collect",
|
|
8641
|
-
`--unit=${unit}`,
|
|
8642
|
-
`--property=TimeoutStopSec=${CONTAINMENT_STOP_GRACE_SEC}s`,
|
|
8643
|
-
// Bind the scope's life to the companion's own unit, so stopping or
|
|
8644
|
-
// restarting the companion still takes its in-flight turns with it —
|
|
8645
|
-
// which is what happens today, and what the scope would otherwise undo.
|
|
8646
|
-
// Omitted outside a unit, where there is nothing to bind to.
|
|
8647
|
-
...ownUnit ? [`--property=PartOf=${ownUnit}`] : [],
|
|
8648
|
-
"--",
|
|
8649
|
-
options.command,
|
|
8650
|
-
...options.args
|
|
8651
|
-
],
|
|
8652
|
-
{
|
|
8653
|
-
...options.cwd ? { cwd: options.cwd } : {},
|
|
8654
|
-
env: options.env,
|
|
8655
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
8656
|
-
}
|
|
8657
|
-
)
|
|
9009
|
+
if (raw.trim().length === 0) return makeStore({});
|
|
9010
|
+
let parsed;
|
|
9011
|
+
try {
|
|
9012
|
+
parsed = JSON.parse(raw);
|
|
9013
|
+
} catch (err) {
|
|
9014
|
+
throw new ConfigError(
|
|
9015
|
+
`${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}. It must be a flat object of "NAME": "value" secret pairs.`
|
|
8658
9016
|
);
|
|
8659
9017
|
}
|
|
8660
|
-
|
|
8661
|
-
|
|
8662
|
-
|
|
8663
|
-
|
|
8664
|
-
|
|
8665
|
-
detached: process.platform !== "win32"
|
|
8666
|
-
});
|
|
8667
|
-
if (child.pid !== void 0) groups.push({ pgid: child.pid, child });
|
|
8668
|
-
deps.log?.("containment: no host-owned containment here \u2014 best effort only", {
|
|
8669
|
-
platform: process.platform,
|
|
8670
|
-
pid: child.pid
|
|
8671
|
-
});
|
|
8672
|
-
return asContained(child);
|
|
9018
|
+
const result = secretStoreSchema.safeParse(parsed);
|
|
9019
|
+
if (!result.success) {
|
|
9020
|
+
throw new ConfigError(
|
|
9021
|
+
`${path} must be a flat object mapping secret names to string values (e.g. { "GITHUB_TOKEN": "ghp_\u2026" }).`
|
|
9022
|
+
);
|
|
8673
9023
|
}
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
|
|
8682
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
8683
|
-
if (classifyStopError(message) === "failure") {
|
|
8684
|
-
result.failures.push({ target: unit, error: message.slice(0, 200) });
|
|
8685
|
-
continue;
|
|
8686
|
-
}
|
|
8687
|
-
result.reaped.push(unit);
|
|
8688
|
-
continue;
|
|
8689
|
-
}
|
|
8690
|
-
try {
|
|
8691
|
-
const { stdout } = await execFileAsync(
|
|
8692
|
-
"systemctl",
|
|
8693
|
-
["--user", "show", `${unit}.scope`, "--property=ActiveState", "--value"],
|
|
8694
|
-
{ env, timeout: 1e4 }
|
|
8695
|
-
);
|
|
8696
|
-
const state = stdout.trim();
|
|
8697
|
-
if (state === "" || state === "inactive" || state === "failed") result.reaped.push(unit);
|
|
8698
|
-
else result.failures.push({ target: unit, error: `scope still ${state} after stop` });
|
|
8699
|
-
} catch (err) {
|
|
8700
|
-
result.failures.push({
|
|
8701
|
-
target: unit,
|
|
8702
|
-
error: `could not confirm the scope stopped: ${(err instanceof Error ? err.message : String(err)).slice(0, 150)}`
|
|
8703
|
-
});
|
|
8704
|
-
}
|
|
8705
|
-
}
|
|
9024
|
+
return makeStore(result.data);
|
|
9025
|
+
}
|
|
9026
|
+
function loadSecretStoreTolerant(onWarn) {
|
|
9027
|
+
try {
|
|
9028
|
+
return loadSecretStore();
|
|
9029
|
+
} catch (err) {
|
|
9030
|
+
onWarn?.(err instanceof Error ? err.message : String(err));
|
|
9031
|
+
return makeStore({});
|
|
8706
9032
|
}
|
|
8707
|
-
|
|
8708
|
-
|
|
8709
|
-
|
|
8710
|
-
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
const
|
|
8723
|
-
|
|
8724
|
-
result.failures.push({
|
|
8725
|
-
target,
|
|
8726
|
-
error: `could not enumerate the process group: ${first.error}`
|
|
8727
|
-
});
|
|
8728
|
-
continue;
|
|
8729
|
-
}
|
|
8730
|
-
if (first.members.length === 0) {
|
|
8731
|
-
result.reaped.push(target);
|
|
8732
|
-
continue;
|
|
8733
|
-
}
|
|
8734
|
-
for (const pid of first.members) {
|
|
8735
|
-
try {
|
|
8736
|
-
process.kill(pid, "SIGTERM");
|
|
8737
|
-
} catch {
|
|
8738
|
-
}
|
|
8739
|
-
}
|
|
8740
|
-
await sleep2(fallbackGraceMs);
|
|
8741
|
-
const afterTerm = processGroupMembers(pgid);
|
|
8742
|
-
if (!afterTerm.ok) {
|
|
8743
|
-
result.failures.push({
|
|
8744
|
-
target,
|
|
8745
|
-
error: `could not re-enumerate the process group after SIGTERM: ${afterTerm.error}`
|
|
8746
|
-
});
|
|
8747
|
-
continue;
|
|
8748
|
-
}
|
|
8749
|
-
for (const pid of afterTerm.members) {
|
|
8750
|
-
try {
|
|
8751
|
-
process.kill(pid, "SIGKILL");
|
|
8752
|
-
} catch {
|
|
8753
|
-
}
|
|
8754
|
-
}
|
|
8755
|
-
const afterKill = processGroupMembers(pgid);
|
|
8756
|
-
if (!afterKill.ok) {
|
|
8757
|
-
result.failures.push({
|
|
8758
|
-
target,
|
|
8759
|
-
error: `could not confirm the process group is empty after SIGKILL: ${afterKill.error}`
|
|
8760
|
-
});
|
|
8761
|
-
continue;
|
|
9033
|
+
}
|
|
9034
|
+
function makeStore(map) {
|
|
9035
|
+
return {
|
|
9036
|
+
get: (name) => map[name],
|
|
9037
|
+
names: () => Object.keys(map).sort()
|
|
9038
|
+
};
|
|
9039
|
+
}
|
|
9040
|
+
function requiredSecretNames(mcpServers) {
|
|
9041
|
+
const names = /* @__PURE__ */ new Set();
|
|
9042
|
+
for (const server of Object.values(mcpServers ?? {})) {
|
|
9043
|
+
const bags = [
|
|
9044
|
+
"env" in server ? server.env : void 0,
|
|
9045
|
+
"headers" in server ? server.headers : void 0
|
|
9046
|
+
];
|
|
9047
|
+
for (const bag of bags) {
|
|
9048
|
+
for (const value of Object.values(bag ?? {})) {
|
|
9049
|
+
for (const [, name] of value.matchAll(PLACEHOLDER_RE)) names.add(name);
|
|
8762
9050
|
}
|
|
8763
|
-
if (afterKill.members.length)
|
|
8764
|
-
result.failures.push({
|
|
8765
|
-
target,
|
|
8766
|
-
error: `${afterKill.members.length} process(es) still in the group after SIGKILL`
|
|
8767
|
-
});
|
|
8768
|
-
else result.reaped.push(target);
|
|
8769
9051
|
}
|
|
8770
9052
|
}
|
|
8771
|
-
return
|
|
8772
|
-
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
|
|
8777
|
-
|
|
8778
|
-
|
|
8779
|
-
|
|
8780
|
-
deps.log?.("containment: resolved strategy for this turn", {
|
|
8781
|
-
kind,
|
|
8782
|
-
platform: process.platform
|
|
8783
|
-
});
|
|
8784
|
-
}
|
|
8785
|
-
return kind === "systemd-scope" ? spawnInScope(options) : spawnInGroup(options);
|
|
8786
|
-
},
|
|
8787
|
-
async reap() {
|
|
8788
|
-
const result = {
|
|
8789
|
-
kind: kind ?? "unused",
|
|
8790
|
-
reaped: [],
|
|
8791
|
-
failures: [],
|
|
8792
|
-
unverified: []
|
|
8793
|
-
};
|
|
8794
|
-
await reapScopes(result);
|
|
8795
|
-
await reapGroups(result);
|
|
8796
|
-
deps.log?.("containment: reap complete", {
|
|
8797
|
-
kind: result.kind,
|
|
8798
|
-
reaped: result.reaped,
|
|
8799
|
-
failures: result.failures
|
|
8800
|
-
});
|
|
8801
|
-
return result;
|
|
9053
|
+
return [...names].sort();
|
|
9054
|
+
}
|
|
9055
|
+
function resolveMcpSecrets(mcpServers, store) {
|
|
9056
|
+
const missing = /* @__PURE__ */ new Set();
|
|
9057
|
+
const interpolate = (value) => value.replace(PLACEHOLDER_RE, (_match, name) => {
|
|
9058
|
+
const resolved = store.get(name);
|
|
9059
|
+
if (resolved === void 0) {
|
|
9060
|
+
missing.add(name);
|
|
9061
|
+
return _match;
|
|
8802
9062
|
}
|
|
9063
|
+
return resolved;
|
|
9064
|
+
});
|
|
9065
|
+
const mapBag = (bag) => {
|
|
9066
|
+
const out = {};
|
|
9067
|
+
for (const [k, v] of Object.entries(bag)) out[k] = interpolate(v);
|
|
9068
|
+
return out;
|
|
8803
9069
|
};
|
|
9070
|
+
const servers = {};
|
|
9071
|
+
for (const [name, server] of Object.entries(mcpServers)) {
|
|
9072
|
+
const next = { ...server };
|
|
9073
|
+
if ("env" in next && next.env) next.env = mapBag(next.env);
|
|
9074
|
+
if ("headers" in next && next.headers) next.headers = mapBag(next.headers);
|
|
9075
|
+
servers[name] = next;
|
|
9076
|
+
}
|
|
9077
|
+
return { servers, missing: [...missing].sort() };
|
|
9078
|
+
}
|
|
9079
|
+
|
|
9080
|
+
// src/transcript-writer.ts
|
|
9081
|
+
import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync6 } from "fs";
|
|
9082
|
+
import { basename, dirname as dirname6, join as join13 } from "path";
|
|
9083
|
+
function transcriptsDir() {
|
|
9084
|
+
return join13(runnerStateDir(), "transcripts");
|
|
9085
|
+
}
|
|
9086
|
+
var RETAIN = 200;
|
|
9087
|
+
var ANOMALY_RETAIN = 50;
|
|
9088
|
+
var TranscriptWriter = class {
|
|
9089
|
+
path;
|
|
9090
|
+
broken = false;
|
|
9091
|
+
onWarn;
|
|
9092
|
+
constructor(dir2, meta, onWarn) {
|
|
9093
|
+
this.onWarn = onWarn;
|
|
9094
|
+
this.path = join13(dir2, fileName(meta));
|
|
9095
|
+
try {
|
|
9096
|
+
mkdirSync8(dir2, { recursive: true });
|
|
9097
|
+
try {
|
|
9098
|
+
chmodSync4(dir2, 448);
|
|
9099
|
+
} catch {
|
|
9100
|
+
}
|
|
9101
|
+
pruneOld(dir2, RETAIN);
|
|
9102
|
+
} catch (err) {
|
|
9103
|
+
this.fail(err);
|
|
9104
|
+
}
|
|
9105
|
+
this.line({ type: "_meta", ...meta });
|
|
9106
|
+
}
|
|
9107
|
+
// Append a raw SDK message verbatim.
|
|
9108
|
+
write(msg) {
|
|
9109
|
+
this.line(msg);
|
|
9110
|
+
}
|
|
9111
|
+
// Append the outcome footer.
|
|
9112
|
+
close(outcome) {
|
|
9113
|
+
this.line({ type: "_outcome", ...outcome });
|
|
9114
|
+
}
|
|
9115
|
+
// CT1145: keep an independent copy of an empty successful envelope after the
|
|
9116
|
+
// footer has landed. The ordinary 200-file FIFO keeps rotating as before;
|
|
9117
|
+
// this bucket retains the newest 50 anomalies regardless of ordinary traffic.
|
|
9118
|
+
// Best-effort like every other transcript operation: observability can never
|
|
9119
|
+
// fail the turn.
|
|
9120
|
+
preserveAnomaly() {
|
|
9121
|
+
if (this.broken) return;
|
|
9122
|
+
try {
|
|
9123
|
+
const dir2 = join13(dirname6(this.path), "anomalies");
|
|
9124
|
+
mkdirSync8(dir2, { recursive: true, mode: 448 });
|
|
9125
|
+
chmodSync4(dir2, 448);
|
|
9126
|
+
const target = join13(dir2, basename(this.path));
|
|
9127
|
+
copyFileSync(this.path, target);
|
|
9128
|
+
chmodSync4(target, 384);
|
|
9129
|
+
pruneOld(dir2, ANOMALY_RETAIN);
|
|
9130
|
+
} catch (err) {
|
|
9131
|
+
this.fail(err);
|
|
9132
|
+
}
|
|
9133
|
+
}
|
|
9134
|
+
line(obj) {
|
|
9135
|
+
if (this.broken) return;
|
|
9136
|
+
try {
|
|
9137
|
+
appendFileSync(this.path, JSON.stringify(obj) + "\n", { mode: 384 });
|
|
9138
|
+
} catch (err) {
|
|
9139
|
+
this.fail(err);
|
|
9140
|
+
}
|
|
9141
|
+
}
|
|
9142
|
+
fail(err) {
|
|
9143
|
+
if (this.broken) return;
|
|
9144
|
+
this.broken = true;
|
|
9145
|
+
this.onWarn?.(`transcript write failed (${err instanceof Error ? err.message : String(err)})`);
|
|
9146
|
+
}
|
|
9147
|
+
};
|
|
9148
|
+
function fileName(meta) {
|
|
9149
|
+
const d = new Date(meta.ts);
|
|
9150
|
+
const stamp = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
9151
|
+
const slug = safe2(meta.workspaceSlug) || "ws";
|
|
9152
|
+
const short = meta.dispatchId.replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "turn";
|
|
9153
|
+
return `${stamp}-${slug}-${short}.jsonl`;
|
|
9154
|
+
}
|
|
9155
|
+
function pad(n) {
|
|
9156
|
+
return String(n).padStart(2, "0");
|
|
9157
|
+
}
|
|
9158
|
+
function safe2(s) {
|
|
9159
|
+
return s.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
9160
|
+
}
|
|
9161
|
+
function pruneOld(dir2, retain) {
|
|
9162
|
+
let files;
|
|
9163
|
+
try {
|
|
9164
|
+
files = readdirSync(dir2).filter((f) => f.endsWith(".jsonl"));
|
|
9165
|
+
} catch {
|
|
9166
|
+
return;
|
|
9167
|
+
}
|
|
9168
|
+
if (files.length <= retain) return;
|
|
9169
|
+
const drop = files.sort().slice(0, files.length - retain);
|
|
9170
|
+
for (const f of drop) {
|
|
9171
|
+
try {
|
|
9172
|
+
rmSync6(join13(dir2, f), { force: true });
|
|
9173
|
+
} catch {
|
|
9174
|
+
}
|
|
9175
|
+
}
|
|
8804
9176
|
}
|
|
8805
9177
|
|
|
8806
9178
|
// src/turn-committer.ts
|
|
@@ -9020,6 +9392,7 @@ function missingSecretReason(missing) {
|
|
|
9020
9392
|
return `${prefix}+${missing.length}`;
|
|
9021
9393
|
}
|
|
9022
9394
|
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:";
|
|
9395
|
+
var NO_TURN_TOKEN_REASON = "no_turn_token: the server issued no turn credential for this turn";
|
|
9023
9396
|
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
9397
|
var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
|
|
9025
9398
|
var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 15 * 6e4;
|
|
@@ -9299,6 +9672,10 @@ var TurnExecution = class {
|
|
|
9299
9672
|
const fetchReason = `fetch_failed: ${describeError(err)}`;
|
|
9300
9673
|
throw this.concluded(fetchReason, fetchReason);
|
|
9301
9674
|
}
|
|
9675
|
+
if (!turnContext.turnToken) {
|
|
9676
|
+
turnLog.warn({ turnId }, "dispatcher: turn context carried no turn token");
|
|
9677
|
+
throw this.concluded(NO_TURN_TOKEN_REASON, NO_TURN_TOKEN_REASON);
|
|
9678
|
+
}
|
|
9302
9679
|
this.turnContext = turnContext;
|
|
9303
9680
|
if (this.resumed) {
|
|
9304
9681
|
const sources = { serverFloor: turnContext.turnSeqFloor, outboxFloor };
|
|
@@ -9346,7 +9723,7 @@ var TurnExecution = class {
|
|
|
9346
9723
|
const prepareHook = this.opts.local.prepareHook;
|
|
9347
9724
|
const cabaneCwd = this.turnContext.cwd;
|
|
9348
9725
|
let effectiveCwd = localCwd ?? cabaneCwd;
|
|
9349
|
-
if (effectiveCwd && !
|
|
9726
|
+
if (effectiveCwd && !existsSync9(effectiveCwd)) {
|
|
9350
9727
|
turnLog.warn(
|
|
9351
9728
|
{ cwd: effectiveCwd },
|
|
9352
9729
|
"The working directory is missing; using the companion's directory instead. Check this agent's working directory setting."
|
|
@@ -9486,7 +9863,9 @@ var TurnExecution = class {
|
|
|
9486
9863
|
try {
|
|
9487
9864
|
await this.opts.api.setActiveRun(workspaceId, payload.conversationId, payload.agentId, {
|
|
9488
9865
|
activeRunStartedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9489
|
-
|
|
9866
|
+
// CT1679: a runner leases under its launcher's boot id — the one the
|
|
9867
|
+
// device heartbeats — or the lost-run sweep reads it as a restart.
|
|
9868
|
+
bootId: this.opts.inRunner?.bootId ?? bootId,
|
|
9490
9869
|
// CT1380: the durable record that this turn resumed, and from where.
|
|
9491
9870
|
...this.resumedFromSeq !== void 0 ? { resumedFromSeq: this.resumedFromSeq } : {},
|
|
9492
9871
|
// CT1454: the issuance the context THIS attempt runs on came from. Read
|
|
@@ -9550,10 +9929,8 @@ var TurnExecution = class {
|
|
|
9550
9929
|
this.request = buildCompanionTurnRequest({
|
|
9551
9930
|
turnContext: this.turnContext,
|
|
9552
9931
|
baseUrl: this.opts.baseUrl,
|
|
9553
|
-
|
|
9554
|
-
|
|
9555
|
-
// the companion PAT (`agentPat`) inside `buildCompanionTurnRequest` otherwise.
|
|
9556
|
-
...this.turnContext.turnToken ? { turnToken: this.turnContext.turnToken } : {},
|
|
9932
|
+
// ACC8: always present; `fetchTurnContext` refused a context without one.
|
|
9933
|
+
turnToken: this.turnContext.turnToken,
|
|
9557
9934
|
// SJ524: the hook-resolved cwd overrides the static local cwd.
|
|
9558
9935
|
...this.effectiveCwd ? { cwd: this.effectiveCwd } : {},
|
|
9559
9936
|
// CT1103: the prepare-hook env plus the authoritative host-access token.
|
|
@@ -9585,10 +9962,14 @@ var TurnExecution = class {
|
|
|
9585
9962
|
const { payload, workspaceId, turnId, turnLog } = this;
|
|
9586
9963
|
await guardBundledBinary(this.integrityContext(), (r, e) => this.concluded(r, e), this.outcome);
|
|
9587
9964
|
const onWarn = (msg, meta) => turnLog.debug(meta ?? {}, msg);
|
|
9965
|
+
const sandbox = this.opts.harnessSandbox ? await this.opts.harnessSandbox({ cwd: this.effectiveCwd, log: turnLog }) : null;
|
|
9588
9966
|
const adapters = [];
|
|
9589
9967
|
if (this.opts.claudeCodeAvailable?.() ?? true) {
|
|
9590
9968
|
this.containment = createTurnContainment(this.turnId, {
|
|
9591
|
-
log: (msg, meta) => turnLog.debug(meta ?? {}, msg)
|
|
9969
|
+
log: (msg, meta) => turnLog.debug(meta ?? {}, msg),
|
|
9970
|
+
// CT1679: a group outside the runner's own is reported, so a launcher
|
|
9971
|
+
// can stop it if the runner dies without reaping it.
|
|
9972
|
+
...this.opts.inRunner?.onProcessGroup ? { onProcessGroup: this.opts.inRunner.onProcessGroup } : {}
|
|
9592
9973
|
});
|
|
9593
9974
|
const containment = this.containment;
|
|
9594
9975
|
adapters.push(
|
|
@@ -9597,20 +9978,43 @@ var TurnExecution = class {
|
|
|
9597
9978
|
onWarn,
|
|
9598
9979
|
augmentOptions: (options) => ({
|
|
9599
9980
|
...options,
|
|
9600
|
-
spawnClaudeCodeProcess: (spawnOptions) => containment.spawn(
|
|
9981
|
+
spawnClaudeCodeProcess: (spawnOptions) => containment.spawn(
|
|
9982
|
+
sandbox ? { ...spawnOptions, ...sandbox.wrap(spawnOptions.command, spawnOptions.args) } : spawnOptions
|
|
9983
|
+
)
|
|
9601
9984
|
})
|
|
9602
9985
|
})
|
|
9603
9986
|
);
|
|
9604
9987
|
}
|
|
9605
9988
|
if (this.opts.opencodeServerUrl) {
|
|
9606
|
-
|
|
9989
|
+
const serverUrl = this.opts.opencodeServerUrl;
|
|
9990
|
+
adapters.push(
|
|
9991
|
+
createOpencodeAdapter(
|
|
9992
|
+
// CT1679: the transport's per-server turn mutex is per process; in a
|
|
9993
|
+
// runner the turn also holds the device-wide lock every runner shares.
|
|
9994
|
+
this.opts.inRunner ? {
|
|
9995
|
+
transport: this.opts.inRunner.opencodeTransport(serverUrl),
|
|
9996
|
+
onWarn
|
|
9997
|
+
} : { serverUrl, onWarn }
|
|
9998
|
+
)
|
|
9999
|
+
);
|
|
9607
10000
|
}
|
|
9608
10001
|
if (this.opts.codexAvailable?.() ?? false) {
|
|
10002
|
+
const inRunner = this.opts.inRunner;
|
|
10003
|
+
const codexBinary = sandbox || inRunner ? (this.opts.resolveBundled ?? resolveBundledBinary)("codex").path : null;
|
|
10004
|
+
let codexPath = codexBinary && sandbox ? sandbox.executableFor(codexBinary) : codexBinary;
|
|
10005
|
+
if (codexPath && inRunner) {
|
|
10006
|
+
this.containment ??= createTurnContainment(this.turnId, {
|
|
10007
|
+
log: (msg, meta) => turnLog.debug(meta ?? {}, msg),
|
|
10008
|
+
...this.opts.inRunner?.onProcessGroup ? { onProcessGroup: this.opts.inRunner.onProcessGroup } : {}
|
|
10009
|
+
});
|
|
10010
|
+
codexPath = inRunner.codexExecutable(codexPath, this.containment);
|
|
10011
|
+
}
|
|
9609
10012
|
adapters.push(
|
|
9610
10013
|
createCodexAdapter({
|
|
9611
10014
|
enabled: true,
|
|
9612
10015
|
onWarn,
|
|
9613
|
-
writeInstructionsFile: writeCodexInstructionsFile
|
|
10016
|
+
writeInstructionsFile: writeCodexInstructionsFile,
|
|
10017
|
+
...codexPath ? { transport: createSdkCodexTransport({ codexPathOverride: codexPath }) } : {}
|
|
9614
10018
|
})
|
|
9615
10019
|
);
|
|
9616
10020
|
}
|
|
@@ -10065,7 +10469,7 @@ function fingerprintSessionState(state) {
|
|
|
10065
10469
|
// src/dispatcher.ts
|
|
10066
10470
|
function checkoutState(cwd) {
|
|
10067
10471
|
if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
|
|
10068
|
-
if (!
|
|
10472
|
+
if (!existsSync10(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
|
|
10069
10473
|
let entries;
|
|
10070
10474
|
try {
|
|
10071
10475
|
entries = readdirSync2(cwd);
|
|
@@ -10075,15 +10479,15 @@ function checkoutState(cwd) {
|
|
|
10075
10479
|
if (entries.length === 0) {
|
|
10076
10480
|
return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
|
|
10077
10481
|
}
|
|
10078
|
-
const gitPath =
|
|
10079
|
-
if (!
|
|
10482
|
+
const gitPath = join14(cwd, ".git");
|
|
10483
|
+
if (!existsSync10(gitPath)) return { ok: true, reason: "usable" };
|
|
10080
10484
|
let stat;
|
|
10081
10485
|
try {
|
|
10082
10486
|
stat = statSync2(gitPath);
|
|
10083
10487
|
} catch (error) {
|
|
10084
10488
|
return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
|
|
10085
10489
|
}
|
|
10086
|
-
if (stat.isDirectory() && !
|
|
10490
|
+
if (stat.isDirectory() && !existsSync10(join14(gitPath, "HEAD")))
|
|
10087
10491
|
return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
|
|
10088
10492
|
return { ok: true, reason: "usable" };
|
|
10089
10493
|
}
|
|
@@ -10147,6 +10551,24 @@ var Dispatcher = class {
|
|
|
10147
10551
|
notifyStart: (info) => this.notifyStart(info),
|
|
10148
10552
|
notifyEnd: (info) => this.notifyEnd(info)
|
|
10149
10553
|
};
|
|
10554
|
+
if (this.opts.runner) {
|
|
10555
|
+
return runTurnInRunner(
|
|
10556
|
+
this.opts,
|
|
10557
|
+
this.opts.runner,
|
|
10558
|
+
{
|
|
10559
|
+
registerAbort: supervisor.registerAbort,
|
|
10560
|
+
releaseAbort: supervisor.releaseAbort,
|
|
10561
|
+
sessionWriteNoticeClaimed: () => this.sessionWriteNotified.has(key),
|
|
10562
|
+
markSessionWriteNotice: () => {
|
|
10563
|
+
this.sessionWriteNotified.add(key);
|
|
10564
|
+
},
|
|
10565
|
+
notifyStart: supervisor.notifyStart,
|
|
10566
|
+
notifyEnd: supervisor.notifyEnd
|
|
10567
|
+
},
|
|
10568
|
+
payload,
|
|
10569
|
+
{ turnId: opts.turnId ?? randomUUID4(), resumed: opts.resumed === true }
|
|
10570
|
+
);
|
|
10571
|
+
}
|
|
10150
10572
|
return new TurnExecution(this.opts, supervisor, payload, opts).run();
|
|
10151
10573
|
}
|
|
10152
10574
|
// SJ383: cancel a specific (conversation, agent) run if one is in flight in
|
|
@@ -10165,6 +10587,176 @@ var Dispatcher = class {
|
|
|
10165
10587
|
}
|
|
10166
10588
|
};
|
|
10167
10589
|
|
|
10590
|
+
// src/providers/local-process.ts
|
|
10591
|
+
import { spawn as spawn6 } from "child_process";
|
|
10592
|
+
import { constants } from "os";
|
|
10593
|
+
var DEFAULT_KILL_GRACE_MS = 3e4;
|
|
10594
|
+
var DEFAULT_REAP_GRACE_MS = 3e3;
|
|
10595
|
+
function exitCode(code, signal) {
|
|
10596
|
+
if (code !== null) return code;
|
|
10597
|
+
const signum = signal ? constants.signals[signal] : void 0;
|
|
10598
|
+
return signum !== void 0 ? 128 + signum : 1;
|
|
10599
|
+
}
|
|
10600
|
+
function createLocalProcessProvider(opts = {}) {
|
|
10601
|
+
const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
|
|
10602
|
+
const spawnFn = opts.spawnFn ?? spawn6;
|
|
10603
|
+
const reapGraceMs = opts.reapGraceMs ?? DEFAULT_REAP_GRACE_MS;
|
|
10604
|
+
const killFn = opts.killFn ?? ((pid, signal) => process.kill(pid, signal));
|
|
10605
|
+
const groupAlive = (pgid) => {
|
|
10606
|
+
try {
|
|
10607
|
+
killFn(-pgid, 0);
|
|
10608
|
+
return true;
|
|
10609
|
+
} catch {
|
|
10610
|
+
return false;
|
|
10611
|
+
}
|
|
10612
|
+
};
|
|
10613
|
+
const signalGroup = (pgid, signal) => {
|
|
10614
|
+
try {
|
|
10615
|
+
killFn(-pgid, signal);
|
|
10616
|
+
} catch {
|
|
10617
|
+
}
|
|
10618
|
+
};
|
|
10619
|
+
return {
|
|
10620
|
+
name: "local-process",
|
|
10621
|
+
async acquire(attempt) {
|
|
10622
|
+
return { provider: "local-process", id: attempt.turnId };
|
|
10623
|
+
},
|
|
10624
|
+
exec(_handle, argv, env, { signal, stdin }) {
|
|
10625
|
+
const [command, ...args] = argv;
|
|
10626
|
+
if (!command) throw new Error("local-process: exec needs a command");
|
|
10627
|
+
const child = spawnFn(command, args, {
|
|
10628
|
+
env: { ...env },
|
|
10629
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
10630
|
+
detached: process.platform !== "win32"
|
|
10631
|
+
});
|
|
10632
|
+
child.stdin?.on("error", () => {
|
|
10633
|
+
});
|
|
10634
|
+
if (stdin) child.stdin?.write(stdin);
|
|
10635
|
+
const exit = new Promise((resolve2) => {
|
|
10636
|
+
let settled = false;
|
|
10637
|
+
const done = (code) => {
|
|
10638
|
+
if (settled) return;
|
|
10639
|
+
settled = true;
|
|
10640
|
+
signal.removeEventListener("abort", stop);
|
|
10641
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
10642
|
+
resolve2(code);
|
|
10643
|
+
};
|
|
10644
|
+
let forceTimer;
|
|
10645
|
+
const stop = () => {
|
|
10646
|
+
child.kill("SIGTERM");
|
|
10647
|
+
forceTimer = setTimeout(() => child.kill("SIGKILL"), killGraceMs);
|
|
10648
|
+
forceTimer.unref?.();
|
|
10649
|
+
};
|
|
10650
|
+
child.once("error", () => done(1));
|
|
10651
|
+
child.once("close", (code, sig) => done(exitCode(code, sig)));
|
|
10652
|
+
if (signal.aborted) stop();
|
|
10653
|
+
else signal.addEventListener("abort", stop, { once: true });
|
|
10654
|
+
});
|
|
10655
|
+
if (!child.stdout || !child.stderr || !child.stdin)
|
|
10656
|
+
throw new Error("local-process: runner stdio missing");
|
|
10657
|
+
const pid = child.pid;
|
|
10658
|
+
return {
|
|
10659
|
+
exit,
|
|
10660
|
+
...pid !== void 0 ? { pid } : {},
|
|
10661
|
+
stdout: child.stdout,
|
|
10662
|
+
stderr: child.stderr,
|
|
10663
|
+
stdin: child.stdin,
|
|
10664
|
+
async reap(groups) {
|
|
10665
|
+
if (process.platform === "win32") return;
|
|
10666
|
+
const targets = [.../* @__PURE__ */ new Set([...pid !== void 0 ? [pid] : [], ...groups])].filter(
|
|
10667
|
+
groupAlive
|
|
10668
|
+
);
|
|
10669
|
+
if (!targets.length) return;
|
|
10670
|
+
for (const pgid of targets) signalGroup(pgid, "SIGTERM");
|
|
10671
|
+
await new Promise((resolve2) => setTimeout(resolve2, reapGraceMs));
|
|
10672
|
+
for (const pgid of targets.filter(groupAlive)) signalGroup(pgid, "SIGKILL");
|
|
10673
|
+
}
|
|
10674
|
+
};
|
|
10675
|
+
},
|
|
10676
|
+
async release() {
|
|
10677
|
+
}
|
|
10678
|
+
};
|
|
10679
|
+
}
|
|
10680
|
+
|
|
10681
|
+
// src/runner/device-locks.ts
|
|
10682
|
+
var DeviceLocks = class {
|
|
10683
|
+
holders = /* @__PURE__ */ new Map();
|
|
10684
|
+
queues = /* @__PURE__ */ new Map();
|
|
10685
|
+
// `owner` is unique across the device; `grant` is called once, when it holds.
|
|
10686
|
+
request(key, owner, grant) {
|
|
10687
|
+
if (!this.holders.has(key)) {
|
|
10688
|
+
this.holders.set(key, owner);
|
|
10689
|
+
grant();
|
|
10690
|
+
return;
|
|
10691
|
+
}
|
|
10692
|
+
const queue = this.queues.get(key) ?? [];
|
|
10693
|
+
queue.push({ owner, grant });
|
|
10694
|
+
this.queues.set(key, queue);
|
|
10695
|
+
}
|
|
10696
|
+
// Gives back what `owner` holds and withdraws what it is waiting for.
|
|
10697
|
+
release(owner) {
|
|
10698
|
+
for (const [key, queue] of this.queues) {
|
|
10699
|
+
const rest = queue.filter((w) => w.owner !== owner);
|
|
10700
|
+
if (rest.length) this.queues.set(key, rest);
|
|
10701
|
+
else this.queues.delete(key);
|
|
10702
|
+
}
|
|
10703
|
+
for (const [key, holder] of [...this.holders]) {
|
|
10704
|
+
if (holder !== owner) continue;
|
|
10705
|
+
this.holders.delete(key);
|
|
10706
|
+
this.handOn(key);
|
|
10707
|
+
}
|
|
10708
|
+
}
|
|
10709
|
+
// What owners starting with `prefix` hold, by key — one runner's, when it
|
|
10710
|
+
// exits, so the launcher can decide which it may give back at once.
|
|
10711
|
+
heldBy(prefix) {
|
|
10712
|
+
return [...this.holders].filter(([, owner]) => owner.startsWith(prefix)).map(([key, owner]) => ({ key, owner }));
|
|
10713
|
+
}
|
|
10714
|
+
// Withdraws every request still waiting from owners starting with `prefix`,
|
|
10715
|
+
// leaving what they hold.
|
|
10716
|
+
withdraw(prefix) {
|
|
10717
|
+
for (const [key, queue] of this.queues) {
|
|
10718
|
+
const rest = queue.filter((w) => !w.owner.startsWith(prefix));
|
|
10719
|
+
if (rest.length) this.queues.set(key, rest);
|
|
10720
|
+
else this.queues.delete(key);
|
|
10721
|
+
}
|
|
10722
|
+
}
|
|
10723
|
+
holder(key) {
|
|
10724
|
+
return this.holders.get(key);
|
|
10725
|
+
}
|
|
10726
|
+
handOn(key) {
|
|
10727
|
+
const queue = this.queues.get(key);
|
|
10728
|
+
const next = queue?.shift();
|
|
10729
|
+
if (!queue?.length) this.queues.delete(key);
|
|
10730
|
+
if (!next) return;
|
|
10731
|
+
this.holders.set(key, next.owner);
|
|
10732
|
+
next.grant();
|
|
10733
|
+
}
|
|
10734
|
+
};
|
|
10735
|
+
|
|
10736
|
+
// src/runner/harness-sandbox.ts
|
|
10737
|
+
import { chmodSync as chmodSync5, mkdirSync as mkdirSync9, mkdtempSync, rmSync as rmSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
10738
|
+
import { homedir as homedir3, tmpdir as tmpdir2, userInfo as userInfo2 } from "os";
|
|
10739
|
+
import { join as join15 } from "path";
|
|
10740
|
+
import { SandboxManager } from "@anthropic-ai/sandbox-runtime";
|
|
10741
|
+
function osSandboxUsable(log) {
|
|
10742
|
+
if (!SandboxManager.isSupportedPlatform()) {
|
|
10743
|
+
log.warn(
|
|
10744
|
+
{ platform: process.platform },
|
|
10745
|
+
'runners: sandbox "os" is not available on this platform; turns run unsandboxed'
|
|
10746
|
+
);
|
|
10747
|
+
return false;
|
|
10748
|
+
}
|
|
10749
|
+
const deps = SandboxManager.checkDependencies();
|
|
10750
|
+
if (deps.errors.length) {
|
|
10751
|
+
log.warn(
|
|
10752
|
+
{ errors: deps.errors },
|
|
10753
|
+
'runners: sandbox "os" is missing what it needs on this host; turns run unsandboxed'
|
|
10754
|
+
);
|
|
10755
|
+
return false;
|
|
10756
|
+
}
|
|
10757
|
+
return true;
|
|
10758
|
+
}
|
|
10759
|
+
|
|
10168
10760
|
// src/opencode-models.ts
|
|
10169
10761
|
var OPENCODE_RUNTIME = "opencode";
|
|
10170
10762
|
function mapOpencodeProviders(json) {
|
|
@@ -10243,12 +10835,12 @@ var DispatchClaims = class {
|
|
|
10243
10835
|
|
|
10244
10836
|
// src/outbox.ts
|
|
10245
10837
|
import {
|
|
10246
|
-
existsSync as
|
|
10838
|
+
existsSync as existsSync11,
|
|
10247
10839
|
mkdirSync as mkdirSync10,
|
|
10248
10840
|
readdirSync as readdirSync3,
|
|
10249
|
-
readFileSync as
|
|
10841
|
+
readFileSync as readFileSync8,
|
|
10250
10842
|
renameSync as renameSync4,
|
|
10251
|
-
rmSync as
|
|
10843
|
+
rmSync as rmSync8,
|
|
10252
10844
|
writeFileSync as writeFileSync7
|
|
10253
10845
|
} from "fs";
|
|
10254
10846
|
import { join as join16 } from "path";
|
|
@@ -10264,7 +10856,7 @@ var Outbox = class {
|
|
|
10264
10856
|
// Resolved lazily (per call, like cursor.ts) so tests that swap HOME between
|
|
10265
10857
|
// cases route writes at the right tmpdir.
|
|
10266
10858
|
dir() {
|
|
10267
|
-
return join16(
|
|
10859
|
+
return join16(runnerStateDir(), "outbox", encodeURIComponent(this.workspaceId));
|
|
10268
10860
|
}
|
|
10269
10861
|
fileFor(turnId, seq) {
|
|
10270
10862
|
return join16(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
|
|
@@ -10282,7 +10874,7 @@ var Outbox = class {
|
|
|
10282
10874
|
renameSync4(tmp, target);
|
|
10283
10875
|
} catch (err) {
|
|
10284
10876
|
try {
|
|
10285
|
-
|
|
10877
|
+
rmSync8(tmp, { force: true });
|
|
10286
10878
|
} catch {
|
|
10287
10879
|
}
|
|
10288
10880
|
this.log?.warn(
|
|
@@ -10299,7 +10891,7 @@ var Outbox = class {
|
|
|
10299
10891
|
// wedging the drain.
|
|
10300
10892
|
list() {
|
|
10301
10893
|
const dir2 = this.dir();
|
|
10302
|
-
if (!
|
|
10894
|
+
if (!existsSync11(dir2)) return [];
|
|
10303
10895
|
let names;
|
|
10304
10896
|
try {
|
|
10305
10897
|
names = readdirSync3(dir2);
|
|
@@ -10311,7 +10903,7 @@ var Outbox = class {
|
|
|
10311
10903
|
if (!name.endsWith(".json")) continue;
|
|
10312
10904
|
const full = join16(dir2, name);
|
|
10313
10905
|
try {
|
|
10314
|
-
const parsed = JSON.parse(
|
|
10906
|
+
const parsed = JSON.parse(readFileSync8(full, "utf8"));
|
|
10315
10907
|
if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
|
|
10316
10908
|
entries.push(parsed);
|
|
10317
10909
|
} else {
|
|
@@ -10345,7 +10937,7 @@ var Outbox = class {
|
|
|
10345
10937
|
// different: it is positive evidence that nothing was ever queued here.
|
|
10346
10938
|
maxSeqForTurn(turnId) {
|
|
10347
10939
|
const dir2 = this.dir();
|
|
10348
|
-
if (!
|
|
10940
|
+
if (!existsSync11(dir2)) return 0;
|
|
10349
10941
|
const names = readdirSync3(dir2);
|
|
10350
10942
|
const prefix = `${encodeURIComponent(turnId)}__`;
|
|
10351
10943
|
let max = 0;
|
|
@@ -10359,13 +10951,13 @@ var Outbox = class {
|
|
|
10359
10951
|
// Remove a delivered (or terminally-discarded) entry. No-op if already gone.
|
|
10360
10952
|
remove(turnId, seq) {
|
|
10361
10953
|
try {
|
|
10362
|
-
|
|
10954
|
+
rmSync8(this.fileFor(turnId, seq), { force: true });
|
|
10363
10955
|
} catch {
|
|
10364
10956
|
}
|
|
10365
10957
|
}
|
|
10366
10958
|
size() {
|
|
10367
10959
|
const dir2 = this.dir();
|
|
10368
|
-
if (!
|
|
10960
|
+
if (!existsSync11(dir2)) return 0;
|
|
10369
10961
|
try {
|
|
10370
10962
|
return readdirSync3(dir2).filter((n) => n.endsWith(".json")).length;
|
|
10371
10963
|
} catch {
|
|
@@ -10378,7 +10970,7 @@ var Outbox = class {
|
|
|
10378
10970
|
"A saved update is unreadable and couldn't be delivered. Check the conversation for missing replies."
|
|
10379
10971
|
);
|
|
10380
10972
|
try {
|
|
10381
|
-
|
|
10973
|
+
rmSync8(full, { force: true });
|
|
10382
10974
|
} catch {
|
|
10383
10975
|
}
|
|
10384
10976
|
}
|
|
@@ -10412,53 +11004,6 @@ var Outbox = class {
|
|
|
10412
11004
|
}
|
|
10413
11005
|
};
|
|
10414
11006
|
|
|
10415
|
-
// src/run-config.ts
|
|
10416
|
-
import { z as z15 } from "zod";
|
|
10417
|
-
var mcpStdioServerSchema = z15.object({
|
|
10418
|
-
type: z15.literal("stdio").optional(),
|
|
10419
|
-
command: z15.string().min(1),
|
|
10420
|
-
args: z15.array(z15.string()).optional(),
|
|
10421
|
-
env: z15.record(z15.string(), z15.string()).optional()
|
|
10422
|
-
});
|
|
10423
|
-
var mcpHttpServerSchema = z15.object({
|
|
10424
|
-
type: z15.literal("http"),
|
|
10425
|
-
url: z15.string().url(),
|
|
10426
|
-
headers: z15.record(z15.string(), z15.string()).optional()
|
|
10427
|
-
});
|
|
10428
|
-
var mcpSseServerSchema = z15.object({
|
|
10429
|
-
type: z15.literal("sse"),
|
|
10430
|
-
url: z15.string().url(),
|
|
10431
|
-
headers: z15.record(z15.string(), z15.string()).optional()
|
|
10432
|
-
});
|
|
10433
|
-
var mcpServerDefSchema = z15.union([mcpHttpServerSchema, mcpSseServerSchema, mcpStdioServerSchema]);
|
|
10434
|
-
var thinkingConfigSchema = z15.discriminatedUnion("type", [
|
|
10435
|
-
z15.object({ type: z15.literal("adaptive") }),
|
|
10436
|
-
z15.object({ type: z15.literal("enabled"), budgetTokens: z15.number().int().positive().optional() }),
|
|
10437
|
-
z15.object({ type: z15.literal("disabled") })
|
|
10438
|
-
]);
|
|
10439
|
-
var effortSchema = z15.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
10440
|
-
var runConfigSchema = z15.object({
|
|
10441
|
-
// CT788: the host-access binary replaced the `assistant`/`coding`/`custom` mode
|
|
10442
|
-
// trio + its custom tool lists — `true` grants the host filesystem/shell, absent
|
|
10443
|
-
// is the locked surface. Kept in lockstep with `@cabane/shared`'s
|
|
10444
|
-
// `agentRunConfigSchema` (independent zod, forward-compatible: unknown keys are
|
|
10445
|
-
// stripped, so an older companion riding a newer server never rejects the config).
|
|
10446
|
-
hostAccess: z15.boolean().optional(),
|
|
10447
|
-
mcpServers: z15.record(z15.string(), mcpServerDefSchema).optional(),
|
|
10448
|
-
model: z15.string().min(1).optional(),
|
|
10449
|
-
thinking: thinkingConfigSchema.optional(),
|
|
10450
|
-
effort: effortSchema.optional()
|
|
10451
|
-
});
|
|
10452
|
-
var EMPTY_RUN_CONFIG = {};
|
|
10453
|
-
function parseRunConfig(raw, onWarn) {
|
|
10454
|
-
const result = runConfigSchema.safeParse(raw ?? {});
|
|
10455
|
-
if (result.success) return result.data;
|
|
10456
|
-
onWarn?.(
|
|
10457
|
-
`run-config from Cabane didn't parse (${result.error.issues.map((i) => i.path.join(".") || "(root)").join(", ")}) \u2014 falling back to the locked assistant default`
|
|
10458
|
-
);
|
|
10459
|
-
return EMPTY_RUN_CONFIG;
|
|
10460
|
-
}
|
|
10461
|
-
|
|
10462
11007
|
// src/sse.ts
|
|
10463
11008
|
import { createParser } from "eventsource-parser";
|
|
10464
11009
|
var MAX_BACKOFF_MS = 3e4;
|
|
@@ -10519,7 +11064,7 @@ var SseSubscriber = class {
|
|
|
10519
11064
|
}
|
|
10520
11065
|
async connect() {
|
|
10521
11066
|
const base = this.opts.baseUrl.endsWith("/") ? this.opts.baseUrl.slice(0, -1) : this.opts.baseUrl;
|
|
10522
|
-
const url =
|
|
11067
|
+
const url = `${base}${this.opts.path}`;
|
|
10523
11068
|
this.controller = new AbortController();
|
|
10524
11069
|
const headers = {
|
|
10525
11070
|
Authorization: `Bearer ${this.opts.token}`,
|
|
@@ -10623,9 +11168,14 @@ var CompanionSupervisor = class {
|
|
|
10623
11168
|
exitFn;
|
|
10624
11169
|
reexecFn;
|
|
10625
11170
|
dispatcherFactory;
|
|
11171
|
+
runnerCommand;
|
|
11172
|
+
providerForRunners;
|
|
10626
11173
|
unrunnableRetryDelaysMs;
|
|
10627
11174
|
deviceApi = null;
|
|
10628
11175
|
deviceSub = null;
|
|
11176
|
+
// ACC8: whether the device stream is open, which is what every workspace's
|
|
11177
|
+
// link on the dashboard reports.
|
|
11178
|
+
deviceConnected = false;
|
|
10629
11179
|
recoveryChecked = false;
|
|
10630
11180
|
recovering = false;
|
|
10631
11181
|
heartbeated = false;
|
|
@@ -10664,6 +11214,8 @@ var CompanionSupervisor = class {
|
|
|
10664
11214
|
this.exitFn = opts.exit ?? ((code) => process.exit(code));
|
|
10665
11215
|
this.reexecFn = opts.reexec ?? defaultReexec;
|
|
10666
11216
|
this.dispatcherFactory = opts.dispatcherFactory;
|
|
11217
|
+
if (opts.runnerCommand) this.runnerCommand = opts.runnerCommand;
|
|
11218
|
+
if (opts.runnerProvider) this.providerForRunners = opts.runnerProvider;
|
|
10667
11219
|
this.unrunnableRetryDelaysMs = opts.unrunnableRetryDelaysMs ?? DEFAULT_UNRUNNABLE_RETRY_DELAYS_MS;
|
|
10668
11220
|
this.latchBootBundledState();
|
|
10669
11221
|
}
|
|
@@ -10692,7 +11244,18 @@ var CompanionSupervisor = class {
|
|
|
10692
11244
|
token: this.config.deviceToken,
|
|
10693
11245
|
log: this.log,
|
|
10694
11246
|
lastEventId: null,
|
|
10695
|
-
onOpen: () =>
|
|
11247
|
+
onOpen: () => {
|
|
11248
|
+
this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ?? hostname2()}`);
|
|
11249
|
+
this.deviceConnected = true;
|
|
11250
|
+
for (const wr of this.workspaces.values()) {
|
|
11251
|
+
this.hub.setConnected(wr.workspaceId, true);
|
|
11252
|
+
for (const a of wr.agents.values()) a.kickDrain();
|
|
11253
|
+
}
|
|
11254
|
+
},
|
|
11255
|
+
onClose: () => {
|
|
11256
|
+
this.deviceConnected = false;
|
|
11257
|
+
for (const wr of this.workspaces.values()) this.hub.setConnected(wr.workspaceId, false);
|
|
11258
|
+
},
|
|
10696
11259
|
onMessage: async (ev) => {
|
|
10697
11260
|
if (ev.event === "assignments_changed") {
|
|
10698
11261
|
if (this.refreshing && this.inFlightRefresh) await this.inFlightRefresh;
|
|
@@ -10724,10 +11287,7 @@ var CompanionSupervisor = class {
|
|
|
10724
11287
|
// at least the loops keeping the event loop alive; the SSE promises let a
|
|
10725
11288
|
// clean teardown resolve.
|
|
10726
11289
|
finishedPromises() {
|
|
10727
|
-
return [
|
|
10728
|
-
...this.deviceSub ? [this.deviceSub.finished] : [],
|
|
10729
|
-
...[...this.workspaces.values()].flatMap((w) => w.sub ? [w.sub.finished] : [])
|
|
10730
|
-
];
|
|
11290
|
+
return [...this.deviceSub ? [this.deviceSub.finished] : []];
|
|
10731
11291
|
}
|
|
10732
11292
|
// ---- device-level loops ----
|
|
10733
11293
|
kickHeartbeat() {
|
|
@@ -10875,11 +11435,8 @@ var CompanionSupervisor = class {
|
|
|
10875
11435
|
workspaceId,
|
|
10876
11436
|
slug: first.workspaceSlug,
|
|
10877
11437
|
name: first.workspaceName,
|
|
10878
|
-
sub: null,
|
|
10879
|
-
sseAgentId: null,
|
|
10880
11438
|
agents: /* @__PURE__ */ new Map(),
|
|
10881
|
-
chains: /* @__PURE__ */ new Map()
|
|
10882
|
-
cursor: new CursorTracker(workspaceId)
|
|
11439
|
+
chains: /* @__PURE__ */ new Map()
|
|
10883
11440
|
};
|
|
10884
11441
|
this.workspaces.set(workspaceId, wr);
|
|
10885
11442
|
this.hub.registerWorkspace({
|
|
@@ -10887,6 +11444,7 @@ var CompanionSupervisor = class {
|
|
|
10887
11444
|
slug: first.workspaceSlug,
|
|
10888
11445
|
name: first.workspaceName
|
|
10889
11446
|
});
|
|
11447
|
+
this.hub.setConnected(workspaceId, this.deviceConnected);
|
|
10890
11448
|
}
|
|
10891
11449
|
const store = loadSecretStoreTolerant();
|
|
10892
11450
|
const exposed = new Set(store.names());
|
|
@@ -10946,7 +11504,7 @@ var CompanionSupervisor = class {
|
|
|
10946
11504
|
);
|
|
10947
11505
|
}
|
|
10948
11506
|
}
|
|
10949
|
-
this.
|
|
11507
|
+
if (wr.agents.size === 0) void this.removeWorkspace(wr.workspaceId);
|
|
10950
11508
|
}
|
|
10951
11509
|
addAgent(wr, it, credential, runConfig) {
|
|
10952
11510
|
const agentLog = this.log.child({ agentName: it.agentDisplayName });
|
|
@@ -11103,6 +11661,21 @@ var CompanionSupervisor = class {
|
|
|
11103
11661
|
);
|
|
11104
11662
|
}
|
|
11105
11663
|
}
|
|
11664
|
+
// CT1679: the locks every runner on this device shares, held here.
|
|
11665
|
+
runnerLocks = new DeviceLocks();
|
|
11666
|
+
// CT1679: `runners.sandbox`, checked against this host once. A host that
|
|
11667
|
+
// can't make the sandbox says so once and runs its turns unwrapped.
|
|
11668
|
+
sandboxUsable;
|
|
11669
|
+
runnerSandbox() {
|
|
11670
|
+
if (this.config.runners?.sandbox !== "os") return "none";
|
|
11671
|
+
this.sandboxUsable ??= osSandboxUsable(this.log);
|
|
11672
|
+
return this.sandboxUsable ? "os" : "none";
|
|
11673
|
+
}
|
|
11674
|
+
// CT1679: one provider for the device, built on first use.
|
|
11675
|
+
runnerProvider() {
|
|
11676
|
+
this.providerForRunners ??= createLocalProcessProvider();
|
|
11677
|
+
return this.providerForRunners;
|
|
11678
|
+
}
|
|
11106
11679
|
buildDispatcher(ctx) {
|
|
11107
11680
|
const local = localAgentConfig(this.config, {
|
|
11108
11681
|
agentId: ctx.agentId,
|
|
@@ -11153,74 +11726,31 @@ var CompanionSupervisor = class {
|
|
|
11153
11726
|
observer: this.hub.observerFor(ctx.workspaceId, ctx.agentId, ctx.workspaceSlug),
|
|
11154
11727
|
transcriptDir: transcriptsDir(),
|
|
11155
11728
|
// CT566: the shared connector-health memory the turn records into on settle.
|
|
11156
|
-
connectorHealth: this.connectorHealth
|
|
11157
|
-
|
|
11158
|
-
|
|
11159
|
-
|
|
11160
|
-
|
|
11161
|
-
|
|
11162
|
-
|
|
11163
|
-
|
|
11164
|
-
|
|
11165
|
-
|
|
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
|
-
}
|
|
11729
|
+
connectorHealth: this.connectorHealth,
|
|
11730
|
+
...runnersEnabled(this.config) ? {
|
|
11731
|
+
runner: {
|
|
11732
|
+
provider: this.runnerProvider(),
|
|
11733
|
+
command: this.runnerCommand ?? selfRunnerCommand(),
|
|
11734
|
+
...this.config.deviceToken ? { deviceToken: this.config.deviceToken } : {},
|
|
11735
|
+
connectorHealth: this.connectorHealth,
|
|
11736
|
+
locks: this.runnerLocks,
|
|
11737
|
+
sandbox: this.runnerSandbox()
|
|
11738
|
+
}
|
|
11739
|
+
} : {}
|
|
11192
11740
|
});
|
|
11193
|
-
wr.sub.start();
|
|
11194
|
-
this.log.debug({ workspaceId: wr.workspaceId }, "companion: subscribed");
|
|
11195
11741
|
}
|
|
11196
11742
|
async removeWorkspace(workspaceId) {
|
|
11197
11743
|
const wr = this.workspaces.get(workspaceId);
|
|
11198
11744
|
if (!wr) return;
|
|
11199
11745
|
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
11746
|
this.workspaces.delete(workspaceId);
|
|
11205
11747
|
this.hub.removeWorkspace(workspaceId);
|
|
11206
11748
|
}
|
|
11207
11749
|
// ---- 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
11750
|
// 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.
|
|
11751
|
+
// device and authenticated as it. ACC8: the only stream this companion opens.
|
|
11752
|
+
// It keeps no cursor: it replays what is still offered on every connect
|
|
11753
|
+
// instead of resuming from a `Last-Event-ID`.
|
|
11224
11754
|
async handleDeviceMessage(ev) {
|
|
11225
11755
|
const wire = this.parseFrame(ev);
|
|
11226
11756
|
if (!wire) return;
|
|
@@ -11243,7 +11773,7 @@ var CompanionSupervisor = class {
|
|
|
11243
11773
|
}
|
|
11244
11774
|
}
|
|
11245
11775
|
this.hub.recordEvent(wr.workspaceId);
|
|
11246
|
-
await this.deliverFrame(wr,
|
|
11776
|
+
await this.deliverFrame(wr, wire);
|
|
11247
11777
|
}
|
|
11248
11778
|
parseFrame(ev) {
|
|
11249
11779
|
try {
|
|
@@ -11256,30 +11786,27 @@ var CompanionSupervisor = class {
|
|
|
11256
11786
|
return null;
|
|
11257
11787
|
}
|
|
11258
11788
|
}
|
|
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.
|
|
11789
|
+
// ACC10: the dedupe gate. A reconnect replays every dispatch still offered,
|
|
11790
|
+
// including one whose turn is already running here; the first copy runs the
|
|
11791
|
+
// turn and a later one waits for it and then does nothing.
|
|
11264
11792
|
//
|
|
11265
11793
|
// A cancel needs no gate: `dispatcher.cancel` on an already-aborted run is a
|
|
11266
11794
|
// no-op that reports `false`, so a second copy is inert.
|
|
11267
|
-
async deliverFrame(wr,
|
|
11795
|
+
async deliverFrame(wr, wire) {
|
|
11268
11796
|
const claim = wire.type === "device:dispatch_requested" && wire.id ? this.claims.claim(wire.id) : null;
|
|
11269
11797
|
if (claim && !claim.first) {
|
|
11270
11798
|
await claim.done;
|
|
11271
|
-
if (cursorId) wr.cursor.settle(cursorId);
|
|
11272
11799
|
return;
|
|
11273
11800
|
}
|
|
11274
11801
|
try {
|
|
11275
|
-
await this.routeFrame(wr,
|
|
11802
|
+
await this.routeFrame(wr, wire);
|
|
11276
11803
|
} finally {
|
|
11277
11804
|
if (claim) this.claims.release(wire.id);
|
|
11278
11805
|
}
|
|
11279
11806
|
}
|
|
11280
11807
|
// Everything below here is the routing as it was: it takes a frame and knows
|
|
11281
11808
|
// nothing about which stream carried it.
|
|
11282
|
-
async routeFrame(wr,
|
|
11809
|
+
async routeFrame(wr, wire) {
|
|
11283
11810
|
if (wire.type === "device:cancel_requested") {
|
|
11284
11811
|
const payload2 = {
|
|
11285
11812
|
type: "device:cancel_requested",
|
|
@@ -11299,12 +11826,10 @@ var CompanionSupervisor = class {
|
|
|
11299
11826
|
);
|
|
11300
11827
|
}
|
|
11301
11828
|
}
|
|
11302
|
-
if (cursorId) wr.cursor.settle(cursorId);
|
|
11303
11829
|
return;
|
|
11304
11830
|
}
|
|
11305
11831
|
const payload = dispatchPayload(wire);
|
|
11306
11832
|
if (!payload) {
|
|
11307
|
-
if (cursorId) wr.cursor.settle(cursorId);
|
|
11308
11833
|
return;
|
|
11309
11834
|
}
|
|
11310
11835
|
if (this.deviceId && payload.deviceId && payload.deviceId !== this.deviceId) {
|
|
@@ -11317,7 +11842,6 @@ var CompanionSupervisor = class {
|
|
|
11317
11842
|
},
|
|
11318
11843
|
"companion: dispatch addressed to another device; ignoring"
|
|
11319
11844
|
);
|
|
11320
|
-
if (cursorId) wr.cursor.settle(cursorId);
|
|
11321
11845
|
return;
|
|
11322
11846
|
}
|
|
11323
11847
|
let agent = wr.agents.get(payload.agentId);
|
|
@@ -11325,16 +11849,14 @@ var CompanionSupervisor = class {
|
|
|
11325
11849
|
agent = await this.recoverRacedAgent(wr, payload);
|
|
11326
11850
|
if (!agent) {
|
|
11327
11851
|
if (this.deviceId && payload.deviceId === this.deviceId) {
|
|
11328
|
-
|
|
11329
|
-
if (!reported) return;
|
|
11852
|
+
await this.reportUnrunnableDispatch(wr.workspaceId, payload);
|
|
11330
11853
|
}
|
|
11331
|
-
if (cursorId) wr.cursor.settle(cursorId);
|
|
11332
11854
|
return;
|
|
11333
11855
|
}
|
|
11334
11856
|
}
|
|
11335
11857
|
const chainKey = `${payload.conversationId}|${payload.agentId}`;
|
|
11336
11858
|
const prev = wr.chains.get(chainKey) ?? Promise.resolve();
|
|
11337
|
-
const tail = prev.then(() => this.runDispatch(wr,
|
|
11859
|
+
const tail = prev.then(() => this.runDispatch(wr, wire.id, payload, agent)).catch((err) => {
|
|
11338
11860
|
this.log.warn(
|
|
11339
11861
|
{
|
|
11340
11862
|
workspaceId: wr.workspaceId,
|
|
@@ -11351,8 +11873,7 @@ var CompanionSupervisor = class {
|
|
|
11351
11873
|
}
|
|
11352
11874
|
// CT1146: tell the server this device cannot run a dispatch it was addressed to.
|
|
11353
11875
|
//
|
|
11354
|
-
// Returns whether the report LANDED
|
|
11355
|
-
// caller may settle the cursor. The server checks the recorded dispatch
|
|
11876
|
+
// Returns whether the report LANDED. The server checks the recorded dispatch
|
|
11356
11877
|
// destination even if the agent's assignment has moved. Reporting promptly
|
|
11357
11878
|
// explains the roster miss; the thirty-minute unanswered-offer deadline is
|
|
11358
11879
|
// only the backstop when no report lands.
|
|
@@ -11446,10 +11967,10 @@ var CompanionSupervisor = class {
|
|
|
11446
11967
|
};
|
|
11447
11968
|
const tail = this.runDispatch(
|
|
11448
11969
|
wr,
|
|
11449
|
-
// No frame delivered this one — it came off `/runs` — so there is
|
|
11450
|
-
//
|
|
11451
|
-
//
|
|
11452
|
-
|
|
11970
|
+
// No frame delivered this one — it came off `/runs` — so there is no
|
|
11971
|
+
// dispatch id to dedupe on. The `recoveredTurns` set above is this
|
|
11972
|
+
// path's own guard.
|
|
11973
|
+
"",
|
|
11453
11974
|
payload,
|
|
11454
11975
|
agent,
|
|
11455
11976
|
run.turnId
|
|
@@ -11471,61 +11992,55 @@ var CompanionSupervisor = class {
|
|
|
11471
11992
|
}
|
|
11472
11993
|
}
|
|
11473
11994
|
// One turn: the SJ516 F7 / CT26 skip-vs-resume dedupe, the dispatch, then
|
|
11474
|
-
//
|
|
11995
|
+
// the console summary. Mirrors the old per-workspace `handleEvent`, now
|
|
11475
11996
|
// scoped to one agent.
|
|
11476
|
-
async runDispatch(wr,
|
|
11997
|
+
async runDispatch(wr, dispatchId, payload, agent, recoveredTurnId) {
|
|
11477
11998
|
const workspaceId = wr.workspaceId;
|
|
11478
|
-
|
|
11479
|
-
if (
|
|
11480
|
-
};
|
|
11481
|
-
if (ids.dispatchId && hasDispatched(workspaceId, ids.dispatchId)) {
|
|
11482
|
-
if (hasCompleted(workspaceId, ids.dispatchId)) {
|
|
11999
|
+
if (dispatchId && hasDispatched(workspaceId, dispatchId)) {
|
|
12000
|
+
if (hasCompleted(workspaceId, dispatchId)) {
|
|
11483
12001
|
this.log.debug(
|
|
11484
|
-
{ workspaceId, eventId:
|
|
12002
|
+
{ workspaceId, eventId: dispatchId },
|
|
11485
12003
|
"companion: skipping already-completed event (resume after restart)"
|
|
11486
12004
|
);
|
|
11487
|
-
settleCursor();
|
|
11488
12005
|
return;
|
|
11489
12006
|
}
|
|
11490
12007
|
if (noResume()) {
|
|
11491
12008
|
this.log.debug(
|
|
11492
|
-
{ workspaceId, eventId:
|
|
12009
|
+
{ workspaceId, eventId: dispatchId },
|
|
11493
12010
|
"companion: skipping interrupted turn (CABANE_COMPANION_NO_RESUME=1) \u2014 resume disabled for this boot"
|
|
11494
12011
|
);
|
|
11495
|
-
markCompleted(workspaceId,
|
|
11496
|
-
settleCursor();
|
|
12012
|
+
markCompleted(workspaceId, dispatchId);
|
|
11497
12013
|
return;
|
|
11498
12014
|
}
|
|
11499
|
-
const attempt = bumpResumeAttempt(workspaceId,
|
|
12015
|
+
const attempt = bumpResumeAttempt(workspaceId, dispatchId);
|
|
11500
12016
|
if (attempt > MAX_RESUME_ATTEMPTS) {
|
|
11501
12017
|
this.log.error(
|
|
11502
|
-
{ workspaceId, eventId:
|
|
12018
|
+
{ workspaceId, eventId: dispatchId, attempt, cap: MAX_RESUME_ATTEMPTS },
|
|
11503
12019
|
"An interrupted turn couldn't finish after several attempts. Reply in Cabane to try again."
|
|
11504
12020
|
);
|
|
11505
|
-
markCompleted(workspaceId,
|
|
11506
|
-
settleCursor();
|
|
12021
|
+
markCompleted(workspaceId, dispatchId);
|
|
11507
12022
|
return;
|
|
11508
12023
|
}
|
|
11509
12024
|
this.log.debug(
|
|
11510
|
-
{ workspaceId, eventId:
|
|
12025
|
+
{ workspaceId, eventId: dispatchId, attempt, cap: MAX_RESUME_ATTEMPTS },
|
|
11511
12026
|
"companion: re-dispatching interrupted turn (resume after restart)"
|
|
11512
12027
|
);
|
|
11513
12028
|
}
|
|
11514
|
-
const resumedTurnId = recoveredTurnId ?? (
|
|
11515
|
-
const turnId = resumedTurnId ??
|
|
11516
|
-
if (
|
|
11517
|
-
const liveTurnIds = rememberTurnId(workspaceId,
|
|
12029
|
+
const resumedTurnId = recoveredTurnId ?? (dispatchId ? turnIdForEvent(workspaceId, dispatchId) : null);
|
|
12030
|
+
const turnId = resumedTurnId ?? randomUUID5();
|
|
12031
|
+
if (dispatchId) {
|
|
12032
|
+
const liveTurnIds = rememberTurnId(workspaceId, dispatchId, turnId);
|
|
11518
12033
|
if (liveTurnIds > TURN_ID_OVERFLOW_WARN) {
|
|
11519
12034
|
this.log.debug(
|
|
11520
12035
|
{ workspaceId, liveTurnIds, threshold: TURN_ID_OVERFLOW_WARN },
|
|
11521
12036
|
"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
12037
|
);
|
|
11523
12038
|
}
|
|
11524
|
-
markDispatched(workspaceId,
|
|
12039
|
+
markDispatched(workspaceId, dispatchId);
|
|
11525
12040
|
}
|
|
11526
12041
|
if (resumedTurnId) {
|
|
11527
12042
|
this.log.debug(
|
|
11528
|
-
{ workspaceId, eventId:
|
|
12043
|
+
{ workspaceId, eventId: dispatchId, turnId },
|
|
11529
12044
|
"companion: resuming an interrupted turn under its original id"
|
|
11530
12045
|
);
|
|
11531
12046
|
}
|
|
@@ -11540,8 +12055,7 @@ var CompanionSupervisor = class {
|
|
|
11540
12055
|
turnId,
|
|
11541
12056
|
resumed: resumedTurnId !== null
|
|
11542
12057
|
});
|
|
11543
|
-
if (
|
|
11544
|
-
settleCursor();
|
|
12058
|
+
if (dispatchId) markCompleted(workspaceId, dispatchId);
|
|
11545
12059
|
const durationS = (result.durationMs / 1e3).toFixed(1);
|
|
11546
12060
|
const bindings = {
|
|
11547
12061
|
...turnBindings,
|
|
@@ -11790,12 +12304,10 @@ var CompanionSupervisor = class {
|
|
|
11790
12304
|
this.deviceSub?.stop();
|
|
11791
12305
|
for (const wr of this.workspaces.values()) {
|
|
11792
12306
|
for (const a of wr.agents.values()) a.cancelDrain();
|
|
11793
|
-
if (wr.sub) wr.sub.stop();
|
|
11794
12307
|
}
|
|
11795
12308
|
await Promise.all([
|
|
11796
12309
|
...heartbeat ? [waitForBoundedHeartbeat(heartbeat)] : [],
|
|
11797
|
-
...this.deviceSub ? [this.deviceSub.finished] : []
|
|
11798
|
-
...[...this.workspaces.values()].flatMap((wr) => wr.sub ? [wr.sub.finished] : [])
|
|
12310
|
+
...this.deviceSub ? [this.deviceSub.finished] : []
|
|
11799
12311
|
]);
|
|
11800
12312
|
}
|
|
11801
12313
|
async requestStop() {
|
|
@@ -11820,9 +12332,9 @@ async function waitForBoundedHeartbeat(heartbeat) {
|
|
|
11820
12332
|
}
|
|
11821
12333
|
function defaultReexec() {
|
|
11822
12334
|
clearRuntimeState();
|
|
11823
|
-
void import("child_process").then(({ spawn:
|
|
12335
|
+
void import("child_process").then(({ spawn: spawn7 }) => {
|
|
11824
12336
|
try {
|
|
11825
|
-
const child =
|
|
12337
|
+
const child = spawn7(process.execPath, process.argv.slice(1), {
|
|
11826
12338
|
stdio: "inherit",
|
|
11827
12339
|
detached: false
|
|
11828
12340
|
});
|
|
@@ -11892,7 +12404,7 @@ function handleUncaught(log, err, origin) {
|
|
|
11892
12404
|
}
|
|
11893
12405
|
|
|
11894
12406
|
// src/crash-marker.ts
|
|
11895
|
-
import { existsSync as
|
|
12407
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync11, readFileSync as readFileSync9, rmSync as rmSync9, writeFileSync as writeFileSync8 } from "fs";
|
|
11896
12408
|
import { join as join17 } from "path";
|
|
11897
12409
|
function crashMarkerPath() {
|
|
11898
12410
|
return join17(cabaneDir(), "last-error.json");
|
|
@@ -11907,7 +12419,7 @@ function recordCrash(rec) {
|
|
|
11907
12419
|
function clearCrash() {
|
|
11908
12420
|
try {
|
|
11909
12421
|
const path = crashMarkerPath();
|
|
11910
|
-
if (
|
|
12422
|
+
if (existsSync12(path)) rmSync9(path, { force: true });
|
|
11911
12423
|
} catch {
|
|
11912
12424
|
}
|
|
11913
12425
|
}
|
|
@@ -11950,7 +12462,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
11950
12462
|
opencodeServerUrl: cfg.opencode?.serverUrl,
|
|
11951
12463
|
codex: isCodexEnabled(cfg)
|
|
11952
12464
|
});
|
|
11953
|
-
const instanceId =
|
|
12465
|
+
const instanceId = randomUUID6();
|
|
11954
12466
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11955
12467
|
const pre = readLiveRuntimeState();
|
|
11956
12468
|
if (pre && await verifyRuntime(pre) === "stale") clearRuntimeState();
|