@cabane/companion 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -29
- package/dist/cli.js +662 -1823
- package/dist/pairing-config.js +29 -53
- package/dist/runtime.js +490 -1585
- package/dist/static/index.html +2 -3
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/daemon.ts
|
|
7
|
-
import { spawn as
|
|
7
|
+
import { spawn as spawn4 } from "child_process";
|
|
8
8
|
import { closeSync as closeSync2, mkdirSync as mkdirSync4, openSync as openSync2 } from "fs";
|
|
9
9
|
import { fileURLToPath } from "url";
|
|
10
10
|
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
} from "fs";
|
|
21
21
|
import { homedir, userInfo } from "os";
|
|
22
22
|
import { dirname, join } from "path";
|
|
23
|
-
import { z as
|
|
23
|
+
import { z as z2 } from "zod";
|
|
24
24
|
|
|
25
25
|
// src/errors.ts
|
|
26
26
|
var CompanionError = class extends Error {
|
|
@@ -53,9 +53,6 @@ var PrepareHookError = class extends CompanionError {
|
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
// src/pairing.ts
|
|
56
|
-
import { z } from "zod";
|
|
57
|
-
var PAIRING_VERSION = 1;
|
|
58
|
-
var DEVICE_TOKEN_PREFIX = "cabdev_";
|
|
59
56
|
function isAllowedBaseUrl(raw) {
|
|
60
57
|
let url;
|
|
61
58
|
try {
|
|
@@ -70,78 +67,24 @@ function isAllowedBaseUrl(raw) {
|
|
|
70
67
|
}
|
|
71
68
|
return false;
|
|
72
69
|
}
|
|
73
|
-
var pairingSchema = z.object({
|
|
74
|
-
// Bumped if the wire shape changes incompatibly. We only accept v1.
|
|
75
|
-
v: z.literal(PAIRING_VERSION),
|
|
76
|
-
baseUrl: z.string().url().refine(isAllowedBaseUrl, {
|
|
77
|
-
message: "baseUrl must be https (loopback http is allowed for local dev only)"
|
|
78
|
-
}),
|
|
79
|
-
// The `cabdev_` device token plaintext — the companion's one durable credential.
|
|
80
|
-
deviceToken: z.string().min(1).startsWith(DEVICE_TOKEN_PREFIX, {
|
|
81
|
-
message: 'deviceToken must be a cabane device token (starts with "cabdev_")'
|
|
82
|
-
}),
|
|
83
|
-
// Optional identity hints the app may include for nicer local display. The
|
|
84
|
-
// companion also learns these from the first assignments pull, so they're not
|
|
85
|
-
// required.
|
|
86
|
-
deviceId: z.string().min(1).optional(),
|
|
87
|
-
deviceLabel: z.string().min(1).optional()
|
|
88
|
-
});
|
|
89
|
-
function decodePairing(raw) {
|
|
90
|
-
const cleaned = raw.replace(/\s+/g, "");
|
|
91
|
-
if (cleaned.length === 0) {
|
|
92
|
-
throw new CompanionError("empty pairing string. Copy it from the cabane app and try again.");
|
|
93
|
-
}
|
|
94
|
-
const bytes = Buffer.from(cleaned, "base64url");
|
|
95
|
-
const json = bytes.toString("utf8");
|
|
96
|
-
let parsed;
|
|
97
|
-
try {
|
|
98
|
-
parsed = JSON.parse(json);
|
|
99
|
-
} catch {
|
|
100
|
-
throw new CompanionError(
|
|
101
|
-
`that doesn't look like a complete pairing string \u2014 received ${cleaned.length} characters that decoded to ${bytes.length} bytes, but they weren't valid JSON. Copy the WHOLE block from the cabane app (a partial or truncated copy is the usual cause).`
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
if (parsed && typeof parsed === "object" && "v" in parsed && parsed.v !== PAIRING_VERSION) {
|
|
105
|
-
throw new CompanionError(
|
|
106
|
-
`this pairing string is version ${String(parsed.v)}, but this companion only understands version ${PAIRING_VERSION}. Update cabane-companion (\`git pull\` + rebuild) and try again.`
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
const result = pairingSchema.safeParse(parsed);
|
|
110
|
-
if (!result.success) {
|
|
111
|
-
if (result.error.issues.some((i) => i.path[0] === "baseUrl")) {
|
|
112
|
-
throw new CompanionError(
|
|
113
|
-
"this pairing string points at a non-https cabane URL. The companion runs the server's prompt with permissions bypassed and sends its credentials over the same channel, so it refuses plaintext http (except localhost for local dev). Use an https base URL."
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
throw new CompanionError(
|
|
117
|
-
`the pairing string is missing or malformed fields: ${result.error.issues.map((i) => i.path.join(".") || "(root)").join(", ")}. Re-copy it from the cabane app.`
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
return result.data;
|
|
121
|
-
}
|
|
122
70
|
|
|
123
71
|
// src/prepare-hook.ts
|
|
124
72
|
import { spawn } from "child_process";
|
|
125
|
-
import { z
|
|
126
|
-
var prepareHookSchema =
|
|
127
|
-
command:
|
|
128
|
-
args:
|
|
73
|
+
import { z } from "zod";
|
|
74
|
+
var prepareHookSchema = z.object({
|
|
75
|
+
command: z.string().min(1),
|
|
76
|
+
args: z.array(z.string()).optional(),
|
|
129
77
|
// Extra env handed to the hook process itself (merged over process.env).
|
|
130
|
-
env:
|
|
78
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
131
79
|
// Wall-clock cap for the hook. Provisioning is slow (minutes), so the
|
|
132
80
|
// default is generous; a hook that hangs past this is killed and the turn
|
|
133
81
|
// fails with a clear timeout message rather than pinning the companion.
|
|
134
|
-
timeoutMs:
|
|
82
|
+
timeoutMs: z.number().int().positive().optional()
|
|
135
83
|
}).strict();
|
|
136
84
|
var DEFAULT_TIMEOUT_MS = 10 * 6e4;
|
|
137
|
-
var prepareResultSchema =
|
|
138
|
-
cwd:
|
|
139
|
-
env:
|
|
140
|
-
nativeWorkAssignment: z2.object({
|
|
141
|
-
itemId: z2.string(),
|
|
142
|
-
executionId: z2.string(),
|
|
143
|
-
activationEpoch: z2.number().int().nonnegative()
|
|
144
|
-
}).strict().optional()
|
|
85
|
+
var prepareResultSchema = z.object({
|
|
86
|
+
cwd: z.string().min(1),
|
|
87
|
+
env: z.record(z.string(), z.string()).optional()
|
|
145
88
|
});
|
|
146
89
|
function parsePrepareOutput(stdout) {
|
|
147
90
|
const last = stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0).at(-1);
|
|
@@ -160,13 +103,12 @@ function parsePrepareOutput(stdout) {
|
|
|
160
103
|
const r = prepareResultSchema.safeParse(parsed);
|
|
161
104
|
if (!r.success) {
|
|
162
105
|
throw new PrepareHookError(
|
|
163
|
-
'prepare hook JSON must carry a non-empty string "cwd" (plus optional "env"
|
|
106
|
+
'prepare hook JSON must carry a non-empty string "cwd" (plus optional "env")'
|
|
164
107
|
);
|
|
165
108
|
}
|
|
166
109
|
return {
|
|
167
110
|
cwd: r.data.cwd,
|
|
168
|
-
...r.data.env ? { env: r.data.env } : {}
|
|
169
|
-
...r.data.nativeWorkAssignment ? { nativeWorkAssignment: r.data.nativeWorkAssignment } : {}
|
|
111
|
+
...r.data.env ? { env: r.data.env } : {}
|
|
170
112
|
};
|
|
171
113
|
}
|
|
172
114
|
return { cwd: last };
|
|
@@ -204,10 +146,10 @@ var runPrepareHook = (hook, input) => {
|
|
|
204
146
|
CABANE_CONVERSATION_TITLE: input.title ?? ""
|
|
205
147
|
}
|
|
206
148
|
});
|
|
207
|
-
} catch (
|
|
149
|
+
} catch (err) {
|
|
208
150
|
reject(
|
|
209
151
|
new PrepareHookError(
|
|
210
|
-
`prepare hook failed to start: ${
|
|
152
|
+
`prepare hook failed to start: ${err instanceof Error ? err.message : String(err)}`
|
|
211
153
|
)
|
|
212
154
|
);
|
|
213
155
|
return;
|
|
@@ -227,8 +169,8 @@ var runPrepareHook = (hook, input) => {
|
|
|
227
169
|
child.stderr?.on("data", (d) => {
|
|
228
170
|
stderr += d.toString();
|
|
229
171
|
});
|
|
230
|
-
child.on("error", (
|
|
231
|
-
finish(() => reject(new PrepareHookError(`prepare hook failed to start: ${
|
|
172
|
+
child.on("error", (err) => {
|
|
173
|
+
finish(() => reject(new PrepareHookError(`prepare hook failed to start: ${err.message}`)));
|
|
232
174
|
});
|
|
233
175
|
child.on("close", (code) => {
|
|
234
176
|
finish(() => {
|
|
@@ -241,8 +183,8 @@ var runPrepareHook = (hook, input) => {
|
|
|
241
183
|
}
|
|
242
184
|
try {
|
|
243
185
|
resolve(parsePrepareOutput(stdout));
|
|
244
|
-
} catch (
|
|
245
|
-
reject(
|
|
186
|
+
} catch (err) {
|
|
187
|
+
reject(err instanceof PrepareHookError ? err : new PrepareHookError(String(err)));
|
|
246
188
|
}
|
|
247
189
|
});
|
|
248
190
|
});
|
|
@@ -281,8 +223,8 @@ function cabaneDir() {
|
|
|
281
223
|
function configPath() {
|
|
282
224
|
return join(cabaneDir(), "config.json");
|
|
283
225
|
}
|
|
284
|
-
var localAgentConfigSchema =
|
|
285
|
-
cwd:
|
|
226
|
+
var localAgentConfigSchema = z2.object({
|
|
227
|
+
cwd: z2.string().optional(),
|
|
286
228
|
prepareHook: prepareHookSchema.optional(),
|
|
287
229
|
// CT289: the Claude Code auto-memory escape hatch. Auto-memory is forced OFF
|
|
288
230
|
// by default on every companion (memory belongs in the Cabane workspace, and a
|
|
@@ -291,13 +233,13 @@ var localAgentConfigSchema = z3.object({
|
|
|
291
233
|
// auto-memory back to your own `~/.claude/settings.json` — Cabane then stops
|
|
292
234
|
// injecting the off switch and your normal Claude Code memory workflow applies
|
|
293
235
|
// (in coding mode, where the checkout's project settings are read).
|
|
294
|
-
claudeCode:
|
|
236
|
+
claudeCode: z2.object({ autoMemory: z2.boolean().optional() }).strict().optional()
|
|
295
237
|
}).strict();
|
|
296
|
-
var companionConfigSchema =
|
|
238
|
+
var companionConfigSchema = z2.object({
|
|
297
239
|
// The cabane instance this device is paired with. SJ515: https-enforced
|
|
298
240
|
// (loopback exempt) so a hand-edited config can't smuggle a plaintext-http
|
|
299
241
|
// base URL onto the MITM-able channel the device token + prompt ride.
|
|
300
|
-
baseUrl:
|
|
242
|
+
baseUrl: z2.string().url().refine(isAllowedBaseUrl, {
|
|
301
243
|
message: "baseUrl must be https (loopback http is allowed for local dev only)"
|
|
302
244
|
}),
|
|
303
245
|
// The `cabdev_` device token plaintext — the companion's one durable credential,
|
|
@@ -305,22 +247,22 @@ var companionConfigSchema = z3.object({
|
|
|
305
247
|
// heartbeat endpoints. Optional so `cabane-companion logout` can strip it (a
|
|
306
248
|
// "paired but logged out" state the supervisor refuses to run) while keeping
|
|
307
249
|
// the rest of the config; `pair` always writes one.
|
|
308
|
-
deviceToken:
|
|
309
|
-
// Identity hints, learned from the
|
|
250
|
+
deviceToken: z2.string().optional(),
|
|
251
|
+
// Identity hints, learned from the device flow and refreshed on the first
|
|
310
252
|
// assignments pull. Cosmetic — used for `status`/dashboard display only.
|
|
311
|
-
deviceId:
|
|
312
|
-
deviceLabel:
|
|
253
|
+
deviceId: z2.string().optional(),
|
|
254
|
+
deviceLabel: z2.string().optional(),
|
|
313
255
|
// Optional per-agent machine-local overrides (cwd / prepareHook), keyed by
|
|
314
256
|
// agentId / username / `slug/username`. Hand-added by the operator; the companion
|
|
315
257
|
// never writes this (it only persists credentials + the device, elsewhere).
|
|
316
|
-
agents:
|
|
258
|
+
agents: z2.record(z2.string(), localAgentConfigSchema).optional(),
|
|
317
259
|
// Dashboard settings (all optional). dashboardPort: preferred bind port (next
|
|
318
260
|
// free one if taken); autoOpen: whether `start` opens the browser (the
|
|
319
261
|
// `--no-open` flag / `COMPANION_NO_OPEN=1` override per-run); logLevel: pino
|
|
320
262
|
// level, live-editable from the dashboard settings panel.
|
|
321
|
-
dashboardPort:
|
|
322
|
-
autoOpen:
|
|
323
|
-
logLevel:
|
|
263
|
+
dashboardPort: z2.number().int().min(1).max(65535).optional(),
|
|
264
|
+
autoOpen: z2.boolean().optional(),
|
|
265
|
+
logLevel: z2.enum(["warn", "info", "debug"]).optional(),
|
|
324
266
|
// CT270: the opencode runtime, when the operator runs one on this machine. The
|
|
325
267
|
// operator installs opencode, starts `opencode serve` (auth via opencode's own
|
|
326
268
|
// `/connect` — Cabane never sees provider keys), and points the companion at it
|
|
@@ -328,8 +270,8 @@ var companionConfigSchema = z3.object({
|
|
|
328
270
|
// heartbeat manifest (so the server offers DeepSeek/opencode models here and
|
|
329
271
|
// routes those turns to this device) AND registers the opencode adapter in the
|
|
330
272
|
// dispatcher. Absent → the device is claude-code-only, exactly as before.
|
|
331
|
-
opencode:
|
|
332
|
-
serverUrl:
|
|
273
|
+
opencode: z2.object({
|
|
274
|
+
serverUrl: z2.string().url()
|
|
333
275
|
}).strict().optional(),
|
|
334
276
|
// CT481: the codex runtime, when the operator runs Codex on this machine. Unlike
|
|
335
277
|
// opencode (a long-lived `opencode serve` addressed by URL), Codex is a local CLI
|
|
@@ -340,20 +282,13 @@ var companionConfigSchema = z3.object({
|
|
|
340
282
|
// keeps the block but turns it off). Enabling makes the device advertise the
|
|
341
283
|
// `codex` runtime on its heartbeat manifest AND registers the codex adapter in
|
|
342
284
|
// the dispatcher. Absent → the device doesn't offer codex, exactly as before.
|
|
343
|
-
codex:
|
|
344
|
-
enabled:
|
|
285
|
+
codex: z2.object({
|
|
286
|
+
enabled: z2.boolean().optional()
|
|
345
287
|
}).strict().optional()
|
|
346
288
|
});
|
|
347
289
|
function isCodexEnabled(cfg) {
|
|
348
290
|
return !!cfg.codex && cfg.codex.enabled !== false;
|
|
349
291
|
}
|
|
350
|
-
function cabaneNativeApiKey() {
|
|
351
|
-
const key = process.env.OPENROUTER_API_KEY?.trim();
|
|
352
|
-
return key ? key : void 0;
|
|
353
|
-
}
|
|
354
|
-
function isCabaneNativeEnabled() {
|
|
355
|
-
return cabaneNativeApiKey() !== void 0;
|
|
356
|
-
}
|
|
357
292
|
function localAgentConfig(cfg, agent) {
|
|
358
293
|
const map = cfg.agents ?? {};
|
|
359
294
|
return map[agent.agentId] ?? map[agent.agentUsername] ?? map[`${agent.workspaceSlug}/${agent.agentUsername}`] ?? {};
|
|
@@ -364,18 +299,18 @@ function loadConfig() {
|
|
|
364
299
|
let raw;
|
|
365
300
|
try {
|
|
366
301
|
raw = readFileSync(path3, "utf8");
|
|
367
|
-
} catch (
|
|
302
|
+
} catch (err) {
|
|
368
303
|
throw new ConfigError(
|
|
369
|
-
`couldn't read ${path3}: ${
|
|
304
|
+
`couldn't read ${path3}: ${err instanceof Error ? err.message : String(err)}`
|
|
370
305
|
);
|
|
371
306
|
}
|
|
372
307
|
if (raw.trim().length === 0) return null;
|
|
373
308
|
let parsed;
|
|
374
309
|
try {
|
|
375
310
|
parsed = JSON.parse(raw);
|
|
376
|
-
} catch (
|
|
311
|
+
} catch (err) {
|
|
377
312
|
throw new ConfigError(
|
|
378
|
-
`${path3} is not valid JSON: ${
|
|
313
|
+
`${path3} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
379
314
|
);
|
|
380
315
|
}
|
|
381
316
|
const result = companionConfigSchema.safeParse(parsed);
|
|
@@ -424,7 +359,7 @@ function loadConfigTolerant() {
|
|
|
424
359
|
}
|
|
425
360
|
const obj = parsed && typeof parsed === "object" ? parsed : {};
|
|
426
361
|
const local = {};
|
|
427
|
-
const agents =
|
|
362
|
+
const agents = z2.record(z2.string(), localAgentConfigSchema).safeParse(obj.agents);
|
|
428
363
|
if (agents.success) local.agents = agents.data;
|
|
429
364
|
if (typeof obj.dashboardPort === "number") local.dashboardPort = obj.dashboardPort;
|
|
430
365
|
if (typeof obj.autoOpen === "boolean") local.autoOpen = obj.autoOpen;
|
|
@@ -451,12 +386,12 @@ function saveConfig(cfg) {
|
|
|
451
386
|
} catch {
|
|
452
387
|
}
|
|
453
388
|
renameSync(tmp, path3);
|
|
454
|
-
} catch (
|
|
389
|
+
} catch (err) {
|
|
455
390
|
try {
|
|
456
391
|
rmSync(tmp, { force: true });
|
|
457
392
|
} catch {
|
|
458
393
|
}
|
|
459
|
-
throw
|
|
394
|
+
throw err;
|
|
460
395
|
}
|
|
461
396
|
}
|
|
462
397
|
function requireConfig() {
|
|
@@ -522,11 +457,94 @@ function getLogger() {
|
|
|
522
457
|
}
|
|
523
458
|
|
|
524
459
|
// src/prereqs.ts
|
|
460
|
+
import { spawn as spawn3 } from "child_process";
|
|
461
|
+
|
|
462
|
+
// src/harness-versions.ts
|
|
525
463
|
import { spawn as spawn2 } from "child_process";
|
|
464
|
+
var EMPTY = { claudeCode: null, opencode: null, codex: null };
|
|
465
|
+
function parseVersionToken(raw) {
|
|
466
|
+
if (!raw) return null;
|
|
467
|
+
const m = raw.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
|
|
468
|
+
return m ? m[0] : null;
|
|
469
|
+
}
|
|
470
|
+
async function probeCliVersion(command, spawnImpl = spawn2) {
|
|
471
|
+
return new Promise((resolve) => {
|
|
472
|
+
let settled = false;
|
|
473
|
+
const done = (v) => {
|
|
474
|
+
if (!settled) {
|
|
475
|
+
settled = true;
|
|
476
|
+
resolve(v);
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
let child;
|
|
480
|
+
try {
|
|
481
|
+
child = spawnImpl(command, ["--version"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
482
|
+
} catch {
|
|
483
|
+
done(null);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
let out = "";
|
|
487
|
+
child.stdout?.on("data", (chunk) => {
|
|
488
|
+
if (out.length < 4096) out += chunk.toString();
|
|
489
|
+
});
|
|
490
|
+
child.once("error", () => done(null));
|
|
491
|
+
child.once("exit", (code) => done(code === 0 ? parseVersionToken(out) : null));
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
async function probeOpencodeVersion(serverUrl, fetchImpl = fetch) {
|
|
495
|
+
try {
|
|
496
|
+
const base = serverUrl.endsWith("/") ? serverUrl.slice(0, -1) : serverUrl;
|
|
497
|
+
const res = await fetchImpl(`${base}/global/health`, {
|
|
498
|
+
headers: { accept: "application/json" }
|
|
499
|
+
});
|
|
500
|
+
if (!res.ok) return null;
|
|
501
|
+
return extractOpencodeVersion(await res.json());
|
|
502
|
+
} catch {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function extractOpencodeVersion(json) {
|
|
507
|
+
if (!json || typeof json !== "object") return null;
|
|
508
|
+
const obj = json;
|
|
509
|
+
const candidates = [obj.version];
|
|
510
|
+
for (const v of Object.values(obj)) {
|
|
511
|
+
if (v && typeof v === "object") candidates.push(v.version);
|
|
512
|
+
}
|
|
513
|
+
for (const c of candidates) {
|
|
514
|
+
if (typeof c === "string") {
|
|
515
|
+
const parsed = parseVersionToken(c);
|
|
516
|
+
if (parsed) return parsed;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
async function probeHarnessVersions(opts, deps = {}) {
|
|
522
|
+
const probeClaudeCode = deps.probeClaudeCode ?? (() => probeCliVersion("claude"));
|
|
523
|
+
const probeCodex = deps.probeCodex ?? (() => probeCliVersion("codex"));
|
|
524
|
+
const probeOpencode = deps.probeOpencode ?? ((url) => probeOpencodeVersion(url));
|
|
525
|
+
const [claudeCode, codex, opencode] = await Promise.all([
|
|
526
|
+
opts.claudeCode ? safe(probeClaudeCode) : Promise.resolve(null),
|
|
527
|
+
opts.codex ? safe(probeCodex) : Promise.resolve(null),
|
|
528
|
+
opts.opencodeServerUrl ? safe(() => probeOpencode(opts.opencodeServerUrl)) : Promise.resolve(null)
|
|
529
|
+
]);
|
|
530
|
+
return { claudeCode, opencode, codex };
|
|
531
|
+
}
|
|
532
|
+
function emptyHarnessVersions() {
|
|
533
|
+
return { ...EMPTY };
|
|
534
|
+
}
|
|
535
|
+
async function safe(fn) {
|
|
536
|
+
try {
|
|
537
|
+
return await fn();
|
|
538
|
+
} catch {
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/prereqs.ts
|
|
526
544
|
async function claudeOnPath() {
|
|
527
545
|
return new Promise((resolve) => {
|
|
528
546
|
let settled = false;
|
|
529
|
-
const child =
|
|
547
|
+
const child = spawn3("claude", ["--version"], { stdio: "ignore" });
|
|
530
548
|
child.once("error", () => {
|
|
531
549
|
if (!settled) {
|
|
532
550
|
settled = true;
|
|
@@ -541,8 +559,20 @@ async function claudeOnPath() {
|
|
|
541
559
|
});
|
|
542
560
|
});
|
|
543
561
|
}
|
|
562
|
+
var CODEX_PROBE_TIMEOUT_MS = 4e3;
|
|
563
|
+
async function codexOnPath() {
|
|
564
|
+
const version = await Promise.race([
|
|
565
|
+
probeCliVersion("codex"),
|
|
566
|
+
new Promise((resolve) => {
|
|
567
|
+
const timer = setTimeout(() => resolve(null), CODEX_PROBE_TIMEOUT_MS);
|
|
568
|
+
timer.unref?.();
|
|
569
|
+
})
|
|
570
|
+
]);
|
|
571
|
+
return version !== null;
|
|
572
|
+
}
|
|
544
573
|
async function ensureRuntimeAvailable(cfg, deps = {}) {
|
|
545
574
|
const probeClaude = deps.probeClaude ?? claudeOnPath;
|
|
575
|
+
const probeCodex = deps.probeCodex ?? codexOnPath;
|
|
546
576
|
const warn = deps.warn ?? ((message) => process.stderr.write(`${message}
|
|
547
577
|
`));
|
|
548
578
|
if (await probeClaude()) return;
|
|
@@ -551,15 +581,15 @@ async function ensureRuntimeAvailable(cfg, deps = {}) {
|
|
|
551
581
|
...isCodexEnabled(cfg) ? ["codex"] : []
|
|
552
582
|
];
|
|
553
583
|
if (alternates.length > 0) {
|
|
584
|
+
const exposed = alternates.join(" + ");
|
|
554
585
|
warn(
|
|
555
|
-
`warning: Claude Code isn\u2019t on your PATH, so this device
|
|
556
|
-
" + "
|
|
557
|
-
)} only \u2014 it advertises just those runtimes, so assign it matching models (a Claude-model agent won\u2019t be routed here). Install Claude Code (\`npm i -g @anthropic-ai/claude-code\`) if you want it to run Claude models too.`
|
|
586
|
+
`warning: Claude Code isn\u2019t on your PATH, so this device exposes ${exposed} only \u2014 it advertises nothing else, so a Claude-model agent won\u2019t be routed here. Assign it agents on models ${exposed} can run, or install Claude Code (\`npm i -g @anthropic-ai/claude-code\`) and log in if you want this device to run Claude models too.`
|
|
558
587
|
);
|
|
559
588
|
return;
|
|
560
589
|
}
|
|
590
|
+
const installedButUnexposed = await probeCodex() ? 'Codex is installed on this machine but not exposed \u2014 enable it with `{ "codex": { "enabled": true } }` in ~/.cabane/config.json. Otherwise, expose a harness:\n' : "Expose at least one:\n";
|
|
561
591
|
throw new CompanionError(
|
|
562
|
-
"
|
|
592
|
+
"This device exposes no harness, so no agent turn can run here. " + installedButUnexposed + ' \u2022 Claude Code \u2014 install it (`npm i -g @anthropic-ai/claude-code`) and log in (`claude`, then follow the prompts); it\u2019s exposed automatically once `claude` is on your PATH.\n \u2022 Codex \u2014 install the Codex CLI and log in (`codex login`), then add `{ "codex": { "enabled": true } }` to ~/.cabane/config.json.\n \u2022 opencode \u2014 run `opencode serve --port 4096`, then add `{ "opencode": { "serverUrl": "http://127.0.0.1:4096" } }` to ~/.cabane/config.json.\nThen run `cabane-companion start` again (see the companion README).'
|
|
563
593
|
);
|
|
564
594
|
}
|
|
565
595
|
|
|
@@ -631,8 +661,8 @@ async function verifyRuntime(state, fetchImpl = fetch) {
|
|
|
631
661
|
let res;
|
|
632
662
|
try {
|
|
633
663
|
res = await fetchImpl(url, { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
|
|
634
|
-
} catch (
|
|
635
|
-
return isConnRefused(
|
|
664
|
+
} catch (err) {
|
|
665
|
+
return isConnRefused(err) ? "stale" : "unknown";
|
|
636
666
|
}
|
|
637
667
|
if (!res.ok) return "unknown";
|
|
638
668
|
let body;
|
|
@@ -644,9 +674,9 @@ async function verifyRuntime(state, fetchImpl = fetch) {
|
|
|
644
674
|
if (typeof body.instance_id !== "string") return "unknown";
|
|
645
675
|
return body.instance_id === state.instanceId ? "ours" : "stale";
|
|
646
676
|
}
|
|
647
|
-
function isConnRefused(
|
|
648
|
-
if (!
|
|
649
|
-
const cause =
|
|
677
|
+
function isConnRefused(err) {
|
|
678
|
+
if (!err || typeof err !== "object") return false;
|
|
679
|
+
const cause = err.cause;
|
|
650
680
|
return !!cause && typeof cause === "object" && cause.code === "ECONNREFUSED";
|
|
651
681
|
}
|
|
652
682
|
function trimSlash(s) {
|
|
@@ -713,7 +743,7 @@ function defaultSpawnDetached(args) {
|
|
|
713
743
|
mkdirSync4(cabaneDir(), { recursive: true });
|
|
714
744
|
const logFd = openSync2(companionLogPath(), "a");
|
|
715
745
|
try {
|
|
716
|
-
return
|
|
746
|
+
return spawn4(process.execPath, [cliPath, ...args], {
|
|
717
747
|
detached: true,
|
|
718
748
|
stdio: ["ignore", logFd, logFd],
|
|
719
749
|
env: { ...process.env, CABANE_COMPANION_DAEMON: "1" }
|
|
@@ -737,11 +767,11 @@ import {
|
|
|
737
767
|
writeFileSync as writeFileSync3
|
|
738
768
|
} from "fs";
|
|
739
769
|
import { dirname as dirname3, join as join4 } from "path";
|
|
740
|
-
import { z as
|
|
770
|
+
import { z as z3 } from "zod";
|
|
741
771
|
function credentialsPath() {
|
|
742
772
|
return join4(cabaneDir(), "credentials.json");
|
|
743
773
|
}
|
|
744
|
-
var credentialStoreSchema =
|
|
774
|
+
var credentialStoreSchema = z3.record(z3.string(), z3.string());
|
|
745
775
|
function load() {
|
|
746
776
|
const path3 = credentialsPath();
|
|
747
777
|
if (!existsSync3(path3)) return {};
|
|
@@ -774,12 +804,12 @@ function save(map) {
|
|
|
774
804
|
} catch {
|
|
775
805
|
}
|
|
776
806
|
renameSync2(tmp, path3);
|
|
777
|
-
} catch (
|
|
807
|
+
} catch (err) {
|
|
778
808
|
try {
|
|
779
809
|
rmSync3(tmp, { force: true });
|
|
780
810
|
} catch {
|
|
781
811
|
}
|
|
782
|
-
throw
|
|
812
|
+
throw err;
|
|
783
813
|
}
|
|
784
814
|
}
|
|
785
815
|
function getCredential(agentId) {
|
|
@@ -818,13 +848,13 @@ async function logout(opts = {}) {
|
|
|
818
848
|
}
|
|
819
849
|
if (!opts.yes) {
|
|
820
850
|
const message = opts.purge ? "Purge the entire local companion config (device + agent overrides + settings)?" : "Log out this device (removes the device token + cached agent credentials; keeps your config)?";
|
|
821
|
-
const
|
|
822
|
-
if (!
|
|
851
|
+
const ok = await confirm({ message, default: false });
|
|
852
|
+
if (!ok) {
|
|
823
853
|
process.stdout.write("cancelled\n");
|
|
824
854
|
return;
|
|
825
855
|
}
|
|
826
856
|
}
|
|
827
|
-
const serverNote = `the device remains registered server-side \u2014 remove it in the cabane app (Settings \u2192
|
|
857
|
+
const serverNote = `the device remains registered server-side \u2014 remove it in the cabane app (Settings \u2192 Connectors) if you want it gone there too.`;
|
|
828
858
|
if (opts.purge) {
|
|
829
859
|
deleteConfig();
|
|
830
860
|
clearCredentials();
|
|
@@ -841,10 +871,6 @@ async function logout(opts = {}) {
|
|
|
841
871
|
);
|
|
842
872
|
}
|
|
843
873
|
|
|
844
|
-
// src/commands/pair.ts
|
|
845
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
846
|
-
import { password } from "@inquirer/prompts";
|
|
847
|
-
|
|
848
874
|
// src/enrollment.ts
|
|
849
875
|
function trimBase(baseUrl) {
|
|
850
876
|
return baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
@@ -857,9 +883,9 @@ async function postJson(baseUrl, path3, body) {
|
|
|
857
883
|
headers: { "content-type": "application/json", accept: "application/json" },
|
|
858
884
|
body: JSON.stringify(body)
|
|
859
885
|
});
|
|
860
|
-
} catch (
|
|
886
|
+
} catch (err) {
|
|
861
887
|
throw new CompanionError(
|
|
862
|
-
`couldn't reach cabane at ${baseUrl}: ${
|
|
888
|
+
`couldn't reach cabane at ${baseUrl}: ${err instanceof Error ? err.message : String(err)}. Check the server URL (pass --server <url>) and your connection.`
|
|
863
889
|
);
|
|
864
890
|
}
|
|
865
891
|
const raw = await res.text();
|
|
@@ -874,7 +900,7 @@ async function postJson(baseUrl, path3, body) {
|
|
|
874
900
|
if (res.status >= 400) {
|
|
875
901
|
if (res.status === 404 && path3.endsWith("/code")) {
|
|
876
902
|
throw new CompanionError(
|
|
877
|
-
`this cabane server (${baseUrl}) doesn't support device-flow pairing yet. Update the server
|
|
903
|
+
`this cabane server (${baseUrl}) doesn't support device-flow pairing yet. Update the server to a version that supports \`cabane-companion pair\`.`
|
|
878
904
|
);
|
|
879
905
|
}
|
|
880
906
|
const msg = parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : `${res.status}`;
|
|
@@ -957,34 +983,6 @@ function writePairedConfig(paired) {
|
|
|
957
983
|
}
|
|
958
984
|
|
|
959
985
|
// src/commands/pair.ts
|
|
960
|
-
async function readStdin() {
|
|
961
|
-
const chunks = [];
|
|
962
|
-
for await (const chunk of process.stdin) {
|
|
963
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
964
|
-
}
|
|
965
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
966
|
-
}
|
|
967
|
-
async function resolvePairingString(opts) {
|
|
968
|
-
if (opts.file !== void 0) {
|
|
969
|
-
try {
|
|
970
|
-
return readFileSync4(opts.file, "utf8");
|
|
971
|
-
} catch (err2) {
|
|
972
|
-
throw new CompanionError(
|
|
973
|
-
`couldn't read the pairing string from ${opts.file}: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
974
|
-
);
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
if (opts.arg !== void 0) {
|
|
978
|
-
process.stderr.write(
|
|
979
|
-
"warning: passing the pairing string as an argument leaves it in your shell history and is readable by other processes while `pair` runs. Prefer `cabane-companion pair --legacy` (you'll be prompted to paste it) or `cabane-companion pair --legacy --file <path>`.\n"
|
|
980
|
-
);
|
|
981
|
-
return opts.arg;
|
|
982
|
-
}
|
|
983
|
-
if (!process.stdin.isTTY) {
|
|
984
|
-
return await readStdin();
|
|
985
|
-
}
|
|
986
|
-
return await password({ message: "Paste the pairing string from cabane:" });
|
|
987
|
-
}
|
|
988
986
|
function writePairedConfigCli(paired) {
|
|
989
987
|
const { note } = writePairedConfig(paired);
|
|
990
988
|
if (note) process.stderr.write(`note: ${note}
|
|
@@ -996,32 +994,35 @@ Run \`cabane-companion start\` \u2014 it will pull the agents assigned to this d
|
|
|
996
994
|
`
|
|
997
995
|
);
|
|
998
996
|
}
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
baseUrl: pairing.baseUrl,
|
|
1006
|
-
deviceToken: pairing.deviceToken,
|
|
1007
|
-
...pairing.deviceId ? { deviceId: pairing.deviceId } : {},
|
|
1008
|
-
...pairing.deviceLabel ? { deviceLabel: pairing.deviceLabel } : {}
|
|
1009
|
-
});
|
|
1010
|
-
return;
|
|
997
|
+
function writeCompletedPairing(raw) {
|
|
998
|
+
let paired;
|
|
999
|
+
try {
|
|
1000
|
+
paired = JSON.parse(raw);
|
|
1001
|
+
} catch {
|
|
1002
|
+
throw new Error("invalid completed pairing payload: expected JSON on stdin.");
|
|
1011
1003
|
}
|
|
1004
|
+
if (!paired || typeof paired !== "object" || typeof paired.baseUrl !== "string" || typeof paired.deviceToken !== "string") {
|
|
1005
|
+
throw new Error("invalid completed pairing payload: baseUrl and deviceToken are required.");
|
|
1006
|
+
}
|
|
1007
|
+
writePairedConfigCli(paired);
|
|
1008
|
+
}
|
|
1009
|
+
async function pair(opts = {}) {
|
|
1012
1010
|
const baseUrl = resolvePairBaseUrl(opts.server);
|
|
1013
1011
|
const paired = await runDeviceFlow(baseUrl, (line) => process.stdout.write(`${line}
|
|
1014
1012
|
`));
|
|
1015
1013
|
writePairedConfigCli(paired);
|
|
1016
1014
|
}
|
|
1017
1015
|
|
|
1016
|
+
// src/cli.ts
|
|
1017
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
1018
|
+
|
|
1018
1019
|
// src/browser.ts
|
|
1019
|
-
import { spawn as
|
|
1020
|
+
import { spawn as spawn5 } from "child_process";
|
|
1020
1021
|
import { platform } from "process";
|
|
1021
1022
|
function openBrowser(url) {
|
|
1022
1023
|
try {
|
|
1023
1024
|
const { command, args } = openerFor(url);
|
|
1024
|
-
const child =
|
|
1025
|
+
const child = spawn5(command, args, { stdio: "ignore", detached: true });
|
|
1025
1026
|
child.on("error", () => {
|
|
1026
1027
|
});
|
|
1027
1028
|
child.unref();
|
|
@@ -1463,15 +1464,15 @@ var DEFAULT_PORT = 7474;
|
|
|
1463
1464
|
var PORT_FALLBACK_SPAN = 10;
|
|
1464
1465
|
function buildDashboardApp(deps) {
|
|
1465
1466
|
const app = new Hono();
|
|
1466
|
-
app.onError((
|
|
1467
|
-
if (
|
|
1468
|
-
const status2 =
|
|
1469
|
-
return c.json({ error:
|
|
1467
|
+
app.onError((err, c) => {
|
|
1468
|
+
if (err instanceof ApiError) {
|
|
1469
|
+
const status2 = err.status >= 400 && err.status < 600 ? err.status : 502;
|
|
1470
|
+
return c.json({ error: err.message }, status2);
|
|
1470
1471
|
}
|
|
1471
|
-
if (
|
|
1472
|
-
return c.json({ error:
|
|
1472
|
+
if (err instanceof CompanionError) {
|
|
1473
|
+
return c.json({ error: err.message }, 400);
|
|
1473
1474
|
}
|
|
1474
|
-
return c.json({ error:
|
|
1475
|
+
return c.json({ error: err instanceof Error ? err.message : "internal error" }, 500);
|
|
1475
1476
|
});
|
|
1476
1477
|
registerRoutes(app, { ...deps, staticDir: resolveStaticDir() });
|
|
1477
1478
|
return app;
|
|
@@ -1492,12 +1493,12 @@ async function startDashboard(opts) {
|
|
|
1492
1493
|
server.closeAllConnections?.();
|
|
1493
1494
|
})
|
|
1494
1495
|
};
|
|
1495
|
-
} catch (
|
|
1496
|
-
if (isAddrInUse(
|
|
1497
|
-
lastErr =
|
|
1496
|
+
} catch (err) {
|
|
1497
|
+
if (isAddrInUse(err)) {
|
|
1498
|
+
lastErr = err;
|
|
1498
1499
|
continue;
|
|
1499
1500
|
}
|
|
1500
|
-
throw
|
|
1501
|
+
throw err;
|
|
1501
1502
|
}
|
|
1502
1503
|
}
|
|
1503
1504
|
throw new CompanionError(
|
|
@@ -1513,102 +1514,21 @@ function listen(app, port) {
|
|
|
1513
1514
|
resolve(server);
|
|
1514
1515
|
}
|
|
1515
1516
|
});
|
|
1516
|
-
server.on("error", (
|
|
1517
|
+
server.on("error", (err) => {
|
|
1517
1518
|
if (!settled) {
|
|
1518
1519
|
settled = true;
|
|
1519
|
-
reject(
|
|
1520
|
+
reject(err);
|
|
1520
1521
|
}
|
|
1521
1522
|
});
|
|
1522
1523
|
});
|
|
1523
1524
|
}
|
|
1524
|
-
function isAddrInUse(
|
|
1525
|
-
return Boolean(
|
|
1525
|
+
function isAddrInUse(err) {
|
|
1526
|
+
return Boolean(err && typeof err === "object" && "code" in err && err.code === "EADDRINUSE");
|
|
1526
1527
|
}
|
|
1527
1528
|
function resolveStaticDir() {
|
|
1528
1529
|
return join6(dirname4(fileURLToPath2(import.meta.url)), "static");
|
|
1529
1530
|
}
|
|
1530
1531
|
|
|
1531
|
-
// src/harness-versions.ts
|
|
1532
|
-
import { spawn as spawn5 } from "child_process";
|
|
1533
|
-
var EMPTY = { claudeCode: null, opencode: null, codex: null };
|
|
1534
|
-
function parseVersionToken(raw) {
|
|
1535
|
-
if (!raw) return null;
|
|
1536
|
-
const m = raw.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
|
|
1537
|
-
return m ? m[0] : null;
|
|
1538
|
-
}
|
|
1539
|
-
async function probeCliVersion(command, spawnImpl = spawn5) {
|
|
1540
|
-
return new Promise((resolve) => {
|
|
1541
|
-
let settled = false;
|
|
1542
|
-
const done = (v) => {
|
|
1543
|
-
if (!settled) {
|
|
1544
|
-
settled = true;
|
|
1545
|
-
resolve(v);
|
|
1546
|
-
}
|
|
1547
|
-
};
|
|
1548
|
-
let child;
|
|
1549
|
-
try {
|
|
1550
|
-
child = spawnImpl(command, ["--version"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
1551
|
-
} catch {
|
|
1552
|
-
done(null);
|
|
1553
|
-
return;
|
|
1554
|
-
}
|
|
1555
|
-
let out = "";
|
|
1556
|
-
child.stdout?.on("data", (chunk) => {
|
|
1557
|
-
if (out.length < 4096) out += chunk.toString();
|
|
1558
|
-
});
|
|
1559
|
-
child.once("error", () => done(null));
|
|
1560
|
-
child.once("exit", (code) => done(code === 0 ? parseVersionToken(out) : null));
|
|
1561
|
-
});
|
|
1562
|
-
}
|
|
1563
|
-
async function probeOpencodeVersion(serverUrl, fetchImpl = fetch) {
|
|
1564
|
-
try {
|
|
1565
|
-
const base = serverUrl.endsWith("/") ? serverUrl.slice(0, -1) : serverUrl;
|
|
1566
|
-
const res = await fetchImpl(`${base}/global/health`, {
|
|
1567
|
-
headers: { accept: "application/json" }
|
|
1568
|
-
});
|
|
1569
|
-
if (!res.ok) return null;
|
|
1570
|
-
return extractOpencodeVersion(await res.json());
|
|
1571
|
-
} catch {
|
|
1572
|
-
return null;
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
function extractOpencodeVersion(json) {
|
|
1576
|
-
if (!json || typeof json !== "object") return null;
|
|
1577
|
-
const obj = json;
|
|
1578
|
-
const candidates = [obj.version];
|
|
1579
|
-
for (const v of Object.values(obj)) {
|
|
1580
|
-
if (v && typeof v === "object") candidates.push(v.version);
|
|
1581
|
-
}
|
|
1582
|
-
for (const c of candidates) {
|
|
1583
|
-
if (typeof c === "string") {
|
|
1584
|
-
const parsed = parseVersionToken(c);
|
|
1585
|
-
if (parsed) return parsed;
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1588
|
-
return null;
|
|
1589
|
-
}
|
|
1590
|
-
async function probeHarnessVersions(opts, deps = {}) {
|
|
1591
|
-
const probeClaudeCode = deps.probeClaudeCode ?? (() => probeCliVersion("claude"));
|
|
1592
|
-
const probeCodex = deps.probeCodex ?? (() => probeCliVersion("codex"));
|
|
1593
|
-
const probeOpencode = deps.probeOpencode ?? ((url) => probeOpencodeVersion(url));
|
|
1594
|
-
const [claudeCode, codex, opencode] = await Promise.all([
|
|
1595
|
-
opts.claudeCode ? safe(probeClaudeCode) : Promise.resolve(null),
|
|
1596
|
-
opts.codex ? safe(probeCodex) : Promise.resolve(null),
|
|
1597
|
-
opts.opencodeServerUrl ? safe(() => probeOpencode(opts.opencodeServerUrl)) : Promise.resolve(null)
|
|
1598
|
-
]);
|
|
1599
|
-
return { claudeCode, opencode, codex };
|
|
1600
|
-
}
|
|
1601
|
-
function emptyHarnessVersions() {
|
|
1602
|
-
return { ...EMPTY };
|
|
1603
|
-
}
|
|
1604
|
-
async function safe(fn) {
|
|
1605
|
-
try {
|
|
1606
|
-
return await fn();
|
|
1607
|
-
} catch {
|
|
1608
|
-
return null;
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
|
|
1612
1532
|
// src/api.ts
|
|
1613
1533
|
var RETRY_BACKOFF_MS = [250, 750];
|
|
1614
1534
|
var ACTIVE_RUN_OUTBOX_SEQ = 0;
|
|
@@ -1665,10 +1585,10 @@ var CabaneApi = class {
|
|
|
1665
1585
|
for (let attempt = 1; ; attempt++) {
|
|
1666
1586
|
try {
|
|
1667
1587
|
return await this.attempt(method, path3, body, signal);
|
|
1668
|
-
} catch (
|
|
1669
|
-
if (attempt >= maxAttempts || signal?.aborted || !isRetryable(
|
|
1588
|
+
} catch (err) {
|
|
1589
|
+
if (attempt >= maxAttempts || signal?.aborted || !isRetryable(err)) throw err;
|
|
1670
1590
|
await sleep2(RETRY_BACKOFF_MS[attempt - 1], signal);
|
|
1671
|
-
if (signal?.aborted) throw
|
|
1591
|
+
if (signal?.aborted) throw err;
|
|
1672
1592
|
}
|
|
1673
1593
|
}
|
|
1674
1594
|
}
|
|
@@ -1696,14 +1616,14 @@ var CabaneApi = class {
|
|
|
1696
1616
|
retry: true,
|
|
1697
1617
|
...signal ? { signal } : {}
|
|
1698
1618
|
});
|
|
1699
|
-
} catch (
|
|
1619
|
+
} catch (err) {
|
|
1700
1620
|
const outbox = this.opts.outbox;
|
|
1701
|
-
if (!outbox) throw
|
|
1702
|
-
if (signal?.aborted || isAbortError(
|
|
1703
|
-
if (!isRetryable(
|
|
1621
|
+
if (!outbox) throw err;
|
|
1622
|
+
if (signal?.aborted || isAbortError(err)) throw err;
|
|
1623
|
+
if (!isRetryable(err)) throw err;
|
|
1704
1624
|
outbox.persist({ enqueuedAt: Date.now(), turnId, seq, method: "POST", path: path3, body, kind });
|
|
1705
1625
|
this.opts.log?.warn(
|
|
1706
|
-
{ kind, turnId, seq, err:
|
|
1626
|
+
{ kind, turnId, seq, err: err instanceof Error ? err.message : String(err) },
|
|
1707
1627
|
"companion: commit queued to outbox after transient failure (will drain when the API returns)"
|
|
1708
1628
|
);
|
|
1709
1629
|
}
|
|
@@ -1727,10 +1647,10 @@ var CabaneApi = class {
|
|
|
1727
1647
|
await this.request(entry.method, entry.path, entry.body, { retry: true });
|
|
1728
1648
|
outbox.remove(entry.turnId, entry.seq);
|
|
1729
1649
|
progressed = true;
|
|
1730
|
-
} catch (
|
|
1731
|
-
if (
|
|
1650
|
+
} catch (err) {
|
|
1651
|
+
if (err instanceof ApiError && err.status >= 400 && err.status < 500) {
|
|
1732
1652
|
this.opts.log?.warn(
|
|
1733
|
-
{ kind: entry.kind, turnId: entry.turnId, seq: entry.seq, status:
|
|
1653
|
+
{ kind: entry.kind, turnId: entry.turnId, seq: entry.seq, status: err.status },
|
|
1734
1654
|
"companion outbox: discarding entry on terminal 4xx (will never land)"
|
|
1735
1655
|
);
|
|
1736
1656
|
outbox.remove(entry.turnId, entry.seq);
|
|
@@ -1878,11 +1798,11 @@ var CabaneApi = class {
|
|
|
1878
1798
|
try {
|
|
1879
1799
|
await this.request("PATCH", path3, body, { retry: true });
|
|
1880
1800
|
outbox?.remove(key, ACTIVE_RUN_OUTBOX_SEQ);
|
|
1881
|
-
} catch (
|
|
1882
|
-
if (!outbox) throw
|
|
1883
|
-
if (!isRetryable(
|
|
1801
|
+
} catch (err) {
|
|
1802
|
+
if (!outbox) throw err;
|
|
1803
|
+
if (!isRetryable(err)) {
|
|
1884
1804
|
outbox.remove(key, ACTIVE_RUN_OUTBOX_SEQ);
|
|
1885
|
-
throw
|
|
1805
|
+
throw err;
|
|
1886
1806
|
}
|
|
1887
1807
|
outbox.persist({
|
|
1888
1808
|
enqueuedAt: Date.now(),
|
|
@@ -1894,7 +1814,7 @@ var CabaneApi = class {
|
|
|
1894
1814
|
kind: "active-run"
|
|
1895
1815
|
});
|
|
1896
1816
|
this.opts.log?.warn(
|
|
1897
|
-
{ conversationId, agentId, err:
|
|
1817
|
+
{ conversationId, agentId, err: err instanceof Error ? err.message : String(err) },
|
|
1898
1818
|
"companion: active-run write queued to outbox after transient failure (will drain when the API returns)"
|
|
1899
1819
|
);
|
|
1900
1820
|
}
|
|
@@ -1993,13 +1913,13 @@ var CabaneApi = class {
|
|
|
1993
1913
|
return res.messages.find((m) => m.id === messageId2) ?? null;
|
|
1994
1914
|
}
|
|
1995
1915
|
};
|
|
1996
|
-
function isRetryable(
|
|
1997
|
-
if (
|
|
1998
|
-
if (isAbortError(
|
|
1916
|
+
function isRetryable(err) {
|
|
1917
|
+
if (err instanceof ApiError) return err.status >= 500;
|
|
1918
|
+
if (isAbortError(err)) return false;
|
|
1999
1919
|
return true;
|
|
2000
1920
|
}
|
|
2001
|
-
function isAbortError(
|
|
2002
|
-
return
|
|
1921
|
+
function isAbortError(err) {
|
|
1922
|
+
return err instanceof Error && err.name === "AbortError";
|
|
2003
1923
|
}
|
|
2004
1924
|
function sleep2(ms, signal) {
|
|
2005
1925
|
return new Promise((resolve) => {
|
|
@@ -2017,8 +1937,8 @@ function sleep2(ms, signal) {
|
|
|
2017
1937
|
}
|
|
2018
1938
|
function errorMessage(status2, body) {
|
|
2019
1939
|
if (body && typeof body === "object" && "error" in body) {
|
|
2020
|
-
const
|
|
2021
|
-
if (typeof
|
|
1940
|
+
const err = body.error;
|
|
1941
|
+
if (typeof err === "string") return `${status2} ${err}`;
|
|
2022
1942
|
}
|
|
2023
1943
|
if (typeof body === "string" && body.length > 0) return `${status2} ${body.slice(0, 200)}`;
|
|
2024
1944
|
return `${status2} error`;
|
|
@@ -2074,15 +1994,15 @@ var DeviceApi = class {
|
|
|
2074
1994
|
};
|
|
2075
1995
|
function errorMessage2(status2, body) {
|
|
2076
1996
|
if (body && typeof body === "object" && "error" in body) {
|
|
2077
|
-
const
|
|
2078
|
-
if (typeof
|
|
1997
|
+
const err = body.error;
|
|
1998
|
+
if (typeof err === "string") return `${status2} ${err}`;
|
|
2079
1999
|
}
|
|
2080
2000
|
if (typeof body === "string" && body.length > 0) return `${status2} ${body.slice(0, 200)}`;
|
|
2081
2001
|
return `${status2} error`;
|
|
2082
2002
|
}
|
|
2083
2003
|
|
|
2084
2004
|
// src/cursor.ts
|
|
2085
|
-
import { mkdirSync as mkdirSync6, readFileSync as
|
|
2005
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync5 } from "fs";
|
|
2086
2006
|
import { join as join7 } from "path";
|
|
2087
2007
|
function pathFor(workspaceId) {
|
|
2088
2008
|
return join7(cabaneDir(), "cursors", encodeURIComponent(workspaceId));
|
|
@@ -2090,7 +2010,7 @@ function pathFor(workspaceId) {
|
|
|
2090
2010
|
function readCursor(workspaceId) {
|
|
2091
2011
|
const path3 = pathFor(workspaceId);
|
|
2092
2012
|
if (!existsSync5(path3)) return null;
|
|
2093
|
-
const raw =
|
|
2013
|
+
const raw = readFileSync4(path3, "utf8").trim();
|
|
2094
2014
|
return raw.length > 0 ? raw : null;
|
|
2095
2015
|
}
|
|
2096
2016
|
function writeCursor(workspaceId, eventId) {
|
|
@@ -2139,7 +2059,7 @@ var CursorTracker = class {
|
|
|
2139
2059
|
};
|
|
2140
2060
|
|
|
2141
2061
|
// src/dispatch-dedupe.ts
|
|
2142
|
-
import { mkdirSync as mkdirSync7, readFileSync as
|
|
2062
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync6 } from "fs";
|
|
2143
2063
|
import { join as join8 } from "path";
|
|
2144
2064
|
var MAX_IDS = 256;
|
|
2145
2065
|
function dir(log) {
|
|
@@ -2152,7 +2072,7 @@ function readIds(log, workspaceId) {
|
|
|
2152
2072
|
const path3 = pathFor2(log, workspaceId);
|
|
2153
2073
|
if (!existsSync6(path3)) return [];
|
|
2154
2074
|
try {
|
|
2155
|
-
return
|
|
2075
|
+
return readFileSync5(path3, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
2156
2076
|
} catch {
|
|
2157
2077
|
return [];
|
|
2158
2078
|
}
|
|
@@ -2192,7 +2112,7 @@ function readResumeCounts(workspaceId) {
|
|
|
2192
2112
|
const path3 = resumePathFor(workspaceId);
|
|
2193
2113
|
if (!existsSync6(path3)) return out;
|
|
2194
2114
|
try {
|
|
2195
|
-
for (const line of
|
|
2115
|
+
for (const line of readFileSync5(path3, "utf8").split("\n")) {
|
|
2196
2116
|
const trimmed = line.trim();
|
|
2197
2117
|
if (!trimmed) continue;
|
|
2198
2118
|
const tab = trimmed.lastIndexOf(" ");
|
|
@@ -2228,44 +2148,44 @@ function noResume() {
|
|
|
2228
2148
|
var TURN_PROTOCOL_VERSION = 1;
|
|
2229
2149
|
|
|
2230
2150
|
// packages/agent-runtime/src/host-policy.ts
|
|
2231
|
-
import { z as
|
|
2232
|
-
var hostPolicySchema =
|
|
2151
|
+
import { z as z4 } from "zod";
|
|
2152
|
+
var hostPolicySchema = z4.object({
|
|
2233
2153
|
// Host filesystem + shell: `Bash`/`Read`/`Write`/`Edit`/`Glob`/`Grep`, the
|
|
2234
2154
|
// notebook read/write pair, git-worktree tools, and the `REPL` host code-exec
|
|
2235
2155
|
// tool. Off under the locked assistant surface (today's `DISALLOWED_TOOLS`);
|
|
2236
2156
|
// on under `coding` mode.
|
|
2237
|
-
hostFs:
|
|
2157
|
+
hostFs: z4.boolean(),
|
|
2238
2158
|
// Public web (today's `WebSearch` / `WebFetch`, the `DEFAULT_WEB_TOOLS`). Public
|
|
2239
2159
|
// web, not host reach — granted by default today, but expressible as a grant.
|
|
2240
|
-
web:
|
|
2160
|
+
web: z4.boolean(),
|
|
2241
2161
|
// Browser automation (the Playwright MCP surface). Varies by host: a companion has
|
|
2242
2162
|
// it, the house executor does not (CT230).
|
|
2243
|
-
browser:
|
|
2163
|
+
browser: z4.boolean(),
|
|
2244
2164
|
// User-configured MCP servers permitted. False for the house executor
|
|
2245
2165
|
// (CT227: Cabane agents run no user MCP servers), true for a personal companion.
|
|
2246
|
-
userMcp:
|
|
2166
|
+
userMcp: z4.boolean(),
|
|
2247
2167
|
// Subagent / sub-dispatch (`Task` / `Agent`). A per-host GRANT (see the CT261
|
|
2248
2168
|
// amendment above): `false` on the locked assistant/house surface (banned via
|
|
2249
2169
|
// `DISALLOWED_TOOLS`), `true` under coding / custom where the fan-out is left to
|
|
2250
2170
|
// the coding allow-hook (which reaches sub-agent tool calls, CT680) or the user
|
|
2251
2171
|
// allowlist. The subagent completes within the turn, so
|
|
2252
2172
|
// it's not the turn-model invariant `scheduling` is.
|
|
2253
|
-
subagents:
|
|
2173
|
+
subagents: z4.boolean(),
|
|
2254
2174
|
// ── Hard platform invariants — always denied, never granted ────────────────
|
|
2255
2175
|
// Deferred re-invocation / scheduling (`ScheduleWakeup`, the `Cron*` / `Task*`
|
|
2256
2176
|
// families, `Monitor`, `Workflow`, …). A turn is one query that resolves when
|
|
2257
2177
|
// `result` fires; a scheduled callback fires after the reply window has closed
|
|
2258
2178
|
// and strands the agent (the CT155/CT156 rule).
|
|
2259
|
-
scheduling:
|
|
2179
|
+
scheduling: z4.literal("never"),
|
|
2260
2180
|
// Human-facing UI prompts (`AskUserQuestion`). A Cabane conversation has no
|
|
2261
2181
|
// handler to answer a structured prompt, so the call hangs the turn
|
|
2262
2182
|
// (`UNSUPPORTED_TOOLS`). The agent asks in its reply instead.
|
|
2263
|
-
uiPrompts:
|
|
2183
|
+
uiPrompts: z4.literal("never")
|
|
2264
2184
|
});
|
|
2265
2185
|
|
|
2266
2186
|
// packages/agent-runtime/src/turn-event.ts
|
|
2267
|
-
import { z as
|
|
2268
|
-
var turnEventSchema =
|
|
2187
|
+
import { z as z5 } from "zod";
|
|
2188
|
+
var turnEventSchema = z5.discriminatedUnion("type", [
|
|
2269
2189
|
// The runtime's opaque session state, emitted when the adapter learns it (e.g.
|
|
2270
2190
|
// the SDK `system/init` frame). The platform stores `state` verbatim per
|
|
2271
2191
|
// (conversation, agent) and hands it back on the next turn; only the adapter
|
|
@@ -2285,19 +2205,19 @@ var turnEventSchema = z6.discriminatedUnion("type", [
|
|
|
2285
2205
|
// on the companion, after the server committed the manifest). Runtime-neutral: a
|
|
2286
2206
|
// plain boolean, not a runtime-specific reason string (that stays in the
|
|
2287
2207
|
// adapter's `onWarn` log). Additive + optional — an old receiver ignores it.
|
|
2288
|
-
|
|
2289
|
-
type:
|
|
2290
|
-
state:
|
|
2291
|
-
degraded:
|
|
2208
|
+
z5.object({
|
|
2209
|
+
type: z5.literal("session"),
|
|
2210
|
+
state: z5.string(),
|
|
2211
|
+
degraded: z5.boolean().optional()
|
|
2292
2212
|
}),
|
|
2293
2213
|
// One readable thinking summary. Maps `onThinking({ text })`. Transient —
|
|
2294
2214
|
// surfaced live, never persisted as durable content.
|
|
2295
|
-
|
|
2215
|
+
z5.object({ type: z5.literal("thinking"), text: z5.string() }),
|
|
2296
2216
|
// Assistant text. Maps `onAssistantText({ text, final })` — `text`→`body`,
|
|
2297
2217
|
// `final`→`terminal`. `terminal: false` is interim narration (commits as a
|
|
2298
2218
|
// `progress` row); `terminal: true` is the turn's closing reply (commits as
|
|
2299
2219
|
// the `final` row).
|
|
2300
|
-
|
|
2220
|
+
z5.object({ type: z5.literal("text"), body: z5.string(), terminal: z5.boolean() }),
|
|
2301
2221
|
// A tool-activity transition. Maps `onToolActivity` — `toolUseId`→`id`,
|
|
2302
2222
|
// `toolName`→`name` (already prefix-stripped: `cabane_read`, not
|
|
2303
2223
|
// `mcp__cabane__cabane_read`), `summary` is the short card label. `phase`
|
|
@@ -2312,15 +2232,15 @@ var turnEventSchema = z6.discriminatedUnion("type", [
|
|
|
2312
2232
|
// dropped the prefix; null for a host / built-in tool. The client tags Cabane
|
|
2313
2233
|
// MCP calls (`=== 'cabane'`) with a subtle glyph. Nullable + optional so a
|
|
2314
2234
|
// pre-CT496 producer that never sets it is unaffected (treated as null).
|
|
2315
|
-
|
|
2316
|
-
type:
|
|
2317
|
-
id:
|
|
2318
|
-
name:
|
|
2319
|
-
phase:
|
|
2320
|
-
summary:
|
|
2321
|
-
input:
|
|
2322
|
-
result:
|
|
2323
|
-
mcpServer:
|
|
2235
|
+
z5.object({
|
|
2236
|
+
type: z5.literal("tool"),
|
|
2237
|
+
id: z5.string(),
|
|
2238
|
+
name: z5.string(),
|
|
2239
|
+
phase: z5.enum(["start", "done", "error"]),
|
|
2240
|
+
summary: z5.string(),
|
|
2241
|
+
input: z5.unknown().optional(),
|
|
2242
|
+
result: z5.unknown().optional(),
|
|
2243
|
+
mcpServer: z5.string().nullable().optional()
|
|
2324
2244
|
}),
|
|
2325
2245
|
// The turn's terminal outcome. Maps the `{ ok, reason }` both loops return
|
|
2326
2246
|
// inline. `ok:false` carries a machine reason (`no_session`, an error code);
|
|
@@ -2364,34 +2284,34 @@ var turnEventSchema = z6.discriminatedUnion("type", [
|
|
|
2364
2284
|
// `message_turn_metadata.resolved_model` / `resolved_config`. Optional ⇒
|
|
2365
2285
|
// backward-compatible: an old adapter/companion omits them, a cancel has no result
|
|
2366
2286
|
// event at all, and the columns stay null → the UI shows `—`.
|
|
2367
|
-
|
|
2368
|
-
type:
|
|
2369
|
-
ok:
|
|
2370
|
-
reason:
|
|
2371
|
-
usage:
|
|
2372
|
-
inputTokens:
|
|
2373
|
-
outputTokens:
|
|
2374
|
-
cacheReadTokens:
|
|
2375
|
-
cacheCreationTokens:
|
|
2376
|
-
contextTokens:
|
|
2377
|
-
contextWindow:
|
|
2287
|
+
z5.object({
|
|
2288
|
+
type: z5.literal("result"),
|
|
2289
|
+
ok: z5.boolean(),
|
|
2290
|
+
reason: z5.string().optional(),
|
|
2291
|
+
usage: z5.object({
|
|
2292
|
+
inputTokens: z5.number(),
|
|
2293
|
+
outputTokens: z5.number(),
|
|
2294
|
+
cacheReadTokens: z5.number().optional(),
|
|
2295
|
+
cacheCreationTokens: z5.number().optional(),
|
|
2296
|
+
contextTokens: z5.number().optional(),
|
|
2297
|
+
contextWindow: z5.number().optional()
|
|
2378
2298
|
}).optional(),
|
|
2379
|
-
resolvedModel:
|
|
2380
|
-
resolvedConfig:
|
|
2381
|
-
effort:
|
|
2382
|
-
thinking:
|
|
2383
|
-
reasoningEffort:
|
|
2299
|
+
resolvedModel: z5.string().optional(),
|
|
2300
|
+
resolvedConfig: z5.object({
|
|
2301
|
+
effort: z5.string().optional(),
|
|
2302
|
+
thinking: z5.string().optional(),
|
|
2303
|
+
reasoningEffort: z5.string().optional()
|
|
2384
2304
|
}).optional()
|
|
2385
2305
|
})
|
|
2386
2306
|
]);
|
|
2387
2307
|
|
|
2388
2308
|
// packages/agent-runtime/src/failure.ts
|
|
2389
|
-
import { z as
|
|
2390
|
-
var turnFailureSchema =
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2309
|
+
import { z as z6 } from "zod";
|
|
2310
|
+
var turnFailureSchema = z6.discriminatedUnion("kind", [
|
|
2311
|
+
z6.object({ kind: z6.literal("usage_capped"), resetsAt: z6.string().optional() }),
|
|
2312
|
+
z6.object({ kind: z6.literal("rate_limited") }),
|
|
2313
|
+
z6.object({ kind: z6.literal("server_error") }),
|
|
2314
|
+
z6.object({ kind: z6.literal("auth_expired") })
|
|
2395
2315
|
]);
|
|
2396
2316
|
var USAGE_CAPPED = "usage_capped";
|
|
2397
2317
|
var RATE_LIMITED = "rate_limited";
|
|
@@ -2497,63 +2417,63 @@ var RATE_PATTERNS = [/rate[_ ]?limit/, /\b429\b/, /too many requests/];
|
|
|
2497
2417
|
var BARE_LIMIT = /\blimit (reached|exceeded)\b/;
|
|
2498
2418
|
|
|
2499
2419
|
// packages/agent-runtime/src/turn-request.ts
|
|
2500
|
-
import { z as
|
|
2501
|
-
var contentBlockSchema =
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
type:
|
|
2505
|
-
source:
|
|
2420
|
+
import { z as z7 } from "zod";
|
|
2421
|
+
var contentBlockSchema = z7.discriminatedUnion("type", [
|
|
2422
|
+
z7.object({ type: z7.literal("text"), text: z7.string() }),
|
|
2423
|
+
z7.object({
|
|
2424
|
+
type: z7.literal("image"),
|
|
2425
|
+
source: z7.object({ type: z7.literal("url"), url: z7.string() })
|
|
2506
2426
|
}),
|
|
2507
|
-
|
|
2508
|
-
type:
|
|
2509
|
-
source:
|
|
2427
|
+
z7.object({
|
|
2428
|
+
type: z7.literal("document"),
|
|
2429
|
+
source: z7.object({ type: z7.literal("url"), url: z7.string() })
|
|
2510
2430
|
})
|
|
2511
2431
|
]);
|
|
2512
|
-
var effortLevelSchema =
|
|
2513
|
-
var resolvedRunConfigSchema =
|
|
2514
|
-
model:
|
|
2432
|
+
var effortLevelSchema = z7.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
2433
|
+
var resolvedRunConfigSchema = z7.object({
|
|
2434
|
+
model: z7.string().nullable(),
|
|
2515
2435
|
effort: effortLevelSchema.optional(),
|
|
2516
|
-
runtimeOptions:
|
|
2436
|
+
runtimeOptions: z7.record(z7.string(), z7.unknown()).optional()
|
|
2517
2437
|
});
|
|
2518
|
-
var resolvedMcpServerSchema =
|
|
2519
|
-
|
|
2520
|
-
type:
|
|
2521
|
-
command:
|
|
2522
|
-
args:
|
|
2523
|
-
env:
|
|
2438
|
+
var resolvedMcpServerSchema = z7.union([
|
|
2439
|
+
z7.object({
|
|
2440
|
+
type: z7.literal("stdio").optional(),
|
|
2441
|
+
command: z7.string(),
|
|
2442
|
+
args: z7.array(z7.string()).optional(),
|
|
2443
|
+
env: z7.record(z7.string(), z7.string()).optional()
|
|
2524
2444
|
}),
|
|
2525
|
-
|
|
2526
|
-
type:
|
|
2527
|
-
url:
|
|
2528
|
-
headers:
|
|
2445
|
+
z7.object({
|
|
2446
|
+
type: z7.literal("http"),
|
|
2447
|
+
url: z7.string(),
|
|
2448
|
+
headers: z7.record(z7.string(), z7.string()).optional()
|
|
2529
2449
|
}),
|
|
2530
|
-
|
|
2531
|
-
type:
|
|
2532
|
-
url:
|
|
2533
|
-
headers:
|
|
2450
|
+
z7.object({
|
|
2451
|
+
type: z7.literal("sse"),
|
|
2452
|
+
url: z7.string(),
|
|
2453
|
+
headers: z7.record(z7.string(), z7.string()).optional()
|
|
2534
2454
|
})
|
|
2535
2455
|
]);
|
|
2536
|
-
var resolvedMcpServersSchema =
|
|
2537
|
-
var hostInjectedServersSchema =
|
|
2538
|
-
var turnRequestSchema =
|
|
2456
|
+
var resolvedMcpServersSchema = z7.record(z7.string(), resolvedMcpServerSchema);
|
|
2457
|
+
var hostInjectedServersSchema = z7.record(z7.string(), z7.unknown());
|
|
2458
|
+
var turnRequestSchema = z7.object({
|
|
2539
2459
|
// Server-composed system prompt (core + capability prose + adapter addendum +
|
|
2540
2460
|
// charter). One string to the adapter.
|
|
2541
|
-
systemPrompt:
|
|
2461
|
+
systemPrompt: z7.string(),
|
|
2542
2462
|
// Server-composed per-turn user text (anchor reminder + the triggering message).
|
|
2543
|
-
prompt:
|
|
2463
|
+
prompt: z7.string(),
|
|
2544
2464
|
// The multi-block user-message body (text + vision).
|
|
2545
|
-
content:
|
|
2465
|
+
content: z7.array(contentBlockSchema),
|
|
2546
2466
|
// Portable-or-dialect run-config (above).
|
|
2547
2467
|
config: resolvedRunConfigSchema,
|
|
2548
2468
|
// Abstract capability grants; the adapter maps them to tool names.
|
|
2549
2469
|
policy: hostPolicySchema,
|
|
2550
2470
|
// Prior opaque session state, or null for a fresh session.
|
|
2551
|
-
session:
|
|
2471
|
+
session: z7.string().nullable(),
|
|
2552
2472
|
// The cabane control-plane coordinates for this turn's MCP + post-back.
|
|
2553
|
-
cabane:
|
|
2554
|
-
mcpUrl:
|
|
2555
|
-
bearer:
|
|
2556
|
-
activeConversationId:
|
|
2473
|
+
cabane: z7.object({
|
|
2474
|
+
mcpUrl: z7.string(),
|
|
2475
|
+
bearer: z7.string(),
|
|
2476
|
+
activeConversationId: z7.string(),
|
|
2557
2477
|
// CT714: the scoped TURN-CONTROL MCP endpoint (`/api/turn-control`). The
|
|
2558
2478
|
// EXTERNAL adapters (Codex / opencode) mount it by URL under the key
|
|
2559
2479
|
// `cabane_companion` — using the same `bearer` (the turn token) and the same
|
|
@@ -2563,7 +2483,7 @@ var turnRequestSchema = z8.object({
|
|
|
2563
2483
|
// claude-code ignores it (it mounts the in-process instance instead), and
|
|
2564
2484
|
// every existing `cabane`-block fixture keeps parsing unchanged; the
|
|
2565
2485
|
// companion always populates it (`build-options.ts`).
|
|
2566
|
-
turnControlUrl:
|
|
2486
|
+
turnControlUrl: z7.string().optional(),
|
|
2567
2487
|
// CT598: the workspace this turn runs in. The claude-code/opencode/codex
|
|
2568
2488
|
// adapters never need it (they reach Cabane through the `cabane` MCP server,
|
|
2569
2489
|
// which takes `workspaceId` as a per-tool arg the model supplies); the
|
|
@@ -2573,53 +2493,39 @@ var turnRequestSchema = z8.object({
|
|
|
2573
2493
|
// adapters' conformance fixtures, tests) keeps parsing unchanged — the companion
|
|
2574
2494
|
// always populates it (`build-options.ts`), and the native adapter fails the
|
|
2575
2495
|
// turn loudly when it is somehow absent rather than guessing.
|
|
2576
|
-
workspaceId:
|
|
2496
|
+
workspaceId: z7.string().optional(),
|
|
2577
2497
|
// CT752: the server-resolved workspace surface this credential exposes.
|
|
2578
2498
|
// Readiness uses this explicit fact to require `sdk` for code mode and the
|
|
2579
2499
|
// granular floor for classic mode; inventory contents alone cannot infer it
|
|
2580
2500
|
// because `sdk` is intentionally also available on the classic surface.
|
|
2581
|
-
workspaceToolSurface:
|
|
2501
|
+
workspaceToolSurface: z7.enum(["code", "classic"]).optional()
|
|
2582
2502
|
}),
|
|
2583
2503
|
// Machine-local resolution (host-filled): the checkout cwd, extra env from a
|
|
2584
2504
|
// prepare hook, and the resolved user MCP servers.
|
|
2585
|
-
local:
|
|
2586
|
-
cwd:
|
|
2587
|
-
env:
|
|
2588
|
-
nativeWorkAssignment: z8.object({
|
|
2589
|
-
itemId: z8.string(),
|
|
2590
|
-
executionId: z8.string(),
|
|
2591
|
-
activationEpoch: z8.number().int().nonnegative()
|
|
2592
|
-
}).strict().optional(),
|
|
2505
|
+
local: z7.object({
|
|
2506
|
+
cwd: z7.string().optional(),
|
|
2507
|
+
env: z7.record(z7.string(), z7.string()).optional(),
|
|
2593
2508
|
mcpServers: resolvedMcpServersSchema.optional(),
|
|
2594
2509
|
// CT289: machine-local claude-code adapter knobs the operator sets on a
|
|
2595
2510
|
// companion they run themselves — the auto-memory escape hatch. `autoMemory:
|
|
2596
2511
|
// true` opts back into Claude Code's auto-memory (governed by the operator's
|
|
2597
2512
|
// own `.claude/settings.json`); absent/false leaves the adapter's force-off
|
|
2598
|
-
// default in place (see `buildClaudeCodeOptions`).
|
|
2599
|
-
|
|
2600
|
-
claudeCode: z8.object({ autoMemory: z8.boolean().optional() }).optional()
|
|
2513
|
+
// default in place (see `buildClaudeCodeOptions`).
|
|
2514
|
+
claudeCode: z7.object({ autoMemory: z7.boolean().optional() }).optional()
|
|
2601
2515
|
}),
|
|
2602
2516
|
// Host-owned injected servers (host-filled) — e.g. the summon server.
|
|
2603
|
-
extra:
|
|
2604
|
-
mcpServers: hostInjectedServersSchema
|
|
2605
|
-
// CT666: the per-turn turn-control HANDLER for the in-process cabane-native
|
|
2606
|
-
// runtime — the seam that gives it ask/wake_me/summon_agent/sub_agent/skip_turn
|
|
2607
|
-
// without a subprocess `cabane_companion` MCP server. Typed `unknown` for the same
|
|
2608
|
-
// reason as `mcpServers`: it's a live host object (a `NativeTurnControl` whose
|
|
2609
|
-
// methods close over the dispatcher's per-turn state), passed to the native
|
|
2610
|
-
// adapter WITHOUT the contract package inspecting it. The subprocess adapters
|
|
2611
|
-
// ignore it (they get the same verbs from the injected SDK server instead).
|
|
2612
|
-
turnControl: z8.unknown().optional()
|
|
2517
|
+
extra: z7.object({
|
|
2518
|
+
mcpServers: hostInjectedServersSchema
|
|
2613
2519
|
})
|
|
2614
2520
|
});
|
|
2615
2521
|
|
|
2616
2522
|
// packages/agent-runtime/src/conformance.ts
|
|
2617
|
-
import { z as
|
|
2618
|
-
var conformanceFixtureSchema =
|
|
2619
|
-
name:
|
|
2523
|
+
import { z as z8 } from "zod";
|
|
2524
|
+
var conformanceFixtureSchema = z8.object({
|
|
2525
|
+
name: z8.string(),
|
|
2620
2526
|
request: turnRequestSchema,
|
|
2621
|
-
nativeStream:
|
|
2622
|
-
expected:
|
|
2527
|
+
nativeStream: z8.array(z8.unknown()),
|
|
2528
|
+
expected: z8.array(turnEventSchema)
|
|
2623
2529
|
});
|
|
2624
2530
|
|
|
2625
2531
|
// packages/agent-runtime/src/transcript.ts
|
|
@@ -2629,8 +2535,8 @@ function createTerminalTextBuffer() {
|
|
|
2629
2535
|
async function safeEmit(emit, event, onError) {
|
|
2630
2536
|
try {
|
|
2631
2537
|
await emit(event);
|
|
2632
|
-
} catch (
|
|
2633
|
-
onError?.(
|
|
2538
|
+
} catch (err) {
|
|
2539
|
+
onError?.(err, event.type);
|
|
2634
2540
|
}
|
|
2635
2541
|
}
|
|
2636
2542
|
async function processAssistantMessage(msg, emit, pending, buffer, onError) {
|
|
@@ -2891,16 +2797,16 @@ var TurnPump = class {
|
|
|
2891
2797
|
// minimal note. Skipped when cancelled or already final. The held-text flush
|
|
2892
2798
|
// that precedes it is a classification concern, driven by the caller before
|
|
2893
2799
|
// this runs.
|
|
2894
|
-
async finalize(
|
|
2895
|
-
if (!
|
|
2800
|
+
async finalize(ok) {
|
|
2801
|
+
if (!ok || this.opts.signal.aborted || this.emittedFinal) return;
|
|
2896
2802
|
const body = this.lastProgressBody ?? this.opts.emptyFinalBody;
|
|
2897
2803
|
const seq = this.opts.nextSeq();
|
|
2898
2804
|
try {
|
|
2899
2805
|
await this.opts.commit.commitMessage({ body, kind: "final", seq });
|
|
2900
2806
|
this.emittedFinal = true;
|
|
2901
2807
|
this.finalReplyBody = body;
|
|
2902
|
-
} catch (
|
|
2903
|
-
this.opts.onError?.(
|
|
2808
|
+
} catch (err) {
|
|
2809
|
+
this.opts.onError?.(err, "empty-final");
|
|
2904
2810
|
}
|
|
2905
2811
|
}
|
|
2906
2812
|
// Whether the turn has committed its `final` row — read by the host to decide
|
|
@@ -2924,7 +2830,7 @@ import {
|
|
|
2924
2830
|
var CLAUDE_CODE_ADDENDUM = "";
|
|
2925
2831
|
|
|
2926
2832
|
// packages/agent-runtime/src/claude-code/policy.ts
|
|
2927
|
-
import { z as
|
|
2833
|
+
import { z as z9 } from "zod";
|
|
2928
2834
|
var HOST_FS_TOOLS = [
|
|
2929
2835
|
// shell + local filesystem
|
|
2930
2836
|
"Bash",
|
|
@@ -2974,30 +2880,20 @@ function withThinkingSummaries(thinking) {
|
|
|
2974
2880
|
if (thinking.type === "disabled") return thinking;
|
|
2975
2881
|
return { display: "summarized", ...thinking };
|
|
2976
2882
|
}
|
|
2977
|
-
var claudeCodeDialectSchema =
|
|
2978
|
-
thinking:
|
|
2979
|
-
|
|
2980
|
-
type:
|
|
2981
|
-
display:
|
|
2883
|
+
var claudeCodeDialectSchema = z9.object({
|
|
2884
|
+
thinking: z9.discriminatedUnion("type", [
|
|
2885
|
+
z9.object({
|
|
2886
|
+
type: z9.literal("adaptive"),
|
|
2887
|
+
display: z9.enum(["summarized", "omitted"]).optional()
|
|
2982
2888
|
}),
|
|
2983
|
-
|
|
2984
|
-
type:
|
|
2985
|
-
budgetTokens:
|
|
2986
|
-
display:
|
|
2889
|
+
z9.object({
|
|
2890
|
+
type: z9.literal("enabled"),
|
|
2891
|
+
budgetTokens: z9.number().int().positive().optional(),
|
|
2892
|
+
display: z9.enum(["summarized", "omitted"]).optional()
|
|
2987
2893
|
}),
|
|
2988
|
-
|
|
2894
|
+
z9.object({ type: z9.literal("disabled") })
|
|
2989
2895
|
]).optional(),
|
|
2990
|
-
|
|
2991
|
-
disallowedTools: z10.array(z10.string()).optional(),
|
|
2992
|
-
// Which claude-code harness shape to run. `coding` switches to the
|
|
2993
|
-
// `claude_code` preset + project settings + always-allow `PreToolUse` hook;
|
|
2994
|
-
// `custom`/`assistant` (or absent) use a plain-string prompt + bypass. This
|
|
2995
|
-
// is the claude-code-specific PRESET selector — kept distinct from
|
|
2996
|
-
// `policy.hostFs` (the host-fs BLOCK), because companion `custom` mode wants host
|
|
2997
|
-
// fs available (via its own allowlist) WITHOUT the coding harness, and in-app
|
|
2998
|
-
// `custom` wants host fs blocked — neither of which a single `hostFs` boolean
|
|
2999
|
-
// can express alongside the preset choice.
|
|
3000
|
-
mode: z10.enum(["assistant", "coding", "custom"]).optional()
|
|
2896
|
+
hostAccess: z9.boolean().optional()
|
|
3001
2897
|
}).loose();
|
|
3002
2898
|
function readThinking(runtimeOptions) {
|
|
3003
2899
|
const dialect = runtimeOptions?.["claude-code"];
|
|
@@ -3076,23 +2972,25 @@ function buildClaudeCodeOptions(req, augment) {
|
|
|
3076
2972
|
};
|
|
3077
2973
|
}
|
|
3078
2974
|
const dialect = claudeCodeDialectSchema.safeParse(config.runtimeOptions?.["claude-code"] ?? {});
|
|
3079
|
-
const
|
|
3080
|
-
const customDisallowed = dialect.success ? dialect.data.disallowedTools ?? [] : [];
|
|
3081
|
-
const useCodingPreset = (dialect.success ? dialect.data.mode : void 0) === "coding";
|
|
2975
|
+
const useCodingPreset = dialect.success ? dialect.data.hostAccess ?? false : false;
|
|
3082
2976
|
const cabaneGlob = `mcp__${CABANE_MCP_SERVER}__*`;
|
|
3083
2977
|
const extraServerGlobs = Object.keys(req.extra.mcpServers).map((name) => `mcp__${name}__*`);
|
|
3084
2978
|
const allowedTools = dedupe([
|
|
3085
2979
|
cabaneGlob,
|
|
3086
2980
|
...extraServerGlobs,
|
|
3087
|
-
...policy.web ? DEFAULT_WEB_TOOLS : []
|
|
3088
|
-
...customAllowed
|
|
2981
|
+
...policy.web ? DEFAULT_WEB_TOOLS : []
|
|
3089
2982
|
]);
|
|
3090
|
-
const disallowedTools = dedupe([...disallowedToolsFor(policy)
|
|
2983
|
+
const disallowedTools = dedupe([...disallowedToolsFor(policy)]);
|
|
3091
2984
|
const resumeDecision = decideResume(req.session, cwd);
|
|
3092
2985
|
const resume = "resume" in resumeDecision ? resumeDecision.resume : null;
|
|
3093
2986
|
const freshReason = "fresh" in resumeDecision ? resumeDecision.reason : void 0;
|
|
3094
2987
|
const devControlsAutoMemory = req.local.claudeCode?.autoMemory === true;
|
|
3095
2988
|
const model = parseClaudeCodeModel(config.model);
|
|
2989
|
+
if (model === null) {
|
|
2990
|
+
console.warn(
|
|
2991
|
+
`[agent-runtime/claude-code] CT823: dispatching a subscription turn with NO concrete model (config.model=${JSON.stringify(config.model)}) \u2014 the SDK will fall back to its bundled default. This should be unreachable post-CT823; the model resolver should have supplied a concrete house default.`
|
|
2992
|
+
);
|
|
2993
|
+
}
|
|
3096
2994
|
const base = {
|
|
3097
2995
|
// model / effort: `model` is pinned only when the config names a real one —
|
|
3098
2996
|
// omitted for "let it choose" (see `parseClaudeCodeModel`), so the SDK picks
|
|
@@ -3153,7 +3051,7 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
3153
3051
|
out.push(event);
|
|
3154
3052
|
};
|
|
3155
3053
|
let sessionEmitted = false;
|
|
3156
|
-
let
|
|
3054
|
+
let ok = false;
|
|
3157
3055
|
let resultReason;
|
|
3158
3056
|
let sawResult = false;
|
|
3159
3057
|
let usage;
|
|
@@ -3199,8 +3097,8 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
3199
3097
|
if (typeof info.rateLimitType === "string") rateLimitType = info.rateLimitType;
|
|
3200
3098
|
}
|
|
3201
3099
|
} else if (msg.type === "auth_status") {
|
|
3202
|
-
const
|
|
3203
|
-
if (typeof
|
|
3100
|
+
const err = msg.error;
|
|
3101
|
+
if (typeof err === "string" && err.length > 0) authError = err;
|
|
3204
3102
|
} else if (msg.type === "result") {
|
|
3205
3103
|
sawResult = true;
|
|
3206
3104
|
usage = readSdkUsage(msg);
|
|
@@ -3212,7 +3110,7 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
3212
3110
|
}
|
|
3213
3111
|
const isError = msg.is_error === true;
|
|
3214
3112
|
if (msg.subtype === "success" && !isError) {
|
|
3215
|
-
|
|
3113
|
+
ok = true;
|
|
3216
3114
|
} else {
|
|
3217
3115
|
const resultText = msg.result ?? "";
|
|
3218
3116
|
const terminalReason = msg.terminal_reason;
|
|
@@ -3226,26 +3124,26 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
3226
3124
|
...rateLimitResetIso ? { resetsAt: rateLimitResetIso } : {}
|
|
3227
3125
|
} : classifyAssistantError(lastAssistantError) ?? classifyErrorText([authError, errorText].filter(Boolean).join(" "));
|
|
3228
3126
|
resultReason = failure ? encodeFailureReason(failure) : isError ? `error:${resultText.slice(0, 200) || "unknown"}` : `result_error:${msg.subtype}`;
|
|
3229
|
-
|
|
3127
|
+
ok = false;
|
|
3230
3128
|
}
|
|
3231
3129
|
break;
|
|
3232
3130
|
}
|
|
3233
3131
|
}
|
|
3234
|
-
} catch (
|
|
3235
|
-
if (ctx.signal.aborted) throw
|
|
3236
|
-
const failure = classifyErrorText(
|
|
3237
|
-
if (!failure) throw
|
|
3238
|
-
|
|
3132
|
+
} catch (err) {
|
|
3133
|
+
if (ctx.signal.aborted) throw err;
|
|
3134
|
+
const failure = classifyErrorText(err instanceof Error ? err.message : String(err));
|
|
3135
|
+
if (!failure) throw err;
|
|
3136
|
+
ok = false;
|
|
3239
3137
|
resultReason = encodeFailureReason(failure);
|
|
3240
3138
|
sawResult = true;
|
|
3241
3139
|
}
|
|
3242
3140
|
if (ctx.signal.aborted) return;
|
|
3243
|
-
await flushHeldText(buffer, emit,
|
|
3141
|
+
await flushHeldText(buffer, emit, ok);
|
|
3244
3142
|
yield* drain(out);
|
|
3245
|
-
if (!
|
|
3143
|
+
if (!ok && !resultReason && !sawResult) resultReason = "no_result";
|
|
3246
3144
|
yield {
|
|
3247
3145
|
type: "result",
|
|
3248
|
-
ok
|
|
3146
|
+
ok,
|
|
3249
3147
|
...resultReason ? { reason: resultReason } : {},
|
|
3250
3148
|
...usage ? { usage } : {},
|
|
3251
3149
|
...resolvedModel ? { resolvedModel } : {}
|
|
@@ -3714,26 +3612,20 @@ function selectAdapter(registry, runtime) {
|
|
|
3714
3612
|
|
|
3715
3613
|
// packages/agent-runtime/src/opencode/addendum.ts
|
|
3716
3614
|
var OPENCODE_ADDENDUM = [
|
|
3717
|
-
"Your Cabane
|
|
3718
|
-
"
|
|
3719
|
-
"
|
|
3615
|
+
"Your one Cabane workspace tool has a plain name \u2014 `sdk` (you act on the",
|
|
3616
|
+
"workspace by writing a TypeScript program and calling `sdk` with it); the",
|
|
3617
|
+
"turn-control verbs (`ask`, `wake_me`, `summon_agent`, `skip_turn`) are",
|
|
3618
|
+
"plain-named too, as are the few ancillary cabane tools (`list_workspaces`,",
|
|
3619
|
+
"`read_binary`, `upload`, `begin_upload`, `finalize_upload`,",
|
|
3620
|
+
"`mint_render_token`), and the host tools are plain verbs (`bash`, `read`,",
|
|
3621
|
+
"`edit`). There is no `write`/`search`/`edit` CABANE tool here \u2014 those are",
|
|
3622
|
+
"`cabane` SDK calls inside your program, not tools (a bare `read`/`edit` is the",
|
|
3623
|
+
"HOST tool). If a tool appears in this prompt with an `mcp__\u2026__` prefix, that",
|
|
3720
3624
|
"prefix is not part of its name \u2014 call the tool by its plain verb. Write your",
|
|
3721
3625
|
"closing reply as the last thing you say in the turn: you can interleave",
|
|
3722
3626
|
"narration with tool calls, but only your final message is recorded as the",
|
|
3723
3627
|
"turn\u2019s reply."
|
|
3724
3628
|
].join(" ");
|
|
3725
|
-
var OPENCODE_ADDENDUM_CODE_MODE = [
|
|
3726
|
-
"Your one Cabane workspace tool has a plain name \u2014 `sdk` (you act on the",
|
|
3727
|
-
"workspace by writing a TypeScript program and calling `sdk` with it); the",
|
|
3728
|
-
"turn-control verbs (`ask`, `wake_me`, `summon_agent`, `skip_turn`) are",
|
|
3729
|
-
"plain-named too, and the host tools are plain verbs (`bash`, `read`, `edit`).",
|
|
3730
|
-
"There is no `write`/`search`/`edit` CABANE tool here \u2014 those are `cabane` SDK",
|
|
3731
|
-
"calls inside your program, not tools (a bare `read`/`edit` is the HOST tool). If",
|
|
3732
|
-
"a tool appears in this prompt with an `mcp__\u2026__` prefix, that prefix is not part",
|
|
3733
|
-
"of its name \u2014 call the tool by its plain verb. Write your closing reply as the",
|
|
3734
|
-
"last thing you say in the turn: you can interleave narration with tool calls, but",
|
|
3735
|
-
"only your final message is recorded as the turn\u2019s reply."
|
|
3736
|
-
].join(" ");
|
|
3737
3629
|
|
|
3738
3630
|
// packages/agent-runtime/src/opencode/events.ts
|
|
3739
3631
|
function asRecord(v) {
|
|
@@ -3808,9 +3700,9 @@ function readSessionId(properties) {
|
|
|
3808
3700
|
}
|
|
3809
3701
|
function readSessionError(properties) {
|
|
3810
3702
|
const props = asRecord(properties);
|
|
3811
|
-
const
|
|
3812
|
-
if (typeof
|
|
3813
|
-
const rec2 = asRecord(
|
|
3703
|
+
const err = props?.error;
|
|
3704
|
+
if (typeof err === "string") return err;
|
|
3705
|
+
const rec2 = asRecord(err);
|
|
3814
3706
|
if (!rec2) return "unknown";
|
|
3815
3707
|
const { name, message } = deepestError(rec2);
|
|
3816
3708
|
if (message && name && !isGenericErrorName(name)) return `${name}: ${message}`;
|
|
@@ -3846,7 +3738,7 @@ async function* decodeOpencodeStream(events, ctx) {
|
|
|
3846
3738
|
const pending = /* @__PURE__ */ new Map();
|
|
3847
3739
|
const startedTools = /* @__PURE__ */ new Set();
|
|
3848
3740
|
const finishedTools = /* @__PURE__ */ new Set();
|
|
3849
|
-
let
|
|
3741
|
+
let ok = false;
|
|
3850
3742
|
let reason;
|
|
3851
3743
|
let settled = false;
|
|
3852
3744
|
const userMessageIds = /* @__PURE__ */ new Set();
|
|
@@ -3909,7 +3801,7 @@ async function* decodeOpencodeStream(events, ctx) {
|
|
|
3909
3801
|
const sealed = sealHeld(held, true);
|
|
3910
3802
|
held = null;
|
|
3911
3803
|
if (sealed) yield sealed;
|
|
3912
|
-
|
|
3804
|
+
ok = true;
|
|
3913
3805
|
settled = true;
|
|
3914
3806
|
break;
|
|
3915
3807
|
} else if (ev.type === "session.error") {
|
|
@@ -3918,7 +3810,7 @@ async function* decodeOpencodeStream(events, ctx) {
|
|
|
3918
3810
|
const sealed = sealHeld(held, false);
|
|
3919
3811
|
held = null;
|
|
3920
3812
|
if (sealed) yield sealed;
|
|
3921
|
-
|
|
3813
|
+
ok = false;
|
|
3922
3814
|
const errorText = readSessionError(ev.properties);
|
|
3923
3815
|
const failure = classifyErrorText(errorText);
|
|
3924
3816
|
reason = failure ? encodeFailureReason(failure) : `error:${errorText.slice(0, 200)}`;
|
|
@@ -3933,7 +3825,7 @@ async function* decodeOpencodeStream(events, ctx) {
|
|
|
3933
3825
|
if (sealed) yield sealed;
|
|
3934
3826
|
reason = "no_terminal";
|
|
3935
3827
|
}
|
|
3936
|
-
yield { type: "result", ok
|
|
3828
|
+
yield { type: "result", ok, ...reason ? { reason } : {} };
|
|
3937
3829
|
}
|
|
3938
3830
|
function hasToolInput(input) {
|
|
3939
3831
|
return !!input && typeof input === "object" && Object.keys(input).length > 0;
|
|
@@ -3947,7 +3839,7 @@ function sealHeld(held, terminal) {
|
|
|
3947
3839
|
}
|
|
3948
3840
|
|
|
3949
3841
|
// packages/agent-runtime/src/opencode/policy.ts
|
|
3950
|
-
import { z as
|
|
3842
|
+
import { z as z10 } from "zod";
|
|
3951
3843
|
var OPENCODE_HOST_TOOLS = [
|
|
3952
3844
|
"bash",
|
|
3953
3845
|
"edit",
|
|
@@ -3974,8 +3866,8 @@ function opencodeToolPolicy(policy) {
|
|
|
3974
3866
|
deny(OPENCODE_UI_PROMPT_TOOLS);
|
|
3975
3867
|
return { tools, allowAllHostTools: policy.hostFs };
|
|
3976
3868
|
}
|
|
3977
|
-
var opencodeDialectSchema =
|
|
3978
|
-
agent:
|
|
3869
|
+
var opencodeDialectSchema = z10.object({
|
|
3870
|
+
agent: z10.string().min(1).optional()
|
|
3979
3871
|
}).loose();
|
|
3980
3872
|
function readOpencodeDialect(runtimeOptions) {
|
|
3981
3873
|
const parsed = opencodeDialectSchema.safeParse(runtimeOptions?.["opencode"] ?? {});
|
|
@@ -4196,9 +4088,9 @@ function createHttpOpencodeTransport(opts) {
|
|
|
4196
4088
|
// The lock is released when this stream finishes draining.
|
|
4197
4089
|
events: releaseAfter(parseSseStream(eventRes.body, sessionId, signal), release)
|
|
4198
4090
|
};
|
|
4199
|
-
} catch (
|
|
4091
|
+
} catch (err) {
|
|
4200
4092
|
release();
|
|
4201
|
-
throw
|
|
4093
|
+
throw err;
|
|
4202
4094
|
}
|
|
4203
4095
|
}
|
|
4204
4096
|
};
|
|
@@ -4285,7 +4177,7 @@ function createOpencodeAdapter(deps = {}) {
|
|
|
4285
4177
|
name: "opencode",
|
|
4286
4178
|
// CT614: surface-aware — a code-mode turn (only `code` mounted) is taught
|
|
4287
4179
|
// `code`, not the granular cabane names it no longer has.
|
|
4288
|
-
promptAddendum: (
|
|
4180
|
+
promptAddendum: () => OPENCODE_ADDENDUM,
|
|
4289
4181
|
dialectSchema: opencodeDialectSchema,
|
|
4290
4182
|
async *runTurn(req, signal) {
|
|
4291
4183
|
if (!transport) {
|
|
@@ -4685,14 +4577,6 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
4685
4577
|
|
|
4686
4578
|
// packages/agent-runtime/src/codex/addendum.ts
|
|
4687
4579
|
var CODEX_ADDENDUM = [
|
|
4688
|
-
"Your Cabane tools have plain names \u2014 `read`, `write`, `search`, `edit`,",
|
|
4689
|
-
"`post_message`, and so on. If a tool appears in this prompt with an `mcp__\u2026__`",
|
|
4690
|
-
"prefix, that prefix is not part of its name \u2014 call the tool by its plain verb.",
|
|
4691
|
-
"Write your closing reply as the last thing you say in the turn: you can",
|
|
4692
|
-
"interleave narration with tool calls, but only your final message is recorded",
|
|
4693
|
-
"as the turn\u2019s reply."
|
|
4694
|
-
].join(" ");
|
|
4695
|
-
var CODEX_ADDENDUM_CODE_MODE = [
|
|
4696
4580
|
"Your Cabane workspace tool is exposed to Codex as the qualified MCP tool",
|
|
4697
4581
|
"`mcp__cabane__sdk`. On Codex versions that defer MCP tools, locate it in the",
|
|
4698
4582
|
"`functions.exec` deferred-tool inventory and invoke that exact qualified tool",
|
|
@@ -4701,13 +4585,14 @@ var CODEX_ADDENDUM_CODE_MODE = [
|
|
|
4701
4585
|
"declare the SDK absent without attempting discovery and invocation. The SDK",
|
|
4702
4586
|
"call runs a TypeScript program against the ambient `cabane` object. The",
|
|
4703
4587
|
"turn-control verbs (`ask`, `wake_me`, `summon_agent`, `skip_turn`) are",
|
|
4704
|
-
"qualified `mcp__cabane_companion__\u2026` tools (and may be deferred too)
|
|
4705
|
-
"
|
|
4706
|
-
"
|
|
4707
|
-
"
|
|
4708
|
-
"
|
|
4709
|
-
"
|
|
4710
|
-
"
|
|
4588
|
+
"qualified `mcp__cabane_companion__\u2026` tools (and may be deferred too), as are",
|
|
4589
|
+
"the few ancillary cabane tools (`mcp__cabane__list_workspaces`,",
|
|
4590
|
+
"`mcp__cabane__read_binary`, the upload tools, `mcp__cabane__mint_render_token`).",
|
|
4591
|
+
"There is no Cabane `read`/`write`/`search`/`edit` tool here \u2014 those are `cabane`",
|
|
4592
|
+
"SDK calls inside your program, not tools. If a tool appears in this prompt with",
|
|
4593
|
+
"an `mcp__\u2026__` prefix, preserve that qualified name. Write your closing reply as",
|
|
4594
|
+
"the last thing you say in the turn: you can interleave narration with tool",
|
|
4595
|
+
"calls, but only your final message is recorded as the turn\u2019s reply."
|
|
4711
4596
|
].join(" ");
|
|
4712
4597
|
|
|
4713
4598
|
// packages/agent-runtime/src/codex/events.ts
|
|
@@ -4791,9 +4676,9 @@ function readItemType(item) {
|
|
|
4791
4676
|
function readErrorMessage(ev) {
|
|
4792
4677
|
const direct = str(ev.message);
|
|
4793
4678
|
if (direct) return direct;
|
|
4794
|
-
const
|
|
4795
|
-
if (
|
|
4796
|
-
const m = str(
|
|
4679
|
+
const err = asRecord2(ev.error);
|
|
4680
|
+
if (err) {
|
|
4681
|
+
const m = str(err.message);
|
|
4797
4682
|
if (m) return m;
|
|
4798
4683
|
}
|
|
4799
4684
|
return "unknown";
|
|
@@ -4849,7 +4734,7 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
4849
4734
|
const startedTools = /* @__PURE__ */ new Set();
|
|
4850
4735
|
const finishedTools = /* @__PURE__ */ new Set();
|
|
4851
4736
|
let sessionEmitted = false;
|
|
4852
|
-
let
|
|
4737
|
+
let ok = false;
|
|
4853
4738
|
let reason;
|
|
4854
4739
|
let usage;
|
|
4855
4740
|
let settled = false;
|
|
@@ -4880,7 +4765,7 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
4880
4765
|
const message = readItemMessage(item);
|
|
4881
4766
|
if (isModelMetadataError(message)) {
|
|
4882
4767
|
yield* flushInterim();
|
|
4883
|
-
|
|
4768
|
+
ok = false;
|
|
4884
4769
|
reason = `model_unavailable:${message.slice(0, 200)}`;
|
|
4885
4770
|
settled = true;
|
|
4886
4771
|
break;
|
|
@@ -4935,13 +4820,13 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
4935
4820
|
held = null;
|
|
4936
4821
|
if (sealed) yield sealed;
|
|
4937
4822
|
usage = readUsage(ev);
|
|
4938
|
-
|
|
4823
|
+
ok = true;
|
|
4939
4824
|
settled = true;
|
|
4940
4825
|
break;
|
|
4941
4826
|
}
|
|
4942
4827
|
if (ev.type === "turn.failed" || ev.type === "error") {
|
|
4943
4828
|
yield* flushInterim();
|
|
4944
|
-
|
|
4829
|
+
ok = false;
|
|
4945
4830
|
const text = readErrorMessage(ev);
|
|
4946
4831
|
const failure = classifyErrorText(text);
|
|
4947
4832
|
reason = failure ? encodeFailureReason(failure) : `error:${text.slice(0, 200)}`;
|
|
@@ -4959,7 +4844,7 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
4959
4844
|
const resolvedConfig = ctx.resolvedReasoningEffort ? { reasoningEffort: ctx.resolvedReasoningEffort } : void 0;
|
|
4960
4845
|
yield {
|
|
4961
4846
|
type: "result",
|
|
4962
|
-
ok
|
|
4847
|
+
ok,
|
|
4963
4848
|
...reason ? { reason } : {},
|
|
4964
4849
|
...usage ? { usage } : {},
|
|
4965
4850
|
...ctx.resolvedModel ? { resolvedModel: ctx.resolvedModel } : {},
|
|
@@ -4977,7 +4862,7 @@ function sealHeld2(held, terminal) {
|
|
|
4977
4862
|
}
|
|
4978
4863
|
|
|
4979
4864
|
// packages/agent-runtime/src/codex/policy.ts
|
|
4980
|
-
import { z as
|
|
4865
|
+
import { z as z11 } from "zod";
|
|
4981
4866
|
function codexToolPolicy(policy) {
|
|
4982
4867
|
return policy.hostFs ? {
|
|
4983
4868
|
permissionProfile: "cabane-coding",
|
|
@@ -4992,8 +4877,8 @@ function codexToolPolicy(policy) {
|
|
|
4992
4877
|
};
|
|
4993
4878
|
}
|
|
4994
4879
|
var CODEX_REASONING_EFFORTS = ["minimal", "low", "medium", "high", "xhigh"];
|
|
4995
|
-
var codexDialectSchema =
|
|
4996
|
-
modelReasoningEffort:
|
|
4880
|
+
var codexDialectSchema = z11.object({
|
|
4881
|
+
modelReasoningEffort: z11.enum(CODEX_REASONING_EFFORTS).optional()
|
|
4997
4882
|
}).loose();
|
|
4998
4883
|
function readCodexDialect(runtimeOptions) {
|
|
4999
4884
|
const parsed = codexDialectSchema.safeParse(runtimeOptions?.["codex"] ?? {});
|
|
@@ -5001,7 +4886,7 @@ function readCodexDialect(runtimeOptions) {
|
|
|
5001
4886
|
}
|
|
5002
4887
|
|
|
5003
4888
|
// packages/agent-runtime/src/codex/model.ts
|
|
5004
|
-
var CODEX_DEFAULT_MODEL_BARES = /* @__PURE__ */ new Set(["default"
|
|
4889
|
+
var CODEX_DEFAULT_MODEL_BARES = /* @__PURE__ */ new Set(["default"]);
|
|
5005
4890
|
function parseCodexModel(model) {
|
|
5006
4891
|
const sep = model.indexOf("/");
|
|
5007
4892
|
const bare = sep === -1 || model.slice(0, sep) !== "openai" ? model : model.slice(sep + 1);
|
|
@@ -5016,10 +4901,16 @@ function buildRunSpec2(req, resumeThreadId) {
|
|
|
5016
4901
|
const { policy, config } = req;
|
|
5017
4902
|
const directory = req.local.cwd ?? "";
|
|
5018
4903
|
const dialect = readCodexDialect(config.runtimeOptions);
|
|
4904
|
+
const model = config.model ? parseCodexModel(config.model) : null;
|
|
4905
|
+
if (model === null) {
|
|
4906
|
+
console.warn(
|
|
4907
|
+
`[agent-runtime/codex] CT823: dispatching a subscription turn with NO concrete model (config.model=${JSON.stringify(config.model)}) \u2014 Codex will fall back to its own default. This should be unreachable post-CT823; the model resolver should have supplied a concrete house default.`
|
|
4908
|
+
);
|
|
4909
|
+
}
|
|
5019
4910
|
return {
|
|
5020
4911
|
resumeThreadId,
|
|
5021
4912
|
directory,
|
|
5022
|
-
model
|
|
4913
|
+
model,
|
|
5023
4914
|
policy: codexToolPolicy(policy),
|
|
5024
4915
|
skipGitRepoCheck: true,
|
|
5025
4916
|
...dialect.modelReasoningEffort ? { modelReasoningEffort: dialect.modelReasoningEffort } : {},
|
|
@@ -5088,9 +4979,11 @@ function buildConfig(req) {
|
|
|
5088
4979
|
};
|
|
5089
4980
|
}
|
|
5090
4981
|
const policy = codexToolPolicy(req.policy);
|
|
4982
|
+
const tmpDir = req.local.env?.TMPDIR;
|
|
5091
4983
|
return {
|
|
5092
4984
|
mcp_servers,
|
|
5093
4985
|
experimental_use_rmcp_client: true,
|
|
4986
|
+
...tmpDir ? { shell_environment_policy: { set: { TMPDIR: tmpDir } } } : {},
|
|
5094
4987
|
...policy.permissionProfile ? {
|
|
5095
4988
|
// CT733: named permission profiles are Codex's split-filesystem path.
|
|
5096
4989
|
// `:root = read` preserves coding-mode host reads; the one explicit
|
|
@@ -5368,7 +5261,7 @@ var CodexExec = class {
|
|
|
5368
5261
|
signal: args.signal
|
|
5369
5262
|
});
|
|
5370
5263
|
let spawnError = null;
|
|
5371
|
-
child.once("error", (
|
|
5264
|
+
child.once("error", (err) => spawnError = err);
|
|
5372
5265
|
if (!child.stdin) {
|
|
5373
5266
|
child.kill();
|
|
5374
5267
|
throw new Error("Child process has no stdin");
|
|
@@ -5685,7 +5578,7 @@ function createCodexAdapter(deps = {}) {
|
|
|
5685
5578
|
name: "codex",
|
|
5686
5579
|
// CT614: surface-aware — a code-mode turn (only `code` mounted) is taught
|
|
5687
5580
|
// `code`, not the granular names it no longer has.
|
|
5688
|
-
promptAddendum: (
|
|
5581
|
+
promptAddendum: () => CODEX_ADDENDUM,
|
|
5689
5582
|
dialectSchema: codexDialectSchema,
|
|
5690
5583
|
async *runTurn(req, signal) {
|
|
5691
5584
|
if (!transport) {
|
|
@@ -6272,1029 +6165,8 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6272
6165
|
}
|
|
6273
6166
|
];
|
|
6274
6167
|
|
|
6275
|
-
// packages/agent-runtime/src/
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
You are running on Cabane's own agent runtime. You have a small, curated set of workspace tools, all prefixed \`cabane_\`:
|
|
6279
|
-
|
|
6280
|
-
- \`cabane_list\` \u2014 list a folder's files and subfolders.
|
|
6281
|
-
- \`cabane_read\` \u2014 read one file's contents by path.
|
|
6282
|
-
- \`cabane_search\` \u2014 substring search across file/folder names, file contents, and conversations.
|
|
6283
|
-
- \`cabane_write\` \u2014 create or overwrite a file (pass \`overwrite: true\` to replace).
|
|
6284
|
-
- \`cabane_edit\` \u2014 find/replace inside an existing file.
|
|
6285
|
-
- \`cabane_mkdir\` \u2014 create a folder (pass \`recursive: true\` to also make missing parents).
|
|
6286
|
-
- \`cabane_move\` \u2014 move or rename a file or folder.
|
|
6287
|
-
- \`cabane_delete\` \u2014 delete a file or folder (folders need \`recursive: true\`; soft-delete, recoverable).
|
|
6288
|
-
- \`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.
|
|
6289
|
-
- \`cabane_messages\` \u2014 read a conversation's messages by id (from \`cabane_search\` / \`cabane_context\`).
|
|
6290
|
-
|
|
6291
|
-
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.`;
|
|
6292
|
-
var CABANE_NATIVE_ADDENDUM_CODE_MODE = `## Your tools (native runtime \u2014 code mode)
|
|
6293
|
-
|
|
6294
|
-
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.
|
|
6295
|
-
|
|
6296
|
-
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.
|
|
6297
|
-
|
|
6298
|
-
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):
|
|
6299
|
-
- \`ask\` \u2014 put a structured question to a human and END your turn (they may answer in days).
|
|
6300
|
-
- \`wake_me\` \u2014 end this turn now and be re-dispatched later to check a condition (a PR merging, a reply landing).
|
|
6301
|
-
- \`summon_agent\` \u2014 pull a peer agent into THIS conversation to reply on your turn.
|
|
6302
|
-
- \`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).
|
|
6303
|
-
- \`skip_turn\` \u2014 end your turn with NO reply, when the message doesn't need one from you.
|
|
6304
|
-
- \`mint_render_token\` \u2014 mint a short-lived credential to load a workspace HTML page in a browser.
|
|
6305
|
-
|
|
6306
|
-
Your reply text is streamed straight into the conversation; there is no separate send step.`;
|
|
6307
|
-
|
|
6308
|
-
// packages/agent-runtime/src/cabane-native/context.ts
|
|
6309
|
-
var DEFAULT_HISTORY_LIMIT = 20;
|
|
6310
|
-
var DEFAULT_MAX_HISTORY_CHARS = 24e3;
|
|
6311
|
-
async function assembleMessages(systemPrompt, content, fallbackPrompt, opts) {
|
|
6312
|
-
const messages = [{ role: "system", content: systemPrompt }];
|
|
6313
|
-
const history = await fetchRecentHistory(opts);
|
|
6314
|
-
for (const m of history) messages.push(m);
|
|
6315
|
-
messages.push({ role: "user", content: currentUserText(content, fallbackPrompt) });
|
|
6316
|
-
return messages;
|
|
6317
|
-
}
|
|
6318
|
-
function currentUserText(content, fallbackPrompt) {
|
|
6319
|
-
const text = content.filter((b) => b.type === "text").map((b) => b.text).join("\n").trim();
|
|
6320
|
-
return text.length > 0 ? text : fallbackPrompt;
|
|
6321
|
-
}
|
|
6322
|
-
async function fetchRecentHistory(opts) {
|
|
6323
|
-
const doFetch = opts.fetchImpl ?? fetch;
|
|
6324
|
-
const limit = opts.historyLimit ?? DEFAULT_HISTORY_LIMIT;
|
|
6325
|
-
const url = new URL(
|
|
6326
|
-
`${opts.apiRoot}/workspaces/${opts.workspaceId}/conversations/${opts.conversationId}/messages`
|
|
6327
|
-
);
|
|
6328
|
-
url.searchParams.set("limit", String(limit));
|
|
6329
|
-
url.searchParams.set("order", "desc");
|
|
6330
|
-
let rows;
|
|
6331
|
-
try {
|
|
6332
|
-
const res = await doFetch(url.toString(), {
|
|
6333
|
-
headers: { Authorization: `Bearer ${opts.bearer}` }
|
|
6334
|
-
});
|
|
6335
|
-
if (!res.ok) return [];
|
|
6336
|
-
const body = await res.json();
|
|
6337
|
-
rows = body.messages ?? [];
|
|
6338
|
-
} catch {
|
|
6339
|
-
return [];
|
|
6340
|
-
}
|
|
6341
|
-
const chronological = [...rows].reverse();
|
|
6342
|
-
while (chronological.length > 0 && chronological[chronological.length - 1].role === "user") {
|
|
6343
|
-
chronological.pop();
|
|
6344
|
-
}
|
|
6345
|
-
const mapped = [];
|
|
6346
|
-
for (const r of chronological) {
|
|
6347
|
-
const role = r.role === "agent" ? "assistant" : r.role === "user" ? "user" : null;
|
|
6348
|
-
if (!role) continue;
|
|
6349
|
-
const body = (r.body ?? "").trim();
|
|
6350
|
-
if (body.length === 0) continue;
|
|
6351
|
-
mapped.push({ role, content: body });
|
|
6352
|
-
}
|
|
6353
|
-
return capHistory(mapped, opts.maxHistoryChars ?? DEFAULT_MAX_HISTORY_CHARS);
|
|
6354
|
-
}
|
|
6355
|
-
function capHistory(messages, maxChars) {
|
|
6356
|
-
let total = messages.reduce((n, m) => n + m.content.length, 0);
|
|
6357
|
-
let start2 = 0;
|
|
6358
|
-
while (total > maxChars && start2 < messages.length) {
|
|
6359
|
-
total -= messages[start2].content.length;
|
|
6360
|
-
start2 += 1;
|
|
6361
|
-
}
|
|
6362
|
-
return messages.slice(start2);
|
|
6363
|
-
}
|
|
6364
|
-
|
|
6365
|
-
// packages/agent-runtime/src/cabane-native/model.ts
|
|
6366
|
-
var CABANE_NATIVE_MODEL_PREFIX = "cabane-native/";
|
|
6367
|
-
function parseCabaneNativeModel(model) {
|
|
6368
|
-
return model.startsWith(CABANE_NATIVE_MODEL_PREFIX) ? model.slice(CABANE_NATIVE_MODEL_PREFIX.length) : model;
|
|
6369
|
-
}
|
|
6370
|
-
|
|
6371
|
-
// packages/agent-runtime/src/cabane-native/policy.ts
|
|
6372
|
-
import { z as z13 } from "zod";
|
|
6373
|
-
var NATIVE_SURFACES = ["code", "classic"];
|
|
6374
|
-
var cabaneNativeDialectSchema = z13.object({ surface: z13.enum(NATIVE_SURFACES).optional() }).loose();
|
|
6375
|
-
function readCabaneNativeDialect(runtimeOptions) {
|
|
6376
|
-
const parsed = cabaneNativeDialectSchema.safeParse(runtimeOptions?.["cabane-native"] ?? {});
|
|
6377
|
-
return parsed.success ? parsed.data : {};
|
|
6378
|
-
}
|
|
6379
|
-
function cabaneNativeSurface(runtimeOptions) {
|
|
6380
|
-
return readCabaneNativeDialect(runtimeOptions).surface ?? "code";
|
|
6381
|
-
}
|
|
6382
|
-
|
|
6383
|
-
// packages/agent-runtime/src/cabane-native/tools.ts
|
|
6384
|
-
var TOOL_RESULT_MAX_CHARS = 8e3;
|
|
6385
|
-
var CABANE_NATIVE_TOOLS = [
|
|
6386
|
-
{
|
|
6387
|
-
type: "function",
|
|
6388
|
-
function: {
|
|
6389
|
-
name: "cabane_list",
|
|
6390
|
-
description: "List the files and subfolders at a workspace folder path. Omit `path` for the root.",
|
|
6391
|
-
parameters: {
|
|
6392
|
-
type: "object",
|
|
6393
|
-
properties: {
|
|
6394
|
-
path: {
|
|
6395
|
-
type: "string",
|
|
6396
|
-
description: "Workspace folder path, e.g. /notes. Defaults to /."
|
|
6397
|
-
}
|
|
6398
|
-
}
|
|
6399
|
-
}
|
|
6400
|
-
}
|
|
6401
|
-
},
|
|
6402
|
-
{
|
|
6403
|
-
type: "function",
|
|
6404
|
-
function: {
|
|
6405
|
-
name: "cabane_read",
|
|
6406
|
-
description: "Read the contents of one file at a workspace path.",
|
|
6407
|
-
parameters: {
|
|
6408
|
-
type: "object",
|
|
6409
|
-
properties: {
|
|
6410
|
-
path: { type: "string", description: "Workspace file path, e.g. /notes/todo.md." }
|
|
6411
|
-
},
|
|
6412
|
-
required: ["path"]
|
|
6413
|
-
}
|
|
6414
|
-
}
|
|
6415
|
-
},
|
|
6416
|
-
{
|
|
6417
|
-
type: "function",
|
|
6418
|
-
function: {
|
|
6419
|
-
name: "cabane_search",
|
|
6420
|
-
description: "Case-insensitive substring search across file names and file contents. Optionally scope to a subtree with `path`.",
|
|
6421
|
-
parameters: {
|
|
6422
|
-
type: "object",
|
|
6423
|
-
properties: {
|
|
6424
|
-
q: { type: "string", description: "The search string." },
|
|
6425
|
-
path: {
|
|
6426
|
-
type: "string",
|
|
6427
|
-
description: "Optional workspace subtree to scope the search to."
|
|
6428
|
-
}
|
|
6429
|
-
},
|
|
6430
|
-
required: ["q"]
|
|
6431
|
-
}
|
|
6432
|
-
}
|
|
6433
|
-
},
|
|
6434
|
-
{
|
|
6435
|
-
type: "function",
|
|
6436
|
-
function: {
|
|
6437
|
-
name: "cabane_write",
|
|
6438
|
-
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.",
|
|
6439
|
-
parameters: {
|
|
6440
|
-
type: "object",
|
|
6441
|
-
properties: {
|
|
6442
|
-
path: { type: "string", description: "Workspace file path, e.g. /notes/new.md." },
|
|
6443
|
-
content: { type: "string", description: "The file contents." },
|
|
6444
|
-
overwrite: { type: "boolean", description: "Replace an existing file (default false)." }
|
|
6445
|
-
},
|
|
6446
|
-
required: ["path", "content"]
|
|
6447
|
-
}
|
|
6448
|
-
}
|
|
6449
|
-
},
|
|
6450
|
-
{
|
|
6451
|
-
type: "function",
|
|
6452
|
-
function: {
|
|
6453
|
-
name: "cabane_edit",
|
|
6454
|
-
description: "Modify an existing file with a single find/replace. By default `find` must occur exactly once; set `replaceAll: true` to replace every occurrence.",
|
|
6455
|
-
parameters: {
|
|
6456
|
-
type: "object",
|
|
6457
|
-
properties: {
|
|
6458
|
-
path: { type: "string", description: "Workspace file path to edit." },
|
|
6459
|
-
find: { type: "string", description: "The substring to find." },
|
|
6460
|
-
replace: { type: "string", description: "The replacement." },
|
|
6461
|
-
replaceAll: { type: "boolean", description: "Replace every occurrence (default false)." }
|
|
6462
|
-
},
|
|
6463
|
-
required: ["path", "find", "replace"]
|
|
6464
|
-
}
|
|
6465
|
-
}
|
|
6466
|
-
},
|
|
6467
|
-
{
|
|
6468
|
-
type: "function",
|
|
6469
|
-
function: {
|
|
6470
|
-
name: "cabane_mkdir",
|
|
6471
|
-
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.",
|
|
6472
|
-
parameters: {
|
|
6473
|
-
type: "object",
|
|
6474
|
-
properties: {
|
|
6475
|
-
path: { type: "string", description: "Workspace folder path, e.g. /notes/archive." },
|
|
6476
|
-
recursive: {
|
|
6477
|
-
type: "boolean",
|
|
6478
|
-
description: "Create missing parent folders too (default false)."
|
|
6479
|
-
}
|
|
6480
|
-
},
|
|
6481
|
-
required: ["path"]
|
|
6482
|
-
}
|
|
6483
|
-
}
|
|
6484
|
-
},
|
|
6485
|
-
{
|
|
6486
|
-
type: "function",
|
|
6487
|
-
function: {
|
|
6488
|
-
name: "cabane_move",
|
|
6489
|
-
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.",
|
|
6490
|
-
parameters: {
|
|
6491
|
-
type: "object",
|
|
6492
|
-
properties: {
|
|
6493
|
-
fromPath: { type: "string", description: "The current file/folder path." },
|
|
6494
|
-
toPath: { type: "string", description: "The new full path (parent + name)." }
|
|
6495
|
-
},
|
|
6496
|
-
required: ["fromPath", "toPath"]
|
|
6497
|
-
}
|
|
6498
|
-
}
|
|
6499
|
-
},
|
|
6500
|
-
{
|
|
6501
|
-
type: "function",
|
|
6502
|
-
function: {
|
|
6503
|
-
name: "cabane_delete",
|
|
6504
|
-
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.",
|
|
6505
|
-
parameters: {
|
|
6506
|
-
type: "object",
|
|
6507
|
-
properties: {
|
|
6508
|
-
path: { type: "string", description: "Workspace file/folder path to delete." },
|
|
6509
|
-
recursive: {
|
|
6510
|
-
type: "boolean",
|
|
6511
|
-
description: "Required to delete a non-empty folder (default false)."
|
|
6512
|
-
}
|
|
6513
|
-
},
|
|
6514
|
-
required: ["path"]
|
|
6515
|
-
}
|
|
6516
|
-
}
|
|
6517
|
-
},
|
|
6518
|
-
{
|
|
6519
|
-
type: "function",
|
|
6520
|
-
function: {
|
|
6521
|
-
name: "cabane_context",
|
|
6522
|
-
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`.",
|
|
6523
|
-
parameters: {
|
|
6524
|
-
type: "object",
|
|
6525
|
-
properties: {
|
|
6526
|
-
kind: {
|
|
6527
|
-
type: "string",
|
|
6528
|
-
enum: ["conversation", "file", "folder", "user", "agent", "channel"],
|
|
6529
|
-
description: "The kind of object to orient around."
|
|
6530
|
-
},
|
|
6531
|
-
ref: {
|
|
6532
|
-
type: "string",
|
|
6533
|
-
description: "The object id (or the workspace path for a file/folder)."
|
|
6534
|
-
}
|
|
6535
|
-
},
|
|
6536
|
-
required: ["kind", "ref"]
|
|
6537
|
-
}
|
|
6538
|
-
}
|
|
6539
|
-
},
|
|
6540
|
-
{
|
|
6541
|
-
type: "function",
|
|
6542
|
-
function: {
|
|
6543
|
-
name: "cabane_messages",
|
|
6544
|
-
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.',
|
|
6545
|
-
parameters: {
|
|
6546
|
-
type: "object",
|
|
6547
|
-
properties: {
|
|
6548
|
-
conversationId: { type: "string", description: "The conversation id to read." },
|
|
6549
|
-
order: {
|
|
6550
|
-
type: "string",
|
|
6551
|
-
enum: ["asc", "desc"],
|
|
6552
|
-
description: "newest-first (desc, default) or oldest-first (asc)."
|
|
6553
|
-
},
|
|
6554
|
-
limit: {
|
|
6555
|
-
type: "number",
|
|
6556
|
-
description: "Max messages to return (default 50, max 100)."
|
|
6557
|
-
}
|
|
6558
|
-
},
|
|
6559
|
-
required: ["conversationId"]
|
|
6560
|
-
}
|
|
6561
|
-
}
|
|
6562
|
-
}
|
|
6563
|
-
];
|
|
6564
|
-
var CABANE_NATIVE_CODE_TOOL = {
|
|
6565
|
-
type: "function",
|
|
6566
|
-
function: {
|
|
6567
|
-
name: "sdk",
|
|
6568
|
-
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.",
|
|
6569
|
-
parameters: {
|
|
6570
|
-
type: "object",
|
|
6571
|
-
properties: {
|
|
6572
|
-
code: {
|
|
6573
|
-
type: "string",
|
|
6574
|
-
description: "The TypeScript program to run (an async function body)."
|
|
6575
|
-
}
|
|
6576
|
-
},
|
|
6577
|
-
required: ["code"]
|
|
6578
|
-
}
|
|
6579
|
-
}
|
|
6580
|
-
};
|
|
6581
|
-
var CABANE_NATIVE_RENDER_TOKEN_TOOL = {
|
|
6582
|
-
type: "function",
|
|
6583
|
-
function: {
|
|
6584
|
-
name: "mint_render_token",
|
|
6585
|
-
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.",
|
|
6586
|
-
parameters: {
|
|
6587
|
-
type: "object",
|
|
6588
|
-
properties: {
|
|
6589
|
-
pathPrefix: {
|
|
6590
|
-
type: "string",
|
|
6591
|
-
description: "Workspace-rooted path-prefix the token authorizes; `/` for whole-workspace."
|
|
6592
|
-
},
|
|
6593
|
-
ttlSeconds: {
|
|
6594
|
-
type: "number",
|
|
6595
|
-
description: "Override the default 30-minute TTL; capped at 24h (86400s)."
|
|
6596
|
-
}
|
|
6597
|
-
}
|
|
6598
|
-
}
|
|
6599
|
-
}
|
|
6600
|
-
};
|
|
6601
|
-
function summarizeCabaneToolArgs(name, args) {
|
|
6602
|
-
if (name === "mint_render_token") return str2(args.pathPrefix) ?? "/";
|
|
6603
|
-
if (name === "sdk") {
|
|
6604
|
-
const code = str2(args.code) ?? "";
|
|
6605
|
-
const firstLine = code.split("\n").find((l) => l.trim().length > 0)?.trim() ?? "";
|
|
6606
|
-
return firstLine.length > 80 ? `${firstLine.slice(0, 80)}\u2026` : firstLine;
|
|
6607
|
-
}
|
|
6608
|
-
if (name === "cabane_search") return str2(args.q) ?? "";
|
|
6609
|
-
if (name === "cabane_move") {
|
|
6610
|
-
const from = str2(args.fromPath) ?? "";
|
|
6611
|
-
const to = str2(args.toPath) ?? "";
|
|
6612
|
-
return from && to ? `${from} \u2192 ${to}` : from || to;
|
|
6613
|
-
}
|
|
6614
|
-
if (name === "cabane_context") return str2(args.ref) ?? "";
|
|
6615
|
-
if (name === "cabane_messages") return str2(args.conversationId) ?? "";
|
|
6616
|
-
return str2(args.path) ?? "";
|
|
6617
|
-
}
|
|
6618
|
-
async function executeCabaneNativeTool(name, args, ctx, signal) {
|
|
6619
|
-
const doFetch = ctx.fetchImpl ?? fetch;
|
|
6620
|
-
const wsBase = `${ctx.apiRoot}/workspaces/${ctx.workspaceId}`;
|
|
6621
|
-
const headers = { Authorization: `Bearer ${ctx.bearer}`, "Content-Type": "application/json" };
|
|
6622
|
-
const call = async (method, path3, init2) => {
|
|
6623
|
-
const url = new URL(`${wsBase}${path3}`);
|
|
6624
|
-
for (const [k, v] of Object.entries(init2?.query ?? {})) {
|
|
6625
|
-
if (v !== void 0) url.searchParams.set(k, v);
|
|
6626
|
-
}
|
|
6627
|
-
let res;
|
|
6628
|
-
try {
|
|
6629
|
-
res = await doFetch(url.toString(), {
|
|
6630
|
-
method,
|
|
6631
|
-
headers,
|
|
6632
|
-
...init2?.body !== void 0 ? { body: JSON.stringify(init2.body) } : {},
|
|
6633
|
-
signal
|
|
6634
|
-
});
|
|
6635
|
-
} catch (err2) {
|
|
6636
|
-
return {
|
|
6637
|
-
ok: false,
|
|
6638
|
-
result: `error: request failed: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
6639
|
-
};
|
|
6640
|
-
}
|
|
6641
|
-
const contentType = res.headers.get("content-type") ?? "";
|
|
6642
|
-
const text = contentType.includes("application/json") ? JSON.stringify(await res.json().catch(() => ({}))) : await res.text().catch(() => "");
|
|
6643
|
-
if (!res.ok) return { ok: false, result: truncate2(`error ${res.status}: ${text}`) };
|
|
6644
|
-
return { ok: true, result: truncate2(text) };
|
|
6645
|
-
};
|
|
6646
|
-
switch (name) {
|
|
6647
|
-
case "sdk":
|
|
6648
|
-
return call("POST", "/sdk", {
|
|
6649
|
-
body: {
|
|
6650
|
-
program: str2(args.code) ?? "",
|
|
6651
|
-
...ctx.conversationId ? { activeConversationId: ctx.conversationId } : {}
|
|
6652
|
-
}
|
|
6653
|
-
});
|
|
6654
|
-
case "mint_render_token":
|
|
6655
|
-
return call("POST", "/render-tokens", {
|
|
6656
|
-
body: {
|
|
6657
|
-
pathPrefix: str2(args.pathPrefix) ?? "/",
|
|
6658
|
-
...typeof args.ttlSeconds === "number" ? { ttlSeconds: args.ttlSeconds } : {}
|
|
6659
|
-
}
|
|
6660
|
-
});
|
|
6661
|
-
case "cabane_list":
|
|
6662
|
-
return call("GET", "/files/tree", { query: { path: str2(args.path) ?? "/" } });
|
|
6663
|
-
case "cabane_read":
|
|
6664
|
-
return call("GET", "/files/content", { query: { path: str2(args.path) } });
|
|
6665
|
-
case "cabane_search":
|
|
6666
|
-
return call("GET", "/search", { query: { q: str2(args.q), path: str2(args.path) } });
|
|
6667
|
-
case "cabane_write": {
|
|
6668
|
-
const overwrite = args.overwrite === true;
|
|
6669
|
-
return overwrite ? call("PUT", "/files/content", {
|
|
6670
|
-
body: { path: str2(args.path), content: str2(args.content) }
|
|
6671
|
-
}) : call("POST", "/files", {
|
|
6672
|
-
body: { path: str2(args.path), content: str2(args.content), mkdirs: true }
|
|
6673
|
-
});
|
|
6674
|
-
}
|
|
6675
|
-
case "cabane_edit":
|
|
6676
|
-
return call("PATCH", "/files", {
|
|
6677
|
-
body: {
|
|
6678
|
-
path: str2(args.path),
|
|
6679
|
-
find: str2(args.find),
|
|
6680
|
-
replace: str2(args.replace) ?? "",
|
|
6681
|
-
...args.replaceAll === true ? { replaceAll: true } : {}
|
|
6682
|
-
}
|
|
6683
|
-
});
|
|
6684
|
-
case "cabane_mkdir":
|
|
6685
|
-
return call("POST", "/folders", {
|
|
6686
|
-
body: {
|
|
6687
|
-
path: str2(args.path),
|
|
6688
|
-
...args.recursive === true ? { recursive: true } : {}
|
|
6689
|
-
}
|
|
6690
|
-
});
|
|
6691
|
-
case "cabane_move":
|
|
6692
|
-
return call("POST", "/entries/move", {
|
|
6693
|
-
body: { fromPath: str2(args.fromPath), toPath: str2(args.toPath) }
|
|
6694
|
-
});
|
|
6695
|
-
case "cabane_delete":
|
|
6696
|
-
return call("DELETE", "/entries", {
|
|
6697
|
-
query: {
|
|
6698
|
-
path: str2(args.path),
|
|
6699
|
-
recursive: args.recursive === true ? "true" : void 0
|
|
6700
|
-
}
|
|
6701
|
-
});
|
|
6702
|
-
case "cabane_context":
|
|
6703
|
-
return call("POST", "/graph/query", {
|
|
6704
|
-
body: { preset: "orient", args: { kind: str2(args.kind), ref: str2(args.ref) } }
|
|
6705
|
-
});
|
|
6706
|
-
case "cabane_messages":
|
|
6707
|
-
return call(
|
|
6708
|
-
"GET",
|
|
6709
|
-
`/conversations/${encodeURIComponent(str2(args.conversationId) ?? "")}/messages`,
|
|
6710
|
-
{
|
|
6711
|
-
query: {
|
|
6712
|
-
order: str2(args.order),
|
|
6713
|
-
limit: typeof args.limit === "number" ? String(args.limit) : void 0
|
|
6714
|
-
}
|
|
6715
|
-
}
|
|
6716
|
-
);
|
|
6717
|
-
default:
|
|
6718
|
-
return { ok: false, result: `error: unknown tool "${name}"` };
|
|
6719
|
-
}
|
|
6720
|
-
}
|
|
6721
|
-
function str2(v) {
|
|
6722
|
-
return typeof v === "string" ? v : void 0;
|
|
6723
|
-
}
|
|
6724
|
-
function truncate2(s) {
|
|
6725
|
-
return s.length > TOOL_RESULT_MAX_CHARS ? `${s.slice(0, TOOL_RESULT_MAX_CHARS)}
|
|
6726
|
-
\u2026 [truncated]` : s;
|
|
6727
|
-
}
|
|
6728
|
-
|
|
6729
|
-
// packages/agent-runtime/src/cabane-native/turn-control.ts
|
|
6730
|
-
function describeSubAgentError(status2, body) {
|
|
6731
|
-
const code = body && typeof body === "object" && "error" in body ? String(body.error) : void 0;
|
|
6732
|
-
switch (code) {
|
|
6733
|
-
case "callout_cap_exceeded":
|
|
6734
|
-
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.";
|
|
6735
|
-
case "callout_depth_exceeded":
|
|
6736
|
-
return "sub_agent: this would nest sub-agents too deep (max 3 levels). Have the current worker report back rather than spawning another layer.";
|
|
6737
|
-
case "dispatch_agent_not_found":
|
|
6738
|
-
return "sub_agent: no live agent in this workspace matches that `agentId`. Check `list_agents`, or omit `agentId` to spawn yourself.";
|
|
6739
|
-
case "dispatch_return_requires_turn":
|
|
6740
|
-
case "dispatch_return_requires_agent":
|
|
6741
|
-
case "dispatch_return_requires_dispatch":
|
|
6742
|
-
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.`;
|
|
6743
|
-
default:
|
|
6744
|
-
return `sub_agent: the spawn failed (${code ?? `HTTP ${status2}`}).`;
|
|
6745
|
-
}
|
|
6746
|
-
}
|
|
6747
|
-
var CABANE_NATIVE_TURN_CONTROL_TOOLS = [
|
|
6748
|
-
{
|
|
6749
|
-
type: "function",
|
|
6750
|
-
function: {
|
|
6751
|
-
name: "ask",
|
|
6752
|
-
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.",
|
|
6753
|
-
parameters: {
|
|
6754
|
-
type: "object",
|
|
6755
|
-
properties: {
|
|
6756
|
-
targetUserId: {
|
|
6757
|
-
type: "string",
|
|
6758
|
-
description: "The workspace member (human) to ask \u2014 a user id from the roster."
|
|
6759
|
-
},
|
|
6760
|
-
question: {
|
|
6761
|
-
type: "string",
|
|
6762
|
-
description: "Single-question form: a short body of framing (one or two sentences)."
|
|
6763
|
-
},
|
|
6764
|
-
headline: {
|
|
6765
|
-
type: "string",
|
|
6766
|
-
description: "Single-question form: the question itself as one clear capitalized sentence ending in `?`."
|
|
6767
|
-
},
|
|
6768
|
-
options: {
|
|
6769
|
-
type: "array",
|
|
6770
|
-
items: { type: "string" },
|
|
6771
|
-
description: "Single-question form: 2\u20134 suggested one-click answers."
|
|
6772
|
-
},
|
|
6773
|
-
questions: {
|
|
6774
|
-
type: "array",
|
|
6775
|
-
items: {
|
|
6776
|
-
type: "object",
|
|
6777
|
-
properties: {
|
|
6778
|
-
headline: { type: "string" },
|
|
6779
|
-
body: { type: "string" },
|
|
6780
|
-
options: { type: "array", items: { type: "string" } }
|
|
6781
|
-
},
|
|
6782
|
-
required: ["headline"]
|
|
6783
|
-
},
|
|
6784
|
-
description: "Multi-question form: 1\u20135 questions to ask at once."
|
|
6785
|
-
}
|
|
6786
|
-
},
|
|
6787
|
-
required: ["targetUserId"]
|
|
6788
|
-
}
|
|
6789
|
-
}
|
|
6790
|
-
},
|
|
6791
|
-
{
|
|
6792
|
-
type: "function",
|
|
6793
|
-
function: {
|
|
6794
|
-
name: "wake_me",
|
|
6795
|
-
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.',
|
|
6796
|
-
parameters: {
|
|
6797
|
-
type: "object",
|
|
6798
|
-
properties: {
|
|
6799
|
-
afterSeconds: {
|
|
6800
|
-
type: "number",
|
|
6801
|
-
description: "Relative delay in seconds from when this turn ends (floor 60)."
|
|
6802
|
-
},
|
|
6803
|
-
at: {
|
|
6804
|
-
type: "string",
|
|
6805
|
-
description: "Absolute ISO-8601 timestamp with a zone (e.g. 2026-07-16T09:00:00-07:00)."
|
|
6806
|
-
},
|
|
6807
|
-
note: {
|
|
6808
|
-
type: "string",
|
|
6809
|
-
description: "A note to your future self \u2014 becomes the wake body (the condition to re-check)."
|
|
6810
|
-
}
|
|
6811
|
-
},
|
|
6812
|
-
required: ["note"]
|
|
6813
|
-
}
|
|
6814
|
-
}
|
|
6815
|
-
},
|
|
6816
|
-
{
|
|
6817
|
-
type: "function",
|
|
6818
|
-
function: {
|
|
6819
|
-
name: "summon_agent",
|
|
6820
|
-
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.",
|
|
6821
|
-
parameters: {
|
|
6822
|
-
type: "object",
|
|
6823
|
-
properties: {
|
|
6824
|
-
agentId: {
|
|
6825
|
-
type: "string",
|
|
6826
|
-
description: "The peer agent to summon \u2014 a workspace agent id from the roster."
|
|
6827
|
-
}
|
|
6828
|
-
},
|
|
6829
|
-
required: ["agentId"]
|
|
6830
|
-
}
|
|
6831
|
-
}
|
|
6832
|
-
},
|
|
6833
|
-
{
|
|
6834
|
-
type: "function",
|
|
6835
|
-
function: {
|
|
6836
|
-
name: "sub_agent",
|
|
6837
|
-
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.",
|
|
6838
|
-
parameters: {
|
|
6839
|
-
type: "object",
|
|
6840
|
-
properties: {
|
|
6841
|
-
prompt: {
|
|
6842
|
-
type: "string",
|
|
6843
|
-
description: "The sub-agent's self-contained opening instruction."
|
|
6844
|
-
},
|
|
6845
|
-
agentId: {
|
|
6846
|
-
type: "string",
|
|
6847
|
-
description: "Optional peer to run the sub-agent as; omit to spawn yourself."
|
|
6848
|
-
},
|
|
6849
|
-
title: { type: "string", description: "Optional title for the child thread." }
|
|
6850
|
-
},
|
|
6851
|
-
required: ["prompt"]
|
|
6852
|
-
}
|
|
6853
|
-
}
|
|
6854
|
-
},
|
|
6855
|
-
{
|
|
6856
|
-
type: "function",
|
|
6857
|
-
function: {
|
|
6858
|
-
name: "skip_turn",
|
|
6859
|
-
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.",
|
|
6860
|
-
parameters: {
|
|
6861
|
-
type: "object",
|
|
6862
|
-
properties: {
|
|
6863
|
-
reason: {
|
|
6864
|
-
type: "string",
|
|
6865
|
-
description: "Short reason you are declining \u2014 used for telemetry."
|
|
6866
|
-
}
|
|
6867
|
-
},
|
|
6868
|
-
required: ["reason"]
|
|
6869
|
-
}
|
|
6870
|
-
}
|
|
6871
|
-
}
|
|
6872
|
-
];
|
|
6873
|
-
var CABANE_NATIVE_TURN_CONTROL_NAMES = new Set(
|
|
6874
|
-
CABANE_NATIVE_TURN_CONTROL_TOOLS.map((t) => t.function.name)
|
|
6875
|
-
);
|
|
6876
|
-
function summarizeTurnControlArgs(name, args) {
|
|
6877
|
-
if (name === "ask") return str3(args.headline) ?? str3(args.question) ?? "";
|
|
6878
|
-
if (name === "wake_me") return str3(args.note) ?? "";
|
|
6879
|
-
if (name === "summon_agent") return str3(args.agentId) ?? "";
|
|
6880
|
-
if (name === "sub_agent") return str3(args.title) ?? truncateHead(str3(args.prompt) ?? "", 60);
|
|
6881
|
-
if (name === "skip_turn") return str3(args.reason) ?? "";
|
|
6882
|
-
return "";
|
|
6883
|
-
}
|
|
6884
|
-
async function executeNativeTurnControl(name, args, tc) {
|
|
6885
|
-
switch (name) {
|
|
6886
|
-
case "summon_agent": {
|
|
6887
|
-
const agentId = str3(args.agentId);
|
|
6888
|
-
if (!agentId) return err("summon_agent: `agentId` is required.");
|
|
6889
|
-
tc.summon(agentId);
|
|
6890
|
-
return ok({ summoned: agentId });
|
|
6891
|
-
}
|
|
6892
|
-
case "skip_turn": {
|
|
6893
|
-
const reason = str3(args.reason);
|
|
6894
|
-
if (!reason) return err("skip_turn: `reason` is required.");
|
|
6895
|
-
tc.skip(reason);
|
|
6896
|
-
return ok({ skipped: true });
|
|
6897
|
-
}
|
|
6898
|
-
case "ask": {
|
|
6899
|
-
const targetUserId = str3(args.targetUserId);
|
|
6900
|
-
if (!targetUserId) return err("ask: `targetUserId` is required.");
|
|
6901
|
-
const hasSingle = args.question !== void 0;
|
|
6902
|
-
const hasArray = Array.isArray(args.questions) && args.questions.length > 0;
|
|
6903
|
-
if (hasSingle && hasArray)
|
|
6904
|
-
return err("ask: provide either `question` or `questions`, not both.");
|
|
6905
|
-
if (!hasSingle && !hasArray) return err("ask: provide `question` or `questions`.");
|
|
6906
|
-
const payload = hasArray ? { targetUserId, questions: args.questions } : {
|
|
6907
|
-
targetUserId,
|
|
6908
|
-
question: str3(args.question),
|
|
6909
|
-
...str3(args.headline) ? { headline: str3(args.headline) } : {},
|
|
6910
|
-
...Array.isArray(args.options) ? { options: args.options } : {}
|
|
6911
|
-
};
|
|
6912
|
-
tc.ask(payload);
|
|
6913
|
-
return ok({ asked: targetUserId });
|
|
6914
|
-
}
|
|
6915
|
-
case "wake_me": {
|
|
6916
|
-
const note = str3(args.note);
|
|
6917
|
-
if (!note) return err("wake_me: `note` is required.");
|
|
6918
|
-
const hasAfter = typeof args.afterSeconds === "number";
|
|
6919
|
-
const hasAt = typeof args.at === "string";
|
|
6920
|
-
if (hasAfter && hasAt)
|
|
6921
|
-
return err("wake_me: provide either `afterSeconds` or `at`, not both.");
|
|
6922
|
-
if (!hasAfter && !hasAt) return err("wake_me: provide `afterSeconds` or `at`.");
|
|
6923
|
-
tc.wake({
|
|
6924
|
-
...hasAfter ? { afterSeconds: args.afterSeconds } : {},
|
|
6925
|
-
...hasAt ? { at: args.at } : {},
|
|
6926
|
-
note
|
|
6927
|
-
});
|
|
6928
|
-
return ok({ armed: hasAt ? { at: args.at } : { afterSeconds: args.afterSeconds } });
|
|
6929
|
-
}
|
|
6930
|
-
case "sub_agent": {
|
|
6931
|
-
const prompt = str3(args.prompt);
|
|
6932
|
-
if (!prompt) return err("sub_agent: `prompt` is required.");
|
|
6933
|
-
const result = await tc.subAgent({
|
|
6934
|
-
prompt,
|
|
6935
|
-
...str3(args.agentId) ? { agentId: str3(args.agentId) } : {},
|
|
6936
|
-
...str3(args.title) ? { title: str3(args.title) } : {}
|
|
6937
|
-
});
|
|
6938
|
-
if (!result.ok) return err(result.error);
|
|
6939
|
-
return ok({
|
|
6940
|
-
conversationId: result.conversationId,
|
|
6941
|
-
note: "Spawned. Don't wait \u2014 finish what else this turn can do, then end your turn; the result posts back here."
|
|
6942
|
-
});
|
|
6943
|
-
}
|
|
6944
|
-
default:
|
|
6945
|
-
return err(`unknown turn-control tool "${name}"`);
|
|
6946
|
-
}
|
|
6947
|
-
}
|
|
6948
|
-
function ok(payload) {
|
|
6949
|
-
return { ok: true, result: JSON.stringify(payload) };
|
|
6950
|
-
}
|
|
6951
|
-
function err(message) {
|
|
6952
|
-
return { ok: false, result: `error: ${message}` };
|
|
6953
|
-
}
|
|
6954
|
-
function str3(v) {
|
|
6955
|
-
return typeof v === "string" ? v : void 0;
|
|
6956
|
-
}
|
|
6957
|
-
function truncateHead(s, n) {
|
|
6958
|
-
return s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
6959
|
-
}
|
|
6960
|
-
|
|
6961
|
-
// packages/agent-runtime/src/cabane-native/loop.ts
|
|
6962
|
-
var DEFAULT_MAX_ITERATIONS = 12;
|
|
6963
|
-
async function* runCabaneNativeTurn(req, signal, deps) {
|
|
6964
|
-
if (!req.config.model) {
|
|
6965
|
-
yield { type: "result", ok: false, reason: "no_model" };
|
|
6966
|
-
return;
|
|
6967
|
-
}
|
|
6968
|
-
const model = parseCabaneNativeModel(req.config.model);
|
|
6969
|
-
const surface = cabaneNativeSurface(req.config.runtimeOptions);
|
|
6970
|
-
const turnControl = req.extra.turnControl;
|
|
6971
|
-
const tools = surface === "code" ? [
|
|
6972
|
-
CABANE_NATIVE_CODE_TOOL,
|
|
6973
|
-
CABANE_NATIVE_RENDER_TOKEN_TOOL,
|
|
6974
|
-
...turnControl ? CABANE_NATIVE_TURN_CONTROL_TOOLS : []
|
|
6975
|
-
] : CABANE_NATIVE_TOOLS;
|
|
6976
|
-
const toolCtx = {
|
|
6977
|
-
apiRoot: deps.apiRoot,
|
|
6978
|
-
workspaceId: deps.workspaceId,
|
|
6979
|
-
bearer: deps.bearer,
|
|
6980
|
-
// CT666: the origin conversation, forwarded by the `sdk` tool to the `/sdk`
|
|
6981
|
-
// endpoint so a code-mode program's returning callout binds to this turn.
|
|
6982
|
-
...deps.conversationId ? { conversationId: deps.conversationId } : {},
|
|
6983
|
-
...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
|
|
6984
|
-
};
|
|
6985
|
-
const messages = await assembleMessages(req.systemPrompt, req.content, req.prompt, {
|
|
6986
|
-
apiRoot: deps.apiRoot,
|
|
6987
|
-
workspaceId: deps.workspaceId,
|
|
6988
|
-
bearer: deps.bearer,
|
|
6989
|
-
conversationId: deps.conversationId,
|
|
6990
|
-
...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {},
|
|
6991
|
-
...deps.historyLimit !== void 0 ? { historyLimit: deps.historyLimit } : {}
|
|
6992
|
-
});
|
|
6993
|
-
if (signal.aborted) return;
|
|
6994
|
-
const maxIterations = deps.maxIterations ?? DEFAULT_MAX_ITERATIONS;
|
|
6995
|
-
let usage;
|
|
6996
|
-
let resolvedModel = model;
|
|
6997
|
-
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
6998
|
-
let text = "";
|
|
6999
|
-
const toolAcc = /* @__PURE__ */ new Map();
|
|
7000
|
-
let finishReason;
|
|
7001
|
-
let errored2;
|
|
7002
|
-
for await (const ev of deps.provider.stream({ model, messages, tools }, signal)) {
|
|
7003
|
-
if (signal.aborted) return;
|
|
7004
|
-
switch (ev.type) {
|
|
7005
|
-
case "text":
|
|
7006
|
-
text += ev.delta;
|
|
7007
|
-
break;
|
|
7008
|
-
case "tool_call": {
|
|
7009
|
-
const cur = toolAcc.get(ev.index) ?? { id: `call_${ev.index}`, name: "", args: "" };
|
|
7010
|
-
if (ev.id) cur.id = ev.id;
|
|
7011
|
-
if (ev.name) cur.name = ev.name;
|
|
7012
|
-
if (ev.argumentsDelta) cur.args += ev.argumentsDelta;
|
|
7013
|
-
toolAcc.set(ev.index, cur);
|
|
7014
|
-
break;
|
|
7015
|
-
}
|
|
7016
|
-
case "usage":
|
|
7017
|
-
usage = {
|
|
7018
|
-
inputTokens: ev.inputTokens,
|
|
7019
|
-
outputTokens: ev.outputTokens,
|
|
7020
|
-
contextTokens: ev.inputTokens
|
|
7021
|
-
};
|
|
7022
|
-
break;
|
|
7023
|
-
case "model":
|
|
7024
|
-
resolvedModel = ev.model;
|
|
7025
|
-
break;
|
|
7026
|
-
case "error":
|
|
7027
|
-
errored2 = ev.message;
|
|
7028
|
-
break;
|
|
7029
|
-
case "done":
|
|
7030
|
-
finishReason = ev.finishReason;
|
|
7031
|
-
break;
|
|
7032
|
-
}
|
|
7033
|
-
}
|
|
7034
|
-
if (signal.aborted) return;
|
|
7035
|
-
if (errored2 !== void 0) {
|
|
7036
|
-
const sealed = sealText(text, false);
|
|
7037
|
-
if (sealed) yield sealed;
|
|
7038
|
-
const failure = classifyErrorText(errored2);
|
|
7039
|
-
yield {
|
|
7040
|
-
type: "result",
|
|
7041
|
-
ok: false,
|
|
7042
|
-
reason: failure ? encodeFailureReason(failure) : `error:${errored2.slice(0, 200)}`,
|
|
7043
|
-
...usage ? { usage } : {},
|
|
7044
|
-
...resolvedModel ? { resolvedModel } : {}
|
|
7045
|
-
};
|
|
7046
|
-
return;
|
|
7047
|
-
}
|
|
7048
|
-
const toolCalls = [...toolAcc.entries()].sort((a, b) => a[0] - b[0]).map(([, v]) => v);
|
|
7049
|
-
if (toolCalls.length === 0) {
|
|
7050
|
-
const sealed = sealText(text, true);
|
|
7051
|
-
if (sealed) yield sealed;
|
|
7052
|
-
yield {
|
|
7053
|
-
type: "result",
|
|
7054
|
-
ok: true,
|
|
7055
|
-
...usage ? { usage } : {},
|
|
7056
|
-
...resolvedModel ? { resolvedModel } : {}
|
|
7057
|
-
};
|
|
7058
|
-
return;
|
|
7059
|
-
}
|
|
7060
|
-
const sealedInterim = sealText(text, false);
|
|
7061
|
-
if (sealedInterim) yield sealedInterim;
|
|
7062
|
-
const assistantToolCalls = toolCalls.map((t) => ({
|
|
7063
|
-
id: t.id,
|
|
7064
|
-
type: "function",
|
|
7065
|
-
function: { name: t.name, arguments: t.args || "{}" }
|
|
7066
|
-
}));
|
|
7067
|
-
messages.push({ role: "assistant", content: text, tool_calls: assistantToolCalls });
|
|
7068
|
-
for (const t of toolCalls) {
|
|
7069
|
-
if (signal.aborted) return;
|
|
7070
|
-
const args = parseArgs(t.args);
|
|
7071
|
-
const displayName = prettyToolName(t.name);
|
|
7072
|
-
const isTurnControl = CABANE_NATIVE_TURN_CONTROL_NAMES.has(t.name);
|
|
7073
|
-
const summary = isTurnControl ? summarizeTurnControlArgs(t.name, args) : summarizeCabaneToolArgs(t.name, args);
|
|
7074
|
-
yield { type: "tool", id: t.id, name: displayName, phase: "start", summary, input: args };
|
|
7075
|
-
const result = isTurnControl && turnControl ? await executeNativeTurnControl(t.name, args, turnControl) : await executeCabaneNativeTool(t.name, args, toolCtx, signal);
|
|
7076
|
-
if (signal.aborted) return;
|
|
7077
|
-
yield {
|
|
7078
|
-
type: "tool",
|
|
7079
|
-
id: t.id,
|
|
7080
|
-
name: displayName,
|
|
7081
|
-
phase: result.ok ? "done" : "error",
|
|
7082
|
-
summary,
|
|
7083
|
-
input: args,
|
|
7084
|
-
result: result.result
|
|
7085
|
-
};
|
|
7086
|
-
messages.push({ role: "tool", tool_call_id: t.id, content: result.result });
|
|
7087
|
-
}
|
|
7088
|
-
}
|
|
7089
|
-
deps.onWarn?.("cabane-native: turn hit the tool-iteration cap; force-settling", {
|
|
7090
|
-
maxIterations
|
|
7091
|
-
});
|
|
7092
|
-
yield {
|
|
7093
|
-
type: "text",
|
|
7094
|
-
body: `(Stopped after ${maxIterations} tool steps without a final answer.)`,
|
|
7095
|
-
terminal: true
|
|
7096
|
-
};
|
|
7097
|
-
yield {
|
|
7098
|
-
type: "result",
|
|
7099
|
-
ok: true,
|
|
7100
|
-
...usage ? { usage } : {},
|
|
7101
|
-
...resolvedModel ? { resolvedModel } : {}
|
|
7102
|
-
};
|
|
7103
|
-
}
|
|
7104
|
-
function sealText(text, terminal) {
|
|
7105
|
-
const body = text.trim();
|
|
7106
|
-
if (body.length === 0) return null;
|
|
7107
|
-
return { type: "text", body, terminal };
|
|
7108
|
-
}
|
|
7109
|
-
function parseArgs(raw) {
|
|
7110
|
-
if (!raw.trim()) return {};
|
|
7111
|
-
try {
|
|
7112
|
-
const parsed = JSON.parse(raw);
|
|
7113
|
-
return parsed && typeof parsed === "object" ? parsed : {};
|
|
7114
|
-
} catch {
|
|
7115
|
-
return {};
|
|
7116
|
-
}
|
|
7117
|
-
}
|
|
7118
|
-
|
|
7119
|
-
// packages/agent-runtime/src/cabane-native/provider.ts
|
|
7120
|
-
var DEFAULT_OPENROUTER_BASE = "https://openrouter.ai/api/v1";
|
|
7121
|
-
function createOpenRouterProvider(opts) {
|
|
7122
|
-
const base = (opts.baseUrl ?? DEFAULT_OPENROUTER_BASE).replace(/\/$/, "");
|
|
7123
|
-
const doFetch = opts.fetchImpl ?? fetch;
|
|
7124
|
-
return {
|
|
7125
|
-
async *stream(req, signal) {
|
|
7126
|
-
let res;
|
|
7127
|
-
try {
|
|
7128
|
-
res = await doFetch(`${base}/chat/completions`, {
|
|
7129
|
-
method: "POST",
|
|
7130
|
-
headers: {
|
|
7131
|
-
Authorization: `Bearer ${opts.apiKey}`,
|
|
7132
|
-
"Content-Type": "application/json",
|
|
7133
|
-
// OpenRouter attribution headers (optional, but polite + used for
|
|
7134
|
-
// routing/analytics on their side).
|
|
7135
|
-
"HTTP-Referer": "https://cabane.ai",
|
|
7136
|
-
"X-Title": "Cabane"
|
|
7137
|
-
},
|
|
7138
|
-
body: JSON.stringify({
|
|
7139
|
-
model: req.model,
|
|
7140
|
-
messages: req.messages,
|
|
7141
|
-
...req.tools.length > 0 ? { tools: req.tools } : {},
|
|
7142
|
-
stream: true,
|
|
7143
|
-
// Ask OpenRouter to append a trailing usage chunk to the stream.
|
|
7144
|
-
stream_options: { include_usage: true }
|
|
7145
|
-
}),
|
|
7146
|
-
signal
|
|
7147
|
-
});
|
|
7148
|
-
} catch (err2) {
|
|
7149
|
-
if (signal.aborted) return;
|
|
7150
|
-
yield { type: "error", message: `request failed: ${errText(err2)}` };
|
|
7151
|
-
return;
|
|
7152
|
-
}
|
|
7153
|
-
if (!res.ok || !res.body) {
|
|
7154
|
-
const bodyText = await res.text().catch(() => "");
|
|
7155
|
-
yield { type: "error", message: providerErrorMessage(res.status, bodyText) };
|
|
7156
|
-
return;
|
|
7157
|
-
}
|
|
7158
|
-
const decoder = new TextDecoder();
|
|
7159
|
-
const reader = res.body.getReader();
|
|
7160
|
-
let buffer = "";
|
|
7161
|
-
let finishReason;
|
|
7162
|
-
let modelEmitted = false;
|
|
7163
|
-
try {
|
|
7164
|
-
for (; ; ) {
|
|
7165
|
-
if (signal.aborted) return;
|
|
7166
|
-
const { value, done } = await reader.read();
|
|
7167
|
-
if (done) break;
|
|
7168
|
-
buffer += decoder.decode(value, { stream: true });
|
|
7169
|
-
let nl;
|
|
7170
|
-
while ((nl = buffer.indexOf("\n")) !== -1) {
|
|
7171
|
-
const line = buffer.slice(0, nl).trim();
|
|
7172
|
-
buffer = buffer.slice(nl + 1);
|
|
7173
|
-
if (!line || line.startsWith(":")) continue;
|
|
7174
|
-
if (!line.startsWith("data:")) continue;
|
|
7175
|
-
const data = line.slice("data:".length).trim();
|
|
7176
|
-
if (data === "[DONE]") {
|
|
7177
|
-
yield { type: "done", ...finishReason ? { finishReason } : {} };
|
|
7178
|
-
return;
|
|
7179
|
-
}
|
|
7180
|
-
let chunk;
|
|
7181
|
-
try {
|
|
7182
|
-
chunk = JSON.parse(data);
|
|
7183
|
-
} catch {
|
|
7184
|
-
continue;
|
|
7185
|
-
}
|
|
7186
|
-
if (chunk.error) {
|
|
7187
|
-
yield { type: "error", message: chunk.error.message ?? "provider error" };
|
|
7188
|
-
return;
|
|
7189
|
-
}
|
|
7190
|
-
if (!modelEmitted && chunk.model) {
|
|
7191
|
-
modelEmitted = true;
|
|
7192
|
-
yield { type: "model", model: chunk.model };
|
|
7193
|
-
}
|
|
7194
|
-
const choice = chunk.choices?.[0];
|
|
7195
|
-
if (choice) {
|
|
7196
|
-
const delta = choice.delta;
|
|
7197
|
-
if (delta?.content) yield { type: "text", delta: delta.content };
|
|
7198
|
-
if (delta?.tool_calls) {
|
|
7199
|
-
for (const tc of delta.tool_calls) {
|
|
7200
|
-
yield {
|
|
7201
|
-
type: "tool_call",
|
|
7202
|
-
index: tc.index,
|
|
7203
|
-
...tc.id ? { id: tc.id } : {},
|
|
7204
|
-
...tc.function?.name ? { name: tc.function.name } : {},
|
|
7205
|
-
...tc.function?.arguments !== void 0 ? { argumentsDelta: tc.function.arguments } : {}
|
|
7206
|
-
};
|
|
7207
|
-
}
|
|
7208
|
-
}
|
|
7209
|
-
if (choice.finish_reason) finishReason = choice.finish_reason;
|
|
7210
|
-
}
|
|
7211
|
-
if (chunk.usage) {
|
|
7212
|
-
yield {
|
|
7213
|
-
type: "usage",
|
|
7214
|
-
inputTokens: chunk.usage.prompt_tokens ?? 0,
|
|
7215
|
-
outputTokens: chunk.usage.completion_tokens ?? 0
|
|
7216
|
-
};
|
|
7217
|
-
}
|
|
7218
|
-
}
|
|
7219
|
-
}
|
|
7220
|
-
} catch (err2) {
|
|
7221
|
-
if (signal.aborted) return;
|
|
7222
|
-
yield { type: "error", message: `stream read failed: ${errText(err2)}` };
|
|
7223
|
-
return;
|
|
7224
|
-
}
|
|
7225
|
-
yield { type: "done", ...finishReason ? { finishReason } : {} };
|
|
7226
|
-
}
|
|
7227
|
-
};
|
|
7228
|
-
}
|
|
7229
|
-
function providerErrorMessage(status2, body) {
|
|
7230
|
-
let detail = body.slice(0, 300);
|
|
7231
|
-
try {
|
|
7232
|
-
const parsed = JSON.parse(body);
|
|
7233
|
-
if (parsed.error?.message) detail = parsed.error.message;
|
|
7234
|
-
} catch {
|
|
7235
|
-
}
|
|
7236
|
-
return `HTTP ${status2}: ${detail}`;
|
|
7237
|
-
}
|
|
7238
|
-
function errText(err2) {
|
|
7239
|
-
return err2 instanceof Error ? err2.message : String(err2);
|
|
7240
|
-
}
|
|
7241
|
-
|
|
7242
|
-
// packages/agent-runtime/src/cabane-native/index.ts
|
|
7243
|
-
var CABANE_NATIVE_RUNTIME_NAME = "cabane-native";
|
|
7244
|
-
function createCabaneNativeAdapter(deps = {}) {
|
|
7245
|
-
const provider = deps.provider ?? (deps.apiKey ? createOpenRouterProvider({
|
|
7246
|
-
apiKey: deps.apiKey,
|
|
7247
|
-
...deps.baseUrl ? { baseUrl: deps.baseUrl } : {},
|
|
7248
|
-
...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
|
|
7249
|
-
}) : null);
|
|
7250
|
-
return {
|
|
7251
|
-
name: CABANE_NATIVE_RUNTIME_NAME,
|
|
7252
|
-
// CT666: the native runtime's addendum is now SURFACE-aware. Code mode (the
|
|
7253
|
-
// default In-Cabane surface) mounts the single `sdk` tool, so it's taught the
|
|
7254
|
-
// code-mode addendum (act by writing a `cabane` SDK program; the `cabane_*`
|
|
7255
|
-
// names don't exist here); classic (the retained CT663 10-tool fallback) is
|
|
7256
|
-
// taught the granular `cabane_*` listing. The caller (turn-context) passes
|
|
7257
|
-
// `codeMode` off the SAME lifted surface value the loop mounts tools from, so
|
|
7258
|
-
// the prompt and the mounted surface can't drift. (Supersedes CT614's
|
|
7259
|
-
// flag-independent addendum: native no longer ignores this arg.)
|
|
7260
|
-
promptAddendum: (codeMode) => codeMode ? CABANE_NATIVE_ADDENDUM_CODE_MODE : CABANE_NATIVE_ADDENDUM,
|
|
7261
|
-
dialectSchema: cabaneNativeDialectSchema,
|
|
7262
|
-
async *runTurn(req, signal) {
|
|
7263
|
-
if (!provider) {
|
|
7264
|
-
yield {
|
|
7265
|
-
type: "result",
|
|
7266
|
-
ok: false,
|
|
7267
|
-
reason: "cabane_native_unavailable:no OPENROUTER_API_KEY configured on this device"
|
|
7268
|
-
};
|
|
7269
|
-
return;
|
|
7270
|
-
}
|
|
7271
|
-
const workspaceId = req.cabane.workspaceId;
|
|
7272
|
-
if (!workspaceId) {
|
|
7273
|
-
yield {
|
|
7274
|
-
type: "result",
|
|
7275
|
-
ok: false,
|
|
7276
|
-
reason: "cabane_native_unavailable:turn carried no workspaceId"
|
|
7277
|
-
};
|
|
7278
|
-
return;
|
|
7279
|
-
}
|
|
7280
|
-
const apiRoot = req.cabane.mcpUrl.replace(/\/mcp\/?$/, "");
|
|
7281
|
-
yield* runCabaneNativeTurn(req, signal, {
|
|
7282
|
-
provider,
|
|
7283
|
-
apiRoot,
|
|
7284
|
-
workspaceId,
|
|
7285
|
-
bearer: req.cabane.bearer,
|
|
7286
|
-
conversationId: req.cabane.activeConversationId,
|
|
7287
|
-
...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {},
|
|
7288
|
-
...deps.onWarn ? { onWarn: deps.onWarn } : {},
|
|
7289
|
-
...deps.maxIterations !== void 0 ? { maxIterations: deps.maxIterations } : {}
|
|
7290
|
-
});
|
|
7291
|
-
}
|
|
7292
|
-
};
|
|
7293
|
-
}
|
|
7294
|
-
var cabaneNativeAdapter = createCabaneNativeAdapter();
|
|
7295
|
-
|
|
7296
|
-
// packages/agent-runtime/src/claude-code/sdk.ts
|
|
7297
|
-
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
6168
|
+
// packages/agent-runtime/src/claude-code/sdk.ts
|
|
6169
|
+
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
7298
6170
|
|
|
7299
6171
|
// src/connector-health.ts
|
|
7300
6172
|
var ConnectorHealthStore = class {
|
|
@@ -7350,7 +6222,7 @@ import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync
|
|
|
7350
6222
|
import { join as join12 } from "path";
|
|
7351
6223
|
|
|
7352
6224
|
// src/summon.ts
|
|
7353
|
-
import { z as
|
|
6225
|
+
import { z as z12 } from "zod";
|
|
7354
6226
|
var COMPANION_LOCAL_MCP_SERVER = "cabane_companion";
|
|
7355
6227
|
var SUMMON_AGENT_TOOL = "summon_agent";
|
|
7356
6228
|
var SUMMON_AGENT_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${SUMMON_AGENT_TOOL}`;
|
|
@@ -7384,7 +6256,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
7384
6256
|
SUMMON_AGENT_TOOL,
|
|
7385
6257
|
"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.",
|
|
7386
6258
|
{
|
|
7387
|
-
agentId:
|
|
6259
|
+
agentId: z12.string().uuid().describe("The peer agent to summon \u2014 a workspace agent id from `list_agents`.")
|
|
7388
6260
|
},
|
|
7389
6261
|
async (args) => {
|
|
7390
6262
|
summonState.agentId = args.agentId;
|
|
@@ -7399,7 +6271,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
7399
6271
|
SKIP_TURN_TOOL,
|
|
7400
6272
|
`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.`,
|
|
7401
6273
|
{
|
|
7402
|
-
reason:
|
|
6274
|
+
reason: z12.string().min(1).max(500).describe("Short reason you are declining \u2014 used for telemetry/debugging.")
|
|
7403
6275
|
},
|
|
7404
6276
|
async (args) => {
|
|
7405
6277
|
skipState.skipped = true;
|
|
@@ -7416,21 +6288,21 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
7416
6288
|
ASK_TOOL,
|
|
7417
6289
|
"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.",
|
|
7418
6290
|
{
|
|
7419
|
-
targetUserId:
|
|
7420
|
-
question:
|
|
6291
|
+
targetUserId: z12.string().uuid().describe("The workspace member (human) to ask \u2014 a user id from `list_members`."),
|
|
6292
|
+
question: z12.string().min(1).max(400).optional().describe(
|
|
7421
6293
|
"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`."
|
|
7422
6294
|
),
|
|
7423
|
-
headline:
|
|
6295
|
+
headline: z12.string().min(1).max(120).optional().describe(
|
|
7424
6296
|
'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.'
|
|
7425
6297
|
),
|
|
7426
|
-
options:
|
|
7427
|
-
questions:
|
|
7428
|
-
|
|
7429
|
-
headline:
|
|
6298
|
+
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."),
|
|
6299
|
+
questions: z12.array(
|
|
6300
|
+
z12.object({
|
|
6301
|
+
headline: z12.string().min(1).max(120).describe(
|
|
7430
6302
|
'The one-sentence question ("Do we go to prod?") \u2014 required for each item.'
|
|
7431
6303
|
),
|
|
7432
|
-
body:
|
|
7433
|
-
options:
|
|
6304
|
+
body: z12.string().min(1).max(400).optional().describe("Optional short framing beneath the headline. NOT a report."),
|
|
6305
|
+
options: z12.array(z12.string().min(1).max(200)).min(2).max(4).optional().describe("Optional 2\u20134 one-click answers for this question.")
|
|
7434
6306
|
})
|
|
7435
6307
|
).min(1).max(5).optional().describe(
|
|
7436
6308
|
"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."
|
|
@@ -7487,13 +6359,13 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
7487
6359
|
SUB_AGENT_TOOL,
|
|
7488
6360
|
"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.",
|
|
7489
6361
|
{
|
|
7490
|
-
prompt:
|
|
6362
|
+
prompt: z12.string().min(1).max(65536).describe(
|
|
7491
6363
|
"The sub-agent's opening instruction \u2014 self-contained (it starts with a fresh context window; only this prompt + the thread it lands in)."
|
|
7492
6364
|
),
|
|
7493
|
-
agentId:
|
|
6365
|
+
agentId: z12.string().uuid().optional().describe(
|
|
7494
6366
|
"Optional peer to run the sub-agent as (a workspace agent id from `list_agents`); omit to spawn yourself with a fresh context window."
|
|
7495
6367
|
),
|
|
7496
|
-
title:
|
|
6368
|
+
title: z12.string().max(200).optional().describe("Optional title for the child thread (result chips link it).")
|
|
7497
6369
|
},
|
|
7498
6370
|
async (args) => {
|
|
7499
6371
|
const result = await subAgentCreate(args);
|
|
@@ -7523,13 +6395,13 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
7523
6395
|
WAKE_ME_TOOL,
|
|
7524
6396
|
'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.',
|
|
7525
6397
|
{
|
|
7526
|
-
afterSeconds:
|
|
6398
|
+
afterSeconds: z12.number().int().positive().optional().describe(
|
|
7527
6399
|
"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."
|
|
7528
6400
|
),
|
|
7529
|
-
at:
|
|
6401
|
+
at: z12.string().datetime({ offset: true }).optional().describe(
|
|
7530
6402
|
"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."
|
|
7531
6403
|
),
|
|
7532
|
-
note:
|
|
6404
|
+
note: z12.string().min(1).max(2e3).describe(
|
|
7533
6405
|
'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").'
|
|
7534
6406
|
)
|
|
7535
6407
|
},
|
|
@@ -7609,7 +6481,6 @@ function buildCompanionTurnRequest(params) {
|
|
|
7609
6481
|
local: {
|
|
7610
6482
|
...params.cwd ? { cwd: params.cwd } : {},
|
|
7611
6483
|
...params.env ? { env: params.env } : {},
|
|
7612
|
-
...params.nativeWorkAssignment ? { nativeWorkAssignment: params.nativeWorkAssignment } : {},
|
|
7613
6484
|
// User MCP servers (already `${PLACEHOLDER}`-resolved). Structurally the
|
|
7614
6485
|
// adapter's `ResolvedMcpServers`.
|
|
7615
6486
|
...Object.keys(params.mcpServers).length > 0 ? { mcpServers: params.mcpServers } : {},
|
|
@@ -7617,10 +6488,9 @@ function buildCompanionTurnRequest(params) {
|
|
|
7617
6488
|
...params.claudeCode ? { claudeCode: params.claudeCode } : {}
|
|
7618
6489
|
},
|
|
7619
6490
|
// Host-injected: the companion-local summon server (for the subprocess adapters,
|
|
7620
|
-
// under its own namespace)
|
|
6491
|
+
// under its own namespace).
|
|
7621
6492
|
extra: {
|
|
7622
|
-
mcpServers: { [COMPANION_LOCAL_MCP_SERVER]: params.summonServer }
|
|
7623
|
-
turnControl: params.turnControl
|
|
6493
|
+
mcpServers: { [COMPANION_LOCAL_MCP_SERVER]: params.summonServer }
|
|
7624
6494
|
}
|
|
7625
6495
|
};
|
|
7626
6496
|
}
|
|
@@ -7629,7 +6499,7 @@ function trimSlash3(s) {
|
|
|
7629
6499
|
}
|
|
7630
6500
|
|
|
7631
6501
|
// src/prepared.ts
|
|
7632
|
-
import { mkdirSync as mkdirSync8, readFileSync as
|
|
6502
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
|
|
7633
6503
|
import { join as join9 } from "path";
|
|
7634
6504
|
function dirFor(workspaceId) {
|
|
7635
6505
|
return join9(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
|
|
@@ -7637,23 +6507,18 @@ function dirFor(workspaceId) {
|
|
|
7637
6507
|
function conversationDir(workspaceId, conversationId) {
|
|
7638
6508
|
return join9(dirFor(workspaceId), encodeURIComponent(conversationId));
|
|
7639
6509
|
}
|
|
7640
|
-
function pathFor3(workspaceId, conversationId, agentId
|
|
7641
|
-
|
|
7642
|
-
return join9(
|
|
7643
|
-
conversationDir(workspaceId, conversationId),
|
|
7644
|
-
`${encodeURIComponent(agentId)}${suffix}.json`
|
|
7645
|
-
);
|
|
6510
|
+
function pathFor3(workspaceId, conversationId, agentId) {
|
|
6511
|
+
return join9(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
|
|
7646
6512
|
}
|
|
7647
|
-
function readPrepared(workspaceId, conversationId, agentId
|
|
7648
|
-
const path3 = pathFor3(workspaceId, conversationId, agentId
|
|
6513
|
+
function readPrepared(workspaceId, conversationId, agentId) {
|
|
6514
|
+
const path3 = pathFor3(workspaceId, conversationId, agentId);
|
|
7649
6515
|
if (!existsSync7(path3)) return null;
|
|
7650
6516
|
try {
|
|
7651
|
-
const parsed = JSON.parse(
|
|
6517
|
+
const parsed = JSON.parse(readFileSync6(path3, "utf8"));
|
|
7652
6518
|
if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
|
|
7653
6519
|
return {
|
|
7654
6520
|
cwd: parsed.cwd,
|
|
7655
|
-
...parsed.env && typeof parsed.env === "object" ? { env: parsed.env } : {}
|
|
7656
|
-
...parsed.nativeWorkAssignment && typeof parsed.nativeWorkAssignment === "object" ? { nativeWorkAssignment: parsed.nativeWorkAssignment } : {}
|
|
6521
|
+
...parsed.env && typeof parsed.env === "object" ? { env: parsed.env } : {}
|
|
7657
6522
|
};
|
|
7658
6523
|
}
|
|
7659
6524
|
return null;
|
|
@@ -7661,42 +6526,42 @@ function readPrepared(workspaceId, conversationId, agentId, assignmentKey) {
|
|
|
7661
6526
|
return null;
|
|
7662
6527
|
}
|
|
7663
6528
|
}
|
|
7664
|
-
function writePrepared(workspaceId, conversationId, agentId, result
|
|
6529
|
+
function writePrepared(workspaceId, conversationId, agentId, result) {
|
|
7665
6530
|
mkdirSync8(conversationDir(workspaceId, conversationId), { recursive: true });
|
|
7666
6531
|
writeFileSync6(
|
|
7667
|
-
pathFor3(workspaceId, conversationId, agentId
|
|
6532
|
+
pathFor3(workspaceId, conversationId, agentId),
|
|
7668
6533
|
JSON.stringify(result) + "\n",
|
|
7669
6534
|
"utf8"
|
|
7670
6535
|
);
|
|
7671
6536
|
}
|
|
7672
6537
|
|
|
7673
6538
|
// src/secrets.ts
|
|
7674
|
-
import { existsSync as existsSync8, readFileSync as
|
|
6539
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
7675
6540
|
import { join as join10 } from "path";
|
|
7676
|
-
import { z as
|
|
6541
|
+
import { z as z13 } from "zod";
|
|
7677
6542
|
var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
7678
6543
|
function secretsPath() {
|
|
7679
6544
|
return join10(cabaneDir(), "secrets.json");
|
|
7680
6545
|
}
|
|
7681
|
-
var secretStoreSchema =
|
|
6546
|
+
var secretStoreSchema = z13.record(z13.string(), z13.string());
|
|
7682
6547
|
function loadSecretStore() {
|
|
7683
6548
|
const path3 = secretsPath();
|
|
7684
6549
|
if (!existsSync8(path3)) return makeStore({});
|
|
7685
6550
|
let raw;
|
|
7686
6551
|
try {
|
|
7687
|
-
raw =
|
|
7688
|
-
} catch (
|
|
6552
|
+
raw = readFileSync7(path3, "utf8");
|
|
6553
|
+
} catch (err) {
|
|
7689
6554
|
throw new ConfigError(
|
|
7690
|
-
`couldn't read ${path3}: ${
|
|
6555
|
+
`couldn't read ${path3}: ${err instanceof Error ? err.message : String(err)}`
|
|
7691
6556
|
);
|
|
7692
6557
|
}
|
|
7693
6558
|
if (raw.trim().length === 0) return makeStore({});
|
|
7694
6559
|
let parsed;
|
|
7695
6560
|
try {
|
|
7696
6561
|
parsed = JSON.parse(raw);
|
|
7697
|
-
} catch (
|
|
6562
|
+
} catch (err) {
|
|
7698
6563
|
throw new ConfigError(
|
|
7699
|
-
`${path3} is not valid JSON: ${
|
|
6564
|
+
`${path3} is not valid JSON: ${err instanceof Error ? err.message : String(err)}. It must be a flat object of "NAME": "value" secret pairs.`
|
|
7700
6565
|
);
|
|
7701
6566
|
}
|
|
7702
6567
|
const result = secretStoreSchema.safeParse(parsed);
|
|
@@ -7710,8 +6575,8 @@ function loadSecretStore() {
|
|
|
7710
6575
|
function loadSecretStoreTolerant(onWarn) {
|
|
7711
6576
|
try {
|
|
7712
6577
|
return loadSecretStore();
|
|
7713
|
-
} catch (
|
|
7714
|
-
onWarn?.(
|
|
6578
|
+
} catch (err) {
|
|
6579
|
+
onWarn?.(err instanceof Error ? err.message : String(err));
|
|
7715
6580
|
return makeStore({});
|
|
7716
6581
|
}
|
|
7717
6582
|
}
|
|
@@ -7782,8 +6647,8 @@ var TranscriptWriter = class {
|
|
|
7782
6647
|
} catch {
|
|
7783
6648
|
}
|
|
7784
6649
|
pruneOld(dir2, RETAIN);
|
|
7785
|
-
} catch (
|
|
7786
|
-
this.fail(
|
|
6650
|
+
} catch (err) {
|
|
6651
|
+
this.fail(err);
|
|
7787
6652
|
}
|
|
7788
6653
|
this.line({ type: "_meta", ...meta });
|
|
7789
6654
|
}
|
|
@@ -7799,14 +6664,14 @@ var TranscriptWriter = class {
|
|
|
7799
6664
|
if (this.broken) return;
|
|
7800
6665
|
try {
|
|
7801
6666
|
appendFileSync(this.path, JSON.stringify(obj) + "\n", { mode: 384 });
|
|
7802
|
-
} catch (
|
|
7803
|
-
this.fail(
|
|
6667
|
+
} catch (err) {
|
|
6668
|
+
this.fail(err);
|
|
7804
6669
|
}
|
|
7805
6670
|
}
|
|
7806
|
-
fail(
|
|
6671
|
+
fail(err) {
|
|
7807
6672
|
if (this.broken) return;
|
|
7808
6673
|
this.broken = true;
|
|
7809
|
-
this.onWarn?.(`transcript write failed (${
|
|
6674
|
+
this.onWarn?.(`transcript write failed (${err instanceof Error ? err.message : String(err)})`);
|
|
7810
6675
|
}
|
|
7811
6676
|
};
|
|
7812
6677
|
function fileName(meta) {
|
|
@@ -7844,9 +6709,9 @@ var EMPTY_FINAL_BODY = "Done \u2014 see the changes above.";
|
|
|
7844
6709
|
var TurnCommitter = class {
|
|
7845
6710
|
constructor(deps) {
|
|
7846
6711
|
this.deps = deps;
|
|
7847
|
-
this.onError = (
|
|
6712
|
+
this.onError = (err, hook) => {
|
|
7848
6713
|
deps.log.warn(
|
|
7849
|
-
{ err:
|
|
6714
|
+
{ err: err instanceof Error ? err.message : String(err), hook },
|
|
7850
6715
|
"dispatcher: transcript callback failed"
|
|
7851
6716
|
);
|
|
7852
6717
|
};
|
|
@@ -7911,9 +6776,9 @@ var TurnCommitter = class {
|
|
|
7911
6776
|
signal: deps.signal,
|
|
7912
6777
|
nextSeq: deps.nextSeq,
|
|
7913
6778
|
emptyFinalBody: EMPTY_FINAL_BODY,
|
|
7914
|
-
onError: (
|
|
6779
|
+
onError: (err) => {
|
|
7915
6780
|
deps.log.warn(
|
|
7916
|
-
{ err:
|
|
6781
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
7917
6782
|
"dispatcher: empty-final commit failed"
|
|
7918
6783
|
);
|
|
7919
6784
|
}
|
|
@@ -7936,8 +6801,8 @@ var TurnCommitter = class {
|
|
|
7936
6801
|
if (event.type === "session" || event.type === "result") return;
|
|
7937
6802
|
try {
|
|
7938
6803
|
await this.emit(event);
|
|
7939
|
-
} catch (
|
|
7940
|
-
this.onError(
|
|
6804
|
+
} catch (err) {
|
|
6805
|
+
this.onError(err, event.type);
|
|
7941
6806
|
}
|
|
7942
6807
|
}
|
|
7943
6808
|
// End-of-turn empty-final promotion. The held-text flush is now the adapter's
|
|
@@ -8133,10 +6998,27 @@ var UNEXPECTED_ROLE_REASON = `refused the wake trigger (role "system" isn't disp
|
|
|
8133
6998
|
var SKIPPED_MARKER_BODY = "(skipped)";
|
|
8134
6999
|
var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
|
|
8135
7000
|
var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
|
|
8136
|
-
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS =
|
|
7001
|
+
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
|
|
8137
7002
|
function runKey(conversationId, agentId) {
|
|
8138
7003
|
return `${conversationId}|${agentId}`;
|
|
8139
7004
|
}
|
|
7005
|
+
function describeSubAgentError(status2, body) {
|
|
7006
|
+
const code = body && typeof body === "object" && "error" in body ? String(body.error) : void 0;
|
|
7007
|
+
switch (code) {
|
|
7008
|
+
case "callout_cap_exceeded":
|
|
7009
|
+
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.";
|
|
7010
|
+
case "callout_depth_exceeded":
|
|
7011
|
+
return "sub_agent: this would nest sub-agents too deep (max 3 levels). Have the current worker report back rather than spawning another layer.";
|
|
7012
|
+
case "dispatch_agent_not_found":
|
|
7013
|
+
return "sub_agent: no live agent in this workspace matches that `agentId`. Check `list_agents`, or omit `agentId` to spawn yourself.";
|
|
7014
|
+
case "dispatch_return_requires_turn":
|
|
7015
|
+
case "dispatch_return_requires_agent":
|
|
7016
|
+
case "dispatch_return_requires_dispatch":
|
|
7017
|
+
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.`;
|
|
7018
|
+
default:
|
|
7019
|
+
return `sub_agent: the spawn failed (${code ?? `HTTP ${status2}`}).`;
|
|
7020
|
+
}
|
|
7021
|
+
}
|
|
8140
7022
|
var Dispatcher = class {
|
|
8141
7023
|
constructor(opts) {
|
|
8142
7024
|
this.opts = opts;
|
|
@@ -8163,7 +7045,7 @@ var Dispatcher = class {
|
|
|
8163
7045
|
// clear it — the SJ383 `finally` after the SDK loop is the one clear, and
|
|
8164
7046
|
// every pre-run exit returns before reaching it. So each pre-run failure has
|
|
8165
7047
|
// to clear `active_run_started_at` itself, mirroring that `finally`, or the
|
|
8166
|
-
// indicator strands until the
|
|
7048
|
+
// indicator strands until the 12h age sweep.
|
|
8167
7049
|
//
|
|
8168
7050
|
// `errorReason` controls the server's duplicate-notice rule (the active-run
|
|
8169
7051
|
// PATCH handler in conversations.ts): a clear carrying `errorReason` makes the
|
|
@@ -8183,9 +7065,9 @@ var Dispatcher = class {
|
|
|
8183
7065
|
payload.agentId,
|
|
8184
7066
|
body
|
|
8185
7067
|
);
|
|
8186
|
-
} catch (
|
|
7068
|
+
} catch (err) {
|
|
8187
7069
|
turnLog.warn(
|
|
8188
|
-
{ err:
|
|
7070
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8189
7071
|
"dispatcher: pre-run active-run clear failed terminally; server age-sweep is the backstop"
|
|
8190
7072
|
);
|
|
8191
7073
|
}
|
|
@@ -8211,16 +7093,16 @@ var Dispatcher = class {
|
|
|
8211
7093
|
payload.messageId,
|
|
8212
7094
|
turnId
|
|
8213
7095
|
);
|
|
8214
|
-
} catch (
|
|
8215
|
-
const status2 =
|
|
7096
|
+
} catch (err) {
|
|
7097
|
+
const status2 = err instanceof ApiError ? err.status : 0;
|
|
8216
7098
|
if (status2 === 404) {
|
|
8217
7099
|
turnLog.warn(
|
|
8218
|
-
{ err:
|
|
7100
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8219
7101
|
"dispatcher: turn-context 404 (stale event \u2014 conversation/message/participant gone); skipping"
|
|
8220
7102
|
);
|
|
8221
7103
|
return this.concludeBeforeRun(payload, turnLog, startedAt, "turn_context_not_found");
|
|
8222
7104
|
}
|
|
8223
|
-
const reason =
|
|
7105
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
8224
7106
|
turnLog.error({ err: reason }, "dispatcher: failed to fetch turn context");
|
|
8225
7107
|
const fetchReason = `fetch_failed: ${reason}`;
|
|
8226
7108
|
return this.concludeBeforeRun(payload, turnLog, startedAt, fetchReason, fetchReason);
|
|
@@ -8283,20 +7165,11 @@ var Dispatcher = class {
|
|
|
8283
7165
|
effectiveCwd = void 0;
|
|
8284
7166
|
}
|
|
8285
7167
|
let hookEnv;
|
|
8286
|
-
let preparedNativeAssignment;
|
|
8287
7168
|
if (prepareHook) {
|
|
8288
|
-
const
|
|
8289
|
-
const assignmentKey = assignment ? `${assignment.executionId}:${assignment.activationEpoch}` : void 0;
|
|
8290
|
-
const cached2 = readPrepared(
|
|
8291
|
-
workspaceId,
|
|
8292
|
-
payload.conversationId,
|
|
8293
|
-
payload.agentId,
|
|
8294
|
-
assignmentKey
|
|
8295
|
-
);
|
|
7169
|
+
const cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
|
|
8296
7170
|
if (cached2) {
|
|
8297
7171
|
effectiveCwd = cached2.cwd;
|
|
8298
7172
|
hookEnv = cached2.env;
|
|
8299
|
-
preparedNativeAssignment = cached2.nativeWorkAssignment;
|
|
8300
7173
|
} else {
|
|
8301
7174
|
const delayMs = this.opts.preparingRowDelayMs ?? DEFAULT_PREPARING_ROW_DELAY_MS;
|
|
8302
7175
|
let preparingStarted = false;
|
|
@@ -8311,9 +7184,9 @@ var Dispatcher = class {
|
|
|
8311
7184
|
summary: "",
|
|
8312
7185
|
phase,
|
|
8313
7186
|
seq
|
|
8314
|
-
}).catch((
|
|
7187
|
+
}).catch((err) => {
|
|
8315
7188
|
turnLog.warn(
|
|
8316
|
-
{ err:
|
|
7189
|
+
{ err: err instanceof Error ? err.message : String(err), phase },
|
|
8317
7190
|
"dispatcher: preparing-activity report failed (continuing with the hook)"
|
|
8318
7191
|
);
|
|
8319
7192
|
});
|
|
@@ -8335,25 +7208,17 @@ var Dispatcher = class {
|
|
|
8335
7208
|
// tasker prepare hook keys its per-task env off. Defaults to `[]` for
|
|
8336
7209
|
// an older API. The conversation anchor is gone (CT319).
|
|
8337
7210
|
triggerEntryPaths: turnContext.conversation.triggerEntryPaths ?? [],
|
|
8338
|
-
...turnContext.conversation.nativeWorkAssignment ? { nativeWorkAssignment: turnContext.conversation.nativeWorkAssignment } : {},
|
|
8339
7211
|
title: turnContext.conversation.title
|
|
8340
7212
|
});
|
|
8341
7213
|
clearTimeout(preparingTimer);
|
|
8342
7214
|
if (preparingStarted) reportPreparing("done");
|
|
8343
|
-
writePrepared(
|
|
8344
|
-
workspaceId,
|
|
8345
|
-
payload.conversationId,
|
|
8346
|
-
payload.agentId,
|
|
8347
|
-
result,
|
|
8348
|
-
assignmentKey
|
|
8349
|
-
);
|
|
7215
|
+
writePrepared(workspaceId, payload.conversationId, payload.agentId, result);
|
|
8350
7216
|
effectiveCwd = result.cwd;
|
|
8351
7217
|
hookEnv = result.env;
|
|
8352
|
-
|
|
8353
|
-
} catch (err2) {
|
|
7218
|
+
} catch (err) {
|
|
8354
7219
|
clearTimeout(preparingTimer);
|
|
8355
7220
|
if (preparingStarted) reportPreparing("error");
|
|
8356
|
-
const reason =
|
|
7221
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
8357
7222
|
turnLog.error({ err: reason }, "dispatcher: prepare hook failed");
|
|
8358
7223
|
try {
|
|
8359
7224
|
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
@@ -8376,6 +7241,19 @@ ${reason}`,
|
|
|
8376
7241
|
}
|
|
8377
7242
|
}
|
|
8378
7243
|
}
|
|
7244
|
+
let turnEnv = hookEnv;
|
|
7245
|
+
if (effectiveCwd && turnContext.runtime === "codex") {
|
|
7246
|
+
const tmpDir = join12(effectiveCwd, "node_modules", ".cache", "cabane-tmp", turnId);
|
|
7247
|
+
try {
|
|
7248
|
+
mkdirSync10(tmpDir, { recursive: true });
|
|
7249
|
+
turnEnv = { ...hookEnv, TMPDIR: tmpDir };
|
|
7250
|
+
} catch (err) {
|
|
7251
|
+
turnLog.warn(
|
|
7252
|
+
{ err: err instanceof Error ? err.message : String(err), tmpDir },
|
|
7253
|
+
"dispatcher: failed to create per-turn TMPDIR \u2014 proceeding with the inherited temp dir"
|
|
7254
|
+
);
|
|
7255
|
+
}
|
|
7256
|
+
}
|
|
8379
7257
|
const key = runKey(payload.conversationId, payload.agentId);
|
|
8380
7258
|
const abortController = new AbortController();
|
|
8381
7259
|
this.aborts.set(key, abortController);
|
|
@@ -8390,9 +7268,9 @@ ${reason}`,
|
|
|
8390
7268
|
// this live turn for an abandoned one and close it with a `stopped`.
|
|
8391
7269
|
turnId
|
|
8392
7270
|
});
|
|
8393
|
-
} catch (
|
|
7271
|
+
} catch (err) {
|
|
8394
7272
|
turnLog.warn(
|
|
8395
|
-
{ err:
|
|
7273
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8396
7274
|
"dispatcher: active-run flag set failed terminally; proceeding"
|
|
8397
7275
|
);
|
|
8398
7276
|
}
|
|
@@ -8429,35 +7307,6 @@ ${reason}`,
|
|
|
8429
7307
|
subAgentCreate,
|
|
8430
7308
|
wakeState
|
|
8431
7309
|
);
|
|
8432
|
-
const nativeTurnControl = {
|
|
8433
|
-
summon: (agentId) => {
|
|
8434
|
-
summonState.agentId = agentId;
|
|
8435
|
-
},
|
|
8436
|
-
skip: (reason) => {
|
|
8437
|
-
skipState.skipped = true;
|
|
8438
|
-
skipState.reason = reason;
|
|
8439
|
-
},
|
|
8440
|
-
ask: (p) => {
|
|
8441
|
-
askState.targetUserId = p.targetUserId;
|
|
8442
|
-
if (p.questions && p.questions.length > 0) {
|
|
8443
|
-
askState.questions = p.questions;
|
|
8444
|
-
askState.question = null;
|
|
8445
|
-
askState.headline = null;
|
|
8446
|
-
askState.options = null;
|
|
8447
|
-
} else {
|
|
8448
|
-
askState.question = p.question ?? null;
|
|
8449
|
-
askState.headline = p.headline ?? null;
|
|
8450
|
-
askState.options = p.options ?? null;
|
|
8451
|
-
askState.questions = null;
|
|
8452
|
-
}
|
|
8453
|
-
},
|
|
8454
|
-
wake: (p) => {
|
|
8455
|
-
wakeState.afterSeconds = p.afterSeconds ?? null;
|
|
8456
|
-
wakeState.at = p.at ?? null;
|
|
8457
|
-
wakeState.note = p.note;
|
|
8458
|
-
},
|
|
8459
|
-
subAgent: subAgentCreate
|
|
8460
|
-
};
|
|
8461
7310
|
const request = buildCompanionTurnRequest({
|
|
8462
7311
|
turnContext,
|
|
8463
7312
|
baseUrl: this.opts.baseUrl,
|
|
@@ -8467,11 +7316,11 @@ ${reason}`,
|
|
|
8467
7316
|
...turnContext.turnToken ? { turnToken: turnContext.turnToken } : {},
|
|
8468
7317
|
// SJ524: the hook-resolved cwd overrides the static local cwd.
|
|
8469
7318
|
...effectiveCwd ? { cwd: effectiveCwd } : {},
|
|
8470
|
-
|
|
8471
|
-
|
|
7319
|
+
// CT804: `turnEnv` = the prepare-hook env plus the per-turn checkout-local
|
|
7320
|
+
// TMPDIR (falls back to `hookEnv` when no cwd was resolved).
|
|
7321
|
+
...turnEnv ? { env: turnEnv } : {},
|
|
8472
7322
|
mcpServers: resolvedMcpServers,
|
|
8473
7323
|
summonServer,
|
|
8474
|
-
turnControl: nativeTurnControl,
|
|
8475
7324
|
// CT238: this turn's conversation, forwarded as the active-conversation
|
|
8476
7325
|
// header so a cross-thread post/spawn stamps its origin.
|
|
8477
7326
|
activeConversationId: payload.conversationId,
|
|
@@ -8489,22 +7338,19 @@ ${reason}`,
|
|
|
8489
7338
|
if (this.opts.codexEnabled) {
|
|
8490
7339
|
adapters.push(createCodexAdapter({ enabled: true, onWarn }));
|
|
8491
7340
|
}
|
|
8492
|
-
if (this.opts.cabaneNativeApiKey) {
|
|
8493
|
-
adapters.push(createCabaneNativeAdapter({ apiKey: this.opts.cabaneNativeApiKey, onWarn }));
|
|
8494
|
-
}
|
|
8495
7341
|
const registry = createAdapterRegistry(adapters);
|
|
8496
7342
|
let adapter;
|
|
8497
7343
|
try {
|
|
8498
7344
|
adapter = selectAdapter(registry, turnContext.runtime);
|
|
8499
|
-
} catch (
|
|
8500
|
-
if (!(
|
|
7345
|
+
} catch (err) {
|
|
7346
|
+
if (!(err instanceof RuntimeUnavailableError)) throw err;
|
|
8501
7347
|
turnLog.error(
|
|
8502
|
-
{ runtime:
|
|
7348
|
+
{ runtime: err.runtime, available: err.available },
|
|
8503
7349
|
"dispatcher: turn runtime not available on this device"
|
|
8504
7350
|
);
|
|
8505
7351
|
try {
|
|
8506
7352
|
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
8507
|
-
body: `${RUNTIME_UNAVAILABLE_PREFIX} ${
|
|
7353
|
+
body: `${RUNTIME_UNAVAILABLE_PREFIX} ${err.message}`,
|
|
8508
7354
|
kind: "final",
|
|
8509
7355
|
turnId,
|
|
8510
7356
|
parentMessageId: payload.messageId
|
|
@@ -8519,7 +7365,7 @@ ${reason}`,
|
|
|
8519
7365
|
payload,
|
|
8520
7366
|
turnLog,
|
|
8521
7367
|
startedAt,
|
|
8522
|
-
`runtime_unavailable:${
|
|
7368
|
+
`runtime_unavailable:${err.runtime}`
|
|
8523
7369
|
);
|
|
8524
7370
|
}
|
|
8525
7371
|
if (prepareHook && hookEnv?.CABANE_TASK_ID) {
|
|
@@ -8653,9 +7499,9 @@ ${reason}`,
|
|
|
8653
7499
|
skipState.skipped = true;
|
|
8654
7500
|
skipState.reason = intent.skipReason;
|
|
8655
7501
|
}
|
|
8656
|
-
} catch (
|
|
7502
|
+
} catch (err) {
|
|
8657
7503
|
turnLog.warn(
|
|
8658
|
-
{ err:
|
|
7504
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8659
7505
|
"dispatcher: turn-control intent fetch failed; turn-control effects for this turn are dropped"
|
|
8660
7506
|
);
|
|
8661
7507
|
}
|
|
@@ -8701,9 +7547,9 @@ ${reason}`,
|
|
|
8701
7547
|
payload.agentId,
|
|
8702
7548
|
{ agentSessionId: event.state }
|
|
8703
7549
|
);
|
|
8704
|
-
} catch (
|
|
7550
|
+
} catch (err) {
|
|
8705
7551
|
turnLog.warn(
|
|
8706
|
-
{ err:
|
|
7552
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8707
7553
|
"dispatcher: session-id write failed (will retry next turn)"
|
|
8708
7554
|
);
|
|
8709
7555
|
}
|
|
@@ -8752,18 +7598,18 @@ ${reason}`,
|
|
|
8752
7598
|
parentMessageId: payload.messageId,
|
|
8753
7599
|
...skipWake ? { wake: skipWake } : {}
|
|
8754
7600
|
});
|
|
8755
|
-
} catch (
|
|
7601
|
+
} catch (err) {
|
|
8756
7602
|
turnLog.warn(
|
|
8757
|
-
{ err:
|
|
7603
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8758
7604
|
"dispatcher: skipped-marker commit failed"
|
|
8759
7605
|
);
|
|
8760
7606
|
}
|
|
8761
7607
|
} else {
|
|
8762
7608
|
await committer.finalize(okResult);
|
|
8763
7609
|
}
|
|
8764
|
-
} catch (
|
|
7610
|
+
} catch (err) {
|
|
8765
7611
|
okResult = false;
|
|
8766
|
-
resultReason =
|
|
7612
|
+
resultReason = err instanceof Error ? err.message : String(err);
|
|
8767
7613
|
turnLog.error({ err: resultReason }, "dispatcher: SDK query threw");
|
|
8768
7614
|
} finally {
|
|
8769
7615
|
if (idleTimer) clearTimeout(idleTimer);
|
|
@@ -8799,9 +7645,9 @@ ${reason}`,
|
|
|
8799
7645
|
// CT113: the stopped marker is still "about" the triggering message.
|
|
8800
7646
|
parentMessageId: payload.messageId
|
|
8801
7647
|
});
|
|
8802
|
-
} catch (
|
|
7648
|
+
} catch (err) {
|
|
8803
7649
|
turnLog.warn(
|
|
8804
|
-
{ err:
|
|
7650
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8805
7651
|
"dispatcher: stopped-marker commit failed"
|
|
8806
7652
|
);
|
|
8807
7653
|
}
|
|
@@ -8842,9 +7688,9 @@ ${reason}`,
|
|
|
8842
7688
|
payload.agentId,
|
|
8843
7689
|
body
|
|
8844
7690
|
);
|
|
8845
|
-
} catch (
|
|
7691
|
+
} catch (err) {
|
|
8846
7692
|
turnLog.warn(
|
|
8847
|
-
{ err:
|
|
7693
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8848
7694
|
"dispatcher: active-run clear failed terminally; server age-sweep is the backstop"
|
|
8849
7695
|
);
|
|
8850
7696
|
}
|
|
@@ -8912,7 +7758,6 @@ function buildCompanionManifest(opts) {
|
|
|
8912
7758
|
if (opts.claudeCode) runtimes.push({ name: "claude-code", version: v.claudeCode ?? null });
|
|
8913
7759
|
if (opts.opencode) runtimes.push({ name: "opencode", version: v.opencode ?? null });
|
|
8914
7760
|
if (opts.codex) runtimes.push({ name: "codex", version: v.codex ?? null });
|
|
8915
|
-
if (opts.cabaneNative) runtimes.push({ name: "cabane-native", version: null });
|
|
8916
7761
|
return { runtimes, capabilities: { ...DEVICE_MANIFEST.capabilities } };
|
|
8917
7762
|
}
|
|
8918
7763
|
|
|
@@ -9035,8 +7880,8 @@ async function probeHarnessSignals(cfg, deps = {}) {
|
|
|
9035
7880
|
return {
|
|
9036
7881
|
claudeOnPath: claudeOnPathResult,
|
|
9037
7882
|
claudeVersion,
|
|
9038
|
-
// A parseable `codex --version` is our presence signal (
|
|
9039
|
-
//
|
|
7883
|
+
// A parseable `codex --version` is our presence signal (presence alone never
|
|
7884
|
+
// exposes codex; its config flag is the manifest gate either way).
|
|
9040
7885
|
codexOnPath: codexVersion !== null,
|
|
9041
7886
|
codexVersion,
|
|
9042
7887
|
codexEnabled: isCodexEnabled(cfg),
|
|
@@ -9116,7 +7961,7 @@ import {
|
|
|
9116
7961
|
existsSync as existsSync10,
|
|
9117
7962
|
mkdirSync as mkdirSync11,
|
|
9118
7963
|
readdirSync as readdirSync2,
|
|
9119
|
-
readFileSync as
|
|
7964
|
+
readFileSync as readFileSync8,
|
|
9120
7965
|
renameSync as renameSync3,
|
|
9121
7966
|
rmSync as rmSync5,
|
|
9122
7967
|
writeFileSync as writeFileSync7
|
|
@@ -9150,13 +7995,13 @@ var Outbox = class {
|
|
|
9150
7995
|
try {
|
|
9151
7996
|
writeFileSync7(tmp, JSON.stringify(entry) + "\n", "utf8");
|
|
9152
7997
|
renameSync3(tmp, target);
|
|
9153
|
-
} catch (
|
|
7998
|
+
} catch (err) {
|
|
9154
7999
|
try {
|
|
9155
8000
|
rmSync5(tmp, { force: true });
|
|
9156
8001
|
} catch {
|
|
9157
8002
|
}
|
|
9158
8003
|
this.log?.warn(
|
|
9159
|
-
{ workspaceId: this.workspaceId, err:
|
|
8004
|
+
{ workspaceId: this.workspaceId, err: err instanceof Error ? err.message : String(err) },
|
|
9160
8005
|
"companion outbox: failed to persist entry"
|
|
9161
8006
|
);
|
|
9162
8007
|
return;
|
|
@@ -9181,7 +8026,7 @@ var Outbox = class {
|
|
|
9181
8026
|
if (!name.endsWith(".json")) continue;
|
|
9182
8027
|
const full = join13(dir2, name);
|
|
9183
8028
|
try {
|
|
9184
|
-
const parsed = JSON.parse(
|
|
8029
|
+
const parsed = JSON.parse(readFileSync8(full, "utf8"));
|
|
9185
8030
|
if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
|
|
9186
8031
|
entries.push(parsed);
|
|
9187
8032
|
} else {
|
|
@@ -9253,40 +8098,43 @@ var Outbox = class {
|
|
|
9253
8098
|
};
|
|
9254
8099
|
|
|
9255
8100
|
// src/run-config.ts
|
|
9256
|
-
import { z as
|
|
9257
|
-
var mcpStdioServerSchema =
|
|
9258
|
-
type:
|
|
9259
|
-
command:
|
|
9260
|
-
args:
|
|
9261
|
-
env:
|
|
8101
|
+
import { z as z14 } from "zod";
|
|
8102
|
+
var mcpStdioServerSchema = z14.object({
|
|
8103
|
+
type: z14.literal("stdio").optional(),
|
|
8104
|
+
command: z14.string().min(1),
|
|
8105
|
+
args: z14.array(z14.string()).optional(),
|
|
8106
|
+
env: z14.record(z14.string(), z14.string()).optional()
|
|
9262
8107
|
});
|
|
9263
|
-
var mcpHttpServerSchema =
|
|
9264
|
-
type:
|
|
9265
|
-
url:
|
|
9266
|
-
headers:
|
|
8108
|
+
var mcpHttpServerSchema = z14.object({
|
|
8109
|
+
type: z14.literal("http"),
|
|
8110
|
+
url: z14.string().url(),
|
|
8111
|
+
headers: z14.record(z14.string(), z14.string()).optional()
|
|
9267
8112
|
});
|
|
9268
|
-
var mcpSseServerSchema =
|
|
9269
|
-
type:
|
|
9270
|
-
url:
|
|
9271
|
-
headers:
|
|
8113
|
+
var mcpSseServerSchema = z14.object({
|
|
8114
|
+
type: z14.literal("sse"),
|
|
8115
|
+
url: z14.string().url(),
|
|
8116
|
+
headers: z14.record(z14.string(), z14.string()).optional()
|
|
9272
8117
|
});
|
|
9273
|
-
var mcpServerDefSchema =
|
|
8118
|
+
var mcpServerDefSchema = z14.union([
|
|
9274
8119
|
mcpHttpServerSchema,
|
|
9275
8120
|
mcpSseServerSchema,
|
|
9276
8121
|
mcpStdioServerSchema
|
|
9277
8122
|
]);
|
|
9278
|
-
var thinkingConfigSchema =
|
|
9279
|
-
|
|
9280
|
-
|
|
9281
|
-
|
|
8123
|
+
var thinkingConfigSchema = z14.discriminatedUnion("type", [
|
|
8124
|
+
z14.object({ type: z14.literal("adaptive") }),
|
|
8125
|
+
z14.object({ type: z14.literal("enabled"), budgetTokens: z14.number().int().positive().optional() }),
|
|
8126
|
+
z14.object({ type: z14.literal("disabled") })
|
|
9282
8127
|
]);
|
|
9283
|
-
var effortSchema =
|
|
9284
|
-
var runConfigSchema =
|
|
9285
|
-
|
|
9286
|
-
|
|
9287
|
-
|
|
9288
|
-
|
|
9289
|
-
|
|
8128
|
+
var effortSchema = z14.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
8129
|
+
var runConfigSchema = z14.object({
|
|
8130
|
+
// CT788: the host-access binary replaced the `assistant`/`coding`/`custom` mode
|
|
8131
|
+
// trio + its custom tool lists — `true` grants the host filesystem/shell, absent
|
|
8132
|
+
// is the locked surface. Kept in lockstep with `@cabane/shared`'s
|
|
8133
|
+
// `agentRunConfigSchema` (independent zod, forward-compatible: unknown keys are
|
|
8134
|
+
// stripped, so an older companion riding a newer server never rejects the config).
|
|
8135
|
+
hostAccess: z14.boolean().optional(),
|
|
8136
|
+
mcpServers: z14.record(z14.string(), mcpServerDefSchema).optional(),
|
|
8137
|
+
model: z14.string().min(1).optional(),
|
|
9290
8138
|
thinking: thinkingConfigSchema.optional(),
|
|
9291
8139
|
effort: effortSchema.optional()
|
|
9292
8140
|
});
|
|
@@ -9331,20 +8179,20 @@ var SseSubscriber = class {
|
|
|
9331
8179
|
try {
|
|
9332
8180
|
await this.connect();
|
|
9333
8181
|
backoff = 500;
|
|
9334
|
-
} catch (
|
|
8182
|
+
} catch (err) {
|
|
9335
8183
|
if (this.aborted) return;
|
|
9336
|
-
if (
|
|
8184
|
+
if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
|
|
9337
8185
|
this.opts.log.error(
|
|
9338
|
-
{ workspaceId: this.opts.workspaceId, status:
|
|
8186
|
+
{ workspaceId: this.opts.workspaceId, status: err.status },
|
|
9339
8187
|
"SSE auth failed \u2014 tearing down this workspace subscriber"
|
|
9340
8188
|
);
|
|
9341
|
-
this.opts.onAuthFailure(
|
|
8189
|
+
this.opts.onAuthFailure(err.status);
|
|
9342
8190
|
return;
|
|
9343
8191
|
}
|
|
9344
8192
|
this.opts.log.warn(
|
|
9345
8193
|
{
|
|
9346
8194
|
workspaceId: this.opts.workspaceId,
|
|
9347
|
-
err:
|
|
8195
|
+
err: err instanceof Error ? err.message : String(err),
|
|
9348
8196
|
backoff
|
|
9349
8197
|
},
|
|
9350
8198
|
"SSE disconnected; reconnecting"
|
|
@@ -9487,7 +8335,7 @@ var CompanionSupervisor = class {
|
|
|
9487
8335
|
if (!this.config.deviceToken) {
|
|
9488
8336
|
this.log.warn("companion: not paired (no device token) \u2014 run `cabane-companion pair`");
|
|
9489
8337
|
process.stdout.write(
|
|
9490
|
-
"companion: this device is not paired \u2014 run `cabane-companion pair
|
|
8338
|
+
"companion: this device is not paired \u2014 run `cabane-companion pair`, then confirm the short code in Settings \u2192 Connectors.\n"
|
|
9491
8339
|
);
|
|
9492
8340
|
return;
|
|
9493
8341
|
}
|
|
@@ -9542,9 +8390,6 @@ var CompanionSupervisor = class {
|
|
|
9542
8390
|
// like opencode — the CLI's presence is the operator's responsibility;
|
|
9543
8391
|
// a misconfigured device fails the turn loudly, never silently).
|
|
9544
8392
|
codex: isCodexEnabled(this.config),
|
|
9545
|
-
// CT598: advertise the native runtime when an OpenRouter key is set. Key
|
|
9546
|
-
// absent → not advertised, so a native turn never routes here.
|
|
9547
|
-
cabaneNative: isCabaneNativeEnabled(),
|
|
9548
8393
|
// CT571/CT586: each runtime's `version` from the latest harness probe
|
|
9549
8394
|
// (fail-soft to null). Informational only — the server matches on name.
|
|
9550
8395
|
versions: this.harnessVersions
|
|
@@ -9562,9 +8407,9 @@ var CompanionSupervisor = class {
|
|
|
9562
8407
|
this.hub.setDevice({ deviceId: res.deviceId });
|
|
9563
8408
|
this.deviceId = res.deviceId;
|
|
9564
8409
|
this.checkVersionSkew(res.serverVersion);
|
|
9565
|
-
} catch (
|
|
8410
|
+
} catch (err) {
|
|
9566
8411
|
this.log.warn(
|
|
9567
|
-
{ err:
|
|
8412
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
9568
8413
|
"companion: device heartbeat failed (will retry on next tick)"
|
|
9569
8414
|
);
|
|
9570
8415
|
}
|
|
@@ -9600,12 +8445,12 @@ var CompanionSupervisor = class {
|
|
|
9600
8445
|
const resp = await this.deviceApi.getAssignments();
|
|
9601
8446
|
items = resp.assignments;
|
|
9602
8447
|
device = resp.device;
|
|
9603
|
-
} catch (
|
|
8448
|
+
} catch (err) {
|
|
9604
8449
|
this.log.error(
|
|
9605
|
-
{ err:
|
|
8450
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
9606
8451
|
"companion: assignments pull failed \u2014 check the device is still active in the cabane app"
|
|
9607
8452
|
);
|
|
9608
|
-
this.hub.setDeviceError(
|
|
8453
|
+
this.hub.setDeviceError(err instanceof Error ? err.message : String(err));
|
|
9609
8454
|
return;
|
|
9610
8455
|
}
|
|
9611
8456
|
this.hub.setDevice({ deviceId: device.id, deviceLabel: device.label });
|
|
@@ -9682,7 +8527,7 @@ var CompanionSupervisor = class {
|
|
|
9682
8527
|
agentId: it.agentId,
|
|
9683
8528
|
username: it.agentUsername,
|
|
9684
8529
|
displayName: it.agentDisplayName,
|
|
9685
|
-
mode: runConfig.
|
|
8530
|
+
mode: runConfig.hostAccess ? "full" : "none",
|
|
9686
8531
|
hasCredential: false,
|
|
9687
8532
|
missingSecrets: missing
|
|
9688
8533
|
});
|
|
@@ -9702,7 +8547,7 @@ var CompanionSupervisor = class {
|
|
|
9702
8547
|
agentId: it.agentId,
|
|
9703
8548
|
username: it.agentUsername,
|
|
9704
8549
|
displayName: it.agentDisplayName,
|
|
9705
|
-
mode: runConfig.
|
|
8550
|
+
mode: runConfig.hostAccess ? "full" : "none",
|
|
9706
8551
|
hasCredential: true,
|
|
9707
8552
|
missingSecrets: missing
|
|
9708
8553
|
});
|
|
@@ -9786,12 +8631,9 @@ var CompanionSupervisor = class {
|
|
|
9786
8631
|
// CT481: register the codex adapter when this device offers codex; unset
|
|
9787
8632
|
// leaves an `openai/…` turn to fail loudly (no silent claude-code fallback).
|
|
9788
8633
|
...isCodexEnabled(this.config) ? { codexEnabled: true } : {},
|
|
9789
|
-
// CT598: register the cabane-native adapter when an OpenRouter key is set;
|
|
9790
|
-
// unset leaves a `cabane-native/…` turn to fail loudly (no silent fallback).
|
|
9791
|
-
...cabaneNativeApiKey() ? { cabaneNativeApiKey: cabaneNativeApiKey() } : {},
|
|
9792
8634
|
// CT556: per-turn timeout watchdog windows, from the companion's own env
|
|
9793
8635
|
// (`AGENT_IDLE_TIMEOUT_MS` / `AGENT_TOTAL_TIMEOUT_MS`). Unset → the
|
|
9794
|
-
// dispatcher's baked-in defaults (10 min idle /
|
|
8636
|
+
// dispatcher's baked-in defaults (10 min idle / 6h total).
|
|
9795
8637
|
...positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") !== void 0 ? { idleTimeoutMs: positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") } : {},
|
|
9796
8638
|
...positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") !== void 0 ? { totalTimeoutMs: positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") } : {},
|
|
9797
8639
|
observer: this.hub.observerFor(ctx.workspaceId, ctx.agentId, ctx.workspaceSlug),
|
|
@@ -9858,9 +8700,9 @@ var CompanionSupervisor = class {
|
|
|
9858
8700
|
let wire;
|
|
9859
8701
|
try {
|
|
9860
8702
|
wire = JSON.parse(ev.data);
|
|
9861
|
-
} catch (
|
|
8703
|
+
} catch (err) {
|
|
9862
8704
|
this.log.warn(
|
|
9863
|
-
{ err:
|
|
8705
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
9864
8706
|
"malformed SSE payload"
|
|
9865
8707
|
);
|
|
9866
8708
|
return;
|
|
@@ -9907,13 +8749,13 @@ var CompanionSupervisor = class {
|
|
|
9907
8749
|
}
|
|
9908
8750
|
const chainKey = `${payload.conversationId}|${payload.agentId}`;
|
|
9909
8751
|
const prev = wr.chains.get(chainKey) ?? Promise.resolve();
|
|
9910
|
-
const tail = prev.then(() => this.runDispatch(wr, ev, payload, agent)).catch((
|
|
8752
|
+
const tail = prev.then(() => this.runDispatch(wr, ev, payload, agent)).catch((err) => {
|
|
9911
8753
|
this.log.warn(
|
|
9912
8754
|
{
|
|
9913
8755
|
workspaceId: wr.workspaceId,
|
|
9914
8756
|
conversationId: payload.conversationId,
|
|
9915
8757
|
agentId: payload.agentId,
|
|
9916
|
-
err:
|
|
8758
|
+
err: err instanceof Error ? err.message : String(err)
|
|
9917
8759
|
},
|
|
9918
8760
|
"companion: conversation turn handler threw"
|
|
9919
8761
|
);
|
|
@@ -10005,10 +8847,10 @@ var CompanionSupervisor = class {
|
|
|
10005
8847
|
} else {
|
|
10006
8848
|
drainDelay = DRAIN_BASE_MS;
|
|
10007
8849
|
}
|
|
10008
|
-
} catch (
|
|
8850
|
+
} catch (err) {
|
|
10009
8851
|
drainDelay = Math.min(drainDelay * 2, DRAIN_MAX_MS);
|
|
10010
8852
|
this.log.warn(
|
|
10011
|
-
{ agentId, err:
|
|
8853
|
+
{ agentId, err: err instanceof Error ? err.message : String(err) },
|
|
10012
8854
|
"companion: outbox drain pass threw (will retry with backoff)"
|
|
10013
8855
|
);
|
|
10014
8856
|
} finally {
|
|
@@ -10075,9 +8917,9 @@ var CompanionSupervisor = class {
|
|
|
10075
8917
|
codex: signals.codexVersion
|
|
10076
8918
|
};
|
|
10077
8919
|
this.hub.setHarnesses(deriveHarnessSnapshot(signals).harnesses);
|
|
10078
|
-
} catch (
|
|
8920
|
+
} catch (err) {
|
|
10079
8921
|
this.log.warn(
|
|
10080
|
-
{ err:
|
|
8922
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
10081
8923
|
"companion: harness probe failed (will retry on next beat)"
|
|
10082
8924
|
);
|
|
10083
8925
|
}
|
|
@@ -10175,10 +9017,13 @@ var CompanionSupervisor = class {
|
|
|
10175
9017
|
await Promise.race([
|
|
10176
9018
|
Promise.allSettled(turns),
|
|
10177
9019
|
new Promise((resolve) => {
|
|
10178
|
-
timer = setTimeout(
|
|
10179
|
-
|
|
10180
|
-
|
|
10181
|
-
|
|
9020
|
+
timer = setTimeout(
|
|
9021
|
+
() => {
|
|
9022
|
+
timedOut = true;
|
|
9023
|
+
resolve();
|
|
9024
|
+
},
|
|
9025
|
+
Math.max(0, graceMs)
|
|
9026
|
+
);
|
|
10182
9027
|
timer.unref?.();
|
|
10183
9028
|
})
|
|
10184
9029
|
]);
|
|
@@ -10232,17 +9077,17 @@ var RECOVERABLE_CODES = /* @__PURE__ */ new Set([
|
|
|
10232
9077
|
"ERR_STREAM_DESTROYED",
|
|
10233
9078
|
"ERR_STREAM_WRITE_AFTER_END"
|
|
10234
9079
|
]);
|
|
10235
|
-
function errorCode(
|
|
10236
|
-
if (
|
|
10237
|
-
const code =
|
|
9080
|
+
function errorCode(err) {
|
|
9081
|
+
if (err && typeof err === "object" && "code" in err) {
|
|
9082
|
+
const code = err.code;
|
|
10238
9083
|
if (typeof code === "string") return code;
|
|
10239
9084
|
}
|
|
10240
9085
|
return void 0;
|
|
10241
9086
|
}
|
|
10242
|
-
function isRecoverableSocketError(
|
|
10243
|
-
const code = errorCode(
|
|
9087
|
+
function isRecoverableSocketError(err) {
|
|
9088
|
+
const code = errorCode(err);
|
|
10244
9089
|
if (code && RECOVERABLE_CODES.has(code)) return true;
|
|
10245
|
-
const message =
|
|
9090
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
10246
9091
|
return /\bEPIPE\b|\bECONNRESET\b/.test(message);
|
|
10247
9092
|
}
|
|
10248
9093
|
function installProcessSafetyNet(log, opts = {}) {
|
|
@@ -10251,16 +9096,16 @@ function installProcessSafetyNet(log, opts = {}) {
|
|
|
10251
9096
|
stream.on("error", () => {
|
|
10252
9097
|
});
|
|
10253
9098
|
}
|
|
10254
|
-
proc.on("uncaughtException", (
|
|
9099
|
+
proc.on("uncaughtException", (err) => handleUncaught(log, err, "uncaughtException"));
|
|
10255
9100
|
proc.on(
|
|
10256
9101
|
"unhandledRejection",
|
|
10257
9102
|
(reason) => handleUncaught(log, reason, "unhandledRejection")
|
|
10258
9103
|
);
|
|
10259
9104
|
}
|
|
10260
|
-
function handleUncaught(log,
|
|
10261
|
-
const message =
|
|
10262
|
-
const code = errorCode(
|
|
10263
|
-
if (isRecoverableSocketError(
|
|
9105
|
+
function handleUncaught(log, err, origin) {
|
|
9106
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
9107
|
+
const code = errorCode(err);
|
|
9108
|
+
if (isRecoverableSocketError(err)) {
|
|
10264
9109
|
log.warn(
|
|
10265
9110
|
{ origin, code, err: message },
|
|
10266
9111
|
"companion: recovered from a socket error (kept running \u2014 a broken pipe never kills the companion)"
|
|
@@ -10268,13 +9113,13 @@ function handleUncaught(log, err2, origin) {
|
|
|
10268
9113
|
return;
|
|
10269
9114
|
}
|
|
10270
9115
|
log.error(
|
|
10271
|
-
{ origin, code, err: message, stack:
|
|
9116
|
+
{ origin, code, err: message, stack: err instanceof Error ? err.stack : void 0 },
|
|
10272
9117
|
"companion: uncaught error (kept running \u2014 see the stack above)"
|
|
10273
9118
|
);
|
|
10274
9119
|
}
|
|
10275
9120
|
|
|
10276
9121
|
// src/crash-marker.ts
|
|
10277
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync12, readFileSync as
|
|
9122
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync12, readFileSync as readFileSync9, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
|
|
10278
9123
|
import { join as join14 } from "path";
|
|
10279
9124
|
function crashMarkerPath() {
|
|
10280
9125
|
return join14(cabaneDir(), "last-error.json");
|
|
@@ -10305,14 +9150,14 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
10305
9150
|
cfg = requireConfig();
|
|
10306
9151
|
claudeCode = await probeClaude();
|
|
10307
9152
|
await ensureRuntimeAvailable(cfg, { probeClaude: async () => claudeCode });
|
|
10308
|
-
} catch (
|
|
9153
|
+
} catch (err) {
|
|
10309
9154
|
recordCrash({
|
|
10310
|
-
reason:
|
|
10311
|
-
...errorCode(
|
|
9155
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
9156
|
+
...errorCode(err) ? { code: errorCode(err) } : {},
|
|
10312
9157
|
origin: "startup",
|
|
10313
9158
|
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
10314
9159
|
});
|
|
10315
|
-
throw
|
|
9160
|
+
throw err;
|
|
10316
9161
|
}
|
|
10317
9162
|
if (cfg.logLevel) log.level = cfg.logLevel;
|
|
10318
9163
|
const harnessVersions = await probeHarnessVersions({
|
|
@@ -10403,6 +9248,8 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
10403
9248
|
|
|
10404
9249
|
// src/commands/start.ts
|
|
10405
9250
|
var FORCE_EXIT_MS = 4e3;
|
|
9251
|
+
var DEPLOY_REEXEC_EXIT = 75;
|
|
9252
|
+
var DEPLOY_GRACE_MS = 6.5 * 60 * 60 * 1e3;
|
|
10406
9253
|
async function start(opts = {}) {
|
|
10407
9254
|
const result = await createCompanionRuntime({
|
|
10408
9255
|
...opts.port !== void 0 ? { port: opts.port } : {}
|
|
@@ -10426,7 +9273,7 @@ Cabane Companion is running.
|
|
|
10426
9273
|
`);
|
|
10427
9274
|
if (!runtime.config.deviceToken) {
|
|
10428
9275
|
process.stdout.write(
|
|
10429
|
-
`This device isn't paired yet \u2014 run \`cabane-companion pair
|
|
9276
|
+
`This device isn't paired yet \u2014 run \`cabane-companion pair\`, then confirm the short code in Settings \u2192 Connectors.
|
|
10430
9277
|
|
|
10431
9278
|
`
|
|
10432
9279
|
);
|
|
@@ -10466,25 +9313,25 @@ companion: received ${signal}, shutting down\u2026
|
|
|
10466
9313
|
if (shuttingDown) return;
|
|
10467
9314
|
shuttingDown = true;
|
|
10468
9315
|
const configuredGrace = Number.parseInt(
|
|
10469
|
-
process.env.CABANE_COMPANION_DEPLOY_GRACE_MS ??
|
|
9316
|
+
process.env.CABANE_COMPANION_DEPLOY_GRACE_MS ?? String(DEPLOY_GRACE_MS),
|
|
10470
9317
|
10
|
|
10471
9318
|
);
|
|
10472
|
-
const graceMs = Number.isFinite(configuredGrace) ? configuredGrace :
|
|
9319
|
+
const graceMs = Number.isFinite(configuredGrace) ? configuredGrace : DEPLOY_GRACE_MS;
|
|
10473
9320
|
process.stdout.write(`
|
|
10474
9321
|
companion: deploy drain requested (${graceMs}ms grace)\u2026
|
|
10475
9322
|
`);
|
|
10476
9323
|
void runtime.drainForRestart(graceMs).then(({ drained }) => {
|
|
10477
9324
|
process.stdout.write(
|
|
10478
|
-
drained ? "companion: deploy drain complete.\n" : "companion: deploy grace expired; unfinished turns
|
|
9325
|
+
drained ? "companion: deploy drain complete; re-execing onto the new dist.\n" : "companion: deploy grace expired; re-execing \u2014 unfinished turns resume after restart.\n"
|
|
10479
9326
|
);
|
|
10480
9327
|
resolve();
|
|
10481
|
-
process.exit(
|
|
10482
|
-
}).catch((
|
|
9328
|
+
process.exit(DEPLOY_REEXEC_EXIT);
|
|
9329
|
+
}).catch((err) => {
|
|
10483
9330
|
process.stderr.write(
|
|
10484
|
-
`companion: deploy drain failed: ${
|
|
9331
|
+
`companion: deploy drain failed: ${err instanceof Error ? err.message : String(err)}
|
|
10485
9332
|
`
|
|
10486
9333
|
);
|
|
10487
|
-
process.exit(
|
|
9334
|
+
process.exit(DEPLOY_REEXEC_EXIT);
|
|
10488
9335
|
});
|
|
10489
9336
|
});
|
|
10490
9337
|
});
|
|
@@ -10496,7 +9343,7 @@ async function status() {
|
|
|
10496
9343
|
const cfg = loadConfig();
|
|
10497
9344
|
if (!cfg || !cfg.deviceToken) {
|
|
10498
9345
|
process.stdout.write(
|
|
10499
|
-
"companion: not paired.
|
|
9346
|
+
"companion: not paired. Run `cabane-companion pair`, then confirm the short code in Settings \u2192 Connectors.\n"
|
|
10500
9347
|
);
|
|
10501
9348
|
process.exitCode = 1;
|
|
10502
9349
|
return;
|
|
@@ -10628,7 +9475,7 @@ function isAlive(kill, pid) {
|
|
|
10628
9475
|
}
|
|
10629
9476
|
|
|
10630
9477
|
// src/commands/transcript.ts
|
|
10631
|
-
import { existsSync as existsSync12, readFileSync as
|
|
9478
|
+
import { existsSync as existsSync12, readFileSync as readFileSync10, readdirSync as readdirSync4 } from "fs";
|
|
10632
9479
|
import { isAbsolute, join as join15 } from "path";
|
|
10633
9480
|
async function transcript(opts = {}) {
|
|
10634
9481
|
const dir2 = transcriptsDir();
|
|
@@ -10722,14 +9569,14 @@ var TranscriptFollower = class {
|
|
|
10722
9569
|
function isComplete(content) {
|
|
10723
9570
|
for (const line of content.split("\n")) {
|
|
10724
9571
|
if (!line.trim()) continue;
|
|
10725
|
-
if (
|
|
9572
|
+
if (str2(rec(safeParse(line))?.type) === "_outcome") return true;
|
|
10726
9573
|
}
|
|
10727
9574
|
return false;
|
|
10728
9575
|
}
|
|
10729
9576
|
async function followTranscripts(dir2) {
|
|
10730
9577
|
const follower = new TranscriptFollower({
|
|
10731
9578
|
listFiles: () => listFiles(dir2),
|
|
10732
|
-
read: (f) =>
|
|
9579
|
+
read: (f) => readFileSync10(join15(dir2, f), "utf8"),
|
|
10733
9580
|
write: (s) => process.stdout.write(s),
|
|
10734
9581
|
// CSI: cursor up `n` lines, then erase from cursor to end of screen.
|
|
10735
9582
|
clearLines: (n) => process.stdout.write(`\x1B[${n}A\x1B[0J`),
|
|
@@ -10771,13 +9618,13 @@ function printList(dir2) {
|
|
|
10771
9618
|
for (const f of files.slice(0, 20)) {
|
|
10772
9619
|
const { meta, outcome } = peek(join15(dir2, f));
|
|
10773
9620
|
const when = fmtTime(rec(meta)?.ts);
|
|
10774
|
-
const ws =
|
|
9621
|
+
const ws = str2(rec(meta)?.workspaceSlug);
|
|
10775
9622
|
const o = rec(outcome);
|
|
10776
|
-
const verdict = o ? o.ok === true ? "ok" : `ERROR${
|
|
9623
|
+
const verdict = o ? o.ok === true ? "ok" : `ERROR${str2(o.reason) ? ` (${str2(o.reason)})` : ""}` : "\u2026";
|
|
10777
9624
|
process.stdout.write(
|
|
10778
9625
|
` ${f}
|
|
10779
9626
|
${when} \xB7 ${ws} \xB7 ${verdict}
|
|
10780
|
-
\u201C${excerpt(
|
|
9627
|
+
\u201C${excerpt(str2(rec(meta)?.message), 70)}\u201D
|
|
10781
9628
|
|
|
10782
9629
|
`
|
|
10783
9630
|
);
|
|
@@ -10790,10 +9637,10 @@ function peek(path3) {
|
|
|
10790
9637
|
let meta;
|
|
10791
9638
|
let outcome;
|
|
10792
9639
|
try {
|
|
10793
|
-
for (const line of
|
|
9640
|
+
for (const line of readFileSync10(path3, "utf8").split("\n")) {
|
|
10794
9641
|
if (!line.trim()) continue;
|
|
10795
9642
|
const o = safeParse(line);
|
|
10796
|
-
const t =
|
|
9643
|
+
const t = str2(rec(o)?.type);
|
|
10797
9644
|
if (t === "_meta") meta = o;
|
|
10798
9645
|
else if (t === "_outcome") outcome = o;
|
|
10799
9646
|
}
|
|
@@ -10823,10 +9670,10 @@ function resolveTarget(dir2, target) {
|
|
|
10823
9670
|
function renderFile(path3) {
|
|
10824
9671
|
let content;
|
|
10825
9672
|
try {
|
|
10826
|
-
content =
|
|
10827
|
-
} catch (
|
|
9673
|
+
content = readFileSync10(path3, "utf8");
|
|
9674
|
+
} catch (err) {
|
|
10828
9675
|
throw new CompanionError(
|
|
10829
|
-
`couldn't read ${path3}: ${
|
|
9676
|
+
`couldn't read ${path3}: ${err instanceof Error ? err.message : String(err)}`
|
|
10830
9677
|
);
|
|
10831
9678
|
}
|
|
10832
9679
|
return renderTranscript(content.split("\n"));
|
|
@@ -10838,19 +9685,19 @@ function renderTranscript(jsonlLines) {
|
|
|
10838
9685
|
if (!raw.trim()) continue;
|
|
10839
9686
|
const obj = rec(safeParse(raw));
|
|
10840
9687
|
if (!obj) continue;
|
|
10841
|
-
switch (
|
|
9688
|
+
switch (str2(obj.type)) {
|
|
10842
9689
|
case "_meta":
|
|
10843
9690
|
out.push(
|
|
10844
|
-
`${fmtTime(obj.ts)} \xB7 workspace=${
|
|
9691
|
+
`${fmtTime(obj.ts)} \xB7 workspace=${str2(obj.workspaceSlug)} \xB7 conversation=${str2(obj.conversationId)}`
|
|
10845
9692
|
);
|
|
10846
|
-
out.push("", `> USER: ${
|
|
9693
|
+
out.push("", `> USER: ${str2(obj.message)}`, "");
|
|
10847
9694
|
break;
|
|
10848
9695
|
case "system":
|
|
10849
|
-
if (
|
|
10850
|
-
out.push(`[session ${
|
|
9696
|
+
if (str2(obj.subtype) === "init") {
|
|
9697
|
+
out.push(`[session ${str2(obj.session_id) || "?"} \xB7 model ${str2(obj.model) || "?"}]`);
|
|
10851
9698
|
const servers = Array.isArray(obj.mcp_servers) ? obj.mcp_servers.map((s) => {
|
|
10852
9699
|
const r = rec(s);
|
|
10853
|
-
return r ? `${
|
|
9700
|
+
return r ? `${str2(r.name) || "?"}${str2(r.status) ? `(${str2(r.status)})` : ""}` : "";
|
|
10854
9701
|
}).filter(Boolean).join(", ") : "";
|
|
10855
9702
|
if (servers) out.push(` MCP servers: ${servers}`);
|
|
10856
9703
|
if (Array.isArray(obj.tools)) out.push(` tools: ${obj.tools.length} available`);
|
|
@@ -10876,8 +9723,8 @@ function renderTranscript(jsonlLines) {
|
|
|
10876
9723
|
if (!Array.isArray(content)) break;
|
|
10877
9724
|
for (const b of content) {
|
|
10878
9725
|
const block = rec(b);
|
|
10879
|
-
if (!block ||
|
|
10880
|
-
const id =
|
|
9726
|
+
if (!block || str2(block.type) !== "tool_result") continue;
|
|
9727
|
+
const id = str2(block.tool_use_id);
|
|
10881
9728
|
const label = pending.get(id) ?? "tool";
|
|
10882
9729
|
pending.delete(id);
|
|
10883
9730
|
const tag = block.is_error === true ? "ERROR" : "ok";
|
|
@@ -10886,18 +9733,18 @@ function renderTranscript(jsonlLines) {
|
|
|
10886
9733
|
break;
|
|
10887
9734
|
}
|
|
10888
9735
|
case "result": {
|
|
10889
|
-
const isErr = obj.is_error === true ||
|
|
9736
|
+
const isErr = obj.is_error === true || str2(obj.subtype) !== "success";
|
|
10890
9737
|
const dur = typeof obj.duration_ms === "number" ? ` \xB7 ${obj.duration_ms}ms` : "";
|
|
10891
9738
|
out.push(
|
|
10892
|
-
`[result ${isErr ? "error" : "ok"}${
|
|
9739
|
+
`[result ${isErr ? "error" : "ok"}${str2(obj.subtype) ? ` \xB7 ${str2(obj.subtype)}` : ""}${dur}]`
|
|
10893
9740
|
);
|
|
10894
|
-
if (isErr &&
|
|
9741
|
+
if (isErr && str2(obj.result).trim()) out.push(` ${indent(str2(obj.result))}`);
|
|
10895
9742
|
break;
|
|
10896
9743
|
}
|
|
10897
9744
|
case "_outcome": {
|
|
10898
9745
|
const dur = typeof obj.durationMs === "number" ? ` \xB7 ${obj.durationMs}ms` : "";
|
|
10899
9746
|
out.push(
|
|
10900
|
-
`[outcome ${obj.ok === true ? "ok" : "error"}${
|
|
9747
|
+
`[outcome ${obj.ok === true ? "ok" : "error"}${str2(obj.reason) ? ` \xB7 ${str2(obj.reason)}` : ""}${dur}]`
|
|
10901
9748
|
);
|
|
10902
9749
|
break;
|
|
10903
9750
|
}
|
|
@@ -10915,7 +9762,7 @@ function safeParse(s) {
|
|
|
10915
9762
|
function rec(v) {
|
|
10916
9763
|
return v && typeof v === "object" ? v : null;
|
|
10917
9764
|
}
|
|
10918
|
-
function
|
|
9765
|
+
function str2(v) {
|
|
10919
9766
|
return typeof v === "string" ? v : "";
|
|
10920
9767
|
}
|
|
10921
9768
|
function fmtTime(ts) {
|
|
@@ -10954,22 +9801,14 @@ program.name("cabane-companion").description(
|
|
|
10954
9801
|
).version(COMPANION_VERSION);
|
|
10955
9802
|
program.command("pair").description(
|
|
10956
9803
|
"pair this device with cabane \u2014 shows a short code you confirm in Settings \u2192 Devices."
|
|
10957
|
-
).
|
|
10958
|
-
|
|
10959
|
-
|
|
10960
|
-
)
|
|
10961
|
-
|
|
10962
|
-
|
|
10963
|
-
)
|
|
10964
|
-
|
|
10965
|
-
await pair({
|
|
10966
|
-
...pairingString !== void 0 ? { arg: pairingString } : {},
|
|
10967
|
-
...opts.server !== void 0 ? { server: opts.server } : {},
|
|
10968
|
-
...opts.legacy ? { legacy: true } : {},
|
|
10969
|
-
...opts.file !== void 0 ? { file: opts.file } : {}
|
|
10970
|
-
});
|
|
10971
|
-
}
|
|
10972
|
-
);
|
|
9804
|
+
).allowExcessArguments(false).option("--server <url>", "the cabane instance to pair with (default https://cabane.ai)").action(async (opts) => {
|
|
9805
|
+
await pair({
|
|
9806
|
+
...opts.server !== void 0 ? { server: opts.server } : {}
|
|
9807
|
+
});
|
|
9808
|
+
});
|
|
9809
|
+
program.command("write-paired-config", { hidden: true }).description("persist an already-completed device enrollment payload from stdin.").action(() => {
|
|
9810
|
+
writeCompletedPairing(readFileSync11(0, "utf8"));
|
|
9811
|
+
});
|
|
10973
9812
|
program.command("start").description("pull this device\u2019s assigned agents from cabane and run them.").option("--open", "open the dashboard in a browser on startup (default: off)").option("--no-open", "don't auto-open the dashboard (overrides config.autoOpen)").option("--daemon", "run detached in the background (terminal returns; replies keep landing)").option("--port <port>", "dashboard port (default 7474; falls through if taken)", parsePort).action(async (opts) => {
|
|
10974
9813
|
if (opts.daemon) {
|
|
10975
9814
|
await startDaemon({
|
|
@@ -11015,19 +9854,19 @@ function parsePort(raw) {
|
|
|
11015
9854
|
}
|
|
11016
9855
|
return n;
|
|
11017
9856
|
}
|
|
11018
|
-
program.parseAsync(process.argv).catch((
|
|
11019
|
-
if (
|
|
11020
|
-
process.stderr.write(`error: ${
|
|
9857
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
9858
|
+
if (err instanceof CompanionError) {
|
|
9859
|
+
process.stderr.write(`error: ${err.message}
|
|
11021
9860
|
`);
|
|
11022
9861
|
process.exitCode = 1;
|
|
11023
9862
|
return;
|
|
11024
9863
|
}
|
|
11025
|
-
if (
|
|
9864
|
+
if (err && typeof err === "object" && "name" in err && err.name === "ExitPromptError") {
|
|
11026
9865
|
process.stderr.write("cancelled\n");
|
|
11027
9866
|
process.exitCode = 130;
|
|
11028
9867
|
return;
|
|
11029
9868
|
}
|
|
11030
|
-
process.stderr.write(`${
|
|
9869
|
+
process.stderr.write(`${err instanceof Error ? err.stack ?? err.message : String(err)}
|
|
11031
9870
|
`);
|
|
11032
9871
|
process.exitCode = 1;
|
|
11033
9872
|
});
|