@cabane/companion 0.6.203 → 0.6.204
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/README.md +25 -1
- package/dist/cli.js +1544 -1730
- package/dist/launch.js +5 -23
- package/dist/pairing-config.js +29 -45
- package/dist/runtime.js +92 -111
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -321,25 +321,7 @@ var init_prepare_hook = __esm({
|
|
|
321
321
|
// Wall-clock cap for the hook. Provisioning is slow (minutes), so the
|
|
322
322
|
// default is generous; a hook that hangs past this is killed and the turn
|
|
323
323
|
// fails with a clear timeout message rather than pinning the companion.
|
|
324
|
-
timeoutMs: z.number().int().positive().optional()
|
|
325
|
-
// Re-invoke the hook when a prepared result already exists, in VALIDATE
|
|
326
|
-
// mode: the cached result is handed back as `prepared` and stdout is
|
|
327
|
-
// ignored, so the cwd cannot move. Its only power is to exit non-zero and
|
|
328
|
-
// fail the turn. The hook resolves once per (conversation, agent), so a
|
|
329
|
-
// later message asking for a different environment is otherwise silently
|
|
330
|
-
// ignored; this lets a hook refuse it out loud.
|
|
331
|
-
//
|
|
332
|
-
// Default OFF: a hook that provisions unconditionally must not start
|
|
333
|
-
// running on every turn because the companion upgraded.
|
|
334
|
-
validateCached: z.boolean().optional(),
|
|
335
|
-
// Independent operator-owned reporter. Receives sanitized failure metadata
|
|
336
|
-
// on stdin; never re-enters this hook and cannot change the turn outcome.
|
|
337
|
-
failureReporter: z.object({
|
|
338
|
-
command: z.string().min(1),
|
|
339
|
-
args: z.array(z.string()).optional(),
|
|
340
|
-
env: z.record(z.string(), z.string()).optional(),
|
|
341
|
-
timeoutMs: z.number().int().positive().max(3e4).optional()
|
|
342
|
-
}).strict().optional()
|
|
324
|
+
timeoutMs: z.number().int().positive().optional()
|
|
343
325
|
}).strict();
|
|
344
326
|
DEFAULT_TIMEOUT_MS = 10 * 6e4;
|
|
345
327
|
prepareResultSchema = z.object({
|
|
@@ -369,15 +351,7 @@ var init_prepare_hook = __esm({
|
|
|
369
351
|
CABANE_AGENT_USERNAME: input.agentUsername,
|
|
370
352
|
CABANE_DEFAULT_CWD: input.defaultCwd,
|
|
371
353
|
CABANE_RUNTIME: input.runtime ?? "",
|
|
372
|
-
|
|
373
|
-
// hook splits on `\n` unambiguously. Empty string when none.
|
|
374
|
-
CABANE_TRIGGER_ENTRY_PATHS: input.triggerEntryPaths.join("\n"),
|
|
375
|
-
CABANE_CONVERSATION_TITLE: input.title ?? "",
|
|
376
|
-
// The dispatching message's text, and — in validate mode only —
|
|
377
|
-
// the prepared result the hook is being asked to check this dispatch
|
|
378
|
-
// against. Both empty/absent on an ordinary first run.
|
|
379
|
-
CABANE_TRIGGER_MESSAGE: input.messageBody ?? "",
|
|
380
|
-
...input.prepared ? { CABANE_PREPARED: JSON.stringify(input.prepared) } : {}
|
|
354
|
+
CABANE_CONVERSATION_TITLE: input.title ?? ""
|
|
381
355
|
}
|
|
382
356
|
});
|
|
383
357
|
} catch (err) {
|
|
@@ -415,10 +389,6 @@ var init_prepare_hook = __esm({
|
|
|
415
389
|
);
|
|
416
390
|
return;
|
|
417
391
|
}
|
|
418
|
-
if (input.prepared) {
|
|
419
|
-
resolve2(input.prepared);
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
392
|
try {
|
|
423
393
|
resolve2(parsePrepareOutput(stdout));
|
|
424
394
|
} catch (err) {
|
|
@@ -487,33 +457,36 @@ function workRoot(cfg) {
|
|
|
487
457
|
if (root.startsWith("~/")) return join4(homedir5(), root.slice(2));
|
|
488
458
|
return root;
|
|
489
459
|
}
|
|
490
|
-
function
|
|
491
|
-
|
|
492
|
-
|
|
460
|
+
function isObject(value) {
|
|
461
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
462
|
+
}
|
|
463
|
+
function splitRetired(raw) {
|
|
493
464
|
const keys = [];
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
465
|
+
const without = (obj, names, path) => {
|
|
466
|
+
const out = {};
|
|
467
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
468
|
+
if (names.includes(k)) keys.push(`${path}${k}`);
|
|
469
|
+
else out[k] = v;
|
|
470
|
+
}
|
|
471
|
+
return out;
|
|
472
|
+
};
|
|
473
|
+
const hook = (row3, path) => isObject(row3.prepareHook) ? { ...row3, prepareHook: without(row3.prepareHook, RETIRED_HOOK_KEYS, `${path}prepareHook.`) } : row3;
|
|
474
|
+
const kept = hook(without(raw, ["runners"], ""), "");
|
|
475
|
+
if (isObject(kept.agents)) {
|
|
476
|
+
const agents = {};
|
|
477
|
+
for (const [key, row3] of Object.entries(kept.agents)) {
|
|
478
|
+
const path = `agents.${key}.`;
|
|
479
|
+
agents[key] = isObject(row3) ? hook(without(row3, RETIRED_ROW_KEYS, path), path) : row3;
|
|
499
480
|
}
|
|
481
|
+
kept.agents = agents;
|
|
500
482
|
}
|
|
501
|
-
return keys;
|
|
483
|
+
return { keys, kept };
|
|
484
|
+
}
|
|
485
|
+
function retiredConfigKeys(raw) {
|
|
486
|
+
return isObject(raw) ? splitRetired(raw).keys : [];
|
|
502
487
|
}
|
|
503
488
|
function withoutRetiredKeys(raw) {
|
|
504
|
-
|
|
505
|
-
const { runners: _runners, ...rest } = raw;
|
|
506
|
-
if (!rest.agents || typeof rest.agents !== "object") return rest;
|
|
507
|
-
const agents = {};
|
|
508
|
-
for (const [key, row3] of Object.entries(rest.agents)) {
|
|
509
|
-
if (row3 && typeof row3 === "object" && "sandbox" in row3) {
|
|
510
|
-
const { sandbox: _sandbox, ...kept } = row3;
|
|
511
|
-
agents[key] = kept;
|
|
512
|
-
} else {
|
|
513
|
-
agents[key] = row3;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
return { ...rest, agents };
|
|
489
|
+
return isObject(raw) ? splitRetired(raw).kept : raw;
|
|
517
490
|
}
|
|
518
491
|
function retiredConfigKeysOnDisk() {
|
|
519
492
|
try {
|
|
@@ -708,7 +681,7 @@ function deleteConfig() {
|
|
|
708
681
|
writeFileSync(path, "", { mode: 384 });
|
|
709
682
|
}
|
|
710
683
|
}
|
|
711
|
-
var HARNESS_RUNTIMES, realHomeCache, localAgentConfigSchema, agentsConfigSchema, companionConfigSchema, legacyOfferSchema;
|
|
684
|
+
var HARNESS_RUNTIMES, realHomeCache, localAgentConfigSchema, agentsConfigSchema, companionConfigSchema, RETIRED_ROW_KEYS, RETIRED_HOOK_KEYS, legacyOfferSchema;
|
|
712
685
|
var init_config = __esm({
|
|
713
686
|
"src/config.ts"() {
|
|
714
687
|
"use strict";
|
|
@@ -753,8 +726,7 @@ var init_config = __esm({
|
|
|
753
726
|
// The DEVICE-LEVEL prepare hook, for every agent assigned here unless its own
|
|
754
727
|
// `agents` row names one. Per-agent hooks alone make the config a second
|
|
755
728
|
// silent roster: an agent nobody listed gets no hook and no error, and starts
|
|
756
|
-
// in the operator's `$HOME
|
|
757
|
-
// 2026-08-17).
|
|
729
|
+
// in the operator's `$HOME`.
|
|
758
730
|
prepareHook: prepareHookSchema.optional(),
|
|
759
731
|
// `dashboardPort` and `autoOpen` are IGNORED by the CLI (no port, no browser)
|
|
760
732
|
// but stay in the schema so an existing file still parses; the Electron shell
|
|
@@ -791,13 +763,16 @@ var init_config = __esm({
|
|
|
791
763
|
root: z2.string().min(1).refine(isRootPath, { message: "must be an absolute path, or start with ~/" }).optional(),
|
|
792
764
|
// An argv prefix the operator puts in front of every turn's runner: the companion starts
|
|
793
765
|
// `<prefix…> <its own run-turn command…>` instead of the runner bare. What the prefix does
|
|
794
|
-
// is the operator's (a
|
|
766
|
+
// is the operator's (a container or VM per turn, say); the companion knows nothing of it
|
|
795
767
|
// beyond handing the runner over, with the turn's inputs in its environment and the
|
|
796
|
-
// credential and secrets on its stdin (`runner/launch.ts`, `
|
|
768
|
+
// credential and secrets on its stdin (`runner/launch.ts`, `wrapped`). The README lists
|
|
769
|
+
// what a wrapper can rely on.
|
|
797
770
|
runner: z2.object({
|
|
798
771
|
command: z2.array(z2.string().min(1)).min(1)
|
|
799
772
|
}).strict().optional()
|
|
800
773
|
});
|
|
774
|
+
RETIRED_ROW_KEYS = ["sandbox"];
|
|
775
|
+
RETIRED_HOOK_KEYS = ["validateCached", "failureReporter"];
|
|
801
776
|
legacyOfferSchema = z2.object({
|
|
802
777
|
claudeCode: z2.object({ enabled: z2.boolean().optional() }).passthrough().optional(),
|
|
803
778
|
codex: z2.object({ enabled: z2.boolean().optional() }).passthrough().optional(),
|
|
@@ -6255,7 +6230,7 @@ function createCodexAdapter(deps = {}) {
|
|
|
6255
6230
|
if (signal.aborted) return;
|
|
6256
6231
|
if (compactions === null) {
|
|
6257
6232
|
deps.onWarn?.(
|
|
6258
|
-
"codex adapter: the thread's compaction count could not be read \u2014 re-sending the standing instructions
|
|
6233
|
+
"codex adapter: the thread's compaction count could not be read \u2014 re-sending the standing instructions",
|
|
6259
6234
|
{ threadId: decision.resume }
|
|
6260
6235
|
);
|
|
6261
6236
|
} else if (decision.compactions === compactions) {
|
|
@@ -6265,7 +6240,7 @@ function createCodexAdapter(deps = {}) {
|
|
|
6265
6240
|
};
|
|
6266
6241
|
} else if (decision.compactions !== null && compactions > decision.compactions) {
|
|
6267
6242
|
deps.onWarn?.(
|
|
6268
|
-
"codex adapter: the thread compacted since the last turn \u2014 re-sending the standing instructions
|
|
6243
|
+
"codex adapter: the thread compacted since the last turn \u2014 re-sending the standing instructions",
|
|
6269
6244
|
{
|
|
6270
6245
|
threadId: decision.resume,
|
|
6271
6246
|
compactions,
|
|
@@ -7866,11 +7841,10 @@ function required(env, name) {
|
|
|
7866
7841
|
function runnerPlace(env) {
|
|
7867
7842
|
const workRoot2 = env[PLACE_ENV.workRoot];
|
|
7868
7843
|
const transcriptDir = env[PLACE_ENV.transcriptDir];
|
|
7869
|
-
const box = env[PLACE_ENV.box];
|
|
7870
7844
|
return {
|
|
7871
7845
|
...workRoot2 ? { workRoot: workRoot2 } : {},
|
|
7872
7846
|
...transcriptDir ? { transcriptDir } : {},
|
|
7873
|
-
...
|
|
7847
|
+
...env[PLACE_ENV.dedicatedRoot] === "1" ? { dedicatedRoot: true } : {}
|
|
7874
7848
|
};
|
|
7875
7849
|
}
|
|
7876
7850
|
function parseRunnerInput(env, stdin) {
|
|
@@ -7955,7 +7929,7 @@ var init_protocol = __esm({
|
|
|
7955
7929
|
PLACE_ENV = {
|
|
7956
7930
|
workRoot: "CABANE_WORK_ROOT",
|
|
7957
7931
|
transcriptDir: "CABANE_TRANSCRIPT_DIR",
|
|
7958
|
-
|
|
7932
|
+
dedicatedRoot: "CABANE_WORK_ROOT_DEDICATED"
|
|
7959
7933
|
};
|
|
7960
7934
|
runtimeHealthSchema = z14.object({
|
|
7961
7935
|
quotaState: z14.enum(["ok", "limited", "auth_expired", "unknown"]),
|
|
@@ -8013,1258 +7987,780 @@ var init_protocol = __esm({
|
|
|
8013
7987
|
}
|
|
8014
7988
|
});
|
|
8015
7989
|
|
|
8016
|
-
// src/
|
|
8017
|
-
import {
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
return
|
|
7990
|
+
// src/grants.ts
|
|
7991
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
|
|
7992
|
+
import { join as join14 } from "path";
|
|
7993
|
+
import { z as z15 } from "zod";
|
|
7994
|
+
function allowedGrantVariable(name) {
|
|
7995
|
+
return name.length <= 100 && VARIABLE_NAME_RE.test(name) && !name.startsWith("CABANE_");
|
|
8022
7996
|
}
|
|
8023
|
-
function
|
|
8024
|
-
return
|
|
7997
|
+
function environmentSafe(value) {
|
|
7998
|
+
return !value.includes("\0");
|
|
8025
7999
|
}
|
|
8026
|
-
function
|
|
8027
|
-
|
|
8028
|
-
for (const [k, v] of Object.entries(base)) {
|
|
8029
|
-
if (v === void 0) continue;
|
|
8030
|
-
if (boxed && BOXED_ENV_DROPPED.includes(k)) continue;
|
|
8031
|
-
env[k] = v;
|
|
8032
|
-
}
|
|
8033
|
-
return env;
|
|
8000
|
+
function grantsPath() {
|
|
8001
|
+
return join14(cabaneDir(), "grants.json");
|
|
8034
8002
|
}
|
|
8035
|
-
function
|
|
8003
|
+
function readGrants(keys, report) {
|
|
8004
|
+
if (keys.length === 0) return {};
|
|
8005
|
+
const file = loadGrantsFile(report);
|
|
8036
8006
|
const out = {};
|
|
8037
|
-
for (const
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8007
|
+
for (const key of keys) {
|
|
8008
|
+
if (!allowedGrantVariable(key) || !Object.hasOwn(file, key)) continue;
|
|
8009
|
+
const entry = {};
|
|
8010
|
+
for (const [variable, value] of Object.entries(file[key])) {
|
|
8011
|
+
if (allowedGrantVariable(variable) && environmentSafe(value)) entry[variable] = value;
|
|
8012
|
+
else report.refusedVariable(key, variable);
|
|
8013
|
+
}
|
|
8014
|
+
out[key] = entry;
|
|
8043
8015
|
}
|
|
8044
8016
|
return out;
|
|
8045
8017
|
}
|
|
8046
|
-
function
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
bootId,
|
|
8051
|
-
workspaceId: opts.workspaceId,
|
|
8052
|
-
workspaceSlug: opts.workspaceSlug,
|
|
8053
|
-
agentId: opts.agentId,
|
|
8054
|
-
agentUsername: opts.agentUsername,
|
|
8055
|
-
agentName: String(opts.log.bindings().agentName ?? opts.agentUsername),
|
|
8056
|
-
local: opts.local,
|
|
8057
|
-
runConfig: opts.runConfig,
|
|
8058
|
-
workRoot: opts.workRoot,
|
|
8059
|
-
...opts.opencodeServerUrl ? { opencodeServerUrl: opts.opencodeServerUrl } : {},
|
|
8060
|
-
// Read now, per turn, the way adapter selection reads them.
|
|
8061
|
-
claudeCodeAvailable: opts.claudeCodeAvailable?.() ?? true,
|
|
8062
|
-
codexAvailable: opts.codexAvailable?.() ?? false,
|
|
8063
|
-
...opts.idleTimeoutMs !== void 0 ? { idleTimeoutMs: opts.idleTimeoutMs } : {},
|
|
8064
|
-
...opts.totalTimeoutMs !== void 0 ? { totalTimeoutMs: opts.totalTimeoutMs } : {},
|
|
8065
|
-
...opts.leaseRenewalMs !== void 0 ? { leaseRenewalMs: opts.leaseRenewalMs } : {},
|
|
8066
|
-
...opts.preparingRowDelayMs !== void 0 ? { preparingRowDelayMs: opts.preparingRowDelayMs } : {},
|
|
8067
|
-
transcripts: opts.transcriptDir !== void 0,
|
|
8068
|
-
logLevel: opts.log.level,
|
|
8069
|
-
connectorHealth: healthSnapshot(launch.connectorHealth),
|
|
8070
|
-
sessionWriteNoticeClaimed: duties.sessionWriteNoticeClaimed()
|
|
8071
|
-
};
|
|
8072
|
-
}
|
|
8073
|
-
function relayRunnerLog(line, log2) {
|
|
8074
|
-
if (!line.trim()) return;
|
|
8075
|
-
let record = null;
|
|
8018
|
+
function loadGrantsFile(report) {
|
|
8019
|
+
const path = grantsPath();
|
|
8020
|
+
if (!existsSync9(path)) return {};
|
|
8021
|
+
let raw;
|
|
8076
8022
|
try {
|
|
8077
|
-
|
|
8078
|
-
|
|
8023
|
+
raw = readFileSync7(path, "utf8");
|
|
8024
|
+
} catch (err) {
|
|
8025
|
+
const code = err.code ?? "unknown error";
|
|
8026
|
+
report.unreadable(`couldn't read ${path}: ${code}`);
|
|
8027
|
+
return {};
|
|
8028
|
+
}
|
|
8029
|
+
if (raw.trim().length === 0) return {};
|
|
8030
|
+
let parsed;
|
|
8031
|
+
try {
|
|
8032
|
+
parsed = JSON.parse(raw);
|
|
8079
8033
|
} catch {
|
|
8080
|
-
|
|
8034
|
+
report.unreadable(`${path} is not valid JSON.`);
|
|
8035
|
+
return {};
|
|
8081
8036
|
}
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8037
|
+
const result = grantsFileSchema.safeParse(parsed);
|
|
8038
|
+
if (!result.success) {
|
|
8039
|
+
report.unreadable(
|
|
8040
|
+
`${path} must map a secret's variable name to an object of "VARIABLE": "value" pairs (e.g. { "HAS_STAGING_DB": { "DATABASE_URL": "postgres://\u2026" } }).`
|
|
8041
|
+
);
|
|
8042
|
+
return {};
|
|
8085
8043
|
}
|
|
8086
|
-
|
|
8087
|
-
level,
|
|
8088
|
-
msg,
|
|
8089
|
-
time: _time,
|
|
8090
|
-
pid: _pid,
|
|
8091
|
-
hostname: _hostname,
|
|
8092
|
-
agentName: _agentName,
|
|
8093
|
-
...fields
|
|
8094
|
-
} = record;
|
|
8095
|
-
const method = PINO_LEVELS[level] ?? "info";
|
|
8096
|
-
log2[method](fields, typeof msg === "string" ? msg : "");
|
|
8044
|
+
return result.data;
|
|
8097
8045
|
}
|
|
8098
|
-
function
|
|
8099
|
-
const
|
|
8100
|
-
|
|
8101
|
-
|
|
8046
|
+
function harnessSecretEnv(parts) {
|
|
8047
|
+
const env = {};
|
|
8048
|
+
const dropped = [];
|
|
8049
|
+
for (const [name, value] of Object.entries({
|
|
8050
|
+
...parts.deviceDefaults,
|
|
8051
|
+
...parts.granted,
|
|
8052
|
+
...parts.issued
|
|
8053
|
+
})) {
|
|
8054
|
+
if (environmentSafe(value)) env[name] = value;
|
|
8055
|
+
else dropped.push(name);
|
|
8056
|
+
}
|
|
8057
|
+
return { env, dropped };
|
|
8102
8058
|
}
|
|
8103
|
-
function
|
|
8104
|
-
|
|
8105
|
-
const
|
|
8106
|
-
|
|
8107
|
-
const
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
continue;
|
|
8059
|
+
function grantEnv(sent, issuedBySecret, issued) {
|
|
8060
|
+
const env = {};
|
|
8061
|
+
for (const [key, entry] of Object.entries(sent ?? {})) {
|
|
8062
|
+
if (!Object.hasOwn(issuedBySecret, key)) continue;
|
|
8063
|
+
for (const [variable, value] of Object.entries(entry)) {
|
|
8064
|
+
if (Object.hasOwn(issued, variable) || Object.hasOwn(env, variable)) continue;
|
|
8065
|
+
env[variable] = value;
|
|
8111
8066
|
}
|
|
8112
|
-
void (async () => {
|
|
8113
|
-
for (let attempt = 1; ; attempt++) {
|
|
8114
|
-
const stopped = await Promise.all(left.map((r) => stop2(r.serverUrl, r.sessionId))).catch(
|
|
8115
|
-
() => [false]
|
|
8116
|
-
);
|
|
8117
|
-
if (stopped.every(Boolean)) break;
|
|
8118
|
-
if (attempt === 1)
|
|
8119
|
-
log2.warn(
|
|
8120
|
-
{ key, sessions: left.map((r) => r.sessionId) },
|
|
8121
|
-
"runner: a dead runner left work on a shared server that could not be confirmed stopped; holding the server until it is"
|
|
8122
|
-
);
|
|
8123
|
-
await new Promise((resolve2) => setTimeout(resolve2, launch.reconcileRetryMs ?? 5e3));
|
|
8124
|
-
}
|
|
8125
|
-
locks.release(owner);
|
|
8126
|
-
})();
|
|
8127
8067
|
}
|
|
8068
|
+
return env;
|
|
8128
8069
|
}
|
|
8129
|
-
|
|
8130
|
-
|
|
8131
|
-
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
if (
|
|
8149
|
-
|
|
8150
|
-
log2.warn(apiErrorLogFields(err), "runner: couldn't get a credential for the turn's runner");
|
|
8151
|
-
const reason = `fetch_failed: ${describeError(err)}`;
|
|
8152
|
-
return { ok: false, reason, errorReason: reason };
|
|
8070
|
+
var VARIABLE_NAME_RE, grantsFileSchema;
|
|
8071
|
+
var init_grants = __esm({
|
|
8072
|
+
"src/grants.ts"() {
|
|
8073
|
+
"use strict";
|
|
8074
|
+
init_config();
|
|
8075
|
+
VARIABLE_NAME_RE = /^[A-Z_][A-Z0-9_]*$/;
|
|
8076
|
+
grantsFileSchema = z15.record(z15.string(), z15.record(z15.string(), z15.string()));
|
|
8077
|
+
}
|
|
8078
|
+
});
|
|
8079
|
+
|
|
8080
|
+
// src/work-dir.ts
|
|
8081
|
+
import { statSync as statSync3 } from "fs";
|
|
8082
|
+
import { join as join15 } from "path";
|
|
8083
|
+
function pairDirectory(root, pair) {
|
|
8084
|
+
return join15(root, pair.workspaceSlug, pair.agentUsername, pair.conversationId);
|
|
8085
|
+
}
|
|
8086
|
+
function chooseStandingDirectory(named, pairDir, isDirectory = directoryExists) {
|
|
8087
|
+
const skipped = [];
|
|
8088
|
+
for (const candidate of named) {
|
|
8089
|
+
if (isDirectory(candidate.path)) return { cwd: candidate.path, skipped };
|
|
8090
|
+
skipped.push(candidate);
|
|
8153
8091
|
}
|
|
8092
|
+
return { cwd: pairDir, skipped };
|
|
8154
8093
|
}
|
|
8155
|
-
|
|
8094
|
+
function directoryExists(path) {
|
|
8156
8095
|
try {
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
|
|
8160
|
-
outcome: "failed",
|
|
8161
|
-
...errorReason ? { errorReason: errorReason.slice(0, ERROR_REASON_MAX) } : {},
|
|
8162
|
-
readThrough: payload.messageId
|
|
8163
|
-
});
|
|
8164
|
-
} catch (err) {
|
|
8165
|
-
log2.warn(
|
|
8166
|
-
{ ...apiErrorLogFields(err), turnId },
|
|
8167
|
-
"Couldn't update the turn's status in Cabane. Check the conversation before trying again."
|
|
8168
|
-
);
|
|
8096
|
+
return statSync3(path).isDirectory();
|
|
8097
|
+
} catch {
|
|
8098
|
+
return false;
|
|
8169
8099
|
}
|
|
8170
8100
|
}
|
|
8171
|
-
|
|
8172
|
-
|
|
8173
|
-
|
|
8174
|
-
const oomKilled = (id) => (launch.oomKilled ?? turnScopeOomKilled)(id).catch(() => false);
|
|
8175
|
-
const log2 = opts.log.child({
|
|
8176
|
-
workspaceId: opts.workspaceId,
|
|
8177
|
-
conversationId: payload.conversationId,
|
|
8178
|
-
agentId: payload.agentId,
|
|
8179
|
-
messageId: payload.messageId
|
|
8180
|
-
});
|
|
8181
|
-
const controller = new AbortController();
|
|
8182
|
-
duties.registerAbort(turnId, controller);
|
|
8183
|
-
const stopRunner = new AbortController();
|
|
8184
|
-
let ready = false;
|
|
8185
|
-
let child;
|
|
8186
|
-
let stopAnnounced = false;
|
|
8187
|
-
const announceStop = () => {
|
|
8188
|
-
if (stopAnnounced || !child?.stdin.writable) return;
|
|
8189
|
-
stopAnnounced = true;
|
|
8190
|
-
child.stdin.write(encodeLauncherMessage({ cabaneLauncher: "stop" }));
|
|
8191
|
-
};
|
|
8192
|
-
const forwardStop = () => {
|
|
8193
|
-
announceStop();
|
|
8194
|
-
if (ready) stopRunner.abort();
|
|
8195
|
-
};
|
|
8196
|
-
controller.signal.addEventListener("abort", forwardStop, { once: true });
|
|
8197
|
-
const attempt = {
|
|
8198
|
-
workspaceId: opts.workspaceId,
|
|
8199
|
-
conversationId: payload.conversationId,
|
|
8200
|
-
agentId: payload.agentId,
|
|
8201
|
-
turnId
|
|
8202
|
-
};
|
|
8203
|
-
const minted = await mintForRunner(opts, payload, handleOpts, controller.signal, log2);
|
|
8204
|
-
if (!minted.ok) {
|
|
8205
|
-
controller.signal.removeEventListener("abort", forwardStop);
|
|
8206
|
-
duties.releaseAbort(turnId, controller);
|
|
8207
|
-
const durationMs2 = Date.now() - startedAt;
|
|
8208
|
-
await settleUnstarted(opts, payload, turnId, minted.errorReason, log2);
|
|
8209
|
-
duties.notifyEnd({ id: payload.messageId, ok: false, durationMs: durationMs2, reason: minted.reason });
|
|
8210
|
-
return { ok: false, durationMs: durationMs2, reason: minted.reason };
|
|
8101
|
+
var init_work_dir = __esm({
|
|
8102
|
+
"src/work-dir.ts"() {
|
|
8103
|
+
"use strict";
|
|
8211
8104
|
}
|
|
8105
|
+
});
|
|
8106
|
+
|
|
8107
|
+
// src/turn-control-tools.ts
|
|
8108
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
8109
|
+
import { z as z16 } from "zod";
|
|
8110
|
+
function createSkipState() {
|
|
8111
|
+
return { skipped: false, reason: null };
|
|
8112
|
+
}
|
|
8113
|
+
function toolError(text) {
|
|
8114
|
+
return { isError: true, content: [{ type: "text", text }] };
|
|
8115
|
+
}
|
|
8116
|
+
async function performAct(acts, act) {
|
|
8212
8117
|
let result;
|
|
8213
|
-
let thrown;
|
|
8214
|
-
let code;
|
|
8215
|
-
let handle;
|
|
8216
|
-
const groups = [];
|
|
8217
|
-
const remote = [];
|
|
8218
|
-
const lockOwner = (id) => `${turnId}#${id}`;
|
|
8219
|
-
const locks = launch.locks;
|
|
8220
|
-
try {
|
|
8221
|
-
handle = await launch.provider.acquire(attempt);
|
|
8222
|
-
} catch (err) {
|
|
8223
|
-
const reason2 = `runner_unavailable: ${describeError(err)}`;
|
|
8224
|
-
log2.warn(
|
|
8225
|
-
{ turnId, provider: launch.provider.name, err: describeError(err) },
|
|
8226
|
-
"runner: no place to run the turn"
|
|
8227
|
-
);
|
|
8228
|
-
controller.signal.removeEventListener("abort", forwardStop);
|
|
8229
|
-
duties.releaseAbort(turnId, controller);
|
|
8230
|
-
await settleUnstarted(opts, payload, turnId, reason2, log2);
|
|
8231
|
-
await launch.reportRunnerGone?.({
|
|
8232
|
-
workspaceId: opts.workspaceId,
|
|
8233
|
-
conversationId: payload.conversationId,
|
|
8234
|
-
agentId: payload.agentId,
|
|
8235
|
-
messageId: payload.messageId,
|
|
8236
|
-
runnerId: minted.runnerId,
|
|
8237
|
-
reason: "provider_failed"
|
|
8238
|
-
});
|
|
8239
|
-
const durationMs2 = Date.now() - startedAt;
|
|
8240
|
-
duties.notifyEnd({ id: payload.messageId, ok: false, durationMs: durationMs2, reason: reason2 });
|
|
8241
|
-
return { ok: false, durationMs: durationMs2, reason: reason2 };
|
|
8242
|
-
}
|
|
8243
8118
|
try {
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
credential: minted.credential,
|
|
8250
|
-
...boxed ? { secrets: launch.secrets?.() ?? {} } : {},
|
|
8251
|
-
...Object.keys(grants).length > 0 ? { grants } : {},
|
|
8252
|
-
dispatch: payload,
|
|
8253
|
-
turnId,
|
|
8254
|
-
resumed: handleOpts.resumed,
|
|
8255
|
-
spec: runnerSpec(opts, launch, duties, minted.runnerId)
|
|
8256
|
-
};
|
|
8257
|
-
Object.assign(env, runnerEnv(input));
|
|
8258
|
-
const running = launch.provider.exec(handle, launch.command, env, {
|
|
8259
|
-
signal: stopRunner.signal,
|
|
8260
|
-
stdin: runnerStdin(input)
|
|
8261
|
-
});
|
|
8262
|
-
child = running;
|
|
8263
|
-
if (controller.signal.aborted) announceStop();
|
|
8264
|
-
log2.debug(
|
|
8265
|
-
{ turnId, runnerPid: running.pid, provider: launch.provider.name },
|
|
8266
|
-
"runner: started"
|
|
8119
|
+
result = await acts.api.turnAct(
|
|
8120
|
+
acts.workspaceId,
|
|
8121
|
+
acts.turnId,
|
|
8122
|
+
act,
|
|
8123
|
+
AbortSignal.timeout(TURN_ACT_TIMEOUT_MS)
|
|
8267
8124
|
);
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
|
|
8125
|
+
} catch (err) {
|
|
8126
|
+
if (err instanceof ApiError && err.status >= 500) {
|
|
8127
|
+
return uncertainOutcome(`${err.status}: ${apiErrorMessage(err)}`);
|
|
8128
|
+
}
|
|
8129
|
+
if (err instanceof ApiError) {
|
|
8130
|
+
if (err.status === 409 && apiErrorCode(err) === "no_active_turn") {
|
|
8131
|
+
return toolError(
|
|
8132
|
+
"No active turn: this turn has already settled, so the act was not performed."
|
|
8133
|
+
);
|
|
8273
8134
|
}
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
|
|
8292
|
-
|
|
8293
|
-
|
|
8294
|
-
|
|
8295
|
-
|
|
8296
|
-
|
|
8297
|
-
|
|
8298
|
-
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
|
|
8310
|
-
|
|
8135
|
+
return toolError(`The act was refused (${err.status}): ${apiErrorMessage(err)}`);
|
|
8136
|
+
}
|
|
8137
|
+
return uncertainOutcome(err instanceof Error ? err.message : String(err));
|
|
8138
|
+
}
|
|
8139
|
+
if (!result.ok) return toolError(JSON.stringify(result));
|
|
8140
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
8141
|
+
}
|
|
8142
|
+
function uncertainOutcome(cause) {
|
|
8143
|
+
return toolError(
|
|
8144
|
+
`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.`
|
|
8145
|
+
);
|
|
8146
|
+
}
|
|
8147
|
+
function createTurnControlMcpServer(acts, skipState) {
|
|
8148
|
+
const ACT_TOOL = { readOnlyHint: false, destructiveHint: false, openWorldHint: false };
|
|
8149
|
+
const CANCEL_TOOL = { ...ACT_TOOL, idempotentHint: true };
|
|
8150
|
+
const RECORDER_TOOL = { readOnlyHint: true, openWorldHint: false };
|
|
8151
|
+
return createSdkMcpServer({
|
|
8152
|
+
name: COMPANION_LOCAL_MCP_SERVER,
|
|
8153
|
+
version: "0.0.0",
|
|
8154
|
+
tools: [
|
|
8155
|
+
...skipState ? [
|
|
8156
|
+
tool(
|
|
8157
|
+
SKIP_TURN_TOOL,
|
|
8158
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.skip_turn,
|
|
8159
|
+
{
|
|
8160
|
+
reason: z16.string().min(1).max(MAX_SKIP_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.skip_turn.reason)
|
|
8161
|
+
},
|
|
8162
|
+
async (args) => {
|
|
8163
|
+
skipState.skipped = true;
|
|
8164
|
+
skipState.reason = args.reason;
|
|
8165
|
+
return {
|
|
8166
|
+
content: [{ type: "text", text: JSON.stringify({ skipped: true }) }]
|
|
8167
|
+
};
|
|
8168
|
+
},
|
|
8169
|
+
{ annotations: RECORDER_TOOL, alwaysLoad: true }
|
|
8170
|
+
)
|
|
8171
|
+
] : [],
|
|
8172
|
+
tool(
|
|
8173
|
+
ASK_TOOL,
|
|
8174
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.ask,
|
|
8175
|
+
{
|
|
8176
|
+
targetUserId: z16.string().regex(ASK_TARGET_PATTERN, { error: ASK_TARGET_INVALID }).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
|
|
8177
|
+
question: z16.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.question),
|
|
8178
|
+
headline: z16.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.headline),
|
|
8179
|
+
options: z16.array(z16.string({ error: ASK_OPTION_NOT_A_STRING }).min(1).max(MAX_ASK_OPTION_BYTES)).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.options),
|
|
8180
|
+
questions: z16.array(
|
|
8181
|
+
z16.object({
|
|
8182
|
+
headline: z16.string().min(1).max(MAX_ASK_HEADLINE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.headline"]),
|
|
8183
|
+
body: z16.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.body"]),
|
|
8184
|
+
options: z16.array(
|
|
8185
|
+
z16.string({ error: ASK_OPTION_NOT_A_STRING }).min(1).max(MAX_ASK_OPTION_BYTES)
|
|
8186
|
+
).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options"])
|
|
8187
|
+
})
|
|
8188
|
+
).min(1).max(MAX_ASK_ITEMS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.questions),
|
|
8189
|
+
replaces: z16.object({
|
|
8190
|
+
askId: z16.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.askId"]),
|
|
8191
|
+
reason: z16.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.reason"])
|
|
8192
|
+
}).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.replaces)
|
|
8193
|
+
},
|
|
8194
|
+
async (args) => {
|
|
8195
|
+
const hasSingle = args.question !== void 0;
|
|
8196
|
+
const hasArray = args.questions !== void 0 && args.questions.length > 0;
|
|
8197
|
+
if (hasSingle && hasArray) {
|
|
8198
|
+
return toolError(
|
|
8199
|
+
"Provide either `question` (single) or `questions` (array), not both."
|
|
8311
8200
|
);
|
|
8312
|
-
return;
|
|
8313
8201
|
}
|
|
8314
|
-
|
|
8315
|
-
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
|
|
8322
|
-
|
|
8323
|
-
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
8328
|
-
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
cause: oom ? "out_of_memory" : null
|
|
8336
|
-
})
|
|
8337
|
-
);
|
|
8202
|
+
if (!hasSingle && !hasArray) {
|
|
8203
|
+
return toolError("Provide `question` (single) or `questions` (array).");
|
|
8204
|
+
}
|
|
8205
|
+
return performAct(acts, {
|
|
8206
|
+
kind: "ask",
|
|
8207
|
+
callId: randomUUID2(),
|
|
8208
|
+
// The swap rides the ask, so the server commits both halves
|
|
8209
|
+
// in one transaction — the person never sees neither or both.
|
|
8210
|
+
...args.replaces ? { replaces: args.replaces } : {},
|
|
8211
|
+
ask: hasArray ? {
|
|
8212
|
+
targetUserId: args.targetUserId,
|
|
8213
|
+
questions: args.questions.map((q) => ({
|
|
8214
|
+
headline: q.headline,
|
|
8215
|
+
...q.body ? { body: q.body } : {},
|
|
8216
|
+
...q.options && q.options.length > 0 ? { options: q.options } : {}
|
|
8217
|
+
}))
|
|
8218
|
+
} : {
|
|
8219
|
+
targetUserId: args.targetUserId,
|
|
8220
|
+
question: args.question,
|
|
8221
|
+
...args.headline ? { headline: args.headline } : {},
|
|
8222
|
+
...args.options && args.options.length > 0 ? { options: args.options } : {}
|
|
8338
8223
|
}
|
|
8339
8224
|
});
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
8351
|
-
|
|
8352
|
-
|
|
8353
|
-
|
|
8354
|
-
|
|
8355
|
-
|
|
8356
|
-
|
|
8357
|
-
|
|
8358
|
-
|
|
8359
|
-
|
|
8360
|
-
|
|
8361
|
-
|
|
8362
|
-
|
|
8363
|
-
|
|
8364
|
-
|
|
8365
|
-
|
|
8366
|
-
|
|
8367
|
-
|
|
8368
|
-
|
|
8369
|
-
|
|
8370
|
-
|
|
8371
|
-
|
|
8372
|
-
|
|
8373
|
-
|
|
8374
|
-
|
|
8375
|
-
|
|
8376
|
-
|
|
8377
|
-
|
|
8378
|
-
|
|
8379
|
-
|
|
8380
|
-
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
|
|
8388
|
-
|
|
8225
|
+
},
|
|
8226
|
+
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
8227
|
+
),
|
|
8228
|
+
tool(
|
|
8229
|
+
WAKE_ME_TOOL,
|
|
8230
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.wake_me,
|
|
8231
|
+
{
|
|
8232
|
+
afterSeconds: z16.number().int().positive().optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.afterSeconds),
|
|
8233
|
+
at: z16.string().datetime({ offset: true }).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.at),
|
|
8234
|
+
note: z16.string().min(1).max(MAX_WAKE_NOTE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.note)
|
|
8235
|
+
},
|
|
8236
|
+
async (args) => {
|
|
8237
|
+
const hasAfter = args.afterSeconds !== void 0;
|
|
8238
|
+
const hasAt = args.at !== void 0;
|
|
8239
|
+
if (hasAfter && hasAt) {
|
|
8240
|
+
return toolError(
|
|
8241
|
+
"Provide either `afterSeconds` (relative) or `at` (absolute), not both."
|
|
8242
|
+
);
|
|
8243
|
+
}
|
|
8244
|
+
if (!hasAfter && !hasAt) {
|
|
8245
|
+
return toolError("Provide `afterSeconds` or `at`.");
|
|
8246
|
+
}
|
|
8247
|
+
return performAct(acts, {
|
|
8248
|
+
kind: "wake",
|
|
8249
|
+
callId: randomUUID2(),
|
|
8250
|
+
wake: {
|
|
8251
|
+
...hasAfter ? { afterSeconds: args.afterSeconds } : {},
|
|
8252
|
+
...hasAt ? { at: args.at } : {},
|
|
8253
|
+
note: args.note
|
|
8254
|
+
}
|
|
8255
|
+
});
|
|
8256
|
+
},
|
|
8257
|
+
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
8258
|
+
),
|
|
8259
|
+
// The standalone withdrawal. Not annotated destructive — an ask
|
|
8260
|
+
// is a question, not data, and a harness that gated this on a live
|
|
8261
|
+
// operator would hang a turn nobody is watching (the `cancel_wake`
|
|
8262
|
+
// reasoning). Not idempotent either: a second call with the same id is
|
|
8263
|
+
// refused with `ask_not_open`, which is information, not a no-op.
|
|
8264
|
+
tool(
|
|
8265
|
+
WITHDRAW_ASK_TOOL,
|
|
8266
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.withdraw_ask,
|
|
8267
|
+
{
|
|
8268
|
+
askId: z16.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.askId),
|
|
8269
|
+
reason: z16.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.reason)
|
|
8270
|
+
},
|
|
8271
|
+
async (args) => performAct(acts, {
|
|
8272
|
+
kind: "withdraw_ask",
|
|
8273
|
+
callId: randomUUID2(),
|
|
8274
|
+
withdraw: { askId: args.askId, reason: args.reason }
|
|
8275
|
+
}),
|
|
8276
|
+
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
8277
|
+
),
|
|
8278
|
+
tool(
|
|
8279
|
+
ANSWER_TOOL2,
|
|
8280
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.answer,
|
|
8281
|
+
{},
|
|
8282
|
+
async () => performAct(acts, { kind: "answer", callId: randomUUID2() }),
|
|
8283
|
+
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
8284
|
+
),
|
|
8285
|
+
// The inverse. No arguments — it can only ever stand down the
|
|
8286
|
+
// caller's own wake in the conversation it's holding a turn in.
|
|
8287
|
+
tool(
|
|
8288
|
+
CANCEL_WAKE_TOOL,
|
|
8289
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.cancel_wake,
|
|
8290
|
+
{},
|
|
8291
|
+
async () => performAct(acts, { kind: "cancel_wake", callId: randomUUID2() }),
|
|
8292
|
+
{ annotations: CANCEL_TOOL, alwaysLoad: true }
|
|
8293
|
+
)
|
|
8294
|
+
]
|
|
8389
8295
|
});
|
|
8390
|
-
if (thrown !== void 0) throw new Error(thrown);
|
|
8391
|
-
if (stopped) log2.info({ turnId, code }, "runner: stopped before it could report");
|
|
8392
|
-
else log2.warn({ turnId, code }, "runner: exited without reporting a result");
|
|
8393
|
-
duties.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
8394
|
-
return { ok: false, durationMs, reason };
|
|
8395
8296
|
}
|
|
8396
|
-
var
|
|
8397
|
-
var
|
|
8398
|
-
"src/
|
|
8297
|
+
var COMPANION_LOCAL_MCP_SERVER, COMPANION_LOCAL_TOOL_GLOB, SKIP_TURN_TOOL, ASK_TOOL, ASK_TOOL_NAME, WITHDRAW_ASK_TOOL, WITHDRAW_ASK_TOOL_NAME, ANSWER_TOOL2, WAKE_ME_TOOL, WAKE_ME_TOOL_NAME, CANCEL_WAKE_TOOL, CANCEL_WAKE_TOOL_NAME, TURN_ACT_TIMEOUT_MS;
|
|
8298
|
+
var init_turn_control_tools = __esm({
|
|
8299
|
+
"src/turn-control-tools.ts"() {
|
|
8399
8300
|
"use strict";
|
|
8400
8301
|
init_src();
|
|
8401
|
-
|
|
8402
|
-
init_api_error_shape();
|
|
8302
|
+
init_descriptions();
|
|
8403
8303
|
init_errors();
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
20: "debug",
|
|
8419
|
-
30: "info",
|
|
8420
|
-
40: "warn",
|
|
8421
|
-
50: "error",
|
|
8422
|
-
60: "fatal"
|
|
8423
|
-
};
|
|
8304
|
+
init_api_error_shape();
|
|
8305
|
+
COMPANION_LOCAL_MCP_SERVER = TURN_CONTROL_MCP_SERVER;
|
|
8306
|
+
COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
|
|
8307
|
+
SKIP_TURN_TOOL = "skip_turn";
|
|
8308
|
+
ASK_TOOL = "ask";
|
|
8309
|
+
ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${ASK_TOOL}`;
|
|
8310
|
+
WITHDRAW_ASK_TOOL = "withdraw_ask";
|
|
8311
|
+
WITHDRAW_ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WITHDRAW_ASK_TOOL}`;
|
|
8312
|
+
ANSWER_TOOL2 = "answer";
|
|
8313
|
+
WAKE_ME_TOOL = "wake_me";
|
|
8314
|
+
WAKE_ME_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WAKE_ME_TOOL}`;
|
|
8315
|
+
CANCEL_WAKE_TOOL = "cancel_wake";
|
|
8316
|
+
CANCEL_WAKE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${CANCEL_WAKE_TOOL}`;
|
|
8317
|
+
TURN_ACT_TIMEOUT_MS = 45e3;
|
|
8424
8318
|
}
|
|
8425
8319
|
});
|
|
8426
8320
|
|
|
8427
|
-
// src/
|
|
8428
|
-
|
|
8429
|
-
|
|
8430
|
-
import { z as z15 } from "zod";
|
|
8431
|
-
function allowedGrantVariable(name) {
|
|
8432
|
-
return name.length <= 100 && VARIABLE_NAME_RE.test(name) && !name.startsWith("CABANE_");
|
|
8433
|
-
}
|
|
8434
|
-
function environmentSafe(value) {
|
|
8435
|
-
return !value.includes("\0");
|
|
8321
|
+
// src/build-options.ts
|
|
8322
|
+
function cabaneMcpUrl(baseUrl) {
|
|
8323
|
+
return `${trimSlash2(baseUrl)}/api/mcp`;
|
|
8436
8324
|
}
|
|
8437
|
-
function
|
|
8438
|
-
return
|
|
8325
|
+
function turnControlMcpUrl(baseUrl) {
|
|
8326
|
+
return `${trimSlash2(baseUrl)}/api/turn-control`;
|
|
8439
8327
|
}
|
|
8440
|
-
function
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
|
|
8328
|
+
function buildCompanionTurnRequest(params) {
|
|
8329
|
+
const { turnContext: t } = params;
|
|
8330
|
+
return {
|
|
8331
|
+
systemPrompt: t.systemPrompt,
|
|
8332
|
+
...t.standing ? { standing: t.standing } : {},
|
|
8333
|
+
prompt: t.prompt,
|
|
8334
|
+
content: t.content,
|
|
8335
|
+
config: t.config,
|
|
8336
|
+
session: t.session,
|
|
8337
|
+
cabane: {
|
|
8338
|
+
mcpUrl: cabaneMcpUrl(params.baseUrl),
|
|
8339
|
+
bearer: params.turnToken,
|
|
8340
|
+
activeConversationId: params.activeConversationId,
|
|
8341
|
+
workspaceId: params.workspaceId,
|
|
8342
|
+
turnControlUrl: turnControlMcpUrl(params.baseUrl)
|
|
8343
|
+
},
|
|
8344
|
+
local: {
|
|
8345
|
+
...params.cwd ? { cwd: params.cwd } : {},
|
|
8346
|
+
...params.env ? { env: params.env } : {},
|
|
8347
|
+
// User MCP servers (already `${PLACEHOLDER}`-resolved). Structurally the
|
|
8348
|
+
// adapter's `ResolvedMcpServers`.
|
|
8349
|
+
...Object.keys(params.mcpServers).length > 0 ? { mcpServers: params.mcpServers } : {},
|
|
8350
|
+
...params.secretEnv && Object.keys(params.secretEnv).length > 0 ? { secretEnv: params.secretEnv } : {},
|
|
8351
|
+
// The auto-memory escape hatch, when the operator set it.
|
|
8352
|
+
...params.claudeCode ? { claudeCode: params.claudeCode } : {},
|
|
8353
|
+
// Whether the person's Codex config sets its own sandbox mode.
|
|
8354
|
+
...params.codex ? { codex: params.codex } : {}
|
|
8355
|
+
},
|
|
8356
|
+
// Host-injected turn-control tools, under their own namespace.
|
|
8357
|
+
extra: {
|
|
8358
|
+
mcpServers: { [COMPANION_LOCAL_MCP_SERVER]: params.turnControlServer }
|
|
8450
8359
|
}
|
|
8451
|
-
|
|
8360
|
+
};
|
|
8361
|
+
}
|
|
8362
|
+
function trimSlash2(s) {
|
|
8363
|
+
return s.endsWith("/") ? s.slice(0, -1) : s;
|
|
8364
|
+
}
|
|
8365
|
+
var init_build_options = __esm({
|
|
8366
|
+
"src/build-options.ts"() {
|
|
8367
|
+
"use strict";
|
|
8368
|
+
init_turn_control_tools();
|
|
8452
8369
|
}
|
|
8453
|
-
|
|
8370
|
+
});
|
|
8371
|
+
|
|
8372
|
+
// src/codex-config.ts
|
|
8373
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
8374
|
+
import { homedir as homedir7 } from "os";
|
|
8375
|
+
import { join as join16 } from "path";
|
|
8376
|
+
import { parse } from "smol-toml";
|
|
8377
|
+
function codexConfigPath(env = process.env, home = homedir7()) {
|
|
8378
|
+
return join16(env.CODEX_HOME || join16(home, ".codex"), "config.toml");
|
|
8454
8379
|
}
|
|
8455
|
-
function
|
|
8456
|
-
|
|
8457
|
-
if (!existsSync9(path)) return {};
|
|
8458
|
-
let raw;
|
|
8380
|
+
async function readCodexConfigSetsSandboxMode(path = codexConfigPath()) {
|
|
8381
|
+
let text;
|
|
8459
8382
|
try {
|
|
8460
|
-
|
|
8461
|
-
} catch
|
|
8462
|
-
|
|
8463
|
-
report.unreadable(`couldn't read ${path}: ${code}`);
|
|
8464
|
-
return {};
|
|
8383
|
+
text = await readFile2(path, "utf8");
|
|
8384
|
+
} catch {
|
|
8385
|
+
return false;
|
|
8465
8386
|
}
|
|
8466
|
-
|
|
8467
|
-
|
|
8387
|
+
return configSetsSandboxMode(text);
|
|
8388
|
+
}
|
|
8389
|
+
function configSetsSandboxMode(text) {
|
|
8468
8390
|
try {
|
|
8469
|
-
|
|
8391
|
+
return parse(text).sandbox_mode !== void 0;
|
|
8470
8392
|
} catch {
|
|
8471
|
-
|
|
8472
|
-
return {};
|
|
8473
|
-
}
|
|
8474
|
-
const result = grantsFileSchema.safeParse(parsed);
|
|
8475
|
-
if (!result.success) {
|
|
8476
|
-
report.unreadable(
|
|
8477
|
-
`${path} must map a secret's variable name to an object of "VARIABLE": "value" pairs (e.g. { "HAS_PROD_ACCESS": { "RENDER_API_KEY": "rnd_\u2026" } }).`
|
|
8478
|
-
);
|
|
8479
|
-
return {};
|
|
8393
|
+
return false;
|
|
8480
8394
|
}
|
|
8481
|
-
return result.data;
|
|
8482
8395
|
}
|
|
8483
|
-
|
|
8484
|
-
|
|
8485
|
-
|
|
8486
|
-
for (const [name, value] of Object.entries({
|
|
8487
|
-
...parts.deviceDefaults,
|
|
8488
|
-
...parts.granted,
|
|
8489
|
-
...parts.issued
|
|
8490
|
-
})) {
|
|
8491
|
-
if (environmentSafe(value)) env[name] = value;
|
|
8492
|
-
else dropped.push(name);
|
|
8396
|
+
var init_codex_config = __esm({
|
|
8397
|
+
"src/codex-config.ts"() {
|
|
8398
|
+
"use strict";
|
|
8493
8399
|
}
|
|
8494
|
-
|
|
8400
|
+
});
|
|
8401
|
+
|
|
8402
|
+
// src/codex-images.ts
|
|
8403
|
+
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
8404
|
+
import { tmpdir } from "os";
|
|
8405
|
+
import { join as join17 } from "path";
|
|
8406
|
+
function createCodexImagePreparer(opts) {
|
|
8407
|
+
const doFetch = opts.fetch ?? fetch;
|
|
8408
|
+
const timeoutMs = opts.timeoutMs ?? FETCH_TIMEOUT_MS;
|
|
8409
|
+
return async (urls, signal) => {
|
|
8410
|
+
signal.throwIfAborted();
|
|
8411
|
+
const dir2 = await mkdtemp(join17(tmpdir(), `cabane-codex-images-${opts.turnId}-`));
|
|
8412
|
+
const cleanup = async () => {
|
|
8413
|
+
try {
|
|
8414
|
+
await rm(dir2, { recursive: true, force: true });
|
|
8415
|
+
} catch (err) {
|
|
8416
|
+
opts.warn(
|
|
8417
|
+
{ dir: dir2, err: message(err) },
|
|
8418
|
+
"codex images: could not remove the scratch directory"
|
|
8419
|
+
);
|
|
8420
|
+
}
|
|
8421
|
+
};
|
|
8422
|
+
const prepareOne = async (url, index) => {
|
|
8423
|
+
try {
|
|
8424
|
+
const res = await doFetch(url, {
|
|
8425
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)])
|
|
8426
|
+
});
|
|
8427
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
8428
|
+
const type = (res.headers.get("content-type") ?? "").split(";")[0].trim().toLowerCase();
|
|
8429
|
+
const ext = EXTENSIONS[type];
|
|
8430
|
+
if (!ext) throw new Error(`not a vision image type: ${type || "none"}`);
|
|
8431
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
8432
|
+
const path = join17(dir2, `image-${index + 1}.${ext}`);
|
|
8433
|
+
await writeFile(path, bytes, { mode: 384 });
|
|
8434
|
+
return path;
|
|
8435
|
+
} catch (err) {
|
|
8436
|
+
if (!signal.aborted) {
|
|
8437
|
+
opts.warn(
|
|
8438
|
+
{ image: index + 1, of: urls.length, err: message(err) },
|
|
8439
|
+
"codex images: an image could not be prepared; the turn runs without it"
|
|
8440
|
+
);
|
|
8441
|
+
}
|
|
8442
|
+
return null;
|
|
8443
|
+
}
|
|
8444
|
+
};
|
|
8445
|
+
const paths = await Promise.all(urls.map(prepareOne));
|
|
8446
|
+
if (signal.aborted) {
|
|
8447
|
+
await cleanup();
|
|
8448
|
+
signal.throwIfAborted();
|
|
8449
|
+
}
|
|
8450
|
+
return { paths: paths.filter((p) => p !== null), cleanup };
|
|
8451
|
+
};
|
|
8495
8452
|
}
|
|
8496
|
-
function
|
|
8497
|
-
|
|
8498
|
-
|
|
8499
|
-
|
|
8500
|
-
|
|
8501
|
-
|
|
8502
|
-
|
|
8453
|
+
function message(err) {
|
|
8454
|
+
return err instanceof Error ? err.message : String(err);
|
|
8455
|
+
}
|
|
8456
|
+
var FETCH_TIMEOUT_MS, EXTENSIONS;
|
|
8457
|
+
var init_codex_images = __esm({
|
|
8458
|
+
"src/codex-images.ts"() {
|
|
8459
|
+
"use strict";
|
|
8460
|
+
FETCH_TIMEOUT_MS = 3e4;
|
|
8461
|
+
EXTENSIONS = {
|
|
8462
|
+
"image/png": "png",
|
|
8463
|
+
"image/jpeg": "jpg",
|
|
8464
|
+
"image/gif": "gif",
|
|
8465
|
+
"image/webp": "webp"
|
|
8466
|
+
};
|
|
8467
|
+
}
|
|
8468
|
+
});
|
|
8469
|
+
|
|
8470
|
+
// src/codex-compactions.ts
|
|
8471
|
+
import { createReadStream } from "fs";
|
|
8472
|
+
import { readdir } from "fs/promises";
|
|
8473
|
+
import { homedir as homedir8 } from "os";
|
|
8474
|
+
import { join as join18 } from "path";
|
|
8475
|
+
import { createInterface as createInterface2 } from "readline";
|
|
8476
|
+
function createCodexCompactionCounter(codexHome = process.env.CODEX_HOME || join18(homedir8(), ".codex")) {
|
|
8477
|
+
return async (threadId) => {
|
|
8478
|
+
try {
|
|
8479
|
+
const rollout = await findRollout(codexHome, threadId);
|
|
8480
|
+
return rollout ? await countCompactions(rollout) : null;
|
|
8481
|
+
} catch {
|
|
8482
|
+
return null;
|
|
8483
|
+
}
|
|
8484
|
+
};
|
|
8485
|
+
}
|
|
8486
|
+
async function findRollout(codexHome, threadId) {
|
|
8487
|
+
const id = UUID_V7.exec(threadId);
|
|
8488
|
+
if (!id) return null;
|
|
8489
|
+
const createdAt = parseInt(`${id[1]}${id[2]}`, 16);
|
|
8490
|
+
const suffix = `-${threadId}.jsonl`;
|
|
8491
|
+
for (const offset of [0, -DAY_MS, DAY_MS]) {
|
|
8492
|
+
const day = new Date(createdAt + offset);
|
|
8493
|
+
const dir2 = join18(
|
|
8494
|
+
codexHome,
|
|
8495
|
+
"sessions",
|
|
8496
|
+
String(day.getUTCFullYear()),
|
|
8497
|
+
String(day.getUTCMonth() + 1).padStart(2, "0"),
|
|
8498
|
+
String(day.getUTCDate()).padStart(2, "0")
|
|
8499
|
+
);
|
|
8500
|
+
let names;
|
|
8501
|
+
try {
|
|
8502
|
+
names = await readdir(dir2);
|
|
8503
|
+
} catch {
|
|
8504
|
+
continue;
|
|
8503
8505
|
}
|
|
8506
|
+
const name = names.find((n) => n.startsWith("rollout-") && n.endsWith(suffix));
|
|
8507
|
+
if (name) return join18(dir2, name);
|
|
8504
8508
|
}
|
|
8505
|
-
return
|
|
8509
|
+
return null;
|
|
8506
8510
|
}
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8511
|
+
async function countCompactions(rollout) {
|
|
8512
|
+
const lines2 = createInterface2({
|
|
8513
|
+
input: createReadStream(rollout, { encoding: "utf8" }),
|
|
8514
|
+
crlfDelay: Infinity
|
|
8515
|
+
});
|
|
8516
|
+
let count = 0;
|
|
8517
|
+
for await (const line of lines2) {
|
|
8518
|
+
if (line.slice(0, RECORD_HEAD_CHARS).includes('"type":"compacted"')) count += 1;
|
|
8519
|
+
}
|
|
8520
|
+
return count;
|
|
8521
|
+
}
|
|
8522
|
+
var DAY_MS, UUID_V7, RECORD_HEAD_CHARS;
|
|
8523
|
+
var init_codex_compactions = __esm({
|
|
8524
|
+
"src/codex-compactions.ts"() {
|
|
8510
8525
|
"use strict";
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
|
|
8526
|
+
DAY_MS = 864e5;
|
|
8527
|
+
UUID_V7 = /^([0-9a-f]{8})-([0-9a-f]{4})-7[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
8528
|
+
RECORD_HEAD_CHARS = 200;
|
|
8514
8529
|
}
|
|
8515
8530
|
});
|
|
8516
8531
|
|
|
8517
|
-
// src/
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
}
|
|
8523
|
-
function chooseStandingDirectory(named, pairDir, isDirectory = directoryExists) {
|
|
8524
|
-
const skipped = [];
|
|
8525
|
-
for (const candidate of named) {
|
|
8526
|
-
if (isDirectory(candidate.path)) return { cwd: candidate.path, skipped };
|
|
8527
|
-
skipped.push(candidate);
|
|
8532
|
+
// src/turn-seq-floor.ts
|
|
8533
|
+
function resolveSeqFloor(sources, ctx2) {
|
|
8534
|
+
const { serverFloor, outboxFloor } = sources;
|
|
8535
|
+
if (serverFloor === void 0) {
|
|
8536
|
+
throw new SeqFloorUnavailable("server sent no committed floor for a resumed turn");
|
|
8528
8537
|
}
|
|
8529
|
-
|
|
8538
|
+
const floor = Math.max(serverFloor, outboxFloor);
|
|
8539
|
+
ctx2.log.debug(
|
|
8540
|
+
{ turnId: ctx2.turnId, floor, outboxFloor, serverFloor },
|
|
8541
|
+
"companion: resumed turn \u2014 seq counter seeded above its committed high-water mark"
|
|
8542
|
+
);
|
|
8543
|
+
return floor;
|
|
8530
8544
|
}
|
|
8531
|
-
function
|
|
8545
|
+
function readOutboxFloor(read2, turnId, log2) {
|
|
8532
8546
|
try {
|
|
8533
|
-
return
|
|
8534
|
-
} catch {
|
|
8535
|
-
|
|
8547
|
+
return read2(turnId);
|
|
8548
|
+
} catch (err) {
|
|
8549
|
+
log2.debug(
|
|
8550
|
+
{ turnId, err: err instanceof Error ? err.message : String(err) },
|
|
8551
|
+
"companion: the on-disk outbox floor is unreadable; refusing to resume"
|
|
8552
|
+
);
|
|
8553
|
+
throw new SeqFloorUnavailable("outbox unreadable");
|
|
8536
8554
|
}
|
|
8537
8555
|
}
|
|
8538
|
-
var
|
|
8539
|
-
|
|
8556
|
+
var SeqFloorUnavailable;
|
|
8557
|
+
var init_turn_seq_floor = __esm({
|
|
8558
|
+
"src/turn-seq-floor.ts"() {
|
|
8540
8559
|
"use strict";
|
|
8560
|
+
SeqFloorUnavailable = class extends Error {
|
|
8561
|
+
constructor(detail) {
|
|
8562
|
+
super(`seq_floor_unavailable: ${detail}`);
|
|
8563
|
+
this.detail = detail;
|
|
8564
|
+
this.name = "SeqFloorUnavailable";
|
|
8565
|
+
}
|
|
8566
|
+
detail;
|
|
8567
|
+
};
|
|
8541
8568
|
}
|
|
8542
8569
|
});
|
|
8543
8570
|
|
|
8544
|
-
// src/
|
|
8545
|
-
import {
|
|
8546
|
-
import {
|
|
8547
|
-
function
|
|
8548
|
-
return
|
|
8571
|
+
// src/prepared.ts
|
|
8572
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync8, rmSync as rmSync5, writeFileSync as writeFileSync5, existsSync as existsSync10 } from "fs";
|
|
8573
|
+
import { join as join19 } from "path";
|
|
8574
|
+
function dirFor(workspaceId) {
|
|
8575
|
+
return join19(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
|
|
8549
8576
|
}
|
|
8550
|
-
function
|
|
8551
|
-
return
|
|
8577
|
+
function conversationDir(workspaceId, conversationId) {
|
|
8578
|
+
return join19(dirFor(workspaceId), encodeURIComponent(conversationId));
|
|
8552
8579
|
}
|
|
8553
|
-
|
|
8554
|
-
|
|
8580
|
+
function pathFor2(workspaceId, conversationId, agentId) {
|
|
8581
|
+
return join19(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
|
|
8582
|
+
}
|
|
8583
|
+
function readPrepared(workspaceId, conversationId, agentId) {
|
|
8584
|
+
const path = pathFor2(workspaceId, conversationId, agentId);
|
|
8585
|
+
if (!existsSync10(path)) return null;
|
|
8555
8586
|
try {
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
|
|
8562
|
-
} catch (err) {
|
|
8563
|
-
if (err instanceof ApiError && err.status >= 500) {
|
|
8564
|
-
return uncertainOutcome(`${err.status}: ${apiErrorMessage(err)}`);
|
|
8565
|
-
}
|
|
8566
|
-
if (err instanceof ApiError) {
|
|
8567
|
-
if (err.status === 409 && apiErrorCode(err) === "no_active_turn") {
|
|
8568
|
-
return toolError(
|
|
8569
|
-
"No active turn: this turn has already settled, so the act was not performed."
|
|
8570
|
-
);
|
|
8571
|
-
}
|
|
8572
|
-
return toolError(`The act was refused (${err.status}): ${apiErrorMessage(err)}`);
|
|
8587
|
+
const parsed = JSON.parse(readFileSync8(path, "utf8"));
|
|
8588
|
+
if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
|
|
8589
|
+
return {
|
|
8590
|
+
cwd: parsed.cwd,
|
|
8591
|
+
...parsed.env && typeof parsed.env === "object" ? { env: parsed.env } : {}
|
|
8592
|
+
};
|
|
8573
8593
|
}
|
|
8574
|
-
return
|
|
8594
|
+
return null;
|
|
8595
|
+
} catch {
|
|
8596
|
+
return null;
|
|
8575
8597
|
}
|
|
8576
|
-
if (!result.ok) return toolError(JSON.stringify(result));
|
|
8577
|
-
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
8578
|
-
}
|
|
8579
|
-
function uncertainOutcome(cause) {
|
|
8580
|
-
return toolError(
|
|
8581
|
-
`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.`
|
|
8582
|
-
);
|
|
8583
8598
|
}
|
|
8584
|
-
function
|
|
8585
|
-
|
|
8586
|
-
|
|
8587
|
-
|
|
8588
|
-
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
tools: [
|
|
8592
|
-
...skipState ? [
|
|
8593
|
-
tool(
|
|
8594
|
-
SKIP_TURN_TOOL,
|
|
8595
|
-
TURN_CONTROL_TOOL_DESCRIPTIONS.skip_turn,
|
|
8596
|
-
{
|
|
8597
|
-
reason: z16.string().min(1).max(MAX_SKIP_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.skip_turn.reason)
|
|
8598
|
-
},
|
|
8599
|
-
async (args) => {
|
|
8600
|
-
skipState.skipped = true;
|
|
8601
|
-
skipState.reason = args.reason;
|
|
8602
|
-
return {
|
|
8603
|
-
content: [{ type: "text", text: JSON.stringify({ skipped: true }) }]
|
|
8604
|
-
};
|
|
8605
|
-
},
|
|
8606
|
-
{ annotations: RECORDER_TOOL, alwaysLoad: true }
|
|
8607
|
-
)
|
|
8608
|
-
] : [],
|
|
8609
|
-
tool(
|
|
8610
|
-
ASK_TOOL,
|
|
8611
|
-
TURN_CONTROL_TOOL_DESCRIPTIONS.ask,
|
|
8612
|
-
{
|
|
8613
|
-
targetUserId: z16.string().regex(ASK_TARGET_PATTERN, { error: ASK_TARGET_INVALID }).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
|
|
8614
|
-
question: z16.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.question),
|
|
8615
|
-
headline: z16.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.headline),
|
|
8616
|
-
options: z16.array(z16.string({ error: ASK_OPTION_NOT_A_STRING }).min(1).max(MAX_ASK_OPTION_BYTES)).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.options),
|
|
8617
|
-
questions: z16.array(
|
|
8618
|
-
z16.object({
|
|
8619
|
-
headline: z16.string().min(1).max(MAX_ASK_HEADLINE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.headline"]),
|
|
8620
|
-
body: z16.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.body"]),
|
|
8621
|
-
options: z16.array(
|
|
8622
|
-
z16.string({ error: ASK_OPTION_NOT_A_STRING }).min(1).max(MAX_ASK_OPTION_BYTES)
|
|
8623
|
-
).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options"])
|
|
8624
|
-
})
|
|
8625
|
-
).min(1).max(MAX_ASK_ITEMS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.questions),
|
|
8626
|
-
replaces: z16.object({
|
|
8627
|
-
askId: z16.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.askId"]),
|
|
8628
|
-
reason: z16.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.reason"])
|
|
8629
|
-
}).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.replaces)
|
|
8630
|
-
},
|
|
8631
|
-
async (args) => {
|
|
8632
|
-
const hasSingle = args.question !== void 0;
|
|
8633
|
-
const hasArray = args.questions !== void 0 && args.questions.length > 0;
|
|
8634
|
-
if (hasSingle && hasArray) {
|
|
8635
|
-
return toolError(
|
|
8636
|
-
"Provide either `question` (single) or `questions` (array), not both."
|
|
8637
|
-
);
|
|
8638
|
-
}
|
|
8639
|
-
if (!hasSingle && !hasArray) {
|
|
8640
|
-
return toolError("Provide `question` (single) or `questions` (array).");
|
|
8641
|
-
}
|
|
8642
|
-
return performAct(acts, {
|
|
8643
|
-
kind: "ask",
|
|
8644
|
-
callId: randomUUID2(),
|
|
8645
|
-
// The swap rides the ask, so the server commits both halves
|
|
8646
|
-
// in one transaction — the person never sees neither or both.
|
|
8647
|
-
...args.replaces ? { replaces: args.replaces } : {},
|
|
8648
|
-
ask: hasArray ? {
|
|
8649
|
-
targetUserId: args.targetUserId,
|
|
8650
|
-
questions: args.questions.map((q) => ({
|
|
8651
|
-
headline: q.headline,
|
|
8652
|
-
...q.body ? { body: q.body } : {},
|
|
8653
|
-
...q.options && q.options.length > 0 ? { options: q.options } : {}
|
|
8654
|
-
}))
|
|
8655
|
-
} : {
|
|
8656
|
-
targetUserId: args.targetUserId,
|
|
8657
|
-
question: args.question,
|
|
8658
|
-
...args.headline ? { headline: args.headline } : {},
|
|
8659
|
-
...args.options && args.options.length > 0 ? { options: args.options } : {}
|
|
8660
|
-
}
|
|
8661
|
-
});
|
|
8662
|
-
},
|
|
8663
|
-
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
8664
|
-
),
|
|
8665
|
-
tool(
|
|
8666
|
-
WAKE_ME_TOOL,
|
|
8667
|
-
TURN_CONTROL_TOOL_DESCRIPTIONS.wake_me,
|
|
8668
|
-
{
|
|
8669
|
-
afterSeconds: z16.number().int().positive().optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.afterSeconds),
|
|
8670
|
-
at: z16.string().datetime({ offset: true }).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.at),
|
|
8671
|
-
note: z16.string().min(1).max(MAX_WAKE_NOTE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.note)
|
|
8672
|
-
},
|
|
8673
|
-
async (args) => {
|
|
8674
|
-
const hasAfter = args.afterSeconds !== void 0;
|
|
8675
|
-
const hasAt = args.at !== void 0;
|
|
8676
|
-
if (hasAfter && hasAt) {
|
|
8677
|
-
return toolError(
|
|
8678
|
-
"Provide either `afterSeconds` (relative) or `at` (absolute), not both."
|
|
8679
|
-
);
|
|
8680
|
-
}
|
|
8681
|
-
if (!hasAfter && !hasAt) {
|
|
8682
|
-
return toolError("Provide `afterSeconds` or `at`.");
|
|
8683
|
-
}
|
|
8684
|
-
return performAct(acts, {
|
|
8685
|
-
kind: "wake",
|
|
8686
|
-
callId: randomUUID2(),
|
|
8687
|
-
wake: {
|
|
8688
|
-
...hasAfter ? { afterSeconds: args.afterSeconds } : {},
|
|
8689
|
-
...hasAt ? { at: args.at } : {},
|
|
8690
|
-
note: args.note
|
|
8691
|
-
}
|
|
8692
|
-
});
|
|
8693
|
-
},
|
|
8694
|
-
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
8695
|
-
),
|
|
8696
|
-
// The standalone withdrawal. Not annotated destructive — an ask
|
|
8697
|
-
// is a question, not data, and a harness that gated this on a live
|
|
8698
|
-
// operator would hang a turn nobody is watching (the `cancel_wake`
|
|
8699
|
-
// reasoning). Not idempotent either: a second call with the same id is
|
|
8700
|
-
// refused with `ask_not_open`, which is information, not a no-op.
|
|
8701
|
-
tool(
|
|
8702
|
-
WITHDRAW_ASK_TOOL,
|
|
8703
|
-
TURN_CONTROL_TOOL_DESCRIPTIONS.withdraw_ask,
|
|
8704
|
-
{
|
|
8705
|
-
askId: z16.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.askId),
|
|
8706
|
-
reason: z16.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.reason)
|
|
8707
|
-
},
|
|
8708
|
-
async (args) => performAct(acts, {
|
|
8709
|
-
kind: "withdraw_ask",
|
|
8710
|
-
callId: randomUUID2(),
|
|
8711
|
-
withdraw: { askId: args.askId, reason: args.reason }
|
|
8712
|
-
}),
|
|
8713
|
-
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
8714
|
-
),
|
|
8715
|
-
tool(
|
|
8716
|
-
ANSWER_TOOL2,
|
|
8717
|
-
TURN_CONTROL_TOOL_DESCRIPTIONS.answer,
|
|
8718
|
-
{},
|
|
8719
|
-
async () => performAct(acts, { kind: "answer", callId: randomUUID2() }),
|
|
8720
|
-
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
8721
|
-
),
|
|
8722
|
-
// The inverse. No arguments — it can only ever stand down the
|
|
8723
|
-
// caller's own wake in the conversation it's holding a turn in.
|
|
8724
|
-
tool(
|
|
8725
|
-
CANCEL_WAKE_TOOL,
|
|
8726
|
-
TURN_CONTROL_TOOL_DESCRIPTIONS.cancel_wake,
|
|
8727
|
-
{},
|
|
8728
|
-
async () => performAct(acts, { kind: "cancel_wake", callId: randomUUID2() }),
|
|
8729
|
-
{ annotations: CANCEL_TOOL, alwaysLoad: true }
|
|
8730
|
-
)
|
|
8731
|
-
]
|
|
8732
|
-
});
|
|
8733
|
-
}
|
|
8734
|
-
var COMPANION_LOCAL_MCP_SERVER, COMPANION_LOCAL_TOOL_GLOB, SKIP_TURN_TOOL, ASK_TOOL, ASK_TOOL_NAME, WITHDRAW_ASK_TOOL, WITHDRAW_ASK_TOOL_NAME, ANSWER_TOOL2, WAKE_ME_TOOL, WAKE_ME_TOOL_NAME, CANCEL_WAKE_TOOL, CANCEL_WAKE_TOOL_NAME, TURN_ACT_TIMEOUT_MS;
|
|
8735
|
-
var init_turn_control_tools = __esm({
|
|
8736
|
-
"src/turn-control-tools.ts"() {
|
|
8737
|
-
"use strict";
|
|
8738
|
-
init_src();
|
|
8739
|
-
init_descriptions();
|
|
8740
|
-
init_errors();
|
|
8741
|
-
init_api_error_shape();
|
|
8742
|
-
COMPANION_LOCAL_MCP_SERVER = TURN_CONTROL_MCP_SERVER;
|
|
8743
|
-
COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
|
|
8744
|
-
SKIP_TURN_TOOL = "skip_turn";
|
|
8745
|
-
ASK_TOOL = "ask";
|
|
8746
|
-
ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${ASK_TOOL}`;
|
|
8747
|
-
WITHDRAW_ASK_TOOL = "withdraw_ask";
|
|
8748
|
-
WITHDRAW_ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WITHDRAW_ASK_TOOL}`;
|
|
8749
|
-
ANSWER_TOOL2 = "answer";
|
|
8750
|
-
WAKE_ME_TOOL = "wake_me";
|
|
8751
|
-
WAKE_ME_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WAKE_ME_TOOL}`;
|
|
8752
|
-
CANCEL_WAKE_TOOL = "cancel_wake";
|
|
8753
|
-
CANCEL_WAKE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${CANCEL_WAKE_TOOL}`;
|
|
8754
|
-
TURN_ACT_TIMEOUT_MS = 45e3;
|
|
8755
|
-
}
|
|
8756
|
-
});
|
|
8757
|
-
|
|
8758
|
-
// src/build-options.ts
|
|
8759
|
-
function cabaneMcpUrl(baseUrl) {
|
|
8760
|
-
return `${trimSlash2(baseUrl)}/api/mcp`;
|
|
8761
|
-
}
|
|
8762
|
-
function turnControlMcpUrl(baseUrl) {
|
|
8763
|
-
return `${trimSlash2(baseUrl)}/api/turn-control`;
|
|
8764
|
-
}
|
|
8765
|
-
function buildCompanionTurnRequest(params) {
|
|
8766
|
-
const { turnContext: t } = params;
|
|
8767
|
-
return {
|
|
8768
|
-
systemPrompt: t.systemPrompt,
|
|
8769
|
-
...t.standing ? { standing: t.standing } : {},
|
|
8770
|
-
prompt: t.prompt,
|
|
8771
|
-
content: t.content,
|
|
8772
|
-
config: t.config,
|
|
8773
|
-
session: t.session,
|
|
8774
|
-
cabane: {
|
|
8775
|
-
mcpUrl: cabaneMcpUrl(params.baseUrl),
|
|
8776
|
-
bearer: params.turnToken,
|
|
8777
|
-
activeConversationId: params.activeConversationId,
|
|
8778
|
-
workspaceId: params.workspaceId,
|
|
8779
|
-
turnControlUrl: turnControlMcpUrl(params.baseUrl)
|
|
8780
|
-
},
|
|
8781
|
-
local: {
|
|
8782
|
-
...params.cwd ? { cwd: params.cwd } : {},
|
|
8783
|
-
...params.env ? { env: params.env } : {},
|
|
8784
|
-
// User MCP servers (already `${PLACEHOLDER}`-resolved). Structurally the
|
|
8785
|
-
// adapter's `ResolvedMcpServers`.
|
|
8786
|
-
...Object.keys(params.mcpServers).length > 0 ? { mcpServers: params.mcpServers } : {},
|
|
8787
|
-
...params.secretEnv && Object.keys(params.secretEnv).length > 0 ? { secretEnv: params.secretEnv } : {},
|
|
8788
|
-
// The auto-memory escape hatch, when the operator set it.
|
|
8789
|
-
...params.claudeCode ? { claudeCode: params.claudeCode } : {},
|
|
8790
|
-
// Whether the person's Codex config sets its own sandbox mode.
|
|
8791
|
-
...params.codex ? { codex: params.codex } : {}
|
|
8792
|
-
},
|
|
8793
|
-
// Host-injected turn-control tools, under their own namespace.
|
|
8794
|
-
extra: {
|
|
8795
|
-
mcpServers: { [COMPANION_LOCAL_MCP_SERVER]: params.turnControlServer }
|
|
8796
|
-
}
|
|
8797
|
-
};
|
|
8599
|
+
function writePrepared(workspaceId, conversationId, agentId, result) {
|
|
8600
|
+
mkdirSync7(conversationDir(workspaceId, conversationId), { recursive: true });
|
|
8601
|
+
writeFileSync5(
|
|
8602
|
+
pathFor2(workspaceId, conversationId, agentId),
|
|
8603
|
+
JSON.stringify(result) + "\n",
|
|
8604
|
+
"utf8"
|
|
8605
|
+
);
|
|
8798
8606
|
}
|
|
8799
|
-
function
|
|
8800
|
-
|
|
8607
|
+
function clearPrepared(workspaceId, conversationId, agentId) {
|
|
8608
|
+
rmSync5(pathFor2(workspaceId, conversationId, agentId), { force: true });
|
|
8801
8609
|
}
|
|
8802
|
-
var
|
|
8803
|
-
"src/
|
|
8610
|
+
var init_prepared = __esm({
|
|
8611
|
+
"src/prepared.ts"() {
|
|
8804
8612
|
"use strict";
|
|
8805
|
-
|
|
8613
|
+
init_config();
|
|
8806
8614
|
}
|
|
8807
8615
|
});
|
|
8808
8616
|
|
|
8809
|
-
// src/
|
|
8810
|
-
import {
|
|
8811
|
-
import {
|
|
8812
|
-
import {
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
return join16(env.CODEX_HOME || join16(home, ".codex"), "config.toml");
|
|
8617
|
+
// src/secrets.ts
|
|
8618
|
+
import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
|
|
8619
|
+
import { join as join20 } from "path";
|
|
8620
|
+
import { z as z17 } from "zod";
|
|
8621
|
+
function secretsPath() {
|
|
8622
|
+
return join20(cabaneDir(), "secrets.json");
|
|
8816
8623
|
}
|
|
8817
|
-
|
|
8818
|
-
|
|
8624
|
+
function loadSecretValues() {
|
|
8625
|
+
const path = secretsPath();
|
|
8626
|
+
if (!existsSync11(path)) return {};
|
|
8627
|
+
let raw;
|
|
8819
8628
|
try {
|
|
8820
|
-
|
|
8821
|
-
} catch {
|
|
8822
|
-
|
|
8629
|
+
raw = readFileSync9(path, "utf8");
|
|
8630
|
+
} catch (err) {
|
|
8631
|
+
throw new ConfigError(
|
|
8632
|
+
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
8633
|
+
);
|
|
8823
8634
|
}
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
function configSetsSandboxMode(text) {
|
|
8635
|
+
if (raw.trim().length === 0) return {};
|
|
8636
|
+
let parsed;
|
|
8827
8637
|
try {
|
|
8828
|
-
|
|
8829
|
-
} catch {
|
|
8830
|
-
|
|
8638
|
+
parsed = JSON.parse(raw);
|
|
8639
|
+
} catch (err) {
|
|
8640
|
+
throw new ConfigError(
|
|
8641
|
+
`${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}. It must be a flat object of "NAME": "value" secret pairs.`
|
|
8642
|
+
);
|
|
8643
|
+
}
|
|
8644
|
+
const result = secretStoreSchema.safeParse(parsed);
|
|
8645
|
+
if (!result.success) {
|
|
8646
|
+
throw new ConfigError(
|
|
8647
|
+
`${path} must be a flat object mapping secret names to string values (e.g. { "GITHUB_TOKEN": "ghp_\u2026" }).`
|
|
8648
|
+
);
|
|
8831
8649
|
}
|
|
8650
|
+
return result.data;
|
|
8832
8651
|
}
|
|
8833
|
-
|
|
8834
|
-
|
|
8835
|
-
|
|
8652
|
+
function loadSecretStoreTolerant(onWarn) {
|
|
8653
|
+
return makeStore(loadSecretValuesTolerant(onWarn));
|
|
8654
|
+
}
|
|
8655
|
+
function loadSecretValuesTolerant(onWarn) {
|
|
8656
|
+
try {
|
|
8657
|
+
return loadSecretValues();
|
|
8658
|
+
} catch (err) {
|
|
8659
|
+
onWarn?.(err instanceof Error ? err.message : String(err));
|
|
8660
|
+
return {};
|
|
8836
8661
|
}
|
|
8837
|
-
}
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
|
|
8841
|
-
|
|
8842
|
-
import { join as join17 } from "path";
|
|
8843
|
-
function createCodexImagePreparer(opts) {
|
|
8844
|
-
const doFetch = opts.fetch ?? fetch;
|
|
8845
|
-
const timeoutMs = opts.timeoutMs ?? FETCH_TIMEOUT_MS;
|
|
8846
|
-
return async (urls, signal) => {
|
|
8847
|
-
signal.throwIfAborted();
|
|
8848
|
-
const dir2 = await mkdtemp(join17(tmpdir(), `cabane-codex-images-${opts.turnId}-`));
|
|
8849
|
-
const cleanup = async () => {
|
|
8850
|
-
try {
|
|
8851
|
-
await rm(dir2, { recursive: true, force: true });
|
|
8852
|
-
} catch (err) {
|
|
8853
|
-
opts.warn(
|
|
8854
|
-
{ dir: dir2, err: message(err) },
|
|
8855
|
-
"codex images: could not remove the scratch directory"
|
|
8856
|
-
);
|
|
8857
|
-
}
|
|
8858
|
-
};
|
|
8859
|
-
const prepareOne = async (url, index) => {
|
|
8860
|
-
try {
|
|
8861
|
-
const res = await doFetch(url, {
|
|
8862
|
-
signal: AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)])
|
|
8863
|
-
});
|
|
8864
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
8865
|
-
const type = (res.headers.get("content-type") ?? "").split(";")[0].trim().toLowerCase();
|
|
8866
|
-
const ext = EXTENSIONS[type];
|
|
8867
|
-
if (!ext) throw new Error(`not a vision image type: ${type || "none"}`);
|
|
8868
|
-
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
8869
|
-
const path = join17(dir2, `image-${index + 1}.${ext}`);
|
|
8870
|
-
await writeFile(path, bytes, { mode: 384 });
|
|
8871
|
-
return path;
|
|
8872
|
-
} catch (err) {
|
|
8873
|
-
if (!signal.aborted) {
|
|
8874
|
-
opts.warn(
|
|
8875
|
-
{ image: index + 1, of: urls.length, err: message(err) },
|
|
8876
|
-
"codex images: an image could not be prepared; the turn runs without it"
|
|
8877
|
-
);
|
|
8878
|
-
}
|
|
8879
|
-
return null;
|
|
8880
|
-
}
|
|
8881
|
-
};
|
|
8882
|
-
const paths = await Promise.all(urls.map(prepareOne));
|
|
8883
|
-
if (signal.aborted) {
|
|
8884
|
-
await cleanup();
|
|
8885
|
-
signal.throwIfAborted();
|
|
8886
|
-
}
|
|
8887
|
-
return { paths: paths.filter((p) => p !== null), cleanup };
|
|
8662
|
+
}
|
|
8663
|
+
function makeStore(map) {
|
|
8664
|
+
return {
|
|
8665
|
+
get: (name) => map[name],
|
|
8666
|
+
names: () => Object.keys(map).sort()
|
|
8888
8667
|
};
|
|
8889
8668
|
}
|
|
8890
|
-
function
|
|
8891
|
-
|
|
8669
|
+
function requiredSecretNames(mcpServers) {
|
|
8670
|
+
const names = /* @__PURE__ */ new Set();
|
|
8671
|
+
for (const server of Object.values(mcpServers ?? {})) {
|
|
8672
|
+
const bags = [
|
|
8673
|
+
"env" in server ? server.env : void 0,
|
|
8674
|
+
"headers" in server ? server.headers : void 0
|
|
8675
|
+
];
|
|
8676
|
+
for (const bag of bags) {
|
|
8677
|
+
for (const value of Object.values(bag ?? {})) {
|
|
8678
|
+
for (const [, name] of value.matchAll(PLACEHOLDER_RE)) names.add(name);
|
|
8679
|
+
}
|
|
8680
|
+
}
|
|
8681
|
+
}
|
|
8682
|
+
return [...names].sort();
|
|
8892
8683
|
}
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
8897
|
-
|
|
8898
|
-
|
|
8899
|
-
|
|
8900
|
-
"image/jpeg": "jpg",
|
|
8901
|
-
"image/gif": "gif",
|
|
8902
|
-
"image/webp": "webp"
|
|
8903
|
-
};
|
|
8684
|
+
function resolveMcpSecrets(mcpServers, store, serverSecretEnv = {}) {
|
|
8685
|
+
const missing = /* @__PURE__ */ new Set();
|
|
8686
|
+
const secretEnv = { ...serverSecretEnv };
|
|
8687
|
+
let counter = 0;
|
|
8688
|
+
for (const name of Object.keys(serverSecretEnv)) {
|
|
8689
|
+
const n = SECRET_ENV_NAME.exec(name);
|
|
8690
|
+
if (n) counter = Math.max(counter, Number(n[1]));
|
|
8904
8691
|
}
|
|
8905
|
-
|
|
8906
|
-
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
import { homedir as homedir8 } from "os";
|
|
8911
|
-
import { join as join18 } from "path";
|
|
8912
|
-
import { createInterface as createInterface2 } from "readline";
|
|
8913
|
-
function createCodexCompactionCounter(codexHome = process.env.CODEX_HOME || join18(homedir8(), ".codex")) {
|
|
8914
|
-
return async (threadId) => {
|
|
8915
|
-
try {
|
|
8916
|
-
const rollout = await findRollout(codexHome, threadId);
|
|
8917
|
-
return rollout ? await countCompactions(rollout) : null;
|
|
8918
|
-
} catch {
|
|
8919
|
-
return null;
|
|
8692
|
+
const interpolate = (value) => value.replace(PLACEHOLDER_RE, (_match, name) => {
|
|
8693
|
+
const resolved = store.get(name);
|
|
8694
|
+
if (resolved === void 0) {
|
|
8695
|
+
missing.add(name);
|
|
8696
|
+
return _match;
|
|
8920
8697
|
}
|
|
8698
|
+
return resolved;
|
|
8699
|
+
});
|
|
8700
|
+
const mapBag = (bag) => {
|
|
8701
|
+
const out = {};
|
|
8702
|
+
for (const [k, v] of Object.entries(bag)) out[k] = interpolate(v);
|
|
8703
|
+
return out;
|
|
8921
8704
|
};
|
|
8922
|
-
}
|
|
8923
|
-
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
continue;
|
|
8705
|
+
const servers = {};
|
|
8706
|
+
for (const [name, server] of Object.entries(mcpServers)) {
|
|
8707
|
+
const next = { ...server };
|
|
8708
|
+
if ("env" in next && next.env) next.env = mapBag(next.env);
|
|
8709
|
+
if ("url" in next && next.headers) {
|
|
8710
|
+
const headers = {};
|
|
8711
|
+
const fromEnv = { ...next.headersFromEnv };
|
|
8712
|
+
for (const [header, value] of Object.entries(next.headers)) {
|
|
8713
|
+
if (!value.match(PLACEHOLDER_RE)) {
|
|
8714
|
+
headers[header] = value;
|
|
8715
|
+
continue;
|
|
8716
|
+
}
|
|
8717
|
+
const envName = `CABANE_MCP_${++counter}`;
|
|
8718
|
+
secretEnv[envName] = interpolate(value);
|
|
8719
|
+
fromEnv[header] = envName;
|
|
8720
|
+
}
|
|
8721
|
+
next.headers = headers;
|
|
8722
|
+
if (Object.keys(headers).length === 0) delete next.headers;
|
|
8723
|
+
if (Object.keys(fromEnv).length > 0) next.headersFromEnv = fromEnv;
|
|
8942
8724
|
}
|
|
8943
|
-
|
|
8944
|
-
if (name) return join18(dir2, name);
|
|
8725
|
+
servers[name] = next;
|
|
8945
8726
|
}
|
|
8946
|
-
return
|
|
8727
|
+
return { servers, secretEnv, missing: [...missing].sort() };
|
|
8947
8728
|
}
|
|
8948
|
-
|
|
8949
|
-
const
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
if (line.slice(0, RECORD_HEAD_CHARS).includes('"type":"compacted"')) count += 1;
|
|
8729
|
+
function missingSecretReason(missing) {
|
|
8730
|
+
const prefix = "missing_secret: ";
|
|
8731
|
+
for (let shown = missing.length; shown >= 0; shown--) {
|
|
8732
|
+
const omitted = missing.length - shown;
|
|
8733
|
+
const parts = [...missing.slice(0, shown), ...omitted > 0 ? [`+${omitted}`] : []];
|
|
8734
|
+
const reason = prefix + parts.join(",");
|
|
8735
|
+
if (reason.length <= ERROR_REASON_MAX) return reason;
|
|
8956
8736
|
}
|
|
8957
|
-
return
|
|
8737
|
+
return `${prefix}+${missing.length}`;
|
|
8958
8738
|
}
|
|
8959
|
-
var
|
|
8960
|
-
var
|
|
8961
|
-
"src/
|
|
8739
|
+
var PLACEHOLDER_RE, secretStoreSchema, SECRET_ENV_NAME;
|
|
8740
|
+
var init_secrets = __esm({
|
|
8741
|
+
"src/secrets.ts"() {
|
|
8962
8742
|
"use strict";
|
|
8963
|
-
|
|
8964
|
-
|
|
8965
|
-
|
|
8743
|
+
init_src();
|
|
8744
|
+
init_config();
|
|
8745
|
+
init_errors();
|
|
8746
|
+
PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
8747
|
+
secretStoreSchema = z17.record(z17.string(), z17.string());
|
|
8748
|
+
SECRET_ENV_NAME = /^CABANE_MCP_(\d+)$/;
|
|
8966
8749
|
}
|
|
8967
8750
|
});
|
|
8968
8751
|
|
|
8969
|
-
// src/turn-
|
|
8970
|
-
function
|
|
8971
|
-
const
|
|
8972
|
-
|
|
8973
|
-
|
|
8752
|
+
// src/turn-env.ts
|
|
8753
|
+
function harnessLoginEnv(store, runtime) {
|
|
8754
|
+
const names = [
|
|
8755
|
+
...runtime in API_KEY_VARIABLES ? API_KEY_VARIABLES[runtime] : [],
|
|
8756
|
+
GITHUB_TOKEN_NAME
|
|
8757
|
+
];
|
|
8758
|
+
const env = {};
|
|
8759
|
+
for (const name of names) {
|
|
8760
|
+
const value = store.get(name);
|
|
8761
|
+
if (value) env[name] = value;
|
|
8974
8762
|
}
|
|
8975
|
-
|
|
8976
|
-
ctx2.log.debug(
|
|
8977
|
-
{ turnId: ctx2.turnId, floor, outboxFloor, serverFloor },
|
|
8978
|
-
"companion: resumed turn \u2014 seq counter seeded above its committed high-water mark"
|
|
8979
|
-
);
|
|
8980
|
-
return floor;
|
|
8981
|
-
}
|
|
8982
|
-
function readOutboxFloor(read2, turnId, log2) {
|
|
8983
|
-
try {
|
|
8984
|
-
return read2(turnId);
|
|
8985
|
-
} catch (err) {
|
|
8986
|
-
log2.debug(
|
|
8987
|
-
{ turnId, err: err instanceof Error ? err.message : String(err) },
|
|
8988
|
-
"companion: the on-disk outbox floor is unreadable; refusing to resume"
|
|
8989
|
-
);
|
|
8990
|
-
throw new SeqFloorUnavailable("outbox unreadable");
|
|
8991
|
-
}
|
|
8992
|
-
}
|
|
8993
|
-
var SeqFloorUnavailable;
|
|
8994
|
-
var init_turn_seq_floor = __esm({
|
|
8995
|
-
"src/turn-seq-floor.ts"() {
|
|
8996
|
-
"use strict";
|
|
8997
|
-
SeqFloorUnavailable = class extends Error {
|
|
8998
|
-
constructor(detail) {
|
|
8999
|
-
super(`seq_floor_unavailable: ${detail}`);
|
|
9000
|
-
this.detail = detail;
|
|
9001
|
-
this.name = "SeqFloorUnavailable";
|
|
9002
|
-
}
|
|
9003
|
-
detail;
|
|
9004
|
-
};
|
|
9005
|
-
}
|
|
9006
|
-
});
|
|
9007
|
-
|
|
9008
|
-
// src/prepare-failure.ts
|
|
9009
|
-
import { spawn as spawn4 } from "child_process";
|
|
9010
|
-
import { hostname } from "os";
|
|
9011
|
-
function prepareFailureCause(error) {
|
|
9012
|
-
const message2 = error instanceof Error ? error.message : String(error);
|
|
9013
|
-
if (/timed out/i.test(message2)) return "prepare hook timed out";
|
|
9014
|
-
if (/MODULE_NOT_FOUND|Cannot find (?:module|package)/.test(message2))
|
|
9015
|
-
return "prepare hook module missing; check the configured command and its installation for this device";
|
|
9016
|
-
if (/not valid JSON|JSON must carry|produced no output/.test(message2))
|
|
9017
|
-
return "prepare hook returned malformed output; expected a cwd path or JSON with a non-empty cwd";
|
|
9018
|
-
if (/ENOENT/.test(message2)) return "prepare hook executable missing (ENOENT)";
|
|
9019
|
-
if (/EACCES/.test(message2)) return "prepare hook executable is not accessible (EACCES)";
|
|
9020
|
-
const exit = message2.match(/exited with code (\d+)/)?.[1];
|
|
9021
|
-
return exit ? `prepare hook exited with code ${exit}` : "prepare hook failed; inspect the device-local hook log";
|
|
9022
|
-
}
|
|
9023
|
-
async function runPrepareWithReporting(runner, hook, input, context, reportError) {
|
|
9024
|
-
try {
|
|
9025
|
-
return await runner(hook, input);
|
|
9026
|
-
} catch (error) {
|
|
9027
|
-
if (hook.failureReporter) {
|
|
9028
|
-
try {
|
|
9029
|
-
await reportFailure(hook.failureReporter, {
|
|
9030
|
-
workspaceId: input.workspaceId,
|
|
9031
|
-
conversationId: input.conversationId,
|
|
9032
|
-
agentId: input.agentId,
|
|
9033
|
-
agentUsername: input.agentUsername,
|
|
9034
|
-
turnId: context.turnId,
|
|
9035
|
-
deviceId: context.deviceId ?? hostname(),
|
|
9036
|
-
cause: prepareFailureCause(error)
|
|
9037
|
-
});
|
|
9038
|
-
} catch {
|
|
9039
|
-
reportError();
|
|
9040
|
-
}
|
|
9041
|
-
}
|
|
9042
|
-
throw error;
|
|
9043
|
-
}
|
|
9044
|
-
}
|
|
9045
|
-
function reportFailure(config, body) {
|
|
9046
|
-
return new Promise((resolve2, reject) => {
|
|
9047
|
-
const child = spawn4(config.command, config.args ?? [], {
|
|
9048
|
-
stdio: ["pipe", "ignore", "ignore"],
|
|
9049
|
-
env: { ...process.env, ...config.env }
|
|
9050
|
-
});
|
|
9051
|
-
const timer = setTimeout(() => {
|
|
9052
|
-
child.kill("SIGKILL");
|
|
9053
|
-
reject(new Error("reporter timeout"));
|
|
9054
|
-
}, config.timeoutMs ?? 15e3);
|
|
9055
|
-
child.stdin.on("error", () => {
|
|
9056
|
-
});
|
|
9057
|
-
child.once("error", () => {
|
|
9058
|
-
clearTimeout(timer);
|
|
9059
|
-
reject(new Error("reporter failed to start"));
|
|
9060
|
-
});
|
|
9061
|
-
child.once("close", (code) => {
|
|
9062
|
-
clearTimeout(timer);
|
|
9063
|
-
if (code === 0) resolve2();
|
|
9064
|
-
else reject(new Error("reporter failed"));
|
|
9065
|
-
});
|
|
9066
|
-
child.stdin.end(JSON.stringify(body));
|
|
9067
|
-
});
|
|
9068
|
-
}
|
|
9069
|
-
var init_prepare_failure = __esm({
|
|
9070
|
-
"src/prepare-failure.ts"() {
|
|
9071
|
-
"use strict";
|
|
9072
|
-
}
|
|
9073
|
-
});
|
|
9074
|
-
|
|
9075
|
-
// src/prepared.ts
|
|
9076
|
-
import { mkdirSync as mkdirSync7, readFileSync as readFileSync8, rmSync as rmSync5, writeFileSync as writeFileSync5, existsSync as existsSync10 } from "fs";
|
|
9077
|
-
import { join as join19 } from "path";
|
|
9078
|
-
function dirFor(workspaceId) {
|
|
9079
|
-
return join19(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
|
|
9080
|
-
}
|
|
9081
|
-
function conversationDir(workspaceId, conversationId) {
|
|
9082
|
-
return join19(dirFor(workspaceId), encodeURIComponent(conversationId));
|
|
9083
|
-
}
|
|
9084
|
-
function pathFor2(workspaceId, conversationId, agentId) {
|
|
9085
|
-
return join19(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
|
|
9086
|
-
}
|
|
9087
|
-
function readPrepared(workspaceId, conversationId, agentId) {
|
|
9088
|
-
const path = pathFor2(workspaceId, conversationId, agentId);
|
|
9089
|
-
if (!existsSync10(path)) return null;
|
|
9090
|
-
try {
|
|
9091
|
-
const parsed = JSON.parse(readFileSync8(path, "utf8"));
|
|
9092
|
-
if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
|
|
9093
|
-
return {
|
|
9094
|
-
cwd: parsed.cwd,
|
|
9095
|
-
...parsed.env && typeof parsed.env === "object" ? { env: parsed.env } : {}
|
|
9096
|
-
};
|
|
9097
|
-
}
|
|
9098
|
-
return null;
|
|
9099
|
-
} catch {
|
|
9100
|
-
return null;
|
|
9101
|
-
}
|
|
9102
|
-
}
|
|
9103
|
-
function writePrepared(workspaceId, conversationId, agentId, result) {
|
|
9104
|
-
mkdirSync7(conversationDir(workspaceId, conversationId), { recursive: true });
|
|
9105
|
-
writeFileSync5(
|
|
9106
|
-
pathFor2(workspaceId, conversationId, agentId),
|
|
9107
|
-
JSON.stringify(result) + "\n",
|
|
9108
|
-
"utf8"
|
|
9109
|
-
);
|
|
9110
|
-
}
|
|
9111
|
-
function clearPrepared(workspaceId, conversationId, agentId) {
|
|
9112
|
-
rmSync5(pathFor2(workspaceId, conversationId, agentId), { force: true });
|
|
9113
|
-
}
|
|
9114
|
-
var init_prepared = __esm({
|
|
9115
|
-
"src/prepared.ts"() {
|
|
9116
|
-
"use strict";
|
|
9117
|
-
init_config();
|
|
9118
|
-
}
|
|
9119
|
-
});
|
|
9120
|
-
|
|
9121
|
-
// src/secrets.ts
|
|
9122
|
-
import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
|
|
9123
|
-
import { join as join20 } from "path";
|
|
9124
|
-
import { z as z17 } from "zod";
|
|
9125
|
-
function secretsPath() {
|
|
9126
|
-
return join20(cabaneDir(), "secrets.json");
|
|
9127
|
-
}
|
|
9128
|
-
function loadSecretValues() {
|
|
9129
|
-
const path = secretsPath();
|
|
9130
|
-
if (!existsSync11(path)) return {};
|
|
9131
|
-
let raw;
|
|
9132
|
-
try {
|
|
9133
|
-
raw = readFileSync9(path, "utf8");
|
|
9134
|
-
} catch (err) {
|
|
9135
|
-
throw new ConfigError(
|
|
9136
|
-
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
9137
|
-
);
|
|
9138
|
-
}
|
|
9139
|
-
if (raw.trim().length === 0) return {};
|
|
9140
|
-
let parsed;
|
|
9141
|
-
try {
|
|
9142
|
-
parsed = JSON.parse(raw);
|
|
9143
|
-
} catch (err) {
|
|
9144
|
-
throw new ConfigError(
|
|
9145
|
-
`${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}. It must be a flat object of "NAME": "value" secret pairs.`
|
|
9146
|
-
);
|
|
9147
|
-
}
|
|
9148
|
-
const result = secretStoreSchema.safeParse(parsed);
|
|
9149
|
-
if (!result.success) {
|
|
9150
|
-
throw new ConfigError(
|
|
9151
|
-
`${path} must be a flat object mapping secret names to string values (e.g. { "GITHUB_TOKEN": "ghp_\u2026" }).`
|
|
9152
|
-
);
|
|
9153
|
-
}
|
|
9154
|
-
return result.data;
|
|
9155
|
-
}
|
|
9156
|
-
function loadSecretStoreTolerant(onWarn) {
|
|
9157
|
-
return makeStore(loadSecretValuesTolerant(onWarn));
|
|
9158
|
-
}
|
|
9159
|
-
function loadSecretValuesTolerant(onWarn) {
|
|
9160
|
-
try {
|
|
9161
|
-
return loadSecretValues();
|
|
9162
|
-
} catch (err) {
|
|
9163
|
-
onWarn?.(err instanceof Error ? err.message : String(err));
|
|
9164
|
-
return {};
|
|
9165
|
-
}
|
|
9166
|
-
}
|
|
9167
|
-
function makeStore(map) {
|
|
9168
|
-
return {
|
|
9169
|
-
get: (name) => map[name],
|
|
9170
|
-
names: () => Object.keys(map).sort()
|
|
9171
|
-
};
|
|
9172
|
-
}
|
|
9173
|
-
function requiredSecretNames(mcpServers) {
|
|
9174
|
-
const names = /* @__PURE__ */ new Set();
|
|
9175
|
-
for (const server of Object.values(mcpServers ?? {})) {
|
|
9176
|
-
const bags = [
|
|
9177
|
-
"env" in server ? server.env : void 0,
|
|
9178
|
-
"headers" in server ? server.headers : void 0
|
|
9179
|
-
];
|
|
9180
|
-
for (const bag of bags) {
|
|
9181
|
-
for (const value of Object.values(bag ?? {})) {
|
|
9182
|
-
for (const [, name] of value.matchAll(PLACEHOLDER_RE)) names.add(name);
|
|
9183
|
-
}
|
|
9184
|
-
}
|
|
9185
|
-
}
|
|
9186
|
-
return [...names].sort();
|
|
9187
|
-
}
|
|
9188
|
-
function resolveMcpSecrets(mcpServers, store, serverSecretEnv = {}) {
|
|
9189
|
-
const missing = /* @__PURE__ */ new Set();
|
|
9190
|
-
const secretEnv = { ...serverSecretEnv };
|
|
9191
|
-
let counter = 0;
|
|
9192
|
-
for (const name of Object.keys(serverSecretEnv)) {
|
|
9193
|
-
const n = SECRET_ENV_NAME.exec(name);
|
|
9194
|
-
if (n) counter = Math.max(counter, Number(n[1]));
|
|
9195
|
-
}
|
|
9196
|
-
const interpolate = (value) => value.replace(PLACEHOLDER_RE, (_match, name) => {
|
|
9197
|
-
const resolved = store.get(name);
|
|
9198
|
-
if (resolved === void 0) {
|
|
9199
|
-
missing.add(name);
|
|
9200
|
-
return _match;
|
|
9201
|
-
}
|
|
9202
|
-
return resolved;
|
|
9203
|
-
});
|
|
9204
|
-
const mapBag = (bag) => {
|
|
9205
|
-
const out = {};
|
|
9206
|
-
for (const [k, v] of Object.entries(bag)) out[k] = interpolate(v);
|
|
9207
|
-
return out;
|
|
9208
|
-
};
|
|
9209
|
-
const servers = {};
|
|
9210
|
-
for (const [name, server] of Object.entries(mcpServers)) {
|
|
9211
|
-
const next = { ...server };
|
|
9212
|
-
if ("env" in next && next.env) next.env = mapBag(next.env);
|
|
9213
|
-
if ("url" in next && next.headers) {
|
|
9214
|
-
const headers = {};
|
|
9215
|
-
const fromEnv = { ...next.headersFromEnv };
|
|
9216
|
-
for (const [header, value] of Object.entries(next.headers)) {
|
|
9217
|
-
if (!value.match(PLACEHOLDER_RE)) {
|
|
9218
|
-
headers[header] = value;
|
|
9219
|
-
continue;
|
|
9220
|
-
}
|
|
9221
|
-
const envName = `CABANE_MCP_${++counter}`;
|
|
9222
|
-
secretEnv[envName] = interpolate(value);
|
|
9223
|
-
fromEnv[header] = envName;
|
|
9224
|
-
}
|
|
9225
|
-
next.headers = headers;
|
|
9226
|
-
if (Object.keys(headers).length === 0) delete next.headers;
|
|
9227
|
-
if (Object.keys(fromEnv).length > 0) next.headersFromEnv = fromEnv;
|
|
9228
|
-
}
|
|
9229
|
-
servers[name] = next;
|
|
9230
|
-
}
|
|
9231
|
-
return { servers, secretEnv, missing: [...missing].sort() };
|
|
9232
|
-
}
|
|
9233
|
-
function missingSecretReason(missing) {
|
|
9234
|
-
const prefix = "missing_secret: ";
|
|
9235
|
-
for (let shown = missing.length; shown >= 0; shown--) {
|
|
9236
|
-
const omitted = missing.length - shown;
|
|
9237
|
-
const parts = [...missing.slice(0, shown), ...omitted > 0 ? [`+${omitted}`] : []];
|
|
9238
|
-
const reason = prefix + parts.join(",");
|
|
9239
|
-
if (reason.length <= ERROR_REASON_MAX) return reason;
|
|
9240
|
-
}
|
|
9241
|
-
return `${prefix}+${missing.length}`;
|
|
9242
|
-
}
|
|
9243
|
-
var PLACEHOLDER_RE, secretStoreSchema, SECRET_ENV_NAME;
|
|
9244
|
-
var init_secrets = __esm({
|
|
9245
|
-
"src/secrets.ts"() {
|
|
9246
|
-
"use strict";
|
|
9247
|
-
init_src();
|
|
9248
|
-
init_config();
|
|
9249
|
-
init_errors();
|
|
9250
|
-
PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
9251
|
-
secretStoreSchema = z17.record(z17.string(), z17.string());
|
|
9252
|
-
SECRET_ENV_NAME = /^CABANE_MCP_(\d+)$/;
|
|
9253
|
-
}
|
|
9254
|
-
});
|
|
9255
|
-
|
|
9256
|
-
// src/turn-env.ts
|
|
9257
|
-
function harnessLoginEnv(store, runtime) {
|
|
9258
|
-
const names = [
|
|
9259
|
-
...runtime in API_KEY_VARIABLES ? API_KEY_VARIABLES[runtime] : [],
|
|
9260
|
-
GITHUB_TOKEN_NAME
|
|
9261
|
-
];
|
|
9262
|
-
const env = {};
|
|
9263
|
-
for (const name of names) {
|
|
9264
|
-
const value = store.get(name);
|
|
9265
|
-
if (value) env[name] = value;
|
|
9266
|
-
}
|
|
9267
|
-
return env;
|
|
8763
|
+
return env;
|
|
9268
8764
|
}
|
|
9269
8765
|
function turnIdentityEnv(ids) {
|
|
9270
8766
|
return { CABANE_CONVERSATION_ID: ids.conversationId, CABANE_AGENT_ID: ids.agentId };
|
|
@@ -9614,7 +9110,7 @@ var init_turn_runtime_integrity = __esm({
|
|
|
9614
9110
|
|
|
9615
9111
|
// src/turn-execution.ts
|
|
9616
9112
|
import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
|
|
9617
|
-
import { mkdirSync as mkdirSync9 } from "fs";
|
|
9113
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync9 } from "fs";
|
|
9618
9114
|
function resolveIdleTimeoutMs(override) {
|
|
9619
9115
|
return override ?? DEFAULT_AGENT_IDLE_TIMEOUT_MS;
|
|
9620
9116
|
}
|
|
@@ -9671,7 +9167,6 @@ var init_turn_execution = __esm({
|
|
|
9671
9167
|
init_errors();
|
|
9672
9168
|
init_turn_seq_floor();
|
|
9673
9169
|
init_prepare_hook();
|
|
9674
|
-
init_prepare_failure();
|
|
9675
9170
|
init_prepared();
|
|
9676
9171
|
init_secrets();
|
|
9677
9172
|
init_turn_env();
|
|
@@ -9680,7 +9175,6 @@ var init_turn_execution = __esm({
|
|
|
9680
9175
|
init_turn_containment();
|
|
9681
9176
|
init_turn_committer();
|
|
9682
9177
|
init_turn_runtime_integrity();
|
|
9683
|
-
init_dispatcher();
|
|
9684
9178
|
PREPARING_TOOL_NAME = "preparing";
|
|
9685
9179
|
HARNESS_UNAVAILABLE_NOTICE = "**This harness isn't available on the companion.** Check the harness on the device, or choose another harness in the agent's settings. Then reply to try again.";
|
|
9686
9180
|
NO_TURN_TOKEN_REASON = "no_turn_token: the server issued no turn credential for this turn";
|
|
@@ -10078,7 +9572,7 @@ var init_turn_execution = __esm({
|
|
|
10078
9572
|
async prepareEnvironment() {
|
|
10079
9573
|
const { payload, workspaceId, turnId, turnLog } = this;
|
|
10080
9574
|
const prepareHook = this.opts.local.prepareHook;
|
|
10081
|
-
const pairDir = this.opts.
|
|
9575
|
+
const pairDir = this.opts.dedicatedRoot ? this.opts.workRoot : pairDirectory(this.opts.workRoot, {
|
|
10082
9576
|
workspaceSlug: this.opts.workspaceSlug,
|
|
10083
9577
|
agentUsername: this.opts.agentUsername,
|
|
10084
9578
|
conversationId: payload.conversationId
|
|
@@ -10106,10 +9600,10 @@ var init_turn_execution = __esm({
|
|
|
10106
9600
|
let hookEnv;
|
|
10107
9601
|
if (prepareHook) {
|
|
10108
9602
|
let cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
|
|
10109
|
-
if (cached2 && !
|
|
9603
|
+
if (cached2 && !existsSync12(cached2.cwd)) {
|
|
10110
9604
|
turnLog.debug(
|
|
10111
|
-
{ cwd: cached2.cwd
|
|
10112
|
-
"dispatcher: the prepared
|
|
9605
|
+
{ cwd: cached2.cwd },
|
|
9606
|
+
"dispatcher: the prepared directory is gone \u2014 re-running the prepare hook"
|
|
10113
9607
|
);
|
|
10114
9608
|
clearPrepared(workspaceId, payload.conversationId, payload.agentId);
|
|
10115
9609
|
cached2 = null;
|
|
@@ -10117,38 +9611,6 @@ var init_turn_execution = __esm({
|
|
|
10117
9611
|
if (cached2) {
|
|
10118
9612
|
effectiveCwd = cached2.cwd;
|
|
10119
9613
|
hookEnv = cached2.env;
|
|
10120
|
-
if (prepareHook.validateCached) {
|
|
10121
|
-
const runHook = this.opts.prepareHookRunner ?? runPrepareHook;
|
|
10122
|
-
try {
|
|
10123
|
-
await runPrepareWithReporting(
|
|
10124
|
-
runHook,
|
|
10125
|
-
prepareHook,
|
|
10126
|
-
{
|
|
10127
|
-
workspaceId,
|
|
10128
|
-
conversationId: payload.conversationId,
|
|
10129
|
-
agentId: payload.agentId,
|
|
10130
|
-
agentUsername: this.opts.agentUsername,
|
|
10131
|
-
defaultCwd: pairDir,
|
|
10132
|
-
runtime: this.turnContext.runtime,
|
|
10133
|
-
triggerEntryPaths: this.turnContext.conversation.triggerEntryPaths ?? [],
|
|
10134
|
-
title: this.turnContext.conversation.title,
|
|
10135
|
-
messageBody: this.turnContext.message.body,
|
|
10136
|
-
prepared: cached2
|
|
10137
|
-
},
|
|
10138
|
-
{ turnId, deviceId: this.opts.deviceId },
|
|
10139
|
-
() => {
|
|
10140
|
-
turnLog.error(
|
|
10141
|
-
"dispatcher: prepare failure reporter failed; original failure preserved"
|
|
10142
|
-
);
|
|
10143
|
-
}
|
|
10144
|
-
);
|
|
10145
|
-
} catch (err) {
|
|
10146
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
10147
|
-
turnLog.debug({ err: reason }, "dispatcher: prepare hook rejected a prepared turn");
|
|
10148
|
-
const failReason = `prepare_failed: ${reason}`;
|
|
10149
|
-
throw this.concluded(failReason, failReason);
|
|
10150
|
-
}
|
|
10151
|
-
}
|
|
10152
9614
|
} else {
|
|
10153
9615
|
const delayMs = this.opts.preparingRowDelayMs ?? DEFAULT_PREPARING_ROW_DELAY_MS;
|
|
10154
9616
|
let preparingStarted = false;
|
|
@@ -10180,33 +9642,17 @@ var init_turn_execution = __esm({
|
|
|
10180
9642
|
preparingTimer.unref?.();
|
|
10181
9643
|
const runHook = this.opts.prepareHookRunner ?? runPrepareHook;
|
|
10182
9644
|
try {
|
|
10183
|
-
const result = await
|
|
10184
|
-
|
|
10185
|
-
|
|
10186
|
-
|
|
10187
|
-
|
|
10188
|
-
|
|
10189
|
-
|
|
10190
|
-
|
|
10191
|
-
|
|
10192
|
-
|
|
10193
|
-
|
|
10194
|
-
runtime: this.turnContext.runtime,
|
|
10195
|
-
// The trigger message's referenced-entry paths, which a prepare hook
|
|
10196
|
-
// may key its per-task env off. `[]` from an older API.
|
|
10197
|
-
triggerEntryPaths: this.turnContext.conversation.triggerEntryPaths ?? [],
|
|
10198
|
-
title: this.turnContext.conversation.title,
|
|
10199
|
-
// Where an `env:` directive rides, so a dispatch can ask for its
|
|
10200
|
-
// environment in words.
|
|
10201
|
-
messageBody: this.turnContext.message.body
|
|
10202
|
-
},
|
|
10203
|
-
{ turnId, deviceId: this.opts.deviceId },
|
|
10204
|
-
() => {
|
|
10205
|
-
turnLog.error(
|
|
10206
|
-
"dispatcher: prepare failure reporter failed; original failure preserved"
|
|
10207
|
-
);
|
|
10208
|
-
}
|
|
10209
|
-
);
|
|
9645
|
+
const result = await runHook(prepareHook, {
|
|
9646
|
+
workspaceId,
|
|
9647
|
+
conversationId: payload.conversationId,
|
|
9648
|
+
agentId: payload.agentId,
|
|
9649
|
+
agentUsername: this.opts.agentUsername,
|
|
9650
|
+
// The pair's own directory, already made, for a hook that only
|
|
9651
|
+
// wants to add to it.
|
|
9652
|
+
defaultCwd: pairDir,
|
|
9653
|
+
runtime: this.turnContext.runtime,
|
|
9654
|
+
title: this.turnContext.conversation.title
|
|
9655
|
+
});
|
|
10210
9656
|
clearTimeout(preparingTimer);
|
|
10211
9657
|
if (preparingStarted) reportPreparing("done");
|
|
10212
9658
|
writePrepared(workspaceId, payload.conversationId, payload.agentId, result);
|
|
@@ -10853,150 +10299,17 @@ var init_turn_execution = __esm({
|
|
|
10853
10299
|
}
|
|
10854
10300
|
});
|
|
10855
10301
|
|
|
10856
|
-
// src/dispatcher.ts
|
|
10857
|
-
import { randomUUID as randomUUID4 } from "crypto";
|
|
10858
|
-
import { existsSync as existsSync12, readdirSync as readdirSync2, statSync as statSync4 } from "fs";
|
|
10859
|
-
import { join as join22 } from "path";
|
|
10860
|
-
function checkoutState(cwd) {
|
|
10861
|
-
if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
|
|
10862
|
-
if (!existsSync12(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
|
|
10863
|
-
let entries;
|
|
10864
|
-
try {
|
|
10865
|
-
entries = readdirSync2(cwd);
|
|
10866
|
-
} catch (error) {
|
|
10867
|
-
return { ok: false, reason: `${cwd} is unreadable (${error.message})` };
|
|
10868
|
-
}
|
|
10869
|
-
if (entries.length === 0) {
|
|
10870
|
-
return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
|
|
10871
|
-
}
|
|
10872
|
-
const gitPath = join22(cwd, ".git");
|
|
10873
|
-
if (!existsSync12(gitPath)) return { ok: true, reason: "usable" };
|
|
10874
|
-
let stat;
|
|
10875
|
-
try {
|
|
10876
|
-
stat = statSync4(gitPath);
|
|
10877
|
-
} catch (error) {
|
|
10878
|
-
return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
|
|
10879
|
-
}
|
|
10880
|
-
if (stat.isDirectory() && !existsSync12(join22(gitPath, "HEAD")))
|
|
10881
|
-
return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
|
|
10882
|
-
return { ok: true, reason: "usable" };
|
|
10883
|
-
}
|
|
10884
|
-
function runKey(conversationId, agentId) {
|
|
10885
|
-
return `${conversationId}|${agentId}`;
|
|
10886
|
-
}
|
|
10887
|
-
var Dispatcher;
|
|
10888
|
-
var init_dispatcher = __esm({
|
|
10889
|
-
"src/dispatcher.ts"() {
|
|
10890
|
-
"use strict";
|
|
10891
|
-
init_launch();
|
|
10892
|
-
init_turn_execution();
|
|
10893
|
-
Dispatcher = class {
|
|
10894
|
-
constructor(opts) {
|
|
10895
|
-
this.opts = opts;
|
|
10896
|
-
this.aborts = opts.aborts ?? /* @__PURE__ */ new Map();
|
|
10897
|
-
}
|
|
10898
|
-
opts;
|
|
10899
|
-
// (conversation|agent) → per-turn controllers. A delayed cancel for an old
|
|
10900
|
-
// turn must never reach its successor.
|
|
10901
|
-
aborts;
|
|
10902
|
-
// Pairs already told their session couldn't be saved: the notice is once per
|
|
10903
|
-
// pair, while the rejection repeats every turn.
|
|
10904
|
-
sessionWriteNotified = /* @__PURE__ */ new Set();
|
|
10905
|
-
notifyStart(info) {
|
|
10906
|
-
try {
|
|
10907
|
-
this.opts.observer?.onStart(info);
|
|
10908
|
-
} catch {
|
|
10909
|
-
}
|
|
10910
|
-
}
|
|
10911
|
-
notifyEnd(info) {
|
|
10912
|
-
try {
|
|
10913
|
-
this.opts.observer?.onEnd(info);
|
|
10914
|
-
} catch {
|
|
10915
|
-
}
|
|
10916
|
-
}
|
|
10917
|
-
// The supervisor supplies a stable turnId on both fresh and resumed dispatch.
|
|
10918
|
-
// resumed is separate: only replay seeds sequence/speech recovery from the
|
|
10919
|
-
// prior span. A direct caller omitting turnId gets a fresh id.
|
|
10920
|
-
async handle(payload, opts = {}) {
|
|
10921
|
-
const key = runKey(payload.conversationId, payload.agentId);
|
|
10922
|
-
return runTurnInRunner(
|
|
10923
|
-
this.opts.turn,
|
|
10924
|
-
this.opts.runner,
|
|
10925
|
-
{
|
|
10926
|
-
registerAbort: (turnId, controller) => {
|
|
10927
|
-
let pairAborts = this.aborts.get(key);
|
|
10928
|
-
if (!pairAborts) {
|
|
10929
|
-
pairAborts = /* @__PURE__ */ new Map();
|
|
10930
|
-
this.aborts.set(key, pairAborts);
|
|
10931
|
-
}
|
|
10932
|
-
pairAborts.set(turnId, controller);
|
|
10933
|
-
},
|
|
10934
|
-
releaseAbort: (turnId, controller) => {
|
|
10935
|
-
const pair = this.aborts.get(key);
|
|
10936
|
-
if (pair?.get(turnId) !== controller) return;
|
|
10937
|
-
pair.delete(turnId);
|
|
10938
|
-
if (pair.size === 0) this.aborts.delete(key);
|
|
10939
|
-
},
|
|
10940
|
-
sessionWriteNoticeClaimed: () => this.sessionWriteNotified.has(key),
|
|
10941
|
-
markSessionWriteNotice: () => {
|
|
10942
|
-
this.sessionWriteNotified.add(key);
|
|
10943
|
-
},
|
|
10944
|
-
notifyStart: (info) => this.notifyStart(info),
|
|
10945
|
-
notifyEnd: (info) => this.notifyEnd(info)
|
|
10946
|
-
},
|
|
10947
|
-
payload,
|
|
10948
|
-
{ turnId: opts.turnId ?? randomUUID4(), resumed: opts.resumed === true }
|
|
10949
|
-
);
|
|
10950
|
-
}
|
|
10951
|
-
// Cancel the named turn. An older server omits turnId and cancels the pair.
|
|
10952
|
-
cancel(conversationId, agentId, turnId) {
|
|
10953
|
-
const key = runKey(conversationId, agentId);
|
|
10954
|
-
const pair = this.aborts.get(key);
|
|
10955
|
-
if (!pair || pair.size === 0) return false;
|
|
10956
|
-
const controllers = turnId === void 0 ? [...pair.values()] : [pair.get(turnId)];
|
|
10957
|
-
let aborted = false;
|
|
10958
|
-
for (const ac of controllers) {
|
|
10959
|
-
if (!ac) continue;
|
|
10960
|
-
aborted = true;
|
|
10961
|
-
try {
|
|
10962
|
-
ac.abort();
|
|
10963
|
-
} catch {
|
|
10964
|
-
}
|
|
10965
|
-
}
|
|
10966
|
-
return aborted;
|
|
10967
|
-
}
|
|
10968
|
-
// Abort every run in flight in THIS process, whatever its conversation. For
|
|
10969
|
-
// an agent taken off this device: nothing it is still running here is
|
|
10970
|
-
// legitimate, and a cancel frame that arrives after it is dropped from the
|
|
10971
|
-
// roster finds no dispatcher to reach. True if any was aborted.
|
|
10972
|
-
cancelAll() {
|
|
10973
|
-
let aborted = false;
|
|
10974
|
-
for (const pair of this.aborts.values()) {
|
|
10975
|
-
for (const ac of [...pair.values()]) {
|
|
10976
|
-
try {
|
|
10977
|
-
ac.abort();
|
|
10978
|
-
aborted = true;
|
|
10979
|
-
} catch {
|
|
10980
|
-
}
|
|
10981
|
-
}
|
|
10982
|
-
}
|
|
10983
|
-
return aborted;
|
|
10984
|
-
}
|
|
10985
|
-
};
|
|
10986
|
-
}
|
|
10987
|
-
});
|
|
10988
|
-
|
|
10989
10302
|
// src/outbox.ts
|
|
10990
10303
|
import {
|
|
10991
10304
|
existsSync as existsSync14,
|
|
10992
10305
|
mkdirSync as mkdirSync10,
|
|
10993
|
-
readdirSync as
|
|
10306
|
+
readdirSync as readdirSync2,
|
|
10994
10307
|
readFileSync as readFileSync11,
|
|
10995
10308
|
renameSync as renameSync5,
|
|
10996
10309
|
rmSync as rmSync7,
|
|
10997
10310
|
writeFileSync as writeFileSync6
|
|
10998
10311
|
} from "fs";
|
|
10999
|
-
import { join as
|
|
10312
|
+
import { join as join22 } from "path";
|
|
11000
10313
|
var MAX_ENTRIES, MAX_AGE_MS, Outbox;
|
|
11001
10314
|
var init_outbox = __esm({
|
|
11002
10315
|
"src/outbox.ts"() {
|
|
@@ -11014,10 +10327,10 @@ var init_outbox = __esm({
|
|
|
11014
10327
|
// Resolved lazily (per call) so tests that swap HOME between
|
|
11015
10328
|
// cases route writes at the right tmpdir.
|
|
11016
10329
|
dir() {
|
|
11017
|
-
return
|
|
10330
|
+
return join22(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
|
|
11018
10331
|
}
|
|
11019
10332
|
fileFor(turnId, seq) {
|
|
11020
|
-
return
|
|
10333
|
+
return join22(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
|
|
11021
10334
|
}
|
|
11022
10335
|
// Persist a commit for later draining. Atomic (temp file + rename) so a
|
|
11023
10336
|
// concurrent `list()` never reads a half-written entry, then enforces the
|
|
@@ -11052,14 +10365,14 @@ var init_outbox = __esm({
|
|
|
11052
10365
|
if (!existsSync14(dir2)) return [];
|
|
11053
10366
|
let names;
|
|
11054
10367
|
try {
|
|
11055
|
-
names =
|
|
10368
|
+
names = readdirSync2(dir2);
|
|
11056
10369
|
} catch {
|
|
11057
10370
|
return [];
|
|
11058
10371
|
}
|
|
11059
10372
|
const entries = [];
|
|
11060
10373
|
for (const name of names) {
|
|
11061
10374
|
if (!name.endsWith(".json")) continue;
|
|
11062
|
-
const full =
|
|
10375
|
+
const full = join22(dir2, name);
|
|
11063
10376
|
try {
|
|
11064
10377
|
const parsed = JSON.parse(readFileSync11(full, "utf8"));
|
|
11065
10378
|
if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
|
|
@@ -11096,7 +10409,7 @@ var init_outbox = __esm({
|
|
|
11096
10409
|
maxSeqForTurn(turnId) {
|
|
11097
10410
|
const dir2 = this.dir();
|
|
11098
10411
|
if (!existsSync14(dir2)) return 0;
|
|
11099
|
-
const names =
|
|
10412
|
+
const names = readdirSync2(dir2);
|
|
11100
10413
|
const prefix = `${encodeURIComponent(turnId)}__`;
|
|
11101
10414
|
let max = 0;
|
|
11102
10415
|
for (const name of names) {
|
|
@@ -11117,7 +10430,7 @@ var init_outbox = __esm({
|
|
|
11117
10430
|
const dir2 = this.dir();
|
|
11118
10431
|
if (!existsSync14(dir2)) return 0;
|
|
11119
10432
|
try {
|
|
11120
|
-
return
|
|
10433
|
+
return readdirSync2(dir2).filter((n) => n.endsWith(".json")).length;
|
|
11121
10434
|
} catch {
|
|
11122
10435
|
return 0;
|
|
11123
10436
|
}
|
|
@@ -11166,7 +10479,7 @@ var init_outbox = __esm({
|
|
|
11166
10479
|
|
|
11167
10480
|
// src/runner/exec-shim.ts
|
|
11168
10481
|
import { chmodSync as chmodSync6, mkdirSync as mkdirSync18, mkdtempSync, rmSync as rmSync13, writeFileSync as writeFileSync11 } from "fs";
|
|
11169
|
-
import { join as
|
|
10482
|
+
import { join as join32 } from "path";
|
|
11170
10483
|
function shellQuote(value) {
|
|
11171
10484
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
11172
10485
|
}
|
|
@@ -11176,10 +10489,10 @@ function createShimDir() {
|
|
|
11176
10489
|
return {
|
|
11177
10490
|
write(argv) {
|
|
11178
10491
|
if (!dir2) {
|
|
11179
|
-
mkdirSync18(
|
|
11180
|
-
dir2 = mkdtempSync(
|
|
10492
|
+
mkdirSync18(join32(cabaneDir(), "runner"), { recursive: true });
|
|
10493
|
+
dir2 = mkdtempSync(join32(cabaneDir(), "runner", "turn-"));
|
|
11181
10494
|
}
|
|
11182
|
-
const path =
|
|
10495
|
+
const path = join32(dir2, `exec-${++count}`);
|
|
11183
10496
|
writeFileSync11(path, `#!/bin/sh
|
|
11184
10497
|
exec ${argv.map(shellQuote).join(" ")} "$@"
|
|
11185
10498
|
`);
|
|
@@ -11346,13 +10659,13 @@ function runnerTurnOptions(input, log2, emit, extras = {}) {
|
|
|
11346
10659
|
local: spec.local,
|
|
11347
10660
|
// The place the runner stands in names its own root and transcript
|
|
11348
10661
|
// directory when it has them (`PLACE_ENV`); the launcher's host-computed
|
|
11349
|
-
// values are what a runner on the person's own machine gets. A
|
|
11350
|
-
// conversation × agent's, so the turn stands in
|
|
10662
|
+
// values are what a runner on the person's own machine gets. A dedicated
|
|
10663
|
+
// root is one conversation × agent's, so the turn stands in it itself.
|
|
11351
10664
|
workRoot: input.place?.workRoot ?? spec.workRoot,
|
|
11352
|
-
...input.place?.
|
|
10665
|
+
...input.place?.dedicatedRoot ? { dedicatedRoot: true } : {},
|
|
11353
10666
|
runConfig: spec.runConfig,
|
|
11354
|
-
// The launcher's copy of the device's secrets, when it sent one: a
|
|
11355
|
-
//
|
|
10667
|
+
// The launcher's copy of the device's secrets, when it sent one: a wrapped
|
|
10668
|
+
// runner may have no `secrets.json` to read.
|
|
11356
10669
|
...input.secrets ? { secretStore: makeStore(input.secrets) } : {},
|
|
11357
10670
|
// The grants-file entries the launcher read for this dispatch's turn.
|
|
11358
10671
|
...input.grants ? { deviceGrants: input.grants } : {},
|
|
@@ -12216,7 +11529,7 @@ async function liveCompanionState() {
|
|
|
12216
11529
|
// src/commands/setup.ts
|
|
12217
11530
|
init_cabane_command();
|
|
12218
11531
|
init_config();
|
|
12219
|
-
import { hostname as
|
|
11532
|
+
import { hostname as hostname2 } from "os";
|
|
12220
11533
|
|
|
12221
11534
|
// src/logger.ts
|
|
12222
11535
|
init_config();
|
|
@@ -12442,8 +11755,8 @@ async function claimSetupCode(baseUrl, input) {
|
|
|
12442
11755
|
..."deviceLabel" in body && typeof body.deviceLabel === "string" ? { deviceLabel: body.deviceLabel } : {}
|
|
12443
11756
|
};
|
|
12444
11757
|
}
|
|
12445
|
-
function deviceLabelFromHostname(
|
|
12446
|
-
const trimmed =
|
|
11758
|
+
function deviceLabelFromHostname(hostname3) {
|
|
11759
|
+
const trimmed = hostname3.trim().replace(/\.local$/i, "");
|
|
12447
11760
|
return trimmed.length ? trimmed.slice(0, 120) : void 0;
|
|
12448
11761
|
}
|
|
12449
11762
|
|
|
@@ -12988,7 +12301,7 @@ var emptyHeartbeatStatus = () => ({
|
|
|
12988
12301
|
|
|
12989
12302
|
// src/supervisor.ts
|
|
12990
12303
|
init_cabane_command();
|
|
12991
|
-
import { hostname
|
|
12304
|
+
import { hostname } from "os";
|
|
12992
12305
|
|
|
12993
12306
|
// src/log-copy.ts
|
|
12994
12307
|
init_failure();
|
|
@@ -13214,241 +12527,745 @@ function save(deviceId, map) {
|
|
|
13214
12527
|
} catch {
|
|
13215
12528
|
}
|
|
13216
12529
|
}
|
|
13217
|
-
const path = devicePath(deviceId);
|
|
13218
|
-
const tmp = `${path}.${process.pid}.tmp`;
|
|
13219
|
-
try {
|
|
13220
|
-
writeFileSync3(tmp, JSON.stringify(map, null, 2) + "\n", { mode: 384 });
|
|
12530
|
+
const path = devicePath(deviceId);
|
|
12531
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
12532
|
+
try {
|
|
12533
|
+
writeFileSync3(tmp, JSON.stringify(map, null, 2) + "\n", { mode: 384 });
|
|
12534
|
+
try {
|
|
12535
|
+
chmodSync3(tmp, 384);
|
|
12536
|
+
} catch {
|
|
12537
|
+
}
|
|
12538
|
+
renameSync3(tmp, path);
|
|
12539
|
+
} catch (err) {
|
|
12540
|
+
try {
|
|
12541
|
+
rmSync4(tmp, { force: true });
|
|
12542
|
+
} catch {
|
|
12543
|
+
}
|
|
12544
|
+
throw err;
|
|
12545
|
+
}
|
|
12546
|
+
}
|
|
12547
|
+
function getCredential(deviceId, agentId) {
|
|
12548
|
+
return load(deviceId)[agentId] ?? null;
|
|
12549
|
+
}
|
|
12550
|
+
function setCredential(deviceId, agentId, pat) {
|
|
12551
|
+
if (read(devicePath(deviceId))?.[agentId] === pat) return;
|
|
12552
|
+
const map = load(deviceId);
|
|
12553
|
+
map[agentId] = pat;
|
|
12554
|
+
save(deviceId, map);
|
|
12555
|
+
}
|
|
12556
|
+
function pruneCredentials(deviceId, keepAgentIds) {
|
|
12557
|
+
const keep = new Set(keepAgentIds);
|
|
12558
|
+
if (keep.size === 0) return [];
|
|
12559
|
+
const own = read(devicePath(deviceId));
|
|
12560
|
+
const map = own ?? load(deviceId);
|
|
12561
|
+
const removed = Object.keys(map).filter((id) => !keep.has(id));
|
|
12562
|
+
for (const id of removed) delete map[id];
|
|
12563
|
+
if (removed.length > 0 || own === null) save(deviceId, map);
|
|
12564
|
+
return removed;
|
|
12565
|
+
}
|
|
12566
|
+
function clearCredentials() {
|
|
12567
|
+
rmSync4(credentialsDir(), { recursive: true, force: true });
|
|
12568
|
+
if (existsSync7(legacyPath())) writeFileSync3(legacyPath(), "", { mode: 384 });
|
|
12569
|
+
}
|
|
12570
|
+
|
|
12571
|
+
// src/dispatch-dedupe.ts
|
|
12572
|
+
init_config();
|
|
12573
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync as renameSync4, existsSync as existsSync8 } from "fs";
|
|
12574
|
+
import { join as join13 } from "path";
|
|
12575
|
+
var MAX_IDS = 256;
|
|
12576
|
+
function dir(log2) {
|
|
12577
|
+
return join13(cabaneDir(), log2);
|
|
12578
|
+
}
|
|
12579
|
+
function pathFor(log2, workspaceId) {
|
|
12580
|
+
return join13(dir(log2), encodeURIComponent(workspaceId));
|
|
12581
|
+
}
|
|
12582
|
+
function readIds(log2, workspaceId) {
|
|
12583
|
+
const path = pathFor(log2, workspaceId);
|
|
12584
|
+
if (!existsSync8(path)) return [];
|
|
12585
|
+
try {
|
|
12586
|
+
return readFileSync5(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
12587
|
+
} catch {
|
|
12588
|
+
return [];
|
|
12589
|
+
}
|
|
12590
|
+
}
|
|
12591
|
+
function has(log2, workspaceId, eventId) {
|
|
12592
|
+
return readIds(log2, workspaceId).includes(eventId);
|
|
12593
|
+
}
|
|
12594
|
+
function mark(log2, workspaceId, eventId) {
|
|
12595
|
+
const ids = readIds(log2, workspaceId);
|
|
12596
|
+
if (ids.includes(eventId)) return;
|
|
12597
|
+
ids.push(eventId);
|
|
12598
|
+
const trimmed = ids.length > MAX_IDS ? ids.slice(-MAX_IDS) : ids;
|
|
12599
|
+
mkdirSync6(dir(log2), { recursive: true });
|
|
12600
|
+
writeFileSync4(pathFor(log2, workspaceId), trimmed.join("\n") + "\n", "utf8");
|
|
12601
|
+
}
|
|
12602
|
+
function hasDispatched(workspaceId, eventId) {
|
|
12603
|
+
return has("dispatched", workspaceId, eventId);
|
|
12604
|
+
}
|
|
12605
|
+
function markDispatched(workspaceId, eventId) {
|
|
12606
|
+
mark("dispatched", workspaceId, eventId);
|
|
12607
|
+
}
|
|
12608
|
+
function hasCompleted(workspaceId, eventId) {
|
|
12609
|
+
return has("completed", workspaceId, eventId);
|
|
12610
|
+
}
|
|
12611
|
+
function markCompleted(workspaceId, eventId) {
|
|
12612
|
+
mark("completed", workspaceId, eventId);
|
|
12613
|
+
forgetTurnId(workspaceId, eventId);
|
|
12614
|
+
}
|
|
12615
|
+
var MAX_RESUME_ATTEMPTS = 3;
|
|
12616
|
+
function resumeDir() {
|
|
12617
|
+
return join13(cabaneDir(), "resume-attempts");
|
|
12618
|
+
}
|
|
12619
|
+
function resumePathFor(workspaceId) {
|
|
12620
|
+
return join13(resumeDir(), encodeURIComponent(workspaceId));
|
|
12621
|
+
}
|
|
12622
|
+
function readResumeCounts(workspaceId) {
|
|
12623
|
+
const out = /* @__PURE__ */ new Map();
|
|
12624
|
+
const path = resumePathFor(workspaceId);
|
|
12625
|
+
if (!existsSync8(path)) return out;
|
|
12626
|
+
try {
|
|
12627
|
+
for (const line of readFileSync5(path, "utf8").split("\n")) {
|
|
12628
|
+
const trimmed = line.trim();
|
|
12629
|
+
if (!trimmed) continue;
|
|
12630
|
+
const tab = trimmed.lastIndexOf(" ");
|
|
12631
|
+
if (tab <= 0) continue;
|
|
12632
|
+
const id = trimmed.slice(0, tab);
|
|
12633
|
+
const count = Number.parseInt(trimmed.slice(tab + 1), 10);
|
|
12634
|
+
if (id && Number.isFinite(count)) out.set(id, count);
|
|
12635
|
+
}
|
|
12636
|
+
} catch {
|
|
12637
|
+
return out;
|
|
12638
|
+
}
|
|
12639
|
+
return out;
|
|
12640
|
+
}
|
|
12641
|
+
function bumpResumeAttempt(workspaceId, eventId) {
|
|
12642
|
+
const counts = readResumeCounts(workspaceId);
|
|
12643
|
+
const next = (counts.get(eventId) ?? 0) + 1;
|
|
12644
|
+
counts.set(eventId, next);
|
|
12645
|
+
const entries = [...counts.entries()];
|
|
12646
|
+
const trimmed = entries.length > MAX_IDS ? entries.slice(-MAX_IDS) : entries;
|
|
12647
|
+
mkdirSync6(resumeDir(), { recursive: true });
|
|
12648
|
+
writeFileSync4(
|
|
12649
|
+
resumePathFor(workspaceId),
|
|
12650
|
+
trimmed.map(([id, c]) => `${id} ${c}`).join("\n") + "\n",
|
|
12651
|
+
"utf8"
|
|
12652
|
+
);
|
|
12653
|
+
return next;
|
|
12654
|
+
}
|
|
12655
|
+
function turnDir() {
|
|
12656
|
+
return join13(cabaneDir(), "turns");
|
|
12657
|
+
}
|
|
12658
|
+
function turnPathFor(workspaceId) {
|
|
12659
|
+
return join13(turnDir(), encodeURIComponent(workspaceId));
|
|
12660
|
+
}
|
|
12661
|
+
var TURN_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
12662
|
+
function readTurnIds(workspaceId) {
|
|
12663
|
+
const out = /* @__PURE__ */ new Map();
|
|
12664
|
+
const path = turnPathFor(workspaceId);
|
|
12665
|
+
if (!existsSync8(path)) return out;
|
|
12666
|
+
try {
|
|
12667
|
+
for (const line of readFileSync5(path, "utf8").split("\n")) {
|
|
12668
|
+
const trimmed = line.trim();
|
|
12669
|
+
if (!trimmed) continue;
|
|
12670
|
+
const tab = trimmed.lastIndexOf(" ");
|
|
12671
|
+
if (tab <= 0) continue;
|
|
12672
|
+
const id = trimmed.slice(0, tab);
|
|
12673
|
+
const turnId = trimmed.slice(tab + 1);
|
|
12674
|
+
if (id && TURN_ID_RE.test(turnId)) out.set(id, turnId);
|
|
12675
|
+
}
|
|
12676
|
+
} catch {
|
|
12677
|
+
return out;
|
|
12678
|
+
}
|
|
12679
|
+
return out;
|
|
12680
|
+
}
|
|
12681
|
+
function turnIdForEvent(workspaceId, eventId) {
|
|
12682
|
+
return readTurnIds(workspaceId).get(eventId) ?? null;
|
|
12683
|
+
}
|
|
12684
|
+
var TURN_ID_OVERFLOW_WARN = 8192;
|
|
12685
|
+
function writeTurnIds(workspaceId, turns) {
|
|
12686
|
+
const entries = [...turns.entries()];
|
|
12687
|
+
mkdirSync6(turnDir(), { recursive: true });
|
|
12688
|
+
const path = turnPathFor(workspaceId);
|
|
12689
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
12690
|
+
writeFileSync4(tmp, entries.map(([id, t]) => `${id} ${t}`).join("\n") + "\n", "utf8");
|
|
12691
|
+
renameSync4(tmp, path);
|
|
12692
|
+
return entries.length;
|
|
12693
|
+
}
|
|
12694
|
+
function rememberTurnId(workspaceId, eventId, turnId) {
|
|
12695
|
+
const turns = readTurnIds(workspaceId);
|
|
12696
|
+
if (turns.get(eventId) === turnId) return turns.size;
|
|
12697
|
+
turns.set(eventId, turnId);
|
|
12698
|
+
return writeTurnIds(workspaceId, turns);
|
|
12699
|
+
}
|
|
12700
|
+
function forgetTurnId(workspaceId, eventId) {
|
|
12701
|
+
const turns = readTurnIds(workspaceId);
|
|
12702
|
+
if (!turns.delete(eventId)) return;
|
|
12703
|
+
writeTurnIds(workspaceId, turns);
|
|
12704
|
+
}
|
|
12705
|
+
function noResume() {
|
|
12706
|
+
return process.env.CABANE_COMPANION_NO_RESUME === "1";
|
|
12707
|
+
}
|
|
12708
|
+
|
|
12709
|
+
// src/supervisor.ts
|
|
12710
|
+
init_bundled_binaries();
|
|
12711
|
+
init_install_kind();
|
|
12712
|
+
|
|
12713
|
+
// src/connector-health.ts
|
|
12714
|
+
init_src();
|
|
12715
|
+
var ConnectorHealthStore = class {
|
|
12716
|
+
byRuntime = /* @__PURE__ */ new Map();
|
|
12717
|
+
// Record a settled turn's outcome for `runtime`. A success heals to `ok`; a
|
|
12718
|
+
// classified failure sets `limited`(+reset) / `auth_expired` with the reason as
|
|
12719
|
+
// `last_error`; anything else (a user Stop, a timeout, an unclassified error)
|
|
12720
|
+
// leaves the last-known status untouched — we only ever move state on real
|
|
12721
|
+
// evidence.
|
|
12722
|
+
recordSettle(runtime, outcome) {
|
|
12723
|
+
if (outcome.ok) {
|
|
12724
|
+
this.byRuntime.set(runtime, { quotaState: "ok", limitedUntil: null, lastError: null });
|
|
12725
|
+
return;
|
|
12726
|
+
}
|
|
12727
|
+
const failure = decodeFailureReason(outcome.errorReason);
|
|
12728
|
+
if (!failure) return;
|
|
12729
|
+
const quota = quotaForFailure(failure);
|
|
12730
|
+
if (!quota) return;
|
|
12731
|
+
this.byRuntime.set(runtime, {
|
|
12732
|
+
quotaState: quota.quotaState,
|
|
12733
|
+
limitedUntil: quota.limitedUntil,
|
|
12734
|
+
lastError: outcome.errorReason ?? null
|
|
12735
|
+
});
|
|
12736
|
+
}
|
|
12737
|
+
// The store's current opinion for one runtime, or undefined when it has
|
|
12738
|
+
// no observation. Read by the dispatcher at idle-timeout fire: an idle reap
|
|
12739
|
+
// while this runtime is definitively `limited` (a present, future `limitedUntil`
|
|
12740
|
+
// recorded by a sibling/earlier turn's classified cap settle) is the account cap
|
|
12741
|
+
// surfacing as silence, not a genuine hang — so the settle reclassifies to
|
|
12742
|
+
// `usage_capped` instead of `timeout_idle`. Returns the stored shape verbatim
|
|
12743
|
+
// (the dispatcher reads `quotaState` + `limitedUntil` and applies its own
|
|
12744
|
+
// future-instant gate); a read never mutates state.
|
|
12745
|
+
lookup(runtime) {
|
|
12746
|
+
return this.byRuntime.get(runtime);
|
|
12747
|
+
}
|
|
12748
|
+
// The per-connector reports to attach to a heartbeat — one entry per runtime the
|
|
12749
|
+
// companion has an observation for. Empty until the first classified failure/heal,
|
|
12750
|
+
// so a companion that has seen nothing sends no `connectors[]` and the server's
|
|
12751
|
+
// manifest synthesis (status-less rows) is unaffected.
|
|
12752
|
+
reports() {
|
|
12753
|
+
return [...this.byRuntime.entries()].map(([runtime, h]) => ({
|
|
12754
|
+
runtime,
|
|
12755
|
+
quotaState: h.quotaState,
|
|
12756
|
+
limitedUntil: h.limitedUntil,
|
|
12757
|
+
lastError: h.lastError
|
|
12758
|
+
}));
|
|
12759
|
+
}
|
|
12760
|
+
};
|
|
12761
|
+
|
|
12762
|
+
// src/dispatcher.ts
|
|
12763
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
12764
|
+
|
|
12765
|
+
// src/runner/launch.ts
|
|
12766
|
+
init_src();
|
|
12767
|
+
init_opencode_lock();
|
|
12768
|
+
init_api_error_shape();
|
|
12769
|
+
init_errors();
|
|
12770
|
+
init_boot_id();
|
|
12771
|
+
init_turn_containment();
|
|
12772
|
+
init_protocol();
|
|
12773
|
+
import { createInterface } from "readline";
|
|
12774
|
+
function selfRunnerCommand() {
|
|
12775
|
+
const script = process.argv[1];
|
|
12776
|
+
if (!script) throw new Error("runner: cannot tell which script started this companion");
|
|
12777
|
+
return [process.execPath, ...process.execArgv, script, "run-turn"];
|
|
12778
|
+
}
|
|
12779
|
+
function wrappedRunnerCommand(prefix, own) {
|
|
12780
|
+
return [...prefix ?? [], ...own];
|
|
12781
|
+
}
|
|
12782
|
+
var WRAPPED_ENV_DROPPED = [
|
|
12783
|
+
"ANTHROPIC_API_KEY",
|
|
12784
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
12785
|
+
"CODEX_API_KEY",
|
|
12786
|
+
"GH_TOKEN",
|
|
12787
|
+
"GITHUB_TOKEN",
|
|
12788
|
+
"DBUS_SESSION_BUS_ADDRESS",
|
|
12789
|
+
"XDG_RUNTIME_DIR"
|
|
12790
|
+
];
|
|
12791
|
+
function runnerExecEnv(base, wrapped) {
|
|
12792
|
+
const env = {};
|
|
12793
|
+
for (const [k, v] of Object.entries(base)) {
|
|
12794
|
+
if (v === void 0) continue;
|
|
12795
|
+
if (wrapped && WRAPPED_ENV_DROPPED.includes(k)) continue;
|
|
12796
|
+
env[k] = v;
|
|
12797
|
+
}
|
|
12798
|
+
return env;
|
|
12799
|
+
}
|
|
12800
|
+
function healthSnapshot(store) {
|
|
12801
|
+
const out = {};
|
|
12802
|
+
for (const r of store?.reports() ?? []) {
|
|
12803
|
+
out[r.runtime] = {
|
|
12804
|
+
quotaState: r.quotaState ?? "unknown",
|
|
12805
|
+
limitedUntil: r.limitedUntil ?? null,
|
|
12806
|
+
lastError: r.lastError ?? null
|
|
12807
|
+
};
|
|
12808
|
+
}
|
|
12809
|
+
return out;
|
|
12810
|
+
}
|
|
12811
|
+
function runnerSpec(opts, launch, duties, runnerId) {
|
|
12812
|
+
return {
|
|
12813
|
+
runnerId,
|
|
12814
|
+
...opts.deviceId ? { deviceId: opts.deviceId } : {},
|
|
12815
|
+
bootId,
|
|
12816
|
+
workspaceId: opts.workspaceId,
|
|
12817
|
+
workspaceSlug: opts.workspaceSlug,
|
|
12818
|
+
agentId: opts.agentId,
|
|
12819
|
+
agentUsername: opts.agentUsername,
|
|
12820
|
+
agentName: String(opts.log.bindings().agentName ?? opts.agentUsername),
|
|
12821
|
+
local: opts.local,
|
|
12822
|
+
runConfig: opts.runConfig,
|
|
12823
|
+
workRoot: opts.workRoot,
|
|
12824
|
+
...opts.opencodeServerUrl ? { opencodeServerUrl: opts.opencodeServerUrl } : {},
|
|
12825
|
+
// Read now, per turn, the way adapter selection reads them.
|
|
12826
|
+
claudeCodeAvailable: opts.claudeCodeAvailable?.() ?? true,
|
|
12827
|
+
codexAvailable: opts.codexAvailable?.() ?? false,
|
|
12828
|
+
...opts.idleTimeoutMs !== void 0 ? { idleTimeoutMs: opts.idleTimeoutMs } : {},
|
|
12829
|
+
...opts.totalTimeoutMs !== void 0 ? { totalTimeoutMs: opts.totalTimeoutMs } : {},
|
|
12830
|
+
...opts.leaseRenewalMs !== void 0 ? { leaseRenewalMs: opts.leaseRenewalMs } : {},
|
|
12831
|
+
...opts.preparingRowDelayMs !== void 0 ? { preparingRowDelayMs: opts.preparingRowDelayMs } : {},
|
|
12832
|
+
transcripts: opts.transcriptDir !== void 0,
|
|
12833
|
+
logLevel: opts.log.level,
|
|
12834
|
+
connectorHealth: healthSnapshot(launch.connectorHealth),
|
|
12835
|
+
sessionWriteNoticeClaimed: duties.sessionWriteNoticeClaimed()
|
|
12836
|
+
};
|
|
12837
|
+
}
|
|
12838
|
+
var PINO_LEVELS = {
|
|
12839
|
+
10: "trace",
|
|
12840
|
+
20: "debug",
|
|
12841
|
+
30: "info",
|
|
12842
|
+
40: "warn",
|
|
12843
|
+
50: "error",
|
|
12844
|
+
60: "fatal"
|
|
12845
|
+
};
|
|
12846
|
+
function relayRunnerLog(line, log2) {
|
|
12847
|
+
if (!line.trim()) return;
|
|
12848
|
+
let record = null;
|
|
12849
|
+
try {
|
|
12850
|
+
const parsed = JSON.parse(line);
|
|
12851
|
+
if (parsed && typeof parsed === "object") record = parsed;
|
|
12852
|
+
} catch {
|
|
12853
|
+
record = null;
|
|
12854
|
+
}
|
|
12855
|
+
if (!record || typeof record.level !== "number") {
|
|
12856
|
+
log2.debug({ runnerStderr: line }, "runner: output");
|
|
12857
|
+
return;
|
|
12858
|
+
}
|
|
12859
|
+
const {
|
|
12860
|
+
level,
|
|
12861
|
+
msg,
|
|
12862
|
+
time: _time,
|
|
12863
|
+
pid: _pid,
|
|
12864
|
+
hostname: _hostname,
|
|
12865
|
+
agentName: _agentName,
|
|
12866
|
+
...fields
|
|
12867
|
+
} = record;
|
|
12868
|
+
const method = PINO_LEVELS[level] ?? "info";
|
|
12869
|
+
log2[method](fields, typeof msg === "string" ? msg : "");
|
|
12870
|
+
}
|
|
12871
|
+
function lines(stream, onLine) {
|
|
12872
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
12873
|
+
rl.on("line", onLine);
|
|
12874
|
+
return new Promise((resolve2) => rl.once("close", resolve2));
|
|
12875
|
+
}
|
|
12876
|
+
function giveBackLocks(locks, prefix, remote, launch, log2) {
|
|
12877
|
+
locks.withdraw(prefix);
|
|
12878
|
+
const stop2 = launch.stopRemoteSession ?? ((url, id) => stopRemoteOpencodeSession(url, id));
|
|
12879
|
+
for (const { key, owner } of locks.heldBy(prefix)) {
|
|
12880
|
+
const left = remote.filter((r) => opencodeLockKey(r.serverUrl) === key);
|
|
12881
|
+
if (!left.length) {
|
|
12882
|
+
locks.release(owner);
|
|
12883
|
+
continue;
|
|
12884
|
+
}
|
|
12885
|
+
void (async () => {
|
|
12886
|
+
for (let attempt = 1; ; attempt++) {
|
|
12887
|
+
const stopped = await Promise.all(left.map((r) => stop2(r.serverUrl, r.sessionId))).catch(
|
|
12888
|
+
() => [false]
|
|
12889
|
+
);
|
|
12890
|
+
if (stopped.every(Boolean)) break;
|
|
12891
|
+
if (attempt === 1)
|
|
12892
|
+
log2.warn(
|
|
12893
|
+
{ key, sessions: left.map((r) => r.sessionId) },
|
|
12894
|
+
"runner: a dead runner left work on a shared server that could not be confirmed stopped; holding the server until it is"
|
|
12895
|
+
);
|
|
12896
|
+
await new Promise((resolve2) => setTimeout(resolve2, launch.reconcileRetryMs ?? 5e3));
|
|
12897
|
+
}
|
|
12898
|
+
locks.release(owner);
|
|
12899
|
+
})();
|
|
12900
|
+
}
|
|
12901
|
+
}
|
|
12902
|
+
async function mintForRunner(opts, payload, handleOpts, signal, log2) {
|
|
12903
|
+
try {
|
|
12904
|
+
const minted = await opts.api.mintRunnerCredential({
|
|
12905
|
+
conversationId: payload.conversationId,
|
|
12906
|
+
messageId: payload.messageId,
|
|
12907
|
+
...payload.episode !== void 0 ? { episode: payload.episode } : {},
|
|
12908
|
+
// The one turn the credential will operate: this run's.
|
|
12909
|
+
turnId: handleOpts.turnId
|
|
12910
|
+
});
|
|
12911
|
+
return {
|
|
12912
|
+
ok: true,
|
|
12913
|
+
credential: minted.credential,
|
|
12914
|
+
runnerId: minted.runnerId,
|
|
12915
|
+
deviceGrants: minted.deviceGrants ?? []
|
|
12916
|
+
};
|
|
12917
|
+
} catch (err) {
|
|
12918
|
+
if (signal.aborted) return { ok: false, reason: "cancelled" };
|
|
12919
|
+
const status2 = err instanceof ApiError ? err.status : 0;
|
|
12920
|
+
if (status2 === 404) return { ok: false, reason: "runner_credential_not_found" };
|
|
12921
|
+
if (status2 === 409 && apiErrorCode(err) === "dispatch_not_admitted")
|
|
12922
|
+
return { ok: false, reason: "runner_credential_not_admitted" };
|
|
12923
|
+
log2.warn(apiErrorLogFields(err), "runner: couldn't get a credential for the turn's runner");
|
|
12924
|
+
const reason = `fetch_failed: ${describeError(err)}`;
|
|
12925
|
+
return { ok: false, reason, errorReason: reason };
|
|
12926
|
+
}
|
|
12927
|
+
}
|
|
12928
|
+
async function settleUnstarted(opts, payload, turnId, errorReason, log2) {
|
|
12929
|
+
try {
|
|
12930
|
+
await opts.api.setActiveRun(opts.workspaceId, payload.conversationId, payload.agentId, {
|
|
12931
|
+
activeRunStartedAt: null,
|
|
12932
|
+
turnId,
|
|
12933
|
+
outcome: "failed",
|
|
12934
|
+
...errorReason ? { errorReason: errorReason.slice(0, ERROR_REASON_MAX) } : {},
|
|
12935
|
+
readThrough: payload.messageId
|
|
12936
|
+
});
|
|
12937
|
+
} catch (err) {
|
|
12938
|
+
log2.warn(
|
|
12939
|
+
{ ...apiErrorLogFields(err), turnId },
|
|
12940
|
+
"Couldn't update the turn's status in Cabane. Check the conversation before trying again."
|
|
12941
|
+
);
|
|
12942
|
+
}
|
|
12943
|
+
}
|
|
12944
|
+
async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
12945
|
+
const startedAt = Date.now();
|
|
12946
|
+
const { turnId } = handleOpts;
|
|
12947
|
+
const oomKilled = (id) => (launch.oomKilled ?? turnScopeOomKilled)(id).catch(() => false);
|
|
12948
|
+
const log2 = opts.log.child({
|
|
12949
|
+
workspaceId: opts.workspaceId,
|
|
12950
|
+
conversationId: payload.conversationId,
|
|
12951
|
+
agentId: payload.agentId,
|
|
12952
|
+
messageId: payload.messageId
|
|
12953
|
+
});
|
|
12954
|
+
const controller = new AbortController();
|
|
12955
|
+
duties.registerAbort(turnId, controller);
|
|
12956
|
+
const stopRunner = new AbortController();
|
|
12957
|
+
let ready = false;
|
|
12958
|
+
let child;
|
|
12959
|
+
let stopAnnounced = false;
|
|
12960
|
+
const announceStop = () => {
|
|
12961
|
+
if (stopAnnounced || !child?.stdin.writable) return;
|
|
12962
|
+
stopAnnounced = true;
|
|
12963
|
+
child.stdin.write(encodeLauncherMessage({ cabaneLauncher: "stop" }));
|
|
12964
|
+
};
|
|
12965
|
+
const forwardStop = () => {
|
|
12966
|
+
announceStop();
|
|
12967
|
+
if (ready) stopRunner.abort();
|
|
12968
|
+
};
|
|
12969
|
+
controller.signal.addEventListener("abort", forwardStop, { once: true });
|
|
12970
|
+
const attempt = {
|
|
12971
|
+
workspaceId: opts.workspaceId,
|
|
12972
|
+
conversationId: payload.conversationId,
|
|
12973
|
+
agentId: payload.agentId,
|
|
12974
|
+
turnId
|
|
12975
|
+
};
|
|
12976
|
+
const minted = await mintForRunner(opts, payload, handleOpts, controller.signal, log2);
|
|
12977
|
+
if (!minted.ok) {
|
|
12978
|
+
controller.signal.removeEventListener("abort", forwardStop);
|
|
12979
|
+
duties.releaseAbort(turnId, controller);
|
|
12980
|
+
const durationMs2 = Date.now() - startedAt;
|
|
12981
|
+
await settleUnstarted(opts, payload, turnId, minted.errorReason, log2);
|
|
12982
|
+
duties.notifyEnd({ id: payload.messageId, ok: false, durationMs: durationMs2, reason: minted.reason });
|
|
12983
|
+
return { ok: false, durationMs: durationMs2, reason: minted.reason };
|
|
12984
|
+
}
|
|
12985
|
+
let result;
|
|
12986
|
+
let thrown;
|
|
12987
|
+
let code;
|
|
12988
|
+
let handle;
|
|
12989
|
+
const groups = [];
|
|
12990
|
+
const remote = [];
|
|
12991
|
+
const lockOwner = (id) => `${turnId}#${id}`;
|
|
12992
|
+
const locks = launch.locks;
|
|
12993
|
+
try {
|
|
12994
|
+
handle = await launch.provider.acquire(attempt);
|
|
12995
|
+
} catch (err) {
|
|
12996
|
+
const reason2 = `runner_unavailable: ${describeError(err)}`;
|
|
12997
|
+
log2.warn(
|
|
12998
|
+
{ turnId, provider: launch.provider.name, err: describeError(err) },
|
|
12999
|
+
"runner: no place to run the turn"
|
|
13000
|
+
);
|
|
13001
|
+
controller.signal.removeEventListener("abort", forwardStop);
|
|
13002
|
+
duties.releaseAbort(turnId, controller);
|
|
13003
|
+
await settleUnstarted(opts, payload, turnId, reason2, log2);
|
|
13004
|
+
await launch.reportRunnerGone?.({
|
|
13005
|
+
workspaceId: opts.workspaceId,
|
|
13006
|
+
conversationId: payload.conversationId,
|
|
13007
|
+
agentId: payload.agentId,
|
|
13008
|
+
messageId: payload.messageId,
|
|
13009
|
+
runnerId: minted.runnerId,
|
|
13010
|
+
reason: "provider_failed"
|
|
13011
|
+
});
|
|
13012
|
+
const durationMs2 = Date.now() - startedAt;
|
|
13013
|
+
duties.notifyEnd({ id: payload.messageId, ok: false, durationMs: durationMs2, reason: reason2 });
|
|
13014
|
+
return { ok: false, durationMs: durationMs2, reason: reason2 };
|
|
13015
|
+
}
|
|
13016
|
+
try {
|
|
13017
|
+
const wrapped = launch.wrapped === true;
|
|
13018
|
+
const env = runnerExecEnv(launch.env ?? process.env, wrapped);
|
|
13019
|
+
const grants = minted.deviceGrants.length > 0 ? launch.grants?.(minted.deviceGrants) ?? {} : {};
|
|
13020
|
+
const input = {
|
|
13021
|
+
baseUrl: opts.baseUrl,
|
|
13022
|
+
credential: minted.credential,
|
|
13023
|
+
...wrapped ? { secrets: launch.secrets?.() ?? {} } : {},
|
|
13024
|
+
...Object.keys(grants).length > 0 ? { grants } : {},
|
|
13025
|
+
dispatch: payload,
|
|
13026
|
+
turnId,
|
|
13027
|
+
resumed: handleOpts.resumed,
|
|
13028
|
+
spec: runnerSpec(opts, launch, duties, minted.runnerId)
|
|
13029
|
+
};
|
|
13030
|
+
Object.assign(env, runnerEnv(input));
|
|
13031
|
+
const running = launch.provider.exec(handle, launch.command, env, {
|
|
13032
|
+
signal: stopRunner.signal,
|
|
13033
|
+
stdin: runnerStdin(input)
|
|
13034
|
+
});
|
|
13035
|
+
child = running;
|
|
13036
|
+
if (controller.signal.aborted) announceStop();
|
|
13037
|
+
log2.debug(
|
|
13038
|
+
{ turnId, runnerPid: running.pid, provider: launch.provider.name },
|
|
13039
|
+
"runner: started"
|
|
13040
|
+
);
|
|
13041
|
+
const stdoutDone = lines(running.stdout, (line) => {
|
|
13042
|
+
const event = decodeRunnerEvent(line);
|
|
13043
|
+
if (!event) {
|
|
13044
|
+
log2.debug({ runnerStdout: line }, "runner: output");
|
|
13045
|
+
return;
|
|
13046
|
+
}
|
|
13047
|
+
switch (event.cabaneRunner) {
|
|
13048
|
+
case "ready":
|
|
13049
|
+
ready = true;
|
|
13050
|
+
if (controller.signal.aborted) stopRunner.abort();
|
|
13051
|
+
return;
|
|
13052
|
+
case "start":
|
|
13053
|
+
duties.notifyStart({
|
|
13054
|
+
id: event.id,
|
|
13055
|
+
conversationId: event.conversationId,
|
|
13056
|
+
message: event.message
|
|
13057
|
+
});
|
|
13058
|
+
return;
|
|
13059
|
+
case "end": {
|
|
13060
|
+
const { cabaneRunner: _kind, ...info } = event;
|
|
13061
|
+
duties.notifyEnd(info);
|
|
13062
|
+
return;
|
|
13063
|
+
}
|
|
13064
|
+
case "session_write_notice":
|
|
13065
|
+
duties.markSessionWriteNotice();
|
|
13066
|
+
return;
|
|
13067
|
+
case "connector_health":
|
|
13068
|
+
launch.connectorHealth?.recordSettle(event.runtime, {
|
|
13069
|
+
ok: event.ok,
|
|
13070
|
+
errorReason: event.errorReason
|
|
13071
|
+
});
|
|
13072
|
+
return;
|
|
13073
|
+
case "result":
|
|
13074
|
+
result = event.result;
|
|
13075
|
+
return;
|
|
13076
|
+
case "error":
|
|
13077
|
+
thrown = event.message;
|
|
13078
|
+
return;
|
|
13079
|
+
case "lock":
|
|
13080
|
+
if (!locks) {
|
|
13081
|
+
log2.warn(
|
|
13082
|
+
{ key: event.key },
|
|
13083
|
+
"runner: asked for a device lock this launcher has none of"
|
|
13084
|
+
);
|
|
13085
|
+
return;
|
|
13086
|
+
}
|
|
13087
|
+
locks.request(
|
|
13088
|
+
event.key,
|
|
13089
|
+
lockOwner(event.id),
|
|
13090
|
+
() => running.stdin.write(encodeLauncherMessage({ cabaneLauncher: "granted", id: event.id }))
|
|
13091
|
+
);
|
|
13092
|
+
return;
|
|
13093
|
+
case "unlock":
|
|
13094
|
+
locks?.release(lockOwner(event.id));
|
|
13095
|
+
return;
|
|
13096
|
+
case "process_group":
|
|
13097
|
+
if (!wrapped) groups.push(event.pgid);
|
|
13098
|
+
return;
|
|
13099
|
+
case "remote_session":
|
|
13100
|
+
remote.push({ serverUrl: event.serverUrl, sessionId: event.sessionId });
|
|
13101
|
+
return;
|
|
13102
|
+
case "terminated":
|
|
13103
|
+
void oomKilled(turnId).then((oom) => {
|
|
13104
|
+
if (running.stdin.writable) {
|
|
13105
|
+
running.stdin.write(
|
|
13106
|
+
encodeLauncherMessage({
|
|
13107
|
+
cabaneLauncher: "cause",
|
|
13108
|
+
cause: oom ? "out_of_memory" : null
|
|
13109
|
+
})
|
|
13110
|
+
);
|
|
13111
|
+
}
|
|
13112
|
+
});
|
|
13113
|
+
return;
|
|
13114
|
+
}
|
|
13115
|
+
});
|
|
13116
|
+
const stderrDone = lines(running.stderr, (line) => relayRunnerLog(line, opts.log));
|
|
13117
|
+
code = await running.exit;
|
|
13118
|
+
await Promise.all([stdoutDone, stderrDone]);
|
|
13119
|
+
} finally {
|
|
13120
|
+
if (locks) giveBackLocks(locks, `${turnId}#`, remote, launch, log2);
|
|
13121
|
+
child?.stdin.end();
|
|
13122
|
+
controller.signal.removeEventListener("abort", forwardStop);
|
|
13123
|
+
duties.releaseAbort(turnId, controller);
|
|
13124
|
+
await launch.provider.release(handle, { destroy: false }).catch(
|
|
13125
|
+
(err) => log2.warn({ err: describeError(err) }, "runner: couldn't give its place back")
|
|
13126
|
+
);
|
|
13127
|
+
}
|
|
13128
|
+
if (result) return result;
|
|
13129
|
+
const durationMs = Date.now() - startedAt;
|
|
13130
|
+
const stopped = controller.signal.aborted && thrown === void 0;
|
|
13131
|
+
const reason = stopped ? "cancelled" : thrown !== void 0 ? `runner_failed: ${thrown}` : await oomKilled(turnId) ? "out_of_memory" : `runner_exited: ${code}`;
|
|
13132
|
+
await child?.reap(groups);
|
|
13133
|
+
const reaped = await (launch.reapTurn ?? reapTurnScopes)(turnId).catch((err) => ({
|
|
13134
|
+
failures: [err instanceof Error ? err.message : String(err)]
|
|
13135
|
+
}));
|
|
13136
|
+
if (reaped.failures.length) {
|
|
13137
|
+
log2.warn(
|
|
13138
|
+
{ turnId, failures: reaped.failures },
|
|
13139
|
+
"runner: couldn't stop what the dead runner's turn left running"
|
|
13140
|
+
);
|
|
13141
|
+
}
|
|
13142
|
+
try {
|
|
13143
|
+
await opts.api.setActiveRun(opts.workspaceId, payload.conversationId, payload.agentId, {
|
|
13144
|
+
activeRunStartedAt: null,
|
|
13145
|
+
turnId,
|
|
13146
|
+
...stopped ? { outcome: "interrupted" } : { outcome: "failed", errorReason: reason.slice(0, ERROR_REASON_MAX) },
|
|
13147
|
+
readThrough: payload.messageId
|
|
13148
|
+
});
|
|
13149
|
+
} catch (err) {
|
|
13150
|
+
log2.warn(
|
|
13151
|
+
{ ...apiErrorLogFields(err), turnId },
|
|
13152
|
+
"Couldn't update the turn's status in Cabane. Check the conversation before trying again."
|
|
13153
|
+
);
|
|
13154
|
+
}
|
|
13155
|
+
await launch.reportRunnerGone?.({
|
|
13156
|
+
workspaceId: opts.workspaceId,
|
|
13157
|
+
conversationId: payload.conversationId,
|
|
13158
|
+
agentId: payload.agentId,
|
|
13159
|
+
messageId: payload.messageId,
|
|
13160
|
+
runnerId: minted.runnerId,
|
|
13161
|
+
reason: "exited"
|
|
13162
|
+
});
|
|
13163
|
+
if (thrown !== void 0) throw new Error(thrown);
|
|
13164
|
+
if (stopped) log2.info({ turnId, code }, "runner: stopped before it could report");
|
|
13165
|
+
else log2.warn({ turnId, code }, "runner: exited without reporting a result");
|
|
13166
|
+
duties.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
13167
|
+
return { ok: false, durationMs, reason };
|
|
13168
|
+
}
|
|
13169
|
+
|
|
13170
|
+
// src/dispatcher.ts
|
|
13171
|
+
init_turn_execution();
|
|
13172
|
+
function runKey(conversationId, agentId) {
|
|
13173
|
+
return `${conversationId}|${agentId}`;
|
|
13174
|
+
}
|
|
13175
|
+
var Dispatcher = class {
|
|
13176
|
+
constructor(opts) {
|
|
13177
|
+
this.opts = opts;
|
|
13178
|
+
this.aborts = opts.aborts ?? /* @__PURE__ */ new Map();
|
|
13179
|
+
}
|
|
13180
|
+
opts;
|
|
13181
|
+
// (conversation|agent) → per-turn controllers. A delayed cancel for an old
|
|
13182
|
+
// turn must never reach its successor.
|
|
13183
|
+
aborts;
|
|
13184
|
+
// Pairs already told their session couldn't be saved: the notice is once per
|
|
13185
|
+
// pair, while the rejection repeats every turn.
|
|
13186
|
+
sessionWriteNotified = /* @__PURE__ */ new Set();
|
|
13187
|
+
notifyStart(info) {
|
|
13221
13188
|
try {
|
|
13222
|
-
|
|
13189
|
+
this.opts.observer?.onStart(info);
|
|
13223
13190
|
} catch {
|
|
13224
13191
|
}
|
|
13225
|
-
|
|
13226
|
-
|
|
13192
|
+
}
|
|
13193
|
+
notifyEnd(info) {
|
|
13227
13194
|
try {
|
|
13228
|
-
|
|
13195
|
+
this.opts.observer?.onEnd(info);
|
|
13229
13196
|
} catch {
|
|
13230
13197
|
}
|
|
13231
|
-
throw err;
|
|
13232
|
-
}
|
|
13233
|
-
}
|
|
13234
|
-
function getCredential(deviceId, agentId) {
|
|
13235
|
-
return load(deviceId)[agentId] ?? null;
|
|
13236
|
-
}
|
|
13237
|
-
function setCredential(deviceId, agentId, pat) {
|
|
13238
|
-
if (read(devicePath(deviceId))?.[agentId] === pat) return;
|
|
13239
|
-
const map = load(deviceId);
|
|
13240
|
-
map[agentId] = pat;
|
|
13241
|
-
save(deviceId, map);
|
|
13242
|
-
}
|
|
13243
|
-
function pruneCredentials(deviceId, keepAgentIds) {
|
|
13244
|
-
const keep = new Set(keepAgentIds);
|
|
13245
|
-
if (keep.size === 0) return [];
|
|
13246
|
-
const own = read(devicePath(deviceId));
|
|
13247
|
-
const map = own ?? load(deviceId);
|
|
13248
|
-
const removed = Object.keys(map).filter((id) => !keep.has(id));
|
|
13249
|
-
for (const id of removed) delete map[id];
|
|
13250
|
-
if (removed.length > 0 || own === null) save(deviceId, map);
|
|
13251
|
-
return removed;
|
|
13252
|
-
}
|
|
13253
|
-
function clearCredentials() {
|
|
13254
|
-
rmSync4(credentialsDir(), { recursive: true, force: true });
|
|
13255
|
-
if (existsSync7(legacyPath())) writeFileSync3(legacyPath(), "", { mode: 384 });
|
|
13256
|
-
}
|
|
13257
|
-
|
|
13258
|
-
// src/dispatch-dedupe.ts
|
|
13259
|
-
init_config();
|
|
13260
|
-
import { mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync as renameSync4, existsSync as existsSync8 } from "fs";
|
|
13261
|
-
import { join as join13 } from "path";
|
|
13262
|
-
var MAX_IDS = 256;
|
|
13263
|
-
function dir(log2) {
|
|
13264
|
-
return join13(cabaneDir(), log2);
|
|
13265
|
-
}
|
|
13266
|
-
function pathFor(log2, workspaceId) {
|
|
13267
|
-
return join13(dir(log2), encodeURIComponent(workspaceId));
|
|
13268
|
-
}
|
|
13269
|
-
function readIds(log2, workspaceId) {
|
|
13270
|
-
const path = pathFor(log2, workspaceId);
|
|
13271
|
-
if (!existsSync8(path)) return [];
|
|
13272
|
-
try {
|
|
13273
|
-
return readFileSync5(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
13274
|
-
} catch {
|
|
13275
|
-
return [];
|
|
13276
13198
|
}
|
|
13277
|
-
|
|
13278
|
-
|
|
13279
|
-
|
|
13280
|
-
}
|
|
13281
|
-
|
|
13282
|
-
|
|
13283
|
-
|
|
13284
|
-
|
|
13285
|
-
|
|
13286
|
-
|
|
13287
|
-
|
|
13288
|
-
|
|
13289
|
-
|
|
13290
|
-
|
|
13291
|
-
}
|
|
13292
|
-
|
|
13293
|
-
|
|
13294
|
-
|
|
13295
|
-
|
|
13296
|
-
|
|
13297
|
-
|
|
13298
|
-
|
|
13299
|
-
|
|
13300
|
-
|
|
13301
|
-
|
|
13302
|
-
|
|
13303
|
-
|
|
13304
|
-
|
|
13305
|
-
|
|
13306
|
-
|
|
13307
|
-
|
|
13308
|
-
}
|
|
13309
|
-
|
|
13310
|
-
const out = /* @__PURE__ */ new Map();
|
|
13311
|
-
const path = resumePathFor(workspaceId);
|
|
13312
|
-
if (!existsSync8(path)) return out;
|
|
13313
|
-
try {
|
|
13314
|
-
for (const line of readFileSync5(path, "utf8").split("\n")) {
|
|
13315
|
-
const trimmed = line.trim();
|
|
13316
|
-
if (!trimmed) continue;
|
|
13317
|
-
const tab = trimmed.lastIndexOf(" ");
|
|
13318
|
-
if (tab <= 0) continue;
|
|
13319
|
-
const id = trimmed.slice(0, tab);
|
|
13320
|
-
const count = Number.parseInt(trimmed.slice(tab + 1), 10);
|
|
13321
|
-
if (id && Number.isFinite(count)) out.set(id, count);
|
|
13322
|
-
}
|
|
13323
|
-
} catch {
|
|
13324
|
-
return out;
|
|
13199
|
+
// The supervisor supplies a stable turnId on both fresh and resumed dispatch.
|
|
13200
|
+
// resumed is separate: only replay seeds sequence/speech recovery from the
|
|
13201
|
+
// prior span. A direct caller omitting turnId gets a fresh id.
|
|
13202
|
+
async handle(payload, opts = {}) {
|
|
13203
|
+
const key = runKey(payload.conversationId, payload.agentId);
|
|
13204
|
+
return runTurnInRunner(
|
|
13205
|
+
this.opts.turn,
|
|
13206
|
+
this.opts.runner,
|
|
13207
|
+
{
|
|
13208
|
+
registerAbort: (turnId, controller) => {
|
|
13209
|
+
let pairAborts = this.aborts.get(key);
|
|
13210
|
+
if (!pairAborts) {
|
|
13211
|
+
pairAborts = /* @__PURE__ */ new Map();
|
|
13212
|
+
this.aborts.set(key, pairAborts);
|
|
13213
|
+
}
|
|
13214
|
+
pairAborts.set(turnId, controller);
|
|
13215
|
+
},
|
|
13216
|
+
releaseAbort: (turnId, controller) => {
|
|
13217
|
+
const pair = this.aborts.get(key);
|
|
13218
|
+
if (pair?.get(turnId) !== controller) return;
|
|
13219
|
+
pair.delete(turnId);
|
|
13220
|
+
if (pair.size === 0) this.aborts.delete(key);
|
|
13221
|
+
},
|
|
13222
|
+
sessionWriteNoticeClaimed: () => this.sessionWriteNotified.has(key),
|
|
13223
|
+
markSessionWriteNotice: () => {
|
|
13224
|
+
this.sessionWriteNotified.add(key);
|
|
13225
|
+
},
|
|
13226
|
+
notifyStart: (info) => this.notifyStart(info),
|
|
13227
|
+
notifyEnd: (info) => this.notifyEnd(info)
|
|
13228
|
+
},
|
|
13229
|
+
payload,
|
|
13230
|
+
{ turnId: opts.turnId ?? randomUUID4(), resumed: opts.resumed === true }
|
|
13231
|
+
);
|
|
13325
13232
|
}
|
|
13326
|
-
|
|
13327
|
-
|
|
13328
|
-
|
|
13329
|
-
|
|
13330
|
-
|
|
13331
|
-
|
|
13332
|
-
|
|
13333
|
-
|
|
13334
|
-
|
|
13335
|
-
|
|
13336
|
-
|
|
13337
|
-
|
|
13338
|
-
|
|
13339
|
-
|
|
13340
|
-
return next;
|
|
13341
|
-
}
|
|
13342
|
-
function turnDir() {
|
|
13343
|
-
return join13(cabaneDir(), "turns");
|
|
13344
|
-
}
|
|
13345
|
-
function turnPathFor(workspaceId) {
|
|
13346
|
-
return join13(turnDir(), encodeURIComponent(workspaceId));
|
|
13347
|
-
}
|
|
13348
|
-
var TURN_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
13349
|
-
function readTurnIds(workspaceId) {
|
|
13350
|
-
const out = /* @__PURE__ */ new Map();
|
|
13351
|
-
const path = turnPathFor(workspaceId);
|
|
13352
|
-
if (!existsSync8(path)) return out;
|
|
13353
|
-
try {
|
|
13354
|
-
for (const line of readFileSync5(path, "utf8").split("\n")) {
|
|
13355
|
-
const trimmed = line.trim();
|
|
13356
|
-
if (!trimmed) continue;
|
|
13357
|
-
const tab = trimmed.lastIndexOf(" ");
|
|
13358
|
-
if (tab <= 0) continue;
|
|
13359
|
-
const id = trimmed.slice(0, tab);
|
|
13360
|
-
const turnId = trimmed.slice(tab + 1);
|
|
13361
|
-
if (id && TURN_ID_RE.test(turnId)) out.set(id, turnId);
|
|
13233
|
+
// Cancel the named turn. An older server omits turnId and cancels the pair.
|
|
13234
|
+
cancel(conversationId, agentId, turnId) {
|
|
13235
|
+
const key = runKey(conversationId, agentId);
|
|
13236
|
+
const pair = this.aborts.get(key);
|
|
13237
|
+
if (!pair || pair.size === 0) return false;
|
|
13238
|
+
const controllers = turnId === void 0 ? [...pair.values()] : [pair.get(turnId)];
|
|
13239
|
+
let aborted = false;
|
|
13240
|
+
for (const ac of controllers) {
|
|
13241
|
+
if (!ac) continue;
|
|
13242
|
+
aborted = true;
|
|
13243
|
+
try {
|
|
13244
|
+
ac.abort();
|
|
13245
|
+
} catch {
|
|
13246
|
+
}
|
|
13362
13247
|
}
|
|
13363
|
-
|
|
13364
|
-
|
|
13365
|
-
|
|
13366
|
-
|
|
13367
|
-
|
|
13368
|
-
|
|
13369
|
-
|
|
13370
|
-
|
|
13371
|
-
|
|
13372
|
-
|
|
13373
|
-
|
|
13374
|
-
|
|
13375
|
-
|
|
13376
|
-
|
|
13377
|
-
|
|
13378
|
-
|
|
13379
|
-
return entries.length;
|
|
13380
|
-
}
|
|
13381
|
-
function rememberTurnId(workspaceId, eventId, turnId) {
|
|
13382
|
-
const turns = readTurnIds(workspaceId);
|
|
13383
|
-
if (turns.get(eventId) === turnId) return turns.size;
|
|
13384
|
-
turns.set(eventId, turnId);
|
|
13385
|
-
return writeTurnIds(workspaceId, turns);
|
|
13386
|
-
}
|
|
13387
|
-
function forgetTurnId(workspaceId, eventId) {
|
|
13388
|
-
const turns = readTurnIds(workspaceId);
|
|
13389
|
-
if (!turns.delete(eventId)) return;
|
|
13390
|
-
writeTurnIds(workspaceId, turns);
|
|
13391
|
-
}
|
|
13392
|
-
function noResume() {
|
|
13393
|
-
return process.env.CABANE_COMPANION_NO_RESUME === "1";
|
|
13394
|
-
}
|
|
13395
|
-
|
|
13396
|
-
// src/supervisor.ts
|
|
13397
|
-
init_bundled_binaries();
|
|
13398
|
-
init_install_kind();
|
|
13399
|
-
|
|
13400
|
-
// src/connector-health.ts
|
|
13401
|
-
init_src();
|
|
13402
|
-
var ConnectorHealthStore = class {
|
|
13403
|
-
byRuntime = /* @__PURE__ */ new Map();
|
|
13404
|
-
// Record a settled turn's outcome for `runtime`. A success heals to `ok`; a
|
|
13405
|
-
// classified failure sets `limited`(+reset) / `auth_expired` with the reason as
|
|
13406
|
-
// `last_error`; anything else (a user Stop, a timeout, an unclassified error)
|
|
13407
|
-
// leaves the last-known status untouched — we only ever move state on real
|
|
13408
|
-
// evidence.
|
|
13409
|
-
recordSettle(runtime, outcome) {
|
|
13410
|
-
if (outcome.ok) {
|
|
13411
|
-
this.byRuntime.set(runtime, { quotaState: "ok", limitedUntil: null, lastError: null });
|
|
13412
|
-
return;
|
|
13248
|
+
return aborted;
|
|
13249
|
+
}
|
|
13250
|
+
// Abort every run in flight in THIS process, whatever its conversation. For
|
|
13251
|
+
// an agent taken off this device: nothing it is still running here is
|
|
13252
|
+
// legitimate, and a cancel frame that arrives after it is dropped from the
|
|
13253
|
+
// roster finds no dispatcher to reach. True if any was aborted.
|
|
13254
|
+
cancelAll() {
|
|
13255
|
+
let aborted = false;
|
|
13256
|
+
for (const pair of this.aborts.values()) {
|
|
13257
|
+
for (const ac of [...pair.values()]) {
|
|
13258
|
+
try {
|
|
13259
|
+
ac.abort();
|
|
13260
|
+
aborted = true;
|
|
13261
|
+
} catch {
|
|
13262
|
+
}
|
|
13263
|
+
}
|
|
13413
13264
|
}
|
|
13414
|
-
|
|
13415
|
-
if (!failure) return;
|
|
13416
|
-
const quota = quotaForFailure(failure);
|
|
13417
|
-
if (!quota) return;
|
|
13418
|
-
this.byRuntime.set(runtime, {
|
|
13419
|
-
quotaState: quota.quotaState,
|
|
13420
|
-
limitedUntil: quota.limitedUntil,
|
|
13421
|
-
lastError: outcome.errorReason ?? null
|
|
13422
|
-
});
|
|
13423
|
-
}
|
|
13424
|
-
// The store's current opinion for one runtime, or undefined when it has
|
|
13425
|
-
// no observation. Read by the dispatcher at idle-timeout fire: an idle reap
|
|
13426
|
-
// while this runtime is definitively `limited` (a present, future `limitedUntil`
|
|
13427
|
-
// recorded by a sibling/earlier turn's classified cap settle) is the account cap
|
|
13428
|
-
// surfacing as silence, not a genuine hang — so the settle reclassifies to
|
|
13429
|
-
// `usage_capped` instead of `timeout_idle`. Returns the stored shape verbatim
|
|
13430
|
-
// (the dispatcher reads `quotaState` + `limitedUntil` and applies its own
|
|
13431
|
-
// future-instant gate); a read never mutates state.
|
|
13432
|
-
lookup(runtime) {
|
|
13433
|
-
return this.byRuntime.get(runtime);
|
|
13434
|
-
}
|
|
13435
|
-
// The per-connector reports to attach to a heartbeat — one entry per runtime the
|
|
13436
|
-
// companion has an observation for. Empty until the first classified failure/heal,
|
|
13437
|
-
// so a companion that has seen nothing sends no `connectors[]` and the server's
|
|
13438
|
-
// manifest synthesis (status-less rows) is unaffected.
|
|
13439
|
-
reports() {
|
|
13440
|
-
return [...this.byRuntime.entries()].map(([runtime, h]) => ({
|
|
13441
|
-
runtime,
|
|
13442
|
-
quotaState: h.quotaState,
|
|
13443
|
-
limitedUntil: h.limitedUntil,
|
|
13444
|
-
lastError: h.lastError
|
|
13445
|
-
}));
|
|
13265
|
+
return aborted;
|
|
13446
13266
|
}
|
|
13447
13267
|
};
|
|
13448
13268
|
|
|
13449
|
-
// src/supervisor.ts
|
|
13450
|
-
init_dispatcher();
|
|
13451
|
-
|
|
13452
13269
|
// src/host-report.ts
|
|
13453
13270
|
import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
|
|
13454
13271
|
import { userInfo as userInfo2 } from "os";
|
|
@@ -13487,7 +13304,7 @@ function hostReport() {
|
|
|
13487
13304
|
}
|
|
13488
13305
|
|
|
13489
13306
|
// src/providers/local-process.ts
|
|
13490
|
-
import { spawn as
|
|
13307
|
+
import { spawn as spawn4 } from "child_process";
|
|
13491
13308
|
import { constants as constants2 } from "os";
|
|
13492
13309
|
var DEFAULT_KILL_GRACE_MS = 3e4;
|
|
13493
13310
|
var DEFAULT_REAP_GRACE_MS = 3e3;
|
|
@@ -13498,7 +13315,7 @@ function exitCode(code, signal) {
|
|
|
13498
13315
|
}
|
|
13499
13316
|
function createLocalProcessProvider(opts = {}) {
|
|
13500
13317
|
const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
|
|
13501
|
-
const spawnFn = opts.spawnFn ??
|
|
13318
|
+
const spawnFn = opts.spawnFn ?? spawn4;
|
|
13502
13319
|
const reapGraceMs = opts.reapGraceMs ?? DEFAULT_REAP_GRACE_MS;
|
|
13503
13320
|
const killFn = opts.killFn ?? ((pid, signal) => process.kill(pid, signal));
|
|
13504
13321
|
const groupAlive = (pgid) => {
|
|
@@ -13632,9 +13449,6 @@ var DeviceLocks = class {
|
|
|
13632
13449
|
}
|
|
13633
13450
|
};
|
|
13634
13451
|
|
|
13635
|
-
// src/supervisor.ts
|
|
13636
|
-
init_launch();
|
|
13637
|
-
|
|
13638
13452
|
// src/manifest.ts
|
|
13639
13453
|
var DEVICE_MANIFEST = {
|
|
13640
13454
|
runtimes: [{ name: "claude-code", version: null }],
|
|
@@ -13877,9 +13691,9 @@ var SseSubscriber = class {
|
|
|
13877
13691
|
init_transcript_writer();
|
|
13878
13692
|
|
|
13879
13693
|
// src/updater.ts
|
|
13880
|
-
import { spawn as
|
|
13694
|
+
import { spawn as spawn5 } from "child_process";
|
|
13881
13695
|
import { copyFileSync as copyFileSync2, existsSync as existsSync16, mkdirSync as mkdirSync14, renameSync as renameSync9, rmSync as rmSync10 } from "fs";
|
|
13882
|
-
import { dirname as dirname11, join as
|
|
13696
|
+
import { dirname as dirname11, join as join26 } from "path";
|
|
13883
13697
|
|
|
13884
13698
|
// src/boot-rule.ts
|
|
13885
13699
|
import {
|
|
@@ -13891,7 +13705,7 @@ import {
|
|
|
13891
13705
|
symlinkSync,
|
|
13892
13706
|
writeFileSync as writeFileSync7
|
|
13893
13707
|
} from "fs";
|
|
13894
|
-
import { dirname as dirname8, join as
|
|
13708
|
+
import { dirname as dirname8, join as join23 } from "path";
|
|
13895
13709
|
var UNCONFIRMED_WINDOW_MS = 10 * 6e4;
|
|
13896
13710
|
var MAX_STAGE_ATTEMPTS = 3;
|
|
13897
13711
|
function superseded(pending, ...running) {
|
|
@@ -13919,38 +13733,38 @@ import {
|
|
|
13919
13733
|
lstatSync,
|
|
13920
13734
|
mkdirSync as mkdirSync12,
|
|
13921
13735
|
readFileSync as readFileSync13,
|
|
13922
|
-
readdirSync as
|
|
13736
|
+
readdirSync as readdirSync3,
|
|
13923
13737
|
realpathSync as realpathSync5,
|
|
13924
13738
|
renameSync as renameSync7,
|
|
13925
13739
|
rmSync as rmSync9,
|
|
13926
13740
|
symlinkSync as symlinkSync2
|
|
13927
13741
|
} from "fs";
|
|
13928
|
-
import { dirname as dirname9, join as
|
|
13742
|
+
import { dirname as dirname9, join as join24 } from "path";
|
|
13929
13743
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
13930
13744
|
function versionsDir() {
|
|
13931
|
-
return
|
|
13745
|
+
return join24(cabaneDir(), "versions");
|
|
13932
13746
|
}
|
|
13933
13747
|
function currentLink() {
|
|
13934
|
-
return
|
|
13748
|
+
return join24(cabaneDir(), "current");
|
|
13935
13749
|
}
|
|
13936
13750
|
function bootGuardPath() {
|
|
13937
|
-
return
|
|
13751
|
+
return join24(cabaneDir(), "boot-guard.mjs");
|
|
13938
13752
|
}
|
|
13939
13753
|
function pinnedNodeDir() {
|
|
13940
|
-
return
|
|
13754
|
+
return join24(cabaneDir(), "node");
|
|
13941
13755
|
}
|
|
13942
13756
|
function pinnedNodePath() {
|
|
13943
|
-
return
|
|
13757
|
+
return join24(pinnedNodeDir(), "bin", "node");
|
|
13944
13758
|
}
|
|
13945
13759
|
function versionPackageRoot(version) {
|
|
13946
|
-
return
|
|
13760
|
+
return join24(versionsDir(), version, "node_modules", "@cabane", "companion");
|
|
13947
13761
|
}
|
|
13948
13762
|
function ownPackageRoot(moduleUrl = import.meta.url) {
|
|
13949
13763
|
return dirname9(dirname9(fileURLToPath5(moduleUrl)));
|
|
13950
13764
|
}
|
|
13951
13765
|
function packageVersion(root) {
|
|
13952
13766
|
try {
|
|
13953
|
-
const pkg2 = JSON.parse(readFileSync13(
|
|
13767
|
+
const pkg2 = JSON.parse(readFileSync13(join24(root, "package.json"), "utf8"));
|
|
13954
13768
|
return typeof pkg2.version === "string" ? pkg2.version : null;
|
|
13955
13769
|
} catch {
|
|
13956
13770
|
return null;
|
|
@@ -13959,7 +13773,7 @@ function packageVersion(root) {
|
|
|
13959
13773
|
function currentRoot() {
|
|
13960
13774
|
try {
|
|
13961
13775
|
const root = realpathSync5(currentLink());
|
|
13962
|
-
return existsSync15(
|
|
13776
|
+
return existsSync15(join24(root, "dist", "cli.js")) ? root : null;
|
|
13963
13777
|
} catch {
|
|
13964
13778
|
return null;
|
|
13965
13779
|
}
|
|
@@ -13980,9 +13794,9 @@ function pruneVersions(keep) {
|
|
|
13980
13794
|
const dir2 = versionsDir();
|
|
13981
13795
|
if (!existsSync15(dir2)) return [];
|
|
13982
13796
|
const removed = [];
|
|
13983
|
-
for (const name of
|
|
13797
|
+
for (const name of readdirSync3(dir2)) {
|
|
13984
13798
|
if (keep.includes(name)) continue;
|
|
13985
|
-
rmSync9(
|
|
13799
|
+
rmSync9(join24(dir2, name), { recursive: true, force: true });
|
|
13986
13800
|
removed.push(name);
|
|
13987
13801
|
}
|
|
13988
13802
|
return removed;
|
|
@@ -14002,7 +13816,7 @@ function compareVersions(a, b) {
|
|
|
14002
13816
|
// src/update-state.ts
|
|
14003
13817
|
init_config();
|
|
14004
13818
|
import { mkdirSync as mkdirSync13, readFileSync as readFileSync14, renameSync as renameSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
14005
|
-
import { dirname as dirname10, join as
|
|
13819
|
+
import { dirname as dirname10, join as join25 } from "path";
|
|
14006
13820
|
import { z as z18 } from "zod";
|
|
14007
13821
|
var resultSchema = z18.object({
|
|
14008
13822
|
kind: z18.enum(["updated", "rolled_back", "failed"]),
|
|
@@ -14026,7 +13840,7 @@ var stateSchema = z18.object({
|
|
|
14026
13840
|
failed: z18.object({ version: z18.string(), attempts: z18.number().int(), at: z18.string() }).optional()
|
|
14027
13841
|
});
|
|
14028
13842
|
function updateStatePath(dir2 = cabaneDir()) {
|
|
14029
|
-
return
|
|
13843
|
+
return join25(dir2, "update.json");
|
|
14030
13844
|
}
|
|
14031
13845
|
function readUpdateState(path = updateStatePath()) {
|
|
14032
13846
|
try {
|
|
@@ -14075,7 +13889,7 @@ function rollBack(pending, reason, now = /* @__PURE__ */ new Date()) {
|
|
|
14075
13889
|
return updateUpdateState((state) => rolledBackState(state, pending, reason, now));
|
|
14076
13890
|
}
|
|
14077
13891
|
function writeBootGuard(root = ownPackageRoot()) {
|
|
14078
|
-
const built =
|
|
13892
|
+
const built = join26(root, "dist", "boot-guard.js");
|
|
14079
13893
|
if (!existsSync16(built)) return false;
|
|
14080
13894
|
const target = bootGuardPath();
|
|
14081
13895
|
mkdirSync14(dirname11(target), { recursive: true });
|
|
@@ -14092,7 +13906,7 @@ var runProcess = (command, args, opts = {}) => new Promise((resolve2) => {
|
|
|
14092
13906
|
let stderr = "";
|
|
14093
13907
|
let child;
|
|
14094
13908
|
try {
|
|
14095
|
-
child =
|
|
13909
|
+
child = spawn5(command, [...args], {
|
|
14096
13910
|
stdio: ["ignore", "pipe", "pipe"],
|
|
14097
13911
|
env: opts.env ?? process.env
|
|
14098
13912
|
});
|
|
@@ -14115,17 +13929,17 @@ var runProcess = (command, args, opts = {}) => new Promise((resolve2) => {
|
|
|
14115
13929
|
async function stageVersion(version, harnesses, deps = {}) {
|
|
14116
13930
|
const run = deps.run ?? runProcess;
|
|
14117
13931
|
const node = deps.execPath ?? process.execPath;
|
|
14118
|
-
const target =
|
|
13932
|
+
const target = join26(versionsDir(), version);
|
|
14119
13933
|
const placed = versionPackageRoot(version);
|
|
14120
13934
|
if (existsSync16(placed)) {
|
|
14121
13935
|
if (!await verifyInstall(placed, version, harnesses, { run, execPath: node }))
|
|
14122
13936
|
return { ok: true, root: placed };
|
|
14123
13937
|
}
|
|
14124
|
-
const npm =
|
|
13938
|
+
const npm = join26(dirname11(node), "npm");
|
|
14125
13939
|
if (!existsSync16(npm))
|
|
14126
13940
|
return { ok: false, reason: `there is no npm beside Node at ${dirname11(node)}` };
|
|
14127
13941
|
mkdirSync14(versionsDir(), { recursive: true });
|
|
14128
|
-
const staging =
|
|
13942
|
+
const staging = join26(versionsDir(), `.${version}.staging-${process.pid}`);
|
|
14129
13943
|
rmSync10(staging, { recursive: true, force: true });
|
|
14130
13944
|
const installed = await run(
|
|
14131
13945
|
npm,
|
|
@@ -14154,7 +13968,7 @@ async function stageVersion(version, harnesses, deps = {}) {
|
|
|
14154
13968
|
return { ok: false, reason: `version ${version} isn't published yet`, unpublished: true };
|
|
14155
13969
|
return { ok: false, reason: `npm couldn't install it: ${lastLine(installed.stderr)}` };
|
|
14156
13970
|
}
|
|
14157
|
-
const root =
|
|
13971
|
+
const root = join26(staging, "node_modules", "@cabane", "companion");
|
|
14158
13972
|
const verdict = await verifyInstall(root, version, harnesses, { run, execPath: node });
|
|
14159
13973
|
if (verdict) {
|
|
14160
13974
|
rmSync10(staging, { recursive: true, force: true });
|
|
@@ -14166,7 +13980,7 @@ async function stageVersion(version, harnesses, deps = {}) {
|
|
|
14166
13980
|
}
|
|
14167
13981
|
async function verifyInstall(root, version, harnesses, deps) {
|
|
14168
13982
|
if (packageVersion(root) !== version) return `the installed package isn't version ${version}`;
|
|
14169
|
-
const answered = await deps.run(deps.execPath, [
|
|
13983
|
+
const answered = await deps.run(deps.execPath, [join26(root, "dist", "bin.js"), "--version"], {
|
|
14170
13984
|
timeoutMs: 6e4,
|
|
14171
13985
|
env: { ...process.env, CABANE_LAUNCH_SELF: "1" }
|
|
14172
13986
|
});
|
|
@@ -14174,7 +13988,7 @@ async function verifyInstall(root, version, harnesses, deps) {
|
|
|
14174
13988
|
return `it doesn't start: ${lastLine(answered.stderr) || "no version printed"}`;
|
|
14175
13989
|
}
|
|
14176
13990
|
if (harnesses.length === 0) return null;
|
|
14177
|
-
const probed = await deps.run(deps.execPath, [
|
|
13991
|
+
const probed = await deps.run(deps.execPath, [join26(root, "dist", "postinstall.js"), "--json"], {
|
|
14178
13992
|
timeoutMs: 6e4
|
|
14179
13993
|
});
|
|
14180
13994
|
let resolutions = {};
|
|
@@ -14592,13 +14406,13 @@ var CompanionSupervisor = class {
|
|
|
14592
14406
|
if (this.stopped) return;
|
|
14593
14407
|
this.log.info(
|
|
14594
14408
|
{ protocolVersion: TURN_PROTOCOL_VERSION, version: COMPANION_VERSION },
|
|
14595
|
-
`Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ??
|
|
14409
|
+
`Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ?? hostname()}`
|
|
14596
14410
|
);
|
|
14597
14411
|
const retired = retiredConfigKeysOnDisk();
|
|
14598
14412
|
if (retired.length > 0) {
|
|
14599
14413
|
this.log.warn(
|
|
14600
14414
|
{ keys: retired },
|
|
14601
|
-
`config.json: ${retired.join(", ")} ${retired.length === 1 ? "is" : "are"} no longer read and can be removed
|
|
14415
|
+
`config.json: ${retired.join(", ")} ${retired.length === 1 ? "is" : "are"} no longer read and can be removed.`
|
|
14602
14416
|
);
|
|
14603
14417
|
}
|
|
14604
14418
|
if (this.config.deviceToken) {
|
|
@@ -14638,7 +14452,7 @@ var CompanionSupervisor = class {
|
|
|
14638
14452
|
log: this.log,
|
|
14639
14453
|
lastEventId: null,
|
|
14640
14454
|
onOpen: () => {
|
|
14641
|
-
this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ??
|
|
14455
|
+
this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ?? hostname()}`);
|
|
14642
14456
|
this.deviceConnected = true;
|
|
14643
14457
|
for (const wr of this.workspaces.values()) {
|
|
14644
14458
|
this.hub.setConnected(wr.workspaceId, true);
|
|
@@ -15184,9 +14998,9 @@ var CompanionSupervisor = class {
|
|
|
15184
14998
|
this.runnerCommand ?? selfRunnerCommand()
|
|
15185
14999
|
),
|
|
15186
15000
|
...this.config.runner?.command ? {
|
|
15187
|
-
|
|
15001
|
+
wrapped: true,
|
|
15188
15002
|
secrets: () => loadSecretValuesTolerant(
|
|
15189
|
-
(m) => this.log.warn({ err: m }, "Could not read the secrets file for a
|
|
15003
|
+
(m) => this.log.warn({ err: m }, "Could not read the secrets file for a wrapped turn.")
|
|
15190
15004
|
)
|
|
15191
15005
|
} : {},
|
|
15192
15006
|
// Read per dispatch, for the secret variables its mint named and no others.
|
|
@@ -15865,9 +15679,9 @@ async function waitForBoundedHeartbeat(heartbeat) {
|
|
|
15865
15679
|
}
|
|
15866
15680
|
function defaultReexec() {
|
|
15867
15681
|
clearRuntimeState();
|
|
15868
|
-
void import("child_process").then(({ spawn:
|
|
15682
|
+
void import("child_process").then(({ spawn: spawn7 }) => {
|
|
15869
15683
|
try {
|
|
15870
|
-
const child =
|
|
15684
|
+
const child = spawn7(process.execPath, process.argv.slice(1), {
|
|
15871
15685
|
stdio: "inherit",
|
|
15872
15686
|
detached: false
|
|
15873
15687
|
});
|
|
@@ -15939,9 +15753,9 @@ function handleUncaught(log2, err, origin) {
|
|
|
15939
15753
|
// src/crash-marker.ts
|
|
15940
15754
|
init_config();
|
|
15941
15755
|
import { existsSync as existsSync17, mkdirSync as mkdirSync15, readFileSync as readFileSync15, rmSync as rmSync11, writeFileSync as writeFileSync9 } from "fs";
|
|
15942
|
-
import { join as
|
|
15756
|
+
import { join as join27 } from "path";
|
|
15943
15757
|
function crashMarkerPath() {
|
|
15944
|
-
return
|
|
15758
|
+
return join27(cabaneDir(), "last-error.json");
|
|
15945
15759
|
}
|
|
15946
15760
|
function recordCrash(rec2) {
|
|
15947
15761
|
try {
|
|
@@ -15962,7 +15776,7 @@ function clearCrash() {
|
|
|
15962
15776
|
init_config();
|
|
15963
15777
|
import { spawnSync } from "child_process";
|
|
15964
15778
|
import { readFileSync as readFileSync16 } from "fs";
|
|
15965
|
-
import { join as
|
|
15779
|
+
import { join as join28 } from "path";
|
|
15966
15780
|
var COMPANION_ENTRY = /@cabane\/companion\/|cabane-companion|(^|[\s/])cabane\s|\.cabane\/(versions|current|boot-guard)/;
|
|
15967
15781
|
var START = /\sstart(\s|$)/;
|
|
15968
15782
|
function companionStarts(psOutput) {
|
|
@@ -15979,7 +15793,7 @@ function companionStarts(psOutput) {
|
|
|
15979
15793
|
}
|
|
15980
15794
|
function stateDirOf(env) {
|
|
15981
15795
|
const home = env.CABANE_COMPANION_HOME || env.HOME;
|
|
15982
|
-
return home ?
|
|
15796
|
+
return home ? join28(home, ".cabane") : null;
|
|
15983
15797
|
}
|
|
15984
15798
|
function envFromPsE(output, command) {
|
|
15985
15799
|
const line = output.replace(/\n$/, "");
|
|
@@ -16254,7 +16068,7 @@ import {
|
|
|
16254
16068
|
writeFileSync as writeFileSync10
|
|
16255
16069
|
} from "fs";
|
|
16256
16070
|
import { homedir as homedir9, userInfo as userInfo3 } from "os";
|
|
16257
|
-
import { dirname as dirname12, join as
|
|
16071
|
+
import { dirname as dirname12, join as join29, sep as sep3 } from "path";
|
|
16258
16072
|
function servicePlatform(platform = process.platform) {
|
|
16259
16073
|
if (platform === "linux") return "systemd";
|
|
16260
16074
|
if (platform === "darwin") return "launchd";
|
|
@@ -16272,15 +16086,15 @@ function serviceNode(execPath = process.execPath) {
|
|
|
16272
16086
|
return existsSync18(pinnedNodePath()) ? pinnedNodePath() : execPath;
|
|
16273
16087
|
}
|
|
16274
16088
|
function servicePaths(platform, env = process.env, home = homedir9(), node = serviceNode()) {
|
|
16275
|
-
const unitFile = platform === "systemd" ?
|
|
16276
|
-
env.XDG_CONFIG_HOME ||
|
|
16089
|
+
const unitFile = platform === "systemd" ? join29(
|
|
16090
|
+
env.XDG_CONFIG_HOME || join29(home, ".config"),
|
|
16277
16091
|
"systemd",
|
|
16278
16092
|
"user",
|
|
16279
16093
|
`${serviceName(env)}.service`
|
|
16280
|
-
) :
|
|
16094
|
+
) : join29(home, "Library", "LaunchAgents", `${launchdLabel(env)}.plist`);
|
|
16281
16095
|
return {
|
|
16282
16096
|
unitFile,
|
|
16283
|
-
envFile:
|
|
16097
|
+
envFile: join29(cabaneDir(), "service.env"),
|
|
16284
16098
|
node,
|
|
16285
16099
|
entry: bootGuardPath()
|
|
16286
16100
|
};
|
|
@@ -16347,7 +16161,7 @@ function renderLaunchdPlist(paths, env = process.env, servicePath) {
|
|
|
16347
16161
|
" <key>ThrottleInterval</key><integer>5</integer>",
|
|
16348
16162
|
// The companion writes its own log; the console copy would only grow a second one.
|
|
16349
16163
|
" <key>StandardOutPath</key><string>/dev/null</string>",
|
|
16350
|
-
` <key>StandardErrorPath</key><string>${xml(
|
|
16164
|
+
` <key>StandardErrorPath</key><string>${xml(join29(cabaneDir(), "service-error.log"))}</string>`,
|
|
16351
16165
|
"</dict>",
|
|
16352
16166
|
"</plist>",
|
|
16353
16167
|
""
|
|
@@ -16487,15 +16301,15 @@ async function seedManagedInstall(deps) {
|
|
|
16487
16301
|
}
|
|
16488
16302
|
const target = versionPackageRoot(version);
|
|
16489
16303
|
if (!existsSync18(target)) {
|
|
16490
|
-
const staging =
|
|
16304
|
+
const staging = join29(versionsDir(), `.${version}.seed-${process.pid}`);
|
|
16491
16305
|
rmSync12(staging, { recursive: true, force: true });
|
|
16492
16306
|
try {
|
|
16493
|
-
cpSync(own,
|
|
16307
|
+
cpSync(own, join29(staging, "node_modules", "@cabane", "companion"), {
|
|
16494
16308
|
recursive: true,
|
|
16495
16309
|
verbatimSymlinks: true
|
|
16496
16310
|
});
|
|
16497
|
-
rmSync12(
|
|
16498
|
-
renameSync10(staging,
|
|
16311
|
+
rmSync12(join29(versionsDir(), version), { recursive: true, force: true });
|
|
16312
|
+
renameSync10(staging, join29(versionsDir(), version));
|
|
16499
16313
|
} catch (err) {
|
|
16500
16314
|
rmSync12(staging, { recursive: true, force: true });
|
|
16501
16315
|
return {
|
|
@@ -16506,7 +16320,7 @@ async function seedManagedInstall(deps) {
|
|
|
16506
16320
|
}
|
|
16507
16321
|
const unrunnable = await verifyInstall(target, version, [], deps);
|
|
16508
16322
|
if (unrunnable) {
|
|
16509
|
-
rmSync12(
|
|
16323
|
+
rmSync12(join29(versionsDir(), version), { recursive: true, force: true });
|
|
16510
16324
|
return { ok: false, reason: `The copy of Cabane for the service can't run: ${unrunnable}.` };
|
|
16511
16325
|
}
|
|
16512
16326
|
pointCurrentAt(target);
|
|
@@ -16588,9 +16402,9 @@ function userName() {
|
|
|
16588
16402
|
return process.env.USER ?? "";
|
|
16589
16403
|
}
|
|
16590
16404
|
}
|
|
16591
|
-
function rewriteScriptWrappers(dir2 =
|
|
16405
|
+
function rewriteScriptWrappers(dir2 = join29(cabaneDir(), "bin")) {
|
|
16592
16406
|
for (const name of ["cabane", "cabane-companion"]) {
|
|
16593
|
-
const path =
|
|
16407
|
+
const path = join29(dir2, name);
|
|
16594
16408
|
let text;
|
|
16595
16409
|
try {
|
|
16596
16410
|
text = readFileSync17(path, "utf8");
|
|
@@ -16640,7 +16454,7 @@ async function controlService(action, deps) {
|
|
|
16640
16454
|
init_cabane_command();
|
|
16641
16455
|
init_cli_entry();
|
|
16642
16456
|
init_config();
|
|
16643
|
-
import { spawn as
|
|
16457
|
+
import { spawn as spawn6 } from "child_process";
|
|
16644
16458
|
import { closeSync as closeSync3, mkdirSync as mkdirSync17, openSync as openSync4 } from "fs";
|
|
16645
16459
|
init_config();
|
|
16646
16460
|
async function startDaemon(opts = {}, deps = {}) {
|
|
@@ -16722,7 +16536,7 @@ function defaultSpawnDetached(args) {
|
|
|
16722
16536
|
mkdirSync17(cabaneDir(), { recursive: true });
|
|
16723
16537
|
const logFd = openSync4(companionLogPath(), "a");
|
|
16724
16538
|
try {
|
|
16725
|
-
const child =
|
|
16539
|
+
const child = spawn6(process.execPath, [cliPath, ...args], {
|
|
16726
16540
|
detached: true,
|
|
16727
16541
|
stdio: ["ignore", logFd, logFd],
|
|
16728
16542
|
env: { ...process.env, CABANE_COMPANION_DAEMON: "1" }
|
|
@@ -17148,7 +16962,7 @@ async function setupScript(opts, deps) {
|
|
|
17148
16962
|
}
|
|
17149
16963
|
device = await (deps.claim ?? claimSetupCode)(baseUrl, {
|
|
17150
16964
|
userCode: opts.code,
|
|
17151
|
-
label: deviceLabelFromHostname(
|
|
16965
|
+
label: deviceLabelFromHostname(hostname2()) ?? "Computer",
|
|
17152
16966
|
...paired && cfg?.deviceToken ? { deviceToken: cfg.deviceToken } : {}
|
|
17153
16967
|
});
|
|
17154
16968
|
writePairedConfig(device);
|
|
@@ -17347,9 +17161,9 @@ async function logs(opts = {}) {
|
|
|
17347
17161
|
init_cabane_command();
|
|
17348
17162
|
init_install_kind();
|
|
17349
17163
|
init_cabane_command();
|
|
17350
|
-
import { readdirSync as
|
|
17164
|
+
import { readdirSync as readdirSync4, realpathSync as realpathSync6, existsSync as existsSync19 } from "fs";
|
|
17351
17165
|
import { homedir as homedir10 } from "os";
|
|
17352
|
-
import { dirname as dirname13, join as
|
|
17166
|
+
import { dirname as dirname13, join as join30 } from "path";
|
|
17353
17167
|
init_config();
|
|
17354
17168
|
init_secrets();
|
|
17355
17169
|
init_term();
|
|
@@ -17635,7 +17449,7 @@ function versionLine() {
|
|
|
17635
17449
|
const install = companionInstall();
|
|
17636
17450
|
const own = installedCommandPath({ install });
|
|
17637
17451
|
if (install === "script") {
|
|
17638
|
-
return `Cabane ${COMPANION_VERSION} \xB7 installed by the install script (${tildePath(
|
|
17452
|
+
return `Cabane ${COMPANION_VERSION} \xB7 installed by the install script (${tildePath(join30(homedir10(), ".cabane"))})`;
|
|
17639
17453
|
}
|
|
17640
17454
|
if (own)
|
|
17641
17455
|
return `Cabane ${COMPANION_VERSION} \xB7 installed with npm (${tildePath(dirname13(dirname13(own)))})`;
|
|
@@ -17661,7 +17475,7 @@ function otherInstall(install, own) {
|
|
|
17661
17475
|
}
|
|
17662
17476
|
};
|
|
17663
17477
|
if (install === "npm") {
|
|
17664
|
-
const script =
|
|
17478
|
+
const script = join30(
|
|
17665
17479
|
homedir10(),
|
|
17666
17480
|
".cabane",
|
|
17667
17481
|
"node",
|
|
@@ -17670,11 +17484,11 @@ function otherInstall(install, own) {
|
|
|
17670
17484
|
"@cabane",
|
|
17671
17485
|
"companion"
|
|
17672
17486
|
);
|
|
17673
|
-
return existsSync19(script) ?
|
|
17487
|
+
return existsSync19(script) ? join30(homedir10(), ".cabane") : null;
|
|
17674
17488
|
}
|
|
17675
17489
|
const found = firstOnPath("cabane-companion", process.env.PATH);
|
|
17676
17490
|
if (!found) return null;
|
|
17677
|
-
const ownAlias = own ? real(
|
|
17491
|
+
const ownAlias = own ? real(join30(dirname13(own), "cabane-companion")) : null;
|
|
17678
17492
|
return real(found) === ownAlias ? null : dirname13(found);
|
|
17679
17493
|
}
|
|
17680
17494
|
function runningMode(running, service2) {
|
|
@@ -17712,7 +17526,7 @@ function transcriptsLine() {
|
|
|
17712
17526
|
let count = 0;
|
|
17713
17527
|
let newest;
|
|
17714
17528
|
try {
|
|
17715
|
-
const files =
|
|
17529
|
+
const files = readdirSync4(dir2).filter((f) => f.endsWith(".jsonl")).sort();
|
|
17716
17530
|
count = files.length;
|
|
17717
17531
|
newest = files[files.length - 1];
|
|
17718
17532
|
} catch {
|
|
@@ -17880,8 +17694,8 @@ init_cabane_command();
|
|
|
17880
17694
|
init_errors();
|
|
17881
17695
|
init_src();
|
|
17882
17696
|
init_transcript_writer();
|
|
17883
|
-
import { existsSync as existsSync20, readFileSync as readFileSync18, readdirSync as
|
|
17884
|
-
import { basename as basename4, isAbsolute as isAbsolute2, join as
|
|
17697
|
+
import { existsSync as existsSync20, readFileSync as readFileSync18, readdirSync as readdirSync5 } from "fs";
|
|
17698
|
+
import { basename as basename4, isAbsolute as isAbsolute2, join as join31 } from "path";
|
|
17885
17699
|
async function transcript(opts = {}) {
|
|
17886
17700
|
const dir2 = transcriptsDir();
|
|
17887
17701
|
if (opts.follow) {
|
|
@@ -17898,7 +17712,7 @@ async function transcript(opts = {}) {
|
|
|
17898
17712
|
process.stdout.write(emptyMessage(dir2));
|
|
17899
17713
|
return;
|
|
17900
17714
|
}
|
|
17901
|
-
process.stdout.write(renderFile(
|
|
17715
|
+
process.stdout.write(renderFile(join31(dir2, newest)) + "\n");
|
|
17902
17716
|
return;
|
|
17903
17717
|
}
|
|
17904
17718
|
printList(dir2);
|
|
@@ -17981,7 +17795,7 @@ function isComplete(content) {
|
|
|
17981
17795
|
async function followTranscripts(dir2) {
|
|
17982
17796
|
const follower = new TranscriptFollower({
|
|
17983
17797
|
listFiles: () => listFiles(dir2),
|
|
17984
|
-
read: (f) => readFileSync18(
|
|
17798
|
+
read: (f) => readFileSync18(join31(dir2, f), "utf8"),
|
|
17985
17799
|
write: (s) => process.stdout.write(s),
|
|
17986
17800
|
// CSI: cursor up `n` lines, then erase from cursor to end of screen.
|
|
17987
17801
|
clearLines: (n) => process.stdout.write(`\x1B[${n}A\x1B[0J`),
|
|
@@ -18007,7 +17821,7 @@ async function followTranscripts(dir2) {
|
|
|
18007
17821
|
function listTranscriptFiles(dir2) {
|
|
18008
17822
|
let entries;
|
|
18009
17823
|
try {
|
|
18010
|
-
entries =
|
|
17824
|
+
entries = readdirSync5(dir2, { withFileTypes: true });
|
|
18011
17825
|
} catch {
|
|
18012
17826
|
return [];
|
|
18013
17827
|
}
|
|
@@ -18017,7 +17831,7 @@ function listTranscriptFiles(dir2) {
|
|
|
18017
17831
|
if (!entry.isDirectory() || entry.name === "anomalies") continue;
|
|
18018
17832
|
let inner;
|
|
18019
17833
|
try {
|
|
18020
|
-
inner =
|
|
17834
|
+
inner = readdirSync5(join31(dir2, entry.name));
|
|
18021
17835
|
} catch {
|
|
18022
17836
|
continue;
|
|
18023
17837
|
}
|
|
@@ -18036,7 +17850,7 @@ function printList(dir2) {
|
|
|
18036
17850
|
|
|
18037
17851
|
`);
|
|
18038
17852
|
for (const f of files.slice(0, 20)) {
|
|
18039
|
-
const { meta, outcome } = peek(
|
|
17853
|
+
const { meta, outcome } = peek(join31(dir2, f));
|
|
18040
17854
|
const when = fmtTime(rec(meta)?.ts);
|
|
18041
17855
|
const ws = str2(rec(meta)?.workspaceSlug);
|
|
18042
17856
|
const o = rec(outcome);
|
|
@@ -18072,13 +17886,13 @@ function peek(path) {
|
|
|
18072
17886
|
function resolveTarget(dir2, target) {
|
|
18073
17887
|
if (isAbsolute2(target) || target.includes("/")) {
|
|
18074
17888
|
if (existsSync20(target)) return target;
|
|
18075
|
-
if (!isAbsolute2(target) && existsSync20(
|
|
17889
|
+
if (!isAbsolute2(target) && existsSync20(join31(dir2, target))) return join31(dir2, target);
|
|
18076
17890
|
throw new CompanionError(`no transcript at ${target}.`);
|
|
18077
17891
|
}
|
|
18078
|
-
const exact =
|
|
17892
|
+
const exact = join31(dir2, target);
|
|
18079
17893
|
if (existsSync20(exact)) return exact;
|
|
18080
17894
|
const matches = listFiles(dir2).filter((f) => f.includes(target));
|
|
18081
|
-
if (matches.length === 1) return
|
|
17895
|
+
if (matches.length === 1) return join31(dir2, matches[0]);
|
|
18082
17896
|
if (matches.length === 0) {
|
|
18083
17897
|
throw new CompanionError(
|
|
18084
17898
|
`no transcript matching "${target}" in ${dir2}. Run \`${cabane("transcript")}\` to list them.`
|