@cabane/companion 0.6.144 → 0.6.146
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +650 -1022
- package/dist/pairing-config.js +56 -95
- package/dist/runtime.js +644 -1014
- package/dist/static/app.js +9 -11
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -110,18 +110,15 @@ var init_prepare_hook = __esm({
|
|
|
110
110
|
// default is generous; a hook that hangs past this is killed and the turn
|
|
111
111
|
// fails with a clear timeout message rather than pinning the companion.
|
|
112
112
|
timeoutMs: z.number().int().positive().optional(),
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
113
|
+
// Re-invoke the hook when a prepared result already exists, in VALIDATE
|
|
114
|
+
// mode: the cached result is handed back as `prepared` and stdout is
|
|
115
|
+
// ignored, so the cwd cannot move. Its only power is to exit non-zero and
|
|
116
|
+
// fail the turn. The hook resolves once per (conversation, agent), so a
|
|
117
|
+
// later message asking for a different environment is otherwise silently
|
|
118
|
+
// ignored; this lets a hook refuse it out loud.
|
|
117
119
|
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
// this, it is silently ignored — the host knows and says nothing, which is
|
|
121
|
-
// the worst available outcome. An opt-in hook can now refuse it out loud.
|
|
122
|
-
//
|
|
123
|
-
// Default OFF, and deliberately: an operator hook that provisions
|
|
124
|
-
// unconditionally must not start running on every turn because it upgraded.
|
|
120
|
+
// Default OFF: a hook that provisions unconditionally must not start
|
|
121
|
+
// running on every turn because the companion upgraded.
|
|
125
122
|
validateCached: z.boolean().optional(),
|
|
126
123
|
// Independent operator-owned reporter. Receives sanitized failure metadata
|
|
127
124
|
// on stdin; never re-enters this hook and cannot change the turn outcome.
|
|
@@ -160,16 +157,15 @@ var init_prepare_hook = __esm({
|
|
|
160
157
|
CABANE_AGENT_USERNAME: input.agentUsername,
|
|
161
158
|
CABANE_RUNTIME: input.runtime ?? "",
|
|
162
159
|
CABANE_HOST_ACCESS: input.hostAccess ? "1" : "0",
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
// them doesn't crash; a hook should key off CABANE_TRIGGER_ENTRY_PATHS.
|
|
160
|
+
// DEPRECATED constants, kept so an old prepare script that reads them
|
|
161
|
+
// doesn't crash; a hook should key off CABANE_TRIGGER_ENTRY_PATHS.
|
|
166
162
|
CABANE_ANCHOR_KIND: "workspace",
|
|
167
163
|
CABANE_ANCHOR_PATH: "",
|
|
168
|
-
//
|
|
164
|
+
// Newline-joined — workspace paths never contain a newline, so a
|
|
169
165
|
// hook splits on `\n` unambiguously. Empty string when none.
|
|
170
166
|
CABANE_TRIGGER_ENTRY_PATHS: input.triggerEntryPaths.join("\n"),
|
|
171
167
|
CABANE_CONVERSATION_TITLE: input.title ?? "",
|
|
172
|
-
//
|
|
168
|
+
// The dispatching message's text, and — in validate mode only —
|
|
173
169
|
// the prepared result the hook is being asked to check this dispatch
|
|
174
170
|
// against. Both empty/absent on an ordinary first run.
|
|
175
171
|
CABANE_TRIGGER_MESSAGE: input.messageBody ?? "",
|
|
@@ -458,18 +454,17 @@ var init_config = __esm({
|
|
|
458
454
|
localAgentConfigSchema = z2.object({
|
|
459
455
|
cwd: z2.string().optional(),
|
|
460
456
|
prepareHook: prepareHookSchema.optional(),
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
//
|
|
457
|
+
// The Claude Code auto-memory escape hatch. Auto-memory is forced OFF by
|
|
458
|
+
// default (memory belongs in the Cabane workspace, and a shared companion
|
|
459
|
+
// would pool one cwd-keyed memory dir across users). On a companion you run
|
|
460
|
+
// yourself, `claudeCode: { autoMemory: true }` hands auto-memory back to your
|
|
461
|
+
// own `~/.claude/settings.json` — Cabane then stops
|
|
466
462
|
// injecting the off switch and your normal Claude Code memory workflow applies
|
|
467
463
|
// (in coding mode, where the checkout's project settings are read).
|
|
468
464
|
claudeCode: z2.object({ autoMemory: z2.boolean().optional() }).strict().optional(),
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
// row keyed by the agent's id (`agentsConfigSchema`).
|
|
465
|
+
// This agent's exception to `runners.sandbox`: an ops agent that runs with
|
|
466
|
+
// the machine on a sandboxed device, or one sandboxed agent on a device that
|
|
467
|
+
// isn't. Absent is "inherit". Only on a row keyed by the agent's id.
|
|
473
468
|
sandbox: z2.enum(["os", "none"]).optional()
|
|
474
469
|
}).strict();
|
|
475
470
|
AGENT_ID_KEY = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -485,9 +480,8 @@ var init_config = __esm({
|
|
|
485
480
|
}
|
|
486
481
|
});
|
|
487
482
|
companionConfigSchema = z2.object({
|
|
488
|
-
//
|
|
489
|
-
//
|
|
490
|
-
// base URL onto the MITM-able channel the device token + prompt ride.
|
|
483
|
+
// https-enforced (loopback exempt), so a hand-edited config can't put the
|
|
484
|
+
// device token and prompt on a plaintext channel.
|
|
491
485
|
baseUrl: z2.string().url().refine(isAllowedBaseUrl, {
|
|
492
486
|
message: "baseUrl must be https (loopback http is allowed for local dev only)"
|
|
493
487
|
}),
|
|
@@ -505,91 +499,59 @@ var init_config = __esm({
|
|
|
505
499
|
// agentId / username / `slug/username`. Hand-added by the operator; the companion
|
|
506
500
|
// never writes this (it only persists credentials + the device, elsewhere).
|
|
507
501
|
agents: agentsConfigSchema.optional(),
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
//
|
|
511
|
-
//
|
|
512
|
-
//
|
|
513
|
-
// config a second silent roster: a device whose provisioning is genuinely
|
|
514
|
-
// per-device (ours resolves a directory from the conversation and agent ids the
|
|
515
|
-
// companion already hands the hook) still had to list every agent by id, and an
|
|
516
|
-
// agent nobody remembered to add got no hook at all — no error, no log, just a
|
|
517
|
-
// turn that started in the operator's `$HOME`. That is how @dale woke in
|
|
518
|
-
// `/home/joshed` on 2026-08-17 with a perfectly correct dispatch.
|
|
519
|
-
//
|
|
520
|
-
// Absent → nothing changes for anyone, which is why this can land inert: a
|
|
521
|
-
// device with no device-level entry behaves exactly as it did before. A hook
|
|
522
|
-
// that fails still fails the dispatch loudly, as now.
|
|
502
|
+
// The DEVICE-LEVEL prepare hook, for every agent assigned here unless its own
|
|
503
|
+
// `agents` row names one. Per-agent hooks alone make the config a second
|
|
504
|
+
// silent roster: an agent nobody listed gets no hook and no error, and starts
|
|
505
|
+
// in the operator's `$HOME` (an agent woke in `/home/joshed` this way on
|
|
506
|
+
// 2026-08-17).
|
|
523
507
|
prepareHook: prepareHookSchema.optional(),
|
|
524
|
-
//
|
|
525
|
-
//
|
|
526
|
-
//
|
|
527
|
-
// what they say to the Electron shell, the one front-end that renders a
|
|
528
|
-
// dashboard. `logLevel` is unaffected: pino level, live-editable.
|
|
508
|
+
// `dashboardPort` and `autoOpen` are IGNORED by the CLI (no port, no browser)
|
|
509
|
+
// but stay in the schema so an existing file still parses; the Electron shell
|
|
510
|
+
// reads them. `logLevel` is pino's level, live-editable.
|
|
529
511
|
dashboardPort: z2.number().int().min(1).max(65535).optional(),
|
|
530
512
|
autoOpen: z2.boolean().optional(),
|
|
531
513
|
logLevel: z2.enum(["warn", "info", "debug"]).optional(),
|
|
532
514
|
logFormat: z2.enum(["human", "json"]).optional(),
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
//
|
|
536
|
-
// here. Setting this makes the device advertise the `opencode` runtime on its
|
|
537
|
-
// heartbeat manifest (so the server offers DeepSeek/opencode models here and
|
|
538
|
-
// routes those turns to this device) AND registers the opencode adapter in the
|
|
539
|
-
// dispatcher. Absent → the device is claude-code-only, exactly as before.
|
|
515
|
+
// An `opencode serve` the operator runs here (auth via opencode's own
|
|
516
|
+
// `/connect`; Cabane never sees provider keys). Set, the device advertises the
|
|
517
|
+
// `opencode` runtime and registers its adapter.
|
|
540
518
|
opencode: z2.object({
|
|
541
519
|
serverUrl: z2.string().url()
|
|
542
520
|
}).strict().optional(),
|
|
543
|
-
//
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
// like codex. CT1379: the binary that must be there is the Agent SDK's own
|
|
548
|
-
// bundled executable — the file a turn actually launches — not the user's
|
|
549
|
-
// `claude` on PATH, which the SDK never runs. The manifest advertises
|
|
550
|
-
// claude-code only when this block says so AND that binary resolves.
|
|
521
|
+
// Whether the user connected Claude Code on this machine: consent, opted into
|
|
522
|
+
// like codex. The manifest advertises claude-code only when this says so AND
|
|
523
|
+
// the Agent SDK's bundled executable resolves (not `claude` on PATH, which the
|
|
524
|
+
// SDK never runs).
|
|
551
525
|
//
|
|
552
|
-
// The block's PRESENCE is also the migration marker
|
|
553
|
-
//
|
|
554
|
-
// asked
|
|
555
|
-
// written from here on carries the block explicitly, including a freshly-paired
|
|
556
|
-
// one, which starts at `enabled: false`: a new device is connected to nothing
|
|
557
|
-
// until its user says otherwise.
|
|
526
|
+
// The block's PRESENCE is also the migration marker
|
|
527
|
+
// (`migrateConnectedHarnesses`): absent means a config whose user was never
|
|
528
|
+
// asked. So every config written carries it, a fresh pair at `enabled: false`.
|
|
558
529
|
//
|
|
559
|
-
// Not
|
|
560
|
-
//
|
|
530
|
+
// Not the per-agent `agents.<key>.claudeCode.autoMemory`, one agent's memory
|
|
531
|
+
// switch.
|
|
561
532
|
claudeCode: z2.object({
|
|
562
533
|
enabled: z2.boolean().optional()
|
|
563
534
|
}).strict().optional(),
|
|
564
|
-
//
|
|
565
|
-
//
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
// `
|
|
569
|
-
// true } }`. Presence of the block enables it (an explicit `enabled: false`
|
|
570
|
-
// keeps the block but turns it off). Absent → the device doesn't offer codex.
|
|
571
|
-
// CT1379: enabling is now half the answer, not all of it — the manifest and the
|
|
572
|
-
// adapter registry additionally require the SDK's vendored `codex` binary to
|
|
573
|
-
// resolve, because the enable flag alone advertised a runtime a device with a
|
|
574
|
-
// half-installed platform package could not start.
|
|
535
|
+
// Codex is a local CLI the SDK spawns per turn, so this is an opt-in flag, not
|
|
536
|
+
// an address. The operator logs in (`codex login` or `CODEX_API_KEY`; Cabane
|
|
537
|
+
// never sees the key). The block's presence enables it; `enabled: false` turns
|
|
538
|
+
// it off. Half the answer: advertising also requires the SDK's vendored
|
|
539
|
+
// `codex` binary to resolve.
|
|
575
540
|
codex: z2.object({
|
|
576
541
|
enabled: z2.boolean().optional()
|
|
577
542
|
}).strict().optional(),
|
|
578
|
-
//
|
|
579
|
-
//
|
|
580
|
-
//
|
|
581
|
-
//
|
|
582
|
-
//
|
|
583
|
-
//
|
|
584
|
-
// (`runner/sandbox-policy.ts`). It is validated there rather than here, so a
|
|
585
|
-
// bad policy refuses the turns it governs, with the field named, instead of
|
|
586
|
-
// keeping the companion from starting at all.
|
|
543
|
+
// Run each turn in a child runner process. Absent or `enabled: false` is the
|
|
544
|
+
// in-process path. `sandbox: 'os'` wraps the runner in an OS sandbox where the
|
|
545
|
+
// host supports one; `'none'` (the default) is a plain child, supported in its
|
|
546
|
+
// own right. `policy` is what the sandbox lets a turn reach, read and hold,
|
|
547
|
+
// validated in `runner/sandbox-policy.ts` so a bad policy refuses the turns it
|
|
548
|
+
// governs, field named, instead of stopping the companion starting.
|
|
587
549
|
runners: z2.object({
|
|
588
550
|
enabled: z2.boolean().optional(),
|
|
589
551
|
provider: z2.enum(["local-process", "docker"]).optional(),
|
|
590
552
|
sandbox: z2.enum(["os", "none"]).optional(),
|
|
591
|
-
//
|
|
592
|
-
//
|
|
553
|
+
// A container per conversation and agent, from one image, on a network
|
|
554
|
+
// whose only way out is an allow-listed proxy.
|
|
593
555
|
docker: z2.object({
|
|
594
556
|
// The base image a container runs. Default: the one this companion
|
|
595
557
|
// builds from its own Dockerfile on first use. One named here must
|
|
@@ -1133,7 +1095,7 @@ var init_api_error_shape = __esm({
|
|
|
1133
1095
|
"dispatch_not_admitted",
|
|
1134
1096
|
"turn_already_ended",
|
|
1135
1097
|
"turn_belongs_elsewhere",
|
|
1136
|
-
//
|
|
1098
|
+
// The context this start was composed from named an attempt the
|
|
1137
1099
|
// lease no longer picks (midnight rolled it, a degrade retired it). Nothing
|
|
1138
1100
|
// was granted; the turn recomposes its context and starts again, once.
|
|
1139
1101
|
"dispatch_context_stale"
|
|
@@ -1207,10 +1169,8 @@ var init_api = __esm({
|
|
|
1207
1169
|
this.opts = opts;
|
|
1208
1170
|
}
|
|
1209
1171
|
opts;
|
|
1210
|
-
//
|
|
1211
|
-
//
|
|
1212
|
-
// this guard two overlapping drains could each pick up the same queued entry
|
|
1213
|
-
// and double-send it (the server dedupes, but the wasted POSTs aren't free).
|
|
1172
|
+
// Re-entrancy guard for `drainOutbox`: drains are kicked from several places,
|
|
1173
|
+
// and two overlapping ones would double-send the same entry.
|
|
1214
1174
|
draining = false;
|
|
1215
1175
|
lastDeliveryWarning = 0;
|
|
1216
1176
|
get base() {
|
|
@@ -1272,19 +1232,13 @@ var init_api = __esm({
|
|
|
1272
1232
|
}
|
|
1273
1233
|
}
|
|
1274
1234
|
}
|
|
1275
|
-
//
|
|
1276
|
-
//
|
|
1277
|
-
//
|
|
1278
|
-
//
|
|
1279
|
-
//
|
|
1280
|
-
// -
|
|
1281
|
-
//
|
|
1282
|
-
// - cancel (the turn was stopped; `signal` aborted or an AbortError) →
|
|
1283
|
-
// rethrow without queuing: the user asked us to stop, and the held text
|
|
1284
|
-
// was already dropped — we must not resurrect it from the queue.
|
|
1285
|
-
// - terminal 4xx (a deterministic rejection a replay can't fix) → rethrow,
|
|
1286
|
-
// so the caller's error path runs as it does today (e.g. the committer
|
|
1287
|
-
// re-arms its empty-final fallback).
|
|
1235
|
+
// Send-or-enqueue for a turn-row commit: the immediate (retry-budgeted) send
|
|
1236
|
+
// first, then on a *retryable* failure persist to the outbox.
|
|
1237
|
+
// - no outbox configured (one-shot CLI / tests) → rethrow.
|
|
1238
|
+
// - cancel (`signal` aborted or an AbortError) → rethrow without queuing:
|
|
1239
|
+
// the user asked us to stop, and the text must not resurrect from the queue.
|
|
1240
|
+
// - terminal 4xx (a replay can't fix it) → rethrow, so the caller's error
|
|
1241
|
+
// path runs.
|
|
1288
1242
|
// - retryable (transport / 5xx — commit outcome may be unknown) → queue
|
|
1289
1243
|
// and return. This acknowledges local delivery responsibility, not server
|
|
1290
1244
|
// persistence. Replays retain the endpoint's identity: messages/thinking/
|
|
@@ -1309,13 +1263,10 @@ var init_api = __esm({
|
|
|
1309
1263
|
);
|
|
1310
1264
|
}
|
|
1311
1265
|
}
|
|
1312
|
-
//
|
|
1313
|
-
//
|
|
1314
|
-
//
|
|
1315
|
-
//
|
|
1316
|
-
// the API is still down: stop the cycle and leave that entry and the rest
|
|
1317
|
-
// queued for the next drain. Returns a summary the supervisor's backoff
|
|
1318
|
-
// scheduler reads. Re-entrancy-guarded so overlapping kicks don't double-send.
|
|
1266
|
+
// Flush the outbox FIFO. An entry goes once the server acknowledges it (2xx)
|
|
1267
|
+
// or rejects it terminally (any 4xx, which a replay can never satisfy: discard
|
|
1268
|
+
// with a log rather than spin). A retryable failure means the API is still
|
|
1269
|
+
// down: stop and leave that entry and the rest for the next drain.
|
|
1319
1270
|
async drainOutbox() {
|
|
1320
1271
|
const outbox = this.opts.outbox;
|
|
1321
1272
|
if (!outbox) return { remaining: 0, progressed: false };
|
|
@@ -1363,40 +1314,24 @@ var init_api = __esm({
|
|
|
1363
1314
|
}
|
|
1364
1315
|
return { remaining: outbox.size(), progressed };
|
|
1365
1316
|
}
|
|
1366
|
-
//
|
|
1367
|
-
//
|
|
1317
|
+
// How many commits are queued; the drain scheduler reads it to decide whether
|
|
1318
|
+
// to keep polling.
|
|
1368
1319
|
outboxSize() {
|
|
1369
1320
|
return this.opts.outbox?.size() ?? 0;
|
|
1370
1321
|
}
|
|
1371
|
-
//
|
|
1372
|
-
//
|
|
1373
|
-
//
|
|
1374
|
-
//
|
|
1375
|
-
//
|
|
1376
|
-
//
|
|
1377
|
-
//
|
|
1378
|
-
//
|
|
1379
|
-
//
|
|
1380
|
-
//
|
|
1381
|
-
//
|
|
1382
|
-
//
|
|
1383
|
-
//
|
|
1384
|
-
// the server should hand back `turnSeqFloor` — the turn's committed seq
|
|
1385
|
-
// high-water mark. Only the supervisor knows (it minted the id or recovered
|
|
1386
|
-
// it), so it is passed down rather than inferred here. It gates the COST of
|
|
1387
|
-
// the aggregate, never the tenancy scoping, which the route applies
|
|
1388
|
-
// unconditionally.
|
|
1389
|
-
// `fresh` asks for the context a session that holds NOTHING needs: the read
|
|
1390
|
-
// from the start of the conversation, the agent's own words included, and
|
|
1391
|
-
// the first-turn material — what the adapter runs on when it refused to
|
|
1392
|
-
// resume the stored session and is starting a fresh one.
|
|
1393
|
-
// CT1543: `episode` is the dispatch event's own, echoed back so the server can
|
|
1394
|
-
// tell this frame from a later wake on the same message. Omitted when the
|
|
1395
|
-
// event didn't carry one, which is the only thing an older event can mean.
|
|
1396
|
-
// CT1657: `signal` is the turn's own abort. It matters here more than on any
|
|
1397
|
-
// other read, because this call now waits up to ~8s across five attempts: a
|
|
1398
|
-
// Stop landing in that window used to leave the whole schedule running, and
|
|
1399
|
-
// the person watched a turn they had already stopped keep trying.
|
|
1322
|
+
// The ONE turn-context fetch: the server composes the whole server portion of
|
|
1323
|
+
// the `TurnRequest`. Agent-PAT authed; the workspace is implied by the PAT.
|
|
1324
|
+
// - `turnId`: the host-minted id the server binds the turn token to, so the
|
|
1325
|
+
// turn-control surface rejects a token whose turn has ended.
|
|
1326
|
+
// - `resumed`: a REPLAY of an interrupted turn, so the server returns
|
|
1327
|
+
// `turnSeqFloor`. Only the supervisor knows. It gates the cost of the
|
|
1328
|
+
// aggregate, never the tenancy scoping.
|
|
1329
|
+
// - `fresh`: the context a session holding NOTHING needs (the whole read,
|
|
1330
|
+
// own words included, plus first-turn material), for an adapter that
|
|
1331
|
+
// refused to resume the stored session.
|
|
1332
|
+
// - `episode`: the dispatch event's own, so the server can tell this frame
|
|
1333
|
+
// from a later wake on the same message. Omitted by an older event.
|
|
1334
|
+
// - `signal`: the turn's abort, so a Stop cuts the ~8s retry schedule short.
|
|
1400
1335
|
getTurnContext(conversationId, messageId2, turnId, resumed, fresh, episode, signal) {
|
|
1401
1336
|
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))}` : "");
|
|
1402
1337
|
return this.request("GET", `/api/agent/turn-context?${q}`, void 0, {
|
|
@@ -1404,9 +1339,8 @@ var init_api = __esm({
|
|
|
1404
1339
|
...signal ? { signal } : {}
|
|
1405
1340
|
});
|
|
1406
1341
|
}
|
|
1407
|
-
//
|
|
1408
|
-
//
|
|
1409
|
-
// test has no durable queue, so nothing can be hiding in it.
|
|
1342
|
+
// The local half of a resumed turn's seq floor (`Outbox.maxSeqForTurn`). 0
|
|
1343
|
+
// with no outbox: nothing can be hiding in a queue that doesn't exist.
|
|
1410
1344
|
outboxMaxSeqForTurn(turnId) {
|
|
1411
1345
|
return this.opts.outbox?.maxSeqForTurn(turnId) ?? 0;
|
|
1412
1346
|
}
|
|
@@ -1422,13 +1356,11 @@ var init_api = __esm({
|
|
|
1422
1356
|
(e) => e.turnId === turnId && e.kind === "message" && e.body?.kind !== "runtime_notice"
|
|
1423
1357
|
);
|
|
1424
1358
|
}
|
|
1425
|
-
//
|
|
1426
|
-
//
|
|
1427
|
-
//
|
|
1428
|
-
//
|
|
1429
|
-
//
|
|
1430
|
-
// refusal — nothing to adopt, the reaper got there first — and the caller
|
|
1431
|
-
// fetches a fresh context rather than working on this one.
|
|
1359
|
+
// ADOPT a turn context's issuance as the execution source of its work, once,
|
|
1360
|
+
// before preparation emits or queues anything, so every row can name a source
|
|
1361
|
+
// the server durably recorded and will never reap. Adoption is idempotent, so
|
|
1362
|
+
// retried. A 409 `execution_source_unavailable` means the reaper got there
|
|
1363
|
+
// first; the caller fetches a fresh context.
|
|
1432
1364
|
adoptExecutionSource(issuanceId, body, signal) {
|
|
1433
1365
|
return this.request(
|
|
1434
1366
|
"POST",
|
|
@@ -1437,10 +1369,10 @@ var init_api = __esm({
|
|
|
1437
1369
|
signal ? { retry: true, signal } : { retry: true }
|
|
1438
1370
|
);
|
|
1439
1371
|
}
|
|
1440
|
-
//
|
|
1441
|
-
//
|
|
1442
|
-
//
|
|
1443
|
-
//
|
|
1372
|
+
// The launcher mints the runner credential for one dispatch, on its PAT and
|
|
1373
|
+
// from its device, and hands it to the runner in place of both. A retry is
|
|
1374
|
+
// safe: the server returns the same runner id and retires the credential the
|
|
1375
|
+
// lost response carried.
|
|
1444
1376
|
mintRunnerCredential(body) {
|
|
1445
1377
|
return this.request("POST", "/api/agent/runner-credentials", body, { retry: true });
|
|
1446
1378
|
}
|
|
@@ -1460,7 +1392,7 @@ var init_api = __esm({
|
|
|
1460
1392
|
// invocation. The server records the effect and receipt in one transaction;
|
|
1461
1393
|
// an ok:false response is a recorded refusal for the tool to report.
|
|
1462
1394
|
// Retries retain the caller's callId and recover the same receipt, even
|
|
1463
|
-
// when the turn settled between attempts
|
|
1395
|
+
// when the turn settled between attempts: the route consults the
|
|
1464
1396
|
// receipt before it refuses an ended turn, so a 4xx proves no attempt wrote.
|
|
1465
1397
|
// The caller's signal bounds how long the model waits for the tool result.
|
|
1466
1398
|
turnAct(workspaceId, turnId, act, signal) {
|
|
@@ -1471,36 +1403,23 @@ var init_api = __esm({
|
|
|
1471
1403
|
signal ? { retry: true, signal } : { retry: true }
|
|
1472
1404
|
);
|
|
1473
1405
|
}
|
|
1474
|
-
//
|
|
1475
|
-
//
|
|
1476
|
-
//
|
|
1477
|
-
// terminalized underneath it stops instead of running to the six-hour cap.
|
|
1406
|
+
// RENEW THE TURN'S LEASE: is this turn still running? Called on a cadence for
|
|
1407
|
+
// the life of the SDK loop and the moment a commit is refused, so a loop whose
|
|
1408
|
+
// turn was terminalized underneath it stops.
|
|
1478
1409
|
//
|
|
1479
|
-
// Single-shot
|
|
1480
|
-
//
|
|
1481
|
-
// and the next tick asks again in a few seconds anyway. Retrying would only
|
|
1482
|
-
// stretch one tick over the next one's slot.
|
|
1410
|
+
// Single-shot, NEVER retried: a failed renewal has no answer worth replaying
|
|
1411
|
+
// and the next tick asks again; retrying stretches one tick over the next.
|
|
1483
1412
|
//
|
|
1484
|
-
// ALL FAILURE IS `unknown
|
|
1485
|
-
//
|
|
1486
|
-
//
|
|
1487
|
-
//
|
|
1488
|
-
//
|
|
1489
|
-
// gap this closes. So the failure direction is fixed here, at the boundary,
|
|
1490
|
-
// rather than left to each caller to remember.
|
|
1413
|
+
// ALL FAILURE IS `unknown`. Only the server SAYING the turn is over may stop a
|
|
1414
|
+
// running loop; a 500, a dropped connection, an expired token or an
|
|
1415
|
+
// unparseable body says nothing about the turn, and reading it as death would
|
|
1416
|
+
// kill healthy turns on every API hiccup. Fixed here, at the boundary, so no
|
|
1417
|
+
// caller has to remember.
|
|
1491
1418
|
//
|
|
1492
|
-
//
|
|
1493
|
-
//
|
|
1494
|
-
//
|
|
1495
|
-
//
|
|
1496
|
-
// on it, so one stalled request latches the check permanently, every later
|
|
1497
|
-
// cadence tick returns immediately, and the loop this exists to stop runs to
|
|
1498
|
-
// the six-hour cap even after connectivity comes back — the exact defect the
|
|
1499
|
-
// task is about, reintroduced through the mechanism meant to close it.
|
|
1500
|
-
//
|
|
1501
|
-
// The deadline turns that into an ordinary `unknown`: the request rejects, the
|
|
1502
|
-
// in-flight latch clears, and the NEXT tick asks again. Well under the renewal
|
|
1503
|
-
// cadence so a stalled probe is finished with before its successor is due.
|
|
1419
|
+
// DEADLINED, because `fetch` has no default timeout: a black-holed request
|
|
1420
|
+
// would hang forever, and the caller single-flights on it, so one stall would
|
|
1421
|
+
// latch the check and let the loop run to the cap. The deadline makes it an
|
|
1422
|
+
// ordinary `unknown` and the next tick asks again.
|
|
1504
1423
|
async checkTurnLease(workspaceId, conversationId, agentId, turnId) {
|
|
1505
1424
|
const q = `turnId=${encodeURIComponent(turnId)}`;
|
|
1506
1425
|
try {
|
|
@@ -1509,11 +1428,7 @@ var init_api = __esm({
|
|
|
1509
1428
|
`/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}/turn-lease?${q}`,
|
|
1510
1429
|
void 0,
|
|
1511
1430
|
{
|
|
1512
|
-
//
|
|
1513
|
-
// is right above — a failed probe has no answer worth replaying and the
|
|
1514
|
-
// next tick asks again in seconds, so retrying inside this one would
|
|
1515
|
-
// stretch a tick over its successor's slot, which is what the deadline
|
|
1516
|
-
// exists to prevent.
|
|
1431
|
+
// The one GET that opts OUT of the default retry; see above.
|
|
1517
1432
|
retry: false,
|
|
1518
1433
|
signal: AbortSignal.timeout(this.opts.leaseCheckTimeoutMs ?? LEASE_CHECK_TIMEOUT_MS)
|
|
1519
1434
|
}
|
|
@@ -1547,9 +1462,8 @@ var init_api = __esm({
|
|
|
1547
1462
|
}
|
|
1548
1463
|
return this.request("PATCH", path, body);
|
|
1549
1464
|
}
|
|
1550
|
-
//
|
|
1551
|
-
//
|
|
1552
|
-
// deliberate differences:
|
|
1465
|
+
// Send-or-enqueue for an active-run flag write, last-writer-wins per
|
|
1466
|
+
// (conversation, agent) pair. Like `durableCommit`, except:
|
|
1553
1467
|
// - No abort signal is threaded. The clear is the turn's teardown and must
|
|
1554
1468
|
// land even when the turn was *cancelled* (the abort signal is set) — a
|
|
1555
1469
|
// cancelled turn still has to drop its "working" indicator.
|
|
@@ -1603,9 +1517,6 @@ var init_api = __esm({
|
|
|
1603
1517
|
);
|
|
1604
1518
|
}
|
|
1605
1519
|
}
|
|
1606
|
-
// CT29: per-device liveness moved off the per-workspace agent PAT and onto the
|
|
1607
|
-
// device token — see `DeviceApi.heartbeat`. There is no agent-PAT heartbeat
|
|
1608
|
-
// anymore.
|
|
1609
1520
|
// Commit the agent's first/last words (message) or a runtime_notice through
|
|
1610
1521
|
// the public messages endpoint. Intermediate working text uses reportText.
|
|
1611
1522
|
// Attribution comes from the agent credential; a new speech row can offer
|
|
@@ -1689,36 +1600,26 @@ var init_host_policy = __esm({
|
|
|
1689
1600
|
"packages/agent-runtime/src/host-policy.ts"() {
|
|
1690
1601
|
"use strict";
|
|
1691
1602
|
hostPolicySchema = z5.object({
|
|
1692
|
-
// Host filesystem
|
|
1693
|
-
//
|
|
1694
|
-
// tool. Off under the locked assistant surface (today's `DISALLOWED_TOOLS`);
|
|
1695
|
-
// on under `coding` mode.
|
|
1603
|
+
// Host filesystem and shell: `Bash`/`Read`/`Write`/`Edit`/`Glob`/`Grep`, the notebook pair,
|
|
1604
|
+
// git-worktree tools and `REPL`. Off on the locked assistant surface; on in the coding posture.
|
|
1696
1605
|
hostFs: z5.boolean(),
|
|
1697
|
-
// Public web (
|
|
1698
|
-
// web, not host reach — granted by default today, but expressible as a grant.
|
|
1606
|
+
// Public web (`WebSearch` / `WebFetch`). Not host reach; a grant like the rest.
|
|
1699
1607
|
web: z5.boolean(),
|
|
1700
|
-
// Browser automation (the Playwright MCP surface). Varies by host
|
|
1701
|
-
// it, the house executor does not (CT230).
|
|
1608
|
+
// Browser automation (the Playwright MCP surface). Varies by host.
|
|
1702
1609
|
browser: z5.boolean(),
|
|
1703
|
-
// User-configured MCP servers permitted.
|
|
1704
|
-
// (CT227: Cabane agents run no user MCP servers), true for a personal companion.
|
|
1610
|
+
// User-configured MCP servers permitted.
|
|
1705
1611
|
userMcp: z5.boolean(),
|
|
1706
|
-
// Subagent
|
|
1707
|
-
//
|
|
1708
|
-
//
|
|
1709
|
-
// the coding allow-hook (which reaches sub-agent tool calls, CT680) or the user
|
|
1710
|
-
// allowlist. The subagent completes within the turn, so
|
|
1711
|
-
// it's not the turn-model invariant `scheduling` is.
|
|
1612
|
+
// Subagent dispatch (`Task` / `Agent`) — a grant: `false` on the locked surface, `true` in the
|
|
1613
|
+
// coding posture, where the coding allow-hook reaches sub-agent tool calls. A subagent completes
|
|
1614
|
+
// within the turn, so it's not an invariant like `scheduling`.
|
|
1712
1615
|
subagents: z5.boolean(),
|
|
1713
1616
|
// ── Hard platform invariants — always denied, never granted ────────────────
|
|
1714
|
-
// Deferred re-invocation / scheduling (`ScheduleWakeup`, the `Cron*` / `Task*`
|
|
1715
|
-
//
|
|
1716
|
-
//
|
|
1717
|
-
// and strands the agent (the CT155/CT156 rule).
|
|
1617
|
+
// Deferred re-invocation / scheduling (`ScheduleWakeup`, the `Cron*` / `Task*` families,
|
|
1618
|
+
// `Monitor`, `Workflow`, …). A turn is one query that resolves on `result`; a scheduled callback
|
|
1619
|
+
// fires after the reply window has closed and strands the agent.
|
|
1718
1620
|
scheduling: z5.literal("never"),
|
|
1719
|
-
// Human-facing UI prompts (`AskUserQuestion`).
|
|
1720
|
-
//
|
|
1721
|
-
// (`UNSUPPORTED_TOOLS`). The agent asks in its reply instead.
|
|
1621
|
+
// Human-facing UI prompts (`AskUserQuestion`). Nothing in a Cabane conversation can answer a
|
|
1622
|
+
// structured prompt, so the call hangs the turn. The agent asks in its reply instead.
|
|
1722
1623
|
uiPrompts: z5.literal("never")
|
|
1723
1624
|
});
|
|
1724
1625
|
}
|
|
@@ -1731,59 +1632,40 @@ var init_turn_event = __esm({
|
|
|
1731
1632
|
"packages/agent-runtime/src/turn-event.ts"() {
|
|
1732
1633
|
"use strict";
|
|
1733
1634
|
turnEventSchema = z6.discriminatedUnion("type", [
|
|
1734
|
-
// The runtime's opaque session state, emitted when the adapter learns it (e.g.
|
|
1735
|
-
//
|
|
1736
|
-
//
|
|
1737
|
-
// knows what it means. Today's companion captures the raw SDK session id here; a
|
|
1738
|
-
// future adapter may encode more (e.g. `{sdkSessionId, cwd}`) — still one
|
|
1739
|
-
// opaque string to the platform.
|
|
1635
|
+
// The runtime's opaque session state, emitted when the adapter learns it (e.g. the SDK
|
|
1636
|
+
// `system/init` frame). The platform stores `state` verbatim per (conversation, agent) and
|
|
1637
|
+
// hands it back next turn; only the adapter knows what it means.
|
|
1740
1638
|
//
|
|
1741
|
-
//
|
|
1742
|
-
//
|
|
1743
|
-
//
|
|
1744
|
-
//
|
|
1745
|
-
//
|
|
1746
|
-
// the
|
|
1747
|
-
//
|
|
1748
|
-
// on settle and the server rewinds the mark, so the NEXT turn rebuilds a full
|
|
1749
|
-
// catch-up (this failing turn is unavoidably lossy — the degrade is only known
|
|
1750
|
-
// on the companion, after the server committed the manifest). Runtime-neutral: a
|
|
1751
|
-
// plain boolean, not a runtime-specific reason string (that stays in the
|
|
1752
|
-
// adapter's `onWarn` log). Additive + optional — an old receiver ignores it.
|
|
1639
|
+
// `degraded: true`: the adapter started this fresh session despite a stored one (a cwd
|
|
1640
|
+
// mismatch, or legacy/foreign state it refused to resume) — never for a genuine first turn.
|
|
1641
|
+
// The server built this turn's catch-up manifest assuming the prior session held everything up
|
|
1642
|
+
// to the high-water mark; a degrade breaks that. The host relays `degraded` on settle and the
|
|
1643
|
+
// server rewinds the mark, so the next turn rebuilds a full catch-up (this turn is unavoidably
|
|
1644
|
+
// lossy — the degrade is known only after the manifest was committed). A plain boolean; the
|
|
1645
|
+
// runtime-specific reason stays in the adapter's `onWarn` log. Optional.
|
|
1753
1646
|
z6.object({
|
|
1754
1647
|
type: z6.literal("session"),
|
|
1755
1648
|
state: z6.string(),
|
|
1756
1649
|
degraded: z6.boolean().optional()
|
|
1757
1650
|
}),
|
|
1758
|
-
// One readable thinking summary.
|
|
1759
|
-
// surfaced live, never persisted as durable content.
|
|
1651
|
+
// One readable thinking summary. Transient — surfaced live, never persisted as content.
|
|
1760
1652
|
z6.object({ type: z6.literal("thinking"), text: z6.string() }),
|
|
1761
|
-
// Assistant text.
|
|
1762
|
-
// `
|
|
1763
|
-
// `progress` row); `terminal: true` is the turn's closing reply (commits as
|
|
1764
|
-
// the `final` row).
|
|
1653
|
+
// Assistant text. `terminal: false` is interim narration (commits as `progress`);
|
|
1654
|
+
// `terminal: true` is the turn's closing reply (commits as `final`).
|
|
1765
1655
|
z6.object({ type: z6.literal("text"), body: z6.string(), terminal: z6.boolean() }),
|
|
1766
|
-
// Runtime/provider-authored prose about the turn — a failure's explanation, or
|
|
1767
|
-
//
|
|
1768
|
-
// `
|
|
1769
|
-
//
|
|
1770
|
-
// voice. Adapters emit it only from positive runtime evidence; the web never
|
|
1771
|
-
// classifies English strings.
|
|
1656
|
+
// Runtime/provider-authored prose about the turn — a failure's explanation, or a line the
|
|
1657
|
+
// harness wrote on a turn it recovered. Not the agent's words: the pump persists it as
|
|
1658
|
+
// `runtime_notice`, drawn in the SystemNote voice. Adapters emit it only from positive runtime
|
|
1659
|
+
// evidence; the web never classifies English strings.
|
|
1772
1660
|
z6.object({ type: z6.literal("runtime_notice"), body: z6.string() }),
|
|
1773
|
-
// A tool-activity transition.
|
|
1774
|
-
// `
|
|
1775
|
-
//
|
|
1776
|
-
// pairs `start` with a later `done`/`error` on the same `id`.
|
|
1661
|
+
// A tool-activity transition. `name` is prefix-stripped (`cabane_read`, not
|
|
1662
|
+
// `mcp__cabane__cabane_read`); `summary` is the short card label; `phase` pairs `start` with a
|
|
1663
|
+
// later `done`/`error` on the same `id`.
|
|
1777
1664
|
//
|
|
1778
|
-
// `input`/`result` are
|
|
1779
|
-
//
|
|
1780
|
-
//
|
|
1781
|
-
//
|
|
1782
|
-
// CT496: `mcpServer` is the MCP server the call came from (`cabane`,
|
|
1783
|
-
// `playwright`, …), derived off the raw `mcp__<server>__` name before `name`
|
|
1784
|
-
// dropped the prefix; null for a host / built-in tool. The client tags Cabane
|
|
1785
|
-
// MCP calls (`=== 'cabane'`) with a subtle glyph. Nullable + optional so a
|
|
1786
|
-
// pre-CT496 producer that never sets it is unaffected (treated as null).
|
|
1665
|
+
// `input`/`result` are `unknown` (the raw tool payloads): rendering and truncation for the tool
|
|
1666
|
+
// log are a consumer concern (docs/turn-protocol.md). `mcpServer` is the MCP server the call
|
|
1667
|
+
// came from (`cabane`, `playwright`, …), derived before `name` dropped the prefix; null for a
|
|
1668
|
+
// host tool. Optional, read as null when absent.
|
|
1787
1669
|
z6.object({
|
|
1788
1670
|
type: z6.literal("tool"),
|
|
1789
1671
|
id: z6.string(),
|
|
@@ -1794,45 +1676,25 @@ var init_turn_event = __esm({
|
|
|
1794
1676
|
result: z6.unknown().optional(),
|
|
1795
1677
|
mcpServer: z6.string().nullable().optional()
|
|
1796
1678
|
}),
|
|
1797
|
-
// The turn's terminal outcome.
|
|
1798
|
-
//
|
|
1799
|
-
// the host's clear-PATCH surfaces it as `errorReason`.
|
|
1800
|
-
//
|
|
1801
|
-
// CT277: an optional `usage` — the SDK result's token counts, which were
|
|
1802
|
-
// dropped on the floor before. `inputTokens` is the full context the model saw
|
|
1803
|
-
// (uncached + cache-read + cache-creation input), so it doubles as the
|
|
1804
|
-
// context-window cost; `outputTokens` the generated tokens. Optional ⇒
|
|
1805
|
-
// backward-compatible: an old companion / adapter that never sets it, and a
|
|
1806
|
-
// receiver that never reads it, are unaffected (the turn's token columns stay
|
|
1807
|
-
// null → the UI shows `—`).
|
|
1679
|
+
// The turn's terminal outcome. `ok:false` carries a machine reason (`no_session`, an error
|
|
1680
|
+
// code), surfaced by the host's clear-PATCH as `errorReason`.
|
|
1808
1681
|
//
|
|
1809
|
-
//
|
|
1810
|
-
//
|
|
1811
|
-
//
|
|
1812
|
-
//
|
|
1813
|
-
// costing later prices a cache-read token far below a fresh input token.
|
|
1682
|
+
// `usage`: the SDK result's token counts. `inputTokens` is the full context the model saw
|
|
1683
|
+
// (uncached + cache-read + cache-creation). The cache counts, when the runtime breaks them out,
|
|
1684
|
+
// are a subset of it, kept because a cache-read token costs far less. All optional; absent
|
|
1685
|
+
// leaves the columns null and the UI shows `—`.
|
|
1814
1686
|
//
|
|
1815
|
-
//
|
|
1816
|
-
//
|
|
1817
|
-
//
|
|
1818
|
-
//
|
|
1819
|
-
//
|
|
1820
|
-
// harness reported them (claude-code's `getContextUsage`, or its final
|
|
1821
|
-
// request's input and `modelUsage` window when that call gives nothing). A
|
|
1822
|
-
// runtime that can't source them (codex's cumulative-only usage) omits both,
|
|
1823
|
-
// and no surface shows a reading for that turn.
|
|
1687
|
+
// `inputTokens` is a billing quantity — for claude-code/codex the cumulative total across every
|
|
1688
|
+
// model request in the turn, growing with tool calls, not "how full is the window".
|
|
1689
|
+
// `contextTokens` is the occupancy read and `contextWindow` its window, as the harness reported
|
|
1690
|
+
// them (claude-code's `getContextUsage`, else its final request's input and `modelUsage`
|
|
1691
|
+
// window). A runtime that can't source them (codex) omits both.
|
|
1824
1692
|
//
|
|
1825
|
-
//
|
|
1826
|
-
// `
|
|
1827
|
-
//
|
|
1828
|
-
//
|
|
1829
|
-
//
|
|
1830
|
-
// per-runtime knobs the harness reported (`{ reasoningEffort }` for codex; none
|
|
1831
|
-
// for claude-code today). Both only known after the run streams — so they ride
|
|
1832
|
-
// the terminal event home, the host relays them on settle, and the server writes
|
|
1833
|
-
// `message_turn_metadata.resolved_model` / `resolved_config`. Optional ⇒
|
|
1834
|
-
// backward-compatible: an old adapter/companion omits them, a cancel has no result
|
|
1835
|
-
// event at all, and the columns stay null → the UI shows `—`.
|
|
1693
|
+
// `resolvedModel` is the concrete model the runtime ran (claude-code learns it from
|
|
1694
|
+
// `system/init` mid-run, even for a subscription default; codex records the model the thread
|
|
1695
|
+
// started with); `resolvedConfig` the knobs the harness reported (`{ reasoningEffort }` for
|
|
1696
|
+
// codex). Known only after the run streams, so they ride this event home and the host relays
|
|
1697
|
+
// them on settle. A cancel has no result event at all.
|
|
1836
1698
|
z6.object({
|
|
1837
1699
|
type: z6.literal("result"),
|
|
1838
1700
|
ok: z6.boolean(),
|
|
@@ -1844,9 +1706,8 @@ var init_turn_event = __esm({
|
|
|
1844
1706
|
cacheCreationTokens: z6.number().optional(),
|
|
1845
1707
|
contextTokens: z6.number().optional(),
|
|
1846
1708
|
contextWindow: z6.number().optional(),
|
|
1847
|
-
//
|
|
1848
|
-
//
|
|
1849
|
-
// compaction it reported during the turn. Claude Code only.
|
|
1709
|
+
// The rest of the harness's context reading, when it gave one: where it will compact, its
|
|
1710
|
+
// `/context` breakdown, and every compaction it reported. Claude Code only.
|
|
1850
1711
|
contextCompactAt: z6.number().optional(),
|
|
1851
1712
|
contextCategories: z6.array(
|
|
1852
1713
|
z6.object({
|
|
@@ -1869,10 +1730,9 @@ var init_turn_event = __esm({
|
|
|
1869
1730
|
thinking: z6.string().optional(),
|
|
1870
1731
|
reasoningEffort: z6.string().optional()
|
|
1871
1732
|
}).optional(),
|
|
1872
|
-
//
|
|
1873
|
-
//
|
|
1874
|
-
//
|
|
1875
|
-
// distinguishes a missing init frame from a real empty inventory.
|
|
1733
|
+
// The harness-reported MCP inventory from this turn's init frame. Diagnostic only: names,
|
|
1734
|
+
// statuses and a count — never server definitions, credentials or session ids.
|
|
1735
|
+
// `initReceived:false` tells a missing init frame from a real empty inventory.
|
|
1876
1736
|
mcpInventory: z6.object({
|
|
1877
1737
|
initReceived: z6.boolean(),
|
|
1878
1738
|
servers: z6.array(z6.object({ name: z6.string(), status: z6.string() })),
|
|
@@ -1989,9 +1849,8 @@ function isContentBearingEvent(event) {
|
|
|
1989
1849
|
return normalizeCommitText(event.text) !== "";
|
|
1990
1850
|
case "tool":
|
|
1991
1851
|
return true;
|
|
1992
|
-
//
|
|
1993
|
-
//
|
|
1994
|
-
// line still produced nothing.
|
|
1852
|
+
// A runtime notice is shown, but it is the harness speaking about the turn, not its output — a
|
|
1853
|
+
// success whose only output is a CLI line still produced nothing.
|
|
1995
1854
|
case "runtime_notice":
|
|
1996
1855
|
case "session":
|
|
1997
1856
|
case "result":
|
|
@@ -2073,11 +1932,10 @@ var init_turn_request = __esm({
|
|
|
2073
1932
|
mcpUrl: z8.string(),
|
|
2074
1933
|
bearer: z8.string(),
|
|
2075
1934
|
activeConversationId: z8.string(),
|
|
2076
|
-
// Scoped turn-control MCP URL. Codex
|
|
2077
|
-
//
|
|
2078
|
-
//
|
|
2079
|
-
//
|
|
2080
|
-
// the companion populates it in build-options.ts.
|
|
1935
|
+
// Scoped turn-control MCP URL. Codex and opencode mount it as `cabane_companion` with the bearer
|
|
1936
|
+
// and active-conversation header, for ask, withdraw_ask, wake_me, cancel_wake and skip_turn.
|
|
1937
|
+
// Claude Code mounts the companion's in-process instance instead. Optional; the companion
|
|
1938
|
+
// populates it.
|
|
2081
1939
|
turnControlUrl: z8.string().optional(),
|
|
2082
1940
|
// Workspace coordinate retained for envelope compatibility. Current
|
|
2083
1941
|
// adapters use MCP, whose workspace operations take workspaceId explicitly.
|
|
@@ -2090,19 +1948,17 @@ var init_turn_request = __esm({
|
|
|
2090
1948
|
cwd: z8.string().optional(),
|
|
2091
1949
|
env: z8.record(z8.string(), z8.string()).optional(),
|
|
2092
1950
|
mcpServers: resolvedMcpServersSchema.optional(),
|
|
2093
|
-
//
|
|
2094
|
-
//
|
|
2095
|
-
//
|
|
2096
|
-
// its own. Absent when no server references one.
|
|
1951
|
+
// The values `headersFromEnv` names, per turn (`CABANE_MCP_<n>` → complete header value). The
|
|
1952
|
+
// adapter merges them into the harness process's environment and nowhere else; the host never
|
|
1953
|
+
// merges them into its own. Absent when no server references one.
|
|
2097
1954
|
secretEnv: z8.record(z8.string(), z8.string()).optional(),
|
|
2098
|
-
//
|
|
2099
|
-
//
|
|
2100
|
-
//
|
|
2101
|
-
//
|
|
2102
|
-
// default in place (see `buildClaudeCodeOptions`).
|
|
1955
|
+
// Machine-local claude-code knobs for an operator running their own companion — the auto-memory
|
|
1956
|
+
// escape hatch. `autoMemory: true` opts back into Claude Code's auto-memory (governed by the
|
|
1957
|
+
// operator's `.claude/settings.json`); absent/false keeps the adapter's force-off default
|
|
1958
|
+
// (`buildClaudeCodeOptions`).
|
|
2103
1959
|
claudeCode: z8.object({ autoMemory: z8.boolean().optional() }).optional()
|
|
2104
1960
|
}),
|
|
2105
|
-
// Host-owned injected servers (host-filled)
|
|
1961
|
+
// Host-owned injected servers (host-filled).
|
|
2106
1962
|
extra: z8.object({
|
|
2107
1963
|
mcpServers: hostInjectedServersSchema
|
|
2108
1964
|
})
|
|
@@ -2190,8 +2046,8 @@ async function emitToolResults(msg, emit, pending, onError) {
|
|
|
2190
2046
|
// result payload off the block.
|
|
2191
2047
|
input: started?.input,
|
|
2192
2048
|
result: block.content,
|
|
2193
|
-
//
|
|
2194
|
-
//
|
|
2049
|
+
// The server captured at `start` (the pretty name here has lost its prefix). Null when the
|
|
2050
|
+
// start wasn't seen.
|
|
2195
2051
|
mcpServer: started?.mcpServer ?? null
|
|
2196
2052
|
},
|
|
2197
2053
|
onError
|
|
@@ -2259,24 +2115,18 @@ function summarizeToolInput(input) {
|
|
|
2259
2115
|
}
|
|
2260
2116
|
return "";
|
|
2261
2117
|
}
|
|
2262
|
-
function
|
|
2118
|
+
function renderToolIo(input) {
|
|
2263
2119
|
if (input === void 0 || input === null) return "";
|
|
2264
|
-
if (typeof input === "string")
|
|
2265
|
-
const trimmed = input.trim();
|
|
2266
|
-
return trimmed.length > 0 ? truncate(trimmed, TOOL_IO_MAX_CHARS) : "";
|
|
2267
|
-
}
|
|
2120
|
+
if (typeof input === "string") return input.trim();
|
|
2268
2121
|
try {
|
|
2269
|
-
return
|
|
2122
|
+
return JSON.stringify(input) ?? "";
|
|
2270
2123
|
} catch {
|
|
2271
2124
|
return "";
|
|
2272
2125
|
}
|
|
2273
2126
|
}
|
|
2274
|
-
function
|
|
2127
|
+
function renderToolResult(content) {
|
|
2275
2128
|
if (content === void 0 || content === null) return "";
|
|
2276
|
-
if (typeof content === "string")
|
|
2277
|
-
const trimmed = content.trim();
|
|
2278
|
-
return trimmed.length > 0 ? truncate(trimmed, TOOL_IO_MAX_CHARS) : "";
|
|
2279
|
-
}
|
|
2129
|
+
if (typeof content === "string") return content.trim();
|
|
2280
2130
|
if (Array.isArray(content)) {
|
|
2281
2131
|
const parts = [];
|
|
2282
2132
|
let sawContentEntry = false;
|
|
@@ -2291,12 +2141,12 @@ function summarizeToolResult(content) {
|
|
|
2291
2141
|
}
|
|
2292
2142
|
}
|
|
2293
2143
|
const joined = parts.join("\n").trim();
|
|
2294
|
-
if (joined.length > 0) return
|
|
2144
|
+
if (joined.length > 0) return joined;
|
|
2295
2145
|
if (sawContentEntry || content.length === 0) return "";
|
|
2296
2146
|
}
|
|
2297
2147
|
if (typeof content !== "object") return "";
|
|
2298
2148
|
if (!Array.isArray(content) && Object.keys(content).length === 0) return "";
|
|
2299
|
-
return
|
|
2149
|
+
return renderToolIo(content);
|
|
2300
2150
|
}
|
|
2301
2151
|
function truncate(s, max) {
|
|
2302
2152
|
if (s.length <= max) return s;
|
|
@@ -2320,15 +2170,19 @@ function sinkEmitter(sink, _onError) {
|
|
|
2320
2170
|
return;
|
|
2321
2171
|
case "tool":
|
|
2322
2172
|
if (sink.onToolActivity) {
|
|
2173
|
+
const input = renderToolIo(event.input);
|
|
2174
|
+
const result = event.result === void 0 ? null : renderToolResult(event.result);
|
|
2323
2175
|
await sink.onToolActivity({
|
|
2324
2176
|
toolUseId: event.id,
|
|
2325
2177
|
toolName: event.name,
|
|
2326
2178
|
summary: event.summary,
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
input:
|
|
2330
|
-
|
|
2331
|
-
|
|
2179
|
+
input: truncate(input, TOOL_IO_MAX_CHARS),
|
|
2180
|
+
result: result === null ? "" : truncate(result, TOOL_IO_MAX_CHARS),
|
|
2181
|
+
// Only when the event carried an input: a completion whose start this process never saw has
|
|
2182
|
+
// none, and a zero would overwrite the size its start recorded.
|
|
2183
|
+
...event.input === void 0 ? {} : { inputChars: input.length },
|
|
2184
|
+
...result === null ? {} : { resultChars: result.length },
|
|
2185
|
+
// The originating MCP server (null when absent / a host tool).
|
|
2332
2186
|
mcpServer: event.mcpServer ?? null,
|
|
2333
2187
|
phase: event.phase
|
|
2334
2188
|
});
|
|
@@ -2400,6 +2254,8 @@ var init_pump = __esm({
|
|
|
2400
2254
|
summary: evt.summary,
|
|
2401
2255
|
input: evt.input,
|
|
2402
2256
|
result: evt.result,
|
|
2257
|
+
...evt.inputChars !== void 0 ? { inputChars: evt.inputChars } : {},
|
|
2258
|
+
...evt.resultChars !== void 0 ? { resultChars: evt.resultChars } : {},
|
|
2403
2259
|
mcpServer: evt.mcpServer,
|
|
2404
2260
|
phase: evt.phase,
|
|
2405
2261
|
seq
|
|
@@ -2432,22 +2288,18 @@ var init_pump = __esm({
|
|
|
2432
2288
|
// The turn's final reply text, captured for the dashboard feed / logging.
|
|
2433
2289
|
finalReplyBody = null;
|
|
2434
2290
|
emittedFinalSource = "none";
|
|
2435
|
-
//
|
|
2436
|
-
//
|
|
2437
|
-
// invocation
|
|
2438
|
-
//
|
|
2439
|
-
//
|
|
2440
|
-
//
|
|
2441
|
-
// together; minting only the id would let two calls overwrite each other's
|
|
2442
|
-
// queued body.
|
|
2291
|
+
// Provider tool id → the persisted identity of its current call. The provider's id is only a
|
|
2292
|
+
// lookup key, unique within one adapter invocation at best (Codex restarts at `item_0` every
|
|
2293
|
+
// invocation, and a resumed turn keeps its turn id), so the row and the outbox entry are keyed on
|
|
2294
|
+
// what the pump mints: a host id for the host's `(turn_id, tool_use_id)` upsert, and a seq for the
|
|
2295
|
+
// outbox's `(turnId, seq)` file. Minted together; minting only the id would let two calls
|
|
2296
|
+
// overwrite each other's queued body.
|
|
2443
2297
|
//
|
|
2444
|
-
// In memory on purpose: it dies with the process, and so does the runtime.
|
|
2445
|
-
//
|
|
2446
|
-
// invocation starts its calls afresh — so there is nothing to recover.
|
|
2298
|
+
// In memory on purpose: it dies with the process, and so does the runtime. After a crash no
|
|
2299
|
+
// in-flight `done` awaits a pre-crash `start` — the resumed invocation starts its calls afresh.
|
|
2447
2300
|
toolCallByProviderId = /* @__PURE__ */ new Map();
|
|
2448
|
-
// The callback consumer the classification (via `sinkEmitter`)
|
|
2449
|
-
//
|
|
2450
|
-
// commits through the injected sink.
|
|
2301
|
+
// The callback consumer the classification (via `sinkEmitter`) and the committer test drive.
|
|
2302
|
+
// Each callback applies the choreography and commits through the injected sink.
|
|
2451
2303
|
sink;
|
|
2452
2304
|
// Whether a message has landed under this span — first words, at least. A
|
|
2453
2305
|
// span that spoke settles; one that never did is a pass.
|
|
@@ -2540,7 +2392,7 @@ var init_descriptions = __esm({
|
|
|
2540
2392
|
wake_me: {
|
|
2541
2393
|
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.",
|
|
2542
2394
|
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.",
|
|
2543
|
-
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").`
|
|
2395
|
+
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.`
|
|
2544
2396
|
},
|
|
2545
2397
|
withdraw_ask: {
|
|
2546
2398
|
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.",
|
|
@@ -2564,7 +2416,7 @@ var init_prompt_guidance = __esm({
|
|
|
2564
2416
|
ask: "put a structured question to a person",
|
|
2565
2417
|
cancel_wake: "cancel your own armed wake in this conversation",
|
|
2566
2418
|
skip_turn: "end without posting a reply",
|
|
2567
|
-
wake_me: "schedule your own return",
|
|
2419
|
+
wake_me: "schedule your own return; when it fires, say first what you came back to check",
|
|
2568
2420
|
withdraw_ask: "withdraw an open ask in this conversation with a reason"
|
|
2569
2421
|
};
|
|
2570
2422
|
}
|
|
@@ -2810,52 +2662,41 @@ function buildClaudeCodeOptions(req, augment) {
|
|
|
2810
2662
|
// pinned `model` arrives as a bare SDK id — the host already resolved the
|
|
2811
2663
|
// namespaced catalog id to it.
|
|
2812
2664
|
...model ? { model } : {},
|
|
2813
|
-
//
|
|
2814
|
-
//
|
|
2815
|
-
//
|
|
2665
|
+
// Surface thinking summaries: default adaptive+summarized, and fill in `display:'summarized'`
|
|
2666
|
+
// on a configured adaptive/enabled block that omits one. Read from the adapter's dialect slice
|
|
2667
|
+
// (`runtimeOptions['claude-code']`).
|
|
2816
2668
|
thinking: withThinkingSummaries(readThinking(config.runtimeOptions)),
|
|
2817
2669
|
...config.effort ? { effort: config.effort } : {},
|
|
2818
2670
|
// Never merge arbitrary user `~/.claude` config; without `strictMcpConfig` the
|
|
2819
2671
|
// user's existing `cabane` MCP registration could shadow ours.
|
|
2820
2672
|
settingSources: [],
|
|
2821
2673
|
strictMcpConfig: true,
|
|
2822
|
-
//
|
|
2674
|
+
// Auto-memory off unless the operator opted back in (see above).
|
|
2823
2675
|
...devControlsAutoMemory ? {} : { settings: { autoMemoryEnabled: false } },
|
|
2824
2676
|
mcpServers,
|
|
2825
2677
|
...cwd ? { cwd } : {},
|
|
2826
|
-
// Extra env (a companion prepare hook's tokens/ports
|
|
2827
|
-
// merged OVER the inherited environment.
|
|
2678
|
+
// Extra env (a companion prepare hook's tokens/ports) merged over the inherited environment.
|
|
2828
2679
|
//
|
|
2829
|
-
//
|
|
2830
|
-
//
|
|
2831
|
-
// a
|
|
2832
|
-
//
|
|
2833
|
-
//
|
|
2834
|
-
//
|
|
2835
|
-
//
|
|
2836
|
-
//
|
|
2837
|
-
//
|
|
2838
|
-
// poisoned the resumed session is an INFERENCE from one incident, consistent
|
|
2839
|
-
// with the transcript but never reproduced, and this change does not rest on
|
|
2840
|
-
// it. `Dl()` reads this variable on the Bash tool's own call
|
|
2841
|
-
// path and sets `canAutoBackground: false` and `turnAbortBackgrounds: false`
|
|
2842
|
-
// together, so a timed-out command comes back as a readable error instead.
|
|
2680
|
+
// The background-tasks kill switch goes last, so nothing in `process.env` or `req.local.env`
|
|
2681
|
+
// turns it back on. Claude Code converts a foreground command that hits its Bash ceiling
|
|
2682
|
+
// (600 000 ms) into a background task instead of erroring, and the agent then holds a phantom
|
|
2683
|
+
// task id inside a live turn; a resumed session was seen with every tool call aborted as
|
|
2684
|
+
// `background` ("The user doesn't want to take this action right now"). The conversion is
|
|
2685
|
+
// reproduced and fixed by this flag; that it poisoned the resumed session is an inference
|
|
2686
|
+
// from one incident, and this change doesn't rest on it. `Dl()` reads the variable on the
|
|
2687
|
+
// Bash tool's call path and sets `canAutoBackground: false` and `turnAbortBackgrounds: false`,
|
|
2688
|
+
// so a timed-out command returns a readable error.
|
|
2843
2689
|
//
|
|
2844
|
-
// The cost is
|
|
2845
|
-
//
|
|
2846
|
-
//
|
|
2847
|
-
//
|
|
2848
|
-
// mitigation for the automatic conversion, NOT a cleanup mechanism: step 0
|
|
2849
|
-
// measured that this flag changes no process-survival outcome on any turn-end
|
|
2850
|
-
// path. Cleanup is its own change.
|
|
2690
|
+
// The cost is deliberate: it also removes `run_in_background` (the parameter leaves the tool
|
|
2691
|
+
// schema). It is the only switch, and there is no opt-out — an escape hatch would contradict
|
|
2692
|
+
// the guarantee. It changes no process-survival outcome on any turn-end path (measured);
|
|
2693
|
+
// cleanup is a separate concern.
|
|
2851
2694
|
//
|
|
2852
|
-
//
|
|
2853
|
-
//
|
|
2854
|
-
// and why this key is now always present — previously it appeared only when
|
|
2855
|
-
// `req.local.env` was set, and an absent `env` inherits `process.env`.
|
|
2695
|
+
// `env` replaces the subprocess environment rather than merging (`Options.env`), which is why
|
|
2696
|
+
// `process.env` is spread here and the key is always present.
|
|
2856
2697
|
//
|
|
2857
|
-
//
|
|
2858
|
-
//
|
|
2698
|
+
// `secretEnv` — the values the MCP headers above reference — joins here and only here: it is
|
|
2699
|
+
// this child's environment, not the host's.
|
|
2859
2700
|
env: {
|
|
2860
2701
|
...process.env,
|
|
2861
2702
|
...req.local.env,
|
|
@@ -3377,8 +3218,7 @@ function makeRequest(overrides2 = {}) {
|
|
|
3377
3218
|
systemPrompt: "system",
|
|
3378
3219
|
prompt: "hi there",
|
|
3379
3220
|
content: [{ type: "text", text: "hi there" }],
|
|
3380
|
-
//
|
|
3381
|
-
// conformance fixture carries a real bare SDK id.
|
|
3221
|
+
// The adapter rejects a null model, so the fixture carries a real bare SDK id.
|
|
3382
3222
|
config: { model: "claude-opus-4-8" },
|
|
3383
3223
|
policy: CABANE_POLICY,
|
|
3384
3224
|
session: null,
|
|
@@ -3513,7 +3353,7 @@ var init_conformance2 = __esm({
|
|
|
3513
3353
|
phase: "start",
|
|
3514
3354
|
summary: "sleep.md",
|
|
3515
3355
|
input: { path: "sleep.md" },
|
|
3516
|
-
//
|
|
3356
|
+
// Derived from the raw `mcp__cabane__` name.
|
|
3517
3357
|
mcpServer: "cabane"
|
|
3518
3358
|
},
|
|
3519
3359
|
{
|
|
@@ -3531,9 +3371,9 @@ var init_conformance2 = __esm({
|
|
|
3531
3371
|
]
|
|
3532
3372
|
},
|
|
3533
3373
|
{
|
|
3534
|
-
//
|
|
3535
|
-
//
|
|
3536
|
-
//
|
|
3374
|
+
// Highest risk: cancel mid-stream. The abort lands after the tool result; the adapter stops
|
|
3375
|
+
// before the closing reply — no final text, no `result` event (the server writes the stopped
|
|
3376
|
+
// marker).
|
|
3537
3377
|
name: "cancel mid-stream",
|
|
3538
3378
|
request: makeRequest(),
|
|
3539
3379
|
nativeStream: [
|
|
@@ -3585,12 +3425,9 @@ var init_conformance2 = __esm({
|
|
|
3585
3425
|
]
|
|
3586
3426
|
},
|
|
3587
3427
|
{
|
|
3588
|
-
//
|
|
3589
|
-
//
|
|
3590
|
-
// `
|
|
3591
|
-
// this one produced nothing at all. The adapter's job is unchanged either way
|
|
3592
|
-
// — report faithfully what the runtime said — so it must still emit
|
|
3593
|
-
// `result{ok:true}` here; the host is what decides that a success with no
|
|
3428
|
+
// The empty-runtime anomaly's captured trace: `system/init` then a successful `result`, nothing
|
|
3429
|
+
// in between. Distinct from `empty-final` (a tool ran, no speech); this produced nothing. The
|
|
3430
|
+
// adapter reports faithfully — `result{ok:true}` — and the host decides that a success with no
|
|
3594
3431
|
// content-bearing event is a failed turn.
|
|
3595
3432
|
name: "zero-content success (empty runtime result)",
|
|
3596
3433
|
request: makeRequest(),
|
|
@@ -3598,9 +3435,9 @@ var init_conformance2 = __esm({
|
|
|
3598
3435
|
expected: [sessionEvent("s1"), { type: "result", ok: true }]
|
|
3599
3436
|
},
|
|
3600
3437
|
{
|
|
3601
|
-
//
|
|
3602
|
-
//
|
|
3603
|
-
//
|
|
3438
|
+
// On resume, a completed background task can emit a notification and its own zero-token result
|
|
3439
|
+
// before the prompt's frames. That result is not the turn boundary; decoding continues through
|
|
3440
|
+
// the reply and real usage.
|
|
3604
3441
|
name: "task-notification result does not end a resumed turn",
|
|
3605
3442
|
request: makeRequest({ session: encodeSession({ sdkSessionId: "s1", cwd: CWD }) }),
|
|
3606
3443
|
nativeStream: [
|
|
@@ -3676,8 +3513,8 @@ var init_conformance2 = __esm({
|
|
|
3676
3513
|
]
|
|
3677
3514
|
},
|
|
3678
3515
|
{
|
|
3679
|
-
//
|
|
3680
|
-
//
|
|
3516
|
+
// The filter is deliberately narrow: an origin-less zero-token result still ends the turn, even
|
|
3517
|
+
// if more frames follow.
|
|
3681
3518
|
name: "origin-less result still ends a resumed turn",
|
|
3682
3519
|
request: makeRequest({ session: encodeSession({ sdkSessionId: "s1", cwd: CWD }) }),
|
|
3683
3520
|
nativeStream: [
|
|
@@ -3748,11 +3585,10 @@ var init_conformance2 = __esm({
|
|
|
3748
3585
|
]
|
|
3749
3586
|
},
|
|
3750
3587
|
{
|
|
3751
|
-
//
|
|
3752
|
-
//
|
|
3753
|
-
//
|
|
3754
|
-
//
|
|
3755
|
-
// notice rather than agent-authored progress.
|
|
3588
|
+
// A subscription cap. The SDK emits a rejected `rate_limit_event` with a named subscription
|
|
3589
|
+
// window; the terminal error result classifies as `usage_capped`. The held provider line
|
|
3590
|
+
// independently classifies to the same kind, so it lands as a runtime notice, not agent
|
|
3591
|
+
// progress.
|
|
3756
3592
|
name: "subscription cap \u2192 usage_capped",
|
|
3757
3593
|
request: makeRequest(),
|
|
3758
3594
|
nativeStream: [
|
|
@@ -3771,9 +3607,8 @@ var init_conformance2 = __esm({
|
|
|
3771
3607
|
]
|
|
3772
3608
|
},
|
|
3773
3609
|
{
|
|
3774
|
-
//
|
|
3775
|
-
//
|
|
3776
|
-
// structured result is the same usage cap.
|
|
3610
|
+
// A failed turn can still contain the agent's own partial narration. No text classification
|
|
3611
|
+
// means no authorship rewrite, even though the structured result is the same usage cap.
|
|
3777
3612
|
name: "partial narration before subscription cap stays progress",
|
|
3778
3613
|
request: makeRequest(),
|
|
3779
3614
|
nativeStream: [
|
|
@@ -3789,8 +3624,8 @@ var init_conformance2 = __esm({
|
|
|
3789
3624
|
]
|
|
3790
3625
|
},
|
|
3791
3626
|
{
|
|
3792
|
-
//
|
|
3793
|
-
//
|
|
3627
|
+
// Recognized runtime prose stays authored when its kind disagrees with the structured terminal
|
|
3628
|
+
// failure.
|
|
3794
3629
|
name: "cap notice before server failure stays progress",
|
|
3795
3630
|
request: makeRequest(),
|
|
3796
3631
|
nativeStream: [
|
|
@@ -3812,10 +3647,9 @@ var init_conformance2 = __esm({
|
|
|
3812
3647
|
]
|
|
3813
3648
|
},
|
|
3814
3649
|
{
|
|
3815
|
-
//
|
|
3816
|
-
//
|
|
3817
|
-
//
|
|
3818
|
-
// later fails with the matching provider kind.
|
|
3650
|
+
// `classifyErrorText` deliberately searches arbitrary error blobs for status tokens. Assistant
|
|
3651
|
+
// narration needs the stricter whole-notice classifier, or an incidental count would be
|
|
3652
|
+
// de-authored when the turn later fails with the matching provider kind.
|
|
3819
3653
|
name: "incidental 5xx-shaped narration before server failure stays progress",
|
|
3820
3654
|
request: makeRequest(),
|
|
3821
3655
|
nativeStream: [
|
|
@@ -3837,9 +3671,8 @@ var init_conformance2 = __esm({
|
|
|
3837
3671
|
]
|
|
3838
3672
|
},
|
|
3839
3673
|
{
|
|
3840
|
-
//
|
|
3841
|
-
//
|
|
3842
|
-
// classifies as the cap (`usage_capped`).
|
|
3674
|
+
// A cap signalled only by the result's `terminal_reason:'blocking_limit'` (no rate-limit event)
|
|
3675
|
+
// still classifies as `usage_capped`.
|
|
3843
3676
|
name: "blocking_limit terminal reason \u2192 usage_capped",
|
|
3844
3677
|
request: makeRequest(),
|
|
3845
3678
|
nativeStream: [
|
|
@@ -3853,8 +3686,8 @@ var init_conformance2 = __esm({
|
|
|
3853
3686
|
expected: [sessionEvent("s1"), { type: "result", ok: false, reason: "usage_capped" }]
|
|
3854
3687
|
},
|
|
3855
3688
|
{
|
|
3856
|
-
//
|
|
3857
|
-
//
|
|
3689
|
+
// An expired login surfaces as an `auth_status` error frame; the failing result classifies as
|
|
3690
|
+
// `auth_expired`.
|
|
3858
3691
|
name: "expired login \u2192 auth_expired",
|
|
3859
3692
|
request: makeRequest(),
|
|
3860
3693
|
nativeStream: [
|
|
@@ -3868,10 +3701,9 @@ var init_conformance2 = __esm({
|
|
|
3868
3701
|
expected: [sessionEvent("s1"), { type: "result", ok: false, reason: "auth_expired" }]
|
|
3869
3702
|
},
|
|
3870
3703
|
{
|
|
3871
|
-
//
|
|
3872
|
-
//
|
|
3873
|
-
//
|
|
3874
|
-
// connector `limited` (the whole point of the honest taxonomy).
|
|
3704
|
+
// A transient provider outage in the terminal result text (no structured event or enum) — a 529
|
|
3705
|
+
// overload — classifies as `server_error` via the text fallback. It must not read as the cap or
|
|
3706
|
+
// stamp the connector `limited`.
|
|
3875
3707
|
name: "provider overload text \u2192 server_error",
|
|
3876
3708
|
request: makeRequest(),
|
|
3877
3709
|
nativeStream: [
|
|
@@ -3884,11 +3716,9 @@ var init_conformance2 = __esm({
|
|
|
3884
3716
|
expected: [sessionEvent("s1"), { type: "result", ok: false, reason: "server_error" }]
|
|
3885
3717
|
},
|
|
3886
3718
|
{
|
|
3887
|
-
//
|
|
3888
|
-
//
|
|
3889
|
-
//
|
|
3890
|
-
// The configured request model is irrelevant here; what's asserted is that the
|
|
3891
|
-
// concrete id the HARNESS reported comes home.
|
|
3719
|
+
// The resolved model is captured off `system/init.model` and attached to the terminal `result`
|
|
3720
|
+
// even on a plain resume (this fixture resumes, so no `session` event fires). What's asserted is
|
|
3721
|
+
// that the concrete id the harness reported comes home.
|
|
3892
3722
|
name: "resolved model captured from system/init",
|
|
3893
3723
|
request: makeRequest({ session: encodeSession({ sdkSessionId: "s1", cwd: CWD }) }),
|
|
3894
3724
|
nativeStream: [init("s1", "claude-opus-4-8"), assistantText("Done."), resultSuccess("s1")],
|
|
@@ -3898,9 +3728,8 @@ var init_conformance2 = __esm({
|
|
|
3898
3728
|
]
|
|
3899
3729
|
},
|
|
3900
3730
|
{
|
|
3901
|
-
// Session resume echo: the stored state matches the
|
|
3902
|
-
//
|
|
3903
|
-
// (the id didn't change).
|
|
3731
|
+
// Session resume echo: the stored state matches the cwd, so its SDK id is resumed and
|
|
3732
|
+
// `system/init` echoes the same id — no session event fires.
|
|
3904
3733
|
name: "session resume (no re-emit on echo)",
|
|
3905
3734
|
request: makeRequest({ session: encodeSession({ sdkSessionId: "s1", cwd: CWD }) }),
|
|
3906
3735
|
nativeStream: [init("s1"), assistantText("Back again."), resultSuccess("s1")],
|
|
@@ -3910,16 +3739,14 @@ var init_conformance2 = __esm({
|
|
|
3910
3739
|
]
|
|
3911
3740
|
},
|
|
3912
3741
|
{
|
|
3913
|
-
//
|
|
3914
|
-
//
|
|
3915
|
-
// reports a new id, and the emitted session event records the CURRENT cwd.
|
|
3742
|
+
// Cwd-mismatch degrade: the stored state was created under another cwd, so the adapter starts
|
|
3743
|
+
// fresh — `system/init` reports a new id, and the session event records the current cwd.
|
|
3916
3744
|
name: "session cwd-mismatch degrades to fresh",
|
|
3917
3745
|
request: makeRequest({ session: encodeSession({ sdkSessionId: "old", cwd: "/env/gone" }) }),
|
|
3918
3746
|
nativeStream: [init("fresh1"), assistantText("Fresh start."), resultSuccess("fresh1")],
|
|
3919
3747
|
expected: [
|
|
3920
|
-
//
|
|
3921
|
-
//
|
|
3922
|
-
// pre-mark context the discarded session no longer carries).
|
|
3748
|
+
// The degrade is flagged on the session event so the host relays it on settle and the server
|
|
3749
|
+
// rewinds the catch-up mark (recovering context the discarded session no longer carries).
|
|
3923
3750
|
sessionEvent("fresh1", CWD, true),
|
|
3924
3751
|
{ type: "text", body: "Fresh start.", terminal: true },
|
|
3925
3752
|
{ type: "result", ok: true }
|
|
@@ -4622,8 +4449,6 @@ function createOpencodeAdapter(deps = {}) {
|
|
|
4622
4449
|
const transport = deps.transport ?? (deps.serverUrl ? createHttpOpencodeTransport({ baseUrl: deps.serverUrl }) : null);
|
|
4623
4450
|
return {
|
|
4624
4451
|
name: "opencode",
|
|
4625
|
-
// CT829: one surface, so one addendum and no parameter — CT614's `codeMode`
|
|
4626
|
-
// argument went with the named tool surface it selected against.
|
|
4627
4452
|
promptAddendum: () => OPENCODE_ADDENDUM,
|
|
4628
4453
|
dialectSchema: opencodeDialectSchema,
|
|
4629
4454
|
async *runTurn(request, signal, hooks) {
|
|
@@ -4812,15 +4637,12 @@ var init_conformance3 = __esm({
|
|
|
4812
4637
|
]
|
|
4813
4638
|
},
|
|
4814
4639
|
{
|
|
4815
|
-
//
|
|
4816
|
-
//
|
|
4817
|
-
//
|
|
4818
|
-
// (
|
|
4819
|
-
//
|
|
4820
|
-
//
|
|
4821
|
-
// upserts) must carry the LATEST non-empty input + its summary, not the
|
|
4822
|
-
// first-seen `{}`. This is the case the pre-CT301 suite structurally couldn't
|
|
4823
|
-
// catch: every other tool fixture bakes the full input into the first snapshot.
|
|
4640
|
+
// A streaming provider's tool input arrives late. opencode re-emits the tool part as a full
|
|
4641
|
+
// snapshot as the args parse: DeepSeek sends `input: {}` on the early `pending`/`running` frame
|
|
4642
|
+
// and the full args on a later one (captured on opencode 1.17: `pending {}` → `running {full}` →
|
|
4643
|
+
// `completed {full}`). `start` fires off the first (empty) frame — the live card — but `done`
|
|
4644
|
+
// (and the row it upserts) must carry the latest non-empty input and its summary. Every other
|
|
4645
|
+
// tool fixture bakes the full input into the first snapshot, so only this one catches it.
|
|
4824
4646
|
name: "streaming tool input \u2014 late args refresh the done event (CT301)",
|
|
4825
4647
|
request: makeRequest2(),
|
|
4826
4648
|
nativeStream: [
|
|
@@ -4858,9 +4680,8 @@ var init_conformance3 = __esm({
|
|
|
4858
4680
|
]
|
|
4859
4681
|
},
|
|
4860
4682
|
{
|
|
4861
|
-
//
|
|
4862
|
-
//
|
|
4863
|
-
// CT1295: the server writes the `kind:'stopped'` marker.
|
|
4683
|
+
// Highest risk: cancel mid-stream. The abort lands after the tool result; the adapter stops before
|
|
4684
|
+
// the closing reply — no final text, no `result` event (the server writes the stopped marker).
|
|
4864
4685
|
name: "cancel mid-stream",
|
|
4865
4686
|
request: makeRequest2(),
|
|
4866
4687
|
nativeStream: [
|
|
@@ -4900,10 +4721,9 @@ var init_conformance3 = __esm({
|
|
|
4900
4721
|
]
|
|
4901
4722
|
},
|
|
4902
4723
|
{
|
|
4903
|
-
//
|
|
4904
|
-
//
|
|
4905
|
-
//
|
|
4906
|
-
// account cap. opencode is text-only so it inherits the CT592 split for free.
|
|
4724
|
+
// A provider 429 throttle. The wrapped `RateLimitError` is unwrapped by `readSessionError` and
|
|
4725
|
+
// text-classified: a bare "rate limit (429)" with no cap noun is the throttle (`rate_limited`),
|
|
4726
|
+
// not the account cap.
|
|
4907
4727
|
name: "provider 429 \u2192 rate_limited (throttle, no reset)",
|
|
4908
4728
|
request: makeRequest2(),
|
|
4909
4729
|
nativeStream: [
|
|
@@ -4919,9 +4739,8 @@ var init_conformance3 = __esm({
|
|
|
4919
4739
|
]
|
|
4920
4740
|
},
|
|
4921
4741
|
{
|
|
4922
|
-
//
|
|
4923
|
-
//
|
|
4924
|
-
// server renders "try again later").
|
|
4742
|
+
// A subscription cap surfaced as text ("usage limit reached") → `usage_capped`. opencode carries
|
|
4743
|
+
// no reset hint, so it's reset-less (the server says "try again later").
|
|
4925
4744
|
name: "usage cap text \u2192 usage_capped (no reset)",
|
|
4926
4745
|
request: makeRequest2(),
|
|
4927
4746
|
nativeStream: [
|
|
@@ -4937,9 +4756,7 @@ var init_conformance3 = __esm({
|
|
|
4937
4756
|
]
|
|
4938
4757
|
},
|
|
4939
4758
|
{
|
|
4940
|
-
//
|
|
4941
|
-
// honest `server_error`, never the cap. This is the outage class that used to
|
|
4942
|
-
// fall through to an opaque `unknown`.
|
|
4759
|
+
// A transient provider outage (5xx / overload) surfaced as text → `server_error`, never the cap.
|
|
4943
4760
|
name: "provider overload text \u2192 server_error",
|
|
4944
4761
|
request: makeRequest2(),
|
|
4945
4762
|
nativeStream: [
|
|
@@ -4957,7 +4774,7 @@ var init_conformance3 = __esm({
|
|
|
4957
4774
|
]
|
|
4958
4775
|
},
|
|
4959
4776
|
{
|
|
4960
|
-
//
|
|
4777
|
+
// An expired or invalid credential classifies as `auth_expired`.
|
|
4961
4778
|
name: "auth failure \u2192 auth_expired",
|
|
4962
4779
|
request: makeRequest2(),
|
|
4963
4780
|
nativeStream: [
|
|
@@ -4973,8 +4790,8 @@ var init_conformance3 = __esm({
|
|
|
4973
4790
|
]
|
|
4974
4791
|
},
|
|
4975
4792
|
{
|
|
4976
|
-
//
|
|
4977
|
-
//
|
|
4793
|
+
// An unclassified failure stays informative: its provider text is visible, and the raw machine
|
|
4794
|
+
// reason is unchanged.
|
|
4978
4795
|
name: "unclassified failure keeps a runtime notice and raw reason",
|
|
4979
4796
|
request: makeRequest2(),
|
|
4980
4797
|
nativeStream: [errored("something local broke")],
|
|
@@ -4995,8 +4812,8 @@ var init_conformance3 = __esm({
|
|
|
4995
4812
|
]
|
|
4996
4813
|
},
|
|
4997
4814
|
{
|
|
4998
|
-
//
|
|
4999
|
-
//
|
|
4815
|
+
// Empty, whitespace-only and bare status-token failures still set the machine reason but add no
|
|
4816
|
+
// empty or unhelpful transcript notice.
|
|
5000
4817
|
name: "empty failure text emits no runtime notice",
|
|
5001
4818
|
request: makeRequest2(),
|
|
5002
4819
|
nativeStream: [errored("")],
|
|
@@ -5028,12 +4845,10 @@ var init_conformance3 = __esm({
|
|
|
5028
4845
|
]
|
|
5029
4846
|
},
|
|
5030
4847
|
{
|
|
5031
|
-
//
|
|
5032
|
-
//
|
|
5033
|
-
//
|
|
5034
|
-
//
|
|
5035
|
-
// runtime that CAN produce this envelope must be shown producing it in the
|
|
5036
|
-
// shape the classifier reads.
|
|
4848
|
+
// The zero-content success the claude-code suite pins, in opencode's vocabulary — the session
|
|
4849
|
+
// goes idle having emitted no part. No opencode sample has been seen; the fixture exists because
|
|
4850
|
+
// the host's classification is runtime-neutral, so a runtime that can produce this envelope must
|
|
4851
|
+
// be shown producing it in the shape the classifier reads.
|
|
5037
4852
|
name: "zero-content success (empty runtime result)",
|
|
5038
4853
|
request: makeRequest2(),
|
|
5039
4854
|
nativeStream: [idle()],
|
|
@@ -5090,10 +4905,9 @@ var init_conformance3 = __esm({
|
|
|
5090
4905
|
]
|
|
5091
4906
|
},
|
|
5092
4907
|
{
|
|
5093
|
-
//
|
|
5094
|
-
//
|
|
5095
|
-
//
|
|
5096
|
-
// removed; restoring it is the crux of restoring cross-turn memory.
|
|
4908
|
+
// The stored state matches the directory, so the adapter resumes its id and the transport reports
|
|
4909
|
+
// `created:false` on the same id — no session event fires. Resume is what carries cross-turn
|
|
4910
|
+
// memory.
|
|
5097
4911
|
name: "session resume (no re-emit on echo)",
|
|
5098
4912
|
request: makeRequest2({ session: encodeSession2({ sessionId: "s1", directory: DIR }) }),
|
|
5099
4913
|
nativeStream: [textPart("p1", "Back again."), idle()],
|
|
@@ -5103,12 +4917,10 @@ var init_conformance3 = __esm({
|
|
|
5103
4917
|
]
|
|
5104
4918
|
},
|
|
5105
4919
|
{
|
|
5106
|
-
//
|
|
5107
|
-
//
|
|
5108
|
-
//
|
|
5109
|
-
//
|
|
5110
|
-
// injected per-turn context still can't leak into Steps. Only the assistant
|
|
5111
|
-
// reply surfaces. No session event (resume echo on the same id).
|
|
4920
|
+
// On a resumed turn the transport mints no client message id (doing so no-ops the turn), so the
|
|
4921
|
+
// decoder gets no `skipMessageId`. It learns the user-message id from the stream — the
|
|
4922
|
+
// `message.updated` user frame precedes the echoed part — and drops the echo, so the injected
|
|
4923
|
+
// per-turn context can't leak into Steps. Only the reply surfaces; no session event.
|
|
5112
4924
|
name: "session resume \u2014 prompt echo filtered via stream-derived user id",
|
|
5113
4925
|
request: makeRequest2({ session: encodeSession2({ sessionId: "s1", directory: DIR }) }),
|
|
5114
4926
|
nativeStream: [
|
|
@@ -5124,17 +4936,15 @@ var init_conformance3 = __esm({
|
|
|
5124
4936
|
]
|
|
5125
4937
|
},
|
|
5126
4938
|
{
|
|
5127
|
-
// Directory-mismatch degrade
|
|
5128
|
-
//
|
|
5129
|
-
//
|
|
5130
|
-
// event records the CURRENT directory.
|
|
4939
|
+
// Directory-mismatch degrade: the stored state was created under another directory, so the
|
|
4940
|
+
// adapter starts fresh — the transport creates a new session, and the session event records the
|
|
4941
|
+
// current directory.
|
|
5131
4942
|
name: "session directory-mismatch degrades to fresh",
|
|
5132
4943
|
request: makeRequest2({ session: encodeSession2({ sessionId: "old", directory: "/env/gone" }) }),
|
|
5133
4944
|
nativeStream: [textPart("p1", "Fresh start."), idle()],
|
|
5134
4945
|
expected: [
|
|
5135
|
-
//
|
|
5136
|
-
//
|
|
5137
|
-
// catch-up mark, recovering the context the discarded session dropped.
|
|
4946
|
+
// The degrade is flagged on the session event so the host relays it on settle and the server
|
|
4947
|
+
// rewinds the catch-up mark, recovering context the discarded session dropped.
|
|
5138
4948
|
sessionEvent2(NEW_SESSION_ID, DIR, true),
|
|
5139
4949
|
{ type: "text", body: "Fresh start.", terminal: true },
|
|
5140
4950
|
{ type: "result", ok: true }
|
|
@@ -5159,11 +4969,9 @@ var init_addendum3 = __esm({
|
|
|
5159
4969
|
"absent without attempting discovery and invocation, and if a call fails, report the recorded",
|
|
5160
4970
|
"error.",
|
|
5161
4971
|
turnControlPromptGuidance("mcp__cabane_companion__"),
|
|
5162
|
-
//
|
|
5163
|
-
//
|
|
5164
|
-
//
|
|
5165
|
-
// so the sentence contradicted itself. The bare `query` / `mutate` below stay:
|
|
5166
|
-
// that sentence is about operation names, after the qualified forms are given.
|
|
4972
|
+
// Every ancillary tool is written as Codex's own tool list shows it — qualified — so the sentence
|
|
4973
|
+
// instructing exact qualified names doesn't contradict itself. The bare `query` / `mutate` below
|
|
4974
|
+
// stay: that sentence is about operation names, after the qualified forms are given.
|
|
5167
4975
|
"The ancillary tools are `mcp__cabane__read_binary`, `mcp__cabane__upload`,",
|
|
5168
4976
|
"`mcp__cabane__begin_upload`, `mcp__cabane__finalize_upload` and",
|
|
5169
4977
|
"`mcp__cabane__mint_render_token` \u2014 all possibly deferred too.",
|
|
@@ -5419,10 +5227,9 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
5419
5227
|
threadId,
|
|
5420
5228
|
cwd: ctx.cwd ?? "",
|
|
5421
5229
|
promptFingerprint: ctx.promptFingerprint ?? null,
|
|
5422
|
-
//
|
|
5423
|
-
//
|
|
5424
|
-
//
|
|
5425
|
-
// The closing write below records the new baseline.
|
|
5230
|
+
// The baseline this turn inherited, carried through unchanged. This early write records the
|
|
5231
|
+
// thread id even if the turn dies mid-stream; it can't yet know what the turn will spend. The
|
|
5232
|
+
// closing write records the new baseline.
|
|
5426
5233
|
usageBaseline: ctx.usageBaseline ?? null
|
|
5427
5234
|
});
|
|
5428
5235
|
if (threadId !== ctx.resumedThreadId || state !== ctx.storedState) {
|
|
@@ -5716,9 +5523,8 @@ function buildConfig(req, baseInstructionsFile) {
|
|
|
5716
5523
|
return {
|
|
5717
5524
|
mcp_servers,
|
|
5718
5525
|
experimental_use_rmcp_client: true,
|
|
5719
|
-
//
|
|
5720
|
-
//
|
|
5721
|
-
// with the harness prompt it belonged to.
|
|
5526
|
+
// The no-host-access prompt replacement — Cabane's composed prompt becomes Codex's base
|
|
5527
|
+
// instructions, and Codex's environment preamble goes with the harness prompt it belonged to.
|
|
5722
5528
|
...baseInstructionsFile ? { model_instructions_file: baseInstructionsFile, include_environment_context: false } : {},
|
|
5723
5529
|
...Object.keys(shellEnv).length ? { shell_environment_policy: { set: shellEnv } } : {}
|
|
5724
5530
|
};
|
|
@@ -5839,16 +5645,11 @@ function createSdkCodexTransport(opts = {}) {
|
|
|
5839
5645
|
const codexOptions = {
|
|
5840
5646
|
...opts.apiKey ? { apiKey: opts.apiKey } : {},
|
|
5841
5647
|
...opts.codexPathOverride ? { codexPathOverride: opts.codexPathOverride } : {},
|
|
5842
|
-
// The `--config` overrides (MCP servers + rmcp flag)
|
|
5843
|
-
//
|
|
5844
|
-
//
|
|
5845
|
-
//
|
|
5846
|
-
//
|
|
5847
|
-
// ignores TOML quoting), so it has to ride inside an inline-table value,
|
|
5848
|
-
// and the SDK's flattener cannot be steered into producing one. This is
|
|
5849
|
-
// also what makes the PUBLISHED companion correct, where the vendored
|
|
5850
|
-
// patch never reached — and why that patch is gone. See
|
|
5851
|
-
// `config-overrides.ts`.
|
|
5648
|
+
// The `--config` overrides (MCP servers + rmcp flag), rendered to finished `--config` strings
|
|
5649
|
+
// here and passed through `configOverrides`, the SDK's raw passthrough — not `config`, which
|
|
5650
|
+
// flattens them itself. A user MCP server named `my.server` can't go in an override's dotted
|
|
5651
|
+
// path (the CLI splits on `.` and ignores TOML quoting), so it must ride inside an inline-table
|
|
5652
|
+
// value, which the SDK's flattener can't produce. See `config-overrides.ts`.
|
|
5852
5653
|
configOverrides: buildConfigOverrides(spec.config),
|
|
5853
5654
|
...spec.secretEnv ? { env: codexProcessEnv(spec.secretEnv) } : {}
|
|
5854
5655
|
};
|
|
@@ -5872,8 +5673,6 @@ function createCodexAdapter(deps = {}) {
|
|
|
5872
5673
|
const transport = deps.transport ?? (deps.enabled ? createSdkCodexTransport({ ...deps.apiKey ? { apiKey: deps.apiKey } : {} }) : null);
|
|
5873
5674
|
return {
|
|
5874
5675
|
name: "codex",
|
|
5875
|
-
// CT829: one surface, so one addendum and no parameter — CT614's `codeMode`
|
|
5876
|
-
// argument went with the named tool surface it selected against.
|
|
5877
5676
|
promptAddendum: () => CODEX_ADDENDUM,
|
|
5878
5677
|
dialectSchema: codexDialectSchema,
|
|
5879
5678
|
async *runTurn(request, signal, hooks) {
|
|
@@ -5965,17 +5764,14 @@ function createCodexAdapter(deps = {}) {
|
|
|
5965
5764
|
cwd: req.local.cwd,
|
|
5966
5765
|
resumedThreadId: resumeThreadId,
|
|
5967
5766
|
degraded,
|
|
5968
|
-
//
|
|
5969
|
-
//
|
|
5970
|
-
//
|
|
5971
|
-
// it, the thread still carries whatever it did before. Read off the spec
|
|
5972
|
-
// rather than recomputed, so the recorded fact can't disagree with what
|
|
5973
|
-
// was actually sent.
|
|
5767
|
+
// Which prompt the thread carries once this turn lands. If we sent one, it's that one — the
|
|
5768
|
+
// later copy is what the model reads. If we withheld it, the thread carries what it did. Read
|
|
5769
|
+
// off the spec, so the record can't disagree with what was sent.
|
|
5974
5770
|
promptFingerprint: spec.promptRidesInput ? spec.promptFingerprint : threadPromptFingerprint,
|
|
5975
5771
|
storedState: req.session,
|
|
5976
5772
|
usageBaseline: threadUsageBaseline,
|
|
5977
|
-
//
|
|
5978
|
-
//
|
|
5773
|
+
// Codex reports no model in-stream, so record what the thread was started with (null on the
|
|
5774
|
+
// default token). Same for the reasoning effort.
|
|
5979
5775
|
resolvedModel: spec.model,
|
|
5980
5776
|
resolvedReasoningEffort: spec.modelReasoningEffort ?? null
|
|
5981
5777
|
});
|
|
@@ -6030,11 +5826,9 @@ function makeRequest3(overrides2 = {}) {
|
|
|
6030
5826
|
systemPrompt: PROMPT,
|
|
6031
5827
|
prompt: "hi there",
|
|
6032
5828
|
content: [{ type: "text", text: "hi there" }],
|
|
6033
|
-
//
|
|
6034
|
-
//
|
|
6035
|
-
//
|
|
6036
|
-
// emitted → their expected results stay unchanged; a dedicated capture fixture
|
|
6037
|
-
// sets a real model + effort.
|
|
5829
|
+
// The resolved model and reasoning effort ride the terminal `result` off the run spec's model.
|
|
5830
|
+
// Behavioural fixtures use the model-less token (no `resolvedModel` emitted); a dedicated capture
|
|
5831
|
+
// fixture sets a real model and effort.
|
|
6038
5832
|
config: { model: null },
|
|
6039
5833
|
policy: COMPANION_POLICY2,
|
|
6040
5834
|
session: null,
|
|
@@ -6106,13 +5900,12 @@ var init_conformance4 = __esm({
|
|
|
6106
5900
|
});
|
|
6107
5901
|
CODEX_CONFORMANCE_FIXTURES = [
|
|
6108
5902
|
{
|
|
6109
|
-
// A plain text reply: the held agent message seals as the terminal final at
|
|
6110
|
-
//
|
|
5903
|
+
// A plain text reply: the held agent message seals as the terminal final at `turn.completed`;
|
|
5904
|
+
// usage rides the `result`.
|
|
6111
5905
|
//
|
|
6112
|
-
//
|
|
6113
|
-
// `
|
|
6114
|
-
//
|
|
6115
|
-
// out beside them. This fixture pinned the double-count before CT1283.
|
|
5906
|
+
// The counters nest — `cached_input_tokens` is a slice of `input_tokens`,
|
|
5907
|
+
// `reasoning_output_tokens` a slice of `output_tokens` — so input is 100 (not 120) and output 50
|
|
5908
|
+
// (not 60), with the cache read broken out beside them.
|
|
6116
5909
|
name: "clean turn",
|
|
6117
5910
|
request: makeRequest3(),
|
|
6118
5911
|
nativeStream: [
|
|
@@ -6129,10 +5922,8 @@ var init_conformance4 = __esm({
|
|
|
6129
5922
|
expected: [
|
|
6130
5923
|
sessionEvent3(NEW_THREAD_ID),
|
|
6131
5924
|
{ type: "text", body: "Hello there.", terminal: true },
|
|
6132
|
-
//
|
|
6133
|
-
//
|
|
6134
|
-
// `result` usage below is the frame verbatim (nothing to subtract yet);
|
|
6135
|
-
// what makes it per-turn is this figure being carried into turn two.
|
|
5925
|
+
// The closing session write — the thread's ending totals, recorded as the baseline the next turn
|
|
5926
|
+
// subtracts. A fresh thread, so the `result` usage below is the frame verbatim.
|
|
6136
5927
|
sessionEvent3(NEW_THREAD_ID, DIR2, false, fingerprintPrompt(PROMPT), {
|
|
6137
5928
|
inputTokens: 100,
|
|
6138
5929
|
outputTokens: 50,
|
|
@@ -6204,8 +5995,8 @@ var init_conformance4 = __esm({
|
|
|
6204
5995
|
]
|
|
6205
5996
|
},
|
|
6206
5997
|
{
|
|
6207
|
-
//
|
|
6208
|
-
//
|
|
5998
|
+
// The recorded MCP success shape: Codex exposes the text copy and the native object together;
|
|
5999
|
+
// normalization must keep the object.
|
|
6209
6000
|
name: "MCP success prefers native structured content",
|
|
6210
6001
|
request: makeRequest3(),
|
|
6211
6002
|
nativeStream: [
|
|
@@ -6255,8 +6046,8 @@ var init_conformance4 = __esm({
|
|
|
6255
6046
|
]
|
|
6256
6047
|
},
|
|
6257
6048
|
{
|
|
6258
|
-
//
|
|
6259
|
-
//
|
|
6049
|
+
// MCP `isError` keeps the failed phase, while the raw camel-case structured payload survives
|
|
6050
|
+
// instead of the generic `error` token.
|
|
6260
6051
|
name: "failed MCP call preserves structured receipt while remaining error phase",
|
|
6261
6052
|
request: makeRequest3(),
|
|
6262
6053
|
nativeStream: [
|
|
@@ -6308,8 +6099,8 @@ var init_conformance4 = __esm({
|
|
|
6308
6099
|
]
|
|
6309
6100
|
},
|
|
6310
6101
|
{
|
|
6311
|
-
// A transport/tool failure
|
|
6312
|
-
//
|
|
6102
|
+
// A transport/tool failure with no MCP result payload still uses the real adapter error message,
|
|
6103
|
+
// never `undefined`.
|
|
6313
6104
|
name: "failed MCP call without result keeps error message fallback",
|
|
6314
6105
|
request: makeRequest3(),
|
|
6315
6106
|
nativeStream: [
|
|
@@ -6349,8 +6140,8 @@ var init_conformance4 = __esm({
|
|
|
6349
6140
|
]
|
|
6350
6141
|
},
|
|
6351
6142
|
{
|
|
6352
|
-
//
|
|
6353
|
-
//
|
|
6143
|
+
// A connector's structured metadata beside an image-only MCP response: no image/base64 payload
|
|
6144
|
+
// enters activity receipts.
|
|
6354
6145
|
name: "image-only MCP result remains empty despite structured metadata",
|
|
6355
6146
|
request: makeRequest3(),
|
|
6356
6147
|
nativeStream: [
|
|
@@ -6443,12 +6234,10 @@ var init_conformance4 = __esm({
|
|
|
6443
6234
|
]
|
|
6444
6235
|
},
|
|
6445
6236
|
{
|
|
6446
|
-
//
|
|
6447
|
-
//
|
|
6448
|
-
//
|
|
6449
|
-
//
|
|
6450
|
-
// and carry the populated query the completed frame filled in. Before CT715 the
|
|
6451
|
-
// card was pinned at "running" forever (status derived from a missing field).
|
|
6237
|
+
// web_search lifecycle. A `web_search` item carries no `status` — completion is signalled by the
|
|
6238
|
+
// frame type (`item.started` → `item.completed`). The `start` fires off the first frame (empty
|
|
6239
|
+
// query); the `done` must resolve off `item.completed` with the populated query, not stay
|
|
6240
|
+
// "running".
|
|
6452
6241
|
name: "web_search lifecycle \u2014 completes off frame type, populated query on done",
|
|
6453
6242
|
request: makeRequest3(),
|
|
6454
6243
|
nativeStream: [
|
|
@@ -6479,9 +6268,8 @@ var init_conformance4 = __esm({
|
|
|
6479
6268
|
]
|
|
6480
6269
|
},
|
|
6481
6270
|
{
|
|
6482
|
-
//
|
|
6483
|
-
//
|
|
6484
|
-
// resolve to `done` (empty query acceptable; stuck-running is not).
|
|
6271
|
+
// A non-text web action (`action.type: "other"`) legitimately completes with an empty query —
|
|
6272
|
+
// Codex's own data. It must still resolve to `done`.
|
|
6485
6273
|
name: "web_search lifecycle \u2014 empty-query completion still resolves to done",
|
|
6486
6274
|
request: makeRequest3(),
|
|
6487
6275
|
nativeStream: [
|
|
@@ -6512,9 +6300,8 @@ var init_conformance4 = __esm({
|
|
|
6512
6300
|
]
|
|
6513
6301
|
},
|
|
6514
6302
|
{
|
|
6515
|
-
//
|
|
6516
|
-
//
|
|
6517
|
-
// CT1295: the server writes the `kind:'stopped'` marker.
|
|
6303
|
+
// Highest risk: cancel mid-stream. The abort lands after the tool result; the adapter stops before
|
|
6304
|
+
// the closing reply — no final text, no `result` event (the server writes the stopped marker).
|
|
6518
6305
|
name: "cancel mid-stream",
|
|
6519
6306
|
request: makeRequest3(),
|
|
6520
6307
|
nativeStream: [
|
|
@@ -6595,11 +6382,8 @@ var init_conformance4 = __esm({
|
|
|
6595
6382
|
]
|
|
6596
6383
|
},
|
|
6597
6384
|
{
|
|
6598
|
-
//
|
|
6599
|
-
//
|
|
6600
|
-
// classifier (Codex is text-only), so a provider 429 / outage / auth / cap
|
|
6601
|
-
// failure splits into the honest taxonomy instead of all reading `unknown`.
|
|
6602
|
-
// This is a real behavior change, not a refactor. One fixture per kind:
|
|
6385
|
+
// The failure text goes through the shared classifier (Codex is text-only), so a provider 429 /
|
|
6386
|
+
// outage / auth / cap failure splits into the honest taxonomy. One fixture per kind:
|
|
6603
6387
|
name: "turn.failed 429 \u2192 rate_limited (throttle)",
|
|
6604
6388
|
request: makeRequest3(),
|
|
6605
6389
|
nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("429 Too Many Requests")],
|
|
@@ -6646,8 +6430,8 @@ var init_conformance4 = __esm({
|
|
|
6646
6430
|
]
|
|
6647
6431
|
},
|
|
6648
6432
|
{
|
|
6649
|
-
//
|
|
6650
|
-
//
|
|
6433
|
+
// A failure text the classifier can't place keeps the raw `error:<text>` reason (the `unknown`
|
|
6434
|
+
// fallback).
|
|
6651
6435
|
name: "turn.failed unclassified \u2192 raw error reason (unchanged)",
|
|
6652
6436
|
request: makeRequest3(),
|
|
6653
6437
|
nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("something local broke")],
|
|
@@ -6671,8 +6455,8 @@ var init_conformance4 = __esm({
|
|
|
6671
6455
|
]
|
|
6672
6456
|
},
|
|
6673
6457
|
{
|
|
6674
|
-
//
|
|
6675
|
-
//
|
|
6458
|
+
// Empty, whitespace-only and bare status-token failures still set the machine reason but add no
|
|
6459
|
+
// empty or unhelpful transcript notice.
|
|
6676
6460
|
name: "empty failure text emits no runtime notice",
|
|
6677
6461
|
request: makeRequest3(),
|
|
6678
6462
|
nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("")],
|
|
@@ -6701,11 +6485,9 @@ var init_conformance4 = __esm({
|
|
|
6701
6485
|
]
|
|
6702
6486
|
},
|
|
6703
6487
|
{
|
|
6704
|
-
//
|
|
6705
|
-
//
|
|
6706
|
-
//
|
|
6707
|
-
// adapter ends the turn the moment the signal lands, with a machine reason — so
|
|
6708
|
-
// a future Codex id-drop errors clean instead of degrading mid-tool-turn.
|
|
6488
|
+
// Fail loud on unknown model metadata. An unknown `--model` makes Codex emit an `error` item
|
|
6489
|
+
// ("… Defaulting to fallback metadata …") and misbehave (the spurious tool call scripted after it
|
|
6490
|
+
// must never be emitted). The adapter ends the turn when the signal lands, with a machine reason.
|
|
6709
6491
|
name: "unknown model metadata \u2192 fail loud (guard)",
|
|
6710
6492
|
request: makeRequest3(),
|
|
6711
6493
|
nativeStream: [
|
|
@@ -6751,12 +6533,10 @@ var init_conformance4 = __esm({
|
|
|
6751
6533
|
]
|
|
6752
6534
|
},
|
|
6753
6535
|
{
|
|
6754
|
-
//
|
|
6755
|
-
//
|
|
6756
|
-
//
|
|
6757
|
-
//
|
|
6758
|
-
// neutral, so a runtime that CAN produce this envelope must be shown
|
|
6759
|
-
// producing it in the shape the classifier reads.
|
|
6536
|
+
// The zero-content success the claude-code suite pins, in Codex's vocabulary — the thread
|
|
6537
|
+
// starts, the turn completes, nothing in between. No Codex sample has been seen; the fixture
|
|
6538
|
+
// exists because the host's classification is runtime-neutral, so a runtime that can produce
|
|
6539
|
+
// this envelope must be shown producing it in the shape the classifier reads.
|
|
6760
6540
|
name: "zero-content success (empty runtime result)",
|
|
6761
6541
|
request: makeRequest3(),
|
|
6762
6542
|
nativeStream: [threadStarted(NEW_THREAD_ID), turnCompleted()],
|
|
@@ -6825,11 +6605,10 @@ var init_conformance4 = __esm({
|
|
|
6825
6605
|
]
|
|
6826
6606
|
},
|
|
6827
6607
|
{
|
|
6828
|
-
//
|
|
6829
|
-
//
|
|
6830
|
-
//
|
|
6831
|
-
//
|
|
6832
|
-
// `resolvedConfig.reasoningEffort`.
|
|
6608
|
+
// Codex reports no model in-stream, so the resolved model and reasoning effort come from what the
|
|
6609
|
+
// thread was started with (the run spec) and ride the terminal `result`: a real model and a
|
|
6610
|
+
// `runtimeOptions.codex.modelReasoningEffort` are asserted through to `resolvedModel` (bare,
|
|
6611
|
+
// `openai/`-stripped) and `resolvedConfig.reasoningEffort`.
|
|
6833
6612
|
name: "resolved model + reasoning effort captured from run spec",
|
|
6834
6613
|
request: makeRequest3({
|
|
6835
6614
|
config: {
|
|
@@ -6850,15 +6629,12 @@ var init_conformance4 = __esm({
|
|
|
6850
6629
|
]
|
|
6851
6630
|
},
|
|
6852
6631
|
{
|
|
6853
|
-
// Session resume: the stored state matches the
|
|
6854
|
-
//
|
|
6855
|
-
// event fires (the id didn't change AND the state is unchanged). This is what
|
|
6856
|
-
// carries cross-turn memory.
|
|
6632
|
+
// Session resume: the stored state matches the directory, so the thread id is resumed and
|
|
6633
|
+
// `thread.started` echoes it — no session event fires. This is what carries cross-turn memory.
|
|
6857
6634
|
//
|
|
6858
|
-
//
|
|
6859
|
-
//
|
|
6860
|
-
//
|
|
6861
|
-
// didn't change and a prompt that didn't need re-sending.
|
|
6635
|
+
// The stored state also says the thread already holds the composed prompt, so this turn
|
|
6636
|
+
// withholds the preamble and the state it would write is byte-identical: the silence proves both
|
|
6637
|
+
// an unchanged id and a prompt that needed no re-send.
|
|
6862
6638
|
name: "session resume (no re-emit on echo)",
|
|
6863
6639
|
request: makeRequest3({
|
|
6864
6640
|
session: encodeSession3({
|
|
@@ -6875,16 +6651,12 @@ var init_conformance4 = __esm({
|
|
|
6875
6651
|
]
|
|
6876
6652
|
},
|
|
6877
6653
|
{
|
|
6878
|
-
//
|
|
6879
|
-
//
|
|
6880
|
-
//
|
|
6881
|
-
//
|
|
6882
|
-
// the fingerprint recorded — on the SAME thread id, which pre-CT1075 emitted
|
|
6883
|
-
// nothing at all. That re-emit is the upgrade path: without it a long-lived
|
|
6884
|
-
// thread would re-send the whole prompt forever.
|
|
6654
|
+
// A resumed thread whose stored state carries no `promptFingerprint` field. The adapter reads the
|
|
6655
|
+
// absence as "can't say which prompt this thread holds", sends the full prompt, and re-emits the
|
|
6656
|
+
// state with the fingerprint — on the same thread id. That re-emit is the upgrade path: without
|
|
6657
|
+
// it a long-lived thread would re-send the whole prompt forever.
|
|
6885
6658
|
//
|
|
6886
|
-
// This
|
|
6887
|
-
// have no evidence about.
|
|
6659
|
+
// This fixture fails if the standing half is withheld on a thread we have no evidence about.
|
|
6888
6660
|
name: "session resume with a pre-CT1075 state re-emits with the prompt fingerprint",
|
|
6889
6661
|
request: makeRequest3({
|
|
6890
6662
|
session: JSON.stringify({ threadId: "th1", cwd: DIR2 })
|
|
@@ -6897,14 +6669,12 @@ var init_conformance4 = __esm({
|
|
|
6897
6669
|
]
|
|
6898
6670
|
},
|
|
6899
6671
|
{
|
|
6900
|
-
//
|
|
6901
|
-
//
|
|
6902
|
-
//
|
|
6903
|
-
// as on a fresh thread and the state is re-recorded against it.
|
|
6672
|
+
// The thread carries a different prompt than this turn composed — a charter edit, a renamed
|
|
6673
|
+
// agent, a redeployed prompt body. The fingerprint doesn't match, so the new prompt is sent as on
|
|
6674
|
+
// a fresh thread and the state re-recorded.
|
|
6904
6675
|
//
|
|
6905
|
-
// This
|
|
6906
|
-
//
|
|
6907
|
-
// every future charter edit from this thread.
|
|
6676
|
+
// This fixture fails if a stale prompt is treated as good enough; a boolean flag would pass the
|
|
6677
|
+
// resume fixture above and withhold every future charter edit.
|
|
6908
6678
|
name: "session resume with a stale prompt fingerprint re-sends the prompt",
|
|
6909
6679
|
request: makeRequest3({
|
|
6910
6680
|
session: encodeSession3({
|
|
@@ -6922,15 +6692,12 @@ var init_conformance4 = __esm({
|
|
|
6922
6692
|
]
|
|
6923
6693
|
},
|
|
6924
6694
|
{
|
|
6925
|
-
// Directory-mismatch degrade
|
|
6926
|
-
//
|
|
6927
|
-
//
|
|
6928
|
-
// records the CURRENT directory with `degraded: true` so the host rewinds the
|
|
6929
|
-
// catch-up mark.
|
|
6695
|
+
// Directory-mismatch degrade: the stored state was created under another directory, so the
|
|
6696
|
+
// adapter starts fresh — `thread.started` reports a new id, and the session event records the
|
|
6697
|
+
// current directory with `degraded: true` so the host rewinds the catch-up mark.
|
|
6930
6698
|
//
|
|
6931
|
-
//
|
|
6932
|
-
//
|
|
6933
|
-
// no longer in. The emitted state records the fresh thread's own answer.
|
|
6699
|
+
// The refused resume means the new thread is empty, so the full prompt goes in even though the
|
|
6700
|
+
// stored state vouched for a copy in a thread we're no longer in.
|
|
6934
6701
|
name: "session directory-mismatch degrades to fresh",
|
|
6935
6702
|
request: makeRequest3({
|
|
6936
6703
|
session: encodeSession3({
|
|
@@ -7319,8 +7086,7 @@ function createTurnContainment(turnId, deps = {}) {
|
|
|
7319
7086
|
`--unit=${unit}`,
|
|
7320
7087
|
`--property=TimeoutStopSec=${CONTAINMENT_STOP_GRACE_SEC}s`,
|
|
7321
7088
|
// Bind the scope's life to the companion's own unit, so stopping or
|
|
7322
|
-
// restarting the companion still takes its in-flight turns with it
|
|
7323
|
-
// which is what happens today, and what the scope would otherwise undo.
|
|
7089
|
+
// restarting the companion still takes its in-flight turns with it.
|
|
7324
7090
|
// Omitted outside a unit, where there is nothing to bind to.
|
|
7325
7091
|
...ownUnit ? [`--property=PartOf=${ownUnit}`] : [],
|
|
7326
7092
|
"--"
|
|
@@ -7671,7 +7437,7 @@ var init_run_config = __esm({
|
|
|
7671
7437
|
type: z15.literal("http"),
|
|
7672
7438
|
url: z15.string().url(),
|
|
7673
7439
|
headers: z15.record(z15.string(), z15.string()).optional(),
|
|
7674
|
-
//
|
|
7440
|
+
// Header → the name of a `secretEnv` entry holding its value. Set by
|
|
7675
7441
|
// the server on a room connection's entry, and by `resolveMcpSecrets` on a
|
|
7676
7442
|
// header whose `${VAR}` it resolved.
|
|
7677
7443
|
headersFromEnv: z15.record(z15.string(), z15.string()).optional()
|
|
@@ -7680,7 +7446,7 @@ var init_run_config = __esm({
|
|
|
7680
7446
|
type: z15.literal("sse"),
|
|
7681
7447
|
url: z15.string().url(),
|
|
7682
7448
|
headers: z15.record(z15.string(), z15.string()).optional(),
|
|
7683
|
-
//
|
|
7449
|
+
// Header → the name of a `secretEnv` entry holding its value. Set by
|
|
7684
7450
|
// the server on a room connection's entry, and by `resolveMcpSecrets` on a
|
|
7685
7451
|
// header whose `${VAR}` it resolved.
|
|
7686
7452
|
headersFromEnv: z15.record(z15.string(), z15.string()).optional()
|
|
@@ -7693,9 +7459,8 @@ var init_run_config = __esm({
|
|
|
7693
7459
|
]);
|
|
7694
7460
|
effortSchema = z15.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
7695
7461
|
runConfigSchema = z15.object({
|
|
7696
|
-
//
|
|
7697
|
-
//
|
|
7698
|
-
// is the locked surface. Kept in lockstep with `@cabane/shared`'s
|
|
7462
|
+
// `true` grants the host filesystem/shell; absent is the locked surface. Kept
|
|
7463
|
+
// in lockstep with `@cabane/shared`'s
|
|
7699
7464
|
// `agentRunConfigSchema` (independent zod, forward-compatible: unknown keys are
|
|
7700
7465
|
// stripped, so an older companion riding a newer server never rejects the config).
|
|
7701
7466
|
hostAccess: z15.boolean().optional(),
|
|
@@ -7786,7 +7551,7 @@ var init_protocol = __esm({
|
|
|
7786
7551
|
dispatch: "CABANE_DISPATCH",
|
|
7787
7552
|
// The turn id the launcher resolved, and whether this is a replay of a turn
|
|
7788
7553
|
// an earlier process started. The seq floor for a replay is derived inside
|
|
7789
|
-
// the turn from the outbox and the server
|
|
7554
|
+
// the turn from the outbox and the server.
|
|
7790
7555
|
turnId: "CABANE_TURN_ID",
|
|
7791
7556
|
resumed: "CABANE_RESUMED",
|
|
7792
7557
|
// Everything else the turn is constructed from, as JSON: see `RunnerSpec`.
|
|
@@ -7822,10 +7587,10 @@ var init_protocol = __esm({
|
|
|
7822
7587
|
// Whether the harness starts under the OS sandbox. The launcher sends 'os'
|
|
7823
7588
|
// only when this host can make one.
|
|
7824
7589
|
sandbox: z16.enum(["os", "none"]),
|
|
7825
|
-
//
|
|
7590
|
+
// The device's sandbox policy, validated by the launcher. Absent
|
|
7826
7591
|
// when the sandbox is off.
|
|
7827
7592
|
sandboxPolicy: sandboxPolicySchema.optional(),
|
|
7828
|
-
//
|
|
7593
|
+
// Every companion store the launcher's latest walk found, denied
|
|
7829
7594
|
// to the harness. Absent when the sandbox is off.
|
|
7830
7595
|
companionStores: z16.array(z16.string()).optional(),
|
|
7831
7596
|
logLevel: z16.string(),
|
|
@@ -7834,15 +7599,15 @@ var init_protocol = __esm({
|
|
|
7834
7599
|
// True when this (conversation, agent) pair was already told its session
|
|
7835
7600
|
// could not be saved: the notice is once per pair, not once per process.
|
|
7836
7601
|
sessionWriteNoticeClaimed: z16.boolean(),
|
|
7837
|
-
//
|
|
7602
|
+
// The runner's identity, minted with its credential. The start
|
|
7838
7603
|
// PATCH carries it so the lease knows which runner holds the span.
|
|
7839
7604
|
runnerId: z16.string(),
|
|
7840
|
-
//
|
|
7605
|
+
// The launcher keeps the runtime's session files beside the attempt
|
|
7841
7606
|
// and restores them into this runner's home, so the runner names the attempt
|
|
7842
7607
|
// each context was composed for and waits for the launcher to say it's in
|
|
7843
7608
|
// place. Absent: nothing to wait for (a runner on the launcher's own disk).
|
|
7844
7609
|
attemptState: z16.boolean().optional(),
|
|
7845
|
-
//
|
|
7610
|
+
// MCP `${NAME}` secrets come from the launcher, by name, instead of
|
|
7846
7611
|
// this machine's secrets.json. Absent: the runner reads the device's file.
|
|
7847
7612
|
secretsFromLauncher: z16.boolean().optional()
|
|
7848
7613
|
});
|
|
@@ -8431,7 +8196,7 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
8431
8196
|
return performAct(acts, {
|
|
8432
8197
|
kind: "ask",
|
|
8433
8198
|
callId: randomUUID2(),
|
|
8434
|
-
//
|
|
8199
|
+
// The swap rides the ask, so the server commits both halves
|
|
8435
8200
|
// in one transaction — the person never sees neither or both.
|
|
8436
8201
|
...args.replaces ? { replaces: args.replaces } : {},
|
|
8437
8202
|
ask: hasArray ? {
|
|
@@ -8482,7 +8247,7 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
8482
8247
|
},
|
|
8483
8248
|
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
8484
8249
|
),
|
|
8485
|
-
//
|
|
8250
|
+
// The standalone withdrawal. Not annotated destructive — an ask
|
|
8486
8251
|
// is a question, not data, and a harness that gated this on a live
|
|
8487
8252
|
// operator would hang a turn nobody is watching (the `cancel_wake`
|
|
8488
8253
|
// reasoning). Not idempotent either: a second call with the same id is
|
|
@@ -8501,7 +8266,7 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
8501
8266
|
}),
|
|
8502
8267
|
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
8503
8268
|
),
|
|
8504
|
-
//
|
|
8269
|
+
// The inverse. No arguments — it can only ever stand down the
|
|
8505
8270
|
// caller's own wake in the conversation it's holding a turn in.
|
|
8506
8271
|
tool(
|
|
8507
8272
|
CANCEL_WAKE_TOOL,
|
|
@@ -8566,7 +8331,7 @@ function buildCompanionTurnRequest(params) {
|
|
|
8566
8331
|
// adapter's `ResolvedMcpServers`.
|
|
8567
8332
|
...Object.keys(params.mcpServers).length > 0 ? { mcpServers: params.mcpServers } : {},
|
|
8568
8333
|
...params.secretEnv && Object.keys(params.secretEnv).length > 0 ? { secretEnv: params.secretEnv } : {},
|
|
8569
|
-
//
|
|
8334
|
+
// The auto-memory escape hatch, when the operator set it.
|
|
8570
8335
|
...params.claudeCode ? { claudeCode: params.claudeCode } : {}
|
|
8571
8336
|
},
|
|
8572
8337
|
// Host-injected turn-control tools, under their own namespace.
|
|
@@ -8889,7 +8654,7 @@ var init_transcript_writer = __esm({
|
|
|
8889
8654
|
}
|
|
8890
8655
|
this.line({ type: "_meta", ...meta });
|
|
8891
8656
|
}
|
|
8892
|
-
// Append
|
|
8657
|
+
// Append one event verbatim.
|
|
8893
8658
|
write(msg) {
|
|
8894
8659
|
this.line(msg);
|
|
8895
8660
|
}
|
|
@@ -8897,7 +8662,7 @@ var init_transcript_writer = __esm({
|
|
|
8897
8662
|
close(outcome) {
|
|
8898
8663
|
this.line({ type: "_outcome", ...outcome });
|
|
8899
8664
|
}
|
|
8900
|
-
//
|
|
8665
|
+
// Keep an independent copy of an empty successful envelope after the
|
|
8901
8666
|
// footer has landed. The ordinary 200-file FIFO keeps rotating as before;
|
|
8902
8667
|
// this bucket retains the newest 50 anomalies regardless of ordinary traffic.
|
|
8903
8668
|
// Best-effort like every other transcript operation: observability can never
|
|
@@ -8973,6 +8738,8 @@ var init_turn_committer = __esm({
|
|
|
8973
8738
|
summary,
|
|
8974
8739
|
input,
|
|
8975
8740
|
result,
|
|
8741
|
+
inputChars,
|
|
8742
|
+
resultChars,
|
|
8976
8743
|
mcpServer,
|
|
8977
8744
|
phase,
|
|
8978
8745
|
seq
|
|
@@ -8988,6 +8755,8 @@ var init_turn_committer = __esm({
|
|
|
8988
8755
|
summary,
|
|
8989
8756
|
input,
|
|
8990
8757
|
result,
|
|
8758
|
+
...inputChars !== void 0 ? { inputChars } : {},
|
|
8759
|
+
...resultChars !== void 0 ? { resultChars } : {},
|
|
8991
8760
|
mcpServer,
|
|
8992
8761
|
phase,
|
|
8993
8762
|
seq,
|
|
@@ -9029,12 +8798,9 @@ var init_turn_committer = __esm({
|
|
|
9029
8798
|
// Renders each `TurnEvent` onto the pump's callback sink (tool-log truncation),
|
|
9030
8799
|
// routing a throwing commit to `onError`.
|
|
9031
8800
|
emit;
|
|
9032
|
-
//
|
|
9033
|
-
//
|
|
9034
|
-
//
|
|
9035
|
-
// POST) is routed to the error hook and swallowed — a failed commit is
|
|
9036
|
-
// warn-only and must never fail the turn (the role the classification's
|
|
9037
|
-
// `safeEmit` used to play before the adapter owned the decode).
|
|
8801
|
+
// `text` / `tool` / `thinking` flow through the pump; `session` / `result` are
|
|
8802
|
+
// the dispatcher's and ignored here. A throwing commit is routed to the error
|
|
8803
|
+
// hook and swallowed: a failed commit must never fail the turn.
|
|
9038
8804
|
async ingestEvent(event) {
|
|
9039
8805
|
if (event.type === "session" || event.type === "result") return;
|
|
9040
8806
|
try {
|
|
@@ -9261,18 +9027,15 @@ var init_turn_execution = __esm({
|
|
|
9261
9027
|
turnLog;
|
|
9262
9028
|
abortController = new AbortController();
|
|
9263
9029
|
outcome = initialOutcome();
|
|
9264
|
-
//
|
|
9265
|
-
//
|
|
9266
|
-
// startup failure has nothing to reap, and reaping is a no-op rather than a
|
|
9267
|
-
// special case.
|
|
9030
|
+
// This turn's shell containment, once the claude-code adapter is built. Null
|
|
9031
|
+
// before that, so a startup failure's reap is a no-op.
|
|
9268
9032
|
containment = null;
|
|
9269
9033
|
seqCounter = 0;
|
|
9270
|
-
//
|
|
9271
|
-
//
|
|
9272
|
-
// CT1380: 0 for a NEW turn only — a resume is seeded in `fetchContext` first.
|
|
9034
|
+
// Per-turn monotonic seq. Starts at 0 for a NEW turn only; a resume is seeded
|
|
9035
|
+
// in `fetchContext` first.
|
|
9273
9036
|
nextSeq = () => ++this.seqCounter;
|
|
9274
|
-
//
|
|
9275
|
-
//
|
|
9037
|
+
// A REPLAY, told to us by the supervisor — not derivable here, since `turnId`
|
|
9038
|
+
// is set on every dispatch. `resumedFromSeq` is what it seeded from.
|
|
9276
9039
|
resumed;
|
|
9277
9040
|
resumedFromSeq;
|
|
9278
9041
|
// Whether the span this resumed run continues already has first words —
|
|
@@ -9280,8 +9043,8 @@ var init_turn_execution = __esm({
|
|
|
9280
9043
|
resumedSpoke = false;
|
|
9281
9044
|
turnContext;
|
|
9282
9045
|
resolvedMcpServers;
|
|
9283
|
-
//
|
|
9284
|
-
//
|
|
9046
|
+
// The harness's secret environment for this turn. Only ever handed to the
|
|
9047
|
+
// adapter; never logged, never merged into `process.env`.
|
|
9285
9048
|
secretEnv = {};
|
|
9286
9049
|
effectiveCwd;
|
|
9287
9050
|
hookEnv;
|
|
@@ -9295,19 +9058,15 @@ var init_turn_execution = __esm({
|
|
|
9295
9058
|
turnControlIntentFetched = false;
|
|
9296
9059
|
applyRecordedTurnControlIntent = async () => {
|
|
9297
9060
|
};
|
|
9298
|
-
//
|
|
9299
|
-
//
|
|
9300
|
-
//
|
|
9301
|
-
// loop, which runs after.
|
|
9061
|
+
// The committer is built before the lease watchdog arms, so the failed-commit
|
|
9062
|
+
// hint reaches it through this slot. A no-op until then is safe: the committer
|
|
9063
|
+
// only fires it from inside the adapter loop, which runs after.
|
|
9302
9064
|
noteCommitFailed = () => {
|
|
9303
9065
|
};
|
|
9304
9066
|
disarmWatchdogs = () => {
|
|
9305
9067
|
};
|
|
9306
|
-
//
|
|
9307
|
-
//
|
|
9308
|
-
// swallowed, because a turn that produced a good answer must not be recorded as
|
|
9309
|
-
// failed on account of a leftover process, and the failure is visible in the log
|
|
9310
|
-
// either way.
|
|
9068
|
+
// Idempotent, and a failed reap is logged and swallowed: a turn that produced a
|
|
9069
|
+
// good answer must not be recorded as failed on account of a leftover process.
|
|
9311
9070
|
async reapContainment() {
|
|
9312
9071
|
const containment = this.containment;
|
|
9313
9072
|
if (!containment) return;
|
|
@@ -9333,20 +9092,18 @@ var init_turn_execution = __esm({
|
|
|
9333
9092
|
);
|
|
9334
9093
|
}
|
|
9335
9094
|
}
|
|
9336
|
-
//
|
|
9337
|
-
//
|
|
9338
|
-
// so every exit after this point must settle THE SPAN, not just clear the
|
|
9095
|
+
// Set the moment `acquireLease`'s PATCH returns: this is the pair's running
|
|
9096
|
+
// span, so every exit after this must settle THE SPAN, not just clear the
|
|
9339
9097
|
// participant flag.
|
|
9340
9098
|
admitted = false;
|
|
9341
|
-
//
|
|
9099
|
+
// One recompose per turn when the lease refuses a stale context.
|
|
9342
9100
|
recomposedForLease = false;
|
|
9343
|
-
//
|
|
9344
|
-
//
|
|
9345
|
-
//
|
|
9346
|
-
//
|
|
9347
|
-
// only before the first adoption; no work runs until it is set.
|
|
9101
|
+
// The execution source the CURRENT context was adopted as. A
|
|
9102
|
+
// `dispatch_context_stale` recompose reassigns it, so each piece of work
|
|
9103
|
+
// (preparation, the committer, a notice) captures it when it begins and keeps
|
|
9104
|
+
// that source. No work runs before it is set.
|
|
9348
9105
|
executionSourceId;
|
|
9349
|
-
//
|
|
9106
|
+
// One fresh fetch per turn when adoption is refused.
|
|
9350
9107
|
refetchedForAdoption = false;
|
|
9351
9108
|
concluded(reason, errorReason) {
|
|
9352
9109
|
return new TurnConcluded(reason, errorReason);
|
|
@@ -9414,21 +9171,18 @@ var init_turn_execution = __esm({
|
|
|
9414
9171
|
this.supervisor.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
9415
9172
|
return { ok: false, durationMs, reason, runtime: this.turnContext?.runtime };
|
|
9416
9173
|
}
|
|
9417
|
-
// The
|
|
9418
|
-
//
|
|
9419
|
-
//
|
|
9420
|
-
//
|
|
9421
|
-
//
|
|
9422
|
-
// rule: a failure names its reason; without one the turn settled — the
|
|
9423
|
-
// runtime-unavailable exit posts its user-facing `final` before concluding,
|
|
9424
|
-
// so the turn genuinely produced its terminal output.
|
|
9174
|
+
// The admitted turn holds the conversation's lease, so the clear must NAME the
|
|
9175
|
+
// turn and DECLARE its outcome; a bare clear resets the participant flag and
|
|
9176
|
+
// leaves the span running until the lost sweep. A failure names its reason;
|
|
9177
|
+
// without one the turn settled (the runtime-unavailable exit has already posted
|
|
9178
|
+
// its `final`).
|
|
9425
9179
|
async concludeAdmittedRun(reason, errorReason) {
|
|
9426
9180
|
const { payload, turnLog, startedAt, turnId } = this;
|
|
9427
9181
|
const body = {
|
|
9428
9182
|
activeRunStartedAt: null,
|
|
9429
9183
|
turnId,
|
|
9430
|
-
//
|
|
9431
|
-
//
|
|
9184
|
+
// `runtimeIncomplete` is a real failure that deliberately carries no
|
|
9185
|
+
// `errorReason`, so the inference alone would read it as settled.
|
|
9432
9186
|
outcome: errorReason || this.outcome.runtimeIncomplete ? "failed" : "settled"
|
|
9433
9187
|
};
|
|
9434
9188
|
if (errorReason) body.errorReason = errorReason.slice(0, 200);
|
|
@@ -9522,11 +9276,10 @@ var init_turn_execution = __esm({
|
|
|
9522
9276
|
throw this.concluded(reason, reason);
|
|
9523
9277
|
}
|
|
9524
9278
|
}
|
|
9525
|
-
//
|
|
9526
|
-
//
|
|
9527
|
-
//
|
|
9528
|
-
//
|
|
9529
|
-
// nothing to adopt any more — earns a fresh fetch.
|
|
9279
|
+
// Register this context's issuance as the execution source of the work about to
|
|
9280
|
+
// run on it. No issuance ends the turn as a failed adoption does: this companion
|
|
9281
|
+
// never commits a row whose provenance it cannot name. Only a refusal earns a
|
|
9282
|
+
// fresh fetch.
|
|
9530
9283
|
async adoptSource(context) {
|
|
9531
9284
|
const { payload, turnId, turnLog } = this;
|
|
9532
9285
|
if (!context.issuanceId) {
|
|
@@ -9575,9 +9328,8 @@ var init_turn_execution = __esm({
|
|
|
9575
9328
|
secretEnv,
|
|
9576
9329
|
missing
|
|
9577
9330
|
} = resolveMcpSecrets(
|
|
9578
|
-
//
|
|
9579
|
-
//
|
|
9580
|
-
// the room connections' entries, whose values arrive in `secretEnv`.
|
|
9331
|
+
// User MCP definitions in placeholder form, plus the room connections'
|
|
9332
|
+
// entries, whose values arrive in `secretEnv`.
|
|
9581
9333
|
this.turnContext.mcpServers ?? {},
|
|
9582
9334
|
secretStore,
|
|
9583
9335
|
this.turnContext.secretEnv
|
|
@@ -9690,15 +9442,12 @@ var init_turn_execution = __esm({
|
|
|
9690
9442
|
agentUsername: this.opts.agentUsername,
|
|
9691
9443
|
runtime: this.turnContext.runtime,
|
|
9692
9444
|
hostAccess: this.turnContext.policy.hostFs,
|
|
9693
|
-
//
|
|
9694
|
-
//
|
|
9695
|
-
// an older API. The conversation anchor is gone (CT319).
|
|
9445
|
+
// The trigger message's referenced-entry paths, which a prepare hook
|
|
9446
|
+
// may key its per-task env off. `[]` from an older API.
|
|
9696
9447
|
triggerEntryPaths: this.turnContext.conversation.triggerEntryPaths ?? [],
|
|
9697
9448
|
title: this.turnContext.conversation.title,
|
|
9698
|
-
//
|
|
9699
|
-
//
|
|
9700
|
-
// context (for the live feed); this is the first thing to read it as
|
|
9701
|
-
// an INPUT, so a dispatch can ask for its environment in words.
|
|
9449
|
+
// Where an `env:` directive rides, so a dispatch can ask for its
|
|
9450
|
+
// environment in words.
|
|
9702
9451
|
messageBody: this.turnContext.message.body
|
|
9703
9452
|
},
|
|
9704
9453
|
{ turnId, deviceId: this.opts.deviceId },
|
|
@@ -9744,37 +9493,30 @@ var init_turn_execution = __esm({
|
|
|
9744
9493
|
payload.agentId,
|
|
9745
9494
|
{
|
|
9746
9495
|
activeRunStartedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9747
|
-
//
|
|
9748
|
-
//
|
|
9496
|
+
// A runner leases under its launcher's boot id, the one the device
|
|
9497
|
+
// heartbeats, or the lost-run sweep reads it as a restart.
|
|
9749
9498
|
bootId: this.opts.inRunner?.bootId ?? bootId,
|
|
9750
|
-
// CT1680: the runner holding the span, beside the device's boot id.
|
|
9751
9499
|
...this.opts.inRunner ? { runnerId: this.opts.inRunner.runnerId } : {},
|
|
9752
|
-
//
|
|
9500
|
+
// The durable record that this turn resumed, and from where.
|
|
9753
9501
|
...this.resumedFromSeq !== void 0 ? { resumedFromSeq: this.resumedFromSeq } : {},
|
|
9754
|
-
//
|
|
9755
|
-
//
|
|
9756
|
-
//
|
|
9757
|
-
//
|
|
9758
|
-
// fresh-session recompose (`freshRequest`) deliberately does not touch it:
|
|
9759
|
-
// it adopts only the prompt, never the run-config, so its issuance must
|
|
9760
|
-
// not become this attempt's evidence.
|
|
9502
|
+
// The issuance of the fetch that composed this request, so a
|
|
9503
|
+
// retransmitted start reuses the same record and a resumed attempt
|
|
9504
|
+
// echoes its own. `freshRequest` deliberately leaves it alone: it adopts
|
|
9505
|
+
// only the prompt, never the run-config.
|
|
9761
9506
|
...this.turnContext.issuanceId ? { issuanceId: this.turnContext.issuanceId } : {},
|
|
9762
|
-
//
|
|
9763
|
-
//
|
|
9764
|
-
// already emitted a "preparing" activity row for this turn above (it
|
|
9765
|
-
// runs before this flip), which would otherwise make the sweep mistake
|
|
9766
|
-
// this live turn for an abandoned one and close it with a `stopped`.
|
|
9507
|
+
// So `closeAbandonedTurns` excludes this turn: a "preparing" row emitted
|
|
9508
|
+
// before this flip would otherwise read as abandoned and be stopped.
|
|
9767
9509
|
turnId,
|
|
9768
9510
|
// The newest message this turn's read covered: the lease records it,
|
|
9769
9511
|
// and settle moves the cursor to it. Omitted when the server sent
|
|
9770
9512
|
// none (an older API), so the cursor stays where it was.
|
|
9771
9513
|
...this.turnContext.readThrough !== void 0 ? { readThrough: this.turnContext.readThrough } : {},
|
|
9772
9514
|
...this.turnContext.readRevision !== void 0 ? { readRevision: this.turnContext.readRevision } : {},
|
|
9773
|
-
//
|
|
9515
|
+
// The attempt this request's session belongs to (null = fresh).
|
|
9774
9516
|
...this.turnContext.attemptId !== void 0 ? { attemptId: this.turnContext.attemptId } : {},
|
|
9775
|
-
//
|
|
9776
|
-
//
|
|
9777
|
-
//
|
|
9517
|
+
// The frame this start acts on. The context fetch checked it before an
|
|
9518
|
+
// assembly this turn awaited; the lease re-checks where the stamp is
|
|
9519
|
+
// taken.
|
|
9778
9520
|
...payload.episode !== void 0 ? { dispatchMessageId: payload.messageId, dispatchEpisode: payload.episode } : {}
|
|
9779
9521
|
}
|
|
9780
9522
|
);
|
|
@@ -9814,21 +9556,18 @@ var init_turn_execution = __esm({
|
|
|
9814
9556
|
this.request = buildCompanionTurnRequest({
|
|
9815
9557
|
turnContext: this.turnContext,
|
|
9816
9558
|
baseUrl: this.opts.baseUrl,
|
|
9817
|
-
//
|
|
9559
|
+
// Always present: `fetchContext` refuses a context without one.
|
|
9818
9560
|
turnToken: this.turnContext.turnToken,
|
|
9819
|
-
// SJ524: the hook-resolved cwd overrides the static local cwd.
|
|
9820
9561
|
...this.effectiveCwd ? { cwd: this.effectiveCwd } : {},
|
|
9821
|
-
// CT1103: the prepare-hook env plus the authoritative host-access token.
|
|
9822
9562
|
env: this.turnEnv,
|
|
9823
9563
|
mcpServers: this.resolvedMcpServers,
|
|
9824
9564
|
secretEnv: this.secretEnv,
|
|
9825
9565
|
turnControlServer,
|
|
9826
|
-
//
|
|
9827
|
-
//
|
|
9566
|
+
// Sent as the active-conversation header so a cross-conversation post
|
|
9567
|
+
// stamps its origin.
|
|
9828
9568
|
activeConversationId: payload.conversationId,
|
|
9829
|
-
// Workspace coordinate retained in the compatible TurnRequest envelope.
|
|
9830
9569
|
workspaceId,
|
|
9831
|
-
//
|
|
9570
|
+
// The operator's machine-local auto-memory escape hatch, when set.
|
|
9832
9571
|
...this.opts.local.claudeCode ? { claudeCode: this.opts.local.claudeCode } : {}
|
|
9833
9572
|
});
|
|
9834
9573
|
}
|
|
@@ -9865,8 +9604,8 @@ var init_turn_execution = __esm({
|
|
|
9865
9604
|
if (this.opts.claudeCodeAvailable?.() ?? true) {
|
|
9866
9605
|
this.containment = createTurnContainment(this.turnId, {
|
|
9867
9606
|
log: (msg, meta) => turnLog.debug(meta ?? {}, msg),
|
|
9868
|
-
//
|
|
9869
|
-
//
|
|
9607
|
+
// A group outside the runner's own is reported, so a launcher can stop
|
|
9608
|
+
// it if the runner dies without reaping it.
|
|
9870
9609
|
...this.opts.inRunner?.onProcessGroup ? { onProcessGroup: this.opts.inRunner.onProcessGroup } : {}
|
|
9871
9610
|
});
|
|
9872
9611
|
const containment = this.containment;
|
|
@@ -9887,8 +9626,8 @@ var init_turn_execution = __esm({
|
|
|
9887
9626
|
const serverUrl = this.opts.opencodeServerUrl;
|
|
9888
9627
|
adapters.push(
|
|
9889
9628
|
createOpencodeAdapter(
|
|
9890
|
-
//
|
|
9891
|
-
//
|
|
9629
|
+
// The transport's per-server turn mutex is per process; in a runner the
|
|
9630
|
+
// turn also holds the device-wide lock every runner shares.
|
|
9892
9631
|
this.opts.inRunner ? {
|
|
9893
9632
|
transport: this.opts.inRunner.opencodeTransport(serverUrl),
|
|
9894
9633
|
onWarn
|
|
@@ -9935,7 +9674,7 @@ var init_turn_execution = __esm({
|
|
|
9935
9674
|
kind: "runtime_notice",
|
|
9936
9675
|
turnId,
|
|
9937
9676
|
parentMessageId: payload.messageId,
|
|
9938
|
-
//
|
|
9677
|
+
// Written by this attempt's source, like its speech.
|
|
9939
9678
|
...this.executionSourceId !== void 0 ? { executionSourceId: this.executionSourceId } : {}
|
|
9940
9679
|
});
|
|
9941
9680
|
} catch (postErr) {
|
|
@@ -9949,16 +9688,13 @@ var init_turn_execution = __esm({
|
|
|
9949
9688
|
throw this.concluded(`runtime_unavailable:${err.runtime}`);
|
|
9950
9689
|
}
|
|
9951
9690
|
}
|
|
9952
|
-
// The adapter refused to resume
|
|
9953
|
-
//
|
|
9954
|
-
//
|
|
9955
|
-
//
|
|
9956
|
-
//
|
|
9957
|
-
//
|
|
9958
|
-
//
|
|
9959
|
-
// The lease already granted stands; the read reaches at least its mark. A
|
|
9960
|
-
// fetch that fails returns null and the adapter falls back to the request
|
|
9961
|
-
// it has, flagged degraded, so the server rewinds the cursor at settle.
|
|
9691
|
+
// The adapter refused to resume the stored session and is starting fresh. The
|
|
9692
|
+
// request it holds assumed the session had the conversation so far (only what
|
|
9693
|
+
// landed since the cursor, the agent's own words left out); a fresh session
|
|
9694
|
+
// has none of it. So refetch as a first turn on an empty session and hand the
|
|
9695
|
+
// adapter that, before it speaks or acts. The granted lease stands. A failed
|
|
9696
|
+
// fetch returns null: the adapter runs the request it has, flagged degraded,
|
|
9697
|
+
// and the server rewinds the cursor at settle.
|
|
9962
9698
|
async freshRequest(reason) {
|
|
9963
9699
|
const { payload, turnId, turnLog } = this;
|
|
9964
9700
|
try {
|
|
@@ -9969,9 +9705,8 @@ var init_turn_execution = __esm({
|
|
|
9969
9705
|
false,
|
|
9970
9706
|
true,
|
|
9971
9707
|
void 0,
|
|
9972
|
-
//
|
|
9973
|
-
//
|
|
9974
|
-
// hold the turn open for the rest of the retry schedule.
|
|
9708
|
+
// The adapter is waiting on this, so a Stop must end the wait rather
|
|
9709
|
+
// than hold the turn open for the rest of the retry schedule.
|
|
9975
9710
|
this.abortController.signal
|
|
9976
9711
|
);
|
|
9977
9712
|
turnLog.debug(
|
|
@@ -10023,19 +9758,17 @@ var init_turn_execution = __esm({
|
|
|
10023
9758
|
conversationId: payload.conversationId,
|
|
10024
9759
|
agentId: payload.agentId,
|
|
10025
9760
|
turnId,
|
|
10026
|
-
//
|
|
10027
|
-
//
|
|
9761
|
+
// The message this turn answers, on every commit so the reply records the
|
|
9762
|
+
// Q→A edge.
|
|
10028
9763
|
parentMessageId: payload.messageId,
|
|
10029
9764
|
signal: abortController.signal,
|
|
10030
9765
|
log: turnLog,
|
|
10031
9766
|
nextSeq: this.nextSeq,
|
|
10032
9767
|
spoke: this.resumedSpoke,
|
|
10033
|
-
//
|
|
9768
|
+
// Admitted by now, so this is the source the model's work runs on.
|
|
10034
9769
|
...this.executionSourceId !== void 0 ? { executionSourceId: this.executionSourceId } : {},
|
|
10035
|
-
//
|
|
10036
|
-
// `noteCommitFailed
|
|
10037
|
-
// arms (it starts as a no-op) — the committer only fires it from inside
|
|
10038
|
-
// the adapter loop, which runs after the watchdog has replaced it.
|
|
9770
|
+
// A commit that didn't land may mean the lease is gone; see
|
|
9771
|
+
// `noteCommitFailed`.
|
|
10039
9772
|
onCommitFailed: (err) => this.noteCommitFailed(err)
|
|
10040
9773
|
});
|
|
10041
9774
|
const usesHttpTurnControl = turnRuntime === "codex" || turnRuntime === "opencode";
|
|
@@ -10139,9 +9872,9 @@ var init_turn_execution = __esm({
|
|
|
10139
9872
|
workspaceId,
|
|
10140
9873
|
payload.conversationId,
|
|
10141
9874
|
payload.agentId,
|
|
10142
|
-
//
|
|
10143
|
-
//
|
|
10144
|
-
//
|
|
9875
|
+
// Named for its turn, which a runner credential requires once
|
|
9876
|
+
// admitted; the server binds the capture to that turn's attempt
|
|
9877
|
+
// rather than the pair's.
|
|
10145
9878
|
{ agentSessionId: event.state, turnId }
|
|
10146
9879
|
);
|
|
10147
9880
|
} catch (err) {
|
|
@@ -10246,9 +9979,7 @@ var init_turn_execution = __esm({
|
|
|
10246
9979
|
}
|
|
10247
9980
|
const body = {
|
|
10248
9981
|
activeRunStartedAt: null,
|
|
10249
|
-
//
|
|
10250
|
-
// off this turn id, so the settle PATCH must carry it — the start PATCH
|
|
10251
|
-
// isn't enough. Always sent (every settle closes a real turn).
|
|
9982
|
+
// The server keys the turn-end metadata (ended_at, tokens) off this id.
|
|
10252
9983
|
turnId
|
|
10253
9984
|
};
|
|
10254
9985
|
if (o.turnUsage) {
|
|
@@ -10417,11 +10148,11 @@ var init_dispatcher = __esm({
|
|
|
10417
10148
|
this.aborts = opts.aborts ?? /* @__PURE__ */ new Map();
|
|
10418
10149
|
}
|
|
10419
10150
|
opts;
|
|
10420
|
-
//
|
|
10421
|
-
//
|
|
10151
|
+
// (conversation|agent) → per-TURN controllers. Stop is pair-scoped and must
|
|
10152
|
+
// reach every live loop for the pair.
|
|
10422
10153
|
aborts;
|
|
10423
|
-
//
|
|
10424
|
-
//
|
|
10154
|
+
// Pairs already told their session couldn't be saved: the notice is once per
|
|
10155
|
+
// pair, while the rejection repeats every turn.
|
|
10425
10156
|
sessionWriteNotified = /* @__PURE__ */ new Set();
|
|
10426
10157
|
notifyStart(info) {
|
|
10427
10158
|
try {
|
|
@@ -10438,11 +10169,6 @@ var init_dispatcher = __esm({
|
|
|
10438
10169
|
// The supervisor supplies a stable turnId on both fresh and resumed dispatch.
|
|
10439
10170
|
// resumed is separate: only replay seeds sequence/speech recovery from the
|
|
10440
10171
|
// prior span. A direct caller omitting turnId gets a fresh id.
|
|
10441
|
-
//
|
|
10442
|
-
// CT1261: a short `handle` creates a `TurnExecution` and runs it. The
|
|
10443
|
-
// supervisor seam hands the execution exactly the three cross-turn duties —
|
|
10444
|
-
// the abort registry, the session-write notice latch, the observer — and
|
|
10445
|
-
// nothing else.
|
|
10446
10172
|
async handle(payload, opts = {}) {
|
|
10447
10173
|
const key = runKey(payload.conversationId, payload.agentId);
|
|
10448
10174
|
const supervisor = {
|
|
@@ -10488,8 +10214,8 @@ var init_dispatcher = __esm({
|
|
|
10488
10214
|
}
|
|
10489
10215
|
return new TurnExecution(this.opts, supervisor, payload, opts).run();
|
|
10490
10216
|
}
|
|
10491
|
-
//
|
|
10492
|
-
//
|
|
10217
|
+
// Cancel a (conversation, agent) run in flight in THIS process. True if one
|
|
10218
|
+
// was aborted.
|
|
10493
10219
|
cancel(conversationId, agentId) {
|
|
10494
10220
|
const key = runKey(conversationId, agentId);
|
|
10495
10221
|
const pair2 = this.aborts.get(key);
|
|
@@ -11212,7 +10938,7 @@ var init_outbox = __esm({
|
|
|
11212
10938
|
}
|
|
11213
10939
|
workspaceId;
|
|
11214
10940
|
log;
|
|
11215
|
-
// Resolved lazily (per call
|
|
10941
|
+
// Resolved lazily (per call) so tests that swap HOME between
|
|
11216
10942
|
// cases route writes at the right tmpdir.
|
|
11217
10943
|
dir() {
|
|
11218
10944
|
return join20(runnerStateDir(), "outbox", encodeURIComponent(this.workspaceId));
|
|
@@ -11278,7 +11004,7 @@ var init_outbox = __esm({
|
|
|
11278
11004
|
);
|
|
11279
11005
|
return entries;
|
|
11280
11006
|
}
|
|
11281
|
-
//
|
|
11007
|
+
// The highest `seq` this turn has queued but not yet delivered — the
|
|
11282
11008
|
// half of a resumed turn's seq floor the SERVER CANNOT SEE. A commit that
|
|
11283
11009
|
// failed transiently before the crash is sitting in this directory with its
|
|
11284
11010
|
// seq already spent, and the restart's drain is fire-and-forget
|
|
@@ -12091,9 +11817,8 @@ function parseHarnessRuntime(raw) {
|
|
|
12091
11817
|
function deriveHarnessSnapshot(signals) {
|
|
12092
11818
|
const advertised = new Set(
|
|
12093
11819
|
buildCompanionManifest({
|
|
12094
|
-
//
|
|
12095
|
-
//
|
|
12096
|
-
// rather than re-decided. PATH is deliberately absent from both gates now.
|
|
11820
|
+
// Connected AND the bundled binary present: the manifest's own rule. PATH
|
|
11821
|
+
// is deliberately in neither gate.
|
|
12097
11822
|
claudeCode: signals.claudeCodeConnected && signals.claudeBundled === "present",
|
|
12098
11823
|
opencode: signals.opencodeConfigured,
|
|
12099
11824
|
codex: signals.codexEnabled && signals.codexBundled === "present"
|
|
@@ -12131,10 +11856,8 @@ function deriveClaudeCode(signals, manifestHas) {
|
|
|
12131
11856
|
...base,
|
|
12132
11857
|
state: "detected_not_exposed",
|
|
12133
11858
|
version: signals.claudeVersion,
|
|
12134
|
-
//
|
|
12135
|
-
//
|
|
12136
|
-
// device still advertises nothing — so this says so up front rather than
|
|
12137
|
-
// promising routing it can't deliver.
|
|
11859
|
+
// Connecting records INTENT, not runnability: with the bundled binary
|
|
11860
|
+
// missing a connect succeeds and nothing is advertised, so say so.
|
|
12138
11861
|
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()}`,
|
|
12139
11862
|
enable: "claude-code"
|
|
12140
11863
|
};
|
|
@@ -12172,8 +11895,7 @@ function deriveCodex(signals, manifestHas) {
|
|
|
12172
11895
|
...base,
|
|
12173
11896
|
state: "detected_not_exposed",
|
|
12174
11897
|
version: signals.codexVersion,
|
|
12175
|
-
//
|
|
12176
|
-
// launchability.
|
|
11898
|
+
// As for Claude Code: enabling is intent, not launchability.
|
|
12177
11899
|
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()}`,
|
|
12178
11900
|
enable: "codex"
|
|
12179
11901
|
};
|
|
@@ -12254,12 +11976,10 @@ async function probeHarnessSignals(cfg, deps = {}) {
|
|
|
12254
11976
|
return {
|
|
12255
11977
|
claudeOnPath: claudePresence.status === "present",
|
|
12256
11978
|
claudeVersion,
|
|
12257
|
-
//
|
|
12258
|
-
// the manifest gate and the probe above is only a suggestion.
|
|
11979
|
+
// The user's opt-in, the manifest gate; the probe above only suggests.
|
|
12259
11980
|
claudeCodeConnected: isClaudeCodeConnected(cfg),
|
|
12260
|
-
//
|
|
12261
|
-
//
|
|
12262
|
-
// next one rather than at turn time.
|
|
11981
|
+
// The executable a Claude turn launches, resolved with every probe so a
|
|
11982
|
+
// binary that disappears is caught on the next beat, not at turn time.
|
|
12263
11983
|
claudeBundled: resolveBundled("claude-code").status,
|
|
12264
11984
|
codexBundled: resolveBundled("codex").status,
|
|
12265
11985
|
// A parseable `codex --version` is our presence signal (presence alone never
|
|
@@ -12268,7 +11988,7 @@ async function probeHarnessSignals(cfg, deps = {}) {
|
|
|
12268
11988
|
codexVersion,
|
|
12269
11989
|
codexEnabled: isCodexEnabled(cfg),
|
|
12270
11990
|
opencodeConfigured: !!configuredServerUrl,
|
|
12271
|
-
// A version came back ⟺ the serve answered its health endpoint
|
|
11991
|
+
// A version came back ⟺ the serve answered its health endpoint.
|
|
12272
11992
|
opencodeReachable: opencodeVersion !== null,
|
|
12273
11993
|
opencodeVersion,
|
|
12274
11994
|
opencodeDetectedUrl: opencodeVersion !== null ? opencodeProbeUrl : null
|
|
@@ -12837,13 +12557,11 @@ function resolvePairBaseUrl(server) {
|
|
|
12837
12557
|
function writePairedConfig(paired) {
|
|
12838
12558
|
const { local, note, hadPriorConfig } = loadConfigTolerant();
|
|
12839
12559
|
const config = {
|
|
12840
|
-
//
|
|
12841
|
-
// written explicitly,
|
|
12842
|
-
//
|
|
12843
|
-
//
|
|
12844
|
-
//
|
|
12845
|
-
// disconnected instead. So it's written only when there was no config here
|
|
12846
|
-
// before. `local` spreads after, so a re-pair keeps the answer already given.
|
|
12560
|
+
// A device pairs connected to NOTHING: `claudeCode: { enabled: false }`
|
|
12561
|
+
// written explicitly, since absence is what the harness migration reads.
|
|
12562
|
+
// Omitted, a new device is auto-connected on first start; stamped on a
|
|
12563
|
+
// RE-pair, an upgrading user's working device is disconnected. So only when
|
|
12564
|
+
// there was no config before; `local` spreads after, keeping a prior answer.
|
|
12847
12565
|
...hadPriorConfig ? {} : { claudeCode: { enabled: false } },
|
|
12848
12566
|
...local,
|
|
12849
12567
|
baseUrl: paired.baseUrl,
|
|
@@ -13151,7 +12869,7 @@ var CompanionStateHub = class {
|
|
|
13151
12869
|
deviceId = null;
|
|
13152
12870
|
deviceLabel = null;
|
|
13153
12871
|
deviceError = null;
|
|
13154
|
-
//
|
|
12872
|
+
// The latest harness snapshot the supervisor probed, or null before the
|
|
13155
12873
|
// first probe (see CompanionStatusJson.harnesses).
|
|
13156
12874
|
harnesses = null;
|
|
13157
12875
|
// ---- subscription (SSE) ----
|
|
@@ -13178,7 +12896,7 @@ var CompanionStateHub = class {
|
|
|
13178
12896
|
this.deviceError = null;
|
|
13179
12897
|
this.emitStatus();
|
|
13180
12898
|
}
|
|
13181
|
-
// ---- harnesses
|
|
12899
|
+
// ---- harnesses ----
|
|
13182
12900
|
// Replace the harness snapshot and push a status change (so the SSE feed flips
|
|
13183
12901
|
// the Companion's Harnesses surface live). The supervisor calls this on each
|
|
13184
12902
|
// heartbeat-cadence probe and on an on-demand recheck.
|
|
@@ -13796,7 +13514,7 @@ var DeviceApi = class {
|
|
|
13796
13514
|
AbortSignal.timeout(5e3)
|
|
13797
13515
|
);
|
|
13798
13516
|
}
|
|
13799
|
-
//
|
|
13517
|
+
// Report that a dispatch addressed to THIS device can't be run, because
|
|
13800
13518
|
// the agent isn't one this device runs (and still wasn't after a forced
|
|
13801
13519
|
// assignments refresh). The server clears the Working flag, retires the dispatch
|
|
13802
13520
|
// and posts an explanatory system message.
|
|
@@ -13810,7 +13528,7 @@ var DeviceApi = class {
|
|
|
13810
13528
|
return this.request("POST", "/api/companion/dispatch-drop", body);
|
|
13811
13529
|
}
|
|
13812
13530
|
// Per-device liveness ping. Reports the companion build version and the env-var
|
|
13813
|
-
// names the operator's secret store exposes (never values), so
|
|
13531
|
+
// names the operator's secret store exposes (never values), so the UI can
|
|
13814
13532
|
// warn pre-emptively about an agent that needs a secret this device lacks.
|
|
13815
13533
|
heartbeat(body) {
|
|
13816
13534
|
return this.request("POST", "/api/companion/heartbeat", body ?? {});
|
|
@@ -13993,7 +13711,7 @@ var ConnectorHealthStore = class {
|
|
|
13993
13711
|
lastError: outcome.errorReason ?? null
|
|
13994
13712
|
});
|
|
13995
13713
|
}
|
|
13996
|
-
//
|
|
13714
|
+
// The store's current opinion for one runtime, or undefined when it has
|
|
13997
13715
|
// no observation. Read by the dispatcher at idle-timeout fire: an idle reap
|
|
13998
13716
|
// while this runtime is definitively `limited` (a present, future `limitedUntil`
|
|
13999
13717
|
// recorded by a sibling/earlier turn's classified cap settle) is the account cap
|
|
@@ -15574,24 +15292,20 @@ var CompanionSupervisor = class {
|
|
|
15574
15292
|
config;
|
|
15575
15293
|
log;
|
|
15576
15294
|
hub;
|
|
15577
|
-
//
|
|
15578
|
-
//
|
|
15579
|
-
//
|
|
15580
|
-
// first harness refresh simply resolves now.
|
|
15295
|
+
// The execution-presence half of both routing gates. Synchronous and cheap (a
|
|
15296
|
+
// module resolve plus a stat), so a caller before the first harness refresh
|
|
15297
|
+
// simply resolves now.
|
|
15581
15298
|
resolveBundled;
|
|
15582
|
-
//
|
|
15583
|
-
//
|
|
15584
|
-
// every 30s beat (the CT484 latch pattern, per runtime).
|
|
15299
|
+
// The last bundled status LOGGED per harness, so a missing binary is reported
|
|
15300
|
+
// at startup and on each healthy↔missing transition, not every beat.
|
|
15585
15301
|
loggedBundledStatus = /* @__PURE__ */ new Map();
|
|
15586
|
-
//
|
|
15587
|
-
//
|
|
15588
|
-
// it, so a harness updated mid-run reports its new version without a restart.
|
|
15302
|
+
// Refreshed by the per-heartbeat re-probe, so a harness updated mid-run
|
|
15303
|
+
// reports its new version without a restart.
|
|
15589
15304
|
harnessVersions;
|
|
15590
|
-
//
|
|
15591
|
-
//
|
|
15592
|
-
//
|
|
15593
|
-
//
|
|
15594
|
-
// probe (the heartbeat then falls back to the boot `claudeCode`).
|
|
15305
|
+
// The latest harness signals (presence, reachability), probed on the heartbeat
|
|
15306
|
+
// cadence and on demand. ONE source for both the manifest and the Harnesses
|
|
15307
|
+
// surface, so the UI never disagrees with what is advertised. Null until the
|
|
15308
|
+
// first probe.
|
|
15595
15309
|
harnessSignals = null;
|
|
15596
15310
|
// Fresh per attempt, never the cached heartbeat signal: installing a harness
|
|
15597
15311
|
// between attempts must be observed immediately.
|
|
@@ -15604,8 +15318,8 @@ var CompanionSupervisor = class {
|
|
|
15604
15318
|
unrunnableRetryDelaysMs;
|
|
15605
15319
|
deviceApi = null;
|
|
15606
15320
|
deviceSub = null;
|
|
15607
|
-
//
|
|
15608
|
-
//
|
|
15321
|
+
// Whether the device stream is open, which is what every workspace's link on
|
|
15322
|
+
// the dashboard reports.
|
|
15609
15323
|
deviceConnected = false;
|
|
15610
15324
|
recoveryChecked = false;
|
|
15611
15325
|
recovering = false;
|
|
@@ -15617,8 +15331,7 @@ var CompanionSupervisor = class {
|
|
|
15617
15331
|
pollTimer = null;
|
|
15618
15332
|
refreshing = false;
|
|
15619
15333
|
stopped = false;
|
|
15620
|
-
//
|
|
15621
|
-
// on every 30s heartbeat.
|
|
15334
|
+
// The version-skew warning is logged once, not every heartbeat.
|
|
15622
15335
|
versionSkewWarned = false;
|
|
15623
15336
|
// This device's id, captured from the heartbeat / assignments response. The SSE
|
|
15624
15337
|
// router uses it to tell a dispatch addressed to THIS device (a freshly-assigned
|
|
@@ -15628,12 +15341,12 @@ var CompanionSupervisor = class {
|
|
|
15628
15341
|
// The in-flight assignments pull, exposed so a caller that needs fresh data
|
|
15629
15342
|
// (pull-on-miss) can `await` the running pull rather than racing past it.
|
|
15630
15343
|
inFlightRefresh = null;
|
|
15631
|
-
//
|
|
15632
|
-
//
|
|
15633
|
-
//
|
|
15344
|
+
// Device-global connector-health memory: every dispatcher records its turn's
|
|
15345
|
+
// classified outcome per runtime; the heartbeat echoes it so the server's
|
|
15346
|
+
// quota state survives a restart or race.
|
|
15634
15347
|
connectorHealth = new ConnectorHealthStore();
|
|
15635
|
-
//
|
|
15636
|
-
//
|
|
15348
|
+
// Which dispatches this process is running, so a redelivered frame doesn't
|
|
15349
|
+
// run the turn a second time.
|
|
15637
15350
|
claims = new DispatchClaims();
|
|
15638
15351
|
constructor(opts) {
|
|
15639
15352
|
this.config = opts.config;
|
|
@@ -15654,10 +15367,10 @@ var CompanionSupervisor = class {
|
|
|
15654
15367
|
}
|
|
15655
15368
|
this.latchBootBundledState();
|
|
15656
15369
|
}
|
|
15657
|
-
//
|
|
15658
|
-
//
|
|
15370
|
+
// What a runner container said it can run, probed once at start; null until
|
|
15371
|
+
// then, and when no container could be started.
|
|
15659
15372
|
runnerImage = null;
|
|
15660
|
-
//
|
|
15373
|
+
// The companion every container mounts: this one's own install.
|
|
15661
15374
|
runnerCompanion() {
|
|
15662
15375
|
return companionSource({
|
|
15663
15376
|
configured: this.config.runners?.docker?.companionDir,
|
|
@@ -15769,10 +15482,7 @@ var CompanionSupervisor = class {
|
|
|
15769
15482
|
this.pollTimer = setInterval(() => void this.refreshAssignments(), ASSIGNMENTS_POLL_MS);
|
|
15770
15483
|
this.pollTimer.unref?.();
|
|
15771
15484
|
}
|
|
15772
|
-
//
|
|
15773
|
-
// keep the foreground process alive. With the poll loop running there's always
|
|
15774
|
-
// at least the loops keeping the event loop alive; the SSE promises let a
|
|
15775
|
-
// clean teardown resolve.
|
|
15485
|
+
// start.ts awaits these; they resolve on a clean teardown.
|
|
15776
15486
|
finishedPromises() {
|
|
15777
15487
|
return [...this.deviceSub ? [this.deviceSub.finished] : []];
|
|
15778
15488
|
}
|
|
@@ -15800,39 +15510,26 @@ var CompanionSupervisor = class {
|
|
|
15800
15510
|
bootId,
|
|
15801
15511
|
version: COMPANION_VERSION,
|
|
15802
15512
|
exposedSecretNames: store.names(),
|
|
15803
|
-
//
|
|
15804
|
-
// CT1379 claude-code and codex when the SDK's own bundled binary is there
|
|
15805
|
-
// and the user asked for the harness, CT270 opencode when the operator
|
|
15806
|
-
// configured an `opencode serve`.
|
|
15513
|
+
// Each runtime only when this device can actually run it.
|
|
15807
15514
|
manifest: buildCompanionManifest({
|
|
15808
|
-
//
|
|
15809
|
-
//
|
|
15810
|
-
// the user's `claudeCode.enabled`. Claude Code rides neither alone.
|
|
15515
|
+
// Consented (`claudeCode.enabled`) AND launchable (the live bundled
|
|
15516
|
+
// binary). Neither alone.
|
|
15811
15517
|
claudeCode: this.claudeCodeOffered(),
|
|
15812
15518
|
opencode: !!this.config.opencode?.serverUrl,
|
|
15813
|
-
//
|
|
15814
|
-
//
|
|
15815
|
-
// which is how a device with a half-installed 331 MB platform package
|
|
15816
|
-
// advertised a runtime that could not start.
|
|
15519
|
+
// Enabled AND its vendored binary present: a half-installed 331 MB
|
|
15520
|
+
// platform package otherwise advertises a runtime that cannot start.
|
|
15817
15521
|
codex: this.codexOffered(),
|
|
15818
|
-
//
|
|
15819
|
-
// above. Diagnostic only — the server never routes on it — and always
|
|
15522
|
+
// WHY an asked-for harness is absent above. Diagnostic only, and always
|
|
15820
15523
|
// sent, so an empty array clears a previously reported issue.
|
|
15821
15524
|
runtimeIssues: this.runtimeIssues(),
|
|
15822
|
-
//
|
|
15823
|
-
// (fail-soft to null). Informational only — the server matches on name.
|
|
15525
|
+
// Informational only; the server matches on name.
|
|
15824
15526
|
versions: this.harnessVersions,
|
|
15825
|
-
//
|
|
15527
|
+
// Shown on the device page.
|
|
15826
15528
|
...runnersEnabled(this.config) ? { runner: this.runnerManifest() } : {}
|
|
15827
15529
|
}),
|
|
15828
|
-
//
|
|
15829
|
-
//
|
|
15830
|
-
// failure/heal, so a fresh companion's beat is unchanged and the server's
|
|
15831
|
-
// manifest synthesis (status-less rows) still runs.
|
|
15530
|
+
// Omitted until the first observed failure or heal, so the server's
|
|
15531
|
+
// status-less manifest synthesis still runs for a fresh companion.
|
|
15832
15532
|
...connectorReports.length > 0 ? { connectors: connectorReports } : {},
|
|
15833
|
-
// CT584: include enumerated models only when the probe SUCCEEDED (non-null).
|
|
15834
|
-
// A null (probe failed / no opencode) omits the field, and the server then
|
|
15835
|
-
// leaves this device's stored availability untouched.
|
|
15836
15533
|
...opencodeModels !== null ? { models: opencodeModels } : {}
|
|
15837
15534
|
});
|
|
15838
15535
|
this.hub.setDevice({ deviceId: res.deviceId });
|
|
@@ -15847,11 +15544,8 @@ var CompanionSupervisor = class {
|
|
|
15847
15544
|
);
|
|
15848
15545
|
}
|
|
15849
15546
|
}
|
|
15850
|
-
//
|
|
15851
|
-
//
|
|
15852
|
-
// companion's, warn loudly (once). This is exactly the skew that bit the M1 walk:
|
|
15853
|
-
// an npm-pinned companion lagging a from-develop server. The elimination (bundled
|
|
15854
|
-
// companion) makes it match by construction; this catches a companion run out of band.
|
|
15547
|
+
// Warn once when the server's build version differs from this companion's: an
|
|
15548
|
+
// npm-pinned companion lagging the server is the usual cause of odd failures.
|
|
15855
15549
|
checkVersionSkew(serverVersion) {
|
|
15856
15550
|
if (this.versionSkewWarned) return;
|
|
15857
15551
|
if (!serverVersion || serverVersion === COMPANION_VERSION) return;
|
|
@@ -16048,10 +15742,9 @@ var CompanionSupervisor = class {
|
|
|
16048
15742
|
`Stopped running ${runner.displayName} (unassigned)`
|
|
16049
15743
|
);
|
|
16050
15744
|
}
|
|
16051
|
-
//
|
|
16052
|
-
//
|
|
16053
|
-
//
|
|
16054
|
-
// the caller's half.
|
|
15745
|
+
// Can this machine LAUNCH the harness — is the SDK's bundled executable there?
|
|
15746
|
+
// The per-beat result, or resolved now before the first refresh. Execution
|
|
15747
|
+
// presence ONLY; consent is the caller's half.
|
|
16055
15748
|
bundledStatus(runtime) {
|
|
16056
15749
|
const cached2 = runtime === "codex" ? this.harnessSignals?.codexBundled : this.harnessSignals?.claudeBundled;
|
|
16057
15750
|
return cached2 ?? this.resolveBundled(runtime).status;
|
|
@@ -16062,23 +15755,19 @@ var CompanionSupervisor = class {
|
|
|
16062
15755
|
bundledResolution(runtime) {
|
|
16063
15756
|
return this.resolveBundled(runtime);
|
|
16064
15757
|
}
|
|
16065
|
-
//
|
|
16066
|
-
//
|
|
16067
|
-
//
|
|
16068
|
-
//
|
|
16069
|
-
// consent in front of it and the real executable underneath it).
|
|
15758
|
+
// Does this device OFFER claude-code — connected by its user, and able to start
|
|
15759
|
+
// the binary a turn runs? The ONE signal both the heartbeat manifest and the
|
|
15760
|
+
// adapter registry read, so what the device advertises and what it can select
|
|
15761
|
+
// can't disagree.
|
|
16070
15762
|
claudeCodeOffered() {
|
|
16071
15763
|
return isClaudeCodeConnected(this.config) && this.bundledStatus("claude-code") === "present";
|
|
16072
15764
|
}
|
|
16073
|
-
//
|
|
16074
|
-
// alone — so an enabled device with no vendored binary advertised a runtime it
|
|
16075
|
-
// could not start. Same shape as Claude Code, same predicate on both sides.
|
|
15765
|
+
// The same question for codex: enabled AND its vendored binary present.
|
|
16076
15766
|
codexOffered() {
|
|
16077
15767
|
return isCodexEnabled(this.config) && this.bundledStatus("codex") === "present";
|
|
16078
15768
|
}
|
|
16079
|
-
//
|
|
16080
|
-
//
|
|
16081
|
-
// for one harness.
|
|
15769
|
+
// Derived from the same two facts as the predicates above, so an issue and an
|
|
15770
|
+
// advertisement are never both made for one harness.
|
|
16082
15771
|
runtimeIssues() {
|
|
16083
15772
|
return runtimeIssuesFor({
|
|
16084
15773
|
claudeCode: {
|
|
@@ -16088,26 +15777,16 @@ var CompanionSupervisor = class {
|
|
|
16088
15777
|
codex: { intended: isCodexEnabled(this.config), bundled: this.bundledStatus("codex") }
|
|
16089
15778
|
});
|
|
16090
15779
|
}
|
|
16091
|
-
//
|
|
16092
|
-
//
|
|
16093
|
-
//
|
|
16094
|
-
// repro and carries nothing that could be a credential or a vendor's output.
|
|
16095
|
-
//
|
|
16096
|
-
// Two things this deliberately does NOT do (Codo's review, blocking finding #2):
|
|
15780
|
+
// A missing binary is reported ONCE, and again only when the answer CHANGES,
|
|
15781
|
+
// with runtime, platform/arch and status: enough to find it in `companion.log`
|
|
15782
|
+
// without a repro, and nothing that could be a credential or vendor output.
|
|
16097
15783
|
//
|
|
16098
|
-
// -
|
|
16099
|
-
//
|
|
16100
|
-
//
|
|
16101
|
-
//
|
|
16102
|
-
//
|
|
16103
|
-
//
|
|
16104
|
-
// debug record instead, present for diagnosis without competing for
|
|
16105
|
-
// attention with the warning the operator already read.
|
|
16106
|
-
// - It does not report a transition in generic prose. A binary that goes
|
|
16107
|
-
// missing at 3am is the case the operator will actually meet, and a line
|
|
16108
|
-
// that says only "binary is missing" leaves them with no repair and no
|
|
16109
|
-
// restart instruction. Transitions carry the specified copy verbatim — the
|
|
16110
|
-
// same sentence the startup path prints.
|
|
15784
|
+
// - The boot state is not re-announced: `warnAboutHarnessReadiness` said it
|
|
15785
|
+
// on stderr at startup, when a person is watching (the `--daemon` launcher
|
|
15786
|
+
// runs that preflight in the FOREGROUND because the child logs to a file).
|
|
15787
|
+
// It is latched in the constructor and recorded at debug.
|
|
15788
|
+
// - A transition carries the same repair copy the startup path prints, not
|
|
15789
|
+
// generic prose: a binary missing at 3am needs the repair and the restart.
|
|
16111
15790
|
logBundledTransitions() {
|
|
16112
15791
|
for (const runtime of ["claude-code", "codex"]) {
|
|
16113
15792
|
const intended = runtime === "codex" ? isCodexEnabled(this.config) : isClaudeCodeConnected(this.config);
|
|
@@ -16130,9 +15809,8 @@ var CompanionSupervisor = class {
|
|
|
16130
15809
|
this.log.debug(meta, "companion: bundled harness binary is missing (harness not in use)");
|
|
16131
15810
|
}
|
|
16132
15811
|
}
|
|
16133
|
-
//
|
|
16134
|
-
//
|
|
16135
|
-
// lookup plus a stat, so it costs nothing and needs no await.
|
|
15812
|
+
// The boot state, latched so the first refresh reports no news, and recorded
|
|
15813
|
+
// once as evidence. Synchronous: a module lookup plus a stat.
|
|
16136
15814
|
latchBootBundledState() {
|
|
16137
15815
|
for (const runtime of ["claude-code", "codex"]) {
|
|
16138
15816
|
const resolution = this.resolveBundled(runtime);
|
|
@@ -16150,35 +15828,34 @@ var CompanionSupervisor = class {
|
|
|
16150
15828
|
);
|
|
16151
15829
|
}
|
|
16152
15830
|
}
|
|
16153
|
-
//
|
|
15831
|
+
// The locks every runner on this device shares.
|
|
16154
15832
|
runnerLocks = new DeviceLocks();
|
|
16155
|
-
//
|
|
16156
|
-
//
|
|
15833
|
+
// `runners.sandbox`, checked against this host once. A host that can't make
|
|
15834
|
+
// the sandbox says so once and runs its turns unwrapped.
|
|
16157
15835
|
sandboxUsable;
|
|
16158
15836
|
sandboxHost = {
|
|
16159
15837
|
osSandboxUsable: () => this.sandboxUsable ??= osSandboxUsable(this.log)
|
|
16160
15838
|
};
|
|
16161
|
-
//
|
|
16162
|
-
//
|
|
15839
|
+
// `runners.policy`, parsed once. An invalid one is kept as the error, which
|
|
15840
|
+
// every sandboxed turn is refused with.
|
|
16163
15841
|
parsedSandboxPolicy;
|
|
16164
15842
|
sandboxPolicy() {
|
|
16165
15843
|
this.parsedSandboxPolicy ??= parseSandboxPolicy(this.config.runners?.policy);
|
|
16166
15844
|
return this.parsedSandboxPolicy;
|
|
16167
15845
|
}
|
|
16168
|
-
//
|
|
15846
|
+
// The walk for other companions' stores, run off the turn path.
|
|
16169
15847
|
storeWatch;
|
|
16170
15848
|
companionStores() {
|
|
16171
15849
|
this.storeWatch ??= companionStoreWatch(this.log);
|
|
16172
15850
|
return this.storeWatch;
|
|
16173
15851
|
}
|
|
16174
|
-
//
|
|
16175
|
-
//
|
|
16176
|
-
//
|
|
15852
|
+
// The sandbox one agent's turns run under: its `agents` row, then the
|
|
15853
|
+
// device's. Resolved from the current config on every ask, so a reload
|
|
15854
|
+
// changes it; only the host probe is kept.
|
|
16177
15855
|
runnerSandboxFor(row) {
|
|
16178
15856
|
return runnerSandboxFor(this.config, row, this.sandboxHost);
|
|
16179
15857
|
}
|
|
16180
|
-
//
|
|
16181
|
-
// against the agents it runs right now.
|
|
15858
|
+
// Resolved against the agents this device runs right now.
|
|
16182
15859
|
runnerManifest() {
|
|
16183
15860
|
const agents = [...this.workspaces.values()].flatMap(
|
|
16184
15861
|
(wr) => [...wr.agents.values()].map((agent) => ({
|
|
@@ -16192,7 +15869,7 @@ var CompanionSupervisor = class {
|
|
|
16192
15869
|
...runnerSandboxReport(this.config, agents, this.sandboxHost)
|
|
16193
15870
|
};
|
|
16194
15871
|
}
|
|
16195
|
-
//
|
|
15872
|
+
// One provider for the device, built on first use.
|
|
16196
15873
|
runnerProvider() {
|
|
16197
15874
|
this.providerForRunners ??= this.dockerRunners() ? this.createDockerRunners() : createLocalProcessProvider();
|
|
16198
15875
|
return this.providerForRunners;
|
|
@@ -16234,8 +15911,7 @@ var CompanionSupervisor = class {
|
|
|
16234
15911
|
log: this.log
|
|
16235
15912
|
});
|
|
16236
15913
|
}
|
|
16237
|
-
//
|
|
16238
|
-
// companion in a container, this process's otherwise.
|
|
15914
|
+
// The image's own companion in a container, this process's otherwise.
|
|
16239
15915
|
runnerCommandFor() {
|
|
16240
15916
|
if (this.runnerCommand) return this.runnerCommand;
|
|
16241
15917
|
return this.dockerRunners() ? ["cabane-companion", "run-turn"] : selfRunnerCommand();
|
|
@@ -16267,36 +15943,20 @@ var CompanionSupervisor = class {
|
|
|
16267
15943
|
aborts: ctx.aborts,
|
|
16268
15944
|
runConfig: ctx.runConfig,
|
|
16269
15945
|
log: this.log.child({ agentName: ctx.agentDisplayName ?? ctx.agentUsername }),
|
|
16270
|
-
//
|
|
16271
|
-
//
|
|
16272
|
-
//
|
|
16273
|
-
//
|
|
16274
|
-
// runnable, so a disconnected harness can't be selected either.
|
|
16275
|
-
// CT1379: and the same for the bundled binary — the predicate is shared with
|
|
16276
|
-
// the heartbeat above, so a runtime advertised by one and refused by the
|
|
16277
|
-
// other is not expressible.
|
|
15946
|
+
// Read PER TURN off the heartbeat's own predicates, so a harness installed
|
|
15947
|
+
// or connected after boot works on the next turn exactly as it appears on
|
|
15948
|
+
// the next beat, and a runtime advertised by one and refused by the other
|
|
15949
|
+
// is not expressible.
|
|
16278
15950
|
claudeCodeAvailable: () => this.claudeCodeOffered(),
|
|
16279
|
-
// CT270: the opencode server URL (operator-configured), when this device
|
|
16280
|
-
// offers the opencode runtime. Threaded so an opencode turn selects the
|
|
16281
|
-
// opencode adapter; unset leaves the device claude-code-only.
|
|
16282
15951
|
...this.config.opencode?.serverUrl ? { opencodeServerUrl: this.config.opencode.serverUrl } : {},
|
|
16283
|
-
// CT481: register the codex adapter when this device offers codex; a miss
|
|
16284
|
-
// leaves an `openai/…` turn to fail loudly (no silent claude-code fallback).
|
|
16285
|
-
// CT1379: read PER TURN, like claude-code above, and off the same predicate
|
|
16286
|
-
// the heartbeat uses — it was captured here at dispatcher-construction time
|
|
16287
|
-
// from the enable flag alone, so a device could register an adapter for a
|
|
16288
|
-
// binary it did not have and keep it registered after the binary came back.
|
|
16289
15952
|
codexAvailable: () => this.codexOffered(),
|
|
16290
|
-
//
|
|
16291
|
-
//
|
|
16292
|
-
//
|
|
16293
|
-
// override set here still wins, and is deliberately not validated against
|
|
16294
|
-
// the tool ceilings it may reorder — see DEFAULT_AGENT_IDLE_TIMEOUT_MS.
|
|
15953
|
+
// Unset uses the dispatcher's defaults. An override is deliberately not
|
|
15954
|
+
// validated against the tool ceilings it may reorder; see
|
|
15955
|
+
// DEFAULT_AGENT_IDLE_TIMEOUT_MS.
|
|
16295
15956
|
...positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") !== void 0 ? { idleTimeoutMs: positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") } : {},
|
|
16296
15957
|
...positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") !== void 0 ? { totalTimeoutMs: positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") } : {},
|
|
16297
15958
|
observer: this.hub.observerFor(ctx.workspaceId, ctx.agentId, ctx.workspaceSlug),
|
|
16298
15959
|
transcriptDir: transcriptsDir(),
|
|
16299
|
-
// CT566: the shared connector-health memory the turn records into on settle.
|
|
16300
15960
|
connectorHealth: this.connectorHealth,
|
|
16301
15961
|
...runnersEnabled(this.config) ? {
|
|
16302
15962
|
runner: {
|
|
@@ -16320,11 +15980,10 @@ var CompanionSupervisor = class {
|
|
|
16320
15980
|
this.workspaces.delete(workspaceId);
|
|
16321
15981
|
this.hub.removeWorkspace(workspaceId);
|
|
16322
15982
|
}
|
|
16323
|
-
// ---- SSE event routing (
|
|
16324
|
-
//
|
|
16325
|
-
//
|
|
16326
|
-
//
|
|
16327
|
-
// instead of resuming from a `Last-Event-ID`.
|
|
15983
|
+
// ---- SSE event routing (BY agentId) ----
|
|
15984
|
+
// Dispatch and cancel, addressed to this device. The stream keeps no cursor:
|
|
15985
|
+
// it replays what is still offered on every connect instead of resuming from
|
|
15986
|
+
// a `Last-Event-ID`.
|
|
16328
15987
|
async handleDeviceMessage(ev) {
|
|
16329
15988
|
const wire = this.parseFrame(ev);
|
|
16330
15989
|
if (!wire) return;
|
|
@@ -16360,9 +16019,9 @@ var CompanionSupervisor = class {
|
|
|
16360
16019
|
return null;
|
|
16361
16020
|
}
|
|
16362
16021
|
}
|
|
16363
|
-
//
|
|
16364
|
-
//
|
|
16365
|
-
//
|
|
16022
|
+
// A reconnect replays every dispatch still offered, including one whose turn
|
|
16023
|
+
// is already running here; the first copy runs it and a later one waits for
|
|
16024
|
+
// it and then does nothing.
|
|
16366
16025
|
//
|
|
16367
16026
|
// A cancel needs no gate: `dispatcher.cancel` on an already-aborted run is a
|
|
16368
16027
|
// no-op that reports `false`, so a second copy is inert.
|
|
@@ -16378,8 +16037,6 @@ var CompanionSupervisor = class {
|
|
|
16378
16037
|
if (claim) this.claims.release(wire.id);
|
|
16379
16038
|
}
|
|
16380
16039
|
}
|
|
16381
|
-
// Everything below here is the routing as it was: it takes a frame and knows
|
|
16382
|
-
// nothing about which stream carried it.
|
|
16383
16040
|
async routeFrame(wr, wire) {
|
|
16384
16041
|
if (wire.type === "device:cancel_requested") {
|
|
16385
16042
|
const payload2 = {
|
|
@@ -16445,17 +16102,12 @@ var CompanionSupervisor = class {
|
|
|
16445
16102
|
await tail;
|
|
16446
16103
|
if (wr.chains.get(chainKey) === tail) wr.chains.delete(chainKey);
|
|
16447
16104
|
}
|
|
16448
|
-
//
|
|
16449
|
-
//
|
|
16105
|
+
// Tell the server this device cannot run a dispatch it was addressed to.
|
|
16450
16106
|
// Returns whether the report LANDED. The server checks the recorded dispatch
|
|
16451
|
-
// destination even if the
|
|
16452
|
-
// explains the roster miss; the thirty-minute unanswered-offer deadline is
|
|
16453
|
-
// only the backstop when no report lands.
|
|
16107
|
+
// destination even if the assignment has moved.
|
|
16454
16108
|
//
|
|
16455
|
-
//
|
|
16456
|
-
//
|
|
16457
|
-
// hard — this runs inline on the event loop, and an unbounded wait here would
|
|
16458
|
-
// wedge the workspace's whole stream.
|
|
16109
|
+
// Retried, because the common failure is transient (a 503 mid-deploy), and
|
|
16110
|
+
// bounded hard: this runs inline, and an unbounded wait would wedge the stream.
|
|
16459
16111
|
async reportUnrunnableDispatch(workspaceId, payload) {
|
|
16460
16112
|
if (!this.deviceApi) return false;
|
|
16461
16113
|
let lastErr;
|
|
@@ -16495,9 +16147,8 @@ var CompanionSupervisor = class {
|
|
|
16495
16147
|
);
|
|
16496
16148
|
return false;
|
|
16497
16149
|
}
|
|
16498
|
-
//
|
|
16499
|
-
//
|
|
16500
|
-
// the one the launcher still holds whatever became of the runner.
|
|
16150
|
+
// A runner is gone without having settled its turn. Same bounded retry, on the
|
|
16151
|
+
// device credential, which the launcher holds whatever became of the runner.
|
|
16501
16152
|
async reportRunnerGone(report) {
|
|
16502
16153
|
if (!this.deviceApi) return;
|
|
16503
16154
|
let lastErr;
|
|
@@ -16522,12 +16173,9 @@ var CompanionSupervisor = class {
|
|
|
16522
16173
|
"Couldn't tell Cabane a turn's runner stopped; its credential expires on its own."
|
|
16523
16174
|
);
|
|
16524
16175
|
}
|
|
16525
|
-
// Pull-on-miss
|
|
16526
|
-
//
|
|
16527
|
-
//
|
|
16528
|
-
// and return the now-live runner. Returns undefined when the dispatch isn't for
|
|
16529
|
-
// this device, or the agent still isn't ours after pulling (a genuine
|
|
16530
|
-
// not-ours dispatch) — the caller then settles and ignores it as before.
|
|
16176
|
+
// Pull-on-miss: a dispatch addressed to THIS device for an agent we don't run
|
|
16177
|
+
// yet means the assignment changed under us. Undefined when it isn't for this
|
|
16178
|
+
// device, or the agent still isn't ours after the pull.
|
|
16531
16179
|
async recoverRacedAgent(wr, payload) {
|
|
16532
16180
|
if (!this.deviceId || payload.deviceId !== this.deviceId) return void 0;
|
|
16533
16181
|
if (this.refreshing && this.inFlightRefresh) await this.inFlightRefresh;
|
|
@@ -16568,9 +16216,8 @@ var CompanionSupervisor = class {
|
|
|
16568
16216
|
};
|
|
16569
16217
|
const tail = this.runDispatch(
|
|
16570
16218
|
wr,
|
|
16571
|
-
//
|
|
16572
|
-
//
|
|
16573
|
-
// path's own guard.
|
|
16219
|
+
// Off `/runs`, not a frame, so no dispatch id to dedupe on;
|
|
16220
|
+
// `recoveredTurns` is this path's guard.
|
|
16574
16221
|
"",
|
|
16575
16222
|
payload,
|
|
16576
16223
|
agent,
|
|
@@ -16592,9 +16239,7 @@ var CompanionSupervisor = class {
|
|
|
16592
16239
|
this.recovering = false;
|
|
16593
16240
|
}
|
|
16594
16241
|
}
|
|
16595
|
-
// One turn: the
|
|
16596
|
-
// the console summary. Mirrors the old per-workspace `handleEvent`, now
|
|
16597
|
-
// scoped to one agent.
|
|
16242
|
+
// One turn: the skip-vs-resume dedupe, the dispatch, then the console summary.
|
|
16598
16243
|
async runDispatch(wr, dispatchId, payload, agent, recoveredTurnId) {
|
|
16599
16244
|
const workspaceId = wr.workspaceId;
|
|
16600
16245
|
if (dispatchId && hasDispatched(workspaceId, dispatchId)) {
|
|
@@ -16679,7 +16324,7 @@ var CompanionSupervisor = class {
|
|
|
16679
16324
|
);
|
|
16680
16325
|
}
|
|
16681
16326
|
}
|
|
16682
|
-
// ---- per-agent outbox drain scheduler
|
|
16327
|
+
// ---- per-agent outbox drain scheduler ----
|
|
16683
16328
|
makeDrain(api, agentId) {
|
|
16684
16329
|
let drainTimer;
|
|
16685
16330
|
let drainDelay = DRAIN_BASE_MS;
|
|
@@ -16748,19 +16393,16 @@ var CompanionSupervisor = class {
|
|
|
16748
16393
|
saveConfig(next);
|
|
16749
16394
|
configureLogger(this.log, next);
|
|
16750
16395
|
}
|
|
16751
|
-
// ---- harnesses
|
|
16752
|
-
// Probe all three harnesses, cache
|
|
16753
|
-
//
|
|
16754
|
-
//
|
|
16755
|
-
// wrapped anyway so a harness refresh can never take a heartbeat down. Called on
|
|
16756
|
-
// the heartbeat cadence (fresh presence for the manifest + UI) and on demand.
|
|
16396
|
+
// ---- harnesses ----
|
|
16397
|
+
// Probe all three harnesses, cache signals and versions, and push the snapshot
|
|
16398
|
+
// to the hub. `probeHarnessSignals` never throws, but this is wrapped anyway so
|
|
16399
|
+
// a refresh can never take a heartbeat down.
|
|
16757
16400
|
async refreshHarnessStatuses(presence = {}) {
|
|
16758
16401
|
try {
|
|
16759
16402
|
const signals = await probeHarnessSignals(this.config, {
|
|
16760
16403
|
probePresence: this.probePresence,
|
|
16761
16404
|
presence,
|
|
16762
|
-
//
|
|
16763
|
-
// CLI probes, through the supervisor's seam so a test pins the host fact.
|
|
16405
|
+
// Through the supervisor's seam, so a test pins the host fact.
|
|
16764
16406
|
resolveBundled: this.resolveBundled
|
|
16765
16407
|
});
|
|
16766
16408
|
this.harnessSignals = signals;
|
|
@@ -16783,11 +16425,10 @@ var CompanionSupervisor = class {
|
|
|
16783
16425
|
async recheckHarnesses() {
|
|
16784
16426
|
await this.refreshHarnessStatuses();
|
|
16785
16427
|
}
|
|
16786
|
-
//
|
|
16787
|
-
//
|
|
16788
|
-
//
|
|
16789
|
-
//
|
|
16790
|
-
// an answer. Coalesces with an in-flight beat rather than stacking a second.
|
|
16428
|
+
// Beat NOW and wait for it to land. `start` calls this before asking the
|
|
16429
|
+
// person anything, so the browser's connect step already shows what this
|
|
16430
|
+
// machine has rather than sitting blank while the terminal waits on an answer.
|
|
16431
|
+
// Coalesces with an in-flight beat.
|
|
16791
16432
|
async heartbeatNow() {
|
|
16792
16433
|
if (this.inFlightHeartbeat) {
|
|
16793
16434
|
await this.inFlightHeartbeat;
|
|
@@ -16796,35 +16437,25 @@ var CompanionSupervisor = class {
|
|
|
16796
16437
|
this.kickHeartbeat();
|
|
16797
16438
|
if (this.inFlightHeartbeat) await this.inFlightHeartbeat;
|
|
16798
16439
|
}
|
|
16799
|
-
//
|
|
16800
|
-
//
|
|
16801
|
-
// against the URL/flag that was just persisted, not the one this process booted
|
|
16802
|
-
// with.
|
|
16440
|
+
// The config as it stands now, after any `enableHarness` write: the control
|
|
16441
|
+
// socket's shake-out check needs what was just persisted, not the boot config.
|
|
16803
16442
|
currentConfig() {
|
|
16804
16443
|
return this.config;
|
|
16805
16444
|
}
|
|
16806
|
-
//
|
|
16807
|
-
// `~/.cabane/config.json
|
|
16808
|
-
// never drives a login (BYO — Decided).
|
|
16445
|
+
// Enable a config-driven harness by flipping the flag the app owns in
|
|
16446
|
+
// `~/.cabane/config.json`. Never installs a binary or drives a login (BYO).
|
|
16809
16447
|
//
|
|
16810
|
-
//
|
|
16811
|
-
//
|
|
16812
|
-
//
|
|
16813
|
-
//
|
|
16814
|
-
//
|
|
16815
|
-
//
|
|
16816
|
-
// -
|
|
16817
|
-
//
|
|
16818
|
-
//
|
|
16819
|
-
//
|
|
16820
|
-
//
|
|
16821
|
-
// - opencode: set `opencode.serverUrl` — but ONLY after health-probing the URL,
|
|
16822
|
-
// so we never advertise a serve that isn't there. An unreachable URL is a
|
|
16823
|
-
// typed failure the UI shows in place, not a silent bad write.
|
|
16824
|
-
// On success, persist, re-probe, and beat immediately, so a runtime that IS
|
|
16825
|
-
// launchable starts taking turns at once rather than on the next tick — and one
|
|
16826
|
-
// that isn't reports its issue just as promptly. Returns a typed outcome (never
|
|
16827
|
-
// throws for a reachable/valid input).
|
|
16448
|
+
// This records INTENT, the user's half: a harness is advertised only when also
|
|
16449
|
+
// its bundled binary resolves, so an enable on a companion whose optional
|
|
16450
|
+
// dependency never installed advertises nothing, and the Harnesses surface
|
|
16451
|
+
// says why.
|
|
16452
|
+
// - claude-code: `claudeCode.enabled`, after a presence check. The terminal
|
|
16453
|
+
// offer and the web pairing flow both call this.
|
|
16454
|
+
// - codex: `codex.enabled`, after a presence check (login stays the user's).
|
|
16455
|
+
// - opencode: `opencode.serverUrl`, ONLY after health-probing the URL; an
|
|
16456
|
+
// unreachable one is a typed failure shown in place.
|
|
16457
|
+
// On success: persist, re-probe and beat at once, so the answer is true in
|
|
16458
|
+
// Cabane promptly either way. Never throws for a valid input.
|
|
16828
16459
|
async enableHarness(input) {
|
|
16829
16460
|
let next;
|
|
16830
16461
|
let checkedPresence = null;
|
|
@@ -16875,9 +16506,8 @@ var CompanionSupervisor = class {
|
|
|
16875
16506
|
return { ok: true };
|
|
16876
16507
|
}
|
|
16877
16508
|
// Re-create every running agent's Dispatcher from the CURRENT config, keeping
|
|
16878
|
-
// the runner (its api
|
|
16879
|
-
//
|
|
16880
|
-
// the adapter registry — pick up their new values without an app restart.
|
|
16509
|
+
// the runner (its api, outbox, chains) intact, so constructor-captured options
|
|
16510
|
+
// pick up a config change without a restart.
|
|
16881
16511
|
rebuildDispatchers() {
|
|
16882
16512
|
for (const wr of this.workspaces.values()) {
|
|
16883
16513
|
for (const runner of wr.agents.values()) {
|
|
@@ -17045,8 +16675,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
17045
16675
|
}));
|
|
17046
16676
|
await warnAboutHarnessReadiness(cfg, {
|
|
17047
16677
|
probeClaude: async () => claudeCode,
|
|
17048
|
-
//
|
|
17049
|
-
// its own words (the `!` block, or the per-harness offer), so it hands us a
|
|
16678
|
+
// The CLI's onboarding script says this in its own words, so it hands a
|
|
17050
16679
|
// sink that logs instead. Every other caller keeps the stderr line.
|
|
17051
16680
|
...opts.onReadinessWarning ? { warn: opts.onReadinessWarning } : {}
|
|
17052
16681
|
});
|
|
@@ -17083,11 +16712,10 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
17083
16712
|
const clearRuntimeOnExit = () => clearRuntimeStateIfOurs(instanceId);
|
|
17084
16713
|
process.on("exit", clearRuntimeOnExit);
|
|
17085
16714
|
const hub = new CompanionStateHub({
|
|
17086
|
-
//
|
|
17087
|
-
// with. The dashboard's connection line shows it.
|
|
16715
|
+
// The dashboard's connection line shows it.
|
|
17088
16716
|
baseUrl: cfg.baseUrl,
|
|
17089
16717
|
companionVersion: COMPANION_VERSION,
|
|
17090
|
-
//
|
|
16718
|
+
// Surfaced on `/api/status` so `stop`/`status` can confirm the
|
|
17091
16719
|
// process behind the marker pid is this companion (not a recycled pid).
|
|
17092
16720
|
instanceId
|
|
17093
16721
|
});
|
|
@@ -17140,7 +16768,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
17140
16768
|
socket: control.path,
|
|
17141
16769
|
...dashboard ? { url: dashboard.url, port: dashboard.port } : {},
|
|
17142
16770
|
startedAt,
|
|
17143
|
-
//
|
|
16771
|
+
// The daemon launcher sets this env on the detached child, so the
|
|
17144
16772
|
// marker records whether this companion is backgrounded (foreground start
|
|
17145
16773
|
// leaves it unset → false).
|
|
17146
16774
|
daemon: process.env.CABANE_COMPANION_DAEMON === "1",
|
|
@@ -17244,7 +16872,7 @@ async function startDaemon(opts = {}, deps = {}) {
|
|
|
17244
16872
|
});
|
|
17245
16873
|
await ensureRuntime(cfg, {
|
|
17246
16874
|
probeClaude: async () => claudeOnPath2,
|
|
17247
|
-
//
|
|
16875
|
+
// The onboarding script already said what's connectable, in its own
|
|
17248
16876
|
// words. Saying it again here, on stderr, in the middle of the closing block
|
|
17249
16877
|
// would be the same news twice.
|
|
17250
16878
|
...quiet ? { warn: () => {
|