@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/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
- // Newline-joined — workspace paths never contain a newline, so a
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 retiredConfigKeys(raw) {
491
- if (!raw || typeof raw !== "object") return [];
492
- const obj = raw;
460
+ function isObject(value) {
461
+ return !!value && typeof value === "object" && !Array.isArray(value);
462
+ }
463
+ function splitRetired(raw) {
493
464
  const keys = [];
494
- if ("runners" in obj) keys.push("runners");
495
- const agents = obj.agents;
496
- if (agents && typeof agents === "object") {
497
- for (const [key, row3] of Object.entries(agents)) {
498
- if (row3 && typeof row3 === "object" && "sandbox" in row3) keys.push(`agents.${key}.sandbox`);
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
- if (retiredConfigKeys(raw).length === 0) return raw;
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` (an agent woke in `/home/joshed` this way on
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 box of their own making, ENG225); the companion knows nothing of it
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`, `boxed`).
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 (ENG210)",
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 (ENG210)",
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
- ...box ? { box } : {}
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
- box: "CABANE_BOX"
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/runner/launch.ts
8017
- import { createInterface } from "readline";
8018
- function selfRunnerCommand() {
8019
- const script = process.argv[1];
8020
- if (!script) throw new Error("runner: cannot tell which script started this companion");
8021
- return [process.execPath, ...process.execArgv, script, "run-turn"];
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 wrappedRunnerCommand(prefix, own) {
8024
- return [...prefix ?? [], ...own];
7997
+ function environmentSafe(value) {
7998
+ return !value.includes("\0");
8025
7999
  }
8026
- function runnerExecEnv(base, boxed) {
8027
- const env = {};
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 healthSnapshot(store) {
8003
+ function readGrants(keys, report) {
8004
+ if (keys.length === 0) return {};
8005
+ const file = loadGrantsFile(report);
8036
8006
  const out = {};
8037
- for (const r of store?.reports() ?? []) {
8038
- out[r.runtime] = {
8039
- quotaState: r.quotaState ?? "unknown",
8040
- limitedUntil: r.limitedUntil ?? null,
8041
- lastError: r.lastError ?? null
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 runnerSpec(opts, launch, duties, runnerId) {
8047
- return {
8048
- runnerId,
8049
- ...opts.deviceId ? { deviceId: opts.deviceId } : {},
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
- const parsed = JSON.parse(line);
8078
- if (parsed && typeof parsed === "object") record = parsed;
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
- record = null;
8034
+ report.unreadable(`${path} is not valid JSON.`);
8035
+ return {};
8081
8036
  }
8082
- if (!record || typeof record.level !== "number") {
8083
- log2.debug({ runnerStderr: line }, "runner: output");
8084
- return;
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
- const {
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 lines(stream, onLine) {
8099
- const rl = createInterface({ input: stream, crlfDelay: Infinity });
8100
- rl.on("line", onLine);
8101
- return new Promise((resolve2) => rl.once("close", resolve2));
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 giveBackLocks(locks, prefix, remote, launch, log2) {
8104
- locks.withdraw(prefix);
8105
- const stop2 = launch.stopRemoteSession ?? ((url, id) => stopRemoteOpencodeSession(url, id));
8106
- for (const { key, owner } of locks.heldBy(prefix)) {
8107
- const left = remote.filter((r) => opencodeLockKey(r.serverUrl) === key);
8108
- if (!left.length) {
8109
- locks.release(owner);
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
- async function mintForRunner(opts, payload, handleOpts, signal, log2) {
8130
- try {
8131
- const minted = await opts.api.mintRunnerCredential({
8132
- conversationId: payload.conversationId,
8133
- messageId: payload.messageId,
8134
- ...payload.episode !== void 0 ? { episode: payload.episode } : {},
8135
- // The one turn the credential will operate: this run's.
8136
- turnId: handleOpts.turnId
8137
- });
8138
- return {
8139
- ok: true,
8140
- credential: minted.credential,
8141
- runnerId: minted.runnerId,
8142
- deviceGrants: minted.deviceGrants ?? []
8143
- };
8144
- } catch (err) {
8145
- if (signal.aborted) return { ok: false, reason: "cancelled" };
8146
- const status2 = err instanceof ApiError ? err.status : 0;
8147
- if (status2 === 404) return { ok: false, reason: "runner_credential_not_found" };
8148
- if (status2 === 409 && apiErrorCode(err) === "dispatch_not_admitted")
8149
- return { ok: false, reason: "runner_credential_not_admitted" };
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
- async function settleUnstarted(opts, payload, turnId, errorReason, log2) {
8094
+ function directoryExists(path) {
8156
8095
  try {
8157
- await opts.api.setActiveRun(opts.workspaceId, payload.conversationId, payload.agentId, {
8158
- activeRunStartedAt: null,
8159
- turnId,
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
- async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
8172
- const startedAt = Date.now();
8173
- const { turnId } = handleOpts;
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
- const boxed = launch.boxed === true;
8245
- const env = runnerExecEnv(launch.env ?? process.env, boxed);
8246
- const grants = minted.deviceGrants.length > 0 ? launch.grants?.(minted.deviceGrants) ?? {} : {};
8247
- const input = {
8248
- baseUrl: opts.baseUrl,
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
- const stdoutDone = lines(running.stdout, (line) => {
8269
- const event = decodeRunnerEvent(line);
8270
- if (!event) {
8271
- log2.debug({ runnerStdout: line }, "runner: output");
8272
- return;
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
- switch (event.cabaneRunner) {
8275
- case "ready":
8276
- ready = true;
8277
- if (controller.signal.aborted) stopRunner.abort();
8278
- return;
8279
- case "start":
8280
- duties.notifyStart({
8281
- id: event.id,
8282
- conversationId: event.conversationId,
8283
- message: event.message
8284
- });
8285
- return;
8286
- case "end": {
8287
- const { cabaneRunner: _kind, ...info } = event;
8288
- duties.notifyEnd(info);
8289
- return;
8290
- }
8291
- case "session_write_notice":
8292
- duties.markSessionWriteNotice();
8293
- return;
8294
- case "connector_health":
8295
- launch.connectorHealth?.recordSettle(event.runtime, {
8296
- ok: event.ok,
8297
- errorReason: event.errorReason
8298
- });
8299
- return;
8300
- case "result":
8301
- result = event.result;
8302
- return;
8303
- case "error":
8304
- thrown = event.message;
8305
- return;
8306
- case "lock":
8307
- if (!locks) {
8308
- log2.warn(
8309
- { key: event.key },
8310
- "runner: asked for a device lock this launcher has none of"
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
- locks.request(
8315
- event.key,
8316
- lockOwner(event.id),
8317
- () => running.stdin.write(encodeLauncherMessage({ cabaneLauncher: "granted", id: event.id }))
8318
- );
8319
- return;
8320
- case "unlock":
8321
- locks?.release(lockOwner(event.id));
8322
- return;
8323
- case "process_group":
8324
- if (!boxed) groups.push(event.pgid);
8325
- return;
8326
- case "remote_session":
8327
- remote.push({ serverUrl: event.serverUrl, sessionId: event.sessionId });
8328
- return;
8329
- case "terminated":
8330
- void oomKilled(turnId).then((oom) => {
8331
- if (running.stdin.writable) {
8332
- running.stdin.write(
8333
- encodeLauncherMessage({
8334
- cabaneLauncher: "cause",
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
- return;
8341
- }
8342
- });
8343
- const stderrDone = lines(running.stderr, (line) => relayRunnerLog(line, opts.log));
8344
- code = await running.exit;
8345
- await Promise.all([stdoutDone, stderrDone]);
8346
- } finally {
8347
- if (locks) giveBackLocks(locks, `${turnId}#`, remote, launch, log2);
8348
- child?.stdin.end();
8349
- controller.signal.removeEventListener("abort", forwardStop);
8350
- duties.releaseAbort(turnId, controller);
8351
- await launch.provider.release(handle, { destroy: false }).catch(
8352
- (err) => log2.warn({ err: describeError(err) }, "runner: couldn't give its place back")
8353
- );
8354
- }
8355
- if (result) return result;
8356
- const durationMs = Date.now() - startedAt;
8357
- const stopped = controller.signal.aborted && thrown === void 0;
8358
- const reason = stopped ? "cancelled" : thrown !== void 0 ? `runner_failed: ${thrown}` : await oomKilled(turnId) ? "out_of_memory" : `runner_exited: ${code}`;
8359
- await child?.reap(groups);
8360
- const reaped = await (launch.reapTurn ?? reapTurnScopes)(turnId).catch((err) => ({
8361
- failures: [err instanceof Error ? err.message : String(err)]
8362
- }));
8363
- if (reaped.failures.length) {
8364
- log2.warn(
8365
- { turnId, failures: reaped.failures },
8366
- "runner: couldn't stop what the dead runner's turn left running"
8367
- );
8368
- }
8369
- try {
8370
- await opts.api.setActiveRun(opts.workspaceId, payload.conversationId, payload.agentId, {
8371
- activeRunStartedAt: null,
8372
- turnId,
8373
- ...stopped ? { outcome: "interrupted" } : { outcome: "failed", errorReason: reason.slice(0, ERROR_REASON_MAX) },
8374
- readThrough: payload.messageId
8375
- });
8376
- } catch (err) {
8377
- log2.warn(
8378
- { ...apiErrorLogFields(err), turnId },
8379
- "Couldn't update the turn's status in Cabane. Check the conversation before trying again."
8380
- );
8381
- }
8382
- await launch.reportRunnerGone?.({
8383
- workspaceId: opts.workspaceId,
8384
- conversationId: payload.conversationId,
8385
- agentId: payload.agentId,
8386
- messageId: payload.messageId,
8387
- runnerId: minted.runnerId,
8388
- reason: "exited"
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 BOXED_ENV_DROPPED, PINO_LEVELS;
8397
- var init_launch = __esm({
8398
- "src/runner/launch.ts"() {
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
- init_opencode_lock();
8402
- init_api_error_shape();
8302
+ init_descriptions();
8403
8303
  init_errors();
8404
- init_boot_id();
8405
- init_turn_containment();
8406
- init_protocol();
8407
- BOXED_ENV_DROPPED = [
8408
- "ANTHROPIC_API_KEY",
8409
- "CLAUDE_CODE_OAUTH_TOKEN",
8410
- "CODEX_API_KEY",
8411
- "GH_TOKEN",
8412
- "GITHUB_TOKEN",
8413
- "DBUS_SESSION_BUS_ADDRESS",
8414
- "XDG_RUNTIME_DIR"
8415
- ];
8416
- PINO_LEVELS = {
8417
- 10: "trace",
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/grants.ts
8428
- import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
8429
- import { join as join14 } from "path";
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 grantsPath() {
8438
- return join14(cabaneDir(), "grants.json");
8325
+ function turnControlMcpUrl(baseUrl) {
8326
+ return `${trimSlash2(baseUrl)}/api/turn-control`;
8439
8327
  }
8440
- function readGrants(keys, report) {
8441
- if (keys.length === 0) return {};
8442
- const file = loadGrantsFile(report);
8443
- const out = {};
8444
- for (const key of keys) {
8445
- if (!allowedGrantVariable(key) || !Object.hasOwn(file, key)) continue;
8446
- const entry = {};
8447
- for (const [variable, value] of Object.entries(file[key])) {
8448
- if (allowedGrantVariable(variable) && environmentSafe(value)) entry[variable] = value;
8449
- else report.refusedVariable(key, variable);
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
- out[key] = entry;
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
- return out;
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 loadGrantsFile(report) {
8456
- const path = grantsPath();
8457
- if (!existsSync9(path)) return {};
8458
- let raw;
8380
+ async function readCodexConfigSetsSandboxMode(path = codexConfigPath()) {
8381
+ let text;
8459
8382
  try {
8460
- raw = readFileSync7(path, "utf8");
8461
- } catch (err) {
8462
- const code = err.code ?? "unknown error";
8463
- report.unreadable(`couldn't read ${path}: ${code}`);
8464
- return {};
8383
+ text = await readFile2(path, "utf8");
8384
+ } catch {
8385
+ return false;
8465
8386
  }
8466
- if (raw.trim().length === 0) return {};
8467
- let parsed;
8387
+ return configSetsSandboxMode(text);
8388
+ }
8389
+ function configSetsSandboxMode(text) {
8468
8390
  try {
8469
- parsed = JSON.parse(raw);
8391
+ return parse(text).sandbox_mode !== void 0;
8470
8392
  } catch {
8471
- report.unreadable(`${path} is not valid JSON.`);
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
- function harnessSecretEnv(parts) {
8484
- const env = {};
8485
- const dropped = [];
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
- return { env, dropped };
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 grantEnv(sent, issuedBySecret, issued) {
8497
- const env = {};
8498
- for (const [key, entry] of Object.entries(sent ?? {})) {
8499
- if (!Object.hasOwn(issuedBySecret, key)) continue;
8500
- for (const [variable, value] of Object.entries(entry)) {
8501
- if (Object.hasOwn(issued, variable) || Object.hasOwn(env, variable)) continue;
8502
- env[variable] = value;
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 env;
8509
+ return null;
8506
8510
  }
8507
- var VARIABLE_NAME_RE, grantsFileSchema;
8508
- var init_grants = __esm({
8509
- "src/grants.ts"() {
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
- init_config();
8512
- VARIABLE_NAME_RE = /^[A-Z_][A-Z0-9_]*$/;
8513
- grantsFileSchema = z15.record(z15.string(), z15.record(z15.string(), z15.string()));
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/work-dir.ts
8518
- import { statSync as statSync3 } from "fs";
8519
- import { join as join15 } from "path";
8520
- function pairDirectory(root, pair) {
8521
- return join15(root, pair.workspaceSlug, pair.agentUsername, pair.conversationId);
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
- return { cwd: pairDir, skipped };
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 directoryExists(path) {
8545
+ function readOutboxFloor(read2, turnId, log2) {
8532
8546
  try {
8533
- return statSync3(path).isDirectory();
8534
- } catch {
8535
- return false;
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 init_work_dir = __esm({
8539
- "src/work-dir.ts"() {
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/turn-control-tools.ts
8545
- import { randomUUID as randomUUID2 } from "crypto";
8546
- import { z as z16 } from "zod";
8547
- function createSkipState() {
8548
- return { skipped: false, reason: null };
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 toolError(text) {
8551
- return { isError: true, content: [{ type: "text", text }] };
8577
+ function conversationDir(workspaceId, conversationId) {
8578
+ return join19(dirFor(workspaceId), encodeURIComponent(conversationId));
8552
8579
  }
8553
- async function performAct(acts, act) {
8554
- let result;
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
- result = await acts.api.turnAct(
8557
- acts.workspaceId,
8558
- acts.turnId,
8559
- act,
8560
- AbortSignal.timeout(TURN_ACT_TIMEOUT_MS)
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 uncertainOutcome(err instanceof Error ? err.message : String(err));
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 createTurnControlMcpServer(acts, skipState) {
8585
- const ACT_TOOL = { readOnlyHint: false, destructiveHint: false, openWorldHint: false };
8586
- const CANCEL_TOOL = { ...ACT_TOOL, idempotentHint: true };
8587
- const RECORDER_TOOL = { readOnlyHint: true, openWorldHint: false };
8588
- return createSdkMcpServer({
8589
- name: COMPANION_LOCAL_MCP_SERVER,
8590
- version: "0.0.0",
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 trimSlash2(s) {
8800
- return s.endsWith("/") ? s.slice(0, -1) : s;
8607
+ function clearPrepared(workspaceId, conversationId, agentId) {
8608
+ rmSync5(pathFor2(workspaceId, conversationId, agentId), { force: true });
8801
8609
  }
8802
- var init_build_options = __esm({
8803
- "src/build-options.ts"() {
8610
+ var init_prepared = __esm({
8611
+ "src/prepared.ts"() {
8804
8612
  "use strict";
8805
- init_turn_control_tools();
8613
+ init_config();
8806
8614
  }
8807
8615
  });
8808
8616
 
8809
- // src/codex-config.ts
8810
- import { readFile as readFile2 } from "fs/promises";
8811
- import { homedir as homedir7 } from "os";
8812
- import { join as join16 } from "path";
8813
- import { parse } from "smol-toml";
8814
- function codexConfigPath(env = process.env, home = homedir7()) {
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
- async function readCodexConfigSetsSandboxMode(path = codexConfigPath()) {
8818
- let text;
8624
+ function loadSecretValues() {
8625
+ const path = secretsPath();
8626
+ if (!existsSync11(path)) return {};
8627
+ let raw;
8819
8628
  try {
8820
- text = await readFile2(path, "utf8");
8821
- } catch {
8822
- return false;
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
- return configSetsSandboxMode(text);
8825
- }
8826
- function configSetsSandboxMode(text) {
8635
+ if (raw.trim().length === 0) return {};
8636
+ let parsed;
8827
8637
  try {
8828
- return parse(text).sandbox_mode !== void 0;
8829
- } catch {
8830
- return false;
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
- var init_codex_config = __esm({
8834
- "src/codex-config.ts"() {
8835
- "use strict";
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
- // src/codex-images.ts
8840
- import { mkdtemp, rm, writeFile } from "fs/promises";
8841
- import { tmpdir } from "os";
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 message(err) {
8891
- return err instanceof Error ? err.message : String(err);
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
- var FETCH_TIMEOUT_MS, EXTENSIONS;
8894
- var init_codex_images = __esm({
8895
- "src/codex-images.ts"() {
8896
- "use strict";
8897
- FETCH_TIMEOUT_MS = 3e4;
8898
- EXTENSIONS = {
8899
- "image/png": "png",
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
- // src/codex-compactions.ts
8908
- import { createReadStream } from "fs";
8909
- import { readdir } from "fs/promises";
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
- async function findRollout(codexHome, threadId) {
8924
- const id = UUID_V7.exec(threadId);
8925
- if (!id) return null;
8926
- const createdAt = parseInt(`${id[1]}${id[2]}`, 16);
8927
- const suffix = `-${threadId}.jsonl`;
8928
- for (const offset of [0, -DAY_MS, DAY_MS]) {
8929
- const day = new Date(createdAt + offset);
8930
- const dir2 = join18(
8931
- codexHome,
8932
- "sessions",
8933
- String(day.getUTCFullYear()),
8934
- String(day.getUTCMonth() + 1).padStart(2, "0"),
8935
- String(day.getUTCDate()).padStart(2, "0")
8936
- );
8937
- let names;
8938
- try {
8939
- names = await readdir(dir2);
8940
- } catch {
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
- const name = names.find((n) => n.startsWith("rollout-") && n.endsWith(suffix));
8944
- if (name) return join18(dir2, name);
8725
+ servers[name] = next;
8945
8726
  }
8946
- return null;
8727
+ return { servers, secretEnv, missing: [...missing].sort() };
8947
8728
  }
8948
- async function countCompactions(rollout) {
8949
- const lines2 = createInterface2({
8950
- input: createReadStream(rollout, { encoding: "utf8" }),
8951
- crlfDelay: Infinity
8952
- });
8953
- let count = 0;
8954
- for await (const line of lines2) {
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 count;
8737
+ return `${prefix}+${missing.length}`;
8958
8738
  }
8959
- var DAY_MS, UUID_V7, RECORD_HEAD_CHARS;
8960
- var init_codex_compactions = __esm({
8961
- "src/codex-compactions.ts"() {
8739
+ var PLACEHOLDER_RE, secretStoreSchema, SECRET_ENV_NAME;
8740
+ var init_secrets = __esm({
8741
+ "src/secrets.ts"() {
8962
8742
  "use strict";
8963
- DAY_MS = 864e5;
8964
- UUID_V7 = /^([0-9a-f]{8})-([0-9a-f]{4})-7[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
8965
- RECORD_HEAD_CHARS = 200;
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-seq-floor.ts
8970
- function resolveSeqFloor(sources, ctx2) {
8971
- const { serverFloor, outboxFloor } = sources;
8972
- if (serverFloor === void 0) {
8973
- throw new SeqFloorUnavailable("server sent no committed floor for a resumed turn");
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
- const floor = Math.max(serverFloor, outboxFloor);
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.boxed ? this.opts.workRoot : pairDirectory(this.opts.workRoot, {
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 && !checkoutState(cached2.cwd).ok) {
9603
+ if (cached2 && !existsSync12(cached2.cwd)) {
10110
9604
  turnLog.debug(
10111
- { cwd: cached2.cwd, reason: checkoutState(cached2.cwd).reason },
10112
- "dispatcher: the prepared checkout is no longer usable \u2014 re-running the prepare hook to restore it"
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 runPrepareWithReporting(
10184
- runHook,
10185
- prepareHook,
10186
- {
10187
- workspaceId,
10188
- conversationId: payload.conversationId,
10189
- agentId: payload.agentId,
10190
- agentUsername: this.opts.agentUsername,
10191
- // The pair's own directory, already made, for a hook that only
10192
- // wants to add to it.
10193
- defaultCwd: pairDir,
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 readdirSync3,
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 join23 } from "path";
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 join23(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
10330
+ return join22(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
11018
10331
  }
11019
10332
  fileFor(turnId, seq) {
11020
- return join23(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
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 = readdirSync3(dir2);
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 = join23(dir2, name);
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 = readdirSync3(dir2);
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 readdirSync3(dir2).filter((n) => n.endsWith(".json")).length;
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 join33 } from "path";
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(join33(cabaneDir(), "runner"), { recursive: true });
11180
- dir2 = mkdtempSync(join33(cabaneDir(), "runner", "turn-"));
10492
+ mkdirSync18(join32(cabaneDir(), "runner"), { recursive: true });
10493
+ dir2 = mkdtempSync(join32(cabaneDir(), "runner", "turn-"));
11181
10494
  }
11182
- const path = join33(dir2, `exec-${++count}`);
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 box is one
11350
- // conversation × agent's, so the turn stands in its root itself.
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?.box ? { boxed: true } : {},
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 runner
11355
- // in a box has no `secrets.json` to read.
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 hostname3 } from "os";
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(hostname4) {
12446
- const trimmed = hostname4.trim().replace(/\.local$/i, "");
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 as hostname2 } from "os";
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
- chmodSync3(tmp, 384);
13189
+ this.opts.observer?.onStart(info);
13223
13190
  } catch {
13224
13191
  }
13225
- renameSync3(tmp, path);
13226
- } catch (err) {
13192
+ }
13193
+ notifyEnd(info) {
13227
13194
  try {
13228
- rmSync4(tmp, { force: true });
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
- function has(log2, workspaceId, eventId) {
13279
- return readIds(log2, workspaceId).includes(eventId);
13280
- }
13281
- function mark(log2, workspaceId, eventId) {
13282
- const ids = readIds(log2, workspaceId);
13283
- if (ids.includes(eventId)) return;
13284
- ids.push(eventId);
13285
- const trimmed = ids.length > MAX_IDS ? ids.slice(-MAX_IDS) : ids;
13286
- mkdirSync6(dir(log2), { recursive: true });
13287
- writeFileSync4(pathFor(log2, workspaceId), trimmed.join("\n") + "\n", "utf8");
13288
- }
13289
- function hasDispatched(workspaceId, eventId) {
13290
- return has("dispatched", workspaceId, eventId);
13291
- }
13292
- function markDispatched(workspaceId, eventId) {
13293
- mark("dispatched", workspaceId, eventId);
13294
- }
13295
- function hasCompleted(workspaceId, eventId) {
13296
- return has("completed", workspaceId, eventId);
13297
- }
13298
- function markCompleted(workspaceId, eventId) {
13299
- mark("completed", workspaceId, eventId);
13300
- forgetTurnId(workspaceId, eventId);
13301
- }
13302
- var MAX_RESUME_ATTEMPTS = 3;
13303
- function resumeDir() {
13304
- return join13(cabaneDir(), "resume-attempts");
13305
- }
13306
- function resumePathFor(workspaceId) {
13307
- return join13(resumeDir(), encodeURIComponent(workspaceId));
13308
- }
13309
- function readResumeCounts(workspaceId) {
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
- return out;
13327
- }
13328
- function bumpResumeAttempt(workspaceId, eventId) {
13329
- const counts = readResumeCounts(workspaceId);
13330
- const next = (counts.get(eventId) ?? 0) + 1;
13331
- counts.set(eventId, next);
13332
- const entries = [...counts.entries()];
13333
- const trimmed = entries.length > MAX_IDS ? entries.slice(-MAX_IDS) : entries;
13334
- mkdirSync6(resumeDir(), { recursive: true });
13335
- writeFileSync4(
13336
- resumePathFor(workspaceId),
13337
- trimmed.map(([id, c]) => `${id} ${c}`).join("\n") + "\n",
13338
- "utf8"
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
- } catch {
13364
- return out;
13365
- }
13366
- return out;
13367
- }
13368
- function turnIdForEvent(workspaceId, eventId) {
13369
- return readTurnIds(workspaceId).get(eventId) ?? null;
13370
- }
13371
- var TURN_ID_OVERFLOW_WARN = 8192;
13372
- function writeTurnIds(workspaceId, turns) {
13373
- const entries = [...turns.entries()];
13374
- mkdirSync6(turnDir(), { recursive: true });
13375
- const path = turnPathFor(workspaceId);
13376
- const tmp = `${path}.${process.pid}.tmp`;
13377
- writeFileSync4(tmp, entries.map(([id, t]) => `${id} ${t}`).join("\n") + "\n", "utf8");
13378
- renameSync4(tmp, path);
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
- const failure = decodeFailureReason(outcome.errorReason);
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 spawn5 } from "child_process";
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 ?? spawn5;
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 spawn6 } from "child_process";
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 join27 } from "path";
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 join24 } from "path";
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 readdirSync4,
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 join25 } from "path";
13742
+ import { dirname as dirname9, join as join24 } from "path";
13929
13743
  import { fileURLToPath as fileURLToPath5 } from "url";
13930
13744
  function versionsDir() {
13931
- return join25(cabaneDir(), "versions");
13745
+ return join24(cabaneDir(), "versions");
13932
13746
  }
13933
13747
  function currentLink() {
13934
- return join25(cabaneDir(), "current");
13748
+ return join24(cabaneDir(), "current");
13935
13749
  }
13936
13750
  function bootGuardPath() {
13937
- return join25(cabaneDir(), "boot-guard.mjs");
13751
+ return join24(cabaneDir(), "boot-guard.mjs");
13938
13752
  }
13939
13753
  function pinnedNodeDir() {
13940
- return join25(cabaneDir(), "node");
13754
+ return join24(cabaneDir(), "node");
13941
13755
  }
13942
13756
  function pinnedNodePath() {
13943
- return join25(pinnedNodeDir(), "bin", "node");
13757
+ return join24(pinnedNodeDir(), "bin", "node");
13944
13758
  }
13945
13759
  function versionPackageRoot(version) {
13946
- return join25(versionsDir(), version, "node_modules", "@cabane", "companion");
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(join25(root, "package.json"), "utf8"));
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(join25(root, "dist", "cli.js")) ? root : null;
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 readdirSync4(dir2)) {
13797
+ for (const name of readdirSync3(dir2)) {
13984
13798
  if (keep.includes(name)) continue;
13985
- rmSync9(join25(dir2, name), { recursive: true, force: true });
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 join26 } from "path";
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 join26(dir2, "update.json");
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 = join27(root, "dist", "boot-guard.js");
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 = spawn6(command, [...args], {
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 = join27(versionsDir(), version);
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 = join27(dirname11(node), "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 = join27(versionsDir(), `.${version}.staging-${process.pid}`);
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 = join27(staging, "node_modules", "@cabane", "companion");
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, [join27(root, "dist", "bin.js"), "--version"], {
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, [join27(root, "dist", "postinstall.js"), "--json"], {
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 ?? hostname2()}`
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. The companion does no sandboxing, and every turn runs in a child process of its own.`
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 ?? hostname2()}`);
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
- boxed: true,
15001
+ wrapped: true,
15188
15002
  secrets: () => loadSecretValuesTolerant(
15189
- (m) => this.log.warn({ err: m }, "Could not read the secrets file for a boxed turn.")
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: spawn8 }) => {
15682
+ void import("child_process").then(({ spawn: spawn7 }) => {
15869
15683
  try {
15870
- const child = spawn8(process.execPath, process.argv.slice(1), {
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 join28 } from "path";
15756
+ import { join as join27 } from "path";
15943
15757
  function crashMarkerPath() {
15944
- return join28(cabaneDir(), "last-error.json");
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 join29 } from "path";
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 ? join29(home, ".cabane") : null;
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 join30, sep as sep3 } from "path";
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" ? join30(
16276
- env.XDG_CONFIG_HOME || join30(home, ".config"),
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
- ) : join30(home, "Library", "LaunchAgents", `${launchdLabel(env)}.plist`);
16094
+ ) : join29(home, "Library", "LaunchAgents", `${launchdLabel(env)}.plist`);
16281
16095
  return {
16282
16096
  unitFile,
16283
- envFile: join30(cabaneDir(), "service.env"),
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(join30(cabaneDir(), "service-error.log"))}</string>`,
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 = join30(versionsDir(), `.${version}.seed-${process.pid}`);
16304
+ const staging = join29(versionsDir(), `.${version}.seed-${process.pid}`);
16491
16305
  rmSync12(staging, { recursive: true, force: true });
16492
16306
  try {
16493
- cpSync(own, join30(staging, "node_modules", "@cabane", "companion"), {
16307
+ cpSync(own, join29(staging, "node_modules", "@cabane", "companion"), {
16494
16308
  recursive: true,
16495
16309
  verbatimSymlinks: true
16496
16310
  });
16497
- rmSync12(join30(versionsDir(), version), { recursive: true, force: true });
16498
- renameSync10(staging, join30(versionsDir(), version));
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(join30(versionsDir(), version), { recursive: true, force: true });
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 = join30(cabaneDir(), "bin")) {
16405
+ function rewriteScriptWrappers(dir2 = join29(cabaneDir(), "bin")) {
16592
16406
  for (const name of ["cabane", "cabane-companion"]) {
16593
- const path = join30(dir2, name);
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 spawn7 } from "child_process";
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 = spawn7(process.execPath, [cliPath, ...args], {
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(hostname3()) ?? "Computer",
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 readdirSync5, realpathSync as realpathSync6, existsSync as existsSync19 } from "fs";
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 join31 } from "path";
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(join31(homedir10(), ".cabane"))})`;
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 = join31(
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) ? join31(homedir10(), ".cabane") : null;
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(join31(dirname13(own), "cabane-companion")) : null;
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 = readdirSync5(dir2).filter((f) => f.endsWith(".jsonl")).sort();
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 readdirSync6 } from "fs";
17884
- import { basename as basename4, isAbsolute as isAbsolute2, join as join32 } from "path";
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(join32(dir2, newest)) + "\n");
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(join32(dir2, f), "utf8"),
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 = readdirSync6(dir2, { withFileTypes: true });
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 = readdirSync6(join32(dir2, entry.name));
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(join32(dir2, f));
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(join32(dir2, target))) return join32(dir2, target);
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 = join32(dir2, target);
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 join32(dir2, matches[0]);
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.`