@cabane/companion 0.6.144 → 0.6.146
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +650 -1022
- package/dist/pairing-config.js +56 -95
- package/dist/runtime.js +644 -1014
- package/dist/static/app.js +9 -11
- package/package.json +1 -1
package/dist/runtime.js
CHANGED
|
@@ -73,18 +73,15 @@ var prepareHookSchema = z.object({
|
|
|
73
73
|
// default is generous; a hook that hangs past this is killed and the turn
|
|
74
74
|
// fails with a clear timeout message rather than pinning the companion.
|
|
75
75
|
timeoutMs: z.number().int().positive().optional(),
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
76
|
+
// Re-invoke the hook when a prepared result already exists, in VALIDATE
|
|
77
|
+
// mode: the cached result is handed back as `prepared` and stdout is
|
|
78
|
+
// ignored, so the cwd cannot move. Its only power is to exit non-zero and
|
|
79
|
+
// fail the turn. The hook resolves once per (conversation, agent), so a
|
|
80
|
+
// later message asking for a different environment is otherwise silently
|
|
81
|
+
// ignored; this lets a hook refuse it out loud.
|
|
80
82
|
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
// this, it is silently ignored — the host knows and says nothing, which is
|
|
84
|
-
// the worst available outcome. An opt-in hook can now refuse it out loud.
|
|
85
|
-
//
|
|
86
|
-
// Default OFF, and deliberately: an operator hook that provisions
|
|
87
|
-
// unconditionally must not start running on every turn because it upgraded.
|
|
83
|
+
// Default OFF: a hook that provisions unconditionally must not start
|
|
84
|
+
// running on every turn because the companion upgraded.
|
|
88
85
|
validateCached: z.boolean().optional(),
|
|
89
86
|
// Independent operator-owned reporter. Receives sanitized failure metadata
|
|
90
87
|
// on stdin; never re-enters this hook and cannot change the turn outcome.
|
|
@@ -150,16 +147,15 @@ var runPrepareHook = (hook, input) => {
|
|
|
150
147
|
CABANE_AGENT_USERNAME: input.agentUsername,
|
|
151
148
|
CABANE_RUNTIME: input.runtime ?? "",
|
|
152
149
|
CABANE_HOST_ACCESS: input.hostAccess ? "1" : "0",
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
// them doesn't crash; a hook should key off CABANE_TRIGGER_ENTRY_PATHS.
|
|
150
|
+
// DEPRECATED constants, kept so an old prepare script that reads them
|
|
151
|
+
// doesn't crash; a hook should key off CABANE_TRIGGER_ENTRY_PATHS.
|
|
156
152
|
CABANE_ANCHOR_KIND: "workspace",
|
|
157
153
|
CABANE_ANCHOR_PATH: "",
|
|
158
|
-
//
|
|
154
|
+
// Newline-joined — workspace paths never contain a newline, so a
|
|
159
155
|
// hook splits on `\n` unambiguously. Empty string when none.
|
|
160
156
|
CABANE_TRIGGER_ENTRY_PATHS: input.triggerEntryPaths.join("\n"),
|
|
161
157
|
CABANE_CONVERSATION_TITLE: input.title ?? "",
|
|
162
|
-
//
|
|
158
|
+
// The dispatching message's text, and — in validate mode only —
|
|
163
159
|
// the prepared result the hook is being asked to check this dispatch
|
|
164
160
|
// against. Both empty/absent on an ordinary first run.
|
|
165
161
|
CABANE_TRIGGER_MESSAGE: input.messageBody ?? "",
|
|
@@ -253,18 +249,17 @@ function configPath() {
|
|
|
253
249
|
var localAgentConfigSchema = z2.object({
|
|
254
250
|
cwd: z2.string().optional(),
|
|
255
251
|
prepareHook: prepareHookSchema.optional(),
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
252
|
+
// The Claude Code auto-memory escape hatch. Auto-memory is forced OFF by
|
|
253
|
+
// default (memory belongs in the Cabane workspace, and a shared companion
|
|
254
|
+
// would pool one cwd-keyed memory dir across users). On a companion you run
|
|
255
|
+
// yourself, `claudeCode: { autoMemory: true }` hands auto-memory back to your
|
|
256
|
+
// own `~/.claude/settings.json` — Cabane then stops
|
|
261
257
|
// injecting the off switch and your normal Claude Code memory workflow applies
|
|
262
258
|
// (in coding mode, where the checkout's project settings are read).
|
|
263
259
|
claudeCode: z2.object({ autoMemory: z2.boolean().optional() }).strict().optional(),
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
// row keyed by the agent's id (`agentsConfigSchema`).
|
|
260
|
+
// This agent's exception to `runners.sandbox`: an ops agent that runs with
|
|
261
|
+
// the machine on a sandboxed device, or one sandboxed agent on a device that
|
|
262
|
+
// isn't. Absent is "inherit". Only on a row keyed by the agent's id.
|
|
268
263
|
sandbox: z2.enum(["os", "none"]).optional()
|
|
269
264
|
}).strict();
|
|
270
265
|
var AGENT_ID_KEY = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -280,9 +275,8 @@ var agentsConfigSchema = z2.record(z2.string(), localAgentConfigSchema).superRef
|
|
|
280
275
|
}
|
|
281
276
|
});
|
|
282
277
|
var companionConfigSchema = z2.object({
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
// base URL onto the MITM-able channel the device token + prompt ride.
|
|
278
|
+
// https-enforced (loopback exempt), so a hand-edited config can't put the
|
|
279
|
+
// device token and prompt on a plaintext channel.
|
|
286
280
|
baseUrl: z2.string().url().refine(isAllowedBaseUrl, {
|
|
287
281
|
message: "baseUrl must be https (loopback http is allowed for local dev only)"
|
|
288
282
|
}),
|
|
@@ -300,91 +294,59 @@ var companionConfigSchema = z2.object({
|
|
|
300
294
|
// agentId / username / `slug/username`. Hand-added by the operator; the companion
|
|
301
295
|
// never writes this (it only persists credentials + the device, elsewhere).
|
|
302
296
|
agents: agentsConfigSchema.optional(),
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
// config a second silent roster: a device whose provisioning is genuinely
|
|
309
|
-
// per-device (ours resolves a directory from the conversation and agent ids the
|
|
310
|
-
// companion already hands the hook) still had to list every agent by id, and an
|
|
311
|
-
// agent nobody remembered to add got no hook at all — no error, no log, just a
|
|
312
|
-
// turn that started in the operator's `$HOME`. That is how @dale woke in
|
|
313
|
-
// `/home/joshed` on 2026-08-17 with a perfectly correct dispatch.
|
|
314
|
-
//
|
|
315
|
-
// Absent → nothing changes for anyone, which is why this can land inert: a
|
|
316
|
-
// device with no device-level entry behaves exactly as it did before. A hook
|
|
317
|
-
// that fails still fails the dispatch loudly, as now.
|
|
297
|
+
// The DEVICE-LEVEL prepare hook, for every agent assigned here unless its own
|
|
298
|
+
// `agents` row names one. Per-agent hooks alone make the config a second
|
|
299
|
+
// silent roster: an agent nobody listed gets no hook and no error, and starts
|
|
300
|
+
// in the operator's `$HOME` (an agent woke in `/home/joshed` this way on
|
|
301
|
+
// 2026-08-17).
|
|
318
302
|
prepareHook: prepareHookSchema.optional(),
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
// what they say to the Electron shell, the one front-end that renders a
|
|
323
|
-
// dashboard. `logLevel` is unaffected: pino level, live-editable.
|
|
303
|
+
// `dashboardPort` and `autoOpen` are IGNORED by the CLI (no port, no browser)
|
|
304
|
+
// but stay in the schema so an existing file still parses; the Electron shell
|
|
305
|
+
// reads them. `logLevel` is pino's level, live-editable.
|
|
324
306
|
dashboardPort: z2.number().int().min(1).max(65535).optional(),
|
|
325
307
|
autoOpen: z2.boolean().optional(),
|
|
326
308
|
logLevel: z2.enum(["warn", "info", "debug"]).optional(),
|
|
327
309
|
logFormat: z2.enum(["human", "json"]).optional(),
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
// here. Setting this makes the device advertise the `opencode` runtime on its
|
|
332
|
-
// heartbeat manifest (so the server offers DeepSeek/opencode models here and
|
|
333
|
-
// routes those turns to this device) AND registers the opencode adapter in the
|
|
334
|
-
// dispatcher. Absent → the device is claude-code-only, exactly as before.
|
|
310
|
+
// An `opencode serve` the operator runs here (auth via opencode's own
|
|
311
|
+
// `/connect`; Cabane never sees provider keys). Set, the device advertises the
|
|
312
|
+
// `opencode` runtime and registers its adapter.
|
|
335
313
|
opencode: z2.object({
|
|
336
314
|
serverUrl: z2.string().url()
|
|
337
315
|
}).strict().optional(),
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
// like codex. CT1379: the binary that must be there is the Agent SDK's own
|
|
343
|
-
// bundled executable — the file a turn actually launches — not the user's
|
|
344
|
-
// `claude` on PATH, which the SDK never runs. The manifest advertises
|
|
345
|
-
// claude-code only when this block says so AND that binary resolves.
|
|
316
|
+
// Whether the user connected Claude Code on this machine: consent, opted into
|
|
317
|
+
// like codex. The manifest advertises claude-code only when this says so AND
|
|
318
|
+
// the Agent SDK's bundled executable resolves (not `claude` on PATH, which the
|
|
319
|
+
// SDK never runs).
|
|
346
320
|
//
|
|
347
|
-
// The block's PRESENCE is also the migration marker
|
|
348
|
-
//
|
|
349
|
-
// asked
|
|
350
|
-
// written from here on carries the block explicitly, including a freshly-paired
|
|
351
|
-
// one, which starts at `enabled: false`: a new device is connected to nothing
|
|
352
|
-
// until its user says otherwise.
|
|
321
|
+
// The block's PRESENCE is also the migration marker
|
|
322
|
+
// (`migrateConnectedHarnesses`): absent means a config whose user was never
|
|
323
|
+
// asked. So every config written carries it, a fresh pair at `enabled: false`.
|
|
353
324
|
//
|
|
354
|
-
// Not
|
|
355
|
-
//
|
|
325
|
+
// Not the per-agent `agents.<key>.claudeCode.autoMemory`, one agent's memory
|
|
326
|
+
// switch.
|
|
356
327
|
claudeCode: z2.object({
|
|
357
328
|
enabled: z2.boolean().optional()
|
|
358
329
|
}).strict().optional(),
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
// `
|
|
364
|
-
// true } }`. Presence of the block enables it (an explicit `enabled: false`
|
|
365
|
-
// keeps the block but turns it off). Absent → the device doesn't offer codex.
|
|
366
|
-
// CT1379: enabling is now half the answer, not all of it — the manifest and the
|
|
367
|
-
// adapter registry additionally require the SDK's vendored `codex` binary to
|
|
368
|
-
// resolve, because the enable flag alone advertised a runtime a device with a
|
|
369
|
-
// half-installed platform package could not start.
|
|
330
|
+
// Codex is a local CLI the SDK spawns per turn, so this is an opt-in flag, not
|
|
331
|
+
// an address. The operator logs in (`codex login` or `CODEX_API_KEY`; Cabane
|
|
332
|
+
// never sees the key). The block's presence enables it; `enabled: false` turns
|
|
333
|
+
// it off. Half the answer: advertising also requires the SDK's vendored
|
|
334
|
+
// `codex` binary to resolve.
|
|
370
335
|
codex: z2.object({
|
|
371
336
|
enabled: z2.boolean().optional()
|
|
372
337
|
}).strict().optional(),
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
// (`runner/sandbox-policy.ts`). It is validated there rather than here, so a
|
|
380
|
-
// bad policy refuses the turns it governs, with the field named, instead of
|
|
381
|
-
// keeping the companion from starting at all.
|
|
338
|
+
// Run each turn in a child runner process. Absent or `enabled: false` is the
|
|
339
|
+
// in-process path. `sandbox: 'os'` wraps the runner in an OS sandbox where the
|
|
340
|
+
// host supports one; `'none'` (the default) is a plain child, supported in its
|
|
341
|
+
// own right. `policy` is what the sandbox lets a turn reach, read and hold,
|
|
342
|
+
// validated in `runner/sandbox-policy.ts` so a bad policy refuses the turns it
|
|
343
|
+
// governs, field named, instead of stopping the companion starting.
|
|
382
344
|
runners: z2.object({
|
|
383
345
|
enabled: z2.boolean().optional(),
|
|
384
346
|
provider: z2.enum(["local-process", "docker"]).optional(),
|
|
385
347
|
sandbox: z2.enum(["os", "none"]).optional(),
|
|
386
|
-
//
|
|
387
|
-
//
|
|
348
|
+
// A container per conversation and agent, from one image, on a network
|
|
349
|
+
// whose only way out is an allow-listed proxy.
|
|
388
350
|
docker: z2.object({
|
|
389
351
|
// The base image a container runs. Default: the one this companion
|
|
390
352
|
// builds from its own Dockerfile on first use. One named here must
|
|
@@ -877,9 +839,8 @@ var DEFAULT_OPENCODE_SERVER_URL = "http://127.0.0.1:4096";
|
|
|
877
839
|
function deriveHarnessSnapshot(signals) {
|
|
878
840
|
const advertised = new Set(
|
|
879
841
|
buildCompanionManifest({
|
|
880
|
-
//
|
|
881
|
-
//
|
|
882
|
-
// rather than re-decided. PATH is deliberately absent from both gates now.
|
|
842
|
+
// Connected AND the bundled binary present: the manifest's own rule. PATH
|
|
843
|
+
// is deliberately in neither gate.
|
|
883
844
|
claudeCode: signals.claudeCodeConnected && signals.claudeBundled === "present",
|
|
884
845
|
opencode: signals.opencodeConfigured,
|
|
885
846
|
codex: signals.codexEnabled && signals.codexBundled === "present"
|
|
@@ -917,10 +878,8 @@ function deriveClaudeCode(signals, manifestHas) {
|
|
|
917
878
|
...base,
|
|
918
879
|
state: "detected_not_exposed",
|
|
919
880
|
version: signals.claudeVersion,
|
|
920
|
-
//
|
|
921
|
-
//
|
|
922
|
-
// device still advertises nothing — so this says so up front rather than
|
|
923
|
-
// promising routing it can't deliver.
|
|
881
|
+
// Connecting records INTENT, not runnability: with the bundled binary
|
|
882
|
+
// missing a connect succeeds and nothing is advertised, so say so.
|
|
924
883
|
detail: signals.claudeBundled === "present" ? "Claude Code is installed here but not connected yet. Connect it to offer Claude Code from this device." : `Claude Code is installed here but not connected yet \u2014 and @cabane/companion's bundled Claude Code ${missingNoun(signals.claudeBundled)} is missing, so connecting alone won't make it runnable. ${repairAction()}`,
|
|
925
884
|
enable: "claude-code"
|
|
926
885
|
};
|
|
@@ -958,8 +917,7 @@ function deriveCodex(signals, manifestHas) {
|
|
|
958
917
|
...base,
|
|
959
918
|
state: "detected_not_exposed",
|
|
960
919
|
version: signals.codexVersion,
|
|
961
|
-
//
|
|
962
|
-
// launchability.
|
|
920
|
+
// As for Claude Code: enabling is intent, not launchability.
|
|
963
921
|
detail: signals.codexBundled === "present" ? "Codex is installed but not enabled yet. Turn it on to offer Codex from this device." : `Codex is installed but not enabled yet \u2014 and @cabane/companion's bundled Codex ${missingNoun(signals.codexBundled)} is missing, so enabling alone won't make it runnable. ${repairAction()}`,
|
|
964
922
|
enable: "codex"
|
|
965
923
|
};
|
|
@@ -1040,12 +998,10 @@ async function probeHarnessSignals(cfg, deps = {}) {
|
|
|
1040
998
|
return {
|
|
1041
999
|
claudeOnPath: claudePresence.status === "present",
|
|
1042
1000
|
claudeVersion,
|
|
1043
|
-
//
|
|
1044
|
-
// the manifest gate and the probe above is only a suggestion.
|
|
1001
|
+
// The user's opt-in, the manifest gate; the probe above only suggests.
|
|
1045
1002
|
claudeCodeConnected: isClaudeCodeConnected(cfg),
|
|
1046
|
-
//
|
|
1047
|
-
//
|
|
1048
|
-
// next one rather than at turn time.
|
|
1003
|
+
// The executable a Claude turn launches, resolved with every probe so a
|
|
1004
|
+
// binary that disappears is caught on the next beat, not at turn time.
|
|
1049
1005
|
claudeBundled: resolveBundled("claude-code").status,
|
|
1050
1006
|
codexBundled: resolveBundled("codex").status,
|
|
1051
1007
|
// A parseable `codex --version` is our presence signal (presence alone never
|
|
@@ -1054,7 +1010,7 @@ async function probeHarnessSignals(cfg, deps = {}) {
|
|
|
1054
1010
|
codexVersion,
|
|
1055
1011
|
codexEnabled: isCodexEnabled(cfg),
|
|
1056
1012
|
opencodeConfigured: !!configuredServerUrl,
|
|
1057
|
-
// A version came back ⟺ the serve answered its health endpoint
|
|
1013
|
+
// A version came back ⟺ the serve answered its health endpoint.
|
|
1058
1014
|
opencodeReachable: opencodeVersion !== null,
|
|
1059
1015
|
opencodeVersion,
|
|
1060
1016
|
opencodeDetectedUrl: opencodeVersion !== null ? opencodeProbeUrl : null
|
|
@@ -1320,7 +1276,7 @@ var CompanionStateHub = class {
|
|
|
1320
1276
|
deviceId = null;
|
|
1321
1277
|
deviceLabel = null;
|
|
1322
1278
|
deviceError = null;
|
|
1323
|
-
//
|
|
1279
|
+
// The latest harness snapshot the supervisor probed, or null before the
|
|
1324
1280
|
// first probe (see CompanionStatusJson.harnesses).
|
|
1325
1281
|
harnesses = null;
|
|
1326
1282
|
// ---- subscription (SSE) ----
|
|
@@ -1347,7 +1303,7 @@ var CompanionStateHub = class {
|
|
|
1347
1303
|
this.deviceError = null;
|
|
1348
1304
|
this.emitStatus();
|
|
1349
1305
|
}
|
|
1350
|
-
// ---- harnesses
|
|
1306
|
+
// ---- harnesses ----
|
|
1351
1307
|
// Replace the harness snapshot and push a status change (so the SSE feed flips
|
|
1352
1308
|
// the Companion's Harnesses surface live). The supervisor calls this on each
|
|
1353
1309
|
// heartbeat-cadence probe and on an on-demand recheck.
|
|
@@ -2292,7 +2248,7 @@ var LEASE_REFUSALS = /* @__PURE__ */ new Set([
|
|
|
2292
2248
|
"dispatch_not_admitted",
|
|
2293
2249
|
"turn_already_ended",
|
|
2294
2250
|
"turn_belongs_elsewhere",
|
|
2295
|
-
//
|
|
2251
|
+
// The context this start was composed from named an attempt the
|
|
2296
2252
|
// lease no longer picks (midnight rolled it, a degrade retired it). Nothing
|
|
2297
2253
|
// was granted; the turn recomposes its context and starts again, once.
|
|
2298
2254
|
"dispatch_context_stale"
|
|
@@ -2390,10 +2346,8 @@ var CabaneApi = class {
|
|
|
2390
2346
|
this.opts = opts;
|
|
2391
2347
|
}
|
|
2392
2348
|
opts;
|
|
2393
|
-
//
|
|
2394
|
-
//
|
|
2395
|
-
// this guard two overlapping drains could each pick up the same queued entry
|
|
2396
|
-
// and double-send it (the server dedupes, but the wasted POSTs aren't free).
|
|
2349
|
+
// Re-entrancy guard for `drainOutbox`: drains are kicked from several places,
|
|
2350
|
+
// and two overlapping ones would double-send the same entry.
|
|
2397
2351
|
draining = false;
|
|
2398
2352
|
lastDeliveryWarning = 0;
|
|
2399
2353
|
get base() {
|
|
@@ -2455,19 +2409,13 @@ var CabaneApi = class {
|
|
|
2455
2409
|
}
|
|
2456
2410
|
}
|
|
2457
2411
|
}
|
|
2458
|
-
//
|
|
2459
|
-
//
|
|
2460
|
-
//
|
|
2461
|
-
//
|
|
2462
|
-
//
|
|
2463
|
-
// -
|
|
2464
|
-
//
|
|
2465
|
-
// - cancel (the turn was stopped; `signal` aborted or an AbortError) →
|
|
2466
|
-
// rethrow without queuing: the user asked us to stop, and the held text
|
|
2467
|
-
// was already dropped — we must not resurrect it from the queue.
|
|
2468
|
-
// - terminal 4xx (a deterministic rejection a replay can't fix) → rethrow,
|
|
2469
|
-
// so the caller's error path runs as it does today (e.g. the committer
|
|
2470
|
-
// re-arms its empty-final fallback).
|
|
2412
|
+
// Send-or-enqueue for a turn-row commit: the immediate (retry-budgeted) send
|
|
2413
|
+
// first, then on a *retryable* failure persist to the outbox.
|
|
2414
|
+
// - no outbox configured (one-shot CLI / tests) → rethrow.
|
|
2415
|
+
// - cancel (`signal` aborted or an AbortError) → rethrow without queuing:
|
|
2416
|
+
// the user asked us to stop, and the text must not resurrect from the queue.
|
|
2417
|
+
// - terminal 4xx (a replay can't fix it) → rethrow, so the caller's error
|
|
2418
|
+
// path runs.
|
|
2471
2419
|
// - retryable (transport / 5xx — commit outcome may be unknown) → queue
|
|
2472
2420
|
// and return. This acknowledges local delivery responsibility, not server
|
|
2473
2421
|
// persistence. Replays retain the endpoint's identity: messages/thinking/
|
|
@@ -2492,13 +2440,10 @@ var CabaneApi = class {
|
|
|
2492
2440
|
);
|
|
2493
2441
|
}
|
|
2494
2442
|
}
|
|
2495
|
-
//
|
|
2496
|
-
//
|
|
2497
|
-
//
|
|
2498
|
-
//
|
|
2499
|
-
// the API is still down: stop the cycle and leave that entry and the rest
|
|
2500
|
-
// queued for the next drain. Returns a summary the supervisor's backoff
|
|
2501
|
-
// scheduler reads. Re-entrancy-guarded so overlapping kicks don't double-send.
|
|
2443
|
+
// Flush the outbox FIFO. An entry goes once the server acknowledges it (2xx)
|
|
2444
|
+
// or rejects it terminally (any 4xx, which a replay can never satisfy: discard
|
|
2445
|
+
// with a log rather than spin). A retryable failure means the API is still
|
|
2446
|
+
// down: stop and leave that entry and the rest for the next drain.
|
|
2502
2447
|
async drainOutbox() {
|
|
2503
2448
|
const outbox = this.opts.outbox;
|
|
2504
2449
|
if (!outbox) return { remaining: 0, progressed: false };
|
|
@@ -2546,40 +2491,24 @@ var CabaneApi = class {
|
|
|
2546
2491
|
}
|
|
2547
2492
|
return { remaining: outbox.size(), progressed };
|
|
2548
2493
|
}
|
|
2549
|
-
//
|
|
2550
|
-
//
|
|
2494
|
+
// How many commits are queued; the drain scheduler reads it to decide whether
|
|
2495
|
+
// to keep polling.
|
|
2551
2496
|
outboxSize() {
|
|
2552
2497
|
return this.opts.outbox?.size() ?? 0;
|
|
2553
2498
|
}
|
|
2554
|
-
//
|
|
2555
|
-
//
|
|
2556
|
-
//
|
|
2557
|
-
//
|
|
2558
|
-
//
|
|
2559
|
-
//
|
|
2560
|
-
//
|
|
2561
|
-
//
|
|
2562
|
-
//
|
|
2563
|
-
//
|
|
2564
|
-
//
|
|
2565
|
-
//
|
|
2566
|
-
//
|
|
2567
|
-
// the server should hand back `turnSeqFloor` — the turn's committed seq
|
|
2568
|
-
// high-water mark. Only the supervisor knows (it minted the id or recovered
|
|
2569
|
-
// it), so it is passed down rather than inferred here. It gates the COST of
|
|
2570
|
-
// the aggregate, never the tenancy scoping, which the route applies
|
|
2571
|
-
// unconditionally.
|
|
2572
|
-
// `fresh` asks for the context a session that holds NOTHING needs: the read
|
|
2573
|
-
// from the start of the conversation, the agent's own words included, and
|
|
2574
|
-
// the first-turn material — what the adapter runs on when it refused to
|
|
2575
|
-
// resume the stored session and is starting a fresh one.
|
|
2576
|
-
// CT1543: `episode` is the dispatch event's own, echoed back so the server can
|
|
2577
|
-
// tell this frame from a later wake on the same message. Omitted when the
|
|
2578
|
-
// event didn't carry one, which is the only thing an older event can mean.
|
|
2579
|
-
// CT1657: `signal` is the turn's own abort. It matters here more than on any
|
|
2580
|
-
// other read, because this call now waits up to ~8s across five attempts: a
|
|
2581
|
-
// Stop landing in that window used to leave the whole schedule running, and
|
|
2582
|
-
// the person watched a turn they had already stopped keep trying.
|
|
2499
|
+
// The ONE turn-context fetch: the server composes the whole server portion of
|
|
2500
|
+
// the `TurnRequest`. Agent-PAT authed; the workspace is implied by the PAT.
|
|
2501
|
+
// - `turnId`: the host-minted id the server binds the turn token to, so the
|
|
2502
|
+
// turn-control surface rejects a token whose turn has ended.
|
|
2503
|
+
// - `resumed`: a REPLAY of an interrupted turn, so the server returns
|
|
2504
|
+
// `turnSeqFloor`. Only the supervisor knows. It gates the cost of the
|
|
2505
|
+
// aggregate, never the tenancy scoping.
|
|
2506
|
+
// - `fresh`: the context a session holding NOTHING needs (the whole read,
|
|
2507
|
+
// own words included, plus first-turn material), for an adapter that
|
|
2508
|
+
// refused to resume the stored session.
|
|
2509
|
+
// - `episode`: the dispatch event's own, so the server can tell this frame
|
|
2510
|
+
// from a later wake on the same message. Omitted by an older event.
|
|
2511
|
+
// - `signal`: the turn's abort, so a Stop cuts the ~8s retry schedule short.
|
|
2583
2512
|
getTurnContext(conversationId, messageId2, turnId, resumed, fresh, episode, signal) {
|
|
2584
2513
|
const q = `conversationId=${encodeURIComponent(conversationId)}&messageId=${encodeURIComponent(messageId2)}` + (turnId ? `&turnId=${encodeURIComponent(turnId)}` : "") + (resumed ? "&resumed=1" : "") + (fresh ? "&fresh=1" : "") + (episode !== void 0 ? `&episode=${encodeURIComponent(String(episode))}` : "");
|
|
2585
2514
|
return this.request("GET", `/api/agent/turn-context?${q}`, void 0, {
|
|
@@ -2587,9 +2516,8 @@ var CabaneApi = class {
|
|
|
2587
2516
|
...signal ? { signal } : {}
|
|
2588
2517
|
});
|
|
2589
2518
|
}
|
|
2590
|
-
//
|
|
2591
|
-
//
|
|
2592
|
-
// test has no durable queue, so nothing can be hiding in it.
|
|
2519
|
+
// The local half of a resumed turn's seq floor (`Outbox.maxSeqForTurn`). 0
|
|
2520
|
+
// with no outbox: nothing can be hiding in a queue that doesn't exist.
|
|
2593
2521
|
outboxMaxSeqForTurn(turnId) {
|
|
2594
2522
|
return this.opts.outbox?.maxSeqForTurn(turnId) ?? 0;
|
|
2595
2523
|
}
|
|
@@ -2605,13 +2533,11 @@ var CabaneApi = class {
|
|
|
2605
2533
|
(e) => e.turnId === turnId && e.kind === "message" && e.body?.kind !== "runtime_notice"
|
|
2606
2534
|
);
|
|
2607
2535
|
}
|
|
2608
|
-
//
|
|
2609
|
-
//
|
|
2610
|
-
//
|
|
2611
|
-
//
|
|
2612
|
-
//
|
|
2613
|
-
// refusal — nothing to adopt, the reaper got there first — and the caller
|
|
2614
|
-
// fetches a fresh context rather than working on this one.
|
|
2536
|
+
// ADOPT a turn context's issuance as the execution source of its work, once,
|
|
2537
|
+
// before preparation emits or queues anything, so every row can name a source
|
|
2538
|
+
// the server durably recorded and will never reap. Adoption is idempotent, so
|
|
2539
|
+
// retried. A 409 `execution_source_unavailable` means the reaper got there
|
|
2540
|
+
// first; the caller fetches a fresh context.
|
|
2615
2541
|
adoptExecutionSource(issuanceId, body, signal) {
|
|
2616
2542
|
return this.request(
|
|
2617
2543
|
"POST",
|
|
@@ -2620,10 +2546,10 @@ var CabaneApi = class {
|
|
|
2620
2546
|
signal ? { retry: true, signal } : { retry: true }
|
|
2621
2547
|
);
|
|
2622
2548
|
}
|
|
2623
|
-
//
|
|
2624
|
-
//
|
|
2625
|
-
//
|
|
2626
|
-
//
|
|
2549
|
+
// The launcher mints the runner credential for one dispatch, on its PAT and
|
|
2550
|
+
// from its device, and hands it to the runner in place of both. A retry is
|
|
2551
|
+
// safe: the server returns the same runner id and retires the credential the
|
|
2552
|
+
// lost response carried.
|
|
2627
2553
|
mintRunnerCredential(body) {
|
|
2628
2554
|
return this.request("POST", "/api/agent/runner-credentials", body, { retry: true });
|
|
2629
2555
|
}
|
|
@@ -2643,7 +2569,7 @@ var CabaneApi = class {
|
|
|
2643
2569
|
// invocation. The server records the effect and receipt in one transaction;
|
|
2644
2570
|
// an ok:false response is a recorded refusal for the tool to report.
|
|
2645
2571
|
// Retries retain the caller's callId and recover the same receipt, even
|
|
2646
|
-
// when the turn settled between attempts
|
|
2572
|
+
// when the turn settled between attempts: the route consults the
|
|
2647
2573
|
// receipt before it refuses an ended turn, so a 4xx proves no attempt wrote.
|
|
2648
2574
|
// The caller's signal bounds how long the model waits for the tool result.
|
|
2649
2575
|
turnAct(workspaceId, turnId, act, signal) {
|
|
@@ -2654,36 +2580,23 @@ var CabaneApi = class {
|
|
|
2654
2580
|
signal ? { retry: true, signal } : { retry: true }
|
|
2655
2581
|
);
|
|
2656
2582
|
}
|
|
2657
|
-
//
|
|
2658
|
-
//
|
|
2659
|
-
//
|
|
2660
|
-
// terminalized underneath it stops instead of running to the six-hour cap.
|
|
2583
|
+
// RENEW THE TURN'S LEASE: is this turn still running? Called on a cadence for
|
|
2584
|
+
// the life of the SDK loop and the moment a commit is refused, so a loop whose
|
|
2585
|
+
// turn was terminalized underneath it stops.
|
|
2661
2586
|
//
|
|
2662
|
-
// Single-shot
|
|
2663
|
-
//
|
|
2664
|
-
// and the next tick asks again in a few seconds anyway. Retrying would only
|
|
2665
|
-
// stretch one tick over the next one's slot.
|
|
2587
|
+
// Single-shot, NEVER retried: a failed renewal has no answer worth replaying
|
|
2588
|
+
// and the next tick asks again; retrying stretches one tick over the next.
|
|
2666
2589
|
//
|
|
2667
|
-
// ALL FAILURE IS `unknown
|
|
2668
|
-
//
|
|
2669
|
-
//
|
|
2670
|
-
//
|
|
2671
|
-
//
|
|
2672
|
-
// gap this closes. So the failure direction is fixed here, at the boundary,
|
|
2673
|
-
// rather than left to each caller to remember.
|
|
2590
|
+
// ALL FAILURE IS `unknown`. Only the server SAYING the turn is over may stop a
|
|
2591
|
+
// running loop; a 500, a dropped connection, an expired token or an
|
|
2592
|
+
// unparseable body says nothing about the turn, and reading it as death would
|
|
2593
|
+
// kill healthy turns on every API hiccup. Fixed here, at the boundary, so no
|
|
2594
|
+
// caller has to remember.
|
|
2674
2595
|
//
|
|
2675
|
-
//
|
|
2676
|
-
//
|
|
2677
|
-
//
|
|
2678
|
-
//
|
|
2679
|
-
// on it, so one stalled request latches the check permanently, every later
|
|
2680
|
-
// cadence tick returns immediately, and the loop this exists to stop runs to
|
|
2681
|
-
// the six-hour cap even after connectivity comes back — the exact defect the
|
|
2682
|
-
// task is about, reintroduced through the mechanism meant to close it.
|
|
2683
|
-
//
|
|
2684
|
-
// The deadline turns that into an ordinary `unknown`: the request rejects, the
|
|
2685
|
-
// in-flight latch clears, and the NEXT tick asks again. Well under the renewal
|
|
2686
|
-
// cadence so a stalled probe is finished with before its successor is due.
|
|
2596
|
+
// DEADLINED, because `fetch` has no default timeout: a black-holed request
|
|
2597
|
+
// would hang forever, and the caller single-flights on it, so one stall would
|
|
2598
|
+
// latch the check and let the loop run to the cap. The deadline makes it an
|
|
2599
|
+
// ordinary `unknown` and the next tick asks again.
|
|
2687
2600
|
async checkTurnLease(workspaceId, conversationId, agentId, turnId) {
|
|
2688
2601
|
const q = `turnId=${encodeURIComponent(turnId)}`;
|
|
2689
2602
|
try {
|
|
@@ -2692,11 +2605,7 @@ var CabaneApi = class {
|
|
|
2692
2605
|
`/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}/turn-lease?${q}`,
|
|
2693
2606
|
void 0,
|
|
2694
2607
|
{
|
|
2695
|
-
//
|
|
2696
|
-
// is right above — a failed probe has no answer worth replaying and the
|
|
2697
|
-
// next tick asks again in seconds, so retrying inside this one would
|
|
2698
|
-
// stretch a tick over its successor's slot, which is what the deadline
|
|
2699
|
-
// exists to prevent.
|
|
2608
|
+
// The one GET that opts OUT of the default retry; see above.
|
|
2700
2609
|
retry: false,
|
|
2701
2610
|
signal: AbortSignal.timeout(this.opts.leaseCheckTimeoutMs ?? LEASE_CHECK_TIMEOUT_MS)
|
|
2702
2611
|
}
|
|
@@ -2730,9 +2639,8 @@ var CabaneApi = class {
|
|
|
2730
2639
|
}
|
|
2731
2640
|
return this.request("PATCH", path, body);
|
|
2732
2641
|
}
|
|
2733
|
-
//
|
|
2734
|
-
//
|
|
2735
|
-
// deliberate differences:
|
|
2642
|
+
// Send-or-enqueue for an active-run flag write, last-writer-wins per
|
|
2643
|
+
// (conversation, agent) pair. Like `durableCommit`, except:
|
|
2736
2644
|
// - No abort signal is threaded. The clear is the turn's teardown and must
|
|
2737
2645
|
// land even when the turn was *cancelled* (the abort signal is set) — a
|
|
2738
2646
|
// cancelled turn still has to drop its "working" indicator.
|
|
@@ -2786,9 +2694,6 @@ var CabaneApi = class {
|
|
|
2786
2694
|
);
|
|
2787
2695
|
}
|
|
2788
2696
|
}
|
|
2789
|
-
// CT29: per-device liveness moved off the per-workspace agent PAT and onto the
|
|
2790
|
-
// device token — see `DeviceApi.heartbeat`. There is no agent-PAT heartbeat
|
|
2791
|
-
// anymore.
|
|
2792
2697
|
// Commit the agent's first/last words (message) or a runtime_notice through
|
|
2793
2698
|
// the public messages endpoint. Intermediate working text uses reportText.
|
|
2794
2699
|
// Attribution comes from the agent credential; a new speech row can offer
|
|
@@ -2937,7 +2842,7 @@ var DeviceApi = class {
|
|
|
2937
2842
|
AbortSignal.timeout(5e3)
|
|
2938
2843
|
);
|
|
2939
2844
|
}
|
|
2940
|
-
//
|
|
2845
|
+
// Report that a dispatch addressed to THIS device can't be run, because
|
|
2941
2846
|
// the agent isn't one this device runs (and still wasn't after a forced
|
|
2942
2847
|
// assignments refresh). The server clears the Working flag, retires the dispatch
|
|
2943
2848
|
// and posts an explanatory system message.
|
|
@@ -2951,7 +2856,7 @@ var DeviceApi = class {
|
|
|
2951
2856
|
return this.request("POST", "/api/companion/dispatch-drop", body);
|
|
2952
2857
|
}
|
|
2953
2858
|
// Per-device liveness ping. Reports the companion build version and the env-var
|
|
2954
|
-
// names the operator's secret store exposes (never values), so
|
|
2859
|
+
// names the operator's secret store exposes (never values), so the UI can
|
|
2955
2860
|
// warn pre-emptively about an agent that needs a secret this device lacks.
|
|
2956
2861
|
heartbeat(body) {
|
|
2957
2862
|
return this.request("POST", "/api/companion/heartbeat", body ?? {});
|
|
@@ -3188,95 +3093,66 @@ var TURN_PROTOCOL_VERSION = 1;
|
|
|
3188
3093
|
// packages/agent-runtime/src/host-policy.ts
|
|
3189
3094
|
import { z as z5 } from "zod";
|
|
3190
3095
|
var hostPolicySchema = z5.object({
|
|
3191
|
-
// Host filesystem
|
|
3192
|
-
//
|
|
3193
|
-
// tool. Off under the locked assistant surface (today's `DISALLOWED_TOOLS`);
|
|
3194
|
-
// on under `coding` mode.
|
|
3096
|
+
// Host filesystem and shell: `Bash`/`Read`/`Write`/`Edit`/`Glob`/`Grep`, the notebook pair,
|
|
3097
|
+
// git-worktree tools and `REPL`. Off on the locked assistant surface; on in the coding posture.
|
|
3195
3098
|
hostFs: z5.boolean(),
|
|
3196
|
-
// Public web (
|
|
3197
|
-
// web, not host reach — granted by default today, but expressible as a grant.
|
|
3099
|
+
// Public web (`WebSearch` / `WebFetch`). Not host reach; a grant like the rest.
|
|
3198
3100
|
web: z5.boolean(),
|
|
3199
|
-
// Browser automation (the Playwright MCP surface). Varies by host
|
|
3200
|
-
// it, the house executor does not (CT230).
|
|
3101
|
+
// Browser automation (the Playwright MCP surface). Varies by host.
|
|
3201
3102
|
browser: z5.boolean(),
|
|
3202
|
-
// User-configured MCP servers permitted.
|
|
3203
|
-
// (CT227: Cabane agents run no user MCP servers), true for a personal companion.
|
|
3103
|
+
// User-configured MCP servers permitted.
|
|
3204
3104
|
userMcp: z5.boolean(),
|
|
3205
|
-
// Subagent
|
|
3206
|
-
//
|
|
3207
|
-
//
|
|
3208
|
-
// the coding allow-hook (which reaches sub-agent tool calls, CT680) or the user
|
|
3209
|
-
// allowlist. The subagent completes within the turn, so
|
|
3210
|
-
// it's not the turn-model invariant `scheduling` is.
|
|
3105
|
+
// Subagent dispatch (`Task` / `Agent`) — a grant: `false` on the locked surface, `true` in the
|
|
3106
|
+
// coding posture, where the coding allow-hook reaches sub-agent tool calls. A subagent completes
|
|
3107
|
+
// within the turn, so it's not an invariant like `scheduling`.
|
|
3211
3108
|
subagents: z5.boolean(),
|
|
3212
3109
|
// ── Hard platform invariants — always denied, never granted ────────────────
|
|
3213
|
-
// Deferred re-invocation / scheduling (`ScheduleWakeup`, the `Cron*` / `Task*`
|
|
3214
|
-
//
|
|
3215
|
-
//
|
|
3216
|
-
// and strands the agent (the CT155/CT156 rule).
|
|
3110
|
+
// Deferred re-invocation / scheduling (`ScheduleWakeup`, the `Cron*` / `Task*` families,
|
|
3111
|
+
// `Monitor`, `Workflow`, …). A turn is one query that resolves on `result`; a scheduled callback
|
|
3112
|
+
// fires after the reply window has closed and strands the agent.
|
|
3217
3113
|
scheduling: z5.literal("never"),
|
|
3218
|
-
// Human-facing UI prompts (`AskUserQuestion`).
|
|
3219
|
-
//
|
|
3220
|
-
// (`UNSUPPORTED_TOOLS`). The agent asks in its reply instead.
|
|
3114
|
+
// Human-facing UI prompts (`AskUserQuestion`). Nothing in a Cabane conversation can answer a
|
|
3115
|
+
// structured prompt, so the call hangs the turn. The agent asks in its reply instead.
|
|
3221
3116
|
uiPrompts: z5.literal("never")
|
|
3222
3117
|
});
|
|
3223
3118
|
|
|
3224
3119
|
// packages/agent-runtime/src/turn-event.ts
|
|
3225
3120
|
import { z as z6 } from "zod";
|
|
3226
3121
|
var turnEventSchema = z6.discriminatedUnion("type", [
|
|
3227
|
-
// The runtime's opaque session state, emitted when the adapter learns it (e.g.
|
|
3228
|
-
//
|
|
3229
|
-
//
|
|
3230
|
-
// knows what it means. Today's companion captures the raw SDK session id here; a
|
|
3231
|
-
// future adapter may encode more (e.g. `{sdkSessionId, cwd}`) — still one
|
|
3232
|
-
// opaque string to the platform.
|
|
3122
|
+
// The runtime's opaque session state, emitted when the adapter learns it (e.g. the SDK
|
|
3123
|
+
// `system/init` frame). The platform stores `state` verbatim per (conversation, agent) and
|
|
3124
|
+
// hands it back next turn; only the adapter knows what it means.
|
|
3233
3125
|
//
|
|
3234
|
-
//
|
|
3235
|
-
//
|
|
3236
|
-
//
|
|
3237
|
-
//
|
|
3238
|
-
//
|
|
3239
|
-
// the
|
|
3240
|
-
//
|
|
3241
|
-
// on settle and the server rewinds the mark, so the NEXT turn rebuilds a full
|
|
3242
|
-
// catch-up (this failing turn is unavoidably lossy — the degrade is only known
|
|
3243
|
-
// on the companion, after the server committed the manifest). Runtime-neutral: a
|
|
3244
|
-
// plain boolean, not a runtime-specific reason string (that stays in the
|
|
3245
|
-
// adapter's `onWarn` log). Additive + optional — an old receiver ignores it.
|
|
3126
|
+
// `degraded: true`: the adapter started this fresh session despite a stored one (a cwd
|
|
3127
|
+
// mismatch, or legacy/foreign state it refused to resume) — never for a genuine first turn.
|
|
3128
|
+
// The server built this turn's catch-up manifest assuming the prior session held everything up
|
|
3129
|
+
// to the high-water mark; a degrade breaks that. The host relays `degraded` on settle and the
|
|
3130
|
+
// server rewinds the mark, so the next turn rebuilds a full catch-up (this turn is unavoidably
|
|
3131
|
+
// lossy — the degrade is known only after the manifest was committed). A plain boolean; the
|
|
3132
|
+
// runtime-specific reason stays in the adapter's `onWarn` log. Optional.
|
|
3246
3133
|
z6.object({
|
|
3247
3134
|
type: z6.literal("session"),
|
|
3248
3135
|
state: z6.string(),
|
|
3249
3136
|
degraded: z6.boolean().optional()
|
|
3250
3137
|
}),
|
|
3251
|
-
// One readable thinking summary.
|
|
3252
|
-
// surfaced live, never persisted as durable content.
|
|
3138
|
+
// One readable thinking summary. Transient — surfaced live, never persisted as content.
|
|
3253
3139
|
z6.object({ type: z6.literal("thinking"), text: z6.string() }),
|
|
3254
|
-
// Assistant text.
|
|
3255
|
-
// `
|
|
3256
|
-
// `progress` row); `terminal: true` is the turn's closing reply (commits as
|
|
3257
|
-
// the `final` row).
|
|
3140
|
+
// Assistant text. `terminal: false` is interim narration (commits as `progress`);
|
|
3141
|
+
// `terminal: true` is the turn's closing reply (commits as `final`).
|
|
3258
3142
|
z6.object({ type: z6.literal("text"), body: z6.string(), terminal: z6.boolean() }),
|
|
3259
|
-
// Runtime/provider-authored prose about the turn — a failure's explanation, or
|
|
3260
|
-
//
|
|
3261
|
-
// `
|
|
3262
|
-
//
|
|
3263
|
-
// voice. Adapters emit it only from positive runtime evidence; the web never
|
|
3264
|
-
// classifies English strings.
|
|
3143
|
+
// Runtime/provider-authored prose about the turn — a failure's explanation, or a line the
|
|
3144
|
+
// harness wrote on a turn it recovered. Not the agent's words: the pump persists it as
|
|
3145
|
+
// `runtime_notice`, drawn in the SystemNote voice. Adapters emit it only from positive runtime
|
|
3146
|
+
// evidence; the web never classifies English strings.
|
|
3265
3147
|
z6.object({ type: z6.literal("runtime_notice"), body: z6.string() }),
|
|
3266
|
-
// A tool-activity transition.
|
|
3267
|
-
// `
|
|
3268
|
-
//
|
|
3269
|
-
// pairs `start` with a later `done`/`error` on the same `id`.
|
|
3148
|
+
// A tool-activity transition. `name` is prefix-stripped (`cabane_read`, not
|
|
3149
|
+
// `mcp__cabane__cabane_read`); `summary` is the short card label; `phase` pairs `start` with a
|
|
3150
|
+
// later `done`/`error` on the same `id`.
|
|
3270
3151
|
//
|
|
3271
|
-
// `input`/`result` are
|
|
3272
|
-
//
|
|
3273
|
-
//
|
|
3274
|
-
//
|
|
3275
|
-
// CT496: `mcpServer` is the MCP server the call came from (`cabane`,
|
|
3276
|
-
// `playwright`, …), derived off the raw `mcp__<server>__` name before `name`
|
|
3277
|
-
// dropped the prefix; null for a host / built-in tool. The client tags Cabane
|
|
3278
|
-
// MCP calls (`=== 'cabane'`) with a subtle glyph. Nullable + optional so a
|
|
3279
|
-
// pre-CT496 producer that never sets it is unaffected (treated as null).
|
|
3152
|
+
// `input`/`result` are `unknown` (the raw tool payloads): rendering and truncation for the tool
|
|
3153
|
+
// log are a consumer concern (docs/turn-protocol.md). `mcpServer` is the MCP server the call
|
|
3154
|
+
// came from (`cabane`, `playwright`, …), derived before `name` dropped the prefix; null for a
|
|
3155
|
+
// host tool. Optional, read as null when absent.
|
|
3280
3156
|
z6.object({
|
|
3281
3157
|
type: z6.literal("tool"),
|
|
3282
3158
|
id: z6.string(),
|
|
@@ -3287,45 +3163,25 @@ var turnEventSchema = z6.discriminatedUnion("type", [
|
|
|
3287
3163
|
result: z6.unknown().optional(),
|
|
3288
3164
|
mcpServer: z6.string().nullable().optional()
|
|
3289
3165
|
}),
|
|
3290
|
-
// The turn's terminal outcome.
|
|
3291
|
-
//
|
|
3292
|
-
// the host's clear-PATCH surfaces it as `errorReason`.
|
|
3293
|
-
//
|
|
3294
|
-
// CT277: an optional `usage` — the SDK result's token counts, which were
|
|
3295
|
-
// dropped on the floor before. `inputTokens` is the full context the model saw
|
|
3296
|
-
// (uncached + cache-read + cache-creation input), so it doubles as the
|
|
3297
|
-
// context-window cost; `outputTokens` the generated tokens. Optional ⇒
|
|
3298
|
-
// backward-compatible: an old companion / adapter that never sets it, and a
|
|
3299
|
-
// receiver that never reads it, are unaffected (the turn's token columns stay
|
|
3300
|
-
// null → the UI shows `—`).
|
|
3166
|
+
// The turn's terminal outcome. `ok:false` carries a machine reason (`no_session`, an error
|
|
3167
|
+
// code), surfaced by the host's clear-PATCH as `errorReason`.
|
|
3301
3168
|
//
|
|
3302
|
-
//
|
|
3303
|
-
//
|
|
3304
|
-
//
|
|
3305
|
-
//
|
|
3306
|
-
// costing later prices a cache-read token far below a fresh input token.
|
|
3169
|
+
// `usage`: the SDK result's token counts. `inputTokens` is the full context the model saw
|
|
3170
|
+
// (uncached + cache-read + cache-creation). The cache counts, when the runtime breaks them out,
|
|
3171
|
+
// are a subset of it, kept because a cache-read token costs far less. All optional; absent
|
|
3172
|
+
// leaves the columns null and the UI shows `—`.
|
|
3307
3173
|
//
|
|
3308
|
-
//
|
|
3309
|
-
//
|
|
3310
|
-
//
|
|
3311
|
-
//
|
|
3312
|
-
//
|
|
3313
|
-
// harness reported them (claude-code's `getContextUsage`, or its final
|
|
3314
|
-
// request's input and `modelUsage` window when that call gives nothing). A
|
|
3315
|
-
// runtime that can't source them (codex's cumulative-only usage) omits both,
|
|
3316
|
-
// and no surface shows a reading for that turn.
|
|
3174
|
+
// `inputTokens` is a billing quantity — for claude-code/codex the cumulative total across every
|
|
3175
|
+
// model request in the turn, growing with tool calls, not "how full is the window".
|
|
3176
|
+
// `contextTokens` is the occupancy read and `contextWindow` its window, as the harness reported
|
|
3177
|
+
// them (claude-code's `getContextUsage`, else its final request's input and `modelUsage`
|
|
3178
|
+
// window). A runtime that can't source them (codex) omits both.
|
|
3317
3179
|
//
|
|
3318
|
-
//
|
|
3319
|
-
// `
|
|
3320
|
-
//
|
|
3321
|
-
//
|
|
3322
|
-
//
|
|
3323
|
-
// per-runtime knobs the harness reported (`{ reasoningEffort }` for codex; none
|
|
3324
|
-
// for claude-code today). Both only known after the run streams — so they ride
|
|
3325
|
-
// the terminal event home, the host relays them on settle, and the server writes
|
|
3326
|
-
// `message_turn_metadata.resolved_model` / `resolved_config`. Optional ⇒
|
|
3327
|
-
// backward-compatible: an old adapter/companion omits them, a cancel has no result
|
|
3328
|
-
// event at all, and the columns stay null → the UI shows `—`.
|
|
3180
|
+
// `resolvedModel` is the concrete model the runtime ran (claude-code learns it from
|
|
3181
|
+
// `system/init` mid-run, even for a subscription default; codex records the model the thread
|
|
3182
|
+
// started with); `resolvedConfig` the knobs the harness reported (`{ reasoningEffort }` for
|
|
3183
|
+
// codex). Known only after the run streams, so they ride this event home and the host relays
|
|
3184
|
+
// them on settle. A cancel has no result event at all.
|
|
3329
3185
|
z6.object({
|
|
3330
3186
|
type: z6.literal("result"),
|
|
3331
3187
|
ok: z6.boolean(),
|
|
@@ -3337,9 +3193,8 @@ var turnEventSchema = z6.discriminatedUnion("type", [
|
|
|
3337
3193
|
cacheCreationTokens: z6.number().optional(),
|
|
3338
3194
|
contextTokens: z6.number().optional(),
|
|
3339
3195
|
contextWindow: z6.number().optional(),
|
|
3340
|
-
//
|
|
3341
|
-
//
|
|
3342
|
-
// compaction it reported during the turn. Claude Code only.
|
|
3196
|
+
// The rest of the harness's context reading, when it gave one: where it will compact, its
|
|
3197
|
+
// `/context` breakdown, and every compaction it reported. Claude Code only.
|
|
3343
3198
|
contextCompactAt: z6.number().optional(),
|
|
3344
3199
|
contextCategories: z6.array(
|
|
3345
3200
|
z6.object({
|
|
@@ -3362,10 +3217,9 @@ var turnEventSchema = z6.discriminatedUnion("type", [
|
|
|
3362
3217
|
thinking: z6.string().optional(),
|
|
3363
3218
|
reasoningEffort: z6.string().optional()
|
|
3364
3219
|
}).optional(),
|
|
3365
|
-
//
|
|
3366
|
-
//
|
|
3367
|
-
//
|
|
3368
|
-
// distinguishes a missing init frame from a real empty inventory.
|
|
3220
|
+
// The harness-reported MCP inventory from this turn's init frame. Diagnostic only: names,
|
|
3221
|
+
// statuses and a count — never server definitions, credentials or session ids.
|
|
3222
|
+
// `initReceived:false` tells a missing init frame from a real empty inventory.
|
|
3369
3223
|
mcpInventory: z6.object({
|
|
3370
3224
|
initReceived: z6.boolean(),
|
|
3371
3225
|
servers: z6.array(z6.object({ name: z6.string(), status: z6.string() })),
|
|
@@ -3468,9 +3322,8 @@ function isContentBearingEvent(event) {
|
|
|
3468
3322
|
return normalizeCommitText(event.text) !== "";
|
|
3469
3323
|
case "tool":
|
|
3470
3324
|
return true;
|
|
3471
|
-
//
|
|
3472
|
-
//
|
|
3473
|
-
// line still produced nothing.
|
|
3325
|
+
// A runtime notice is shown, but it is the harness speaking about the turn, not its output — a
|
|
3326
|
+
// success whose only output is a CLI line still produced nothing.
|
|
3474
3327
|
case "runtime_notice":
|
|
3475
3328
|
case "session":
|
|
3476
3329
|
case "result":
|
|
@@ -3542,11 +3395,10 @@ var turnRequestSchema = z8.object({
|
|
|
3542
3395
|
mcpUrl: z8.string(),
|
|
3543
3396
|
bearer: z8.string(),
|
|
3544
3397
|
activeConversationId: z8.string(),
|
|
3545
|
-
// Scoped turn-control MCP URL. Codex
|
|
3546
|
-
//
|
|
3547
|
-
//
|
|
3548
|
-
//
|
|
3549
|
-
// the companion populates it in build-options.ts.
|
|
3398
|
+
// Scoped turn-control MCP URL. Codex and opencode mount it as `cabane_companion` with the bearer
|
|
3399
|
+
// and active-conversation header, for ask, withdraw_ask, wake_me, cancel_wake and skip_turn.
|
|
3400
|
+
// Claude Code mounts the companion's in-process instance instead. Optional; the companion
|
|
3401
|
+
// populates it.
|
|
3550
3402
|
turnControlUrl: z8.string().optional(),
|
|
3551
3403
|
// Workspace coordinate retained for envelope compatibility. Current
|
|
3552
3404
|
// adapters use MCP, whose workspace operations take workspaceId explicitly.
|
|
@@ -3559,19 +3411,17 @@ var turnRequestSchema = z8.object({
|
|
|
3559
3411
|
cwd: z8.string().optional(),
|
|
3560
3412
|
env: z8.record(z8.string(), z8.string()).optional(),
|
|
3561
3413
|
mcpServers: resolvedMcpServersSchema.optional(),
|
|
3562
|
-
//
|
|
3563
|
-
//
|
|
3564
|
-
//
|
|
3565
|
-
// its own. Absent when no server references one.
|
|
3414
|
+
// The values `headersFromEnv` names, per turn (`CABANE_MCP_<n>` → complete header value). The
|
|
3415
|
+
// adapter merges them into the harness process's environment and nowhere else; the host never
|
|
3416
|
+
// merges them into its own. Absent when no server references one.
|
|
3566
3417
|
secretEnv: z8.record(z8.string(), z8.string()).optional(),
|
|
3567
|
-
//
|
|
3568
|
-
//
|
|
3569
|
-
//
|
|
3570
|
-
//
|
|
3571
|
-
// default in place (see `buildClaudeCodeOptions`).
|
|
3418
|
+
// Machine-local claude-code knobs for an operator running their own companion — the auto-memory
|
|
3419
|
+
// escape hatch. `autoMemory: true` opts back into Claude Code's auto-memory (governed by the
|
|
3420
|
+
// operator's `.claude/settings.json`); absent/false keeps the adapter's force-off default
|
|
3421
|
+
// (`buildClaudeCodeOptions`).
|
|
3572
3422
|
claudeCode: z8.object({ autoMemory: z8.boolean().optional() }).optional()
|
|
3573
3423
|
}),
|
|
3574
|
-
// Host-owned injected servers (host-filled)
|
|
3424
|
+
// Host-owned injected servers (host-filled).
|
|
3575
3425
|
extra: z8.object({
|
|
3576
3426
|
mcpServers: hostInjectedServersSchema
|
|
3577
3427
|
})
|
|
@@ -3649,8 +3499,8 @@ async function emitToolResults(msg, emit, pending, onError) {
|
|
|
3649
3499
|
// result payload off the block.
|
|
3650
3500
|
input: started?.input,
|
|
3651
3501
|
result: block.content,
|
|
3652
|
-
//
|
|
3653
|
-
//
|
|
3502
|
+
// The server captured at `start` (the pretty name here has lost its prefix). Null when the
|
|
3503
|
+
// start wasn't seen.
|
|
3654
3504
|
mcpServer: started?.mcpServer ?? null
|
|
3655
3505
|
},
|
|
3656
3506
|
onError
|
|
@@ -3719,24 +3569,18 @@ function summarizeToolInput(input) {
|
|
|
3719
3569
|
return "";
|
|
3720
3570
|
}
|
|
3721
3571
|
var TOOL_IO_MAX_CHARS = 1e4;
|
|
3722
|
-
function
|
|
3572
|
+
function renderToolIo(input) {
|
|
3723
3573
|
if (input === void 0 || input === null) return "";
|
|
3724
|
-
if (typeof input === "string")
|
|
3725
|
-
const trimmed = input.trim();
|
|
3726
|
-
return trimmed.length > 0 ? truncate(trimmed, TOOL_IO_MAX_CHARS) : "";
|
|
3727
|
-
}
|
|
3574
|
+
if (typeof input === "string") return input.trim();
|
|
3728
3575
|
try {
|
|
3729
|
-
return
|
|
3576
|
+
return JSON.stringify(input) ?? "";
|
|
3730
3577
|
} catch {
|
|
3731
3578
|
return "";
|
|
3732
3579
|
}
|
|
3733
3580
|
}
|
|
3734
|
-
function
|
|
3581
|
+
function renderToolResult(content) {
|
|
3735
3582
|
if (content === void 0 || content === null) return "";
|
|
3736
|
-
if (typeof content === "string")
|
|
3737
|
-
const trimmed = content.trim();
|
|
3738
|
-
return trimmed.length > 0 ? truncate(trimmed, TOOL_IO_MAX_CHARS) : "";
|
|
3739
|
-
}
|
|
3583
|
+
if (typeof content === "string") return content.trim();
|
|
3740
3584
|
if (Array.isArray(content)) {
|
|
3741
3585
|
const parts = [];
|
|
3742
3586
|
let sawContentEntry = false;
|
|
@@ -3751,12 +3595,12 @@ function summarizeToolResult(content) {
|
|
|
3751
3595
|
}
|
|
3752
3596
|
}
|
|
3753
3597
|
const joined = parts.join("\n").trim();
|
|
3754
|
-
if (joined.length > 0) return
|
|
3598
|
+
if (joined.length > 0) return joined;
|
|
3755
3599
|
if (sawContentEntry || content.length === 0) return "";
|
|
3756
3600
|
}
|
|
3757
3601
|
if (typeof content !== "object") return "";
|
|
3758
3602
|
if (!Array.isArray(content) && Object.keys(content).length === 0) return "";
|
|
3759
|
-
return
|
|
3603
|
+
return renderToolIo(content);
|
|
3760
3604
|
}
|
|
3761
3605
|
function truncate(s, max) {
|
|
3762
3606
|
if (s.length <= max) return s;
|
|
@@ -3773,15 +3617,19 @@ function sinkEmitter(sink, _onError) {
|
|
|
3773
3617
|
return;
|
|
3774
3618
|
case "tool":
|
|
3775
3619
|
if (sink.onToolActivity) {
|
|
3620
|
+
const input = renderToolIo(event.input);
|
|
3621
|
+
const result = event.result === void 0 ? null : renderToolResult(event.result);
|
|
3776
3622
|
await sink.onToolActivity({
|
|
3777
3623
|
toolUseId: event.id,
|
|
3778
3624
|
toolName: event.name,
|
|
3779
3625
|
summary: event.summary,
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
input:
|
|
3783
|
-
|
|
3784
|
-
|
|
3626
|
+
input: truncate(input, TOOL_IO_MAX_CHARS),
|
|
3627
|
+
result: result === null ? "" : truncate(result, TOOL_IO_MAX_CHARS),
|
|
3628
|
+
// Only when the event carried an input: a completion whose start this process never saw has
|
|
3629
|
+
// none, and a zero would overwrite the size its start recorded.
|
|
3630
|
+
...event.input === void 0 ? {} : { inputChars: input.length },
|
|
3631
|
+
...result === null ? {} : { resultChars: result.length },
|
|
3632
|
+
// The originating MCP server (null when absent / a host tool).
|
|
3785
3633
|
mcpServer: event.mcpServer ?? null,
|
|
3786
3634
|
phase: event.phase
|
|
3787
3635
|
});
|
|
@@ -3841,6 +3689,8 @@ var TurnPump = class {
|
|
|
3841
3689
|
summary: evt.summary,
|
|
3842
3690
|
input: evt.input,
|
|
3843
3691
|
result: evt.result,
|
|
3692
|
+
...evt.inputChars !== void 0 ? { inputChars: evt.inputChars } : {},
|
|
3693
|
+
...evt.resultChars !== void 0 ? { resultChars: evt.resultChars } : {},
|
|
3844
3694
|
mcpServer: evt.mcpServer,
|
|
3845
3695
|
phase: evt.phase,
|
|
3846
3696
|
seq
|
|
@@ -3873,22 +3723,18 @@ var TurnPump = class {
|
|
|
3873
3723
|
// The turn's final reply text, captured for the dashboard feed / logging.
|
|
3874
3724
|
finalReplyBody = null;
|
|
3875
3725
|
emittedFinalSource = "none";
|
|
3876
|
-
//
|
|
3877
|
-
//
|
|
3878
|
-
// invocation
|
|
3879
|
-
//
|
|
3880
|
-
//
|
|
3881
|
-
//
|
|
3882
|
-
// together; minting only the id would let two calls overwrite each other's
|
|
3883
|
-
// queued body.
|
|
3726
|
+
// Provider tool id → the persisted identity of its current call. The provider's id is only a
|
|
3727
|
+
// lookup key, unique within one adapter invocation at best (Codex restarts at `item_0` every
|
|
3728
|
+
// invocation, and a resumed turn keeps its turn id), so the row and the outbox entry are keyed on
|
|
3729
|
+
// what the pump mints: a host id for the host's `(turn_id, tool_use_id)` upsert, and a seq for the
|
|
3730
|
+
// outbox's `(turnId, seq)` file. Minted together; minting only the id would let two calls
|
|
3731
|
+
// overwrite each other's queued body.
|
|
3884
3732
|
//
|
|
3885
|
-
// In memory on purpose: it dies with the process, and so does the runtime.
|
|
3886
|
-
//
|
|
3887
|
-
// invocation starts its calls afresh — so there is nothing to recover.
|
|
3733
|
+
// In memory on purpose: it dies with the process, and so does the runtime. After a crash no
|
|
3734
|
+
// in-flight `done` awaits a pre-crash `start` — the resumed invocation starts its calls afresh.
|
|
3888
3735
|
toolCallByProviderId = /* @__PURE__ */ new Map();
|
|
3889
|
-
// The callback consumer the classification (via `sinkEmitter`)
|
|
3890
|
-
//
|
|
3891
|
-
// commits through the injected sink.
|
|
3736
|
+
// The callback consumer the classification (via `sinkEmitter`) and the committer test drive.
|
|
3737
|
+
// Each callback applies the choreography and commits through the injected sink.
|
|
3892
3738
|
sink;
|
|
3893
3739
|
// Whether a message has landed under this span — first words, at least. A
|
|
3894
3740
|
// span that spoke settles; one that never did is a pass.
|
|
@@ -3980,7 +3826,7 @@ var TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
|
3980
3826
|
wake_me: {
|
|
3981
3827
|
afterSeconds: "Relative delay in seconds from NOW \u2014 this call (e.g. 300 = five minutes). Provide EITHER this or `at`, not both. Floor 60s, horizon 14 days \u2014 enforced server-side.",
|
|
3982
3828
|
at: "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.",
|
|
3983
|
-
note: `A note to your future self \u2014 becomes the body of the wake message that re-dispatches you, up to ${MAX_WAKE_NOTE_BYTES} characters. Write the condition to re-check ("check whether the PR merged").`
|
|
3829
|
+
note: `A note to your future self \u2014 becomes the body of the wake message that re-dispatches you, up to ${MAX_WAKE_NOTE_BYTES} characters. Write the condition to re-check in a sentence or two ("check whether the PR merged"), not a plan. You never need to name yourself: the wake reaches you on its own.`
|
|
3984
3830
|
},
|
|
3985
3831
|
withdraw_ask: {
|
|
3986
3832
|
askId: "The open ask to withdraw. Ask ids ride the conversation's message rows (`ask: { id, status, items }`), and your turn context lists the ones you have open.",
|
|
@@ -3993,7 +3839,7 @@ var MEANINGS = {
|
|
|
3993
3839
|
ask: "put a structured question to a person",
|
|
3994
3840
|
cancel_wake: "cancel your own armed wake in this conversation",
|
|
3995
3841
|
skip_turn: "end without posting a reply",
|
|
3996
|
-
wake_me: "schedule your own return",
|
|
3842
|
+
wake_me: "schedule your own return; when it fires, say first what you came back to check",
|
|
3997
3843
|
withdraw_ask: "withdraw an open ask in this conversation with a reason"
|
|
3998
3844
|
};
|
|
3999
3845
|
function turnControlPromptGuidance(prefix) {
|
|
@@ -4214,52 +4060,41 @@ function buildClaudeCodeOptions(req, augment) {
|
|
|
4214
4060
|
// pinned `model` arrives as a bare SDK id — the host already resolved the
|
|
4215
4061
|
// namespaced catalog id to it.
|
|
4216
4062
|
...model ? { model } : {},
|
|
4217
|
-
//
|
|
4218
|
-
//
|
|
4219
|
-
//
|
|
4063
|
+
// Surface thinking summaries: default adaptive+summarized, and fill in `display:'summarized'`
|
|
4064
|
+
// on a configured adaptive/enabled block that omits one. Read from the adapter's dialect slice
|
|
4065
|
+
// (`runtimeOptions['claude-code']`).
|
|
4220
4066
|
thinking: withThinkingSummaries(readThinking(config.runtimeOptions)),
|
|
4221
4067
|
...config.effort ? { effort: config.effort } : {},
|
|
4222
4068
|
// Never merge arbitrary user `~/.claude` config; without `strictMcpConfig` the
|
|
4223
4069
|
// user's existing `cabane` MCP registration could shadow ours.
|
|
4224
4070
|
settingSources: [],
|
|
4225
4071
|
strictMcpConfig: true,
|
|
4226
|
-
//
|
|
4072
|
+
// Auto-memory off unless the operator opted back in (see above).
|
|
4227
4073
|
...devControlsAutoMemory ? {} : { settings: { autoMemoryEnabled: false } },
|
|
4228
4074
|
mcpServers,
|
|
4229
4075
|
...cwd ? { cwd } : {},
|
|
4230
|
-
// Extra env (a companion prepare hook's tokens/ports
|
|
4231
|
-
// merged OVER the inherited environment.
|
|
4076
|
+
// Extra env (a companion prepare hook's tokens/ports) merged over the inherited environment.
|
|
4232
4077
|
//
|
|
4233
|
-
//
|
|
4234
|
-
//
|
|
4235
|
-
// a
|
|
4236
|
-
//
|
|
4237
|
-
//
|
|
4238
|
-
//
|
|
4239
|
-
//
|
|
4240
|
-
//
|
|
4241
|
-
//
|
|
4242
|
-
// poisoned the resumed session is an INFERENCE from one incident, consistent
|
|
4243
|
-
// with the transcript but never reproduced, and this change does not rest on
|
|
4244
|
-
// it. `Dl()` reads this variable on the Bash tool's own call
|
|
4245
|
-
// path and sets `canAutoBackground: false` and `turnAbortBackgrounds: false`
|
|
4246
|
-
// together, so a timed-out command comes back as a readable error instead.
|
|
4078
|
+
// The background-tasks kill switch goes last, so nothing in `process.env` or `req.local.env`
|
|
4079
|
+
// turns it back on. Claude Code converts a foreground command that hits its Bash ceiling
|
|
4080
|
+
// (600 000 ms) into a background task instead of erroring, and the agent then holds a phantom
|
|
4081
|
+
// task id inside a live turn; a resumed session was seen with every tool call aborted as
|
|
4082
|
+
// `background` ("The user doesn't want to take this action right now"). The conversion is
|
|
4083
|
+
// reproduced and fixed by this flag; that it poisoned the resumed session is an inference
|
|
4084
|
+
// from one incident, and this change doesn't rest on it. `Dl()` reads the variable on the
|
|
4085
|
+
// Bash tool's call path and sets `canAutoBackground: false` and `turnAbortBackgrounds: false`,
|
|
4086
|
+
// so a timed-out command returns a readable error.
|
|
4247
4087
|
//
|
|
4248
|
-
// The cost is
|
|
4249
|
-
//
|
|
4250
|
-
//
|
|
4251
|
-
//
|
|
4252
|
-
// mitigation for the automatic conversion, NOT a cleanup mechanism: step 0
|
|
4253
|
-
// measured that this flag changes no process-survival outcome on any turn-end
|
|
4254
|
-
// path. Cleanup is its own change.
|
|
4088
|
+
// The cost is deliberate: it also removes `run_in_background` (the parameter leaves the tool
|
|
4089
|
+
// schema). It is the only switch, and there is no opt-out — an escape hatch would contradict
|
|
4090
|
+
// the guarantee. It changes no process-survival outcome on any turn-end path (measured);
|
|
4091
|
+
// cleanup is a separate concern.
|
|
4255
4092
|
//
|
|
4256
|
-
//
|
|
4257
|
-
//
|
|
4258
|
-
// and why this key is now always present — previously it appeared only when
|
|
4259
|
-
// `req.local.env` was set, and an absent `env` inherits `process.env`.
|
|
4093
|
+
// `env` replaces the subprocess environment rather than merging (`Options.env`), which is why
|
|
4094
|
+
// `process.env` is spread here and the key is always present.
|
|
4260
4095
|
//
|
|
4261
|
-
//
|
|
4262
|
-
//
|
|
4096
|
+
// `secretEnv` — the values the MCP headers above reference — joins here and only here: it is
|
|
4097
|
+
// this child's environment, not the host's.
|
|
4263
4098
|
env: {
|
|
4264
4099
|
...process.env,
|
|
4265
4100
|
...req.local.env,
|
|
@@ -4750,8 +4585,7 @@ function makeRequest(overrides2 = {}) {
|
|
|
4750
4585
|
systemPrompt: "system",
|
|
4751
4586
|
prompt: "hi there",
|
|
4752
4587
|
content: [{ type: "text", text: "hi there" }],
|
|
4753
|
-
//
|
|
4754
|
-
// conformance fixture carries a real bare SDK id.
|
|
4588
|
+
// The adapter rejects a null model, so the fixture carries a real bare SDK id.
|
|
4755
4589
|
config: { model: "claude-opus-4-8" },
|
|
4756
4590
|
policy: CABANE_POLICY,
|
|
4757
4591
|
session: null,
|
|
@@ -4864,7 +4698,7 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
4864
4698
|
phase: "start",
|
|
4865
4699
|
summary: "sleep.md",
|
|
4866
4700
|
input: { path: "sleep.md" },
|
|
4867
|
-
//
|
|
4701
|
+
// Derived from the raw `mcp__cabane__` name.
|
|
4868
4702
|
mcpServer: "cabane"
|
|
4869
4703
|
},
|
|
4870
4704
|
{
|
|
@@ -4882,9 +4716,9 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
4882
4716
|
]
|
|
4883
4717
|
},
|
|
4884
4718
|
{
|
|
4885
|
-
//
|
|
4886
|
-
//
|
|
4887
|
-
//
|
|
4719
|
+
// Highest risk: cancel mid-stream. The abort lands after the tool result; the adapter stops
|
|
4720
|
+
// before the closing reply — no final text, no `result` event (the server writes the stopped
|
|
4721
|
+
// marker).
|
|
4888
4722
|
name: "cancel mid-stream",
|
|
4889
4723
|
request: makeRequest(),
|
|
4890
4724
|
nativeStream: [
|
|
@@ -4936,12 +4770,9 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
4936
4770
|
]
|
|
4937
4771
|
},
|
|
4938
4772
|
{
|
|
4939
|
-
//
|
|
4940
|
-
//
|
|
4941
|
-
// `
|
|
4942
|
-
// this one produced nothing at all. The adapter's job is unchanged either way
|
|
4943
|
-
// — report faithfully what the runtime said — so it must still emit
|
|
4944
|
-
// `result{ok:true}` here; the host is what decides that a success with no
|
|
4773
|
+
// The empty-runtime anomaly's captured trace: `system/init` then a successful `result`, nothing
|
|
4774
|
+
// in between. Distinct from `empty-final` (a tool ran, no speech); this produced nothing. The
|
|
4775
|
+
// adapter reports faithfully — `result{ok:true}` — and the host decides that a success with no
|
|
4945
4776
|
// content-bearing event is a failed turn.
|
|
4946
4777
|
name: "zero-content success (empty runtime result)",
|
|
4947
4778
|
request: makeRequest(),
|
|
@@ -4949,9 +4780,9 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
4949
4780
|
expected: [sessionEvent("s1"), { type: "result", ok: true }]
|
|
4950
4781
|
},
|
|
4951
4782
|
{
|
|
4952
|
-
//
|
|
4953
|
-
//
|
|
4954
|
-
//
|
|
4783
|
+
// On resume, a completed background task can emit a notification and its own zero-token result
|
|
4784
|
+
// before the prompt's frames. That result is not the turn boundary; decoding continues through
|
|
4785
|
+
// the reply and real usage.
|
|
4955
4786
|
name: "task-notification result does not end a resumed turn",
|
|
4956
4787
|
request: makeRequest({ session: encodeSession({ sdkSessionId: "s1", cwd: CWD }) }),
|
|
4957
4788
|
nativeStream: [
|
|
@@ -5027,8 +4858,8 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
5027
4858
|
]
|
|
5028
4859
|
},
|
|
5029
4860
|
{
|
|
5030
|
-
//
|
|
5031
|
-
//
|
|
4861
|
+
// The filter is deliberately narrow: an origin-less zero-token result still ends the turn, even
|
|
4862
|
+
// if more frames follow.
|
|
5032
4863
|
name: "origin-less result still ends a resumed turn",
|
|
5033
4864
|
request: makeRequest({ session: encodeSession({ sdkSessionId: "s1", cwd: CWD }) }),
|
|
5034
4865
|
nativeStream: [
|
|
@@ -5099,11 +4930,10 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
5099
4930
|
]
|
|
5100
4931
|
},
|
|
5101
4932
|
{
|
|
5102
|
-
//
|
|
5103
|
-
//
|
|
5104
|
-
//
|
|
5105
|
-
//
|
|
5106
|
-
// notice rather than agent-authored progress.
|
|
4933
|
+
// A subscription cap. The SDK emits a rejected `rate_limit_event` with a named subscription
|
|
4934
|
+
// window; the terminal error result classifies as `usage_capped`. The held provider line
|
|
4935
|
+
// independently classifies to the same kind, so it lands as a runtime notice, not agent
|
|
4936
|
+
// progress.
|
|
5107
4937
|
name: "subscription cap \u2192 usage_capped",
|
|
5108
4938
|
request: makeRequest(),
|
|
5109
4939
|
nativeStream: [
|
|
@@ -5122,9 +4952,8 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
5122
4952
|
]
|
|
5123
4953
|
},
|
|
5124
4954
|
{
|
|
5125
|
-
//
|
|
5126
|
-
//
|
|
5127
|
-
// structured result is the same usage cap.
|
|
4955
|
+
// A failed turn can still contain the agent's own partial narration. No text classification
|
|
4956
|
+
// means no authorship rewrite, even though the structured result is the same usage cap.
|
|
5128
4957
|
name: "partial narration before subscription cap stays progress",
|
|
5129
4958
|
request: makeRequest(),
|
|
5130
4959
|
nativeStream: [
|
|
@@ -5140,8 +4969,8 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
5140
4969
|
]
|
|
5141
4970
|
},
|
|
5142
4971
|
{
|
|
5143
|
-
//
|
|
5144
|
-
//
|
|
4972
|
+
// Recognized runtime prose stays authored when its kind disagrees with the structured terminal
|
|
4973
|
+
// failure.
|
|
5145
4974
|
name: "cap notice before server failure stays progress",
|
|
5146
4975
|
request: makeRequest(),
|
|
5147
4976
|
nativeStream: [
|
|
@@ -5163,10 +4992,9 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
5163
4992
|
]
|
|
5164
4993
|
},
|
|
5165
4994
|
{
|
|
5166
|
-
//
|
|
5167
|
-
//
|
|
5168
|
-
//
|
|
5169
|
-
// later fails with the matching provider kind.
|
|
4995
|
+
// `classifyErrorText` deliberately searches arbitrary error blobs for status tokens. Assistant
|
|
4996
|
+
// narration needs the stricter whole-notice classifier, or an incidental count would be
|
|
4997
|
+
// de-authored when the turn later fails with the matching provider kind.
|
|
5170
4998
|
name: "incidental 5xx-shaped narration before server failure stays progress",
|
|
5171
4999
|
request: makeRequest(),
|
|
5172
5000
|
nativeStream: [
|
|
@@ -5188,9 +5016,8 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
5188
5016
|
]
|
|
5189
5017
|
},
|
|
5190
5018
|
{
|
|
5191
|
-
//
|
|
5192
|
-
//
|
|
5193
|
-
// classifies as the cap (`usage_capped`).
|
|
5019
|
+
// A cap signalled only by the result's `terminal_reason:'blocking_limit'` (no rate-limit event)
|
|
5020
|
+
// still classifies as `usage_capped`.
|
|
5194
5021
|
name: "blocking_limit terminal reason \u2192 usage_capped",
|
|
5195
5022
|
request: makeRequest(),
|
|
5196
5023
|
nativeStream: [
|
|
@@ -5204,8 +5031,8 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
5204
5031
|
expected: [sessionEvent("s1"), { type: "result", ok: false, reason: "usage_capped" }]
|
|
5205
5032
|
},
|
|
5206
5033
|
{
|
|
5207
|
-
//
|
|
5208
|
-
//
|
|
5034
|
+
// An expired login surfaces as an `auth_status` error frame; the failing result classifies as
|
|
5035
|
+
// `auth_expired`.
|
|
5209
5036
|
name: "expired login \u2192 auth_expired",
|
|
5210
5037
|
request: makeRequest(),
|
|
5211
5038
|
nativeStream: [
|
|
@@ -5219,10 +5046,9 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
5219
5046
|
expected: [sessionEvent("s1"), { type: "result", ok: false, reason: "auth_expired" }]
|
|
5220
5047
|
},
|
|
5221
5048
|
{
|
|
5222
|
-
//
|
|
5223
|
-
//
|
|
5224
|
-
//
|
|
5225
|
-
// connector `limited` (the whole point of the honest taxonomy).
|
|
5049
|
+
// A transient provider outage in the terminal result text (no structured event or enum) — a 529
|
|
5050
|
+
// overload — classifies as `server_error` via the text fallback. It must not read as the cap or
|
|
5051
|
+
// stamp the connector `limited`.
|
|
5226
5052
|
name: "provider overload text \u2192 server_error",
|
|
5227
5053
|
request: makeRequest(),
|
|
5228
5054
|
nativeStream: [
|
|
@@ -5235,11 +5061,9 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
5235
5061
|
expected: [sessionEvent("s1"), { type: "result", ok: false, reason: "server_error" }]
|
|
5236
5062
|
},
|
|
5237
5063
|
{
|
|
5238
|
-
//
|
|
5239
|
-
//
|
|
5240
|
-
//
|
|
5241
|
-
// The configured request model is irrelevant here; what's asserted is that the
|
|
5242
|
-
// concrete id the HARNESS reported comes home.
|
|
5064
|
+
// The resolved model is captured off `system/init.model` and attached to the terminal `result`
|
|
5065
|
+
// even on a plain resume (this fixture resumes, so no `session` event fires). What's asserted is
|
|
5066
|
+
// that the concrete id the harness reported comes home.
|
|
5243
5067
|
name: "resolved model captured from system/init",
|
|
5244
5068
|
request: makeRequest({ session: encodeSession({ sdkSessionId: "s1", cwd: CWD }) }),
|
|
5245
5069
|
nativeStream: [init("s1", "claude-opus-4-8"), assistantText("Done."), resultSuccess("s1")],
|
|
@@ -5249,9 +5073,8 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
5249
5073
|
]
|
|
5250
5074
|
},
|
|
5251
5075
|
{
|
|
5252
|
-
// Session resume echo: the stored state matches the
|
|
5253
|
-
//
|
|
5254
|
-
// (the id didn't change).
|
|
5076
|
+
// Session resume echo: the stored state matches the cwd, so its SDK id is resumed and
|
|
5077
|
+
// `system/init` echoes the same id — no session event fires.
|
|
5255
5078
|
name: "session resume (no re-emit on echo)",
|
|
5256
5079
|
request: makeRequest({ session: encodeSession({ sdkSessionId: "s1", cwd: CWD }) }),
|
|
5257
5080
|
nativeStream: [init("s1"), assistantText("Back again."), resultSuccess("s1")],
|
|
@@ -5261,16 +5084,14 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
5261
5084
|
]
|
|
5262
5085
|
},
|
|
5263
5086
|
{
|
|
5264
|
-
//
|
|
5265
|
-
//
|
|
5266
|
-
// reports a new id, and the emitted session event records the CURRENT cwd.
|
|
5087
|
+
// Cwd-mismatch degrade: the stored state was created under another cwd, so the adapter starts
|
|
5088
|
+
// fresh — `system/init` reports a new id, and the session event records the current cwd.
|
|
5267
5089
|
name: "session cwd-mismatch degrades to fresh",
|
|
5268
5090
|
request: makeRequest({ session: encodeSession({ sdkSessionId: "old", cwd: "/env/gone" }) }),
|
|
5269
5091
|
nativeStream: [init("fresh1"), assistantText("Fresh start."), resultSuccess("fresh1")],
|
|
5270
5092
|
expected: [
|
|
5271
|
-
//
|
|
5272
|
-
//
|
|
5273
|
-
// pre-mark context the discarded session no longer carries).
|
|
5093
|
+
// The degrade is flagged on the session event so the host relays it on settle and the server
|
|
5094
|
+
// rewinds the catch-up mark (recovering context the discarded session no longer carries).
|
|
5274
5095
|
sessionEvent("fresh1", CWD, true),
|
|
5275
5096
|
{ type: "text", body: "Fresh start.", terminal: true },
|
|
5276
5097
|
{ type: "result", ok: true }
|
|
@@ -5913,8 +5734,6 @@ function createOpencodeAdapter(deps = {}) {
|
|
|
5913
5734
|
const transport = deps.transport ?? (deps.serverUrl ? createHttpOpencodeTransport({ baseUrl: deps.serverUrl }) : null);
|
|
5914
5735
|
return {
|
|
5915
5736
|
name: "opencode",
|
|
5916
|
-
// CT829: one surface, so one addendum and no parameter — CT614's `codeMode`
|
|
5917
|
-
// argument went with the named tool surface it selected against.
|
|
5918
5737
|
promptAddendum: () => OPENCODE_ADDENDUM,
|
|
5919
5738
|
dialectSchema: opencodeDialectSchema,
|
|
5920
5739
|
async *runTurn(request, signal, hooks) {
|
|
@@ -6077,15 +5896,12 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
6077
5896
|
]
|
|
6078
5897
|
},
|
|
6079
5898
|
{
|
|
6080
|
-
//
|
|
6081
|
-
//
|
|
6082
|
-
//
|
|
6083
|
-
// (
|
|
6084
|
-
//
|
|
6085
|
-
//
|
|
6086
|
-
// upserts) must carry the LATEST non-empty input + its summary, not the
|
|
6087
|
-
// first-seen `{}`. This is the case the pre-CT301 suite structurally couldn't
|
|
6088
|
-
// catch: every other tool fixture bakes the full input into the first snapshot.
|
|
5899
|
+
// A streaming provider's tool input arrives late. opencode re-emits the tool part as a full
|
|
5900
|
+
// snapshot as the args parse: DeepSeek sends `input: {}` on the early `pending`/`running` frame
|
|
5901
|
+
// and the full args on a later one (captured on opencode 1.17: `pending {}` → `running {full}` →
|
|
5902
|
+
// `completed {full}`). `start` fires off the first (empty) frame — the live card — but `done`
|
|
5903
|
+
// (and the row it upserts) must carry the latest non-empty input and its summary. Every other
|
|
5904
|
+
// tool fixture bakes the full input into the first snapshot, so only this one catches it.
|
|
6089
5905
|
name: "streaming tool input \u2014 late args refresh the done event (CT301)",
|
|
6090
5906
|
request: makeRequest2(),
|
|
6091
5907
|
nativeStream: [
|
|
@@ -6123,9 +5939,8 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
6123
5939
|
]
|
|
6124
5940
|
},
|
|
6125
5941
|
{
|
|
6126
|
-
//
|
|
6127
|
-
//
|
|
6128
|
-
// CT1295: the server writes the `kind:'stopped'` marker.
|
|
5942
|
+
// Highest risk: cancel mid-stream. The abort lands after the tool result; the adapter stops before
|
|
5943
|
+
// the closing reply — no final text, no `result` event (the server writes the stopped marker).
|
|
6129
5944
|
name: "cancel mid-stream",
|
|
6130
5945
|
request: makeRequest2(),
|
|
6131
5946
|
nativeStream: [
|
|
@@ -6165,10 +5980,9 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
6165
5980
|
]
|
|
6166
5981
|
},
|
|
6167
5982
|
{
|
|
6168
|
-
//
|
|
6169
|
-
//
|
|
6170
|
-
//
|
|
6171
|
-
// account cap. opencode is text-only so it inherits the CT592 split for free.
|
|
5983
|
+
// A provider 429 throttle. The wrapped `RateLimitError` is unwrapped by `readSessionError` and
|
|
5984
|
+
// text-classified: a bare "rate limit (429)" with no cap noun is the throttle (`rate_limited`),
|
|
5985
|
+
// not the account cap.
|
|
6172
5986
|
name: "provider 429 \u2192 rate_limited (throttle, no reset)",
|
|
6173
5987
|
request: makeRequest2(),
|
|
6174
5988
|
nativeStream: [
|
|
@@ -6184,9 +5998,8 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
6184
5998
|
]
|
|
6185
5999
|
},
|
|
6186
6000
|
{
|
|
6187
|
-
//
|
|
6188
|
-
//
|
|
6189
|
-
// server renders "try again later").
|
|
6001
|
+
// A subscription cap surfaced as text ("usage limit reached") → `usage_capped`. opencode carries
|
|
6002
|
+
// no reset hint, so it's reset-less (the server says "try again later").
|
|
6190
6003
|
name: "usage cap text \u2192 usage_capped (no reset)",
|
|
6191
6004
|
request: makeRequest2(),
|
|
6192
6005
|
nativeStream: [
|
|
@@ -6202,9 +6015,7 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
6202
6015
|
]
|
|
6203
6016
|
},
|
|
6204
6017
|
{
|
|
6205
|
-
//
|
|
6206
|
-
// honest `server_error`, never the cap. This is the outage class that used to
|
|
6207
|
-
// fall through to an opaque `unknown`.
|
|
6018
|
+
// A transient provider outage (5xx / overload) surfaced as text → `server_error`, never the cap.
|
|
6208
6019
|
name: "provider overload text \u2192 server_error",
|
|
6209
6020
|
request: makeRequest2(),
|
|
6210
6021
|
nativeStream: [
|
|
@@ -6222,7 +6033,7 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
6222
6033
|
]
|
|
6223
6034
|
},
|
|
6224
6035
|
{
|
|
6225
|
-
//
|
|
6036
|
+
// An expired or invalid credential classifies as `auth_expired`.
|
|
6226
6037
|
name: "auth failure \u2192 auth_expired",
|
|
6227
6038
|
request: makeRequest2(),
|
|
6228
6039
|
nativeStream: [
|
|
@@ -6238,8 +6049,8 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
6238
6049
|
]
|
|
6239
6050
|
},
|
|
6240
6051
|
{
|
|
6241
|
-
//
|
|
6242
|
-
//
|
|
6052
|
+
// An unclassified failure stays informative: its provider text is visible, and the raw machine
|
|
6053
|
+
// reason is unchanged.
|
|
6243
6054
|
name: "unclassified failure keeps a runtime notice and raw reason",
|
|
6244
6055
|
request: makeRequest2(),
|
|
6245
6056
|
nativeStream: [errored("something local broke")],
|
|
@@ -6260,8 +6071,8 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
6260
6071
|
]
|
|
6261
6072
|
},
|
|
6262
6073
|
{
|
|
6263
|
-
//
|
|
6264
|
-
//
|
|
6074
|
+
// Empty, whitespace-only and bare status-token failures still set the machine reason but add no
|
|
6075
|
+
// empty or unhelpful transcript notice.
|
|
6265
6076
|
name: "empty failure text emits no runtime notice",
|
|
6266
6077
|
request: makeRequest2(),
|
|
6267
6078
|
nativeStream: [errored("")],
|
|
@@ -6293,12 +6104,10 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
6293
6104
|
]
|
|
6294
6105
|
},
|
|
6295
6106
|
{
|
|
6296
|
-
//
|
|
6297
|
-
//
|
|
6298
|
-
//
|
|
6299
|
-
//
|
|
6300
|
-
// runtime that CAN produce this envelope must be shown producing it in the
|
|
6301
|
-
// shape the classifier reads.
|
|
6107
|
+
// The zero-content success the claude-code suite pins, in opencode's vocabulary — the session
|
|
6108
|
+
// goes idle having emitted no part. No opencode sample has been seen; the fixture exists because
|
|
6109
|
+
// the host's classification is runtime-neutral, so a runtime that can produce this envelope must
|
|
6110
|
+
// be shown producing it in the shape the classifier reads.
|
|
6302
6111
|
name: "zero-content success (empty runtime result)",
|
|
6303
6112
|
request: makeRequest2(),
|
|
6304
6113
|
nativeStream: [idle()],
|
|
@@ -6355,10 +6164,9 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
6355
6164
|
]
|
|
6356
6165
|
},
|
|
6357
6166
|
{
|
|
6358
|
-
//
|
|
6359
|
-
//
|
|
6360
|
-
//
|
|
6361
|
-
// removed; restoring it is the crux of restoring cross-turn memory.
|
|
6167
|
+
// The stored state matches the directory, so the adapter resumes its id and the transport reports
|
|
6168
|
+
// `created:false` on the same id — no session event fires. Resume is what carries cross-turn
|
|
6169
|
+
// memory.
|
|
6362
6170
|
name: "session resume (no re-emit on echo)",
|
|
6363
6171
|
request: makeRequest2({ session: encodeSession2({ sessionId: "s1", directory: DIR }) }),
|
|
6364
6172
|
nativeStream: [textPart("p1", "Back again."), idle()],
|
|
@@ -6368,12 +6176,10 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
6368
6176
|
]
|
|
6369
6177
|
},
|
|
6370
6178
|
{
|
|
6371
|
-
//
|
|
6372
|
-
//
|
|
6373
|
-
//
|
|
6374
|
-
//
|
|
6375
|
-
// injected per-turn context still can't leak into Steps. Only the assistant
|
|
6376
|
-
// reply surfaces. No session event (resume echo on the same id).
|
|
6179
|
+
// On a resumed turn the transport mints no client message id (doing so no-ops the turn), so the
|
|
6180
|
+
// decoder gets no `skipMessageId`. It learns the user-message id from the stream — the
|
|
6181
|
+
// `message.updated` user frame precedes the echoed part — and drops the echo, so the injected
|
|
6182
|
+
// per-turn context can't leak into Steps. Only the reply surfaces; no session event.
|
|
6377
6183
|
name: "session resume \u2014 prompt echo filtered via stream-derived user id",
|
|
6378
6184
|
request: makeRequest2({ session: encodeSession2({ sessionId: "s1", directory: DIR }) }),
|
|
6379
6185
|
nativeStream: [
|
|
@@ -6389,17 +6195,15 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
6389
6195
|
]
|
|
6390
6196
|
},
|
|
6391
6197
|
{
|
|
6392
|
-
// Directory-mismatch degrade
|
|
6393
|
-
//
|
|
6394
|
-
//
|
|
6395
|
-
// event records the CURRENT directory.
|
|
6198
|
+
// Directory-mismatch degrade: the stored state was created under another directory, so the
|
|
6199
|
+
// adapter starts fresh — the transport creates a new session, and the session event records the
|
|
6200
|
+
// current directory.
|
|
6396
6201
|
name: "session directory-mismatch degrades to fresh",
|
|
6397
6202
|
request: makeRequest2({ session: encodeSession2({ sessionId: "old", directory: "/env/gone" }) }),
|
|
6398
6203
|
nativeStream: [textPart("p1", "Fresh start."), idle()],
|
|
6399
6204
|
expected: [
|
|
6400
|
-
//
|
|
6401
|
-
//
|
|
6402
|
-
// catch-up mark, recovering the context the discarded session dropped.
|
|
6205
|
+
// The degrade is flagged on the session event so the host relays it on settle and the server
|
|
6206
|
+
// rewinds the catch-up mark, recovering context the discarded session dropped.
|
|
6403
6207
|
sessionEvent2(NEW_SESSION_ID, DIR, true),
|
|
6404
6208
|
{ type: "text", body: "Fresh start.", terminal: true },
|
|
6405
6209
|
{ type: "result", ok: true }
|
|
@@ -6417,11 +6221,9 @@ var CODEX_ADDENDUM = [
|
|
|
6417
6221
|
"absent without attempting discovery and invocation, and if a call fails, report the recorded",
|
|
6418
6222
|
"error.",
|
|
6419
6223
|
turnControlPromptGuidance("mcp__cabane_companion__"),
|
|
6420
|
-
//
|
|
6421
|
-
//
|
|
6422
|
-
//
|
|
6423
|
-
// so the sentence contradicted itself. The bare `query` / `mutate` below stay:
|
|
6424
|
-
// that sentence is about operation names, after the qualified forms are given.
|
|
6224
|
+
// Every ancillary tool is written as Codex's own tool list shows it — qualified — so the sentence
|
|
6225
|
+
// instructing exact qualified names doesn't contradict itself. The bare `query` / `mutate` below
|
|
6226
|
+
// stay: that sentence is about operation names, after the qualified forms are given.
|
|
6425
6227
|
"The ancillary tools are `mcp__cabane__read_binary`, `mcp__cabane__upload`,",
|
|
6426
6228
|
"`mcp__cabane__begin_upload`, `mcp__cabane__finalize_upload` and",
|
|
6427
6229
|
"`mcp__cabane__mint_render_token` \u2014 all possibly deferred too.",
|
|
@@ -6665,10 +6467,9 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
6665
6467
|
threadId,
|
|
6666
6468
|
cwd: ctx.cwd ?? "",
|
|
6667
6469
|
promptFingerprint: ctx.promptFingerprint ?? null,
|
|
6668
|
-
//
|
|
6669
|
-
//
|
|
6670
|
-
//
|
|
6671
|
-
// The closing write below records the new baseline.
|
|
6470
|
+
// The baseline this turn inherited, carried through unchanged. This early write records the
|
|
6471
|
+
// thread id even if the turn dies mid-stream; it can't yet know what the turn will spend. The
|
|
6472
|
+
// closing write records the new baseline.
|
|
6672
6473
|
usageBaseline: ctx.usageBaseline ?? null
|
|
6673
6474
|
});
|
|
6674
6475
|
if (threadId !== ctx.resumedThreadId || state !== ctx.storedState) {
|
|
@@ -6952,9 +6753,8 @@ function buildConfig(req, baseInstructionsFile) {
|
|
|
6952
6753
|
return {
|
|
6953
6754
|
mcp_servers,
|
|
6954
6755
|
experimental_use_rmcp_client: true,
|
|
6955
|
-
//
|
|
6956
|
-
//
|
|
6957
|
-
// with the harness prompt it belonged to.
|
|
6756
|
+
// The no-host-access prompt replacement — Cabane's composed prompt becomes Codex's base
|
|
6757
|
+
// instructions, and Codex's environment preamble goes with the harness prompt it belonged to.
|
|
6958
6758
|
...baseInstructionsFile ? { model_instructions_file: baseInstructionsFile, include_environment_context: false } : {},
|
|
6959
6759
|
...Object.keys(shellEnv).length ? { shell_environment_policy: { set: shellEnv } } : {}
|
|
6960
6760
|
};
|
|
@@ -7051,16 +6851,11 @@ function createSdkCodexTransport(opts = {}) {
|
|
|
7051
6851
|
const codexOptions = {
|
|
7052
6852
|
...opts.apiKey ? { apiKey: opts.apiKey } : {},
|
|
7053
6853
|
...opts.codexPathOverride ? { codexPathOverride: opts.codexPathOverride } : {},
|
|
7054
|
-
// The `--config` overrides (MCP servers + rmcp flag)
|
|
7055
|
-
//
|
|
7056
|
-
//
|
|
7057
|
-
//
|
|
7058
|
-
//
|
|
7059
|
-
// ignores TOML quoting), so it has to ride inside an inline-table value,
|
|
7060
|
-
// and the SDK's flattener cannot be steered into producing one. This is
|
|
7061
|
-
// also what makes the PUBLISHED companion correct, where the vendored
|
|
7062
|
-
// patch never reached — and why that patch is gone. See
|
|
7063
|
-
// `config-overrides.ts`.
|
|
6854
|
+
// The `--config` overrides (MCP servers + rmcp flag), rendered to finished `--config` strings
|
|
6855
|
+
// here and passed through `configOverrides`, the SDK's raw passthrough — not `config`, which
|
|
6856
|
+
// flattens them itself. A user MCP server named `my.server` can't go in an override's dotted
|
|
6857
|
+
// path (the CLI splits on `.` and ignores TOML quoting), so it must ride inside an inline-table
|
|
6858
|
+
// value, which the SDK's flattener can't produce. See `config-overrides.ts`.
|
|
7064
6859
|
configOverrides: buildConfigOverrides(spec.config),
|
|
7065
6860
|
...spec.secretEnv ? { env: codexProcessEnv(spec.secretEnv) } : {}
|
|
7066
6861
|
};
|
|
@@ -7078,8 +6873,6 @@ function createCodexAdapter(deps = {}) {
|
|
|
7078
6873
|
const transport = deps.transport ?? (deps.enabled ? createSdkCodexTransport({ ...deps.apiKey ? { apiKey: deps.apiKey } : {} }) : null);
|
|
7079
6874
|
return {
|
|
7080
6875
|
name: "codex",
|
|
7081
|
-
// CT829: one surface, so one addendum and no parameter — CT614's `codeMode`
|
|
7082
|
-
// argument went with the named tool surface it selected against.
|
|
7083
6876
|
promptAddendum: () => CODEX_ADDENDUM,
|
|
7084
6877
|
dialectSchema: codexDialectSchema,
|
|
7085
6878
|
async *runTurn(request, signal, hooks) {
|
|
@@ -7171,17 +6964,14 @@ function createCodexAdapter(deps = {}) {
|
|
|
7171
6964
|
cwd: req.local.cwd,
|
|
7172
6965
|
resumedThreadId: resumeThreadId,
|
|
7173
6966
|
degraded,
|
|
7174
|
-
//
|
|
7175
|
-
//
|
|
7176
|
-
//
|
|
7177
|
-
// it, the thread still carries whatever it did before. Read off the spec
|
|
7178
|
-
// rather than recomputed, so the recorded fact can't disagree with what
|
|
7179
|
-
// was actually sent.
|
|
6967
|
+
// Which prompt the thread carries once this turn lands. If we sent one, it's that one — the
|
|
6968
|
+
// later copy is what the model reads. If we withheld it, the thread carries what it did. Read
|
|
6969
|
+
// off the spec, so the record can't disagree with what was sent.
|
|
7180
6970
|
promptFingerprint: spec.promptRidesInput ? spec.promptFingerprint : threadPromptFingerprint,
|
|
7181
6971
|
storedState: req.session,
|
|
7182
6972
|
usageBaseline: threadUsageBaseline,
|
|
7183
|
-
//
|
|
7184
|
-
//
|
|
6973
|
+
// Codex reports no model in-stream, so record what the thread was started with (null on the
|
|
6974
|
+
// default token). Same for the reasoning effort.
|
|
7185
6975
|
resolvedModel: spec.model,
|
|
7186
6976
|
resolvedReasoningEffort: spec.modelReasoningEffort ?? null
|
|
7187
6977
|
});
|
|
@@ -7230,11 +7020,9 @@ function makeRequest3(overrides2 = {}) {
|
|
|
7230
7020
|
systemPrompt: PROMPT,
|
|
7231
7021
|
prompt: "hi there",
|
|
7232
7022
|
content: [{ type: "text", text: "hi there" }],
|
|
7233
|
-
//
|
|
7234
|
-
//
|
|
7235
|
-
//
|
|
7236
|
-
// emitted → their expected results stay unchanged; a dedicated capture fixture
|
|
7237
|
-
// sets a real model + effort.
|
|
7023
|
+
// The resolved model and reasoning effort ride the terminal `result` off the run spec's model.
|
|
7024
|
+
// Behavioural fixtures use the model-less token (no `resolvedModel` emitted); a dedicated capture
|
|
7025
|
+
// fixture sets a real model and effort.
|
|
7238
7026
|
config: { model: null },
|
|
7239
7027
|
policy: COMPANION_POLICY2,
|
|
7240
7028
|
session: null,
|
|
@@ -7286,13 +7074,12 @@ var sessionEvent3 = (threadId, cwd = DIR2, degraded = false, promptFingerprint =
|
|
|
7286
7074
|
});
|
|
7287
7075
|
var CODEX_CONFORMANCE_FIXTURES = [
|
|
7288
7076
|
{
|
|
7289
|
-
// A plain text reply: the held agent message seals as the terminal final at
|
|
7290
|
-
//
|
|
7077
|
+
// A plain text reply: the held agent message seals as the terminal final at `turn.completed`;
|
|
7078
|
+
// usage rides the `result`.
|
|
7291
7079
|
//
|
|
7292
|
-
//
|
|
7293
|
-
// `
|
|
7294
|
-
//
|
|
7295
|
-
// out beside them. This fixture pinned the double-count before CT1283.
|
|
7080
|
+
// The counters nest — `cached_input_tokens` is a slice of `input_tokens`,
|
|
7081
|
+
// `reasoning_output_tokens` a slice of `output_tokens` — so input is 100 (not 120) and output 50
|
|
7082
|
+
// (not 60), with the cache read broken out beside them.
|
|
7296
7083
|
name: "clean turn",
|
|
7297
7084
|
request: makeRequest3(),
|
|
7298
7085
|
nativeStream: [
|
|
@@ -7309,10 +7096,8 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7309
7096
|
expected: [
|
|
7310
7097
|
sessionEvent3(NEW_THREAD_ID),
|
|
7311
7098
|
{ type: "text", body: "Hello there.", terminal: true },
|
|
7312
|
-
//
|
|
7313
|
-
//
|
|
7314
|
-
// `result` usage below is the frame verbatim (nothing to subtract yet);
|
|
7315
|
-
// what makes it per-turn is this figure being carried into turn two.
|
|
7099
|
+
// The closing session write — the thread's ending totals, recorded as the baseline the next turn
|
|
7100
|
+
// subtracts. A fresh thread, so the `result` usage below is the frame verbatim.
|
|
7316
7101
|
sessionEvent3(NEW_THREAD_ID, DIR2, false, fingerprintPrompt(PROMPT), {
|
|
7317
7102
|
inputTokens: 100,
|
|
7318
7103
|
outputTokens: 50,
|
|
@@ -7384,8 +7169,8 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7384
7169
|
]
|
|
7385
7170
|
},
|
|
7386
7171
|
{
|
|
7387
|
-
//
|
|
7388
|
-
//
|
|
7172
|
+
// The recorded MCP success shape: Codex exposes the text copy and the native object together;
|
|
7173
|
+
// normalization must keep the object.
|
|
7389
7174
|
name: "MCP success prefers native structured content",
|
|
7390
7175
|
request: makeRequest3(),
|
|
7391
7176
|
nativeStream: [
|
|
@@ -7435,8 +7220,8 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7435
7220
|
]
|
|
7436
7221
|
},
|
|
7437
7222
|
{
|
|
7438
|
-
//
|
|
7439
|
-
//
|
|
7223
|
+
// MCP `isError` keeps the failed phase, while the raw camel-case structured payload survives
|
|
7224
|
+
// instead of the generic `error` token.
|
|
7440
7225
|
name: "failed MCP call preserves structured receipt while remaining error phase",
|
|
7441
7226
|
request: makeRequest3(),
|
|
7442
7227
|
nativeStream: [
|
|
@@ -7488,8 +7273,8 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7488
7273
|
]
|
|
7489
7274
|
},
|
|
7490
7275
|
{
|
|
7491
|
-
// A transport/tool failure
|
|
7492
|
-
//
|
|
7276
|
+
// A transport/tool failure with no MCP result payload still uses the real adapter error message,
|
|
7277
|
+
// never `undefined`.
|
|
7493
7278
|
name: "failed MCP call without result keeps error message fallback",
|
|
7494
7279
|
request: makeRequest3(),
|
|
7495
7280
|
nativeStream: [
|
|
@@ -7529,8 +7314,8 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7529
7314
|
]
|
|
7530
7315
|
},
|
|
7531
7316
|
{
|
|
7532
|
-
//
|
|
7533
|
-
//
|
|
7317
|
+
// A connector's structured metadata beside an image-only MCP response: no image/base64 payload
|
|
7318
|
+
// enters activity receipts.
|
|
7534
7319
|
name: "image-only MCP result remains empty despite structured metadata",
|
|
7535
7320
|
request: makeRequest3(),
|
|
7536
7321
|
nativeStream: [
|
|
@@ -7623,12 +7408,10 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7623
7408
|
]
|
|
7624
7409
|
},
|
|
7625
7410
|
{
|
|
7626
|
-
//
|
|
7627
|
-
//
|
|
7628
|
-
//
|
|
7629
|
-
//
|
|
7630
|
-
// and carry the populated query the completed frame filled in. Before CT715 the
|
|
7631
|
-
// card was pinned at "running" forever (status derived from a missing field).
|
|
7411
|
+
// web_search lifecycle. A `web_search` item carries no `status` — completion is signalled by the
|
|
7412
|
+
// frame type (`item.started` → `item.completed`). The `start` fires off the first frame (empty
|
|
7413
|
+
// query); the `done` must resolve off `item.completed` with the populated query, not stay
|
|
7414
|
+
// "running".
|
|
7632
7415
|
name: "web_search lifecycle \u2014 completes off frame type, populated query on done",
|
|
7633
7416
|
request: makeRequest3(),
|
|
7634
7417
|
nativeStream: [
|
|
@@ -7659,9 +7442,8 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7659
7442
|
]
|
|
7660
7443
|
},
|
|
7661
7444
|
{
|
|
7662
|
-
//
|
|
7663
|
-
//
|
|
7664
|
-
// resolve to `done` (empty query acceptable; stuck-running is not).
|
|
7445
|
+
// A non-text web action (`action.type: "other"`) legitimately completes with an empty query —
|
|
7446
|
+
// Codex's own data. It must still resolve to `done`.
|
|
7665
7447
|
name: "web_search lifecycle \u2014 empty-query completion still resolves to done",
|
|
7666
7448
|
request: makeRequest3(),
|
|
7667
7449
|
nativeStream: [
|
|
@@ -7692,9 +7474,8 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7692
7474
|
]
|
|
7693
7475
|
},
|
|
7694
7476
|
{
|
|
7695
|
-
//
|
|
7696
|
-
//
|
|
7697
|
-
// CT1295: the server writes the `kind:'stopped'` marker.
|
|
7477
|
+
// Highest risk: cancel mid-stream. The abort lands after the tool result; the adapter stops before
|
|
7478
|
+
// the closing reply — no final text, no `result` event (the server writes the stopped marker).
|
|
7698
7479
|
name: "cancel mid-stream",
|
|
7699
7480
|
request: makeRequest3(),
|
|
7700
7481
|
nativeStream: [
|
|
@@ -7775,11 +7556,8 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7775
7556
|
]
|
|
7776
7557
|
},
|
|
7777
7558
|
{
|
|
7778
|
-
//
|
|
7779
|
-
//
|
|
7780
|
-
// classifier (Codex is text-only), so a provider 429 / outage / auth / cap
|
|
7781
|
-
// failure splits into the honest taxonomy instead of all reading `unknown`.
|
|
7782
|
-
// This is a real behavior change, not a refactor. One fixture per kind:
|
|
7559
|
+
// The failure text goes through the shared classifier (Codex is text-only), so a provider 429 /
|
|
7560
|
+
// outage / auth / cap failure splits into the honest taxonomy. One fixture per kind:
|
|
7783
7561
|
name: "turn.failed 429 \u2192 rate_limited (throttle)",
|
|
7784
7562
|
request: makeRequest3(),
|
|
7785
7563
|
nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("429 Too Many Requests")],
|
|
@@ -7826,8 +7604,8 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7826
7604
|
]
|
|
7827
7605
|
},
|
|
7828
7606
|
{
|
|
7829
|
-
//
|
|
7830
|
-
//
|
|
7607
|
+
// A failure text the classifier can't place keeps the raw `error:<text>` reason (the `unknown`
|
|
7608
|
+
// fallback).
|
|
7831
7609
|
name: "turn.failed unclassified \u2192 raw error reason (unchanged)",
|
|
7832
7610
|
request: makeRequest3(),
|
|
7833
7611
|
nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("something local broke")],
|
|
@@ -7851,8 +7629,8 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7851
7629
|
]
|
|
7852
7630
|
},
|
|
7853
7631
|
{
|
|
7854
|
-
//
|
|
7855
|
-
//
|
|
7632
|
+
// Empty, whitespace-only and bare status-token failures still set the machine reason but add no
|
|
7633
|
+
// empty or unhelpful transcript notice.
|
|
7856
7634
|
name: "empty failure text emits no runtime notice",
|
|
7857
7635
|
request: makeRequest3(),
|
|
7858
7636
|
nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("")],
|
|
@@ -7881,11 +7659,9 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7881
7659
|
]
|
|
7882
7660
|
},
|
|
7883
7661
|
{
|
|
7884
|
-
//
|
|
7885
|
-
//
|
|
7886
|
-
//
|
|
7887
|
-
// adapter ends the turn the moment the signal lands, with a machine reason — so
|
|
7888
|
-
// a future Codex id-drop errors clean instead of degrading mid-tool-turn.
|
|
7662
|
+
// Fail loud on unknown model metadata. An unknown `--model` makes Codex emit an `error` item
|
|
7663
|
+
// ("… Defaulting to fallback metadata …") and misbehave (the spurious tool call scripted after it
|
|
7664
|
+
// must never be emitted). The adapter ends the turn when the signal lands, with a machine reason.
|
|
7889
7665
|
name: "unknown model metadata \u2192 fail loud (guard)",
|
|
7890
7666
|
request: makeRequest3(),
|
|
7891
7667
|
nativeStream: [
|
|
@@ -7931,12 +7707,10 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7931
7707
|
]
|
|
7932
7708
|
},
|
|
7933
7709
|
{
|
|
7934
|
-
//
|
|
7935
|
-
//
|
|
7936
|
-
//
|
|
7937
|
-
//
|
|
7938
|
-
// neutral, so a runtime that CAN produce this envelope must be shown
|
|
7939
|
-
// producing it in the shape the classifier reads.
|
|
7710
|
+
// The zero-content success the claude-code suite pins, in Codex's vocabulary — the thread
|
|
7711
|
+
// starts, the turn completes, nothing in between. No Codex sample has been seen; the fixture
|
|
7712
|
+
// exists because the host's classification is runtime-neutral, so a runtime that can produce
|
|
7713
|
+
// this envelope must be shown producing it in the shape the classifier reads.
|
|
7940
7714
|
name: "zero-content success (empty runtime result)",
|
|
7941
7715
|
request: makeRequest3(),
|
|
7942
7716
|
nativeStream: [threadStarted(NEW_THREAD_ID), turnCompleted()],
|
|
@@ -8005,11 +7779,10 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
8005
7779
|
]
|
|
8006
7780
|
},
|
|
8007
7781
|
{
|
|
8008
|
-
//
|
|
8009
|
-
//
|
|
8010
|
-
//
|
|
8011
|
-
//
|
|
8012
|
-
// `resolvedConfig.reasoningEffort`.
|
|
7782
|
+
// Codex reports no model in-stream, so the resolved model and reasoning effort come from what the
|
|
7783
|
+
// thread was started with (the run spec) and ride the terminal `result`: a real model and a
|
|
7784
|
+
// `runtimeOptions.codex.modelReasoningEffort` are asserted through to `resolvedModel` (bare,
|
|
7785
|
+
// `openai/`-stripped) and `resolvedConfig.reasoningEffort`.
|
|
8013
7786
|
name: "resolved model + reasoning effort captured from run spec",
|
|
8014
7787
|
request: makeRequest3({
|
|
8015
7788
|
config: {
|
|
@@ -8030,15 +7803,12 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
8030
7803
|
]
|
|
8031
7804
|
},
|
|
8032
7805
|
{
|
|
8033
|
-
// Session resume: the stored state matches the
|
|
8034
|
-
//
|
|
8035
|
-
// event fires (the id didn't change AND the state is unchanged). This is what
|
|
8036
|
-
// carries cross-turn memory.
|
|
7806
|
+
// Session resume: the stored state matches the directory, so the thread id is resumed and
|
|
7807
|
+
// `thread.started` echoes it — no session event fires. This is what carries cross-turn memory.
|
|
8037
7808
|
//
|
|
8038
|
-
//
|
|
8039
|
-
//
|
|
8040
|
-
//
|
|
8041
|
-
// didn't change and a prompt that didn't need re-sending.
|
|
7809
|
+
// The stored state also says the thread already holds the composed prompt, so this turn
|
|
7810
|
+
// withholds the preamble and the state it would write is byte-identical: the silence proves both
|
|
7811
|
+
// an unchanged id and a prompt that needed no re-send.
|
|
8042
7812
|
name: "session resume (no re-emit on echo)",
|
|
8043
7813
|
request: makeRequest3({
|
|
8044
7814
|
session: encodeSession3({
|
|
@@ -8055,16 +7825,12 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
8055
7825
|
]
|
|
8056
7826
|
},
|
|
8057
7827
|
{
|
|
8058
|
-
//
|
|
8059
|
-
//
|
|
8060
|
-
//
|
|
8061
|
-
//
|
|
8062
|
-
// the fingerprint recorded — on the SAME thread id, which pre-CT1075 emitted
|
|
8063
|
-
// nothing at all. That re-emit is the upgrade path: without it a long-lived
|
|
8064
|
-
// thread would re-send the whole prompt forever.
|
|
7828
|
+
// A resumed thread whose stored state carries no `promptFingerprint` field. The adapter reads the
|
|
7829
|
+
// absence as "can't say which prompt this thread holds", sends the full prompt, and re-emits the
|
|
7830
|
+
// state with the fingerprint — on the same thread id. That re-emit is the upgrade path: without
|
|
7831
|
+
// it a long-lived thread would re-send the whole prompt forever.
|
|
8065
7832
|
//
|
|
8066
|
-
// This
|
|
8067
|
-
// have no evidence about.
|
|
7833
|
+
// This fixture fails if the standing half is withheld on a thread we have no evidence about.
|
|
8068
7834
|
name: "session resume with a pre-CT1075 state re-emits with the prompt fingerprint",
|
|
8069
7835
|
request: makeRequest3({
|
|
8070
7836
|
session: JSON.stringify({ threadId: "th1", cwd: DIR2 })
|
|
@@ -8077,14 +7843,12 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
8077
7843
|
]
|
|
8078
7844
|
},
|
|
8079
7845
|
{
|
|
8080
|
-
//
|
|
8081
|
-
//
|
|
8082
|
-
//
|
|
8083
|
-
// as on a fresh thread and the state is re-recorded against it.
|
|
7846
|
+
// The thread carries a different prompt than this turn composed — a charter edit, a renamed
|
|
7847
|
+
// agent, a redeployed prompt body. The fingerprint doesn't match, so the new prompt is sent as on
|
|
7848
|
+
// a fresh thread and the state re-recorded.
|
|
8084
7849
|
//
|
|
8085
|
-
// This
|
|
8086
|
-
//
|
|
8087
|
-
// every future charter edit from this thread.
|
|
7850
|
+
// This fixture fails if a stale prompt is treated as good enough; a boolean flag would pass the
|
|
7851
|
+
// resume fixture above and withhold every future charter edit.
|
|
8088
7852
|
name: "session resume with a stale prompt fingerprint re-sends the prompt",
|
|
8089
7853
|
request: makeRequest3({
|
|
8090
7854
|
session: encodeSession3({
|
|
@@ -8102,15 +7866,12 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
8102
7866
|
]
|
|
8103
7867
|
},
|
|
8104
7868
|
{
|
|
8105
|
-
// Directory-mismatch degrade
|
|
8106
|
-
//
|
|
8107
|
-
//
|
|
8108
|
-
// records the CURRENT directory with `degraded: true` so the host rewinds the
|
|
8109
|
-
// catch-up mark.
|
|
7869
|
+
// Directory-mismatch degrade: the stored state was created under another directory, so the
|
|
7870
|
+
// adapter starts fresh — `thread.started` reports a new id, and the session event records the
|
|
7871
|
+
// current directory with `degraded: true` so the host rewinds the catch-up mark.
|
|
8110
7872
|
//
|
|
8111
|
-
//
|
|
8112
|
-
//
|
|
8113
|
-
// no longer in. The emitted state records the fresh thread's own answer.
|
|
7873
|
+
// The refused resume means the new thread is empty, so the full prompt goes in even though the
|
|
7874
|
+
// stored state vouched for a copy in a thread we're no longer in.
|
|
8114
7875
|
name: "session directory-mismatch degrades to fresh",
|
|
8115
7876
|
request: makeRequest3({
|
|
8116
7877
|
session: encodeSession3({
|
|
@@ -8159,7 +7920,7 @@ var ConnectorHealthStore = class {
|
|
|
8159
7920
|
lastError: outcome.errorReason ?? null
|
|
8160
7921
|
});
|
|
8161
7922
|
}
|
|
8162
|
-
//
|
|
7923
|
+
// The store's current opinion for one runtime, or undefined when it has
|
|
8163
7924
|
// no observation. Read by the dispatcher at idle-timeout fire: an idle reap
|
|
8164
7925
|
// while this runtime is definitively `limited` (a present, future `limitedUntil`
|
|
8165
7926
|
// recorded by a sibling/earlier turn's classified cap settle) is the account cap
|
|
@@ -8463,8 +8224,7 @@ function createTurnContainment(turnId, deps = {}) {
|
|
|
8463
8224
|
`--unit=${unit}`,
|
|
8464
8225
|
`--property=TimeoutStopSec=${CONTAINMENT_STOP_GRACE_SEC}s`,
|
|
8465
8226
|
// Bind the scope's life to the companion's own unit, so stopping or
|
|
8466
|
-
// restarting the companion still takes its in-flight turns with it
|
|
8467
|
-
// which is what happens today, and what the scope would otherwise undo.
|
|
8227
|
+
// restarting the companion still takes its in-flight turns with it.
|
|
8468
8228
|
// Omitted outside a unit, where there is nothing to bind to.
|
|
8469
8229
|
...ownUnit ? [`--property=PartOf=${ownUnit}`] : [],
|
|
8470
8230
|
"--"
|
|
@@ -8784,7 +8544,7 @@ var mcpHttpServerSchema = z15.object({
|
|
|
8784
8544
|
type: z15.literal("http"),
|
|
8785
8545
|
url: z15.string().url(),
|
|
8786
8546
|
headers: z15.record(z15.string(), z15.string()).optional(),
|
|
8787
|
-
//
|
|
8547
|
+
// Header → the name of a `secretEnv` entry holding its value. Set by
|
|
8788
8548
|
// the server on a room connection's entry, and by `resolveMcpSecrets` on a
|
|
8789
8549
|
// header whose `${VAR}` it resolved.
|
|
8790
8550
|
headersFromEnv: z15.record(z15.string(), z15.string()).optional()
|
|
@@ -8793,7 +8553,7 @@ var mcpSseServerSchema = z15.object({
|
|
|
8793
8553
|
type: z15.literal("sse"),
|
|
8794
8554
|
url: z15.string().url(),
|
|
8795
8555
|
headers: z15.record(z15.string(), z15.string()).optional(),
|
|
8796
|
-
//
|
|
8556
|
+
// Header → the name of a `secretEnv` entry holding its value. Set by
|
|
8797
8557
|
// the server on a room connection's entry, and by `resolveMcpSecrets` on a
|
|
8798
8558
|
// header whose `${VAR}` it resolved.
|
|
8799
8559
|
headersFromEnv: z15.record(z15.string(), z15.string()).optional()
|
|
@@ -8806,9 +8566,8 @@ var thinkingConfigSchema = z15.discriminatedUnion("type", [
|
|
|
8806
8566
|
]);
|
|
8807
8567
|
var effortSchema = z15.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
8808
8568
|
var runConfigSchema = z15.object({
|
|
8809
|
-
//
|
|
8810
|
-
//
|
|
8811
|
-
// is the locked surface. Kept in lockstep with `@cabane/shared`'s
|
|
8569
|
+
// `true` grants the host filesystem/shell; absent is the locked surface. Kept
|
|
8570
|
+
// in lockstep with `@cabane/shared`'s
|
|
8812
8571
|
// `agentRunConfigSchema` (independent zod, forward-compatible: unknown keys are
|
|
8813
8572
|
// stripped, so an older companion riding a newer server never rejects the config).
|
|
8814
8573
|
hostAccess: z15.boolean().optional(),
|
|
@@ -8835,7 +8594,7 @@ var RUNNER_ENV = {
|
|
|
8835
8594
|
dispatch: "CABANE_DISPATCH",
|
|
8836
8595
|
// The turn id the launcher resolved, and whether this is a replay of a turn
|
|
8837
8596
|
// an earlier process started. The seq floor for a replay is derived inside
|
|
8838
|
-
// the turn from the outbox and the server
|
|
8597
|
+
// the turn from the outbox and the server.
|
|
8839
8598
|
turnId: "CABANE_TURN_ID",
|
|
8840
8599
|
resumed: "CABANE_RESUMED",
|
|
8841
8600
|
// Everything else the turn is constructed from, as JSON: see `RunnerSpec`.
|
|
@@ -8871,10 +8630,10 @@ var runnerSpecSchema = z16.object({
|
|
|
8871
8630
|
// Whether the harness starts under the OS sandbox. The launcher sends 'os'
|
|
8872
8631
|
// only when this host can make one.
|
|
8873
8632
|
sandbox: z16.enum(["os", "none"]),
|
|
8874
|
-
//
|
|
8633
|
+
// The device's sandbox policy, validated by the launcher. Absent
|
|
8875
8634
|
// when the sandbox is off.
|
|
8876
8635
|
sandboxPolicy: sandboxPolicySchema.optional(),
|
|
8877
|
-
//
|
|
8636
|
+
// Every companion store the launcher's latest walk found, denied
|
|
8878
8637
|
// to the harness. Absent when the sandbox is off.
|
|
8879
8638
|
companionStores: z16.array(z16.string()).optional(),
|
|
8880
8639
|
logLevel: z16.string(),
|
|
@@ -8883,15 +8642,15 @@ var runnerSpecSchema = z16.object({
|
|
|
8883
8642
|
// True when this (conversation, agent) pair was already told its session
|
|
8884
8643
|
// could not be saved: the notice is once per pair, not once per process.
|
|
8885
8644
|
sessionWriteNoticeClaimed: z16.boolean(),
|
|
8886
|
-
//
|
|
8645
|
+
// The runner's identity, minted with its credential. The start
|
|
8887
8646
|
// PATCH carries it so the lease knows which runner holds the span.
|
|
8888
8647
|
runnerId: z16.string(),
|
|
8889
|
-
//
|
|
8648
|
+
// The launcher keeps the runtime's session files beside the attempt
|
|
8890
8649
|
// and restores them into this runner's home, so the runner names the attempt
|
|
8891
8650
|
// each context was composed for and waits for the launcher to say it's in
|
|
8892
8651
|
// place. Absent: nothing to wait for (a runner on the launcher's own disk).
|
|
8893
8652
|
attemptState: z16.boolean().optional(),
|
|
8894
|
-
//
|
|
8653
|
+
// MCP `${NAME}` secrets come from the launcher, by name, instead of
|
|
8895
8654
|
// this machine's secrets.json. Absent: the runner reads the device's file.
|
|
8896
8655
|
secretsFromLauncher: z16.boolean().optional()
|
|
8897
8656
|
});
|
|
@@ -9520,7 +9279,7 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
9520
9279
|
return performAct(acts, {
|
|
9521
9280
|
kind: "ask",
|
|
9522
9281
|
callId: randomUUID2(),
|
|
9523
|
-
//
|
|
9282
|
+
// The swap rides the ask, so the server commits both halves
|
|
9524
9283
|
// in one transaction — the person never sees neither or both.
|
|
9525
9284
|
...args.replaces ? { replaces: args.replaces } : {},
|
|
9526
9285
|
ask: hasArray ? {
|
|
@@ -9571,7 +9330,7 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
9571
9330
|
},
|
|
9572
9331
|
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
9573
9332
|
),
|
|
9574
|
-
//
|
|
9333
|
+
// The standalone withdrawal. Not annotated destructive — an ask
|
|
9575
9334
|
// is a question, not data, and a harness that gated this on a live
|
|
9576
9335
|
// operator would hang a turn nobody is watching (the `cancel_wake`
|
|
9577
9336
|
// reasoning). Not idempotent either: a second call with the same id is
|
|
@@ -9590,7 +9349,7 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
9590
9349
|
}),
|
|
9591
9350
|
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
9592
9351
|
),
|
|
9593
|
-
//
|
|
9352
|
+
// The inverse. No arguments — it can only ever stand down the
|
|
9594
9353
|
// caller's own wake in the conversation it's holding a turn in.
|
|
9595
9354
|
tool(
|
|
9596
9355
|
CANCEL_WAKE_TOOL,
|
|
@@ -9633,7 +9392,7 @@ function buildCompanionTurnRequest(params) {
|
|
|
9633
9392
|
// adapter's `ResolvedMcpServers`.
|
|
9634
9393
|
...Object.keys(params.mcpServers).length > 0 ? { mcpServers: params.mcpServers } : {},
|
|
9635
9394
|
...params.secretEnv && Object.keys(params.secretEnv).length > 0 ? { secretEnv: params.secretEnv } : {},
|
|
9636
|
-
//
|
|
9395
|
+
// The auto-memory escape hatch, when the operator set it.
|
|
9637
9396
|
...params.claudeCode ? { claudeCode: params.claudeCode } : {}
|
|
9638
9397
|
},
|
|
9639
9398
|
// Host-injected turn-control tools, under their own namespace.
|
|
@@ -9887,7 +9646,7 @@ var TranscriptWriter = class {
|
|
|
9887
9646
|
}
|
|
9888
9647
|
this.line({ type: "_meta", ...meta });
|
|
9889
9648
|
}
|
|
9890
|
-
// Append
|
|
9649
|
+
// Append one event verbatim.
|
|
9891
9650
|
write(msg) {
|
|
9892
9651
|
this.line(msg);
|
|
9893
9652
|
}
|
|
@@ -9895,7 +9654,7 @@ var TranscriptWriter = class {
|
|
|
9895
9654
|
close(outcome) {
|
|
9896
9655
|
this.line({ type: "_outcome", ...outcome });
|
|
9897
9656
|
}
|
|
9898
|
-
//
|
|
9657
|
+
// Keep an independent copy of an empty successful envelope after the
|
|
9899
9658
|
// footer has landed. The ordinary 200-file FIFO keeps rotating as before;
|
|
9900
9659
|
// this bucket retains the newest 50 anomalies regardless of ordinary traffic.
|
|
9901
9660
|
// Best-effort like every other transcript operation: observability can never
|
|
@@ -9992,6 +9751,8 @@ var TurnCommitter = class {
|
|
|
9992
9751
|
summary,
|
|
9993
9752
|
input,
|
|
9994
9753
|
result,
|
|
9754
|
+
inputChars,
|
|
9755
|
+
resultChars,
|
|
9995
9756
|
mcpServer,
|
|
9996
9757
|
phase,
|
|
9997
9758
|
seq
|
|
@@ -10007,6 +9768,8 @@ var TurnCommitter = class {
|
|
|
10007
9768
|
summary,
|
|
10008
9769
|
input,
|
|
10009
9770
|
result,
|
|
9771
|
+
...inputChars !== void 0 ? { inputChars } : {},
|
|
9772
|
+
...resultChars !== void 0 ? { resultChars } : {},
|
|
10010
9773
|
mcpServer,
|
|
10011
9774
|
phase,
|
|
10012
9775
|
seq,
|
|
@@ -10048,12 +9811,9 @@ var TurnCommitter = class {
|
|
|
10048
9811
|
// Renders each `TurnEvent` onto the pump's callback sink (tool-log truncation),
|
|
10049
9812
|
// routing a throwing commit to `onError`.
|
|
10050
9813
|
emit;
|
|
10051
|
-
//
|
|
10052
|
-
//
|
|
10053
|
-
//
|
|
10054
|
-
// POST) is routed to the error hook and swallowed — a failed commit is
|
|
10055
|
-
// warn-only and must never fail the turn (the role the classification's
|
|
10056
|
-
// `safeEmit` used to play before the adapter owned the decode).
|
|
9814
|
+
// `text` / `tool` / `thinking` flow through the pump; `session` / `result` are
|
|
9815
|
+
// the dispatcher's and ignored here. A throwing commit is routed to the error
|
|
9816
|
+
// hook and swallowed: a failed commit must never fail the turn.
|
|
10057
9817
|
async ingestEvent(event) {
|
|
10058
9818
|
if (event.type === "session" || event.type === "result") return;
|
|
10059
9819
|
try {
|
|
@@ -10232,18 +9992,15 @@ var TurnExecution = class {
|
|
|
10232
9992
|
turnLog;
|
|
10233
9993
|
abortController = new AbortController();
|
|
10234
9994
|
outcome = initialOutcome();
|
|
10235
|
-
//
|
|
10236
|
-
//
|
|
10237
|
-
// startup failure has nothing to reap, and reaping is a no-op rather than a
|
|
10238
|
-
// special case.
|
|
9995
|
+
// This turn's shell containment, once the claude-code adapter is built. Null
|
|
9996
|
+
// before that, so a startup failure's reap is a no-op.
|
|
10239
9997
|
containment = null;
|
|
10240
9998
|
seqCounter = 0;
|
|
10241
|
-
//
|
|
10242
|
-
//
|
|
10243
|
-
// CT1380: 0 for a NEW turn only — a resume is seeded in `fetchContext` first.
|
|
9999
|
+
// Per-turn monotonic seq. Starts at 0 for a NEW turn only; a resume is seeded
|
|
10000
|
+
// in `fetchContext` first.
|
|
10244
10001
|
nextSeq = () => ++this.seqCounter;
|
|
10245
|
-
//
|
|
10246
|
-
//
|
|
10002
|
+
// A REPLAY, told to us by the supervisor — not derivable here, since `turnId`
|
|
10003
|
+
// is set on every dispatch. `resumedFromSeq` is what it seeded from.
|
|
10247
10004
|
resumed;
|
|
10248
10005
|
resumedFromSeq;
|
|
10249
10006
|
// Whether the span this resumed run continues already has first words —
|
|
@@ -10251,8 +10008,8 @@ var TurnExecution = class {
|
|
|
10251
10008
|
resumedSpoke = false;
|
|
10252
10009
|
turnContext;
|
|
10253
10010
|
resolvedMcpServers;
|
|
10254
|
-
//
|
|
10255
|
-
//
|
|
10011
|
+
// The harness's secret environment for this turn. Only ever handed to the
|
|
10012
|
+
// adapter; never logged, never merged into `process.env`.
|
|
10256
10013
|
secretEnv = {};
|
|
10257
10014
|
effectiveCwd;
|
|
10258
10015
|
hookEnv;
|
|
@@ -10266,19 +10023,15 @@ var TurnExecution = class {
|
|
|
10266
10023
|
turnControlIntentFetched = false;
|
|
10267
10024
|
applyRecordedTurnControlIntent = async () => {
|
|
10268
10025
|
};
|
|
10269
|
-
//
|
|
10270
|
-
//
|
|
10271
|
-
//
|
|
10272
|
-
// loop, which runs after.
|
|
10026
|
+
// The committer is built before the lease watchdog arms, so the failed-commit
|
|
10027
|
+
// hint reaches it through this slot. A no-op until then is safe: the committer
|
|
10028
|
+
// only fires it from inside the adapter loop, which runs after.
|
|
10273
10029
|
noteCommitFailed = () => {
|
|
10274
10030
|
};
|
|
10275
10031
|
disarmWatchdogs = () => {
|
|
10276
10032
|
};
|
|
10277
|
-
//
|
|
10278
|
-
//
|
|
10279
|
-
// swallowed, because a turn that produced a good answer must not be recorded as
|
|
10280
|
-
// failed on account of a leftover process, and the failure is visible in the log
|
|
10281
|
-
// either way.
|
|
10033
|
+
// Idempotent, and a failed reap is logged and swallowed: a turn that produced a
|
|
10034
|
+
// good answer must not be recorded as failed on account of a leftover process.
|
|
10282
10035
|
async reapContainment() {
|
|
10283
10036
|
const containment = this.containment;
|
|
10284
10037
|
if (!containment) return;
|
|
@@ -10304,20 +10057,18 @@ var TurnExecution = class {
|
|
|
10304
10057
|
);
|
|
10305
10058
|
}
|
|
10306
10059
|
}
|
|
10307
|
-
//
|
|
10308
|
-
//
|
|
10309
|
-
// so every exit after this point must settle THE SPAN, not just clear the
|
|
10060
|
+
// Set the moment `acquireLease`'s PATCH returns: this is the pair's running
|
|
10061
|
+
// span, so every exit after this must settle THE SPAN, not just clear the
|
|
10310
10062
|
// participant flag.
|
|
10311
10063
|
admitted = false;
|
|
10312
|
-
//
|
|
10064
|
+
// One recompose per turn when the lease refuses a stale context.
|
|
10313
10065
|
recomposedForLease = false;
|
|
10314
|
-
//
|
|
10315
|
-
//
|
|
10316
|
-
//
|
|
10317
|
-
//
|
|
10318
|
-
// only before the first adoption; no work runs until it is set.
|
|
10066
|
+
// The execution source the CURRENT context was adopted as. A
|
|
10067
|
+
// `dispatch_context_stale` recompose reassigns it, so each piece of work
|
|
10068
|
+
// (preparation, the committer, a notice) captures it when it begins and keeps
|
|
10069
|
+
// that source. No work runs before it is set.
|
|
10319
10070
|
executionSourceId;
|
|
10320
|
-
//
|
|
10071
|
+
// One fresh fetch per turn when adoption is refused.
|
|
10321
10072
|
refetchedForAdoption = false;
|
|
10322
10073
|
concluded(reason, errorReason) {
|
|
10323
10074
|
return new TurnConcluded(reason, errorReason);
|
|
@@ -10385,21 +10136,18 @@ var TurnExecution = class {
|
|
|
10385
10136
|
this.supervisor.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
10386
10137
|
return { ok: false, durationMs, reason, runtime: this.turnContext?.runtime };
|
|
10387
10138
|
}
|
|
10388
|
-
// The
|
|
10389
|
-
//
|
|
10390
|
-
//
|
|
10391
|
-
//
|
|
10392
|
-
//
|
|
10393
|
-
// rule: a failure names its reason; without one the turn settled — the
|
|
10394
|
-
// runtime-unavailable exit posts its user-facing `final` before concluding,
|
|
10395
|
-
// so the turn genuinely produced its terminal output.
|
|
10139
|
+
// The admitted turn holds the conversation's lease, so the clear must NAME the
|
|
10140
|
+
// turn and DECLARE its outcome; a bare clear resets the participant flag and
|
|
10141
|
+
// leaves the span running until the lost sweep. A failure names its reason;
|
|
10142
|
+
// without one the turn settled (the runtime-unavailable exit has already posted
|
|
10143
|
+
// its `final`).
|
|
10396
10144
|
async concludeAdmittedRun(reason, errorReason) {
|
|
10397
10145
|
const { payload, turnLog, startedAt, turnId } = this;
|
|
10398
10146
|
const body = {
|
|
10399
10147
|
activeRunStartedAt: null,
|
|
10400
10148
|
turnId,
|
|
10401
|
-
//
|
|
10402
|
-
//
|
|
10149
|
+
// `runtimeIncomplete` is a real failure that deliberately carries no
|
|
10150
|
+
// `errorReason`, so the inference alone would read it as settled.
|
|
10403
10151
|
outcome: errorReason || this.outcome.runtimeIncomplete ? "failed" : "settled"
|
|
10404
10152
|
};
|
|
10405
10153
|
if (errorReason) body.errorReason = errorReason.slice(0, 200);
|
|
@@ -10493,11 +10241,10 @@ var TurnExecution = class {
|
|
|
10493
10241
|
throw this.concluded(reason, reason);
|
|
10494
10242
|
}
|
|
10495
10243
|
}
|
|
10496
|
-
//
|
|
10497
|
-
//
|
|
10498
|
-
//
|
|
10499
|
-
//
|
|
10500
|
-
// nothing to adopt any more — earns a fresh fetch.
|
|
10244
|
+
// Register this context's issuance as the execution source of the work about to
|
|
10245
|
+
// run on it. No issuance ends the turn as a failed adoption does: this companion
|
|
10246
|
+
// never commits a row whose provenance it cannot name. Only a refusal earns a
|
|
10247
|
+
// fresh fetch.
|
|
10501
10248
|
async adoptSource(context) {
|
|
10502
10249
|
const { payload, turnId, turnLog } = this;
|
|
10503
10250
|
if (!context.issuanceId) {
|
|
@@ -10546,9 +10293,8 @@ var TurnExecution = class {
|
|
|
10546
10293
|
secretEnv,
|
|
10547
10294
|
missing
|
|
10548
10295
|
} = resolveMcpSecrets(
|
|
10549
|
-
//
|
|
10550
|
-
//
|
|
10551
|
-
// the room connections' entries, whose values arrive in `secretEnv`.
|
|
10296
|
+
// User MCP definitions in placeholder form, plus the room connections'
|
|
10297
|
+
// entries, whose values arrive in `secretEnv`.
|
|
10552
10298
|
this.turnContext.mcpServers ?? {},
|
|
10553
10299
|
secretStore,
|
|
10554
10300
|
this.turnContext.secretEnv
|
|
@@ -10661,15 +10407,12 @@ var TurnExecution = class {
|
|
|
10661
10407
|
agentUsername: this.opts.agentUsername,
|
|
10662
10408
|
runtime: this.turnContext.runtime,
|
|
10663
10409
|
hostAccess: this.turnContext.policy.hostFs,
|
|
10664
|
-
//
|
|
10665
|
-
//
|
|
10666
|
-
// an older API. The conversation anchor is gone (CT319).
|
|
10410
|
+
// The trigger message's referenced-entry paths, which a prepare hook
|
|
10411
|
+
// may key its per-task env off. `[]` from an older API.
|
|
10667
10412
|
triggerEntryPaths: this.turnContext.conversation.triggerEntryPaths ?? [],
|
|
10668
10413
|
title: this.turnContext.conversation.title,
|
|
10669
|
-
//
|
|
10670
|
-
//
|
|
10671
|
-
// context (for the live feed); this is the first thing to read it as
|
|
10672
|
-
// an INPUT, so a dispatch can ask for its environment in words.
|
|
10414
|
+
// Where an `env:` directive rides, so a dispatch can ask for its
|
|
10415
|
+
// environment in words.
|
|
10673
10416
|
messageBody: this.turnContext.message.body
|
|
10674
10417
|
},
|
|
10675
10418
|
{ turnId, deviceId: this.opts.deviceId },
|
|
@@ -10715,37 +10458,30 @@ var TurnExecution = class {
|
|
|
10715
10458
|
payload.agentId,
|
|
10716
10459
|
{
|
|
10717
10460
|
activeRunStartedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10718
|
-
//
|
|
10719
|
-
//
|
|
10461
|
+
// A runner leases under its launcher's boot id, the one the device
|
|
10462
|
+
// heartbeats, or the lost-run sweep reads it as a restart.
|
|
10720
10463
|
bootId: this.opts.inRunner?.bootId ?? bootId,
|
|
10721
|
-
// CT1680: the runner holding the span, beside the device's boot id.
|
|
10722
10464
|
...this.opts.inRunner ? { runnerId: this.opts.inRunner.runnerId } : {},
|
|
10723
|
-
//
|
|
10465
|
+
// The durable record that this turn resumed, and from where.
|
|
10724
10466
|
...this.resumedFromSeq !== void 0 ? { resumedFromSeq: this.resumedFromSeq } : {},
|
|
10725
|
-
//
|
|
10726
|
-
//
|
|
10727
|
-
//
|
|
10728
|
-
//
|
|
10729
|
-
// fresh-session recompose (`freshRequest`) deliberately does not touch it:
|
|
10730
|
-
// it adopts only the prompt, never the run-config, so its issuance must
|
|
10731
|
-
// not become this attempt's evidence.
|
|
10467
|
+
// The issuance of the fetch that composed this request, so a
|
|
10468
|
+
// retransmitted start reuses the same record and a resumed attempt
|
|
10469
|
+
// echoes its own. `freshRequest` deliberately leaves it alone: it adopts
|
|
10470
|
+
// only the prompt, never the run-config.
|
|
10732
10471
|
...this.turnContext.issuanceId ? { issuanceId: this.turnContext.issuanceId } : {},
|
|
10733
|
-
//
|
|
10734
|
-
//
|
|
10735
|
-
// already emitted a "preparing" activity row for this turn above (it
|
|
10736
|
-
// runs before this flip), which would otherwise make the sweep mistake
|
|
10737
|
-
// this live turn for an abandoned one and close it with a `stopped`.
|
|
10472
|
+
// So `closeAbandonedTurns` excludes this turn: a "preparing" row emitted
|
|
10473
|
+
// before this flip would otherwise read as abandoned and be stopped.
|
|
10738
10474
|
turnId,
|
|
10739
10475
|
// The newest message this turn's read covered: the lease records it,
|
|
10740
10476
|
// and settle moves the cursor to it. Omitted when the server sent
|
|
10741
10477
|
// none (an older API), so the cursor stays where it was.
|
|
10742
10478
|
...this.turnContext.readThrough !== void 0 ? { readThrough: this.turnContext.readThrough } : {},
|
|
10743
10479
|
...this.turnContext.readRevision !== void 0 ? { readRevision: this.turnContext.readRevision } : {},
|
|
10744
|
-
//
|
|
10480
|
+
// The attempt this request's session belongs to (null = fresh).
|
|
10745
10481
|
...this.turnContext.attemptId !== void 0 ? { attemptId: this.turnContext.attemptId } : {},
|
|
10746
|
-
//
|
|
10747
|
-
//
|
|
10748
|
-
//
|
|
10482
|
+
// The frame this start acts on. The context fetch checked it before an
|
|
10483
|
+
// assembly this turn awaited; the lease re-checks where the stamp is
|
|
10484
|
+
// taken.
|
|
10749
10485
|
...payload.episode !== void 0 ? { dispatchMessageId: payload.messageId, dispatchEpisode: payload.episode } : {}
|
|
10750
10486
|
}
|
|
10751
10487
|
);
|
|
@@ -10785,21 +10521,18 @@ var TurnExecution = class {
|
|
|
10785
10521
|
this.request = buildCompanionTurnRequest({
|
|
10786
10522
|
turnContext: this.turnContext,
|
|
10787
10523
|
baseUrl: this.opts.baseUrl,
|
|
10788
|
-
//
|
|
10524
|
+
// Always present: `fetchContext` refuses a context without one.
|
|
10789
10525
|
turnToken: this.turnContext.turnToken,
|
|
10790
|
-
// SJ524: the hook-resolved cwd overrides the static local cwd.
|
|
10791
10526
|
...this.effectiveCwd ? { cwd: this.effectiveCwd } : {},
|
|
10792
|
-
// CT1103: the prepare-hook env plus the authoritative host-access token.
|
|
10793
10527
|
env: this.turnEnv,
|
|
10794
10528
|
mcpServers: this.resolvedMcpServers,
|
|
10795
10529
|
secretEnv: this.secretEnv,
|
|
10796
10530
|
turnControlServer,
|
|
10797
|
-
//
|
|
10798
|
-
//
|
|
10531
|
+
// Sent as the active-conversation header so a cross-conversation post
|
|
10532
|
+
// stamps its origin.
|
|
10799
10533
|
activeConversationId: payload.conversationId,
|
|
10800
|
-
// Workspace coordinate retained in the compatible TurnRequest envelope.
|
|
10801
10534
|
workspaceId,
|
|
10802
|
-
//
|
|
10535
|
+
// The operator's machine-local auto-memory escape hatch, when set.
|
|
10803
10536
|
...this.opts.local.claudeCode ? { claudeCode: this.opts.local.claudeCode } : {}
|
|
10804
10537
|
});
|
|
10805
10538
|
}
|
|
@@ -10836,8 +10569,8 @@ var TurnExecution = class {
|
|
|
10836
10569
|
if (this.opts.claudeCodeAvailable?.() ?? true) {
|
|
10837
10570
|
this.containment = createTurnContainment(this.turnId, {
|
|
10838
10571
|
log: (msg, meta) => turnLog.debug(meta ?? {}, msg),
|
|
10839
|
-
//
|
|
10840
|
-
//
|
|
10572
|
+
// A group outside the runner's own is reported, so a launcher can stop
|
|
10573
|
+
// it if the runner dies without reaping it.
|
|
10841
10574
|
...this.opts.inRunner?.onProcessGroup ? { onProcessGroup: this.opts.inRunner.onProcessGroup } : {}
|
|
10842
10575
|
});
|
|
10843
10576
|
const containment = this.containment;
|
|
@@ -10858,8 +10591,8 @@ var TurnExecution = class {
|
|
|
10858
10591
|
const serverUrl = this.opts.opencodeServerUrl;
|
|
10859
10592
|
adapters.push(
|
|
10860
10593
|
createOpencodeAdapter(
|
|
10861
|
-
//
|
|
10862
|
-
//
|
|
10594
|
+
// The transport's per-server turn mutex is per process; in a runner the
|
|
10595
|
+
// turn also holds the device-wide lock every runner shares.
|
|
10863
10596
|
this.opts.inRunner ? {
|
|
10864
10597
|
transport: this.opts.inRunner.opencodeTransport(serverUrl),
|
|
10865
10598
|
onWarn
|
|
@@ -10906,7 +10639,7 @@ var TurnExecution = class {
|
|
|
10906
10639
|
kind: "runtime_notice",
|
|
10907
10640
|
turnId,
|
|
10908
10641
|
parentMessageId: payload.messageId,
|
|
10909
|
-
//
|
|
10642
|
+
// Written by this attempt's source, like its speech.
|
|
10910
10643
|
...this.executionSourceId !== void 0 ? { executionSourceId: this.executionSourceId } : {}
|
|
10911
10644
|
});
|
|
10912
10645
|
} catch (postErr) {
|
|
@@ -10920,16 +10653,13 @@ var TurnExecution = class {
|
|
|
10920
10653
|
throw this.concluded(`runtime_unavailable:${err.runtime}`);
|
|
10921
10654
|
}
|
|
10922
10655
|
}
|
|
10923
|
-
// The adapter refused to resume
|
|
10924
|
-
//
|
|
10925
|
-
//
|
|
10926
|
-
//
|
|
10927
|
-
//
|
|
10928
|
-
//
|
|
10929
|
-
//
|
|
10930
|
-
// The lease already granted stands; the read reaches at least its mark. A
|
|
10931
|
-
// fetch that fails returns null and the adapter falls back to the request
|
|
10932
|
-
// it has, flagged degraded, so the server rewinds the cursor at settle.
|
|
10656
|
+
// The adapter refused to resume the stored session and is starting fresh. The
|
|
10657
|
+
// request it holds assumed the session had the conversation so far (only what
|
|
10658
|
+
// landed since the cursor, the agent's own words left out); a fresh session
|
|
10659
|
+
// has none of it. So refetch as a first turn on an empty session and hand the
|
|
10660
|
+
// adapter that, before it speaks or acts. The granted lease stands. A failed
|
|
10661
|
+
// fetch returns null: the adapter runs the request it has, flagged degraded,
|
|
10662
|
+
// and the server rewinds the cursor at settle.
|
|
10933
10663
|
async freshRequest(reason) {
|
|
10934
10664
|
const { payload, turnId, turnLog } = this;
|
|
10935
10665
|
try {
|
|
@@ -10940,9 +10670,8 @@ var TurnExecution = class {
|
|
|
10940
10670
|
false,
|
|
10941
10671
|
true,
|
|
10942
10672
|
void 0,
|
|
10943
|
-
//
|
|
10944
|
-
//
|
|
10945
|
-
// hold the turn open for the rest of the retry schedule.
|
|
10673
|
+
// The adapter is waiting on this, so a Stop must end the wait rather
|
|
10674
|
+
// than hold the turn open for the rest of the retry schedule.
|
|
10946
10675
|
this.abortController.signal
|
|
10947
10676
|
);
|
|
10948
10677
|
turnLog.debug(
|
|
@@ -10994,19 +10723,17 @@ var TurnExecution = class {
|
|
|
10994
10723
|
conversationId: payload.conversationId,
|
|
10995
10724
|
agentId: payload.agentId,
|
|
10996
10725
|
turnId,
|
|
10997
|
-
//
|
|
10998
|
-
//
|
|
10726
|
+
// The message this turn answers, on every commit so the reply records the
|
|
10727
|
+
// Q→A edge.
|
|
10999
10728
|
parentMessageId: payload.messageId,
|
|
11000
10729
|
signal: abortController.signal,
|
|
11001
10730
|
log: turnLog,
|
|
11002
10731
|
nextSeq: this.nextSeq,
|
|
11003
10732
|
spoke: this.resumedSpoke,
|
|
11004
|
-
//
|
|
10733
|
+
// Admitted by now, so this is the source the model's work runs on.
|
|
11005
10734
|
...this.executionSourceId !== void 0 ? { executionSourceId: this.executionSourceId } : {},
|
|
11006
|
-
//
|
|
11007
|
-
// `noteCommitFailed
|
|
11008
|
-
// arms (it starts as a no-op) — the committer only fires it from inside
|
|
11009
|
-
// the adapter loop, which runs after the watchdog has replaced it.
|
|
10735
|
+
// A commit that didn't land may mean the lease is gone; see
|
|
10736
|
+
// `noteCommitFailed`.
|
|
11010
10737
|
onCommitFailed: (err) => this.noteCommitFailed(err)
|
|
11011
10738
|
});
|
|
11012
10739
|
const usesHttpTurnControl = turnRuntime === "codex" || turnRuntime === "opencode";
|
|
@@ -11110,9 +10837,9 @@ var TurnExecution = class {
|
|
|
11110
10837
|
workspaceId,
|
|
11111
10838
|
payload.conversationId,
|
|
11112
10839
|
payload.agentId,
|
|
11113
|
-
//
|
|
11114
|
-
//
|
|
11115
|
-
//
|
|
10840
|
+
// Named for its turn, which a runner credential requires once
|
|
10841
|
+
// admitted; the server binds the capture to that turn's attempt
|
|
10842
|
+
// rather than the pair's.
|
|
11116
10843
|
{ agentSessionId: event.state, turnId }
|
|
11117
10844
|
);
|
|
11118
10845
|
} catch (err) {
|
|
@@ -11217,9 +10944,7 @@ var TurnExecution = class {
|
|
|
11217
10944
|
}
|
|
11218
10945
|
const body = {
|
|
11219
10946
|
activeRunStartedAt: null,
|
|
11220
|
-
//
|
|
11221
|
-
// off this turn id, so the settle PATCH must carry it — the start PATCH
|
|
11222
|
-
// isn't enough. Always sent (every settle closes a real turn).
|
|
10947
|
+
// The server keys the turn-end metadata (ended_at, tokens) off this id.
|
|
11223
10948
|
turnId
|
|
11224
10949
|
};
|
|
11225
10950
|
if (o.turnUsage) {
|
|
@@ -11387,11 +11112,11 @@ var Dispatcher = class {
|
|
|
11387
11112
|
this.aborts = opts.aborts ?? /* @__PURE__ */ new Map();
|
|
11388
11113
|
}
|
|
11389
11114
|
opts;
|
|
11390
|
-
//
|
|
11391
|
-
//
|
|
11115
|
+
// (conversation|agent) → per-TURN controllers. Stop is pair-scoped and must
|
|
11116
|
+
// reach every live loop for the pair.
|
|
11392
11117
|
aborts;
|
|
11393
|
-
//
|
|
11394
|
-
//
|
|
11118
|
+
// Pairs already told their session couldn't be saved: the notice is once per
|
|
11119
|
+
// pair, while the rejection repeats every turn.
|
|
11395
11120
|
sessionWriteNotified = /* @__PURE__ */ new Set();
|
|
11396
11121
|
notifyStart(info) {
|
|
11397
11122
|
try {
|
|
@@ -11408,11 +11133,6 @@ var Dispatcher = class {
|
|
|
11408
11133
|
// The supervisor supplies a stable turnId on both fresh and resumed dispatch.
|
|
11409
11134
|
// resumed is separate: only replay seeds sequence/speech recovery from the
|
|
11410
11135
|
// prior span. A direct caller omitting turnId gets a fresh id.
|
|
11411
|
-
//
|
|
11412
|
-
// CT1261: a short `handle` creates a `TurnExecution` and runs it. The
|
|
11413
|
-
// supervisor seam hands the execution exactly the three cross-turn duties —
|
|
11414
|
-
// the abort registry, the session-write notice latch, the observer — and
|
|
11415
|
-
// nothing else.
|
|
11416
11136
|
async handle(payload, opts = {}) {
|
|
11417
11137
|
const key = runKey(payload.conversationId, payload.agentId);
|
|
11418
11138
|
const supervisor = {
|
|
@@ -11458,8 +11178,8 @@ var Dispatcher = class {
|
|
|
11458
11178
|
}
|
|
11459
11179
|
return new TurnExecution(this.opts, supervisor, payload, opts).run();
|
|
11460
11180
|
}
|
|
11461
|
-
//
|
|
11462
|
-
//
|
|
11181
|
+
// Cancel a (conversation, agent) run in flight in THIS process. True if one
|
|
11182
|
+
// was aborted.
|
|
11463
11183
|
cancel(conversationId, agentId) {
|
|
11464
11184
|
const key = runKey(conversationId, agentId);
|
|
11465
11185
|
const pair = this.aborts.get(key);
|
|
@@ -13156,7 +12876,7 @@ var Outbox = class {
|
|
|
13156
12876
|
}
|
|
13157
12877
|
workspaceId;
|
|
13158
12878
|
log;
|
|
13159
|
-
// Resolved lazily (per call
|
|
12879
|
+
// Resolved lazily (per call) so tests that swap HOME between
|
|
13160
12880
|
// cases route writes at the right tmpdir.
|
|
13161
12881
|
dir() {
|
|
13162
12882
|
return join20(runnerStateDir(), "outbox", encodeURIComponent(this.workspaceId));
|
|
@@ -13222,7 +12942,7 @@ var Outbox = class {
|
|
|
13222
12942
|
);
|
|
13223
12943
|
return entries;
|
|
13224
12944
|
}
|
|
13225
|
-
//
|
|
12945
|
+
// The highest `seq` this turn has queued but not yet delivered — the
|
|
13226
12946
|
// half of a resumed turn's seq floor the SERVER CANNOT SEE. A commit that
|
|
13227
12947
|
// failed transiently before the crash is sitting in this directory with its
|
|
13228
12948
|
// seq already spent, and the restart's drain is fire-and-forget
|
|
@@ -13446,24 +13166,20 @@ var CompanionSupervisor = class {
|
|
|
13446
13166
|
config;
|
|
13447
13167
|
log;
|
|
13448
13168
|
hub;
|
|
13449
|
-
//
|
|
13450
|
-
//
|
|
13451
|
-
//
|
|
13452
|
-
// first harness refresh simply resolves now.
|
|
13169
|
+
// The execution-presence half of both routing gates. Synchronous and cheap (a
|
|
13170
|
+
// module resolve plus a stat), so a caller before the first harness refresh
|
|
13171
|
+
// simply resolves now.
|
|
13453
13172
|
resolveBundled;
|
|
13454
|
-
//
|
|
13455
|
-
//
|
|
13456
|
-
// every 30s beat (the CT484 latch pattern, per runtime).
|
|
13173
|
+
// The last bundled status LOGGED per harness, so a missing binary is reported
|
|
13174
|
+
// at startup and on each healthy↔missing transition, not every beat.
|
|
13457
13175
|
loggedBundledStatus = /* @__PURE__ */ new Map();
|
|
13458
|
-
//
|
|
13459
|
-
//
|
|
13460
|
-
// it, so a harness updated mid-run reports its new version without a restart.
|
|
13176
|
+
// Refreshed by the per-heartbeat re-probe, so a harness updated mid-run
|
|
13177
|
+
// reports its new version without a restart.
|
|
13461
13178
|
harnessVersions;
|
|
13462
|
-
//
|
|
13463
|
-
//
|
|
13464
|
-
//
|
|
13465
|
-
//
|
|
13466
|
-
// probe (the heartbeat then falls back to the boot `claudeCode`).
|
|
13179
|
+
// The latest harness signals (presence, reachability), probed on the heartbeat
|
|
13180
|
+
// cadence and on demand. ONE source for both the manifest and the Harnesses
|
|
13181
|
+
// surface, so the UI never disagrees with what is advertised. Null until the
|
|
13182
|
+
// first probe.
|
|
13467
13183
|
harnessSignals = null;
|
|
13468
13184
|
// Fresh per attempt, never the cached heartbeat signal: installing a harness
|
|
13469
13185
|
// between attempts must be observed immediately.
|
|
@@ -13476,8 +13192,8 @@ var CompanionSupervisor = class {
|
|
|
13476
13192
|
unrunnableRetryDelaysMs;
|
|
13477
13193
|
deviceApi = null;
|
|
13478
13194
|
deviceSub = null;
|
|
13479
|
-
//
|
|
13480
|
-
//
|
|
13195
|
+
// Whether the device stream is open, which is what every workspace's link on
|
|
13196
|
+
// the dashboard reports.
|
|
13481
13197
|
deviceConnected = false;
|
|
13482
13198
|
recoveryChecked = false;
|
|
13483
13199
|
recovering = false;
|
|
@@ -13489,8 +13205,7 @@ var CompanionSupervisor = class {
|
|
|
13489
13205
|
pollTimer = null;
|
|
13490
13206
|
refreshing = false;
|
|
13491
13207
|
stopped = false;
|
|
13492
|
-
//
|
|
13493
|
-
// on every 30s heartbeat.
|
|
13208
|
+
// The version-skew warning is logged once, not every heartbeat.
|
|
13494
13209
|
versionSkewWarned = false;
|
|
13495
13210
|
// This device's id, captured from the heartbeat / assignments response. The SSE
|
|
13496
13211
|
// router uses it to tell a dispatch addressed to THIS device (a freshly-assigned
|
|
@@ -13500,12 +13215,12 @@ var CompanionSupervisor = class {
|
|
|
13500
13215
|
// The in-flight assignments pull, exposed so a caller that needs fresh data
|
|
13501
13216
|
// (pull-on-miss) can `await` the running pull rather than racing past it.
|
|
13502
13217
|
inFlightRefresh = null;
|
|
13503
|
-
//
|
|
13504
|
-
//
|
|
13505
|
-
//
|
|
13218
|
+
// Device-global connector-health memory: every dispatcher records its turn's
|
|
13219
|
+
// classified outcome per runtime; the heartbeat echoes it so the server's
|
|
13220
|
+
// quota state survives a restart or race.
|
|
13506
13221
|
connectorHealth = new ConnectorHealthStore();
|
|
13507
|
-
//
|
|
13508
|
-
//
|
|
13222
|
+
// Which dispatches this process is running, so a redelivered frame doesn't
|
|
13223
|
+
// run the turn a second time.
|
|
13509
13224
|
claims = new DispatchClaims();
|
|
13510
13225
|
constructor(opts) {
|
|
13511
13226
|
this.config = opts.config;
|
|
@@ -13526,10 +13241,10 @@ var CompanionSupervisor = class {
|
|
|
13526
13241
|
}
|
|
13527
13242
|
this.latchBootBundledState();
|
|
13528
13243
|
}
|
|
13529
|
-
//
|
|
13530
|
-
//
|
|
13244
|
+
// What a runner container said it can run, probed once at start; null until
|
|
13245
|
+
// then, and when no container could be started.
|
|
13531
13246
|
runnerImage = null;
|
|
13532
|
-
//
|
|
13247
|
+
// The companion every container mounts: this one's own install.
|
|
13533
13248
|
runnerCompanion() {
|
|
13534
13249
|
return companionSource({
|
|
13535
13250
|
configured: this.config.runners?.docker?.companionDir,
|
|
@@ -13641,10 +13356,7 @@ var CompanionSupervisor = class {
|
|
|
13641
13356
|
this.pollTimer = setInterval(() => void this.refreshAssignments(), ASSIGNMENTS_POLL_MS);
|
|
13642
13357
|
this.pollTimer.unref?.();
|
|
13643
13358
|
}
|
|
13644
|
-
//
|
|
13645
|
-
// keep the foreground process alive. With the poll loop running there's always
|
|
13646
|
-
// at least the loops keeping the event loop alive; the SSE promises let a
|
|
13647
|
-
// clean teardown resolve.
|
|
13359
|
+
// start.ts awaits these; they resolve on a clean teardown.
|
|
13648
13360
|
finishedPromises() {
|
|
13649
13361
|
return [...this.deviceSub ? [this.deviceSub.finished] : []];
|
|
13650
13362
|
}
|
|
@@ -13672,39 +13384,26 @@ var CompanionSupervisor = class {
|
|
|
13672
13384
|
bootId,
|
|
13673
13385
|
version: COMPANION_VERSION,
|
|
13674
13386
|
exposedSecretNames: store.names(),
|
|
13675
|
-
//
|
|
13676
|
-
// CT1379 claude-code and codex when the SDK's own bundled binary is there
|
|
13677
|
-
// and the user asked for the harness, CT270 opencode when the operator
|
|
13678
|
-
// configured an `opencode serve`.
|
|
13387
|
+
// Each runtime only when this device can actually run it.
|
|
13679
13388
|
manifest: buildCompanionManifest({
|
|
13680
|
-
//
|
|
13681
|
-
//
|
|
13682
|
-
// the user's `claudeCode.enabled`. Claude Code rides neither alone.
|
|
13389
|
+
// Consented (`claudeCode.enabled`) AND launchable (the live bundled
|
|
13390
|
+
// binary). Neither alone.
|
|
13683
13391
|
claudeCode: this.claudeCodeOffered(),
|
|
13684
13392
|
opencode: !!this.config.opencode?.serverUrl,
|
|
13685
|
-
//
|
|
13686
|
-
//
|
|
13687
|
-
// which is how a device with a half-installed 331 MB platform package
|
|
13688
|
-
// advertised a runtime that could not start.
|
|
13393
|
+
// Enabled AND its vendored binary present: a half-installed 331 MB
|
|
13394
|
+
// platform package otherwise advertises a runtime that cannot start.
|
|
13689
13395
|
codex: this.codexOffered(),
|
|
13690
|
-
//
|
|
13691
|
-
// above. Diagnostic only — the server never routes on it — and always
|
|
13396
|
+
// WHY an asked-for harness is absent above. Diagnostic only, and always
|
|
13692
13397
|
// sent, so an empty array clears a previously reported issue.
|
|
13693
13398
|
runtimeIssues: this.runtimeIssues(),
|
|
13694
|
-
//
|
|
13695
|
-
// (fail-soft to null). Informational only — the server matches on name.
|
|
13399
|
+
// Informational only; the server matches on name.
|
|
13696
13400
|
versions: this.harnessVersions,
|
|
13697
|
-
//
|
|
13401
|
+
// Shown on the device page.
|
|
13698
13402
|
...runnersEnabled(this.config) ? { runner: this.runnerManifest() } : {}
|
|
13699
13403
|
}),
|
|
13700
|
-
//
|
|
13701
|
-
//
|
|
13702
|
-
// failure/heal, so a fresh companion's beat is unchanged and the server's
|
|
13703
|
-
// manifest synthesis (status-less rows) still runs.
|
|
13404
|
+
// Omitted until the first observed failure or heal, so the server's
|
|
13405
|
+
// status-less manifest synthesis still runs for a fresh companion.
|
|
13704
13406
|
...connectorReports.length > 0 ? { connectors: connectorReports } : {},
|
|
13705
|
-
// CT584: include enumerated models only when the probe SUCCEEDED (non-null).
|
|
13706
|
-
// A null (probe failed / no opencode) omits the field, and the server then
|
|
13707
|
-
// leaves this device's stored availability untouched.
|
|
13708
13407
|
...opencodeModels !== null ? { models: opencodeModels } : {}
|
|
13709
13408
|
});
|
|
13710
13409
|
this.hub.setDevice({ deviceId: res.deviceId });
|
|
@@ -13719,11 +13418,8 @@ var CompanionSupervisor = class {
|
|
|
13719
13418
|
);
|
|
13720
13419
|
}
|
|
13721
13420
|
}
|
|
13722
|
-
//
|
|
13723
|
-
//
|
|
13724
|
-
// companion's, warn loudly (once). This is exactly the skew that bit the M1 walk:
|
|
13725
|
-
// an npm-pinned companion lagging a from-develop server. The elimination (bundled
|
|
13726
|
-
// companion) makes it match by construction; this catches a companion run out of band.
|
|
13421
|
+
// Warn once when the server's build version differs from this companion's: an
|
|
13422
|
+
// npm-pinned companion lagging the server is the usual cause of odd failures.
|
|
13727
13423
|
checkVersionSkew(serverVersion) {
|
|
13728
13424
|
if (this.versionSkewWarned) return;
|
|
13729
13425
|
if (!serverVersion || serverVersion === COMPANION_VERSION) return;
|
|
@@ -13920,10 +13616,9 @@ var CompanionSupervisor = class {
|
|
|
13920
13616
|
`Stopped running ${runner.displayName} (unassigned)`
|
|
13921
13617
|
);
|
|
13922
13618
|
}
|
|
13923
|
-
//
|
|
13924
|
-
//
|
|
13925
|
-
//
|
|
13926
|
-
// the caller's half.
|
|
13619
|
+
// Can this machine LAUNCH the harness — is the SDK's bundled executable there?
|
|
13620
|
+
// The per-beat result, or resolved now before the first refresh. Execution
|
|
13621
|
+
// presence ONLY; consent is the caller's half.
|
|
13927
13622
|
bundledStatus(runtime) {
|
|
13928
13623
|
const cached2 = runtime === "codex" ? this.harnessSignals?.codexBundled : this.harnessSignals?.claudeBundled;
|
|
13929
13624
|
return cached2 ?? this.resolveBundled(runtime).status;
|
|
@@ -13934,23 +13629,19 @@ var CompanionSupervisor = class {
|
|
|
13934
13629
|
bundledResolution(runtime) {
|
|
13935
13630
|
return this.resolveBundled(runtime);
|
|
13936
13631
|
}
|
|
13937
|
-
//
|
|
13938
|
-
//
|
|
13939
|
-
//
|
|
13940
|
-
//
|
|
13941
|
-
// consent in front of it and the real executable underneath it).
|
|
13632
|
+
// Does this device OFFER claude-code — connected by its user, and able to start
|
|
13633
|
+
// the binary a turn runs? The ONE signal both the heartbeat manifest and the
|
|
13634
|
+
// adapter registry read, so what the device advertises and what it can select
|
|
13635
|
+
// can't disagree.
|
|
13942
13636
|
claudeCodeOffered() {
|
|
13943
13637
|
return isClaudeCodeConnected(this.config) && this.bundledStatus("claude-code") === "present";
|
|
13944
13638
|
}
|
|
13945
|
-
//
|
|
13946
|
-
// alone — so an enabled device with no vendored binary advertised a runtime it
|
|
13947
|
-
// could not start. Same shape as Claude Code, same predicate on both sides.
|
|
13639
|
+
// The same question for codex: enabled AND its vendored binary present.
|
|
13948
13640
|
codexOffered() {
|
|
13949
13641
|
return isCodexEnabled(this.config) && this.bundledStatus("codex") === "present";
|
|
13950
13642
|
}
|
|
13951
|
-
//
|
|
13952
|
-
//
|
|
13953
|
-
// for one harness.
|
|
13643
|
+
// Derived from the same two facts as the predicates above, so an issue and an
|
|
13644
|
+
// advertisement are never both made for one harness.
|
|
13954
13645
|
runtimeIssues() {
|
|
13955
13646
|
return runtimeIssuesFor({
|
|
13956
13647
|
claudeCode: {
|
|
@@ -13960,26 +13651,16 @@ var CompanionSupervisor = class {
|
|
|
13960
13651
|
codex: { intended: isCodexEnabled(this.config), bundled: this.bundledStatus("codex") }
|
|
13961
13652
|
});
|
|
13962
13653
|
}
|
|
13963
|
-
//
|
|
13964
|
-
//
|
|
13965
|
-
//
|
|
13966
|
-
// repro and carries nothing that could be a credential or a vendor's output.
|
|
13967
|
-
//
|
|
13968
|
-
// Two things this deliberately does NOT do (Codo's review, blocking finding #2):
|
|
13654
|
+
// A missing binary is reported ONCE, and again only when the answer CHANGES,
|
|
13655
|
+
// with runtime, platform/arch and status: enough to find it in `companion.log`
|
|
13656
|
+
// without a repro, and nothing that could be a credential or vendor output.
|
|
13969
13657
|
//
|
|
13970
|
-
// -
|
|
13971
|
-
//
|
|
13972
|
-
//
|
|
13973
|
-
//
|
|
13974
|
-
//
|
|
13975
|
-
//
|
|
13976
|
-
// debug record instead, present for diagnosis without competing for
|
|
13977
|
-
// attention with the warning the operator already read.
|
|
13978
|
-
// - It does not report a transition in generic prose. A binary that goes
|
|
13979
|
-
// missing at 3am is the case the operator will actually meet, and a line
|
|
13980
|
-
// that says only "binary is missing" leaves them with no repair and no
|
|
13981
|
-
// restart instruction. Transitions carry the specified copy verbatim — the
|
|
13982
|
-
// same sentence the startup path prints.
|
|
13658
|
+
// - The boot state is not re-announced: `warnAboutHarnessReadiness` said it
|
|
13659
|
+
// on stderr at startup, when a person is watching (the `--daemon` launcher
|
|
13660
|
+
// runs that preflight in the FOREGROUND because the child logs to a file).
|
|
13661
|
+
// It is latched in the constructor and recorded at debug.
|
|
13662
|
+
// - A transition carries the same repair copy the startup path prints, not
|
|
13663
|
+
// generic prose: a binary missing at 3am needs the repair and the restart.
|
|
13983
13664
|
logBundledTransitions() {
|
|
13984
13665
|
for (const runtime of ["claude-code", "codex"]) {
|
|
13985
13666
|
const intended = runtime === "codex" ? isCodexEnabled(this.config) : isClaudeCodeConnected(this.config);
|
|
@@ -14002,9 +13683,8 @@ var CompanionSupervisor = class {
|
|
|
14002
13683
|
this.log.debug(meta, "companion: bundled harness binary is missing (harness not in use)");
|
|
14003
13684
|
}
|
|
14004
13685
|
}
|
|
14005
|
-
//
|
|
14006
|
-
//
|
|
14007
|
-
// lookup plus a stat, so it costs nothing and needs no await.
|
|
13686
|
+
// The boot state, latched so the first refresh reports no news, and recorded
|
|
13687
|
+
// once as evidence. Synchronous: a module lookup plus a stat.
|
|
14008
13688
|
latchBootBundledState() {
|
|
14009
13689
|
for (const runtime of ["claude-code", "codex"]) {
|
|
14010
13690
|
const resolution = this.resolveBundled(runtime);
|
|
@@ -14022,35 +13702,34 @@ var CompanionSupervisor = class {
|
|
|
14022
13702
|
);
|
|
14023
13703
|
}
|
|
14024
13704
|
}
|
|
14025
|
-
//
|
|
13705
|
+
// The locks every runner on this device shares.
|
|
14026
13706
|
runnerLocks = new DeviceLocks();
|
|
14027
|
-
//
|
|
14028
|
-
//
|
|
13707
|
+
// `runners.sandbox`, checked against this host once. A host that can't make
|
|
13708
|
+
// the sandbox says so once and runs its turns unwrapped.
|
|
14029
13709
|
sandboxUsable;
|
|
14030
13710
|
sandboxHost = {
|
|
14031
13711
|
osSandboxUsable: () => this.sandboxUsable ??= osSandboxUsable(this.log)
|
|
14032
13712
|
};
|
|
14033
|
-
//
|
|
14034
|
-
//
|
|
13713
|
+
// `runners.policy`, parsed once. An invalid one is kept as the error, which
|
|
13714
|
+
// every sandboxed turn is refused with.
|
|
14035
13715
|
parsedSandboxPolicy;
|
|
14036
13716
|
sandboxPolicy() {
|
|
14037
13717
|
this.parsedSandboxPolicy ??= parseSandboxPolicy(this.config.runners?.policy);
|
|
14038
13718
|
return this.parsedSandboxPolicy;
|
|
14039
13719
|
}
|
|
14040
|
-
//
|
|
13720
|
+
// The walk for other companions' stores, run off the turn path.
|
|
14041
13721
|
storeWatch;
|
|
14042
13722
|
companionStores() {
|
|
14043
13723
|
this.storeWatch ??= companionStoreWatch(this.log);
|
|
14044
13724
|
return this.storeWatch;
|
|
14045
13725
|
}
|
|
14046
|
-
//
|
|
14047
|
-
//
|
|
14048
|
-
//
|
|
13726
|
+
// The sandbox one agent's turns run under: its `agents` row, then the
|
|
13727
|
+
// device's. Resolved from the current config on every ask, so a reload
|
|
13728
|
+
// changes it; only the host probe is kept.
|
|
14049
13729
|
runnerSandboxFor(row) {
|
|
14050
13730
|
return runnerSandboxFor(this.config, row, this.sandboxHost);
|
|
14051
13731
|
}
|
|
14052
|
-
//
|
|
14053
|
-
// against the agents it runs right now.
|
|
13732
|
+
// Resolved against the agents this device runs right now.
|
|
14054
13733
|
runnerManifest() {
|
|
14055
13734
|
const agents = [...this.workspaces.values()].flatMap(
|
|
14056
13735
|
(wr) => [...wr.agents.values()].map((agent) => ({
|
|
@@ -14064,7 +13743,7 @@ var CompanionSupervisor = class {
|
|
|
14064
13743
|
...runnerSandboxReport(this.config, agents, this.sandboxHost)
|
|
14065
13744
|
};
|
|
14066
13745
|
}
|
|
14067
|
-
//
|
|
13746
|
+
// One provider for the device, built on first use.
|
|
14068
13747
|
runnerProvider() {
|
|
14069
13748
|
this.providerForRunners ??= this.dockerRunners() ? this.createDockerRunners() : createLocalProcessProvider();
|
|
14070
13749
|
return this.providerForRunners;
|
|
@@ -14106,8 +13785,7 @@ var CompanionSupervisor = class {
|
|
|
14106
13785
|
log: this.log
|
|
14107
13786
|
});
|
|
14108
13787
|
}
|
|
14109
|
-
//
|
|
14110
|
-
// companion in a container, this process's otherwise.
|
|
13788
|
+
// The image's own companion in a container, this process's otherwise.
|
|
14111
13789
|
runnerCommandFor() {
|
|
14112
13790
|
if (this.runnerCommand) return this.runnerCommand;
|
|
14113
13791
|
return this.dockerRunners() ? ["cabane-companion", "run-turn"] : selfRunnerCommand();
|
|
@@ -14139,36 +13817,20 @@ var CompanionSupervisor = class {
|
|
|
14139
13817
|
aborts: ctx.aborts,
|
|
14140
13818
|
runConfig: ctx.runConfig,
|
|
14141
13819
|
log: this.log.child({ agentName: ctx.agentDisplayName ?? ctx.agentUsername }),
|
|
14142
|
-
//
|
|
14143
|
-
//
|
|
14144
|
-
//
|
|
14145
|
-
//
|
|
14146
|
-
// runnable, so a disconnected harness can't be selected either.
|
|
14147
|
-
// CT1379: and the same for the bundled binary — the predicate is shared with
|
|
14148
|
-
// the heartbeat above, so a runtime advertised by one and refused by the
|
|
14149
|
-
// other is not expressible.
|
|
13820
|
+
// Read PER TURN off the heartbeat's own predicates, so a harness installed
|
|
13821
|
+
// or connected after boot works on the next turn exactly as it appears on
|
|
13822
|
+
// the next beat, and a runtime advertised by one and refused by the other
|
|
13823
|
+
// is not expressible.
|
|
14150
13824
|
claudeCodeAvailable: () => this.claudeCodeOffered(),
|
|
14151
|
-
// CT270: the opencode server URL (operator-configured), when this device
|
|
14152
|
-
// offers the opencode runtime. Threaded so an opencode turn selects the
|
|
14153
|
-
// opencode adapter; unset leaves the device claude-code-only.
|
|
14154
13825
|
...this.config.opencode?.serverUrl ? { opencodeServerUrl: this.config.opencode.serverUrl } : {},
|
|
14155
|
-
// CT481: register the codex adapter when this device offers codex; a miss
|
|
14156
|
-
// leaves an `openai/…` turn to fail loudly (no silent claude-code fallback).
|
|
14157
|
-
// CT1379: read PER TURN, like claude-code above, and off the same predicate
|
|
14158
|
-
// the heartbeat uses — it was captured here at dispatcher-construction time
|
|
14159
|
-
// from the enable flag alone, so a device could register an adapter for a
|
|
14160
|
-
// binary it did not have and keep it registered after the binary came back.
|
|
14161
13826
|
codexAvailable: () => this.codexOffered(),
|
|
14162
|
-
//
|
|
14163
|
-
//
|
|
14164
|
-
//
|
|
14165
|
-
// override set here still wins, and is deliberately not validated against
|
|
14166
|
-
// the tool ceilings it may reorder — see DEFAULT_AGENT_IDLE_TIMEOUT_MS.
|
|
13827
|
+
// Unset uses the dispatcher's defaults. An override is deliberately not
|
|
13828
|
+
// validated against the tool ceilings it may reorder; see
|
|
13829
|
+
// DEFAULT_AGENT_IDLE_TIMEOUT_MS.
|
|
14167
13830
|
...positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") !== void 0 ? { idleTimeoutMs: positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") } : {},
|
|
14168
13831
|
...positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") !== void 0 ? { totalTimeoutMs: positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") } : {},
|
|
14169
13832
|
observer: this.hub.observerFor(ctx.workspaceId, ctx.agentId, ctx.workspaceSlug),
|
|
14170
13833
|
transcriptDir: transcriptsDir(),
|
|
14171
|
-
// CT566: the shared connector-health memory the turn records into on settle.
|
|
14172
13834
|
connectorHealth: this.connectorHealth,
|
|
14173
13835
|
...runnersEnabled(this.config) ? {
|
|
14174
13836
|
runner: {
|
|
@@ -14192,11 +13854,10 @@ var CompanionSupervisor = class {
|
|
|
14192
13854
|
this.workspaces.delete(workspaceId);
|
|
14193
13855
|
this.hub.removeWorkspace(workspaceId);
|
|
14194
13856
|
}
|
|
14195
|
-
// ---- SSE event routing (
|
|
14196
|
-
//
|
|
14197
|
-
//
|
|
14198
|
-
//
|
|
14199
|
-
// instead of resuming from a `Last-Event-ID`.
|
|
13857
|
+
// ---- SSE event routing (BY agentId) ----
|
|
13858
|
+
// Dispatch and cancel, addressed to this device. The stream keeps no cursor:
|
|
13859
|
+
// it replays what is still offered on every connect instead of resuming from
|
|
13860
|
+
// a `Last-Event-ID`.
|
|
14200
13861
|
async handleDeviceMessage(ev) {
|
|
14201
13862
|
const wire = this.parseFrame(ev);
|
|
14202
13863
|
if (!wire) return;
|
|
@@ -14232,9 +13893,9 @@ var CompanionSupervisor = class {
|
|
|
14232
13893
|
return null;
|
|
14233
13894
|
}
|
|
14234
13895
|
}
|
|
14235
|
-
//
|
|
14236
|
-
//
|
|
14237
|
-
//
|
|
13896
|
+
// A reconnect replays every dispatch still offered, including one whose turn
|
|
13897
|
+
// is already running here; the first copy runs it and a later one waits for
|
|
13898
|
+
// it and then does nothing.
|
|
14238
13899
|
//
|
|
14239
13900
|
// A cancel needs no gate: `dispatcher.cancel` on an already-aborted run is a
|
|
14240
13901
|
// no-op that reports `false`, so a second copy is inert.
|
|
@@ -14250,8 +13911,6 @@ var CompanionSupervisor = class {
|
|
|
14250
13911
|
if (claim) this.claims.release(wire.id);
|
|
14251
13912
|
}
|
|
14252
13913
|
}
|
|
14253
|
-
// Everything below here is the routing as it was: it takes a frame and knows
|
|
14254
|
-
// nothing about which stream carried it.
|
|
14255
13914
|
async routeFrame(wr, wire) {
|
|
14256
13915
|
if (wire.type === "device:cancel_requested") {
|
|
14257
13916
|
const payload2 = {
|
|
@@ -14317,17 +13976,12 @@ var CompanionSupervisor = class {
|
|
|
14317
13976
|
await tail;
|
|
14318
13977
|
if (wr.chains.get(chainKey) === tail) wr.chains.delete(chainKey);
|
|
14319
13978
|
}
|
|
14320
|
-
//
|
|
14321
|
-
//
|
|
13979
|
+
// Tell the server this device cannot run a dispatch it was addressed to.
|
|
14322
13980
|
// Returns whether the report LANDED. The server checks the recorded dispatch
|
|
14323
|
-
// destination even if the
|
|
14324
|
-
// explains the roster miss; the thirty-minute unanswered-offer deadline is
|
|
14325
|
-
// only the backstop when no report lands.
|
|
13981
|
+
// destination even if the assignment has moved.
|
|
14326
13982
|
//
|
|
14327
|
-
//
|
|
14328
|
-
//
|
|
14329
|
-
// hard — this runs inline on the event loop, and an unbounded wait here would
|
|
14330
|
-
// wedge the workspace's whole stream.
|
|
13983
|
+
// Retried, because the common failure is transient (a 503 mid-deploy), and
|
|
13984
|
+
// bounded hard: this runs inline, and an unbounded wait would wedge the stream.
|
|
14331
13985
|
async reportUnrunnableDispatch(workspaceId, payload) {
|
|
14332
13986
|
if (!this.deviceApi) return false;
|
|
14333
13987
|
let lastErr;
|
|
@@ -14367,9 +14021,8 @@ var CompanionSupervisor = class {
|
|
|
14367
14021
|
);
|
|
14368
14022
|
return false;
|
|
14369
14023
|
}
|
|
14370
|
-
//
|
|
14371
|
-
//
|
|
14372
|
-
// the one the launcher still holds whatever became of the runner.
|
|
14024
|
+
// A runner is gone without having settled its turn. Same bounded retry, on the
|
|
14025
|
+
// device credential, which the launcher holds whatever became of the runner.
|
|
14373
14026
|
async reportRunnerGone(report) {
|
|
14374
14027
|
if (!this.deviceApi) return;
|
|
14375
14028
|
let lastErr;
|
|
@@ -14394,12 +14047,9 @@ var CompanionSupervisor = class {
|
|
|
14394
14047
|
"Couldn't tell Cabane a turn's runner stopped; its credential expires on its own."
|
|
14395
14048
|
);
|
|
14396
14049
|
}
|
|
14397
|
-
// Pull-on-miss
|
|
14398
|
-
//
|
|
14399
|
-
//
|
|
14400
|
-
// and return the now-live runner. Returns undefined when the dispatch isn't for
|
|
14401
|
-
// this device, or the agent still isn't ours after pulling (a genuine
|
|
14402
|
-
// not-ours dispatch) — the caller then settles and ignores it as before.
|
|
14050
|
+
// Pull-on-miss: a dispatch addressed to THIS device for an agent we don't run
|
|
14051
|
+
// yet means the assignment changed under us. Undefined when it isn't for this
|
|
14052
|
+
// device, or the agent still isn't ours after the pull.
|
|
14403
14053
|
async recoverRacedAgent(wr, payload) {
|
|
14404
14054
|
if (!this.deviceId || payload.deviceId !== this.deviceId) return void 0;
|
|
14405
14055
|
if (this.refreshing && this.inFlightRefresh) await this.inFlightRefresh;
|
|
@@ -14440,9 +14090,8 @@ var CompanionSupervisor = class {
|
|
|
14440
14090
|
};
|
|
14441
14091
|
const tail = this.runDispatch(
|
|
14442
14092
|
wr,
|
|
14443
|
-
//
|
|
14444
|
-
//
|
|
14445
|
-
// path's own guard.
|
|
14093
|
+
// Off `/runs`, not a frame, so no dispatch id to dedupe on;
|
|
14094
|
+
// `recoveredTurns` is this path's guard.
|
|
14446
14095
|
"",
|
|
14447
14096
|
payload,
|
|
14448
14097
|
agent,
|
|
@@ -14464,9 +14113,7 @@ var CompanionSupervisor = class {
|
|
|
14464
14113
|
this.recovering = false;
|
|
14465
14114
|
}
|
|
14466
14115
|
}
|
|
14467
|
-
// One turn: the
|
|
14468
|
-
// the console summary. Mirrors the old per-workspace `handleEvent`, now
|
|
14469
|
-
// scoped to one agent.
|
|
14116
|
+
// One turn: the skip-vs-resume dedupe, the dispatch, then the console summary.
|
|
14470
14117
|
async runDispatch(wr, dispatchId, payload, agent, recoveredTurnId) {
|
|
14471
14118
|
const workspaceId = wr.workspaceId;
|
|
14472
14119
|
if (dispatchId && hasDispatched(workspaceId, dispatchId)) {
|
|
@@ -14551,7 +14198,7 @@ var CompanionSupervisor = class {
|
|
|
14551
14198
|
);
|
|
14552
14199
|
}
|
|
14553
14200
|
}
|
|
14554
|
-
// ---- per-agent outbox drain scheduler
|
|
14201
|
+
// ---- per-agent outbox drain scheduler ----
|
|
14555
14202
|
makeDrain(api, agentId) {
|
|
14556
14203
|
let drainTimer;
|
|
14557
14204
|
let drainDelay = DRAIN_BASE_MS;
|
|
@@ -14620,19 +14267,16 @@ var CompanionSupervisor = class {
|
|
|
14620
14267
|
saveConfig(next);
|
|
14621
14268
|
configureLogger(this.log, next);
|
|
14622
14269
|
}
|
|
14623
|
-
// ---- harnesses
|
|
14624
|
-
// Probe all three harnesses, cache
|
|
14625
|
-
//
|
|
14626
|
-
//
|
|
14627
|
-
// wrapped anyway so a harness refresh can never take a heartbeat down. Called on
|
|
14628
|
-
// the heartbeat cadence (fresh presence for the manifest + UI) and on demand.
|
|
14270
|
+
// ---- harnesses ----
|
|
14271
|
+
// Probe all three harnesses, cache signals and versions, and push the snapshot
|
|
14272
|
+
// to the hub. `probeHarnessSignals` never throws, but this is wrapped anyway so
|
|
14273
|
+
// a refresh can never take a heartbeat down.
|
|
14629
14274
|
async refreshHarnessStatuses(presence = {}) {
|
|
14630
14275
|
try {
|
|
14631
14276
|
const signals = await probeHarnessSignals(this.config, {
|
|
14632
14277
|
probePresence: this.probePresence,
|
|
14633
14278
|
presence,
|
|
14634
|
-
//
|
|
14635
|
-
// CLI probes, through the supervisor's seam so a test pins the host fact.
|
|
14279
|
+
// Through the supervisor's seam, so a test pins the host fact.
|
|
14636
14280
|
resolveBundled: this.resolveBundled
|
|
14637
14281
|
});
|
|
14638
14282
|
this.harnessSignals = signals;
|
|
@@ -14655,11 +14299,10 @@ var CompanionSupervisor = class {
|
|
|
14655
14299
|
async recheckHarnesses() {
|
|
14656
14300
|
await this.refreshHarnessStatuses();
|
|
14657
14301
|
}
|
|
14658
|
-
//
|
|
14659
|
-
//
|
|
14660
|
-
//
|
|
14661
|
-
//
|
|
14662
|
-
// an answer. Coalesces with an in-flight beat rather than stacking a second.
|
|
14302
|
+
// Beat NOW and wait for it to land. `start` calls this before asking the
|
|
14303
|
+
// person anything, so the browser's connect step already shows what this
|
|
14304
|
+
// machine has rather than sitting blank while the terminal waits on an answer.
|
|
14305
|
+
// Coalesces with an in-flight beat.
|
|
14663
14306
|
async heartbeatNow() {
|
|
14664
14307
|
if (this.inFlightHeartbeat) {
|
|
14665
14308
|
await this.inFlightHeartbeat;
|
|
@@ -14668,35 +14311,25 @@ var CompanionSupervisor = class {
|
|
|
14668
14311
|
this.kickHeartbeat();
|
|
14669
14312
|
if (this.inFlightHeartbeat) await this.inFlightHeartbeat;
|
|
14670
14313
|
}
|
|
14671
|
-
//
|
|
14672
|
-
//
|
|
14673
|
-
// against the URL/flag that was just persisted, not the one this process booted
|
|
14674
|
-
// with.
|
|
14314
|
+
// The config as it stands now, after any `enableHarness` write: the control
|
|
14315
|
+
// socket's shake-out check needs what was just persisted, not the boot config.
|
|
14675
14316
|
currentConfig() {
|
|
14676
14317
|
return this.config;
|
|
14677
14318
|
}
|
|
14678
|
-
//
|
|
14679
|
-
// `~/.cabane/config.json
|
|
14680
|
-
// never drives a login (BYO — Decided).
|
|
14319
|
+
// Enable a config-driven harness by flipping the flag the app owns in
|
|
14320
|
+
// `~/.cabane/config.json`. Never installs a binary or drives a login (BYO).
|
|
14681
14321
|
//
|
|
14682
|
-
//
|
|
14683
|
-
//
|
|
14684
|
-
//
|
|
14685
|
-
//
|
|
14686
|
-
//
|
|
14687
|
-
//
|
|
14688
|
-
// -
|
|
14689
|
-
//
|
|
14690
|
-
//
|
|
14691
|
-
//
|
|
14692
|
-
//
|
|
14693
|
-
// - opencode: set `opencode.serverUrl` — but ONLY after health-probing the URL,
|
|
14694
|
-
// so we never advertise a serve that isn't there. An unreachable URL is a
|
|
14695
|
-
// typed failure the UI shows in place, not a silent bad write.
|
|
14696
|
-
// On success, persist, re-probe, and beat immediately, so a runtime that IS
|
|
14697
|
-
// launchable starts taking turns at once rather than on the next tick — and one
|
|
14698
|
-
// that isn't reports its issue just as promptly. Returns a typed outcome (never
|
|
14699
|
-
// throws for a reachable/valid input).
|
|
14322
|
+
// This records INTENT, the user's half: a harness is advertised only when also
|
|
14323
|
+
// its bundled binary resolves, so an enable on a companion whose optional
|
|
14324
|
+
// dependency never installed advertises nothing, and the Harnesses surface
|
|
14325
|
+
// says why.
|
|
14326
|
+
// - claude-code: `claudeCode.enabled`, after a presence check. The terminal
|
|
14327
|
+
// offer and the web pairing flow both call this.
|
|
14328
|
+
// - codex: `codex.enabled`, after a presence check (login stays the user's).
|
|
14329
|
+
// - opencode: `opencode.serverUrl`, ONLY after health-probing the URL; an
|
|
14330
|
+
// unreachable one is a typed failure shown in place.
|
|
14331
|
+
// On success: persist, re-probe and beat at once, so the answer is true in
|
|
14332
|
+
// Cabane promptly either way. Never throws for a valid input.
|
|
14700
14333
|
async enableHarness(input) {
|
|
14701
14334
|
let next;
|
|
14702
14335
|
let checkedPresence = null;
|
|
@@ -14747,9 +14380,8 @@ var CompanionSupervisor = class {
|
|
|
14747
14380
|
return { ok: true };
|
|
14748
14381
|
}
|
|
14749
14382
|
// Re-create every running agent's Dispatcher from the CURRENT config, keeping
|
|
14750
|
-
// the runner (its api
|
|
14751
|
-
//
|
|
14752
|
-
// the adapter registry — pick up their new values without an app restart.
|
|
14383
|
+
// the runner (its api, outbox, chains) intact, so constructor-captured options
|
|
14384
|
+
// pick up a config change without a restart.
|
|
14753
14385
|
rebuildDispatchers() {
|
|
14754
14386
|
for (const wr of this.workspaces.values()) {
|
|
14755
14387
|
for (const runner of wr.agents.values()) {
|
|
@@ -14913,8 +14545,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
14913
14545
|
}));
|
|
14914
14546
|
await warnAboutHarnessReadiness(cfg, {
|
|
14915
14547
|
probeClaude: async () => claudeCode,
|
|
14916
|
-
//
|
|
14917
|
-
// its own words (the `!` block, or the per-harness offer), so it hands us a
|
|
14548
|
+
// The CLI's onboarding script says this in its own words, so it hands a
|
|
14918
14549
|
// sink that logs instead. Every other caller keeps the stderr line.
|
|
14919
14550
|
...opts.onReadinessWarning ? { warn: opts.onReadinessWarning } : {}
|
|
14920
14551
|
});
|
|
@@ -14951,11 +14582,10 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
14951
14582
|
const clearRuntimeOnExit = () => clearRuntimeStateIfOurs(instanceId);
|
|
14952
14583
|
process.on("exit", clearRuntimeOnExit);
|
|
14953
14584
|
const hub = new CompanionStateHub({
|
|
14954
|
-
//
|
|
14955
|
-
// with. The dashboard's connection line shows it.
|
|
14585
|
+
// The dashboard's connection line shows it.
|
|
14956
14586
|
baseUrl: cfg.baseUrl,
|
|
14957
14587
|
companionVersion: COMPANION_VERSION,
|
|
14958
|
-
//
|
|
14588
|
+
// Surfaced on `/api/status` so `stop`/`status` can confirm the
|
|
14959
14589
|
// process behind the marker pid is this companion (not a recycled pid).
|
|
14960
14590
|
instanceId
|
|
14961
14591
|
});
|
|
@@ -15008,7 +14638,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
15008
14638
|
socket: control.path,
|
|
15009
14639
|
...dashboard ? { url: dashboard.url, port: dashboard.port } : {},
|
|
15010
14640
|
startedAt,
|
|
15011
|
-
//
|
|
14641
|
+
// The daemon launcher sets this env on the detached child, so the
|
|
15012
14642
|
// marker records whether this companion is backgrounded (foreground start
|
|
15013
14643
|
// leaves it unset → false).
|
|
15014
14644
|
daemon: process.env.CABANE_COMPANION_DAEMON === "1",
|