@cabane/companion 0.6.145 → 0.6.147
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -51
- package/dist/cli.js +631 -1008
- package/dist/pairing-config.js +56 -95
- package/dist/runtime.js +625 -1000
- package/dist/static/app.js +9 -11
- package/package.json +1 -1
package/dist/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
|
|
@@ -2322,12 +2178,11 @@ function sinkEmitter(sink, _onError) {
|
|
|
2322
2178
|
summary: event.summary,
|
|
2323
2179
|
input: truncate(input, TOOL_IO_MAX_CHARS),
|
|
2324
2180
|
result: result === null ? "" : truncate(result, TOOL_IO_MAX_CHARS),
|
|
2325
|
-
//
|
|
2326
|
-
//
|
|
2327
|
-
// the size its start recorded.
|
|
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.
|
|
2328
2183
|
...event.input === void 0 ? {} : { inputChars: input.length },
|
|
2329
2184
|
...result === null ? {} : { resultChars: result.length },
|
|
2330
|
-
//
|
|
2185
|
+
// The originating MCP server (null when absent / a host tool).
|
|
2331
2186
|
mcpServer: event.mcpServer ?? null,
|
|
2332
2187
|
phase: event.phase
|
|
2333
2188
|
});
|
|
@@ -2433,22 +2288,18 @@ var init_pump = __esm({
|
|
|
2433
2288
|
// The turn's final reply text, captured for the dashboard feed / logging.
|
|
2434
2289
|
finalReplyBody = null;
|
|
2435
2290
|
emittedFinalSource = "none";
|
|
2436
|
-
//
|
|
2437
|
-
//
|
|
2438
|
-
// invocation
|
|
2439
|
-
//
|
|
2440
|
-
//
|
|
2441
|
-
//
|
|
2442
|
-
// together; minting only the id would let two calls overwrite each other's
|
|
2443
|
-
// 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.
|
|
2444
2297
|
//
|
|
2445
|
-
// In memory on purpose: it dies with the process, and so does the runtime.
|
|
2446
|
-
//
|
|
2447
|
-
// 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.
|
|
2448
2300
|
toolCallByProviderId = /* @__PURE__ */ new Map();
|
|
2449
|
-
// The callback consumer the classification (via `sinkEmitter`)
|
|
2450
|
-
//
|
|
2451
|
-
// 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.
|
|
2452
2303
|
sink;
|
|
2453
2304
|
// Whether a message has landed under this span — first words, at least. A
|
|
2454
2305
|
// span that spoke settles; one that never did is a pass.
|
|
@@ -2541,7 +2392,7 @@ var init_descriptions = __esm({
|
|
|
2541
2392
|
wake_me: {
|
|
2542
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.",
|
|
2543
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.",
|
|
2544
|
-
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.`
|
|
2545
2396
|
},
|
|
2546
2397
|
withdraw_ask: {
|
|
2547
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.",
|
|
@@ -2565,7 +2416,7 @@ var init_prompt_guidance = __esm({
|
|
|
2565
2416
|
ask: "put a structured question to a person",
|
|
2566
2417
|
cancel_wake: "cancel your own armed wake in this conversation",
|
|
2567
2418
|
skip_turn: "end without posting a reply",
|
|
2568
|
-
wake_me: "schedule your own return",
|
|
2419
|
+
wake_me: "schedule your own return; when it fires, say first what you came back to check",
|
|
2569
2420
|
withdraw_ask: "withdraw an open ask in this conversation with a reason"
|
|
2570
2421
|
};
|
|
2571
2422
|
}
|
|
@@ -2811,52 +2662,41 @@ function buildClaudeCodeOptions(req, augment) {
|
|
|
2811
2662
|
// pinned `model` arrives as a bare SDK id — the host already resolved the
|
|
2812
2663
|
// namespaced catalog id to it.
|
|
2813
2664
|
...model ? { model } : {},
|
|
2814
|
-
//
|
|
2815
|
-
//
|
|
2816
|
-
//
|
|
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']`).
|
|
2817
2668
|
thinking: withThinkingSummaries(readThinking(config.runtimeOptions)),
|
|
2818
2669
|
...config.effort ? { effort: config.effort } : {},
|
|
2819
2670
|
// Never merge arbitrary user `~/.claude` config; without `strictMcpConfig` the
|
|
2820
2671
|
// user's existing `cabane` MCP registration could shadow ours.
|
|
2821
2672
|
settingSources: [],
|
|
2822
2673
|
strictMcpConfig: true,
|
|
2823
|
-
//
|
|
2674
|
+
// Auto-memory off unless the operator opted back in (see above).
|
|
2824
2675
|
...devControlsAutoMemory ? {} : { settings: { autoMemoryEnabled: false } },
|
|
2825
2676
|
mcpServers,
|
|
2826
2677
|
...cwd ? { cwd } : {},
|
|
2827
|
-
// Extra env (a companion prepare hook's tokens/ports
|
|
2828
|
-
// merged OVER the inherited environment.
|
|
2678
|
+
// Extra env (a companion prepare hook's tokens/ports) merged over the inherited environment.
|
|
2829
2679
|
//
|
|
2830
|
-
//
|
|
2831
|
-
//
|
|
2832
|
-
// a
|
|
2833
|
-
//
|
|
2834
|
-
//
|
|
2835
|
-
//
|
|
2836
|
-
//
|
|
2837
|
-
//
|
|
2838
|
-
//
|
|
2839
|
-
// poisoned the resumed session is an INFERENCE from one incident, consistent
|
|
2840
|
-
// with the transcript but never reproduced, and this change does not rest on
|
|
2841
|
-
// it. `Dl()` reads this variable on the Bash tool's own call
|
|
2842
|
-
// path and sets `canAutoBackground: false` and `turnAbortBackgrounds: false`
|
|
2843
|
-
// 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.
|
|
2844
2689
|
//
|
|
2845
|
-
// The cost is
|
|
2846
|
-
//
|
|
2847
|
-
//
|
|
2848
|
-
//
|
|
2849
|
-
// mitigation for the automatic conversion, NOT a cleanup mechanism: step 0
|
|
2850
|
-
// measured that this flag changes no process-survival outcome on any turn-end
|
|
2851
|
-
// 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.
|
|
2852
2694
|
//
|
|
2853
|
-
//
|
|
2854
|
-
//
|
|
2855
|
-
// and why this key is now always present — previously it appeared only when
|
|
2856
|
-
// `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.
|
|
2857
2697
|
//
|
|
2858
|
-
//
|
|
2859
|
-
//
|
|
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.
|
|
2860
2700
|
env: {
|
|
2861
2701
|
...process.env,
|
|
2862
2702
|
...req.local.env,
|
|
@@ -3378,8 +3218,7 @@ function makeRequest(overrides2 = {}) {
|
|
|
3378
3218
|
systemPrompt: "system",
|
|
3379
3219
|
prompt: "hi there",
|
|
3380
3220
|
content: [{ type: "text", text: "hi there" }],
|
|
3381
|
-
//
|
|
3382
|
-
// conformance fixture carries a real bare SDK id.
|
|
3221
|
+
// The adapter rejects a null model, so the fixture carries a real bare SDK id.
|
|
3383
3222
|
config: { model: "claude-opus-4-8" },
|
|
3384
3223
|
policy: CABANE_POLICY,
|
|
3385
3224
|
session: null,
|
|
@@ -3514,7 +3353,7 @@ var init_conformance2 = __esm({
|
|
|
3514
3353
|
phase: "start",
|
|
3515
3354
|
summary: "sleep.md",
|
|
3516
3355
|
input: { path: "sleep.md" },
|
|
3517
|
-
//
|
|
3356
|
+
// Derived from the raw `mcp__cabane__` name.
|
|
3518
3357
|
mcpServer: "cabane"
|
|
3519
3358
|
},
|
|
3520
3359
|
{
|
|
@@ -3532,9 +3371,9 @@ var init_conformance2 = __esm({
|
|
|
3532
3371
|
]
|
|
3533
3372
|
},
|
|
3534
3373
|
{
|
|
3535
|
-
//
|
|
3536
|
-
//
|
|
3537
|
-
//
|
|
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).
|
|
3538
3377
|
name: "cancel mid-stream",
|
|
3539
3378
|
request: makeRequest(),
|
|
3540
3379
|
nativeStream: [
|
|
@@ -3586,12 +3425,9 @@ var init_conformance2 = __esm({
|
|
|
3586
3425
|
]
|
|
3587
3426
|
},
|
|
3588
3427
|
{
|
|
3589
|
-
//
|
|
3590
|
-
//
|
|
3591
|
-
// `
|
|
3592
|
-
// this one produced nothing at all. The adapter's job is unchanged either way
|
|
3593
|
-
// — report faithfully what the runtime said — so it must still emit
|
|
3594
|
-
// `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
|
|
3595
3431
|
// content-bearing event is a failed turn.
|
|
3596
3432
|
name: "zero-content success (empty runtime result)",
|
|
3597
3433
|
request: makeRequest(),
|
|
@@ -3599,9 +3435,9 @@ var init_conformance2 = __esm({
|
|
|
3599
3435
|
expected: [sessionEvent("s1"), { type: "result", ok: true }]
|
|
3600
3436
|
},
|
|
3601
3437
|
{
|
|
3602
|
-
//
|
|
3603
|
-
//
|
|
3604
|
-
//
|
|
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.
|
|
3605
3441
|
name: "task-notification result does not end a resumed turn",
|
|
3606
3442
|
request: makeRequest({ session: encodeSession({ sdkSessionId: "s1", cwd: CWD }) }),
|
|
3607
3443
|
nativeStream: [
|
|
@@ -3677,8 +3513,8 @@ var init_conformance2 = __esm({
|
|
|
3677
3513
|
]
|
|
3678
3514
|
},
|
|
3679
3515
|
{
|
|
3680
|
-
//
|
|
3681
|
-
//
|
|
3516
|
+
// The filter is deliberately narrow: an origin-less zero-token result still ends the turn, even
|
|
3517
|
+
// if more frames follow.
|
|
3682
3518
|
name: "origin-less result still ends a resumed turn",
|
|
3683
3519
|
request: makeRequest({ session: encodeSession({ sdkSessionId: "s1", cwd: CWD }) }),
|
|
3684
3520
|
nativeStream: [
|
|
@@ -3749,11 +3585,10 @@ var init_conformance2 = __esm({
|
|
|
3749
3585
|
]
|
|
3750
3586
|
},
|
|
3751
3587
|
{
|
|
3752
|
-
//
|
|
3753
|
-
//
|
|
3754
|
-
//
|
|
3755
|
-
//
|
|
3756
|
-
// 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.
|
|
3757
3592
|
name: "subscription cap \u2192 usage_capped",
|
|
3758
3593
|
request: makeRequest(),
|
|
3759
3594
|
nativeStream: [
|
|
@@ -3772,9 +3607,8 @@ var init_conformance2 = __esm({
|
|
|
3772
3607
|
]
|
|
3773
3608
|
},
|
|
3774
3609
|
{
|
|
3775
|
-
//
|
|
3776
|
-
//
|
|
3777
|
-
// 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.
|
|
3778
3612
|
name: "partial narration before subscription cap stays progress",
|
|
3779
3613
|
request: makeRequest(),
|
|
3780
3614
|
nativeStream: [
|
|
@@ -3790,8 +3624,8 @@ var init_conformance2 = __esm({
|
|
|
3790
3624
|
]
|
|
3791
3625
|
},
|
|
3792
3626
|
{
|
|
3793
|
-
//
|
|
3794
|
-
//
|
|
3627
|
+
// Recognized runtime prose stays authored when its kind disagrees with the structured terminal
|
|
3628
|
+
// failure.
|
|
3795
3629
|
name: "cap notice before server failure stays progress",
|
|
3796
3630
|
request: makeRequest(),
|
|
3797
3631
|
nativeStream: [
|
|
@@ -3813,10 +3647,9 @@ var init_conformance2 = __esm({
|
|
|
3813
3647
|
]
|
|
3814
3648
|
},
|
|
3815
3649
|
{
|
|
3816
|
-
//
|
|
3817
|
-
//
|
|
3818
|
-
//
|
|
3819
|
-
// 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.
|
|
3820
3653
|
name: "incidental 5xx-shaped narration before server failure stays progress",
|
|
3821
3654
|
request: makeRequest(),
|
|
3822
3655
|
nativeStream: [
|
|
@@ -3838,9 +3671,8 @@ var init_conformance2 = __esm({
|
|
|
3838
3671
|
]
|
|
3839
3672
|
},
|
|
3840
3673
|
{
|
|
3841
|
-
//
|
|
3842
|
-
//
|
|
3843
|
-
// 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`.
|
|
3844
3676
|
name: "blocking_limit terminal reason \u2192 usage_capped",
|
|
3845
3677
|
request: makeRequest(),
|
|
3846
3678
|
nativeStream: [
|
|
@@ -3854,8 +3686,8 @@ var init_conformance2 = __esm({
|
|
|
3854
3686
|
expected: [sessionEvent("s1"), { type: "result", ok: false, reason: "usage_capped" }]
|
|
3855
3687
|
},
|
|
3856
3688
|
{
|
|
3857
|
-
//
|
|
3858
|
-
//
|
|
3689
|
+
// An expired login surfaces as an `auth_status` error frame; the failing result classifies as
|
|
3690
|
+
// `auth_expired`.
|
|
3859
3691
|
name: "expired login \u2192 auth_expired",
|
|
3860
3692
|
request: makeRequest(),
|
|
3861
3693
|
nativeStream: [
|
|
@@ -3869,10 +3701,9 @@ var init_conformance2 = __esm({
|
|
|
3869
3701
|
expected: [sessionEvent("s1"), { type: "result", ok: false, reason: "auth_expired" }]
|
|
3870
3702
|
},
|
|
3871
3703
|
{
|
|
3872
|
-
//
|
|
3873
|
-
//
|
|
3874
|
-
//
|
|
3875
|
-
// 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`.
|
|
3876
3707
|
name: "provider overload text \u2192 server_error",
|
|
3877
3708
|
request: makeRequest(),
|
|
3878
3709
|
nativeStream: [
|
|
@@ -3885,11 +3716,9 @@ var init_conformance2 = __esm({
|
|
|
3885
3716
|
expected: [sessionEvent("s1"), { type: "result", ok: false, reason: "server_error" }]
|
|
3886
3717
|
},
|
|
3887
3718
|
{
|
|
3888
|
-
//
|
|
3889
|
-
//
|
|
3890
|
-
//
|
|
3891
|
-
// The configured request model is irrelevant here; what's asserted is that the
|
|
3892
|
-
// 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.
|
|
3893
3722
|
name: "resolved model captured from system/init",
|
|
3894
3723
|
request: makeRequest({ session: encodeSession({ sdkSessionId: "s1", cwd: CWD }) }),
|
|
3895
3724
|
nativeStream: [init("s1", "claude-opus-4-8"), assistantText("Done."), resultSuccess("s1")],
|
|
@@ -3899,9 +3728,8 @@ var init_conformance2 = __esm({
|
|
|
3899
3728
|
]
|
|
3900
3729
|
},
|
|
3901
3730
|
{
|
|
3902
|
-
// Session resume echo: the stored state matches the
|
|
3903
|
-
//
|
|
3904
|
-
// (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.
|
|
3905
3733
|
name: "session resume (no re-emit on echo)",
|
|
3906
3734
|
request: makeRequest({ session: encodeSession({ sdkSessionId: "s1", cwd: CWD }) }),
|
|
3907
3735
|
nativeStream: [init("s1"), assistantText("Back again."), resultSuccess("s1")],
|
|
@@ -3911,16 +3739,14 @@ var init_conformance2 = __esm({
|
|
|
3911
3739
|
]
|
|
3912
3740
|
},
|
|
3913
3741
|
{
|
|
3914
|
-
//
|
|
3915
|
-
//
|
|
3916
|
-
// 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.
|
|
3917
3744
|
name: "session cwd-mismatch degrades to fresh",
|
|
3918
3745
|
request: makeRequest({ session: encodeSession({ sdkSessionId: "old", cwd: "/env/gone" }) }),
|
|
3919
3746
|
nativeStream: [init("fresh1"), assistantText("Fresh start."), resultSuccess("fresh1")],
|
|
3920
3747
|
expected: [
|
|
3921
|
-
//
|
|
3922
|
-
//
|
|
3923
|
-
// 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).
|
|
3924
3750
|
sessionEvent("fresh1", CWD, true),
|
|
3925
3751
|
{ type: "text", body: "Fresh start.", terminal: true },
|
|
3926
3752
|
{ type: "result", ok: true }
|
|
@@ -4623,8 +4449,6 @@ function createOpencodeAdapter(deps = {}) {
|
|
|
4623
4449
|
const transport = deps.transport ?? (deps.serverUrl ? createHttpOpencodeTransport({ baseUrl: deps.serverUrl }) : null);
|
|
4624
4450
|
return {
|
|
4625
4451
|
name: "opencode",
|
|
4626
|
-
// CT829: one surface, so one addendum and no parameter — CT614's `codeMode`
|
|
4627
|
-
// argument went with the named tool surface it selected against.
|
|
4628
4452
|
promptAddendum: () => OPENCODE_ADDENDUM,
|
|
4629
4453
|
dialectSchema: opencodeDialectSchema,
|
|
4630
4454
|
async *runTurn(request, signal, hooks) {
|
|
@@ -4813,15 +4637,12 @@ var init_conformance3 = __esm({
|
|
|
4813
4637
|
]
|
|
4814
4638
|
},
|
|
4815
4639
|
{
|
|
4816
|
-
//
|
|
4817
|
-
//
|
|
4818
|
-
//
|
|
4819
|
-
// (
|
|
4820
|
-
//
|
|
4821
|
-
//
|
|
4822
|
-
// upserts) must carry the LATEST non-empty input + its summary, not the
|
|
4823
|
-
// first-seen `{}`. This is the case the pre-CT301 suite structurally couldn't
|
|
4824
|
-
// 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.
|
|
4825
4646
|
name: "streaming tool input \u2014 late args refresh the done event (CT301)",
|
|
4826
4647
|
request: makeRequest2(),
|
|
4827
4648
|
nativeStream: [
|
|
@@ -4859,9 +4680,8 @@ var init_conformance3 = __esm({
|
|
|
4859
4680
|
]
|
|
4860
4681
|
},
|
|
4861
4682
|
{
|
|
4862
|
-
//
|
|
4863
|
-
//
|
|
4864
|
-
// 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).
|
|
4865
4685
|
name: "cancel mid-stream",
|
|
4866
4686
|
request: makeRequest2(),
|
|
4867
4687
|
nativeStream: [
|
|
@@ -4901,10 +4721,9 @@ var init_conformance3 = __esm({
|
|
|
4901
4721
|
]
|
|
4902
4722
|
},
|
|
4903
4723
|
{
|
|
4904
|
-
//
|
|
4905
|
-
//
|
|
4906
|
-
//
|
|
4907
|
-
// 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.
|
|
4908
4727
|
name: "provider 429 \u2192 rate_limited (throttle, no reset)",
|
|
4909
4728
|
request: makeRequest2(),
|
|
4910
4729
|
nativeStream: [
|
|
@@ -4920,9 +4739,8 @@ var init_conformance3 = __esm({
|
|
|
4920
4739
|
]
|
|
4921
4740
|
},
|
|
4922
4741
|
{
|
|
4923
|
-
//
|
|
4924
|
-
//
|
|
4925
|
-
// 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").
|
|
4926
4744
|
name: "usage cap text \u2192 usage_capped (no reset)",
|
|
4927
4745
|
request: makeRequest2(),
|
|
4928
4746
|
nativeStream: [
|
|
@@ -4938,9 +4756,7 @@ var init_conformance3 = __esm({
|
|
|
4938
4756
|
]
|
|
4939
4757
|
},
|
|
4940
4758
|
{
|
|
4941
|
-
//
|
|
4942
|
-
// honest `server_error`, never the cap. This is the outage class that used to
|
|
4943
|
-
// fall through to an opaque `unknown`.
|
|
4759
|
+
// A transient provider outage (5xx / overload) surfaced as text → `server_error`, never the cap.
|
|
4944
4760
|
name: "provider overload text \u2192 server_error",
|
|
4945
4761
|
request: makeRequest2(),
|
|
4946
4762
|
nativeStream: [
|
|
@@ -4958,7 +4774,7 @@ var init_conformance3 = __esm({
|
|
|
4958
4774
|
]
|
|
4959
4775
|
},
|
|
4960
4776
|
{
|
|
4961
|
-
//
|
|
4777
|
+
// An expired or invalid credential classifies as `auth_expired`.
|
|
4962
4778
|
name: "auth failure \u2192 auth_expired",
|
|
4963
4779
|
request: makeRequest2(),
|
|
4964
4780
|
nativeStream: [
|
|
@@ -4974,8 +4790,8 @@ var init_conformance3 = __esm({
|
|
|
4974
4790
|
]
|
|
4975
4791
|
},
|
|
4976
4792
|
{
|
|
4977
|
-
//
|
|
4978
|
-
//
|
|
4793
|
+
// An unclassified failure stays informative: its provider text is visible, and the raw machine
|
|
4794
|
+
// reason is unchanged.
|
|
4979
4795
|
name: "unclassified failure keeps a runtime notice and raw reason",
|
|
4980
4796
|
request: makeRequest2(),
|
|
4981
4797
|
nativeStream: [errored("something local broke")],
|
|
@@ -4996,8 +4812,8 @@ var init_conformance3 = __esm({
|
|
|
4996
4812
|
]
|
|
4997
4813
|
},
|
|
4998
4814
|
{
|
|
4999
|
-
//
|
|
5000
|
-
//
|
|
4815
|
+
// Empty, whitespace-only and bare status-token failures still set the machine reason but add no
|
|
4816
|
+
// empty or unhelpful transcript notice.
|
|
5001
4817
|
name: "empty failure text emits no runtime notice",
|
|
5002
4818
|
request: makeRequest2(),
|
|
5003
4819
|
nativeStream: [errored("")],
|
|
@@ -5029,12 +4845,10 @@ var init_conformance3 = __esm({
|
|
|
5029
4845
|
]
|
|
5030
4846
|
},
|
|
5031
4847
|
{
|
|
5032
|
-
//
|
|
5033
|
-
//
|
|
5034
|
-
//
|
|
5035
|
-
//
|
|
5036
|
-
// runtime that CAN produce this envelope must be shown producing it in the
|
|
5037
|
-
// 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.
|
|
5038
4852
|
name: "zero-content success (empty runtime result)",
|
|
5039
4853
|
request: makeRequest2(),
|
|
5040
4854
|
nativeStream: [idle()],
|
|
@@ -5091,10 +4905,9 @@ var init_conformance3 = __esm({
|
|
|
5091
4905
|
]
|
|
5092
4906
|
},
|
|
5093
4907
|
{
|
|
5094
|
-
//
|
|
5095
|
-
//
|
|
5096
|
-
//
|
|
5097
|
-
// 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.
|
|
5098
4911
|
name: "session resume (no re-emit on echo)",
|
|
5099
4912
|
request: makeRequest2({ session: encodeSession2({ sessionId: "s1", directory: DIR }) }),
|
|
5100
4913
|
nativeStream: [textPart("p1", "Back again."), idle()],
|
|
@@ -5104,12 +4917,10 @@ var init_conformance3 = __esm({
|
|
|
5104
4917
|
]
|
|
5105
4918
|
},
|
|
5106
4919
|
{
|
|
5107
|
-
//
|
|
5108
|
-
//
|
|
5109
|
-
//
|
|
5110
|
-
//
|
|
5111
|
-
// injected per-turn context still can't leak into Steps. Only the assistant
|
|
5112
|
-
// 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.
|
|
5113
4924
|
name: "session resume \u2014 prompt echo filtered via stream-derived user id",
|
|
5114
4925
|
request: makeRequest2({ session: encodeSession2({ sessionId: "s1", directory: DIR }) }),
|
|
5115
4926
|
nativeStream: [
|
|
@@ -5125,17 +4936,15 @@ var init_conformance3 = __esm({
|
|
|
5125
4936
|
]
|
|
5126
4937
|
},
|
|
5127
4938
|
{
|
|
5128
|
-
// Directory-mismatch degrade
|
|
5129
|
-
//
|
|
5130
|
-
//
|
|
5131
|
-
// 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.
|
|
5132
4942
|
name: "session directory-mismatch degrades to fresh",
|
|
5133
4943
|
request: makeRequest2({ session: encodeSession2({ sessionId: "old", directory: "/env/gone" }) }),
|
|
5134
4944
|
nativeStream: [textPart("p1", "Fresh start."), idle()],
|
|
5135
4945
|
expected: [
|
|
5136
|
-
//
|
|
5137
|
-
//
|
|
5138
|
-
// 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.
|
|
5139
4948
|
sessionEvent2(NEW_SESSION_ID, DIR, true),
|
|
5140
4949
|
{ type: "text", body: "Fresh start.", terminal: true },
|
|
5141
4950
|
{ type: "result", ok: true }
|
|
@@ -5160,11 +4969,9 @@ var init_addendum3 = __esm({
|
|
|
5160
4969
|
"absent without attempting discovery and invocation, and if a call fails, report the recorded",
|
|
5161
4970
|
"error.",
|
|
5162
4971
|
turnControlPromptGuidance("mcp__cabane_companion__"),
|
|
5163
|
-
//
|
|
5164
|
-
//
|
|
5165
|
-
//
|
|
5166
|
-
// so the sentence contradicted itself. The bare `query` / `mutate` below stay:
|
|
5167
|
-
// 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.
|
|
5168
4975
|
"The ancillary tools are `mcp__cabane__read_binary`, `mcp__cabane__upload`,",
|
|
5169
4976
|
"`mcp__cabane__begin_upload`, `mcp__cabane__finalize_upload` and",
|
|
5170
4977
|
"`mcp__cabane__mint_render_token` \u2014 all possibly deferred too.",
|
|
@@ -5420,10 +5227,9 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
5420
5227
|
threadId,
|
|
5421
5228
|
cwd: ctx.cwd ?? "",
|
|
5422
5229
|
promptFingerprint: ctx.promptFingerprint ?? null,
|
|
5423
|
-
//
|
|
5424
|
-
//
|
|
5425
|
-
//
|
|
5426
|
-
// 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.
|
|
5427
5233
|
usageBaseline: ctx.usageBaseline ?? null
|
|
5428
5234
|
});
|
|
5429
5235
|
if (threadId !== ctx.resumedThreadId || state !== ctx.storedState) {
|
|
@@ -5717,9 +5523,8 @@ function buildConfig(req, baseInstructionsFile) {
|
|
|
5717
5523
|
return {
|
|
5718
5524
|
mcp_servers,
|
|
5719
5525
|
experimental_use_rmcp_client: true,
|
|
5720
|
-
//
|
|
5721
|
-
//
|
|
5722
|
-
// 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.
|
|
5723
5528
|
...baseInstructionsFile ? { model_instructions_file: baseInstructionsFile, include_environment_context: false } : {},
|
|
5724
5529
|
...Object.keys(shellEnv).length ? { shell_environment_policy: { set: shellEnv } } : {}
|
|
5725
5530
|
};
|
|
@@ -5840,16 +5645,11 @@ function createSdkCodexTransport(opts = {}) {
|
|
|
5840
5645
|
const codexOptions = {
|
|
5841
5646
|
...opts.apiKey ? { apiKey: opts.apiKey } : {},
|
|
5842
5647
|
...opts.codexPathOverride ? { codexPathOverride: opts.codexPathOverride } : {},
|
|
5843
|
-
// The `--config` overrides (MCP servers + rmcp flag)
|
|
5844
|
-
//
|
|
5845
|
-
//
|
|
5846
|
-
//
|
|
5847
|
-
//
|
|
5848
|
-
// ignores TOML quoting), so it has to ride inside an inline-table value,
|
|
5849
|
-
// and the SDK's flattener cannot be steered into producing one. This is
|
|
5850
|
-
// also what makes the PUBLISHED companion correct, where the vendored
|
|
5851
|
-
// patch never reached — and why that patch is gone. See
|
|
5852
|
-
// `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`.
|
|
5853
5653
|
configOverrides: buildConfigOverrides(spec.config),
|
|
5854
5654
|
...spec.secretEnv ? { env: codexProcessEnv(spec.secretEnv) } : {}
|
|
5855
5655
|
};
|
|
@@ -5873,8 +5673,6 @@ function createCodexAdapter(deps = {}) {
|
|
|
5873
5673
|
const transport = deps.transport ?? (deps.enabled ? createSdkCodexTransport({ ...deps.apiKey ? { apiKey: deps.apiKey } : {} }) : null);
|
|
5874
5674
|
return {
|
|
5875
5675
|
name: "codex",
|
|
5876
|
-
// CT829: one surface, so one addendum and no parameter — CT614's `codeMode`
|
|
5877
|
-
// argument went with the named tool surface it selected against.
|
|
5878
5676
|
promptAddendum: () => CODEX_ADDENDUM,
|
|
5879
5677
|
dialectSchema: codexDialectSchema,
|
|
5880
5678
|
async *runTurn(request, signal, hooks) {
|
|
@@ -5966,17 +5764,14 @@ function createCodexAdapter(deps = {}) {
|
|
|
5966
5764
|
cwd: req.local.cwd,
|
|
5967
5765
|
resumedThreadId: resumeThreadId,
|
|
5968
5766
|
degraded,
|
|
5969
|
-
//
|
|
5970
|
-
//
|
|
5971
|
-
//
|
|
5972
|
-
// it, the thread still carries whatever it did before. Read off the spec
|
|
5973
|
-
// rather than recomputed, so the recorded fact can't disagree with what
|
|
5974
|
-
// 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.
|
|
5975
5770
|
promptFingerprint: spec.promptRidesInput ? spec.promptFingerprint : threadPromptFingerprint,
|
|
5976
5771
|
storedState: req.session,
|
|
5977
5772
|
usageBaseline: threadUsageBaseline,
|
|
5978
|
-
//
|
|
5979
|
-
//
|
|
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.
|
|
5980
5775
|
resolvedModel: spec.model,
|
|
5981
5776
|
resolvedReasoningEffort: spec.modelReasoningEffort ?? null
|
|
5982
5777
|
});
|
|
@@ -6031,11 +5826,9 @@ function makeRequest3(overrides2 = {}) {
|
|
|
6031
5826
|
systemPrompt: PROMPT,
|
|
6032
5827
|
prompt: "hi there",
|
|
6033
5828
|
content: [{ type: "text", text: "hi there" }],
|
|
6034
|
-
//
|
|
6035
|
-
//
|
|
6036
|
-
//
|
|
6037
|
-
// emitted → their expected results stay unchanged; a dedicated capture fixture
|
|
6038
|
-
// 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.
|
|
6039
5832
|
config: { model: null },
|
|
6040
5833
|
policy: COMPANION_POLICY2,
|
|
6041
5834
|
session: null,
|
|
@@ -6107,13 +5900,12 @@ var init_conformance4 = __esm({
|
|
|
6107
5900
|
});
|
|
6108
5901
|
CODEX_CONFORMANCE_FIXTURES = [
|
|
6109
5902
|
{
|
|
6110
|
-
// A plain text reply: the held agent message seals as the terminal final at
|
|
6111
|
-
//
|
|
5903
|
+
// A plain text reply: the held agent message seals as the terminal final at `turn.completed`;
|
|
5904
|
+
// usage rides the `result`.
|
|
6112
5905
|
//
|
|
6113
|
-
//
|
|
6114
|
-
// `
|
|
6115
|
-
//
|
|
6116
|
-
// 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.
|
|
6117
5909
|
name: "clean turn",
|
|
6118
5910
|
request: makeRequest3(),
|
|
6119
5911
|
nativeStream: [
|
|
@@ -6130,10 +5922,8 @@ var init_conformance4 = __esm({
|
|
|
6130
5922
|
expected: [
|
|
6131
5923
|
sessionEvent3(NEW_THREAD_ID),
|
|
6132
5924
|
{ type: "text", body: "Hello there.", terminal: true },
|
|
6133
|
-
//
|
|
6134
|
-
//
|
|
6135
|
-
// `result` usage below is the frame verbatim (nothing to subtract yet);
|
|
6136
|
-
// 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.
|
|
6137
5927
|
sessionEvent3(NEW_THREAD_ID, DIR2, false, fingerprintPrompt(PROMPT), {
|
|
6138
5928
|
inputTokens: 100,
|
|
6139
5929
|
outputTokens: 50,
|
|
@@ -6205,8 +5995,8 @@ var init_conformance4 = __esm({
|
|
|
6205
5995
|
]
|
|
6206
5996
|
},
|
|
6207
5997
|
{
|
|
6208
|
-
//
|
|
6209
|
-
//
|
|
5998
|
+
// The recorded MCP success shape: Codex exposes the text copy and the native object together;
|
|
5999
|
+
// normalization must keep the object.
|
|
6210
6000
|
name: "MCP success prefers native structured content",
|
|
6211
6001
|
request: makeRequest3(),
|
|
6212
6002
|
nativeStream: [
|
|
@@ -6256,8 +6046,8 @@ var init_conformance4 = __esm({
|
|
|
6256
6046
|
]
|
|
6257
6047
|
},
|
|
6258
6048
|
{
|
|
6259
|
-
//
|
|
6260
|
-
//
|
|
6049
|
+
// MCP `isError` keeps the failed phase, while the raw camel-case structured payload survives
|
|
6050
|
+
// instead of the generic `error` token.
|
|
6261
6051
|
name: "failed MCP call preserves structured receipt while remaining error phase",
|
|
6262
6052
|
request: makeRequest3(),
|
|
6263
6053
|
nativeStream: [
|
|
@@ -6309,8 +6099,8 @@ var init_conformance4 = __esm({
|
|
|
6309
6099
|
]
|
|
6310
6100
|
},
|
|
6311
6101
|
{
|
|
6312
|
-
// A transport/tool failure
|
|
6313
|
-
//
|
|
6102
|
+
// A transport/tool failure with no MCP result payload still uses the real adapter error message,
|
|
6103
|
+
// never `undefined`.
|
|
6314
6104
|
name: "failed MCP call without result keeps error message fallback",
|
|
6315
6105
|
request: makeRequest3(),
|
|
6316
6106
|
nativeStream: [
|
|
@@ -6350,8 +6140,8 @@ var init_conformance4 = __esm({
|
|
|
6350
6140
|
]
|
|
6351
6141
|
},
|
|
6352
6142
|
{
|
|
6353
|
-
//
|
|
6354
|
-
//
|
|
6143
|
+
// A connector's structured metadata beside an image-only MCP response: no image/base64 payload
|
|
6144
|
+
// enters activity receipts.
|
|
6355
6145
|
name: "image-only MCP result remains empty despite structured metadata",
|
|
6356
6146
|
request: makeRequest3(),
|
|
6357
6147
|
nativeStream: [
|
|
@@ -6444,12 +6234,10 @@ var init_conformance4 = __esm({
|
|
|
6444
6234
|
]
|
|
6445
6235
|
},
|
|
6446
6236
|
{
|
|
6447
|
-
//
|
|
6448
|
-
//
|
|
6449
|
-
//
|
|
6450
|
-
//
|
|
6451
|
-
// and carry the populated query the completed frame filled in. Before CT715 the
|
|
6452
|
-
// 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".
|
|
6453
6241
|
name: "web_search lifecycle \u2014 completes off frame type, populated query on done",
|
|
6454
6242
|
request: makeRequest3(),
|
|
6455
6243
|
nativeStream: [
|
|
@@ -6480,9 +6268,8 @@ var init_conformance4 = __esm({
|
|
|
6480
6268
|
]
|
|
6481
6269
|
},
|
|
6482
6270
|
{
|
|
6483
|
-
//
|
|
6484
|
-
//
|
|
6485
|
-
// 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`.
|
|
6486
6273
|
name: "web_search lifecycle \u2014 empty-query completion still resolves to done",
|
|
6487
6274
|
request: makeRequest3(),
|
|
6488
6275
|
nativeStream: [
|
|
@@ -6513,9 +6300,8 @@ var init_conformance4 = __esm({
|
|
|
6513
6300
|
]
|
|
6514
6301
|
},
|
|
6515
6302
|
{
|
|
6516
|
-
//
|
|
6517
|
-
//
|
|
6518
|
-
// 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).
|
|
6519
6305
|
name: "cancel mid-stream",
|
|
6520
6306
|
request: makeRequest3(),
|
|
6521
6307
|
nativeStream: [
|
|
@@ -6596,11 +6382,8 @@ var init_conformance4 = __esm({
|
|
|
6596
6382
|
]
|
|
6597
6383
|
},
|
|
6598
6384
|
{
|
|
6599
|
-
//
|
|
6600
|
-
//
|
|
6601
|
-
// classifier (Codex is text-only), so a provider 429 / outage / auth / cap
|
|
6602
|
-
// failure splits into the honest taxonomy instead of all reading `unknown`.
|
|
6603
|
-
// 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:
|
|
6604
6387
|
name: "turn.failed 429 \u2192 rate_limited (throttle)",
|
|
6605
6388
|
request: makeRequest3(),
|
|
6606
6389
|
nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("429 Too Many Requests")],
|
|
@@ -6647,8 +6430,8 @@ var init_conformance4 = __esm({
|
|
|
6647
6430
|
]
|
|
6648
6431
|
},
|
|
6649
6432
|
{
|
|
6650
|
-
//
|
|
6651
|
-
//
|
|
6433
|
+
// A failure text the classifier can't place keeps the raw `error:<text>` reason (the `unknown`
|
|
6434
|
+
// fallback).
|
|
6652
6435
|
name: "turn.failed unclassified \u2192 raw error reason (unchanged)",
|
|
6653
6436
|
request: makeRequest3(),
|
|
6654
6437
|
nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("something local broke")],
|
|
@@ -6672,8 +6455,8 @@ var init_conformance4 = __esm({
|
|
|
6672
6455
|
]
|
|
6673
6456
|
},
|
|
6674
6457
|
{
|
|
6675
|
-
//
|
|
6676
|
-
//
|
|
6458
|
+
// Empty, whitespace-only and bare status-token failures still set the machine reason but add no
|
|
6459
|
+
// empty or unhelpful transcript notice.
|
|
6677
6460
|
name: "empty failure text emits no runtime notice",
|
|
6678
6461
|
request: makeRequest3(),
|
|
6679
6462
|
nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("")],
|
|
@@ -6702,11 +6485,9 @@ var init_conformance4 = __esm({
|
|
|
6702
6485
|
]
|
|
6703
6486
|
},
|
|
6704
6487
|
{
|
|
6705
|
-
//
|
|
6706
|
-
//
|
|
6707
|
-
//
|
|
6708
|
-
// adapter ends the turn the moment the signal lands, with a machine reason — so
|
|
6709
|
-
// 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.
|
|
6710
6491
|
name: "unknown model metadata \u2192 fail loud (guard)",
|
|
6711
6492
|
request: makeRequest3(),
|
|
6712
6493
|
nativeStream: [
|
|
@@ -6752,12 +6533,10 @@ var init_conformance4 = __esm({
|
|
|
6752
6533
|
]
|
|
6753
6534
|
},
|
|
6754
6535
|
{
|
|
6755
|
-
//
|
|
6756
|
-
//
|
|
6757
|
-
//
|
|
6758
|
-
//
|
|
6759
|
-
// neutral, so a runtime that CAN produce this envelope must be shown
|
|
6760
|
-
// 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.
|
|
6761
6540
|
name: "zero-content success (empty runtime result)",
|
|
6762
6541
|
request: makeRequest3(),
|
|
6763
6542
|
nativeStream: [threadStarted(NEW_THREAD_ID), turnCompleted()],
|
|
@@ -6826,11 +6605,10 @@ var init_conformance4 = __esm({
|
|
|
6826
6605
|
]
|
|
6827
6606
|
},
|
|
6828
6607
|
{
|
|
6829
|
-
//
|
|
6830
|
-
//
|
|
6831
|
-
//
|
|
6832
|
-
//
|
|
6833
|
-
// `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`.
|
|
6834
6612
|
name: "resolved model + reasoning effort captured from run spec",
|
|
6835
6613
|
request: makeRequest3({
|
|
6836
6614
|
config: {
|
|
@@ -6851,15 +6629,12 @@ var init_conformance4 = __esm({
|
|
|
6851
6629
|
]
|
|
6852
6630
|
},
|
|
6853
6631
|
{
|
|
6854
|
-
// Session resume: the stored state matches the
|
|
6855
|
-
//
|
|
6856
|
-
// event fires (the id didn't change AND the state is unchanged). This is what
|
|
6857
|
-
// 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.
|
|
6858
6634
|
//
|
|
6859
|
-
//
|
|
6860
|
-
//
|
|
6861
|
-
//
|
|
6862
|
-
// 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.
|
|
6863
6638
|
name: "session resume (no re-emit on echo)",
|
|
6864
6639
|
request: makeRequest3({
|
|
6865
6640
|
session: encodeSession3({
|
|
@@ -6876,16 +6651,12 @@ var init_conformance4 = __esm({
|
|
|
6876
6651
|
]
|
|
6877
6652
|
},
|
|
6878
6653
|
{
|
|
6879
|
-
//
|
|
6880
|
-
//
|
|
6881
|
-
//
|
|
6882
|
-
//
|
|
6883
|
-
// the fingerprint recorded — on the SAME thread id, which pre-CT1075 emitted
|
|
6884
|
-
// nothing at all. That re-emit is the upgrade path: without it a long-lived
|
|
6885
|
-
// 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.
|
|
6886
6658
|
//
|
|
6887
|
-
// This
|
|
6888
|
-
// have no evidence about.
|
|
6659
|
+
// This fixture fails if the standing half is withheld on a thread we have no evidence about.
|
|
6889
6660
|
name: "session resume with a pre-CT1075 state re-emits with the prompt fingerprint",
|
|
6890
6661
|
request: makeRequest3({
|
|
6891
6662
|
session: JSON.stringify({ threadId: "th1", cwd: DIR2 })
|
|
@@ -6898,14 +6669,12 @@ var init_conformance4 = __esm({
|
|
|
6898
6669
|
]
|
|
6899
6670
|
},
|
|
6900
6671
|
{
|
|
6901
|
-
//
|
|
6902
|
-
//
|
|
6903
|
-
//
|
|
6904
|
-
// 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.
|
|
6905
6675
|
//
|
|
6906
|
-
// This
|
|
6907
|
-
//
|
|
6908
|
-
// 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.
|
|
6909
6678
|
name: "session resume with a stale prompt fingerprint re-sends the prompt",
|
|
6910
6679
|
request: makeRequest3({
|
|
6911
6680
|
session: encodeSession3({
|
|
@@ -6923,15 +6692,12 @@ var init_conformance4 = __esm({
|
|
|
6923
6692
|
]
|
|
6924
6693
|
},
|
|
6925
6694
|
{
|
|
6926
|
-
// Directory-mismatch degrade
|
|
6927
|
-
//
|
|
6928
|
-
//
|
|
6929
|
-
// records the CURRENT directory with `degraded: true` so the host rewinds the
|
|
6930
|
-
// 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.
|
|
6931
6698
|
//
|
|
6932
|
-
//
|
|
6933
|
-
//
|
|
6934
|
-
// 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.
|
|
6935
6701
|
name: "session directory-mismatch degrades to fresh",
|
|
6936
6702
|
request: makeRequest3({
|
|
6937
6703
|
session: encodeSession3({
|
|
@@ -7320,8 +7086,7 @@ function createTurnContainment(turnId, deps = {}) {
|
|
|
7320
7086
|
`--unit=${unit}`,
|
|
7321
7087
|
`--property=TimeoutStopSec=${CONTAINMENT_STOP_GRACE_SEC}s`,
|
|
7322
7088
|
// Bind the scope's life to the companion's own unit, so stopping or
|
|
7323
|
-
// restarting the companion still takes its in-flight turns with it
|
|
7324
|
-
// which is what happens today, and what the scope would otherwise undo.
|
|
7089
|
+
// restarting the companion still takes its in-flight turns with it.
|
|
7325
7090
|
// Omitted outside a unit, where there is nothing to bind to.
|
|
7326
7091
|
...ownUnit ? [`--property=PartOf=${ownUnit}`] : [],
|
|
7327
7092
|
"--"
|
|
@@ -7672,7 +7437,7 @@ var init_run_config = __esm({
|
|
|
7672
7437
|
type: z15.literal("http"),
|
|
7673
7438
|
url: z15.string().url(),
|
|
7674
7439
|
headers: z15.record(z15.string(), z15.string()).optional(),
|
|
7675
|
-
//
|
|
7440
|
+
// Header → the name of a `secretEnv` entry holding its value. Set by
|
|
7676
7441
|
// the server on a room connection's entry, and by `resolveMcpSecrets` on a
|
|
7677
7442
|
// header whose `${VAR}` it resolved.
|
|
7678
7443
|
headersFromEnv: z15.record(z15.string(), z15.string()).optional()
|
|
@@ -7681,7 +7446,7 @@ var init_run_config = __esm({
|
|
|
7681
7446
|
type: z15.literal("sse"),
|
|
7682
7447
|
url: z15.string().url(),
|
|
7683
7448
|
headers: z15.record(z15.string(), z15.string()).optional(),
|
|
7684
|
-
//
|
|
7449
|
+
// Header → the name of a `secretEnv` entry holding its value. Set by
|
|
7685
7450
|
// the server on a room connection's entry, and by `resolveMcpSecrets` on a
|
|
7686
7451
|
// header whose `${VAR}` it resolved.
|
|
7687
7452
|
headersFromEnv: z15.record(z15.string(), z15.string()).optional()
|
|
@@ -7694,9 +7459,8 @@ var init_run_config = __esm({
|
|
|
7694
7459
|
]);
|
|
7695
7460
|
effortSchema = z15.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
7696
7461
|
runConfigSchema = z15.object({
|
|
7697
|
-
//
|
|
7698
|
-
//
|
|
7699
|
-
// 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
|
|
7700
7464
|
// `agentRunConfigSchema` (independent zod, forward-compatible: unknown keys are
|
|
7701
7465
|
// stripped, so an older companion riding a newer server never rejects the config).
|
|
7702
7466
|
hostAccess: z15.boolean().optional(),
|
|
@@ -7787,7 +7551,7 @@ var init_protocol = __esm({
|
|
|
7787
7551
|
dispatch: "CABANE_DISPATCH",
|
|
7788
7552
|
// The turn id the launcher resolved, and whether this is a replay of a turn
|
|
7789
7553
|
// an earlier process started. The seq floor for a replay is derived inside
|
|
7790
|
-
// the turn from the outbox and the server
|
|
7554
|
+
// the turn from the outbox and the server.
|
|
7791
7555
|
turnId: "CABANE_TURN_ID",
|
|
7792
7556
|
resumed: "CABANE_RESUMED",
|
|
7793
7557
|
// Everything else the turn is constructed from, as JSON: see `RunnerSpec`.
|
|
@@ -7823,10 +7587,10 @@ var init_protocol = __esm({
|
|
|
7823
7587
|
// Whether the harness starts under the OS sandbox. The launcher sends 'os'
|
|
7824
7588
|
// only when this host can make one.
|
|
7825
7589
|
sandbox: z16.enum(["os", "none"]),
|
|
7826
|
-
//
|
|
7590
|
+
// The device's sandbox policy, validated by the launcher. Absent
|
|
7827
7591
|
// when the sandbox is off.
|
|
7828
7592
|
sandboxPolicy: sandboxPolicySchema.optional(),
|
|
7829
|
-
//
|
|
7593
|
+
// Every companion store the launcher's latest walk found, denied
|
|
7830
7594
|
// to the harness. Absent when the sandbox is off.
|
|
7831
7595
|
companionStores: z16.array(z16.string()).optional(),
|
|
7832
7596
|
logLevel: z16.string(),
|
|
@@ -7835,15 +7599,15 @@ var init_protocol = __esm({
|
|
|
7835
7599
|
// True when this (conversation, agent) pair was already told its session
|
|
7836
7600
|
// could not be saved: the notice is once per pair, not once per process.
|
|
7837
7601
|
sessionWriteNoticeClaimed: z16.boolean(),
|
|
7838
|
-
//
|
|
7602
|
+
// The runner's identity, minted with its credential. The start
|
|
7839
7603
|
// PATCH carries it so the lease knows which runner holds the span.
|
|
7840
7604
|
runnerId: z16.string(),
|
|
7841
|
-
//
|
|
7605
|
+
// The launcher keeps the runtime's session files beside the attempt
|
|
7842
7606
|
// and restores them into this runner's home, so the runner names the attempt
|
|
7843
7607
|
// each context was composed for and waits for the launcher to say it's in
|
|
7844
7608
|
// place. Absent: nothing to wait for (a runner on the launcher's own disk).
|
|
7845
7609
|
attemptState: z16.boolean().optional(),
|
|
7846
|
-
//
|
|
7610
|
+
// MCP `${NAME}` secrets come from the launcher, by name, instead of
|
|
7847
7611
|
// this machine's secrets.json. Absent: the runner reads the device's file.
|
|
7848
7612
|
secretsFromLauncher: z16.boolean().optional()
|
|
7849
7613
|
});
|
|
@@ -8432,7 +8196,7 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
8432
8196
|
return performAct(acts, {
|
|
8433
8197
|
kind: "ask",
|
|
8434
8198
|
callId: randomUUID2(),
|
|
8435
|
-
//
|
|
8199
|
+
// The swap rides the ask, so the server commits both halves
|
|
8436
8200
|
// in one transaction — the person never sees neither or both.
|
|
8437
8201
|
...args.replaces ? { replaces: args.replaces } : {},
|
|
8438
8202
|
ask: hasArray ? {
|
|
@@ -8483,7 +8247,7 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
8483
8247
|
},
|
|
8484
8248
|
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
8485
8249
|
),
|
|
8486
|
-
//
|
|
8250
|
+
// The standalone withdrawal. Not annotated destructive — an ask
|
|
8487
8251
|
// is a question, not data, and a harness that gated this on a live
|
|
8488
8252
|
// operator would hang a turn nobody is watching (the `cancel_wake`
|
|
8489
8253
|
// reasoning). Not idempotent either: a second call with the same id is
|
|
@@ -8502,7 +8266,7 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
8502
8266
|
}),
|
|
8503
8267
|
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
8504
8268
|
),
|
|
8505
|
-
//
|
|
8269
|
+
// The inverse. No arguments — it can only ever stand down the
|
|
8506
8270
|
// caller's own wake in the conversation it's holding a turn in.
|
|
8507
8271
|
tool(
|
|
8508
8272
|
CANCEL_WAKE_TOOL,
|
|
@@ -8567,7 +8331,7 @@ function buildCompanionTurnRequest(params) {
|
|
|
8567
8331
|
// adapter's `ResolvedMcpServers`.
|
|
8568
8332
|
...Object.keys(params.mcpServers).length > 0 ? { mcpServers: params.mcpServers } : {},
|
|
8569
8333
|
...params.secretEnv && Object.keys(params.secretEnv).length > 0 ? { secretEnv: params.secretEnv } : {},
|
|
8570
|
-
//
|
|
8334
|
+
// The auto-memory escape hatch, when the operator set it.
|
|
8571
8335
|
...params.claudeCode ? { claudeCode: params.claudeCode } : {}
|
|
8572
8336
|
},
|
|
8573
8337
|
// Host-injected turn-control tools, under their own namespace.
|
|
@@ -8890,7 +8654,7 @@ var init_transcript_writer = __esm({
|
|
|
8890
8654
|
}
|
|
8891
8655
|
this.line({ type: "_meta", ...meta });
|
|
8892
8656
|
}
|
|
8893
|
-
// Append
|
|
8657
|
+
// Append one event verbatim.
|
|
8894
8658
|
write(msg) {
|
|
8895
8659
|
this.line(msg);
|
|
8896
8660
|
}
|
|
@@ -8898,7 +8662,7 @@ var init_transcript_writer = __esm({
|
|
|
8898
8662
|
close(outcome) {
|
|
8899
8663
|
this.line({ type: "_outcome", ...outcome });
|
|
8900
8664
|
}
|
|
8901
|
-
//
|
|
8665
|
+
// Keep an independent copy of an empty successful envelope after the
|
|
8902
8666
|
// footer has landed. The ordinary 200-file FIFO keeps rotating as before;
|
|
8903
8667
|
// this bucket retains the newest 50 anomalies regardless of ordinary traffic.
|
|
8904
8668
|
// Best-effort like every other transcript operation: observability can never
|
|
@@ -9034,12 +8798,9 @@ var init_turn_committer = __esm({
|
|
|
9034
8798
|
// Renders each `TurnEvent` onto the pump's callback sink (tool-log truncation),
|
|
9035
8799
|
// routing a throwing commit to `onError`.
|
|
9036
8800
|
emit;
|
|
9037
|
-
//
|
|
9038
|
-
//
|
|
9039
|
-
//
|
|
9040
|
-
// POST) is routed to the error hook and swallowed — a failed commit is
|
|
9041
|
-
// warn-only and must never fail the turn (the role the classification's
|
|
9042
|
-
// `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.
|
|
9043
8804
|
async ingestEvent(event) {
|
|
9044
8805
|
if (event.type === "session" || event.type === "result") return;
|
|
9045
8806
|
try {
|
|
@@ -9266,18 +9027,15 @@ var init_turn_execution = __esm({
|
|
|
9266
9027
|
turnLog;
|
|
9267
9028
|
abortController = new AbortController();
|
|
9268
9029
|
outcome = initialOutcome();
|
|
9269
|
-
//
|
|
9270
|
-
//
|
|
9271
|
-
// startup failure has nothing to reap, and reaping is a no-op rather than a
|
|
9272
|
-
// 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.
|
|
9273
9032
|
containment = null;
|
|
9274
9033
|
seqCounter = 0;
|
|
9275
|
-
//
|
|
9276
|
-
//
|
|
9277
|
-
// 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.
|
|
9278
9036
|
nextSeq = () => ++this.seqCounter;
|
|
9279
|
-
//
|
|
9280
|
-
//
|
|
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.
|
|
9281
9039
|
resumed;
|
|
9282
9040
|
resumedFromSeq;
|
|
9283
9041
|
// Whether the span this resumed run continues already has first words —
|
|
@@ -9285,8 +9043,8 @@ var init_turn_execution = __esm({
|
|
|
9285
9043
|
resumedSpoke = false;
|
|
9286
9044
|
turnContext;
|
|
9287
9045
|
resolvedMcpServers;
|
|
9288
|
-
//
|
|
9289
|
-
//
|
|
9046
|
+
// The harness's secret environment for this turn. Only ever handed to the
|
|
9047
|
+
// adapter; never logged, never merged into `process.env`.
|
|
9290
9048
|
secretEnv = {};
|
|
9291
9049
|
effectiveCwd;
|
|
9292
9050
|
hookEnv;
|
|
@@ -9300,19 +9058,15 @@ var init_turn_execution = __esm({
|
|
|
9300
9058
|
turnControlIntentFetched = false;
|
|
9301
9059
|
applyRecordedTurnControlIntent = async () => {
|
|
9302
9060
|
};
|
|
9303
|
-
//
|
|
9304
|
-
//
|
|
9305
|
-
//
|
|
9306
|
-
// 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.
|
|
9307
9064
|
noteCommitFailed = () => {
|
|
9308
9065
|
};
|
|
9309
9066
|
disarmWatchdogs = () => {
|
|
9310
9067
|
};
|
|
9311
|
-
//
|
|
9312
|
-
//
|
|
9313
|
-
// swallowed, because a turn that produced a good answer must not be recorded as
|
|
9314
|
-
// failed on account of a leftover process, and the failure is visible in the log
|
|
9315
|
-
// 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.
|
|
9316
9070
|
async reapContainment() {
|
|
9317
9071
|
const containment = this.containment;
|
|
9318
9072
|
if (!containment) return;
|
|
@@ -9338,20 +9092,18 @@ var init_turn_execution = __esm({
|
|
|
9338
9092
|
);
|
|
9339
9093
|
}
|
|
9340
9094
|
}
|
|
9341
|
-
//
|
|
9342
|
-
//
|
|
9343
|
-
// 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
|
|
9344
9097
|
// participant flag.
|
|
9345
9098
|
admitted = false;
|
|
9346
|
-
//
|
|
9099
|
+
// One recompose per turn when the lease refuses a stale context.
|
|
9347
9100
|
recomposedForLease = false;
|
|
9348
|
-
//
|
|
9349
|
-
//
|
|
9350
|
-
//
|
|
9351
|
-
//
|
|
9352
|
-
// 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.
|
|
9353
9105
|
executionSourceId;
|
|
9354
|
-
//
|
|
9106
|
+
// One fresh fetch per turn when adoption is refused.
|
|
9355
9107
|
refetchedForAdoption = false;
|
|
9356
9108
|
concluded(reason, errorReason) {
|
|
9357
9109
|
return new TurnConcluded(reason, errorReason);
|
|
@@ -9419,21 +9171,18 @@ var init_turn_execution = __esm({
|
|
|
9419
9171
|
this.supervisor.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
9420
9172
|
return { ok: false, durationMs, reason, runtime: this.turnContext?.runtime };
|
|
9421
9173
|
}
|
|
9422
|
-
// The
|
|
9423
|
-
//
|
|
9424
|
-
//
|
|
9425
|
-
//
|
|
9426
|
-
//
|
|
9427
|
-
// rule: a failure names its reason; without one the turn settled — the
|
|
9428
|
-
// runtime-unavailable exit posts its user-facing `final` before concluding,
|
|
9429
|
-
// 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`).
|
|
9430
9179
|
async concludeAdmittedRun(reason, errorReason) {
|
|
9431
9180
|
const { payload, turnLog, startedAt, turnId } = this;
|
|
9432
9181
|
const body = {
|
|
9433
9182
|
activeRunStartedAt: null,
|
|
9434
9183
|
turnId,
|
|
9435
|
-
//
|
|
9436
|
-
//
|
|
9184
|
+
// `runtimeIncomplete` is a real failure that deliberately carries no
|
|
9185
|
+
// `errorReason`, so the inference alone would read it as settled.
|
|
9437
9186
|
outcome: errorReason || this.outcome.runtimeIncomplete ? "failed" : "settled"
|
|
9438
9187
|
};
|
|
9439
9188
|
if (errorReason) body.errorReason = errorReason.slice(0, 200);
|
|
@@ -9527,11 +9276,10 @@ var init_turn_execution = __esm({
|
|
|
9527
9276
|
throw this.concluded(reason, reason);
|
|
9528
9277
|
}
|
|
9529
9278
|
}
|
|
9530
|
-
//
|
|
9531
|
-
//
|
|
9532
|
-
//
|
|
9533
|
-
//
|
|
9534
|
-
// 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.
|
|
9535
9283
|
async adoptSource(context) {
|
|
9536
9284
|
const { payload, turnId, turnLog } = this;
|
|
9537
9285
|
if (!context.issuanceId) {
|
|
@@ -9580,9 +9328,8 @@ var init_turn_execution = __esm({
|
|
|
9580
9328
|
secretEnv,
|
|
9581
9329
|
missing
|
|
9582
9330
|
} = resolveMcpSecrets(
|
|
9583
|
-
//
|
|
9584
|
-
//
|
|
9585
|
-
// 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`.
|
|
9586
9333
|
this.turnContext.mcpServers ?? {},
|
|
9587
9334
|
secretStore,
|
|
9588
9335
|
this.turnContext.secretEnv
|
|
@@ -9695,15 +9442,12 @@ var init_turn_execution = __esm({
|
|
|
9695
9442
|
agentUsername: this.opts.agentUsername,
|
|
9696
9443
|
runtime: this.turnContext.runtime,
|
|
9697
9444
|
hostAccess: this.turnContext.policy.hostFs,
|
|
9698
|
-
//
|
|
9699
|
-
//
|
|
9700
|
-
// 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.
|
|
9701
9447
|
triggerEntryPaths: this.turnContext.conversation.triggerEntryPaths ?? [],
|
|
9702
9448
|
title: this.turnContext.conversation.title,
|
|
9703
|
-
//
|
|
9704
|
-
//
|
|
9705
|
-
// context (for the live feed); this is the first thing to read it as
|
|
9706
|
-
// 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.
|
|
9707
9451
|
messageBody: this.turnContext.message.body
|
|
9708
9452
|
},
|
|
9709
9453
|
{ turnId, deviceId: this.opts.deviceId },
|
|
@@ -9749,37 +9493,30 @@ var init_turn_execution = __esm({
|
|
|
9749
9493
|
payload.agentId,
|
|
9750
9494
|
{
|
|
9751
9495
|
activeRunStartedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9752
|
-
//
|
|
9753
|
-
//
|
|
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.
|
|
9754
9498
|
bootId: this.opts.inRunner?.bootId ?? bootId,
|
|
9755
|
-
// CT1680: the runner holding the span, beside the device's boot id.
|
|
9756
9499
|
...this.opts.inRunner ? { runnerId: this.opts.inRunner.runnerId } : {},
|
|
9757
|
-
//
|
|
9500
|
+
// The durable record that this turn resumed, and from where.
|
|
9758
9501
|
...this.resumedFromSeq !== void 0 ? { resumedFromSeq: this.resumedFromSeq } : {},
|
|
9759
|
-
//
|
|
9760
|
-
//
|
|
9761
|
-
//
|
|
9762
|
-
//
|
|
9763
|
-
// fresh-session recompose (`freshRequest`) deliberately does not touch it:
|
|
9764
|
-
// it adopts only the prompt, never the run-config, so its issuance must
|
|
9765
|
-
// 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.
|
|
9766
9506
|
...this.turnContext.issuanceId ? { issuanceId: this.turnContext.issuanceId } : {},
|
|
9767
|
-
//
|
|
9768
|
-
//
|
|
9769
|
-
// already emitted a "preparing" activity row for this turn above (it
|
|
9770
|
-
// runs before this flip), which would otherwise make the sweep mistake
|
|
9771
|
-
// 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.
|
|
9772
9509
|
turnId,
|
|
9773
9510
|
// The newest message this turn's read covered: the lease records it,
|
|
9774
9511
|
// and settle moves the cursor to it. Omitted when the server sent
|
|
9775
9512
|
// none (an older API), so the cursor stays where it was.
|
|
9776
9513
|
...this.turnContext.readThrough !== void 0 ? { readThrough: this.turnContext.readThrough } : {},
|
|
9777
9514
|
...this.turnContext.readRevision !== void 0 ? { readRevision: this.turnContext.readRevision } : {},
|
|
9778
|
-
//
|
|
9515
|
+
// The attempt this request's session belongs to (null = fresh).
|
|
9779
9516
|
...this.turnContext.attemptId !== void 0 ? { attemptId: this.turnContext.attemptId } : {},
|
|
9780
|
-
//
|
|
9781
|
-
//
|
|
9782
|
-
//
|
|
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.
|
|
9783
9520
|
...payload.episode !== void 0 ? { dispatchMessageId: payload.messageId, dispatchEpisode: payload.episode } : {}
|
|
9784
9521
|
}
|
|
9785
9522
|
);
|
|
@@ -9819,21 +9556,18 @@ var init_turn_execution = __esm({
|
|
|
9819
9556
|
this.request = buildCompanionTurnRequest({
|
|
9820
9557
|
turnContext: this.turnContext,
|
|
9821
9558
|
baseUrl: this.opts.baseUrl,
|
|
9822
|
-
//
|
|
9559
|
+
// Always present: `fetchContext` refuses a context without one.
|
|
9823
9560
|
turnToken: this.turnContext.turnToken,
|
|
9824
|
-
// SJ524: the hook-resolved cwd overrides the static local cwd.
|
|
9825
9561
|
...this.effectiveCwd ? { cwd: this.effectiveCwd } : {},
|
|
9826
|
-
// CT1103: the prepare-hook env plus the authoritative host-access token.
|
|
9827
9562
|
env: this.turnEnv,
|
|
9828
9563
|
mcpServers: this.resolvedMcpServers,
|
|
9829
9564
|
secretEnv: this.secretEnv,
|
|
9830
9565
|
turnControlServer,
|
|
9831
|
-
//
|
|
9832
|
-
//
|
|
9566
|
+
// Sent as the active-conversation header so a cross-conversation post
|
|
9567
|
+
// stamps its origin.
|
|
9833
9568
|
activeConversationId: payload.conversationId,
|
|
9834
|
-
// Workspace coordinate retained in the compatible TurnRequest envelope.
|
|
9835
9569
|
workspaceId,
|
|
9836
|
-
//
|
|
9570
|
+
// The operator's machine-local auto-memory escape hatch, when set.
|
|
9837
9571
|
...this.opts.local.claudeCode ? { claudeCode: this.opts.local.claudeCode } : {}
|
|
9838
9572
|
});
|
|
9839
9573
|
}
|
|
@@ -9870,8 +9604,8 @@ var init_turn_execution = __esm({
|
|
|
9870
9604
|
if (this.opts.claudeCodeAvailable?.() ?? true) {
|
|
9871
9605
|
this.containment = createTurnContainment(this.turnId, {
|
|
9872
9606
|
log: (msg, meta) => turnLog.debug(meta ?? {}, msg),
|
|
9873
|
-
//
|
|
9874
|
-
//
|
|
9607
|
+
// A group outside the runner's own is reported, so a launcher can stop
|
|
9608
|
+
// it if the runner dies without reaping it.
|
|
9875
9609
|
...this.opts.inRunner?.onProcessGroup ? { onProcessGroup: this.opts.inRunner.onProcessGroup } : {}
|
|
9876
9610
|
});
|
|
9877
9611
|
const containment = this.containment;
|
|
@@ -9892,8 +9626,8 @@ var init_turn_execution = __esm({
|
|
|
9892
9626
|
const serverUrl = this.opts.opencodeServerUrl;
|
|
9893
9627
|
adapters.push(
|
|
9894
9628
|
createOpencodeAdapter(
|
|
9895
|
-
//
|
|
9896
|
-
//
|
|
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.
|
|
9897
9631
|
this.opts.inRunner ? {
|
|
9898
9632
|
transport: this.opts.inRunner.opencodeTransport(serverUrl),
|
|
9899
9633
|
onWarn
|
|
@@ -9940,7 +9674,7 @@ var init_turn_execution = __esm({
|
|
|
9940
9674
|
kind: "runtime_notice",
|
|
9941
9675
|
turnId,
|
|
9942
9676
|
parentMessageId: payload.messageId,
|
|
9943
|
-
//
|
|
9677
|
+
// Written by this attempt's source, like its speech.
|
|
9944
9678
|
...this.executionSourceId !== void 0 ? { executionSourceId: this.executionSourceId } : {}
|
|
9945
9679
|
});
|
|
9946
9680
|
} catch (postErr) {
|
|
@@ -9954,16 +9688,13 @@ var init_turn_execution = __esm({
|
|
|
9954
9688
|
throw this.concluded(`runtime_unavailable:${err.runtime}`);
|
|
9955
9689
|
}
|
|
9956
9690
|
}
|
|
9957
|
-
// The adapter refused to resume
|
|
9958
|
-
//
|
|
9959
|
-
//
|
|
9960
|
-
//
|
|
9961
|
-
//
|
|
9962
|
-
//
|
|
9963
|
-
//
|
|
9964
|
-
// The lease already granted stands; the read reaches at least its mark. A
|
|
9965
|
-
// fetch that fails returns null and the adapter falls back to the request
|
|
9966
|
-
// 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.
|
|
9967
9698
|
async freshRequest(reason) {
|
|
9968
9699
|
const { payload, turnId, turnLog } = this;
|
|
9969
9700
|
try {
|
|
@@ -9974,9 +9705,8 @@ var init_turn_execution = __esm({
|
|
|
9974
9705
|
false,
|
|
9975
9706
|
true,
|
|
9976
9707
|
void 0,
|
|
9977
|
-
//
|
|
9978
|
-
//
|
|
9979
|
-
// 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.
|
|
9980
9710
|
this.abortController.signal
|
|
9981
9711
|
);
|
|
9982
9712
|
turnLog.debug(
|
|
@@ -10028,19 +9758,17 @@ var init_turn_execution = __esm({
|
|
|
10028
9758
|
conversationId: payload.conversationId,
|
|
10029
9759
|
agentId: payload.agentId,
|
|
10030
9760
|
turnId,
|
|
10031
|
-
//
|
|
10032
|
-
//
|
|
9761
|
+
// The message this turn answers, on every commit so the reply records the
|
|
9762
|
+
// Q→A edge.
|
|
10033
9763
|
parentMessageId: payload.messageId,
|
|
10034
9764
|
signal: abortController.signal,
|
|
10035
9765
|
log: turnLog,
|
|
10036
9766
|
nextSeq: this.nextSeq,
|
|
10037
9767
|
spoke: this.resumedSpoke,
|
|
10038
|
-
//
|
|
9768
|
+
// Admitted by now, so this is the source the model's work runs on.
|
|
10039
9769
|
...this.executionSourceId !== void 0 ? { executionSourceId: this.executionSourceId } : {},
|
|
10040
|
-
//
|
|
10041
|
-
// `noteCommitFailed
|
|
10042
|
-
// arms (it starts as a no-op) — the committer only fires it from inside
|
|
10043
|
-
// 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`.
|
|
10044
9772
|
onCommitFailed: (err) => this.noteCommitFailed(err)
|
|
10045
9773
|
});
|
|
10046
9774
|
const usesHttpTurnControl = turnRuntime === "codex" || turnRuntime === "opencode";
|
|
@@ -10144,9 +9872,9 @@ var init_turn_execution = __esm({
|
|
|
10144
9872
|
workspaceId,
|
|
10145
9873
|
payload.conversationId,
|
|
10146
9874
|
payload.agentId,
|
|
10147
|
-
//
|
|
10148
|
-
//
|
|
10149
|
-
//
|
|
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.
|
|
10150
9878
|
{ agentSessionId: event.state, turnId }
|
|
10151
9879
|
);
|
|
10152
9880
|
} catch (err) {
|
|
@@ -10251,9 +9979,7 @@ var init_turn_execution = __esm({
|
|
|
10251
9979
|
}
|
|
10252
9980
|
const body = {
|
|
10253
9981
|
activeRunStartedAt: null,
|
|
10254
|
-
//
|
|
10255
|
-
// off this turn id, so the settle PATCH must carry it — the start PATCH
|
|
10256
|
-
// 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.
|
|
10257
9983
|
turnId
|
|
10258
9984
|
};
|
|
10259
9985
|
if (o.turnUsage) {
|
|
@@ -10422,11 +10148,11 @@ var init_dispatcher = __esm({
|
|
|
10422
10148
|
this.aborts = opts.aborts ?? /* @__PURE__ */ new Map();
|
|
10423
10149
|
}
|
|
10424
10150
|
opts;
|
|
10425
|
-
//
|
|
10426
|
-
//
|
|
10151
|
+
// (conversation|agent) → per-TURN controllers. Stop is pair-scoped and must
|
|
10152
|
+
// reach every live loop for the pair.
|
|
10427
10153
|
aborts;
|
|
10428
|
-
//
|
|
10429
|
-
//
|
|
10154
|
+
// Pairs already told their session couldn't be saved: the notice is once per
|
|
10155
|
+
// pair, while the rejection repeats every turn.
|
|
10430
10156
|
sessionWriteNotified = /* @__PURE__ */ new Set();
|
|
10431
10157
|
notifyStart(info) {
|
|
10432
10158
|
try {
|
|
@@ -10443,11 +10169,6 @@ var init_dispatcher = __esm({
|
|
|
10443
10169
|
// The supervisor supplies a stable turnId on both fresh and resumed dispatch.
|
|
10444
10170
|
// resumed is separate: only replay seeds sequence/speech recovery from the
|
|
10445
10171
|
// prior span. A direct caller omitting turnId gets a fresh id.
|
|
10446
|
-
//
|
|
10447
|
-
// CT1261: a short `handle` creates a `TurnExecution` and runs it. The
|
|
10448
|
-
// supervisor seam hands the execution exactly the three cross-turn duties —
|
|
10449
|
-
// the abort registry, the session-write notice latch, the observer — and
|
|
10450
|
-
// nothing else.
|
|
10451
10172
|
async handle(payload, opts = {}) {
|
|
10452
10173
|
const key = runKey(payload.conversationId, payload.agentId);
|
|
10453
10174
|
const supervisor = {
|
|
@@ -10493,8 +10214,8 @@ var init_dispatcher = __esm({
|
|
|
10493
10214
|
}
|
|
10494
10215
|
return new TurnExecution(this.opts, supervisor, payload, opts).run();
|
|
10495
10216
|
}
|
|
10496
|
-
//
|
|
10497
|
-
//
|
|
10217
|
+
// Cancel a (conversation, agent) run in flight in THIS process. True if one
|
|
10218
|
+
// was aborted.
|
|
10498
10219
|
cancel(conversationId, agentId) {
|
|
10499
10220
|
const key = runKey(conversationId, agentId);
|
|
10500
10221
|
const pair2 = this.aborts.get(key);
|
|
@@ -11217,7 +10938,7 @@ var init_outbox = __esm({
|
|
|
11217
10938
|
}
|
|
11218
10939
|
workspaceId;
|
|
11219
10940
|
log;
|
|
11220
|
-
// Resolved lazily (per call
|
|
10941
|
+
// Resolved lazily (per call) so tests that swap HOME between
|
|
11221
10942
|
// cases route writes at the right tmpdir.
|
|
11222
10943
|
dir() {
|
|
11223
10944
|
return join20(runnerStateDir(), "outbox", encodeURIComponent(this.workspaceId));
|
|
@@ -11283,7 +11004,7 @@ var init_outbox = __esm({
|
|
|
11283
11004
|
);
|
|
11284
11005
|
return entries;
|
|
11285
11006
|
}
|
|
11286
|
-
//
|
|
11007
|
+
// The highest `seq` this turn has queued but not yet delivered — the
|
|
11287
11008
|
// half of a resumed turn's seq floor the SERVER CANNOT SEE. A commit that
|
|
11288
11009
|
// failed transiently before the crash is sitting in this directory with its
|
|
11289
11010
|
// seq already spent, and the restart's drain is fire-and-forget
|
|
@@ -12096,9 +11817,8 @@ function parseHarnessRuntime(raw) {
|
|
|
12096
11817
|
function deriveHarnessSnapshot(signals) {
|
|
12097
11818
|
const advertised = new Set(
|
|
12098
11819
|
buildCompanionManifest({
|
|
12099
|
-
//
|
|
12100
|
-
//
|
|
12101
|
-
// 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.
|
|
12102
11822
|
claudeCode: signals.claudeCodeConnected && signals.claudeBundled === "present",
|
|
12103
11823
|
opencode: signals.opencodeConfigured,
|
|
12104
11824
|
codex: signals.codexEnabled && signals.codexBundled === "present"
|
|
@@ -12136,10 +11856,8 @@ function deriveClaudeCode(signals, manifestHas) {
|
|
|
12136
11856
|
...base,
|
|
12137
11857
|
state: "detected_not_exposed",
|
|
12138
11858
|
version: signals.claudeVersion,
|
|
12139
|
-
//
|
|
12140
|
-
//
|
|
12141
|
-
// device still advertises nothing — so this says so up front rather than
|
|
12142
|
-
// 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.
|
|
12143
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()}`,
|
|
12144
11862
|
enable: "claude-code"
|
|
12145
11863
|
};
|
|
@@ -12177,8 +11895,7 @@ function deriveCodex(signals, manifestHas) {
|
|
|
12177
11895
|
...base,
|
|
12178
11896
|
state: "detected_not_exposed",
|
|
12179
11897
|
version: signals.codexVersion,
|
|
12180
|
-
//
|
|
12181
|
-
// launchability.
|
|
11898
|
+
// As for Claude Code: enabling is intent, not launchability.
|
|
12182
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()}`,
|
|
12183
11900
|
enable: "codex"
|
|
12184
11901
|
};
|
|
@@ -12259,12 +11976,10 @@ async function probeHarnessSignals(cfg, deps = {}) {
|
|
|
12259
11976
|
return {
|
|
12260
11977
|
claudeOnPath: claudePresence.status === "present",
|
|
12261
11978
|
claudeVersion,
|
|
12262
|
-
//
|
|
12263
|
-
// 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.
|
|
12264
11980
|
claudeCodeConnected: isClaudeCodeConnected(cfg),
|
|
12265
|
-
//
|
|
12266
|
-
//
|
|
12267
|
-
// 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.
|
|
12268
11983
|
claudeBundled: resolveBundled("claude-code").status,
|
|
12269
11984
|
codexBundled: resolveBundled("codex").status,
|
|
12270
11985
|
// A parseable `codex --version` is our presence signal (presence alone never
|
|
@@ -12273,7 +11988,7 @@ async function probeHarnessSignals(cfg, deps = {}) {
|
|
|
12273
11988
|
codexVersion,
|
|
12274
11989
|
codexEnabled: isCodexEnabled(cfg),
|
|
12275
11990
|
opencodeConfigured: !!configuredServerUrl,
|
|
12276
|
-
// A version came back ⟺ the serve answered its health endpoint
|
|
11991
|
+
// A version came back ⟺ the serve answered its health endpoint.
|
|
12277
11992
|
opencodeReachable: opencodeVersion !== null,
|
|
12278
11993
|
opencodeVersion,
|
|
12279
11994
|
opencodeDetectedUrl: opencodeVersion !== null ? opencodeProbeUrl : null
|
|
@@ -12842,13 +12557,11 @@ function resolvePairBaseUrl(server) {
|
|
|
12842
12557
|
function writePairedConfig(paired) {
|
|
12843
12558
|
const { local, note, hadPriorConfig } = loadConfigTolerant();
|
|
12844
12559
|
const config = {
|
|
12845
|
-
//
|
|
12846
|
-
// written explicitly,
|
|
12847
|
-
//
|
|
12848
|
-
//
|
|
12849
|
-
//
|
|
12850
|
-
// disconnected instead. So it's written only when there was no config here
|
|
12851
|
-
// 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.
|
|
12852
12565
|
...hadPriorConfig ? {} : { claudeCode: { enabled: false } },
|
|
12853
12566
|
...local,
|
|
12854
12567
|
baseUrl: paired.baseUrl,
|
|
@@ -13156,7 +12869,7 @@ var CompanionStateHub = class {
|
|
|
13156
12869
|
deviceId = null;
|
|
13157
12870
|
deviceLabel = null;
|
|
13158
12871
|
deviceError = null;
|
|
13159
|
-
//
|
|
12872
|
+
// The latest harness snapshot the supervisor probed, or null before the
|
|
13160
12873
|
// first probe (see CompanionStatusJson.harnesses).
|
|
13161
12874
|
harnesses = null;
|
|
13162
12875
|
// ---- subscription (SSE) ----
|
|
@@ -13183,7 +12896,7 @@ var CompanionStateHub = class {
|
|
|
13183
12896
|
this.deviceError = null;
|
|
13184
12897
|
this.emitStatus();
|
|
13185
12898
|
}
|
|
13186
|
-
// ---- harnesses
|
|
12899
|
+
// ---- harnesses ----
|
|
13187
12900
|
// Replace the harness snapshot and push a status change (so the SSE feed flips
|
|
13188
12901
|
// the Companion's Harnesses surface live). The supervisor calls this on each
|
|
13189
12902
|
// heartbeat-cadence probe and on an on-demand recheck.
|
|
@@ -13801,7 +13514,7 @@ var DeviceApi = class {
|
|
|
13801
13514
|
AbortSignal.timeout(5e3)
|
|
13802
13515
|
);
|
|
13803
13516
|
}
|
|
13804
|
-
//
|
|
13517
|
+
// Report that a dispatch addressed to THIS device can't be run, because
|
|
13805
13518
|
// the agent isn't one this device runs (and still wasn't after a forced
|
|
13806
13519
|
// assignments refresh). The server clears the Working flag, retires the dispatch
|
|
13807
13520
|
// and posts an explanatory system message.
|
|
@@ -13815,7 +13528,7 @@ var DeviceApi = class {
|
|
|
13815
13528
|
return this.request("POST", "/api/companion/dispatch-drop", body);
|
|
13816
13529
|
}
|
|
13817
13530
|
// Per-device liveness ping. Reports the companion build version and the env-var
|
|
13818
|
-
// names the operator's secret store exposes (never values), so
|
|
13531
|
+
// names the operator's secret store exposes (never values), so the UI can
|
|
13819
13532
|
// warn pre-emptively about an agent that needs a secret this device lacks.
|
|
13820
13533
|
heartbeat(body) {
|
|
13821
13534
|
return this.request("POST", "/api/companion/heartbeat", body ?? {});
|
|
@@ -13998,7 +13711,7 @@ var ConnectorHealthStore = class {
|
|
|
13998
13711
|
lastError: outcome.errorReason ?? null
|
|
13999
13712
|
});
|
|
14000
13713
|
}
|
|
14001
|
-
//
|
|
13714
|
+
// The store's current opinion for one runtime, or undefined when it has
|
|
14002
13715
|
// no observation. Read by the dispatcher at idle-timeout fire: an idle reap
|
|
14003
13716
|
// while this runtime is definitively `limited` (a present, future `limitedUntil`
|
|
14004
13717
|
// recorded by a sibling/earlier turn's classified cap settle) is the account cap
|
|
@@ -15579,24 +15292,20 @@ var CompanionSupervisor = class {
|
|
|
15579
15292
|
config;
|
|
15580
15293
|
log;
|
|
15581
15294
|
hub;
|
|
15582
|
-
//
|
|
15583
|
-
//
|
|
15584
|
-
//
|
|
15585
|
-
// 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.
|
|
15586
15298
|
resolveBundled;
|
|
15587
|
-
//
|
|
15588
|
-
//
|
|
15589
|
-
// 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.
|
|
15590
15301
|
loggedBundledStatus = /* @__PURE__ */ new Map();
|
|
15591
|
-
//
|
|
15592
|
-
//
|
|
15593
|
-
// 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.
|
|
15594
15304
|
harnessVersions;
|
|
15595
|
-
//
|
|
15596
|
-
//
|
|
15597
|
-
//
|
|
15598
|
-
//
|
|
15599
|
-
// 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.
|
|
15600
15309
|
harnessSignals = null;
|
|
15601
15310
|
// Fresh per attempt, never the cached heartbeat signal: installing a harness
|
|
15602
15311
|
// between attempts must be observed immediately.
|
|
@@ -15609,8 +15318,8 @@ var CompanionSupervisor = class {
|
|
|
15609
15318
|
unrunnableRetryDelaysMs;
|
|
15610
15319
|
deviceApi = null;
|
|
15611
15320
|
deviceSub = null;
|
|
15612
|
-
//
|
|
15613
|
-
//
|
|
15321
|
+
// Whether the device stream is open, which is what every workspace's link on
|
|
15322
|
+
// the dashboard reports.
|
|
15614
15323
|
deviceConnected = false;
|
|
15615
15324
|
recoveryChecked = false;
|
|
15616
15325
|
recovering = false;
|
|
@@ -15622,8 +15331,7 @@ var CompanionSupervisor = class {
|
|
|
15622
15331
|
pollTimer = null;
|
|
15623
15332
|
refreshing = false;
|
|
15624
15333
|
stopped = false;
|
|
15625
|
-
//
|
|
15626
|
-
// on every 30s heartbeat.
|
|
15334
|
+
// The version-skew warning is logged once, not every heartbeat.
|
|
15627
15335
|
versionSkewWarned = false;
|
|
15628
15336
|
// This device's id, captured from the heartbeat / assignments response. The SSE
|
|
15629
15337
|
// router uses it to tell a dispatch addressed to THIS device (a freshly-assigned
|
|
@@ -15633,12 +15341,12 @@ var CompanionSupervisor = class {
|
|
|
15633
15341
|
// The in-flight assignments pull, exposed so a caller that needs fresh data
|
|
15634
15342
|
// (pull-on-miss) can `await` the running pull rather than racing past it.
|
|
15635
15343
|
inFlightRefresh = null;
|
|
15636
|
-
//
|
|
15637
|
-
//
|
|
15638
|
-
//
|
|
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.
|
|
15639
15347
|
connectorHealth = new ConnectorHealthStore();
|
|
15640
|
-
//
|
|
15641
|
-
//
|
|
15348
|
+
// Which dispatches this process is running, so a redelivered frame doesn't
|
|
15349
|
+
// run the turn a second time.
|
|
15642
15350
|
claims = new DispatchClaims();
|
|
15643
15351
|
constructor(opts) {
|
|
15644
15352
|
this.config = opts.config;
|
|
@@ -15659,10 +15367,10 @@ var CompanionSupervisor = class {
|
|
|
15659
15367
|
}
|
|
15660
15368
|
this.latchBootBundledState();
|
|
15661
15369
|
}
|
|
15662
|
-
//
|
|
15663
|
-
//
|
|
15370
|
+
// What a runner container said it can run, probed once at start; null until
|
|
15371
|
+
// then, and when no container could be started.
|
|
15664
15372
|
runnerImage = null;
|
|
15665
|
-
//
|
|
15373
|
+
// The companion every container mounts: this one's own install.
|
|
15666
15374
|
runnerCompanion() {
|
|
15667
15375
|
return companionSource({
|
|
15668
15376
|
configured: this.config.runners?.docker?.companionDir,
|
|
@@ -15774,10 +15482,7 @@ var CompanionSupervisor = class {
|
|
|
15774
15482
|
this.pollTimer = setInterval(() => void this.refreshAssignments(), ASSIGNMENTS_POLL_MS);
|
|
15775
15483
|
this.pollTimer.unref?.();
|
|
15776
15484
|
}
|
|
15777
|
-
//
|
|
15778
|
-
// keep the foreground process alive. With the poll loop running there's always
|
|
15779
|
-
// at least the loops keeping the event loop alive; the SSE promises let a
|
|
15780
|
-
// clean teardown resolve.
|
|
15485
|
+
// start.ts awaits these; they resolve on a clean teardown.
|
|
15781
15486
|
finishedPromises() {
|
|
15782
15487
|
return [...this.deviceSub ? [this.deviceSub.finished] : []];
|
|
15783
15488
|
}
|
|
@@ -15805,39 +15510,26 @@ var CompanionSupervisor = class {
|
|
|
15805
15510
|
bootId,
|
|
15806
15511
|
version: COMPANION_VERSION,
|
|
15807
15512
|
exposedSecretNames: store.names(),
|
|
15808
|
-
//
|
|
15809
|
-
// CT1379 claude-code and codex when the SDK's own bundled binary is there
|
|
15810
|
-
// and the user asked for the harness, CT270 opencode when the operator
|
|
15811
|
-
// configured an `opencode serve`.
|
|
15513
|
+
// Each runtime only when this device can actually run it.
|
|
15812
15514
|
manifest: buildCompanionManifest({
|
|
15813
|
-
//
|
|
15814
|
-
//
|
|
15815
|
-
// the user's `claudeCode.enabled`. Claude Code rides neither alone.
|
|
15515
|
+
// Consented (`claudeCode.enabled`) AND launchable (the live bundled
|
|
15516
|
+
// binary). Neither alone.
|
|
15816
15517
|
claudeCode: this.claudeCodeOffered(),
|
|
15817
15518
|
opencode: !!this.config.opencode?.serverUrl,
|
|
15818
|
-
//
|
|
15819
|
-
//
|
|
15820
|
-
// which is how a device with a half-installed 331 MB platform package
|
|
15821
|
-
// 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.
|
|
15822
15521
|
codex: this.codexOffered(),
|
|
15823
|
-
//
|
|
15824
|
-
// above. Diagnostic only — the server never routes on it — and always
|
|
15522
|
+
// WHY an asked-for harness is absent above. Diagnostic only, and always
|
|
15825
15523
|
// sent, so an empty array clears a previously reported issue.
|
|
15826
15524
|
runtimeIssues: this.runtimeIssues(),
|
|
15827
|
-
//
|
|
15828
|
-
// (fail-soft to null). Informational only — the server matches on name.
|
|
15525
|
+
// Informational only; the server matches on name.
|
|
15829
15526
|
versions: this.harnessVersions,
|
|
15830
|
-
//
|
|
15527
|
+
// Shown on the device page.
|
|
15831
15528
|
...runnersEnabled(this.config) ? { runner: this.runnerManifest() } : {}
|
|
15832
15529
|
}),
|
|
15833
|
-
//
|
|
15834
|
-
//
|
|
15835
|
-
// failure/heal, so a fresh companion's beat is unchanged and the server's
|
|
15836
|
-
// 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.
|
|
15837
15532
|
...connectorReports.length > 0 ? { connectors: connectorReports } : {},
|
|
15838
|
-
// CT584: include enumerated models only when the probe SUCCEEDED (non-null).
|
|
15839
|
-
// A null (probe failed / no opencode) omits the field, and the server then
|
|
15840
|
-
// leaves this device's stored availability untouched.
|
|
15841
15533
|
...opencodeModels !== null ? { models: opencodeModels } : {}
|
|
15842
15534
|
});
|
|
15843
15535
|
this.hub.setDevice({ deviceId: res.deviceId });
|
|
@@ -15852,11 +15544,8 @@ var CompanionSupervisor = class {
|
|
|
15852
15544
|
);
|
|
15853
15545
|
}
|
|
15854
15546
|
}
|
|
15855
|
-
//
|
|
15856
|
-
//
|
|
15857
|
-
// companion's, warn loudly (once). This is exactly the skew that bit the M1 walk:
|
|
15858
|
-
// an npm-pinned companion lagging a from-develop server. The elimination (bundled
|
|
15859
|
-
// 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.
|
|
15860
15549
|
checkVersionSkew(serverVersion) {
|
|
15861
15550
|
if (this.versionSkewWarned) return;
|
|
15862
15551
|
if (!serverVersion || serverVersion === COMPANION_VERSION) return;
|
|
@@ -16053,10 +15742,9 @@ var CompanionSupervisor = class {
|
|
|
16053
15742
|
`Stopped running ${runner.displayName} (unassigned)`
|
|
16054
15743
|
);
|
|
16055
15744
|
}
|
|
16056
|
-
//
|
|
16057
|
-
//
|
|
16058
|
-
//
|
|
16059
|
-
// 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.
|
|
16060
15748
|
bundledStatus(runtime) {
|
|
16061
15749
|
const cached2 = runtime === "codex" ? this.harnessSignals?.codexBundled : this.harnessSignals?.claudeBundled;
|
|
16062
15750
|
return cached2 ?? this.resolveBundled(runtime).status;
|
|
@@ -16067,23 +15755,19 @@ var CompanionSupervisor = class {
|
|
|
16067
15755
|
bundledResolution(runtime) {
|
|
16068
15756
|
return this.resolveBundled(runtime);
|
|
16069
15757
|
}
|
|
16070
|
-
//
|
|
16071
|
-
//
|
|
16072
|
-
//
|
|
16073
|
-
//
|
|
16074
|
-
// 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.
|
|
16075
15762
|
claudeCodeOffered() {
|
|
16076
15763
|
return isClaudeCodeConnected(this.config) && this.bundledStatus("claude-code") === "present";
|
|
16077
15764
|
}
|
|
16078
|
-
//
|
|
16079
|
-
// alone — so an enabled device with no vendored binary advertised a runtime it
|
|
16080
|
-
// 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.
|
|
16081
15766
|
codexOffered() {
|
|
16082
15767
|
return isCodexEnabled(this.config) && this.bundledStatus("codex") === "present";
|
|
16083
15768
|
}
|
|
16084
|
-
//
|
|
16085
|
-
//
|
|
16086
|
-
// 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.
|
|
16087
15771
|
runtimeIssues() {
|
|
16088
15772
|
return runtimeIssuesFor({
|
|
16089
15773
|
claudeCode: {
|
|
@@ -16093,26 +15777,16 @@ var CompanionSupervisor = class {
|
|
|
16093
15777
|
codex: { intended: isCodexEnabled(this.config), bundled: this.bundledStatus("codex") }
|
|
16094
15778
|
});
|
|
16095
15779
|
}
|
|
16096
|
-
//
|
|
16097
|
-
//
|
|
16098
|
-
//
|
|
16099
|
-
// repro and carries nothing that could be a credential or a vendor's output.
|
|
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.
|
|
16100
15783
|
//
|
|
16101
|
-
//
|
|
16102
|
-
//
|
|
16103
|
-
//
|
|
16104
|
-
//
|
|
16105
|
-
//
|
|
16106
|
-
//
|
|
16107
|
-
// child's output goes to a file. The boot state is latched in the
|
|
16108
|
-
// constructor, so the first refresh reports nothing new; it lands here as a
|
|
16109
|
-
// debug record instead, present for diagnosis without competing for
|
|
16110
|
-
// attention with the warning the operator already read.
|
|
16111
|
-
// - It does not report a transition in generic prose. A binary that goes
|
|
16112
|
-
// missing at 3am is the case the operator will actually meet, and a line
|
|
16113
|
-
// that says only "binary is missing" leaves them with no repair and no
|
|
16114
|
-
// restart instruction. Transitions carry the specified copy verbatim — the
|
|
16115
|
-
// 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.
|
|
16116
15790
|
logBundledTransitions() {
|
|
16117
15791
|
for (const runtime of ["claude-code", "codex"]) {
|
|
16118
15792
|
const intended = runtime === "codex" ? isCodexEnabled(this.config) : isClaudeCodeConnected(this.config);
|
|
@@ -16135,9 +15809,8 @@ var CompanionSupervisor = class {
|
|
|
16135
15809
|
this.log.debug(meta, "companion: bundled harness binary is missing (harness not in use)");
|
|
16136
15810
|
}
|
|
16137
15811
|
}
|
|
16138
|
-
//
|
|
16139
|
-
//
|
|
16140
|
-
// 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.
|
|
16141
15814
|
latchBootBundledState() {
|
|
16142
15815
|
for (const runtime of ["claude-code", "codex"]) {
|
|
16143
15816
|
const resolution = this.resolveBundled(runtime);
|
|
@@ -16155,35 +15828,34 @@ var CompanionSupervisor = class {
|
|
|
16155
15828
|
);
|
|
16156
15829
|
}
|
|
16157
15830
|
}
|
|
16158
|
-
//
|
|
15831
|
+
// The locks every runner on this device shares.
|
|
16159
15832
|
runnerLocks = new DeviceLocks();
|
|
16160
|
-
//
|
|
16161
|
-
//
|
|
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.
|
|
16162
15835
|
sandboxUsable;
|
|
16163
15836
|
sandboxHost = {
|
|
16164
15837
|
osSandboxUsable: () => this.sandboxUsable ??= osSandboxUsable(this.log)
|
|
16165
15838
|
};
|
|
16166
|
-
//
|
|
16167
|
-
//
|
|
15839
|
+
// `runners.policy`, parsed once. An invalid one is kept as the error, which
|
|
15840
|
+
// every sandboxed turn is refused with.
|
|
16168
15841
|
parsedSandboxPolicy;
|
|
16169
15842
|
sandboxPolicy() {
|
|
16170
15843
|
this.parsedSandboxPolicy ??= parseSandboxPolicy(this.config.runners?.policy);
|
|
16171
15844
|
return this.parsedSandboxPolicy;
|
|
16172
15845
|
}
|
|
16173
|
-
//
|
|
15846
|
+
// The walk for other companions' stores, run off the turn path.
|
|
16174
15847
|
storeWatch;
|
|
16175
15848
|
companionStores() {
|
|
16176
15849
|
this.storeWatch ??= companionStoreWatch(this.log);
|
|
16177
15850
|
return this.storeWatch;
|
|
16178
15851
|
}
|
|
16179
|
-
//
|
|
16180
|
-
//
|
|
16181
|
-
//
|
|
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.
|
|
16182
15855
|
runnerSandboxFor(row) {
|
|
16183
15856
|
return runnerSandboxFor(this.config, row, this.sandboxHost);
|
|
16184
15857
|
}
|
|
16185
|
-
//
|
|
16186
|
-
// against the agents it runs right now.
|
|
15858
|
+
// Resolved against the agents this device runs right now.
|
|
16187
15859
|
runnerManifest() {
|
|
16188
15860
|
const agents = [...this.workspaces.values()].flatMap(
|
|
16189
15861
|
(wr) => [...wr.agents.values()].map((agent) => ({
|
|
@@ -16197,7 +15869,7 @@ var CompanionSupervisor = class {
|
|
|
16197
15869
|
...runnerSandboxReport(this.config, agents, this.sandboxHost)
|
|
16198
15870
|
};
|
|
16199
15871
|
}
|
|
16200
|
-
//
|
|
15872
|
+
// One provider for the device, built on first use.
|
|
16201
15873
|
runnerProvider() {
|
|
16202
15874
|
this.providerForRunners ??= this.dockerRunners() ? this.createDockerRunners() : createLocalProcessProvider();
|
|
16203
15875
|
return this.providerForRunners;
|
|
@@ -16239,8 +15911,7 @@ var CompanionSupervisor = class {
|
|
|
16239
15911
|
log: this.log
|
|
16240
15912
|
});
|
|
16241
15913
|
}
|
|
16242
|
-
//
|
|
16243
|
-
// companion in a container, this process's otherwise.
|
|
15914
|
+
// The image's own companion in a container, this process's otherwise.
|
|
16244
15915
|
runnerCommandFor() {
|
|
16245
15916
|
if (this.runnerCommand) return this.runnerCommand;
|
|
16246
15917
|
return this.dockerRunners() ? ["cabane-companion", "run-turn"] : selfRunnerCommand();
|
|
@@ -16272,36 +15943,20 @@ var CompanionSupervisor = class {
|
|
|
16272
15943
|
aborts: ctx.aborts,
|
|
16273
15944
|
runConfig: ctx.runConfig,
|
|
16274
15945
|
log: this.log.child({ agentName: ctx.agentDisplayName ?? ctx.agentUsername }),
|
|
16275
|
-
//
|
|
16276
|
-
//
|
|
16277
|
-
//
|
|
16278
|
-
//
|
|
16279
|
-
// runnable, so a disconnected harness can't be selected either.
|
|
16280
|
-
// CT1379: and the same for the bundled binary — the predicate is shared with
|
|
16281
|
-
// the heartbeat above, so a runtime advertised by one and refused by the
|
|
16282
|
-
// 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.
|
|
16283
15950
|
claudeCodeAvailable: () => this.claudeCodeOffered(),
|
|
16284
|
-
// CT270: the opencode server URL (operator-configured), when this device
|
|
16285
|
-
// offers the opencode runtime. Threaded so an opencode turn selects the
|
|
16286
|
-
// opencode adapter; unset leaves the device claude-code-only.
|
|
16287
15951
|
...this.config.opencode?.serverUrl ? { opencodeServerUrl: this.config.opencode.serverUrl } : {},
|
|
16288
|
-
// CT481: register the codex adapter when this device offers codex; a miss
|
|
16289
|
-
// leaves an `openai/…` turn to fail loudly (no silent claude-code fallback).
|
|
16290
|
-
// CT1379: read PER TURN, like claude-code above, and off the same predicate
|
|
16291
|
-
// the heartbeat uses — it was captured here at dispatcher-construction time
|
|
16292
|
-
// from the enable flag alone, so a device could register an adapter for a
|
|
16293
|
-
// binary it did not have and keep it registered after the binary came back.
|
|
16294
15952
|
codexAvailable: () => this.codexOffered(),
|
|
16295
|
-
//
|
|
16296
|
-
//
|
|
16297
|
-
//
|
|
16298
|
-
// override set here still wins, and is deliberately not validated against
|
|
16299
|
-
// 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.
|
|
16300
15956
|
...positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") !== void 0 ? { idleTimeoutMs: positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") } : {},
|
|
16301
15957
|
...positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") !== void 0 ? { totalTimeoutMs: positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") } : {},
|
|
16302
15958
|
observer: this.hub.observerFor(ctx.workspaceId, ctx.agentId, ctx.workspaceSlug),
|
|
16303
15959
|
transcriptDir: transcriptsDir(),
|
|
16304
|
-
// CT566: the shared connector-health memory the turn records into on settle.
|
|
16305
15960
|
connectorHealth: this.connectorHealth,
|
|
16306
15961
|
...runnersEnabled(this.config) ? {
|
|
16307
15962
|
runner: {
|
|
@@ -16325,11 +15980,10 @@ var CompanionSupervisor = class {
|
|
|
16325
15980
|
this.workspaces.delete(workspaceId);
|
|
16326
15981
|
this.hub.removeWorkspace(workspaceId);
|
|
16327
15982
|
}
|
|
16328
|
-
// ---- SSE event routing (
|
|
16329
|
-
//
|
|
16330
|
-
//
|
|
16331
|
-
//
|
|
16332
|
-
// 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`.
|
|
16333
15987
|
async handleDeviceMessage(ev) {
|
|
16334
15988
|
const wire = this.parseFrame(ev);
|
|
16335
15989
|
if (!wire) return;
|
|
@@ -16365,9 +16019,9 @@ var CompanionSupervisor = class {
|
|
|
16365
16019
|
return null;
|
|
16366
16020
|
}
|
|
16367
16021
|
}
|
|
16368
|
-
//
|
|
16369
|
-
//
|
|
16370
|
-
//
|
|
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.
|
|
16371
16025
|
//
|
|
16372
16026
|
// A cancel needs no gate: `dispatcher.cancel` on an already-aborted run is a
|
|
16373
16027
|
// no-op that reports `false`, so a second copy is inert.
|
|
@@ -16383,8 +16037,6 @@ var CompanionSupervisor = class {
|
|
|
16383
16037
|
if (claim) this.claims.release(wire.id);
|
|
16384
16038
|
}
|
|
16385
16039
|
}
|
|
16386
|
-
// Everything below here is the routing as it was: it takes a frame and knows
|
|
16387
|
-
// nothing about which stream carried it.
|
|
16388
16040
|
async routeFrame(wr, wire) {
|
|
16389
16041
|
if (wire.type === "device:cancel_requested") {
|
|
16390
16042
|
const payload2 = {
|
|
@@ -16450,17 +16102,12 @@ var CompanionSupervisor = class {
|
|
|
16450
16102
|
await tail;
|
|
16451
16103
|
if (wr.chains.get(chainKey) === tail) wr.chains.delete(chainKey);
|
|
16452
16104
|
}
|
|
16453
|
-
//
|
|
16454
|
-
//
|
|
16105
|
+
// Tell the server this device cannot run a dispatch it was addressed to.
|
|
16455
16106
|
// Returns whether the report LANDED. The server checks the recorded dispatch
|
|
16456
|
-
// destination even if the
|
|
16457
|
-
// explains the roster miss; the thirty-minute unanswered-offer deadline is
|
|
16458
|
-
// only the backstop when no report lands.
|
|
16107
|
+
// destination even if the assignment has moved.
|
|
16459
16108
|
//
|
|
16460
|
-
//
|
|
16461
|
-
//
|
|
16462
|
-
// hard — this runs inline on the event loop, and an unbounded wait here would
|
|
16463
|
-
// 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.
|
|
16464
16111
|
async reportUnrunnableDispatch(workspaceId, payload) {
|
|
16465
16112
|
if (!this.deviceApi) return false;
|
|
16466
16113
|
let lastErr;
|
|
@@ -16500,9 +16147,8 @@ var CompanionSupervisor = class {
|
|
|
16500
16147
|
);
|
|
16501
16148
|
return false;
|
|
16502
16149
|
}
|
|
16503
|
-
//
|
|
16504
|
-
//
|
|
16505
|
-
// 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.
|
|
16506
16152
|
async reportRunnerGone(report) {
|
|
16507
16153
|
if (!this.deviceApi) return;
|
|
16508
16154
|
let lastErr;
|
|
@@ -16527,12 +16173,9 @@ var CompanionSupervisor = class {
|
|
|
16527
16173
|
"Couldn't tell Cabane a turn's runner stopped; its credential expires on its own."
|
|
16528
16174
|
);
|
|
16529
16175
|
}
|
|
16530
|
-
// Pull-on-miss
|
|
16531
|
-
//
|
|
16532
|
-
//
|
|
16533
|
-
// and return the now-live runner. Returns undefined when the dispatch isn't for
|
|
16534
|
-
// this device, or the agent still isn't ours after pulling (a genuine
|
|
16535
|
-
// 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.
|
|
16536
16179
|
async recoverRacedAgent(wr, payload) {
|
|
16537
16180
|
if (!this.deviceId || payload.deviceId !== this.deviceId) return void 0;
|
|
16538
16181
|
if (this.refreshing && this.inFlightRefresh) await this.inFlightRefresh;
|
|
@@ -16573,9 +16216,8 @@ var CompanionSupervisor = class {
|
|
|
16573
16216
|
};
|
|
16574
16217
|
const tail = this.runDispatch(
|
|
16575
16218
|
wr,
|
|
16576
|
-
//
|
|
16577
|
-
//
|
|
16578
|
-
// path's own guard.
|
|
16219
|
+
// Off `/runs`, not a frame, so no dispatch id to dedupe on;
|
|
16220
|
+
// `recoveredTurns` is this path's guard.
|
|
16579
16221
|
"",
|
|
16580
16222
|
payload,
|
|
16581
16223
|
agent,
|
|
@@ -16597,9 +16239,7 @@ var CompanionSupervisor = class {
|
|
|
16597
16239
|
this.recovering = false;
|
|
16598
16240
|
}
|
|
16599
16241
|
}
|
|
16600
|
-
// One turn: the
|
|
16601
|
-
// the console summary. Mirrors the old per-workspace `handleEvent`, now
|
|
16602
|
-
// scoped to one agent.
|
|
16242
|
+
// One turn: the skip-vs-resume dedupe, the dispatch, then the console summary.
|
|
16603
16243
|
async runDispatch(wr, dispatchId, payload, agent, recoveredTurnId) {
|
|
16604
16244
|
const workspaceId = wr.workspaceId;
|
|
16605
16245
|
if (dispatchId && hasDispatched(workspaceId, dispatchId)) {
|
|
@@ -16684,7 +16324,7 @@ var CompanionSupervisor = class {
|
|
|
16684
16324
|
);
|
|
16685
16325
|
}
|
|
16686
16326
|
}
|
|
16687
|
-
// ---- per-agent outbox drain scheduler
|
|
16327
|
+
// ---- per-agent outbox drain scheduler ----
|
|
16688
16328
|
makeDrain(api, agentId) {
|
|
16689
16329
|
let drainTimer;
|
|
16690
16330
|
let drainDelay = DRAIN_BASE_MS;
|
|
@@ -16753,19 +16393,16 @@ var CompanionSupervisor = class {
|
|
|
16753
16393
|
saveConfig(next);
|
|
16754
16394
|
configureLogger(this.log, next);
|
|
16755
16395
|
}
|
|
16756
|
-
// ---- harnesses
|
|
16757
|
-
// Probe all three harnesses, cache
|
|
16758
|
-
//
|
|
16759
|
-
//
|
|
16760
|
-
// wrapped anyway so a harness refresh can never take a heartbeat down. Called on
|
|
16761
|
-
// 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.
|
|
16762
16400
|
async refreshHarnessStatuses(presence = {}) {
|
|
16763
16401
|
try {
|
|
16764
16402
|
const signals = await probeHarnessSignals(this.config, {
|
|
16765
16403
|
probePresence: this.probePresence,
|
|
16766
16404
|
presence,
|
|
16767
|
-
//
|
|
16768
|
-
// 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.
|
|
16769
16406
|
resolveBundled: this.resolveBundled
|
|
16770
16407
|
});
|
|
16771
16408
|
this.harnessSignals = signals;
|
|
@@ -16788,11 +16425,10 @@ var CompanionSupervisor = class {
|
|
|
16788
16425
|
async recheckHarnesses() {
|
|
16789
16426
|
await this.refreshHarnessStatuses();
|
|
16790
16427
|
}
|
|
16791
|
-
//
|
|
16792
|
-
//
|
|
16793
|
-
//
|
|
16794
|
-
//
|
|
16795
|
-
// 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.
|
|
16796
16432
|
async heartbeatNow() {
|
|
16797
16433
|
if (this.inFlightHeartbeat) {
|
|
16798
16434
|
await this.inFlightHeartbeat;
|
|
@@ -16801,35 +16437,25 @@ var CompanionSupervisor = class {
|
|
|
16801
16437
|
this.kickHeartbeat();
|
|
16802
16438
|
if (this.inFlightHeartbeat) await this.inFlightHeartbeat;
|
|
16803
16439
|
}
|
|
16804
|
-
//
|
|
16805
|
-
//
|
|
16806
|
-
// against the URL/flag that was just persisted, not the one this process booted
|
|
16807
|
-
// 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.
|
|
16808
16442
|
currentConfig() {
|
|
16809
16443
|
return this.config;
|
|
16810
16444
|
}
|
|
16811
|
-
//
|
|
16812
|
-
// `~/.cabane/config.json
|
|
16813
|
-
// 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).
|
|
16814
16447
|
//
|
|
16815
|
-
//
|
|
16816
|
-
//
|
|
16817
|
-
//
|
|
16818
|
-
//
|
|
16819
|
-
//
|
|
16820
|
-
//
|
|
16821
|
-
// -
|
|
16822
|
-
//
|
|
16823
|
-
//
|
|
16824
|
-
//
|
|
16825
|
-
//
|
|
16826
|
-
// - opencode: set `opencode.serverUrl` — but ONLY after health-probing the URL,
|
|
16827
|
-
// so we never advertise a serve that isn't there. An unreachable URL is a
|
|
16828
|
-
// typed failure the UI shows in place, not a silent bad write.
|
|
16829
|
-
// On success, persist, re-probe, and beat immediately, so a runtime that IS
|
|
16830
|
-
// launchable starts taking turns at once rather than on the next tick — and one
|
|
16831
|
-
// that isn't reports its issue just as promptly. Returns a typed outcome (never
|
|
16832
|
-
// 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.
|
|
16833
16459
|
async enableHarness(input) {
|
|
16834
16460
|
let next;
|
|
16835
16461
|
let checkedPresence = null;
|
|
@@ -16880,9 +16506,8 @@ var CompanionSupervisor = class {
|
|
|
16880
16506
|
return { ok: true };
|
|
16881
16507
|
}
|
|
16882
16508
|
// Re-create every running agent's Dispatcher from the CURRENT config, keeping
|
|
16883
|
-
// the runner (its api
|
|
16884
|
-
//
|
|
16885
|
-
// 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.
|
|
16886
16511
|
rebuildDispatchers() {
|
|
16887
16512
|
for (const wr of this.workspaces.values()) {
|
|
16888
16513
|
for (const runner of wr.agents.values()) {
|
|
@@ -17050,8 +16675,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
17050
16675
|
}));
|
|
17051
16676
|
await warnAboutHarnessReadiness(cfg, {
|
|
17052
16677
|
probeClaude: async () => claudeCode,
|
|
17053
|
-
//
|
|
17054
|
-
// 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
|
|
17055
16679
|
// sink that logs instead. Every other caller keeps the stderr line.
|
|
17056
16680
|
...opts.onReadinessWarning ? { warn: opts.onReadinessWarning } : {}
|
|
17057
16681
|
});
|
|
@@ -17088,11 +16712,10 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
17088
16712
|
const clearRuntimeOnExit = () => clearRuntimeStateIfOurs(instanceId);
|
|
17089
16713
|
process.on("exit", clearRuntimeOnExit);
|
|
17090
16714
|
const hub = new CompanionStateHub({
|
|
17091
|
-
//
|
|
17092
|
-
// with. The dashboard's connection line shows it.
|
|
16715
|
+
// The dashboard's connection line shows it.
|
|
17093
16716
|
baseUrl: cfg.baseUrl,
|
|
17094
16717
|
companionVersion: COMPANION_VERSION,
|
|
17095
|
-
//
|
|
16718
|
+
// Surfaced on `/api/status` so `stop`/`status` can confirm the
|
|
17096
16719
|
// process behind the marker pid is this companion (not a recycled pid).
|
|
17097
16720
|
instanceId
|
|
17098
16721
|
});
|
|
@@ -17145,7 +16768,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
17145
16768
|
socket: control.path,
|
|
17146
16769
|
...dashboard ? { url: dashboard.url, port: dashboard.port } : {},
|
|
17147
16770
|
startedAt,
|
|
17148
|
-
//
|
|
16771
|
+
// The daemon launcher sets this env on the detached child, so the
|
|
17149
16772
|
// marker records whether this companion is backgrounded (foreground start
|
|
17150
16773
|
// leaves it unset → false).
|
|
17151
16774
|
daemon: process.env.CABANE_COMPANION_DAEMON === "1",
|
|
@@ -17249,7 +16872,7 @@ async function startDaemon(opts = {}, deps = {}) {
|
|
|
17249
16872
|
});
|
|
17250
16873
|
await ensureRuntime(cfg, {
|
|
17251
16874
|
probeClaude: async () => claudeOnPath2,
|
|
17252
|
-
//
|
|
16875
|
+
// The onboarding script already said what's connectable, in its own
|
|
17253
16876
|
// words. Saying it again here, on stderr, in the middle of the closing block
|
|
17254
16877
|
// would be the same news twice.
|
|
17255
16878
|
...quiet ? { warn: () => {
|