@cabane/companion 0.6.0 → 0.6.2

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/runtime.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  } from "fs";
14
14
  import { homedir, userInfo } from "os";
15
15
  import { dirname, join } from "path";
16
- import { z as z3 } from "zod";
16
+ import { z as z2 } from "zod";
17
17
 
18
18
  // src/errors.ts
19
19
  var CompanionError = class extends Error {
@@ -46,9 +46,6 @@ var PrepareHookError = class extends CompanionError {
46
46
  };
47
47
 
48
48
  // src/pairing.ts
49
- import { z } from "zod";
50
- var PAIRING_VERSION = 1;
51
- var DEVICE_TOKEN_PREFIX = "cabdev_";
52
49
  function isAllowedBaseUrl(raw) {
53
50
  let url;
54
51
  try {
@@ -63,45 +60,24 @@ function isAllowedBaseUrl(raw) {
63
60
  }
64
61
  return false;
65
62
  }
66
- var pairingSchema = z.object({
67
- // Bumped if the wire shape changes incompatibly. We only accept v1.
68
- v: z.literal(PAIRING_VERSION),
69
- baseUrl: z.string().url().refine(isAllowedBaseUrl, {
70
- message: "baseUrl must be https (loopback http is allowed for local dev only)"
71
- }),
72
- // The `cabdev_` device token plaintext — the companion's one durable credential.
73
- deviceToken: z.string().min(1).startsWith(DEVICE_TOKEN_PREFIX, {
74
- message: 'deviceToken must be a cabane device token (starts with "cabdev_")'
75
- }),
76
- // Optional identity hints the app may include for nicer local display. The
77
- // companion also learns these from the first assignments pull, so they're not
78
- // required.
79
- deviceId: z.string().min(1).optional(),
80
- deviceLabel: z.string().min(1).optional()
81
- });
82
63
 
83
64
  // src/prepare-hook.ts
84
65
  import { spawn } from "child_process";
85
- import { z as z2 } from "zod";
86
- var prepareHookSchema = z2.object({
87
- command: z2.string().min(1),
88
- args: z2.array(z2.string()).optional(),
66
+ import { z } from "zod";
67
+ var prepareHookSchema = z.object({
68
+ command: z.string().min(1),
69
+ args: z.array(z.string()).optional(),
89
70
  // Extra env handed to the hook process itself (merged over process.env).
90
- env: z2.record(z2.string(), z2.string()).optional(),
71
+ env: z.record(z.string(), z.string()).optional(),
91
72
  // Wall-clock cap for the hook. Provisioning is slow (minutes), so the
92
73
  // default is generous; a hook that hangs past this is killed and the turn
93
74
  // fails with a clear timeout message rather than pinning the companion.
94
- timeoutMs: z2.number().int().positive().optional()
75
+ timeoutMs: z.number().int().positive().optional()
95
76
  }).strict();
96
77
  var DEFAULT_TIMEOUT_MS = 10 * 6e4;
97
- var prepareResultSchema = z2.object({
98
- cwd: z2.string().min(1),
99
- env: z2.record(z2.string(), z2.string()).optional(),
100
- nativeWorkAssignment: z2.object({
101
- itemId: z2.string(),
102
- executionId: z2.string(),
103
- activationEpoch: z2.number().int().nonnegative()
104
- }).strict().optional()
78
+ var prepareResultSchema = z.object({
79
+ cwd: z.string().min(1),
80
+ env: z.record(z.string(), z.string()).optional()
105
81
  });
106
82
  function parsePrepareOutput(stdout) {
107
83
  const last = stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0).at(-1);
@@ -120,13 +96,12 @@ function parsePrepareOutput(stdout) {
120
96
  const r = prepareResultSchema.safeParse(parsed);
121
97
  if (!r.success) {
122
98
  throw new PrepareHookError(
123
- 'prepare hook JSON must carry a non-empty string "cwd" (plus optional "env" or "nativeWorkAssignment")'
99
+ 'prepare hook JSON must carry a non-empty string "cwd" (plus optional "env")'
124
100
  );
125
101
  }
126
102
  return {
127
103
  cwd: r.data.cwd,
128
- ...r.data.env ? { env: r.data.env } : {},
129
- ...r.data.nativeWorkAssignment ? { nativeWorkAssignment: r.data.nativeWorkAssignment } : {}
104
+ ...r.data.env ? { env: r.data.env } : {}
130
105
  };
131
106
  }
132
107
  return { cwd: last };
@@ -164,10 +139,10 @@ var runPrepareHook = (hook, input) => {
164
139
  CABANE_CONVERSATION_TITLE: input.title ?? ""
165
140
  }
166
141
  });
167
- } catch (err2) {
142
+ } catch (err) {
168
143
  reject(
169
144
  new PrepareHookError(
170
- `prepare hook failed to start: ${err2 instanceof Error ? err2.message : String(err2)}`
145
+ `prepare hook failed to start: ${err instanceof Error ? err.message : String(err)}`
171
146
  )
172
147
  );
173
148
  return;
@@ -187,8 +162,8 @@ var runPrepareHook = (hook, input) => {
187
162
  child.stderr?.on("data", (d) => {
188
163
  stderr += d.toString();
189
164
  });
190
- child.on("error", (err2) => {
191
- finish(() => reject(new PrepareHookError(`prepare hook failed to start: ${err2.message}`)));
165
+ child.on("error", (err) => {
166
+ finish(() => reject(new PrepareHookError(`prepare hook failed to start: ${err.message}`)));
192
167
  });
193
168
  child.on("close", (code) => {
194
169
  finish(() => {
@@ -201,8 +176,8 @@ var runPrepareHook = (hook, input) => {
201
176
  }
202
177
  try {
203
178
  resolve(parsePrepareOutput(stdout));
204
- } catch (err2) {
205
- reject(err2 instanceof PrepareHookError ? err2 : new PrepareHookError(String(err2)));
179
+ } catch (err) {
180
+ reject(err instanceof PrepareHookError ? err : new PrepareHookError(String(err)));
206
181
  }
207
182
  });
208
183
  });
@@ -241,8 +216,8 @@ function cabaneDir() {
241
216
  function configPath() {
242
217
  return join(cabaneDir(), "config.json");
243
218
  }
244
- var localAgentConfigSchema = z3.object({
245
- cwd: z3.string().optional(),
219
+ var localAgentConfigSchema = z2.object({
220
+ cwd: z2.string().optional(),
246
221
  prepareHook: prepareHookSchema.optional(),
247
222
  // CT289: the Claude Code auto-memory escape hatch. Auto-memory is forced OFF
248
223
  // by default on every companion (memory belongs in the Cabane workspace, and a
@@ -251,13 +226,13 @@ var localAgentConfigSchema = z3.object({
251
226
  // auto-memory back to your own `~/.claude/settings.json` — Cabane then stops
252
227
  // injecting the off switch and your normal Claude Code memory workflow applies
253
228
  // (in coding mode, where the checkout's project settings are read).
254
- claudeCode: z3.object({ autoMemory: z3.boolean().optional() }).strict().optional()
229
+ claudeCode: z2.object({ autoMemory: z2.boolean().optional() }).strict().optional()
255
230
  }).strict();
256
- var companionConfigSchema = z3.object({
231
+ var companionConfigSchema = z2.object({
257
232
  // The cabane instance this device is paired with. SJ515: https-enforced
258
233
  // (loopback exempt) so a hand-edited config can't smuggle a plaintext-http
259
234
  // base URL onto the MITM-able channel the device token + prompt ride.
260
- baseUrl: z3.string().url().refine(isAllowedBaseUrl, {
235
+ baseUrl: z2.string().url().refine(isAllowedBaseUrl, {
261
236
  message: "baseUrl must be https (loopback http is allowed for local dev only)"
262
237
  }),
263
238
  // The `cabdev_` device token plaintext — the companion's one durable credential,
@@ -265,22 +240,22 @@ var companionConfigSchema = z3.object({
265
240
  // heartbeat endpoints. Optional so `cabane-companion logout` can strip it (a
266
241
  // "paired but logged out" state the supervisor refuses to run) while keeping
267
242
  // the rest of the config; `pair` always writes one.
268
- deviceToken: z3.string().optional(),
269
- // Identity hints, learned from the pairing string and refreshed on the first
243
+ deviceToken: z2.string().optional(),
244
+ // Identity hints, learned from the device flow and refreshed on the first
270
245
  // assignments pull. Cosmetic — used for `status`/dashboard display only.
271
- deviceId: z3.string().optional(),
272
- deviceLabel: z3.string().optional(),
246
+ deviceId: z2.string().optional(),
247
+ deviceLabel: z2.string().optional(),
273
248
  // Optional per-agent machine-local overrides (cwd / prepareHook), keyed by
274
249
  // agentId / username / `slug/username`. Hand-added by the operator; the companion
275
250
  // never writes this (it only persists credentials + the device, elsewhere).
276
- agents: z3.record(z3.string(), localAgentConfigSchema).optional(),
251
+ agents: z2.record(z2.string(), localAgentConfigSchema).optional(),
277
252
  // Dashboard settings (all optional). dashboardPort: preferred bind port (next
278
253
  // free one if taken); autoOpen: whether `start` opens the browser (the
279
254
  // `--no-open` flag / `COMPANION_NO_OPEN=1` override per-run); logLevel: pino
280
255
  // level, live-editable from the dashboard settings panel.
281
- dashboardPort: z3.number().int().min(1).max(65535).optional(),
282
- autoOpen: z3.boolean().optional(),
283
- logLevel: z3.enum(["warn", "info", "debug"]).optional(),
256
+ dashboardPort: z2.number().int().min(1).max(65535).optional(),
257
+ autoOpen: z2.boolean().optional(),
258
+ logLevel: z2.enum(["warn", "info", "debug"]).optional(),
284
259
  // CT270: the opencode runtime, when the operator runs one on this machine. The
285
260
  // operator installs opencode, starts `opencode serve` (auth via opencode's own
286
261
  // `/connect` — Cabane never sees provider keys), and points the companion at it
@@ -288,8 +263,8 @@ var companionConfigSchema = z3.object({
288
263
  // heartbeat manifest (so the server offers DeepSeek/opencode models here and
289
264
  // routes those turns to this device) AND registers the opencode adapter in the
290
265
  // dispatcher. Absent → the device is claude-code-only, exactly as before.
291
- opencode: z3.object({
292
- serverUrl: z3.string().url()
266
+ opencode: z2.object({
267
+ serverUrl: z2.string().url()
293
268
  }).strict().optional(),
294
269
  // CT481: the codex runtime, when the operator runs Codex on this machine. Unlike
295
270
  // opencode (a long-lived `opencode serve` addressed by URL), Codex is a local CLI
@@ -300,20 +275,13 @@ var companionConfigSchema = z3.object({
300
275
  // keeps the block but turns it off). Enabling makes the device advertise the
301
276
  // `codex` runtime on its heartbeat manifest AND registers the codex adapter in
302
277
  // the dispatcher. Absent → the device doesn't offer codex, exactly as before.
303
- codex: z3.object({
304
- enabled: z3.boolean().optional()
278
+ codex: z2.object({
279
+ enabled: z2.boolean().optional()
305
280
  }).strict().optional()
306
281
  });
307
282
  function isCodexEnabled(cfg) {
308
283
  return !!cfg.codex && cfg.codex.enabled !== false;
309
284
  }
310
- function cabaneNativeApiKey() {
311
- const key = process.env.OPENROUTER_API_KEY?.trim();
312
- return key ? key : void 0;
313
- }
314
- function isCabaneNativeEnabled() {
315
- return cabaneNativeApiKey() !== void 0;
316
- }
317
285
  function localAgentConfig(cfg, agent) {
318
286
  const map = cfg.agents ?? {};
319
287
  return map[agent.agentId] ?? map[agent.agentUsername] ?? map[`${agent.workspaceSlug}/${agent.agentUsername}`] ?? {};
@@ -324,18 +292,18 @@ function loadConfig() {
324
292
  let raw;
325
293
  try {
326
294
  raw = readFileSync(path3, "utf8");
327
- } catch (err2) {
295
+ } catch (err) {
328
296
  throw new ConfigError(
329
- `couldn't read ${path3}: ${err2 instanceof Error ? err2.message : String(err2)}`
297
+ `couldn't read ${path3}: ${err instanceof Error ? err.message : String(err)}`
330
298
  );
331
299
  }
332
300
  if (raw.trim().length === 0) return null;
333
301
  let parsed;
334
302
  try {
335
303
  parsed = JSON.parse(raw);
336
- } catch (err2) {
304
+ } catch (err) {
337
305
  throw new ConfigError(
338
- `${path3} is not valid JSON: ${err2 instanceof Error ? err2.message : String(err2)}`
306
+ `${path3} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
339
307
  );
340
308
  }
341
309
  const result = companionConfigSchema.safeParse(parsed);
@@ -367,12 +335,12 @@ function saveConfig(cfg) {
367
335
  } catch {
368
336
  }
369
337
  renameSync(tmp, path3);
370
- } catch (err2) {
338
+ } catch (err) {
371
339
  try {
372
340
  rmSync(tmp, { force: true });
373
341
  } catch {
374
342
  }
375
- throw err2;
343
+ throw err;
376
344
  }
377
345
  }
378
346
  function requireConfig() {
@@ -845,15 +813,15 @@ var DEFAULT_PORT = 7474;
845
813
  var PORT_FALLBACK_SPAN = 10;
846
814
  function buildDashboardApp(deps) {
847
815
  const app = new Hono();
848
- app.onError((err2, c) => {
849
- if (err2 instanceof ApiError) {
850
- const status = err2.status >= 400 && err2.status < 600 ? err2.status : 502;
851
- return c.json({ error: err2.message }, status);
816
+ app.onError((err, c) => {
817
+ if (err instanceof ApiError) {
818
+ const status = err.status >= 400 && err.status < 600 ? err.status : 502;
819
+ return c.json({ error: err.message }, status);
852
820
  }
853
- if (err2 instanceof CompanionError) {
854
- return c.json({ error: err2.message }, 400);
821
+ if (err instanceof CompanionError) {
822
+ return c.json({ error: err.message }, 400);
855
823
  }
856
- return c.json({ error: err2 instanceof Error ? err2.message : "internal error" }, 500);
824
+ return c.json({ error: err instanceof Error ? err.message : "internal error" }, 500);
857
825
  });
858
826
  registerRoutes(app, { ...deps, staticDir: resolveStaticDir() });
859
827
  return app;
@@ -874,12 +842,12 @@ async function startDashboard(opts) {
874
842
  server.closeAllConnections?.();
875
843
  })
876
844
  };
877
- } catch (err2) {
878
- if (isAddrInUse(err2)) {
879
- lastErr = err2;
845
+ } catch (err) {
846
+ if (isAddrInUse(err)) {
847
+ lastErr = err;
880
848
  continue;
881
849
  }
882
- throw err2;
850
+ throw err;
883
851
  }
884
852
  }
885
853
  throw new CompanionError(
@@ -895,16 +863,16 @@ function listen(app, port) {
895
863
  resolve(server);
896
864
  }
897
865
  });
898
- server.on("error", (err2) => {
866
+ server.on("error", (err) => {
899
867
  if (!settled) {
900
868
  settled = true;
901
- reject(err2);
869
+ reject(err);
902
870
  }
903
871
  });
904
872
  });
905
873
  }
906
- function isAddrInUse(err2) {
907
- return Boolean(err2 && typeof err2 === "object" && "code" in err2 && err2.code === "EADDRINUSE");
874
+ function isAddrInUse(err) {
875
+ return Boolean(err && typeof err === "object" && "code" in err && err.code === "EADDRINUSE");
908
876
  }
909
877
  function resolveStaticDir() {
910
878
  return join4(dirname3(fileURLToPath(import.meta.url)), "static");
@@ -1011,8 +979,20 @@ async function claudeOnPath() {
1011
979
  });
1012
980
  });
1013
981
  }
982
+ var CODEX_PROBE_TIMEOUT_MS = 4e3;
983
+ async function codexOnPath() {
984
+ const version = await Promise.race([
985
+ probeCliVersion("codex"),
986
+ new Promise((resolve) => {
987
+ const timer = setTimeout(() => resolve(null), CODEX_PROBE_TIMEOUT_MS);
988
+ timer.unref?.();
989
+ })
990
+ ]);
991
+ return version !== null;
992
+ }
1014
993
  async function ensureRuntimeAvailable(cfg, deps = {}) {
1015
994
  const probeClaude = deps.probeClaude ?? claudeOnPath;
995
+ const probeCodex = deps.probeCodex ?? codexOnPath;
1016
996
  const warn = deps.warn ?? ((message) => process.stderr.write(`${message}
1017
997
  `));
1018
998
  if (await probeClaude()) return;
@@ -1021,15 +1001,15 @@ async function ensureRuntimeAvailable(cfg, deps = {}) {
1021
1001
  ...isCodexEnabled(cfg) ? ["codex"] : []
1022
1002
  ];
1023
1003
  if (alternates.length > 0) {
1004
+ const exposed = alternates.join(" + ");
1024
1005
  warn(
1025
- `warning: Claude Code isn\u2019t on your PATH, so this device will run ${alternates.join(
1026
- " + "
1027
- )} only \u2014 it advertises just those runtimes, so assign it matching models (a Claude-model agent won\u2019t be routed here). Install Claude Code (\`npm i -g @anthropic-ai/claude-code\`) if you want it to run Claude models too.`
1006
+ `warning: Claude Code isn\u2019t on your PATH, so this device exposes ${exposed} only \u2014 it advertises nothing else, so a Claude-model agent won\u2019t be routed here. Assign it agents on models ${exposed} can run, or install Claude Code (\`npm i -g @anthropic-ai/claude-code\`) and log in if you want this device to run Claude models too.`
1028
1007
  );
1029
1008
  return;
1030
1009
  }
1010
+ const installedButUnexposed = await probeCodex() ? 'Codex is installed on this machine but not exposed \u2014 enable it with `{ "codex": { "enabled": true } }` in ~/.cabane/config.json. Otherwise, expose a harness:\n' : "Expose at least one:\n";
1031
1011
  throw new CompanionError(
1032
- "Claude Code, the companion\u2019s default runtime, is not on your PATH. Install it with `npm i -g @anthropic-ai/claude-code`, log in (`claude` then follow the prompts), and run `cabane-companion start` again. The default runtime uses your local Claude Code subscription to run each agent turn; opencode is supported as an alternate runtime you configure per device (see the companion README)."
1012
+ "This device exposes no harness, so no agent turn can run here. " + installedButUnexposed + ' \u2022 Claude Code \u2014 install it (`npm i -g @anthropic-ai/claude-code`) and log in (`claude`, then follow the prompts); it\u2019s exposed automatically once `claude` is on your PATH.\n \u2022 Codex \u2014 install the Codex CLI and log in (`codex login`), then add `{ "codex": { "enabled": true } }` to ~/.cabane/config.json.\n \u2022 opencode \u2014 run `opencode serve --port 4096`, then add `{ "opencode": { "serverUrl": "http://127.0.0.1:4096" } }` to ~/.cabane/config.json.\nThen run `cabane-companion start` again (see the companion README).'
1033
1013
  );
1034
1014
  }
1035
1015
 
@@ -1101,8 +1081,8 @@ async function verifyRuntime(state, fetchImpl = fetch) {
1101
1081
  let res;
1102
1082
  try {
1103
1083
  res = await fetchImpl(url, { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
1104
- } catch (err2) {
1105
- return isConnRefused(err2) ? "stale" : "unknown";
1084
+ } catch (err) {
1085
+ return isConnRefused(err) ? "stale" : "unknown";
1106
1086
  }
1107
1087
  if (!res.ok) return "unknown";
1108
1088
  let body;
@@ -1114,9 +1094,9 @@ async function verifyRuntime(state, fetchImpl = fetch) {
1114
1094
  if (typeof body.instance_id !== "string") return "unknown";
1115
1095
  return body.instance_id === state.instanceId ? "ours" : "stale";
1116
1096
  }
1117
- function isConnRefused(err2) {
1118
- if (!err2 || typeof err2 !== "object") return false;
1119
- const cause = err2.cause;
1097
+ function isConnRefused(err) {
1098
+ if (!err || typeof err !== "object") return false;
1099
+ const cause = err.cause;
1120
1100
  return !!cause && typeof cause === "object" && cause.code === "ECONNREFUSED";
1121
1101
  }
1122
1102
  function trimSlash(s) {
@@ -1179,10 +1159,10 @@ var CabaneApi = class {
1179
1159
  for (let attempt = 1; ; attempt++) {
1180
1160
  try {
1181
1161
  return await this.attempt(method, path3, body, signal);
1182
- } catch (err2) {
1183
- if (attempt >= maxAttempts || signal?.aborted || !isRetryable(err2)) throw err2;
1162
+ } catch (err) {
1163
+ if (attempt >= maxAttempts || signal?.aborted || !isRetryable(err)) throw err;
1184
1164
  await sleep(RETRY_BACKOFF_MS[attempt - 1], signal);
1185
- if (signal?.aborted) throw err2;
1165
+ if (signal?.aborted) throw err;
1186
1166
  }
1187
1167
  }
1188
1168
  }
@@ -1210,14 +1190,14 @@ var CabaneApi = class {
1210
1190
  retry: true,
1211
1191
  ...signal ? { signal } : {}
1212
1192
  });
1213
- } catch (err2) {
1193
+ } catch (err) {
1214
1194
  const outbox = this.opts.outbox;
1215
- if (!outbox) throw err2;
1216
- if (signal?.aborted || isAbortError(err2)) throw err2;
1217
- if (!isRetryable(err2)) throw err2;
1195
+ if (!outbox) throw err;
1196
+ if (signal?.aborted || isAbortError(err)) throw err;
1197
+ if (!isRetryable(err)) throw err;
1218
1198
  outbox.persist({ enqueuedAt: Date.now(), turnId, seq, method: "POST", path: path3, body, kind });
1219
1199
  this.opts.log?.warn(
1220
- { kind, turnId, seq, err: err2 instanceof Error ? err2.message : String(err2) },
1200
+ { kind, turnId, seq, err: err instanceof Error ? err.message : String(err) },
1221
1201
  "companion: commit queued to outbox after transient failure (will drain when the API returns)"
1222
1202
  );
1223
1203
  }
@@ -1241,10 +1221,10 @@ var CabaneApi = class {
1241
1221
  await this.request(entry.method, entry.path, entry.body, { retry: true });
1242
1222
  outbox.remove(entry.turnId, entry.seq);
1243
1223
  progressed = true;
1244
- } catch (err2) {
1245
- if (err2 instanceof ApiError && err2.status >= 400 && err2.status < 500) {
1224
+ } catch (err) {
1225
+ if (err instanceof ApiError && err.status >= 400 && err.status < 500) {
1246
1226
  this.opts.log?.warn(
1247
- { kind: entry.kind, turnId: entry.turnId, seq: entry.seq, status: err2.status },
1227
+ { kind: entry.kind, turnId: entry.turnId, seq: entry.seq, status: err.status },
1248
1228
  "companion outbox: discarding entry on terminal 4xx (will never land)"
1249
1229
  );
1250
1230
  outbox.remove(entry.turnId, entry.seq);
@@ -1392,11 +1372,11 @@ var CabaneApi = class {
1392
1372
  try {
1393
1373
  await this.request("PATCH", path3, body, { retry: true });
1394
1374
  outbox?.remove(key, ACTIVE_RUN_OUTBOX_SEQ);
1395
- } catch (err2) {
1396
- if (!outbox) throw err2;
1397
- if (!isRetryable(err2)) {
1375
+ } catch (err) {
1376
+ if (!outbox) throw err;
1377
+ if (!isRetryable(err)) {
1398
1378
  outbox.remove(key, ACTIVE_RUN_OUTBOX_SEQ);
1399
- throw err2;
1379
+ throw err;
1400
1380
  }
1401
1381
  outbox.persist({
1402
1382
  enqueuedAt: Date.now(),
@@ -1408,7 +1388,7 @@ var CabaneApi = class {
1408
1388
  kind: "active-run"
1409
1389
  });
1410
1390
  this.opts.log?.warn(
1411
- { conversationId, agentId, err: err2 instanceof Error ? err2.message : String(err2) },
1391
+ { conversationId, agentId, err: err instanceof Error ? err.message : String(err) },
1412
1392
  "companion: active-run write queued to outbox after transient failure (will drain when the API returns)"
1413
1393
  );
1414
1394
  }
@@ -1507,13 +1487,13 @@ var CabaneApi = class {
1507
1487
  return res.messages.find((m) => m.id === messageId2) ?? null;
1508
1488
  }
1509
1489
  };
1510
- function isRetryable(err2) {
1511
- if (err2 instanceof ApiError) return err2.status >= 500;
1512
- if (isAbortError(err2)) return false;
1490
+ function isRetryable(err) {
1491
+ if (err instanceof ApiError) return err.status >= 500;
1492
+ if (isAbortError(err)) return false;
1513
1493
  return true;
1514
1494
  }
1515
- function isAbortError(err2) {
1516
- return err2 instanceof Error && err2.name === "AbortError";
1495
+ function isAbortError(err) {
1496
+ return err instanceof Error && err.name === "AbortError";
1517
1497
  }
1518
1498
  function sleep(ms, signal) {
1519
1499
  return new Promise((resolve) => {
@@ -1531,8 +1511,8 @@ function sleep(ms, signal) {
1531
1511
  }
1532
1512
  function errorMessage(status, body) {
1533
1513
  if (body && typeof body === "object" && "error" in body) {
1534
- const err2 = body.error;
1535
- if (typeof err2 === "string") return `${status} ${err2}`;
1514
+ const err = body.error;
1515
+ if (typeof err === "string") return `${status} ${err}`;
1536
1516
  }
1537
1517
  if (typeof body === "string" && body.length > 0) return `${status} ${body.slice(0, 200)}`;
1538
1518
  return `${status} error`;
@@ -1588,8 +1568,8 @@ var DeviceApi = class {
1588
1568
  };
1589
1569
  function errorMessage2(status, body) {
1590
1570
  if (body && typeof body === "object" && "error" in body) {
1591
- const err2 = body.error;
1592
- if (typeof err2 === "string") return `${status} ${err2}`;
1571
+ const err = body.error;
1572
+ if (typeof err === "string") return `${status} ${err}`;
1593
1573
  }
1594
1574
  if (typeof body === "string" && body.length > 0) return `${status} ${body.slice(0, 200)}`;
1595
1575
  return `${status} error`;
@@ -1606,11 +1586,11 @@ import {
1606
1586
  writeFileSync as writeFileSync3
1607
1587
  } from "fs";
1608
1588
  import { dirname as dirname4, join as join6 } from "path";
1609
- import { z as z4 } from "zod";
1589
+ import { z as z3 } from "zod";
1610
1590
  function credentialsPath() {
1611
1591
  return join6(cabaneDir(), "credentials.json");
1612
1592
  }
1613
- var credentialStoreSchema = z4.record(z4.string(), z4.string());
1593
+ var credentialStoreSchema = z3.record(z3.string(), z3.string());
1614
1594
  function load() {
1615
1595
  const path3 = credentialsPath();
1616
1596
  if (!existsSync4(path3)) return {};
@@ -1643,12 +1623,12 @@ function save(map) {
1643
1623
  } catch {
1644
1624
  }
1645
1625
  renameSync2(tmp, path3);
1646
- } catch (err2) {
1626
+ } catch (err) {
1647
1627
  try {
1648
1628
  rmSync3(tmp, { force: true });
1649
1629
  } catch {
1650
1630
  }
1651
- throw err2;
1631
+ throw err;
1652
1632
  }
1653
1633
  }
1654
1634
  function getCredential(agentId) {
@@ -1821,44 +1801,44 @@ function noResume() {
1821
1801
  var TURN_PROTOCOL_VERSION = 1;
1822
1802
 
1823
1803
  // packages/agent-runtime/src/host-policy.ts
1824
- import { z as z5 } from "zod";
1825
- var hostPolicySchema = z5.object({
1804
+ import { z as z4 } from "zod";
1805
+ var hostPolicySchema = z4.object({
1826
1806
  // Host filesystem + shell: `Bash`/`Read`/`Write`/`Edit`/`Glob`/`Grep`, the
1827
1807
  // notebook read/write pair, git-worktree tools, and the `REPL` host code-exec
1828
1808
  // tool. Off under the locked assistant surface (today's `DISALLOWED_TOOLS`);
1829
1809
  // on under `coding` mode.
1830
- hostFs: z5.boolean(),
1810
+ hostFs: z4.boolean(),
1831
1811
  // Public web (today's `WebSearch` / `WebFetch`, the `DEFAULT_WEB_TOOLS`). Public
1832
1812
  // web, not host reach — granted by default today, but expressible as a grant.
1833
- web: z5.boolean(),
1813
+ web: z4.boolean(),
1834
1814
  // Browser automation (the Playwright MCP surface). Varies by host: a companion has
1835
1815
  // it, the house executor does not (CT230).
1836
- browser: z5.boolean(),
1816
+ browser: z4.boolean(),
1837
1817
  // User-configured MCP servers permitted. False for the house executor
1838
1818
  // (CT227: Cabane agents run no user MCP servers), true for a personal companion.
1839
- userMcp: z5.boolean(),
1819
+ userMcp: z4.boolean(),
1840
1820
  // Subagent / sub-dispatch (`Task` / `Agent`). A per-host GRANT (see the CT261
1841
1821
  // amendment above): `false` on the locked assistant/house surface (banned via
1842
1822
  // `DISALLOWED_TOOLS`), `true` under coding / custom where the fan-out is left to
1843
1823
  // the coding allow-hook (which reaches sub-agent tool calls, CT680) or the user
1844
1824
  // allowlist. The subagent completes within the turn, so
1845
1825
  // it's not the turn-model invariant `scheduling` is.
1846
- subagents: z5.boolean(),
1826
+ subagents: z4.boolean(),
1847
1827
  // ── Hard platform invariants — always denied, never granted ────────────────
1848
1828
  // Deferred re-invocation / scheduling (`ScheduleWakeup`, the `Cron*` / `Task*`
1849
1829
  // families, `Monitor`, `Workflow`, …). A turn is one query that resolves when
1850
1830
  // `result` fires; a scheduled callback fires after the reply window has closed
1851
1831
  // and strands the agent (the CT155/CT156 rule).
1852
- scheduling: z5.literal("never"),
1832
+ scheduling: z4.literal("never"),
1853
1833
  // Human-facing UI prompts (`AskUserQuestion`). A Cabane conversation has no
1854
1834
  // handler to answer a structured prompt, so the call hangs the turn
1855
1835
  // (`UNSUPPORTED_TOOLS`). The agent asks in its reply instead.
1856
- uiPrompts: z5.literal("never")
1836
+ uiPrompts: z4.literal("never")
1857
1837
  });
1858
1838
 
1859
1839
  // packages/agent-runtime/src/turn-event.ts
1860
- import { z as z6 } from "zod";
1861
- var turnEventSchema = z6.discriminatedUnion("type", [
1840
+ import { z as z5 } from "zod";
1841
+ var turnEventSchema = z5.discriminatedUnion("type", [
1862
1842
  // The runtime's opaque session state, emitted when the adapter learns it (e.g.
1863
1843
  // the SDK `system/init` frame). The platform stores `state` verbatim per
1864
1844
  // (conversation, agent) and hands it back on the next turn; only the adapter
@@ -1878,19 +1858,19 @@ var turnEventSchema = z6.discriminatedUnion("type", [
1878
1858
  // on the companion, after the server committed the manifest). Runtime-neutral: a
1879
1859
  // plain boolean, not a runtime-specific reason string (that stays in the
1880
1860
  // adapter's `onWarn` log). Additive + optional — an old receiver ignores it.
1881
- z6.object({
1882
- type: z6.literal("session"),
1883
- state: z6.string(),
1884
- degraded: z6.boolean().optional()
1861
+ z5.object({
1862
+ type: z5.literal("session"),
1863
+ state: z5.string(),
1864
+ degraded: z5.boolean().optional()
1885
1865
  }),
1886
1866
  // One readable thinking summary. Maps `onThinking({ text })`. Transient —
1887
1867
  // surfaced live, never persisted as durable content.
1888
- z6.object({ type: z6.literal("thinking"), text: z6.string() }),
1868
+ z5.object({ type: z5.literal("thinking"), text: z5.string() }),
1889
1869
  // Assistant text. Maps `onAssistantText({ text, final })` — `text`→`body`,
1890
1870
  // `final`→`terminal`. `terminal: false` is interim narration (commits as a
1891
1871
  // `progress` row); `terminal: true` is the turn's closing reply (commits as
1892
1872
  // the `final` row).
1893
- z6.object({ type: z6.literal("text"), body: z6.string(), terminal: z6.boolean() }),
1873
+ z5.object({ type: z5.literal("text"), body: z5.string(), terminal: z5.boolean() }),
1894
1874
  // A tool-activity transition. Maps `onToolActivity` — `toolUseId`→`id`,
1895
1875
  // `toolName`→`name` (already prefix-stripped: `cabane_read`, not
1896
1876
  // `mcp__cabane__cabane_read`), `summary` is the short card label. `phase`
@@ -1905,15 +1885,15 @@ var turnEventSchema = z6.discriminatedUnion("type", [
1905
1885
  // dropped the prefix; null for a host / built-in tool. The client tags Cabane
1906
1886
  // MCP calls (`=== 'cabane'`) with a subtle glyph. Nullable + optional so a
1907
1887
  // pre-CT496 producer that never sets it is unaffected (treated as null).
1908
- z6.object({
1909
- type: z6.literal("tool"),
1910
- id: z6.string(),
1911
- name: z6.string(),
1912
- phase: z6.enum(["start", "done", "error"]),
1913
- summary: z6.string(),
1914
- input: z6.unknown().optional(),
1915
- result: z6.unknown().optional(),
1916
- mcpServer: z6.string().nullable().optional()
1888
+ z5.object({
1889
+ type: z5.literal("tool"),
1890
+ id: z5.string(),
1891
+ name: z5.string(),
1892
+ phase: z5.enum(["start", "done", "error"]),
1893
+ summary: z5.string(),
1894
+ input: z5.unknown().optional(),
1895
+ result: z5.unknown().optional(),
1896
+ mcpServer: z5.string().nullable().optional()
1917
1897
  }),
1918
1898
  // The turn's terminal outcome. Maps the `{ ok, reason }` both loops return
1919
1899
  // inline. `ok:false` carries a machine reason (`no_session`, an error code);
@@ -1957,34 +1937,34 @@ var turnEventSchema = z6.discriminatedUnion("type", [
1957
1937
  // `message_turn_metadata.resolved_model` / `resolved_config`. Optional ⇒
1958
1938
  // backward-compatible: an old adapter/companion omits them, a cancel has no result
1959
1939
  // event at all, and the columns stay null → the UI shows `—`.
1960
- z6.object({
1961
- type: z6.literal("result"),
1962
- ok: z6.boolean(),
1963
- reason: z6.string().optional(),
1964
- usage: z6.object({
1965
- inputTokens: z6.number(),
1966
- outputTokens: z6.number(),
1967
- cacheReadTokens: z6.number().optional(),
1968
- cacheCreationTokens: z6.number().optional(),
1969
- contextTokens: z6.number().optional(),
1970
- contextWindow: z6.number().optional()
1940
+ z5.object({
1941
+ type: z5.literal("result"),
1942
+ ok: z5.boolean(),
1943
+ reason: z5.string().optional(),
1944
+ usage: z5.object({
1945
+ inputTokens: z5.number(),
1946
+ outputTokens: z5.number(),
1947
+ cacheReadTokens: z5.number().optional(),
1948
+ cacheCreationTokens: z5.number().optional(),
1949
+ contextTokens: z5.number().optional(),
1950
+ contextWindow: z5.number().optional()
1971
1951
  }).optional(),
1972
- resolvedModel: z6.string().optional(),
1973
- resolvedConfig: z6.object({
1974
- effort: z6.string().optional(),
1975
- thinking: z6.string().optional(),
1976
- reasoningEffort: z6.string().optional()
1952
+ resolvedModel: z5.string().optional(),
1953
+ resolvedConfig: z5.object({
1954
+ effort: z5.string().optional(),
1955
+ thinking: z5.string().optional(),
1956
+ reasoningEffort: z5.string().optional()
1977
1957
  }).optional()
1978
1958
  })
1979
1959
  ]);
1980
1960
 
1981
1961
  // packages/agent-runtime/src/failure.ts
1982
- import { z as z7 } from "zod";
1983
- var turnFailureSchema = z7.discriminatedUnion("kind", [
1984
- z7.object({ kind: z7.literal("usage_capped"), resetsAt: z7.string().optional() }),
1985
- z7.object({ kind: z7.literal("rate_limited") }),
1986
- z7.object({ kind: z7.literal("server_error") }),
1987
- z7.object({ kind: z7.literal("auth_expired") })
1962
+ import { z as z6 } from "zod";
1963
+ var turnFailureSchema = z6.discriminatedUnion("kind", [
1964
+ z6.object({ kind: z6.literal("usage_capped"), resetsAt: z6.string().optional() }),
1965
+ z6.object({ kind: z6.literal("rate_limited") }),
1966
+ z6.object({ kind: z6.literal("server_error") }),
1967
+ z6.object({ kind: z6.literal("auth_expired") })
1988
1968
  ]);
1989
1969
  var USAGE_CAPPED = "usage_capped";
1990
1970
  var RATE_LIMITED = "rate_limited";
@@ -2090,63 +2070,63 @@ var RATE_PATTERNS = [/rate[_ ]?limit/, /\b429\b/, /too many requests/];
2090
2070
  var BARE_LIMIT = /\blimit (reached|exceeded)\b/;
2091
2071
 
2092
2072
  // packages/agent-runtime/src/turn-request.ts
2093
- import { z as z8 } from "zod";
2094
- var contentBlockSchema = z8.discriminatedUnion("type", [
2095
- z8.object({ type: z8.literal("text"), text: z8.string() }),
2096
- z8.object({
2097
- type: z8.literal("image"),
2098
- source: z8.object({ type: z8.literal("url"), url: z8.string() })
2073
+ import { z as z7 } from "zod";
2074
+ var contentBlockSchema = z7.discriminatedUnion("type", [
2075
+ z7.object({ type: z7.literal("text"), text: z7.string() }),
2076
+ z7.object({
2077
+ type: z7.literal("image"),
2078
+ source: z7.object({ type: z7.literal("url"), url: z7.string() })
2099
2079
  }),
2100
- z8.object({
2101
- type: z8.literal("document"),
2102
- source: z8.object({ type: z8.literal("url"), url: z8.string() })
2080
+ z7.object({
2081
+ type: z7.literal("document"),
2082
+ source: z7.object({ type: z7.literal("url"), url: z7.string() })
2103
2083
  })
2104
2084
  ]);
2105
- var effortLevelSchema = z8.enum(["low", "medium", "high", "xhigh", "max"]);
2106
- var resolvedRunConfigSchema = z8.object({
2107
- model: z8.string().nullable(),
2085
+ var effortLevelSchema = z7.enum(["low", "medium", "high", "xhigh", "max"]);
2086
+ var resolvedRunConfigSchema = z7.object({
2087
+ model: z7.string().nullable(),
2108
2088
  effort: effortLevelSchema.optional(),
2109
- runtimeOptions: z8.record(z8.string(), z8.unknown()).optional()
2089
+ runtimeOptions: z7.record(z7.string(), z7.unknown()).optional()
2110
2090
  });
2111
- var resolvedMcpServerSchema = z8.union([
2112
- z8.object({
2113
- type: z8.literal("stdio").optional(),
2114
- command: z8.string(),
2115
- args: z8.array(z8.string()).optional(),
2116
- env: z8.record(z8.string(), z8.string()).optional()
2091
+ var resolvedMcpServerSchema = z7.union([
2092
+ z7.object({
2093
+ type: z7.literal("stdio").optional(),
2094
+ command: z7.string(),
2095
+ args: z7.array(z7.string()).optional(),
2096
+ env: z7.record(z7.string(), z7.string()).optional()
2117
2097
  }),
2118
- z8.object({
2119
- type: z8.literal("http"),
2120
- url: z8.string(),
2121
- headers: z8.record(z8.string(), z8.string()).optional()
2098
+ z7.object({
2099
+ type: z7.literal("http"),
2100
+ url: z7.string(),
2101
+ headers: z7.record(z7.string(), z7.string()).optional()
2122
2102
  }),
2123
- z8.object({
2124
- type: z8.literal("sse"),
2125
- url: z8.string(),
2126
- headers: z8.record(z8.string(), z8.string()).optional()
2103
+ z7.object({
2104
+ type: z7.literal("sse"),
2105
+ url: z7.string(),
2106
+ headers: z7.record(z7.string(), z7.string()).optional()
2127
2107
  })
2128
2108
  ]);
2129
- var resolvedMcpServersSchema = z8.record(z8.string(), resolvedMcpServerSchema);
2130
- var hostInjectedServersSchema = z8.record(z8.string(), z8.unknown());
2131
- var turnRequestSchema = z8.object({
2109
+ var resolvedMcpServersSchema = z7.record(z7.string(), resolvedMcpServerSchema);
2110
+ var hostInjectedServersSchema = z7.record(z7.string(), z7.unknown());
2111
+ var turnRequestSchema = z7.object({
2132
2112
  // Server-composed system prompt (core + capability prose + adapter addendum +
2133
2113
  // charter). One string to the adapter.
2134
- systemPrompt: z8.string(),
2114
+ systemPrompt: z7.string(),
2135
2115
  // Server-composed per-turn user text (anchor reminder + the triggering message).
2136
- prompt: z8.string(),
2116
+ prompt: z7.string(),
2137
2117
  // The multi-block user-message body (text + vision).
2138
- content: z8.array(contentBlockSchema),
2118
+ content: z7.array(contentBlockSchema),
2139
2119
  // Portable-or-dialect run-config (above).
2140
2120
  config: resolvedRunConfigSchema,
2141
2121
  // Abstract capability grants; the adapter maps them to tool names.
2142
2122
  policy: hostPolicySchema,
2143
2123
  // Prior opaque session state, or null for a fresh session.
2144
- session: z8.string().nullable(),
2124
+ session: z7.string().nullable(),
2145
2125
  // The cabane control-plane coordinates for this turn's MCP + post-back.
2146
- cabane: z8.object({
2147
- mcpUrl: z8.string(),
2148
- bearer: z8.string(),
2149
- activeConversationId: z8.string(),
2126
+ cabane: z7.object({
2127
+ mcpUrl: z7.string(),
2128
+ bearer: z7.string(),
2129
+ activeConversationId: z7.string(),
2150
2130
  // CT714: the scoped TURN-CONTROL MCP endpoint (`/api/turn-control`). The
2151
2131
  // EXTERNAL adapters (Codex / opencode) mount it by URL under the key
2152
2132
  // `cabane_companion` — using the same `bearer` (the turn token) and the same
@@ -2156,7 +2136,7 @@ var turnRequestSchema = z8.object({
2156
2136
  // claude-code ignores it (it mounts the in-process instance instead), and
2157
2137
  // every existing `cabane`-block fixture keeps parsing unchanged; the
2158
2138
  // companion always populates it (`build-options.ts`).
2159
- turnControlUrl: z8.string().optional(),
2139
+ turnControlUrl: z7.string().optional(),
2160
2140
  // CT598: the workspace this turn runs in. The claude-code/opencode/codex
2161
2141
  // adapters never need it (they reach Cabane through the `cabane` MCP server,
2162
2142
  // which takes `workspaceId` as a per-tool arg the model supplies); the
@@ -2166,53 +2146,39 @@ var turnRequestSchema = z8.object({
2166
2146
  // adapters' conformance fixtures, tests) keeps parsing unchanged — the companion
2167
2147
  // always populates it (`build-options.ts`), and the native adapter fails the
2168
2148
  // turn loudly when it is somehow absent rather than guessing.
2169
- workspaceId: z8.string().optional(),
2149
+ workspaceId: z7.string().optional(),
2170
2150
  // CT752: the server-resolved workspace surface this credential exposes.
2171
2151
  // Readiness uses this explicit fact to require `sdk` for code mode and the
2172
2152
  // granular floor for classic mode; inventory contents alone cannot infer it
2173
2153
  // because `sdk` is intentionally also available on the classic surface.
2174
- workspaceToolSurface: z8.enum(["code", "classic"]).optional()
2154
+ workspaceToolSurface: z7.enum(["code", "classic"]).optional()
2175
2155
  }),
2176
2156
  // Machine-local resolution (host-filled): the checkout cwd, extra env from a
2177
2157
  // prepare hook, and the resolved user MCP servers.
2178
- local: z8.object({
2179
- cwd: z8.string().optional(),
2180
- env: z8.record(z8.string(), z8.string()).optional(),
2181
- nativeWorkAssignment: z8.object({
2182
- itemId: z8.string(),
2183
- executionId: z8.string(),
2184
- activationEpoch: z8.number().int().nonnegative()
2185
- }).strict().optional(),
2158
+ local: z7.object({
2159
+ cwd: z7.string().optional(),
2160
+ env: z7.record(z7.string(), z7.string()).optional(),
2186
2161
  mcpServers: resolvedMcpServersSchema.optional(),
2187
2162
  // CT289: machine-local claude-code adapter knobs the operator sets on a
2188
2163
  // companion they run themselves — the auto-memory escape hatch. `autoMemory:
2189
2164
  // true` opts back into Claude Code's auto-memory (governed by the operator's
2190
2165
  // own `.claude/settings.json`); absent/false leaves the adapter's force-off
2191
- // default in place (see `buildClaudeCodeOptions`). The In-Cabane executor never
2192
- // sets it, so house stays force-off unconditionally.
2193
- claudeCode: z8.object({ autoMemory: z8.boolean().optional() }).optional()
2166
+ // default in place (see `buildClaudeCodeOptions`).
2167
+ claudeCode: z7.object({ autoMemory: z7.boolean().optional() }).optional()
2194
2168
  }),
2195
2169
  // Host-owned injected servers (host-filled) — e.g. the summon server.
2196
- extra: z8.object({
2197
- mcpServers: hostInjectedServersSchema,
2198
- // CT666: the per-turn turn-control HANDLER for the in-process cabane-native
2199
- // runtime — the seam that gives it ask/wake_me/summon_agent/sub_agent/skip_turn
2200
- // without a subprocess `cabane_companion` MCP server. Typed `unknown` for the same
2201
- // reason as `mcpServers`: it's a live host object (a `NativeTurnControl` whose
2202
- // methods close over the dispatcher's per-turn state), passed to the native
2203
- // adapter WITHOUT the contract package inspecting it. The subprocess adapters
2204
- // ignore it (they get the same verbs from the injected SDK server instead).
2205
- turnControl: z8.unknown().optional()
2170
+ extra: z7.object({
2171
+ mcpServers: hostInjectedServersSchema
2206
2172
  })
2207
2173
  });
2208
2174
 
2209
2175
  // packages/agent-runtime/src/conformance.ts
2210
- import { z as z9 } from "zod";
2211
- var conformanceFixtureSchema = z9.object({
2212
- name: z9.string(),
2176
+ import { z as z8 } from "zod";
2177
+ var conformanceFixtureSchema = z8.object({
2178
+ name: z8.string(),
2213
2179
  request: turnRequestSchema,
2214
- nativeStream: z9.array(z9.unknown()),
2215
- expected: z9.array(turnEventSchema)
2180
+ nativeStream: z8.array(z8.unknown()),
2181
+ expected: z8.array(turnEventSchema)
2216
2182
  });
2217
2183
 
2218
2184
  // packages/agent-runtime/src/transcript.ts
@@ -2222,8 +2188,8 @@ function createTerminalTextBuffer() {
2222
2188
  async function safeEmit(emit, event, onError) {
2223
2189
  try {
2224
2190
  await emit(event);
2225
- } catch (err2) {
2226
- onError?.(err2, event.type);
2191
+ } catch (err) {
2192
+ onError?.(err, event.type);
2227
2193
  }
2228
2194
  }
2229
2195
  async function processAssistantMessage(msg, emit, pending, buffer, onError) {
@@ -2484,16 +2450,16 @@ var TurnPump = class {
2484
2450
  // minimal note. Skipped when cancelled or already final. The held-text flush
2485
2451
  // that precedes it is a classification concern, driven by the caller before
2486
2452
  // this runs.
2487
- async finalize(ok2) {
2488
- if (!ok2 || this.opts.signal.aborted || this.emittedFinal) return;
2453
+ async finalize(ok) {
2454
+ if (!ok || this.opts.signal.aborted || this.emittedFinal) return;
2489
2455
  const body = this.lastProgressBody ?? this.opts.emptyFinalBody;
2490
2456
  const seq = this.opts.nextSeq();
2491
2457
  try {
2492
2458
  await this.opts.commit.commitMessage({ body, kind: "final", seq });
2493
2459
  this.emittedFinal = true;
2494
2460
  this.finalReplyBody = body;
2495
- } catch (err2) {
2496
- this.opts.onError?.(err2, "empty-final");
2461
+ } catch (err) {
2462
+ this.opts.onError?.(err, "empty-final");
2497
2463
  }
2498
2464
  }
2499
2465
  // Whether the turn has committed its `final` row — read by the host to decide
@@ -2517,7 +2483,7 @@ import {
2517
2483
  var CLAUDE_CODE_ADDENDUM = "";
2518
2484
 
2519
2485
  // packages/agent-runtime/src/claude-code/policy.ts
2520
- import { z as z10 } from "zod";
2486
+ import { z as z9 } from "zod";
2521
2487
  var HOST_FS_TOOLS = [
2522
2488
  // shell + local filesystem
2523
2489
  "Bash",
@@ -2567,30 +2533,20 @@ function withThinkingSummaries(thinking) {
2567
2533
  if (thinking.type === "disabled") return thinking;
2568
2534
  return { display: "summarized", ...thinking };
2569
2535
  }
2570
- var claudeCodeDialectSchema = z10.object({
2571
- thinking: z10.discriminatedUnion("type", [
2572
- z10.object({
2573
- type: z10.literal("adaptive"),
2574
- display: z10.enum(["summarized", "omitted"]).optional()
2536
+ var claudeCodeDialectSchema = z9.object({
2537
+ thinking: z9.discriminatedUnion("type", [
2538
+ z9.object({
2539
+ type: z9.literal("adaptive"),
2540
+ display: z9.enum(["summarized", "omitted"]).optional()
2575
2541
  }),
2576
- z10.object({
2577
- type: z10.literal("enabled"),
2578
- budgetTokens: z10.number().int().positive().optional(),
2579
- display: z10.enum(["summarized", "omitted"]).optional()
2542
+ z9.object({
2543
+ type: z9.literal("enabled"),
2544
+ budgetTokens: z9.number().int().positive().optional(),
2545
+ display: z9.enum(["summarized", "omitted"]).optional()
2580
2546
  }),
2581
- z10.object({ type: z10.literal("disabled") })
2547
+ z9.object({ type: z9.literal("disabled") })
2582
2548
  ]).optional(),
2583
- allowedTools: z10.array(z10.string()).optional(),
2584
- disallowedTools: z10.array(z10.string()).optional(),
2585
- // Which claude-code harness shape to run. `coding` switches to the
2586
- // `claude_code` preset + project settings + always-allow `PreToolUse` hook;
2587
- // `custom`/`assistant` (or absent) use a plain-string prompt + bypass. This
2588
- // is the claude-code-specific PRESET selector — kept distinct from
2589
- // `policy.hostFs` (the host-fs BLOCK), because companion `custom` mode wants host
2590
- // fs available (via its own allowlist) WITHOUT the coding harness, and in-app
2591
- // `custom` wants host fs blocked — neither of which a single `hostFs` boolean
2592
- // can express alongside the preset choice.
2593
- mode: z10.enum(["assistant", "coding", "custom"]).optional()
2549
+ hostAccess: z9.boolean().optional()
2594
2550
  }).loose();
2595
2551
  function readThinking(runtimeOptions) {
2596
2552
  const dialect = runtimeOptions?.["claude-code"];
@@ -2669,23 +2625,25 @@ function buildClaudeCodeOptions(req, augment) {
2669
2625
  };
2670
2626
  }
2671
2627
  const dialect = claudeCodeDialectSchema.safeParse(config.runtimeOptions?.["claude-code"] ?? {});
2672
- const customAllowed = dialect.success ? dialect.data.allowedTools ?? [] : [];
2673
- const customDisallowed = dialect.success ? dialect.data.disallowedTools ?? [] : [];
2674
- const useCodingPreset = (dialect.success ? dialect.data.mode : void 0) === "coding";
2628
+ const useCodingPreset = dialect.success ? dialect.data.hostAccess ?? false : false;
2675
2629
  const cabaneGlob = `mcp__${CABANE_MCP_SERVER}__*`;
2676
2630
  const extraServerGlobs = Object.keys(req.extra.mcpServers).map((name) => `mcp__${name}__*`);
2677
2631
  const allowedTools = dedupe([
2678
2632
  cabaneGlob,
2679
2633
  ...extraServerGlobs,
2680
- ...policy.web ? DEFAULT_WEB_TOOLS : [],
2681
- ...customAllowed
2634
+ ...policy.web ? DEFAULT_WEB_TOOLS : []
2682
2635
  ]);
2683
- const disallowedTools = dedupe([...disallowedToolsFor(policy), ...customDisallowed]);
2636
+ const disallowedTools = dedupe([...disallowedToolsFor(policy)]);
2684
2637
  const resumeDecision = decideResume(req.session, cwd);
2685
2638
  const resume = "resume" in resumeDecision ? resumeDecision.resume : null;
2686
2639
  const freshReason = "fresh" in resumeDecision ? resumeDecision.reason : void 0;
2687
2640
  const devControlsAutoMemory = req.local.claudeCode?.autoMemory === true;
2688
2641
  const model = parseClaudeCodeModel(config.model);
2642
+ if (model === null) {
2643
+ console.warn(
2644
+ `[agent-runtime/claude-code] CT823: dispatching a subscription turn with NO concrete model (config.model=${JSON.stringify(config.model)}) \u2014 the SDK will fall back to its bundled default. This should be unreachable post-CT823; the model resolver should have supplied a concrete house default.`
2645
+ );
2646
+ }
2689
2647
  const base = {
2690
2648
  // model / effort: `model` is pinned only when the config names a real one —
2691
2649
  // omitted for "let it choose" (see `parseClaudeCodeModel`), so the SDK picks
@@ -2746,7 +2704,7 @@ async function* decodeSdkStream(iter, ctx) {
2746
2704
  out.push(event);
2747
2705
  };
2748
2706
  let sessionEmitted = false;
2749
- let ok2 = false;
2707
+ let ok = false;
2750
2708
  let resultReason;
2751
2709
  let sawResult = false;
2752
2710
  let usage;
@@ -2792,8 +2750,8 @@ async function* decodeSdkStream(iter, ctx) {
2792
2750
  if (typeof info.rateLimitType === "string") rateLimitType = info.rateLimitType;
2793
2751
  }
2794
2752
  } else if (msg.type === "auth_status") {
2795
- const err2 = msg.error;
2796
- if (typeof err2 === "string" && err2.length > 0) authError = err2;
2753
+ const err = msg.error;
2754
+ if (typeof err === "string" && err.length > 0) authError = err;
2797
2755
  } else if (msg.type === "result") {
2798
2756
  sawResult = true;
2799
2757
  usage = readSdkUsage(msg);
@@ -2805,7 +2763,7 @@ async function* decodeSdkStream(iter, ctx) {
2805
2763
  }
2806
2764
  const isError = msg.is_error === true;
2807
2765
  if (msg.subtype === "success" && !isError) {
2808
- ok2 = true;
2766
+ ok = true;
2809
2767
  } else {
2810
2768
  const resultText = msg.result ?? "";
2811
2769
  const terminalReason = msg.terminal_reason;
@@ -2819,26 +2777,26 @@ async function* decodeSdkStream(iter, ctx) {
2819
2777
  ...rateLimitResetIso ? { resetsAt: rateLimitResetIso } : {}
2820
2778
  } : classifyAssistantError(lastAssistantError) ?? classifyErrorText([authError, errorText].filter(Boolean).join(" "));
2821
2779
  resultReason = failure ? encodeFailureReason(failure) : isError ? `error:${resultText.slice(0, 200) || "unknown"}` : `result_error:${msg.subtype}`;
2822
- ok2 = false;
2780
+ ok = false;
2823
2781
  }
2824
2782
  break;
2825
2783
  }
2826
2784
  }
2827
- } catch (err2) {
2828
- if (ctx.signal.aborted) throw err2;
2829
- const failure = classifyErrorText(err2 instanceof Error ? err2.message : String(err2));
2830
- if (!failure) throw err2;
2831
- ok2 = false;
2785
+ } catch (err) {
2786
+ if (ctx.signal.aborted) throw err;
2787
+ const failure = classifyErrorText(err instanceof Error ? err.message : String(err));
2788
+ if (!failure) throw err;
2789
+ ok = false;
2832
2790
  resultReason = encodeFailureReason(failure);
2833
2791
  sawResult = true;
2834
2792
  }
2835
2793
  if (ctx.signal.aborted) return;
2836
- await flushHeldText(buffer, emit, ok2);
2794
+ await flushHeldText(buffer, emit, ok);
2837
2795
  yield* drain(out);
2838
- if (!ok2 && !resultReason && !sawResult) resultReason = "no_result";
2796
+ if (!ok && !resultReason && !sawResult) resultReason = "no_result";
2839
2797
  yield {
2840
2798
  type: "result",
2841
- ok: ok2,
2799
+ ok,
2842
2800
  ...resultReason ? { reason: resultReason } : {},
2843
2801
  ...usage ? { usage } : {},
2844
2802
  ...resolvedModel ? { resolvedModel } : {}
@@ -3307,26 +3265,20 @@ function selectAdapter(registry, runtime) {
3307
3265
 
3308
3266
  // packages/agent-runtime/src/opencode/addendum.ts
3309
3267
  var OPENCODE_ADDENDUM = [
3310
- "Your Cabane tools have plain names \u2014 `read`, `write`, `search`, `edit`,",
3311
- "`post_message`, and so on; the host tools are plain verbs too (`bash`, `read`,",
3312
- "`edit`). If a tool appears in this prompt with an `mcp__\u2026__` prefix, that",
3268
+ "Your one Cabane workspace tool has a plain name \u2014 `sdk` (you act on the",
3269
+ "workspace by writing a TypeScript program and calling `sdk` with it); the",
3270
+ "turn-control verbs (`ask`, `wake_me`, `summon_agent`, `skip_turn`) are",
3271
+ "plain-named too, as are the few ancillary cabane tools (`list_workspaces`,",
3272
+ "`read_binary`, `upload`, `begin_upload`, `finalize_upload`,",
3273
+ "`mint_render_token`), and the host tools are plain verbs (`bash`, `read`,",
3274
+ "`edit`). There is no `write`/`search`/`edit` CABANE tool here \u2014 those are",
3275
+ "`cabane` SDK calls inside your program, not tools (a bare `read`/`edit` is the",
3276
+ "HOST tool). If a tool appears in this prompt with an `mcp__\u2026__` prefix, that",
3313
3277
  "prefix is not part of its name \u2014 call the tool by its plain verb. Write your",
3314
3278
  "closing reply as the last thing you say in the turn: you can interleave",
3315
3279
  "narration with tool calls, but only your final message is recorded as the",
3316
3280
  "turn\u2019s reply."
3317
3281
  ].join(" ");
3318
- var OPENCODE_ADDENDUM_CODE_MODE = [
3319
- "Your one Cabane workspace tool has a plain name \u2014 `sdk` (you act on the",
3320
- "workspace by writing a TypeScript program and calling `sdk` with it); the",
3321
- "turn-control verbs (`ask`, `wake_me`, `summon_agent`, `skip_turn`) are",
3322
- "plain-named too, and the host tools are plain verbs (`bash`, `read`, `edit`).",
3323
- "There is no `write`/`search`/`edit` CABANE tool here \u2014 those are `cabane` SDK",
3324
- "calls inside your program, not tools (a bare `read`/`edit` is the HOST tool). If",
3325
- "a tool appears in this prompt with an `mcp__\u2026__` prefix, that prefix is not part",
3326
- "of its name \u2014 call the tool by its plain verb. Write your closing reply as the",
3327
- "last thing you say in the turn: you can interleave narration with tool calls, but",
3328
- "only your final message is recorded as the turn\u2019s reply."
3329
- ].join(" ");
3330
3282
 
3331
3283
  // packages/agent-runtime/src/opencode/events.ts
3332
3284
  function asRecord(v) {
@@ -3401,9 +3353,9 @@ function readSessionId(properties) {
3401
3353
  }
3402
3354
  function readSessionError(properties) {
3403
3355
  const props = asRecord(properties);
3404
- const err2 = props?.error;
3405
- if (typeof err2 === "string") return err2;
3406
- const rec = asRecord(err2);
3356
+ const err = props?.error;
3357
+ if (typeof err === "string") return err;
3358
+ const rec = asRecord(err);
3407
3359
  if (!rec) return "unknown";
3408
3360
  const { name, message } = deepestError(rec);
3409
3361
  if (message && name && !isGenericErrorName(name)) return `${name}: ${message}`;
@@ -3439,7 +3391,7 @@ async function* decodeOpencodeStream(events, ctx) {
3439
3391
  const pending = /* @__PURE__ */ new Map();
3440
3392
  const startedTools = /* @__PURE__ */ new Set();
3441
3393
  const finishedTools = /* @__PURE__ */ new Set();
3442
- let ok2 = false;
3394
+ let ok = false;
3443
3395
  let reason;
3444
3396
  let settled = false;
3445
3397
  const userMessageIds = /* @__PURE__ */ new Set();
@@ -3502,7 +3454,7 @@ async function* decodeOpencodeStream(events, ctx) {
3502
3454
  const sealed = sealHeld(held, true);
3503
3455
  held = null;
3504
3456
  if (sealed) yield sealed;
3505
- ok2 = true;
3457
+ ok = true;
3506
3458
  settled = true;
3507
3459
  break;
3508
3460
  } else if (ev.type === "session.error") {
@@ -3511,7 +3463,7 @@ async function* decodeOpencodeStream(events, ctx) {
3511
3463
  const sealed = sealHeld(held, false);
3512
3464
  held = null;
3513
3465
  if (sealed) yield sealed;
3514
- ok2 = false;
3466
+ ok = false;
3515
3467
  const errorText = readSessionError(ev.properties);
3516
3468
  const failure = classifyErrorText(errorText);
3517
3469
  reason = failure ? encodeFailureReason(failure) : `error:${errorText.slice(0, 200)}`;
@@ -3526,7 +3478,7 @@ async function* decodeOpencodeStream(events, ctx) {
3526
3478
  if (sealed) yield sealed;
3527
3479
  reason = "no_terminal";
3528
3480
  }
3529
- yield { type: "result", ok: ok2, ...reason ? { reason } : {} };
3481
+ yield { type: "result", ok, ...reason ? { reason } : {} };
3530
3482
  }
3531
3483
  function hasToolInput(input) {
3532
3484
  return !!input && typeof input === "object" && Object.keys(input).length > 0;
@@ -3540,7 +3492,7 @@ function sealHeld(held, terminal) {
3540
3492
  }
3541
3493
 
3542
3494
  // packages/agent-runtime/src/opencode/policy.ts
3543
- import { z as z11 } from "zod";
3495
+ import { z as z10 } from "zod";
3544
3496
  var OPENCODE_HOST_TOOLS = [
3545
3497
  "bash",
3546
3498
  "edit",
@@ -3567,8 +3519,8 @@ function opencodeToolPolicy(policy) {
3567
3519
  deny(OPENCODE_UI_PROMPT_TOOLS);
3568
3520
  return { tools, allowAllHostTools: policy.hostFs };
3569
3521
  }
3570
- var opencodeDialectSchema = z11.object({
3571
- agent: z11.string().min(1).optional()
3522
+ var opencodeDialectSchema = z10.object({
3523
+ agent: z10.string().min(1).optional()
3572
3524
  }).loose();
3573
3525
  function readOpencodeDialect(runtimeOptions) {
3574
3526
  const parsed = opencodeDialectSchema.safeParse(runtimeOptions?.["opencode"] ?? {});
@@ -3789,9 +3741,9 @@ function createHttpOpencodeTransport(opts) {
3789
3741
  // The lock is released when this stream finishes draining.
3790
3742
  events: releaseAfter(parseSseStream(eventRes.body, sessionId, signal), release)
3791
3743
  };
3792
- } catch (err2) {
3744
+ } catch (err) {
3793
3745
  release();
3794
- throw err2;
3746
+ throw err;
3795
3747
  }
3796
3748
  }
3797
3749
  };
@@ -3878,7 +3830,7 @@ function createOpencodeAdapter(deps = {}) {
3878
3830
  name: "opencode",
3879
3831
  // CT614: surface-aware — a code-mode turn (only `code` mounted) is taught
3880
3832
  // `code`, not the granular cabane names it no longer has.
3881
- promptAddendum: (codeMode = false) => codeMode ? OPENCODE_ADDENDUM_CODE_MODE : OPENCODE_ADDENDUM,
3833
+ promptAddendum: () => OPENCODE_ADDENDUM,
3882
3834
  dialectSchema: opencodeDialectSchema,
3883
3835
  async *runTurn(req, signal) {
3884
3836
  if (!transport) {
@@ -4278,14 +4230,6 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
4278
4230
 
4279
4231
  // packages/agent-runtime/src/codex/addendum.ts
4280
4232
  var CODEX_ADDENDUM = [
4281
- "Your Cabane tools have plain names \u2014 `read`, `write`, `search`, `edit`,",
4282
- "`post_message`, and so on. If a tool appears in this prompt with an `mcp__\u2026__`",
4283
- "prefix, that prefix is not part of its name \u2014 call the tool by its plain verb.",
4284
- "Write your closing reply as the last thing you say in the turn: you can",
4285
- "interleave narration with tool calls, but only your final message is recorded",
4286
- "as the turn\u2019s reply."
4287
- ].join(" ");
4288
- var CODEX_ADDENDUM_CODE_MODE = [
4289
4233
  "Your Cabane workspace tool is exposed to Codex as the qualified MCP tool",
4290
4234
  "`mcp__cabane__sdk`. On Codex versions that defer MCP tools, locate it in the",
4291
4235
  "`functions.exec` deferred-tool inventory and invoke that exact qualified tool",
@@ -4294,13 +4238,14 @@ var CODEX_ADDENDUM_CODE_MODE = [
4294
4238
  "declare the SDK absent without attempting discovery and invocation. The SDK",
4295
4239
  "call runs a TypeScript program against the ambient `cabane` object. The",
4296
4240
  "turn-control verbs (`ask`, `wake_me`, `summon_agent`, `skip_turn`) are",
4297
- "qualified `mcp__cabane_companion__\u2026` tools (and may be deferred too). There is",
4298
- "no Cabane `read`/`write`/`search`/`edit` tool here \u2014 those",
4299
- "are `cabane` SDK calls inside your program, not tools. If a tool appears in this",
4300
- "prompt with an `mcp__\u2026__` prefix, preserve that qualified name. Write your",
4301
- "closing reply as the last thing you say in the",
4302
- "turn: you can interleave narration with tool calls, but only your final message",
4303
- "is recorded as the turn\u2019s reply."
4241
+ "qualified `mcp__cabane_companion__\u2026` tools (and may be deferred too), as are",
4242
+ "the few ancillary cabane tools (`mcp__cabane__list_workspaces`,",
4243
+ "`mcp__cabane__read_binary`, the upload tools, `mcp__cabane__mint_render_token`).",
4244
+ "There is no Cabane `read`/`write`/`search`/`edit` tool here \u2014 those are `cabane`",
4245
+ "SDK calls inside your program, not tools. If a tool appears in this prompt with",
4246
+ "an `mcp__\u2026__` prefix, preserve that qualified name. Write your closing reply as",
4247
+ "the last thing you say in the turn: you can interleave narration with tool",
4248
+ "calls, but only your final message is recorded as the turn\u2019s reply."
4304
4249
  ].join(" ");
4305
4250
 
4306
4251
  // packages/agent-runtime/src/codex/events.ts
@@ -4384,9 +4329,9 @@ function readItemType(item) {
4384
4329
  function readErrorMessage(ev) {
4385
4330
  const direct = str(ev.message);
4386
4331
  if (direct) return direct;
4387
- const err2 = asRecord2(ev.error);
4388
- if (err2) {
4389
- const m = str(err2.message);
4332
+ const err = asRecord2(ev.error);
4333
+ if (err) {
4334
+ const m = str(err.message);
4390
4335
  if (m) return m;
4391
4336
  }
4392
4337
  return "unknown";
@@ -4442,7 +4387,7 @@ async function* decodeCodexStream(events, ctx) {
4442
4387
  const startedTools = /* @__PURE__ */ new Set();
4443
4388
  const finishedTools = /* @__PURE__ */ new Set();
4444
4389
  let sessionEmitted = false;
4445
- let ok2 = false;
4390
+ let ok = false;
4446
4391
  let reason;
4447
4392
  let usage;
4448
4393
  let settled = false;
@@ -4473,7 +4418,7 @@ async function* decodeCodexStream(events, ctx) {
4473
4418
  const message = readItemMessage(item);
4474
4419
  if (isModelMetadataError(message)) {
4475
4420
  yield* flushInterim();
4476
- ok2 = false;
4421
+ ok = false;
4477
4422
  reason = `model_unavailable:${message.slice(0, 200)}`;
4478
4423
  settled = true;
4479
4424
  break;
@@ -4528,13 +4473,13 @@ async function* decodeCodexStream(events, ctx) {
4528
4473
  held = null;
4529
4474
  if (sealed) yield sealed;
4530
4475
  usage = readUsage(ev);
4531
- ok2 = true;
4476
+ ok = true;
4532
4477
  settled = true;
4533
4478
  break;
4534
4479
  }
4535
4480
  if (ev.type === "turn.failed" || ev.type === "error") {
4536
4481
  yield* flushInterim();
4537
- ok2 = false;
4482
+ ok = false;
4538
4483
  const text = readErrorMessage(ev);
4539
4484
  const failure = classifyErrorText(text);
4540
4485
  reason = failure ? encodeFailureReason(failure) : `error:${text.slice(0, 200)}`;
@@ -4552,7 +4497,7 @@ async function* decodeCodexStream(events, ctx) {
4552
4497
  const resolvedConfig = ctx.resolvedReasoningEffort ? { reasoningEffort: ctx.resolvedReasoningEffort } : void 0;
4553
4498
  yield {
4554
4499
  type: "result",
4555
- ok: ok2,
4500
+ ok,
4556
4501
  ...reason ? { reason } : {},
4557
4502
  ...usage ? { usage } : {},
4558
4503
  ...ctx.resolvedModel ? { resolvedModel: ctx.resolvedModel } : {},
@@ -4570,7 +4515,7 @@ function sealHeld2(held, terminal) {
4570
4515
  }
4571
4516
 
4572
4517
  // packages/agent-runtime/src/codex/policy.ts
4573
- import { z as z12 } from "zod";
4518
+ import { z as z11 } from "zod";
4574
4519
  function codexToolPolicy(policy) {
4575
4520
  return policy.hostFs ? {
4576
4521
  permissionProfile: "cabane-coding",
@@ -4585,8 +4530,8 @@ function codexToolPolicy(policy) {
4585
4530
  };
4586
4531
  }
4587
4532
  var CODEX_REASONING_EFFORTS = ["minimal", "low", "medium", "high", "xhigh"];
4588
- var codexDialectSchema = z12.object({
4589
- modelReasoningEffort: z12.enum(CODEX_REASONING_EFFORTS).optional()
4533
+ var codexDialectSchema = z11.object({
4534
+ modelReasoningEffort: z11.enum(CODEX_REASONING_EFFORTS).optional()
4590
4535
  }).loose();
4591
4536
  function readCodexDialect(runtimeOptions) {
4592
4537
  const parsed = codexDialectSchema.safeParse(runtimeOptions?.["codex"] ?? {});
@@ -4594,7 +4539,7 @@ function readCodexDialect(runtimeOptions) {
4594
4539
  }
4595
4540
 
4596
4541
  // packages/agent-runtime/src/codex/model.ts
4597
- var CODEX_DEFAULT_MODEL_BARES = /* @__PURE__ */ new Set(["default", "codex"]);
4542
+ var CODEX_DEFAULT_MODEL_BARES = /* @__PURE__ */ new Set(["default"]);
4598
4543
  function parseCodexModel(model) {
4599
4544
  const sep = model.indexOf("/");
4600
4545
  const bare = sep === -1 || model.slice(0, sep) !== "openai" ? model : model.slice(sep + 1);
@@ -4609,10 +4554,16 @@ function buildRunSpec2(req, resumeThreadId) {
4609
4554
  const { policy, config } = req;
4610
4555
  const directory = req.local.cwd ?? "";
4611
4556
  const dialect = readCodexDialect(config.runtimeOptions);
4557
+ const model = config.model ? parseCodexModel(config.model) : null;
4558
+ if (model === null) {
4559
+ console.warn(
4560
+ `[agent-runtime/codex] CT823: dispatching a subscription turn with NO concrete model (config.model=${JSON.stringify(config.model)}) \u2014 Codex will fall back to its own default. This should be unreachable post-CT823; the model resolver should have supplied a concrete house default.`
4561
+ );
4562
+ }
4612
4563
  return {
4613
4564
  resumeThreadId,
4614
4565
  directory,
4615
- model: config.model ? parseCodexModel(config.model) : null,
4566
+ model,
4616
4567
  policy: codexToolPolicy(policy),
4617
4568
  skipGitRepoCheck: true,
4618
4569
  ...dialect.modelReasoningEffort ? { modelReasoningEffort: dialect.modelReasoningEffort } : {},
@@ -4681,9 +4632,11 @@ function buildConfig(req) {
4681
4632
  };
4682
4633
  }
4683
4634
  const policy = codexToolPolicy(req.policy);
4635
+ const tmpDir = req.local.env?.TMPDIR;
4684
4636
  return {
4685
4637
  mcp_servers,
4686
4638
  experimental_use_rmcp_client: true,
4639
+ ...tmpDir ? { shell_environment_policy: { set: { TMPDIR: tmpDir } } } : {},
4687
4640
  ...policy.permissionProfile ? {
4688
4641
  // CT733: named permission profiles are Codex's split-filesystem path.
4689
4642
  // `:root = read` preserves coding-mode host reads; the one explicit
@@ -4961,7 +4914,7 @@ var CodexExec = class {
4961
4914
  signal: args.signal
4962
4915
  });
4963
4916
  let spawnError = null;
4964
- child.once("error", (err2) => spawnError = err2);
4917
+ child.once("error", (err) => spawnError = err);
4965
4918
  if (!child.stdin) {
4966
4919
  child.kill();
4967
4920
  throw new Error("Child process has no stdin");
@@ -5278,7 +5231,7 @@ function createCodexAdapter(deps = {}) {
5278
5231
  name: "codex",
5279
5232
  // CT614: surface-aware — a code-mode turn (only `code` mounted) is taught
5280
5233
  // `code`, not the granular names it no longer has.
5281
- promptAddendum: (codeMode = false) => codeMode ? CODEX_ADDENDUM_CODE_MODE : CODEX_ADDENDUM,
5234
+ promptAddendum: () => CODEX_ADDENDUM,
5282
5235
  dialectSchema: codexDialectSchema,
5283
5236
  async *runTurn(req, signal) {
5284
5237
  if (!transport) {
@@ -5865,1027 +5818,6 @@ var CODEX_CONFORMANCE_FIXTURES = [
5865
5818
  }
5866
5819
  ];
5867
5820
 
5868
- // packages/agent-runtime/src/cabane-native/addendum.ts
5869
- var CABANE_NATIVE_ADDENDUM = `## Your tools (native runtime)
5870
-
5871
- You are running on Cabane's own agent runtime. You have a small, curated set of workspace tools, all prefixed \`cabane_\`:
5872
-
5873
- - \`cabane_list\` \u2014 list a folder's files and subfolders.
5874
- - \`cabane_read\` \u2014 read one file's contents by path.
5875
- - \`cabane_search\` \u2014 substring search across file/folder names, file contents, and conversations.
5876
- - \`cabane_write\` \u2014 create or overwrite a file (pass \`overwrite: true\` to replace).
5877
- - \`cabane_edit\` \u2014 find/replace inside an existing file.
5878
- - \`cabane_mkdir\` \u2014 create a folder (pass \`recursive: true\` to also make missing parents).
5879
- - \`cabane_move\` \u2014 move or rename a file or folder.
5880
- - \`cabane_delete\` \u2014 delete a file or folder (folders need \`recursive: true\`; soft-delete, recoverable).
5881
- - \`cabane_context\` \u2014 gather an object plus its related context (files, conversations, people) in one call \u2014 your first move to understand what surrounds something.
5882
- - \`cabane_messages\` \u2014 read a conversation's messages by id (from \`cabane_search\` / \`cabane_context\`).
5883
-
5884
- Paths are workspace-relative with a leading slash (\`/notes/todo.md\`). This is a deliberately curated tool set \u2014 if a job needs an operation that isn't here, say so plainly in your reply rather than inventing a tool. Your reply text is streamed straight into the conversation; there is no separate send step.`;
5885
- var CABANE_NATIVE_ADDENDUM_CODE_MODE = `## Your tools (native runtime \u2014 code mode)
5886
-
5887
- You are running on Cabane's own agent runtime, in **code mode**. Your one workspace tool is \`sdk\`: you act on the workspace by writing a short TypeScript program against the ambient \`cabane\` object and submitting it as the \`code\` argument. Only what your program \`return\`s (plus \`console.log\`) comes back \u2014 so gather, filter, and summarize *inside* the program and return the small answer, not the raw material.
5888
-
5889
- There is **no** \`cabane_read\` / \`cabane_write\` / \`cabane_search\` / \`cabane_edit\` tool \u2014 those are \`cabane\` SDK calls *inside* your program (\`cabane.files.read(path)\`, \`cabane.find.search(q)\`, \u2026), not tools you call directly. The full SDK surface \u2014 every method and its options \u2014 is documented below this note in your prompt.
5890
-
5891
- Beside \`sdk\` you also have a few **plain-named** tools that act on the TURN itself, not the workspace (they can't be a program's \`return\` value):
5892
- - \`ask\` \u2014 put a structured question to a human and END your turn (they may answer in days).
5893
- - \`wake_me\` \u2014 end this turn now and be re-dispatched later to check a condition (a PR merging, a reply landing).
5894
- - \`summon_agent\` \u2014 pull a peer agent into THIS conversation to reply on your turn.
5895
- - \`sub_agent\` \u2014 spawn a private worker with a fresh context window; its result comes back here later (don't wait \u2014 end your turn).
5896
- - \`skip_turn\` \u2014 end your turn with NO reply, when the message doesn't need one from you.
5897
- - \`mint_render_token\` \u2014 mint a short-lived credential to load a workspace HTML page in a browser.
5898
-
5899
- Your reply text is streamed straight into the conversation; there is no separate send step.`;
5900
-
5901
- // packages/agent-runtime/src/cabane-native/context.ts
5902
- var DEFAULT_HISTORY_LIMIT = 20;
5903
- var DEFAULT_MAX_HISTORY_CHARS = 24e3;
5904
- async function assembleMessages(systemPrompt, content, fallbackPrompt, opts) {
5905
- const messages = [{ role: "system", content: systemPrompt }];
5906
- const history = await fetchRecentHistory(opts);
5907
- for (const m of history) messages.push(m);
5908
- messages.push({ role: "user", content: currentUserText(content, fallbackPrompt) });
5909
- return messages;
5910
- }
5911
- function currentUserText(content, fallbackPrompt) {
5912
- const text = content.filter((b) => b.type === "text").map((b) => b.text).join("\n").trim();
5913
- return text.length > 0 ? text : fallbackPrompt;
5914
- }
5915
- async function fetchRecentHistory(opts) {
5916
- const doFetch = opts.fetchImpl ?? fetch;
5917
- const limit = opts.historyLimit ?? DEFAULT_HISTORY_LIMIT;
5918
- const url = new URL(
5919
- `${opts.apiRoot}/workspaces/${opts.workspaceId}/conversations/${opts.conversationId}/messages`
5920
- );
5921
- url.searchParams.set("limit", String(limit));
5922
- url.searchParams.set("order", "desc");
5923
- let rows;
5924
- try {
5925
- const res = await doFetch(url.toString(), {
5926
- headers: { Authorization: `Bearer ${opts.bearer}` }
5927
- });
5928
- if (!res.ok) return [];
5929
- const body = await res.json();
5930
- rows = body.messages ?? [];
5931
- } catch {
5932
- return [];
5933
- }
5934
- const chronological = [...rows].reverse();
5935
- while (chronological.length > 0 && chronological[chronological.length - 1].role === "user") {
5936
- chronological.pop();
5937
- }
5938
- const mapped = [];
5939
- for (const r of chronological) {
5940
- const role = r.role === "agent" ? "assistant" : r.role === "user" ? "user" : null;
5941
- if (!role) continue;
5942
- const body = (r.body ?? "").trim();
5943
- if (body.length === 0) continue;
5944
- mapped.push({ role, content: body });
5945
- }
5946
- return capHistory(mapped, opts.maxHistoryChars ?? DEFAULT_MAX_HISTORY_CHARS);
5947
- }
5948
- function capHistory(messages, maxChars) {
5949
- let total = messages.reduce((n, m) => n + m.content.length, 0);
5950
- let start = 0;
5951
- while (total > maxChars && start < messages.length) {
5952
- total -= messages[start].content.length;
5953
- start += 1;
5954
- }
5955
- return messages.slice(start);
5956
- }
5957
-
5958
- // packages/agent-runtime/src/cabane-native/model.ts
5959
- var CABANE_NATIVE_MODEL_PREFIX = "cabane-native/";
5960
- function parseCabaneNativeModel(model) {
5961
- return model.startsWith(CABANE_NATIVE_MODEL_PREFIX) ? model.slice(CABANE_NATIVE_MODEL_PREFIX.length) : model;
5962
- }
5963
-
5964
- // packages/agent-runtime/src/cabane-native/policy.ts
5965
- import { z as z13 } from "zod";
5966
- var NATIVE_SURFACES = ["code", "classic"];
5967
- var cabaneNativeDialectSchema = z13.object({ surface: z13.enum(NATIVE_SURFACES).optional() }).loose();
5968
- function readCabaneNativeDialect(runtimeOptions) {
5969
- const parsed = cabaneNativeDialectSchema.safeParse(runtimeOptions?.["cabane-native"] ?? {});
5970
- return parsed.success ? parsed.data : {};
5971
- }
5972
- function cabaneNativeSurface(runtimeOptions) {
5973
- return readCabaneNativeDialect(runtimeOptions).surface ?? "code";
5974
- }
5975
-
5976
- // packages/agent-runtime/src/cabane-native/tools.ts
5977
- var TOOL_RESULT_MAX_CHARS = 8e3;
5978
- var CABANE_NATIVE_TOOLS = [
5979
- {
5980
- type: "function",
5981
- function: {
5982
- name: "cabane_list",
5983
- description: "List the files and subfolders at a workspace folder path. Omit `path` for the root.",
5984
- parameters: {
5985
- type: "object",
5986
- properties: {
5987
- path: {
5988
- type: "string",
5989
- description: "Workspace folder path, e.g. /notes. Defaults to /."
5990
- }
5991
- }
5992
- }
5993
- }
5994
- },
5995
- {
5996
- type: "function",
5997
- function: {
5998
- name: "cabane_read",
5999
- description: "Read the contents of one file at a workspace path.",
6000
- parameters: {
6001
- type: "object",
6002
- properties: {
6003
- path: { type: "string", description: "Workspace file path, e.g. /notes/todo.md." }
6004
- },
6005
- required: ["path"]
6006
- }
6007
- }
6008
- },
6009
- {
6010
- type: "function",
6011
- function: {
6012
- name: "cabane_search",
6013
- description: "Case-insensitive substring search across file names and file contents. Optionally scope to a subtree with `path`.",
6014
- parameters: {
6015
- type: "object",
6016
- properties: {
6017
- q: { type: "string", description: "The search string." },
6018
- path: {
6019
- type: "string",
6020
- description: "Optional workspace subtree to scope the search to."
6021
- }
6022
- },
6023
- required: ["q"]
6024
- }
6025
- }
6026
- },
6027
- {
6028
- type: "function",
6029
- function: {
6030
- name: "cabane_write",
6031
- description: "Create a file at a workspace path (missing parent folders are created). Pass `overwrite: true` to replace an existing file instead of failing on a name conflict.",
6032
- parameters: {
6033
- type: "object",
6034
- properties: {
6035
- path: { type: "string", description: "Workspace file path, e.g. /notes/new.md." },
6036
- content: { type: "string", description: "The file contents." },
6037
- overwrite: { type: "boolean", description: "Replace an existing file (default false)." }
6038
- },
6039
- required: ["path", "content"]
6040
- }
6041
- }
6042
- },
6043
- {
6044
- type: "function",
6045
- function: {
6046
- name: "cabane_edit",
6047
- description: "Modify an existing file with a single find/replace. By default `find` must occur exactly once; set `replaceAll: true` to replace every occurrence.",
6048
- parameters: {
6049
- type: "object",
6050
- properties: {
6051
- path: { type: "string", description: "Workspace file path to edit." },
6052
- find: { type: "string", description: "The substring to find." },
6053
- replace: { type: "string", description: "The replacement." },
6054
- replaceAll: { type: "boolean", description: "Replace every occurrence (default false)." }
6055
- },
6056
- required: ["path", "find", "replace"]
6057
- }
6058
- }
6059
- },
6060
- {
6061
- type: "function",
6062
- function: {
6063
- name: "cabane_mkdir",
6064
- description: "Create a folder at a workspace path. Pass `recursive: true` to also create any missing parent folders (like `mkdir -p`); otherwise the parent must already exist.",
6065
- parameters: {
6066
- type: "object",
6067
- properties: {
6068
- path: { type: "string", description: "Workspace folder path, e.g. /notes/archive." },
6069
- recursive: {
6070
- type: "boolean",
6071
- description: "Create missing parent folders too (default false)."
6072
- }
6073
- },
6074
- required: ["path"]
6075
- }
6076
- }
6077
- },
6078
- {
6079
- type: "function",
6080
- function: {
6081
- name: "cabane_move",
6082
- description: "Move and/or rename a file or folder. `fromPath` is the current path; `toPath` is the new full path (its parent must already exist). Same parent + new name renames in place; a different parent moves it.",
6083
- parameters: {
6084
- type: "object",
6085
- properties: {
6086
- fromPath: { type: "string", description: "The current file/folder path." },
6087
- toPath: { type: "string", description: "The new full path (parent + name)." }
6088
- },
6089
- required: ["fromPath", "toPath"]
6090
- }
6091
- }
6092
- },
6093
- {
6094
- type: "function",
6095
- function: {
6096
- name: "cabane_delete",
6097
- description: "Delete the file or folder at a workspace path. Folders require `recursive: true` (deletes everything inside). Soft delete \u2014 the entry is recoverable from the trash, not erased.",
6098
- parameters: {
6099
- type: "object",
6100
- properties: {
6101
- path: { type: "string", description: "Workspace file/folder path to delete." },
6102
- recursive: {
6103
- type: "boolean",
6104
- description: "Required to delete a non-empty folder (default false)."
6105
- }
6106
- },
6107
- required: ["path"]
6108
- }
6109
- }
6110
- },
6111
- {
6112
- type: "function",
6113
- function: {
6114
- name: "cabane_context",
6115
- description: "Gather a workspace object plus its most relevant connected context \u2014 related files, conversations, people \u2014 in one call. Your first move to understand what surrounds something before acting. `ref` is the object id, or its workspace path when `kind` is `file` or `folder`.",
6116
- parameters: {
6117
- type: "object",
6118
- properties: {
6119
- kind: {
6120
- type: "string",
6121
- enum: ["conversation", "file", "folder", "user", "agent", "channel"],
6122
- description: "The kind of object to orient around."
6123
- },
6124
- ref: {
6125
- type: "string",
6126
- description: "The object id (or the workspace path for a file/folder)."
6127
- }
6128
- },
6129
- required: ["kind", "ref"]
6130
- }
6131
- }
6132
- },
6133
- {
6134
- type: "function",
6135
- function: {
6136
- name: "cabane_messages",
6137
- description: 'Read a page of messages from a conversation by id (get ids from `cabane_search` or `cabane_context`). `order: "desc"` (default) returns newest-first \u2014 where a thread is now; `order: "asc"` returns oldest-first \u2014 how it began. Your own current thread is already in context; use this to catch up on other conversations or to page further back.',
6138
- parameters: {
6139
- type: "object",
6140
- properties: {
6141
- conversationId: { type: "string", description: "The conversation id to read." },
6142
- order: {
6143
- type: "string",
6144
- enum: ["asc", "desc"],
6145
- description: "newest-first (desc, default) or oldest-first (asc)."
6146
- },
6147
- limit: {
6148
- type: "number",
6149
- description: "Max messages to return (default 50, max 100)."
6150
- }
6151
- },
6152
- required: ["conversationId"]
6153
- }
6154
- }
6155
- }
6156
- ];
6157
- var CABANE_NATIVE_CODE_TOOL = {
6158
- type: "function",
6159
- function: {
6160
- name: "sdk",
6161
- description: "Run a TypeScript program against the workspace. `code` is the program \u2014 the body of an async function, so top-level `await` works and `return <value>` produces the result. Write against the ambient `cabane` object (no imports); only what you `return` plus `console.log` comes back, so gather/filter/summarize inside the program and return the small answer. The full `cabane` SDK surface \u2014 every method and its options \u2014 is documented in your system prompt above.",
6162
- parameters: {
6163
- type: "object",
6164
- properties: {
6165
- code: {
6166
- type: "string",
6167
- description: "The TypeScript program to run (an async function body)."
6168
- }
6169
- },
6170
- required: ["code"]
6171
- }
6172
- }
6173
- };
6174
- var CABANE_NATIVE_RENDER_TOKEN_TOOL = {
6175
- type: "function",
6176
- function: {
6177
- name: "mint_render_token",
6178
- description: "Mint a short-lived, scoped credential that lets a browser load a workspace HTML page at the content-isolation origin. Returns `{ url, cookieName, cookieValue, expiresAt, pathPrefix }`. `pathPrefix` (default `/`) narrows the grant to a folder or single file; `ttlSeconds` (default 30 min, max 86400) overrides the TTL.",
6179
- parameters: {
6180
- type: "object",
6181
- properties: {
6182
- pathPrefix: {
6183
- type: "string",
6184
- description: "Workspace-rooted path-prefix the token authorizes; `/` for whole-workspace."
6185
- },
6186
- ttlSeconds: {
6187
- type: "number",
6188
- description: "Override the default 30-minute TTL; capped at 24h (86400s)."
6189
- }
6190
- }
6191
- }
6192
- }
6193
- };
6194
- function summarizeCabaneToolArgs(name, args) {
6195
- if (name === "mint_render_token") return str2(args.pathPrefix) ?? "/";
6196
- if (name === "sdk") {
6197
- const code = str2(args.code) ?? "";
6198
- const firstLine = code.split("\n").find((l) => l.trim().length > 0)?.trim() ?? "";
6199
- return firstLine.length > 80 ? `${firstLine.slice(0, 80)}\u2026` : firstLine;
6200
- }
6201
- if (name === "cabane_search") return str2(args.q) ?? "";
6202
- if (name === "cabane_move") {
6203
- const from = str2(args.fromPath) ?? "";
6204
- const to = str2(args.toPath) ?? "";
6205
- return from && to ? `${from} \u2192 ${to}` : from || to;
6206
- }
6207
- if (name === "cabane_context") return str2(args.ref) ?? "";
6208
- if (name === "cabane_messages") return str2(args.conversationId) ?? "";
6209
- return str2(args.path) ?? "";
6210
- }
6211
- async function executeCabaneNativeTool(name, args, ctx, signal) {
6212
- const doFetch = ctx.fetchImpl ?? fetch;
6213
- const wsBase = `${ctx.apiRoot}/workspaces/${ctx.workspaceId}`;
6214
- const headers = { Authorization: `Bearer ${ctx.bearer}`, "Content-Type": "application/json" };
6215
- const call = async (method, path3, init2) => {
6216
- const url = new URL(`${wsBase}${path3}`);
6217
- for (const [k, v] of Object.entries(init2?.query ?? {})) {
6218
- if (v !== void 0) url.searchParams.set(k, v);
6219
- }
6220
- let res;
6221
- try {
6222
- res = await doFetch(url.toString(), {
6223
- method,
6224
- headers,
6225
- ...init2?.body !== void 0 ? { body: JSON.stringify(init2.body) } : {},
6226
- signal
6227
- });
6228
- } catch (err2) {
6229
- return {
6230
- ok: false,
6231
- result: `error: request failed: ${err2 instanceof Error ? err2.message : String(err2)}`
6232
- };
6233
- }
6234
- const contentType = res.headers.get("content-type") ?? "";
6235
- const text = contentType.includes("application/json") ? JSON.stringify(await res.json().catch(() => ({}))) : await res.text().catch(() => "");
6236
- if (!res.ok) return { ok: false, result: truncate2(`error ${res.status}: ${text}`) };
6237
- return { ok: true, result: truncate2(text) };
6238
- };
6239
- switch (name) {
6240
- case "sdk":
6241
- return call("POST", "/sdk", {
6242
- body: {
6243
- program: str2(args.code) ?? "",
6244
- ...ctx.conversationId ? { activeConversationId: ctx.conversationId } : {}
6245
- }
6246
- });
6247
- case "mint_render_token":
6248
- return call("POST", "/render-tokens", {
6249
- body: {
6250
- pathPrefix: str2(args.pathPrefix) ?? "/",
6251
- ...typeof args.ttlSeconds === "number" ? { ttlSeconds: args.ttlSeconds } : {}
6252
- }
6253
- });
6254
- case "cabane_list":
6255
- return call("GET", "/files/tree", { query: { path: str2(args.path) ?? "/" } });
6256
- case "cabane_read":
6257
- return call("GET", "/files/content", { query: { path: str2(args.path) } });
6258
- case "cabane_search":
6259
- return call("GET", "/search", { query: { q: str2(args.q), path: str2(args.path) } });
6260
- case "cabane_write": {
6261
- const overwrite = args.overwrite === true;
6262
- return overwrite ? call("PUT", "/files/content", {
6263
- body: { path: str2(args.path), content: str2(args.content) }
6264
- }) : call("POST", "/files", {
6265
- body: { path: str2(args.path), content: str2(args.content), mkdirs: true }
6266
- });
6267
- }
6268
- case "cabane_edit":
6269
- return call("PATCH", "/files", {
6270
- body: {
6271
- path: str2(args.path),
6272
- find: str2(args.find),
6273
- replace: str2(args.replace) ?? "",
6274
- ...args.replaceAll === true ? { replaceAll: true } : {}
6275
- }
6276
- });
6277
- case "cabane_mkdir":
6278
- return call("POST", "/folders", {
6279
- body: {
6280
- path: str2(args.path),
6281
- ...args.recursive === true ? { recursive: true } : {}
6282
- }
6283
- });
6284
- case "cabane_move":
6285
- return call("POST", "/entries/move", {
6286
- body: { fromPath: str2(args.fromPath), toPath: str2(args.toPath) }
6287
- });
6288
- case "cabane_delete":
6289
- return call("DELETE", "/entries", {
6290
- query: {
6291
- path: str2(args.path),
6292
- recursive: args.recursive === true ? "true" : void 0
6293
- }
6294
- });
6295
- case "cabane_context":
6296
- return call("POST", "/graph/query", {
6297
- body: { preset: "orient", args: { kind: str2(args.kind), ref: str2(args.ref) } }
6298
- });
6299
- case "cabane_messages":
6300
- return call(
6301
- "GET",
6302
- `/conversations/${encodeURIComponent(str2(args.conversationId) ?? "")}/messages`,
6303
- {
6304
- query: {
6305
- order: str2(args.order),
6306
- limit: typeof args.limit === "number" ? String(args.limit) : void 0
6307
- }
6308
- }
6309
- );
6310
- default:
6311
- return { ok: false, result: `error: unknown tool "${name}"` };
6312
- }
6313
- }
6314
- function str2(v) {
6315
- return typeof v === "string" ? v : void 0;
6316
- }
6317
- function truncate2(s) {
6318
- return s.length > TOOL_RESULT_MAX_CHARS ? `${s.slice(0, TOOL_RESULT_MAX_CHARS)}
6319
- \u2026 [truncated]` : s;
6320
- }
6321
-
6322
- // packages/agent-runtime/src/cabane-native/turn-control.ts
6323
- function describeSubAgentError(status, body) {
6324
- const code = body && typeof body === "object" && "error" in body ? String(body.error) : void 0;
6325
- switch (code) {
6326
- case "callout_cap_exceeded":
6327
- return "sub_agent: you already have the maximum open sub-agents for this thread. Wait for some to return \u2014 you're woken once they're all back \u2014 before spawning more.";
6328
- case "callout_depth_exceeded":
6329
- return "sub_agent: this would nest sub-agents too deep (max 3 levels). Have the current worker report back rather than spawning another layer.";
6330
- case "dispatch_agent_not_found":
6331
- return "sub_agent: no live agent in this workspace matches that `agentId`. Check `list_agents`, or omit `agentId` to spawn yourself.";
6332
- case "dispatch_return_requires_turn":
6333
- case "dispatch_return_requires_agent":
6334
- case "dispatch_return_requires_dispatch":
6335
- return `sub_agent: the spawn was rejected (${code}). This is a turn-context problem, not something to retry blindly \u2014 report it rather than looping.`;
6336
- default:
6337
- return `sub_agent: the spawn failed (${code ?? `HTTP ${status}`}).`;
6338
- }
6339
- }
6340
- var CABANE_NATIVE_TURN_CONTROL_TOOLS = [
6341
- {
6342
- type: "function",
6343
- function: {
6344
- name: "ask",
6345
- description: "Ask a HUMAN a structured question you need answered to continue, then END your turn (do not wait \u2014 a reply may take days). Pass `targetUserId` (a workspace member id). Single form: a one-sentence `headline` (the question, capitalized, ending in `?`) + a short `question` body of framing, with 2\u20134 `options` when the answer is a bounded/yes-no choice. Or `questions`: 1\u20135 items each `{ headline, body?, options? }` when several decisions land at once. Provide EITHER `question` or `questions`, never both. Your surrounding context goes in your reply; the ask carries the question.",
6346
- parameters: {
6347
- type: "object",
6348
- properties: {
6349
- targetUserId: {
6350
- type: "string",
6351
- description: "The workspace member (human) to ask \u2014 a user id from the roster."
6352
- },
6353
- question: {
6354
- type: "string",
6355
- description: "Single-question form: a short body of framing (one or two sentences)."
6356
- },
6357
- headline: {
6358
- type: "string",
6359
- description: "Single-question form: the question itself as one clear capitalized sentence ending in `?`."
6360
- },
6361
- options: {
6362
- type: "array",
6363
- items: { type: "string" },
6364
- description: "Single-question form: 2\u20134 suggested one-click answers."
6365
- },
6366
- questions: {
6367
- type: "array",
6368
- items: {
6369
- type: "object",
6370
- properties: {
6371
- headline: { type: "string" },
6372
- body: { type: "string" },
6373
- options: { type: "array", items: { type: "string" } }
6374
- },
6375
- required: ["headline"]
6376
- },
6377
- description: "Multi-question form: 1\u20135 questions to ask at once."
6378
- }
6379
- },
6380
- required: ["targetUserId"]
6381
- }
6382
- }
6383
- },
6384
- {
6385
- type: "function",
6386
- function: {
6387
- name: "wake_me",
6388
- description: 'Wake yourself later \u2014 end this turn now and be re-dispatched at a time you pick, to CHECK a condition that has not happened yet (a PR merging, a reply landing, "check back in five minutes"). Pass EXACTLY ONE of `afterSeconds` (a relative delay, \u226560) or `at` (an absolute ISO-8601 timestamp WITH a zone you compute yourself). `note` is the message to your future self \u2014 write the condition to re-check. It arms when the turn ends; one wake per turn.',
6389
- parameters: {
6390
- type: "object",
6391
- properties: {
6392
- afterSeconds: {
6393
- type: "number",
6394
- description: "Relative delay in seconds from when this turn ends (floor 60)."
6395
- },
6396
- at: {
6397
- type: "string",
6398
- description: "Absolute ISO-8601 timestamp with a zone (e.g. 2026-07-16T09:00:00-07:00)."
6399
- },
6400
- note: {
6401
- type: "string",
6402
- description: "A note to your future self \u2014 becomes the wake body (the condition to re-check)."
6403
- }
6404
- },
6405
- required: ["note"]
6406
- }
6407
- }
6408
- },
6409
- {
6410
- type: "function",
6411
- function: {
6412
- name: "summon_agent",
6413
- description: "Summon another agent into THIS conversation \u2014 dispatch a peer to reply here on your turn. Pass the peer `agentId` (from the agent roster). The peer is dispatched on your final reply and picks up this conversation as context, so write the context/ask into your reply first. Writing `@handle` in prose summons no one \u2014 this tool is the only in-thread lever. Single target, last call wins; summoning yourself is a no-op.",
6414
- parameters: {
6415
- type: "object",
6416
- properties: {
6417
- agentId: {
6418
- type: "string",
6419
- description: "The peer agent to summon \u2014 a workspace agent id from the roster."
6420
- }
6421
- },
6422
- required: ["agentId"]
6423
- }
6424
- }
6425
- },
6426
- {
6427
- type: "function",
6428
- function: {
6429
- name: "sub_agent",
6430
- description: "Spawn a sub-agent \u2014 hand a piece of work to a private worker with a fresh context window whose result comes back to you automatically. It does NOT return inline: this returns the child's id immediately and the outcome lands LATER as a message in this conversation (you're woken once every sub-agent you have out here has returned). So don't wait \u2014 finish what else this turn can do and end your turn. `prompt` is the child's self-contained opening instruction; `agentId` (optional) dispatches a peer instead of yourself; `title` (optional) names the child thread. Reach for it to isolate a big read or fan out N independent pieces in parallel.",
6431
- parameters: {
6432
- type: "object",
6433
- properties: {
6434
- prompt: {
6435
- type: "string",
6436
- description: "The sub-agent's self-contained opening instruction."
6437
- },
6438
- agentId: {
6439
- type: "string",
6440
- description: "Optional peer to run the sub-agent as; omit to spawn yourself."
6441
- },
6442
- title: { type: "string", description: "Optional title for the child thread." }
6443
- },
6444
- required: ["prompt"]
6445
- }
6446
- }
6447
- },
6448
- {
6449
- type: "function",
6450
- function: {
6451
- name: "skip_turn",
6452
- description: "End your current turn WITHOUT posting a reply. Call this when you've been dispatched but the message genuinely doesn't need a response from you \u2014 an aside, a question already answered, a pile-on someone else has. Your turn ends silently (no message bubble). `reason` is a short free-text note for telemetry. Prefer this over posting a low-value \"ok!\"; skipping IS the whole turn.",
6453
- parameters: {
6454
- type: "object",
6455
- properties: {
6456
- reason: {
6457
- type: "string",
6458
- description: "Short reason you are declining \u2014 used for telemetry."
6459
- }
6460
- },
6461
- required: ["reason"]
6462
- }
6463
- }
6464
- }
6465
- ];
6466
- var CABANE_NATIVE_TURN_CONTROL_NAMES = new Set(
6467
- CABANE_NATIVE_TURN_CONTROL_TOOLS.map((t) => t.function.name)
6468
- );
6469
- function summarizeTurnControlArgs(name, args) {
6470
- if (name === "ask") return str3(args.headline) ?? str3(args.question) ?? "";
6471
- if (name === "wake_me") return str3(args.note) ?? "";
6472
- if (name === "summon_agent") return str3(args.agentId) ?? "";
6473
- if (name === "sub_agent") return str3(args.title) ?? truncateHead(str3(args.prompt) ?? "", 60);
6474
- if (name === "skip_turn") return str3(args.reason) ?? "";
6475
- return "";
6476
- }
6477
- async function executeNativeTurnControl(name, args, tc) {
6478
- switch (name) {
6479
- case "summon_agent": {
6480
- const agentId = str3(args.agentId);
6481
- if (!agentId) return err("summon_agent: `agentId` is required.");
6482
- tc.summon(agentId);
6483
- return ok({ summoned: agentId });
6484
- }
6485
- case "skip_turn": {
6486
- const reason = str3(args.reason);
6487
- if (!reason) return err("skip_turn: `reason` is required.");
6488
- tc.skip(reason);
6489
- return ok({ skipped: true });
6490
- }
6491
- case "ask": {
6492
- const targetUserId = str3(args.targetUserId);
6493
- if (!targetUserId) return err("ask: `targetUserId` is required.");
6494
- const hasSingle = args.question !== void 0;
6495
- const hasArray = Array.isArray(args.questions) && args.questions.length > 0;
6496
- if (hasSingle && hasArray)
6497
- return err("ask: provide either `question` or `questions`, not both.");
6498
- if (!hasSingle && !hasArray) return err("ask: provide `question` or `questions`.");
6499
- const payload = hasArray ? { targetUserId, questions: args.questions } : {
6500
- targetUserId,
6501
- question: str3(args.question),
6502
- ...str3(args.headline) ? { headline: str3(args.headline) } : {},
6503
- ...Array.isArray(args.options) ? { options: args.options } : {}
6504
- };
6505
- tc.ask(payload);
6506
- return ok({ asked: targetUserId });
6507
- }
6508
- case "wake_me": {
6509
- const note = str3(args.note);
6510
- if (!note) return err("wake_me: `note` is required.");
6511
- const hasAfter = typeof args.afterSeconds === "number";
6512
- const hasAt = typeof args.at === "string";
6513
- if (hasAfter && hasAt)
6514
- return err("wake_me: provide either `afterSeconds` or `at`, not both.");
6515
- if (!hasAfter && !hasAt) return err("wake_me: provide `afterSeconds` or `at`.");
6516
- tc.wake({
6517
- ...hasAfter ? { afterSeconds: args.afterSeconds } : {},
6518
- ...hasAt ? { at: args.at } : {},
6519
- note
6520
- });
6521
- return ok({ armed: hasAt ? { at: args.at } : { afterSeconds: args.afterSeconds } });
6522
- }
6523
- case "sub_agent": {
6524
- const prompt = str3(args.prompt);
6525
- if (!prompt) return err("sub_agent: `prompt` is required.");
6526
- const result = await tc.subAgent({
6527
- prompt,
6528
- ...str3(args.agentId) ? { agentId: str3(args.agentId) } : {},
6529
- ...str3(args.title) ? { title: str3(args.title) } : {}
6530
- });
6531
- if (!result.ok) return err(result.error);
6532
- return ok({
6533
- conversationId: result.conversationId,
6534
- note: "Spawned. Don't wait \u2014 finish what else this turn can do, then end your turn; the result posts back here."
6535
- });
6536
- }
6537
- default:
6538
- return err(`unknown turn-control tool "${name}"`);
6539
- }
6540
- }
6541
- function ok(payload) {
6542
- return { ok: true, result: JSON.stringify(payload) };
6543
- }
6544
- function err(message) {
6545
- return { ok: false, result: `error: ${message}` };
6546
- }
6547
- function str3(v) {
6548
- return typeof v === "string" ? v : void 0;
6549
- }
6550
- function truncateHead(s, n) {
6551
- return s.length > n ? `${s.slice(0, n)}\u2026` : s;
6552
- }
6553
-
6554
- // packages/agent-runtime/src/cabane-native/loop.ts
6555
- var DEFAULT_MAX_ITERATIONS = 12;
6556
- async function* runCabaneNativeTurn(req, signal, deps) {
6557
- if (!req.config.model) {
6558
- yield { type: "result", ok: false, reason: "no_model" };
6559
- return;
6560
- }
6561
- const model = parseCabaneNativeModel(req.config.model);
6562
- const surface = cabaneNativeSurface(req.config.runtimeOptions);
6563
- const turnControl = req.extra.turnControl;
6564
- const tools = surface === "code" ? [
6565
- CABANE_NATIVE_CODE_TOOL,
6566
- CABANE_NATIVE_RENDER_TOKEN_TOOL,
6567
- ...turnControl ? CABANE_NATIVE_TURN_CONTROL_TOOLS : []
6568
- ] : CABANE_NATIVE_TOOLS;
6569
- const toolCtx = {
6570
- apiRoot: deps.apiRoot,
6571
- workspaceId: deps.workspaceId,
6572
- bearer: deps.bearer,
6573
- // CT666: the origin conversation, forwarded by the `sdk` tool to the `/sdk`
6574
- // endpoint so a code-mode program's returning callout binds to this turn.
6575
- ...deps.conversationId ? { conversationId: deps.conversationId } : {},
6576
- ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
6577
- };
6578
- const messages = await assembleMessages(req.systemPrompt, req.content, req.prompt, {
6579
- apiRoot: deps.apiRoot,
6580
- workspaceId: deps.workspaceId,
6581
- bearer: deps.bearer,
6582
- conversationId: deps.conversationId,
6583
- ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {},
6584
- ...deps.historyLimit !== void 0 ? { historyLimit: deps.historyLimit } : {}
6585
- });
6586
- if (signal.aborted) return;
6587
- const maxIterations = deps.maxIterations ?? DEFAULT_MAX_ITERATIONS;
6588
- let usage;
6589
- let resolvedModel = model;
6590
- for (let iteration = 0; iteration < maxIterations; iteration++) {
6591
- let text = "";
6592
- const toolAcc = /* @__PURE__ */ new Map();
6593
- let finishReason;
6594
- let errored2;
6595
- for await (const ev of deps.provider.stream({ model, messages, tools }, signal)) {
6596
- if (signal.aborted) return;
6597
- switch (ev.type) {
6598
- case "text":
6599
- text += ev.delta;
6600
- break;
6601
- case "tool_call": {
6602
- const cur = toolAcc.get(ev.index) ?? { id: `call_${ev.index}`, name: "", args: "" };
6603
- if (ev.id) cur.id = ev.id;
6604
- if (ev.name) cur.name = ev.name;
6605
- if (ev.argumentsDelta) cur.args += ev.argumentsDelta;
6606
- toolAcc.set(ev.index, cur);
6607
- break;
6608
- }
6609
- case "usage":
6610
- usage = {
6611
- inputTokens: ev.inputTokens,
6612
- outputTokens: ev.outputTokens,
6613
- contextTokens: ev.inputTokens
6614
- };
6615
- break;
6616
- case "model":
6617
- resolvedModel = ev.model;
6618
- break;
6619
- case "error":
6620
- errored2 = ev.message;
6621
- break;
6622
- case "done":
6623
- finishReason = ev.finishReason;
6624
- break;
6625
- }
6626
- }
6627
- if (signal.aborted) return;
6628
- if (errored2 !== void 0) {
6629
- const sealed = sealText(text, false);
6630
- if (sealed) yield sealed;
6631
- const failure = classifyErrorText(errored2);
6632
- yield {
6633
- type: "result",
6634
- ok: false,
6635
- reason: failure ? encodeFailureReason(failure) : `error:${errored2.slice(0, 200)}`,
6636
- ...usage ? { usage } : {},
6637
- ...resolvedModel ? { resolvedModel } : {}
6638
- };
6639
- return;
6640
- }
6641
- const toolCalls = [...toolAcc.entries()].sort((a, b) => a[0] - b[0]).map(([, v]) => v);
6642
- if (toolCalls.length === 0) {
6643
- const sealed = sealText(text, true);
6644
- if (sealed) yield sealed;
6645
- yield {
6646
- type: "result",
6647
- ok: true,
6648
- ...usage ? { usage } : {},
6649
- ...resolvedModel ? { resolvedModel } : {}
6650
- };
6651
- return;
6652
- }
6653
- const sealedInterim = sealText(text, false);
6654
- if (sealedInterim) yield sealedInterim;
6655
- const assistantToolCalls = toolCalls.map((t) => ({
6656
- id: t.id,
6657
- type: "function",
6658
- function: { name: t.name, arguments: t.args || "{}" }
6659
- }));
6660
- messages.push({ role: "assistant", content: text, tool_calls: assistantToolCalls });
6661
- for (const t of toolCalls) {
6662
- if (signal.aborted) return;
6663
- const args = parseArgs(t.args);
6664
- const displayName = prettyToolName(t.name);
6665
- const isTurnControl = CABANE_NATIVE_TURN_CONTROL_NAMES.has(t.name);
6666
- const summary = isTurnControl ? summarizeTurnControlArgs(t.name, args) : summarizeCabaneToolArgs(t.name, args);
6667
- yield { type: "tool", id: t.id, name: displayName, phase: "start", summary, input: args };
6668
- const result = isTurnControl && turnControl ? await executeNativeTurnControl(t.name, args, turnControl) : await executeCabaneNativeTool(t.name, args, toolCtx, signal);
6669
- if (signal.aborted) return;
6670
- yield {
6671
- type: "tool",
6672
- id: t.id,
6673
- name: displayName,
6674
- phase: result.ok ? "done" : "error",
6675
- summary,
6676
- input: args,
6677
- result: result.result
6678
- };
6679
- messages.push({ role: "tool", tool_call_id: t.id, content: result.result });
6680
- }
6681
- }
6682
- deps.onWarn?.("cabane-native: turn hit the tool-iteration cap; force-settling", {
6683
- maxIterations
6684
- });
6685
- yield {
6686
- type: "text",
6687
- body: `(Stopped after ${maxIterations} tool steps without a final answer.)`,
6688
- terminal: true
6689
- };
6690
- yield {
6691
- type: "result",
6692
- ok: true,
6693
- ...usage ? { usage } : {},
6694
- ...resolvedModel ? { resolvedModel } : {}
6695
- };
6696
- }
6697
- function sealText(text, terminal) {
6698
- const body = text.trim();
6699
- if (body.length === 0) return null;
6700
- return { type: "text", body, terminal };
6701
- }
6702
- function parseArgs(raw) {
6703
- if (!raw.trim()) return {};
6704
- try {
6705
- const parsed = JSON.parse(raw);
6706
- return parsed && typeof parsed === "object" ? parsed : {};
6707
- } catch {
6708
- return {};
6709
- }
6710
- }
6711
-
6712
- // packages/agent-runtime/src/cabane-native/provider.ts
6713
- var DEFAULT_OPENROUTER_BASE = "https://openrouter.ai/api/v1";
6714
- function createOpenRouterProvider(opts) {
6715
- const base = (opts.baseUrl ?? DEFAULT_OPENROUTER_BASE).replace(/\/$/, "");
6716
- const doFetch = opts.fetchImpl ?? fetch;
6717
- return {
6718
- async *stream(req, signal) {
6719
- let res;
6720
- try {
6721
- res = await doFetch(`${base}/chat/completions`, {
6722
- method: "POST",
6723
- headers: {
6724
- Authorization: `Bearer ${opts.apiKey}`,
6725
- "Content-Type": "application/json",
6726
- // OpenRouter attribution headers (optional, but polite + used for
6727
- // routing/analytics on their side).
6728
- "HTTP-Referer": "https://cabane.ai",
6729
- "X-Title": "Cabane"
6730
- },
6731
- body: JSON.stringify({
6732
- model: req.model,
6733
- messages: req.messages,
6734
- ...req.tools.length > 0 ? { tools: req.tools } : {},
6735
- stream: true,
6736
- // Ask OpenRouter to append a trailing usage chunk to the stream.
6737
- stream_options: { include_usage: true }
6738
- }),
6739
- signal
6740
- });
6741
- } catch (err2) {
6742
- if (signal.aborted) return;
6743
- yield { type: "error", message: `request failed: ${errText(err2)}` };
6744
- return;
6745
- }
6746
- if (!res.ok || !res.body) {
6747
- const bodyText = await res.text().catch(() => "");
6748
- yield { type: "error", message: providerErrorMessage(res.status, bodyText) };
6749
- return;
6750
- }
6751
- const decoder = new TextDecoder();
6752
- const reader = res.body.getReader();
6753
- let buffer = "";
6754
- let finishReason;
6755
- let modelEmitted = false;
6756
- try {
6757
- for (; ; ) {
6758
- if (signal.aborted) return;
6759
- const { value, done } = await reader.read();
6760
- if (done) break;
6761
- buffer += decoder.decode(value, { stream: true });
6762
- let nl;
6763
- while ((nl = buffer.indexOf("\n")) !== -1) {
6764
- const line = buffer.slice(0, nl).trim();
6765
- buffer = buffer.slice(nl + 1);
6766
- if (!line || line.startsWith(":")) continue;
6767
- if (!line.startsWith("data:")) continue;
6768
- const data = line.slice("data:".length).trim();
6769
- if (data === "[DONE]") {
6770
- yield { type: "done", ...finishReason ? { finishReason } : {} };
6771
- return;
6772
- }
6773
- let chunk;
6774
- try {
6775
- chunk = JSON.parse(data);
6776
- } catch {
6777
- continue;
6778
- }
6779
- if (chunk.error) {
6780
- yield { type: "error", message: chunk.error.message ?? "provider error" };
6781
- return;
6782
- }
6783
- if (!modelEmitted && chunk.model) {
6784
- modelEmitted = true;
6785
- yield { type: "model", model: chunk.model };
6786
- }
6787
- const choice = chunk.choices?.[0];
6788
- if (choice) {
6789
- const delta = choice.delta;
6790
- if (delta?.content) yield { type: "text", delta: delta.content };
6791
- if (delta?.tool_calls) {
6792
- for (const tc of delta.tool_calls) {
6793
- yield {
6794
- type: "tool_call",
6795
- index: tc.index,
6796
- ...tc.id ? { id: tc.id } : {},
6797
- ...tc.function?.name ? { name: tc.function.name } : {},
6798
- ...tc.function?.arguments !== void 0 ? { argumentsDelta: tc.function.arguments } : {}
6799
- };
6800
- }
6801
- }
6802
- if (choice.finish_reason) finishReason = choice.finish_reason;
6803
- }
6804
- if (chunk.usage) {
6805
- yield {
6806
- type: "usage",
6807
- inputTokens: chunk.usage.prompt_tokens ?? 0,
6808
- outputTokens: chunk.usage.completion_tokens ?? 0
6809
- };
6810
- }
6811
- }
6812
- }
6813
- } catch (err2) {
6814
- if (signal.aborted) return;
6815
- yield { type: "error", message: `stream read failed: ${errText(err2)}` };
6816
- return;
6817
- }
6818
- yield { type: "done", ...finishReason ? { finishReason } : {} };
6819
- }
6820
- };
6821
- }
6822
- function providerErrorMessage(status, body) {
6823
- let detail = body.slice(0, 300);
6824
- try {
6825
- const parsed = JSON.parse(body);
6826
- if (parsed.error?.message) detail = parsed.error.message;
6827
- } catch {
6828
- }
6829
- return `HTTP ${status}: ${detail}`;
6830
- }
6831
- function errText(err2) {
6832
- return err2 instanceof Error ? err2.message : String(err2);
6833
- }
6834
-
6835
- // packages/agent-runtime/src/cabane-native/index.ts
6836
- var CABANE_NATIVE_RUNTIME_NAME = "cabane-native";
6837
- function createCabaneNativeAdapter(deps = {}) {
6838
- const provider = deps.provider ?? (deps.apiKey ? createOpenRouterProvider({
6839
- apiKey: deps.apiKey,
6840
- ...deps.baseUrl ? { baseUrl: deps.baseUrl } : {},
6841
- ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
6842
- }) : null);
6843
- return {
6844
- name: CABANE_NATIVE_RUNTIME_NAME,
6845
- // CT666: the native runtime's addendum is now SURFACE-aware. Code mode (the
6846
- // default In-Cabane surface) mounts the single `sdk` tool, so it's taught the
6847
- // code-mode addendum (act by writing a `cabane` SDK program; the `cabane_*`
6848
- // names don't exist here); classic (the retained CT663 10-tool fallback) is
6849
- // taught the granular `cabane_*` listing. The caller (turn-context) passes
6850
- // `codeMode` off the SAME lifted surface value the loop mounts tools from, so
6851
- // the prompt and the mounted surface can't drift. (Supersedes CT614's
6852
- // flag-independent addendum: native no longer ignores this arg.)
6853
- promptAddendum: (codeMode) => codeMode ? CABANE_NATIVE_ADDENDUM_CODE_MODE : CABANE_NATIVE_ADDENDUM,
6854
- dialectSchema: cabaneNativeDialectSchema,
6855
- async *runTurn(req, signal) {
6856
- if (!provider) {
6857
- yield {
6858
- type: "result",
6859
- ok: false,
6860
- reason: "cabane_native_unavailable:no OPENROUTER_API_KEY configured on this device"
6861
- };
6862
- return;
6863
- }
6864
- const workspaceId = req.cabane.workspaceId;
6865
- if (!workspaceId) {
6866
- yield {
6867
- type: "result",
6868
- ok: false,
6869
- reason: "cabane_native_unavailable:turn carried no workspaceId"
6870
- };
6871
- return;
6872
- }
6873
- const apiRoot = req.cabane.mcpUrl.replace(/\/mcp\/?$/, "");
6874
- yield* runCabaneNativeTurn(req, signal, {
6875
- provider,
6876
- apiRoot,
6877
- workspaceId,
6878
- bearer: req.cabane.bearer,
6879
- conversationId: req.cabane.activeConversationId,
6880
- ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {},
6881
- ...deps.onWarn ? { onWarn: deps.onWarn } : {},
6882
- ...deps.maxIterations !== void 0 ? { maxIterations: deps.maxIterations } : {}
6883
- });
6884
- }
6885
- };
6886
- }
6887
- var cabaneNativeAdapter = createCabaneNativeAdapter();
6888
-
6889
5821
  // packages/agent-runtime/src/claude-code/sdk.ts
6890
5822
  import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
6891
5823
 
@@ -6943,7 +5875,7 @@ import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync
6943
5875
  import { join as join12 } from "path";
6944
5876
 
6945
5877
  // src/summon.ts
6946
- import { z as z14 } from "zod";
5878
+ import { z as z12 } from "zod";
6947
5879
  var COMPANION_LOCAL_MCP_SERVER = "cabane_companion";
6948
5880
  var SUMMON_AGENT_TOOL = "summon_agent";
6949
5881
  var SUMMON_AGENT_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${SUMMON_AGENT_TOOL}`;
@@ -6977,7 +5909,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
6977
5909
  SUMMON_AGENT_TOOL,
6978
5910
  "Summon another agent into THIS conversation \u2014 dispatch a peer to reply here on your turn. Use it to hand part of the work to a teammate, or pull in an expert, without leaving the conversation. Pass the peer's `agentId` (discover handles + ids via `list_agents`). The peer is dispatched on your turn's final reply, so write the context/ask into that reply first \u2014 it receives your message + this conversation to work from. Writing `@handle` in your prose does NOT summon anyone (agent prose never dispatches); this tool is the only in-thread lever. Single target \u2014 the last call wins. Summoning yourself is a no-op. Reach for it when the human wants the peer's answer right HERE, in front of them \u2014 the reply lands in this thread, so there's no return to wire (a return is for work YOU consume, never a courtesy notification). A handoff to a DIFFERENT conversation is `create_conversation` / `post_message` with their `dispatch` field instead.",
6979
5911
  {
6980
- agentId: z14.string().uuid().describe("The peer agent to summon \u2014 a workspace agent id from `list_agents`.")
5912
+ agentId: z12.string().uuid().describe("The peer agent to summon \u2014 a workspace agent id from `list_agents`.")
6981
5913
  },
6982
5914
  async (args) => {
6983
5915
  summonState.agentId = args.agentId;
@@ -6992,7 +5924,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
6992
5924
  SKIP_TURN_TOOL,
6993
5925
  `End your current turn WITHOUT posting a reply. Call this when you've been dispatched but the message genuinely doesn't need a response from you \u2014 a thanks/aside, a question already answered, chatter outside your lane, or a pile-on where someone else has it. Your turn ends silently: no message bubble is posted. The \`reason\` is a short free-text note for telemetry (e.g. "already answered by cabane", "thanks, nothing to add"). Prefer this over posting a low-value "ok!"/"got it" reply. Don't also write a reply when you skip \u2014 skipping IS the whole turn.`,
6994
5926
  {
6995
- reason: z14.string().min(1).max(500).describe("Short reason you are declining \u2014 used for telemetry/debugging.")
5927
+ reason: z12.string().min(1).max(500).describe("Short reason you are declining \u2014 used for telemetry/debugging.")
6996
5928
  },
6997
5929
  async (args) => {
6998
5930
  skipState.skipped = true;
@@ -7009,21 +5941,21 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
7009
5941
  ASK_TOOL,
7010
5942
  "Ask a HUMAN a structured question (or a short LIST of them) you need answered to continue, then END your turn \u2014 don't wait for the reply. Use it when you genuinely can't proceed without a person's input (a decision only they can make, a missing fact, a go/no-go). Pass `targetUserId` (a workspace member's user id \u2014 get it from `mcp__cabane__list_members`). Two forms: a SINGLE question \u2014 a `headline` (the actual question as one clear, capitalized sentence ending in `?`, \"Do we go to prod?\") plus a short `question` body for the framing the headline can't hold \u2014 OR, when a plan ends with SEVERAL bounded decisions at once (\"three calls before I build: A? B? C?\"), a `questions` array of 1\u20135 items, each `{ headline, body?, options? }`. **Prefer the list over cramming the extra decisions into prose or dropping them** \u2014 end the turn with one ask carrying every question, never pick one and bury the rest. Each question keeps the same form rules: a one-sentence `headline`, a short `body` frame (NOT a report \u2014 your status, links, and detail go in your REPLY, and the body renders inline markdown only: links/emphasis/inline code, no bulleted lists or headings), and 2\u20134 `options` when the answer is a bounded choice \u2014 for a yes/no go-ahead always pass them, so it's one click, not a typed reply. An option can be a short button label OR a sentence that carries its own context; short/binary sets render as inline buttons, long ones stack full-width. Provide EITHER `question` (single) or `questions` (array), never both. The ask is recorded as a first-class attention item aimed at that person; your final reply carries the surrounding CONTEXT (what you found, why you're stuck), the ask carries the QUESTION(S). An open ask marks you as blocked until EVERY question is answered, so raise one only when you truly can't proceed \u2014 never ceremonially. One ask per turn (last call wins). After asking, stop \u2014 when the person replies addressed to you, the ask resolves and you resume; other people's or agents' messages may wake you but leave it open. Targets a human only; to hand work to another AGENT use summon/dispatch instead.",
7011
5943
  {
7012
- targetUserId: z14.string().uuid().describe("The workspace member (human) to ask \u2014 a user id from `list_members`."),
7013
- question: z14.string().min(1).max(400).optional().describe(
5944
+ targetUserId: z12.string().uuid().describe("The workspace member (human) to ask \u2014 a user id from `list_members`."),
5945
+ question: z12.string().min(1).max(400).optional().describe(
7014
5946
  "SINGLE-question form: a short body \u2014 one or two sentences of framing the headline can't hold. NOT a report (capped, inline markdown only). Provide EITHER this or `questions`, not both. Put the crisp one-sentence question in `headline`."
7015
5947
  ),
7016
- headline: z14.string().min(1).max(120).optional().describe(
5948
+ headline: z12.string().min(1).max(120).optional().describe(
7017
5949
  'SINGLE-question form: the question itself as ONE clear, capitalized sentence ending in `?` ("Do we go to prod?"). What the human reads first in the inbox and the chip \u2014 one scannable question, no elaboration (that goes in `question`). Strongly encouraged.'
7018
5950
  ),
7019
- options: z14.array(z14.string().min(1).max(200)).min(2).max(4).optional().describe("SINGLE-question form: optional 2\u20134 suggested one-click answers."),
7020
- questions: z14.array(
7021
- z14.object({
7022
- headline: z14.string().min(1).max(120).describe(
5951
+ options: z12.array(z12.string().min(1).max(200)).min(2).max(4).optional().describe("SINGLE-question form: optional 2\u20134 suggested one-click answers."),
5952
+ questions: z12.array(
5953
+ z12.object({
5954
+ headline: z12.string().min(1).max(120).describe(
7023
5955
  'The one-sentence question ("Do we go to prod?") \u2014 required for each item.'
7024
5956
  ),
7025
- body: z14.string().min(1).max(400).optional().describe("Optional short framing beneath the headline. NOT a report."),
7026
- options: z14.array(z14.string().min(1).max(200)).min(2).max(4).optional().describe("Optional 2\u20134 one-click answers for this question.")
5957
+ body: z12.string().min(1).max(400).optional().describe("Optional short framing beneath the headline. NOT a report."),
5958
+ options: z12.array(z12.string().min(1).max(200)).min(2).max(4).optional().describe("Optional 2\u20134 one-click answers for this question.")
7027
5959
  })
7028
5960
  ).min(1).max(5).optional().describe(
7029
5961
  "MULTI-question form: 1\u20135 questions to ask at once, when a plan ends with several bounded decisions. Provide EITHER this or `question`/`headline`/`options`, not both."
@@ -7080,13 +6012,13 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
7080
6012
  SUB_AGENT_TOOL,
7081
6013
  "Spawn a sub-agent \u2014 hand a piece of work to a private worker with a fresh context window, whose result comes back to you automatically. Modeled on the Task tool, with ONE deliberate difference: it does NOT return the result inline. A callee's turn can run for minutes and no turn may hold an unbounded wait, so the shape is spawn-now, results-on-wake \u2014 this returns immediately with the child's `conversationId`, and the outcome lands LATER as a message in THIS conversation; you're woken once every sub-agent you have out in this conversation has returned. So DON'T wait for it: after spawning, finish whatever else this turn can do and end your turn (never poll the child with reads in a loop \u2014 the wake is automatic). Parallel fan-out = call this N times in one turn (they run concurrently; ONE wake when all are in); series = one call per turn. `prompt` is the child's opening instruction \u2014 make it self-contained (the sub-agent starts fresh, with only this prompt + the thread it lands in). `agentId` (optional) dispatches a PEER instead of yourself \u2014 same mechanics, a different mind (use for capability/context you lack); default (self) is the pure sub-worker with a clean context window. `title` (optional) names the child thread (results link it, so a legible title helps). A single sub-agent has no wall-clock advantage (you idle either way) \u2014 it pays when the callee has capability/context you lack, or to isolate a big read from your own session; the real win is fan-out. Don't spawn one for a lookup you can do in-turn with your own tools. The result returns to YOU to act on \u2014 reach for it when you're the consumer of the output, not as a way to notify a human: if a person just wants to read the result, dispatch a plain (no-return) conversation and link it instead of spawning a sub-agent.",
7082
6014
  {
7083
- prompt: z14.string().min(1).max(65536).describe(
6015
+ prompt: z12.string().min(1).max(65536).describe(
7084
6016
  "The sub-agent's opening instruction \u2014 self-contained (it starts with a fresh context window; only this prompt + the thread it lands in)."
7085
6017
  ),
7086
- agentId: z14.string().uuid().optional().describe(
6018
+ agentId: z12.string().uuid().optional().describe(
7087
6019
  "Optional peer to run the sub-agent as (a workspace agent id from `list_agents`); omit to spawn yourself with a fresh context window."
7088
6020
  ),
7089
- title: z14.string().max(200).optional().describe("Optional title for the child thread (result chips link it).")
6021
+ title: z12.string().max(200).optional().describe("Optional title for the child thread (result chips link it).")
7090
6022
  },
7091
6023
  async (args) => {
7092
6024
  const result = await subAgentCreate(args);
@@ -7116,13 +6048,13 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
7116
6048
  WAKE_ME_TOOL,
7117
6049
  'Wake yourself later \u2014 end this turn now and be re-dispatched at a time you pick, with a note you write to yourself. Use it for "wait until X": when the thing you need hasn\'t happened yet (a conversation isn\'t done, a PR isn\'t merged, a human hasn\'t answered), arm a wake, end your turn, and you\'re woken later to CHECK \u2014 read the workspace, and either act or re-arm. This is the loop behind "check back in five minutes", "keep checking until {condition}", and a scheduled re-try ("re-send once that agent\'s limit resets"). Ground the delay before you arm it. Almost every wake is short \u2014 seconds to a couple of hours \u2014 waiting on a condition you can name: a session limit resetting, a PR merging, a deploy landing, a throttled dispatch to re-send. Reach past a few hours only when (a) a human asked for that timing, or (b) the wait is pinned to a real external event you can name \u2014 a report that only runs Mondays, a scheduled appointment, a known reset time. A speculative far-future check-in you invented yourself \u2014 "in two weeks I\'ll see whether this feature is used" \u2014 is the one thing not to arm: if no one asked and you can\'t name both what clears the wait and why it takes that long, don\'t arm it \u2014 finish now, or raise an `ask`. Pass EXACTLY ONE of `afterSeconds` (a relative delay \u2014 `300` for five minutes) or `at` (an absolute ISO-8601 timestamp WITH a zone, e.g. `2026-07-16T09:00:00-07:00` \u2014 YOU compute it from a phrase like "tomorrow morning"; the system never parses natural-language time). `note` is a message to your future self \u2014 it becomes the body of the wake message that re-dispatches you, so write the condition to re-check ("check whether CT441 merged yet"). The wake is armed when your turn SETTLES, not now, so the delay counts from the turn ending; one wake per turn (last call wins). This is the sanctioned way to schedule your own continuation \u2014 the ONLY one; never reach for a host cron/scheduler. Guardrails: at least 60s out, at most 14 days; widen the interval as a loop ages (5m \u2192 15m \u2192 1h\u2026) rather than hammering; after many consecutive re-arms with no other activity you\'ll be steered to raise an `ask` to the human instead. If a wake can\'t be armed you\'re re-dispatched with a note explaining why \u2014 never a silent drop.',
7118
6050
  {
7119
- afterSeconds: z14.number().int().positive().optional().describe(
6051
+ afterSeconds: z12.number().int().positive().optional().describe(
7120
6052
  "Relative delay in seconds from when this turn ends (e.g. 300 = five minutes). Provide EITHER this or `at`, not both. Floor 60s, horizon 14 days \u2014 enforced server-side."
7121
6053
  ),
7122
- at: z14.string().datetime({ offset: true }).optional().describe(
6054
+ at: z12.string().datetime({ offset: true }).optional().describe(
7123
6055
  "Absolute ISO-8601 timestamp WITH a zone (`Z` or `\xB1HH:MM`), e.g. `2026-07-16T09:00:00-07:00`. YOU compute it from a natural-language phrase using the current datetime in your turn context. Provide EITHER this or `afterSeconds`, not both."
7124
6056
  ),
7125
- note: z14.string().min(1).max(2e3).describe(
6057
+ note: z12.string().min(1).max(2e3).describe(
7126
6058
  'A note to your future self \u2014 becomes the body of the wake message that re-dispatches you. Write the condition to re-check ("check whether the PR merged").'
7127
6059
  )
7128
6060
  },
@@ -7202,7 +6134,6 @@ function buildCompanionTurnRequest(params) {
7202
6134
  local: {
7203
6135
  ...params.cwd ? { cwd: params.cwd } : {},
7204
6136
  ...params.env ? { env: params.env } : {},
7205
- ...params.nativeWorkAssignment ? { nativeWorkAssignment: params.nativeWorkAssignment } : {},
7206
6137
  // User MCP servers (already `${PLACEHOLDER}`-resolved). Structurally the
7207
6138
  // adapter's `ResolvedMcpServers`.
7208
6139
  ...Object.keys(params.mcpServers).length > 0 ? { mcpServers: params.mcpServers } : {},
@@ -7210,10 +6141,9 @@ function buildCompanionTurnRequest(params) {
7210
6141
  ...params.claudeCode ? { claudeCode: params.claudeCode } : {}
7211
6142
  },
7212
6143
  // Host-injected: the companion-local summon server (for the subprocess adapters,
7213
- // under its own namespace) + the cabane-native turn-control handler (CT666).
6144
+ // under its own namespace).
7214
6145
  extra: {
7215
- mcpServers: { [COMPANION_LOCAL_MCP_SERVER]: params.summonServer },
7216
- turnControl: params.turnControl
6146
+ mcpServers: { [COMPANION_LOCAL_MCP_SERVER]: params.summonServer }
7217
6147
  }
7218
6148
  };
7219
6149
  }
@@ -7230,23 +6160,18 @@ function dirFor(workspaceId) {
7230
6160
  function conversationDir(workspaceId, conversationId) {
7231
6161
  return join9(dirFor(workspaceId), encodeURIComponent(conversationId));
7232
6162
  }
7233
- function pathFor3(workspaceId, conversationId, agentId, assignmentKey) {
7234
- const suffix = assignmentKey ? `--${encodeURIComponent(assignmentKey)}` : "";
7235
- return join9(
7236
- conversationDir(workspaceId, conversationId),
7237
- `${encodeURIComponent(agentId)}${suffix}.json`
7238
- );
6163
+ function pathFor3(workspaceId, conversationId, agentId) {
6164
+ return join9(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
7239
6165
  }
7240
- function readPrepared(workspaceId, conversationId, agentId, assignmentKey) {
7241
- const path3 = pathFor3(workspaceId, conversationId, agentId, assignmentKey);
6166
+ function readPrepared(workspaceId, conversationId, agentId) {
6167
+ const path3 = pathFor3(workspaceId, conversationId, agentId);
7242
6168
  if (!existsSync7(path3)) return null;
7243
6169
  try {
7244
6170
  const parsed = JSON.parse(readFileSync6(path3, "utf8"));
7245
6171
  if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
7246
6172
  return {
7247
6173
  cwd: parsed.cwd,
7248
- ...parsed.env && typeof parsed.env === "object" ? { env: parsed.env } : {},
7249
- ...parsed.nativeWorkAssignment && typeof parsed.nativeWorkAssignment === "object" ? { nativeWorkAssignment: parsed.nativeWorkAssignment } : {}
6174
+ ...parsed.env && typeof parsed.env === "object" ? { env: parsed.env } : {}
7250
6175
  };
7251
6176
  }
7252
6177
  return null;
@@ -7254,10 +6179,10 @@ function readPrepared(workspaceId, conversationId, agentId, assignmentKey) {
7254
6179
  return null;
7255
6180
  }
7256
6181
  }
7257
- function writePrepared(workspaceId, conversationId, agentId, result, assignmentKey) {
6182
+ function writePrepared(workspaceId, conversationId, agentId, result) {
7258
6183
  mkdirSync7(conversationDir(workspaceId, conversationId), { recursive: true });
7259
6184
  writeFileSync6(
7260
- pathFor3(workspaceId, conversationId, agentId, assignmentKey),
6185
+ pathFor3(workspaceId, conversationId, agentId),
7261
6186
  JSON.stringify(result) + "\n",
7262
6187
  "utf8"
7263
6188
  );
@@ -7266,30 +6191,30 @@ function writePrepared(workspaceId, conversationId, agentId, result, assignmentK
7266
6191
  // src/secrets.ts
7267
6192
  import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
7268
6193
  import { join as join10 } from "path";
7269
- import { z as z15 } from "zod";
6194
+ import { z as z13 } from "zod";
7270
6195
  var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
7271
6196
  function secretsPath() {
7272
6197
  return join10(cabaneDir(), "secrets.json");
7273
6198
  }
7274
- var secretStoreSchema = z15.record(z15.string(), z15.string());
6199
+ var secretStoreSchema = z13.record(z13.string(), z13.string());
7275
6200
  function loadSecretStore() {
7276
6201
  const path3 = secretsPath();
7277
6202
  if (!existsSync8(path3)) return makeStore({});
7278
6203
  let raw;
7279
6204
  try {
7280
6205
  raw = readFileSync7(path3, "utf8");
7281
- } catch (err2) {
6206
+ } catch (err) {
7282
6207
  throw new ConfigError(
7283
- `couldn't read ${path3}: ${err2 instanceof Error ? err2.message : String(err2)}`
6208
+ `couldn't read ${path3}: ${err instanceof Error ? err.message : String(err)}`
7284
6209
  );
7285
6210
  }
7286
6211
  if (raw.trim().length === 0) return makeStore({});
7287
6212
  let parsed;
7288
6213
  try {
7289
6214
  parsed = JSON.parse(raw);
7290
- } catch (err2) {
6215
+ } catch (err) {
7291
6216
  throw new ConfigError(
7292
- `${path3} is not valid JSON: ${err2 instanceof Error ? err2.message : String(err2)}. It must be a flat object of "NAME": "value" secret pairs.`
6217
+ `${path3} is not valid JSON: ${err instanceof Error ? err.message : String(err)}. It must be a flat object of "NAME": "value" secret pairs.`
7293
6218
  );
7294
6219
  }
7295
6220
  const result = secretStoreSchema.safeParse(parsed);
@@ -7303,8 +6228,8 @@ function loadSecretStore() {
7303
6228
  function loadSecretStoreTolerant(onWarn) {
7304
6229
  try {
7305
6230
  return loadSecretStore();
7306
- } catch (err2) {
7307
- onWarn?.(err2 instanceof Error ? err2.message : String(err2));
6231
+ } catch (err) {
6232
+ onWarn?.(err instanceof Error ? err.message : String(err));
7308
6233
  return makeStore({});
7309
6234
  }
7310
6235
  }
@@ -7375,8 +6300,8 @@ var TranscriptWriter = class {
7375
6300
  } catch {
7376
6301
  }
7377
6302
  pruneOld(dir2, RETAIN);
7378
- } catch (err2) {
7379
- this.fail(err2);
6303
+ } catch (err) {
6304
+ this.fail(err);
7380
6305
  }
7381
6306
  this.line({ type: "_meta", ...meta });
7382
6307
  }
@@ -7392,14 +6317,14 @@ var TranscriptWriter = class {
7392
6317
  if (this.broken) return;
7393
6318
  try {
7394
6319
  appendFileSync(this.path, JSON.stringify(obj) + "\n", { mode: 384 });
7395
- } catch (err2) {
7396
- this.fail(err2);
6320
+ } catch (err) {
6321
+ this.fail(err);
7397
6322
  }
7398
6323
  }
7399
- fail(err2) {
6324
+ fail(err) {
7400
6325
  if (this.broken) return;
7401
6326
  this.broken = true;
7402
- this.onWarn?.(`transcript write failed (${err2 instanceof Error ? err2.message : String(err2)})`);
6327
+ this.onWarn?.(`transcript write failed (${err instanceof Error ? err.message : String(err)})`);
7403
6328
  }
7404
6329
  };
7405
6330
  function fileName(meta) {
@@ -7437,9 +6362,9 @@ var EMPTY_FINAL_BODY = "Done \u2014 see the changes above.";
7437
6362
  var TurnCommitter = class {
7438
6363
  constructor(deps) {
7439
6364
  this.deps = deps;
7440
- this.onError = (err2, hook) => {
6365
+ this.onError = (err, hook) => {
7441
6366
  deps.log.warn(
7442
- { err: err2 instanceof Error ? err2.message : String(err2), hook },
6367
+ { err: err instanceof Error ? err.message : String(err), hook },
7443
6368
  "dispatcher: transcript callback failed"
7444
6369
  );
7445
6370
  };
@@ -7504,9 +6429,9 @@ var TurnCommitter = class {
7504
6429
  signal: deps.signal,
7505
6430
  nextSeq: deps.nextSeq,
7506
6431
  emptyFinalBody: EMPTY_FINAL_BODY,
7507
- onError: (err2) => {
6432
+ onError: (err) => {
7508
6433
  deps.log.warn(
7509
- { err: err2 instanceof Error ? err2.message : String(err2) },
6434
+ { err: err instanceof Error ? err.message : String(err) },
7510
6435
  "dispatcher: empty-final commit failed"
7511
6436
  );
7512
6437
  }
@@ -7529,8 +6454,8 @@ var TurnCommitter = class {
7529
6454
  if (event.type === "session" || event.type === "result") return;
7530
6455
  try {
7531
6456
  await this.emit(event);
7532
- } catch (err2) {
7533
- this.onError(err2, event.type);
6457
+ } catch (err) {
6458
+ this.onError(err, event.type);
7534
6459
  }
7535
6460
  }
7536
6461
  // End-of-turn empty-final promotion. The held-text flush is now the adapter's
@@ -7726,10 +6651,27 @@ var UNEXPECTED_ROLE_REASON = `refused the wake trigger (role "system" isn't disp
7726
6651
  var SKIPPED_MARKER_BODY = "(skipped)";
7727
6652
  var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
7728
6653
  var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
7729
- var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 45 * 6e4;
6654
+ var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
7730
6655
  function runKey(conversationId, agentId) {
7731
6656
  return `${conversationId}|${agentId}`;
7732
6657
  }
6658
+ function describeSubAgentError(status, body) {
6659
+ const code = body && typeof body === "object" && "error" in body ? String(body.error) : void 0;
6660
+ switch (code) {
6661
+ case "callout_cap_exceeded":
6662
+ return "sub_agent: you already have the maximum open sub-agents for this thread. Wait for some to return \u2014 you're woken once they're all back \u2014 before spawning more.";
6663
+ case "callout_depth_exceeded":
6664
+ return "sub_agent: this would nest sub-agents too deep (max 3 levels). Have the current worker report back rather than spawning another layer.";
6665
+ case "dispatch_agent_not_found":
6666
+ return "sub_agent: no live agent in this workspace matches that `agentId`. Check `list_agents`, or omit `agentId` to spawn yourself.";
6667
+ case "dispatch_return_requires_turn":
6668
+ case "dispatch_return_requires_agent":
6669
+ case "dispatch_return_requires_dispatch":
6670
+ return `sub_agent: the spawn was rejected (${code}). This is a turn-context problem, not something to retry blindly \u2014 report it rather than looping.`;
6671
+ default:
6672
+ return `sub_agent: the spawn failed (${code ?? `HTTP ${status}`}).`;
6673
+ }
6674
+ }
7733
6675
  var Dispatcher = class {
7734
6676
  constructor(opts) {
7735
6677
  this.opts = opts;
@@ -7756,7 +6698,7 @@ var Dispatcher = class {
7756
6698
  // clear it — the SJ383 `finally` after the SDK loop is the one clear, and
7757
6699
  // every pre-run exit returns before reaching it. So each pre-run failure has
7758
6700
  // to clear `active_run_started_at` itself, mirroring that `finally`, or the
7759
- // indicator strands until the 90-min age sweep.
6701
+ // indicator strands until the 12h age sweep.
7760
6702
  //
7761
6703
  // `errorReason` controls the server's duplicate-notice rule (the active-run
7762
6704
  // PATCH handler in conversations.ts): a clear carrying `errorReason` makes the
@@ -7776,9 +6718,9 @@ var Dispatcher = class {
7776
6718
  payload.agentId,
7777
6719
  body
7778
6720
  );
7779
- } catch (err2) {
6721
+ } catch (err) {
7780
6722
  turnLog.warn(
7781
- { err: err2 instanceof Error ? err2.message : String(err2) },
6723
+ { err: err instanceof Error ? err.message : String(err) },
7782
6724
  "dispatcher: pre-run active-run clear failed terminally; server age-sweep is the backstop"
7783
6725
  );
7784
6726
  }
@@ -7804,16 +6746,16 @@ var Dispatcher = class {
7804
6746
  payload.messageId,
7805
6747
  turnId
7806
6748
  );
7807
- } catch (err2) {
7808
- const status = err2 instanceof ApiError ? err2.status : 0;
6749
+ } catch (err) {
6750
+ const status = err instanceof ApiError ? err.status : 0;
7809
6751
  if (status === 404) {
7810
6752
  turnLog.warn(
7811
- { err: err2 instanceof Error ? err2.message : String(err2) },
6753
+ { err: err instanceof Error ? err.message : String(err) },
7812
6754
  "dispatcher: turn-context 404 (stale event \u2014 conversation/message/participant gone); skipping"
7813
6755
  );
7814
6756
  return this.concludeBeforeRun(payload, turnLog, startedAt, "turn_context_not_found");
7815
6757
  }
7816
- const reason = err2 instanceof Error ? err2.message : String(err2);
6758
+ const reason = err instanceof Error ? err.message : String(err);
7817
6759
  turnLog.error({ err: reason }, "dispatcher: failed to fetch turn context");
7818
6760
  const fetchReason = `fetch_failed: ${reason}`;
7819
6761
  return this.concludeBeforeRun(payload, turnLog, startedAt, fetchReason, fetchReason);
@@ -7876,20 +6818,11 @@ var Dispatcher = class {
7876
6818
  effectiveCwd = void 0;
7877
6819
  }
7878
6820
  let hookEnv;
7879
- let preparedNativeAssignment;
7880
6821
  if (prepareHook) {
7881
- const assignment = turnContext.conversation.nativeWorkAssignment;
7882
- const assignmentKey = assignment ? `${assignment.executionId}:${assignment.activationEpoch}` : void 0;
7883
- const cached2 = readPrepared(
7884
- workspaceId,
7885
- payload.conversationId,
7886
- payload.agentId,
7887
- assignmentKey
7888
- );
6822
+ const cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
7889
6823
  if (cached2) {
7890
6824
  effectiveCwd = cached2.cwd;
7891
6825
  hookEnv = cached2.env;
7892
- preparedNativeAssignment = cached2.nativeWorkAssignment;
7893
6826
  } else {
7894
6827
  const delayMs = this.opts.preparingRowDelayMs ?? DEFAULT_PREPARING_ROW_DELAY_MS;
7895
6828
  let preparingStarted = false;
@@ -7904,9 +6837,9 @@ var Dispatcher = class {
7904
6837
  summary: "",
7905
6838
  phase,
7906
6839
  seq
7907
- }).catch((err2) => {
6840
+ }).catch((err) => {
7908
6841
  turnLog.warn(
7909
- { err: err2 instanceof Error ? err2.message : String(err2), phase },
6842
+ { err: err instanceof Error ? err.message : String(err), phase },
7910
6843
  "dispatcher: preparing-activity report failed (continuing with the hook)"
7911
6844
  );
7912
6845
  });
@@ -7928,25 +6861,17 @@ var Dispatcher = class {
7928
6861
  // tasker prepare hook keys its per-task env off. Defaults to `[]` for
7929
6862
  // an older API. The conversation anchor is gone (CT319).
7930
6863
  triggerEntryPaths: turnContext.conversation.triggerEntryPaths ?? [],
7931
- ...turnContext.conversation.nativeWorkAssignment ? { nativeWorkAssignment: turnContext.conversation.nativeWorkAssignment } : {},
7932
6864
  title: turnContext.conversation.title
7933
6865
  });
7934
6866
  clearTimeout(preparingTimer);
7935
6867
  if (preparingStarted) reportPreparing("done");
7936
- writePrepared(
7937
- workspaceId,
7938
- payload.conversationId,
7939
- payload.agentId,
7940
- result,
7941
- assignmentKey
7942
- );
6868
+ writePrepared(workspaceId, payload.conversationId, payload.agentId, result);
7943
6869
  effectiveCwd = result.cwd;
7944
6870
  hookEnv = result.env;
7945
- preparedNativeAssignment = result.nativeWorkAssignment;
7946
- } catch (err2) {
6871
+ } catch (err) {
7947
6872
  clearTimeout(preparingTimer);
7948
6873
  if (preparingStarted) reportPreparing("error");
7949
- const reason = err2 instanceof Error ? err2.message : String(err2);
6874
+ const reason = err instanceof Error ? err.message : String(err);
7950
6875
  turnLog.error({ err: reason }, "dispatcher: prepare hook failed");
7951
6876
  try {
7952
6877
  await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
@@ -7969,6 +6894,19 @@ ${reason}`,
7969
6894
  }
7970
6895
  }
7971
6896
  }
6897
+ let turnEnv = hookEnv;
6898
+ if (effectiveCwd && turnContext.runtime === "codex") {
6899
+ const tmpDir = join12(effectiveCwd, "node_modules", ".cache", "cabane-tmp", turnId);
6900
+ try {
6901
+ mkdirSync9(tmpDir, { recursive: true });
6902
+ turnEnv = { ...hookEnv, TMPDIR: tmpDir };
6903
+ } catch (err) {
6904
+ turnLog.warn(
6905
+ { err: err instanceof Error ? err.message : String(err), tmpDir },
6906
+ "dispatcher: failed to create per-turn TMPDIR \u2014 proceeding with the inherited temp dir"
6907
+ );
6908
+ }
6909
+ }
7972
6910
  const key = runKey(payload.conversationId, payload.agentId);
7973
6911
  const abortController = new AbortController();
7974
6912
  this.aborts.set(key, abortController);
@@ -7983,9 +6921,9 @@ ${reason}`,
7983
6921
  // this live turn for an abandoned one and close it with a `stopped`.
7984
6922
  turnId
7985
6923
  });
7986
- } catch (err2) {
6924
+ } catch (err) {
7987
6925
  turnLog.warn(
7988
- { err: err2 instanceof Error ? err2.message : String(err2) },
6926
+ { err: err instanceof Error ? err.message : String(err) },
7989
6927
  "dispatcher: active-run flag set failed terminally; proceeding"
7990
6928
  );
7991
6929
  }
@@ -8022,35 +6960,6 @@ ${reason}`,
8022
6960
  subAgentCreate,
8023
6961
  wakeState
8024
6962
  );
8025
- const nativeTurnControl = {
8026
- summon: (agentId) => {
8027
- summonState.agentId = agentId;
8028
- },
8029
- skip: (reason) => {
8030
- skipState.skipped = true;
8031
- skipState.reason = reason;
8032
- },
8033
- ask: (p) => {
8034
- askState.targetUserId = p.targetUserId;
8035
- if (p.questions && p.questions.length > 0) {
8036
- askState.questions = p.questions;
8037
- askState.question = null;
8038
- askState.headline = null;
8039
- askState.options = null;
8040
- } else {
8041
- askState.question = p.question ?? null;
8042
- askState.headline = p.headline ?? null;
8043
- askState.options = p.options ?? null;
8044
- askState.questions = null;
8045
- }
8046
- },
8047
- wake: (p) => {
8048
- wakeState.afterSeconds = p.afterSeconds ?? null;
8049
- wakeState.at = p.at ?? null;
8050
- wakeState.note = p.note;
8051
- },
8052
- subAgent: subAgentCreate
8053
- };
8054
6963
  const request = buildCompanionTurnRequest({
8055
6964
  turnContext,
8056
6965
  baseUrl: this.opts.baseUrl,
@@ -8060,11 +6969,11 @@ ${reason}`,
8060
6969
  ...turnContext.turnToken ? { turnToken: turnContext.turnToken } : {},
8061
6970
  // SJ524: the hook-resolved cwd overrides the static local cwd.
8062
6971
  ...effectiveCwd ? { cwd: effectiveCwd } : {},
8063
- ...hookEnv ? { env: hookEnv } : {},
8064
- ...preparedNativeAssignment ? { nativeWorkAssignment: preparedNativeAssignment } : {},
6972
+ // CT804: `turnEnv` = the prepare-hook env plus the per-turn checkout-local
6973
+ // TMPDIR (falls back to `hookEnv` when no cwd was resolved).
6974
+ ...turnEnv ? { env: turnEnv } : {},
8065
6975
  mcpServers: resolvedMcpServers,
8066
6976
  summonServer,
8067
- turnControl: nativeTurnControl,
8068
6977
  // CT238: this turn's conversation, forwarded as the active-conversation
8069
6978
  // header so a cross-thread post/spawn stamps its origin.
8070
6979
  activeConversationId: payload.conversationId,
@@ -8082,22 +6991,19 @@ ${reason}`,
8082
6991
  if (this.opts.codexEnabled) {
8083
6992
  adapters.push(createCodexAdapter({ enabled: true, onWarn }));
8084
6993
  }
8085
- if (this.opts.cabaneNativeApiKey) {
8086
- adapters.push(createCabaneNativeAdapter({ apiKey: this.opts.cabaneNativeApiKey, onWarn }));
8087
- }
8088
6994
  const registry = createAdapterRegistry(adapters);
8089
6995
  let adapter;
8090
6996
  try {
8091
6997
  adapter = selectAdapter(registry, turnContext.runtime);
8092
- } catch (err2) {
8093
- if (!(err2 instanceof RuntimeUnavailableError)) throw err2;
6998
+ } catch (err) {
6999
+ if (!(err instanceof RuntimeUnavailableError)) throw err;
8094
7000
  turnLog.error(
8095
- { runtime: err2.runtime, available: err2.available },
7001
+ { runtime: err.runtime, available: err.available },
8096
7002
  "dispatcher: turn runtime not available on this device"
8097
7003
  );
8098
7004
  try {
8099
7005
  await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
8100
- body: `${RUNTIME_UNAVAILABLE_PREFIX} ${err2.message}`,
7006
+ body: `${RUNTIME_UNAVAILABLE_PREFIX} ${err.message}`,
8101
7007
  kind: "final",
8102
7008
  turnId,
8103
7009
  parentMessageId: payload.messageId
@@ -8112,7 +7018,7 @@ ${reason}`,
8112
7018
  payload,
8113
7019
  turnLog,
8114
7020
  startedAt,
8115
- `runtime_unavailable:${err2.runtime}`
7021
+ `runtime_unavailable:${err.runtime}`
8116
7022
  );
8117
7023
  }
8118
7024
  if (prepareHook && hookEnv?.CABANE_TASK_ID) {
@@ -8246,9 +7152,9 @@ ${reason}`,
8246
7152
  skipState.skipped = true;
8247
7153
  skipState.reason = intent.skipReason;
8248
7154
  }
8249
- } catch (err2) {
7155
+ } catch (err) {
8250
7156
  turnLog.warn(
8251
- { err: err2 instanceof Error ? err2.message : String(err2) },
7157
+ { err: err instanceof Error ? err.message : String(err) },
8252
7158
  "dispatcher: turn-control intent fetch failed; turn-control effects for this turn are dropped"
8253
7159
  );
8254
7160
  }
@@ -8294,9 +7200,9 @@ ${reason}`,
8294
7200
  payload.agentId,
8295
7201
  { agentSessionId: event.state }
8296
7202
  );
8297
- } catch (err2) {
7203
+ } catch (err) {
8298
7204
  turnLog.warn(
8299
- { err: err2 instanceof Error ? err2.message : String(err2) },
7205
+ { err: err instanceof Error ? err.message : String(err) },
8300
7206
  "dispatcher: session-id write failed (will retry next turn)"
8301
7207
  );
8302
7208
  }
@@ -8345,18 +7251,18 @@ ${reason}`,
8345
7251
  parentMessageId: payload.messageId,
8346
7252
  ...skipWake ? { wake: skipWake } : {}
8347
7253
  });
8348
- } catch (err2) {
7254
+ } catch (err) {
8349
7255
  turnLog.warn(
8350
- { err: err2 instanceof Error ? err2.message : String(err2) },
7256
+ { err: err instanceof Error ? err.message : String(err) },
8351
7257
  "dispatcher: skipped-marker commit failed"
8352
7258
  );
8353
7259
  }
8354
7260
  } else {
8355
7261
  await committer.finalize(okResult);
8356
7262
  }
8357
- } catch (err2) {
7263
+ } catch (err) {
8358
7264
  okResult = false;
8359
- resultReason = err2 instanceof Error ? err2.message : String(err2);
7265
+ resultReason = err instanceof Error ? err.message : String(err);
8360
7266
  turnLog.error({ err: resultReason }, "dispatcher: SDK query threw");
8361
7267
  } finally {
8362
7268
  if (idleTimer) clearTimeout(idleTimer);
@@ -8392,9 +7298,9 @@ ${reason}`,
8392
7298
  // CT113: the stopped marker is still "about" the triggering message.
8393
7299
  parentMessageId: payload.messageId
8394
7300
  });
8395
- } catch (err2) {
7301
+ } catch (err) {
8396
7302
  turnLog.warn(
8397
- { err: err2 instanceof Error ? err2.message : String(err2) },
7303
+ { err: err instanceof Error ? err.message : String(err) },
8398
7304
  "dispatcher: stopped-marker commit failed"
8399
7305
  );
8400
7306
  }
@@ -8435,9 +7341,9 @@ ${reason}`,
8435
7341
  payload.agentId,
8436
7342
  body
8437
7343
  );
8438
- } catch (err2) {
7344
+ } catch (err) {
8439
7345
  turnLog.warn(
8440
- { err: err2 instanceof Error ? err2.message : String(err2) },
7346
+ { err: err instanceof Error ? err.message : String(err) },
8441
7347
  "dispatcher: active-run clear failed terminally; server age-sweep is the backstop"
8442
7348
  );
8443
7349
  }
@@ -8505,7 +7411,6 @@ function buildCompanionManifest(opts) {
8505
7411
  if (opts.claudeCode) runtimes.push({ name: "claude-code", version: v.claudeCode ?? null });
8506
7412
  if (opts.opencode) runtimes.push({ name: "opencode", version: v.opencode ?? null });
8507
7413
  if (opts.codex) runtimes.push({ name: "codex", version: v.codex ?? null });
8508
- if (opts.cabaneNative) runtimes.push({ name: "cabane-native", version: null });
8509
7414
  return { runtimes, capabilities: { ...DEVICE_MANIFEST.capabilities } };
8510
7415
  }
8511
7416
 
@@ -8628,8 +7533,8 @@ async function probeHarnessSignals(cfg, deps = {}) {
8628
7533
  return {
8629
7534
  claudeOnPath: claudeOnPathResult,
8630
7535
  claudeVersion,
8631
- // A parseable `codex --version` is our presence signal (codex isn't the
8632
- // default runtime; its config flag is the manifest gate either way).
7536
+ // A parseable `codex --version` is our presence signal (presence alone never
7537
+ // exposes codex; its config flag is the manifest gate either way).
8633
7538
  codexOnPath: codexVersion !== null,
8634
7539
  codexVersion,
8635
7540
  codexEnabled: isCodexEnabled(cfg),
@@ -8743,13 +7648,13 @@ var Outbox = class {
8743
7648
  try {
8744
7649
  writeFileSync7(tmp, JSON.stringify(entry) + "\n", "utf8");
8745
7650
  renameSync3(tmp, target);
8746
- } catch (err2) {
7651
+ } catch (err) {
8747
7652
  try {
8748
7653
  rmSync5(tmp, { force: true });
8749
7654
  } catch {
8750
7655
  }
8751
7656
  this.log?.warn(
8752
- { workspaceId: this.workspaceId, err: err2 instanceof Error ? err2.message : String(err2) },
7657
+ { workspaceId: this.workspaceId, err: err instanceof Error ? err.message : String(err) },
8753
7658
  "companion outbox: failed to persist entry"
8754
7659
  );
8755
7660
  return;
@@ -8846,40 +7751,43 @@ var Outbox = class {
8846
7751
  };
8847
7752
 
8848
7753
  // src/run-config.ts
8849
- import { z as z16 } from "zod";
8850
- var mcpStdioServerSchema = z16.object({
8851
- type: z16.literal("stdio").optional(),
8852
- command: z16.string().min(1),
8853
- args: z16.array(z16.string()).optional(),
8854
- env: z16.record(z16.string(), z16.string()).optional()
7754
+ import { z as z14 } from "zod";
7755
+ var mcpStdioServerSchema = z14.object({
7756
+ type: z14.literal("stdio").optional(),
7757
+ command: z14.string().min(1),
7758
+ args: z14.array(z14.string()).optional(),
7759
+ env: z14.record(z14.string(), z14.string()).optional()
8855
7760
  });
8856
- var mcpHttpServerSchema = z16.object({
8857
- type: z16.literal("http"),
8858
- url: z16.string().url(),
8859
- headers: z16.record(z16.string(), z16.string()).optional()
7761
+ var mcpHttpServerSchema = z14.object({
7762
+ type: z14.literal("http"),
7763
+ url: z14.string().url(),
7764
+ headers: z14.record(z14.string(), z14.string()).optional()
8860
7765
  });
8861
- var mcpSseServerSchema = z16.object({
8862
- type: z16.literal("sse"),
8863
- url: z16.string().url(),
8864
- headers: z16.record(z16.string(), z16.string()).optional()
7766
+ var mcpSseServerSchema = z14.object({
7767
+ type: z14.literal("sse"),
7768
+ url: z14.string().url(),
7769
+ headers: z14.record(z14.string(), z14.string()).optional()
8865
7770
  });
8866
- var mcpServerDefSchema = z16.union([
7771
+ var mcpServerDefSchema = z14.union([
8867
7772
  mcpHttpServerSchema,
8868
7773
  mcpSseServerSchema,
8869
7774
  mcpStdioServerSchema
8870
7775
  ]);
8871
- var thinkingConfigSchema = z16.discriminatedUnion("type", [
8872
- z16.object({ type: z16.literal("adaptive") }),
8873
- z16.object({ type: z16.literal("enabled"), budgetTokens: z16.number().int().positive().optional() }),
8874
- z16.object({ type: z16.literal("disabled") })
7776
+ var thinkingConfigSchema = z14.discriminatedUnion("type", [
7777
+ z14.object({ type: z14.literal("adaptive") }),
7778
+ z14.object({ type: z14.literal("enabled"), budgetTokens: z14.number().int().positive().optional() }),
7779
+ z14.object({ type: z14.literal("disabled") })
8875
7780
  ]);
8876
- var effortSchema = z16.enum(["low", "medium", "high", "xhigh", "max"]);
8877
- var runConfigSchema = z16.object({
8878
- mode: z16.enum(["assistant", "coding", "custom"]).optional(),
8879
- allowedTools: z16.array(z16.string()).optional(),
8880
- disallowedTools: z16.array(z16.string()).optional(),
8881
- mcpServers: z16.record(z16.string(), mcpServerDefSchema).optional(),
8882
- model: z16.string().min(1).optional(),
7781
+ var effortSchema = z14.enum(["low", "medium", "high", "xhigh", "max"]);
7782
+ var runConfigSchema = z14.object({
7783
+ // CT788: the host-access binary replaced the `assistant`/`coding`/`custom` mode
7784
+ // trio + its custom tool lists — `true` grants the host filesystem/shell, absent
7785
+ // is the locked surface. Kept in lockstep with `@cabane/shared`'s
7786
+ // `agentRunConfigSchema` (independent zod, forward-compatible: unknown keys are
7787
+ // stripped, so an older companion riding a newer server never rejects the config).
7788
+ hostAccess: z14.boolean().optional(),
7789
+ mcpServers: z14.record(z14.string(), mcpServerDefSchema).optional(),
7790
+ model: z14.string().min(1).optional(),
8883
7791
  thinking: thinkingConfigSchema.optional(),
8884
7792
  effort: effortSchema.optional()
8885
7793
  });
@@ -8924,20 +7832,20 @@ var SseSubscriber = class {
8924
7832
  try {
8925
7833
  await this.connect();
8926
7834
  backoff = 500;
8927
- } catch (err2) {
7835
+ } catch (err) {
8928
7836
  if (this.aborted) return;
8929
- if (err2 instanceof ApiError && (err2.status === 401 || err2.status === 403)) {
7837
+ if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
8930
7838
  this.opts.log.error(
8931
- { workspaceId: this.opts.workspaceId, status: err2.status },
7839
+ { workspaceId: this.opts.workspaceId, status: err.status },
8932
7840
  "SSE auth failed \u2014 tearing down this workspace subscriber"
8933
7841
  );
8934
- this.opts.onAuthFailure(err2.status);
7842
+ this.opts.onAuthFailure(err.status);
8935
7843
  return;
8936
7844
  }
8937
7845
  this.opts.log.warn(
8938
7846
  {
8939
7847
  workspaceId: this.opts.workspaceId,
8940
- err: err2 instanceof Error ? err2.message : String(err2),
7848
+ err: err instanceof Error ? err.message : String(err),
8941
7849
  backoff
8942
7850
  },
8943
7851
  "SSE disconnected; reconnecting"
@@ -9080,7 +7988,7 @@ var CompanionSupervisor = class {
9080
7988
  if (!this.config.deviceToken) {
9081
7989
  this.log.warn("companion: not paired (no device token) \u2014 run `cabane-companion pair`");
9082
7990
  process.stdout.write(
9083
- "companion: this device is not paired \u2014 run `cabane-companion pair` and paste the string from the cabane app.\n"
7991
+ "companion: this device is not paired \u2014 run `cabane-companion pair`, then confirm the short code in Settings \u2192 Connectors.\n"
9084
7992
  );
9085
7993
  return;
9086
7994
  }
@@ -9135,9 +8043,6 @@ var CompanionSupervisor = class {
9135
8043
  // like opencode — the CLI's presence is the operator's responsibility;
9136
8044
  // a misconfigured device fails the turn loudly, never silently).
9137
8045
  codex: isCodexEnabled(this.config),
9138
- // CT598: advertise the native runtime when an OpenRouter key is set. Key
9139
- // absent → not advertised, so a native turn never routes here.
9140
- cabaneNative: isCabaneNativeEnabled(),
9141
8046
  // CT571/CT586: each runtime's `version` from the latest harness probe
9142
8047
  // (fail-soft to null). Informational only — the server matches on name.
9143
8048
  versions: this.harnessVersions
@@ -9155,9 +8060,9 @@ var CompanionSupervisor = class {
9155
8060
  this.hub.setDevice({ deviceId: res.deviceId });
9156
8061
  this.deviceId = res.deviceId;
9157
8062
  this.checkVersionSkew(res.serverVersion);
9158
- } catch (err2) {
8063
+ } catch (err) {
9159
8064
  this.log.warn(
9160
- { err: err2 instanceof Error ? err2.message : String(err2) },
8065
+ { err: err instanceof Error ? err.message : String(err) },
9161
8066
  "companion: device heartbeat failed (will retry on next tick)"
9162
8067
  );
9163
8068
  }
@@ -9193,12 +8098,12 @@ var CompanionSupervisor = class {
9193
8098
  const resp = await this.deviceApi.getAssignments();
9194
8099
  items = resp.assignments;
9195
8100
  device = resp.device;
9196
- } catch (err2) {
8101
+ } catch (err) {
9197
8102
  this.log.error(
9198
- { err: err2 instanceof Error ? err2.message : String(err2) },
8103
+ { err: err instanceof Error ? err.message : String(err) },
9199
8104
  "companion: assignments pull failed \u2014 check the device is still active in the cabane app"
9200
8105
  );
9201
- this.hub.setDeviceError(err2 instanceof Error ? err2.message : String(err2));
8106
+ this.hub.setDeviceError(err instanceof Error ? err.message : String(err));
9202
8107
  return;
9203
8108
  }
9204
8109
  this.hub.setDevice({ deviceId: device.id, deviceLabel: device.label });
@@ -9275,7 +8180,7 @@ var CompanionSupervisor = class {
9275
8180
  agentId: it.agentId,
9276
8181
  username: it.agentUsername,
9277
8182
  displayName: it.agentDisplayName,
9278
- mode: runConfig.mode ?? "assistant",
8183
+ mode: runConfig.hostAccess ? "full" : "none",
9279
8184
  hasCredential: false,
9280
8185
  missingSecrets: missing
9281
8186
  });
@@ -9295,7 +8200,7 @@ var CompanionSupervisor = class {
9295
8200
  agentId: it.agentId,
9296
8201
  username: it.agentUsername,
9297
8202
  displayName: it.agentDisplayName,
9298
- mode: runConfig.mode ?? "assistant",
8203
+ mode: runConfig.hostAccess ? "full" : "none",
9299
8204
  hasCredential: true,
9300
8205
  missingSecrets: missing
9301
8206
  });
@@ -9379,12 +8284,9 @@ var CompanionSupervisor = class {
9379
8284
  // CT481: register the codex adapter when this device offers codex; unset
9380
8285
  // leaves an `openai/…` turn to fail loudly (no silent claude-code fallback).
9381
8286
  ...isCodexEnabled(this.config) ? { codexEnabled: true } : {},
9382
- // CT598: register the cabane-native adapter when an OpenRouter key is set;
9383
- // unset leaves a `cabane-native/…` turn to fail loudly (no silent fallback).
9384
- ...cabaneNativeApiKey() ? { cabaneNativeApiKey: cabaneNativeApiKey() } : {},
9385
8287
  // CT556: per-turn timeout watchdog windows, from the companion's own env
9386
8288
  // (`AGENT_IDLE_TIMEOUT_MS` / `AGENT_TOTAL_TIMEOUT_MS`). Unset → the
9387
- // dispatcher's baked-in defaults (10 min idle / 45 min total).
8289
+ // dispatcher's baked-in defaults (10 min idle / 6h total).
9388
8290
  ...positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") !== void 0 ? { idleTimeoutMs: positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") } : {},
9389
8291
  ...positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") !== void 0 ? { totalTimeoutMs: positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") } : {},
9390
8292
  observer: this.hub.observerFor(ctx.workspaceId, ctx.agentId, ctx.workspaceSlug),
@@ -9451,9 +8353,9 @@ var CompanionSupervisor = class {
9451
8353
  let wire;
9452
8354
  try {
9453
8355
  wire = JSON.parse(ev.data);
9454
- } catch (err2) {
8356
+ } catch (err) {
9455
8357
  this.log.warn(
9456
- { err: err2 instanceof Error ? err2.message : String(err2) },
8358
+ { err: err instanceof Error ? err.message : String(err) },
9457
8359
  "malformed SSE payload"
9458
8360
  );
9459
8361
  return;
@@ -9500,13 +8402,13 @@ var CompanionSupervisor = class {
9500
8402
  }
9501
8403
  const chainKey = `${payload.conversationId}|${payload.agentId}`;
9502
8404
  const prev = wr.chains.get(chainKey) ?? Promise.resolve();
9503
- const tail = prev.then(() => this.runDispatch(wr, ev, payload, agent)).catch((err2) => {
8405
+ const tail = prev.then(() => this.runDispatch(wr, ev, payload, agent)).catch((err) => {
9504
8406
  this.log.warn(
9505
8407
  {
9506
8408
  workspaceId: wr.workspaceId,
9507
8409
  conversationId: payload.conversationId,
9508
8410
  agentId: payload.agentId,
9509
- err: err2 instanceof Error ? err2.message : String(err2)
8411
+ err: err instanceof Error ? err.message : String(err)
9510
8412
  },
9511
8413
  "companion: conversation turn handler threw"
9512
8414
  );
@@ -9598,10 +8500,10 @@ var CompanionSupervisor = class {
9598
8500
  } else {
9599
8501
  drainDelay = DRAIN_BASE_MS;
9600
8502
  }
9601
- } catch (err2) {
8503
+ } catch (err) {
9602
8504
  drainDelay = Math.min(drainDelay * 2, DRAIN_MAX_MS);
9603
8505
  this.log.warn(
9604
- { agentId, err: err2 instanceof Error ? err2.message : String(err2) },
8506
+ { agentId, err: err instanceof Error ? err.message : String(err) },
9605
8507
  "companion: outbox drain pass threw (will retry with backoff)"
9606
8508
  );
9607
8509
  } finally {
@@ -9668,9 +8570,9 @@ var CompanionSupervisor = class {
9668
8570
  codex: signals.codexVersion
9669
8571
  };
9670
8572
  this.hub.setHarnesses(deriveHarnessSnapshot(signals).harnesses);
9671
- } catch (err2) {
8573
+ } catch (err) {
9672
8574
  this.log.warn(
9673
- { err: err2 instanceof Error ? err2.message : String(err2) },
8575
+ { err: err instanceof Error ? err.message : String(err) },
9674
8576
  "companion: harness probe failed (will retry on next beat)"
9675
8577
  );
9676
8578
  }
@@ -9768,10 +8670,13 @@ var CompanionSupervisor = class {
9768
8670
  await Promise.race([
9769
8671
  Promise.allSettled(turns),
9770
8672
  new Promise((resolve) => {
9771
- timer = setTimeout(() => {
9772
- timedOut = true;
9773
- resolve();
9774
- }, Math.max(0, graceMs));
8673
+ timer = setTimeout(
8674
+ () => {
8675
+ timedOut = true;
8676
+ resolve();
8677
+ },
8678
+ Math.max(0, graceMs)
8679
+ );
9775
8680
  timer.unref?.();
9776
8681
  })
9777
8682
  ]);
@@ -9825,17 +8730,17 @@ var RECOVERABLE_CODES = /* @__PURE__ */ new Set([
9825
8730
  "ERR_STREAM_DESTROYED",
9826
8731
  "ERR_STREAM_WRITE_AFTER_END"
9827
8732
  ]);
9828
- function errorCode(err2) {
9829
- if (err2 && typeof err2 === "object" && "code" in err2) {
9830
- const code = err2.code;
8733
+ function errorCode(err) {
8734
+ if (err && typeof err === "object" && "code" in err) {
8735
+ const code = err.code;
9831
8736
  if (typeof code === "string") return code;
9832
8737
  }
9833
8738
  return void 0;
9834
8739
  }
9835
- function isRecoverableSocketError(err2) {
9836
- const code = errorCode(err2);
8740
+ function isRecoverableSocketError(err) {
8741
+ const code = errorCode(err);
9837
8742
  if (code && RECOVERABLE_CODES.has(code)) return true;
9838
- const message = err2 instanceof Error ? err2.message : String(err2);
8743
+ const message = err instanceof Error ? err.message : String(err);
9839
8744
  return /\bEPIPE\b|\bECONNRESET\b/.test(message);
9840
8745
  }
9841
8746
  function installProcessSafetyNet(log, opts = {}) {
@@ -9844,16 +8749,16 @@ function installProcessSafetyNet(log, opts = {}) {
9844
8749
  stream.on("error", () => {
9845
8750
  });
9846
8751
  }
9847
- proc.on("uncaughtException", (err2) => handleUncaught(log, err2, "uncaughtException"));
8752
+ proc.on("uncaughtException", (err) => handleUncaught(log, err, "uncaughtException"));
9848
8753
  proc.on(
9849
8754
  "unhandledRejection",
9850
8755
  (reason) => handleUncaught(log, reason, "unhandledRejection")
9851
8756
  );
9852
8757
  }
9853
- function handleUncaught(log, err2, origin) {
9854
- const message = err2 instanceof Error ? err2.message : String(err2);
9855
- const code = errorCode(err2);
9856
- if (isRecoverableSocketError(err2)) {
8758
+ function handleUncaught(log, err, origin) {
8759
+ const message = err instanceof Error ? err.message : String(err);
8760
+ const code = errorCode(err);
8761
+ if (isRecoverableSocketError(err)) {
9857
8762
  log.warn(
9858
8763
  { origin, code, err: message },
9859
8764
  "companion: recovered from a socket error (kept running \u2014 a broken pipe never kills the companion)"
@@ -9861,7 +8766,7 @@ function handleUncaught(log, err2, origin) {
9861
8766
  return;
9862
8767
  }
9863
8768
  log.error(
9864
- { origin, code, err: message, stack: err2 instanceof Error ? err2.stack : void 0 },
8769
+ { origin, code, err: message, stack: err instanceof Error ? err.stack : void 0 },
9865
8770
  "companion: uncaught error (kept running \u2014 see the stack above)"
9866
8771
  );
9867
8772
  }
@@ -9898,14 +8803,14 @@ async function createCompanionRuntime(opts = {}) {
9898
8803
  cfg = requireConfig();
9899
8804
  claudeCode = await probeClaude();
9900
8805
  await ensureRuntimeAvailable(cfg, { probeClaude: async () => claudeCode });
9901
- } catch (err2) {
8806
+ } catch (err) {
9902
8807
  recordCrash({
9903
- reason: err2 instanceof Error ? err2.message : String(err2),
9904
- ...errorCode(err2) ? { code: errorCode(err2) } : {},
8808
+ reason: err instanceof Error ? err.message : String(err),
8809
+ ...errorCode(err) ? { code: errorCode(err) } : {},
9905
8810
  origin: "startup",
9906
8811
  at: (/* @__PURE__ */ new Date()).toISOString()
9907
8812
  });
9908
- throw err2;
8813
+ throw err;
9909
8814
  }
9910
8815
  if (cfg.logLevel) log.level = cfg.logLevel;
9911
8816
  const harnessVersions = await probeHarnessVersions({