@cabane/companion 0.6.145 → 0.6.147
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -51
- package/dist/cli.js +631 -1008
- package/dist/pairing-config.js +56 -95
- package/dist/runtime.js +625 -1000
- 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
|
|
@@ -3775,12 +3625,11 @@ function sinkEmitter(sink, _onError) {
|
|
|
3775
3625
|
summary: event.summary,
|
|
3776
3626
|
input: truncate(input, TOOL_IO_MAX_CHARS),
|
|
3777
3627
|
result: result === null ? "" : truncate(result, TOOL_IO_MAX_CHARS),
|
|
3778
|
-
//
|
|
3779
|
-
//
|
|
3780
|
-
// the size its start recorded.
|
|
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.
|
|
3781
3630
|
...event.input === void 0 ? {} : { inputChars: input.length },
|
|
3782
3631
|
...result === null ? {} : { resultChars: result.length },
|
|
3783
|
-
//
|
|
3632
|
+
// The originating MCP server (null when absent / a host tool).
|
|
3784
3633
|
mcpServer: event.mcpServer ?? null,
|
|
3785
3634
|
phase: event.phase
|
|
3786
3635
|
});
|
|
@@ -3874,22 +3723,18 @@ var TurnPump = class {
|
|
|
3874
3723
|
// The turn's final reply text, captured for the dashboard feed / logging.
|
|
3875
3724
|
finalReplyBody = null;
|
|
3876
3725
|
emittedFinalSource = "none";
|
|
3877
|
-
//
|
|
3878
|
-
//
|
|
3879
|
-
// invocation
|
|
3880
|
-
//
|
|
3881
|
-
//
|
|
3882
|
-
//
|
|
3883
|
-
// together; minting only the id would let two calls overwrite each other's
|
|
3884
|
-
// 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.
|
|
3885
3732
|
//
|
|
3886
|
-
// In memory on purpose: it dies with the process, and so does the runtime.
|
|
3887
|
-
//
|
|
3888
|
-
// 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.
|
|
3889
3735
|
toolCallByProviderId = /* @__PURE__ */ new Map();
|
|
3890
|
-
// The callback consumer the classification (via `sinkEmitter`)
|
|
3891
|
-
//
|
|
3892
|
-
// 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.
|
|
3893
3738
|
sink;
|
|
3894
3739
|
// Whether a message has landed under this span — first words, at least. A
|
|
3895
3740
|
// span that spoke settles; one that never did is a pass.
|
|
@@ -3981,7 +3826,7 @@ var TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
|
3981
3826
|
wake_me: {
|
|
3982
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.",
|
|
3983
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.",
|
|
3984
|
-
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.`
|
|
3985
3830
|
},
|
|
3986
3831
|
withdraw_ask: {
|
|
3987
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.",
|
|
@@ -3994,7 +3839,7 @@ var MEANINGS = {
|
|
|
3994
3839
|
ask: "put a structured question to a person",
|
|
3995
3840
|
cancel_wake: "cancel your own armed wake in this conversation",
|
|
3996
3841
|
skip_turn: "end without posting a reply",
|
|
3997
|
-
wake_me: "schedule your own return",
|
|
3842
|
+
wake_me: "schedule your own return; when it fires, say first what you came back to check",
|
|
3998
3843
|
withdraw_ask: "withdraw an open ask in this conversation with a reason"
|
|
3999
3844
|
};
|
|
4000
3845
|
function turnControlPromptGuidance(prefix) {
|
|
@@ -4215,52 +4060,41 @@ function buildClaudeCodeOptions(req, augment) {
|
|
|
4215
4060
|
// pinned `model` arrives as a bare SDK id — the host already resolved the
|
|
4216
4061
|
// namespaced catalog id to it.
|
|
4217
4062
|
...model ? { model } : {},
|
|
4218
|
-
//
|
|
4219
|
-
//
|
|
4220
|
-
//
|
|
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']`).
|
|
4221
4066
|
thinking: withThinkingSummaries(readThinking(config.runtimeOptions)),
|
|
4222
4067
|
...config.effort ? { effort: config.effort } : {},
|
|
4223
4068
|
// Never merge arbitrary user `~/.claude` config; without `strictMcpConfig` the
|
|
4224
4069
|
// user's existing `cabane` MCP registration could shadow ours.
|
|
4225
4070
|
settingSources: [],
|
|
4226
4071
|
strictMcpConfig: true,
|
|
4227
|
-
//
|
|
4072
|
+
// Auto-memory off unless the operator opted back in (see above).
|
|
4228
4073
|
...devControlsAutoMemory ? {} : { settings: { autoMemoryEnabled: false } },
|
|
4229
4074
|
mcpServers,
|
|
4230
4075
|
...cwd ? { cwd } : {},
|
|
4231
|
-
// Extra env (a companion prepare hook's tokens/ports
|
|
4232
|
-
// merged OVER the inherited environment.
|
|
4076
|
+
// Extra env (a companion prepare hook's tokens/ports) merged over the inherited environment.
|
|
4233
4077
|
//
|
|
4234
|
-
//
|
|
4235
|
-
//
|
|
4236
|
-
// a
|
|
4237
|
-
//
|
|
4238
|
-
//
|
|
4239
|
-
//
|
|
4240
|
-
//
|
|
4241
|
-
//
|
|
4242
|
-
//
|
|
4243
|
-
// poisoned the resumed session is an INFERENCE from one incident, consistent
|
|
4244
|
-
// with the transcript but never reproduced, and this change does not rest on
|
|
4245
|
-
// it. `Dl()` reads this variable on the Bash tool's own call
|
|
4246
|
-
// path and sets `canAutoBackground: false` and `turnAbortBackgrounds: false`
|
|
4247
|
-
// 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.
|
|
4248
4087
|
//
|
|
4249
|
-
// The cost is
|
|
4250
|
-
//
|
|
4251
|
-
//
|
|
4252
|
-
//
|
|
4253
|
-
// mitigation for the automatic conversion, NOT a cleanup mechanism: step 0
|
|
4254
|
-
// measured that this flag changes no process-survival outcome on any turn-end
|
|
4255
|
-
// 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.
|
|
4256
4092
|
//
|
|
4257
|
-
//
|
|
4258
|
-
//
|
|
4259
|
-
// and why this key is now always present — previously it appeared only when
|
|
4260
|
-
// `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.
|
|
4261
4095
|
//
|
|
4262
|
-
//
|
|
4263
|
-
//
|
|
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.
|
|
4264
4098
|
env: {
|
|
4265
4099
|
...process.env,
|
|
4266
4100
|
...req.local.env,
|
|
@@ -4751,8 +4585,7 @@ function makeRequest(overrides2 = {}) {
|
|
|
4751
4585
|
systemPrompt: "system",
|
|
4752
4586
|
prompt: "hi there",
|
|
4753
4587
|
content: [{ type: "text", text: "hi there" }],
|
|
4754
|
-
//
|
|
4755
|
-
// conformance fixture carries a real bare SDK id.
|
|
4588
|
+
// The adapter rejects a null model, so the fixture carries a real bare SDK id.
|
|
4756
4589
|
config: { model: "claude-opus-4-8" },
|
|
4757
4590
|
policy: CABANE_POLICY,
|
|
4758
4591
|
session: null,
|
|
@@ -4865,7 +4698,7 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
4865
4698
|
phase: "start",
|
|
4866
4699
|
summary: "sleep.md",
|
|
4867
4700
|
input: { path: "sleep.md" },
|
|
4868
|
-
//
|
|
4701
|
+
// Derived from the raw `mcp__cabane__` name.
|
|
4869
4702
|
mcpServer: "cabane"
|
|
4870
4703
|
},
|
|
4871
4704
|
{
|
|
@@ -4883,9 +4716,9 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
4883
4716
|
]
|
|
4884
4717
|
},
|
|
4885
4718
|
{
|
|
4886
|
-
//
|
|
4887
|
-
//
|
|
4888
|
-
//
|
|
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).
|
|
4889
4722
|
name: "cancel mid-stream",
|
|
4890
4723
|
request: makeRequest(),
|
|
4891
4724
|
nativeStream: [
|
|
@@ -4937,12 +4770,9 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
4937
4770
|
]
|
|
4938
4771
|
},
|
|
4939
4772
|
{
|
|
4940
|
-
//
|
|
4941
|
-
//
|
|
4942
|
-
// `
|
|
4943
|
-
// this one produced nothing at all. The adapter's job is unchanged either way
|
|
4944
|
-
// — report faithfully what the runtime said — so it must still emit
|
|
4945
|
-
// `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
|
|
4946
4776
|
// content-bearing event is a failed turn.
|
|
4947
4777
|
name: "zero-content success (empty runtime result)",
|
|
4948
4778
|
request: makeRequest(),
|
|
@@ -4950,9 +4780,9 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
4950
4780
|
expected: [sessionEvent("s1"), { type: "result", ok: true }]
|
|
4951
4781
|
},
|
|
4952
4782
|
{
|
|
4953
|
-
//
|
|
4954
|
-
//
|
|
4955
|
-
//
|
|
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.
|
|
4956
4786
|
name: "task-notification result does not end a resumed turn",
|
|
4957
4787
|
request: makeRequest({ session: encodeSession({ sdkSessionId: "s1", cwd: CWD }) }),
|
|
4958
4788
|
nativeStream: [
|
|
@@ -5028,8 +4858,8 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
5028
4858
|
]
|
|
5029
4859
|
},
|
|
5030
4860
|
{
|
|
5031
|
-
//
|
|
5032
|
-
//
|
|
4861
|
+
// The filter is deliberately narrow: an origin-less zero-token result still ends the turn, even
|
|
4862
|
+
// if more frames follow.
|
|
5033
4863
|
name: "origin-less result still ends a resumed turn",
|
|
5034
4864
|
request: makeRequest({ session: encodeSession({ sdkSessionId: "s1", cwd: CWD }) }),
|
|
5035
4865
|
nativeStream: [
|
|
@@ -5100,11 +4930,10 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
5100
4930
|
]
|
|
5101
4931
|
},
|
|
5102
4932
|
{
|
|
5103
|
-
//
|
|
5104
|
-
//
|
|
5105
|
-
//
|
|
5106
|
-
//
|
|
5107
|
-
// 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.
|
|
5108
4937
|
name: "subscription cap \u2192 usage_capped",
|
|
5109
4938
|
request: makeRequest(),
|
|
5110
4939
|
nativeStream: [
|
|
@@ -5123,9 +4952,8 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
5123
4952
|
]
|
|
5124
4953
|
},
|
|
5125
4954
|
{
|
|
5126
|
-
//
|
|
5127
|
-
//
|
|
5128
|
-
// 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.
|
|
5129
4957
|
name: "partial narration before subscription cap stays progress",
|
|
5130
4958
|
request: makeRequest(),
|
|
5131
4959
|
nativeStream: [
|
|
@@ -5141,8 +4969,8 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
5141
4969
|
]
|
|
5142
4970
|
},
|
|
5143
4971
|
{
|
|
5144
|
-
//
|
|
5145
|
-
//
|
|
4972
|
+
// Recognized runtime prose stays authored when its kind disagrees with the structured terminal
|
|
4973
|
+
// failure.
|
|
5146
4974
|
name: "cap notice before server failure stays progress",
|
|
5147
4975
|
request: makeRequest(),
|
|
5148
4976
|
nativeStream: [
|
|
@@ -5164,10 +4992,9 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
5164
4992
|
]
|
|
5165
4993
|
},
|
|
5166
4994
|
{
|
|
5167
|
-
//
|
|
5168
|
-
//
|
|
5169
|
-
//
|
|
5170
|
-
// 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.
|
|
5171
4998
|
name: "incidental 5xx-shaped narration before server failure stays progress",
|
|
5172
4999
|
request: makeRequest(),
|
|
5173
5000
|
nativeStream: [
|
|
@@ -5189,9 +5016,8 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
5189
5016
|
]
|
|
5190
5017
|
},
|
|
5191
5018
|
{
|
|
5192
|
-
//
|
|
5193
|
-
//
|
|
5194
|
-
// 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`.
|
|
5195
5021
|
name: "blocking_limit terminal reason \u2192 usage_capped",
|
|
5196
5022
|
request: makeRequest(),
|
|
5197
5023
|
nativeStream: [
|
|
@@ -5205,8 +5031,8 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
5205
5031
|
expected: [sessionEvent("s1"), { type: "result", ok: false, reason: "usage_capped" }]
|
|
5206
5032
|
},
|
|
5207
5033
|
{
|
|
5208
|
-
//
|
|
5209
|
-
//
|
|
5034
|
+
// An expired login surfaces as an `auth_status` error frame; the failing result classifies as
|
|
5035
|
+
// `auth_expired`.
|
|
5210
5036
|
name: "expired login \u2192 auth_expired",
|
|
5211
5037
|
request: makeRequest(),
|
|
5212
5038
|
nativeStream: [
|
|
@@ -5220,10 +5046,9 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
5220
5046
|
expected: [sessionEvent("s1"), { type: "result", ok: false, reason: "auth_expired" }]
|
|
5221
5047
|
},
|
|
5222
5048
|
{
|
|
5223
|
-
//
|
|
5224
|
-
//
|
|
5225
|
-
//
|
|
5226
|
-
// 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`.
|
|
5227
5052
|
name: "provider overload text \u2192 server_error",
|
|
5228
5053
|
request: makeRequest(),
|
|
5229
5054
|
nativeStream: [
|
|
@@ -5236,11 +5061,9 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
5236
5061
|
expected: [sessionEvent("s1"), { type: "result", ok: false, reason: "server_error" }]
|
|
5237
5062
|
},
|
|
5238
5063
|
{
|
|
5239
|
-
//
|
|
5240
|
-
//
|
|
5241
|
-
//
|
|
5242
|
-
// The configured request model is irrelevant here; what's asserted is that the
|
|
5243
|
-
// 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.
|
|
5244
5067
|
name: "resolved model captured from system/init",
|
|
5245
5068
|
request: makeRequest({ session: encodeSession({ sdkSessionId: "s1", cwd: CWD }) }),
|
|
5246
5069
|
nativeStream: [init("s1", "claude-opus-4-8"), assistantText("Done."), resultSuccess("s1")],
|
|
@@ -5250,9 +5073,8 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
5250
5073
|
]
|
|
5251
5074
|
},
|
|
5252
5075
|
{
|
|
5253
|
-
// Session resume echo: the stored state matches the
|
|
5254
|
-
//
|
|
5255
|
-
// (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.
|
|
5256
5078
|
name: "session resume (no re-emit on echo)",
|
|
5257
5079
|
request: makeRequest({ session: encodeSession({ sdkSessionId: "s1", cwd: CWD }) }),
|
|
5258
5080
|
nativeStream: [init("s1"), assistantText("Back again."), resultSuccess("s1")],
|
|
@@ -5262,16 +5084,14 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
5262
5084
|
]
|
|
5263
5085
|
},
|
|
5264
5086
|
{
|
|
5265
|
-
//
|
|
5266
|
-
//
|
|
5267
|
-
// 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.
|
|
5268
5089
|
name: "session cwd-mismatch degrades to fresh",
|
|
5269
5090
|
request: makeRequest({ session: encodeSession({ sdkSessionId: "old", cwd: "/env/gone" }) }),
|
|
5270
5091
|
nativeStream: [init("fresh1"), assistantText("Fresh start."), resultSuccess("fresh1")],
|
|
5271
5092
|
expected: [
|
|
5272
|
-
//
|
|
5273
|
-
//
|
|
5274
|
-
// 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).
|
|
5275
5095
|
sessionEvent("fresh1", CWD, true),
|
|
5276
5096
|
{ type: "text", body: "Fresh start.", terminal: true },
|
|
5277
5097
|
{ type: "result", ok: true }
|
|
@@ -5914,8 +5734,6 @@ function createOpencodeAdapter(deps = {}) {
|
|
|
5914
5734
|
const transport = deps.transport ?? (deps.serverUrl ? createHttpOpencodeTransport({ baseUrl: deps.serverUrl }) : null);
|
|
5915
5735
|
return {
|
|
5916
5736
|
name: "opencode",
|
|
5917
|
-
// CT829: one surface, so one addendum and no parameter — CT614's `codeMode`
|
|
5918
|
-
// argument went with the named tool surface it selected against.
|
|
5919
5737
|
promptAddendum: () => OPENCODE_ADDENDUM,
|
|
5920
5738
|
dialectSchema: opencodeDialectSchema,
|
|
5921
5739
|
async *runTurn(request, signal, hooks) {
|
|
@@ -6078,15 +5896,12 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
6078
5896
|
]
|
|
6079
5897
|
},
|
|
6080
5898
|
{
|
|
6081
|
-
//
|
|
6082
|
-
//
|
|
6083
|
-
//
|
|
6084
|
-
// (
|
|
6085
|
-
//
|
|
6086
|
-
//
|
|
6087
|
-
// upserts) must carry the LATEST non-empty input + its summary, not the
|
|
6088
|
-
// first-seen `{}`. This is the case the pre-CT301 suite structurally couldn't
|
|
6089
|
-
// 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.
|
|
6090
5905
|
name: "streaming tool input \u2014 late args refresh the done event (CT301)",
|
|
6091
5906
|
request: makeRequest2(),
|
|
6092
5907
|
nativeStream: [
|
|
@@ -6124,9 +5939,8 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
6124
5939
|
]
|
|
6125
5940
|
},
|
|
6126
5941
|
{
|
|
6127
|
-
//
|
|
6128
|
-
//
|
|
6129
|
-
// 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).
|
|
6130
5944
|
name: "cancel mid-stream",
|
|
6131
5945
|
request: makeRequest2(),
|
|
6132
5946
|
nativeStream: [
|
|
@@ -6166,10 +5980,9 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
6166
5980
|
]
|
|
6167
5981
|
},
|
|
6168
5982
|
{
|
|
6169
|
-
//
|
|
6170
|
-
//
|
|
6171
|
-
//
|
|
6172
|
-
// 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.
|
|
6173
5986
|
name: "provider 429 \u2192 rate_limited (throttle, no reset)",
|
|
6174
5987
|
request: makeRequest2(),
|
|
6175
5988
|
nativeStream: [
|
|
@@ -6185,9 +5998,8 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
6185
5998
|
]
|
|
6186
5999
|
},
|
|
6187
6000
|
{
|
|
6188
|
-
//
|
|
6189
|
-
//
|
|
6190
|
-
// 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").
|
|
6191
6003
|
name: "usage cap text \u2192 usage_capped (no reset)",
|
|
6192
6004
|
request: makeRequest2(),
|
|
6193
6005
|
nativeStream: [
|
|
@@ -6203,9 +6015,7 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
6203
6015
|
]
|
|
6204
6016
|
},
|
|
6205
6017
|
{
|
|
6206
|
-
//
|
|
6207
|
-
// honest `server_error`, never the cap. This is the outage class that used to
|
|
6208
|
-
// fall through to an opaque `unknown`.
|
|
6018
|
+
// A transient provider outage (5xx / overload) surfaced as text → `server_error`, never the cap.
|
|
6209
6019
|
name: "provider overload text \u2192 server_error",
|
|
6210
6020
|
request: makeRequest2(),
|
|
6211
6021
|
nativeStream: [
|
|
@@ -6223,7 +6033,7 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
6223
6033
|
]
|
|
6224
6034
|
},
|
|
6225
6035
|
{
|
|
6226
|
-
//
|
|
6036
|
+
// An expired or invalid credential classifies as `auth_expired`.
|
|
6227
6037
|
name: "auth failure \u2192 auth_expired",
|
|
6228
6038
|
request: makeRequest2(),
|
|
6229
6039
|
nativeStream: [
|
|
@@ -6239,8 +6049,8 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
6239
6049
|
]
|
|
6240
6050
|
},
|
|
6241
6051
|
{
|
|
6242
|
-
//
|
|
6243
|
-
//
|
|
6052
|
+
// An unclassified failure stays informative: its provider text is visible, and the raw machine
|
|
6053
|
+
// reason is unchanged.
|
|
6244
6054
|
name: "unclassified failure keeps a runtime notice and raw reason",
|
|
6245
6055
|
request: makeRequest2(),
|
|
6246
6056
|
nativeStream: [errored("something local broke")],
|
|
@@ -6261,8 +6071,8 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
6261
6071
|
]
|
|
6262
6072
|
},
|
|
6263
6073
|
{
|
|
6264
|
-
//
|
|
6265
|
-
//
|
|
6074
|
+
// Empty, whitespace-only and bare status-token failures still set the machine reason but add no
|
|
6075
|
+
// empty or unhelpful transcript notice.
|
|
6266
6076
|
name: "empty failure text emits no runtime notice",
|
|
6267
6077
|
request: makeRequest2(),
|
|
6268
6078
|
nativeStream: [errored("")],
|
|
@@ -6294,12 +6104,10 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
6294
6104
|
]
|
|
6295
6105
|
},
|
|
6296
6106
|
{
|
|
6297
|
-
//
|
|
6298
|
-
//
|
|
6299
|
-
//
|
|
6300
|
-
//
|
|
6301
|
-
// runtime that CAN produce this envelope must be shown producing it in the
|
|
6302
|
-
// 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.
|
|
6303
6111
|
name: "zero-content success (empty runtime result)",
|
|
6304
6112
|
request: makeRequest2(),
|
|
6305
6113
|
nativeStream: [idle()],
|
|
@@ -6356,10 +6164,9 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
6356
6164
|
]
|
|
6357
6165
|
},
|
|
6358
6166
|
{
|
|
6359
|
-
//
|
|
6360
|
-
//
|
|
6361
|
-
//
|
|
6362
|
-
// 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.
|
|
6363
6170
|
name: "session resume (no re-emit on echo)",
|
|
6364
6171
|
request: makeRequest2({ session: encodeSession2({ sessionId: "s1", directory: DIR }) }),
|
|
6365
6172
|
nativeStream: [textPart("p1", "Back again."), idle()],
|
|
@@ -6369,12 +6176,10 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
6369
6176
|
]
|
|
6370
6177
|
},
|
|
6371
6178
|
{
|
|
6372
|
-
//
|
|
6373
|
-
//
|
|
6374
|
-
//
|
|
6375
|
-
//
|
|
6376
|
-
// injected per-turn context still can't leak into Steps. Only the assistant
|
|
6377
|
-
// 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.
|
|
6378
6183
|
name: "session resume \u2014 prompt echo filtered via stream-derived user id",
|
|
6379
6184
|
request: makeRequest2({ session: encodeSession2({ sessionId: "s1", directory: DIR }) }),
|
|
6380
6185
|
nativeStream: [
|
|
@@ -6390,17 +6195,15 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
6390
6195
|
]
|
|
6391
6196
|
},
|
|
6392
6197
|
{
|
|
6393
|
-
// Directory-mismatch degrade
|
|
6394
|
-
//
|
|
6395
|
-
//
|
|
6396
|
-
// 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.
|
|
6397
6201
|
name: "session directory-mismatch degrades to fresh",
|
|
6398
6202
|
request: makeRequest2({ session: encodeSession2({ sessionId: "old", directory: "/env/gone" }) }),
|
|
6399
6203
|
nativeStream: [textPart("p1", "Fresh start."), idle()],
|
|
6400
6204
|
expected: [
|
|
6401
|
-
//
|
|
6402
|
-
//
|
|
6403
|
-
// 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.
|
|
6404
6207
|
sessionEvent2(NEW_SESSION_ID, DIR, true),
|
|
6405
6208
|
{ type: "text", body: "Fresh start.", terminal: true },
|
|
6406
6209
|
{ type: "result", ok: true }
|
|
@@ -6418,11 +6221,9 @@ var CODEX_ADDENDUM = [
|
|
|
6418
6221
|
"absent without attempting discovery and invocation, and if a call fails, report the recorded",
|
|
6419
6222
|
"error.",
|
|
6420
6223
|
turnControlPromptGuidance("mcp__cabane_companion__"),
|
|
6421
|
-
//
|
|
6422
|
-
//
|
|
6423
|
-
//
|
|
6424
|
-
// so the sentence contradicted itself. The bare `query` / `mutate` below stay:
|
|
6425
|
-
// 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.
|
|
6426
6227
|
"The ancillary tools are `mcp__cabane__read_binary`, `mcp__cabane__upload`,",
|
|
6427
6228
|
"`mcp__cabane__begin_upload`, `mcp__cabane__finalize_upload` and",
|
|
6428
6229
|
"`mcp__cabane__mint_render_token` \u2014 all possibly deferred too.",
|
|
@@ -6666,10 +6467,9 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
6666
6467
|
threadId,
|
|
6667
6468
|
cwd: ctx.cwd ?? "",
|
|
6668
6469
|
promptFingerprint: ctx.promptFingerprint ?? null,
|
|
6669
|
-
//
|
|
6670
|
-
//
|
|
6671
|
-
//
|
|
6672
|
-
// 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.
|
|
6673
6473
|
usageBaseline: ctx.usageBaseline ?? null
|
|
6674
6474
|
});
|
|
6675
6475
|
if (threadId !== ctx.resumedThreadId || state !== ctx.storedState) {
|
|
@@ -6953,9 +6753,8 @@ function buildConfig(req, baseInstructionsFile) {
|
|
|
6953
6753
|
return {
|
|
6954
6754
|
mcp_servers,
|
|
6955
6755
|
experimental_use_rmcp_client: true,
|
|
6956
|
-
//
|
|
6957
|
-
//
|
|
6958
|
-
// 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.
|
|
6959
6758
|
...baseInstructionsFile ? { model_instructions_file: baseInstructionsFile, include_environment_context: false } : {},
|
|
6960
6759
|
...Object.keys(shellEnv).length ? { shell_environment_policy: { set: shellEnv } } : {}
|
|
6961
6760
|
};
|
|
@@ -7052,16 +6851,11 @@ function createSdkCodexTransport(opts = {}) {
|
|
|
7052
6851
|
const codexOptions = {
|
|
7053
6852
|
...opts.apiKey ? { apiKey: opts.apiKey } : {},
|
|
7054
6853
|
...opts.codexPathOverride ? { codexPathOverride: opts.codexPathOverride } : {},
|
|
7055
|
-
// The `--config` overrides (MCP servers + rmcp flag)
|
|
7056
|
-
//
|
|
7057
|
-
//
|
|
7058
|
-
//
|
|
7059
|
-
//
|
|
7060
|
-
// ignores TOML quoting), so it has to ride inside an inline-table value,
|
|
7061
|
-
// and the SDK's flattener cannot be steered into producing one. This is
|
|
7062
|
-
// also what makes the PUBLISHED companion correct, where the vendored
|
|
7063
|
-
// patch never reached — and why that patch is gone. See
|
|
7064
|
-
// `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`.
|
|
7065
6859
|
configOverrides: buildConfigOverrides(spec.config),
|
|
7066
6860
|
...spec.secretEnv ? { env: codexProcessEnv(spec.secretEnv) } : {}
|
|
7067
6861
|
};
|
|
@@ -7079,8 +6873,6 @@ function createCodexAdapter(deps = {}) {
|
|
|
7079
6873
|
const transport = deps.transport ?? (deps.enabled ? createSdkCodexTransport({ ...deps.apiKey ? { apiKey: deps.apiKey } : {} }) : null);
|
|
7080
6874
|
return {
|
|
7081
6875
|
name: "codex",
|
|
7082
|
-
// CT829: one surface, so one addendum and no parameter — CT614's `codeMode`
|
|
7083
|
-
// argument went with the named tool surface it selected against.
|
|
7084
6876
|
promptAddendum: () => CODEX_ADDENDUM,
|
|
7085
6877
|
dialectSchema: codexDialectSchema,
|
|
7086
6878
|
async *runTurn(request, signal, hooks) {
|
|
@@ -7172,17 +6964,14 @@ function createCodexAdapter(deps = {}) {
|
|
|
7172
6964
|
cwd: req.local.cwd,
|
|
7173
6965
|
resumedThreadId: resumeThreadId,
|
|
7174
6966
|
degraded,
|
|
7175
|
-
//
|
|
7176
|
-
//
|
|
7177
|
-
//
|
|
7178
|
-
// it, the thread still carries whatever it did before. Read off the spec
|
|
7179
|
-
// rather than recomputed, so the recorded fact can't disagree with what
|
|
7180
|
-
// 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.
|
|
7181
6970
|
promptFingerprint: spec.promptRidesInput ? spec.promptFingerprint : threadPromptFingerprint,
|
|
7182
6971
|
storedState: req.session,
|
|
7183
6972
|
usageBaseline: threadUsageBaseline,
|
|
7184
|
-
//
|
|
7185
|
-
//
|
|
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.
|
|
7186
6975
|
resolvedModel: spec.model,
|
|
7187
6976
|
resolvedReasoningEffort: spec.modelReasoningEffort ?? null
|
|
7188
6977
|
});
|
|
@@ -7231,11 +7020,9 @@ function makeRequest3(overrides2 = {}) {
|
|
|
7231
7020
|
systemPrompt: PROMPT,
|
|
7232
7021
|
prompt: "hi there",
|
|
7233
7022
|
content: [{ type: "text", text: "hi there" }],
|
|
7234
|
-
//
|
|
7235
|
-
//
|
|
7236
|
-
//
|
|
7237
|
-
// emitted → their expected results stay unchanged; a dedicated capture fixture
|
|
7238
|
-
// 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.
|
|
7239
7026
|
config: { model: null },
|
|
7240
7027
|
policy: COMPANION_POLICY2,
|
|
7241
7028
|
session: null,
|
|
@@ -7287,13 +7074,12 @@ var sessionEvent3 = (threadId, cwd = DIR2, degraded = false, promptFingerprint =
|
|
|
7287
7074
|
});
|
|
7288
7075
|
var CODEX_CONFORMANCE_FIXTURES = [
|
|
7289
7076
|
{
|
|
7290
|
-
// A plain text reply: the held agent message seals as the terminal final at
|
|
7291
|
-
//
|
|
7077
|
+
// A plain text reply: the held agent message seals as the terminal final at `turn.completed`;
|
|
7078
|
+
// usage rides the `result`.
|
|
7292
7079
|
//
|
|
7293
|
-
//
|
|
7294
|
-
// `
|
|
7295
|
-
//
|
|
7296
|
-
// 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.
|
|
7297
7083
|
name: "clean turn",
|
|
7298
7084
|
request: makeRequest3(),
|
|
7299
7085
|
nativeStream: [
|
|
@@ -7310,10 +7096,8 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7310
7096
|
expected: [
|
|
7311
7097
|
sessionEvent3(NEW_THREAD_ID),
|
|
7312
7098
|
{ type: "text", body: "Hello there.", terminal: true },
|
|
7313
|
-
//
|
|
7314
|
-
//
|
|
7315
|
-
// `result` usage below is the frame verbatim (nothing to subtract yet);
|
|
7316
|
-
// 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.
|
|
7317
7101
|
sessionEvent3(NEW_THREAD_ID, DIR2, false, fingerprintPrompt(PROMPT), {
|
|
7318
7102
|
inputTokens: 100,
|
|
7319
7103
|
outputTokens: 50,
|
|
@@ -7385,8 +7169,8 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7385
7169
|
]
|
|
7386
7170
|
},
|
|
7387
7171
|
{
|
|
7388
|
-
//
|
|
7389
|
-
//
|
|
7172
|
+
// The recorded MCP success shape: Codex exposes the text copy and the native object together;
|
|
7173
|
+
// normalization must keep the object.
|
|
7390
7174
|
name: "MCP success prefers native structured content",
|
|
7391
7175
|
request: makeRequest3(),
|
|
7392
7176
|
nativeStream: [
|
|
@@ -7436,8 +7220,8 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7436
7220
|
]
|
|
7437
7221
|
},
|
|
7438
7222
|
{
|
|
7439
|
-
//
|
|
7440
|
-
//
|
|
7223
|
+
// MCP `isError` keeps the failed phase, while the raw camel-case structured payload survives
|
|
7224
|
+
// instead of the generic `error` token.
|
|
7441
7225
|
name: "failed MCP call preserves structured receipt while remaining error phase",
|
|
7442
7226
|
request: makeRequest3(),
|
|
7443
7227
|
nativeStream: [
|
|
@@ -7489,8 +7273,8 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7489
7273
|
]
|
|
7490
7274
|
},
|
|
7491
7275
|
{
|
|
7492
|
-
// A transport/tool failure
|
|
7493
|
-
//
|
|
7276
|
+
// A transport/tool failure with no MCP result payload still uses the real adapter error message,
|
|
7277
|
+
// never `undefined`.
|
|
7494
7278
|
name: "failed MCP call without result keeps error message fallback",
|
|
7495
7279
|
request: makeRequest3(),
|
|
7496
7280
|
nativeStream: [
|
|
@@ -7530,8 +7314,8 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7530
7314
|
]
|
|
7531
7315
|
},
|
|
7532
7316
|
{
|
|
7533
|
-
//
|
|
7534
|
-
//
|
|
7317
|
+
// A connector's structured metadata beside an image-only MCP response: no image/base64 payload
|
|
7318
|
+
// enters activity receipts.
|
|
7535
7319
|
name: "image-only MCP result remains empty despite structured metadata",
|
|
7536
7320
|
request: makeRequest3(),
|
|
7537
7321
|
nativeStream: [
|
|
@@ -7624,12 +7408,10 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7624
7408
|
]
|
|
7625
7409
|
},
|
|
7626
7410
|
{
|
|
7627
|
-
//
|
|
7628
|
-
//
|
|
7629
|
-
//
|
|
7630
|
-
//
|
|
7631
|
-
// and carry the populated query the completed frame filled in. Before CT715 the
|
|
7632
|
-
// 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".
|
|
7633
7415
|
name: "web_search lifecycle \u2014 completes off frame type, populated query on done",
|
|
7634
7416
|
request: makeRequest3(),
|
|
7635
7417
|
nativeStream: [
|
|
@@ -7660,9 +7442,8 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7660
7442
|
]
|
|
7661
7443
|
},
|
|
7662
7444
|
{
|
|
7663
|
-
//
|
|
7664
|
-
//
|
|
7665
|
-
// 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`.
|
|
7666
7447
|
name: "web_search lifecycle \u2014 empty-query completion still resolves to done",
|
|
7667
7448
|
request: makeRequest3(),
|
|
7668
7449
|
nativeStream: [
|
|
@@ -7693,9 +7474,8 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7693
7474
|
]
|
|
7694
7475
|
},
|
|
7695
7476
|
{
|
|
7696
|
-
//
|
|
7697
|
-
//
|
|
7698
|
-
// 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).
|
|
7699
7479
|
name: "cancel mid-stream",
|
|
7700
7480
|
request: makeRequest3(),
|
|
7701
7481
|
nativeStream: [
|
|
@@ -7776,11 +7556,8 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7776
7556
|
]
|
|
7777
7557
|
},
|
|
7778
7558
|
{
|
|
7779
|
-
//
|
|
7780
|
-
//
|
|
7781
|
-
// classifier (Codex is text-only), so a provider 429 / outage / auth / cap
|
|
7782
|
-
// failure splits into the honest taxonomy instead of all reading `unknown`.
|
|
7783
|
-
// 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:
|
|
7784
7561
|
name: "turn.failed 429 \u2192 rate_limited (throttle)",
|
|
7785
7562
|
request: makeRequest3(),
|
|
7786
7563
|
nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("429 Too Many Requests")],
|
|
@@ -7827,8 +7604,8 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7827
7604
|
]
|
|
7828
7605
|
},
|
|
7829
7606
|
{
|
|
7830
|
-
//
|
|
7831
|
-
//
|
|
7607
|
+
// A failure text the classifier can't place keeps the raw `error:<text>` reason (the `unknown`
|
|
7608
|
+
// fallback).
|
|
7832
7609
|
name: "turn.failed unclassified \u2192 raw error reason (unchanged)",
|
|
7833
7610
|
request: makeRequest3(),
|
|
7834
7611
|
nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("something local broke")],
|
|
@@ -7852,8 +7629,8 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7852
7629
|
]
|
|
7853
7630
|
},
|
|
7854
7631
|
{
|
|
7855
|
-
//
|
|
7856
|
-
//
|
|
7632
|
+
// Empty, whitespace-only and bare status-token failures still set the machine reason but add no
|
|
7633
|
+
// empty or unhelpful transcript notice.
|
|
7857
7634
|
name: "empty failure text emits no runtime notice",
|
|
7858
7635
|
request: makeRequest3(),
|
|
7859
7636
|
nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("")],
|
|
@@ -7882,11 +7659,9 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7882
7659
|
]
|
|
7883
7660
|
},
|
|
7884
7661
|
{
|
|
7885
|
-
//
|
|
7886
|
-
//
|
|
7887
|
-
//
|
|
7888
|
-
// adapter ends the turn the moment the signal lands, with a machine reason — so
|
|
7889
|
-
// 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.
|
|
7890
7665
|
name: "unknown model metadata \u2192 fail loud (guard)",
|
|
7891
7666
|
request: makeRequest3(),
|
|
7892
7667
|
nativeStream: [
|
|
@@ -7932,12 +7707,10 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7932
7707
|
]
|
|
7933
7708
|
},
|
|
7934
7709
|
{
|
|
7935
|
-
//
|
|
7936
|
-
//
|
|
7937
|
-
//
|
|
7938
|
-
//
|
|
7939
|
-
// neutral, so a runtime that CAN produce this envelope must be shown
|
|
7940
|
-
// 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.
|
|
7941
7714
|
name: "zero-content success (empty runtime result)",
|
|
7942
7715
|
request: makeRequest3(),
|
|
7943
7716
|
nativeStream: [threadStarted(NEW_THREAD_ID), turnCompleted()],
|
|
@@ -8006,11 +7779,10 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
8006
7779
|
]
|
|
8007
7780
|
},
|
|
8008
7781
|
{
|
|
8009
|
-
//
|
|
8010
|
-
//
|
|
8011
|
-
//
|
|
8012
|
-
//
|
|
8013
|
-
// `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`.
|
|
8014
7786
|
name: "resolved model + reasoning effort captured from run spec",
|
|
8015
7787
|
request: makeRequest3({
|
|
8016
7788
|
config: {
|
|
@@ -8031,15 +7803,12 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
8031
7803
|
]
|
|
8032
7804
|
},
|
|
8033
7805
|
{
|
|
8034
|
-
// Session resume: the stored state matches the
|
|
8035
|
-
//
|
|
8036
|
-
// event fires (the id didn't change AND the state is unchanged). This is what
|
|
8037
|
-
// 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.
|
|
8038
7808
|
//
|
|
8039
|
-
//
|
|
8040
|
-
//
|
|
8041
|
-
//
|
|
8042
|
-
// 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.
|
|
8043
7812
|
name: "session resume (no re-emit on echo)",
|
|
8044
7813
|
request: makeRequest3({
|
|
8045
7814
|
session: encodeSession3({
|
|
@@ -8056,16 +7825,12 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
8056
7825
|
]
|
|
8057
7826
|
},
|
|
8058
7827
|
{
|
|
8059
|
-
//
|
|
8060
|
-
//
|
|
8061
|
-
//
|
|
8062
|
-
//
|
|
8063
|
-
// the fingerprint recorded — on the SAME thread id, which pre-CT1075 emitted
|
|
8064
|
-
// nothing at all. That re-emit is the upgrade path: without it a long-lived
|
|
8065
|
-
// 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.
|
|
8066
7832
|
//
|
|
8067
|
-
// This
|
|
8068
|
-
// have no evidence about.
|
|
7833
|
+
// This fixture fails if the standing half is withheld on a thread we have no evidence about.
|
|
8069
7834
|
name: "session resume with a pre-CT1075 state re-emits with the prompt fingerprint",
|
|
8070
7835
|
request: makeRequest3({
|
|
8071
7836
|
session: JSON.stringify({ threadId: "th1", cwd: DIR2 })
|
|
@@ -8078,14 +7843,12 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
8078
7843
|
]
|
|
8079
7844
|
},
|
|
8080
7845
|
{
|
|
8081
|
-
//
|
|
8082
|
-
//
|
|
8083
|
-
//
|
|
8084
|
-
// 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.
|
|
8085
7849
|
//
|
|
8086
|
-
// This
|
|
8087
|
-
//
|
|
8088
|
-
// 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.
|
|
8089
7852
|
name: "session resume with a stale prompt fingerprint re-sends the prompt",
|
|
8090
7853
|
request: makeRequest3({
|
|
8091
7854
|
session: encodeSession3({
|
|
@@ -8103,15 +7866,12 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
8103
7866
|
]
|
|
8104
7867
|
},
|
|
8105
7868
|
{
|
|
8106
|
-
// Directory-mismatch degrade
|
|
8107
|
-
//
|
|
8108
|
-
//
|
|
8109
|
-
// records the CURRENT directory with `degraded: true` so the host rewinds the
|
|
8110
|
-
// 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.
|
|
8111
7872
|
//
|
|
8112
|
-
//
|
|
8113
|
-
//
|
|
8114
|
-
// 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.
|
|
8115
7875
|
name: "session directory-mismatch degrades to fresh",
|
|
8116
7876
|
request: makeRequest3({
|
|
8117
7877
|
session: encodeSession3({
|
|
@@ -8160,7 +7920,7 @@ var ConnectorHealthStore = class {
|
|
|
8160
7920
|
lastError: outcome.errorReason ?? null
|
|
8161
7921
|
});
|
|
8162
7922
|
}
|
|
8163
|
-
//
|
|
7923
|
+
// The store's current opinion for one runtime, or undefined when it has
|
|
8164
7924
|
// no observation. Read by the dispatcher at idle-timeout fire: an idle reap
|
|
8165
7925
|
// while this runtime is definitively `limited` (a present, future `limitedUntil`
|
|
8166
7926
|
// recorded by a sibling/earlier turn's classified cap settle) is the account cap
|
|
@@ -8464,8 +8224,7 @@ function createTurnContainment(turnId, deps = {}) {
|
|
|
8464
8224
|
`--unit=${unit}`,
|
|
8465
8225
|
`--property=TimeoutStopSec=${CONTAINMENT_STOP_GRACE_SEC}s`,
|
|
8466
8226
|
// Bind the scope's life to the companion's own unit, so stopping or
|
|
8467
|
-
// restarting the companion still takes its in-flight turns with it
|
|
8468
|
-
// which is what happens today, and what the scope would otherwise undo.
|
|
8227
|
+
// restarting the companion still takes its in-flight turns with it.
|
|
8469
8228
|
// Omitted outside a unit, where there is nothing to bind to.
|
|
8470
8229
|
...ownUnit ? [`--property=PartOf=${ownUnit}`] : [],
|
|
8471
8230
|
"--"
|
|
@@ -8785,7 +8544,7 @@ var mcpHttpServerSchema = z15.object({
|
|
|
8785
8544
|
type: z15.literal("http"),
|
|
8786
8545
|
url: z15.string().url(),
|
|
8787
8546
|
headers: z15.record(z15.string(), z15.string()).optional(),
|
|
8788
|
-
//
|
|
8547
|
+
// Header → the name of a `secretEnv` entry holding its value. Set by
|
|
8789
8548
|
// the server on a room connection's entry, and by `resolveMcpSecrets` on a
|
|
8790
8549
|
// header whose `${VAR}` it resolved.
|
|
8791
8550
|
headersFromEnv: z15.record(z15.string(), z15.string()).optional()
|
|
@@ -8794,7 +8553,7 @@ var mcpSseServerSchema = z15.object({
|
|
|
8794
8553
|
type: z15.literal("sse"),
|
|
8795
8554
|
url: z15.string().url(),
|
|
8796
8555
|
headers: z15.record(z15.string(), z15.string()).optional(),
|
|
8797
|
-
//
|
|
8556
|
+
// Header → the name of a `secretEnv` entry holding its value. Set by
|
|
8798
8557
|
// the server on a room connection's entry, and by `resolveMcpSecrets` on a
|
|
8799
8558
|
// header whose `${VAR}` it resolved.
|
|
8800
8559
|
headersFromEnv: z15.record(z15.string(), z15.string()).optional()
|
|
@@ -8807,9 +8566,8 @@ var thinkingConfigSchema = z15.discriminatedUnion("type", [
|
|
|
8807
8566
|
]);
|
|
8808
8567
|
var effortSchema = z15.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
8809
8568
|
var runConfigSchema = z15.object({
|
|
8810
|
-
//
|
|
8811
|
-
//
|
|
8812
|
-
// 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
|
|
8813
8571
|
// `agentRunConfigSchema` (independent zod, forward-compatible: unknown keys are
|
|
8814
8572
|
// stripped, so an older companion riding a newer server never rejects the config).
|
|
8815
8573
|
hostAccess: z15.boolean().optional(),
|
|
@@ -8836,7 +8594,7 @@ var RUNNER_ENV = {
|
|
|
8836
8594
|
dispatch: "CABANE_DISPATCH",
|
|
8837
8595
|
// The turn id the launcher resolved, and whether this is a replay of a turn
|
|
8838
8596
|
// an earlier process started. The seq floor for a replay is derived inside
|
|
8839
|
-
// the turn from the outbox and the server
|
|
8597
|
+
// the turn from the outbox and the server.
|
|
8840
8598
|
turnId: "CABANE_TURN_ID",
|
|
8841
8599
|
resumed: "CABANE_RESUMED",
|
|
8842
8600
|
// Everything else the turn is constructed from, as JSON: see `RunnerSpec`.
|
|
@@ -8872,10 +8630,10 @@ var runnerSpecSchema = z16.object({
|
|
|
8872
8630
|
// Whether the harness starts under the OS sandbox. The launcher sends 'os'
|
|
8873
8631
|
// only when this host can make one.
|
|
8874
8632
|
sandbox: z16.enum(["os", "none"]),
|
|
8875
|
-
//
|
|
8633
|
+
// The device's sandbox policy, validated by the launcher. Absent
|
|
8876
8634
|
// when the sandbox is off.
|
|
8877
8635
|
sandboxPolicy: sandboxPolicySchema.optional(),
|
|
8878
|
-
//
|
|
8636
|
+
// Every companion store the launcher's latest walk found, denied
|
|
8879
8637
|
// to the harness. Absent when the sandbox is off.
|
|
8880
8638
|
companionStores: z16.array(z16.string()).optional(),
|
|
8881
8639
|
logLevel: z16.string(),
|
|
@@ -8884,15 +8642,15 @@ var runnerSpecSchema = z16.object({
|
|
|
8884
8642
|
// True when this (conversation, agent) pair was already told its session
|
|
8885
8643
|
// could not be saved: the notice is once per pair, not once per process.
|
|
8886
8644
|
sessionWriteNoticeClaimed: z16.boolean(),
|
|
8887
|
-
//
|
|
8645
|
+
// The runner's identity, minted with its credential. The start
|
|
8888
8646
|
// PATCH carries it so the lease knows which runner holds the span.
|
|
8889
8647
|
runnerId: z16.string(),
|
|
8890
|
-
//
|
|
8648
|
+
// The launcher keeps the runtime's session files beside the attempt
|
|
8891
8649
|
// and restores them into this runner's home, so the runner names the attempt
|
|
8892
8650
|
// each context was composed for and waits for the launcher to say it's in
|
|
8893
8651
|
// place. Absent: nothing to wait for (a runner on the launcher's own disk).
|
|
8894
8652
|
attemptState: z16.boolean().optional(),
|
|
8895
|
-
//
|
|
8653
|
+
// MCP `${NAME}` secrets come from the launcher, by name, instead of
|
|
8896
8654
|
// this machine's secrets.json. Absent: the runner reads the device's file.
|
|
8897
8655
|
secretsFromLauncher: z16.boolean().optional()
|
|
8898
8656
|
});
|
|
@@ -9521,7 +9279,7 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
9521
9279
|
return performAct(acts, {
|
|
9522
9280
|
kind: "ask",
|
|
9523
9281
|
callId: randomUUID2(),
|
|
9524
|
-
//
|
|
9282
|
+
// The swap rides the ask, so the server commits both halves
|
|
9525
9283
|
// in one transaction — the person never sees neither or both.
|
|
9526
9284
|
...args.replaces ? { replaces: args.replaces } : {},
|
|
9527
9285
|
ask: hasArray ? {
|
|
@@ -9572,7 +9330,7 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
9572
9330
|
},
|
|
9573
9331
|
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
9574
9332
|
),
|
|
9575
|
-
//
|
|
9333
|
+
// The standalone withdrawal. Not annotated destructive — an ask
|
|
9576
9334
|
// is a question, not data, and a harness that gated this on a live
|
|
9577
9335
|
// operator would hang a turn nobody is watching (the `cancel_wake`
|
|
9578
9336
|
// reasoning). Not idempotent either: a second call with the same id is
|
|
@@ -9591,7 +9349,7 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
9591
9349
|
}),
|
|
9592
9350
|
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
9593
9351
|
),
|
|
9594
|
-
//
|
|
9352
|
+
// The inverse. No arguments — it can only ever stand down the
|
|
9595
9353
|
// caller's own wake in the conversation it's holding a turn in.
|
|
9596
9354
|
tool(
|
|
9597
9355
|
CANCEL_WAKE_TOOL,
|
|
@@ -9634,7 +9392,7 @@ function buildCompanionTurnRequest(params) {
|
|
|
9634
9392
|
// adapter's `ResolvedMcpServers`.
|
|
9635
9393
|
...Object.keys(params.mcpServers).length > 0 ? { mcpServers: params.mcpServers } : {},
|
|
9636
9394
|
...params.secretEnv && Object.keys(params.secretEnv).length > 0 ? { secretEnv: params.secretEnv } : {},
|
|
9637
|
-
//
|
|
9395
|
+
// The auto-memory escape hatch, when the operator set it.
|
|
9638
9396
|
...params.claudeCode ? { claudeCode: params.claudeCode } : {}
|
|
9639
9397
|
},
|
|
9640
9398
|
// Host-injected turn-control tools, under their own namespace.
|
|
@@ -9888,7 +9646,7 @@ var TranscriptWriter = class {
|
|
|
9888
9646
|
}
|
|
9889
9647
|
this.line({ type: "_meta", ...meta });
|
|
9890
9648
|
}
|
|
9891
|
-
// Append
|
|
9649
|
+
// Append one event verbatim.
|
|
9892
9650
|
write(msg) {
|
|
9893
9651
|
this.line(msg);
|
|
9894
9652
|
}
|
|
@@ -9896,7 +9654,7 @@ var TranscriptWriter = class {
|
|
|
9896
9654
|
close(outcome) {
|
|
9897
9655
|
this.line({ type: "_outcome", ...outcome });
|
|
9898
9656
|
}
|
|
9899
|
-
//
|
|
9657
|
+
// Keep an independent copy of an empty successful envelope after the
|
|
9900
9658
|
// footer has landed. The ordinary 200-file FIFO keeps rotating as before;
|
|
9901
9659
|
// this bucket retains the newest 50 anomalies regardless of ordinary traffic.
|
|
9902
9660
|
// Best-effort like every other transcript operation: observability can never
|
|
@@ -10053,12 +9811,9 @@ var TurnCommitter = class {
|
|
|
10053
9811
|
// Renders each `TurnEvent` onto the pump's callback sink (tool-log truncation),
|
|
10054
9812
|
// routing a throwing commit to `onError`.
|
|
10055
9813
|
emit;
|
|
10056
|
-
//
|
|
10057
|
-
//
|
|
10058
|
-
//
|
|
10059
|
-
// POST) is routed to the error hook and swallowed — a failed commit is
|
|
10060
|
-
// warn-only and must never fail the turn (the role the classification's
|
|
10061
|
-
// `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.
|
|
10062
9817
|
async ingestEvent(event) {
|
|
10063
9818
|
if (event.type === "session" || event.type === "result") return;
|
|
10064
9819
|
try {
|
|
@@ -10237,18 +9992,15 @@ var TurnExecution = class {
|
|
|
10237
9992
|
turnLog;
|
|
10238
9993
|
abortController = new AbortController();
|
|
10239
9994
|
outcome = initialOutcome();
|
|
10240
|
-
//
|
|
10241
|
-
//
|
|
10242
|
-
// startup failure has nothing to reap, and reaping is a no-op rather than a
|
|
10243
|
-
// 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.
|
|
10244
9997
|
containment = null;
|
|
10245
9998
|
seqCounter = 0;
|
|
10246
|
-
//
|
|
10247
|
-
//
|
|
10248
|
-
// 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.
|
|
10249
10001
|
nextSeq = () => ++this.seqCounter;
|
|
10250
|
-
//
|
|
10251
|
-
//
|
|
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.
|
|
10252
10004
|
resumed;
|
|
10253
10005
|
resumedFromSeq;
|
|
10254
10006
|
// Whether the span this resumed run continues already has first words —
|
|
@@ -10256,8 +10008,8 @@ var TurnExecution = class {
|
|
|
10256
10008
|
resumedSpoke = false;
|
|
10257
10009
|
turnContext;
|
|
10258
10010
|
resolvedMcpServers;
|
|
10259
|
-
//
|
|
10260
|
-
//
|
|
10011
|
+
// The harness's secret environment for this turn. Only ever handed to the
|
|
10012
|
+
// adapter; never logged, never merged into `process.env`.
|
|
10261
10013
|
secretEnv = {};
|
|
10262
10014
|
effectiveCwd;
|
|
10263
10015
|
hookEnv;
|
|
@@ -10271,19 +10023,15 @@ var TurnExecution = class {
|
|
|
10271
10023
|
turnControlIntentFetched = false;
|
|
10272
10024
|
applyRecordedTurnControlIntent = async () => {
|
|
10273
10025
|
};
|
|
10274
|
-
//
|
|
10275
|
-
//
|
|
10276
|
-
//
|
|
10277
|
-
// 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.
|
|
10278
10029
|
noteCommitFailed = () => {
|
|
10279
10030
|
};
|
|
10280
10031
|
disarmWatchdogs = () => {
|
|
10281
10032
|
};
|
|
10282
|
-
//
|
|
10283
|
-
//
|
|
10284
|
-
// swallowed, because a turn that produced a good answer must not be recorded as
|
|
10285
|
-
// failed on account of a leftover process, and the failure is visible in the log
|
|
10286
|
-
// 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.
|
|
10287
10035
|
async reapContainment() {
|
|
10288
10036
|
const containment = this.containment;
|
|
10289
10037
|
if (!containment) return;
|
|
@@ -10309,20 +10057,18 @@ var TurnExecution = class {
|
|
|
10309
10057
|
);
|
|
10310
10058
|
}
|
|
10311
10059
|
}
|
|
10312
|
-
//
|
|
10313
|
-
//
|
|
10314
|
-
// 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
|
|
10315
10062
|
// participant flag.
|
|
10316
10063
|
admitted = false;
|
|
10317
|
-
//
|
|
10064
|
+
// One recompose per turn when the lease refuses a stale context.
|
|
10318
10065
|
recomposedForLease = false;
|
|
10319
|
-
//
|
|
10320
|
-
//
|
|
10321
|
-
//
|
|
10322
|
-
//
|
|
10323
|
-
// 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.
|
|
10324
10070
|
executionSourceId;
|
|
10325
|
-
//
|
|
10071
|
+
// One fresh fetch per turn when adoption is refused.
|
|
10326
10072
|
refetchedForAdoption = false;
|
|
10327
10073
|
concluded(reason, errorReason) {
|
|
10328
10074
|
return new TurnConcluded(reason, errorReason);
|
|
@@ -10390,21 +10136,18 @@ var TurnExecution = class {
|
|
|
10390
10136
|
this.supervisor.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
10391
10137
|
return { ok: false, durationMs, reason, runtime: this.turnContext?.runtime };
|
|
10392
10138
|
}
|
|
10393
|
-
// The
|
|
10394
|
-
//
|
|
10395
|
-
//
|
|
10396
|
-
//
|
|
10397
|
-
//
|
|
10398
|
-
// rule: a failure names its reason; without one the turn settled — the
|
|
10399
|
-
// runtime-unavailable exit posts its user-facing `final` before concluding,
|
|
10400
|
-
// 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`).
|
|
10401
10144
|
async concludeAdmittedRun(reason, errorReason) {
|
|
10402
10145
|
const { payload, turnLog, startedAt, turnId } = this;
|
|
10403
10146
|
const body = {
|
|
10404
10147
|
activeRunStartedAt: null,
|
|
10405
10148
|
turnId,
|
|
10406
|
-
//
|
|
10407
|
-
//
|
|
10149
|
+
// `runtimeIncomplete` is a real failure that deliberately carries no
|
|
10150
|
+
// `errorReason`, so the inference alone would read it as settled.
|
|
10408
10151
|
outcome: errorReason || this.outcome.runtimeIncomplete ? "failed" : "settled"
|
|
10409
10152
|
};
|
|
10410
10153
|
if (errorReason) body.errorReason = errorReason.slice(0, 200);
|
|
@@ -10498,11 +10241,10 @@ var TurnExecution = class {
|
|
|
10498
10241
|
throw this.concluded(reason, reason);
|
|
10499
10242
|
}
|
|
10500
10243
|
}
|
|
10501
|
-
//
|
|
10502
|
-
//
|
|
10503
|
-
//
|
|
10504
|
-
//
|
|
10505
|
-
// 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.
|
|
10506
10248
|
async adoptSource(context) {
|
|
10507
10249
|
const { payload, turnId, turnLog } = this;
|
|
10508
10250
|
if (!context.issuanceId) {
|
|
@@ -10551,9 +10293,8 @@ var TurnExecution = class {
|
|
|
10551
10293
|
secretEnv,
|
|
10552
10294
|
missing
|
|
10553
10295
|
} = resolveMcpSecrets(
|
|
10554
|
-
//
|
|
10555
|
-
//
|
|
10556
|
-
// 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`.
|
|
10557
10298
|
this.turnContext.mcpServers ?? {},
|
|
10558
10299
|
secretStore,
|
|
10559
10300
|
this.turnContext.secretEnv
|
|
@@ -10666,15 +10407,12 @@ var TurnExecution = class {
|
|
|
10666
10407
|
agentUsername: this.opts.agentUsername,
|
|
10667
10408
|
runtime: this.turnContext.runtime,
|
|
10668
10409
|
hostAccess: this.turnContext.policy.hostFs,
|
|
10669
|
-
//
|
|
10670
|
-
//
|
|
10671
|
-
// 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.
|
|
10672
10412
|
triggerEntryPaths: this.turnContext.conversation.triggerEntryPaths ?? [],
|
|
10673
10413
|
title: this.turnContext.conversation.title,
|
|
10674
|
-
//
|
|
10675
|
-
//
|
|
10676
|
-
// context (for the live feed); this is the first thing to read it as
|
|
10677
|
-
// 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.
|
|
10678
10416
|
messageBody: this.turnContext.message.body
|
|
10679
10417
|
},
|
|
10680
10418
|
{ turnId, deviceId: this.opts.deviceId },
|
|
@@ -10720,37 +10458,30 @@ var TurnExecution = class {
|
|
|
10720
10458
|
payload.agentId,
|
|
10721
10459
|
{
|
|
10722
10460
|
activeRunStartedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10723
|
-
//
|
|
10724
|
-
//
|
|
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.
|
|
10725
10463
|
bootId: this.opts.inRunner?.bootId ?? bootId,
|
|
10726
|
-
// CT1680: the runner holding the span, beside the device's boot id.
|
|
10727
10464
|
...this.opts.inRunner ? { runnerId: this.opts.inRunner.runnerId } : {},
|
|
10728
|
-
//
|
|
10465
|
+
// The durable record that this turn resumed, and from where.
|
|
10729
10466
|
...this.resumedFromSeq !== void 0 ? { resumedFromSeq: this.resumedFromSeq } : {},
|
|
10730
|
-
//
|
|
10731
|
-
//
|
|
10732
|
-
//
|
|
10733
|
-
//
|
|
10734
|
-
// fresh-session recompose (`freshRequest`) deliberately does not touch it:
|
|
10735
|
-
// it adopts only the prompt, never the run-config, so its issuance must
|
|
10736
|
-
// 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.
|
|
10737
10471
|
...this.turnContext.issuanceId ? { issuanceId: this.turnContext.issuanceId } : {},
|
|
10738
|
-
//
|
|
10739
|
-
//
|
|
10740
|
-
// already emitted a "preparing" activity row for this turn above (it
|
|
10741
|
-
// runs before this flip), which would otherwise make the sweep mistake
|
|
10742
|
-
// 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.
|
|
10743
10474
|
turnId,
|
|
10744
10475
|
// The newest message this turn's read covered: the lease records it,
|
|
10745
10476
|
// and settle moves the cursor to it. Omitted when the server sent
|
|
10746
10477
|
// none (an older API), so the cursor stays where it was.
|
|
10747
10478
|
...this.turnContext.readThrough !== void 0 ? { readThrough: this.turnContext.readThrough } : {},
|
|
10748
10479
|
...this.turnContext.readRevision !== void 0 ? { readRevision: this.turnContext.readRevision } : {},
|
|
10749
|
-
//
|
|
10480
|
+
// The attempt this request's session belongs to (null = fresh).
|
|
10750
10481
|
...this.turnContext.attemptId !== void 0 ? { attemptId: this.turnContext.attemptId } : {},
|
|
10751
|
-
//
|
|
10752
|
-
//
|
|
10753
|
-
//
|
|
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.
|
|
10754
10485
|
...payload.episode !== void 0 ? { dispatchMessageId: payload.messageId, dispatchEpisode: payload.episode } : {}
|
|
10755
10486
|
}
|
|
10756
10487
|
);
|
|
@@ -10790,21 +10521,18 @@ var TurnExecution = class {
|
|
|
10790
10521
|
this.request = buildCompanionTurnRequest({
|
|
10791
10522
|
turnContext: this.turnContext,
|
|
10792
10523
|
baseUrl: this.opts.baseUrl,
|
|
10793
|
-
//
|
|
10524
|
+
// Always present: `fetchContext` refuses a context without one.
|
|
10794
10525
|
turnToken: this.turnContext.turnToken,
|
|
10795
|
-
// SJ524: the hook-resolved cwd overrides the static local cwd.
|
|
10796
10526
|
...this.effectiveCwd ? { cwd: this.effectiveCwd } : {},
|
|
10797
|
-
// CT1103: the prepare-hook env plus the authoritative host-access token.
|
|
10798
10527
|
env: this.turnEnv,
|
|
10799
10528
|
mcpServers: this.resolvedMcpServers,
|
|
10800
10529
|
secretEnv: this.secretEnv,
|
|
10801
10530
|
turnControlServer,
|
|
10802
|
-
//
|
|
10803
|
-
//
|
|
10531
|
+
// Sent as the active-conversation header so a cross-conversation post
|
|
10532
|
+
// stamps its origin.
|
|
10804
10533
|
activeConversationId: payload.conversationId,
|
|
10805
|
-
// Workspace coordinate retained in the compatible TurnRequest envelope.
|
|
10806
10534
|
workspaceId,
|
|
10807
|
-
//
|
|
10535
|
+
// The operator's machine-local auto-memory escape hatch, when set.
|
|
10808
10536
|
...this.opts.local.claudeCode ? { claudeCode: this.opts.local.claudeCode } : {}
|
|
10809
10537
|
});
|
|
10810
10538
|
}
|
|
@@ -10841,8 +10569,8 @@ var TurnExecution = class {
|
|
|
10841
10569
|
if (this.opts.claudeCodeAvailable?.() ?? true) {
|
|
10842
10570
|
this.containment = createTurnContainment(this.turnId, {
|
|
10843
10571
|
log: (msg, meta) => turnLog.debug(meta ?? {}, msg),
|
|
10844
|
-
//
|
|
10845
|
-
//
|
|
10572
|
+
// A group outside the runner's own is reported, so a launcher can stop
|
|
10573
|
+
// it if the runner dies without reaping it.
|
|
10846
10574
|
...this.opts.inRunner?.onProcessGroup ? { onProcessGroup: this.opts.inRunner.onProcessGroup } : {}
|
|
10847
10575
|
});
|
|
10848
10576
|
const containment = this.containment;
|
|
@@ -10863,8 +10591,8 @@ var TurnExecution = class {
|
|
|
10863
10591
|
const serverUrl = this.opts.opencodeServerUrl;
|
|
10864
10592
|
adapters.push(
|
|
10865
10593
|
createOpencodeAdapter(
|
|
10866
|
-
//
|
|
10867
|
-
//
|
|
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.
|
|
10868
10596
|
this.opts.inRunner ? {
|
|
10869
10597
|
transport: this.opts.inRunner.opencodeTransport(serverUrl),
|
|
10870
10598
|
onWarn
|
|
@@ -10911,7 +10639,7 @@ var TurnExecution = class {
|
|
|
10911
10639
|
kind: "runtime_notice",
|
|
10912
10640
|
turnId,
|
|
10913
10641
|
parentMessageId: payload.messageId,
|
|
10914
|
-
//
|
|
10642
|
+
// Written by this attempt's source, like its speech.
|
|
10915
10643
|
...this.executionSourceId !== void 0 ? { executionSourceId: this.executionSourceId } : {}
|
|
10916
10644
|
});
|
|
10917
10645
|
} catch (postErr) {
|
|
@@ -10925,16 +10653,13 @@ var TurnExecution = class {
|
|
|
10925
10653
|
throw this.concluded(`runtime_unavailable:${err.runtime}`);
|
|
10926
10654
|
}
|
|
10927
10655
|
}
|
|
10928
|
-
// The adapter refused to resume
|
|
10929
|
-
//
|
|
10930
|
-
//
|
|
10931
|
-
//
|
|
10932
|
-
//
|
|
10933
|
-
//
|
|
10934
|
-
//
|
|
10935
|
-
// The lease already granted stands; the read reaches at least its mark. A
|
|
10936
|
-
// fetch that fails returns null and the adapter falls back to the request
|
|
10937
|
-
// 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.
|
|
10938
10663
|
async freshRequest(reason) {
|
|
10939
10664
|
const { payload, turnId, turnLog } = this;
|
|
10940
10665
|
try {
|
|
@@ -10945,9 +10670,8 @@ var TurnExecution = class {
|
|
|
10945
10670
|
false,
|
|
10946
10671
|
true,
|
|
10947
10672
|
void 0,
|
|
10948
|
-
//
|
|
10949
|
-
//
|
|
10950
|
-
// 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.
|
|
10951
10675
|
this.abortController.signal
|
|
10952
10676
|
);
|
|
10953
10677
|
turnLog.debug(
|
|
@@ -10999,19 +10723,17 @@ var TurnExecution = class {
|
|
|
10999
10723
|
conversationId: payload.conversationId,
|
|
11000
10724
|
agentId: payload.agentId,
|
|
11001
10725
|
turnId,
|
|
11002
|
-
//
|
|
11003
|
-
//
|
|
10726
|
+
// The message this turn answers, on every commit so the reply records the
|
|
10727
|
+
// Q→A edge.
|
|
11004
10728
|
parentMessageId: payload.messageId,
|
|
11005
10729
|
signal: abortController.signal,
|
|
11006
10730
|
log: turnLog,
|
|
11007
10731
|
nextSeq: this.nextSeq,
|
|
11008
10732
|
spoke: this.resumedSpoke,
|
|
11009
|
-
//
|
|
10733
|
+
// Admitted by now, so this is the source the model's work runs on.
|
|
11010
10734
|
...this.executionSourceId !== void 0 ? { executionSourceId: this.executionSourceId } : {},
|
|
11011
|
-
//
|
|
11012
|
-
// `noteCommitFailed
|
|
11013
|
-
// arms (it starts as a no-op) — the committer only fires it from inside
|
|
11014
|
-
// 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`.
|
|
11015
10737
|
onCommitFailed: (err) => this.noteCommitFailed(err)
|
|
11016
10738
|
});
|
|
11017
10739
|
const usesHttpTurnControl = turnRuntime === "codex" || turnRuntime === "opencode";
|
|
@@ -11115,9 +10837,9 @@ var TurnExecution = class {
|
|
|
11115
10837
|
workspaceId,
|
|
11116
10838
|
payload.conversationId,
|
|
11117
10839
|
payload.agentId,
|
|
11118
|
-
//
|
|
11119
|
-
//
|
|
11120
|
-
//
|
|
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.
|
|
11121
10843
|
{ agentSessionId: event.state, turnId }
|
|
11122
10844
|
);
|
|
11123
10845
|
} catch (err) {
|
|
@@ -11222,9 +10944,7 @@ var TurnExecution = class {
|
|
|
11222
10944
|
}
|
|
11223
10945
|
const body = {
|
|
11224
10946
|
activeRunStartedAt: null,
|
|
11225
|
-
//
|
|
11226
|
-
// off this turn id, so the settle PATCH must carry it — the start PATCH
|
|
11227
|
-
// 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.
|
|
11228
10948
|
turnId
|
|
11229
10949
|
};
|
|
11230
10950
|
if (o.turnUsage) {
|
|
@@ -11392,11 +11112,11 @@ var Dispatcher = class {
|
|
|
11392
11112
|
this.aborts = opts.aborts ?? /* @__PURE__ */ new Map();
|
|
11393
11113
|
}
|
|
11394
11114
|
opts;
|
|
11395
|
-
//
|
|
11396
|
-
//
|
|
11115
|
+
// (conversation|agent) → per-TURN controllers. Stop is pair-scoped and must
|
|
11116
|
+
// reach every live loop for the pair.
|
|
11397
11117
|
aborts;
|
|
11398
|
-
//
|
|
11399
|
-
//
|
|
11118
|
+
// Pairs already told their session couldn't be saved: the notice is once per
|
|
11119
|
+
// pair, while the rejection repeats every turn.
|
|
11400
11120
|
sessionWriteNotified = /* @__PURE__ */ new Set();
|
|
11401
11121
|
notifyStart(info) {
|
|
11402
11122
|
try {
|
|
@@ -11413,11 +11133,6 @@ var Dispatcher = class {
|
|
|
11413
11133
|
// The supervisor supplies a stable turnId on both fresh and resumed dispatch.
|
|
11414
11134
|
// resumed is separate: only replay seeds sequence/speech recovery from the
|
|
11415
11135
|
// prior span. A direct caller omitting turnId gets a fresh id.
|
|
11416
|
-
//
|
|
11417
|
-
// CT1261: a short `handle` creates a `TurnExecution` and runs it. The
|
|
11418
|
-
// supervisor seam hands the execution exactly the three cross-turn duties —
|
|
11419
|
-
// the abort registry, the session-write notice latch, the observer — and
|
|
11420
|
-
// nothing else.
|
|
11421
11136
|
async handle(payload, opts = {}) {
|
|
11422
11137
|
const key = runKey(payload.conversationId, payload.agentId);
|
|
11423
11138
|
const supervisor = {
|
|
@@ -11463,8 +11178,8 @@ var Dispatcher = class {
|
|
|
11463
11178
|
}
|
|
11464
11179
|
return new TurnExecution(this.opts, supervisor, payload, opts).run();
|
|
11465
11180
|
}
|
|
11466
|
-
//
|
|
11467
|
-
//
|
|
11181
|
+
// Cancel a (conversation, agent) run in flight in THIS process. True if one
|
|
11182
|
+
// was aborted.
|
|
11468
11183
|
cancel(conversationId, agentId) {
|
|
11469
11184
|
const key = runKey(conversationId, agentId);
|
|
11470
11185
|
const pair = this.aborts.get(key);
|
|
@@ -13161,7 +12876,7 @@ var Outbox = class {
|
|
|
13161
12876
|
}
|
|
13162
12877
|
workspaceId;
|
|
13163
12878
|
log;
|
|
13164
|
-
// Resolved lazily (per call
|
|
12879
|
+
// Resolved lazily (per call) so tests that swap HOME between
|
|
13165
12880
|
// cases route writes at the right tmpdir.
|
|
13166
12881
|
dir() {
|
|
13167
12882
|
return join20(runnerStateDir(), "outbox", encodeURIComponent(this.workspaceId));
|
|
@@ -13227,7 +12942,7 @@ var Outbox = class {
|
|
|
13227
12942
|
);
|
|
13228
12943
|
return entries;
|
|
13229
12944
|
}
|
|
13230
|
-
//
|
|
12945
|
+
// The highest `seq` this turn has queued but not yet delivered — the
|
|
13231
12946
|
// half of a resumed turn's seq floor the SERVER CANNOT SEE. A commit that
|
|
13232
12947
|
// failed transiently before the crash is sitting in this directory with its
|
|
13233
12948
|
// seq already spent, and the restart's drain is fire-and-forget
|
|
@@ -13451,24 +13166,20 @@ var CompanionSupervisor = class {
|
|
|
13451
13166
|
config;
|
|
13452
13167
|
log;
|
|
13453
13168
|
hub;
|
|
13454
|
-
//
|
|
13455
|
-
//
|
|
13456
|
-
//
|
|
13457
|
-
// 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.
|
|
13458
13172
|
resolveBundled;
|
|
13459
|
-
//
|
|
13460
|
-
//
|
|
13461
|
-
// 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.
|
|
13462
13175
|
loggedBundledStatus = /* @__PURE__ */ new Map();
|
|
13463
|
-
//
|
|
13464
|
-
//
|
|
13465
|
-
// 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.
|
|
13466
13178
|
harnessVersions;
|
|
13467
|
-
//
|
|
13468
|
-
//
|
|
13469
|
-
//
|
|
13470
|
-
//
|
|
13471
|
-
// 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.
|
|
13472
13183
|
harnessSignals = null;
|
|
13473
13184
|
// Fresh per attempt, never the cached heartbeat signal: installing a harness
|
|
13474
13185
|
// between attempts must be observed immediately.
|
|
@@ -13481,8 +13192,8 @@ var CompanionSupervisor = class {
|
|
|
13481
13192
|
unrunnableRetryDelaysMs;
|
|
13482
13193
|
deviceApi = null;
|
|
13483
13194
|
deviceSub = null;
|
|
13484
|
-
//
|
|
13485
|
-
//
|
|
13195
|
+
// Whether the device stream is open, which is what every workspace's link on
|
|
13196
|
+
// the dashboard reports.
|
|
13486
13197
|
deviceConnected = false;
|
|
13487
13198
|
recoveryChecked = false;
|
|
13488
13199
|
recovering = false;
|
|
@@ -13494,8 +13205,7 @@ var CompanionSupervisor = class {
|
|
|
13494
13205
|
pollTimer = null;
|
|
13495
13206
|
refreshing = false;
|
|
13496
13207
|
stopped = false;
|
|
13497
|
-
//
|
|
13498
|
-
// on every 30s heartbeat.
|
|
13208
|
+
// The version-skew warning is logged once, not every heartbeat.
|
|
13499
13209
|
versionSkewWarned = false;
|
|
13500
13210
|
// This device's id, captured from the heartbeat / assignments response. The SSE
|
|
13501
13211
|
// router uses it to tell a dispatch addressed to THIS device (a freshly-assigned
|
|
@@ -13505,12 +13215,12 @@ var CompanionSupervisor = class {
|
|
|
13505
13215
|
// The in-flight assignments pull, exposed so a caller that needs fresh data
|
|
13506
13216
|
// (pull-on-miss) can `await` the running pull rather than racing past it.
|
|
13507
13217
|
inFlightRefresh = null;
|
|
13508
|
-
//
|
|
13509
|
-
//
|
|
13510
|
-
//
|
|
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.
|
|
13511
13221
|
connectorHealth = new ConnectorHealthStore();
|
|
13512
|
-
//
|
|
13513
|
-
//
|
|
13222
|
+
// Which dispatches this process is running, so a redelivered frame doesn't
|
|
13223
|
+
// run the turn a second time.
|
|
13514
13224
|
claims = new DispatchClaims();
|
|
13515
13225
|
constructor(opts) {
|
|
13516
13226
|
this.config = opts.config;
|
|
@@ -13531,10 +13241,10 @@ var CompanionSupervisor = class {
|
|
|
13531
13241
|
}
|
|
13532
13242
|
this.latchBootBundledState();
|
|
13533
13243
|
}
|
|
13534
|
-
//
|
|
13535
|
-
//
|
|
13244
|
+
// What a runner container said it can run, probed once at start; null until
|
|
13245
|
+
// then, and when no container could be started.
|
|
13536
13246
|
runnerImage = null;
|
|
13537
|
-
//
|
|
13247
|
+
// The companion every container mounts: this one's own install.
|
|
13538
13248
|
runnerCompanion() {
|
|
13539
13249
|
return companionSource({
|
|
13540
13250
|
configured: this.config.runners?.docker?.companionDir,
|
|
@@ -13646,10 +13356,7 @@ var CompanionSupervisor = class {
|
|
|
13646
13356
|
this.pollTimer = setInterval(() => void this.refreshAssignments(), ASSIGNMENTS_POLL_MS);
|
|
13647
13357
|
this.pollTimer.unref?.();
|
|
13648
13358
|
}
|
|
13649
|
-
//
|
|
13650
|
-
// keep the foreground process alive. With the poll loop running there's always
|
|
13651
|
-
// at least the loops keeping the event loop alive; the SSE promises let a
|
|
13652
|
-
// clean teardown resolve.
|
|
13359
|
+
// start.ts awaits these; they resolve on a clean teardown.
|
|
13653
13360
|
finishedPromises() {
|
|
13654
13361
|
return [...this.deviceSub ? [this.deviceSub.finished] : []];
|
|
13655
13362
|
}
|
|
@@ -13677,39 +13384,26 @@ var CompanionSupervisor = class {
|
|
|
13677
13384
|
bootId,
|
|
13678
13385
|
version: COMPANION_VERSION,
|
|
13679
13386
|
exposedSecretNames: store.names(),
|
|
13680
|
-
//
|
|
13681
|
-
// CT1379 claude-code and codex when the SDK's own bundled binary is there
|
|
13682
|
-
// and the user asked for the harness, CT270 opencode when the operator
|
|
13683
|
-
// configured an `opencode serve`.
|
|
13387
|
+
// Each runtime only when this device can actually run it.
|
|
13684
13388
|
manifest: buildCompanionManifest({
|
|
13685
|
-
//
|
|
13686
|
-
//
|
|
13687
|
-
// the user's `claudeCode.enabled`. Claude Code rides neither alone.
|
|
13389
|
+
// Consented (`claudeCode.enabled`) AND launchable (the live bundled
|
|
13390
|
+
// binary). Neither alone.
|
|
13688
13391
|
claudeCode: this.claudeCodeOffered(),
|
|
13689
13392
|
opencode: !!this.config.opencode?.serverUrl,
|
|
13690
|
-
//
|
|
13691
|
-
//
|
|
13692
|
-
// which is how a device with a half-installed 331 MB platform package
|
|
13693
|
-
// 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.
|
|
13694
13395
|
codex: this.codexOffered(),
|
|
13695
|
-
//
|
|
13696
|
-
// above. Diagnostic only — the server never routes on it — and always
|
|
13396
|
+
// WHY an asked-for harness is absent above. Diagnostic only, and always
|
|
13697
13397
|
// sent, so an empty array clears a previously reported issue.
|
|
13698
13398
|
runtimeIssues: this.runtimeIssues(),
|
|
13699
|
-
//
|
|
13700
|
-
// (fail-soft to null). Informational only — the server matches on name.
|
|
13399
|
+
// Informational only; the server matches on name.
|
|
13701
13400
|
versions: this.harnessVersions,
|
|
13702
|
-
//
|
|
13401
|
+
// Shown on the device page.
|
|
13703
13402
|
...runnersEnabled(this.config) ? { runner: this.runnerManifest() } : {}
|
|
13704
13403
|
}),
|
|
13705
|
-
//
|
|
13706
|
-
//
|
|
13707
|
-
// failure/heal, so a fresh companion's beat is unchanged and the server's
|
|
13708
|
-
// 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.
|
|
13709
13406
|
...connectorReports.length > 0 ? { connectors: connectorReports } : {},
|
|
13710
|
-
// CT584: include enumerated models only when the probe SUCCEEDED (non-null).
|
|
13711
|
-
// A null (probe failed / no opencode) omits the field, and the server then
|
|
13712
|
-
// leaves this device's stored availability untouched.
|
|
13713
13407
|
...opencodeModels !== null ? { models: opencodeModels } : {}
|
|
13714
13408
|
});
|
|
13715
13409
|
this.hub.setDevice({ deviceId: res.deviceId });
|
|
@@ -13724,11 +13418,8 @@ var CompanionSupervisor = class {
|
|
|
13724
13418
|
);
|
|
13725
13419
|
}
|
|
13726
13420
|
}
|
|
13727
|
-
//
|
|
13728
|
-
//
|
|
13729
|
-
// companion's, warn loudly (once). This is exactly the skew that bit the M1 walk:
|
|
13730
|
-
// an npm-pinned companion lagging a from-develop server. The elimination (bundled
|
|
13731
|
-
// 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.
|
|
13732
13423
|
checkVersionSkew(serverVersion) {
|
|
13733
13424
|
if (this.versionSkewWarned) return;
|
|
13734
13425
|
if (!serverVersion || serverVersion === COMPANION_VERSION) return;
|
|
@@ -13925,10 +13616,9 @@ var CompanionSupervisor = class {
|
|
|
13925
13616
|
`Stopped running ${runner.displayName} (unassigned)`
|
|
13926
13617
|
);
|
|
13927
13618
|
}
|
|
13928
|
-
//
|
|
13929
|
-
//
|
|
13930
|
-
//
|
|
13931
|
-
// 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.
|
|
13932
13622
|
bundledStatus(runtime) {
|
|
13933
13623
|
const cached2 = runtime === "codex" ? this.harnessSignals?.codexBundled : this.harnessSignals?.claudeBundled;
|
|
13934
13624
|
return cached2 ?? this.resolveBundled(runtime).status;
|
|
@@ -13939,23 +13629,19 @@ var CompanionSupervisor = class {
|
|
|
13939
13629
|
bundledResolution(runtime) {
|
|
13940
13630
|
return this.resolveBundled(runtime);
|
|
13941
13631
|
}
|
|
13942
|
-
//
|
|
13943
|
-
//
|
|
13944
|
-
//
|
|
13945
|
-
//
|
|
13946
|
-
// 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.
|
|
13947
13636
|
claudeCodeOffered() {
|
|
13948
13637
|
return isClaudeCodeConnected(this.config) && this.bundledStatus("claude-code") === "present";
|
|
13949
13638
|
}
|
|
13950
|
-
//
|
|
13951
|
-
// alone — so an enabled device with no vendored binary advertised a runtime it
|
|
13952
|
-
// 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.
|
|
13953
13640
|
codexOffered() {
|
|
13954
13641
|
return isCodexEnabled(this.config) && this.bundledStatus("codex") === "present";
|
|
13955
13642
|
}
|
|
13956
|
-
//
|
|
13957
|
-
//
|
|
13958
|
-
// 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.
|
|
13959
13645
|
runtimeIssues() {
|
|
13960
13646
|
return runtimeIssuesFor({
|
|
13961
13647
|
claudeCode: {
|
|
@@ -13965,26 +13651,16 @@ var CompanionSupervisor = class {
|
|
|
13965
13651
|
codex: { intended: isCodexEnabled(this.config), bundled: this.bundledStatus("codex") }
|
|
13966
13652
|
});
|
|
13967
13653
|
}
|
|
13968
|
-
//
|
|
13969
|
-
//
|
|
13970
|
-
//
|
|
13971
|
-
// repro and carries nothing that could be a credential or a vendor's output.
|
|
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.
|
|
13972
13657
|
//
|
|
13973
|
-
//
|
|
13974
|
-
//
|
|
13975
|
-
//
|
|
13976
|
-
//
|
|
13977
|
-
//
|
|
13978
|
-
//
|
|
13979
|
-
// child's output goes to a file. The boot state is latched in the
|
|
13980
|
-
// constructor, so the first refresh reports nothing new; it lands here as a
|
|
13981
|
-
// debug record instead, present for diagnosis without competing for
|
|
13982
|
-
// attention with the warning the operator already read.
|
|
13983
|
-
// - It does not report a transition in generic prose. A binary that goes
|
|
13984
|
-
// missing at 3am is the case the operator will actually meet, and a line
|
|
13985
|
-
// that says only "binary is missing" leaves them with no repair and no
|
|
13986
|
-
// restart instruction. Transitions carry the specified copy verbatim — the
|
|
13987
|
-
// 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.
|
|
13988
13664
|
logBundledTransitions() {
|
|
13989
13665
|
for (const runtime of ["claude-code", "codex"]) {
|
|
13990
13666
|
const intended = runtime === "codex" ? isCodexEnabled(this.config) : isClaudeCodeConnected(this.config);
|
|
@@ -14007,9 +13683,8 @@ var CompanionSupervisor = class {
|
|
|
14007
13683
|
this.log.debug(meta, "companion: bundled harness binary is missing (harness not in use)");
|
|
14008
13684
|
}
|
|
14009
13685
|
}
|
|
14010
|
-
//
|
|
14011
|
-
//
|
|
14012
|
-
// 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.
|
|
14013
13688
|
latchBootBundledState() {
|
|
14014
13689
|
for (const runtime of ["claude-code", "codex"]) {
|
|
14015
13690
|
const resolution = this.resolveBundled(runtime);
|
|
@@ -14027,35 +13702,34 @@ var CompanionSupervisor = class {
|
|
|
14027
13702
|
);
|
|
14028
13703
|
}
|
|
14029
13704
|
}
|
|
14030
|
-
//
|
|
13705
|
+
// The locks every runner on this device shares.
|
|
14031
13706
|
runnerLocks = new DeviceLocks();
|
|
14032
|
-
//
|
|
14033
|
-
//
|
|
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.
|
|
14034
13709
|
sandboxUsable;
|
|
14035
13710
|
sandboxHost = {
|
|
14036
13711
|
osSandboxUsable: () => this.sandboxUsable ??= osSandboxUsable(this.log)
|
|
14037
13712
|
};
|
|
14038
|
-
//
|
|
14039
|
-
//
|
|
13713
|
+
// `runners.policy`, parsed once. An invalid one is kept as the error, which
|
|
13714
|
+
// every sandboxed turn is refused with.
|
|
14040
13715
|
parsedSandboxPolicy;
|
|
14041
13716
|
sandboxPolicy() {
|
|
14042
13717
|
this.parsedSandboxPolicy ??= parseSandboxPolicy(this.config.runners?.policy);
|
|
14043
13718
|
return this.parsedSandboxPolicy;
|
|
14044
13719
|
}
|
|
14045
|
-
//
|
|
13720
|
+
// The walk for other companions' stores, run off the turn path.
|
|
14046
13721
|
storeWatch;
|
|
14047
13722
|
companionStores() {
|
|
14048
13723
|
this.storeWatch ??= companionStoreWatch(this.log);
|
|
14049
13724
|
return this.storeWatch;
|
|
14050
13725
|
}
|
|
14051
|
-
//
|
|
14052
|
-
//
|
|
14053
|
-
//
|
|
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.
|
|
14054
13729
|
runnerSandboxFor(row) {
|
|
14055
13730
|
return runnerSandboxFor(this.config, row, this.sandboxHost);
|
|
14056
13731
|
}
|
|
14057
|
-
//
|
|
14058
|
-
// against the agents it runs right now.
|
|
13732
|
+
// Resolved against the agents this device runs right now.
|
|
14059
13733
|
runnerManifest() {
|
|
14060
13734
|
const agents = [...this.workspaces.values()].flatMap(
|
|
14061
13735
|
(wr) => [...wr.agents.values()].map((agent) => ({
|
|
@@ -14069,7 +13743,7 @@ var CompanionSupervisor = class {
|
|
|
14069
13743
|
...runnerSandboxReport(this.config, agents, this.sandboxHost)
|
|
14070
13744
|
};
|
|
14071
13745
|
}
|
|
14072
|
-
//
|
|
13746
|
+
// One provider for the device, built on first use.
|
|
14073
13747
|
runnerProvider() {
|
|
14074
13748
|
this.providerForRunners ??= this.dockerRunners() ? this.createDockerRunners() : createLocalProcessProvider();
|
|
14075
13749
|
return this.providerForRunners;
|
|
@@ -14111,8 +13785,7 @@ var CompanionSupervisor = class {
|
|
|
14111
13785
|
log: this.log
|
|
14112
13786
|
});
|
|
14113
13787
|
}
|
|
14114
|
-
//
|
|
14115
|
-
// companion in a container, this process's otherwise.
|
|
13788
|
+
// The image's own companion in a container, this process's otherwise.
|
|
14116
13789
|
runnerCommandFor() {
|
|
14117
13790
|
if (this.runnerCommand) return this.runnerCommand;
|
|
14118
13791
|
return this.dockerRunners() ? ["cabane-companion", "run-turn"] : selfRunnerCommand();
|
|
@@ -14144,36 +13817,20 @@ var CompanionSupervisor = class {
|
|
|
14144
13817
|
aborts: ctx.aborts,
|
|
14145
13818
|
runConfig: ctx.runConfig,
|
|
14146
13819
|
log: this.log.child({ agentName: ctx.agentDisplayName ?? ctx.agentUsername }),
|
|
14147
|
-
//
|
|
14148
|
-
//
|
|
14149
|
-
//
|
|
14150
|
-
//
|
|
14151
|
-
// runnable, so a disconnected harness can't be selected either.
|
|
14152
|
-
// CT1379: and the same for the bundled binary — the predicate is shared with
|
|
14153
|
-
// the heartbeat above, so a runtime advertised by one and refused by the
|
|
14154
|
-
// 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.
|
|
14155
13824
|
claudeCodeAvailable: () => this.claudeCodeOffered(),
|
|
14156
|
-
// CT270: the opencode server URL (operator-configured), when this device
|
|
14157
|
-
// offers the opencode runtime. Threaded so an opencode turn selects the
|
|
14158
|
-
// opencode adapter; unset leaves the device claude-code-only.
|
|
14159
13825
|
...this.config.opencode?.serverUrl ? { opencodeServerUrl: this.config.opencode.serverUrl } : {},
|
|
14160
|
-
// CT481: register the codex adapter when this device offers codex; a miss
|
|
14161
|
-
// leaves an `openai/…` turn to fail loudly (no silent claude-code fallback).
|
|
14162
|
-
// CT1379: read PER TURN, like claude-code above, and off the same predicate
|
|
14163
|
-
// the heartbeat uses — it was captured here at dispatcher-construction time
|
|
14164
|
-
// from the enable flag alone, so a device could register an adapter for a
|
|
14165
|
-
// binary it did not have and keep it registered after the binary came back.
|
|
14166
13826
|
codexAvailable: () => this.codexOffered(),
|
|
14167
|
-
//
|
|
14168
|
-
//
|
|
14169
|
-
//
|
|
14170
|
-
// override set here still wins, and is deliberately not validated against
|
|
14171
|
-
// 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.
|
|
14172
13830
|
...positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") !== void 0 ? { idleTimeoutMs: positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") } : {},
|
|
14173
13831
|
...positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") !== void 0 ? { totalTimeoutMs: positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") } : {},
|
|
14174
13832
|
observer: this.hub.observerFor(ctx.workspaceId, ctx.agentId, ctx.workspaceSlug),
|
|
14175
13833
|
transcriptDir: transcriptsDir(),
|
|
14176
|
-
// CT566: the shared connector-health memory the turn records into on settle.
|
|
14177
13834
|
connectorHealth: this.connectorHealth,
|
|
14178
13835
|
...runnersEnabled(this.config) ? {
|
|
14179
13836
|
runner: {
|
|
@@ -14197,11 +13854,10 @@ var CompanionSupervisor = class {
|
|
|
14197
13854
|
this.workspaces.delete(workspaceId);
|
|
14198
13855
|
this.hub.removeWorkspace(workspaceId);
|
|
14199
13856
|
}
|
|
14200
|
-
// ---- SSE event routing (
|
|
14201
|
-
//
|
|
14202
|
-
//
|
|
14203
|
-
//
|
|
14204
|
-
// 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`.
|
|
14205
13861
|
async handleDeviceMessage(ev) {
|
|
14206
13862
|
const wire = this.parseFrame(ev);
|
|
14207
13863
|
if (!wire) return;
|
|
@@ -14237,9 +13893,9 @@ var CompanionSupervisor = class {
|
|
|
14237
13893
|
return null;
|
|
14238
13894
|
}
|
|
14239
13895
|
}
|
|
14240
|
-
//
|
|
14241
|
-
//
|
|
14242
|
-
//
|
|
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.
|
|
14243
13899
|
//
|
|
14244
13900
|
// A cancel needs no gate: `dispatcher.cancel` on an already-aborted run is a
|
|
14245
13901
|
// no-op that reports `false`, so a second copy is inert.
|
|
@@ -14255,8 +13911,6 @@ var CompanionSupervisor = class {
|
|
|
14255
13911
|
if (claim) this.claims.release(wire.id);
|
|
14256
13912
|
}
|
|
14257
13913
|
}
|
|
14258
|
-
// Everything below here is the routing as it was: it takes a frame and knows
|
|
14259
|
-
// nothing about which stream carried it.
|
|
14260
13914
|
async routeFrame(wr, wire) {
|
|
14261
13915
|
if (wire.type === "device:cancel_requested") {
|
|
14262
13916
|
const payload2 = {
|
|
@@ -14322,17 +13976,12 @@ var CompanionSupervisor = class {
|
|
|
14322
13976
|
await tail;
|
|
14323
13977
|
if (wr.chains.get(chainKey) === tail) wr.chains.delete(chainKey);
|
|
14324
13978
|
}
|
|
14325
|
-
//
|
|
14326
|
-
//
|
|
13979
|
+
// Tell the server this device cannot run a dispatch it was addressed to.
|
|
14327
13980
|
// Returns whether the report LANDED. The server checks the recorded dispatch
|
|
14328
|
-
// destination even if the
|
|
14329
|
-
// explains the roster miss; the thirty-minute unanswered-offer deadline is
|
|
14330
|
-
// only the backstop when no report lands.
|
|
13981
|
+
// destination even if the assignment has moved.
|
|
14331
13982
|
//
|
|
14332
|
-
//
|
|
14333
|
-
//
|
|
14334
|
-
// hard — this runs inline on the event loop, and an unbounded wait here would
|
|
14335
|
-
// 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.
|
|
14336
13985
|
async reportUnrunnableDispatch(workspaceId, payload) {
|
|
14337
13986
|
if (!this.deviceApi) return false;
|
|
14338
13987
|
let lastErr;
|
|
@@ -14372,9 +14021,8 @@ var CompanionSupervisor = class {
|
|
|
14372
14021
|
);
|
|
14373
14022
|
return false;
|
|
14374
14023
|
}
|
|
14375
|
-
//
|
|
14376
|
-
//
|
|
14377
|
-
// 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.
|
|
14378
14026
|
async reportRunnerGone(report) {
|
|
14379
14027
|
if (!this.deviceApi) return;
|
|
14380
14028
|
let lastErr;
|
|
@@ -14399,12 +14047,9 @@ var CompanionSupervisor = class {
|
|
|
14399
14047
|
"Couldn't tell Cabane a turn's runner stopped; its credential expires on its own."
|
|
14400
14048
|
);
|
|
14401
14049
|
}
|
|
14402
|
-
// Pull-on-miss
|
|
14403
|
-
//
|
|
14404
|
-
//
|
|
14405
|
-
// and return the now-live runner. Returns undefined when the dispatch isn't for
|
|
14406
|
-
// this device, or the agent still isn't ours after pulling (a genuine
|
|
14407
|
-
// 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.
|
|
14408
14053
|
async recoverRacedAgent(wr, payload) {
|
|
14409
14054
|
if (!this.deviceId || payload.deviceId !== this.deviceId) return void 0;
|
|
14410
14055
|
if (this.refreshing && this.inFlightRefresh) await this.inFlightRefresh;
|
|
@@ -14445,9 +14090,8 @@ var CompanionSupervisor = class {
|
|
|
14445
14090
|
};
|
|
14446
14091
|
const tail = this.runDispatch(
|
|
14447
14092
|
wr,
|
|
14448
|
-
//
|
|
14449
|
-
//
|
|
14450
|
-
// path's own guard.
|
|
14093
|
+
// Off `/runs`, not a frame, so no dispatch id to dedupe on;
|
|
14094
|
+
// `recoveredTurns` is this path's guard.
|
|
14451
14095
|
"",
|
|
14452
14096
|
payload,
|
|
14453
14097
|
agent,
|
|
@@ -14469,9 +14113,7 @@ var CompanionSupervisor = class {
|
|
|
14469
14113
|
this.recovering = false;
|
|
14470
14114
|
}
|
|
14471
14115
|
}
|
|
14472
|
-
// One turn: the
|
|
14473
|
-
// the console summary. Mirrors the old per-workspace `handleEvent`, now
|
|
14474
|
-
// scoped to one agent.
|
|
14116
|
+
// One turn: the skip-vs-resume dedupe, the dispatch, then the console summary.
|
|
14475
14117
|
async runDispatch(wr, dispatchId, payload, agent, recoveredTurnId) {
|
|
14476
14118
|
const workspaceId = wr.workspaceId;
|
|
14477
14119
|
if (dispatchId && hasDispatched(workspaceId, dispatchId)) {
|
|
@@ -14556,7 +14198,7 @@ var CompanionSupervisor = class {
|
|
|
14556
14198
|
);
|
|
14557
14199
|
}
|
|
14558
14200
|
}
|
|
14559
|
-
// ---- per-agent outbox drain scheduler
|
|
14201
|
+
// ---- per-agent outbox drain scheduler ----
|
|
14560
14202
|
makeDrain(api, agentId) {
|
|
14561
14203
|
let drainTimer;
|
|
14562
14204
|
let drainDelay = DRAIN_BASE_MS;
|
|
@@ -14625,19 +14267,16 @@ var CompanionSupervisor = class {
|
|
|
14625
14267
|
saveConfig(next);
|
|
14626
14268
|
configureLogger(this.log, next);
|
|
14627
14269
|
}
|
|
14628
|
-
// ---- harnesses
|
|
14629
|
-
// Probe all three harnesses, cache
|
|
14630
|
-
//
|
|
14631
|
-
//
|
|
14632
|
-
// wrapped anyway so a harness refresh can never take a heartbeat down. Called on
|
|
14633
|
-
// 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.
|
|
14634
14274
|
async refreshHarnessStatuses(presence = {}) {
|
|
14635
14275
|
try {
|
|
14636
14276
|
const signals = await probeHarnessSignals(this.config, {
|
|
14637
14277
|
probePresence: this.probePresence,
|
|
14638
14278
|
presence,
|
|
14639
|
-
//
|
|
14640
|
-
// 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.
|
|
14641
14280
|
resolveBundled: this.resolveBundled
|
|
14642
14281
|
});
|
|
14643
14282
|
this.harnessSignals = signals;
|
|
@@ -14660,11 +14299,10 @@ var CompanionSupervisor = class {
|
|
|
14660
14299
|
async recheckHarnesses() {
|
|
14661
14300
|
await this.refreshHarnessStatuses();
|
|
14662
14301
|
}
|
|
14663
|
-
//
|
|
14664
|
-
//
|
|
14665
|
-
//
|
|
14666
|
-
//
|
|
14667
|
-
// 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.
|
|
14668
14306
|
async heartbeatNow() {
|
|
14669
14307
|
if (this.inFlightHeartbeat) {
|
|
14670
14308
|
await this.inFlightHeartbeat;
|
|
@@ -14673,35 +14311,25 @@ var CompanionSupervisor = class {
|
|
|
14673
14311
|
this.kickHeartbeat();
|
|
14674
14312
|
if (this.inFlightHeartbeat) await this.inFlightHeartbeat;
|
|
14675
14313
|
}
|
|
14676
|
-
//
|
|
14677
|
-
//
|
|
14678
|
-
// against the URL/flag that was just persisted, not the one this process booted
|
|
14679
|
-
// 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.
|
|
14680
14316
|
currentConfig() {
|
|
14681
14317
|
return this.config;
|
|
14682
14318
|
}
|
|
14683
|
-
//
|
|
14684
|
-
// `~/.cabane/config.json
|
|
14685
|
-
// 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).
|
|
14686
14321
|
//
|
|
14687
|
-
//
|
|
14688
|
-
//
|
|
14689
|
-
//
|
|
14690
|
-
//
|
|
14691
|
-
//
|
|
14692
|
-
//
|
|
14693
|
-
// -
|
|
14694
|
-
//
|
|
14695
|
-
//
|
|
14696
|
-
//
|
|
14697
|
-
//
|
|
14698
|
-
// - opencode: set `opencode.serverUrl` — but ONLY after health-probing the URL,
|
|
14699
|
-
// so we never advertise a serve that isn't there. An unreachable URL is a
|
|
14700
|
-
// typed failure the UI shows in place, not a silent bad write.
|
|
14701
|
-
// On success, persist, re-probe, and beat immediately, so a runtime that IS
|
|
14702
|
-
// launchable starts taking turns at once rather than on the next tick — and one
|
|
14703
|
-
// that isn't reports its issue just as promptly. Returns a typed outcome (never
|
|
14704
|
-
// 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.
|
|
14705
14333
|
async enableHarness(input) {
|
|
14706
14334
|
let next;
|
|
14707
14335
|
let checkedPresence = null;
|
|
@@ -14752,9 +14380,8 @@ var CompanionSupervisor = class {
|
|
|
14752
14380
|
return { ok: true };
|
|
14753
14381
|
}
|
|
14754
14382
|
// Re-create every running agent's Dispatcher from the CURRENT config, keeping
|
|
14755
|
-
// the runner (its api
|
|
14756
|
-
//
|
|
14757
|
-
// 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.
|
|
14758
14385
|
rebuildDispatchers() {
|
|
14759
14386
|
for (const wr of this.workspaces.values()) {
|
|
14760
14387
|
for (const runner of wr.agents.values()) {
|
|
@@ -14918,8 +14545,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
14918
14545
|
}));
|
|
14919
14546
|
await warnAboutHarnessReadiness(cfg, {
|
|
14920
14547
|
probeClaude: async () => claudeCode,
|
|
14921
|
-
//
|
|
14922
|
-
// 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
|
|
14923
14549
|
// sink that logs instead. Every other caller keeps the stderr line.
|
|
14924
14550
|
...opts.onReadinessWarning ? { warn: opts.onReadinessWarning } : {}
|
|
14925
14551
|
});
|
|
@@ -14956,11 +14582,10 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
14956
14582
|
const clearRuntimeOnExit = () => clearRuntimeStateIfOurs(instanceId);
|
|
14957
14583
|
process.on("exit", clearRuntimeOnExit);
|
|
14958
14584
|
const hub = new CompanionStateHub({
|
|
14959
|
-
//
|
|
14960
|
-
// with. The dashboard's connection line shows it.
|
|
14585
|
+
// The dashboard's connection line shows it.
|
|
14961
14586
|
baseUrl: cfg.baseUrl,
|
|
14962
14587
|
companionVersion: COMPANION_VERSION,
|
|
14963
|
-
//
|
|
14588
|
+
// Surfaced on `/api/status` so `stop`/`status` can confirm the
|
|
14964
14589
|
// process behind the marker pid is this companion (not a recycled pid).
|
|
14965
14590
|
instanceId
|
|
14966
14591
|
});
|
|
@@ -15013,7 +14638,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
15013
14638
|
socket: control.path,
|
|
15014
14639
|
...dashboard ? { url: dashboard.url, port: dashboard.port } : {},
|
|
15015
14640
|
startedAt,
|
|
15016
|
-
//
|
|
14641
|
+
// The daemon launcher sets this env on the detached child, so the
|
|
15017
14642
|
// marker records whether this companion is backgrounded (foreground start
|
|
15018
14643
|
// leaves it unset → false).
|
|
15019
14644
|
daemon: process.env.CABANE_COMPANION_DAEMON === "1",
|