@cabane/companion 0.6.0 → 0.6.1

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");
@@ -1101,8 +1069,8 @@ async function verifyRuntime(state, fetchImpl = fetch) {
1101
1069
  let res;
1102
1070
  try {
1103
1071
  res = await fetchImpl(url, { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
1104
- } catch (err2) {
1105
- return isConnRefused(err2) ? "stale" : "unknown";
1072
+ } catch (err) {
1073
+ return isConnRefused(err) ? "stale" : "unknown";
1106
1074
  }
1107
1075
  if (!res.ok) return "unknown";
1108
1076
  let body;
@@ -1114,9 +1082,9 @@ async function verifyRuntime(state, fetchImpl = fetch) {
1114
1082
  if (typeof body.instance_id !== "string") return "unknown";
1115
1083
  return body.instance_id === state.instanceId ? "ours" : "stale";
1116
1084
  }
1117
- function isConnRefused(err2) {
1118
- if (!err2 || typeof err2 !== "object") return false;
1119
- const cause = err2.cause;
1085
+ function isConnRefused(err) {
1086
+ if (!err || typeof err !== "object") return false;
1087
+ const cause = err.cause;
1120
1088
  return !!cause && typeof cause === "object" && cause.code === "ECONNREFUSED";
1121
1089
  }
1122
1090
  function trimSlash(s) {
@@ -1179,10 +1147,10 @@ var CabaneApi = class {
1179
1147
  for (let attempt = 1; ; attempt++) {
1180
1148
  try {
1181
1149
  return await this.attempt(method, path3, body, signal);
1182
- } catch (err2) {
1183
- if (attempt >= maxAttempts || signal?.aborted || !isRetryable(err2)) throw err2;
1150
+ } catch (err) {
1151
+ if (attempt >= maxAttempts || signal?.aborted || !isRetryable(err)) throw err;
1184
1152
  await sleep(RETRY_BACKOFF_MS[attempt - 1], signal);
1185
- if (signal?.aborted) throw err2;
1153
+ if (signal?.aborted) throw err;
1186
1154
  }
1187
1155
  }
1188
1156
  }
@@ -1210,14 +1178,14 @@ var CabaneApi = class {
1210
1178
  retry: true,
1211
1179
  ...signal ? { signal } : {}
1212
1180
  });
1213
- } catch (err2) {
1181
+ } catch (err) {
1214
1182
  const outbox = this.opts.outbox;
1215
- if (!outbox) throw err2;
1216
- if (signal?.aborted || isAbortError(err2)) throw err2;
1217
- if (!isRetryable(err2)) throw err2;
1183
+ if (!outbox) throw err;
1184
+ if (signal?.aborted || isAbortError(err)) throw err;
1185
+ if (!isRetryable(err)) throw err;
1218
1186
  outbox.persist({ enqueuedAt: Date.now(), turnId, seq, method: "POST", path: path3, body, kind });
1219
1187
  this.opts.log?.warn(
1220
- { kind, turnId, seq, err: err2 instanceof Error ? err2.message : String(err2) },
1188
+ { kind, turnId, seq, err: err instanceof Error ? err.message : String(err) },
1221
1189
  "companion: commit queued to outbox after transient failure (will drain when the API returns)"
1222
1190
  );
1223
1191
  }
@@ -1241,10 +1209,10 @@ var CabaneApi = class {
1241
1209
  await this.request(entry.method, entry.path, entry.body, { retry: true });
1242
1210
  outbox.remove(entry.turnId, entry.seq);
1243
1211
  progressed = true;
1244
- } catch (err2) {
1245
- if (err2 instanceof ApiError && err2.status >= 400 && err2.status < 500) {
1212
+ } catch (err) {
1213
+ if (err instanceof ApiError && err.status >= 400 && err.status < 500) {
1246
1214
  this.opts.log?.warn(
1247
- { kind: entry.kind, turnId: entry.turnId, seq: entry.seq, status: err2.status },
1215
+ { kind: entry.kind, turnId: entry.turnId, seq: entry.seq, status: err.status },
1248
1216
  "companion outbox: discarding entry on terminal 4xx (will never land)"
1249
1217
  );
1250
1218
  outbox.remove(entry.turnId, entry.seq);
@@ -1392,11 +1360,11 @@ var CabaneApi = class {
1392
1360
  try {
1393
1361
  await this.request("PATCH", path3, body, { retry: true });
1394
1362
  outbox?.remove(key, ACTIVE_RUN_OUTBOX_SEQ);
1395
- } catch (err2) {
1396
- if (!outbox) throw err2;
1397
- if (!isRetryable(err2)) {
1363
+ } catch (err) {
1364
+ if (!outbox) throw err;
1365
+ if (!isRetryable(err)) {
1398
1366
  outbox.remove(key, ACTIVE_RUN_OUTBOX_SEQ);
1399
- throw err2;
1367
+ throw err;
1400
1368
  }
1401
1369
  outbox.persist({
1402
1370
  enqueuedAt: Date.now(),
@@ -1408,7 +1376,7 @@ var CabaneApi = class {
1408
1376
  kind: "active-run"
1409
1377
  });
1410
1378
  this.opts.log?.warn(
1411
- { conversationId, agentId, err: err2 instanceof Error ? err2.message : String(err2) },
1379
+ { conversationId, agentId, err: err instanceof Error ? err.message : String(err) },
1412
1380
  "companion: active-run write queued to outbox after transient failure (will drain when the API returns)"
1413
1381
  );
1414
1382
  }
@@ -1507,13 +1475,13 @@ var CabaneApi = class {
1507
1475
  return res.messages.find((m) => m.id === messageId2) ?? null;
1508
1476
  }
1509
1477
  };
1510
- function isRetryable(err2) {
1511
- if (err2 instanceof ApiError) return err2.status >= 500;
1512
- if (isAbortError(err2)) return false;
1478
+ function isRetryable(err) {
1479
+ if (err instanceof ApiError) return err.status >= 500;
1480
+ if (isAbortError(err)) return false;
1513
1481
  return true;
1514
1482
  }
1515
- function isAbortError(err2) {
1516
- return err2 instanceof Error && err2.name === "AbortError";
1483
+ function isAbortError(err) {
1484
+ return err instanceof Error && err.name === "AbortError";
1517
1485
  }
1518
1486
  function sleep(ms, signal) {
1519
1487
  return new Promise((resolve) => {
@@ -1531,8 +1499,8 @@ function sleep(ms, signal) {
1531
1499
  }
1532
1500
  function errorMessage(status, body) {
1533
1501
  if (body && typeof body === "object" && "error" in body) {
1534
- const err2 = body.error;
1535
- if (typeof err2 === "string") return `${status} ${err2}`;
1502
+ const err = body.error;
1503
+ if (typeof err === "string") return `${status} ${err}`;
1536
1504
  }
1537
1505
  if (typeof body === "string" && body.length > 0) return `${status} ${body.slice(0, 200)}`;
1538
1506
  return `${status} error`;
@@ -1588,8 +1556,8 @@ var DeviceApi = class {
1588
1556
  };
1589
1557
  function errorMessage2(status, body) {
1590
1558
  if (body && typeof body === "object" && "error" in body) {
1591
- const err2 = body.error;
1592
- if (typeof err2 === "string") return `${status} ${err2}`;
1559
+ const err = body.error;
1560
+ if (typeof err === "string") return `${status} ${err}`;
1593
1561
  }
1594
1562
  if (typeof body === "string" && body.length > 0) return `${status} ${body.slice(0, 200)}`;
1595
1563
  return `${status} error`;
@@ -1606,11 +1574,11 @@ import {
1606
1574
  writeFileSync as writeFileSync3
1607
1575
  } from "fs";
1608
1576
  import { dirname as dirname4, join as join6 } from "path";
1609
- import { z as z4 } from "zod";
1577
+ import { z as z3 } from "zod";
1610
1578
  function credentialsPath() {
1611
1579
  return join6(cabaneDir(), "credentials.json");
1612
1580
  }
1613
- var credentialStoreSchema = z4.record(z4.string(), z4.string());
1581
+ var credentialStoreSchema = z3.record(z3.string(), z3.string());
1614
1582
  function load() {
1615
1583
  const path3 = credentialsPath();
1616
1584
  if (!existsSync4(path3)) return {};
@@ -1643,12 +1611,12 @@ function save(map) {
1643
1611
  } catch {
1644
1612
  }
1645
1613
  renameSync2(tmp, path3);
1646
- } catch (err2) {
1614
+ } catch (err) {
1647
1615
  try {
1648
1616
  rmSync3(tmp, { force: true });
1649
1617
  } catch {
1650
1618
  }
1651
- throw err2;
1619
+ throw err;
1652
1620
  }
1653
1621
  }
1654
1622
  function getCredential(agentId) {
@@ -1821,44 +1789,44 @@ function noResume() {
1821
1789
  var TURN_PROTOCOL_VERSION = 1;
1822
1790
 
1823
1791
  // packages/agent-runtime/src/host-policy.ts
1824
- import { z as z5 } from "zod";
1825
- var hostPolicySchema = z5.object({
1792
+ import { z as z4 } from "zod";
1793
+ var hostPolicySchema = z4.object({
1826
1794
  // Host filesystem + shell: `Bash`/`Read`/`Write`/`Edit`/`Glob`/`Grep`, the
1827
1795
  // notebook read/write pair, git-worktree tools, and the `REPL` host code-exec
1828
1796
  // tool. Off under the locked assistant surface (today's `DISALLOWED_TOOLS`);
1829
1797
  // on under `coding` mode.
1830
- hostFs: z5.boolean(),
1798
+ hostFs: z4.boolean(),
1831
1799
  // Public web (today's `WebSearch` / `WebFetch`, the `DEFAULT_WEB_TOOLS`). Public
1832
1800
  // web, not host reach — granted by default today, but expressible as a grant.
1833
- web: z5.boolean(),
1801
+ web: z4.boolean(),
1834
1802
  // Browser automation (the Playwright MCP surface). Varies by host: a companion has
1835
1803
  // it, the house executor does not (CT230).
1836
- browser: z5.boolean(),
1804
+ browser: z4.boolean(),
1837
1805
  // User-configured MCP servers permitted. False for the house executor
1838
1806
  // (CT227: Cabane agents run no user MCP servers), true for a personal companion.
1839
- userMcp: z5.boolean(),
1807
+ userMcp: z4.boolean(),
1840
1808
  // Subagent / sub-dispatch (`Task` / `Agent`). A per-host GRANT (see the CT261
1841
1809
  // amendment above): `false` on the locked assistant/house surface (banned via
1842
1810
  // `DISALLOWED_TOOLS`), `true` under coding / custom where the fan-out is left to
1843
1811
  // the coding allow-hook (which reaches sub-agent tool calls, CT680) or the user
1844
1812
  // allowlist. The subagent completes within the turn, so
1845
1813
  // it's not the turn-model invariant `scheduling` is.
1846
- subagents: z5.boolean(),
1814
+ subagents: z4.boolean(),
1847
1815
  // ── Hard platform invariants — always denied, never granted ────────────────
1848
1816
  // Deferred re-invocation / scheduling (`ScheduleWakeup`, the `Cron*` / `Task*`
1849
1817
  // families, `Monitor`, `Workflow`, …). A turn is one query that resolves when
1850
1818
  // `result` fires; a scheduled callback fires after the reply window has closed
1851
1819
  // and strands the agent (the CT155/CT156 rule).
1852
- scheduling: z5.literal("never"),
1820
+ scheduling: z4.literal("never"),
1853
1821
  // Human-facing UI prompts (`AskUserQuestion`). A Cabane conversation has no
1854
1822
  // handler to answer a structured prompt, so the call hangs the turn
1855
1823
  // (`UNSUPPORTED_TOOLS`). The agent asks in its reply instead.
1856
- uiPrompts: z5.literal("never")
1824
+ uiPrompts: z4.literal("never")
1857
1825
  });
1858
1826
 
1859
1827
  // packages/agent-runtime/src/turn-event.ts
1860
- import { z as z6 } from "zod";
1861
- var turnEventSchema = z6.discriminatedUnion("type", [
1828
+ import { z as z5 } from "zod";
1829
+ var turnEventSchema = z5.discriminatedUnion("type", [
1862
1830
  // The runtime's opaque session state, emitted when the adapter learns it (e.g.
1863
1831
  // the SDK `system/init` frame). The platform stores `state` verbatim per
1864
1832
  // (conversation, agent) and hands it back on the next turn; only the adapter
@@ -1878,19 +1846,19 @@ var turnEventSchema = z6.discriminatedUnion("type", [
1878
1846
  // on the companion, after the server committed the manifest). Runtime-neutral: a
1879
1847
  // plain boolean, not a runtime-specific reason string (that stays in the
1880
1848
  // 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()
1849
+ z5.object({
1850
+ type: z5.literal("session"),
1851
+ state: z5.string(),
1852
+ degraded: z5.boolean().optional()
1885
1853
  }),
1886
1854
  // One readable thinking summary. Maps `onThinking({ text })`. Transient —
1887
1855
  // surfaced live, never persisted as durable content.
1888
- z6.object({ type: z6.literal("thinking"), text: z6.string() }),
1856
+ z5.object({ type: z5.literal("thinking"), text: z5.string() }),
1889
1857
  // Assistant text. Maps `onAssistantText({ text, final })` — `text`→`body`,
1890
1858
  // `final`→`terminal`. `terminal: false` is interim narration (commits as a
1891
1859
  // `progress` row); `terminal: true` is the turn's closing reply (commits as
1892
1860
  // the `final` row).
1893
- z6.object({ type: z6.literal("text"), body: z6.string(), terminal: z6.boolean() }),
1861
+ z5.object({ type: z5.literal("text"), body: z5.string(), terminal: z5.boolean() }),
1894
1862
  // A tool-activity transition. Maps `onToolActivity` — `toolUseId`→`id`,
1895
1863
  // `toolName`→`name` (already prefix-stripped: `cabane_read`, not
1896
1864
  // `mcp__cabane__cabane_read`), `summary` is the short card label. `phase`
@@ -1905,15 +1873,15 @@ var turnEventSchema = z6.discriminatedUnion("type", [
1905
1873
  // dropped the prefix; null for a host / built-in tool. The client tags Cabane
1906
1874
  // MCP calls (`=== 'cabane'`) with a subtle glyph. Nullable + optional so a
1907
1875
  // 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()
1876
+ z5.object({
1877
+ type: z5.literal("tool"),
1878
+ id: z5.string(),
1879
+ name: z5.string(),
1880
+ phase: z5.enum(["start", "done", "error"]),
1881
+ summary: z5.string(),
1882
+ input: z5.unknown().optional(),
1883
+ result: z5.unknown().optional(),
1884
+ mcpServer: z5.string().nullable().optional()
1917
1885
  }),
1918
1886
  // The turn's terminal outcome. Maps the `{ ok, reason }` both loops return
1919
1887
  // inline. `ok:false` carries a machine reason (`no_session`, an error code);
@@ -1957,34 +1925,34 @@ var turnEventSchema = z6.discriminatedUnion("type", [
1957
1925
  // `message_turn_metadata.resolved_model` / `resolved_config`. Optional ⇒
1958
1926
  // backward-compatible: an old adapter/companion omits them, a cancel has no result
1959
1927
  // 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()
1928
+ z5.object({
1929
+ type: z5.literal("result"),
1930
+ ok: z5.boolean(),
1931
+ reason: z5.string().optional(),
1932
+ usage: z5.object({
1933
+ inputTokens: z5.number(),
1934
+ outputTokens: z5.number(),
1935
+ cacheReadTokens: z5.number().optional(),
1936
+ cacheCreationTokens: z5.number().optional(),
1937
+ contextTokens: z5.number().optional(),
1938
+ contextWindow: z5.number().optional()
1971
1939
  }).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()
1940
+ resolvedModel: z5.string().optional(),
1941
+ resolvedConfig: z5.object({
1942
+ effort: z5.string().optional(),
1943
+ thinking: z5.string().optional(),
1944
+ reasoningEffort: z5.string().optional()
1977
1945
  }).optional()
1978
1946
  })
1979
1947
  ]);
1980
1948
 
1981
1949
  // 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") })
1950
+ import { z as z6 } from "zod";
1951
+ var turnFailureSchema = z6.discriminatedUnion("kind", [
1952
+ z6.object({ kind: z6.literal("usage_capped"), resetsAt: z6.string().optional() }),
1953
+ z6.object({ kind: z6.literal("rate_limited") }),
1954
+ z6.object({ kind: z6.literal("server_error") }),
1955
+ z6.object({ kind: z6.literal("auth_expired") })
1988
1956
  ]);
1989
1957
  var USAGE_CAPPED = "usage_capped";
1990
1958
  var RATE_LIMITED = "rate_limited";
@@ -2090,63 +2058,63 @@ var RATE_PATTERNS = [/rate[_ ]?limit/, /\b429\b/, /too many requests/];
2090
2058
  var BARE_LIMIT = /\blimit (reached|exceeded)\b/;
2091
2059
 
2092
2060
  // 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() })
2061
+ import { z as z7 } from "zod";
2062
+ var contentBlockSchema = z7.discriminatedUnion("type", [
2063
+ z7.object({ type: z7.literal("text"), text: z7.string() }),
2064
+ z7.object({
2065
+ type: z7.literal("image"),
2066
+ source: z7.object({ type: z7.literal("url"), url: z7.string() })
2099
2067
  }),
2100
- z8.object({
2101
- type: z8.literal("document"),
2102
- source: z8.object({ type: z8.literal("url"), url: z8.string() })
2068
+ z7.object({
2069
+ type: z7.literal("document"),
2070
+ source: z7.object({ type: z7.literal("url"), url: z7.string() })
2103
2071
  })
2104
2072
  ]);
2105
- var effortLevelSchema = z8.enum(["low", "medium", "high", "xhigh", "max"]);
2106
- var resolvedRunConfigSchema = z8.object({
2107
- model: z8.string().nullable(),
2073
+ var effortLevelSchema = z7.enum(["low", "medium", "high", "xhigh", "max"]);
2074
+ var resolvedRunConfigSchema = z7.object({
2075
+ model: z7.string().nullable(),
2108
2076
  effort: effortLevelSchema.optional(),
2109
- runtimeOptions: z8.record(z8.string(), z8.unknown()).optional()
2077
+ runtimeOptions: z7.record(z7.string(), z7.unknown()).optional()
2110
2078
  });
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()
2079
+ var resolvedMcpServerSchema = z7.union([
2080
+ z7.object({
2081
+ type: z7.literal("stdio").optional(),
2082
+ command: z7.string(),
2083
+ args: z7.array(z7.string()).optional(),
2084
+ env: z7.record(z7.string(), z7.string()).optional()
2117
2085
  }),
2118
- z8.object({
2119
- type: z8.literal("http"),
2120
- url: z8.string(),
2121
- headers: z8.record(z8.string(), z8.string()).optional()
2086
+ z7.object({
2087
+ type: z7.literal("http"),
2088
+ url: z7.string(),
2089
+ headers: z7.record(z7.string(), z7.string()).optional()
2122
2090
  }),
2123
- z8.object({
2124
- type: z8.literal("sse"),
2125
- url: z8.string(),
2126
- headers: z8.record(z8.string(), z8.string()).optional()
2091
+ z7.object({
2092
+ type: z7.literal("sse"),
2093
+ url: z7.string(),
2094
+ headers: z7.record(z7.string(), z7.string()).optional()
2127
2095
  })
2128
2096
  ]);
2129
- var resolvedMcpServersSchema = z8.record(z8.string(), resolvedMcpServerSchema);
2130
- var hostInjectedServersSchema = z8.record(z8.string(), z8.unknown());
2131
- var turnRequestSchema = z8.object({
2097
+ var resolvedMcpServersSchema = z7.record(z7.string(), resolvedMcpServerSchema);
2098
+ var hostInjectedServersSchema = z7.record(z7.string(), z7.unknown());
2099
+ var turnRequestSchema = z7.object({
2132
2100
  // Server-composed system prompt (core + capability prose + adapter addendum +
2133
2101
  // charter). One string to the adapter.
2134
- systemPrompt: z8.string(),
2102
+ systemPrompt: z7.string(),
2135
2103
  // Server-composed per-turn user text (anchor reminder + the triggering message).
2136
- prompt: z8.string(),
2104
+ prompt: z7.string(),
2137
2105
  // The multi-block user-message body (text + vision).
2138
- content: z8.array(contentBlockSchema),
2106
+ content: z7.array(contentBlockSchema),
2139
2107
  // Portable-or-dialect run-config (above).
2140
2108
  config: resolvedRunConfigSchema,
2141
2109
  // Abstract capability grants; the adapter maps them to tool names.
2142
2110
  policy: hostPolicySchema,
2143
2111
  // Prior opaque session state, or null for a fresh session.
2144
- session: z8.string().nullable(),
2112
+ session: z7.string().nullable(),
2145
2113
  // 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(),
2114
+ cabane: z7.object({
2115
+ mcpUrl: z7.string(),
2116
+ bearer: z7.string(),
2117
+ activeConversationId: z7.string(),
2150
2118
  // CT714: the scoped TURN-CONTROL MCP endpoint (`/api/turn-control`). The
2151
2119
  // EXTERNAL adapters (Codex / opencode) mount it by URL under the key
2152
2120
  // `cabane_companion` — using the same `bearer` (the turn token) and the same
@@ -2156,7 +2124,7 @@ var turnRequestSchema = z8.object({
2156
2124
  // claude-code ignores it (it mounts the in-process instance instead), and
2157
2125
  // every existing `cabane`-block fixture keeps parsing unchanged; the
2158
2126
  // companion always populates it (`build-options.ts`).
2159
- turnControlUrl: z8.string().optional(),
2127
+ turnControlUrl: z7.string().optional(),
2160
2128
  // CT598: the workspace this turn runs in. The claude-code/opencode/codex
2161
2129
  // adapters never need it (they reach Cabane through the `cabane` MCP server,
2162
2130
  // which takes `workspaceId` as a per-tool arg the model supplies); the
@@ -2166,53 +2134,39 @@ var turnRequestSchema = z8.object({
2166
2134
  // adapters' conformance fixtures, tests) keeps parsing unchanged — the companion
2167
2135
  // always populates it (`build-options.ts`), and the native adapter fails the
2168
2136
  // turn loudly when it is somehow absent rather than guessing.
2169
- workspaceId: z8.string().optional(),
2137
+ workspaceId: z7.string().optional(),
2170
2138
  // CT752: the server-resolved workspace surface this credential exposes.
2171
2139
  // Readiness uses this explicit fact to require `sdk` for code mode and the
2172
2140
  // granular floor for classic mode; inventory contents alone cannot infer it
2173
2141
  // because `sdk` is intentionally also available on the classic surface.
2174
- workspaceToolSurface: z8.enum(["code", "classic"]).optional()
2142
+ workspaceToolSurface: z7.enum(["code", "classic"]).optional()
2175
2143
  }),
2176
2144
  // Machine-local resolution (host-filled): the checkout cwd, extra env from a
2177
2145
  // 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(),
2146
+ local: z7.object({
2147
+ cwd: z7.string().optional(),
2148
+ env: z7.record(z7.string(), z7.string()).optional(),
2186
2149
  mcpServers: resolvedMcpServersSchema.optional(),
2187
2150
  // CT289: machine-local claude-code adapter knobs the operator sets on a
2188
2151
  // companion they run themselves — the auto-memory escape hatch. `autoMemory:
2189
2152
  // true` opts back into Claude Code's auto-memory (governed by the operator's
2190
2153
  // 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()
2154
+ // default in place (see `buildClaudeCodeOptions`).
2155
+ claudeCode: z7.object({ autoMemory: z7.boolean().optional() }).optional()
2194
2156
  }),
2195
2157
  // 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()
2158
+ extra: z7.object({
2159
+ mcpServers: hostInjectedServersSchema
2206
2160
  })
2207
2161
  });
2208
2162
 
2209
2163
  // packages/agent-runtime/src/conformance.ts
2210
- import { z as z9 } from "zod";
2211
- var conformanceFixtureSchema = z9.object({
2212
- name: z9.string(),
2164
+ import { z as z8 } from "zod";
2165
+ var conformanceFixtureSchema = z8.object({
2166
+ name: z8.string(),
2213
2167
  request: turnRequestSchema,
2214
- nativeStream: z9.array(z9.unknown()),
2215
- expected: z9.array(turnEventSchema)
2168
+ nativeStream: z8.array(z8.unknown()),
2169
+ expected: z8.array(turnEventSchema)
2216
2170
  });
2217
2171
 
2218
2172
  // packages/agent-runtime/src/transcript.ts
@@ -2222,8 +2176,8 @@ function createTerminalTextBuffer() {
2222
2176
  async function safeEmit(emit, event, onError) {
2223
2177
  try {
2224
2178
  await emit(event);
2225
- } catch (err2) {
2226
- onError?.(err2, event.type);
2179
+ } catch (err) {
2180
+ onError?.(err, event.type);
2227
2181
  }
2228
2182
  }
2229
2183
  async function processAssistantMessage(msg, emit, pending, buffer, onError) {
@@ -2484,16 +2438,16 @@ var TurnPump = class {
2484
2438
  // minimal note. Skipped when cancelled or already final. The held-text flush
2485
2439
  // that precedes it is a classification concern, driven by the caller before
2486
2440
  // this runs.
2487
- async finalize(ok2) {
2488
- if (!ok2 || this.opts.signal.aborted || this.emittedFinal) return;
2441
+ async finalize(ok) {
2442
+ if (!ok || this.opts.signal.aborted || this.emittedFinal) return;
2489
2443
  const body = this.lastProgressBody ?? this.opts.emptyFinalBody;
2490
2444
  const seq = this.opts.nextSeq();
2491
2445
  try {
2492
2446
  await this.opts.commit.commitMessage({ body, kind: "final", seq });
2493
2447
  this.emittedFinal = true;
2494
2448
  this.finalReplyBody = body;
2495
- } catch (err2) {
2496
- this.opts.onError?.(err2, "empty-final");
2449
+ } catch (err) {
2450
+ this.opts.onError?.(err, "empty-final");
2497
2451
  }
2498
2452
  }
2499
2453
  // Whether the turn has committed its `final` row — read by the host to decide
@@ -2517,7 +2471,7 @@ import {
2517
2471
  var CLAUDE_CODE_ADDENDUM = "";
2518
2472
 
2519
2473
  // packages/agent-runtime/src/claude-code/policy.ts
2520
- import { z as z10 } from "zod";
2474
+ import { z as z9 } from "zod";
2521
2475
  var HOST_FS_TOOLS = [
2522
2476
  // shell + local filesystem
2523
2477
  "Bash",
@@ -2567,30 +2521,20 @@ function withThinkingSummaries(thinking) {
2567
2521
  if (thinking.type === "disabled") return thinking;
2568
2522
  return { display: "summarized", ...thinking };
2569
2523
  }
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()
2524
+ var claudeCodeDialectSchema = z9.object({
2525
+ thinking: z9.discriminatedUnion("type", [
2526
+ z9.object({
2527
+ type: z9.literal("adaptive"),
2528
+ display: z9.enum(["summarized", "omitted"]).optional()
2575
2529
  }),
2576
- z10.object({
2577
- type: z10.literal("enabled"),
2578
- budgetTokens: z10.number().int().positive().optional(),
2579
- display: z10.enum(["summarized", "omitted"]).optional()
2530
+ z9.object({
2531
+ type: z9.literal("enabled"),
2532
+ budgetTokens: z9.number().int().positive().optional(),
2533
+ display: z9.enum(["summarized", "omitted"]).optional()
2580
2534
  }),
2581
- z10.object({ type: z10.literal("disabled") })
2535
+ z9.object({ type: z9.literal("disabled") })
2582
2536
  ]).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()
2537
+ hostAccess: z9.boolean().optional()
2594
2538
  }).loose();
2595
2539
  function readThinking(runtimeOptions) {
2596
2540
  const dialect = runtimeOptions?.["claude-code"];
@@ -2669,18 +2613,15 @@ function buildClaudeCodeOptions(req, augment) {
2669
2613
  };
2670
2614
  }
2671
2615
  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";
2616
+ const useCodingPreset = dialect.success ? dialect.data.hostAccess ?? false : false;
2675
2617
  const cabaneGlob = `mcp__${CABANE_MCP_SERVER}__*`;
2676
2618
  const extraServerGlobs = Object.keys(req.extra.mcpServers).map((name) => `mcp__${name}__*`);
2677
2619
  const allowedTools = dedupe([
2678
2620
  cabaneGlob,
2679
2621
  ...extraServerGlobs,
2680
- ...policy.web ? DEFAULT_WEB_TOOLS : [],
2681
- ...customAllowed
2622
+ ...policy.web ? DEFAULT_WEB_TOOLS : []
2682
2623
  ]);
2683
- const disallowedTools = dedupe([...disallowedToolsFor(policy), ...customDisallowed]);
2624
+ const disallowedTools = dedupe([...disallowedToolsFor(policy)]);
2684
2625
  const resumeDecision = decideResume(req.session, cwd);
2685
2626
  const resume = "resume" in resumeDecision ? resumeDecision.resume : null;
2686
2627
  const freshReason = "fresh" in resumeDecision ? resumeDecision.reason : void 0;
@@ -2746,7 +2687,7 @@ async function* decodeSdkStream(iter, ctx) {
2746
2687
  out.push(event);
2747
2688
  };
2748
2689
  let sessionEmitted = false;
2749
- let ok2 = false;
2690
+ let ok = false;
2750
2691
  let resultReason;
2751
2692
  let sawResult = false;
2752
2693
  let usage;
@@ -2792,8 +2733,8 @@ async function* decodeSdkStream(iter, ctx) {
2792
2733
  if (typeof info.rateLimitType === "string") rateLimitType = info.rateLimitType;
2793
2734
  }
2794
2735
  } else if (msg.type === "auth_status") {
2795
- const err2 = msg.error;
2796
- if (typeof err2 === "string" && err2.length > 0) authError = err2;
2736
+ const err = msg.error;
2737
+ if (typeof err === "string" && err.length > 0) authError = err;
2797
2738
  } else if (msg.type === "result") {
2798
2739
  sawResult = true;
2799
2740
  usage = readSdkUsage(msg);
@@ -2805,7 +2746,7 @@ async function* decodeSdkStream(iter, ctx) {
2805
2746
  }
2806
2747
  const isError = msg.is_error === true;
2807
2748
  if (msg.subtype === "success" && !isError) {
2808
- ok2 = true;
2749
+ ok = true;
2809
2750
  } else {
2810
2751
  const resultText = msg.result ?? "";
2811
2752
  const terminalReason = msg.terminal_reason;
@@ -2819,26 +2760,26 @@ async function* decodeSdkStream(iter, ctx) {
2819
2760
  ...rateLimitResetIso ? { resetsAt: rateLimitResetIso } : {}
2820
2761
  } : classifyAssistantError(lastAssistantError) ?? classifyErrorText([authError, errorText].filter(Boolean).join(" "));
2821
2762
  resultReason = failure ? encodeFailureReason(failure) : isError ? `error:${resultText.slice(0, 200) || "unknown"}` : `result_error:${msg.subtype}`;
2822
- ok2 = false;
2763
+ ok = false;
2823
2764
  }
2824
2765
  break;
2825
2766
  }
2826
2767
  }
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;
2768
+ } catch (err) {
2769
+ if (ctx.signal.aborted) throw err;
2770
+ const failure = classifyErrorText(err instanceof Error ? err.message : String(err));
2771
+ if (!failure) throw err;
2772
+ ok = false;
2832
2773
  resultReason = encodeFailureReason(failure);
2833
2774
  sawResult = true;
2834
2775
  }
2835
2776
  if (ctx.signal.aborted) return;
2836
- await flushHeldText(buffer, emit, ok2);
2777
+ await flushHeldText(buffer, emit, ok);
2837
2778
  yield* drain(out);
2838
- if (!ok2 && !resultReason && !sawResult) resultReason = "no_result";
2779
+ if (!ok && !resultReason && !sawResult) resultReason = "no_result";
2839
2780
  yield {
2840
2781
  type: "result",
2841
- ok: ok2,
2782
+ ok,
2842
2783
  ...resultReason ? { reason: resultReason } : {},
2843
2784
  ...usage ? { usage } : {},
2844
2785
  ...resolvedModel ? { resolvedModel } : {}
@@ -3401,9 +3342,9 @@ function readSessionId(properties) {
3401
3342
  }
3402
3343
  function readSessionError(properties) {
3403
3344
  const props = asRecord(properties);
3404
- const err2 = props?.error;
3405
- if (typeof err2 === "string") return err2;
3406
- const rec = asRecord(err2);
3345
+ const err = props?.error;
3346
+ if (typeof err === "string") return err;
3347
+ const rec = asRecord(err);
3407
3348
  if (!rec) return "unknown";
3408
3349
  const { name, message } = deepestError(rec);
3409
3350
  if (message && name && !isGenericErrorName(name)) return `${name}: ${message}`;
@@ -3439,7 +3380,7 @@ async function* decodeOpencodeStream(events, ctx) {
3439
3380
  const pending = /* @__PURE__ */ new Map();
3440
3381
  const startedTools = /* @__PURE__ */ new Set();
3441
3382
  const finishedTools = /* @__PURE__ */ new Set();
3442
- let ok2 = false;
3383
+ let ok = false;
3443
3384
  let reason;
3444
3385
  let settled = false;
3445
3386
  const userMessageIds = /* @__PURE__ */ new Set();
@@ -3502,7 +3443,7 @@ async function* decodeOpencodeStream(events, ctx) {
3502
3443
  const sealed = sealHeld(held, true);
3503
3444
  held = null;
3504
3445
  if (sealed) yield sealed;
3505
- ok2 = true;
3446
+ ok = true;
3506
3447
  settled = true;
3507
3448
  break;
3508
3449
  } else if (ev.type === "session.error") {
@@ -3511,7 +3452,7 @@ async function* decodeOpencodeStream(events, ctx) {
3511
3452
  const sealed = sealHeld(held, false);
3512
3453
  held = null;
3513
3454
  if (sealed) yield sealed;
3514
- ok2 = false;
3455
+ ok = false;
3515
3456
  const errorText = readSessionError(ev.properties);
3516
3457
  const failure = classifyErrorText(errorText);
3517
3458
  reason = failure ? encodeFailureReason(failure) : `error:${errorText.slice(0, 200)}`;
@@ -3526,7 +3467,7 @@ async function* decodeOpencodeStream(events, ctx) {
3526
3467
  if (sealed) yield sealed;
3527
3468
  reason = "no_terminal";
3528
3469
  }
3529
- yield { type: "result", ok: ok2, ...reason ? { reason } : {} };
3470
+ yield { type: "result", ok, ...reason ? { reason } : {} };
3530
3471
  }
3531
3472
  function hasToolInput(input) {
3532
3473
  return !!input && typeof input === "object" && Object.keys(input).length > 0;
@@ -3540,7 +3481,7 @@ function sealHeld(held, terminal) {
3540
3481
  }
3541
3482
 
3542
3483
  // packages/agent-runtime/src/opencode/policy.ts
3543
- import { z as z11 } from "zod";
3484
+ import { z as z10 } from "zod";
3544
3485
  var OPENCODE_HOST_TOOLS = [
3545
3486
  "bash",
3546
3487
  "edit",
@@ -3567,8 +3508,8 @@ function opencodeToolPolicy(policy) {
3567
3508
  deny(OPENCODE_UI_PROMPT_TOOLS);
3568
3509
  return { tools, allowAllHostTools: policy.hostFs };
3569
3510
  }
3570
- var opencodeDialectSchema = z11.object({
3571
- agent: z11.string().min(1).optional()
3511
+ var opencodeDialectSchema = z10.object({
3512
+ agent: z10.string().min(1).optional()
3572
3513
  }).loose();
3573
3514
  function readOpencodeDialect(runtimeOptions) {
3574
3515
  const parsed = opencodeDialectSchema.safeParse(runtimeOptions?.["opencode"] ?? {});
@@ -3789,9 +3730,9 @@ function createHttpOpencodeTransport(opts) {
3789
3730
  // The lock is released when this stream finishes draining.
3790
3731
  events: releaseAfter(parseSseStream(eventRes.body, sessionId, signal), release)
3791
3732
  };
3792
- } catch (err2) {
3733
+ } catch (err) {
3793
3734
  release();
3794
- throw err2;
3735
+ throw err;
3795
3736
  }
3796
3737
  }
3797
3738
  };
@@ -4384,9 +4325,9 @@ function readItemType(item) {
4384
4325
  function readErrorMessage(ev) {
4385
4326
  const direct = str(ev.message);
4386
4327
  if (direct) return direct;
4387
- const err2 = asRecord2(ev.error);
4388
- if (err2) {
4389
- const m = str(err2.message);
4328
+ const err = asRecord2(ev.error);
4329
+ if (err) {
4330
+ const m = str(err.message);
4390
4331
  if (m) return m;
4391
4332
  }
4392
4333
  return "unknown";
@@ -4442,7 +4383,7 @@ async function* decodeCodexStream(events, ctx) {
4442
4383
  const startedTools = /* @__PURE__ */ new Set();
4443
4384
  const finishedTools = /* @__PURE__ */ new Set();
4444
4385
  let sessionEmitted = false;
4445
- let ok2 = false;
4386
+ let ok = false;
4446
4387
  let reason;
4447
4388
  let usage;
4448
4389
  let settled = false;
@@ -4473,7 +4414,7 @@ async function* decodeCodexStream(events, ctx) {
4473
4414
  const message = readItemMessage(item);
4474
4415
  if (isModelMetadataError(message)) {
4475
4416
  yield* flushInterim();
4476
- ok2 = false;
4417
+ ok = false;
4477
4418
  reason = `model_unavailable:${message.slice(0, 200)}`;
4478
4419
  settled = true;
4479
4420
  break;
@@ -4528,13 +4469,13 @@ async function* decodeCodexStream(events, ctx) {
4528
4469
  held = null;
4529
4470
  if (sealed) yield sealed;
4530
4471
  usage = readUsage(ev);
4531
- ok2 = true;
4472
+ ok = true;
4532
4473
  settled = true;
4533
4474
  break;
4534
4475
  }
4535
4476
  if (ev.type === "turn.failed" || ev.type === "error") {
4536
4477
  yield* flushInterim();
4537
- ok2 = false;
4478
+ ok = false;
4538
4479
  const text = readErrorMessage(ev);
4539
4480
  const failure = classifyErrorText(text);
4540
4481
  reason = failure ? encodeFailureReason(failure) : `error:${text.slice(0, 200)}`;
@@ -4552,7 +4493,7 @@ async function* decodeCodexStream(events, ctx) {
4552
4493
  const resolvedConfig = ctx.resolvedReasoningEffort ? { reasoningEffort: ctx.resolvedReasoningEffort } : void 0;
4553
4494
  yield {
4554
4495
  type: "result",
4555
- ok: ok2,
4496
+ ok,
4556
4497
  ...reason ? { reason } : {},
4557
4498
  ...usage ? { usage } : {},
4558
4499
  ...ctx.resolvedModel ? { resolvedModel: ctx.resolvedModel } : {},
@@ -4570,7 +4511,7 @@ function sealHeld2(held, terminal) {
4570
4511
  }
4571
4512
 
4572
4513
  // packages/agent-runtime/src/codex/policy.ts
4573
- import { z as z12 } from "zod";
4514
+ import { z as z11 } from "zod";
4574
4515
  function codexToolPolicy(policy) {
4575
4516
  return policy.hostFs ? {
4576
4517
  permissionProfile: "cabane-coding",
@@ -4585,8 +4526,8 @@ function codexToolPolicy(policy) {
4585
4526
  };
4586
4527
  }
4587
4528
  var CODEX_REASONING_EFFORTS = ["minimal", "low", "medium", "high", "xhigh"];
4588
- var codexDialectSchema = z12.object({
4589
- modelReasoningEffort: z12.enum(CODEX_REASONING_EFFORTS).optional()
4529
+ var codexDialectSchema = z11.object({
4530
+ modelReasoningEffort: z11.enum(CODEX_REASONING_EFFORTS).optional()
4590
4531
  }).loose();
4591
4532
  function readCodexDialect(runtimeOptions) {
4592
4533
  const parsed = codexDialectSchema.safeParse(runtimeOptions?.["codex"] ?? {});
@@ -4594,7 +4535,7 @@ function readCodexDialect(runtimeOptions) {
4594
4535
  }
4595
4536
 
4596
4537
  // packages/agent-runtime/src/codex/model.ts
4597
- var CODEX_DEFAULT_MODEL_BARES = /* @__PURE__ */ new Set(["default", "codex"]);
4538
+ var CODEX_DEFAULT_MODEL_BARES = /* @__PURE__ */ new Set(["default"]);
4598
4539
  function parseCodexModel(model) {
4599
4540
  const sep = model.indexOf("/");
4600
4541
  const bare = sep === -1 || model.slice(0, sep) !== "openai" ? model : model.slice(sep + 1);
@@ -4681,9 +4622,11 @@ function buildConfig(req) {
4681
4622
  };
4682
4623
  }
4683
4624
  const policy = codexToolPolicy(req.policy);
4625
+ const tmpDir = req.local.env?.TMPDIR;
4684
4626
  return {
4685
4627
  mcp_servers,
4686
4628
  experimental_use_rmcp_client: true,
4629
+ ...tmpDir ? { shell_environment_policy: { set: { TMPDIR: tmpDir } } } : {},
4687
4630
  ...policy.permissionProfile ? {
4688
4631
  // CT733: named permission profiles are Codex's split-filesystem path.
4689
4632
  // `:root = read` preserves coding-mode host reads; the one explicit
@@ -4961,7 +4904,7 @@ var CodexExec = class {
4961
4904
  signal: args.signal
4962
4905
  });
4963
4906
  let spawnError = null;
4964
- child.once("error", (err2) => spawnError = err2);
4907
+ child.once("error", (err) => spawnError = err);
4965
4908
  if (!child.stdin) {
4966
4909
  child.kill();
4967
4910
  throw new Error("Child process has no stdin");
@@ -5865,1027 +5808,6 @@ var CODEX_CONFORMANCE_FIXTURES = [
5865
5808
  }
5866
5809
  ];
5867
5810
 
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
5811
  // packages/agent-runtime/src/claude-code/sdk.ts
6890
5812
  import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
6891
5813
 
@@ -6943,7 +5865,7 @@ import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync
6943
5865
  import { join as join12 } from "path";
6944
5866
 
6945
5867
  // src/summon.ts
6946
- import { z as z14 } from "zod";
5868
+ import { z as z12 } from "zod";
6947
5869
  var COMPANION_LOCAL_MCP_SERVER = "cabane_companion";
6948
5870
  var SUMMON_AGENT_TOOL = "summon_agent";
6949
5871
  var SUMMON_AGENT_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${SUMMON_AGENT_TOOL}`;
@@ -6977,7 +5899,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
6977
5899
  SUMMON_AGENT_TOOL,
6978
5900
  "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
5901
  {
6980
- agentId: z14.string().uuid().describe("The peer agent to summon \u2014 a workspace agent id from `list_agents`.")
5902
+ agentId: z12.string().uuid().describe("The peer agent to summon \u2014 a workspace agent id from `list_agents`.")
6981
5903
  },
6982
5904
  async (args) => {
6983
5905
  summonState.agentId = args.agentId;
@@ -6992,7 +5914,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
6992
5914
  SKIP_TURN_TOOL,
6993
5915
  `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
5916
  {
6995
- reason: z14.string().min(1).max(500).describe("Short reason you are declining \u2014 used for telemetry/debugging.")
5917
+ reason: z12.string().min(1).max(500).describe("Short reason you are declining \u2014 used for telemetry/debugging.")
6996
5918
  },
6997
5919
  async (args) => {
6998
5920
  skipState.skipped = true;
@@ -7009,21 +5931,21 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
7009
5931
  ASK_TOOL,
7010
5932
  "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
5933
  {
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(
5934
+ targetUserId: z12.string().uuid().describe("The workspace member (human) to ask \u2014 a user id from `list_members`."),
5935
+ question: z12.string().min(1).max(400).optional().describe(
7014
5936
  "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
5937
  ),
7016
- headline: z14.string().min(1).max(120).optional().describe(
5938
+ headline: z12.string().min(1).max(120).optional().describe(
7017
5939
  '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
5940
  ),
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(
5941
+ 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."),
5942
+ questions: z12.array(
5943
+ z12.object({
5944
+ headline: z12.string().min(1).max(120).describe(
7023
5945
  'The one-sentence question ("Do we go to prod?") \u2014 required for each item.'
7024
5946
  ),
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.")
5947
+ body: z12.string().min(1).max(400).optional().describe("Optional short framing beneath the headline. NOT a report."),
5948
+ 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
5949
  })
7028
5950
  ).min(1).max(5).optional().describe(
7029
5951
  "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 +6002,13 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
7080
6002
  SUB_AGENT_TOOL,
7081
6003
  "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
6004
  {
7083
- prompt: z14.string().min(1).max(65536).describe(
6005
+ prompt: z12.string().min(1).max(65536).describe(
7084
6006
  "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
6007
  ),
7086
- agentId: z14.string().uuid().optional().describe(
6008
+ agentId: z12.string().uuid().optional().describe(
7087
6009
  "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
6010
  ),
7089
- title: z14.string().max(200).optional().describe("Optional title for the child thread (result chips link it).")
6011
+ title: z12.string().max(200).optional().describe("Optional title for the child thread (result chips link it).")
7090
6012
  },
7091
6013
  async (args) => {
7092
6014
  const result = await subAgentCreate(args);
@@ -7116,13 +6038,13 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
7116
6038
  WAKE_ME_TOOL,
7117
6039
  '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
6040
  {
7119
- afterSeconds: z14.number().int().positive().optional().describe(
6041
+ afterSeconds: z12.number().int().positive().optional().describe(
7120
6042
  "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
6043
  ),
7122
- at: z14.string().datetime({ offset: true }).optional().describe(
6044
+ at: z12.string().datetime({ offset: true }).optional().describe(
7123
6045
  "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
6046
  ),
7125
- note: z14.string().min(1).max(2e3).describe(
6047
+ note: z12.string().min(1).max(2e3).describe(
7126
6048
  '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
6049
  )
7128
6050
  },
@@ -7202,7 +6124,6 @@ function buildCompanionTurnRequest(params) {
7202
6124
  local: {
7203
6125
  ...params.cwd ? { cwd: params.cwd } : {},
7204
6126
  ...params.env ? { env: params.env } : {},
7205
- ...params.nativeWorkAssignment ? { nativeWorkAssignment: params.nativeWorkAssignment } : {},
7206
6127
  // User MCP servers (already `${PLACEHOLDER}`-resolved). Structurally the
7207
6128
  // adapter's `ResolvedMcpServers`.
7208
6129
  ...Object.keys(params.mcpServers).length > 0 ? { mcpServers: params.mcpServers } : {},
@@ -7210,10 +6131,9 @@ function buildCompanionTurnRequest(params) {
7210
6131
  ...params.claudeCode ? { claudeCode: params.claudeCode } : {}
7211
6132
  },
7212
6133
  // Host-injected: the companion-local summon server (for the subprocess adapters,
7213
- // under its own namespace) + the cabane-native turn-control handler (CT666).
6134
+ // under its own namespace).
7214
6135
  extra: {
7215
- mcpServers: { [COMPANION_LOCAL_MCP_SERVER]: params.summonServer },
7216
- turnControl: params.turnControl
6136
+ mcpServers: { [COMPANION_LOCAL_MCP_SERVER]: params.summonServer }
7217
6137
  }
7218
6138
  };
7219
6139
  }
@@ -7230,23 +6150,18 @@ function dirFor(workspaceId) {
7230
6150
  function conversationDir(workspaceId, conversationId) {
7231
6151
  return join9(dirFor(workspaceId), encodeURIComponent(conversationId));
7232
6152
  }
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
- );
6153
+ function pathFor3(workspaceId, conversationId, agentId) {
6154
+ return join9(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
7239
6155
  }
7240
- function readPrepared(workspaceId, conversationId, agentId, assignmentKey) {
7241
- const path3 = pathFor3(workspaceId, conversationId, agentId, assignmentKey);
6156
+ function readPrepared(workspaceId, conversationId, agentId) {
6157
+ const path3 = pathFor3(workspaceId, conversationId, agentId);
7242
6158
  if (!existsSync7(path3)) return null;
7243
6159
  try {
7244
6160
  const parsed = JSON.parse(readFileSync6(path3, "utf8"));
7245
6161
  if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
7246
6162
  return {
7247
6163
  cwd: parsed.cwd,
7248
- ...parsed.env && typeof parsed.env === "object" ? { env: parsed.env } : {},
7249
- ...parsed.nativeWorkAssignment && typeof parsed.nativeWorkAssignment === "object" ? { nativeWorkAssignment: parsed.nativeWorkAssignment } : {}
6164
+ ...parsed.env && typeof parsed.env === "object" ? { env: parsed.env } : {}
7250
6165
  };
7251
6166
  }
7252
6167
  return null;
@@ -7254,10 +6169,10 @@ function readPrepared(workspaceId, conversationId, agentId, assignmentKey) {
7254
6169
  return null;
7255
6170
  }
7256
6171
  }
7257
- function writePrepared(workspaceId, conversationId, agentId, result, assignmentKey) {
6172
+ function writePrepared(workspaceId, conversationId, agentId, result) {
7258
6173
  mkdirSync7(conversationDir(workspaceId, conversationId), { recursive: true });
7259
6174
  writeFileSync6(
7260
- pathFor3(workspaceId, conversationId, agentId, assignmentKey),
6175
+ pathFor3(workspaceId, conversationId, agentId),
7261
6176
  JSON.stringify(result) + "\n",
7262
6177
  "utf8"
7263
6178
  );
@@ -7266,30 +6181,30 @@ function writePrepared(workspaceId, conversationId, agentId, result, assignmentK
7266
6181
  // src/secrets.ts
7267
6182
  import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
7268
6183
  import { join as join10 } from "path";
7269
- import { z as z15 } from "zod";
6184
+ import { z as z13 } from "zod";
7270
6185
  var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
7271
6186
  function secretsPath() {
7272
6187
  return join10(cabaneDir(), "secrets.json");
7273
6188
  }
7274
- var secretStoreSchema = z15.record(z15.string(), z15.string());
6189
+ var secretStoreSchema = z13.record(z13.string(), z13.string());
7275
6190
  function loadSecretStore() {
7276
6191
  const path3 = secretsPath();
7277
6192
  if (!existsSync8(path3)) return makeStore({});
7278
6193
  let raw;
7279
6194
  try {
7280
6195
  raw = readFileSync7(path3, "utf8");
7281
- } catch (err2) {
6196
+ } catch (err) {
7282
6197
  throw new ConfigError(
7283
- `couldn't read ${path3}: ${err2 instanceof Error ? err2.message : String(err2)}`
6198
+ `couldn't read ${path3}: ${err instanceof Error ? err.message : String(err)}`
7284
6199
  );
7285
6200
  }
7286
6201
  if (raw.trim().length === 0) return makeStore({});
7287
6202
  let parsed;
7288
6203
  try {
7289
6204
  parsed = JSON.parse(raw);
7290
- } catch (err2) {
6205
+ } catch (err) {
7291
6206
  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.`
6207
+ `${path3} is not valid JSON: ${err instanceof Error ? err.message : String(err)}. It must be a flat object of "NAME": "value" secret pairs.`
7293
6208
  );
7294
6209
  }
7295
6210
  const result = secretStoreSchema.safeParse(parsed);
@@ -7303,8 +6218,8 @@ function loadSecretStore() {
7303
6218
  function loadSecretStoreTolerant(onWarn) {
7304
6219
  try {
7305
6220
  return loadSecretStore();
7306
- } catch (err2) {
7307
- onWarn?.(err2 instanceof Error ? err2.message : String(err2));
6221
+ } catch (err) {
6222
+ onWarn?.(err instanceof Error ? err.message : String(err));
7308
6223
  return makeStore({});
7309
6224
  }
7310
6225
  }
@@ -7375,8 +6290,8 @@ var TranscriptWriter = class {
7375
6290
  } catch {
7376
6291
  }
7377
6292
  pruneOld(dir2, RETAIN);
7378
- } catch (err2) {
7379
- this.fail(err2);
6293
+ } catch (err) {
6294
+ this.fail(err);
7380
6295
  }
7381
6296
  this.line({ type: "_meta", ...meta });
7382
6297
  }
@@ -7392,14 +6307,14 @@ var TranscriptWriter = class {
7392
6307
  if (this.broken) return;
7393
6308
  try {
7394
6309
  appendFileSync(this.path, JSON.stringify(obj) + "\n", { mode: 384 });
7395
- } catch (err2) {
7396
- this.fail(err2);
6310
+ } catch (err) {
6311
+ this.fail(err);
7397
6312
  }
7398
6313
  }
7399
- fail(err2) {
6314
+ fail(err) {
7400
6315
  if (this.broken) return;
7401
6316
  this.broken = true;
7402
- this.onWarn?.(`transcript write failed (${err2 instanceof Error ? err2.message : String(err2)})`);
6317
+ this.onWarn?.(`transcript write failed (${err instanceof Error ? err.message : String(err)})`);
7403
6318
  }
7404
6319
  };
7405
6320
  function fileName(meta) {
@@ -7437,9 +6352,9 @@ var EMPTY_FINAL_BODY = "Done \u2014 see the changes above.";
7437
6352
  var TurnCommitter = class {
7438
6353
  constructor(deps) {
7439
6354
  this.deps = deps;
7440
- this.onError = (err2, hook) => {
6355
+ this.onError = (err, hook) => {
7441
6356
  deps.log.warn(
7442
- { err: err2 instanceof Error ? err2.message : String(err2), hook },
6357
+ { err: err instanceof Error ? err.message : String(err), hook },
7443
6358
  "dispatcher: transcript callback failed"
7444
6359
  );
7445
6360
  };
@@ -7504,9 +6419,9 @@ var TurnCommitter = class {
7504
6419
  signal: deps.signal,
7505
6420
  nextSeq: deps.nextSeq,
7506
6421
  emptyFinalBody: EMPTY_FINAL_BODY,
7507
- onError: (err2) => {
6422
+ onError: (err) => {
7508
6423
  deps.log.warn(
7509
- { err: err2 instanceof Error ? err2.message : String(err2) },
6424
+ { err: err instanceof Error ? err.message : String(err) },
7510
6425
  "dispatcher: empty-final commit failed"
7511
6426
  );
7512
6427
  }
@@ -7529,8 +6444,8 @@ var TurnCommitter = class {
7529
6444
  if (event.type === "session" || event.type === "result") return;
7530
6445
  try {
7531
6446
  await this.emit(event);
7532
- } catch (err2) {
7533
- this.onError(err2, event.type);
6447
+ } catch (err) {
6448
+ this.onError(err, event.type);
7534
6449
  }
7535
6450
  }
7536
6451
  // End-of-turn empty-final promotion. The held-text flush is now the adapter's
@@ -7726,10 +6641,27 @@ var UNEXPECTED_ROLE_REASON = `refused the wake trigger (role "system" isn't disp
7726
6641
  var SKIPPED_MARKER_BODY = "(skipped)";
7727
6642
  var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
7728
6643
  var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
7729
- var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 45 * 6e4;
6644
+ var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
7730
6645
  function runKey(conversationId, agentId) {
7731
6646
  return `${conversationId}|${agentId}`;
7732
6647
  }
6648
+ function describeSubAgentError(status, body) {
6649
+ const code = body && typeof body === "object" && "error" in body ? String(body.error) : void 0;
6650
+ switch (code) {
6651
+ case "callout_cap_exceeded":
6652
+ 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.";
6653
+ case "callout_depth_exceeded":
6654
+ return "sub_agent: this would nest sub-agents too deep (max 3 levels). Have the current worker report back rather than spawning another layer.";
6655
+ case "dispatch_agent_not_found":
6656
+ return "sub_agent: no live agent in this workspace matches that `agentId`. Check `list_agents`, or omit `agentId` to spawn yourself.";
6657
+ case "dispatch_return_requires_turn":
6658
+ case "dispatch_return_requires_agent":
6659
+ case "dispatch_return_requires_dispatch":
6660
+ 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.`;
6661
+ default:
6662
+ return `sub_agent: the spawn failed (${code ?? `HTTP ${status}`}).`;
6663
+ }
6664
+ }
7733
6665
  var Dispatcher = class {
7734
6666
  constructor(opts) {
7735
6667
  this.opts = opts;
@@ -7756,7 +6688,7 @@ var Dispatcher = class {
7756
6688
  // clear it — the SJ383 `finally` after the SDK loop is the one clear, and
7757
6689
  // every pre-run exit returns before reaching it. So each pre-run failure has
7758
6690
  // to clear `active_run_started_at` itself, mirroring that `finally`, or the
7759
- // indicator strands until the 90-min age sweep.
6691
+ // indicator strands until the 12h age sweep.
7760
6692
  //
7761
6693
  // `errorReason` controls the server's duplicate-notice rule (the active-run
7762
6694
  // PATCH handler in conversations.ts): a clear carrying `errorReason` makes the
@@ -7776,9 +6708,9 @@ var Dispatcher = class {
7776
6708
  payload.agentId,
7777
6709
  body
7778
6710
  );
7779
- } catch (err2) {
6711
+ } catch (err) {
7780
6712
  turnLog.warn(
7781
- { err: err2 instanceof Error ? err2.message : String(err2) },
6713
+ { err: err instanceof Error ? err.message : String(err) },
7782
6714
  "dispatcher: pre-run active-run clear failed terminally; server age-sweep is the backstop"
7783
6715
  );
7784
6716
  }
@@ -7804,16 +6736,16 @@ var Dispatcher = class {
7804
6736
  payload.messageId,
7805
6737
  turnId
7806
6738
  );
7807
- } catch (err2) {
7808
- const status = err2 instanceof ApiError ? err2.status : 0;
6739
+ } catch (err) {
6740
+ const status = err instanceof ApiError ? err.status : 0;
7809
6741
  if (status === 404) {
7810
6742
  turnLog.warn(
7811
- { err: err2 instanceof Error ? err2.message : String(err2) },
6743
+ { err: err instanceof Error ? err.message : String(err) },
7812
6744
  "dispatcher: turn-context 404 (stale event \u2014 conversation/message/participant gone); skipping"
7813
6745
  );
7814
6746
  return this.concludeBeforeRun(payload, turnLog, startedAt, "turn_context_not_found");
7815
6747
  }
7816
- const reason = err2 instanceof Error ? err2.message : String(err2);
6748
+ const reason = err instanceof Error ? err.message : String(err);
7817
6749
  turnLog.error({ err: reason }, "dispatcher: failed to fetch turn context");
7818
6750
  const fetchReason = `fetch_failed: ${reason}`;
7819
6751
  return this.concludeBeforeRun(payload, turnLog, startedAt, fetchReason, fetchReason);
@@ -7876,20 +6808,11 @@ var Dispatcher = class {
7876
6808
  effectiveCwd = void 0;
7877
6809
  }
7878
6810
  let hookEnv;
7879
- let preparedNativeAssignment;
7880
6811
  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
- );
6812
+ const cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
7889
6813
  if (cached2) {
7890
6814
  effectiveCwd = cached2.cwd;
7891
6815
  hookEnv = cached2.env;
7892
- preparedNativeAssignment = cached2.nativeWorkAssignment;
7893
6816
  } else {
7894
6817
  const delayMs = this.opts.preparingRowDelayMs ?? DEFAULT_PREPARING_ROW_DELAY_MS;
7895
6818
  let preparingStarted = false;
@@ -7904,9 +6827,9 @@ var Dispatcher = class {
7904
6827
  summary: "",
7905
6828
  phase,
7906
6829
  seq
7907
- }).catch((err2) => {
6830
+ }).catch((err) => {
7908
6831
  turnLog.warn(
7909
- { err: err2 instanceof Error ? err2.message : String(err2), phase },
6832
+ { err: err instanceof Error ? err.message : String(err), phase },
7910
6833
  "dispatcher: preparing-activity report failed (continuing with the hook)"
7911
6834
  );
7912
6835
  });
@@ -7928,25 +6851,17 @@ var Dispatcher = class {
7928
6851
  // tasker prepare hook keys its per-task env off. Defaults to `[]` for
7929
6852
  // an older API. The conversation anchor is gone (CT319).
7930
6853
  triggerEntryPaths: turnContext.conversation.triggerEntryPaths ?? [],
7931
- ...turnContext.conversation.nativeWorkAssignment ? { nativeWorkAssignment: turnContext.conversation.nativeWorkAssignment } : {},
7932
6854
  title: turnContext.conversation.title
7933
6855
  });
7934
6856
  clearTimeout(preparingTimer);
7935
6857
  if (preparingStarted) reportPreparing("done");
7936
- writePrepared(
7937
- workspaceId,
7938
- payload.conversationId,
7939
- payload.agentId,
7940
- result,
7941
- assignmentKey
7942
- );
6858
+ writePrepared(workspaceId, payload.conversationId, payload.agentId, result);
7943
6859
  effectiveCwd = result.cwd;
7944
6860
  hookEnv = result.env;
7945
- preparedNativeAssignment = result.nativeWorkAssignment;
7946
- } catch (err2) {
6861
+ } catch (err) {
7947
6862
  clearTimeout(preparingTimer);
7948
6863
  if (preparingStarted) reportPreparing("error");
7949
- const reason = err2 instanceof Error ? err2.message : String(err2);
6864
+ const reason = err instanceof Error ? err.message : String(err);
7950
6865
  turnLog.error({ err: reason }, "dispatcher: prepare hook failed");
7951
6866
  try {
7952
6867
  await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
@@ -7969,6 +6884,19 @@ ${reason}`,
7969
6884
  }
7970
6885
  }
7971
6886
  }
6887
+ let turnEnv = hookEnv;
6888
+ if (effectiveCwd && turnContext.runtime === "codex") {
6889
+ const tmpDir = join12(effectiveCwd, "node_modules", ".cache", "cabane-tmp", turnId);
6890
+ try {
6891
+ mkdirSync9(tmpDir, { recursive: true });
6892
+ turnEnv = { ...hookEnv, TMPDIR: tmpDir };
6893
+ } catch (err) {
6894
+ turnLog.warn(
6895
+ { err: err instanceof Error ? err.message : String(err), tmpDir },
6896
+ "dispatcher: failed to create per-turn TMPDIR \u2014 proceeding with the inherited temp dir"
6897
+ );
6898
+ }
6899
+ }
7972
6900
  const key = runKey(payload.conversationId, payload.agentId);
7973
6901
  const abortController = new AbortController();
7974
6902
  this.aborts.set(key, abortController);
@@ -7983,9 +6911,9 @@ ${reason}`,
7983
6911
  // this live turn for an abandoned one and close it with a `stopped`.
7984
6912
  turnId
7985
6913
  });
7986
- } catch (err2) {
6914
+ } catch (err) {
7987
6915
  turnLog.warn(
7988
- { err: err2 instanceof Error ? err2.message : String(err2) },
6916
+ { err: err instanceof Error ? err.message : String(err) },
7989
6917
  "dispatcher: active-run flag set failed terminally; proceeding"
7990
6918
  );
7991
6919
  }
@@ -8022,35 +6950,6 @@ ${reason}`,
8022
6950
  subAgentCreate,
8023
6951
  wakeState
8024
6952
  );
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
6953
  const request = buildCompanionTurnRequest({
8055
6954
  turnContext,
8056
6955
  baseUrl: this.opts.baseUrl,
@@ -8060,11 +6959,11 @@ ${reason}`,
8060
6959
  ...turnContext.turnToken ? { turnToken: turnContext.turnToken } : {},
8061
6960
  // SJ524: the hook-resolved cwd overrides the static local cwd.
8062
6961
  ...effectiveCwd ? { cwd: effectiveCwd } : {},
8063
- ...hookEnv ? { env: hookEnv } : {},
8064
- ...preparedNativeAssignment ? { nativeWorkAssignment: preparedNativeAssignment } : {},
6962
+ // CT804: `turnEnv` = the prepare-hook env plus the per-turn checkout-local
6963
+ // TMPDIR (falls back to `hookEnv` when no cwd was resolved).
6964
+ ...turnEnv ? { env: turnEnv } : {},
8065
6965
  mcpServers: resolvedMcpServers,
8066
6966
  summonServer,
8067
- turnControl: nativeTurnControl,
8068
6967
  // CT238: this turn's conversation, forwarded as the active-conversation
8069
6968
  // header so a cross-thread post/spawn stamps its origin.
8070
6969
  activeConversationId: payload.conversationId,
@@ -8082,22 +6981,19 @@ ${reason}`,
8082
6981
  if (this.opts.codexEnabled) {
8083
6982
  adapters.push(createCodexAdapter({ enabled: true, onWarn }));
8084
6983
  }
8085
- if (this.opts.cabaneNativeApiKey) {
8086
- adapters.push(createCabaneNativeAdapter({ apiKey: this.opts.cabaneNativeApiKey, onWarn }));
8087
- }
8088
6984
  const registry = createAdapterRegistry(adapters);
8089
6985
  let adapter;
8090
6986
  try {
8091
6987
  adapter = selectAdapter(registry, turnContext.runtime);
8092
- } catch (err2) {
8093
- if (!(err2 instanceof RuntimeUnavailableError)) throw err2;
6988
+ } catch (err) {
6989
+ if (!(err instanceof RuntimeUnavailableError)) throw err;
8094
6990
  turnLog.error(
8095
- { runtime: err2.runtime, available: err2.available },
6991
+ { runtime: err.runtime, available: err.available },
8096
6992
  "dispatcher: turn runtime not available on this device"
8097
6993
  );
8098
6994
  try {
8099
6995
  await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
8100
- body: `${RUNTIME_UNAVAILABLE_PREFIX} ${err2.message}`,
6996
+ body: `${RUNTIME_UNAVAILABLE_PREFIX} ${err.message}`,
8101
6997
  kind: "final",
8102
6998
  turnId,
8103
6999
  parentMessageId: payload.messageId
@@ -8112,7 +7008,7 @@ ${reason}`,
8112
7008
  payload,
8113
7009
  turnLog,
8114
7010
  startedAt,
8115
- `runtime_unavailable:${err2.runtime}`
7011
+ `runtime_unavailable:${err.runtime}`
8116
7012
  );
8117
7013
  }
8118
7014
  if (prepareHook && hookEnv?.CABANE_TASK_ID) {
@@ -8246,9 +7142,9 @@ ${reason}`,
8246
7142
  skipState.skipped = true;
8247
7143
  skipState.reason = intent.skipReason;
8248
7144
  }
8249
- } catch (err2) {
7145
+ } catch (err) {
8250
7146
  turnLog.warn(
8251
- { err: err2 instanceof Error ? err2.message : String(err2) },
7147
+ { err: err instanceof Error ? err.message : String(err) },
8252
7148
  "dispatcher: turn-control intent fetch failed; turn-control effects for this turn are dropped"
8253
7149
  );
8254
7150
  }
@@ -8294,9 +7190,9 @@ ${reason}`,
8294
7190
  payload.agentId,
8295
7191
  { agentSessionId: event.state }
8296
7192
  );
8297
- } catch (err2) {
7193
+ } catch (err) {
8298
7194
  turnLog.warn(
8299
- { err: err2 instanceof Error ? err2.message : String(err2) },
7195
+ { err: err instanceof Error ? err.message : String(err) },
8300
7196
  "dispatcher: session-id write failed (will retry next turn)"
8301
7197
  );
8302
7198
  }
@@ -8345,18 +7241,18 @@ ${reason}`,
8345
7241
  parentMessageId: payload.messageId,
8346
7242
  ...skipWake ? { wake: skipWake } : {}
8347
7243
  });
8348
- } catch (err2) {
7244
+ } catch (err) {
8349
7245
  turnLog.warn(
8350
- { err: err2 instanceof Error ? err2.message : String(err2) },
7246
+ { err: err instanceof Error ? err.message : String(err) },
8351
7247
  "dispatcher: skipped-marker commit failed"
8352
7248
  );
8353
7249
  }
8354
7250
  } else {
8355
7251
  await committer.finalize(okResult);
8356
7252
  }
8357
- } catch (err2) {
7253
+ } catch (err) {
8358
7254
  okResult = false;
8359
- resultReason = err2 instanceof Error ? err2.message : String(err2);
7255
+ resultReason = err instanceof Error ? err.message : String(err);
8360
7256
  turnLog.error({ err: resultReason }, "dispatcher: SDK query threw");
8361
7257
  } finally {
8362
7258
  if (idleTimer) clearTimeout(idleTimer);
@@ -8392,9 +7288,9 @@ ${reason}`,
8392
7288
  // CT113: the stopped marker is still "about" the triggering message.
8393
7289
  parentMessageId: payload.messageId
8394
7290
  });
8395
- } catch (err2) {
7291
+ } catch (err) {
8396
7292
  turnLog.warn(
8397
- { err: err2 instanceof Error ? err2.message : String(err2) },
7293
+ { err: err instanceof Error ? err.message : String(err) },
8398
7294
  "dispatcher: stopped-marker commit failed"
8399
7295
  );
8400
7296
  }
@@ -8435,9 +7331,9 @@ ${reason}`,
8435
7331
  payload.agentId,
8436
7332
  body
8437
7333
  );
8438
- } catch (err2) {
7334
+ } catch (err) {
8439
7335
  turnLog.warn(
8440
- { err: err2 instanceof Error ? err2.message : String(err2) },
7336
+ { err: err instanceof Error ? err.message : String(err) },
8441
7337
  "dispatcher: active-run clear failed terminally; server age-sweep is the backstop"
8442
7338
  );
8443
7339
  }
@@ -8505,7 +7401,6 @@ function buildCompanionManifest(opts) {
8505
7401
  if (opts.claudeCode) runtimes.push({ name: "claude-code", version: v.claudeCode ?? null });
8506
7402
  if (opts.opencode) runtimes.push({ name: "opencode", version: v.opencode ?? null });
8507
7403
  if (opts.codex) runtimes.push({ name: "codex", version: v.codex ?? null });
8508
- if (opts.cabaneNative) runtimes.push({ name: "cabane-native", version: null });
8509
7404
  return { runtimes, capabilities: { ...DEVICE_MANIFEST.capabilities } };
8510
7405
  }
8511
7406
 
@@ -8743,13 +7638,13 @@ var Outbox = class {
8743
7638
  try {
8744
7639
  writeFileSync7(tmp, JSON.stringify(entry) + "\n", "utf8");
8745
7640
  renameSync3(tmp, target);
8746
- } catch (err2) {
7641
+ } catch (err) {
8747
7642
  try {
8748
7643
  rmSync5(tmp, { force: true });
8749
7644
  } catch {
8750
7645
  }
8751
7646
  this.log?.warn(
8752
- { workspaceId: this.workspaceId, err: err2 instanceof Error ? err2.message : String(err2) },
7647
+ { workspaceId: this.workspaceId, err: err instanceof Error ? err.message : String(err) },
8753
7648
  "companion outbox: failed to persist entry"
8754
7649
  );
8755
7650
  return;
@@ -8846,40 +7741,43 @@ var Outbox = class {
8846
7741
  };
8847
7742
 
8848
7743
  // 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()
7744
+ import { z as z14 } from "zod";
7745
+ var mcpStdioServerSchema = z14.object({
7746
+ type: z14.literal("stdio").optional(),
7747
+ command: z14.string().min(1),
7748
+ args: z14.array(z14.string()).optional(),
7749
+ env: z14.record(z14.string(), z14.string()).optional()
8855
7750
  });
8856
- var mcpHttpServerSchema = z16.object({
8857
- type: z16.literal("http"),
8858
- url: z16.string().url(),
8859
- headers: z16.record(z16.string(), z16.string()).optional()
7751
+ var mcpHttpServerSchema = z14.object({
7752
+ type: z14.literal("http"),
7753
+ url: z14.string().url(),
7754
+ headers: z14.record(z14.string(), z14.string()).optional()
8860
7755
  });
8861
- var mcpSseServerSchema = z16.object({
8862
- type: z16.literal("sse"),
8863
- url: z16.string().url(),
8864
- headers: z16.record(z16.string(), z16.string()).optional()
7756
+ var mcpSseServerSchema = z14.object({
7757
+ type: z14.literal("sse"),
7758
+ url: z14.string().url(),
7759
+ headers: z14.record(z14.string(), z14.string()).optional()
8865
7760
  });
8866
- var mcpServerDefSchema = z16.union([
7761
+ var mcpServerDefSchema = z14.union([
8867
7762
  mcpHttpServerSchema,
8868
7763
  mcpSseServerSchema,
8869
7764
  mcpStdioServerSchema
8870
7765
  ]);
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") })
7766
+ var thinkingConfigSchema = z14.discriminatedUnion("type", [
7767
+ z14.object({ type: z14.literal("adaptive") }),
7768
+ z14.object({ type: z14.literal("enabled"), budgetTokens: z14.number().int().positive().optional() }),
7769
+ z14.object({ type: z14.literal("disabled") })
8875
7770
  ]);
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(),
7771
+ var effortSchema = z14.enum(["low", "medium", "high", "xhigh", "max"]);
7772
+ var runConfigSchema = z14.object({
7773
+ // CT788: the host-access binary replaced the `assistant`/`coding`/`custom` mode
7774
+ // trio + its custom tool lists — `true` grants the host filesystem/shell, absent
7775
+ // is the locked surface. Kept in lockstep with `@cabane/shared`'s
7776
+ // `agentRunConfigSchema` (independent zod, forward-compatible: unknown keys are
7777
+ // stripped, so an older companion riding a newer server never rejects the config).
7778
+ hostAccess: z14.boolean().optional(),
7779
+ mcpServers: z14.record(z14.string(), mcpServerDefSchema).optional(),
7780
+ model: z14.string().min(1).optional(),
8883
7781
  thinking: thinkingConfigSchema.optional(),
8884
7782
  effort: effortSchema.optional()
8885
7783
  });
@@ -8924,20 +7822,20 @@ var SseSubscriber = class {
8924
7822
  try {
8925
7823
  await this.connect();
8926
7824
  backoff = 500;
8927
- } catch (err2) {
7825
+ } catch (err) {
8928
7826
  if (this.aborted) return;
8929
- if (err2 instanceof ApiError && (err2.status === 401 || err2.status === 403)) {
7827
+ if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
8930
7828
  this.opts.log.error(
8931
- { workspaceId: this.opts.workspaceId, status: err2.status },
7829
+ { workspaceId: this.opts.workspaceId, status: err.status },
8932
7830
  "SSE auth failed \u2014 tearing down this workspace subscriber"
8933
7831
  );
8934
- this.opts.onAuthFailure(err2.status);
7832
+ this.opts.onAuthFailure(err.status);
8935
7833
  return;
8936
7834
  }
8937
7835
  this.opts.log.warn(
8938
7836
  {
8939
7837
  workspaceId: this.opts.workspaceId,
8940
- err: err2 instanceof Error ? err2.message : String(err2),
7838
+ err: err instanceof Error ? err.message : String(err),
8941
7839
  backoff
8942
7840
  },
8943
7841
  "SSE disconnected; reconnecting"
@@ -9080,7 +7978,7 @@ var CompanionSupervisor = class {
9080
7978
  if (!this.config.deviceToken) {
9081
7979
  this.log.warn("companion: not paired (no device token) \u2014 run `cabane-companion pair`");
9082
7980
  process.stdout.write(
9083
- "companion: this device is not paired \u2014 run `cabane-companion pair` and paste the string from the cabane app.\n"
7981
+ "companion: this device is not paired \u2014 run `cabane-companion pair`, then confirm the short code in Settings \u2192 Connectors.\n"
9084
7982
  );
9085
7983
  return;
9086
7984
  }
@@ -9135,9 +8033,6 @@ var CompanionSupervisor = class {
9135
8033
  // like opencode — the CLI's presence is the operator's responsibility;
9136
8034
  // a misconfigured device fails the turn loudly, never silently).
9137
8035
  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
8036
  // CT571/CT586: each runtime's `version` from the latest harness probe
9142
8037
  // (fail-soft to null). Informational only — the server matches on name.
9143
8038
  versions: this.harnessVersions
@@ -9155,9 +8050,9 @@ var CompanionSupervisor = class {
9155
8050
  this.hub.setDevice({ deviceId: res.deviceId });
9156
8051
  this.deviceId = res.deviceId;
9157
8052
  this.checkVersionSkew(res.serverVersion);
9158
- } catch (err2) {
8053
+ } catch (err) {
9159
8054
  this.log.warn(
9160
- { err: err2 instanceof Error ? err2.message : String(err2) },
8055
+ { err: err instanceof Error ? err.message : String(err) },
9161
8056
  "companion: device heartbeat failed (will retry on next tick)"
9162
8057
  );
9163
8058
  }
@@ -9193,12 +8088,12 @@ var CompanionSupervisor = class {
9193
8088
  const resp = await this.deviceApi.getAssignments();
9194
8089
  items = resp.assignments;
9195
8090
  device = resp.device;
9196
- } catch (err2) {
8091
+ } catch (err) {
9197
8092
  this.log.error(
9198
- { err: err2 instanceof Error ? err2.message : String(err2) },
8093
+ { err: err instanceof Error ? err.message : String(err) },
9199
8094
  "companion: assignments pull failed \u2014 check the device is still active in the cabane app"
9200
8095
  );
9201
- this.hub.setDeviceError(err2 instanceof Error ? err2.message : String(err2));
8096
+ this.hub.setDeviceError(err instanceof Error ? err.message : String(err));
9202
8097
  return;
9203
8098
  }
9204
8099
  this.hub.setDevice({ deviceId: device.id, deviceLabel: device.label });
@@ -9275,7 +8170,7 @@ var CompanionSupervisor = class {
9275
8170
  agentId: it.agentId,
9276
8171
  username: it.agentUsername,
9277
8172
  displayName: it.agentDisplayName,
9278
- mode: runConfig.mode ?? "assistant",
8173
+ mode: runConfig.hostAccess ? "full" : "none",
9279
8174
  hasCredential: false,
9280
8175
  missingSecrets: missing
9281
8176
  });
@@ -9295,7 +8190,7 @@ var CompanionSupervisor = class {
9295
8190
  agentId: it.agentId,
9296
8191
  username: it.agentUsername,
9297
8192
  displayName: it.agentDisplayName,
9298
- mode: runConfig.mode ?? "assistant",
8193
+ mode: runConfig.hostAccess ? "full" : "none",
9299
8194
  hasCredential: true,
9300
8195
  missingSecrets: missing
9301
8196
  });
@@ -9379,12 +8274,9 @@ var CompanionSupervisor = class {
9379
8274
  // CT481: register the codex adapter when this device offers codex; unset
9380
8275
  // leaves an `openai/…` turn to fail loudly (no silent claude-code fallback).
9381
8276
  ...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
8277
  // CT556: per-turn timeout watchdog windows, from the companion's own env
9386
8278
  // (`AGENT_IDLE_TIMEOUT_MS` / `AGENT_TOTAL_TIMEOUT_MS`). Unset → the
9387
- // dispatcher's baked-in defaults (10 min idle / 45 min total).
8279
+ // dispatcher's baked-in defaults (10 min idle / 6h total).
9388
8280
  ...positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") !== void 0 ? { idleTimeoutMs: positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") } : {},
9389
8281
  ...positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") !== void 0 ? { totalTimeoutMs: positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") } : {},
9390
8282
  observer: this.hub.observerFor(ctx.workspaceId, ctx.agentId, ctx.workspaceSlug),
@@ -9451,9 +8343,9 @@ var CompanionSupervisor = class {
9451
8343
  let wire;
9452
8344
  try {
9453
8345
  wire = JSON.parse(ev.data);
9454
- } catch (err2) {
8346
+ } catch (err) {
9455
8347
  this.log.warn(
9456
- { err: err2 instanceof Error ? err2.message : String(err2) },
8348
+ { err: err instanceof Error ? err.message : String(err) },
9457
8349
  "malformed SSE payload"
9458
8350
  );
9459
8351
  return;
@@ -9500,13 +8392,13 @@ var CompanionSupervisor = class {
9500
8392
  }
9501
8393
  const chainKey = `${payload.conversationId}|${payload.agentId}`;
9502
8394
  const prev = wr.chains.get(chainKey) ?? Promise.resolve();
9503
- const tail = prev.then(() => this.runDispatch(wr, ev, payload, agent)).catch((err2) => {
8395
+ const tail = prev.then(() => this.runDispatch(wr, ev, payload, agent)).catch((err) => {
9504
8396
  this.log.warn(
9505
8397
  {
9506
8398
  workspaceId: wr.workspaceId,
9507
8399
  conversationId: payload.conversationId,
9508
8400
  agentId: payload.agentId,
9509
- err: err2 instanceof Error ? err2.message : String(err2)
8401
+ err: err instanceof Error ? err.message : String(err)
9510
8402
  },
9511
8403
  "companion: conversation turn handler threw"
9512
8404
  );
@@ -9598,10 +8490,10 @@ var CompanionSupervisor = class {
9598
8490
  } else {
9599
8491
  drainDelay = DRAIN_BASE_MS;
9600
8492
  }
9601
- } catch (err2) {
8493
+ } catch (err) {
9602
8494
  drainDelay = Math.min(drainDelay * 2, DRAIN_MAX_MS);
9603
8495
  this.log.warn(
9604
- { agentId, err: err2 instanceof Error ? err2.message : String(err2) },
8496
+ { agentId, err: err instanceof Error ? err.message : String(err) },
9605
8497
  "companion: outbox drain pass threw (will retry with backoff)"
9606
8498
  );
9607
8499
  } finally {
@@ -9668,9 +8560,9 @@ var CompanionSupervisor = class {
9668
8560
  codex: signals.codexVersion
9669
8561
  };
9670
8562
  this.hub.setHarnesses(deriveHarnessSnapshot(signals).harnesses);
9671
- } catch (err2) {
8563
+ } catch (err) {
9672
8564
  this.log.warn(
9673
- { err: err2 instanceof Error ? err2.message : String(err2) },
8565
+ { err: err instanceof Error ? err.message : String(err) },
9674
8566
  "companion: harness probe failed (will retry on next beat)"
9675
8567
  );
9676
8568
  }
@@ -9768,10 +8660,13 @@ var CompanionSupervisor = class {
9768
8660
  await Promise.race([
9769
8661
  Promise.allSettled(turns),
9770
8662
  new Promise((resolve) => {
9771
- timer = setTimeout(() => {
9772
- timedOut = true;
9773
- resolve();
9774
- }, Math.max(0, graceMs));
8663
+ timer = setTimeout(
8664
+ () => {
8665
+ timedOut = true;
8666
+ resolve();
8667
+ },
8668
+ Math.max(0, graceMs)
8669
+ );
9775
8670
  timer.unref?.();
9776
8671
  })
9777
8672
  ]);
@@ -9825,17 +8720,17 @@ var RECOVERABLE_CODES = /* @__PURE__ */ new Set([
9825
8720
  "ERR_STREAM_DESTROYED",
9826
8721
  "ERR_STREAM_WRITE_AFTER_END"
9827
8722
  ]);
9828
- function errorCode(err2) {
9829
- if (err2 && typeof err2 === "object" && "code" in err2) {
9830
- const code = err2.code;
8723
+ function errorCode(err) {
8724
+ if (err && typeof err === "object" && "code" in err) {
8725
+ const code = err.code;
9831
8726
  if (typeof code === "string") return code;
9832
8727
  }
9833
8728
  return void 0;
9834
8729
  }
9835
- function isRecoverableSocketError(err2) {
9836
- const code = errorCode(err2);
8730
+ function isRecoverableSocketError(err) {
8731
+ const code = errorCode(err);
9837
8732
  if (code && RECOVERABLE_CODES.has(code)) return true;
9838
- const message = err2 instanceof Error ? err2.message : String(err2);
8733
+ const message = err instanceof Error ? err.message : String(err);
9839
8734
  return /\bEPIPE\b|\bECONNRESET\b/.test(message);
9840
8735
  }
9841
8736
  function installProcessSafetyNet(log, opts = {}) {
@@ -9844,16 +8739,16 @@ function installProcessSafetyNet(log, opts = {}) {
9844
8739
  stream.on("error", () => {
9845
8740
  });
9846
8741
  }
9847
- proc.on("uncaughtException", (err2) => handleUncaught(log, err2, "uncaughtException"));
8742
+ proc.on("uncaughtException", (err) => handleUncaught(log, err, "uncaughtException"));
9848
8743
  proc.on(
9849
8744
  "unhandledRejection",
9850
8745
  (reason) => handleUncaught(log, reason, "unhandledRejection")
9851
8746
  );
9852
8747
  }
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)) {
8748
+ function handleUncaught(log, err, origin) {
8749
+ const message = err instanceof Error ? err.message : String(err);
8750
+ const code = errorCode(err);
8751
+ if (isRecoverableSocketError(err)) {
9857
8752
  log.warn(
9858
8753
  { origin, code, err: message },
9859
8754
  "companion: recovered from a socket error (kept running \u2014 a broken pipe never kills the companion)"
@@ -9861,7 +8756,7 @@ function handleUncaught(log, err2, origin) {
9861
8756
  return;
9862
8757
  }
9863
8758
  log.error(
9864
- { origin, code, err: message, stack: err2 instanceof Error ? err2.stack : void 0 },
8759
+ { origin, code, err: message, stack: err instanceof Error ? err.stack : void 0 },
9865
8760
  "companion: uncaught error (kept running \u2014 see the stack above)"
9866
8761
  );
9867
8762
  }
@@ -9898,14 +8793,14 @@ async function createCompanionRuntime(opts = {}) {
9898
8793
  cfg = requireConfig();
9899
8794
  claudeCode = await probeClaude();
9900
8795
  await ensureRuntimeAvailable(cfg, { probeClaude: async () => claudeCode });
9901
- } catch (err2) {
8796
+ } catch (err) {
9902
8797
  recordCrash({
9903
- reason: err2 instanceof Error ? err2.message : String(err2),
9904
- ...errorCode(err2) ? { code: errorCode(err2) } : {},
8798
+ reason: err instanceof Error ? err.message : String(err),
8799
+ ...errorCode(err) ? { code: errorCode(err) } : {},
9905
8800
  origin: "startup",
9906
8801
  at: (/* @__PURE__ */ new Date()).toISOString()
9907
8802
  });
9908
- throw err2;
8803
+ throw err;
9909
8804
  }
9910
8805
  if (cfg.logLevel) log.level = cfg.logLevel;
9911
8806
  const harnessVersions = await probeHarnessVersions({