@cabane/companion 0.6.121 → 0.6.123
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 +11754 -9824
- package/dist/pairing-config.js +10 -0
- package/dist/runtime.js +1424 -643
- 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";
|
|
@@ -2500,6 +2516,13 @@ var CabaneApi = class {
|
|
|
2500
2516
|
(e) => e.turnId === turnId && e.kind === "message" && e.body?.kind !== "runtime_notice"
|
|
2501
2517
|
);
|
|
2502
2518
|
}
|
|
2519
|
+
// CT1680: the launcher mints the runner credential for one dispatch, on its
|
|
2520
|
+
// PAT and from its device, and hands it to the runner in place of both. A
|
|
2521
|
+
// retry is safe: the server returns the same runner id and retires the
|
|
2522
|
+
// credential the lost response carried.
|
|
2523
|
+
mintRunnerCredential(body) {
|
|
2524
|
+
return this.request("POST", "/api/agent/runner-credentials", body, { retry: true });
|
|
2525
|
+
}
|
|
2503
2526
|
// Read the skip intent recorded by the URL turn-control MCP surface.
|
|
2504
2527
|
// Codex/opencode use this server-side state; settle imports skipped/skipReason
|
|
2505
2528
|
// into the same local skip state used by claude-code's in-process tools.
|
|
@@ -7811,257 +7834,1039 @@ var ConnectorHealthStore = class {
|
|
|
7811
7834
|
};
|
|
7812
7835
|
|
|
7813
7836
|
// src/dispatcher.ts
|
|
7837
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
7814
7838
|
import { existsSync as existsSync10, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
7815
7839
|
import { join as join14 } from "path";
|
|
7816
7840
|
|
|
7817
|
-
// src/
|
|
7841
|
+
// src/runner/launch.ts
|
|
7818
7842
|
import { createInterface } from "readline";
|
|
7819
|
-
import { homedir as homedir2 } from "os";
|
|
7820
|
-
import pc from "picocolors";
|
|
7821
|
-
function tildePath(path) {
|
|
7822
|
-
const home = homedir2();
|
|
7823
|
-
if (home && path.startsWith(home + "/")) return `~${path.slice(home.length)}`;
|
|
7824
|
-
return path;
|
|
7825
|
-
}
|
|
7826
7843
|
|
|
7827
|
-
// src/
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
// src/turn-control-tools.ts
|
|
7832
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
7833
|
-
import { z as z13 } from "zod";
|
|
7834
|
-
var COMPANION_LOCAL_MCP_SERVER = "cabane_companion";
|
|
7835
|
-
var COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
|
|
7836
|
-
var SKIP_TURN_TOOL = "skip_turn";
|
|
7837
|
-
var ASK_TOOL = "ask";
|
|
7838
|
-
var ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${ASK_TOOL}`;
|
|
7839
|
-
var WITHDRAW_ASK_TOOL = "withdraw_ask";
|
|
7840
|
-
var WITHDRAW_ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WITHDRAW_ASK_TOOL}`;
|
|
7841
|
-
var WAKE_ME_TOOL = "wake_me";
|
|
7842
|
-
var WAKE_ME_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WAKE_ME_TOOL}`;
|
|
7843
|
-
var CANCEL_WAKE_TOOL = "cancel_wake";
|
|
7844
|
-
var CANCEL_WAKE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${CANCEL_WAKE_TOOL}`;
|
|
7845
|
-
var TURN_ACT_TIMEOUT_MS = 45e3;
|
|
7846
|
-
function createSkipState() {
|
|
7847
|
-
return { skipped: false, reason: null };
|
|
7848
|
-
}
|
|
7849
|
-
function toolError(text) {
|
|
7850
|
-
return { isError: true, content: [{ type: "text", text }] };
|
|
7844
|
+
// src/runner/opencode-lock.ts
|
|
7845
|
+
function opencodeLockKey(serverUrl) {
|
|
7846
|
+
return `opencode:${serverUrl}`;
|
|
7851
7847
|
}
|
|
7852
|
-
async function
|
|
7853
|
-
|
|
7848
|
+
async function stopRemoteOpencodeSession(serverUrl, sessionId, opts = {}) {
|
|
7849
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
7850
|
+
const base = serverUrl.replace(/\/+$/, "");
|
|
7851
|
+
const id = encodeURIComponent(sessionId);
|
|
7854
7852
|
try {
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
);
|
|
7853
|
+
await doFetch(`${base}/session/${id}/abort`, {
|
|
7854
|
+
method: "POST",
|
|
7855
|
+
signal: AbortSignal.timeout(5e3)
|
|
7856
|
+
});
|
|
7857
|
+
} catch {
|
|
7858
|
+
}
|
|
7859
|
+
const attempts = opts.attempts ?? 20;
|
|
7860
|
+
for (let i = 0; i < attempts; i++) {
|
|
7861
|
+
try {
|
|
7862
|
+
const res = await doFetch(`${base}/session/status`, { signal: AbortSignal.timeout(5e3) });
|
|
7863
|
+
if (res.ok) {
|
|
7864
|
+
const status = await res.json();
|
|
7865
|
+
const state = status[sessionId];
|
|
7866
|
+
if (!state || state.type === "idle") return true;
|
|
7870
7867
|
}
|
|
7871
|
-
|
|
7868
|
+
} catch {
|
|
7872
7869
|
}
|
|
7873
|
-
|
|
7870
|
+
await new Promise((resolve2) => setTimeout(resolve2, opts.pollMs ?? 500));
|
|
7874
7871
|
}
|
|
7875
|
-
|
|
7876
|
-
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
7872
|
+
return false;
|
|
7877
7873
|
}
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7874
|
+
|
|
7875
|
+
// src/turn-containment.ts
|
|
7876
|
+
import { execFile, execFileSync, spawn as spawn4 } from "child_process";
|
|
7877
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
7878
|
+
import { promisify } from "util";
|
|
7879
|
+
var execFileAsync = promisify(execFile);
|
|
7880
|
+
var asContained = (child) => child;
|
|
7881
|
+
var CONTAINMENT_STOP_GRACE_SEC = 5;
|
|
7882
|
+
var FALLBACK_GRACE_MS = CONTAINMENT_STOP_GRACE_SEC * 1e3;
|
|
7883
|
+
var CAPABILITY_PROBE_TIMEOUT_MS = 4e3;
|
|
7884
|
+
function turnScopeUnit(turnId, attempt) {
|
|
7885
|
+
const safe3 = turnId.replace(/[^A-Za-z0-9_.-]/g, "-");
|
|
7886
|
+
return `cabane-turn-${safe3}-${attempt}`;
|
|
7882
7887
|
}
|
|
7883
|
-
function
|
|
7884
|
-
|
|
7885
|
-
const
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
{
|
|
7896
|
-
reason: z13.string().min(1).max(MAX_SKIP_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.skip_turn.reason)
|
|
7897
|
-
},
|
|
7898
|
-
async (args) => {
|
|
7899
|
-
skipState.skipped = true;
|
|
7900
|
-
skipState.reason = args.reason;
|
|
7901
|
-
return {
|
|
7902
|
-
content: [{ type: "text", text: JSON.stringify({ skipped: true }) }]
|
|
7903
|
-
};
|
|
7904
|
-
},
|
|
7905
|
-
{ annotations: RECORDER_TOOL, alwaysLoad: true }
|
|
7906
|
-
)
|
|
7907
|
-
] : [],
|
|
7908
|
-
tool(
|
|
7909
|
-
ASK_TOOL,
|
|
7910
|
-
TURN_CONTROL_TOOL_DESCRIPTIONS.ask,
|
|
7911
|
-
{
|
|
7912
|
-
targetUserId: z13.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
|
|
7913
|
-
question: z13.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.question),
|
|
7914
|
-
headline: z13.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.headline),
|
|
7915
|
-
options: z13.array(
|
|
7916
|
-
z13.union([
|
|
7917
|
-
z13.string().min(1).max(MAX_ASK_OPTION_BYTES),
|
|
7918
|
-
z13.object({
|
|
7919
|
-
label: z13.string().min(1).max(MAX_ASK_OPTION_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.label"]),
|
|
7920
|
-
move: z13.object({
|
|
7921
|
-
task: z13.string().min(1).max(200).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move.task"]),
|
|
7922
|
-
status: z13.enum(TASK_STAGES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move.status"])
|
|
7923
|
-
}).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move"])
|
|
7924
|
-
})
|
|
7925
|
-
])
|
|
7926
|
-
).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.options),
|
|
7927
|
-
questions: z13.array(
|
|
7928
|
-
z13.object({
|
|
7929
|
-
headline: z13.string().min(1).max(MAX_ASK_HEADLINE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.headline"]),
|
|
7930
|
-
body: z13.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.body"]),
|
|
7931
|
-
options: z13.array(
|
|
7932
|
-
z13.union([
|
|
7933
|
-
z13.string().min(1).max(MAX_ASK_OPTION_BYTES),
|
|
7934
|
-
z13.object({
|
|
7935
|
-
label: z13.string().min(1).max(MAX_ASK_OPTION_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.label"]),
|
|
7936
|
-
move: z13.object({
|
|
7937
|
-
task: z13.string().min(1).max(200).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move.task"]),
|
|
7938
|
-
status: z13.enum(TASK_STAGES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move.status"])
|
|
7939
|
-
}).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move"])
|
|
7940
|
-
})
|
|
7941
|
-
])
|
|
7942
|
-
).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options"])
|
|
7943
|
-
})
|
|
7944
|
-
).min(1).max(MAX_ASK_ITEMS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.questions),
|
|
7945
|
-
replaces: z13.object({
|
|
7946
|
-
askId: z13.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.askId"]),
|
|
7947
|
-
reason: z13.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.reason"])
|
|
7948
|
-
}).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.replaces)
|
|
7949
|
-
},
|
|
7950
|
-
async (args) => {
|
|
7951
|
-
const hasSingle = args.question !== void 0;
|
|
7952
|
-
const hasArray = args.questions !== void 0 && args.questions.length > 0;
|
|
7953
|
-
if (hasSingle && hasArray) {
|
|
7954
|
-
return toolError(
|
|
7955
|
-
"Provide either `question` (single) or `questions` (array), not both."
|
|
7956
|
-
);
|
|
7957
|
-
}
|
|
7958
|
-
if (!hasSingle && !hasArray) {
|
|
7959
|
-
return toolError("Provide `question` (single) or `questions` (array).");
|
|
7960
|
-
}
|
|
7961
|
-
return performAct(acts, {
|
|
7962
|
-
kind: "ask",
|
|
7963
|
-
callId: randomUUID2(),
|
|
7964
|
-
// CT1350: the swap rides the ask, so the server commits both halves
|
|
7965
|
-
// in one transaction — the person never sees neither or both.
|
|
7966
|
-
...args.replaces ? { replaces: args.replaces } : {},
|
|
7967
|
-
ask: hasArray ? {
|
|
7968
|
-
targetUserId: args.targetUserId,
|
|
7969
|
-
questions: args.questions.map((q) => ({
|
|
7970
|
-
headline: q.headline,
|
|
7971
|
-
...q.body ? { body: q.body } : {},
|
|
7972
|
-
...q.options && q.options.length > 0 ? { options: q.options } : {}
|
|
7973
|
-
}))
|
|
7974
|
-
} : {
|
|
7975
|
-
targetUserId: args.targetUserId,
|
|
7976
|
-
question: args.question,
|
|
7977
|
-
...args.headline ? { headline: args.headline } : {},
|
|
7978
|
-
...args.options && args.options.length > 0 ? { options: args.options } : {}
|
|
7979
|
-
}
|
|
7980
|
-
});
|
|
7981
|
-
},
|
|
7982
|
-
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
7983
|
-
),
|
|
7984
|
-
tool(
|
|
7985
|
-
WAKE_ME_TOOL,
|
|
7986
|
-
TURN_CONTROL_TOOL_DESCRIPTIONS.wake_me,
|
|
7987
|
-
{
|
|
7988
|
-
afterSeconds: z13.number().int().positive().optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.afterSeconds),
|
|
7989
|
-
at: z13.string().datetime({ offset: true }).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.at),
|
|
7990
|
-
note: z13.string().min(1).max(MAX_WAKE_NOTE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.note)
|
|
7991
|
-
},
|
|
7992
|
-
async (args) => {
|
|
7993
|
-
const hasAfter = args.afterSeconds !== void 0;
|
|
7994
|
-
const hasAt = args.at !== void 0;
|
|
7995
|
-
if (hasAfter && hasAt) {
|
|
7996
|
-
return toolError(
|
|
7997
|
-
"Provide either `afterSeconds` (relative) or `at` (absolute), not both."
|
|
7998
|
-
);
|
|
7999
|
-
}
|
|
8000
|
-
if (!hasAfter && !hasAt) {
|
|
8001
|
-
return toolError("Provide `afterSeconds` or `at`.");
|
|
8002
|
-
}
|
|
8003
|
-
return performAct(acts, {
|
|
8004
|
-
kind: "wake",
|
|
8005
|
-
callId: randomUUID2(),
|
|
8006
|
-
wake: {
|
|
8007
|
-
...hasAfter ? { afterSeconds: args.afterSeconds } : {},
|
|
8008
|
-
...hasAt ? { at: args.at } : {},
|
|
8009
|
-
note: args.note
|
|
8010
|
-
}
|
|
8011
|
-
});
|
|
8012
|
-
},
|
|
8013
|
-
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
8014
|
-
),
|
|
8015
|
-
// CT1350: the standalone withdrawal. Not annotated destructive — an ask
|
|
8016
|
-
// is a question, not data, and a harness that gated this on a live
|
|
8017
|
-
// operator would hang a turn nobody is watching (the `cancel_wake`
|
|
8018
|
-
// reasoning). Not idempotent either: a second call with the same id is
|
|
8019
|
-
// refused with `ask_not_open`, which is information, not a no-op.
|
|
8020
|
-
tool(
|
|
8021
|
-
WITHDRAW_ASK_TOOL,
|
|
8022
|
-
TURN_CONTROL_TOOL_DESCRIPTIONS.withdraw_ask,
|
|
8023
|
-
{
|
|
8024
|
-
askId: z13.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.askId),
|
|
8025
|
-
reason: z13.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.reason)
|
|
8026
|
-
},
|
|
8027
|
-
async (args) => performAct(acts, {
|
|
8028
|
-
kind: "withdraw_ask",
|
|
8029
|
-
callId: randomUUID2(),
|
|
8030
|
-
withdraw: { askId: args.askId, reason: args.reason }
|
|
8031
|
-
}),
|
|
8032
|
-
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
8033
|
-
),
|
|
8034
|
-
// CT990: the inverse. No arguments — it can only ever stand down the
|
|
8035
|
-
// caller's own wake in the conversation it's holding a turn in.
|
|
8036
|
-
tool(
|
|
8037
|
-
CANCEL_WAKE_TOOL,
|
|
8038
|
-
TURN_CONTROL_TOOL_DESCRIPTIONS.cancel_wake,
|
|
8039
|
-
{},
|
|
8040
|
-
async () => performAct(acts, { kind: "cancel_wake", callId: randomUUID2() }),
|
|
8041
|
-
{ annotations: CANCEL_TOOL, alwaysLoad: true }
|
|
8042
|
-
)
|
|
8043
|
-
]
|
|
8044
|
-
});
|
|
7888
|
+
async function reapTurnScopes(turnId) {
|
|
7889
|
+
if (process.platform !== "linux") return { failures: [] };
|
|
7890
|
+
const pattern = `${turnScopeUnit(turnId, 0).replace(/-0$/, "")}-*.scope`;
|
|
7891
|
+
try {
|
|
7892
|
+
await execFileAsync("systemctl", ["--user", "stop", pattern], {
|
|
7893
|
+
timeout: (CONTAINMENT_STOP_GRACE_SEC + 10) * 1e3
|
|
7894
|
+
});
|
|
7895
|
+
return { failures: [] };
|
|
7896
|
+
} catch (err) {
|
|
7897
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7898
|
+
return classifyStopError(message) === "absent" ? { failures: [] } : { failures: [message.slice(0, 200)] };
|
|
7899
|
+
}
|
|
8045
7900
|
}
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
7901
|
+
function scopeCapabilityUsable(env, timeoutMs = CAPABILITY_PROBE_TIMEOUT_MS) {
|
|
7902
|
+
if (process.platform !== "linux") return false;
|
|
7903
|
+
try {
|
|
7904
|
+
execFileSync("systemd-run", ["--user", "--scope", "--quiet", "--collect", "--", "/bin/true"], {
|
|
7905
|
+
env,
|
|
7906
|
+
timeout: timeoutMs,
|
|
7907
|
+
stdio: "ignore"
|
|
7908
|
+
});
|
|
7909
|
+
return true;
|
|
7910
|
+
} catch {
|
|
7911
|
+
return false;
|
|
7912
|
+
}
|
|
8050
7913
|
}
|
|
8051
|
-
function
|
|
8052
|
-
|
|
7914
|
+
function classifyStopError(message) {
|
|
7915
|
+
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(
|
|
7916
|
+
message
|
|
7917
|
+
)) {
|
|
7918
|
+
return "failure";
|
|
7919
|
+
}
|
|
7920
|
+
if (/not loaded|could not be found|no such unit|unit .* not found/i.test(message))
|
|
7921
|
+
return "absent";
|
|
7922
|
+
return "failure";
|
|
8053
7923
|
}
|
|
8054
|
-
function
|
|
8055
|
-
const
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
7924
|
+
function currentSystemdUnit(cgroupText) {
|
|
7925
|
+
for (const line of cgroupText.split("\n")) {
|
|
7926
|
+
const path = line.trim().split(":").pop();
|
|
7927
|
+
if (!path?.startsWith("/")) continue;
|
|
7928
|
+
const leaf = path.split("/").filter(Boolean).pop();
|
|
7929
|
+
if (leaf && (leaf.endsWith(".service") || leaf.endsWith(".scope"))) return leaf;
|
|
7930
|
+
}
|
|
7931
|
+
return null;
|
|
7932
|
+
}
|
|
7933
|
+
function readCurrentSystemdUnit() {
|
|
7934
|
+
try {
|
|
7935
|
+
return currentSystemdUnit(readFileSync5("/proc/self/cgroup", "utf8"));
|
|
7936
|
+
} catch {
|
|
7937
|
+
return null;
|
|
7938
|
+
}
|
|
7939
|
+
}
|
|
7940
|
+
function processGroupMembers(pgid) {
|
|
7941
|
+
try {
|
|
7942
|
+
const out = execFileSync("ps", ["-eo", "pid=,pgid="], { timeout: 5e3 }).toString();
|
|
7943
|
+
return {
|
|
7944
|
+
ok: true,
|
|
7945
|
+
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]))
|
|
7946
|
+
};
|
|
7947
|
+
} catch (err) {
|
|
7948
|
+
return { ok: false, error: (err instanceof Error ? err.message : String(err)).slice(0, 150) };
|
|
7949
|
+
}
|
|
7950
|
+
}
|
|
7951
|
+
var sleep2 = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
7952
|
+
var MANAGER_ENV_KEYS = ["DBUS_SESSION_BUS_ADDRESS", "XDG_RUNTIME_DIR", "PATH"];
|
|
7953
|
+
var managerEnvKey = (env) => MANAGER_ENV_KEYS.map((k) => `${k}=${env[k] ?? ""}`).join("\0");
|
|
7954
|
+
function createTurnContainment(turnId, deps = {}) {
|
|
7955
|
+
const probe = deps.capabilityProbe ?? scopeCapabilityUsable;
|
|
7956
|
+
const fallbackGraceMs = deps.fallbackGraceMs ?? FALLBACK_GRACE_MS;
|
|
7957
|
+
const ownUnit = (deps.ownSystemdUnit ?? readCurrentSystemdUnit)();
|
|
7958
|
+
const scopeUnits = [];
|
|
7959
|
+
const groups = [];
|
|
7960
|
+
let kind = null;
|
|
7961
|
+
let probedEnvKey = null;
|
|
7962
|
+
let attempt = 0;
|
|
7963
|
+
function scopeArgs(unit) {
|
|
7964
|
+
return [
|
|
7965
|
+
"--user",
|
|
7966
|
+
"--scope",
|
|
7967
|
+
"--quiet",
|
|
7968
|
+
"--collect",
|
|
7969
|
+
`--unit=${unit}`,
|
|
7970
|
+
`--property=TimeoutStopSec=${CONTAINMENT_STOP_GRACE_SEC}s`,
|
|
7971
|
+
// Bind the scope's life to the companion's own unit, so stopping or
|
|
7972
|
+
// restarting the companion still takes its in-flight turns with it —
|
|
7973
|
+
// which is what happens today, and what the scope would otherwise undo.
|
|
7974
|
+
// Omitted outside a unit, where there is nothing to bind to.
|
|
7975
|
+
...ownUnit ? [`--property=PartOf=${ownUnit}`] : [],
|
|
7976
|
+
"--"
|
|
7977
|
+
];
|
|
7978
|
+
}
|
|
7979
|
+
function resolveKind(env) {
|
|
7980
|
+
const envKey = managerEnvKey(env);
|
|
7981
|
+
if (kind === null || envKey !== probedEnvKey) {
|
|
7982
|
+
kind = probe(env) ? "systemd-scope" : "process-group";
|
|
7983
|
+
probedEnvKey = envKey;
|
|
7984
|
+
deps.log?.("containment: resolved strategy for this turn", {
|
|
7985
|
+
kind,
|
|
7986
|
+
platform: process.platform
|
|
7987
|
+
});
|
|
7988
|
+
}
|
|
7989
|
+
return kind;
|
|
7990
|
+
}
|
|
7991
|
+
function spawnInScope(options) {
|
|
7992
|
+
const unit = turnScopeUnit(turnId, attempt++);
|
|
7993
|
+
scopeUnits.push({ unit, env: options.env });
|
|
7994
|
+
deps.log?.("containment: spawning the turn CLI into a systemd scope", {
|
|
7995
|
+
unit,
|
|
7996
|
+
partOf: ownUnit,
|
|
7997
|
+
command: options.command
|
|
7998
|
+
});
|
|
7999
|
+
return asContained(
|
|
8000
|
+
spawn4("systemd-run", [...scopeArgs(unit), options.command, ...options.args], {
|
|
8001
|
+
...options.cwd ? { cwd: options.cwd } : {},
|
|
8002
|
+
env: options.env,
|
|
8003
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
8004
|
+
})
|
|
8005
|
+
);
|
|
8006
|
+
}
|
|
8007
|
+
function spawnInGroup(options) {
|
|
8008
|
+
const child = spawn4(options.command, options.args, {
|
|
8009
|
+
...options.cwd ? { cwd: options.cwd } : {},
|
|
8010
|
+
env: options.env,
|
|
8011
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
8012
|
+
detached: process.platform !== "win32"
|
|
8013
|
+
});
|
|
8014
|
+
if (child.pid !== void 0) {
|
|
8015
|
+
groups.push({ pgid: child.pid, child });
|
|
8016
|
+
deps.onProcessGroup?.(child.pid);
|
|
8017
|
+
}
|
|
8018
|
+
deps.log?.("containment: no host-owned containment here \u2014 best effort only", {
|
|
8019
|
+
platform: process.platform,
|
|
8020
|
+
pid: child.pid
|
|
8021
|
+
});
|
|
8022
|
+
return asContained(child);
|
|
8023
|
+
}
|
|
8024
|
+
async function reapScopes(result) {
|
|
8025
|
+
for (const { unit, env } of scopeUnits) {
|
|
8026
|
+
try {
|
|
8027
|
+
await execFileAsync("systemctl", ["--user", "stop", `${unit}.scope`], {
|
|
8028
|
+
env,
|
|
8029
|
+
timeout: (CONTAINMENT_STOP_GRACE_SEC + 10) * 1e3
|
|
8030
|
+
});
|
|
8031
|
+
} catch (err) {
|
|
8032
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
8033
|
+
if (classifyStopError(message) === "failure") {
|
|
8034
|
+
result.failures.push({ target: unit, error: message.slice(0, 200) });
|
|
8035
|
+
continue;
|
|
8036
|
+
}
|
|
8037
|
+
result.reaped.push(unit);
|
|
8038
|
+
continue;
|
|
8039
|
+
}
|
|
8040
|
+
try {
|
|
8041
|
+
const { stdout } = await execFileAsync(
|
|
8042
|
+
"systemctl",
|
|
8043
|
+
["--user", "show", `${unit}.scope`, "--property=ActiveState", "--value"],
|
|
8044
|
+
{ env, timeout: 1e4 }
|
|
8045
|
+
);
|
|
8046
|
+
const state = stdout.trim();
|
|
8047
|
+
if (state === "" || state === "inactive" || state === "failed") result.reaped.push(unit);
|
|
8048
|
+
else result.failures.push({ target: unit, error: `scope still ${state} after stop` });
|
|
8049
|
+
} catch (err) {
|
|
8050
|
+
result.failures.push({
|
|
8051
|
+
target: unit,
|
|
8052
|
+
error: `could not confirm the scope stopped: ${(err instanceof Error ? err.message : String(err)).slice(0, 150)}`
|
|
8053
|
+
});
|
|
8054
|
+
}
|
|
8055
|
+
}
|
|
8056
|
+
}
|
|
8057
|
+
async function reapGroups(result) {
|
|
8058
|
+
for (const { pgid, child } of groups) {
|
|
8059
|
+
const target = String(pgid);
|
|
8060
|
+
if (process.platform === "win32") {
|
|
8061
|
+
try {
|
|
8062
|
+
if (child.exitCode === null) child.kill();
|
|
8063
|
+
result.unverified.push(target);
|
|
8064
|
+
} catch (err) {
|
|
8065
|
+
result.failures.push({
|
|
8066
|
+
target,
|
|
8067
|
+
error: (err instanceof Error ? err.message : String(err)).slice(0, 200)
|
|
8068
|
+
});
|
|
8069
|
+
}
|
|
8070
|
+
continue;
|
|
8071
|
+
}
|
|
8072
|
+
const first = processGroupMembers(pgid);
|
|
8073
|
+
if (!first.ok) {
|
|
8074
|
+
result.failures.push({
|
|
8075
|
+
target,
|
|
8076
|
+
error: `could not enumerate the process group: ${first.error}`
|
|
8077
|
+
});
|
|
8078
|
+
continue;
|
|
8079
|
+
}
|
|
8080
|
+
if (first.members.length === 0) {
|
|
8081
|
+
result.reaped.push(target);
|
|
8082
|
+
continue;
|
|
8083
|
+
}
|
|
8084
|
+
for (const pid of first.members) {
|
|
8085
|
+
try {
|
|
8086
|
+
process.kill(pid, "SIGTERM");
|
|
8087
|
+
} catch {
|
|
8088
|
+
}
|
|
8089
|
+
}
|
|
8090
|
+
await sleep2(fallbackGraceMs);
|
|
8091
|
+
const afterTerm = processGroupMembers(pgid);
|
|
8092
|
+
if (!afterTerm.ok) {
|
|
8093
|
+
result.failures.push({
|
|
8094
|
+
target,
|
|
8095
|
+
error: `could not re-enumerate the process group after SIGTERM: ${afterTerm.error}`
|
|
8096
|
+
});
|
|
8097
|
+
continue;
|
|
8098
|
+
}
|
|
8099
|
+
for (const pid of afterTerm.members) {
|
|
8100
|
+
try {
|
|
8101
|
+
process.kill(pid, "SIGKILL");
|
|
8102
|
+
} catch {
|
|
8103
|
+
}
|
|
8104
|
+
}
|
|
8105
|
+
const afterKill = processGroupMembers(pgid);
|
|
8106
|
+
if (!afterKill.ok) {
|
|
8107
|
+
result.failures.push({
|
|
8108
|
+
target,
|
|
8109
|
+
error: `could not confirm the process group is empty after SIGKILL: ${afterKill.error}`
|
|
8110
|
+
});
|
|
8111
|
+
continue;
|
|
8112
|
+
}
|
|
8113
|
+
if (afterKill.members.length)
|
|
8114
|
+
result.failures.push({
|
|
8115
|
+
target,
|
|
8116
|
+
error: `${afterKill.members.length} process(es) still in the group after SIGKILL`
|
|
8117
|
+
});
|
|
8118
|
+
else result.reaped.push(target);
|
|
8119
|
+
}
|
|
8120
|
+
}
|
|
8121
|
+
return {
|
|
8122
|
+
get kind() {
|
|
8123
|
+
return kind;
|
|
8124
|
+
},
|
|
8125
|
+
spawn(options) {
|
|
8126
|
+
return resolveKind(options.env) === "systemd-scope" ? spawnInScope(options) : spawnInGroup(options);
|
|
8127
|
+
},
|
|
8128
|
+
scopePrefix(env) {
|
|
8129
|
+
if (resolveKind(env) !== "systemd-scope") return null;
|
|
8130
|
+
const unit = turnScopeUnit(turnId, attempt++);
|
|
8131
|
+
scopeUnits.push({ unit, env });
|
|
8132
|
+
deps.log?.("containment: a harness will start in a systemd scope", { unit });
|
|
8133
|
+
return ["systemd-run", ...scopeArgs(unit)];
|
|
8134
|
+
},
|
|
8135
|
+
async reap() {
|
|
8136
|
+
const result = {
|
|
8137
|
+
kind: kind ?? "unused",
|
|
8138
|
+
reaped: [],
|
|
8139
|
+
failures: [],
|
|
8140
|
+
unverified: []
|
|
8141
|
+
};
|
|
8142
|
+
await reapScopes(result);
|
|
8143
|
+
await reapGroups(result);
|
|
8144
|
+
deps.log?.("containment: reap complete", {
|
|
8145
|
+
kind: result.kind,
|
|
8146
|
+
reaped: result.reaped,
|
|
8147
|
+
failures: result.failures
|
|
8148
|
+
});
|
|
8149
|
+
return result;
|
|
8150
|
+
}
|
|
8151
|
+
};
|
|
8152
|
+
}
|
|
8153
|
+
|
|
8154
|
+
// src/runner/protocol.ts
|
|
8155
|
+
import { z as z14 } from "zod";
|
|
8156
|
+
|
|
8157
|
+
// src/run-config.ts
|
|
8158
|
+
import { z as z13 } from "zod";
|
|
8159
|
+
var mcpStdioServerSchema = z13.object({
|
|
8160
|
+
type: z13.literal("stdio").optional(),
|
|
8161
|
+
command: z13.string().min(1),
|
|
8162
|
+
args: z13.array(z13.string()).optional(),
|
|
8163
|
+
env: z13.record(z13.string(), z13.string()).optional()
|
|
8164
|
+
});
|
|
8165
|
+
var mcpHttpServerSchema = z13.object({
|
|
8166
|
+
type: z13.literal("http"),
|
|
8167
|
+
url: z13.string().url(),
|
|
8168
|
+
headers: z13.record(z13.string(), z13.string()).optional()
|
|
8169
|
+
});
|
|
8170
|
+
var mcpSseServerSchema = z13.object({
|
|
8171
|
+
type: z13.literal("sse"),
|
|
8172
|
+
url: z13.string().url(),
|
|
8173
|
+
headers: z13.record(z13.string(), z13.string()).optional()
|
|
8174
|
+
});
|
|
8175
|
+
var mcpServerDefSchema = z13.union([mcpHttpServerSchema, mcpSseServerSchema, mcpStdioServerSchema]);
|
|
8176
|
+
var thinkingConfigSchema = z13.discriminatedUnion("type", [
|
|
8177
|
+
z13.object({ type: z13.literal("adaptive") }),
|
|
8178
|
+
z13.object({ type: z13.literal("enabled"), budgetTokens: z13.number().int().positive().optional() }),
|
|
8179
|
+
z13.object({ type: z13.literal("disabled") })
|
|
8180
|
+
]);
|
|
8181
|
+
var effortSchema = z13.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
8182
|
+
var runConfigSchema = z13.object({
|
|
8183
|
+
// CT788: the host-access binary replaced the `assistant`/`coding`/`custom` mode
|
|
8184
|
+
// trio + its custom tool lists — `true` grants the host filesystem/shell, absent
|
|
8185
|
+
// is the locked surface. Kept in lockstep with `@cabane/shared`'s
|
|
8186
|
+
// `agentRunConfigSchema` (independent zod, forward-compatible: unknown keys are
|
|
8187
|
+
// stripped, so an older companion riding a newer server never rejects the config).
|
|
8188
|
+
hostAccess: z13.boolean().optional(),
|
|
8189
|
+
mcpServers: z13.record(z13.string(), mcpServerDefSchema).optional(),
|
|
8190
|
+
model: z13.string().min(1).optional(),
|
|
8191
|
+
thinking: thinkingConfigSchema.optional(),
|
|
8192
|
+
effort: effortSchema.optional()
|
|
8193
|
+
});
|
|
8194
|
+
var EMPTY_RUN_CONFIG = {};
|
|
8195
|
+
function parseRunConfig(raw, onWarn) {
|
|
8196
|
+
const result = runConfigSchema.safeParse(raw ?? {});
|
|
8197
|
+
if (result.success) return result.data;
|
|
8198
|
+
onWarn?.(
|
|
8199
|
+
`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`
|
|
8200
|
+
);
|
|
8201
|
+
return EMPTY_RUN_CONFIG;
|
|
8202
|
+
}
|
|
8203
|
+
|
|
8204
|
+
// src/runner/protocol.ts
|
|
8205
|
+
var RUNNER_ENV = {
|
|
8206
|
+
// The wire the runner speaks.
|
|
8207
|
+
url: "CABANE_URL",
|
|
8208
|
+
// The dispatch as the device stream delivered it.
|
|
8209
|
+
dispatch: "CABANE_DISPATCH",
|
|
8210
|
+
// The turn id the launcher resolved, and whether this is a replay of a turn
|
|
8211
|
+
// an earlier process started. The seq floor for a replay is derived inside
|
|
8212
|
+
// the turn from the outbox and the server, as it always was.
|
|
8213
|
+
turnId: "CABANE_TURN_ID",
|
|
8214
|
+
resumed: "CABANE_RESUMED",
|
|
8215
|
+
// Everything else the turn is constructed from, as JSON: see `RunnerSpec`.
|
|
8216
|
+
spec: "CABANE_RUNNER_SPEC"
|
|
8217
|
+
};
|
|
8218
|
+
var runtimeHealthSchema = z14.object({
|
|
8219
|
+
quotaState: z14.enum(["ok", "limited", "auth_expired", "unknown"]),
|
|
8220
|
+
limitedUntil: z14.string().nullable(),
|
|
8221
|
+
lastError: z14.string().nullable()
|
|
8222
|
+
});
|
|
8223
|
+
var runnerSpecSchema = z14.object({
|
|
8224
|
+
deviceId: z14.string().optional(),
|
|
8225
|
+
// The launcher's boot id: the one its heartbeat carries, so a runner's lease
|
|
8226
|
+
// matches the device that holds it.
|
|
8227
|
+
bootId: z14.string(),
|
|
8228
|
+
workspaceId: z14.string(),
|
|
8229
|
+
workspaceSlug: z14.string(),
|
|
8230
|
+
agentId: z14.string(),
|
|
8231
|
+
agentUsername: z14.string(),
|
|
8232
|
+
agentName: z14.string(),
|
|
8233
|
+
local: localAgentConfigSchema,
|
|
8234
|
+
runConfig: runConfigSchema,
|
|
8235
|
+
opencodeServerUrl: z14.string().optional(),
|
|
8236
|
+
claudeCodeAvailable: z14.boolean(),
|
|
8237
|
+
codexAvailable: z14.boolean(),
|
|
8238
|
+
idleTimeoutMs: z14.number().int().positive().optional(),
|
|
8239
|
+
totalTimeoutMs: z14.number().int().positive().optional(),
|
|
8240
|
+
leaseRenewalMs: z14.number().int().positive().optional(),
|
|
8241
|
+
preparingRowDelayMs: z14.number().int().nonnegative().optional(),
|
|
8242
|
+
// Whether the turn writes a transcript. The directory is the runner's own
|
|
8243
|
+
// (`transcriptsDir()`), so `CABANE_RUNNER_STATE_DIR` moves it with the outbox.
|
|
8244
|
+
transcripts: z14.boolean(),
|
|
8245
|
+
// Whether the harness starts under the OS sandbox. The launcher sends 'os'
|
|
8246
|
+
// only when this host can make one.
|
|
8247
|
+
sandbox: z14.enum(["os", "none"]),
|
|
8248
|
+
logLevel: z14.string(),
|
|
8249
|
+
// The launcher's connector-health observations, by runtime, as of the start.
|
|
8250
|
+
connectorHealth: z14.record(z14.string(), runtimeHealthSchema),
|
|
8251
|
+
// True when this (conversation, agent) pair was already told its session
|
|
8252
|
+
// could not be saved: the notice is once per pair, not once per process.
|
|
8253
|
+
sessionWriteNoticeClaimed: z14.boolean(),
|
|
8254
|
+
// CT1680: the runner's identity, minted with its credential. The start
|
|
8255
|
+
// PATCH carries it so the lease knows which runner holds the span.
|
|
8256
|
+
runnerId: z14.string()
|
|
8257
|
+
});
|
|
8258
|
+
var dispatchSchema = z14.object({
|
|
8259
|
+
type: z14.literal("device:dispatch_requested"),
|
|
8260
|
+
agentId: z14.string(),
|
|
8261
|
+
deviceId: z14.string(),
|
|
8262
|
+
conversationId: z14.string(),
|
|
8263
|
+
messageId: z14.string(),
|
|
8264
|
+
episode: z14.number().int().optional()
|
|
8265
|
+
});
|
|
8266
|
+
var credentialsSchema = z14.object({
|
|
8267
|
+
credential: z14.string().startsWith("cabrn_")
|
|
8268
|
+
});
|
|
8269
|
+
function runnerEnv(input) {
|
|
8270
|
+
return {
|
|
8271
|
+
[RUNNER_ENV.url]: input.baseUrl,
|
|
8272
|
+
[RUNNER_ENV.dispatch]: JSON.stringify(input.dispatch),
|
|
8273
|
+
[RUNNER_ENV.turnId]: input.turnId,
|
|
8274
|
+
...input.resumed ? { [RUNNER_ENV.resumed]: "1" } : {},
|
|
8275
|
+
[RUNNER_ENV.spec]: JSON.stringify(input.spec)
|
|
8276
|
+
};
|
|
8277
|
+
}
|
|
8278
|
+
function runnerStdin(input) {
|
|
8279
|
+
return `${JSON.stringify({ credential: input.credential })}
|
|
8280
|
+
`;
|
|
8281
|
+
}
|
|
8282
|
+
function encodeLauncherMessage(message) {
|
|
8283
|
+
return `${JSON.stringify(message)}
|
|
8284
|
+
`;
|
|
8285
|
+
}
|
|
8286
|
+
function decodeRunnerEvent(line) {
|
|
8287
|
+
if (!line.startsWith("{")) return null;
|
|
8288
|
+
let value;
|
|
8289
|
+
try {
|
|
8290
|
+
value = JSON.parse(line);
|
|
8291
|
+
} catch {
|
|
8292
|
+
return null;
|
|
8293
|
+
}
|
|
8294
|
+
if (!value || typeof value !== "object" || !("cabaneRunner" in value)) return null;
|
|
8295
|
+
return value;
|
|
8296
|
+
}
|
|
8297
|
+
|
|
8298
|
+
// src/runner/launch.ts
|
|
8299
|
+
function selfRunnerCommand() {
|
|
8300
|
+
const script = process.argv[1];
|
|
8301
|
+
if (!script) throw new Error("runners: cannot tell which script started this companion");
|
|
8302
|
+
return [process.execPath, ...process.execArgv, script, "run-turn"];
|
|
8303
|
+
}
|
|
8304
|
+
function healthSnapshot(store) {
|
|
8305
|
+
const out = {};
|
|
8306
|
+
for (const r of store?.reports() ?? []) {
|
|
8307
|
+
out[r.runtime] = {
|
|
8308
|
+
quotaState: r.quotaState ?? "unknown",
|
|
8309
|
+
limitedUntil: r.limitedUntil ?? null,
|
|
8310
|
+
lastError: r.lastError ?? null
|
|
8311
|
+
};
|
|
8312
|
+
}
|
|
8313
|
+
return out;
|
|
8314
|
+
}
|
|
8315
|
+
function runnerSpec(opts, launch, duties, runnerId) {
|
|
8316
|
+
return {
|
|
8317
|
+
runnerId,
|
|
8318
|
+
...opts.deviceId ? { deviceId: opts.deviceId } : {},
|
|
8319
|
+
bootId,
|
|
8320
|
+
workspaceId: opts.workspaceId,
|
|
8321
|
+
workspaceSlug: opts.workspaceSlug,
|
|
8322
|
+
agentId: opts.agentId,
|
|
8323
|
+
agentUsername: opts.agentUsername,
|
|
8324
|
+
agentName: String(opts.log.bindings().agentName ?? opts.agentUsername),
|
|
8325
|
+
local: opts.local,
|
|
8326
|
+
runConfig: opts.runConfig,
|
|
8327
|
+
...opts.opencodeServerUrl ? { opencodeServerUrl: opts.opencodeServerUrl } : {},
|
|
8328
|
+
// Read now, per turn, as the in-process path reads them at adapter selection.
|
|
8329
|
+
claudeCodeAvailable: opts.claudeCodeAvailable?.() ?? true,
|
|
8330
|
+
codexAvailable: opts.codexAvailable?.() ?? false,
|
|
8331
|
+
...opts.idleTimeoutMs !== void 0 ? { idleTimeoutMs: opts.idleTimeoutMs } : {},
|
|
8332
|
+
...opts.totalTimeoutMs !== void 0 ? { totalTimeoutMs: opts.totalTimeoutMs } : {},
|
|
8333
|
+
...opts.leaseRenewalMs !== void 0 ? { leaseRenewalMs: opts.leaseRenewalMs } : {},
|
|
8334
|
+
...opts.preparingRowDelayMs !== void 0 ? { preparingRowDelayMs: opts.preparingRowDelayMs } : {},
|
|
8335
|
+
transcripts: opts.transcriptDir !== void 0,
|
|
8336
|
+
sandbox: launch.sandbox ?? "none",
|
|
8337
|
+
logLevel: opts.log.level,
|
|
8338
|
+
connectorHealth: healthSnapshot(launch.connectorHealth),
|
|
8339
|
+
sessionWriteNoticeClaimed: duties.sessionWriteNoticeClaimed()
|
|
8340
|
+
};
|
|
8341
|
+
}
|
|
8342
|
+
var PINO_LEVELS = {
|
|
8343
|
+
10: "trace",
|
|
8344
|
+
20: "debug",
|
|
8345
|
+
30: "info",
|
|
8346
|
+
40: "warn",
|
|
8347
|
+
50: "error",
|
|
8348
|
+
60: "fatal"
|
|
8349
|
+
};
|
|
8350
|
+
function relayRunnerLog(line, log) {
|
|
8351
|
+
if (!line.trim()) return;
|
|
8352
|
+
let record = null;
|
|
8353
|
+
try {
|
|
8354
|
+
const parsed = JSON.parse(line);
|
|
8355
|
+
if (parsed && typeof parsed === "object") record = parsed;
|
|
8356
|
+
} catch {
|
|
8357
|
+
record = null;
|
|
8358
|
+
}
|
|
8359
|
+
if (!record || typeof record.level !== "number") {
|
|
8360
|
+
log.debug({ runnerStderr: line }, "runner: output");
|
|
8361
|
+
return;
|
|
8362
|
+
}
|
|
8363
|
+
const {
|
|
8364
|
+
level,
|
|
8365
|
+
msg,
|
|
8366
|
+
time: _time,
|
|
8367
|
+
pid: _pid,
|
|
8368
|
+
hostname: _hostname,
|
|
8369
|
+
agentName: _agentName,
|
|
8370
|
+
...fields
|
|
8371
|
+
} = record;
|
|
8372
|
+
const method = PINO_LEVELS[level] ?? "info";
|
|
8373
|
+
log[method](fields, typeof msg === "string" ? msg : "");
|
|
8374
|
+
}
|
|
8375
|
+
function lines(stream, onLine) {
|
|
8376
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
8377
|
+
rl.on("line", onLine);
|
|
8378
|
+
return new Promise((resolve2) => rl.once("close", resolve2));
|
|
8379
|
+
}
|
|
8380
|
+
function giveBackLocks(locks, prefix, remote, launch, log) {
|
|
8381
|
+
locks.withdraw(prefix);
|
|
8382
|
+
const stop = launch.stopRemoteSession ?? ((url, id) => stopRemoteOpencodeSession(url, id));
|
|
8383
|
+
for (const { key, owner } of locks.heldBy(prefix)) {
|
|
8384
|
+
const left = remote.filter((r) => opencodeLockKey(r.serverUrl) === key);
|
|
8385
|
+
if (!left.length) {
|
|
8386
|
+
locks.release(owner);
|
|
8387
|
+
continue;
|
|
8388
|
+
}
|
|
8389
|
+
void (async () => {
|
|
8390
|
+
for (let attempt = 1; ; attempt++) {
|
|
8391
|
+
const stopped = await Promise.all(left.map((r) => stop(r.serverUrl, r.sessionId))).catch(
|
|
8392
|
+
() => [false]
|
|
8393
|
+
);
|
|
8394
|
+
if (stopped.every(Boolean)) break;
|
|
8395
|
+
if (attempt === 1)
|
|
8396
|
+
log.warn(
|
|
8397
|
+
{ key, sessions: left.map((r) => r.sessionId) },
|
|
8398
|
+
"runner: a dead runner left work on a shared server that could not be confirmed stopped; holding the server until it is"
|
|
8399
|
+
);
|
|
8400
|
+
await new Promise((resolve2) => setTimeout(resolve2, launch.reconcileRetryMs ?? 5e3));
|
|
8401
|
+
}
|
|
8402
|
+
locks.release(owner);
|
|
8403
|
+
})();
|
|
8404
|
+
}
|
|
8405
|
+
}
|
|
8406
|
+
async function mintForRunner(opts, payload, handleOpts, signal, log) {
|
|
8407
|
+
try {
|
|
8408
|
+
const minted = await opts.api.mintRunnerCredential({
|
|
8409
|
+
conversationId: payload.conversationId,
|
|
8410
|
+
messageId: payload.messageId,
|
|
8411
|
+
...payload.episode !== void 0 ? { episode: payload.episode } : {},
|
|
8412
|
+
// The one turn the credential will operate: this run's.
|
|
8413
|
+
turnId: handleOpts.turnId
|
|
8414
|
+
});
|
|
8415
|
+
return { ok: true, credential: minted.credential, runnerId: minted.runnerId };
|
|
8416
|
+
} catch (err) {
|
|
8417
|
+
if (signal.aborted) return { ok: false, reason: "cancelled" };
|
|
8418
|
+
const status = err instanceof ApiError ? err.status : 0;
|
|
8419
|
+
if (status === 404) return { ok: false, reason: "runner_credential_not_found" };
|
|
8420
|
+
if (status === 409 && apiErrorCode(err) === "dispatch_not_admitted")
|
|
8421
|
+
return { ok: false, reason: "runner_credential_not_admitted" };
|
|
8422
|
+
log.warn(apiErrorLogFields(err), "runner: couldn't get a credential for the turn's runner");
|
|
8423
|
+
const reason = `fetch_failed: ${describeError(err)}`;
|
|
8424
|
+
return { ok: false, reason, errorReason: reason };
|
|
8425
|
+
}
|
|
8426
|
+
}
|
|
8427
|
+
async function settleUnstarted(opts, payload, turnId, errorReason, log) {
|
|
8428
|
+
try {
|
|
8429
|
+
await opts.api.setActiveRun(opts.workspaceId, payload.conversationId, payload.agentId, {
|
|
8430
|
+
activeRunStartedAt: null,
|
|
8431
|
+
turnId,
|
|
8432
|
+
outcome: "failed",
|
|
8433
|
+
...errorReason ? { errorReason: errorReason.slice(0, 200) } : {},
|
|
8434
|
+
readThrough: payload.messageId
|
|
8435
|
+
});
|
|
8436
|
+
} catch (err) {
|
|
8437
|
+
log.warn(
|
|
8438
|
+
{ ...apiErrorLogFields(err), turnId },
|
|
8439
|
+
"Couldn't update the turn's status in Cabane. Check the conversation before trying again."
|
|
8440
|
+
);
|
|
8441
|
+
}
|
|
8442
|
+
}
|
|
8443
|
+
async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
8444
|
+
const startedAt = Date.now();
|
|
8445
|
+
const { turnId } = handleOpts;
|
|
8446
|
+
const log = opts.log.child({
|
|
8447
|
+
workspaceId: opts.workspaceId,
|
|
8448
|
+
conversationId: payload.conversationId,
|
|
8449
|
+
agentId: payload.agentId,
|
|
8450
|
+
messageId: payload.messageId
|
|
8451
|
+
});
|
|
8452
|
+
const controller = new AbortController();
|
|
8453
|
+
duties.registerAbort(turnId, controller);
|
|
8454
|
+
const stopRunner = new AbortController();
|
|
8455
|
+
let ready = false;
|
|
8456
|
+
const forwardStop = () => {
|
|
8457
|
+
if (ready) stopRunner.abort();
|
|
8458
|
+
};
|
|
8459
|
+
controller.signal.addEventListener("abort", forwardStop, { once: true });
|
|
8460
|
+
const attempt = {
|
|
8461
|
+
workspaceId: opts.workspaceId,
|
|
8462
|
+
conversationId: payload.conversationId,
|
|
8463
|
+
agentId: payload.agentId,
|
|
8464
|
+
turnId
|
|
8465
|
+
};
|
|
8466
|
+
const minted = await mintForRunner(opts, payload, handleOpts, controller.signal, log);
|
|
8467
|
+
if (!minted.ok) {
|
|
8468
|
+
controller.signal.removeEventListener("abort", forwardStop);
|
|
8469
|
+
duties.releaseAbort(turnId, controller);
|
|
8470
|
+
const durationMs2 = Date.now() - startedAt;
|
|
8471
|
+
await settleUnstarted(opts, payload, turnId, minted.errorReason, log);
|
|
8472
|
+
duties.notifyEnd({ id: payload.messageId, ok: false, durationMs: durationMs2, reason: minted.reason });
|
|
8473
|
+
return { ok: false, durationMs: durationMs2, reason: minted.reason };
|
|
8474
|
+
}
|
|
8475
|
+
let result;
|
|
8476
|
+
let thrown;
|
|
8477
|
+
let code;
|
|
8478
|
+
let handle;
|
|
8479
|
+
let child;
|
|
8480
|
+
const groups = [];
|
|
8481
|
+
const remote = [];
|
|
8482
|
+
const lockOwner = (id) => `${turnId}#${id}`;
|
|
8483
|
+
const locks = launch.locks;
|
|
8484
|
+
try {
|
|
8485
|
+
handle = await launch.provider.acquire(attempt);
|
|
8486
|
+
const env = {};
|
|
8487
|
+
for (const [k, v] of Object.entries(launch.env ?? process.env)) {
|
|
8488
|
+
if (v !== void 0) env[k] = v;
|
|
8489
|
+
}
|
|
8490
|
+
const input = {
|
|
8491
|
+
baseUrl: opts.baseUrl,
|
|
8492
|
+
credential: minted.credential,
|
|
8493
|
+
dispatch: payload,
|
|
8494
|
+
turnId,
|
|
8495
|
+
resumed: handleOpts.resumed,
|
|
8496
|
+
spec: runnerSpec(opts, launch, duties, minted.runnerId)
|
|
8497
|
+
};
|
|
8498
|
+
Object.assign(env, runnerEnv(input));
|
|
8499
|
+
const running = launch.provider.exec(handle, launch.command, env, {
|
|
8500
|
+
signal: stopRunner.signal,
|
|
8501
|
+
stdin: runnerStdin(input)
|
|
8502
|
+
});
|
|
8503
|
+
child = running;
|
|
8504
|
+
log.debug(
|
|
8505
|
+
{ turnId, runnerPid: running.pid, provider: launch.provider.name },
|
|
8506
|
+
"runner: started"
|
|
8507
|
+
);
|
|
8508
|
+
const stdoutDone = lines(running.stdout, (line) => {
|
|
8509
|
+
const event = decodeRunnerEvent(line);
|
|
8510
|
+
if (!event) {
|
|
8511
|
+
log.debug({ runnerStdout: line }, "runner: output");
|
|
8512
|
+
return;
|
|
8513
|
+
}
|
|
8514
|
+
switch (event.cabaneRunner) {
|
|
8515
|
+
case "ready":
|
|
8516
|
+
ready = true;
|
|
8517
|
+
if (controller.signal.aborted) stopRunner.abort();
|
|
8518
|
+
return;
|
|
8519
|
+
case "start":
|
|
8520
|
+
duties.notifyStart({
|
|
8521
|
+
id: event.id,
|
|
8522
|
+
conversationId: event.conversationId,
|
|
8523
|
+
message: event.message
|
|
8524
|
+
});
|
|
8525
|
+
return;
|
|
8526
|
+
case "end": {
|
|
8527
|
+
const { cabaneRunner: _kind, ...info } = event;
|
|
8528
|
+
duties.notifyEnd(info);
|
|
8529
|
+
return;
|
|
8530
|
+
}
|
|
8531
|
+
case "session_write_notice":
|
|
8532
|
+
duties.markSessionWriteNotice();
|
|
8533
|
+
return;
|
|
8534
|
+
case "connector_health":
|
|
8535
|
+
launch.connectorHealth?.recordSettle(event.runtime, {
|
|
8536
|
+
ok: event.ok,
|
|
8537
|
+
errorReason: event.errorReason
|
|
8538
|
+
});
|
|
8539
|
+
return;
|
|
8540
|
+
case "result":
|
|
8541
|
+
result = event.result;
|
|
8542
|
+
return;
|
|
8543
|
+
case "error":
|
|
8544
|
+
thrown = event.message;
|
|
8545
|
+
return;
|
|
8546
|
+
case "lock":
|
|
8547
|
+
if (!locks) {
|
|
8548
|
+
log.warn(
|
|
8549
|
+
{ key: event.key },
|
|
8550
|
+
"runner: asked for a device lock this launcher has none of"
|
|
8551
|
+
);
|
|
8552
|
+
return;
|
|
8553
|
+
}
|
|
8554
|
+
locks.request(
|
|
8555
|
+
event.key,
|
|
8556
|
+
lockOwner(event.id),
|
|
8557
|
+
() => running.stdin.write(encodeLauncherMessage({ cabaneLauncher: "granted", id: event.id }))
|
|
8558
|
+
);
|
|
8559
|
+
return;
|
|
8560
|
+
case "unlock":
|
|
8561
|
+
locks?.release(lockOwner(event.id));
|
|
8562
|
+
return;
|
|
8563
|
+
case "process_group":
|
|
8564
|
+
groups.push(event.pgid);
|
|
8565
|
+
return;
|
|
8566
|
+
case "remote_session":
|
|
8567
|
+
remote.push({ serverUrl: event.serverUrl, sessionId: event.sessionId });
|
|
8568
|
+
return;
|
|
8569
|
+
}
|
|
8570
|
+
});
|
|
8571
|
+
const stderrDone = lines(running.stderr, (line) => relayRunnerLog(line, opts.log));
|
|
8572
|
+
code = await running.exit;
|
|
8573
|
+
await Promise.all([stdoutDone, stderrDone]);
|
|
8574
|
+
} finally {
|
|
8575
|
+
if (locks) giveBackLocks(locks, `${turnId}#`, remote, launch, log);
|
|
8576
|
+
child?.stdin.end();
|
|
8577
|
+
controller.signal.removeEventListener("abort", forwardStop);
|
|
8578
|
+
duties.releaseAbort(turnId, controller);
|
|
8579
|
+
if (handle) await launch.provider.release(handle, { destroy: false });
|
|
8580
|
+
}
|
|
8581
|
+
if (result) return result;
|
|
8582
|
+
const durationMs = Date.now() - startedAt;
|
|
8583
|
+
const reason = thrown !== void 0 ? `runner_failed: ${thrown}` : `runner_exited: ${code}`;
|
|
8584
|
+
await child?.reap(groups);
|
|
8585
|
+
const reaped = await (launch.reapTurn ?? reapTurnScopes)(turnId).catch((err) => ({
|
|
8586
|
+
failures: [err instanceof Error ? err.message : String(err)]
|
|
8587
|
+
}));
|
|
8588
|
+
if (reaped.failures.length) {
|
|
8589
|
+
log.warn(
|
|
8590
|
+
{ turnId, failures: reaped.failures },
|
|
8591
|
+
"runner: couldn't stop what the dead runner's turn left running"
|
|
8592
|
+
);
|
|
8593
|
+
}
|
|
8594
|
+
try {
|
|
8595
|
+
await opts.api.setActiveRun(opts.workspaceId, payload.conversationId, payload.agentId, {
|
|
8596
|
+
activeRunStartedAt: null,
|
|
8597
|
+
turnId,
|
|
8598
|
+
outcome: "failed",
|
|
8599
|
+
errorReason: reason.slice(0, 200),
|
|
8600
|
+
readThrough: payload.messageId
|
|
8601
|
+
});
|
|
8602
|
+
} catch (err) {
|
|
8603
|
+
log.warn(
|
|
8604
|
+
{ ...apiErrorLogFields(err), turnId },
|
|
8605
|
+
"Couldn't update the turn's status in Cabane. Check the conversation before trying again."
|
|
8606
|
+
);
|
|
8607
|
+
}
|
|
8608
|
+
await launch.reportRunnerGone?.({
|
|
8609
|
+
workspaceId: opts.workspaceId,
|
|
8610
|
+
conversationId: payload.conversationId,
|
|
8611
|
+
agentId: payload.agentId,
|
|
8612
|
+
messageId: payload.messageId,
|
|
8613
|
+
runnerId: minted.runnerId,
|
|
8614
|
+
reason: "exited"
|
|
8615
|
+
});
|
|
8616
|
+
if (thrown !== void 0) throw new Error(thrown);
|
|
8617
|
+
log.warn({ turnId, code }, "runner: exited without reporting a result");
|
|
8618
|
+
duties.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
8619
|
+
return { ok: false, durationMs, reason };
|
|
8620
|
+
}
|
|
8621
|
+
|
|
8622
|
+
// src/term.ts
|
|
8623
|
+
import { createInterface as createInterface2 } from "readline";
|
|
8624
|
+
import { homedir as homedir2 } from "os";
|
|
8625
|
+
import pc from "picocolors";
|
|
8626
|
+
function tildePath(path) {
|
|
8627
|
+
const home = homedir2();
|
|
8628
|
+
if (home && path.startsWith(home + "/")) return `~${path.slice(home.length)}`;
|
|
8629
|
+
return path;
|
|
8630
|
+
}
|
|
8631
|
+
|
|
8632
|
+
// src/turn-execution.ts
|
|
8633
|
+
import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
|
|
8634
|
+
import { existsSync as existsSync9 } from "fs";
|
|
8635
|
+
|
|
8636
|
+
// src/turn-control-tools.ts
|
|
8637
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
8638
|
+
import { z as z15 } from "zod";
|
|
8639
|
+
var COMPANION_LOCAL_MCP_SERVER = "cabane_companion";
|
|
8640
|
+
var COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
|
|
8641
|
+
var SKIP_TURN_TOOL = "skip_turn";
|
|
8642
|
+
var ASK_TOOL = "ask";
|
|
8643
|
+
var ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${ASK_TOOL}`;
|
|
8644
|
+
var WITHDRAW_ASK_TOOL = "withdraw_ask";
|
|
8645
|
+
var WITHDRAW_ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WITHDRAW_ASK_TOOL}`;
|
|
8646
|
+
var WAKE_ME_TOOL = "wake_me";
|
|
8647
|
+
var WAKE_ME_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WAKE_ME_TOOL}`;
|
|
8648
|
+
var CANCEL_WAKE_TOOL = "cancel_wake";
|
|
8649
|
+
var CANCEL_WAKE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${CANCEL_WAKE_TOOL}`;
|
|
8650
|
+
var TURN_ACT_TIMEOUT_MS = 45e3;
|
|
8651
|
+
function createSkipState() {
|
|
8652
|
+
return { skipped: false, reason: null };
|
|
8653
|
+
}
|
|
8654
|
+
function toolError(text) {
|
|
8655
|
+
return { isError: true, content: [{ type: "text", text }] };
|
|
8656
|
+
}
|
|
8657
|
+
async function performAct(acts, act) {
|
|
8658
|
+
let result;
|
|
8659
|
+
try {
|
|
8660
|
+
result = await acts.api.turnAct(
|
|
8661
|
+
acts.workspaceId,
|
|
8662
|
+
acts.turnId,
|
|
8663
|
+
act,
|
|
8664
|
+
AbortSignal.timeout(TURN_ACT_TIMEOUT_MS)
|
|
8665
|
+
);
|
|
8666
|
+
} catch (err) {
|
|
8667
|
+
if (err instanceof ApiError && err.status >= 500) {
|
|
8668
|
+
return uncertainOutcome(`${err.status}: ${apiErrorMessage(err)}`);
|
|
8669
|
+
}
|
|
8670
|
+
if (err instanceof ApiError) {
|
|
8671
|
+
if (err.status === 409 && apiErrorCode(err) === "no_active_turn") {
|
|
8672
|
+
return toolError(
|
|
8673
|
+
"No active turn: this turn has already settled, so the act was not performed."
|
|
8674
|
+
);
|
|
8675
|
+
}
|
|
8676
|
+
return toolError(`The act was refused (${err.status}): ${apiErrorMessage(err)}`);
|
|
8677
|
+
}
|
|
8678
|
+
return uncertainOutcome(err instanceof Error ? err.message : String(err));
|
|
8679
|
+
}
|
|
8680
|
+
if (!result.ok) return toolError(JSON.stringify(result));
|
|
8681
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
8682
|
+
}
|
|
8683
|
+
function uncertainOutcome(cause) {
|
|
8684
|
+
return toolError(
|
|
8685
|
+
`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.`
|
|
8686
|
+
);
|
|
8687
|
+
}
|
|
8688
|
+
function createTurnControlMcpServer(acts, skipState) {
|
|
8689
|
+
const ACT_TOOL = { readOnlyHint: false, destructiveHint: false, openWorldHint: false };
|
|
8690
|
+
const CANCEL_TOOL = { ...ACT_TOOL, idempotentHint: true };
|
|
8691
|
+
const RECORDER_TOOL = { readOnlyHint: true, openWorldHint: false };
|
|
8692
|
+
return createSdkMcpServer({
|
|
8693
|
+
name: COMPANION_LOCAL_MCP_SERVER,
|
|
8694
|
+
version: "0.0.0",
|
|
8695
|
+
tools: [
|
|
8696
|
+
...skipState ? [
|
|
8697
|
+
tool(
|
|
8698
|
+
SKIP_TURN_TOOL,
|
|
8699
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.skip_turn,
|
|
8700
|
+
{
|
|
8701
|
+
reason: z15.string().min(1).max(MAX_SKIP_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.skip_turn.reason)
|
|
8702
|
+
},
|
|
8703
|
+
async (args) => {
|
|
8704
|
+
skipState.skipped = true;
|
|
8705
|
+
skipState.reason = args.reason;
|
|
8706
|
+
return {
|
|
8707
|
+
content: [{ type: "text", text: JSON.stringify({ skipped: true }) }]
|
|
8708
|
+
};
|
|
8709
|
+
},
|
|
8710
|
+
{ annotations: RECORDER_TOOL, alwaysLoad: true }
|
|
8711
|
+
)
|
|
8712
|
+
] : [],
|
|
8713
|
+
tool(
|
|
8714
|
+
ASK_TOOL,
|
|
8715
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.ask,
|
|
8716
|
+
{
|
|
8717
|
+
targetUserId: z15.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
|
|
8718
|
+
question: z15.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.question),
|
|
8719
|
+
headline: z15.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.headline),
|
|
8720
|
+
options: z15.array(
|
|
8721
|
+
z15.union([
|
|
8722
|
+
z15.string().min(1).max(MAX_ASK_OPTION_BYTES),
|
|
8723
|
+
z15.object({
|
|
8724
|
+
label: z15.string().min(1).max(MAX_ASK_OPTION_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.label"]),
|
|
8725
|
+
move: z15.object({
|
|
8726
|
+
task: z15.string().min(1).max(200).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move.task"]),
|
|
8727
|
+
status: z15.enum(TASK_STAGES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move.status"])
|
|
8728
|
+
}).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move"])
|
|
8729
|
+
})
|
|
8730
|
+
])
|
|
8731
|
+
).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.options),
|
|
8732
|
+
questions: z15.array(
|
|
8733
|
+
z15.object({
|
|
8734
|
+
headline: z15.string().min(1).max(MAX_ASK_HEADLINE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.headline"]),
|
|
8735
|
+
body: z15.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.body"]),
|
|
8736
|
+
options: z15.array(
|
|
8737
|
+
z15.union([
|
|
8738
|
+
z15.string().min(1).max(MAX_ASK_OPTION_BYTES),
|
|
8739
|
+
z15.object({
|
|
8740
|
+
label: z15.string().min(1).max(MAX_ASK_OPTION_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.label"]),
|
|
8741
|
+
move: z15.object({
|
|
8742
|
+
task: z15.string().min(1).max(200).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move.task"]),
|
|
8743
|
+
status: z15.enum(TASK_STAGES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move.status"])
|
|
8744
|
+
}).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move"])
|
|
8745
|
+
})
|
|
8746
|
+
])
|
|
8747
|
+
).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options"])
|
|
8748
|
+
})
|
|
8749
|
+
).min(1).max(MAX_ASK_ITEMS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.questions),
|
|
8750
|
+
replaces: z15.object({
|
|
8751
|
+
askId: z15.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.askId"]),
|
|
8752
|
+
reason: z15.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.reason"])
|
|
8753
|
+
}).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.replaces)
|
|
8754
|
+
},
|
|
8755
|
+
async (args) => {
|
|
8756
|
+
const hasSingle = args.question !== void 0;
|
|
8757
|
+
const hasArray = args.questions !== void 0 && args.questions.length > 0;
|
|
8758
|
+
if (hasSingle && hasArray) {
|
|
8759
|
+
return toolError(
|
|
8760
|
+
"Provide either `question` (single) or `questions` (array), not both."
|
|
8761
|
+
);
|
|
8762
|
+
}
|
|
8763
|
+
if (!hasSingle && !hasArray) {
|
|
8764
|
+
return toolError("Provide `question` (single) or `questions` (array).");
|
|
8765
|
+
}
|
|
8766
|
+
return performAct(acts, {
|
|
8767
|
+
kind: "ask",
|
|
8768
|
+
callId: randomUUID2(),
|
|
8769
|
+
// CT1350: the swap rides the ask, so the server commits both halves
|
|
8770
|
+
// in one transaction — the person never sees neither or both.
|
|
8771
|
+
...args.replaces ? { replaces: args.replaces } : {},
|
|
8772
|
+
ask: hasArray ? {
|
|
8773
|
+
targetUserId: args.targetUserId,
|
|
8774
|
+
questions: args.questions.map((q) => ({
|
|
8775
|
+
headline: q.headline,
|
|
8776
|
+
...q.body ? { body: q.body } : {},
|
|
8777
|
+
...q.options && q.options.length > 0 ? { options: q.options } : {}
|
|
8778
|
+
}))
|
|
8779
|
+
} : {
|
|
8780
|
+
targetUserId: args.targetUserId,
|
|
8781
|
+
question: args.question,
|
|
8782
|
+
...args.headline ? { headline: args.headline } : {},
|
|
8783
|
+
...args.options && args.options.length > 0 ? { options: args.options } : {}
|
|
8784
|
+
}
|
|
8785
|
+
});
|
|
8786
|
+
},
|
|
8787
|
+
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
8788
|
+
),
|
|
8789
|
+
tool(
|
|
8790
|
+
WAKE_ME_TOOL,
|
|
8791
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.wake_me,
|
|
8792
|
+
{
|
|
8793
|
+
afterSeconds: z15.number().int().positive().optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.afterSeconds),
|
|
8794
|
+
at: z15.string().datetime({ offset: true }).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.at),
|
|
8795
|
+
note: z15.string().min(1).max(MAX_WAKE_NOTE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.note)
|
|
8796
|
+
},
|
|
8797
|
+
async (args) => {
|
|
8798
|
+
const hasAfter = args.afterSeconds !== void 0;
|
|
8799
|
+
const hasAt = args.at !== void 0;
|
|
8800
|
+
if (hasAfter && hasAt) {
|
|
8801
|
+
return toolError(
|
|
8802
|
+
"Provide either `afterSeconds` (relative) or `at` (absolute), not both."
|
|
8803
|
+
);
|
|
8804
|
+
}
|
|
8805
|
+
if (!hasAfter && !hasAt) {
|
|
8806
|
+
return toolError("Provide `afterSeconds` or `at`.");
|
|
8807
|
+
}
|
|
8808
|
+
return performAct(acts, {
|
|
8809
|
+
kind: "wake",
|
|
8810
|
+
callId: randomUUID2(),
|
|
8811
|
+
wake: {
|
|
8812
|
+
...hasAfter ? { afterSeconds: args.afterSeconds } : {},
|
|
8813
|
+
...hasAt ? { at: args.at } : {},
|
|
8814
|
+
note: args.note
|
|
8815
|
+
}
|
|
8816
|
+
});
|
|
8817
|
+
},
|
|
8818
|
+
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
8819
|
+
),
|
|
8820
|
+
// CT1350: the standalone withdrawal. Not annotated destructive — an ask
|
|
8821
|
+
// is a question, not data, and a harness that gated this on a live
|
|
8822
|
+
// operator would hang a turn nobody is watching (the `cancel_wake`
|
|
8823
|
+
// reasoning). Not idempotent either: a second call with the same id is
|
|
8824
|
+
// refused with `ask_not_open`, which is information, not a no-op.
|
|
8825
|
+
tool(
|
|
8826
|
+
WITHDRAW_ASK_TOOL,
|
|
8827
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.withdraw_ask,
|
|
8828
|
+
{
|
|
8829
|
+
askId: z15.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.askId),
|
|
8830
|
+
reason: z15.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.reason)
|
|
8831
|
+
},
|
|
8832
|
+
async (args) => performAct(acts, {
|
|
8833
|
+
kind: "withdraw_ask",
|
|
8834
|
+
callId: randomUUID2(),
|
|
8835
|
+
withdraw: { askId: args.askId, reason: args.reason }
|
|
8836
|
+
}),
|
|
8837
|
+
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
8838
|
+
),
|
|
8839
|
+
// CT990: the inverse. No arguments — it can only ever stand down the
|
|
8840
|
+
// caller's own wake in the conversation it's holding a turn in.
|
|
8841
|
+
tool(
|
|
8842
|
+
CANCEL_WAKE_TOOL,
|
|
8843
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.cancel_wake,
|
|
8844
|
+
{},
|
|
8845
|
+
async () => performAct(acts, { kind: "cancel_wake", callId: randomUUID2() }),
|
|
8846
|
+
{ annotations: CANCEL_TOOL, alwaysLoad: true }
|
|
8847
|
+
)
|
|
8848
|
+
]
|
|
8849
|
+
});
|
|
8850
|
+
}
|
|
8851
|
+
|
|
8852
|
+
// src/build-options.ts
|
|
8853
|
+
function cabaneMcpUrl(baseUrl) {
|
|
8854
|
+
return `${trimSlash2(baseUrl)}/api/mcp`;
|
|
8855
|
+
}
|
|
8856
|
+
function turnControlMcpUrl(baseUrl) {
|
|
8857
|
+
return `${trimSlash2(baseUrl)}/api/turn-control`;
|
|
8858
|
+
}
|
|
8859
|
+
function buildCompanionTurnRequest(params) {
|
|
8860
|
+
const { turnContext: t } = params;
|
|
8861
|
+
return {
|
|
8862
|
+
systemPrompt: t.systemPrompt,
|
|
8863
|
+
prompt: t.prompt,
|
|
8864
|
+
content: t.content,
|
|
8865
|
+
config: t.config,
|
|
8866
|
+
policy: t.policy,
|
|
8867
|
+
session: t.session,
|
|
8868
|
+
cabane: {
|
|
8869
|
+
mcpUrl: cabaneMcpUrl(params.baseUrl),
|
|
8065
8870
|
bearer: params.turnToken,
|
|
8066
8871
|
activeConversationId: params.activeConversationId,
|
|
8067
8872
|
workspaceId: params.workspaceId,
|
|
@@ -8137,7 +8942,7 @@ function readOutboxFloor(read, turnId, log) {
|
|
|
8137
8942
|
}
|
|
8138
8943
|
|
|
8139
8944
|
// src/prepare-failure.ts
|
|
8140
|
-
import { spawn as
|
|
8945
|
+
import { spawn as spawn5 } from "child_process";
|
|
8141
8946
|
import { hostname } from "os";
|
|
8142
8947
|
function prepareFailureCause(error) {
|
|
8143
8948
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -8175,7 +8980,7 @@ async function runPrepareWithReporting(runner, hook, input, context, reportError
|
|
|
8175
8980
|
}
|
|
8176
8981
|
function reportFailure(config, body) {
|
|
8177
8982
|
return new Promise((resolve2, reject) => {
|
|
8178
|
-
const child =
|
|
8983
|
+
const child = spawn5(config.command, config.args ?? [], {
|
|
8179
8984
|
stdio: ["pipe", "ignore", "ignore"],
|
|
8180
8985
|
env: { ...process.env, ...config.env }
|
|
8181
8986
|
});
|
|
@@ -8199,7 +9004,7 @@ function reportFailure(config, body) {
|
|
|
8199
9004
|
}
|
|
8200
9005
|
|
|
8201
9006
|
// src/prepared.ts
|
|
8202
|
-
import { mkdirSync as mkdirSync7, readFileSync as
|
|
9007
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync6, rmSync as rmSync5, writeFileSync as writeFileSync5, existsSync as existsSync7 } from "fs";
|
|
8203
9008
|
import { join as join11 } from "path";
|
|
8204
9009
|
function dirFor(workspaceId) {
|
|
8205
9010
|
return join11(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
|
|
@@ -8214,7 +9019,7 @@ function readPrepared(workspaceId, conversationId, agentId) {
|
|
|
8214
9019
|
const path = pathFor2(workspaceId, conversationId, agentId);
|
|
8215
9020
|
if (!existsSync7(path)) return null;
|
|
8216
9021
|
try {
|
|
8217
|
-
const parsed = JSON.parse(
|
|
9022
|
+
const parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
8218
9023
|
if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
|
|
8219
9024
|
return {
|
|
8220
9025
|
cwd: parsed.cwd,
|
|
@@ -8239,20 +9044,20 @@ function clearPrepared(workspaceId, conversationId, agentId) {
|
|
|
8239
9044
|
}
|
|
8240
9045
|
|
|
8241
9046
|
// src/secrets.ts
|
|
8242
|
-
import { existsSync as existsSync8, readFileSync as
|
|
9047
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
8243
9048
|
import { join as join12 } from "path";
|
|
8244
|
-
import { z as
|
|
9049
|
+
import { z as z16 } from "zod";
|
|
8245
9050
|
var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
8246
9051
|
function secretsPath() {
|
|
8247
9052
|
return join12(cabaneDir(), "secrets.json");
|
|
8248
9053
|
}
|
|
8249
|
-
var secretStoreSchema =
|
|
9054
|
+
var secretStoreSchema = z16.record(z16.string(), z16.string());
|
|
8250
9055
|
function loadSecretStore() {
|
|
8251
9056
|
const path = secretsPath();
|
|
8252
9057
|
if (!existsSync8(path)) return makeStore({});
|
|
8253
9058
|
let raw;
|
|
8254
9059
|
try {
|
|
8255
|
-
raw =
|
|
9060
|
+
raw = readFileSync7(path, "utf8");
|
|
8256
9061
|
} catch (err) {
|
|
8257
9062
|
throw new ConfigError(
|
|
8258
9063
|
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -8333,7 +9138,7 @@ function resolveMcpSecrets(mcpServers, store) {
|
|
|
8333
9138
|
import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync6 } from "fs";
|
|
8334
9139
|
import { basename, dirname as dirname6, join as join13 } from "path";
|
|
8335
9140
|
function transcriptsDir() {
|
|
8336
|
-
return join13(
|
|
9141
|
+
return join13(runnerStateDir(), "transcripts");
|
|
8337
9142
|
}
|
|
8338
9143
|
var RETAIN = 200;
|
|
8339
9144
|
var ANOMALY_RETAIN = 50;
|
|
@@ -8362,325 +9167,69 @@ var TranscriptWriter = class {
|
|
|
8362
9167
|
}
|
|
8363
9168
|
// Append the outcome footer.
|
|
8364
9169
|
close(outcome) {
|
|
8365
|
-
this.line({ type: "_outcome", ...outcome });
|
|
8366
|
-
}
|
|
8367
|
-
// CT1145: keep an independent copy of an empty successful envelope after the
|
|
8368
|
-
// footer has landed. The ordinary 200-file FIFO keeps rotating as before;
|
|
8369
|
-
// this bucket retains the newest 50 anomalies regardless of ordinary traffic.
|
|
8370
|
-
// Best-effort like every other transcript operation: observability can never
|
|
8371
|
-
// fail the turn.
|
|
8372
|
-
preserveAnomaly() {
|
|
8373
|
-
if (this.broken) return;
|
|
8374
|
-
try {
|
|
8375
|
-
const dir2 = join13(dirname6(this.path), "anomalies");
|
|
8376
|
-
mkdirSync8(dir2, { recursive: true, mode: 448 });
|
|
8377
|
-
chmodSync4(dir2, 448);
|
|
8378
|
-
const target = join13(dir2, basename(this.path));
|
|
8379
|
-
copyFileSync(this.path, target);
|
|
8380
|
-
chmodSync4(target, 384);
|
|
8381
|
-
pruneOld(dir2, ANOMALY_RETAIN);
|
|
8382
|
-
} catch (err) {
|
|
8383
|
-
this.fail(err);
|
|
8384
|
-
}
|
|
8385
|
-
}
|
|
8386
|
-
line(obj) {
|
|
8387
|
-
if (this.broken) return;
|
|
8388
|
-
try {
|
|
8389
|
-
appendFileSync(this.path, JSON.stringify(obj) + "\n", { mode: 384 });
|
|
8390
|
-
} catch (err) {
|
|
8391
|
-
this.fail(err);
|
|
8392
|
-
}
|
|
8393
|
-
}
|
|
8394
|
-
fail(err) {
|
|
8395
|
-
if (this.broken) return;
|
|
8396
|
-
this.broken = true;
|
|
8397
|
-
this.onWarn?.(`transcript write failed (${err instanceof Error ? err.message : String(err)})`);
|
|
8398
|
-
}
|
|
8399
|
-
};
|
|
8400
|
-
function fileName(meta) {
|
|
8401
|
-
const d = new Date(meta.ts);
|
|
8402
|
-
const stamp = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
8403
|
-
const slug = safe2(meta.workspaceSlug) || "ws";
|
|
8404
|
-
const short = meta.dispatchId.replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "turn";
|
|
8405
|
-
return `${stamp}-${slug}-${short}.jsonl`;
|
|
8406
|
-
}
|
|
8407
|
-
function pad(n) {
|
|
8408
|
-
return String(n).padStart(2, "0");
|
|
8409
|
-
}
|
|
8410
|
-
function safe2(s) {
|
|
8411
|
-
return s.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
8412
|
-
}
|
|
8413
|
-
function pruneOld(dir2, retain) {
|
|
8414
|
-
let files;
|
|
8415
|
-
try {
|
|
8416
|
-
files = readdirSync(dir2).filter((f) => f.endsWith(".jsonl"));
|
|
8417
|
-
} catch {
|
|
8418
|
-
return;
|
|
8419
|
-
}
|
|
8420
|
-
if (files.length <= retain) return;
|
|
8421
|
-
const drop = files.sort().slice(0, files.length - retain);
|
|
8422
|
-
for (const f of drop) {
|
|
8423
|
-
try {
|
|
8424
|
-
rmSync6(join13(dir2, f), { force: true });
|
|
8425
|
-
} catch {
|
|
8426
|
-
}
|
|
8427
|
-
}
|
|
8428
|
-
}
|
|
8429
|
-
|
|
8430
|
-
// src/turn-containment.ts
|
|
8431
|
-
import { execFile, execFileSync, spawn as spawn5 } from "child_process";
|
|
8432
|
-
import { readFileSync as readFileSync7 } from "fs";
|
|
8433
|
-
import { promisify } from "util";
|
|
8434
|
-
var execFileAsync = promisify(execFile);
|
|
8435
|
-
var asContained = (child) => child;
|
|
8436
|
-
var CONTAINMENT_STOP_GRACE_SEC = 5;
|
|
8437
|
-
var FALLBACK_GRACE_MS = CONTAINMENT_STOP_GRACE_SEC * 1e3;
|
|
8438
|
-
var CAPABILITY_PROBE_TIMEOUT_MS = 4e3;
|
|
8439
|
-
function turnScopeUnit(turnId, attempt) {
|
|
8440
|
-
const safe3 = turnId.replace(/[^A-Za-z0-9_.-]/g, "-");
|
|
8441
|
-
return `cabane-turn-${safe3}-${attempt}`;
|
|
8442
|
-
}
|
|
8443
|
-
function scopeCapabilityUsable(env, timeoutMs = CAPABILITY_PROBE_TIMEOUT_MS) {
|
|
8444
|
-
if (process.platform !== "linux") return false;
|
|
8445
|
-
try {
|
|
8446
|
-
execFileSync("systemd-run", ["--user", "--scope", "--quiet", "--collect", "--", "/bin/true"], {
|
|
8447
|
-
env,
|
|
8448
|
-
timeout: timeoutMs,
|
|
8449
|
-
stdio: "ignore"
|
|
8450
|
-
});
|
|
8451
|
-
return true;
|
|
8452
|
-
} catch {
|
|
8453
|
-
return false;
|
|
8454
|
-
}
|
|
8455
|
-
}
|
|
8456
|
-
function classifyStopError(message) {
|
|
8457
|
-
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(
|
|
8458
|
-
message
|
|
8459
|
-
)) {
|
|
8460
|
-
return "failure";
|
|
8461
|
-
}
|
|
8462
|
-
if (/not loaded|could not be found|no such unit|unit .* not found/i.test(message))
|
|
8463
|
-
return "absent";
|
|
8464
|
-
return "failure";
|
|
8465
|
-
}
|
|
8466
|
-
function currentSystemdUnit(cgroupText) {
|
|
8467
|
-
for (const line of cgroupText.split("\n")) {
|
|
8468
|
-
const path = line.trim().split(":").pop();
|
|
8469
|
-
if (!path?.startsWith("/")) continue;
|
|
8470
|
-
const leaf = path.split("/").filter(Boolean).pop();
|
|
8471
|
-
if (leaf && (leaf.endsWith(".service") || leaf.endsWith(".scope"))) return leaf;
|
|
8472
|
-
}
|
|
8473
|
-
return null;
|
|
8474
|
-
}
|
|
8475
|
-
function readCurrentSystemdUnit() {
|
|
8476
|
-
try {
|
|
8477
|
-
return currentSystemdUnit(readFileSync7("/proc/self/cgroup", "utf8"));
|
|
8478
|
-
} catch {
|
|
8479
|
-
return null;
|
|
8480
|
-
}
|
|
8481
|
-
}
|
|
8482
|
-
function processGroupMembers(pgid) {
|
|
8483
|
-
try {
|
|
8484
|
-
const out = execFileSync("ps", ["-eo", "pid=,pgid="], { timeout: 5e3 }).toString();
|
|
8485
|
-
return {
|
|
8486
|
-
ok: true,
|
|
8487
|
-
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]))
|
|
8488
|
-
};
|
|
8489
|
-
} catch (err) {
|
|
8490
|
-
return { ok: false, error: (err instanceof Error ? err.message : String(err)).slice(0, 150) };
|
|
8491
|
-
}
|
|
8492
|
-
}
|
|
8493
|
-
var sleep2 = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
8494
|
-
var MANAGER_ENV_KEYS = ["DBUS_SESSION_BUS_ADDRESS", "XDG_RUNTIME_DIR", "PATH"];
|
|
8495
|
-
var managerEnvKey = (env) => MANAGER_ENV_KEYS.map((k) => `${k}=${env[k] ?? ""}`).join("\0");
|
|
8496
|
-
function createTurnContainment(turnId, deps = {}) {
|
|
8497
|
-
const probe = deps.capabilityProbe ?? scopeCapabilityUsable;
|
|
8498
|
-
const fallbackGraceMs = deps.fallbackGraceMs ?? FALLBACK_GRACE_MS;
|
|
8499
|
-
const ownUnit = (deps.ownSystemdUnit ?? readCurrentSystemdUnit)();
|
|
8500
|
-
const scopeUnits = [];
|
|
8501
|
-
const groups = [];
|
|
8502
|
-
let kind = null;
|
|
8503
|
-
let probedEnvKey = null;
|
|
8504
|
-
let attempt = 0;
|
|
8505
|
-
function spawnInScope(options) {
|
|
8506
|
-
const unit = turnScopeUnit(turnId, attempt++);
|
|
8507
|
-
scopeUnits.push({ unit, env: options.env });
|
|
8508
|
-
deps.log?.("containment: spawning the turn CLI into a systemd scope", {
|
|
8509
|
-
unit,
|
|
8510
|
-
partOf: ownUnit,
|
|
8511
|
-
command: options.command
|
|
8512
|
-
});
|
|
8513
|
-
return asContained(
|
|
8514
|
-
spawn5(
|
|
8515
|
-
"systemd-run",
|
|
8516
|
-
[
|
|
8517
|
-
"--user",
|
|
8518
|
-
"--scope",
|
|
8519
|
-
"--quiet",
|
|
8520
|
-
"--collect",
|
|
8521
|
-
`--unit=${unit}`,
|
|
8522
|
-
`--property=TimeoutStopSec=${CONTAINMENT_STOP_GRACE_SEC}s`,
|
|
8523
|
-
// Bind the scope's life to the companion's own unit, so stopping or
|
|
8524
|
-
// restarting the companion still takes its in-flight turns with it —
|
|
8525
|
-
// which is what happens today, and what the scope would otherwise undo.
|
|
8526
|
-
// Omitted outside a unit, where there is nothing to bind to.
|
|
8527
|
-
...ownUnit ? [`--property=PartOf=${ownUnit}`] : [],
|
|
8528
|
-
"--",
|
|
8529
|
-
options.command,
|
|
8530
|
-
...options.args
|
|
8531
|
-
],
|
|
8532
|
-
{
|
|
8533
|
-
...options.cwd ? { cwd: options.cwd } : {},
|
|
8534
|
-
env: options.env,
|
|
8535
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
8536
|
-
}
|
|
8537
|
-
)
|
|
8538
|
-
);
|
|
8539
|
-
}
|
|
8540
|
-
function spawnInGroup(options) {
|
|
8541
|
-
const child = spawn5(options.command, options.args, {
|
|
8542
|
-
...options.cwd ? { cwd: options.cwd } : {},
|
|
8543
|
-
env: options.env,
|
|
8544
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
8545
|
-
detached: process.platform !== "win32"
|
|
8546
|
-
});
|
|
8547
|
-
if (child.pid !== void 0) groups.push({ pgid: child.pid, child });
|
|
8548
|
-
deps.log?.("containment: no host-owned containment here \u2014 best effort only", {
|
|
8549
|
-
platform: process.platform,
|
|
8550
|
-
pid: child.pid
|
|
8551
|
-
});
|
|
8552
|
-
return asContained(child);
|
|
8553
|
-
}
|
|
8554
|
-
async function reapScopes(result) {
|
|
8555
|
-
for (const { unit, env } of scopeUnits) {
|
|
8556
|
-
try {
|
|
8557
|
-
await execFileAsync("systemctl", ["--user", "stop", `${unit}.scope`], {
|
|
8558
|
-
env,
|
|
8559
|
-
timeout: (CONTAINMENT_STOP_GRACE_SEC + 10) * 1e3
|
|
8560
|
-
});
|
|
8561
|
-
} catch (err) {
|
|
8562
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
8563
|
-
if (classifyStopError(message) === "failure") {
|
|
8564
|
-
result.failures.push({ target: unit, error: message.slice(0, 200) });
|
|
8565
|
-
continue;
|
|
8566
|
-
}
|
|
8567
|
-
result.reaped.push(unit);
|
|
8568
|
-
continue;
|
|
8569
|
-
}
|
|
8570
|
-
try {
|
|
8571
|
-
const { stdout } = await execFileAsync(
|
|
8572
|
-
"systemctl",
|
|
8573
|
-
["--user", "show", `${unit}.scope`, "--property=ActiveState", "--value"],
|
|
8574
|
-
{ env, timeout: 1e4 }
|
|
8575
|
-
);
|
|
8576
|
-
const state = stdout.trim();
|
|
8577
|
-
if (state === "" || state === "inactive" || state === "failed") result.reaped.push(unit);
|
|
8578
|
-
else result.failures.push({ target: unit, error: `scope still ${state} after stop` });
|
|
8579
|
-
} catch (err) {
|
|
8580
|
-
result.failures.push({
|
|
8581
|
-
target: unit,
|
|
8582
|
-
error: `could not confirm the scope stopped: ${(err instanceof Error ? err.message : String(err)).slice(0, 150)}`
|
|
8583
|
-
});
|
|
8584
|
-
}
|
|
9170
|
+
this.line({ type: "_outcome", ...outcome });
|
|
9171
|
+
}
|
|
9172
|
+
// CT1145: keep an independent copy of an empty successful envelope after the
|
|
9173
|
+
// footer has landed. The ordinary 200-file FIFO keeps rotating as before;
|
|
9174
|
+
// this bucket retains the newest 50 anomalies regardless of ordinary traffic.
|
|
9175
|
+
// Best-effort like every other transcript operation: observability can never
|
|
9176
|
+
// fail the turn.
|
|
9177
|
+
preserveAnomaly() {
|
|
9178
|
+
if (this.broken) return;
|
|
9179
|
+
try {
|
|
9180
|
+
const dir2 = join13(dirname6(this.path), "anomalies");
|
|
9181
|
+
mkdirSync8(dir2, { recursive: true, mode: 448 });
|
|
9182
|
+
chmodSync4(dir2, 448);
|
|
9183
|
+
const target = join13(dir2, basename(this.path));
|
|
9184
|
+
copyFileSync(this.path, target);
|
|
9185
|
+
chmodSync4(target, 384);
|
|
9186
|
+
pruneOld(dir2, ANOMALY_RETAIN);
|
|
9187
|
+
} catch (err) {
|
|
9188
|
+
this.fail(err);
|
|
8585
9189
|
}
|
|
8586
9190
|
}
|
|
8587
|
-
|
|
8588
|
-
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
result.unverified.push(target);
|
|
8594
|
-
} catch (err) {
|
|
8595
|
-
result.failures.push({
|
|
8596
|
-
target,
|
|
8597
|
-
error: (err instanceof Error ? err.message : String(err)).slice(0, 200)
|
|
8598
|
-
});
|
|
8599
|
-
}
|
|
8600
|
-
continue;
|
|
8601
|
-
}
|
|
8602
|
-
const first = processGroupMembers(pgid);
|
|
8603
|
-
if (!first.ok) {
|
|
8604
|
-
result.failures.push({
|
|
8605
|
-
target,
|
|
8606
|
-
error: `could not enumerate the process group: ${first.error}`
|
|
8607
|
-
});
|
|
8608
|
-
continue;
|
|
8609
|
-
}
|
|
8610
|
-
if (first.members.length === 0) {
|
|
8611
|
-
result.reaped.push(target);
|
|
8612
|
-
continue;
|
|
8613
|
-
}
|
|
8614
|
-
for (const pid of first.members) {
|
|
8615
|
-
try {
|
|
8616
|
-
process.kill(pid, "SIGTERM");
|
|
8617
|
-
} catch {
|
|
8618
|
-
}
|
|
8619
|
-
}
|
|
8620
|
-
await sleep2(fallbackGraceMs);
|
|
8621
|
-
const afterTerm = processGroupMembers(pgid);
|
|
8622
|
-
if (!afterTerm.ok) {
|
|
8623
|
-
result.failures.push({
|
|
8624
|
-
target,
|
|
8625
|
-
error: `could not re-enumerate the process group after SIGTERM: ${afterTerm.error}`
|
|
8626
|
-
});
|
|
8627
|
-
continue;
|
|
8628
|
-
}
|
|
8629
|
-
for (const pid of afterTerm.members) {
|
|
8630
|
-
try {
|
|
8631
|
-
process.kill(pid, "SIGKILL");
|
|
8632
|
-
} catch {
|
|
8633
|
-
}
|
|
8634
|
-
}
|
|
8635
|
-
const afterKill = processGroupMembers(pgid);
|
|
8636
|
-
if (!afterKill.ok) {
|
|
8637
|
-
result.failures.push({
|
|
8638
|
-
target,
|
|
8639
|
-
error: `could not confirm the process group is empty after SIGKILL: ${afterKill.error}`
|
|
8640
|
-
});
|
|
8641
|
-
continue;
|
|
8642
|
-
}
|
|
8643
|
-
if (afterKill.members.length)
|
|
8644
|
-
result.failures.push({
|
|
8645
|
-
target,
|
|
8646
|
-
error: `${afterKill.members.length} process(es) still in the group after SIGKILL`
|
|
8647
|
-
});
|
|
8648
|
-
else result.reaped.push(target);
|
|
9191
|
+
line(obj) {
|
|
9192
|
+
if (this.broken) return;
|
|
9193
|
+
try {
|
|
9194
|
+
appendFileSync(this.path, JSON.stringify(obj) + "\n", { mode: 384 });
|
|
9195
|
+
} catch (err) {
|
|
9196
|
+
this.fail(err);
|
|
8649
9197
|
}
|
|
8650
9198
|
}
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
}
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
8662
|
-
|
|
8663
|
-
|
|
8664
|
-
|
|
8665
|
-
|
|
8666
|
-
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
|
|
9199
|
+
fail(err) {
|
|
9200
|
+
if (this.broken) return;
|
|
9201
|
+
this.broken = true;
|
|
9202
|
+
this.onWarn?.(`transcript write failed (${err instanceof Error ? err.message : String(err)})`);
|
|
9203
|
+
}
|
|
9204
|
+
};
|
|
9205
|
+
function fileName(meta) {
|
|
9206
|
+
const d = new Date(meta.ts);
|
|
9207
|
+
const stamp = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
9208
|
+
const slug = safe2(meta.workspaceSlug) || "ws";
|
|
9209
|
+
const short = meta.dispatchId.replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "turn";
|
|
9210
|
+
return `${stamp}-${slug}-${short}.jsonl`;
|
|
9211
|
+
}
|
|
9212
|
+
function pad(n) {
|
|
9213
|
+
return String(n).padStart(2, "0");
|
|
9214
|
+
}
|
|
9215
|
+
function safe2(s) {
|
|
9216
|
+
return s.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
9217
|
+
}
|
|
9218
|
+
function pruneOld(dir2, retain) {
|
|
9219
|
+
let files;
|
|
9220
|
+
try {
|
|
9221
|
+
files = readdirSync(dir2).filter((f) => f.endsWith(".jsonl"));
|
|
9222
|
+
} catch {
|
|
9223
|
+
return;
|
|
9224
|
+
}
|
|
9225
|
+
if (files.length <= retain) return;
|
|
9226
|
+
const drop = files.sort().slice(0, files.length - retain);
|
|
9227
|
+
for (const f of drop) {
|
|
9228
|
+
try {
|
|
9229
|
+
rmSync6(join13(dir2, f), { force: true });
|
|
9230
|
+
} catch {
|
|
8682
9231
|
}
|
|
8683
|
-
}
|
|
9232
|
+
}
|
|
8684
9233
|
}
|
|
8685
9234
|
|
|
8686
9235
|
// src/turn-committer.ts
|
|
@@ -9291,14 +9840,15 @@ var TurnExecution = class {
|
|
|
9291
9840
|
const reportPreparing = (phase) => {
|
|
9292
9841
|
preparing ??= { id: `prepare:${randomUUID3()}`, seq: this.nextSeq() };
|
|
9293
9842
|
const { id, seq } = preparing;
|
|
9294
|
-
|
|
9843
|
+
const body = {
|
|
9295
9844
|
turnId,
|
|
9296
9845
|
toolUseId: id,
|
|
9297
9846
|
toolName: PREPARING_TOOL_NAME,
|
|
9298
9847
|
summary: "",
|
|
9299
9848
|
phase,
|
|
9300
9849
|
seq
|
|
9301
|
-
}
|
|
9850
|
+
};
|
|
9851
|
+
void this.opts.api.reportActivity(workspaceId, payload.conversationId, payload.agentId, body).catch((err) => {
|
|
9302
9852
|
turnLog.debug(
|
|
9303
9853
|
{ err: err instanceof Error ? err.message : String(err), phase },
|
|
9304
9854
|
"dispatcher: preparing-activity report failed (continuing with the hook)"
|
|
@@ -9371,7 +9921,11 @@ var TurnExecution = class {
|
|
|
9371
9921
|
try {
|
|
9372
9922
|
await this.opts.api.setActiveRun(workspaceId, payload.conversationId, payload.agentId, {
|
|
9373
9923
|
activeRunStartedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9374
|
-
|
|
9924
|
+
// CT1679: a runner leases under its launcher's boot id — the one the
|
|
9925
|
+
// device heartbeats — or the lost-run sweep reads it as a restart.
|
|
9926
|
+
bootId: this.opts.inRunner?.bootId ?? bootId,
|
|
9927
|
+
// CT1680: the runner holding the span, beside the device's boot id.
|
|
9928
|
+
...this.opts.inRunner ? { runnerId: this.opts.inRunner.runnerId } : {},
|
|
9375
9929
|
// CT1380: the durable record that this turn resumed, and from where.
|
|
9376
9930
|
...this.resumedFromSeq !== void 0 ? { resumedFromSeq: this.resumedFromSeq } : {},
|
|
9377
9931
|
// CT1454: the issuance the context THIS attempt runs on came from. Read
|
|
@@ -9468,10 +10022,14 @@ var TurnExecution = class {
|
|
|
9468
10022
|
const { payload, workspaceId, turnId, turnLog } = this;
|
|
9469
10023
|
await guardBundledBinary(this.integrityContext(), (r, e) => this.concluded(r, e), this.outcome);
|
|
9470
10024
|
const onWarn = (msg, meta) => turnLog.debug(meta ?? {}, msg);
|
|
10025
|
+
const sandbox = this.opts.harnessSandbox ? await this.opts.harnessSandbox({ cwd: this.effectiveCwd, log: turnLog }) : null;
|
|
9471
10026
|
const adapters = [];
|
|
9472
10027
|
if (this.opts.claudeCodeAvailable?.() ?? true) {
|
|
9473
10028
|
this.containment = createTurnContainment(this.turnId, {
|
|
9474
|
-
log: (msg, meta) => turnLog.debug(meta ?? {}, msg)
|
|
10029
|
+
log: (msg, meta) => turnLog.debug(meta ?? {}, msg),
|
|
10030
|
+
// CT1679: a group outside the runner's own is reported, so a launcher
|
|
10031
|
+
// can stop it if the runner dies without reaping it.
|
|
10032
|
+
...this.opts.inRunner?.onProcessGroup ? { onProcessGroup: this.opts.inRunner.onProcessGroup } : {}
|
|
9475
10033
|
});
|
|
9476
10034
|
const containment = this.containment;
|
|
9477
10035
|
adapters.push(
|
|
@@ -9480,20 +10038,43 @@ var TurnExecution = class {
|
|
|
9480
10038
|
onWarn,
|
|
9481
10039
|
augmentOptions: (options) => ({
|
|
9482
10040
|
...options,
|
|
9483
|
-
spawnClaudeCodeProcess: (spawnOptions) => containment.spawn(
|
|
10041
|
+
spawnClaudeCodeProcess: (spawnOptions) => containment.spawn(
|
|
10042
|
+
sandbox ? { ...spawnOptions, ...sandbox.wrap(spawnOptions.command, spawnOptions.args) } : spawnOptions
|
|
10043
|
+
)
|
|
9484
10044
|
})
|
|
9485
10045
|
})
|
|
9486
10046
|
);
|
|
9487
10047
|
}
|
|
9488
10048
|
if (this.opts.opencodeServerUrl) {
|
|
9489
|
-
|
|
10049
|
+
const serverUrl = this.opts.opencodeServerUrl;
|
|
10050
|
+
adapters.push(
|
|
10051
|
+
createOpencodeAdapter(
|
|
10052
|
+
// CT1679: the transport's per-server turn mutex is per process; in a
|
|
10053
|
+
// runner the turn also holds the device-wide lock every runner shares.
|
|
10054
|
+
this.opts.inRunner ? {
|
|
10055
|
+
transport: this.opts.inRunner.opencodeTransport(serverUrl),
|
|
10056
|
+
onWarn
|
|
10057
|
+
} : { serverUrl, onWarn }
|
|
10058
|
+
)
|
|
10059
|
+
);
|
|
9490
10060
|
}
|
|
9491
10061
|
if (this.opts.codexAvailable?.() ?? false) {
|
|
10062
|
+
const inRunner = this.opts.inRunner;
|
|
10063
|
+
const codexBinary = sandbox || inRunner ? (this.opts.resolveBundled ?? resolveBundledBinary)("codex").path : null;
|
|
10064
|
+
let codexPath = codexBinary && sandbox ? sandbox.executableFor(codexBinary) : codexBinary;
|
|
10065
|
+
if (codexPath && inRunner) {
|
|
10066
|
+
this.containment ??= createTurnContainment(this.turnId, {
|
|
10067
|
+
log: (msg, meta) => turnLog.debug(meta ?? {}, msg),
|
|
10068
|
+
...this.opts.inRunner?.onProcessGroup ? { onProcessGroup: this.opts.inRunner.onProcessGroup } : {}
|
|
10069
|
+
});
|
|
10070
|
+
codexPath = inRunner.codexExecutable(codexPath, this.containment);
|
|
10071
|
+
}
|
|
9492
10072
|
adapters.push(
|
|
9493
10073
|
createCodexAdapter({
|
|
9494
10074
|
enabled: true,
|
|
9495
10075
|
onWarn,
|
|
9496
|
-
writeInstructionsFile: writeCodexInstructionsFile
|
|
10076
|
+
writeInstructionsFile: writeCodexInstructionsFile,
|
|
10077
|
+
...codexPath ? { transport: createSdkCodexTransport({ codexPathOverride: codexPath }) } : {}
|
|
9497
10078
|
})
|
|
9498
10079
|
);
|
|
9499
10080
|
}
|
|
@@ -9712,7 +10293,10 @@ var TurnExecution = class {
|
|
|
9712
10293
|
workspaceId,
|
|
9713
10294
|
payload.conversationId,
|
|
9714
10295
|
payload.agentId,
|
|
9715
|
-
|
|
10296
|
+
// CT1680: named for its turn, which a runner credential
|
|
10297
|
+
// requires once its start is admitted; the server then binds
|
|
10298
|
+
// the capture to that turn's attempt rather than the pair's.
|
|
10299
|
+
{ agentSessionId: event.state, turnId }
|
|
9716
10300
|
);
|
|
9717
10301
|
} catch (err) {
|
|
9718
10302
|
const status = err instanceof ApiError ? err.status : void 0;
|
|
@@ -10030,6 +10614,24 @@ var Dispatcher = class {
|
|
|
10030
10614
|
notifyStart: (info) => this.notifyStart(info),
|
|
10031
10615
|
notifyEnd: (info) => this.notifyEnd(info)
|
|
10032
10616
|
};
|
|
10617
|
+
if (this.opts.runner) {
|
|
10618
|
+
return runTurnInRunner(
|
|
10619
|
+
this.opts,
|
|
10620
|
+
this.opts.runner,
|
|
10621
|
+
{
|
|
10622
|
+
registerAbort: supervisor.registerAbort,
|
|
10623
|
+
releaseAbort: supervisor.releaseAbort,
|
|
10624
|
+
sessionWriteNoticeClaimed: () => this.sessionWriteNotified.has(key),
|
|
10625
|
+
markSessionWriteNotice: () => {
|
|
10626
|
+
this.sessionWriteNotified.add(key);
|
|
10627
|
+
},
|
|
10628
|
+
notifyStart: supervisor.notifyStart,
|
|
10629
|
+
notifyEnd: supervisor.notifyEnd
|
|
10630
|
+
},
|
|
10631
|
+
payload,
|
|
10632
|
+
{ turnId: opts.turnId ?? randomUUID4(), resumed: opts.resumed === true }
|
|
10633
|
+
);
|
|
10634
|
+
}
|
|
10033
10635
|
return new TurnExecution(this.opts, supervisor, payload, opts).run();
|
|
10034
10636
|
}
|
|
10035
10637
|
// SJ383: cancel a specific (conversation, agent) run if one is in flight in
|
|
@@ -10048,6 +10650,176 @@ var Dispatcher = class {
|
|
|
10048
10650
|
}
|
|
10049
10651
|
};
|
|
10050
10652
|
|
|
10653
|
+
// src/providers/local-process.ts
|
|
10654
|
+
import { spawn as spawn6 } from "child_process";
|
|
10655
|
+
import { constants } from "os";
|
|
10656
|
+
var DEFAULT_KILL_GRACE_MS = 3e4;
|
|
10657
|
+
var DEFAULT_REAP_GRACE_MS = 3e3;
|
|
10658
|
+
function exitCode(code, signal) {
|
|
10659
|
+
if (code !== null) return code;
|
|
10660
|
+
const signum = signal ? constants.signals[signal] : void 0;
|
|
10661
|
+
return signum !== void 0 ? 128 + signum : 1;
|
|
10662
|
+
}
|
|
10663
|
+
function createLocalProcessProvider(opts = {}) {
|
|
10664
|
+
const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
|
|
10665
|
+
const spawnFn = opts.spawnFn ?? spawn6;
|
|
10666
|
+
const reapGraceMs = opts.reapGraceMs ?? DEFAULT_REAP_GRACE_MS;
|
|
10667
|
+
const killFn = opts.killFn ?? ((pid, signal) => process.kill(pid, signal));
|
|
10668
|
+
const groupAlive = (pgid) => {
|
|
10669
|
+
try {
|
|
10670
|
+
killFn(-pgid, 0);
|
|
10671
|
+
return true;
|
|
10672
|
+
} catch {
|
|
10673
|
+
return false;
|
|
10674
|
+
}
|
|
10675
|
+
};
|
|
10676
|
+
const signalGroup = (pgid, signal) => {
|
|
10677
|
+
try {
|
|
10678
|
+
killFn(-pgid, signal);
|
|
10679
|
+
} catch {
|
|
10680
|
+
}
|
|
10681
|
+
};
|
|
10682
|
+
return {
|
|
10683
|
+
name: "local-process",
|
|
10684
|
+
async acquire(attempt) {
|
|
10685
|
+
return { provider: "local-process", id: attempt.turnId };
|
|
10686
|
+
},
|
|
10687
|
+
exec(_handle, argv, env, { signal, stdin }) {
|
|
10688
|
+
const [command, ...args] = argv;
|
|
10689
|
+
if (!command) throw new Error("local-process: exec needs a command");
|
|
10690
|
+
const child = spawnFn(command, args, {
|
|
10691
|
+
env: { ...env },
|
|
10692
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
10693
|
+
detached: process.platform !== "win32"
|
|
10694
|
+
});
|
|
10695
|
+
child.stdin?.on("error", () => {
|
|
10696
|
+
});
|
|
10697
|
+
if (stdin) child.stdin?.write(stdin);
|
|
10698
|
+
const exit = new Promise((resolve2) => {
|
|
10699
|
+
let settled = false;
|
|
10700
|
+
const done = (code) => {
|
|
10701
|
+
if (settled) return;
|
|
10702
|
+
settled = true;
|
|
10703
|
+
signal.removeEventListener("abort", stop);
|
|
10704
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
10705
|
+
resolve2(code);
|
|
10706
|
+
};
|
|
10707
|
+
let forceTimer;
|
|
10708
|
+
const stop = () => {
|
|
10709
|
+
child.kill("SIGTERM");
|
|
10710
|
+
forceTimer = setTimeout(() => child.kill("SIGKILL"), killGraceMs);
|
|
10711
|
+
forceTimer.unref?.();
|
|
10712
|
+
};
|
|
10713
|
+
child.once("error", () => done(1));
|
|
10714
|
+
child.once("close", (code, sig) => done(exitCode(code, sig)));
|
|
10715
|
+
if (signal.aborted) stop();
|
|
10716
|
+
else signal.addEventListener("abort", stop, { once: true });
|
|
10717
|
+
});
|
|
10718
|
+
if (!child.stdout || !child.stderr || !child.stdin)
|
|
10719
|
+
throw new Error("local-process: runner stdio missing");
|
|
10720
|
+
const pid = child.pid;
|
|
10721
|
+
return {
|
|
10722
|
+
exit,
|
|
10723
|
+
...pid !== void 0 ? { pid } : {},
|
|
10724
|
+
stdout: child.stdout,
|
|
10725
|
+
stderr: child.stderr,
|
|
10726
|
+
stdin: child.stdin,
|
|
10727
|
+
async reap(groups) {
|
|
10728
|
+
if (process.platform === "win32") return;
|
|
10729
|
+
const targets = [.../* @__PURE__ */ new Set([...pid !== void 0 ? [pid] : [], ...groups])].filter(
|
|
10730
|
+
groupAlive
|
|
10731
|
+
);
|
|
10732
|
+
if (!targets.length) return;
|
|
10733
|
+
for (const pgid of targets) signalGroup(pgid, "SIGTERM");
|
|
10734
|
+
await new Promise((resolve2) => setTimeout(resolve2, reapGraceMs));
|
|
10735
|
+
for (const pgid of targets.filter(groupAlive)) signalGroup(pgid, "SIGKILL");
|
|
10736
|
+
}
|
|
10737
|
+
};
|
|
10738
|
+
},
|
|
10739
|
+
async release() {
|
|
10740
|
+
}
|
|
10741
|
+
};
|
|
10742
|
+
}
|
|
10743
|
+
|
|
10744
|
+
// src/runner/device-locks.ts
|
|
10745
|
+
var DeviceLocks = class {
|
|
10746
|
+
holders = /* @__PURE__ */ new Map();
|
|
10747
|
+
queues = /* @__PURE__ */ new Map();
|
|
10748
|
+
// `owner` is unique across the device; `grant` is called once, when it holds.
|
|
10749
|
+
request(key, owner, grant) {
|
|
10750
|
+
if (!this.holders.has(key)) {
|
|
10751
|
+
this.holders.set(key, owner);
|
|
10752
|
+
grant();
|
|
10753
|
+
return;
|
|
10754
|
+
}
|
|
10755
|
+
const queue = this.queues.get(key) ?? [];
|
|
10756
|
+
queue.push({ owner, grant });
|
|
10757
|
+
this.queues.set(key, queue);
|
|
10758
|
+
}
|
|
10759
|
+
// Gives back what `owner` holds and withdraws what it is waiting for.
|
|
10760
|
+
release(owner) {
|
|
10761
|
+
for (const [key, queue] of this.queues) {
|
|
10762
|
+
const rest = queue.filter((w) => w.owner !== owner);
|
|
10763
|
+
if (rest.length) this.queues.set(key, rest);
|
|
10764
|
+
else this.queues.delete(key);
|
|
10765
|
+
}
|
|
10766
|
+
for (const [key, holder] of [...this.holders]) {
|
|
10767
|
+
if (holder !== owner) continue;
|
|
10768
|
+
this.holders.delete(key);
|
|
10769
|
+
this.handOn(key);
|
|
10770
|
+
}
|
|
10771
|
+
}
|
|
10772
|
+
// What owners starting with `prefix` hold, by key — one runner's, when it
|
|
10773
|
+
// exits, so the launcher can decide which it may give back at once.
|
|
10774
|
+
heldBy(prefix) {
|
|
10775
|
+
return [...this.holders].filter(([, owner]) => owner.startsWith(prefix)).map(([key, owner]) => ({ key, owner }));
|
|
10776
|
+
}
|
|
10777
|
+
// Withdraws every request still waiting from owners starting with `prefix`,
|
|
10778
|
+
// leaving what they hold.
|
|
10779
|
+
withdraw(prefix) {
|
|
10780
|
+
for (const [key, queue] of this.queues) {
|
|
10781
|
+
const rest = queue.filter((w) => !w.owner.startsWith(prefix));
|
|
10782
|
+
if (rest.length) this.queues.set(key, rest);
|
|
10783
|
+
else this.queues.delete(key);
|
|
10784
|
+
}
|
|
10785
|
+
}
|
|
10786
|
+
holder(key) {
|
|
10787
|
+
return this.holders.get(key);
|
|
10788
|
+
}
|
|
10789
|
+
handOn(key) {
|
|
10790
|
+
const queue = this.queues.get(key);
|
|
10791
|
+
const next = queue?.shift();
|
|
10792
|
+
if (!queue?.length) this.queues.delete(key);
|
|
10793
|
+
if (!next) return;
|
|
10794
|
+
this.holders.set(key, next.owner);
|
|
10795
|
+
next.grant();
|
|
10796
|
+
}
|
|
10797
|
+
};
|
|
10798
|
+
|
|
10799
|
+
// src/runner/harness-sandbox.ts
|
|
10800
|
+
import { chmodSync as chmodSync5, mkdirSync as mkdirSync9, mkdtempSync, rmSync as rmSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
10801
|
+
import { homedir as homedir3, tmpdir as tmpdir2, userInfo as userInfo2 } from "os";
|
|
10802
|
+
import { join as join15 } from "path";
|
|
10803
|
+
import { SandboxManager } from "@anthropic-ai/sandbox-runtime";
|
|
10804
|
+
function osSandboxUsable(log) {
|
|
10805
|
+
if (!SandboxManager.isSupportedPlatform()) {
|
|
10806
|
+
log.warn(
|
|
10807
|
+
{ platform: process.platform },
|
|
10808
|
+
'runners: sandbox "os" is not available on this platform; turns run unsandboxed'
|
|
10809
|
+
);
|
|
10810
|
+
return false;
|
|
10811
|
+
}
|
|
10812
|
+
const deps = SandboxManager.checkDependencies();
|
|
10813
|
+
if (deps.errors.length) {
|
|
10814
|
+
log.warn(
|
|
10815
|
+
{ errors: deps.errors },
|
|
10816
|
+
'runners: sandbox "os" is missing what it needs on this host; turns run unsandboxed'
|
|
10817
|
+
);
|
|
10818
|
+
return false;
|
|
10819
|
+
}
|
|
10820
|
+
return true;
|
|
10821
|
+
}
|
|
10822
|
+
|
|
10051
10823
|
// src/opencode-models.ts
|
|
10052
10824
|
var OPENCODE_RUNTIME = "opencode";
|
|
10053
10825
|
function mapOpencodeProviders(json) {
|
|
@@ -10127,14 +10899,14 @@ var DispatchClaims = class {
|
|
|
10127
10899
|
// src/outbox.ts
|
|
10128
10900
|
import {
|
|
10129
10901
|
existsSync as existsSync11,
|
|
10130
|
-
mkdirSync as
|
|
10902
|
+
mkdirSync as mkdirSync10,
|
|
10131
10903
|
readdirSync as readdirSync3,
|
|
10132
10904
|
readFileSync as readFileSync8,
|
|
10133
10905
|
renameSync as renameSync4,
|
|
10134
|
-
rmSync as
|
|
10135
|
-
writeFileSync as
|
|
10906
|
+
rmSync as rmSync8,
|
|
10907
|
+
writeFileSync as writeFileSync7
|
|
10136
10908
|
} from "fs";
|
|
10137
|
-
import { join as
|
|
10909
|
+
import { join as join16 } from "path";
|
|
10138
10910
|
var MAX_ENTRIES = 2e3;
|
|
10139
10911
|
var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
10140
10912
|
var Outbox = class {
|
|
@@ -10147,25 +10919,25 @@ var Outbox = class {
|
|
|
10147
10919
|
// Resolved lazily (per call, like cursor.ts) so tests that swap HOME between
|
|
10148
10920
|
// cases route writes at the right tmpdir.
|
|
10149
10921
|
dir() {
|
|
10150
|
-
return
|
|
10922
|
+
return join16(runnerStateDir(), "outbox", encodeURIComponent(this.workspaceId));
|
|
10151
10923
|
}
|
|
10152
10924
|
fileFor(turnId, seq) {
|
|
10153
|
-
return
|
|
10925
|
+
return join16(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
|
|
10154
10926
|
}
|
|
10155
10927
|
// Persist a commit for later draining. Atomic (temp file + rename) so a
|
|
10156
10928
|
// concurrent `list()` never reads a half-written entry, then enforces the
|
|
10157
10929
|
// per-workspace bounds.
|
|
10158
10930
|
persist(entry) {
|
|
10159
10931
|
const dir2 = this.dir();
|
|
10160
|
-
|
|
10932
|
+
mkdirSync10(dir2, { recursive: true });
|
|
10161
10933
|
const target = this.fileFor(entry.turnId, entry.seq);
|
|
10162
10934
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
10163
10935
|
try {
|
|
10164
|
-
|
|
10936
|
+
writeFileSync7(tmp, JSON.stringify(entry) + "\n", "utf8");
|
|
10165
10937
|
renameSync4(tmp, target);
|
|
10166
10938
|
} catch (err) {
|
|
10167
10939
|
try {
|
|
10168
|
-
|
|
10940
|
+
rmSync8(tmp, { force: true });
|
|
10169
10941
|
} catch {
|
|
10170
10942
|
}
|
|
10171
10943
|
this.log?.warn(
|
|
@@ -10192,7 +10964,7 @@ var Outbox = class {
|
|
|
10192
10964
|
const entries = [];
|
|
10193
10965
|
for (const name of names) {
|
|
10194
10966
|
if (!name.endsWith(".json")) continue;
|
|
10195
|
-
const full =
|
|
10967
|
+
const full = join16(dir2, name);
|
|
10196
10968
|
try {
|
|
10197
10969
|
const parsed = JSON.parse(readFileSync8(full, "utf8"));
|
|
10198
10970
|
if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
|
|
@@ -10242,7 +11014,7 @@ var Outbox = class {
|
|
|
10242
11014
|
// Remove a delivered (or terminally-discarded) entry. No-op if already gone.
|
|
10243
11015
|
remove(turnId, seq) {
|
|
10244
11016
|
try {
|
|
10245
|
-
|
|
11017
|
+
rmSync8(this.fileFor(turnId, seq), { force: true });
|
|
10246
11018
|
} catch {
|
|
10247
11019
|
}
|
|
10248
11020
|
}
|
|
@@ -10261,7 +11033,7 @@ var Outbox = class {
|
|
|
10261
11033
|
"A saved update is unreadable and couldn't be delivered. Check the conversation for missing replies."
|
|
10262
11034
|
);
|
|
10263
11035
|
try {
|
|
10264
|
-
|
|
11036
|
+
rmSync8(full, { force: true });
|
|
10265
11037
|
} catch {
|
|
10266
11038
|
}
|
|
10267
11039
|
}
|
|
@@ -10295,53 +11067,6 @@ var Outbox = class {
|
|
|
10295
11067
|
}
|
|
10296
11068
|
};
|
|
10297
11069
|
|
|
10298
|
-
// src/run-config.ts
|
|
10299
|
-
import { z as z15 } from "zod";
|
|
10300
|
-
var mcpStdioServerSchema = z15.object({
|
|
10301
|
-
type: z15.literal("stdio").optional(),
|
|
10302
|
-
command: z15.string().min(1),
|
|
10303
|
-
args: z15.array(z15.string()).optional(),
|
|
10304
|
-
env: z15.record(z15.string(), z15.string()).optional()
|
|
10305
|
-
});
|
|
10306
|
-
var mcpHttpServerSchema = z15.object({
|
|
10307
|
-
type: z15.literal("http"),
|
|
10308
|
-
url: z15.string().url(),
|
|
10309
|
-
headers: z15.record(z15.string(), z15.string()).optional()
|
|
10310
|
-
});
|
|
10311
|
-
var mcpSseServerSchema = z15.object({
|
|
10312
|
-
type: z15.literal("sse"),
|
|
10313
|
-
url: z15.string().url(),
|
|
10314
|
-
headers: z15.record(z15.string(), z15.string()).optional()
|
|
10315
|
-
});
|
|
10316
|
-
var mcpServerDefSchema = z15.union([mcpHttpServerSchema, mcpSseServerSchema, mcpStdioServerSchema]);
|
|
10317
|
-
var thinkingConfigSchema = z15.discriminatedUnion("type", [
|
|
10318
|
-
z15.object({ type: z15.literal("adaptive") }),
|
|
10319
|
-
z15.object({ type: z15.literal("enabled"), budgetTokens: z15.number().int().positive().optional() }),
|
|
10320
|
-
z15.object({ type: z15.literal("disabled") })
|
|
10321
|
-
]);
|
|
10322
|
-
var effortSchema = z15.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
10323
|
-
var runConfigSchema = z15.object({
|
|
10324
|
-
// CT788: the host-access binary replaced the `assistant`/`coding`/`custom` mode
|
|
10325
|
-
// trio + its custom tool lists — `true` grants the host filesystem/shell, absent
|
|
10326
|
-
// is the locked surface. Kept in lockstep with `@cabane/shared`'s
|
|
10327
|
-
// `agentRunConfigSchema` (independent zod, forward-compatible: unknown keys are
|
|
10328
|
-
// stripped, so an older companion riding a newer server never rejects the config).
|
|
10329
|
-
hostAccess: z15.boolean().optional(),
|
|
10330
|
-
mcpServers: z15.record(z15.string(), mcpServerDefSchema).optional(),
|
|
10331
|
-
model: z15.string().min(1).optional(),
|
|
10332
|
-
thinking: thinkingConfigSchema.optional(),
|
|
10333
|
-
effort: effortSchema.optional()
|
|
10334
|
-
});
|
|
10335
|
-
var EMPTY_RUN_CONFIG = {};
|
|
10336
|
-
function parseRunConfig(raw, onWarn) {
|
|
10337
|
-
const result = runConfigSchema.safeParse(raw ?? {});
|
|
10338
|
-
if (result.success) return result.data;
|
|
10339
|
-
onWarn?.(
|
|
10340
|
-
`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`
|
|
10341
|
-
);
|
|
10342
|
-
return EMPTY_RUN_CONFIG;
|
|
10343
|
-
}
|
|
10344
|
-
|
|
10345
11070
|
// src/sse.ts
|
|
10346
11071
|
import { createParser } from "eventsource-parser";
|
|
10347
11072
|
var MAX_BACKOFF_MS = 3e4;
|
|
@@ -10506,6 +11231,8 @@ var CompanionSupervisor = class {
|
|
|
10506
11231
|
exitFn;
|
|
10507
11232
|
reexecFn;
|
|
10508
11233
|
dispatcherFactory;
|
|
11234
|
+
runnerCommand;
|
|
11235
|
+
providerForRunners;
|
|
10509
11236
|
unrunnableRetryDelaysMs;
|
|
10510
11237
|
deviceApi = null;
|
|
10511
11238
|
deviceSub = null;
|
|
@@ -10550,6 +11277,8 @@ var CompanionSupervisor = class {
|
|
|
10550
11277
|
this.exitFn = opts.exit ?? ((code) => process.exit(code));
|
|
10551
11278
|
this.reexecFn = opts.reexec ?? defaultReexec;
|
|
10552
11279
|
this.dispatcherFactory = opts.dispatcherFactory;
|
|
11280
|
+
if (opts.runnerCommand) this.runnerCommand = opts.runnerCommand;
|
|
11281
|
+
if (opts.runnerProvider) this.providerForRunners = opts.runnerProvider;
|
|
10553
11282
|
this.unrunnableRetryDelaysMs = opts.unrunnableRetryDelaysMs ?? DEFAULT_UNRUNNABLE_RETRY_DELAYS_MS;
|
|
10554
11283
|
this.latchBootBundledState();
|
|
10555
11284
|
}
|
|
@@ -10995,6 +11724,21 @@ var CompanionSupervisor = class {
|
|
|
10995
11724
|
);
|
|
10996
11725
|
}
|
|
10997
11726
|
}
|
|
11727
|
+
// CT1679: the locks every runner on this device shares, held here.
|
|
11728
|
+
runnerLocks = new DeviceLocks();
|
|
11729
|
+
// CT1679: `runners.sandbox`, checked against this host once. A host that
|
|
11730
|
+
// can't make the sandbox says so once and runs its turns unwrapped.
|
|
11731
|
+
sandboxUsable;
|
|
11732
|
+
runnerSandbox() {
|
|
11733
|
+
if (this.config.runners?.sandbox !== "os") return "none";
|
|
11734
|
+
this.sandboxUsable ??= osSandboxUsable(this.log);
|
|
11735
|
+
return this.sandboxUsable ? "os" : "none";
|
|
11736
|
+
}
|
|
11737
|
+
// CT1679: one provider for the device, built on first use.
|
|
11738
|
+
runnerProvider() {
|
|
11739
|
+
this.providerForRunners ??= createLocalProcessProvider();
|
|
11740
|
+
return this.providerForRunners;
|
|
11741
|
+
}
|
|
10998
11742
|
buildDispatcher(ctx) {
|
|
10999
11743
|
const local = localAgentConfig(this.config, {
|
|
11000
11744
|
agentId: ctx.agentId,
|
|
@@ -11045,7 +11789,17 @@ var CompanionSupervisor = class {
|
|
|
11045
11789
|
observer: this.hub.observerFor(ctx.workspaceId, ctx.agentId, ctx.workspaceSlug),
|
|
11046
11790
|
transcriptDir: transcriptsDir(),
|
|
11047
11791
|
// CT566: the shared connector-health memory the turn records into on settle.
|
|
11048
|
-
connectorHealth: this.connectorHealth
|
|
11792
|
+
connectorHealth: this.connectorHealth,
|
|
11793
|
+
...runnersEnabled(this.config) ? {
|
|
11794
|
+
runner: {
|
|
11795
|
+
provider: this.runnerProvider(),
|
|
11796
|
+
command: this.runnerCommand ?? selfRunnerCommand(),
|
|
11797
|
+
reportRunnerGone: (report) => this.reportRunnerGone(report),
|
|
11798
|
+
connectorHealth: this.connectorHealth,
|
|
11799
|
+
locks: this.runnerLocks,
|
|
11800
|
+
sandbox: this.runnerSandbox()
|
|
11801
|
+
}
|
|
11802
|
+
} : {}
|
|
11049
11803
|
});
|
|
11050
11804
|
}
|
|
11051
11805
|
async removeWorkspace(workspaceId) {
|
|
@@ -11230,6 +11984,33 @@ var CompanionSupervisor = class {
|
|
|
11230
11984
|
);
|
|
11231
11985
|
return false;
|
|
11232
11986
|
}
|
|
11987
|
+
// CT1680: a runner is gone without having settled its turn. Same bounded
|
|
11988
|
+
// retry as the unrunnable report above, on the device credential, which is
|
|
11989
|
+
// the one the launcher still holds whatever became of the runner.
|
|
11990
|
+
async reportRunnerGone(report) {
|
|
11991
|
+
if (!this.deviceApi) return;
|
|
11992
|
+
let lastErr;
|
|
11993
|
+
for (const wait of this.unrunnableRetryDelaysMs) {
|
|
11994
|
+
if (this.stopped) return;
|
|
11995
|
+
if (wait > 0) await new Promise((r) => setTimeout(r, wait));
|
|
11996
|
+
try {
|
|
11997
|
+
await this.deviceApi.reportDispatchDrop(report);
|
|
11998
|
+
return;
|
|
11999
|
+
} catch (err) {
|
|
12000
|
+
lastErr = err;
|
|
12001
|
+
}
|
|
12002
|
+
}
|
|
12003
|
+
this.log.error(
|
|
12004
|
+
{
|
|
12005
|
+
workspaceId: report.workspaceId,
|
|
12006
|
+
conversationId: report.conversationId,
|
|
12007
|
+
agentId: report.agentId,
|
|
12008
|
+
runnerId: report.runnerId,
|
|
12009
|
+
err: lastErr instanceof Error ? lastErr.message : String(lastErr)
|
|
12010
|
+
},
|
|
12011
|
+
"Couldn't tell Cabane a turn's runner stopped; its credential expires on its own."
|
|
12012
|
+
);
|
|
12013
|
+
}
|
|
11233
12014
|
// Pull-on-miss for the assignment-change race: a dispatch arrived for an agent
|
|
11234
12015
|
// we don't yet run. If it's addressed to THIS device, the assignment just
|
|
11235
12016
|
// changed under us (the poll hasn't caught it) — force a fresh assignments pull
|
|
@@ -11336,7 +12117,7 @@ var CompanionSupervisor = class {
|
|
|
11336
12117
|
);
|
|
11337
12118
|
}
|
|
11338
12119
|
const resumedTurnId = recoveredTurnId ?? (dispatchId ? turnIdForEvent(workspaceId, dispatchId) : null);
|
|
11339
|
-
const turnId = resumedTurnId ??
|
|
12120
|
+
const turnId = resumedTurnId ?? randomUUID5();
|
|
11340
12121
|
if (dispatchId) {
|
|
11341
12122
|
const liveTurnIds = rememberTurnId(workspaceId, dispatchId, turnId);
|
|
11342
12123
|
if (liveTurnIds > TURN_ID_OVERFLOW_WARN) {
|
|
@@ -11641,9 +12422,9 @@ async function waitForBoundedHeartbeat(heartbeat) {
|
|
|
11641
12422
|
}
|
|
11642
12423
|
function defaultReexec() {
|
|
11643
12424
|
clearRuntimeState();
|
|
11644
|
-
void import("child_process").then(({ spawn:
|
|
12425
|
+
void import("child_process").then(({ spawn: spawn7 }) => {
|
|
11645
12426
|
try {
|
|
11646
|
-
const child =
|
|
12427
|
+
const child = spawn7(process.execPath, process.argv.slice(1), {
|
|
11647
12428
|
stdio: "inherit",
|
|
11648
12429
|
detached: false
|
|
11649
12430
|
});
|
|
@@ -11713,22 +12494,22 @@ function handleUncaught(log, err, origin) {
|
|
|
11713
12494
|
}
|
|
11714
12495
|
|
|
11715
12496
|
// src/crash-marker.ts
|
|
11716
|
-
import { existsSync as existsSync12, mkdirSync as
|
|
11717
|
-
import { join as
|
|
12497
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync11, readFileSync as readFileSync9, rmSync as rmSync9, writeFileSync as writeFileSync8 } from "fs";
|
|
12498
|
+
import { join as join17 } from "path";
|
|
11718
12499
|
function crashMarkerPath() {
|
|
11719
|
-
return
|
|
12500
|
+
return join17(cabaneDir(), "last-error.json");
|
|
11720
12501
|
}
|
|
11721
12502
|
function recordCrash(rec) {
|
|
11722
12503
|
try {
|
|
11723
|
-
|
|
11724
|
-
|
|
12504
|
+
mkdirSync11(cabaneDir(), { recursive: true });
|
|
12505
|
+
writeFileSync8(crashMarkerPath(), JSON.stringify(rec, null, 2) + "\n");
|
|
11725
12506
|
} catch {
|
|
11726
12507
|
}
|
|
11727
12508
|
}
|
|
11728
12509
|
function clearCrash() {
|
|
11729
12510
|
try {
|
|
11730
12511
|
const path = crashMarkerPath();
|
|
11731
|
-
if (existsSync12(path))
|
|
12512
|
+
if (existsSync12(path)) rmSync9(path, { force: true });
|
|
11732
12513
|
} catch {
|
|
11733
12514
|
}
|
|
11734
12515
|
}
|
|
@@ -11771,7 +12552,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
11771
12552
|
opencodeServerUrl: cfg.opencode?.serverUrl,
|
|
11772
12553
|
codex: isCodexEnabled(cfg)
|
|
11773
12554
|
});
|
|
11774
|
-
const instanceId =
|
|
12555
|
+
const instanceId = randomUUID6();
|
|
11775
12556
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11776
12557
|
const pre = readLiveRuntimeState();
|
|
11777
12558
|
if (pre && await verifyRuntime(pre) === "stale") clearRuntimeState();
|