@cabane/companion 0.6.202 → 0.6.204
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 +25 -1
- package/dist/cli.js +1454 -1324
- package/dist/launch.js +5 -23
- package/dist/pairing-config.js +29 -45
- package/dist/runtime.js +356 -139
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -321,25 +321,7 @@ var init_prepare_hook = __esm({
|
|
|
321
321
|
// Wall-clock cap for the hook. Provisioning is slow (minutes), so the
|
|
322
322
|
// default is generous; a hook that hangs past this is killed and the turn
|
|
323
323
|
// fails with a clear timeout message rather than pinning the companion.
|
|
324
|
-
timeoutMs: z.number().int().positive().optional()
|
|
325
|
-
// Re-invoke the hook when a prepared result already exists, in VALIDATE
|
|
326
|
-
// mode: the cached result is handed back as `prepared` and stdout is
|
|
327
|
-
// ignored, so the cwd cannot move. Its only power is to exit non-zero and
|
|
328
|
-
// fail the turn. The hook resolves once per (conversation, agent), so a
|
|
329
|
-
// later message asking for a different environment is otherwise silently
|
|
330
|
-
// ignored; this lets a hook refuse it out loud.
|
|
331
|
-
//
|
|
332
|
-
// Default OFF: a hook that provisions unconditionally must not start
|
|
333
|
-
// running on every turn because the companion upgraded.
|
|
334
|
-
validateCached: z.boolean().optional(),
|
|
335
|
-
// Independent operator-owned reporter. Receives sanitized failure metadata
|
|
336
|
-
// on stdin; never re-enters this hook and cannot change the turn outcome.
|
|
337
|
-
failureReporter: z.object({
|
|
338
|
-
command: z.string().min(1),
|
|
339
|
-
args: z.array(z.string()).optional(),
|
|
340
|
-
env: z.record(z.string(), z.string()).optional(),
|
|
341
|
-
timeoutMs: z.number().int().positive().max(3e4).optional()
|
|
342
|
-
}).strict().optional()
|
|
324
|
+
timeoutMs: z.number().int().positive().optional()
|
|
343
325
|
}).strict();
|
|
344
326
|
DEFAULT_TIMEOUT_MS = 10 * 6e4;
|
|
345
327
|
prepareResultSchema = z.object({
|
|
@@ -369,15 +351,7 @@ var init_prepare_hook = __esm({
|
|
|
369
351
|
CABANE_AGENT_USERNAME: input.agentUsername,
|
|
370
352
|
CABANE_DEFAULT_CWD: input.defaultCwd,
|
|
371
353
|
CABANE_RUNTIME: input.runtime ?? "",
|
|
372
|
-
|
|
373
|
-
// hook splits on `\n` unambiguously. Empty string when none.
|
|
374
|
-
CABANE_TRIGGER_ENTRY_PATHS: input.triggerEntryPaths.join("\n"),
|
|
375
|
-
CABANE_CONVERSATION_TITLE: input.title ?? "",
|
|
376
|
-
// The dispatching message's text, and — in validate mode only —
|
|
377
|
-
// the prepared result the hook is being asked to check this dispatch
|
|
378
|
-
// against. Both empty/absent on an ordinary first run.
|
|
379
|
-
CABANE_TRIGGER_MESSAGE: input.messageBody ?? "",
|
|
380
|
-
...input.prepared ? { CABANE_PREPARED: JSON.stringify(input.prepared) } : {}
|
|
354
|
+
CABANE_CONVERSATION_TITLE: input.title ?? ""
|
|
381
355
|
}
|
|
382
356
|
});
|
|
383
357
|
} catch (err) {
|
|
@@ -415,10 +389,6 @@ var init_prepare_hook = __esm({
|
|
|
415
389
|
);
|
|
416
390
|
return;
|
|
417
391
|
}
|
|
418
|
-
if (input.prepared) {
|
|
419
|
-
resolve2(input.prepared);
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
392
|
try {
|
|
423
393
|
resolve2(parsePrepareOutput(stdout));
|
|
424
394
|
} catch (err) {
|
|
@@ -487,33 +457,36 @@ function workRoot(cfg) {
|
|
|
487
457
|
if (root.startsWith("~/")) return join4(homedir5(), root.slice(2));
|
|
488
458
|
return root;
|
|
489
459
|
}
|
|
490
|
-
function
|
|
491
|
-
|
|
492
|
-
|
|
460
|
+
function isObject(value) {
|
|
461
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
462
|
+
}
|
|
463
|
+
function splitRetired(raw) {
|
|
493
464
|
const keys = [];
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
465
|
+
const without = (obj, names, path) => {
|
|
466
|
+
const out = {};
|
|
467
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
468
|
+
if (names.includes(k)) keys.push(`${path}${k}`);
|
|
469
|
+
else out[k] = v;
|
|
470
|
+
}
|
|
471
|
+
return out;
|
|
472
|
+
};
|
|
473
|
+
const hook = (row3, path) => isObject(row3.prepareHook) ? { ...row3, prepareHook: without(row3.prepareHook, RETIRED_HOOK_KEYS, `${path}prepareHook.`) } : row3;
|
|
474
|
+
const kept = hook(without(raw, ["runners"], ""), "");
|
|
475
|
+
if (isObject(kept.agents)) {
|
|
476
|
+
const agents = {};
|
|
477
|
+
for (const [key, row3] of Object.entries(kept.agents)) {
|
|
478
|
+
const path = `agents.${key}.`;
|
|
479
|
+
agents[key] = isObject(row3) ? hook(without(row3, RETIRED_ROW_KEYS, path), path) : row3;
|
|
499
480
|
}
|
|
481
|
+
kept.agents = agents;
|
|
500
482
|
}
|
|
501
|
-
return keys;
|
|
483
|
+
return { keys, kept };
|
|
484
|
+
}
|
|
485
|
+
function retiredConfigKeys(raw) {
|
|
486
|
+
return isObject(raw) ? splitRetired(raw).keys : [];
|
|
502
487
|
}
|
|
503
488
|
function withoutRetiredKeys(raw) {
|
|
504
|
-
|
|
505
|
-
const { runners: _runners, ...rest } = raw;
|
|
506
|
-
if (!rest.agents || typeof rest.agents !== "object") return rest;
|
|
507
|
-
const agents = {};
|
|
508
|
-
for (const [key, row3] of Object.entries(rest.agents)) {
|
|
509
|
-
if (row3 && typeof row3 === "object" && "sandbox" in row3) {
|
|
510
|
-
const { sandbox: _sandbox, ...kept } = row3;
|
|
511
|
-
agents[key] = kept;
|
|
512
|
-
} else {
|
|
513
|
-
agents[key] = row3;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
return { ...rest, agents };
|
|
489
|
+
return isObject(raw) ? splitRetired(raw).kept : raw;
|
|
517
490
|
}
|
|
518
491
|
function retiredConfigKeysOnDisk() {
|
|
519
492
|
try {
|
|
@@ -708,7 +681,7 @@ function deleteConfig() {
|
|
|
708
681
|
writeFileSync(path, "", { mode: 384 });
|
|
709
682
|
}
|
|
710
683
|
}
|
|
711
|
-
var HARNESS_RUNTIMES, realHomeCache, localAgentConfigSchema, agentsConfigSchema, companionConfigSchema, legacyOfferSchema;
|
|
684
|
+
var HARNESS_RUNTIMES, realHomeCache, localAgentConfigSchema, agentsConfigSchema, companionConfigSchema, RETIRED_ROW_KEYS, RETIRED_HOOK_KEYS, legacyOfferSchema;
|
|
712
685
|
var init_config = __esm({
|
|
713
686
|
"src/config.ts"() {
|
|
714
687
|
"use strict";
|
|
@@ -753,8 +726,7 @@ var init_config = __esm({
|
|
|
753
726
|
// The DEVICE-LEVEL prepare hook, for every agent assigned here unless its own
|
|
754
727
|
// `agents` row names one. Per-agent hooks alone make the config a second
|
|
755
728
|
// silent roster: an agent nobody listed gets no hook and no error, and starts
|
|
756
|
-
// in the operator's `$HOME
|
|
757
|
-
// 2026-08-17).
|
|
729
|
+
// in the operator's `$HOME`.
|
|
758
730
|
prepareHook: prepareHookSchema.optional(),
|
|
759
731
|
// `dashboardPort` and `autoOpen` are IGNORED by the CLI (no port, no browser)
|
|
760
732
|
// but stay in the schema so an existing file still parses; the Electron shell
|
|
@@ -791,13 +763,16 @@ var init_config = __esm({
|
|
|
791
763
|
root: z2.string().min(1).refine(isRootPath, { message: "must be an absolute path, or start with ~/" }).optional(),
|
|
792
764
|
// An argv prefix the operator puts in front of every turn's runner: the companion starts
|
|
793
765
|
// `<prefix…> <its own run-turn command…>` instead of the runner bare. What the prefix does
|
|
794
|
-
// is the operator's (a
|
|
766
|
+
// is the operator's (a container or VM per turn, say); the companion knows nothing of it
|
|
795
767
|
// beyond handing the runner over, with the turn's inputs in its environment and the
|
|
796
|
-
// credential and secrets on its stdin (`runner/launch.ts`, `
|
|
768
|
+
// credential and secrets on its stdin (`runner/launch.ts`, `wrapped`). The README lists
|
|
769
|
+
// what a wrapper can rely on.
|
|
797
770
|
runner: z2.object({
|
|
798
771
|
command: z2.array(z2.string().min(1)).min(1)
|
|
799
772
|
}).strict().optional()
|
|
800
773
|
});
|
|
774
|
+
RETIRED_ROW_KEYS = ["sandbox"];
|
|
775
|
+
RETIRED_HOOK_KEYS = ["validateCached", "failureReporter"];
|
|
801
776
|
legacyOfferSchema = z2.object({
|
|
802
777
|
claudeCode: z2.object({ enabled: z2.boolean().optional() }).passthrough().optional(),
|
|
803
778
|
codex: z2.object({ enabled: z2.boolean().optional() }).passthrough().optional(),
|
|
@@ -1395,6 +1370,18 @@ var init_boot_id = __esm({
|
|
|
1395
1370
|
function activeRunOutboxKey(conversationId, agentId) {
|
|
1396
1371
|
return `activerun:${conversationId}:${agentId}`;
|
|
1397
1372
|
}
|
|
1373
|
+
function isAlreadyPublished(err) {
|
|
1374
|
+
return err instanceof ApiError && err.status === 409 && apiErrorCode(err) === "already_published";
|
|
1375
|
+
}
|
|
1376
|
+
function trailOf(body) {
|
|
1377
|
+
const b = body;
|
|
1378
|
+
return {
|
|
1379
|
+
turnId: b.turnId,
|
|
1380
|
+
text: b.body,
|
|
1381
|
+
...b.seq !== void 0 ? { seq: b.seq } : {},
|
|
1382
|
+
...b.executionSourceId !== void 0 ? { executionSourceId: b.executionSourceId } : {}
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1398
1385
|
function isRetryable(err) {
|
|
1399
1386
|
if (err instanceof ApiError) return err.status >= 500;
|
|
1400
1387
|
if (isAbortError(err)) return false;
|
|
@@ -1522,7 +1509,7 @@ var init_api = __esm({
|
|
|
1522
1509
|
// text use (turnId, seq) within each kind for nonzero seq; tool activity
|
|
1523
1510
|
// uses (turnId, toolUseId). Legacy zero/missing seq bypasses row deduplication.
|
|
1524
1511
|
// Server fences and the outbox's discard/eviction bounds still apply.
|
|
1525
|
-
async durableCommit(kind, path, body, turnId, seq, signal) {
|
|
1512
|
+
async durableCommit(kind, path, body, turnId, seq, signal, trailPath) {
|
|
1526
1513
|
try {
|
|
1527
1514
|
await this.request("POST", path, body, {
|
|
1528
1515
|
retry: true,
|
|
@@ -1533,7 +1520,16 @@ var init_api = __esm({
|
|
|
1533
1520
|
if (!outbox) throw err;
|
|
1534
1521
|
if (signal?.aborted || isAbortError(err)) throw err;
|
|
1535
1522
|
if (!isRetryable(err)) throw err;
|
|
1536
|
-
outbox.persist({
|
|
1523
|
+
outbox.persist({
|
|
1524
|
+
enqueuedAt: Date.now(),
|
|
1525
|
+
turnId,
|
|
1526
|
+
seq,
|
|
1527
|
+
method: "POST",
|
|
1528
|
+
path,
|
|
1529
|
+
body,
|
|
1530
|
+
kind,
|
|
1531
|
+
...trailPath ? { trailPath } : {}
|
|
1532
|
+
});
|
|
1537
1533
|
this.opts.log?.debug(
|
|
1538
1534
|
{ kind, turnId, seq, err: err instanceof Error ? err.message : String(err) },
|
|
1539
1535
|
"companion: commit queued to outbox after transient failure (will drain when the API returns)"
|
|
@@ -1558,6 +1554,29 @@ var init_api = __esm({
|
|
|
1558
1554
|
progressed = true;
|
|
1559
1555
|
} catch (err) {
|
|
1560
1556
|
if (err instanceof ApiError && err.status >= 400 && err.status < 500) {
|
|
1557
|
+
if (entry.kind === "message" && entry.trailPath && isAlreadyPublished(err)) {
|
|
1558
|
+
try {
|
|
1559
|
+
await this.request("POST", entry.trailPath, trailOf(entry.body), {
|
|
1560
|
+
retry: true
|
|
1561
|
+
});
|
|
1562
|
+
} catch (trailErr) {
|
|
1563
|
+
if (!(trailErr instanceof ApiError && trailErr.status >= 400 && trailErr.status < 500)) {
|
|
1564
|
+
break;
|
|
1565
|
+
}
|
|
1566
|
+
this.opts.log?.warn(
|
|
1567
|
+
{
|
|
1568
|
+
kind: entry.kind,
|
|
1569
|
+
turnId: entry.turnId,
|
|
1570
|
+
seq: entry.seq,
|
|
1571
|
+
...apiErrorLogFields(trailErr)
|
|
1572
|
+
},
|
|
1573
|
+
`Cabane rejected a postscript; it could not be filed. ${mayNeedUpdate()}`
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
outbox.remove(entry.turnId, entry.seq);
|
|
1577
|
+
progressed = true;
|
|
1578
|
+
continue;
|
|
1579
|
+
}
|
|
1561
1580
|
if (entry.kind === "active-run" && isTurnEndedRefusal(err)) {
|
|
1562
1581
|
this.opts.log?.debug(
|
|
1563
1582
|
{ kind: entry.kind, turnId: entry.turnId, ...apiErrorLogFields(err) },
|
|
@@ -1633,6 +1652,19 @@ var init_api = __esm({
|
|
|
1633
1652
|
(e) => e.turnId === turnId && e.kind === "message" && e.body?.kind !== "runtime_notice"
|
|
1634
1653
|
);
|
|
1635
1654
|
}
|
|
1655
|
+
// The `seq` of this turn's SELECTED ANSWER still waiting in the outbox — the
|
|
1656
|
+
// `declared` commit the server's `answerArmed` cannot see — or null when none
|
|
1657
|
+
// is queued. A resumed run that re-armed over it would let a newer text
|
|
1658
|
+
// consume the arm first; with the selection queued, the resumed run starts
|
|
1659
|
+
// declared, its texts are trail, and the drain delivers the selection.
|
|
1660
|
+
outboxDeclaredSeqForTurn(turnId) {
|
|
1661
|
+
const outbox = this.opts.outbox;
|
|
1662
|
+
if (!outbox) return null;
|
|
1663
|
+
const queued = outbox.list().find(
|
|
1664
|
+
(e) => e.turnId === turnId && e.kind === "message" && e.body?.declared === true
|
|
1665
|
+
);
|
|
1666
|
+
return queued ? queued.seq : null;
|
|
1667
|
+
}
|
|
1636
1668
|
// ADOPT a turn context's issuance as the execution source of its work, once,
|
|
1637
1669
|
// before preparation emits or queues anything, so every row can name a source
|
|
1638
1670
|
// the server durably recorded and will never reap. Adoption is idempotent, so
|
|
@@ -1806,15 +1838,36 @@ var init_api = __esm({
|
|
|
1806
1838
|
// seq also orders speech against the turn's tool/thinking/text trail.
|
|
1807
1839
|
// Transport/5xx failures get bounded retries and optional outbox delivery;
|
|
1808
1840
|
// terminal 4xx and cancellation stop delivery rather than proving no commit.
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1841
|
+
//
|
|
1842
|
+
// A turn's answer can be out before a later speech commit arrives — the
|
|
1843
|
+
// declaration published it, or another host's selection drained first — and
|
|
1844
|
+
// the server refuses the commit `409 already_published`. The text is a
|
|
1845
|
+
// postscript then: with `trail` (the committing agent, which names the text
|
|
1846
|
+
// route), it is filed as trail at the same seq, live here and from the outbox
|
|
1847
|
+
// on a drain. Without `trail` (a runtime notice) the refusal is terminal.
|
|
1848
|
+
async postTurnMessage(workspaceId, conversationId, body, signal, trail) {
|
|
1849
|
+
const trailPath = trail ? `/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${trail.agentId}/text` : void 0;
|
|
1850
|
+
try {
|
|
1851
|
+
await this.durableCommit(
|
|
1852
|
+
"message",
|
|
1853
|
+
`/api/workspaces/${workspaceId}/conversations/${conversationId}/messages`,
|
|
1854
|
+
body,
|
|
1855
|
+
body.turnId,
|
|
1856
|
+
body.seq ?? 0,
|
|
1857
|
+
signal,
|
|
1858
|
+
trailPath
|
|
1859
|
+
);
|
|
1860
|
+
} catch (err) {
|
|
1861
|
+
if (!trailPath || !isAlreadyPublished(err)) throw err;
|
|
1862
|
+
await this.durableCommit(
|
|
1863
|
+
"text",
|
|
1864
|
+
trailPath,
|
|
1865
|
+
trailOf(body),
|
|
1866
|
+
body.turnId,
|
|
1867
|
+
body.seq ?? 0,
|
|
1868
|
+
signal
|
|
1869
|
+
);
|
|
1870
|
+
}
|
|
1818
1871
|
}
|
|
1819
1872
|
// Report a tool transition (start/done/error). The server checks span
|
|
1820
1873
|
// ownership, upserts by (turnId, toolUseId), and publishes agent_activity SSE.
|
|
@@ -2017,6 +2070,8 @@ var init_turn_diagnostics = __esm({
|
|
|
2017
2070
|
turnSessionModes = ["fresh", "resumed", "degraded"];
|
|
2018
2071
|
turnRuntimeResultKinds = ["success", "error", "no_terminal"];
|
|
2019
2072
|
turnFinalSources = [
|
|
2073
|
+
// The agent declared its answer with the `answer` turn act (CT1973).
|
|
2074
|
+
"declared",
|
|
2020
2075
|
"runtime_text",
|
|
2021
2076
|
"progress_promotion",
|
|
2022
2077
|
"host_fallback",
|
|
@@ -2495,39 +2550,145 @@ var init_transcript_sink = __esm({
|
|
|
2495
2550
|
}
|
|
2496
2551
|
});
|
|
2497
2552
|
|
|
2553
|
+
// packages/agent-runtime/src/turn-control/descriptions.ts
|
|
2554
|
+
var MAX_ASK_QUESTION_BYTES, MAX_ASK_HEADLINE_BYTES, MAX_ASK_ITEMS, MAX_ASK_OPTION_BYTES, ASK_OPTION_NOT_A_STRING, MIN_ASK_OPTIONS, MAX_ASK_OPTIONS, ASK_TARGET_PATTERN, ASK_TARGET_INVALID, MAX_WAKE_NOTE_BYTES, MAX_WITHDRAW_REASON_BYTES, MAX_SKIP_REASON_BYTES, TURN_CONTROL_MCP_SERVER, TURN_CONTROL_TOOLS, TURN_CONTROL_TOOL_DESCRIPTIONS, TURN_CONTROL_FIELD_DESCRIPTIONS;
|
|
2555
|
+
var init_descriptions = __esm({
|
|
2556
|
+
"packages/agent-runtime/src/turn-control/descriptions.ts"() {
|
|
2557
|
+
"use strict";
|
|
2558
|
+
MAX_ASK_QUESTION_BYTES = 400;
|
|
2559
|
+
MAX_ASK_HEADLINE_BYTES = 120;
|
|
2560
|
+
MAX_ASK_ITEMS = 20;
|
|
2561
|
+
MAX_ASK_OPTION_BYTES = 200;
|
|
2562
|
+
ASK_OPTION_NOT_A_STRING = "An ask option is a plain string label.";
|
|
2563
|
+
MIN_ASK_OPTIONS = 2;
|
|
2564
|
+
MAX_ASK_OPTIONS = 4;
|
|
2565
|
+
ASK_TARGET_PATTERN = /^(?:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|@?[A-Za-z0-9-]{2,32})$/;
|
|
2566
|
+
ASK_TARGET_INVALID = "An ask target is a user id or an @handle.";
|
|
2567
|
+
MAX_WAKE_NOTE_BYTES = 2e3;
|
|
2568
|
+
MAX_WITHDRAW_REASON_BYTES = 400;
|
|
2569
|
+
MAX_SKIP_REASON_BYTES = 500;
|
|
2570
|
+
TURN_CONTROL_MCP_SERVER = "cabane_companion";
|
|
2571
|
+
TURN_CONTROL_TOOLS = [
|
|
2572
|
+
"answer",
|
|
2573
|
+
"ask",
|
|
2574
|
+
"cancel_wake",
|
|
2575
|
+
"skip_turn",
|
|
2576
|
+
"wake_me",
|
|
2577
|
+
"withdraw_ask"
|
|
2578
|
+
];
|
|
2579
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS = {
|
|
2580
|
+
answer: `Call this just before you write your answer: the text you write next is what the conversation keeps as your message. Call it once.`,
|
|
2581
|
+
ask: `Ask a person a structured question you need answered to continue, then end your turn; don't wait for the reply. \`targetUserId\` is a person on your roster, by their \`@handle\`; to bring in an agent, name them with \`@handle\` in your reply.
|
|
2582
|
+
- Form: one question (\`headline\`, optional \`question\` body, \`options\`) or \`questions\`, 1\u2013${MAX_ASK_ITEMS} \`{ headline, body?, options? }\`; never both. Decisions that arise together go in ONE call, in the order you want them answered. A second \`ask\` is a separate item, never an edit; raise one only for a genuinely new question.
|
|
2583
|
+
- \`headline\`: one sentence ending in \`?\`. Body (\`question\` or each item's \`body\`): a sentence or two of framing, not a report; inline Markdown only, no lists or headings. \`options\`: ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} one-click answers (always for a yes/no go-ahead), each a distinct course of action you can carry out without asking back.
|
|
2584
|
+
- Voice, in headline, body and options: "I" for yourself, never your own name; name everyone else who decides or acts, as on your roster. Never "you", "your", "we", "us", "our" or their contractions, nor "he", "she" or "they" for a named actor. Each option reads correctly alone. A quotation stays accurate: say who said it.
|
|
2585
|
+
- It must stand alone, since it can arrive first: the reply carries the findings, the ask the question; don't repeat the reply in it.
|
|
2586
|
+
- Recorded when you call this; delivered when your turn ends, or after 3 minutes if it is still running. Ask as soon as you know. The answer reaches you on a later turn.
|
|
2587
|
+
- The person sends one response; unanswered questions end as Unanswered. A person can dismiss an ask; you aren't told and nothing wakes you. Check an ask's status before raising it again.
|
|
2588
|
+
- To replace a stale ask (yours or anyone's here), pass \`replaces: { askId, reason }\`: both land in one write and the person sees the reason. To withdraw without a replacement, use \`withdraw_ask\`.`,
|
|
2589
|
+
cancel_wake: `Stand down the wake you have armed in this conversation, now, when you call it. No arguments. Use it when what the wake was to check has already happened or the work it watched is over; an unneeded wake fires into a turn with nothing to do. Your turn context prints the armed wake ("Wake armed: \u2026"); one no longer printed has fired or been dismissed. Nothing armed is a clean no-op. Calls act in order: \`cancel_wake\` then \`wake_me\` leaves the new wake armed. It reaches only your own wake here, never a peer's and never a recurring schedule (\`schedules.*\`).`,
|
|
2590
|
+
skip_turn: `End your current turn WITHOUT posting a reply. Call this when you've been woken to read a message that genuinely doesn't need a response from you \u2014 a thanks/aside, a question already answered, chatter outside your lane, or something another participant clearly owns. Your turn ends silently: no message bubble is posted. The \`reason\` is a short free-text note for telemetry (e.g. "already answered by cabane", "thanks, nothing to add"). Prefer this over posting a low-value "ok!"/"got it" reply. Don't also write a reply when you skip \u2014 skipping IS the whole turn.`,
|
|
2591
|
+
wake_me: `Wake yourself later: end this turn now and be re-dispatched at a time you pick, with a note to yourself. Use it to wait for a condition you can name (a PR merging, a session limit resetting, an answer landing): arm, end the turn, and you're woken to CHECK and either act or re-arm. Most wakes are short, seconds to a couple of hours. Reach past a few hours only when a person asked for that timing or the wait is pinned to a real external event (a report that runs Mondays); if no one asked and you can't name both what clears the wait and why it takes that long, don't arm it: finish now, or raise an \`ask\`. Pass EXACTLY ONE of \`afterSeconds\` (\`300\` for five minutes) or \`at\` (an ISO-8601 timestamp WITH a zone, e.g. \`2026-07-16T09:00:00-07:00\`, which YOU compute; nothing parses natural-language time). \`note\` becomes the body of the message that re-dispatches you: write the condition to re-check. The wake arms NOW, when you call this; a later \`wake_me\` in the same turn replaces it, \`cancel_wake\` stands it down, and one that comes due mid-turn is delivered after your reply. This is the ONLY way to schedule your own continuation; never a host cron or scheduler. Floor 60s, horizon 14 days, enforced here so you can recompute. Widen the interval as a loop ages (5m \u2192 15m \u2192 1h\u2026); after many re-arms with no other activity you'll be steered to an \`ask\` instead.`,
|
|
2592
|
+
withdraw_ask: `Withdraw an open ask \u2014 yours or anyone's in this conversation \u2014 because the question is moot, was wrong, or has been overtaken. \`reason\` is required and is shown to the person and, if it wasn't your ask, to the agent who asked it. If there is a current question to put in its place, don't call this \u2014 call \`ask\` with \`replaces\` so the two land as one and the person never sees neither or both. Never withdraw to hurry an answer; to act on a deadline, arm a \`wake_me\` and decide then.`
|
|
2593
|
+
};
|
|
2594
|
+
TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
2595
|
+
answer: {},
|
|
2596
|
+
ask: {
|
|
2597
|
+
targetUserId: "The person to ask: their `@handle` from your turn context's roster (preferred), or their user id.",
|
|
2598
|
+
question: `SINGLE-question form: a short body \u2014 one or two sentences of framing the headline can't hold. NOT a report (up to ${MAX_ASK_QUESTION_BYTES} characters, inline markdown only). Provide EITHER this or \`questions\`, not both. Put the crisp one-sentence question in \`headline\`.`,
|
|
2599
|
+
headline: `SINGLE-question form: the question itself as ONE clear, capitalized sentence ending in \`?\` ("Should Ledger deploy once Mia approves?"), up to ${MAX_ASK_HEADLINE_BYTES} characters. What the person reads first in For you and on the chip \u2014 one scannable question, no elaboration (that goes in \`question\`). Strongly encouraged.`,
|
|
2600
|
+
options: `SINGLE-question form: optional ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} suggested one-click answers, up to ${MAX_ASK_OPTION_BYTES} characters each. Each option must make sense on its own.`,
|
|
2601
|
+
questions: `MULTI-question form: 1\u2013${MAX_ASK_ITEMS} questions to ask at once, when a plan ends with several bounded decisions. Provide EITHER this or \`question\`/\`headline\`/\`options\`, not both.`,
|
|
2602
|
+
"questions.headline": `The one-sentence question ("Should Ledger deploy once Mia approves?") \u2014 required for each item, up to ${MAX_ASK_HEADLINE_BYTES} characters.`,
|
|
2603
|
+
"questions.body": `Optional short framing beneath the headline, up to ${MAX_ASK_QUESTION_BYTES} characters, inline markdown only. NOT a report.`,
|
|
2604
|
+
"questions.options": `Optional ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} one-click answers for this question, up to ${MAX_ASK_OPTION_BYTES} characters each. Each option must make sense on its own.`,
|
|
2605
|
+
replaces: "Withdraw a stale ask in this conversation as part of raising this one. Both land in a single write, so the person goes from the old question to the new one and never sees neither or both.",
|
|
2606
|
+
"replaces.askId": "The id of the open ask to withdraw.",
|
|
2607
|
+
"replaces.reason": `Why the old question is no longer the question \u2014 shown to the person on their Done row. Up to ${MAX_WITHDRAW_REASON_BYTES} characters.`
|
|
2608
|
+
},
|
|
2609
|
+
cancel_wake: {},
|
|
2610
|
+
skip_turn: {
|
|
2611
|
+
reason: `Short reason you are declining \u2014 used for telemetry/debugging, never posted. Up to ${MAX_SKIP_REASON_BYTES} characters.`
|
|
2612
|
+
},
|
|
2613
|
+
wake_me: {
|
|
2614
|
+
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.",
|
|
2615
|
+
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.",
|
|
2616
|
+
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.`
|
|
2617
|
+
},
|
|
2618
|
+
withdraw_ask: {
|
|
2619
|
+
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.",
|
|
2620
|
+
reason: `Why this question is no longer the question ("merged via GitHub #1551"). Required, and shown to the person on their Done row \u2014 a bare "withdrawn" tells them nothing. Up to ${MAX_WITHDRAW_REASON_BYTES} characters.`
|
|
2621
|
+
}
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
});
|
|
2625
|
+
|
|
2498
2626
|
// packages/agent-runtime/src/pump.ts
|
|
2499
|
-
|
|
2627
|
+
function isTurnControlAnswer(evt) {
|
|
2628
|
+
if (evt.mcpServer !== TURN_CONTROL_MCP_SERVER) return false;
|
|
2629
|
+
const name = evt.toolName;
|
|
2630
|
+
return name === ANSWER_TOOL || name === `${TURN_CONTROL_MCP_SERVER}.${ANSWER_TOOL}` || name === `${TURN_CONTROL_MCP_SERVER}_${ANSWER_TOOL}`;
|
|
2631
|
+
}
|
|
2632
|
+
var ANSWER_TOOL, THINKING_TEXT_MAX_CHARS, TurnPump;
|
|
2500
2633
|
var init_pump = __esm({
|
|
2501
2634
|
"packages/agent-runtime/src/pump.ts"() {
|
|
2502
2635
|
"use strict";
|
|
2503
2636
|
init_empty_result();
|
|
2504
2637
|
init_transcript();
|
|
2638
|
+
init_descriptions();
|
|
2639
|
+
ANSWER_TOOL = "answer";
|
|
2505
2640
|
THINKING_TEXT_MAX_CHARS = 3900;
|
|
2506
2641
|
TurnPump = class {
|
|
2507
2642
|
constructor(opts) {
|
|
2508
2643
|
this.opts = opts;
|
|
2509
2644
|
this.hasSpoken = opts.spoke ?? false;
|
|
2645
|
+
this.armed = opts.armed ?? false;
|
|
2646
|
+
if (opts.declaredSeq !== void 0) {
|
|
2647
|
+
this.hasSpoken = true;
|
|
2648
|
+
this.armed = false;
|
|
2649
|
+
this.declared = true;
|
|
2650
|
+
this.emittedFinal = true;
|
|
2651
|
+
this.finalSeq = opts.declaredSeq;
|
|
2652
|
+
this.emittedFinalSource = "declared";
|
|
2653
|
+
}
|
|
2510
2654
|
this.sink = {
|
|
2511
2655
|
onAssistantText: async (evt) => {
|
|
2512
2656
|
if (opts.signal.aborted) return;
|
|
2513
2657
|
const body = normalizeCommitText(evt.text);
|
|
2514
2658
|
if (!body) return;
|
|
2515
2659
|
const seq = opts.nextSeq();
|
|
2516
|
-
if (
|
|
2660
|
+
if (this.armed) {
|
|
2661
|
+
await opts.commit.commitMessage({ body, kind: "message", seq, declared: true });
|
|
2662
|
+
this.armed = false;
|
|
2663
|
+
this.declared = true;
|
|
2664
|
+
this.hasSpoken = true;
|
|
2665
|
+
this.emittedFinal = true;
|
|
2666
|
+
this.finalSeq = seq;
|
|
2667
|
+
this.lastTextSeq = seq;
|
|
2668
|
+
this.finalReplyBody = body;
|
|
2669
|
+
this.emittedFinalSource = "declared";
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
if (this.declared || !evt.final && this.hasSpoken) {
|
|
2517
2673
|
await opts.commit.reportText({ text: body, seq });
|
|
2674
|
+
this.lastTextSeq = seq;
|
|
2518
2675
|
return;
|
|
2519
2676
|
}
|
|
2520
2677
|
await opts.commit.commitMessage({ body, kind: "message", seq });
|
|
2521
2678
|
this.hasSpoken = true;
|
|
2679
|
+
this.lastTextSeq = seq;
|
|
2522
2680
|
if (evt.final) {
|
|
2523
2681
|
this.emittedFinal = true;
|
|
2524
|
-
this.
|
|
2682
|
+
this.finalSeq = seq;
|
|
2525
2683
|
this.finalReplyBody = body;
|
|
2526
2684
|
this.emittedFinalSource = "runtime_text";
|
|
2527
2685
|
}
|
|
2528
2686
|
},
|
|
2529
2687
|
onToolActivity: async (evt) => {
|
|
2530
2688
|
if (opts.signal.aborted) return;
|
|
2689
|
+
if (evt.phase === "done" && !this.declared && isTurnControlAnswer(evt)) {
|
|
2690
|
+
this.armed = true;
|
|
2691
|
+
}
|
|
2531
2692
|
let call = this.toolCallByProviderId.get(evt.toolUseId);
|
|
2532
2693
|
if (evt.phase === "start" || call === void 0) {
|
|
2533
2694
|
call = { persistedId: crypto.randomUUID(), seq: opts.nextSeq() };
|
|
@@ -2568,9 +2729,16 @@ var init_pump = __esm({
|
|
|
2568
2729
|
// A message has landed under this span — its first words. Every later
|
|
2569
2730
|
// non-terminal text is the trail. Seeded from the host on a resume.
|
|
2570
2731
|
hasSpoken;
|
|
2571
|
-
//
|
|
2732
|
+
// An accepted `answer` act this turn has not yet written the text for.
|
|
2733
|
+
armed;
|
|
2734
|
+
// The answer committed with `declared: true`; every text after it is trail.
|
|
2735
|
+
declared = false;
|
|
2736
|
+
// The answer landed — declared, or the adapter's terminal text — and its `seq`.
|
|
2572
2737
|
emittedFinal = false;
|
|
2573
|
-
|
|
2738
|
+
finalSeq = null;
|
|
2739
|
+
// The `seq` of the turn's last text through either door. Undeclared, this is
|
|
2740
|
+
// what the settle names: the server makes that text the body at the end.
|
|
2741
|
+
lastTextSeq = null;
|
|
2574
2742
|
// The turn's final reply text, captured for the dashboard feed / logging.
|
|
2575
2743
|
finalReplyBody = null;
|
|
2576
2744
|
emittedFinalSource = "none";
|
|
@@ -2592,14 +2760,20 @@ var init_pump = __esm({
|
|
|
2592
2760
|
get spoke() {
|
|
2593
2761
|
return this.hasSpoken;
|
|
2594
2762
|
}
|
|
2595
|
-
// Whether the turn has committed its
|
|
2763
|
+
// Whether the turn has committed its answer: declared, or the adapter's
|
|
2764
|
+
// terminal text.
|
|
2596
2765
|
get finalEmitted() {
|
|
2597
2766
|
return this.emittedFinal;
|
|
2598
2767
|
}
|
|
2599
|
-
//
|
|
2600
|
-
//
|
|
2768
|
+
// What the settle names, by `seq`: the declared answer, or — undeclared —
|
|
2769
|
+
// the turn's last text through either door, which the server makes the body
|
|
2770
|
+
// at the end. Null when the turn wrote no text at all.
|
|
2601
2771
|
get lastMessageSeq() {
|
|
2602
|
-
return this.
|
|
2772
|
+
return this.declared ? this.finalSeq : this.lastTextSeq;
|
|
2773
|
+
}
|
|
2774
|
+
// Whether an accepted `answer` act is still waiting for its text.
|
|
2775
|
+
get answerArmed() {
|
|
2776
|
+
return this.armed;
|
|
2603
2777
|
}
|
|
2604
2778
|
// The turn's final reply text, or null if none landed (failure / cancel /
|
|
2605
2779
|
// empty). Used for the dashboard feed's reply preview.
|
|
@@ -2613,75 +2787,6 @@ var init_pump = __esm({
|
|
|
2613
2787
|
}
|
|
2614
2788
|
});
|
|
2615
2789
|
|
|
2616
|
-
// packages/agent-runtime/src/turn-control/descriptions.ts
|
|
2617
|
-
var MAX_ASK_QUESTION_BYTES, MAX_ASK_HEADLINE_BYTES, MAX_ASK_ITEMS, MAX_ASK_OPTION_BYTES, ASK_OPTION_NOT_A_STRING, MIN_ASK_OPTIONS, MAX_ASK_OPTIONS, ASK_TARGET_PATTERN, ASK_TARGET_INVALID, MAX_WAKE_NOTE_BYTES, MAX_WITHDRAW_REASON_BYTES, MAX_SKIP_REASON_BYTES, TURN_CONTROL_TOOLS, TURN_CONTROL_TOOL_DESCRIPTIONS, TURN_CONTROL_FIELD_DESCRIPTIONS;
|
|
2618
|
-
var init_descriptions = __esm({
|
|
2619
|
-
"packages/agent-runtime/src/turn-control/descriptions.ts"() {
|
|
2620
|
-
"use strict";
|
|
2621
|
-
MAX_ASK_QUESTION_BYTES = 400;
|
|
2622
|
-
MAX_ASK_HEADLINE_BYTES = 120;
|
|
2623
|
-
MAX_ASK_ITEMS = 20;
|
|
2624
|
-
MAX_ASK_OPTION_BYTES = 200;
|
|
2625
|
-
ASK_OPTION_NOT_A_STRING = "An ask option is a plain string label.";
|
|
2626
|
-
MIN_ASK_OPTIONS = 2;
|
|
2627
|
-
MAX_ASK_OPTIONS = 4;
|
|
2628
|
-
ASK_TARGET_PATTERN = /^(?:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|@?[A-Za-z0-9-]{2,32})$/;
|
|
2629
|
-
ASK_TARGET_INVALID = "An ask target is a user id or an @handle.";
|
|
2630
|
-
MAX_WAKE_NOTE_BYTES = 2e3;
|
|
2631
|
-
MAX_WITHDRAW_REASON_BYTES = 400;
|
|
2632
|
-
MAX_SKIP_REASON_BYTES = 500;
|
|
2633
|
-
TURN_CONTROL_TOOLS = [
|
|
2634
|
-
"ask",
|
|
2635
|
-
"cancel_wake",
|
|
2636
|
-
"skip_turn",
|
|
2637
|
-
"wake_me",
|
|
2638
|
-
"withdraw_ask"
|
|
2639
|
-
];
|
|
2640
|
-
TURN_CONTROL_TOOL_DESCRIPTIONS = {
|
|
2641
|
-
ask: `Ask a person a structured question you need answered to continue, then end your turn; don't wait for the reply. \`targetUserId\` is a person on your roster, by their \`@handle\`; to bring in an agent, name them with \`@handle\` in your reply.
|
|
2642
|
-
- Form: one question (\`headline\`, optional \`question\` body, \`options\`) or \`questions\`, 1\u2013${MAX_ASK_ITEMS} \`{ headline, body?, options? }\`; never both. Decisions that arise together go in ONE call, in the order you want them answered. A second \`ask\` is a separate item, never an edit; raise one only for a genuinely new question.
|
|
2643
|
-
- \`headline\`: one sentence ending in \`?\`. Body (\`question\` or each item's \`body\`): a sentence or two of framing, not a report; inline Markdown only, no lists or headings. \`options\`: ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} one-click answers (always for a yes/no go-ahead), each a distinct course of action you can carry out without asking back.
|
|
2644
|
-
- Voice, in headline, body and options: "I" for yourself, never your own name; name everyone else who decides or acts, as on your roster. Never "you", "your", "we", "us", "our" or their contractions, nor "he", "she" or "they" for a named actor. Each option reads correctly alone. A quotation stays accurate: say who said it.
|
|
2645
|
-
- It must stand alone, since it can arrive first: the reply carries the findings, the ask the question; don't repeat the reply in it.
|
|
2646
|
-
- Recorded when you call this; delivered when your turn ends, or after 3 minutes if it is still running. Ask as soon as you know. The answer reaches you on a later turn.
|
|
2647
|
-
- The person sends one response; unanswered questions end as Unanswered. A person can dismiss an ask; you aren't told and nothing wakes you. Check an ask's status before raising it again.
|
|
2648
|
-
- To replace a stale ask (yours or anyone's here), pass \`replaces: { askId, reason }\`: both land in one write and the person sees the reason. To withdraw without a replacement, use \`withdraw_ask\`.`,
|
|
2649
|
-
cancel_wake: `Stand down the wake you have armed in this conversation, now, when you call it. No arguments. Use it when what the wake was to check has already happened or the work it watched is over; an unneeded wake fires into a turn with nothing to do. Your turn context prints the armed wake ("Wake armed: \u2026"); one no longer printed has fired or been dismissed. Nothing armed is a clean no-op. Calls act in order: \`cancel_wake\` then \`wake_me\` leaves the new wake armed. It reaches only your own wake here, never a peer's and never a recurring schedule (\`schedules.*\`).`,
|
|
2650
|
-
skip_turn: `End your current turn WITHOUT posting a reply. Call this when you've been woken to read a message that genuinely doesn't need a response from you \u2014 a thanks/aside, a question already answered, chatter outside your lane, or something another participant clearly owns. Your turn ends silently: no message bubble is posted. The \`reason\` is a short free-text note for telemetry (e.g. "already answered by cabane", "thanks, nothing to add"). Prefer this over posting a low-value "ok!"/"got it" reply. Don't also write a reply when you skip \u2014 skipping IS the whole turn.`,
|
|
2651
|
-
wake_me: `Wake yourself later: end this turn now and be re-dispatched at a time you pick, with a note to yourself. Use it to wait for a condition you can name (a PR merging, a session limit resetting, an answer landing): arm, end the turn, and you're woken to CHECK and either act or re-arm. Most wakes are short, seconds to a couple of hours. Reach past a few hours only when a person asked for that timing or the wait is pinned to a real external event (a report that runs Mondays); if no one asked and you can't name both what clears the wait and why it takes that long, don't arm it: finish now, or raise an \`ask\`. Pass EXACTLY ONE of \`afterSeconds\` (\`300\` for five minutes) or \`at\` (an ISO-8601 timestamp WITH a zone, e.g. \`2026-07-16T09:00:00-07:00\`, which YOU compute; nothing parses natural-language time). \`note\` becomes the body of the message that re-dispatches you: write the condition to re-check. The wake arms NOW, when you call this; a later \`wake_me\` in the same turn replaces it, \`cancel_wake\` stands it down, and one that comes due mid-turn is delivered after your reply. This is the ONLY way to schedule your own continuation; never a host cron or scheduler. Floor 60s, horizon 14 days, enforced here so you can recompute. Widen the interval as a loop ages (5m \u2192 15m \u2192 1h\u2026); after many re-arms with no other activity you'll be steered to an \`ask\` instead.`,
|
|
2652
|
-
withdraw_ask: `Withdraw an open ask \u2014 yours or anyone's in this conversation \u2014 because the question is moot, was wrong, or has been overtaken. \`reason\` is required and is shown to the person and, if it wasn't your ask, to the agent who asked it. If there is a current question to put in its place, don't call this \u2014 call \`ask\` with \`replaces\` so the two land as one and the person never sees neither or both. Never withdraw to hurry an answer; to act on a deadline, arm a \`wake_me\` and decide then.`
|
|
2653
|
-
};
|
|
2654
|
-
TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
2655
|
-
ask: {
|
|
2656
|
-
targetUserId: "The person to ask: their `@handle` from your turn context's roster (preferred), or their user id.",
|
|
2657
|
-
question: `SINGLE-question form: a short body \u2014 one or two sentences of framing the headline can't hold. NOT a report (up to ${MAX_ASK_QUESTION_BYTES} characters, inline markdown only). Provide EITHER this or \`questions\`, not both. Put the crisp one-sentence question in \`headline\`.`,
|
|
2658
|
-
headline: `SINGLE-question form: the question itself as ONE clear, capitalized sentence ending in \`?\` ("Should Ledger deploy once Mia approves?"), up to ${MAX_ASK_HEADLINE_BYTES} characters. What the person reads first in For you and on the chip \u2014 one scannable question, no elaboration (that goes in \`question\`). Strongly encouraged.`,
|
|
2659
|
-
options: `SINGLE-question form: optional ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} suggested one-click answers, up to ${MAX_ASK_OPTION_BYTES} characters each. Each option must make sense on its own.`,
|
|
2660
|
-
questions: `MULTI-question form: 1\u2013${MAX_ASK_ITEMS} questions to ask at once, when a plan ends with several bounded decisions. Provide EITHER this or \`question\`/\`headline\`/\`options\`, not both.`,
|
|
2661
|
-
"questions.headline": `The one-sentence question ("Should Ledger deploy once Mia approves?") \u2014 required for each item, up to ${MAX_ASK_HEADLINE_BYTES} characters.`,
|
|
2662
|
-
"questions.body": `Optional short framing beneath the headline, up to ${MAX_ASK_QUESTION_BYTES} characters, inline markdown only. NOT a report.`,
|
|
2663
|
-
"questions.options": `Optional ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} one-click answers for this question, up to ${MAX_ASK_OPTION_BYTES} characters each. Each option must make sense on its own.`,
|
|
2664
|
-
replaces: "Withdraw a stale ask in this conversation as part of raising this one. Both land in a single write, so the person goes from the old question to the new one and never sees neither or both.",
|
|
2665
|
-
"replaces.askId": "The id of the open ask to withdraw.",
|
|
2666
|
-
"replaces.reason": `Why the old question is no longer the question \u2014 shown to the person on their Done row. Up to ${MAX_WITHDRAW_REASON_BYTES} characters.`
|
|
2667
|
-
},
|
|
2668
|
-
cancel_wake: {},
|
|
2669
|
-
skip_turn: {
|
|
2670
|
-
reason: `Short reason you are declining \u2014 used for telemetry/debugging, never posted. Up to ${MAX_SKIP_REASON_BYTES} characters.`
|
|
2671
|
-
},
|
|
2672
|
-
wake_me: {
|
|
2673
|
-
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.",
|
|
2674
|
-
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.",
|
|
2675
|
-
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.`
|
|
2676
|
-
},
|
|
2677
|
-
withdraw_ask: {
|
|
2678
|
-
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.",
|
|
2679
|
-
reason: `Why this question is no longer the question ("merged via GitHub #1551"). Required, and shown to the person on their Done row \u2014 a bare "withdrawn" tells them nothing. Up to ${MAX_WITHDRAW_REASON_BYTES} characters.`
|
|
2680
|
-
}
|
|
2681
|
-
};
|
|
2682
|
-
}
|
|
2683
|
-
});
|
|
2684
|
-
|
|
2685
2790
|
// packages/agent-runtime/src/turn-control/prompt-guidance.ts
|
|
2686
2791
|
function turnControlPromptGuidance(prefix) {
|
|
2687
2792
|
const controls = TURN_CONTROL_TOOLS.map((name) => `\`${prefix}${name}\` (${MEANINGS[name]})`);
|
|
@@ -2693,6 +2798,7 @@ var init_prompt_guidance = __esm({
|
|
|
2693
2798
|
"use strict";
|
|
2694
2799
|
init_descriptions();
|
|
2695
2800
|
MEANINGS = {
|
|
2801
|
+
answer: "declare the text you write next as your answer",
|
|
2696
2802
|
ask: "put a structured question to a person",
|
|
2697
2803
|
cancel_wake: "cancel your own armed wake in this conversation",
|
|
2698
2804
|
skip_turn: "end without posting a reply",
|
|
@@ -4231,6 +4337,15 @@ var init_events = __esm({
|
|
|
4231
4337
|
});
|
|
4232
4338
|
|
|
4233
4339
|
// packages/agent-runtime/src/opencode/loop.ts
|
|
4340
|
+
function opencodeMcpServerOf(tool2, servers) {
|
|
4341
|
+
let best = null;
|
|
4342
|
+
for (const server of servers ?? []) {
|
|
4343
|
+
if (tool2.length > server.length + 1 && tool2.startsWith(`${server}_`)) {
|
|
4344
|
+
if (best === null || server.length > best.length) best = server;
|
|
4345
|
+
}
|
|
4346
|
+
}
|
|
4347
|
+
return best;
|
|
4348
|
+
}
|
|
4234
4349
|
async function* decodeOpencodeStream(events, ctx2) {
|
|
4235
4350
|
let held = null;
|
|
4236
4351
|
const pending = /* @__PURE__ */ new Map();
|
|
@@ -4257,12 +4372,14 @@ async function* decodeOpencodeStream(events, ctx2) {
|
|
|
4257
4372
|
if (sealed) yield sealed;
|
|
4258
4373
|
const key = part.callID ?? part.id;
|
|
4259
4374
|
const input = part.state.input;
|
|
4375
|
+
const server = opencodeMcpServerOf(part.tool, ctx2.mcpServers);
|
|
4376
|
+
const origin = server ? { mcpServer: server } : {};
|
|
4260
4377
|
if (!startedTools.has(key)) {
|
|
4261
4378
|
const name = prettyToolName(part.tool);
|
|
4262
4379
|
const summary = summarizeToolInput(input);
|
|
4263
4380
|
pending.set(key, { name, summary, input });
|
|
4264
4381
|
startedTools.add(key);
|
|
4265
|
-
yield { type: "tool", id: key, name, phase: "start", summary, input };
|
|
4382
|
+
yield { type: "tool", id: key, name, phase: "start", summary, input, ...origin };
|
|
4266
4383
|
}
|
|
4267
4384
|
const entry = pending.get(key);
|
|
4268
4385
|
if (entry && hasToolInput(input)) {
|
|
@@ -4282,7 +4399,8 @@ async function* decodeOpencodeStream(events, ctx2) {
|
|
|
4282
4399
|
phase,
|
|
4283
4400
|
summary: started?.summary ?? "",
|
|
4284
4401
|
input: started?.input ?? input,
|
|
4285
|
-
result
|
|
4402
|
+
result,
|
|
4403
|
+
...origin
|
|
4286
4404
|
};
|
|
4287
4405
|
}
|
|
4288
4406
|
} else {
|
|
@@ -4480,15 +4598,15 @@ function buildMcp(req) {
|
|
|
4480
4598
|
function isStringRecord(v) {
|
|
4481
4599
|
return !!v && typeof v === "object" && Object.values(v).every((x) => typeof x === "string");
|
|
4482
4600
|
}
|
|
4483
|
-
var CABANE_MCP_SERVER2,
|
|
4601
|
+
var CABANE_MCP_SERVER2, ACTIVE_CONVERSATION_HEADER2;
|
|
4484
4602
|
var init_run_spec = __esm({
|
|
4485
4603
|
"packages/agent-runtime/src/opencode/run-spec.ts"() {
|
|
4486
4604
|
"use strict";
|
|
4605
|
+
init_descriptions();
|
|
4487
4606
|
init_mcp_secret_env();
|
|
4488
4607
|
init_policy2();
|
|
4489
4608
|
init_model2();
|
|
4490
4609
|
CABANE_MCP_SERVER2 = "cabane";
|
|
4491
|
-
TURN_CONTROL_MCP_SERVER = "cabane_companion";
|
|
4492
4610
|
ACTIVE_CONVERSATION_HEADER2 = "x-cabane-active-conversation";
|
|
4493
4611
|
}
|
|
4494
4612
|
});
|
|
@@ -4799,7 +4917,10 @@ function createOpencodeAdapter(deps = {}) {
|
|
|
4799
4917
|
}
|
|
4800
4918
|
yield* decodeOpencodeStream(result.events, {
|
|
4801
4919
|
signal,
|
|
4802
|
-
sessionId: result.sessionId
|
|
4920
|
+
sessionId: result.sessionId,
|
|
4921
|
+
// The keys the turn's servers were installed under: what the decoder
|
|
4922
|
+
// splits a raw `<server>_<tool>` id on.
|
|
4923
|
+
mcpServers: Object.keys(spec.mcp)
|
|
4803
4924
|
});
|
|
4804
4925
|
}
|
|
4805
4926
|
};
|
|
@@ -4961,7 +5082,15 @@ var init_conformance3 = __esm({
|
|
|
4961
5082
|
expected: [
|
|
4962
5083
|
sessionEvent2(NEW_SESSION_ID),
|
|
4963
5084
|
// start: fires off the first (empty) snapshot — empty input + summary, by design.
|
|
4964
|
-
{
|
|
5085
|
+
{
|
|
5086
|
+
type: "tool",
|
|
5087
|
+
id: "call_1",
|
|
5088
|
+
name: "cabane_list",
|
|
5089
|
+
phase: "start",
|
|
5090
|
+
summary: "",
|
|
5091
|
+
input: {},
|
|
5092
|
+
mcpServer: "cabane"
|
|
5093
|
+
},
|
|
4965
5094
|
// done: carries the full args captured from the later snapshot, and the
|
|
4966
5095
|
// summary recomputed from them — NOT the first-seen `{}`.
|
|
4967
5096
|
{
|
|
@@ -4971,8 +5100,60 @@ var init_conformance3 = __esm({
|
|
|
4971
5100
|
phase: "done",
|
|
4972
5101
|
summary: "/product/tasks",
|
|
4973
5102
|
input: { path: "/product/tasks" },
|
|
4974
|
-
result: "CT301.md"
|
|
5103
|
+
result: "CT301.md",
|
|
5104
|
+
mcpServer: "cabane"
|
|
5105
|
+
},
|
|
5106
|
+
{ type: "result", ok: true }
|
|
5107
|
+
]
|
|
5108
|
+
},
|
|
5109
|
+
{
|
|
5110
|
+
// The turn-control server's `answer` (CT1973). opencode names an MCP tool `<server>_<tool>`,
|
|
5111
|
+
// so the installed keys split the raw id: `cabane_companion_answer` is the turn-control
|
|
5112
|
+
// server's, never the workspace server's `companion_answer`. The pump binds on that.
|
|
5113
|
+
name: "turn-control answer is read as the turn-control server\u2019s, not cabane\u2019s",
|
|
5114
|
+
request: makeRequest2({
|
|
5115
|
+
cabane: {
|
|
5116
|
+
mcpUrl: "https://cabane.test/api/mcp",
|
|
5117
|
+
turnControlUrl: "https://cabane.test/api/turn-control",
|
|
5118
|
+
bearer: "tok",
|
|
5119
|
+
activeConversationId: "conv"
|
|
5120
|
+
}
|
|
5121
|
+
}),
|
|
5122
|
+
nativeStream: [
|
|
5123
|
+
textPart("p1", "On it."),
|
|
5124
|
+
toolPart("t1", "cabane_companion_answer", { status: "running", input: {} }, "call_1"),
|
|
5125
|
+
toolPart(
|
|
5126
|
+
"t1",
|
|
5127
|
+
"cabane_companion_answer",
|
|
5128
|
+
{ status: "completed", input: {}, output: '{"ok":true,"kind":"answer"}' },
|
|
5129
|
+
"call_1"
|
|
5130
|
+
),
|
|
5131
|
+
textPart("p2", "The answer."),
|
|
5132
|
+
idle()
|
|
5133
|
+
],
|
|
5134
|
+
expected: [
|
|
5135
|
+
sessionEvent2(NEW_SESSION_ID),
|
|
5136
|
+
{ type: "text", body: "On it.", terminal: false },
|
|
5137
|
+
{
|
|
5138
|
+
type: "tool",
|
|
5139
|
+
id: "call_1",
|
|
5140
|
+
name: "cabane_companion_answer",
|
|
5141
|
+
phase: "start",
|
|
5142
|
+
summary: "",
|
|
5143
|
+
input: {},
|
|
5144
|
+
mcpServer: "cabane_companion"
|
|
5145
|
+
},
|
|
5146
|
+
{
|
|
5147
|
+
type: "tool",
|
|
5148
|
+
id: "call_1",
|
|
5149
|
+
name: "cabane_companion_answer",
|
|
5150
|
+
phase: "done",
|
|
5151
|
+
summary: "",
|
|
5152
|
+
input: {},
|
|
5153
|
+
result: '{"ok":true,"kind":"answer"}',
|
|
5154
|
+
mcpServer: "cabane_companion"
|
|
4975
5155
|
},
|
|
5156
|
+
{ type: "text", body: "The answer.", terminal: true },
|
|
4976
5157
|
{ type: "result", ok: true }
|
|
4977
5158
|
]
|
|
4978
5159
|
},
|
|
@@ -5339,6 +5520,7 @@ function readToolItem(item, eventType) {
|
|
|
5339
5520
|
name: server && server !== "cabane" ? `${server}.${tool2}` : tool2,
|
|
5340
5521
|
input: item.arguments,
|
|
5341
5522
|
status: toStatus(item.status),
|
|
5523
|
+
...server ? { server } : {},
|
|
5342
5524
|
...item.status === "completed" ? { result } : item.status === "failed" ? { result: hasMcpReceipt(result) ? result : readMcpError(item.error) } : {}
|
|
5343
5525
|
};
|
|
5344
5526
|
}
|
|
@@ -5577,11 +5759,20 @@ async function* decodeCodexStream(events, ctx2) {
|
|
|
5577
5759
|
if (!tool2) continue;
|
|
5578
5760
|
yield* flushInterim();
|
|
5579
5761
|
const name = prettyToolName(tool2.name);
|
|
5762
|
+
const origin = tool2.server ? { mcpServer: tool2.server } : {};
|
|
5580
5763
|
if (!startedTools.has(tool2.id)) {
|
|
5581
5764
|
const summary = summarizeToolInput(tool2.input);
|
|
5582
5765
|
pending.set(tool2.id, { name, summary, input: tool2.input });
|
|
5583
5766
|
startedTools.add(tool2.id);
|
|
5584
|
-
yield {
|
|
5767
|
+
yield {
|
|
5768
|
+
type: "tool",
|
|
5769
|
+
id: tool2.id,
|
|
5770
|
+
name,
|
|
5771
|
+
phase: "start",
|
|
5772
|
+
summary,
|
|
5773
|
+
input: tool2.input,
|
|
5774
|
+
...origin
|
|
5775
|
+
};
|
|
5585
5776
|
}
|
|
5586
5777
|
const entry = pending.get(tool2.id);
|
|
5587
5778
|
if (entry && hasToolInput2(tool2.input)) {
|
|
@@ -5599,7 +5790,8 @@ async function* decodeCodexStream(events, ctx2) {
|
|
|
5599
5790
|
phase: tool2.status === "completed" ? "done" : "error",
|
|
5600
5791
|
summary: started?.summary ?? "",
|
|
5601
5792
|
input: started?.input ?? tool2.input,
|
|
5602
|
-
result: tool2.result
|
|
5793
|
+
result: tool2.result,
|
|
5794
|
+
...origin
|
|
5603
5795
|
};
|
|
5604
5796
|
}
|
|
5605
5797
|
continue;
|
|
@@ -5830,7 +6022,7 @@ function buildConfig(req) {
|
|
|
5830
6022
|
default_tools_approval_mode: "approve"
|
|
5831
6023
|
};
|
|
5832
6024
|
if (req.cabane.turnControlUrl) {
|
|
5833
|
-
mcp_servers[
|
|
6025
|
+
mcp_servers[TURN_CONTROL_MCP_SERVER] = {
|
|
5834
6026
|
url: req.cabane.turnControlUrl,
|
|
5835
6027
|
http_headers: {
|
|
5836
6028
|
Authorization: `Bearer ${req.cabane.bearer}`,
|
|
@@ -5855,16 +6047,16 @@ function buildConfig(req) {
|
|
|
5855
6047
|
function isStringRecord2(v) {
|
|
5856
6048
|
return !!v && typeof v === "object" && Object.values(v).every((x) => typeof x === "string");
|
|
5857
6049
|
}
|
|
5858
|
-
var CABANE_MCP_SERVER3,
|
|
6050
|
+
var CABANE_MCP_SERVER3, ACTIVE_CONVERSATION_HEADER3, ENV_ENVELOPE_KEYS, CORE_REPLACES, CONVERSATION_REPLACES;
|
|
5859
6051
|
var init_run_spec2 = __esm({
|
|
5860
6052
|
"packages/agent-runtime/src/codex/run-spec.ts"() {
|
|
5861
6053
|
"use strict";
|
|
6054
|
+
init_descriptions();
|
|
5862
6055
|
init_standing();
|
|
5863
6056
|
init_policy3();
|
|
5864
6057
|
init_model3();
|
|
5865
6058
|
init_session3();
|
|
5866
6059
|
CABANE_MCP_SERVER3 = "cabane";
|
|
5867
|
-
TURN_CONTROL_MCP_SERVER2 = "cabane_companion";
|
|
5868
6060
|
ACTIVE_CONVERSATION_HEADER3 = "x-cabane-active-conversation";
|
|
5869
6061
|
ENV_ENVELOPE_KEYS = [
|
|
5870
6062
|
"CABANE_CONVERSATION_ID",
|
|
@@ -6038,7 +6230,7 @@ function createCodexAdapter(deps = {}) {
|
|
|
6038
6230
|
if (signal.aborted) return;
|
|
6039
6231
|
if (compactions === null) {
|
|
6040
6232
|
deps.onWarn?.(
|
|
6041
|
-
"codex adapter: the thread's compaction count could not be read \u2014 re-sending the standing instructions
|
|
6233
|
+
"codex adapter: the thread's compaction count could not be read \u2014 re-sending the standing instructions",
|
|
6042
6234
|
{ threadId: decision.resume }
|
|
6043
6235
|
);
|
|
6044
6236
|
} else if (decision.compactions === compactions) {
|
|
@@ -6048,7 +6240,7 @@ function createCodexAdapter(deps = {}) {
|
|
|
6048
6240
|
};
|
|
6049
6241
|
} else if (decision.compactions !== null && compactions > decision.compactions) {
|
|
6050
6242
|
deps.onWarn?.(
|
|
6051
|
-
"codex adapter: the thread compacted since the last turn \u2014 re-sending the standing instructions
|
|
6243
|
+
"codex adapter: the thread compacted since the last turn \u2014 re-sending the standing instructions",
|
|
6052
6244
|
{
|
|
6053
6245
|
threadId: decision.resume,
|
|
6054
6246
|
compactions,
|
|
@@ -6336,6 +6528,7 @@ var init_conformance4 = __esm({
|
|
|
6336
6528
|
type: "tool",
|
|
6337
6529
|
id: "t1",
|
|
6338
6530
|
name: "read",
|
|
6531
|
+
mcpServer: "cabane",
|
|
6339
6532
|
phase: "start",
|
|
6340
6533
|
summary: "sleep.md",
|
|
6341
6534
|
input: { path: "sleep.md" }
|
|
@@ -6344,6 +6537,7 @@ var init_conformance4 = __esm({
|
|
|
6344
6537
|
type: "tool",
|
|
6345
6538
|
id: "t1",
|
|
6346
6539
|
name: "read",
|
|
6540
|
+
mcpServer: "cabane",
|
|
6347
6541
|
phase: "done",
|
|
6348
6542
|
summary: "sleep.md",
|
|
6349
6543
|
input: { path: "sleep.md" },
|
|
@@ -6389,6 +6583,7 @@ var init_conformance4 = __esm({
|
|
|
6389
6583
|
type: "tool",
|
|
6390
6584
|
id: "query-success",
|
|
6391
6585
|
name: "query",
|
|
6586
|
+
mcpServer: "cabane",
|
|
6392
6587
|
phase: "start",
|
|
6393
6588
|
summary: "",
|
|
6394
6589
|
input: { op: "files.read", args: { path: "/ABOUT.md" } }
|
|
@@ -6397,6 +6592,7 @@ var init_conformance4 = __esm({
|
|
|
6397
6592
|
type: "tool",
|
|
6398
6593
|
id: "query-success",
|
|
6399
6594
|
name: "query",
|
|
6595
|
+
mcpServer: "cabane",
|
|
6400
6596
|
phase: "done",
|
|
6401
6597
|
summary: "",
|
|
6402
6598
|
input: { op: "files.read", args: { path: "/ABOUT.md" } },
|
|
@@ -6443,6 +6639,7 @@ var init_conformance4 = __esm({
|
|
|
6443
6639
|
type: "tool",
|
|
6444
6640
|
id: "query-failed",
|
|
6445
6641
|
name: "query",
|
|
6642
|
+
mcpServer: "cabane",
|
|
6446
6643
|
phase: "start",
|
|
6447
6644
|
summary: "",
|
|
6448
6645
|
input: { op: "files.read", args: { path: "/ABOUT.md" } }
|
|
@@ -6451,6 +6648,7 @@ var init_conformance4 = __esm({
|
|
|
6451
6648
|
type: "tool",
|
|
6452
6649
|
id: "query-failed",
|
|
6453
6650
|
name: "query",
|
|
6651
|
+
mcpServer: "cabane",
|
|
6454
6652
|
phase: "error",
|
|
6455
6653
|
summary: "",
|
|
6456
6654
|
input: { op: "files.read", args: { path: "/ABOUT.md" } },
|
|
@@ -6461,17 +6659,65 @@ var init_conformance4 = __esm({
|
|
|
6461
6659
|
]
|
|
6462
6660
|
},
|
|
6463
6661
|
{
|
|
6464
|
-
//
|
|
6465
|
-
//
|
|
6466
|
-
|
|
6662
|
+
// The turn-control server's `answer` (CT1973): the tool event carries the server Codex names
|
|
6663
|
+
// beside the tool, which is what the pump binds the declaration to. A non-cabane server keeps
|
|
6664
|
+
// its prefix on the card as before.
|
|
6665
|
+
name: "turn-control answer carries its server",
|
|
6467
6666
|
request: makeRequest3(),
|
|
6468
6667
|
nativeStream: [
|
|
6469
6668
|
threadStarted(NEW_THREAD_ID),
|
|
6669
|
+
agentMessage("m1", "On it."),
|
|
6470
6670
|
toolFrame("item.completed", {
|
|
6471
|
-
id: "
|
|
6671
|
+
id: "answer-1",
|
|
6472
6672
|
type: "mcp_tool_call",
|
|
6473
|
-
server: "
|
|
6474
|
-
tool: "
|
|
6673
|
+
server: "cabane_companion",
|
|
6674
|
+
tool: "answer",
|
|
6675
|
+
arguments: {},
|
|
6676
|
+
status: "completed",
|
|
6677
|
+
result: { content: [{ type: "text", text: '{"ok":true,"kind":"answer"}' }] }
|
|
6678
|
+
}),
|
|
6679
|
+
agentMessage("m2", "The answer."),
|
|
6680
|
+
turnCompleted()
|
|
6681
|
+
],
|
|
6682
|
+
expected: [
|
|
6683
|
+
startedSession(NEW_THREAD_ID),
|
|
6684
|
+
{ type: "text", body: "On it.", terminal: false },
|
|
6685
|
+
{
|
|
6686
|
+
type: "tool",
|
|
6687
|
+
id: "answer-1",
|
|
6688
|
+
name: "cabane_companion.answer",
|
|
6689
|
+
mcpServer: "cabane_companion",
|
|
6690
|
+
phase: "start",
|
|
6691
|
+
summary: "",
|
|
6692
|
+
input: {}
|
|
6693
|
+
},
|
|
6694
|
+
{
|
|
6695
|
+
type: "tool",
|
|
6696
|
+
id: "answer-1",
|
|
6697
|
+
name: "cabane_companion.answer",
|
|
6698
|
+
mcpServer: "cabane_companion",
|
|
6699
|
+
phase: "done",
|
|
6700
|
+
summary: "",
|
|
6701
|
+
input: {},
|
|
6702
|
+
result: [{ type: "text", text: '{"ok":true,"kind":"answer"}' }]
|
|
6703
|
+
},
|
|
6704
|
+
{ type: "text", body: "The answer.", terminal: true },
|
|
6705
|
+
closingSession(NEW_THREAD_ID),
|
|
6706
|
+
{ type: "result", ok: true }
|
|
6707
|
+
]
|
|
6708
|
+
},
|
|
6709
|
+
{
|
|
6710
|
+
// A transport/tool failure with no MCP result payload still uses the real adapter error message,
|
|
6711
|
+
// never `undefined`.
|
|
6712
|
+
name: "failed MCP call without result keeps error message fallback",
|
|
6713
|
+
request: makeRequest3(),
|
|
6714
|
+
nativeStream: [
|
|
6715
|
+
threadStarted(NEW_THREAD_ID),
|
|
6716
|
+
toolFrame("item.completed", {
|
|
6717
|
+
id: "mcp-failed",
|
|
6718
|
+
type: "mcp_tool_call",
|
|
6719
|
+
server: "other",
|
|
6720
|
+
tool: "lookup",
|
|
6475
6721
|
arguments: { q: "receipt" },
|
|
6476
6722
|
status: "failed",
|
|
6477
6723
|
result: { content: [], isError: true },
|
|
@@ -6485,6 +6731,7 @@ var init_conformance4 = __esm({
|
|
|
6485
6731
|
type: "tool",
|
|
6486
6732
|
id: "mcp-failed",
|
|
6487
6733
|
name: "other.lookup",
|
|
6734
|
+
mcpServer: "other",
|
|
6488
6735
|
phase: "start",
|
|
6489
6736
|
summary: "receipt",
|
|
6490
6737
|
input: { q: "receipt" }
|
|
@@ -6493,6 +6740,7 @@ var init_conformance4 = __esm({
|
|
|
6493
6740
|
type: "tool",
|
|
6494
6741
|
id: "mcp-failed",
|
|
6495
6742
|
name: "other.lookup",
|
|
6743
|
+
mcpServer: "other",
|
|
6496
6744
|
phase: "error",
|
|
6497
6745
|
summary: "receipt",
|
|
6498
6746
|
input: { q: "receipt" },
|
|
@@ -6529,6 +6777,7 @@ var init_conformance4 = __esm({
|
|
|
6529
6777
|
type: "tool",
|
|
6530
6778
|
id: "mcp-image",
|
|
6531
6779
|
name: "other.render",
|
|
6780
|
+
mcpServer: "other",
|
|
6532
6781
|
phase: "start",
|
|
6533
6782
|
summary: "",
|
|
6534
6783
|
input: {}
|
|
@@ -6537,6 +6786,7 @@ var init_conformance4 = __esm({
|
|
|
6537
6786
|
type: "tool",
|
|
6538
6787
|
id: "mcp-image",
|
|
6539
6788
|
name: "other.render",
|
|
6789
|
+
mcpServer: "other",
|
|
6540
6790
|
phase: "done",
|
|
6541
6791
|
summary: "",
|
|
6542
6792
|
input: {},
|
|
@@ -6584,11 +6834,20 @@ var init_conformance4 = __esm({
|
|
|
6584
6834
|
],
|
|
6585
6835
|
expected: [
|
|
6586
6836
|
startedSession(NEW_THREAD_ID),
|
|
6587
|
-
{ type: "tool", id: "t1", name: "list", phase: "start", summary: "", input: {} },
|
|
6588
6837
|
{
|
|
6589
6838
|
type: "tool",
|
|
6590
6839
|
id: "t1",
|
|
6591
6840
|
name: "list",
|
|
6841
|
+
mcpServer: "cabane",
|
|
6842
|
+
phase: "start",
|
|
6843
|
+
summary: "",
|
|
6844
|
+
input: {}
|
|
6845
|
+
},
|
|
6846
|
+
{
|
|
6847
|
+
type: "tool",
|
|
6848
|
+
id: "t1",
|
|
6849
|
+
name: "list",
|
|
6850
|
+
mcpServer: "cabane",
|
|
6592
6851
|
phase: "done",
|
|
6593
6852
|
summary: "/product/tasks",
|
|
6594
6853
|
input: { path: "/product/tasks" },
|
|
@@ -7582,11 +7841,10 @@ function required(env, name) {
|
|
|
7582
7841
|
function runnerPlace(env) {
|
|
7583
7842
|
const workRoot2 = env[PLACE_ENV.workRoot];
|
|
7584
7843
|
const transcriptDir = env[PLACE_ENV.transcriptDir];
|
|
7585
|
-
const box = env[PLACE_ENV.box];
|
|
7586
7844
|
return {
|
|
7587
7845
|
...workRoot2 ? { workRoot: workRoot2 } : {},
|
|
7588
7846
|
...transcriptDir ? { transcriptDir } : {},
|
|
7589
|
-
...
|
|
7847
|
+
...env[PLACE_ENV.dedicatedRoot] === "1" ? { dedicatedRoot: true } : {}
|
|
7590
7848
|
};
|
|
7591
7849
|
}
|
|
7592
7850
|
function parseRunnerInput(env, stdin) {
|
|
@@ -7671,7 +7929,7 @@ var init_protocol = __esm({
|
|
|
7671
7929
|
PLACE_ENV = {
|
|
7672
7930
|
workRoot: "CABANE_WORK_ROOT",
|
|
7673
7931
|
transcriptDir: "CABANE_TRANSCRIPT_DIR",
|
|
7674
|
-
|
|
7932
|
+
dedicatedRoot: "CABANE_WORK_ROOT_DEDICATED"
|
|
7675
7933
|
};
|
|
7676
7934
|
runtimeHealthSchema = z14.object({
|
|
7677
7935
|
quotaState: z14.enum(["ok", "limited", "auth_expired", "unknown"]),
|
|
@@ -7729,568 +7987,157 @@ var init_protocol = __esm({
|
|
|
7729
7987
|
}
|
|
7730
7988
|
});
|
|
7731
7989
|
|
|
7732
|
-
// src/
|
|
7733
|
-
import {
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
return
|
|
7990
|
+
// src/grants.ts
|
|
7991
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
|
|
7992
|
+
import { join as join14 } from "path";
|
|
7993
|
+
import { z as z15 } from "zod";
|
|
7994
|
+
function allowedGrantVariable(name) {
|
|
7995
|
+
return name.length <= 100 && VARIABLE_NAME_RE.test(name) && !name.startsWith("CABANE_");
|
|
7738
7996
|
}
|
|
7739
|
-
function
|
|
7740
|
-
return
|
|
7997
|
+
function environmentSafe(value) {
|
|
7998
|
+
return !value.includes("\0");
|
|
7741
7999
|
}
|
|
7742
|
-
function
|
|
7743
|
-
|
|
7744
|
-
for (const [k, v] of Object.entries(base)) {
|
|
7745
|
-
if (v === void 0) continue;
|
|
7746
|
-
if (boxed && BOXED_ENV_DROPPED.includes(k)) continue;
|
|
7747
|
-
env[k] = v;
|
|
7748
|
-
}
|
|
7749
|
-
return env;
|
|
8000
|
+
function grantsPath() {
|
|
8001
|
+
return join14(cabaneDir(), "grants.json");
|
|
7750
8002
|
}
|
|
7751
|
-
function
|
|
8003
|
+
function readGrants(keys, report) {
|
|
8004
|
+
if (keys.length === 0) return {};
|
|
8005
|
+
const file = loadGrantsFile(report);
|
|
7752
8006
|
const out = {};
|
|
7753
|
-
for (const
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
|
|
8007
|
+
for (const key of keys) {
|
|
8008
|
+
if (!allowedGrantVariable(key) || !Object.hasOwn(file, key)) continue;
|
|
8009
|
+
const entry = {};
|
|
8010
|
+
for (const [variable, value] of Object.entries(file[key])) {
|
|
8011
|
+
if (allowedGrantVariable(variable) && environmentSafe(value)) entry[variable] = value;
|
|
8012
|
+
else report.refusedVariable(key, variable);
|
|
8013
|
+
}
|
|
8014
|
+
out[key] = entry;
|
|
7759
8015
|
}
|
|
7760
8016
|
return out;
|
|
7761
8017
|
}
|
|
7762
|
-
function
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
bootId,
|
|
7767
|
-
workspaceId: opts.workspaceId,
|
|
7768
|
-
workspaceSlug: opts.workspaceSlug,
|
|
7769
|
-
agentId: opts.agentId,
|
|
7770
|
-
agentUsername: opts.agentUsername,
|
|
7771
|
-
agentName: String(opts.log.bindings().agentName ?? opts.agentUsername),
|
|
7772
|
-
local: opts.local,
|
|
7773
|
-
runConfig: opts.runConfig,
|
|
7774
|
-
workRoot: opts.workRoot,
|
|
7775
|
-
...opts.opencodeServerUrl ? { opencodeServerUrl: opts.opencodeServerUrl } : {},
|
|
7776
|
-
// Read now, per turn, the way adapter selection reads them.
|
|
7777
|
-
claudeCodeAvailable: opts.claudeCodeAvailable?.() ?? true,
|
|
7778
|
-
codexAvailable: opts.codexAvailable?.() ?? false,
|
|
7779
|
-
...opts.idleTimeoutMs !== void 0 ? { idleTimeoutMs: opts.idleTimeoutMs } : {},
|
|
7780
|
-
...opts.totalTimeoutMs !== void 0 ? { totalTimeoutMs: opts.totalTimeoutMs } : {},
|
|
7781
|
-
...opts.leaseRenewalMs !== void 0 ? { leaseRenewalMs: opts.leaseRenewalMs } : {},
|
|
7782
|
-
...opts.preparingRowDelayMs !== void 0 ? { preparingRowDelayMs: opts.preparingRowDelayMs } : {},
|
|
7783
|
-
transcripts: opts.transcriptDir !== void 0,
|
|
7784
|
-
logLevel: opts.log.level,
|
|
7785
|
-
connectorHealth: healthSnapshot(launch.connectorHealth),
|
|
7786
|
-
sessionWriteNoticeClaimed: duties.sessionWriteNoticeClaimed()
|
|
7787
|
-
};
|
|
7788
|
-
}
|
|
7789
|
-
function relayRunnerLog(line, log2) {
|
|
7790
|
-
if (!line.trim()) return;
|
|
7791
|
-
let record = null;
|
|
8018
|
+
function loadGrantsFile(report) {
|
|
8019
|
+
const path = grantsPath();
|
|
8020
|
+
if (!existsSync9(path)) return {};
|
|
8021
|
+
let raw;
|
|
7792
8022
|
try {
|
|
7793
|
-
|
|
7794
|
-
|
|
8023
|
+
raw = readFileSync7(path, "utf8");
|
|
8024
|
+
} catch (err) {
|
|
8025
|
+
const code = err.code ?? "unknown error";
|
|
8026
|
+
report.unreadable(`couldn't read ${path}: ${code}`);
|
|
8027
|
+
return {};
|
|
8028
|
+
}
|
|
8029
|
+
if (raw.trim().length === 0) return {};
|
|
8030
|
+
let parsed;
|
|
8031
|
+
try {
|
|
8032
|
+
parsed = JSON.parse(raw);
|
|
7795
8033
|
} catch {
|
|
7796
|
-
|
|
8034
|
+
report.unreadable(`${path} is not valid JSON.`);
|
|
8035
|
+
return {};
|
|
7797
8036
|
}
|
|
7798
|
-
|
|
7799
|
-
|
|
7800
|
-
|
|
8037
|
+
const result = grantsFileSchema.safeParse(parsed);
|
|
8038
|
+
if (!result.success) {
|
|
8039
|
+
report.unreadable(
|
|
8040
|
+
`${path} must map a secret's variable name to an object of "VARIABLE": "value" pairs (e.g. { "HAS_STAGING_DB": { "DATABASE_URL": "postgres://\u2026" } }).`
|
|
8041
|
+
);
|
|
8042
|
+
return {};
|
|
7801
8043
|
}
|
|
7802
|
-
|
|
7803
|
-
level,
|
|
7804
|
-
msg,
|
|
7805
|
-
time: _time,
|
|
7806
|
-
pid: _pid,
|
|
7807
|
-
hostname: _hostname,
|
|
7808
|
-
agentName: _agentName,
|
|
7809
|
-
...fields
|
|
7810
|
-
} = record;
|
|
7811
|
-
const method = PINO_LEVELS[level] ?? "info";
|
|
7812
|
-
log2[method](fields, typeof msg === "string" ? msg : "");
|
|
8044
|
+
return result.data;
|
|
7813
8045
|
}
|
|
7814
|
-
function
|
|
7815
|
-
const
|
|
7816
|
-
|
|
7817
|
-
|
|
8046
|
+
function harnessSecretEnv(parts) {
|
|
8047
|
+
const env = {};
|
|
8048
|
+
const dropped = [];
|
|
8049
|
+
for (const [name, value] of Object.entries({
|
|
8050
|
+
...parts.deviceDefaults,
|
|
8051
|
+
...parts.granted,
|
|
8052
|
+
...parts.issued
|
|
8053
|
+
})) {
|
|
8054
|
+
if (environmentSafe(value)) env[name] = value;
|
|
8055
|
+
else dropped.push(name);
|
|
8056
|
+
}
|
|
8057
|
+
return { env, dropped };
|
|
7818
8058
|
}
|
|
7819
|
-
function
|
|
7820
|
-
|
|
7821
|
-
const
|
|
7822
|
-
|
|
7823
|
-
const
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
continue;
|
|
8059
|
+
function grantEnv(sent, issuedBySecret, issued) {
|
|
8060
|
+
const env = {};
|
|
8061
|
+
for (const [key, entry] of Object.entries(sent ?? {})) {
|
|
8062
|
+
if (!Object.hasOwn(issuedBySecret, key)) continue;
|
|
8063
|
+
for (const [variable, value] of Object.entries(entry)) {
|
|
8064
|
+
if (Object.hasOwn(issued, variable) || Object.hasOwn(env, variable)) continue;
|
|
8065
|
+
env[variable] = value;
|
|
7827
8066
|
}
|
|
7828
|
-
void (async () => {
|
|
7829
|
-
for (let attempt = 1; ; attempt++) {
|
|
7830
|
-
const stopped = await Promise.all(left.map((r) => stop2(r.serverUrl, r.sessionId))).catch(
|
|
7831
|
-
() => [false]
|
|
7832
|
-
);
|
|
7833
|
-
if (stopped.every(Boolean)) break;
|
|
7834
|
-
if (attempt === 1)
|
|
7835
|
-
log2.warn(
|
|
7836
|
-
{ key, sessions: left.map((r) => r.sessionId) },
|
|
7837
|
-
"runner: a dead runner left work on a shared server that could not be confirmed stopped; holding the server until it is"
|
|
7838
|
-
);
|
|
7839
|
-
await new Promise((resolve2) => setTimeout(resolve2, launch.reconcileRetryMs ?? 5e3));
|
|
7840
|
-
}
|
|
7841
|
-
locks.release(owner);
|
|
7842
|
-
})();
|
|
7843
8067
|
}
|
|
8068
|
+
return env;
|
|
7844
8069
|
}
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
|
|
7850
|
-
|
|
7851
|
-
|
|
7852
|
-
turnId: handleOpts.turnId
|
|
7853
|
-
});
|
|
7854
|
-
return {
|
|
7855
|
-
ok: true,
|
|
7856
|
-
credential: minted.credential,
|
|
7857
|
-
runnerId: minted.runnerId,
|
|
7858
|
-
deviceGrants: minted.deviceGrants ?? []
|
|
7859
|
-
};
|
|
7860
|
-
} catch (err) {
|
|
7861
|
-
if (signal.aborted) return { ok: false, reason: "cancelled" };
|
|
7862
|
-
const status2 = err instanceof ApiError ? err.status : 0;
|
|
7863
|
-
if (status2 === 404) return { ok: false, reason: "runner_credential_not_found" };
|
|
7864
|
-
if (status2 === 409 && apiErrorCode(err) === "dispatch_not_admitted")
|
|
7865
|
-
return { ok: false, reason: "runner_credential_not_admitted" };
|
|
7866
|
-
log2.warn(apiErrorLogFields(err), "runner: couldn't get a credential for the turn's runner");
|
|
7867
|
-
const reason = `fetch_failed: ${describeError(err)}`;
|
|
7868
|
-
return { ok: false, reason, errorReason: reason };
|
|
8070
|
+
var VARIABLE_NAME_RE, grantsFileSchema;
|
|
8071
|
+
var init_grants = __esm({
|
|
8072
|
+
"src/grants.ts"() {
|
|
8073
|
+
"use strict";
|
|
8074
|
+
init_config();
|
|
8075
|
+
VARIABLE_NAME_RE = /^[A-Z_][A-Z0-9_]*$/;
|
|
8076
|
+
grantsFileSchema = z15.record(z15.string(), z15.record(z15.string(), z15.string()));
|
|
7869
8077
|
}
|
|
8078
|
+
});
|
|
8079
|
+
|
|
8080
|
+
// src/work-dir.ts
|
|
8081
|
+
import { statSync as statSync3 } from "fs";
|
|
8082
|
+
import { join as join15 } from "path";
|
|
8083
|
+
function pairDirectory(root, pair) {
|
|
8084
|
+
return join15(root, pair.workspaceSlug, pair.agentUsername, pair.conversationId);
|
|
7870
8085
|
}
|
|
7871
|
-
|
|
8086
|
+
function chooseStandingDirectory(named, pairDir, isDirectory = directoryExists) {
|
|
8087
|
+
const skipped = [];
|
|
8088
|
+
for (const candidate of named) {
|
|
8089
|
+
if (isDirectory(candidate.path)) return { cwd: candidate.path, skipped };
|
|
8090
|
+
skipped.push(candidate);
|
|
8091
|
+
}
|
|
8092
|
+
return { cwd: pairDir, skipped };
|
|
8093
|
+
}
|
|
8094
|
+
function directoryExists(path) {
|
|
7872
8095
|
try {
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
outcome: "failed",
|
|
7877
|
-
...errorReason ? { errorReason: errorReason.slice(0, ERROR_REASON_MAX) } : {},
|
|
7878
|
-
readThrough: payload.messageId
|
|
7879
|
-
});
|
|
7880
|
-
} catch (err) {
|
|
7881
|
-
log2.warn(
|
|
7882
|
-
{ ...apiErrorLogFields(err), turnId },
|
|
7883
|
-
"Couldn't update the turn's status in Cabane. Check the conversation before trying again."
|
|
7884
|
-
);
|
|
8096
|
+
return statSync3(path).isDirectory();
|
|
8097
|
+
} catch {
|
|
8098
|
+
return false;
|
|
7885
8099
|
}
|
|
7886
8100
|
}
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
const oomKilled = (id) => (launch.oomKilled ?? turnScopeOomKilled)(id).catch(() => false);
|
|
7891
|
-
const log2 = opts.log.child({
|
|
7892
|
-
workspaceId: opts.workspaceId,
|
|
7893
|
-
conversationId: payload.conversationId,
|
|
7894
|
-
agentId: payload.agentId,
|
|
7895
|
-
messageId: payload.messageId
|
|
7896
|
-
});
|
|
7897
|
-
const controller = new AbortController();
|
|
7898
|
-
duties.registerAbort(turnId, controller);
|
|
7899
|
-
const stopRunner = new AbortController();
|
|
7900
|
-
let ready = false;
|
|
7901
|
-
let child;
|
|
7902
|
-
let stopAnnounced = false;
|
|
7903
|
-
const announceStop = () => {
|
|
7904
|
-
if (stopAnnounced || !child?.stdin.writable) return;
|
|
7905
|
-
stopAnnounced = true;
|
|
7906
|
-
child.stdin.write(encodeLauncherMessage({ cabaneLauncher: "stop" }));
|
|
7907
|
-
};
|
|
7908
|
-
const forwardStop = () => {
|
|
7909
|
-
announceStop();
|
|
7910
|
-
if (ready) stopRunner.abort();
|
|
7911
|
-
};
|
|
7912
|
-
controller.signal.addEventListener("abort", forwardStop, { once: true });
|
|
7913
|
-
const attempt = {
|
|
7914
|
-
workspaceId: opts.workspaceId,
|
|
7915
|
-
conversationId: payload.conversationId,
|
|
7916
|
-
agentId: payload.agentId,
|
|
7917
|
-
turnId
|
|
7918
|
-
};
|
|
7919
|
-
const minted = await mintForRunner(opts, payload, handleOpts, controller.signal, log2);
|
|
7920
|
-
if (!minted.ok) {
|
|
7921
|
-
controller.signal.removeEventListener("abort", forwardStop);
|
|
7922
|
-
duties.releaseAbort(turnId, controller);
|
|
7923
|
-
const durationMs2 = Date.now() - startedAt;
|
|
7924
|
-
await settleUnstarted(opts, payload, turnId, minted.errorReason, log2);
|
|
7925
|
-
duties.notifyEnd({ id: payload.messageId, ok: false, durationMs: durationMs2, reason: minted.reason });
|
|
7926
|
-
return { ok: false, durationMs: durationMs2, reason: minted.reason };
|
|
8101
|
+
var init_work_dir = __esm({
|
|
8102
|
+
"src/work-dir.ts"() {
|
|
8103
|
+
"use strict";
|
|
7927
8104
|
}
|
|
8105
|
+
});
|
|
8106
|
+
|
|
8107
|
+
// src/turn-control-tools.ts
|
|
8108
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
8109
|
+
import { z as z16 } from "zod";
|
|
8110
|
+
function createSkipState() {
|
|
8111
|
+
return { skipped: false, reason: null };
|
|
8112
|
+
}
|
|
8113
|
+
function toolError(text) {
|
|
8114
|
+
return { isError: true, content: [{ type: "text", text }] };
|
|
8115
|
+
}
|
|
8116
|
+
async function performAct(acts, act) {
|
|
7928
8117
|
let result;
|
|
7929
|
-
let thrown;
|
|
7930
|
-
let code;
|
|
7931
|
-
let handle;
|
|
7932
|
-
const groups = [];
|
|
7933
|
-
const remote = [];
|
|
7934
|
-
const lockOwner = (id) => `${turnId}#${id}`;
|
|
7935
|
-
const locks = launch.locks;
|
|
7936
|
-
try {
|
|
7937
|
-
handle = await launch.provider.acquire(attempt);
|
|
7938
|
-
} catch (err) {
|
|
7939
|
-
const reason2 = `runner_unavailable: ${describeError(err)}`;
|
|
7940
|
-
log2.warn(
|
|
7941
|
-
{ turnId, provider: launch.provider.name, err: describeError(err) },
|
|
7942
|
-
"runner: no place to run the turn"
|
|
7943
|
-
);
|
|
7944
|
-
controller.signal.removeEventListener("abort", forwardStop);
|
|
7945
|
-
duties.releaseAbort(turnId, controller);
|
|
7946
|
-
await settleUnstarted(opts, payload, turnId, reason2, log2);
|
|
7947
|
-
await launch.reportRunnerGone?.({
|
|
7948
|
-
workspaceId: opts.workspaceId,
|
|
7949
|
-
conversationId: payload.conversationId,
|
|
7950
|
-
agentId: payload.agentId,
|
|
7951
|
-
messageId: payload.messageId,
|
|
7952
|
-
runnerId: minted.runnerId,
|
|
7953
|
-
reason: "provider_failed"
|
|
7954
|
-
});
|
|
7955
|
-
const durationMs2 = Date.now() - startedAt;
|
|
7956
|
-
duties.notifyEnd({ id: payload.messageId, ok: false, durationMs: durationMs2, reason: reason2 });
|
|
7957
|
-
return { ok: false, durationMs: durationMs2, reason: reason2 };
|
|
7958
|
-
}
|
|
7959
8118
|
try {
|
|
7960
|
-
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
|
|
7965
|
-
credential: minted.credential,
|
|
7966
|
-
...boxed ? { secrets: launch.secrets?.() ?? {} } : {},
|
|
7967
|
-
...Object.keys(grants).length > 0 ? { grants } : {},
|
|
7968
|
-
dispatch: payload,
|
|
7969
|
-
turnId,
|
|
7970
|
-
resumed: handleOpts.resumed,
|
|
7971
|
-
spec: runnerSpec(opts, launch, duties, minted.runnerId)
|
|
7972
|
-
};
|
|
7973
|
-
Object.assign(env, runnerEnv(input));
|
|
7974
|
-
const running = launch.provider.exec(handle, launch.command, env, {
|
|
7975
|
-
signal: stopRunner.signal,
|
|
7976
|
-
stdin: runnerStdin(input)
|
|
7977
|
-
});
|
|
7978
|
-
child = running;
|
|
7979
|
-
if (controller.signal.aborted) announceStop();
|
|
7980
|
-
log2.debug(
|
|
7981
|
-
{ turnId, runnerPid: running.pid, provider: launch.provider.name },
|
|
7982
|
-
"runner: started"
|
|
7983
|
-
);
|
|
7984
|
-
const stdoutDone = lines(running.stdout, (line) => {
|
|
7985
|
-
const event = decodeRunnerEvent(line);
|
|
7986
|
-
if (!event) {
|
|
7987
|
-
log2.debug({ runnerStdout: line }, "runner: output");
|
|
7988
|
-
return;
|
|
7989
|
-
}
|
|
7990
|
-
switch (event.cabaneRunner) {
|
|
7991
|
-
case "ready":
|
|
7992
|
-
ready = true;
|
|
7993
|
-
if (controller.signal.aborted) stopRunner.abort();
|
|
7994
|
-
return;
|
|
7995
|
-
case "start":
|
|
7996
|
-
duties.notifyStart({
|
|
7997
|
-
id: event.id,
|
|
7998
|
-
conversationId: event.conversationId,
|
|
7999
|
-
message: event.message
|
|
8000
|
-
});
|
|
8001
|
-
return;
|
|
8002
|
-
case "end": {
|
|
8003
|
-
const { cabaneRunner: _kind, ...info } = event;
|
|
8004
|
-
duties.notifyEnd(info);
|
|
8005
|
-
return;
|
|
8006
|
-
}
|
|
8007
|
-
case "session_write_notice":
|
|
8008
|
-
duties.markSessionWriteNotice();
|
|
8009
|
-
return;
|
|
8010
|
-
case "connector_health":
|
|
8011
|
-
launch.connectorHealth?.recordSettle(event.runtime, {
|
|
8012
|
-
ok: event.ok,
|
|
8013
|
-
errorReason: event.errorReason
|
|
8014
|
-
});
|
|
8015
|
-
return;
|
|
8016
|
-
case "result":
|
|
8017
|
-
result = event.result;
|
|
8018
|
-
return;
|
|
8019
|
-
case "error":
|
|
8020
|
-
thrown = event.message;
|
|
8021
|
-
return;
|
|
8022
|
-
case "lock":
|
|
8023
|
-
if (!locks) {
|
|
8024
|
-
log2.warn(
|
|
8025
|
-
{ key: event.key },
|
|
8026
|
-
"runner: asked for a device lock this launcher has none of"
|
|
8027
|
-
);
|
|
8028
|
-
return;
|
|
8029
|
-
}
|
|
8030
|
-
locks.request(
|
|
8031
|
-
event.key,
|
|
8032
|
-
lockOwner(event.id),
|
|
8033
|
-
() => running.stdin.write(encodeLauncherMessage({ cabaneLauncher: "granted", id: event.id }))
|
|
8034
|
-
);
|
|
8035
|
-
return;
|
|
8036
|
-
case "unlock":
|
|
8037
|
-
locks?.release(lockOwner(event.id));
|
|
8038
|
-
return;
|
|
8039
|
-
case "process_group":
|
|
8040
|
-
if (!boxed) groups.push(event.pgid);
|
|
8041
|
-
return;
|
|
8042
|
-
case "remote_session":
|
|
8043
|
-
remote.push({ serverUrl: event.serverUrl, sessionId: event.sessionId });
|
|
8044
|
-
return;
|
|
8045
|
-
case "terminated":
|
|
8046
|
-
void oomKilled(turnId).then((oom) => {
|
|
8047
|
-
if (running.stdin.writable) {
|
|
8048
|
-
running.stdin.write(
|
|
8049
|
-
encodeLauncherMessage({
|
|
8050
|
-
cabaneLauncher: "cause",
|
|
8051
|
-
cause: oom ? "out_of_memory" : null
|
|
8052
|
-
})
|
|
8053
|
-
);
|
|
8054
|
-
}
|
|
8055
|
-
});
|
|
8056
|
-
return;
|
|
8057
|
-
}
|
|
8058
|
-
});
|
|
8059
|
-
const stderrDone = lines(running.stderr, (line) => relayRunnerLog(line, opts.log));
|
|
8060
|
-
code = await running.exit;
|
|
8061
|
-
await Promise.all([stdoutDone, stderrDone]);
|
|
8062
|
-
} finally {
|
|
8063
|
-
if (locks) giveBackLocks(locks, `${turnId}#`, remote, launch, log2);
|
|
8064
|
-
child?.stdin.end();
|
|
8065
|
-
controller.signal.removeEventListener("abort", forwardStop);
|
|
8066
|
-
duties.releaseAbort(turnId, controller);
|
|
8067
|
-
await launch.provider.release(handle, { destroy: false }).catch(
|
|
8068
|
-
(err) => log2.warn({ err: describeError(err) }, "runner: couldn't give its place back")
|
|
8069
|
-
);
|
|
8070
|
-
}
|
|
8071
|
-
if (result) return result;
|
|
8072
|
-
const durationMs = Date.now() - startedAt;
|
|
8073
|
-
const stopped = controller.signal.aborted && thrown === void 0;
|
|
8074
|
-
const reason = stopped ? "cancelled" : thrown !== void 0 ? `runner_failed: ${thrown}` : await oomKilled(turnId) ? "out_of_memory" : `runner_exited: ${code}`;
|
|
8075
|
-
await child?.reap(groups);
|
|
8076
|
-
const reaped = await (launch.reapTurn ?? reapTurnScopes)(turnId).catch((err) => ({
|
|
8077
|
-
failures: [err instanceof Error ? err.message : String(err)]
|
|
8078
|
-
}));
|
|
8079
|
-
if (reaped.failures.length) {
|
|
8080
|
-
log2.warn(
|
|
8081
|
-
{ turnId, failures: reaped.failures },
|
|
8082
|
-
"runner: couldn't stop what the dead runner's turn left running"
|
|
8119
|
+
result = await acts.api.turnAct(
|
|
8120
|
+
acts.workspaceId,
|
|
8121
|
+
acts.turnId,
|
|
8122
|
+
act,
|
|
8123
|
+
AbortSignal.timeout(TURN_ACT_TIMEOUT_MS)
|
|
8083
8124
|
);
|
|
8084
|
-
}
|
|
8085
|
-
try {
|
|
8086
|
-
await opts.api.setActiveRun(opts.workspaceId, payload.conversationId, payload.agentId, {
|
|
8087
|
-
activeRunStartedAt: null,
|
|
8088
|
-
turnId,
|
|
8089
|
-
...stopped ? { outcome: "interrupted" } : { outcome: "failed", errorReason: reason.slice(0, ERROR_REASON_MAX) },
|
|
8090
|
-
readThrough: payload.messageId
|
|
8091
|
-
});
|
|
8092
8125
|
} catch (err) {
|
|
8093
|
-
|
|
8094
|
-
{
|
|
8095
|
-
|
|
8096
|
-
)
|
|
8126
|
+
if (err instanceof ApiError && err.status >= 500) {
|
|
8127
|
+
return uncertainOutcome(`${err.status}: ${apiErrorMessage(err)}`);
|
|
8128
|
+
}
|
|
8129
|
+
if (err instanceof ApiError) {
|
|
8130
|
+
if (err.status === 409 && apiErrorCode(err) === "no_active_turn") {
|
|
8131
|
+
return toolError(
|
|
8132
|
+
"No active turn: this turn has already settled, so the act was not performed."
|
|
8133
|
+
);
|
|
8134
|
+
}
|
|
8135
|
+
return toolError(`The act was refused (${err.status}): ${apiErrorMessage(err)}`);
|
|
8136
|
+
}
|
|
8137
|
+
return uncertainOutcome(err instanceof Error ? err.message : String(err));
|
|
8097
8138
|
}
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
conversationId: payload.conversationId,
|
|
8101
|
-
agentId: payload.agentId,
|
|
8102
|
-
messageId: payload.messageId,
|
|
8103
|
-
runnerId: minted.runnerId,
|
|
8104
|
-
reason: "exited"
|
|
8105
|
-
});
|
|
8106
|
-
if (thrown !== void 0) throw new Error(thrown);
|
|
8107
|
-
if (stopped) log2.info({ turnId, code }, "runner: stopped before it could report");
|
|
8108
|
-
else log2.warn({ turnId, code }, "runner: exited without reporting a result");
|
|
8109
|
-
duties.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
8110
|
-
return { ok: false, durationMs, reason };
|
|
8111
|
-
}
|
|
8112
|
-
var BOXED_ENV_DROPPED, PINO_LEVELS;
|
|
8113
|
-
var init_launch = __esm({
|
|
8114
|
-
"src/runner/launch.ts"() {
|
|
8115
|
-
"use strict";
|
|
8116
|
-
init_src();
|
|
8117
|
-
init_opencode_lock();
|
|
8118
|
-
init_api_error_shape();
|
|
8119
|
-
init_errors();
|
|
8120
|
-
init_boot_id();
|
|
8121
|
-
init_turn_containment();
|
|
8122
|
-
init_protocol();
|
|
8123
|
-
BOXED_ENV_DROPPED = [
|
|
8124
|
-
"ANTHROPIC_API_KEY",
|
|
8125
|
-
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
8126
|
-
"CODEX_API_KEY",
|
|
8127
|
-
"GH_TOKEN",
|
|
8128
|
-
"GITHUB_TOKEN",
|
|
8129
|
-
"DBUS_SESSION_BUS_ADDRESS",
|
|
8130
|
-
"XDG_RUNTIME_DIR"
|
|
8131
|
-
];
|
|
8132
|
-
PINO_LEVELS = {
|
|
8133
|
-
10: "trace",
|
|
8134
|
-
20: "debug",
|
|
8135
|
-
30: "info",
|
|
8136
|
-
40: "warn",
|
|
8137
|
-
50: "error",
|
|
8138
|
-
60: "fatal"
|
|
8139
|
-
};
|
|
8140
|
-
}
|
|
8141
|
-
});
|
|
8142
|
-
|
|
8143
|
-
// src/grants.ts
|
|
8144
|
-
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
|
|
8145
|
-
import { join as join14 } from "path";
|
|
8146
|
-
import { z as z15 } from "zod";
|
|
8147
|
-
function allowedGrantVariable(name) {
|
|
8148
|
-
return name.length <= 100 && VARIABLE_NAME_RE.test(name) && !name.startsWith("CABANE_");
|
|
8149
|
-
}
|
|
8150
|
-
function environmentSafe(value) {
|
|
8151
|
-
return !value.includes("\0");
|
|
8152
|
-
}
|
|
8153
|
-
function grantsPath() {
|
|
8154
|
-
return join14(cabaneDir(), "grants.json");
|
|
8155
|
-
}
|
|
8156
|
-
function readGrants(keys, report) {
|
|
8157
|
-
if (keys.length === 0) return {};
|
|
8158
|
-
const file = loadGrantsFile(report);
|
|
8159
|
-
const out = {};
|
|
8160
|
-
for (const key of keys) {
|
|
8161
|
-
if (!allowedGrantVariable(key) || !Object.hasOwn(file, key)) continue;
|
|
8162
|
-
const entry = {};
|
|
8163
|
-
for (const [variable, value] of Object.entries(file[key])) {
|
|
8164
|
-
if (allowedGrantVariable(variable) && environmentSafe(value)) entry[variable] = value;
|
|
8165
|
-
else report.refusedVariable(key, variable);
|
|
8166
|
-
}
|
|
8167
|
-
out[key] = entry;
|
|
8168
|
-
}
|
|
8169
|
-
return out;
|
|
8170
|
-
}
|
|
8171
|
-
function loadGrantsFile(report) {
|
|
8172
|
-
const path = grantsPath();
|
|
8173
|
-
if (!existsSync9(path)) return {};
|
|
8174
|
-
let raw;
|
|
8175
|
-
try {
|
|
8176
|
-
raw = readFileSync7(path, "utf8");
|
|
8177
|
-
} catch (err) {
|
|
8178
|
-
const code = err.code ?? "unknown error";
|
|
8179
|
-
report.unreadable(`couldn't read ${path}: ${code}`);
|
|
8180
|
-
return {};
|
|
8181
|
-
}
|
|
8182
|
-
if (raw.trim().length === 0) return {};
|
|
8183
|
-
let parsed;
|
|
8184
|
-
try {
|
|
8185
|
-
parsed = JSON.parse(raw);
|
|
8186
|
-
} catch {
|
|
8187
|
-
report.unreadable(`${path} is not valid JSON.`);
|
|
8188
|
-
return {};
|
|
8189
|
-
}
|
|
8190
|
-
const result = grantsFileSchema.safeParse(parsed);
|
|
8191
|
-
if (!result.success) {
|
|
8192
|
-
report.unreadable(
|
|
8193
|
-
`${path} must map a secret's variable name to an object of "VARIABLE": "value" pairs (e.g. { "HAS_PROD_ACCESS": { "RENDER_API_KEY": "rnd_\u2026" } }).`
|
|
8194
|
-
);
|
|
8195
|
-
return {};
|
|
8196
|
-
}
|
|
8197
|
-
return result.data;
|
|
8198
|
-
}
|
|
8199
|
-
function harnessSecretEnv(parts) {
|
|
8200
|
-
const env = {};
|
|
8201
|
-
const dropped = [];
|
|
8202
|
-
for (const [name, value] of Object.entries({
|
|
8203
|
-
...parts.deviceDefaults,
|
|
8204
|
-
...parts.granted,
|
|
8205
|
-
...parts.issued
|
|
8206
|
-
})) {
|
|
8207
|
-
if (environmentSafe(value)) env[name] = value;
|
|
8208
|
-
else dropped.push(name);
|
|
8209
|
-
}
|
|
8210
|
-
return { env, dropped };
|
|
8211
|
-
}
|
|
8212
|
-
function grantEnv(sent, issuedBySecret, issued) {
|
|
8213
|
-
const env = {};
|
|
8214
|
-
for (const [key, entry] of Object.entries(sent ?? {})) {
|
|
8215
|
-
if (!Object.hasOwn(issuedBySecret, key)) continue;
|
|
8216
|
-
for (const [variable, value] of Object.entries(entry)) {
|
|
8217
|
-
if (Object.hasOwn(issued, variable) || Object.hasOwn(env, variable)) continue;
|
|
8218
|
-
env[variable] = value;
|
|
8219
|
-
}
|
|
8220
|
-
}
|
|
8221
|
-
return env;
|
|
8222
|
-
}
|
|
8223
|
-
var VARIABLE_NAME_RE, grantsFileSchema;
|
|
8224
|
-
var init_grants = __esm({
|
|
8225
|
-
"src/grants.ts"() {
|
|
8226
|
-
"use strict";
|
|
8227
|
-
init_config();
|
|
8228
|
-
VARIABLE_NAME_RE = /^[A-Z_][A-Z0-9_]*$/;
|
|
8229
|
-
grantsFileSchema = z15.record(z15.string(), z15.record(z15.string(), z15.string()));
|
|
8230
|
-
}
|
|
8231
|
-
});
|
|
8232
|
-
|
|
8233
|
-
// src/work-dir.ts
|
|
8234
|
-
import { statSync as statSync3 } from "fs";
|
|
8235
|
-
import { join as join15 } from "path";
|
|
8236
|
-
function pairDirectory(root, pair) {
|
|
8237
|
-
return join15(root, pair.workspaceSlug, pair.agentUsername, pair.conversationId);
|
|
8238
|
-
}
|
|
8239
|
-
function chooseStandingDirectory(named, pairDir, isDirectory = directoryExists) {
|
|
8240
|
-
const skipped = [];
|
|
8241
|
-
for (const candidate of named) {
|
|
8242
|
-
if (isDirectory(candidate.path)) return { cwd: candidate.path, skipped };
|
|
8243
|
-
skipped.push(candidate);
|
|
8244
|
-
}
|
|
8245
|
-
return { cwd: pairDir, skipped };
|
|
8246
|
-
}
|
|
8247
|
-
function directoryExists(path) {
|
|
8248
|
-
try {
|
|
8249
|
-
return statSync3(path).isDirectory();
|
|
8250
|
-
} catch {
|
|
8251
|
-
return false;
|
|
8252
|
-
}
|
|
8253
|
-
}
|
|
8254
|
-
var init_work_dir = __esm({
|
|
8255
|
-
"src/work-dir.ts"() {
|
|
8256
|
-
"use strict";
|
|
8257
|
-
}
|
|
8258
|
-
});
|
|
8259
|
-
|
|
8260
|
-
// src/turn-control-tools.ts
|
|
8261
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
8262
|
-
import { z as z16 } from "zod";
|
|
8263
|
-
function createSkipState() {
|
|
8264
|
-
return { skipped: false, reason: null };
|
|
8265
|
-
}
|
|
8266
|
-
function toolError(text) {
|
|
8267
|
-
return { isError: true, content: [{ type: "text", text }] };
|
|
8268
|
-
}
|
|
8269
|
-
async function performAct(acts, act) {
|
|
8270
|
-
let result;
|
|
8271
|
-
try {
|
|
8272
|
-
result = await acts.api.turnAct(
|
|
8273
|
-
acts.workspaceId,
|
|
8274
|
-
acts.turnId,
|
|
8275
|
-
act,
|
|
8276
|
-
AbortSignal.timeout(TURN_ACT_TIMEOUT_MS)
|
|
8277
|
-
);
|
|
8278
|
-
} catch (err) {
|
|
8279
|
-
if (err instanceof ApiError && err.status >= 500) {
|
|
8280
|
-
return uncertainOutcome(`${err.status}: ${apiErrorMessage(err)}`);
|
|
8281
|
-
}
|
|
8282
|
-
if (err instanceof ApiError) {
|
|
8283
|
-
if (err.status === 409 && apiErrorCode(err) === "no_active_turn") {
|
|
8284
|
-
return toolError(
|
|
8285
|
-
"No active turn: this turn has already settled, so the act was not performed."
|
|
8286
|
-
);
|
|
8287
|
-
}
|
|
8288
|
-
return toolError(`The act was refused (${err.status}): ${apiErrorMessage(err)}`);
|
|
8289
|
-
}
|
|
8290
|
-
return uncertainOutcome(err instanceof Error ? err.message : String(err));
|
|
8291
|
-
}
|
|
8292
|
-
if (!result.ok) return toolError(JSON.stringify(result));
|
|
8293
|
-
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
8139
|
+
if (!result.ok) return toolError(JSON.stringify(result));
|
|
8140
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
8294
8141
|
}
|
|
8295
8142
|
function uncertainOutcome(cause) {
|
|
8296
8143
|
return toolError(
|
|
@@ -8428,6 +8275,13 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
8428
8275
|
}),
|
|
8429
8276
|
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
8430
8277
|
),
|
|
8278
|
+
tool(
|
|
8279
|
+
ANSWER_TOOL2,
|
|
8280
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.answer,
|
|
8281
|
+
{},
|
|
8282
|
+
async () => performAct(acts, { kind: "answer", callId: randomUUID2() }),
|
|
8283
|
+
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
8284
|
+
),
|
|
8431
8285
|
// The inverse. No arguments — it can only ever stand down the
|
|
8432
8286
|
// caller's own wake in the conversation it's holding a turn in.
|
|
8433
8287
|
tool(
|
|
@@ -8440,7 +8294,7 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
8440
8294
|
]
|
|
8441
8295
|
});
|
|
8442
8296
|
}
|
|
8443
|
-
var COMPANION_LOCAL_MCP_SERVER, COMPANION_LOCAL_TOOL_GLOB, SKIP_TURN_TOOL, ASK_TOOL, ASK_TOOL_NAME, WITHDRAW_ASK_TOOL, WITHDRAW_ASK_TOOL_NAME, WAKE_ME_TOOL, WAKE_ME_TOOL_NAME, CANCEL_WAKE_TOOL, CANCEL_WAKE_TOOL_NAME, TURN_ACT_TIMEOUT_MS;
|
|
8297
|
+
var COMPANION_LOCAL_MCP_SERVER, COMPANION_LOCAL_TOOL_GLOB, SKIP_TURN_TOOL, ASK_TOOL, ASK_TOOL_NAME, WITHDRAW_ASK_TOOL, WITHDRAW_ASK_TOOL_NAME, ANSWER_TOOL2, WAKE_ME_TOOL, WAKE_ME_TOOL_NAME, CANCEL_WAKE_TOOL, CANCEL_WAKE_TOOL_NAME, TURN_ACT_TIMEOUT_MS;
|
|
8444
8298
|
var init_turn_control_tools = __esm({
|
|
8445
8299
|
"src/turn-control-tools.ts"() {
|
|
8446
8300
|
"use strict";
|
|
@@ -8448,13 +8302,14 @@ var init_turn_control_tools = __esm({
|
|
|
8448
8302
|
init_descriptions();
|
|
8449
8303
|
init_errors();
|
|
8450
8304
|
init_api_error_shape();
|
|
8451
|
-
COMPANION_LOCAL_MCP_SERVER =
|
|
8305
|
+
COMPANION_LOCAL_MCP_SERVER = TURN_CONTROL_MCP_SERVER;
|
|
8452
8306
|
COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
|
|
8453
8307
|
SKIP_TURN_TOOL = "skip_turn";
|
|
8454
8308
|
ASK_TOOL = "ask";
|
|
8455
8309
|
ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${ASK_TOOL}`;
|
|
8456
8310
|
WITHDRAW_ASK_TOOL = "withdraw_ask";
|
|
8457
8311
|
WITHDRAW_ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WITHDRAW_ASK_TOOL}`;
|
|
8312
|
+
ANSWER_TOOL2 = "answer";
|
|
8458
8313
|
WAKE_ME_TOOL = "wake_me";
|
|
8459
8314
|
WAKE_ME_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WAKE_ME_TOOL}`;
|
|
8460
8315
|
CANCEL_WAKE_TOOL = "cancel_wake";
|
|
@@ -8713,73 +8568,6 @@ var init_turn_seq_floor = __esm({
|
|
|
8713
8568
|
}
|
|
8714
8569
|
});
|
|
8715
8570
|
|
|
8716
|
-
// src/prepare-failure.ts
|
|
8717
|
-
import { spawn as spawn4 } from "child_process";
|
|
8718
|
-
import { hostname } from "os";
|
|
8719
|
-
function prepareFailureCause(error) {
|
|
8720
|
-
const message2 = error instanceof Error ? error.message : String(error);
|
|
8721
|
-
if (/timed out/i.test(message2)) return "prepare hook timed out";
|
|
8722
|
-
if (/MODULE_NOT_FOUND|Cannot find (?:module|package)/.test(message2))
|
|
8723
|
-
return "prepare hook module missing; check the configured command and its installation for this device";
|
|
8724
|
-
if (/not valid JSON|JSON must carry|produced no output/.test(message2))
|
|
8725
|
-
return "prepare hook returned malformed output; expected a cwd path or JSON with a non-empty cwd";
|
|
8726
|
-
if (/ENOENT/.test(message2)) return "prepare hook executable missing (ENOENT)";
|
|
8727
|
-
if (/EACCES/.test(message2)) return "prepare hook executable is not accessible (EACCES)";
|
|
8728
|
-
const exit = message2.match(/exited with code (\d+)/)?.[1];
|
|
8729
|
-
return exit ? `prepare hook exited with code ${exit}` : "prepare hook failed; inspect the device-local hook log";
|
|
8730
|
-
}
|
|
8731
|
-
async function runPrepareWithReporting(runner, hook, input, context, reportError) {
|
|
8732
|
-
try {
|
|
8733
|
-
return await runner(hook, input);
|
|
8734
|
-
} catch (error) {
|
|
8735
|
-
if (hook.failureReporter) {
|
|
8736
|
-
try {
|
|
8737
|
-
await reportFailure(hook.failureReporter, {
|
|
8738
|
-
workspaceId: input.workspaceId,
|
|
8739
|
-
conversationId: input.conversationId,
|
|
8740
|
-
agentId: input.agentId,
|
|
8741
|
-
agentUsername: input.agentUsername,
|
|
8742
|
-
turnId: context.turnId,
|
|
8743
|
-
deviceId: context.deviceId ?? hostname(),
|
|
8744
|
-
cause: prepareFailureCause(error)
|
|
8745
|
-
});
|
|
8746
|
-
} catch {
|
|
8747
|
-
reportError();
|
|
8748
|
-
}
|
|
8749
|
-
}
|
|
8750
|
-
throw error;
|
|
8751
|
-
}
|
|
8752
|
-
}
|
|
8753
|
-
function reportFailure(config, body) {
|
|
8754
|
-
return new Promise((resolve2, reject) => {
|
|
8755
|
-
const child = spawn4(config.command, config.args ?? [], {
|
|
8756
|
-
stdio: ["pipe", "ignore", "ignore"],
|
|
8757
|
-
env: { ...process.env, ...config.env }
|
|
8758
|
-
});
|
|
8759
|
-
const timer = setTimeout(() => {
|
|
8760
|
-
child.kill("SIGKILL");
|
|
8761
|
-
reject(new Error("reporter timeout"));
|
|
8762
|
-
}, config.timeoutMs ?? 15e3);
|
|
8763
|
-
child.stdin.on("error", () => {
|
|
8764
|
-
});
|
|
8765
|
-
child.once("error", () => {
|
|
8766
|
-
clearTimeout(timer);
|
|
8767
|
-
reject(new Error("reporter failed to start"));
|
|
8768
|
-
});
|
|
8769
|
-
child.once("close", (code) => {
|
|
8770
|
-
clearTimeout(timer);
|
|
8771
|
-
if (code === 0) resolve2();
|
|
8772
|
-
else reject(new Error("reporter failed"));
|
|
8773
|
-
});
|
|
8774
|
-
child.stdin.end(JSON.stringify(body));
|
|
8775
|
-
});
|
|
8776
|
-
}
|
|
8777
|
-
var init_prepare_failure = __esm({
|
|
8778
|
-
"src/prepare-failure.ts"() {
|
|
8779
|
-
"use strict";
|
|
8780
|
-
}
|
|
8781
|
-
});
|
|
8782
|
-
|
|
8783
8571
|
// src/prepared.ts
|
|
8784
8572
|
import { mkdirSync as mkdirSync7, readFileSync as readFileSync8, rmSync as rmSync5, writeFileSync as writeFileSync5, existsSync as existsSync10 } from "fs";
|
|
8785
8573
|
import { join as join19 } from "path";
|
|
@@ -9111,7 +8899,7 @@ var init_turn_committer = __esm({
|
|
|
9111
8899
|
deps.onCommitFailed?.(err);
|
|
9112
8900
|
};
|
|
9113
8901
|
const commit = {
|
|
9114
|
-
commitMessage: async ({ body, kind, seq }) => {
|
|
8902
|
+
commitMessage: async ({ body, kind, seq, declared }) => {
|
|
9115
8903
|
await deps.api.postTurnMessage(
|
|
9116
8904
|
deps.workspaceId,
|
|
9117
8905
|
deps.conversationId,
|
|
@@ -9121,9 +8909,11 @@ var init_turn_committer = __esm({
|
|
|
9121
8909
|
turnId: deps.turnId,
|
|
9122
8910
|
seq,
|
|
9123
8911
|
parentMessageId: deps.parentMessageId,
|
|
9124
|
-
...source
|
|
8912
|
+
...source,
|
|
8913
|
+
...declared ? { declared } : {}
|
|
9125
8914
|
},
|
|
9126
|
-
deps.signal
|
|
8915
|
+
deps.signal,
|
|
8916
|
+
{ agentId: deps.agentId }
|
|
9127
8917
|
);
|
|
9128
8918
|
},
|
|
9129
8919
|
reportTool: async ({
|
|
@@ -9182,7 +8972,9 @@ var init_turn_committer = __esm({
|
|
|
9182
8972
|
commit,
|
|
9183
8973
|
signal: deps.signal,
|
|
9184
8974
|
nextSeq: deps.nextSeq,
|
|
9185
|
-
...deps.spoke !== void 0 ? { spoke: deps.spoke } : {}
|
|
8975
|
+
...deps.spoke !== void 0 ? { spoke: deps.spoke } : {},
|
|
8976
|
+
...deps.armed !== void 0 ? { armed: deps.armed } : {},
|
|
8977
|
+
...deps.declaredSeq !== void 0 ? { declaredSeq: deps.declaredSeq } : {}
|
|
9186
8978
|
});
|
|
9187
8979
|
this.emit = sinkEmitter(this.pump.sink, this.onError);
|
|
9188
8980
|
}
|
|
@@ -9204,14 +8996,15 @@ var init_turn_committer = __esm({
|
|
|
9204
8996
|
}
|
|
9205
8997
|
}
|
|
9206
8998
|
// A message has landed under this span — its first words at least. A span
|
|
9207
|
-
// that spoke settles; one that never did is a pass.
|
|
9208
|
-
// the
|
|
9209
|
-
//
|
|
9210
|
-
//
|
|
8999
|
+
// that spoke settles; one that never did is a pass. The held-text flush is
|
|
9000
|
+
// the adapter's (it emits the closing reply as a terminal `text` event before
|
|
9001
|
+
// `result`); a span that ended on working text names that text as its last,
|
|
9002
|
+
// and the server makes it the body at the end.
|
|
9211
9003
|
get spoke() {
|
|
9212
9004
|
return this.pump.spoke;
|
|
9213
9005
|
}
|
|
9214
|
-
//
|
|
9006
|
+
// What the settle names by `seq`: the declared answer, or the turn's last
|
|
9007
|
+
// text through either door; null when the turn wrote none.
|
|
9215
9008
|
get lastMessageSeq() {
|
|
9216
9009
|
return this.pump.lastMessageSeq;
|
|
9217
9010
|
}
|
|
@@ -9317,7 +9110,7 @@ var init_turn_runtime_integrity = __esm({
|
|
|
9317
9110
|
|
|
9318
9111
|
// src/turn-execution.ts
|
|
9319
9112
|
import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
|
|
9320
|
-
import { mkdirSync as mkdirSync9 } from "fs";
|
|
9113
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync9 } from "fs";
|
|
9321
9114
|
function resolveIdleTimeoutMs(override) {
|
|
9322
9115
|
return override ?? DEFAULT_AGENT_IDLE_TIMEOUT_MS;
|
|
9323
9116
|
}
|
|
@@ -9374,7 +9167,6 @@ var init_turn_execution = __esm({
|
|
|
9374
9167
|
init_errors();
|
|
9375
9168
|
init_turn_seq_floor();
|
|
9376
9169
|
init_prepare_hook();
|
|
9377
|
-
init_prepare_failure();
|
|
9378
9170
|
init_prepared();
|
|
9379
9171
|
init_secrets();
|
|
9380
9172
|
init_turn_env();
|
|
@@ -9383,7 +9175,6 @@ var init_turn_execution = __esm({
|
|
|
9383
9175
|
init_turn_containment();
|
|
9384
9176
|
init_turn_committer();
|
|
9385
9177
|
init_turn_runtime_integrity();
|
|
9386
|
-
init_dispatcher();
|
|
9387
9178
|
PREPARING_TOOL_NAME = "preparing";
|
|
9388
9179
|
HARNESS_UNAVAILABLE_NOTICE = "**This harness isn't available on the companion.** Check the harness on the device, or choose another harness in the agent's settings. Then reply to try again.";
|
|
9389
9180
|
NO_TURN_TOKEN_REASON = "no_turn_token: the server issued no turn credential for this turn";
|
|
@@ -9442,6 +9233,12 @@ var init_turn_execution = __esm({
|
|
|
9442
9233
|
// Whether the span this resumed run continues already has first words —
|
|
9443
9234
|
// from the server (`turnSpoke`) or from speech still queued in the outbox.
|
|
9444
9235
|
resumedSpoke = false;
|
|
9236
|
+
// Whether the resumed run starts with the `answer` act armed: the server
|
|
9237
|
+
// holds an accepted act (`answerArmed`) and this device's outbox holds no
|
|
9238
|
+
// `declared` commit for the turn — a queued selection stands, and the run
|
|
9239
|
+
// starts declared at its seq instead.
|
|
9240
|
+
resumedArmed = false;
|
|
9241
|
+
resumedDeclaredSeq;
|
|
9445
9242
|
turnContext;
|
|
9446
9243
|
resolvedMcpServers;
|
|
9447
9244
|
// The harness's secret environment for this turn. Only ever handed to the
|
|
@@ -9627,6 +9424,7 @@ var init_turn_execution = __esm({
|
|
|
9627
9424
|
const { payload, turnId, turnLog } = this;
|
|
9628
9425
|
const outboxFloor = this.resumed ? readOutboxFloor((id) => this.opts.api.outboxMaxSeqForTurn(id), turnId, turnLog) : 0;
|
|
9629
9426
|
const outboxSpoke = this.resumed && this.opts.api.outboxHasSpeechForTurn(turnId);
|
|
9427
|
+
const outboxDeclaredSeq = this.resumed ? this.opts.api.outboxDeclaredSeqForTurn(turnId) : null;
|
|
9630
9428
|
let turnContext;
|
|
9631
9429
|
try {
|
|
9632
9430
|
turnContext = await this.opts.api.getTurnContext(
|
|
@@ -9681,6 +9479,16 @@ var init_turn_execution = __esm({
|
|
|
9681
9479
|
this.seqCounter = Math.max(this.seqCounter, this.resumedFromSeq);
|
|
9682
9480
|
this.resumedSpoke = turnContext.turnSpoke === true || outboxSpoke;
|
|
9683
9481
|
if (this.resumedSpoke) turnLog.debug({ turnId }, "dispatcher: resumed span already spoke");
|
|
9482
|
+
this.resumedArmed = turnContext.answerArmed === true && outboxDeclaredSeq === null;
|
|
9483
|
+
this.resumedDeclaredSeq = outboxDeclaredSeq ?? void 0;
|
|
9484
|
+
if (this.resumedArmed)
|
|
9485
|
+
turnLog.debug({ turnId }, "dispatcher: resumed turn re-armed its answer");
|
|
9486
|
+
if (outboxDeclaredSeq !== null) {
|
|
9487
|
+
turnLog.debug(
|
|
9488
|
+
{ turnId, seq: outboxDeclaredSeq },
|
|
9489
|
+
"dispatcher: resumed turn keeps the answer queued in its outbox"
|
|
9490
|
+
);
|
|
9491
|
+
}
|
|
9684
9492
|
}
|
|
9685
9493
|
}
|
|
9686
9494
|
// Register this context's issuance as the execution source of the work about to
|
|
@@ -9764,7 +9572,7 @@ var init_turn_execution = __esm({
|
|
|
9764
9572
|
async prepareEnvironment() {
|
|
9765
9573
|
const { payload, workspaceId, turnId, turnLog } = this;
|
|
9766
9574
|
const prepareHook = this.opts.local.prepareHook;
|
|
9767
|
-
const pairDir = this.opts.
|
|
9575
|
+
const pairDir = this.opts.dedicatedRoot ? this.opts.workRoot : pairDirectory(this.opts.workRoot, {
|
|
9768
9576
|
workspaceSlug: this.opts.workspaceSlug,
|
|
9769
9577
|
agentUsername: this.opts.agentUsername,
|
|
9770
9578
|
conversationId: payload.conversationId
|
|
@@ -9792,10 +9600,10 @@ var init_turn_execution = __esm({
|
|
|
9792
9600
|
let hookEnv;
|
|
9793
9601
|
if (prepareHook) {
|
|
9794
9602
|
let cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
|
|
9795
|
-
if (cached2 && !
|
|
9603
|
+
if (cached2 && !existsSync12(cached2.cwd)) {
|
|
9796
9604
|
turnLog.debug(
|
|
9797
|
-
{ cwd: cached2.cwd
|
|
9798
|
-
"dispatcher: the prepared
|
|
9605
|
+
{ cwd: cached2.cwd },
|
|
9606
|
+
"dispatcher: the prepared directory is gone \u2014 re-running the prepare hook"
|
|
9799
9607
|
);
|
|
9800
9608
|
clearPrepared(workspaceId, payload.conversationId, payload.agentId);
|
|
9801
9609
|
cached2 = null;
|
|
@@ -9803,38 +9611,6 @@ var init_turn_execution = __esm({
|
|
|
9803
9611
|
if (cached2) {
|
|
9804
9612
|
effectiveCwd = cached2.cwd;
|
|
9805
9613
|
hookEnv = cached2.env;
|
|
9806
|
-
if (prepareHook.validateCached) {
|
|
9807
|
-
const runHook = this.opts.prepareHookRunner ?? runPrepareHook;
|
|
9808
|
-
try {
|
|
9809
|
-
await runPrepareWithReporting(
|
|
9810
|
-
runHook,
|
|
9811
|
-
prepareHook,
|
|
9812
|
-
{
|
|
9813
|
-
workspaceId,
|
|
9814
|
-
conversationId: payload.conversationId,
|
|
9815
|
-
agentId: payload.agentId,
|
|
9816
|
-
agentUsername: this.opts.agentUsername,
|
|
9817
|
-
defaultCwd: pairDir,
|
|
9818
|
-
runtime: this.turnContext.runtime,
|
|
9819
|
-
triggerEntryPaths: this.turnContext.conversation.triggerEntryPaths ?? [],
|
|
9820
|
-
title: this.turnContext.conversation.title,
|
|
9821
|
-
messageBody: this.turnContext.message.body,
|
|
9822
|
-
prepared: cached2
|
|
9823
|
-
},
|
|
9824
|
-
{ turnId, deviceId: this.opts.deviceId },
|
|
9825
|
-
() => {
|
|
9826
|
-
turnLog.error(
|
|
9827
|
-
"dispatcher: prepare failure reporter failed; original failure preserved"
|
|
9828
|
-
);
|
|
9829
|
-
}
|
|
9830
|
-
);
|
|
9831
|
-
} catch (err) {
|
|
9832
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
9833
|
-
turnLog.debug({ err: reason }, "dispatcher: prepare hook rejected a prepared turn");
|
|
9834
|
-
const failReason = `prepare_failed: ${reason}`;
|
|
9835
|
-
throw this.concluded(failReason, failReason);
|
|
9836
|
-
}
|
|
9837
|
-
}
|
|
9838
9614
|
} else {
|
|
9839
9615
|
const delayMs = this.opts.preparingRowDelayMs ?? DEFAULT_PREPARING_ROW_DELAY_MS;
|
|
9840
9616
|
let preparingStarted = false;
|
|
@@ -9866,33 +9642,17 @@ var init_turn_execution = __esm({
|
|
|
9866
9642
|
preparingTimer.unref?.();
|
|
9867
9643
|
const runHook = this.opts.prepareHookRunner ?? runPrepareHook;
|
|
9868
9644
|
try {
|
|
9869
|
-
const result = await
|
|
9870
|
-
|
|
9871
|
-
|
|
9872
|
-
|
|
9873
|
-
|
|
9874
|
-
|
|
9875
|
-
|
|
9876
|
-
|
|
9877
|
-
|
|
9878
|
-
|
|
9879
|
-
|
|
9880
|
-
runtime: this.turnContext.runtime,
|
|
9881
|
-
// The trigger message's referenced-entry paths, which a prepare hook
|
|
9882
|
-
// may key its per-task env off. `[]` from an older API.
|
|
9883
|
-
triggerEntryPaths: this.turnContext.conversation.triggerEntryPaths ?? [],
|
|
9884
|
-
title: this.turnContext.conversation.title,
|
|
9885
|
-
// Where an `env:` directive rides, so a dispatch can ask for its
|
|
9886
|
-
// environment in words.
|
|
9887
|
-
messageBody: this.turnContext.message.body
|
|
9888
|
-
},
|
|
9889
|
-
{ turnId, deviceId: this.opts.deviceId },
|
|
9890
|
-
() => {
|
|
9891
|
-
turnLog.error(
|
|
9892
|
-
"dispatcher: prepare failure reporter failed; original failure preserved"
|
|
9893
|
-
);
|
|
9894
|
-
}
|
|
9895
|
-
);
|
|
9645
|
+
const result = await runHook(prepareHook, {
|
|
9646
|
+
workspaceId,
|
|
9647
|
+
conversationId: payload.conversationId,
|
|
9648
|
+
agentId: payload.agentId,
|
|
9649
|
+
agentUsername: this.opts.agentUsername,
|
|
9650
|
+
// The pair's own directory, already made, for a hook that only
|
|
9651
|
+
// wants to add to it.
|
|
9652
|
+
defaultCwd: pairDir,
|
|
9653
|
+
runtime: this.turnContext.runtime,
|
|
9654
|
+
title: this.turnContext.conversation.title
|
|
9655
|
+
});
|
|
9896
9656
|
clearTimeout(preparingTimer);
|
|
9897
9657
|
if (preparingStarted) reportPreparing("done");
|
|
9898
9658
|
writePrepared(workspaceId, payload.conversationId, payload.agentId, result);
|
|
@@ -10188,6 +9948,8 @@ var init_turn_execution = __esm({
|
|
|
10188
9948
|
log: turnLog,
|
|
10189
9949
|
nextSeq: this.nextSeq,
|
|
10190
9950
|
spoke: this.resumedSpoke,
|
|
9951
|
+
armed: this.resumedArmed,
|
|
9952
|
+
...this.resumedDeclaredSeq !== void 0 ? { declaredSeq: this.resumedDeclaredSeq } : {},
|
|
10191
9953
|
// Admitted by now, so this is the source the model's work runs on.
|
|
10192
9954
|
...this.executionSourceId !== void 0 ? { executionSourceId: this.executionSourceId } : {},
|
|
10193
9955
|
// A commit that didn't land may mean the lease is gone; see
|
|
@@ -10537,171 +10299,38 @@ var init_turn_execution = __esm({
|
|
|
10537
10299
|
}
|
|
10538
10300
|
});
|
|
10539
10301
|
|
|
10540
|
-
// src/
|
|
10541
|
-
import {
|
|
10542
|
-
|
|
10302
|
+
// src/outbox.ts
|
|
10303
|
+
import {
|
|
10304
|
+
existsSync as existsSync14,
|
|
10305
|
+
mkdirSync as mkdirSync10,
|
|
10306
|
+
readdirSync as readdirSync2,
|
|
10307
|
+
readFileSync as readFileSync11,
|
|
10308
|
+
renameSync as renameSync5,
|
|
10309
|
+
rmSync as rmSync7,
|
|
10310
|
+
writeFileSync as writeFileSync6
|
|
10311
|
+
} from "fs";
|
|
10543
10312
|
import { join as join22 } from "path";
|
|
10544
|
-
|
|
10545
|
-
|
|
10546
|
-
|
|
10547
|
-
let entries;
|
|
10548
|
-
try {
|
|
10549
|
-
entries = readdirSync2(cwd);
|
|
10550
|
-
} catch (error) {
|
|
10551
|
-
return { ok: false, reason: `${cwd} is unreadable (${error.message})` };
|
|
10552
|
-
}
|
|
10553
|
-
if (entries.length === 0) {
|
|
10554
|
-
return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
|
|
10555
|
-
}
|
|
10556
|
-
const gitPath = join22(cwd, ".git");
|
|
10557
|
-
if (!existsSync12(gitPath)) return { ok: true, reason: "usable" };
|
|
10558
|
-
let stat;
|
|
10559
|
-
try {
|
|
10560
|
-
stat = statSync4(gitPath);
|
|
10561
|
-
} catch (error) {
|
|
10562
|
-
return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
|
|
10563
|
-
}
|
|
10564
|
-
if (stat.isDirectory() && !existsSync12(join22(gitPath, "HEAD")))
|
|
10565
|
-
return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
|
|
10566
|
-
return { ok: true, reason: "usable" };
|
|
10567
|
-
}
|
|
10568
|
-
function runKey(conversationId, agentId) {
|
|
10569
|
-
return `${conversationId}|${agentId}`;
|
|
10570
|
-
}
|
|
10571
|
-
var Dispatcher;
|
|
10572
|
-
var init_dispatcher = __esm({
|
|
10573
|
-
"src/dispatcher.ts"() {
|
|
10313
|
+
var MAX_ENTRIES, MAX_AGE_MS, Outbox;
|
|
10314
|
+
var init_outbox = __esm({
|
|
10315
|
+
"src/outbox.ts"() {
|
|
10574
10316
|
"use strict";
|
|
10575
|
-
|
|
10576
|
-
|
|
10577
|
-
|
|
10578
|
-
|
|
10579
|
-
|
|
10580
|
-
this.
|
|
10317
|
+
init_config();
|
|
10318
|
+
MAX_ENTRIES = 2e3;
|
|
10319
|
+
MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
10320
|
+
Outbox = class {
|
|
10321
|
+
constructor(workspaceId, log2) {
|
|
10322
|
+
this.workspaceId = workspaceId;
|
|
10323
|
+
this.log = log2;
|
|
10581
10324
|
}
|
|
10582
|
-
|
|
10583
|
-
|
|
10584
|
-
//
|
|
10585
|
-
|
|
10586
|
-
|
|
10587
|
-
|
|
10588
|
-
sessionWriteNotified = /* @__PURE__ */ new Set();
|
|
10589
|
-
notifyStart(info) {
|
|
10590
|
-
try {
|
|
10591
|
-
this.opts.observer?.onStart(info);
|
|
10592
|
-
} catch {
|
|
10593
|
-
}
|
|
10594
|
-
}
|
|
10595
|
-
notifyEnd(info) {
|
|
10596
|
-
try {
|
|
10597
|
-
this.opts.observer?.onEnd(info);
|
|
10598
|
-
} catch {
|
|
10599
|
-
}
|
|
10600
|
-
}
|
|
10601
|
-
// The supervisor supplies a stable turnId on both fresh and resumed dispatch.
|
|
10602
|
-
// resumed is separate: only replay seeds sequence/speech recovery from the
|
|
10603
|
-
// prior span. A direct caller omitting turnId gets a fresh id.
|
|
10604
|
-
async handle(payload, opts = {}) {
|
|
10605
|
-
const key = runKey(payload.conversationId, payload.agentId);
|
|
10606
|
-
return runTurnInRunner(
|
|
10607
|
-
this.opts.turn,
|
|
10608
|
-
this.opts.runner,
|
|
10609
|
-
{
|
|
10610
|
-
registerAbort: (turnId, controller) => {
|
|
10611
|
-
let pairAborts = this.aborts.get(key);
|
|
10612
|
-
if (!pairAborts) {
|
|
10613
|
-
pairAborts = /* @__PURE__ */ new Map();
|
|
10614
|
-
this.aborts.set(key, pairAborts);
|
|
10615
|
-
}
|
|
10616
|
-
pairAborts.set(turnId, controller);
|
|
10617
|
-
},
|
|
10618
|
-
releaseAbort: (turnId, controller) => {
|
|
10619
|
-
const pair = this.aborts.get(key);
|
|
10620
|
-
if (pair?.get(turnId) !== controller) return;
|
|
10621
|
-
pair.delete(turnId);
|
|
10622
|
-
if (pair.size === 0) this.aborts.delete(key);
|
|
10623
|
-
},
|
|
10624
|
-
sessionWriteNoticeClaimed: () => this.sessionWriteNotified.has(key),
|
|
10625
|
-
markSessionWriteNotice: () => {
|
|
10626
|
-
this.sessionWriteNotified.add(key);
|
|
10627
|
-
},
|
|
10628
|
-
notifyStart: (info) => this.notifyStart(info),
|
|
10629
|
-
notifyEnd: (info) => this.notifyEnd(info)
|
|
10630
|
-
},
|
|
10631
|
-
payload,
|
|
10632
|
-
{ turnId: opts.turnId ?? randomUUID4(), resumed: opts.resumed === true }
|
|
10633
|
-
);
|
|
10634
|
-
}
|
|
10635
|
-
// Cancel the named turn. An older server omits turnId and cancels the pair.
|
|
10636
|
-
cancel(conversationId, agentId, turnId) {
|
|
10637
|
-
const key = runKey(conversationId, agentId);
|
|
10638
|
-
const pair = this.aborts.get(key);
|
|
10639
|
-
if (!pair || pair.size === 0) return false;
|
|
10640
|
-
const controllers = turnId === void 0 ? [...pair.values()] : [pair.get(turnId)];
|
|
10641
|
-
let aborted = false;
|
|
10642
|
-
for (const ac of controllers) {
|
|
10643
|
-
if (!ac) continue;
|
|
10644
|
-
aborted = true;
|
|
10645
|
-
try {
|
|
10646
|
-
ac.abort();
|
|
10647
|
-
} catch {
|
|
10648
|
-
}
|
|
10649
|
-
}
|
|
10650
|
-
return aborted;
|
|
10651
|
-
}
|
|
10652
|
-
// Abort every run in flight in THIS process, whatever its conversation. For
|
|
10653
|
-
// an agent taken off this device: nothing it is still running here is
|
|
10654
|
-
// legitimate, and a cancel frame that arrives after it is dropped from the
|
|
10655
|
-
// roster finds no dispatcher to reach. True if any was aborted.
|
|
10656
|
-
cancelAll() {
|
|
10657
|
-
let aborted = false;
|
|
10658
|
-
for (const pair of this.aborts.values()) {
|
|
10659
|
-
for (const ac of [...pair.values()]) {
|
|
10660
|
-
try {
|
|
10661
|
-
ac.abort();
|
|
10662
|
-
aborted = true;
|
|
10663
|
-
} catch {
|
|
10664
|
-
}
|
|
10665
|
-
}
|
|
10666
|
-
}
|
|
10667
|
-
return aborted;
|
|
10668
|
-
}
|
|
10669
|
-
};
|
|
10670
|
-
}
|
|
10671
|
-
});
|
|
10672
|
-
|
|
10673
|
-
// src/outbox.ts
|
|
10674
|
-
import {
|
|
10675
|
-
existsSync as existsSync14,
|
|
10676
|
-
mkdirSync as mkdirSync10,
|
|
10677
|
-
readdirSync as readdirSync3,
|
|
10678
|
-
readFileSync as readFileSync11,
|
|
10679
|
-
renameSync as renameSync5,
|
|
10680
|
-
rmSync as rmSync7,
|
|
10681
|
-
writeFileSync as writeFileSync6
|
|
10682
|
-
} from "fs";
|
|
10683
|
-
import { join as join23 } from "path";
|
|
10684
|
-
var MAX_ENTRIES, MAX_AGE_MS, Outbox;
|
|
10685
|
-
var init_outbox = __esm({
|
|
10686
|
-
"src/outbox.ts"() {
|
|
10687
|
-
"use strict";
|
|
10688
|
-
init_config();
|
|
10689
|
-
MAX_ENTRIES = 2e3;
|
|
10690
|
-
MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
10691
|
-
Outbox = class {
|
|
10692
|
-
constructor(workspaceId, log2) {
|
|
10693
|
-
this.workspaceId = workspaceId;
|
|
10694
|
-
this.log = log2;
|
|
10695
|
-
}
|
|
10696
|
-
workspaceId;
|
|
10697
|
-
log;
|
|
10698
|
-
// Resolved lazily (per call) so tests that swap HOME between
|
|
10699
|
-
// cases route writes at the right tmpdir.
|
|
10700
|
-
dir() {
|
|
10701
|
-
return join23(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
|
|
10325
|
+
workspaceId;
|
|
10326
|
+
log;
|
|
10327
|
+
// Resolved lazily (per call) so tests that swap HOME between
|
|
10328
|
+
// cases route writes at the right tmpdir.
|
|
10329
|
+
dir() {
|
|
10330
|
+
return join22(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
|
|
10702
10331
|
}
|
|
10703
10332
|
fileFor(turnId, seq) {
|
|
10704
|
-
return
|
|
10333
|
+
return join22(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
|
|
10705
10334
|
}
|
|
10706
10335
|
// Persist a commit for later draining. Atomic (temp file + rename) so a
|
|
10707
10336
|
// concurrent `list()` never reads a half-written entry, then enforces the
|
|
@@ -10736,14 +10365,14 @@ var init_outbox = __esm({
|
|
|
10736
10365
|
if (!existsSync14(dir2)) return [];
|
|
10737
10366
|
let names;
|
|
10738
10367
|
try {
|
|
10739
|
-
names =
|
|
10368
|
+
names = readdirSync2(dir2);
|
|
10740
10369
|
} catch {
|
|
10741
10370
|
return [];
|
|
10742
10371
|
}
|
|
10743
10372
|
const entries = [];
|
|
10744
10373
|
for (const name of names) {
|
|
10745
10374
|
if (!name.endsWith(".json")) continue;
|
|
10746
|
-
const full =
|
|
10375
|
+
const full = join22(dir2, name);
|
|
10747
10376
|
try {
|
|
10748
10377
|
const parsed = JSON.parse(readFileSync11(full, "utf8"));
|
|
10749
10378
|
if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
|
|
@@ -10780,7 +10409,7 @@ var init_outbox = __esm({
|
|
|
10780
10409
|
maxSeqForTurn(turnId) {
|
|
10781
10410
|
const dir2 = this.dir();
|
|
10782
10411
|
if (!existsSync14(dir2)) return 0;
|
|
10783
|
-
const names =
|
|
10412
|
+
const names = readdirSync2(dir2);
|
|
10784
10413
|
const prefix = `${encodeURIComponent(turnId)}__`;
|
|
10785
10414
|
let max = 0;
|
|
10786
10415
|
for (const name of names) {
|
|
@@ -10801,7 +10430,7 @@ var init_outbox = __esm({
|
|
|
10801
10430
|
const dir2 = this.dir();
|
|
10802
10431
|
if (!existsSync14(dir2)) return 0;
|
|
10803
10432
|
try {
|
|
10804
|
-
return
|
|
10433
|
+
return readdirSync2(dir2).filter((n) => n.endsWith(".json")).length;
|
|
10805
10434
|
} catch {
|
|
10806
10435
|
return 0;
|
|
10807
10436
|
}
|
|
@@ -10850,7 +10479,7 @@ var init_outbox = __esm({
|
|
|
10850
10479
|
|
|
10851
10480
|
// src/runner/exec-shim.ts
|
|
10852
10481
|
import { chmodSync as chmodSync6, mkdirSync as mkdirSync18, mkdtempSync, rmSync as rmSync13, writeFileSync as writeFileSync11 } from "fs";
|
|
10853
|
-
import { join as
|
|
10482
|
+
import { join as join32 } from "path";
|
|
10854
10483
|
function shellQuote(value) {
|
|
10855
10484
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
10856
10485
|
}
|
|
@@ -10860,10 +10489,10 @@ function createShimDir() {
|
|
|
10860
10489
|
return {
|
|
10861
10490
|
write(argv) {
|
|
10862
10491
|
if (!dir2) {
|
|
10863
|
-
mkdirSync18(
|
|
10864
|
-
dir2 = mkdtempSync(
|
|
10492
|
+
mkdirSync18(join32(cabaneDir(), "runner"), { recursive: true });
|
|
10493
|
+
dir2 = mkdtempSync(join32(cabaneDir(), "runner", "turn-"));
|
|
10865
10494
|
}
|
|
10866
|
-
const path =
|
|
10495
|
+
const path = join32(dir2, `exec-${++count}`);
|
|
10867
10496
|
writeFileSync11(path, `#!/bin/sh
|
|
10868
10497
|
exec ${argv.map(shellQuote).join(" ")} "$@"
|
|
10869
10498
|
`);
|
|
@@ -11030,13 +10659,13 @@ function runnerTurnOptions(input, log2, emit, extras = {}) {
|
|
|
11030
10659
|
local: spec.local,
|
|
11031
10660
|
// The place the runner stands in names its own root and transcript
|
|
11032
10661
|
// directory when it has them (`PLACE_ENV`); the launcher's host-computed
|
|
11033
|
-
// values are what a runner on the person's own machine gets. A
|
|
11034
|
-
// conversation × agent's, so the turn stands in
|
|
10662
|
+
// values are what a runner on the person's own machine gets. A dedicated
|
|
10663
|
+
// root is one conversation × agent's, so the turn stands in it itself.
|
|
11035
10664
|
workRoot: input.place?.workRoot ?? spec.workRoot,
|
|
11036
|
-
...input.place?.
|
|
10665
|
+
...input.place?.dedicatedRoot ? { dedicatedRoot: true } : {},
|
|
11037
10666
|
runConfig: spec.runConfig,
|
|
11038
|
-
// The launcher's copy of the device's secrets, when it sent one: a
|
|
11039
|
-
//
|
|
10667
|
+
// The launcher's copy of the device's secrets, when it sent one: a wrapped
|
|
10668
|
+
// runner may have no `secrets.json` to read.
|
|
11040
10669
|
...input.secrets ? { secretStore: makeStore(input.secrets) } : {},
|
|
11041
10670
|
// The grants-file entries the launcher read for this dispatch's turn.
|
|
11042
10671
|
...input.grants ? { deviceGrants: input.grants } : {},
|
|
@@ -11900,7 +11529,7 @@ async function liveCompanionState() {
|
|
|
11900
11529
|
// src/commands/setup.ts
|
|
11901
11530
|
init_cabane_command();
|
|
11902
11531
|
init_config();
|
|
11903
|
-
import { hostname as
|
|
11532
|
+
import { hostname as hostname2 } from "os";
|
|
11904
11533
|
|
|
11905
11534
|
// src/logger.ts
|
|
11906
11535
|
init_config();
|
|
@@ -12126,8 +11755,8 @@ async function claimSetupCode(baseUrl, input) {
|
|
|
12126
11755
|
..."deviceLabel" in body && typeof body.deviceLabel === "string" ? { deviceLabel: body.deviceLabel } : {}
|
|
12127
11756
|
};
|
|
12128
11757
|
}
|
|
12129
|
-
function deviceLabelFromHostname(
|
|
12130
|
-
const trimmed =
|
|
11758
|
+
function deviceLabelFromHostname(hostname3) {
|
|
11759
|
+
const trimmed = hostname3.trim().replace(/\.local$/i, "");
|
|
12131
11760
|
return trimmed.length ? trimmed.slice(0, 120) : void 0;
|
|
12132
11761
|
}
|
|
12133
11762
|
|
|
@@ -12672,7 +12301,7 @@ var emptyHeartbeatStatus = () => ({
|
|
|
12672
12301
|
|
|
12673
12302
|
// src/supervisor.ts
|
|
12674
12303
|
init_cabane_command();
|
|
12675
|
-
import { hostname
|
|
12304
|
+
import { hostname } from "os";
|
|
12676
12305
|
|
|
12677
12306
|
// src/log-copy.ts
|
|
12678
12307
|
init_failure();
|
|
@@ -12898,241 +12527,745 @@ function save(deviceId, map) {
|
|
|
12898
12527
|
} catch {
|
|
12899
12528
|
}
|
|
12900
12529
|
}
|
|
12901
|
-
const path = devicePath(deviceId);
|
|
12902
|
-
const tmp = `${path}.${process.pid}.tmp`;
|
|
12903
|
-
try {
|
|
12904
|
-
writeFileSync3(tmp, JSON.stringify(map, null, 2) + "\n", { mode: 384 });
|
|
12530
|
+
const path = devicePath(deviceId);
|
|
12531
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
12532
|
+
try {
|
|
12533
|
+
writeFileSync3(tmp, JSON.stringify(map, null, 2) + "\n", { mode: 384 });
|
|
12534
|
+
try {
|
|
12535
|
+
chmodSync3(tmp, 384);
|
|
12536
|
+
} catch {
|
|
12537
|
+
}
|
|
12538
|
+
renameSync3(tmp, path);
|
|
12539
|
+
} catch (err) {
|
|
12540
|
+
try {
|
|
12541
|
+
rmSync4(tmp, { force: true });
|
|
12542
|
+
} catch {
|
|
12543
|
+
}
|
|
12544
|
+
throw err;
|
|
12545
|
+
}
|
|
12546
|
+
}
|
|
12547
|
+
function getCredential(deviceId, agentId) {
|
|
12548
|
+
return load(deviceId)[agentId] ?? null;
|
|
12549
|
+
}
|
|
12550
|
+
function setCredential(deviceId, agentId, pat) {
|
|
12551
|
+
if (read(devicePath(deviceId))?.[agentId] === pat) return;
|
|
12552
|
+
const map = load(deviceId);
|
|
12553
|
+
map[agentId] = pat;
|
|
12554
|
+
save(deviceId, map);
|
|
12555
|
+
}
|
|
12556
|
+
function pruneCredentials(deviceId, keepAgentIds) {
|
|
12557
|
+
const keep = new Set(keepAgentIds);
|
|
12558
|
+
if (keep.size === 0) return [];
|
|
12559
|
+
const own = read(devicePath(deviceId));
|
|
12560
|
+
const map = own ?? load(deviceId);
|
|
12561
|
+
const removed = Object.keys(map).filter((id) => !keep.has(id));
|
|
12562
|
+
for (const id of removed) delete map[id];
|
|
12563
|
+
if (removed.length > 0 || own === null) save(deviceId, map);
|
|
12564
|
+
return removed;
|
|
12565
|
+
}
|
|
12566
|
+
function clearCredentials() {
|
|
12567
|
+
rmSync4(credentialsDir(), { recursive: true, force: true });
|
|
12568
|
+
if (existsSync7(legacyPath())) writeFileSync3(legacyPath(), "", { mode: 384 });
|
|
12569
|
+
}
|
|
12570
|
+
|
|
12571
|
+
// src/dispatch-dedupe.ts
|
|
12572
|
+
init_config();
|
|
12573
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync as renameSync4, existsSync as existsSync8 } from "fs";
|
|
12574
|
+
import { join as join13 } from "path";
|
|
12575
|
+
var MAX_IDS = 256;
|
|
12576
|
+
function dir(log2) {
|
|
12577
|
+
return join13(cabaneDir(), log2);
|
|
12578
|
+
}
|
|
12579
|
+
function pathFor(log2, workspaceId) {
|
|
12580
|
+
return join13(dir(log2), encodeURIComponent(workspaceId));
|
|
12581
|
+
}
|
|
12582
|
+
function readIds(log2, workspaceId) {
|
|
12583
|
+
const path = pathFor(log2, workspaceId);
|
|
12584
|
+
if (!existsSync8(path)) return [];
|
|
12585
|
+
try {
|
|
12586
|
+
return readFileSync5(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
12587
|
+
} catch {
|
|
12588
|
+
return [];
|
|
12589
|
+
}
|
|
12590
|
+
}
|
|
12591
|
+
function has(log2, workspaceId, eventId) {
|
|
12592
|
+
return readIds(log2, workspaceId).includes(eventId);
|
|
12593
|
+
}
|
|
12594
|
+
function mark(log2, workspaceId, eventId) {
|
|
12595
|
+
const ids = readIds(log2, workspaceId);
|
|
12596
|
+
if (ids.includes(eventId)) return;
|
|
12597
|
+
ids.push(eventId);
|
|
12598
|
+
const trimmed = ids.length > MAX_IDS ? ids.slice(-MAX_IDS) : ids;
|
|
12599
|
+
mkdirSync6(dir(log2), { recursive: true });
|
|
12600
|
+
writeFileSync4(pathFor(log2, workspaceId), trimmed.join("\n") + "\n", "utf8");
|
|
12601
|
+
}
|
|
12602
|
+
function hasDispatched(workspaceId, eventId) {
|
|
12603
|
+
return has("dispatched", workspaceId, eventId);
|
|
12604
|
+
}
|
|
12605
|
+
function markDispatched(workspaceId, eventId) {
|
|
12606
|
+
mark("dispatched", workspaceId, eventId);
|
|
12607
|
+
}
|
|
12608
|
+
function hasCompleted(workspaceId, eventId) {
|
|
12609
|
+
return has("completed", workspaceId, eventId);
|
|
12610
|
+
}
|
|
12611
|
+
function markCompleted(workspaceId, eventId) {
|
|
12612
|
+
mark("completed", workspaceId, eventId);
|
|
12613
|
+
forgetTurnId(workspaceId, eventId);
|
|
12614
|
+
}
|
|
12615
|
+
var MAX_RESUME_ATTEMPTS = 3;
|
|
12616
|
+
function resumeDir() {
|
|
12617
|
+
return join13(cabaneDir(), "resume-attempts");
|
|
12618
|
+
}
|
|
12619
|
+
function resumePathFor(workspaceId) {
|
|
12620
|
+
return join13(resumeDir(), encodeURIComponent(workspaceId));
|
|
12621
|
+
}
|
|
12622
|
+
function readResumeCounts(workspaceId) {
|
|
12623
|
+
const out = /* @__PURE__ */ new Map();
|
|
12624
|
+
const path = resumePathFor(workspaceId);
|
|
12625
|
+
if (!existsSync8(path)) return out;
|
|
12626
|
+
try {
|
|
12627
|
+
for (const line of readFileSync5(path, "utf8").split("\n")) {
|
|
12628
|
+
const trimmed = line.trim();
|
|
12629
|
+
if (!trimmed) continue;
|
|
12630
|
+
const tab = trimmed.lastIndexOf(" ");
|
|
12631
|
+
if (tab <= 0) continue;
|
|
12632
|
+
const id = trimmed.slice(0, tab);
|
|
12633
|
+
const count = Number.parseInt(trimmed.slice(tab + 1), 10);
|
|
12634
|
+
if (id && Number.isFinite(count)) out.set(id, count);
|
|
12635
|
+
}
|
|
12636
|
+
} catch {
|
|
12637
|
+
return out;
|
|
12638
|
+
}
|
|
12639
|
+
return out;
|
|
12640
|
+
}
|
|
12641
|
+
function bumpResumeAttempt(workspaceId, eventId) {
|
|
12642
|
+
const counts = readResumeCounts(workspaceId);
|
|
12643
|
+
const next = (counts.get(eventId) ?? 0) + 1;
|
|
12644
|
+
counts.set(eventId, next);
|
|
12645
|
+
const entries = [...counts.entries()];
|
|
12646
|
+
const trimmed = entries.length > MAX_IDS ? entries.slice(-MAX_IDS) : entries;
|
|
12647
|
+
mkdirSync6(resumeDir(), { recursive: true });
|
|
12648
|
+
writeFileSync4(
|
|
12649
|
+
resumePathFor(workspaceId),
|
|
12650
|
+
trimmed.map(([id, c]) => `${id} ${c}`).join("\n") + "\n",
|
|
12651
|
+
"utf8"
|
|
12652
|
+
);
|
|
12653
|
+
return next;
|
|
12654
|
+
}
|
|
12655
|
+
function turnDir() {
|
|
12656
|
+
return join13(cabaneDir(), "turns");
|
|
12657
|
+
}
|
|
12658
|
+
function turnPathFor(workspaceId) {
|
|
12659
|
+
return join13(turnDir(), encodeURIComponent(workspaceId));
|
|
12660
|
+
}
|
|
12661
|
+
var TURN_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
12662
|
+
function readTurnIds(workspaceId) {
|
|
12663
|
+
const out = /* @__PURE__ */ new Map();
|
|
12664
|
+
const path = turnPathFor(workspaceId);
|
|
12665
|
+
if (!existsSync8(path)) return out;
|
|
12666
|
+
try {
|
|
12667
|
+
for (const line of readFileSync5(path, "utf8").split("\n")) {
|
|
12668
|
+
const trimmed = line.trim();
|
|
12669
|
+
if (!trimmed) continue;
|
|
12670
|
+
const tab = trimmed.lastIndexOf(" ");
|
|
12671
|
+
if (tab <= 0) continue;
|
|
12672
|
+
const id = trimmed.slice(0, tab);
|
|
12673
|
+
const turnId = trimmed.slice(tab + 1);
|
|
12674
|
+
if (id && TURN_ID_RE.test(turnId)) out.set(id, turnId);
|
|
12675
|
+
}
|
|
12676
|
+
} catch {
|
|
12677
|
+
return out;
|
|
12678
|
+
}
|
|
12679
|
+
return out;
|
|
12680
|
+
}
|
|
12681
|
+
function turnIdForEvent(workspaceId, eventId) {
|
|
12682
|
+
return readTurnIds(workspaceId).get(eventId) ?? null;
|
|
12683
|
+
}
|
|
12684
|
+
var TURN_ID_OVERFLOW_WARN = 8192;
|
|
12685
|
+
function writeTurnIds(workspaceId, turns) {
|
|
12686
|
+
const entries = [...turns.entries()];
|
|
12687
|
+
mkdirSync6(turnDir(), { recursive: true });
|
|
12688
|
+
const path = turnPathFor(workspaceId);
|
|
12689
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
12690
|
+
writeFileSync4(tmp, entries.map(([id, t]) => `${id} ${t}`).join("\n") + "\n", "utf8");
|
|
12691
|
+
renameSync4(tmp, path);
|
|
12692
|
+
return entries.length;
|
|
12693
|
+
}
|
|
12694
|
+
function rememberTurnId(workspaceId, eventId, turnId) {
|
|
12695
|
+
const turns = readTurnIds(workspaceId);
|
|
12696
|
+
if (turns.get(eventId) === turnId) return turns.size;
|
|
12697
|
+
turns.set(eventId, turnId);
|
|
12698
|
+
return writeTurnIds(workspaceId, turns);
|
|
12699
|
+
}
|
|
12700
|
+
function forgetTurnId(workspaceId, eventId) {
|
|
12701
|
+
const turns = readTurnIds(workspaceId);
|
|
12702
|
+
if (!turns.delete(eventId)) return;
|
|
12703
|
+
writeTurnIds(workspaceId, turns);
|
|
12704
|
+
}
|
|
12705
|
+
function noResume() {
|
|
12706
|
+
return process.env.CABANE_COMPANION_NO_RESUME === "1";
|
|
12707
|
+
}
|
|
12708
|
+
|
|
12709
|
+
// src/supervisor.ts
|
|
12710
|
+
init_bundled_binaries();
|
|
12711
|
+
init_install_kind();
|
|
12712
|
+
|
|
12713
|
+
// src/connector-health.ts
|
|
12714
|
+
init_src();
|
|
12715
|
+
var ConnectorHealthStore = class {
|
|
12716
|
+
byRuntime = /* @__PURE__ */ new Map();
|
|
12717
|
+
// Record a settled turn's outcome for `runtime`. A success heals to `ok`; a
|
|
12718
|
+
// classified failure sets `limited`(+reset) / `auth_expired` with the reason as
|
|
12719
|
+
// `last_error`; anything else (a user Stop, a timeout, an unclassified error)
|
|
12720
|
+
// leaves the last-known status untouched — we only ever move state on real
|
|
12721
|
+
// evidence.
|
|
12722
|
+
recordSettle(runtime, outcome) {
|
|
12723
|
+
if (outcome.ok) {
|
|
12724
|
+
this.byRuntime.set(runtime, { quotaState: "ok", limitedUntil: null, lastError: null });
|
|
12725
|
+
return;
|
|
12726
|
+
}
|
|
12727
|
+
const failure = decodeFailureReason(outcome.errorReason);
|
|
12728
|
+
if (!failure) return;
|
|
12729
|
+
const quota = quotaForFailure(failure);
|
|
12730
|
+
if (!quota) return;
|
|
12731
|
+
this.byRuntime.set(runtime, {
|
|
12732
|
+
quotaState: quota.quotaState,
|
|
12733
|
+
limitedUntil: quota.limitedUntil,
|
|
12734
|
+
lastError: outcome.errorReason ?? null
|
|
12735
|
+
});
|
|
12736
|
+
}
|
|
12737
|
+
// The store's current opinion for one runtime, or undefined when it has
|
|
12738
|
+
// no observation. Read by the dispatcher at idle-timeout fire: an idle reap
|
|
12739
|
+
// while this runtime is definitively `limited` (a present, future `limitedUntil`
|
|
12740
|
+
// recorded by a sibling/earlier turn's classified cap settle) is the account cap
|
|
12741
|
+
// surfacing as silence, not a genuine hang — so the settle reclassifies to
|
|
12742
|
+
// `usage_capped` instead of `timeout_idle`. Returns the stored shape verbatim
|
|
12743
|
+
// (the dispatcher reads `quotaState` + `limitedUntil` and applies its own
|
|
12744
|
+
// future-instant gate); a read never mutates state.
|
|
12745
|
+
lookup(runtime) {
|
|
12746
|
+
return this.byRuntime.get(runtime);
|
|
12747
|
+
}
|
|
12748
|
+
// The per-connector reports to attach to a heartbeat — one entry per runtime the
|
|
12749
|
+
// companion has an observation for. Empty until the first classified failure/heal,
|
|
12750
|
+
// so a companion that has seen nothing sends no `connectors[]` and the server's
|
|
12751
|
+
// manifest synthesis (status-less rows) is unaffected.
|
|
12752
|
+
reports() {
|
|
12753
|
+
return [...this.byRuntime.entries()].map(([runtime, h]) => ({
|
|
12754
|
+
runtime,
|
|
12755
|
+
quotaState: h.quotaState,
|
|
12756
|
+
limitedUntil: h.limitedUntil,
|
|
12757
|
+
lastError: h.lastError
|
|
12758
|
+
}));
|
|
12759
|
+
}
|
|
12760
|
+
};
|
|
12761
|
+
|
|
12762
|
+
// src/dispatcher.ts
|
|
12763
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
12764
|
+
|
|
12765
|
+
// src/runner/launch.ts
|
|
12766
|
+
init_src();
|
|
12767
|
+
init_opencode_lock();
|
|
12768
|
+
init_api_error_shape();
|
|
12769
|
+
init_errors();
|
|
12770
|
+
init_boot_id();
|
|
12771
|
+
init_turn_containment();
|
|
12772
|
+
init_protocol();
|
|
12773
|
+
import { createInterface } from "readline";
|
|
12774
|
+
function selfRunnerCommand() {
|
|
12775
|
+
const script = process.argv[1];
|
|
12776
|
+
if (!script) throw new Error("runner: cannot tell which script started this companion");
|
|
12777
|
+
return [process.execPath, ...process.execArgv, script, "run-turn"];
|
|
12778
|
+
}
|
|
12779
|
+
function wrappedRunnerCommand(prefix, own) {
|
|
12780
|
+
return [...prefix ?? [], ...own];
|
|
12781
|
+
}
|
|
12782
|
+
var WRAPPED_ENV_DROPPED = [
|
|
12783
|
+
"ANTHROPIC_API_KEY",
|
|
12784
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
12785
|
+
"CODEX_API_KEY",
|
|
12786
|
+
"GH_TOKEN",
|
|
12787
|
+
"GITHUB_TOKEN",
|
|
12788
|
+
"DBUS_SESSION_BUS_ADDRESS",
|
|
12789
|
+
"XDG_RUNTIME_DIR"
|
|
12790
|
+
];
|
|
12791
|
+
function runnerExecEnv(base, wrapped) {
|
|
12792
|
+
const env = {};
|
|
12793
|
+
for (const [k, v] of Object.entries(base)) {
|
|
12794
|
+
if (v === void 0) continue;
|
|
12795
|
+
if (wrapped && WRAPPED_ENV_DROPPED.includes(k)) continue;
|
|
12796
|
+
env[k] = v;
|
|
12797
|
+
}
|
|
12798
|
+
return env;
|
|
12799
|
+
}
|
|
12800
|
+
function healthSnapshot(store) {
|
|
12801
|
+
const out = {};
|
|
12802
|
+
for (const r of store?.reports() ?? []) {
|
|
12803
|
+
out[r.runtime] = {
|
|
12804
|
+
quotaState: r.quotaState ?? "unknown",
|
|
12805
|
+
limitedUntil: r.limitedUntil ?? null,
|
|
12806
|
+
lastError: r.lastError ?? null
|
|
12807
|
+
};
|
|
12808
|
+
}
|
|
12809
|
+
return out;
|
|
12810
|
+
}
|
|
12811
|
+
function runnerSpec(opts, launch, duties, runnerId) {
|
|
12812
|
+
return {
|
|
12813
|
+
runnerId,
|
|
12814
|
+
...opts.deviceId ? { deviceId: opts.deviceId } : {},
|
|
12815
|
+
bootId,
|
|
12816
|
+
workspaceId: opts.workspaceId,
|
|
12817
|
+
workspaceSlug: opts.workspaceSlug,
|
|
12818
|
+
agentId: opts.agentId,
|
|
12819
|
+
agentUsername: opts.agentUsername,
|
|
12820
|
+
agentName: String(opts.log.bindings().agentName ?? opts.agentUsername),
|
|
12821
|
+
local: opts.local,
|
|
12822
|
+
runConfig: opts.runConfig,
|
|
12823
|
+
workRoot: opts.workRoot,
|
|
12824
|
+
...opts.opencodeServerUrl ? { opencodeServerUrl: opts.opencodeServerUrl } : {},
|
|
12825
|
+
// Read now, per turn, the way adapter selection reads them.
|
|
12826
|
+
claudeCodeAvailable: opts.claudeCodeAvailable?.() ?? true,
|
|
12827
|
+
codexAvailable: opts.codexAvailable?.() ?? false,
|
|
12828
|
+
...opts.idleTimeoutMs !== void 0 ? { idleTimeoutMs: opts.idleTimeoutMs } : {},
|
|
12829
|
+
...opts.totalTimeoutMs !== void 0 ? { totalTimeoutMs: opts.totalTimeoutMs } : {},
|
|
12830
|
+
...opts.leaseRenewalMs !== void 0 ? { leaseRenewalMs: opts.leaseRenewalMs } : {},
|
|
12831
|
+
...opts.preparingRowDelayMs !== void 0 ? { preparingRowDelayMs: opts.preparingRowDelayMs } : {},
|
|
12832
|
+
transcripts: opts.transcriptDir !== void 0,
|
|
12833
|
+
logLevel: opts.log.level,
|
|
12834
|
+
connectorHealth: healthSnapshot(launch.connectorHealth),
|
|
12835
|
+
sessionWriteNoticeClaimed: duties.sessionWriteNoticeClaimed()
|
|
12836
|
+
};
|
|
12837
|
+
}
|
|
12838
|
+
var PINO_LEVELS = {
|
|
12839
|
+
10: "trace",
|
|
12840
|
+
20: "debug",
|
|
12841
|
+
30: "info",
|
|
12842
|
+
40: "warn",
|
|
12843
|
+
50: "error",
|
|
12844
|
+
60: "fatal"
|
|
12845
|
+
};
|
|
12846
|
+
function relayRunnerLog(line, log2) {
|
|
12847
|
+
if (!line.trim()) return;
|
|
12848
|
+
let record = null;
|
|
12849
|
+
try {
|
|
12850
|
+
const parsed = JSON.parse(line);
|
|
12851
|
+
if (parsed && typeof parsed === "object") record = parsed;
|
|
12852
|
+
} catch {
|
|
12853
|
+
record = null;
|
|
12854
|
+
}
|
|
12855
|
+
if (!record || typeof record.level !== "number") {
|
|
12856
|
+
log2.debug({ runnerStderr: line }, "runner: output");
|
|
12857
|
+
return;
|
|
12858
|
+
}
|
|
12859
|
+
const {
|
|
12860
|
+
level,
|
|
12861
|
+
msg,
|
|
12862
|
+
time: _time,
|
|
12863
|
+
pid: _pid,
|
|
12864
|
+
hostname: _hostname,
|
|
12865
|
+
agentName: _agentName,
|
|
12866
|
+
...fields
|
|
12867
|
+
} = record;
|
|
12868
|
+
const method = PINO_LEVELS[level] ?? "info";
|
|
12869
|
+
log2[method](fields, typeof msg === "string" ? msg : "");
|
|
12870
|
+
}
|
|
12871
|
+
function lines(stream, onLine) {
|
|
12872
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
12873
|
+
rl.on("line", onLine);
|
|
12874
|
+
return new Promise((resolve2) => rl.once("close", resolve2));
|
|
12875
|
+
}
|
|
12876
|
+
function giveBackLocks(locks, prefix, remote, launch, log2) {
|
|
12877
|
+
locks.withdraw(prefix);
|
|
12878
|
+
const stop2 = launch.stopRemoteSession ?? ((url, id) => stopRemoteOpencodeSession(url, id));
|
|
12879
|
+
for (const { key, owner } of locks.heldBy(prefix)) {
|
|
12880
|
+
const left = remote.filter((r) => opencodeLockKey(r.serverUrl) === key);
|
|
12881
|
+
if (!left.length) {
|
|
12882
|
+
locks.release(owner);
|
|
12883
|
+
continue;
|
|
12884
|
+
}
|
|
12885
|
+
void (async () => {
|
|
12886
|
+
for (let attempt = 1; ; attempt++) {
|
|
12887
|
+
const stopped = await Promise.all(left.map((r) => stop2(r.serverUrl, r.sessionId))).catch(
|
|
12888
|
+
() => [false]
|
|
12889
|
+
);
|
|
12890
|
+
if (stopped.every(Boolean)) break;
|
|
12891
|
+
if (attempt === 1)
|
|
12892
|
+
log2.warn(
|
|
12893
|
+
{ key, sessions: left.map((r) => r.sessionId) },
|
|
12894
|
+
"runner: a dead runner left work on a shared server that could not be confirmed stopped; holding the server until it is"
|
|
12895
|
+
);
|
|
12896
|
+
await new Promise((resolve2) => setTimeout(resolve2, launch.reconcileRetryMs ?? 5e3));
|
|
12897
|
+
}
|
|
12898
|
+
locks.release(owner);
|
|
12899
|
+
})();
|
|
12900
|
+
}
|
|
12901
|
+
}
|
|
12902
|
+
async function mintForRunner(opts, payload, handleOpts, signal, log2) {
|
|
12903
|
+
try {
|
|
12904
|
+
const minted = await opts.api.mintRunnerCredential({
|
|
12905
|
+
conversationId: payload.conversationId,
|
|
12906
|
+
messageId: payload.messageId,
|
|
12907
|
+
...payload.episode !== void 0 ? { episode: payload.episode } : {},
|
|
12908
|
+
// The one turn the credential will operate: this run's.
|
|
12909
|
+
turnId: handleOpts.turnId
|
|
12910
|
+
});
|
|
12911
|
+
return {
|
|
12912
|
+
ok: true,
|
|
12913
|
+
credential: minted.credential,
|
|
12914
|
+
runnerId: minted.runnerId,
|
|
12915
|
+
deviceGrants: minted.deviceGrants ?? []
|
|
12916
|
+
};
|
|
12917
|
+
} catch (err) {
|
|
12918
|
+
if (signal.aborted) return { ok: false, reason: "cancelled" };
|
|
12919
|
+
const status2 = err instanceof ApiError ? err.status : 0;
|
|
12920
|
+
if (status2 === 404) return { ok: false, reason: "runner_credential_not_found" };
|
|
12921
|
+
if (status2 === 409 && apiErrorCode(err) === "dispatch_not_admitted")
|
|
12922
|
+
return { ok: false, reason: "runner_credential_not_admitted" };
|
|
12923
|
+
log2.warn(apiErrorLogFields(err), "runner: couldn't get a credential for the turn's runner");
|
|
12924
|
+
const reason = `fetch_failed: ${describeError(err)}`;
|
|
12925
|
+
return { ok: false, reason, errorReason: reason };
|
|
12926
|
+
}
|
|
12927
|
+
}
|
|
12928
|
+
async function settleUnstarted(opts, payload, turnId, errorReason, log2) {
|
|
12929
|
+
try {
|
|
12930
|
+
await opts.api.setActiveRun(opts.workspaceId, payload.conversationId, payload.agentId, {
|
|
12931
|
+
activeRunStartedAt: null,
|
|
12932
|
+
turnId,
|
|
12933
|
+
outcome: "failed",
|
|
12934
|
+
...errorReason ? { errorReason: errorReason.slice(0, ERROR_REASON_MAX) } : {},
|
|
12935
|
+
readThrough: payload.messageId
|
|
12936
|
+
});
|
|
12937
|
+
} catch (err) {
|
|
12938
|
+
log2.warn(
|
|
12939
|
+
{ ...apiErrorLogFields(err), turnId },
|
|
12940
|
+
"Couldn't update the turn's status in Cabane. Check the conversation before trying again."
|
|
12941
|
+
);
|
|
12942
|
+
}
|
|
12943
|
+
}
|
|
12944
|
+
async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
12945
|
+
const startedAt = Date.now();
|
|
12946
|
+
const { turnId } = handleOpts;
|
|
12947
|
+
const oomKilled = (id) => (launch.oomKilled ?? turnScopeOomKilled)(id).catch(() => false);
|
|
12948
|
+
const log2 = opts.log.child({
|
|
12949
|
+
workspaceId: opts.workspaceId,
|
|
12950
|
+
conversationId: payload.conversationId,
|
|
12951
|
+
agentId: payload.agentId,
|
|
12952
|
+
messageId: payload.messageId
|
|
12953
|
+
});
|
|
12954
|
+
const controller = new AbortController();
|
|
12955
|
+
duties.registerAbort(turnId, controller);
|
|
12956
|
+
const stopRunner = new AbortController();
|
|
12957
|
+
let ready = false;
|
|
12958
|
+
let child;
|
|
12959
|
+
let stopAnnounced = false;
|
|
12960
|
+
const announceStop = () => {
|
|
12961
|
+
if (stopAnnounced || !child?.stdin.writable) return;
|
|
12962
|
+
stopAnnounced = true;
|
|
12963
|
+
child.stdin.write(encodeLauncherMessage({ cabaneLauncher: "stop" }));
|
|
12964
|
+
};
|
|
12965
|
+
const forwardStop = () => {
|
|
12966
|
+
announceStop();
|
|
12967
|
+
if (ready) stopRunner.abort();
|
|
12968
|
+
};
|
|
12969
|
+
controller.signal.addEventListener("abort", forwardStop, { once: true });
|
|
12970
|
+
const attempt = {
|
|
12971
|
+
workspaceId: opts.workspaceId,
|
|
12972
|
+
conversationId: payload.conversationId,
|
|
12973
|
+
agentId: payload.agentId,
|
|
12974
|
+
turnId
|
|
12975
|
+
};
|
|
12976
|
+
const minted = await mintForRunner(opts, payload, handleOpts, controller.signal, log2);
|
|
12977
|
+
if (!minted.ok) {
|
|
12978
|
+
controller.signal.removeEventListener("abort", forwardStop);
|
|
12979
|
+
duties.releaseAbort(turnId, controller);
|
|
12980
|
+
const durationMs2 = Date.now() - startedAt;
|
|
12981
|
+
await settleUnstarted(opts, payload, turnId, minted.errorReason, log2);
|
|
12982
|
+
duties.notifyEnd({ id: payload.messageId, ok: false, durationMs: durationMs2, reason: minted.reason });
|
|
12983
|
+
return { ok: false, durationMs: durationMs2, reason: minted.reason };
|
|
12984
|
+
}
|
|
12985
|
+
let result;
|
|
12986
|
+
let thrown;
|
|
12987
|
+
let code;
|
|
12988
|
+
let handle;
|
|
12989
|
+
const groups = [];
|
|
12990
|
+
const remote = [];
|
|
12991
|
+
const lockOwner = (id) => `${turnId}#${id}`;
|
|
12992
|
+
const locks = launch.locks;
|
|
12993
|
+
try {
|
|
12994
|
+
handle = await launch.provider.acquire(attempt);
|
|
12995
|
+
} catch (err) {
|
|
12996
|
+
const reason2 = `runner_unavailable: ${describeError(err)}`;
|
|
12997
|
+
log2.warn(
|
|
12998
|
+
{ turnId, provider: launch.provider.name, err: describeError(err) },
|
|
12999
|
+
"runner: no place to run the turn"
|
|
13000
|
+
);
|
|
13001
|
+
controller.signal.removeEventListener("abort", forwardStop);
|
|
13002
|
+
duties.releaseAbort(turnId, controller);
|
|
13003
|
+
await settleUnstarted(opts, payload, turnId, reason2, log2);
|
|
13004
|
+
await launch.reportRunnerGone?.({
|
|
13005
|
+
workspaceId: opts.workspaceId,
|
|
13006
|
+
conversationId: payload.conversationId,
|
|
13007
|
+
agentId: payload.agentId,
|
|
13008
|
+
messageId: payload.messageId,
|
|
13009
|
+
runnerId: minted.runnerId,
|
|
13010
|
+
reason: "provider_failed"
|
|
13011
|
+
});
|
|
13012
|
+
const durationMs2 = Date.now() - startedAt;
|
|
13013
|
+
duties.notifyEnd({ id: payload.messageId, ok: false, durationMs: durationMs2, reason: reason2 });
|
|
13014
|
+
return { ok: false, durationMs: durationMs2, reason: reason2 };
|
|
13015
|
+
}
|
|
13016
|
+
try {
|
|
13017
|
+
const wrapped = launch.wrapped === true;
|
|
13018
|
+
const env = runnerExecEnv(launch.env ?? process.env, wrapped);
|
|
13019
|
+
const grants = minted.deviceGrants.length > 0 ? launch.grants?.(minted.deviceGrants) ?? {} : {};
|
|
13020
|
+
const input = {
|
|
13021
|
+
baseUrl: opts.baseUrl,
|
|
13022
|
+
credential: minted.credential,
|
|
13023
|
+
...wrapped ? { secrets: launch.secrets?.() ?? {} } : {},
|
|
13024
|
+
...Object.keys(grants).length > 0 ? { grants } : {},
|
|
13025
|
+
dispatch: payload,
|
|
13026
|
+
turnId,
|
|
13027
|
+
resumed: handleOpts.resumed,
|
|
13028
|
+
spec: runnerSpec(opts, launch, duties, minted.runnerId)
|
|
13029
|
+
};
|
|
13030
|
+
Object.assign(env, runnerEnv(input));
|
|
13031
|
+
const running = launch.provider.exec(handle, launch.command, env, {
|
|
13032
|
+
signal: stopRunner.signal,
|
|
13033
|
+
stdin: runnerStdin(input)
|
|
13034
|
+
});
|
|
13035
|
+
child = running;
|
|
13036
|
+
if (controller.signal.aborted) announceStop();
|
|
13037
|
+
log2.debug(
|
|
13038
|
+
{ turnId, runnerPid: running.pid, provider: launch.provider.name },
|
|
13039
|
+
"runner: started"
|
|
13040
|
+
);
|
|
13041
|
+
const stdoutDone = lines(running.stdout, (line) => {
|
|
13042
|
+
const event = decodeRunnerEvent(line);
|
|
13043
|
+
if (!event) {
|
|
13044
|
+
log2.debug({ runnerStdout: line }, "runner: output");
|
|
13045
|
+
return;
|
|
13046
|
+
}
|
|
13047
|
+
switch (event.cabaneRunner) {
|
|
13048
|
+
case "ready":
|
|
13049
|
+
ready = true;
|
|
13050
|
+
if (controller.signal.aborted) stopRunner.abort();
|
|
13051
|
+
return;
|
|
13052
|
+
case "start":
|
|
13053
|
+
duties.notifyStart({
|
|
13054
|
+
id: event.id,
|
|
13055
|
+
conversationId: event.conversationId,
|
|
13056
|
+
message: event.message
|
|
13057
|
+
});
|
|
13058
|
+
return;
|
|
13059
|
+
case "end": {
|
|
13060
|
+
const { cabaneRunner: _kind, ...info } = event;
|
|
13061
|
+
duties.notifyEnd(info);
|
|
13062
|
+
return;
|
|
13063
|
+
}
|
|
13064
|
+
case "session_write_notice":
|
|
13065
|
+
duties.markSessionWriteNotice();
|
|
13066
|
+
return;
|
|
13067
|
+
case "connector_health":
|
|
13068
|
+
launch.connectorHealth?.recordSettle(event.runtime, {
|
|
13069
|
+
ok: event.ok,
|
|
13070
|
+
errorReason: event.errorReason
|
|
13071
|
+
});
|
|
13072
|
+
return;
|
|
13073
|
+
case "result":
|
|
13074
|
+
result = event.result;
|
|
13075
|
+
return;
|
|
13076
|
+
case "error":
|
|
13077
|
+
thrown = event.message;
|
|
13078
|
+
return;
|
|
13079
|
+
case "lock":
|
|
13080
|
+
if (!locks) {
|
|
13081
|
+
log2.warn(
|
|
13082
|
+
{ key: event.key },
|
|
13083
|
+
"runner: asked for a device lock this launcher has none of"
|
|
13084
|
+
);
|
|
13085
|
+
return;
|
|
13086
|
+
}
|
|
13087
|
+
locks.request(
|
|
13088
|
+
event.key,
|
|
13089
|
+
lockOwner(event.id),
|
|
13090
|
+
() => running.stdin.write(encodeLauncherMessage({ cabaneLauncher: "granted", id: event.id }))
|
|
13091
|
+
);
|
|
13092
|
+
return;
|
|
13093
|
+
case "unlock":
|
|
13094
|
+
locks?.release(lockOwner(event.id));
|
|
13095
|
+
return;
|
|
13096
|
+
case "process_group":
|
|
13097
|
+
if (!wrapped) groups.push(event.pgid);
|
|
13098
|
+
return;
|
|
13099
|
+
case "remote_session":
|
|
13100
|
+
remote.push({ serverUrl: event.serverUrl, sessionId: event.sessionId });
|
|
13101
|
+
return;
|
|
13102
|
+
case "terminated":
|
|
13103
|
+
void oomKilled(turnId).then((oom) => {
|
|
13104
|
+
if (running.stdin.writable) {
|
|
13105
|
+
running.stdin.write(
|
|
13106
|
+
encodeLauncherMessage({
|
|
13107
|
+
cabaneLauncher: "cause",
|
|
13108
|
+
cause: oom ? "out_of_memory" : null
|
|
13109
|
+
})
|
|
13110
|
+
);
|
|
13111
|
+
}
|
|
13112
|
+
});
|
|
13113
|
+
return;
|
|
13114
|
+
}
|
|
13115
|
+
});
|
|
13116
|
+
const stderrDone = lines(running.stderr, (line) => relayRunnerLog(line, opts.log));
|
|
13117
|
+
code = await running.exit;
|
|
13118
|
+
await Promise.all([stdoutDone, stderrDone]);
|
|
13119
|
+
} finally {
|
|
13120
|
+
if (locks) giveBackLocks(locks, `${turnId}#`, remote, launch, log2);
|
|
13121
|
+
child?.stdin.end();
|
|
13122
|
+
controller.signal.removeEventListener("abort", forwardStop);
|
|
13123
|
+
duties.releaseAbort(turnId, controller);
|
|
13124
|
+
await launch.provider.release(handle, { destroy: false }).catch(
|
|
13125
|
+
(err) => log2.warn({ err: describeError(err) }, "runner: couldn't give its place back")
|
|
13126
|
+
);
|
|
13127
|
+
}
|
|
13128
|
+
if (result) return result;
|
|
13129
|
+
const durationMs = Date.now() - startedAt;
|
|
13130
|
+
const stopped = controller.signal.aborted && thrown === void 0;
|
|
13131
|
+
const reason = stopped ? "cancelled" : thrown !== void 0 ? `runner_failed: ${thrown}` : await oomKilled(turnId) ? "out_of_memory" : `runner_exited: ${code}`;
|
|
13132
|
+
await child?.reap(groups);
|
|
13133
|
+
const reaped = await (launch.reapTurn ?? reapTurnScopes)(turnId).catch((err) => ({
|
|
13134
|
+
failures: [err instanceof Error ? err.message : String(err)]
|
|
13135
|
+
}));
|
|
13136
|
+
if (reaped.failures.length) {
|
|
13137
|
+
log2.warn(
|
|
13138
|
+
{ turnId, failures: reaped.failures },
|
|
13139
|
+
"runner: couldn't stop what the dead runner's turn left running"
|
|
13140
|
+
);
|
|
13141
|
+
}
|
|
13142
|
+
try {
|
|
13143
|
+
await opts.api.setActiveRun(opts.workspaceId, payload.conversationId, payload.agentId, {
|
|
13144
|
+
activeRunStartedAt: null,
|
|
13145
|
+
turnId,
|
|
13146
|
+
...stopped ? { outcome: "interrupted" } : { outcome: "failed", errorReason: reason.slice(0, ERROR_REASON_MAX) },
|
|
13147
|
+
readThrough: payload.messageId
|
|
13148
|
+
});
|
|
13149
|
+
} catch (err) {
|
|
13150
|
+
log2.warn(
|
|
13151
|
+
{ ...apiErrorLogFields(err), turnId },
|
|
13152
|
+
"Couldn't update the turn's status in Cabane. Check the conversation before trying again."
|
|
13153
|
+
);
|
|
13154
|
+
}
|
|
13155
|
+
await launch.reportRunnerGone?.({
|
|
13156
|
+
workspaceId: opts.workspaceId,
|
|
13157
|
+
conversationId: payload.conversationId,
|
|
13158
|
+
agentId: payload.agentId,
|
|
13159
|
+
messageId: payload.messageId,
|
|
13160
|
+
runnerId: minted.runnerId,
|
|
13161
|
+
reason: "exited"
|
|
13162
|
+
});
|
|
13163
|
+
if (thrown !== void 0) throw new Error(thrown);
|
|
13164
|
+
if (stopped) log2.info({ turnId, code }, "runner: stopped before it could report");
|
|
13165
|
+
else log2.warn({ turnId, code }, "runner: exited without reporting a result");
|
|
13166
|
+
duties.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
13167
|
+
return { ok: false, durationMs, reason };
|
|
13168
|
+
}
|
|
13169
|
+
|
|
13170
|
+
// src/dispatcher.ts
|
|
13171
|
+
init_turn_execution();
|
|
13172
|
+
function runKey(conversationId, agentId) {
|
|
13173
|
+
return `${conversationId}|${agentId}`;
|
|
13174
|
+
}
|
|
13175
|
+
var Dispatcher = class {
|
|
13176
|
+
constructor(opts) {
|
|
13177
|
+
this.opts = opts;
|
|
13178
|
+
this.aborts = opts.aborts ?? /* @__PURE__ */ new Map();
|
|
13179
|
+
}
|
|
13180
|
+
opts;
|
|
13181
|
+
// (conversation|agent) → per-turn controllers. A delayed cancel for an old
|
|
13182
|
+
// turn must never reach its successor.
|
|
13183
|
+
aborts;
|
|
13184
|
+
// Pairs already told their session couldn't be saved: the notice is once per
|
|
13185
|
+
// pair, while the rejection repeats every turn.
|
|
13186
|
+
sessionWriteNotified = /* @__PURE__ */ new Set();
|
|
13187
|
+
notifyStart(info) {
|
|
12905
13188
|
try {
|
|
12906
|
-
|
|
13189
|
+
this.opts.observer?.onStart(info);
|
|
12907
13190
|
} catch {
|
|
12908
13191
|
}
|
|
12909
|
-
|
|
12910
|
-
|
|
13192
|
+
}
|
|
13193
|
+
notifyEnd(info) {
|
|
12911
13194
|
try {
|
|
12912
|
-
|
|
13195
|
+
this.opts.observer?.onEnd(info);
|
|
12913
13196
|
} catch {
|
|
12914
13197
|
}
|
|
12915
|
-
throw err;
|
|
12916
|
-
}
|
|
12917
|
-
}
|
|
12918
|
-
function getCredential(deviceId, agentId) {
|
|
12919
|
-
return load(deviceId)[agentId] ?? null;
|
|
12920
|
-
}
|
|
12921
|
-
function setCredential(deviceId, agentId, pat) {
|
|
12922
|
-
if (read(devicePath(deviceId))?.[agentId] === pat) return;
|
|
12923
|
-
const map = load(deviceId);
|
|
12924
|
-
map[agentId] = pat;
|
|
12925
|
-
save(deviceId, map);
|
|
12926
|
-
}
|
|
12927
|
-
function pruneCredentials(deviceId, keepAgentIds) {
|
|
12928
|
-
const keep = new Set(keepAgentIds);
|
|
12929
|
-
if (keep.size === 0) return [];
|
|
12930
|
-
const own = read(devicePath(deviceId));
|
|
12931
|
-
const map = own ?? load(deviceId);
|
|
12932
|
-
const removed = Object.keys(map).filter((id) => !keep.has(id));
|
|
12933
|
-
for (const id of removed) delete map[id];
|
|
12934
|
-
if (removed.length > 0 || own === null) save(deviceId, map);
|
|
12935
|
-
return removed;
|
|
12936
|
-
}
|
|
12937
|
-
function clearCredentials() {
|
|
12938
|
-
rmSync4(credentialsDir(), { recursive: true, force: true });
|
|
12939
|
-
if (existsSync7(legacyPath())) writeFileSync3(legacyPath(), "", { mode: 384 });
|
|
12940
|
-
}
|
|
12941
|
-
|
|
12942
|
-
// src/dispatch-dedupe.ts
|
|
12943
|
-
init_config();
|
|
12944
|
-
import { mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync as renameSync4, existsSync as existsSync8 } from "fs";
|
|
12945
|
-
import { join as join13 } from "path";
|
|
12946
|
-
var MAX_IDS = 256;
|
|
12947
|
-
function dir(log2) {
|
|
12948
|
-
return join13(cabaneDir(), log2);
|
|
12949
|
-
}
|
|
12950
|
-
function pathFor(log2, workspaceId) {
|
|
12951
|
-
return join13(dir(log2), encodeURIComponent(workspaceId));
|
|
12952
|
-
}
|
|
12953
|
-
function readIds(log2, workspaceId) {
|
|
12954
|
-
const path = pathFor(log2, workspaceId);
|
|
12955
|
-
if (!existsSync8(path)) return [];
|
|
12956
|
-
try {
|
|
12957
|
-
return readFileSync5(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
12958
|
-
} catch {
|
|
12959
|
-
return [];
|
|
12960
13198
|
}
|
|
12961
|
-
|
|
12962
|
-
|
|
12963
|
-
|
|
12964
|
-
}
|
|
12965
|
-
|
|
12966
|
-
|
|
12967
|
-
|
|
12968
|
-
|
|
12969
|
-
|
|
12970
|
-
|
|
12971
|
-
|
|
12972
|
-
|
|
12973
|
-
|
|
12974
|
-
|
|
12975
|
-
}
|
|
12976
|
-
|
|
12977
|
-
|
|
12978
|
-
|
|
12979
|
-
|
|
12980
|
-
|
|
12981
|
-
|
|
12982
|
-
|
|
12983
|
-
|
|
12984
|
-
|
|
12985
|
-
|
|
12986
|
-
|
|
12987
|
-
|
|
12988
|
-
|
|
12989
|
-
|
|
12990
|
-
|
|
12991
|
-
|
|
12992
|
-
}
|
|
12993
|
-
|
|
12994
|
-
const out = /* @__PURE__ */ new Map();
|
|
12995
|
-
const path = resumePathFor(workspaceId);
|
|
12996
|
-
if (!existsSync8(path)) return out;
|
|
12997
|
-
try {
|
|
12998
|
-
for (const line of readFileSync5(path, "utf8").split("\n")) {
|
|
12999
|
-
const trimmed = line.trim();
|
|
13000
|
-
if (!trimmed) continue;
|
|
13001
|
-
const tab = trimmed.lastIndexOf(" ");
|
|
13002
|
-
if (tab <= 0) continue;
|
|
13003
|
-
const id = trimmed.slice(0, tab);
|
|
13004
|
-
const count = Number.parseInt(trimmed.slice(tab + 1), 10);
|
|
13005
|
-
if (id && Number.isFinite(count)) out.set(id, count);
|
|
13006
|
-
}
|
|
13007
|
-
} catch {
|
|
13008
|
-
return out;
|
|
13199
|
+
// The supervisor supplies a stable turnId on both fresh and resumed dispatch.
|
|
13200
|
+
// resumed is separate: only replay seeds sequence/speech recovery from the
|
|
13201
|
+
// prior span. A direct caller omitting turnId gets a fresh id.
|
|
13202
|
+
async handle(payload, opts = {}) {
|
|
13203
|
+
const key = runKey(payload.conversationId, payload.agentId);
|
|
13204
|
+
return runTurnInRunner(
|
|
13205
|
+
this.opts.turn,
|
|
13206
|
+
this.opts.runner,
|
|
13207
|
+
{
|
|
13208
|
+
registerAbort: (turnId, controller) => {
|
|
13209
|
+
let pairAborts = this.aborts.get(key);
|
|
13210
|
+
if (!pairAborts) {
|
|
13211
|
+
pairAborts = /* @__PURE__ */ new Map();
|
|
13212
|
+
this.aborts.set(key, pairAborts);
|
|
13213
|
+
}
|
|
13214
|
+
pairAborts.set(turnId, controller);
|
|
13215
|
+
},
|
|
13216
|
+
releaseAbort: (turnId, controller) => {
|
|
13217
|
+
const pair = this.aborts.get(key);
|
|
13218
|
+
if (pair?.get(turnId) !== controller) return;
|
|
13219
|
+
pair.delete(turnId);
|
|
13220
|
+
if (pair.size === 0) this.aborts.delete(key);
|
|
13221
|
+
},
|
|
13222
|
+
sessionWriteNoticeClaimed: () => this.sessionWriteNotified.has(key),
|
|
13223
|
+
markSessionWriteNotice: () => {
|
|
13224
|
+
this.sessionWriteNotified.add(key);
|
|
13225
|
+
},
|
|
13226
|
+
notifyStart: (info) => this.notifyStart(info),
|
|
13227
|
+
notifyEnd: (info) => this.notifyEnd(info)
|
|
13228
|
+
},
|
|
13229
|
+
payload,
|
|
13230
|
+
{ turnId: opts.turnId ?? randomUUID4(), resumed: opts.resumed === true }
|
|
13231
|
+
);
|
|
13009
13232
|
}
|
|
13010
|
-
|
|
13011
|
-
|
|
13012
|
-
|
|
13013
|
-
|
|
13014
|
-
|
|
13015
|
-
|
|
13016
|
-
|
|
13017
|
-
|
|
13018
|
-
|
|
13019
|
-
|
|
13020
|
-
|
|
13021
|
-
|
|
13022
|
-
|
|
13023
|
-
|
|
13024
|
-
return next;
|
|
13025
|
-
}
|
|
13026
|
-
function turnDir() {
|
|
13027
|
-
return join13(cabaneDir(), "turns");
|
|
13028
|
-
}
|
|
13029
|
-
function turnPathFor(workspaceId) {
|
|
13030
|
-
return join13(turnDir(), encodeURIComponent(workspaceId));
|
|
13031
|
-
}
|
|
13032
|
-
var TURN_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
13033
|
-
function readTurnIds(workspaceId) {
|
|
13034
|
-
const out = /* @__PURE__ */ new Map();
|
|
13035
|
-
const path = turnPathFor(workspaceId);
|
|
13036
|
-
if (!existsSync8(path)) return out;
|
|
13037
|
-
try {
|
|
13038
|
-
for (const line of readFileSync5(path, "utf8").split("\n")) {
|
|
13039
|
-
const trimmed = line.trim();
|
|
13040
|
-
if (!trimmed) continue;
|
|
13041
|
-
const tab = trimmed.lastIndexOf(" ");
|
|
13042
|
-
if (tab <= 0) continue;
|
|
13043
|
-
const id = trimmed.slice(0, tab);
|
|
13044
|
-
const turnId = trimmed.slice(tab + 1);
|
|
13045
|
-
if (id && TURN_ID_RE.test(turnId)) out.set(id, turnId);
|
|
13233
|
+
// Cancel the named turn. An older server omits turnId and cancels the pair.
|
|
13234
|
+
cancel(conversationId, agentId, turnId) {
|
|
13235
|
+
const key = runKey(conversationId, agentId);
|
|
13236
|
+
const pair = this.aborts.get(key);
|
|
13237
|
+
if (!pair || pair.size === 0) return false;
|
|
13238
|
+
const controllers = turnId === void 0 ? [...pair.values()] : [pair.get(turnId)];
|
|
13239
|
+
let aborted = false;
|
|
13240
|
+
for (const ac of controllers) {
|
|
13241
|
+
if (!ac) continue;
|
|
13242
|
+
aborted = true;
|
|
13243
|
+
try {
|
|
13244
|
+
ac.abort();
|
|
13245
|
+
} catch {
|
|
13246
|
+
}
|
|
13046
13247
|
}
|
|
13047
|
-
|
|
13048
|
-
|
|
13049
|
-
|
|
13050
|
-
|
|
13051
|
-
|
|
13052
|
-
|
|
13053
|
-
|
|
13054
|
-
|
|
13055
|
-
|
|
13056
|
-
|
|
13057
|
-
|
|
13058
|
-
|
|
13059
|
-
|
|
13060
|
-
|
|
13061
|
-
|
|
13062
|
-
|
|
13063
|
-
return entries.length;
|
|
13064
|
-
}
|
|
13065
|
-
function rememberTurnId(workspaceId, eventId, turnId) {
|
|
13066
|
-
const turns = readTurnIds(workspaceId);
|
|
13067
|
-
if (turns.get(eventId) === turnId) return turns.size;
|
|
13068
|
-
turns.set(eventId, turnId);
|
|
13069
|
-
return writeTurnIds(workspaceId, turns);
|
|
13070
|
-
}
|
|
13071
|
-
function forgetTurnId(workspaceId, eventId) {
|
|
13072
|
-
const turns = readTurnIds(workspaceId);
|
|
13073
|
-
if (!turns.delete(eventId)) return;
|
|
13074
|
-
writeTurnIds(workspaceId, turns);
|
|
13075
|
-
}
|
|
13076
|
-
function noResume() {
|
|
13077
|
-
return process.env.CABANE_COMPANION_NO_RESUME === "1";
|
|
13078
|
-
}
|
|
13079
|
-
|
|
13080
|
-
// src/supervisor.ts
|
|
13081
|
-
init_bundled_binaries();
|
|
13082
|
-
init_install_kind();
|
|
13083
|
-
|
|
13084
|
-
// src/connector-health.ts
|
|
13085
|
-
init_src();
|
|
13086
|
-
var ConnectorHealthStore = class {
|
|
13087
|
-
byRuntime = /* @__PURE__ */ new Map();
|
|
13088
|
-
// Record a settled turn's outcome for `runtime`. A success heals to `ok`; a
|
|
13089
|
-
// classified failure sets `limited`(+reset) / `auth_expired` with the reason as
|
|
13090
|
-
// `last_error`; anything else (a user Stop, a timeout, an unclassified error)
|
|
13091
|
-
// leaves the last-known status untouched — we only ever move state on real
|
|
13092
|
-
// evidence.
|
|
13093
|
-
recordSettle(runtime, outcome) {
|
|
13094
|
-
if (outcome.ok) {
|
|
13095
|
-
this.byRuntime.set(runtime, { quotaState: "ok", limitedUntil: null, lastError: null });
|
|
13096
|
-
return;
|
|
13248
|
+
return aborted;
|
|
13249
|
+
}
|
|
13250
|
+
// Abort every run in flight in THIS process, whatever its conversation. For
|
|
13251
|
+
// an agent taken off this device: nothing it is still running here is
|
|
13252
|
+
// legitimate, and a cancel frame that arrives after it is dropped from the
|
|
13253
|
+
// roster finds no dispatcher to reach. True if any was aborted.
|
|
13254
|
+
cancelAll() {
|
|
13255
|
+
let aborted = false;
|
|
13256
|
+
for (const pair of this.aborts.values()) {
|
|
13257
|
+
for (const ac of [...pair.values()]) {
|
|
13258
|
+
try {
|
|
13259
|
+
ac.abort();
|
|
13260
|
+
aborted = true;
|
|
13261
|
+
} catch {
|
|
13262
|
+
}
|
|
13263
|
+
}
|
|
13097
13264
|
}
|
|
13098
|
-
|
|
13099
|
-
if (!failure) return;
|
|
13100
|
-
const quota = quotaForFailure(failure);
|
|
13101
|
-
if (!quota) return;
|
|
13102
|
-
this.byRuntime.set(runtime, {
|
|
13103
|
-
quotaState: quota.quotaState,
|
|
13104
|
-
limitedUntil: quota.limitedUntil,
|
|
13105
|
-
lastError: outcome.errorReason ?? null
|
|
13106
|
-
});
|
|
13107
|
-
}
|
|
13108
|
-
// The store's current opinion for one runtime, or undefined when it has
|
|
13109
|
-
// no observation. Read by the dispatcher at idle-timeout fire: an idle reap
|
|
13110
|
-
// while this runtime is definitively `limited` (a present, future `limitedUntil`
|
|
13111
|
-
// recorded by a sibling/earlier turn's classified cap settle) is the account cap
|
|
13112
|
-
// surfacing as silence, not a genuine hang — so the settle reclassifies to
|
|
13113
|
-
// `usage_capped` instead of `timeout_idle`. Returns the stored shape verbatim
|
|
13114
|
-
// (the dispatcher reads `quotaState` + `limitedUntil` and applies its own
|
|
13115
|
-
// future-instant gate); a read never mutates state.
|
|
13116
|
-
lookup(runtime) {
|
|
13117
|
-
return this.byRuntime.get(runtime);
|
|
13118
|
-
}
|
|
13119
|
-
// The per-connector reports to attach to a heartbeat — one entry per runtime the
|
|
13120
|
-
// companion has an observation for. Empty until the first classified failure/heal,
|
|
13121
|
-
// so a companion that has seen nothing sends no `connectors[]` and the server's
|
|
13122
|
-
// manifest synthesis (status-less rows) is unaffected.
|
|
13123
|
-
reports() {
|
|
13124
|
-
return [...this.byRuntime.entries()].map(([runtime, h]) => ({
|
|
13125
|
-
runtime,
|
|
13126
|
-
quotaState: h.quotaState,
|
|
13127
|
-
limitedUntil: h.limitedUntil,
|
|
13128
|
-
lastError: h.lastError
|
|
13129
|
-
}));
|
|
13265
|
+
return aborted;
|
|
13130
13266
|
}
|
|
13131
13267
|
};
|
|
13132
13268
|
|
|
13133
|
-
// src/supervisor.ts
|
|
13134
|
-
init_dispatcher();
|
|
13135
|
-
|
|
13136
13269
|
// src/host-report.ts
|
|
13137
13270
|
import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
|
|
13138
13271
|
import { userInfo as userInfo2 } from "os";
|
|
@@ -13171,7 +13304,7 @@ function hostReport() {
|
|
|
13171
13304
|
}
|
|
13172
13305
|
|
|
13173
13306
|
// src/providers/local-process.ts
|
|
13174
|
-
import { spawn as
|
|
13307
|
+
import { spawn as spawn4 } from "child_process";
|
|
13175
13308
|
import { constants as constants2 } from "os";
|
|
13176
13309
|
var DEFAULT_KILL_GRACE_MS = 3e4;
|
|
13177
13310
|
var DEFAULT_REAP_GRACE_MS = 3e3;
|
|
@@ -13182,7 +13315,7 @@ function exitCode(code, signal) {
|
|
|
13182
13315
|
}
|
|
13183
13316
|
function createLocalProcessProvider(opts = {}) {
|
|
13184
13317
|
const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
|
|
13185
|
-
const spawnFn = opts.spawnFn ??
|
|
13318
|
+
const spawnFn = opts.spawnFn ?? spawn4;
|
|
13186
13319
|
const reapGraceMs = opts.reapGraceMs ?? DEFAULT_REAP_GRACE_MS;
|
|
13187
13320
|
const killFn = opts.killFn ?? ((pid, signal) => process.kill(pid, signal));
|
|
13188
13321
|
const groupAlive = (pgid) => {
|
|
@@ -13316,9 +13449,6 @@ var DeviceLocks = class {
|
|
|
13316
13449
|
}
|
|
13317
13450
|
};
|
|
13318
13451
|
|
|
13319
|
-
// src/supervisor.ts
|
|
13320
|
-
init_launch();
|
|
13321
|
-
|
|
13322
13452
|
// src/manifest.ts
|
|
13323
13453
|
var DEVICE_MANIFEST = {
|
|
13324
13454
|
runtimes: [{ name: "claude-code", version: null }],
|
|
@@ -13561,9 +13691,9 @@ var SseSubscriber = class {
|
|
|
13561
13691
|
init_transcript_writer();
|
|
13562
13692
|
|
|
13563
13693
|
// src/updater.ts
|
|
13564
|
-
import { spawn as
|
|
13694
|
+
import { spawn as spawn5 } from "child_process";
|
|
13565
13695
|
import { copyFileSync as copyFileSync2, existsSync as existsSync16, mkdirSync as mkdirSync14, renameSync as renameSync9, rmSync as rmSync10 } from "fs";
|
|
13566
|
-
import { dirname as dirname11, join as
|
|
13696
|
+
import { dirname as dirname11, join as join26 } from "path";
|
|
13567
13697
|
|
|
13568
13698
|
// src/boot-rule.ts
|
|
13569
13699
|
import {
|
|
@@ -13575,7 +13705,7 @@ import {
|
|
|
13575
13705
|
symlinkSync,
|
|
13576
13706
|
writeFileSync as writeFileSync7
|
|
13577
13707
|
} from "fs";
|
|
13578
|
-
import { dirname as dirname8, join as
|
|
13708
|
+
import { dirname as dirname8, join as join23 } from "path";
|
|
13579
13709
|
var UNCONFIRMED_WINDOW_MS = 10 * 6e4;
|
|
13580
13710
|
var MAX_STAGE_ATTEMPTS = 3;
|
|
13581
13711
|
function superseded(pending, ...running) {
|
|
@@ -13603,38 +13733,38 @@ import {
|
|
|
13603
13733
|
lstatSync,
|
|
13604
13734
|
mkdirSync as mkdirSync12,
|
|
13605
13735
|
readFileSync as readFileSync13,
|
|
13606
|
-
readdirSync as
|
|
13736
|
+
readdirSync as readdirSync3,
|
|
13607
13737
|
realpathSync as realpathSync5,
|
|
13608
13738
|
renameSync as renameSync7,
|
|
13609
13739
|
rmSync as rmSync9,
|
|
13610
13740
|
symlinkSync as symlinkSync2
|
|
13611
13741
|
} from "fs";
|
|
13612
|
-
import { dirname as dirname9, join as
|
|
13742
|
+
import { dirname as dirname9, join as join24 } from "path";
|
|
13613
13743
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
13614
13744
|
function versionsDir() {
|
|
13615
|
-
return
|
|
13745
|
+
return join24(cabaneDir(), "versions");
|
|
13616
13746
|
}
|
|
13617
13747
|
function currentLink() {
|
|
13618
|
-
return
|
|
13748
|
+
return join24(cabaneDir(), "current");
|
|
13619
13749
|
}
|
|
13620
13750
|
function bootGuardPath() {
|
|
13621
|
-
return
|
|
13751
|
+
return join24(cabaneDir(), "boot-guard.mjs");
|
|
13622
13752
|
}
|
|
13623
13753
|
function pinnedNodeDir() {
|
|
13624
|
-
return
|
|
13754
|
+
return join24(cabaneDir(), "node");
|
|
13625
13755
|
}
|
|
13626
13756
|
function pinnedNodePath() {
|
|
13627
|
-
return
|
|
13757
|
+
return join24(pinnedNodeDir(), "bin", "node");
|
|
13628
13758
|
}
|
|
13629
13759
|
function versionPackageRoot(version) {
|
|
13630
|
-
return
|
|
13760
|
+
return join24(versionsDir(), version, "node_modules", "@cabane", "companion");
|
|
13631
13761
|
}
|
|
13632
13762
|
function ownPackageRoot(moduleUrl = import.meta.url) {
|
|
13633
13763
|
return dirname9(dirname9(fileURLToPath5(moduleUrl)));
|
|
13634
13764
|
}
|
|
13635
13765
|
function packageVersion(root) {
|
|
13636
13766
|
try {
|
|
13637
|
-
const pkg2 = JSON.parse(readFileSync13(
|
|
13767
|
+
const pkg2 = JSON.parse(readFileSync13(join24(root, "package.json"), "utf8"));
|
|
13638
13768
|
return typeof pkg2.version === "string" ? pkg2.version : null;
|
|
13639
13769
|
} catch {
|
|
13640
13770
|
return null;
|
|
@@ -13643,7 +13773,7 @@ function packageVersion(root) {
|
|
|
13643
13773
|
function currentRoot() {
|
|
13644
13774
|
try {
|
|
13645
13775
|
const root = realpathSync5(currentLink());
|
|
13646
|
-
return existsSync15(
|
|
13776
|
+
return existsSync15(join24(root, "dist", "cli.js")) ? root : null;
|
|
13647
13777
|
} catch {
|
|
13648
13778
|
return null;
|
|
13649
13779
|
}
|
|
@@ -13664,9 +13794,9 @@ function pruneVersions(keep) {
|
|
|
13664
13794
|
const dir2 = versionsDir();
|
|
13665
13795
|
if (!existsSync15(dir2)) return [];
|
|
13666
13796
|
const removed = [];
|
|
13667
|
-
for (const name of
|
|
13797
|
+
for (const name of readdirSync3(dir2)) {
|
|
13668
13798
|
if (keep.includes(name)) continue;
|
|
13669
|
-
rmSync9(
|
|
13799
|
+
rmSync9(join24(dir2, name), { recursive: true, force: true });
|
|
13670
13800
|
removed.push(name);
|
|
13671
13801
|
}
|
|
13672
13802
|
return removed;
|
|
@@ -13686,7 +13816,7 @@ function compareVersions(a, b) {
|
|
|
13686
13816
|
// src/update-state.ts
|
|
13687
13817
|
init_config();
|
|
13688
13818
|
import { mkdirSync as mkdirSync13, readFileSync as readFileSync14, renameSync as renameSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
13689
|
-
import { dirname as dirname10, join as
|
|
13819
|
+
import { dirname as dirname10, join as join25 } from "path";
|
|
13690
13820
|
import { z as z18 } from "zod";
|
|
13691
13821
|
var resultSchema = z18.object({
|
|
13692
13822
|
kind: z18.enum(["updated", "rolled_back", "failed"]),
|
|
@@ -13710,7 +13840,7 @@ var stateSchema = z18.object({
|
|
|
13710
13840
|
failed: z18.object({ version: z18.string(), attempts: z18.number().int(), at: z18.string() }).optional()
|
|
13711
13841
|
});
|
|
13712
13842
|
function updateStatePath(dir2 = cabaneDir()) {
|
|
13713
|
-
return
|
|
13843
|
+
return join25(dir2, "update.json");
|
|
13714
13844
|
}
|
|
13715
13845
|
function readUpdateState(path = updateStatePath()) {
|
|
13716
13846
|
try {
|
|
@@ -13759,7 +13889,7 @@ function rollBack(pending, reason, now = /* @__PURE__ */ new Date()) {
|
|
|
13759
13889
|
return updateUpdateState((state) => rolledBackState(state, pending, reason, now));
|
|
13760
13890
|
}
|
|
13761
13891
|
function writeBootGuard(root = ownPackageRoot()) {
|
|
13762
|
-
const built =
|
|
13892
|
+
const built = join26(root, "dist", "boot-guard.js");
|
|
13763
13893
|
if (!existsSync16(built)) return false;
|
|
13764
13894
|
const target = bootGuardPath();
|
|
13765
13895
|
mkdirSync14(dirname11(target), { recursive: true });
|
|
@@ -13776,7 +13906,7 @@ var runProcess = (command, args, opts = {}) => new Promise((resolve2) => {
|
|
|
13776
13906
|
let stderr = "";
|
|
13777
13907
|
let child;
|
|
13778
13908
|
try {
|
|
13779
|
-
child =
|
|
13909
|
+
child = spawn5(command, [...args], {
|
|
13780
13910
|
stdio: ["ignore", "pipe", "pipe"],
|
|
13781
13911
|
env: opts.env ?? process.env
|
|
13782
13912
|
});
|
|
@@ -13799,17 +13929,17 @@ var runProcess = (command, args, opts = {}) => new Promise((resolve2) => {
|
|
|
13799
13929
|
async function stageVersion(version, harnesses, deps = {}) {
|
|
13800
13930
|
const run = deps.run ?? runProcess;
|
|
13801
13931
|
const node = deps.execPath ?? process.execPath;
|
|
13802
|
-
const target =
|
|
13932
|
+
const target = join26(versionsDir(), version);
|
|
13803
13933
|
const placed = versionPackageRoot(version);
|
|
13804
13934
|
if (existsSync16(placed)) {
|
|
13805
13935
|
if (!await verifyInstall(placed, version, harnesses, { run, execPath: node }))
|
|
13806
13936
|
return { ok: true, root: placed };
|
|
13807
13937
|
}
|
|
13808
|
-
const npm =
|
|
13938
|
+
const npm = join26(dirname11(node), "npm");
|
|
13809
13939
|
if (!existsSync16(npm))
|
|
13810
13940
|
return { ok: false, reason: `there is no npm beside Node at ${dirname11(node)}` };
|
|
13811
13941
|
mkdirSync14(versionsDir(), { recursive: true });
|
|
13812
|
-
const staging =
|
|
13942
|
+
const staging = join26(versionsDir(), `.${version}.staging-${process.pid}`);
|
|
13813
13943
|
rmSync10(staging, { recursive: true, force: true });
|
|
13814
13944
|
const installed = await run(
|
|
13815
13945
|
npm,
|
|
@@ -13838,7 +13968,7 @@ async function stageVersion(version, harnesses, deps = {}) {
|
|
|
13838
13968
|
return { ok: false, reason: `version ${version} isn't published yet`, unpublished: true };
|
|
13839
13969
|
return { ok: false, reason: `npm couldn't install it: ${lastLine(installed.stderr)}` };
|
|
13840
13970
|
}
|
|
13841
|
-
const root =
|
|
13971
|
+
const root = join26(staging, "node_modules", "@cabane", "companion");
|
|
13842
13972
|
const verdict = await verifyInstall(root, version, harnesses, { run, execPath: node });
|
|
13843
13973
|
if (verdict) {
|
|
13844
13974
|
rmSync10(staging, { recursive: true, force: true });
|
|
@@ -13850,7 +13980,7 @@ async function stageVersion(version, harnesses, deps = {}) {
|
|
|
13850
13980
|
}
|
|
13851
13981
|
async function verifyInstall(root, version, harnesses, deps) {
|
|
13852
13982
|
if (packageVersion(root) !== version) return `the installed package isn't version ${version}`;
|
|
13853
|
-
const answered = await deps.run(deps.execPath, [
|
|
13983
|
+
const answered = await deps.run(deps.execPath, [join26(root, "dist", "bin.js"), "--version"], {
|
|
13854
13984
|
timeoutMs: 6e4,
|
|
13855
13985
|
env: { ...process.env, CABANE_LAUNCH_SELF: "1" }
|
|
13856
13986
|
});
|
|
@@ -13858,7 +13988,7 @@ async function verifyInstall(root, version, harnesses, deps) {
|
|
|
13858
13988
|
return `it doesn't start: ${lastLine(answered.stderr) || "no version printed"}`;
|
|
13859
13989
|
}
|
|
13860
13990
|
if (harnesses.length === 0) return null;
|
|
13861
|
-
const probed = await deps.run(deps.execPath, [
|
|
13991
|
+
const probed = await deps.run(deps.execPath, [join26(root, "dist", "postinstall.js"), "--json"], {
|
|
13862
13992
|
timeoutMs: 6e4
|
|
13863
13993
|
});
|
|
13864
13994
|
let resolutions = {};
|
|
@@ -14276,13 +14406,13 @@ var CompanionSupervisor = class {
|
|
|
14276
14406
|
if (this.stopped) return;
|
|
14277
14407
|
this.log.info(
|
|
14278
14408
|
{ protocolVersion: TURN_PROTOCOL_VERSION, version: COMPANION_VERSION },
|
|
14279
|
-
`Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ??
|
|
14409
|
+
`Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ?? hostname()}`
|
|
14280
14410
|
);
|
|
14281
14411
|
const retired = retiredConfigKeysOnDisk();
|
|
14282
14412
|
if (retired.length > 0) {
|
|
14283
14413
|
this.log.warn(
|
|
14284
14414
|
{ keys: retired },
|
|
14285
|
-
`config.json: ${retired.join(", ")} ${retired.length === 1 ? "is" : "are"} no longer read and can be removed
|
|
14415
|
+
`config.json: ${retired.join(", ")} ${retired.length === 1 ? "is" : "are"} no longer read and can be removed.`
|
|
14286
14416
|
);
|
|
14287
14417
|
}
|
|
14288
14418
|
if (this.config.deviceToken) {
|
|
@@ -14322,7 +14452,7 @@ var CompanionSupervisor = class {
|
|
|
14322
14452
|
log: this.log,
|
|
14323
14453
|
lastEventId: null,
|
|
14324
14454
|
onOpen: () => {
|
|
14325
|
-
this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ??
|
|
14455
|
+
this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ?? hostname()}`);
|
|
14326
14456
|
this.deviceConnected = true;
|
|
14327
14457
|
for (const wr of this.workspaces.values()) {
|
|
14328
14458
|
this.hub.setConnected(wr.workspaceId, true);
|
|
@@ -14868,9 +14998,9 @@ var CompanionSupervisor = class {
|
|
|
14868
14998
|
this.runnerCommand ?? selfRunnerCommand()
|
|
14869
14999
|
),
|
|
14870
15000
|
...this.config.runner?.command ? {
|
|
14871
|
-
|
|
15001
|
+
wrapped: true,
|
|
14872
15002
|
secrets: () => loadSecretValuesTolerant(
|
|
14873
|
-
(m) => this.log.warn({ err: m }, "Could not read the secrets file for a
|
|
15003
|
+
(m) => this.log.warn({ err: m }, "Could not read the secrets file for a wrapped turn.")
|
|
14874
15004
|
)
|
|
14875
15005
|
} : {},
|
|
14876
15006
|
// Read per dispatch, for the secret variables its mint named and no others.
|
|
@@ -15549,9 +15679,9 @@ async function waitForBoundedHeartbeat(heartbeat) {
|
|
|
15549
15679
|
}
|
|
15550
15680
|
function defaultReexec() {
|
|
15551
15681
|
clearRuntimeState();
|
|
15552
|
-
void import("child_process").then(({ spawn:
|
|
15682
|
+
void import("child_process").then(({ spawn: spawn7 }) => {
|
|
15553
15683
|
try {
|
|
15554
|
-
const child =
|
|
15684
|
+
const child = spawn7(process.execPath, process.argv.slice(1), {
|
|
15555
15685
|
stdio: "inherit",
|
|
15556
15686
|
detached: false
|
|
15557
15687
|
});
|
|
@@ -15623,9 +15753,9 @@ function handleUncaught(log2, err, origin) {
|
|
|
15623
15753
|
// src/crash-marker.ts
|
|
15624
15754
|
init_config();
|
|
15625
15755
|
import { existsSync as existsSync17, mkdirSync as mkdirSync15, readFileSync as readFileSync15, rmSync as rmSync11, writeFileSync as writeFileSync9 } from "fs";
|
|
15626
|
-
import { join as
|
|
15756
|
+
import { join as join27 } from "path";
|
|
15627
15757
|
function crashMarkerPath() {
|
|
15628
|
-
return
|
|
15758
|
+
return join27(cabaneDir(), "last-error.json");
|
|
15629
15759
|
}
|
|
15630
15760
|
function recordCrash(rec2) {
|
|
15631
15761
|
try {
|
|
@@ -15646,7 +15776,7 @@ function clearCrash() {
|
|
|
15646
15776
|
init_config();
|
|
15647
15777
|
import { spawnSync } from "child_process";
|
|
15648
15778
|
import { readFileSync as readFileSync16 } from "fs";
|
|
15649
|
-
import { join as
|
|
15779
|
+
import { join as join28 } from "path";
|
|
15650
15780
|
var COMPANION_ENTRY = /@cabane\/companion\/|cabane-companion|(^|[\s/])cabane\s|\.cabane\/(versions|current|boot-guard)/;
|
|
15651
15781
|
var START = /\sstart(\s|$)/;
|
|
15652
15782
|
function companionStarts(psOutput) {
|
|
@@ -15663,7 +15793,7 @@ function companionStarts(psOutput) {
|
|
|
15663
15793
|
}
|
|
15664
15794
|
function stateDirOf(env) {
|
|
15665
15795
|
const home = env.CABANE_COMPANION_HOME || env.HOME;
|
|
15666
|
-
return home ?
|
|
15796
|
+
return home ? join28(home, ".cabane") : null;
|
|
15667
15797
|
}
|
|
15668
15798
|
function envFromPsE(output, command) {
|
|
15669
15799
|
const line = output.replace(/\n$/, "");
|
|
@@ -15938,7 +16068,7 @@ import {
|
|
|
15938
16068
|
writeFileSync as writeFileSync10
|
|
15939
16069
|
} from "fs";
|
|
15940
16070
|
import { homedir as homedir9, userInfo as userInfo3 } from "os";
|
|
15941
|
-
import { dirname as dirname12, join as
|
|
16071
|
+
import { dirname as dirname12, join as join29, sep as sep3 } from "path";
|
|
15942
16072
|
function servicePlatform(platform = process.platform) {
|
|
15943
16073
|
if (platform === "linux") return "systemd";
|
|
15944
16074
|
if (platform === "darwin") return "launchd";
|
|
@@ -15956,15 +16086,15 @@ function serviceNode(execPath = process.execPath) {
|
|
|
15956
16086
|
return existsSync18(pinnedNodePath()) ? pinnedNodePath() : execPath;
|
|
15957
16087
|
}
|
|
15958
16088
|
function servicePaths(platform, env = process.env, home = homedir9(), node = serviceNode()) {
|
|
15959
|
-
const unitFile = platform === "systemd" ?
|
|
15960
|
-
env.XDG_CONFIG_HOME ||
|
|
16089
|
+
const unitFile = platform === "systemd" ? join29(
|
|
16090
|
+
env.XDG_CONFIG_HOME || join29(home, ".config"),
|
|
15961
16091
|
"systemd",
|
|
15962
16092
|
"user",
|
|
15963
16093
|
`${serviceName(env)}.service`
|
|
15964
|
-
) :
|
|
16094
|
+
) : join29(home, "Library", "LaunchAgents", `${launchdLabel(env)}.plist`);
|
|
15965
16095
|
return {
|
|
15966
16096
|
unitFile,
|
|
15967
|
-
envFile:
|
|
16097
|
+
envFile: join29(cabaneDir(), "service.env"),
|
|
15968
16098
|
node,
|
|
15969
16099
|
entry: bootGuardPath()
|
|
15970
16100
|
};
|
|
@@ -16031,7 +16161,7 @@ function renderLaunchdPlist(paths, env = process.env, servicePath) {
|
|
|
16031
16161
|
" <key>ThrottleInterval</key><integer>5</integer>",
|
|
16032
16162
|
// The companion writes its own log; the console copy would only grow a second one.
|
|
16033
16163
|
" <key>StandardOutPath</key><string>/dev/null</string>",
|
|
16034
|
-
` <key>StandardErrorPath</key><string>${xml(
|
|
16164
|
+
` <key>StandardErrorPath</key><string>${xml(join29(cabaneDir(), "service-error.log"))}</string>`,
|
|
16035
16165
|
"</dict>",
|
|
16036
16166
|
"</plist>",
|
|
16037
16167
|
""
|
|
@@ -16171,15 +16301,15 @@ async function seedManagedInstall(deps) {
|
|
|
16171
16301
|
}
|
|
16172
16302
|
const target = versionPackageRoot(version);
|
|
16173
16303
|
if (!existsSync18(target)) {
|
|
16174
|
-
const staging =
|
|
16304
|
+
const staging = join29(versionsDir(), `.${version}.seed-${process.pid}`);
|
|
16175
16305
|
rmSync12(staging, { recursive: true, force: true });
|
|
16176
16306
|
try {
|
|
16177
|
-
cpSync(own,
|
|
16307
|
+
cpSync(own, join29(staging, "node_modules", "@cabane", "companion"), {
|
|
16178
16308
|
recursive: true,
|
|
16179
16309
|
verbatimSymlinks: true
|
|
16180
16310
|
});
|
|
16181
|
-
rmSync12(
|
|
16182
|
-
renameSync10(staging,
|
|
16311
|
+
rmSync12(join29(versionsDir(), version), { recursive: true, force: true });
|
|
16312
|
+
renameSync10(staging, join29(versionsDir(), version));
|
|
16183
16313
|
} catch (err) {
|
|
16184
16314
|
rmSync12(staging, { recursive: true, force: true });
|
|
16185
16315
|
return {
|
|
@@ -16190,7 +16320,7 @@ async function seedManagedInstall(deps) {
|
|
|
16190
16320
|
}
|
|
16191
16321
|
const unrunnable = await verifyInstall(target, version, [], deps);
|
|
16192
16322
|
if (unrunnable) {
|
|
16193
|
-
rmSync12(
|
|
16323
|
+
rmSync12(join29(versionsDir(), version), { recursive: true, force: true });
|
|
16194
16324
|
return { ok: false, reason: `The copy of Cabane for the service can't run: ${unrunnable}.` };
|
|
16195
16325
|
}
|
|
16196
16326
|
pointCurrentAt(target);
|
|
@@ -16272,9 +16402,9 @@ function userName() {
|
|
|
16272
16402
|
return process.env.USER ?? "";
|
|
16273
16403
|
}
|
|
16274
16404
|
}
|
|
16275
|
-
function rewriteScriptWrappers(dir2 =
|
|
16405
|
+
function rewriteScriptWrappers(dir2 = join29(cabaneDir(), "bin")) {
|
|
16276
16406
|
for (const name of ["cabane", "cabane-companion"]) {
|
|
16277
|
-
const path =
|
|
16407
|
+
const path = join29(dir2, name);
|
|
16278
16408
|
let text;
|
|
16279
16409
|
try {
|
|
16280
16410
|
text = readFileSync17(path, "utf8");
|
|
@@ -16324,7 +16454,7 @@ async function controlService(action, deps) {
|
|
|
16324
16454
|
init_cabane_command();
|
|
16325
16455
|
init_cli_entry();
|
|
16326
16456
|
init_config();
|
|
16327
|
-
import { spawn as
|
|
16457
|
+
import { spawn as spawn6 } from "child_process";
|
|
16328
16458
|
import { closeSync as closeSync3, mkdirSync as mkdirSync17, openSync as openSync4 } from "fs";
|
|
16329
16459
|
init_config();
|
|
16330
16460
|
async function startDaemon(opts = {}, deps = {}) {
|
|
@@ -16406,7 +16536,7 @@ function defaultSpawnDetached(args) {
|
|
|
16406
16536
|
mkdirSync17(cabaneDir(), { recursive: true });
|
|
16407
16537
|
const logFd = openSync4(companionLogPath(), "a");
|
|
16408
16538
|
try {
|
|
16409
|
-
const child =
|
|
16539
|
+
const child = spawn6(process.execPath, [cliPath, ...args], {
|
|
16410
16540
|
detached: true,
|
|
16411
16541
|
stdio: ["ignore", logFd, logFd],
|
|
16412
16542
|
env: { ...process.env, CABANE_COMPANION_DAEMON: "1" }
|
|
@@ -16832,7 +16962,7 @@ async function setupScript(opts, deps) {
|
|
|
16832
16962
|
}
|
|
16833
16963
|
device = await (deps.claim ?? claimSetupCode)(baseUrl, {
|
|
16834
16964
|
userCode: opts.code,
|
|
16835
|
-
label: deviceLabelFromHostname(
|
|
16965
|
+
label: deviceLabelFromHostname(hostname2()) ?? "Computer",
|
|
16836
16966
|
...paired && cfg?.deviceToken ? { deviceToken: cfg.deviceToken } : {}
|
|
16837
16967
|
});
|
|
16838
16968
|
writePairedConfig(device);
|
|
@@ -17031,9 +17161,9 @@ async function logs(opts = {}) {
|
|
|
17031
17161
|
init_cabane_command();
|
|
17032
17162
|
init_install_kind();
|
|
17033
17163
|
init_cabane_command();
|
|
17034
|
-
import { readdirSync as
|
|
17164
|
+
import { readdirSync as readdirSync4, realpathSync as realpathSync6, existsSync as existsSync19 } from "fs";
|
|
17035
17165
|
import { homedir as homedir10 } from "os";
|
|
17036
|
-
import { dirname as dirname13, join as
|
|
17166
|
+
import { dirname as dirname13, join as join30 } from "path";
|
|
17037
17167
|
init_config();
|
|
17038
17168
|
init_secrets();
|
|
17039
17169
|
init_term();
|
|
@@ -17319,7 +17449,7 @@ function versionLine() {
|
|
|
17319
17449
|
const install = companionInstall();
|
|
17320
17450
|
const own = installedCommandPath({ install });
|
|
17321
17451
|
if (install === "script") {
|
|
17322
|
-
return `Cabane ${COMPANION_VERSION} \xB7 installed by the install script (${tildePath(
|
|
17452
|
+
return `Cabane ${COMPANION_VERSION} \xB7 installed by the install script (${tildePath(join30(homedir10(), ".cabane"))})`;
|
|
17323
17453
|
}
|
|
17324
17454
|
if (own)
|
|
17325
17455
|
return `Cabane ${COMPANION_VERSION} \xB7 installed with npm (${tildePath(dirname13(dirname13(own)))})`;
|
|
@@ -17345,7 +17475,7 @@ function otherInstall(install, own) {
|
|
|
17345
17475
|
}
|
|
17346
17476
|
};
|
|
17347
17477
|
if (install === "npm") {
|
|
17348
|
-
const script =
|
|
17478
|
+
const script = join30(
|
|
17349
17479
|
homedir10(),
|
|
17350
17480
|
".cabane",
|
|
17351
17481
|
"node",
|
|
@@ -17354,11 +17484,11 @@ function otherInstall(install, own) {
|
|
|
17354
17484
|
"@cabane",
|
|
17355
17485
|
"companion"
|
|
17356
17486
|
);
|
|
17357
|
-
return existsSync19(script) ?
|
|
17487
|
+
return existsSync19(script) ? join30(homedir10(), ".cabane") : null;
|
|
17358
17488
|
}
|
|
17359
17489
|
const found = firstOnPath("cabane-companion", process.env.PATH);
|
|
17360
17490
|
if (!found) return null;
|
|
17361
|
-
const ownAlias = own ? real(
|
|
17491
|
+
const ownAlias = own ? real(join30(dirname13(own), "cabane-companion")) : null;
|
|
17362
17492
|
return real(found) === ownAlias ? null : dirname13(found);
|
|
17363
17493
|
}
|
|
17364
17494
|
function runningMode(running, service2) {
|
|
@@ -17396,7 +17526,7 @@ function transcriptsLine() {
|
|
|
17396
17526
|
let count = 0;
|
|
17397
17527
|
let newest;
|
|
17398
17528
|
try {
|
|
17399
|
-
const files =
|
|
17529
|
+
const files = readdirSync4(dir2).filter((f) => f.endsWith(".jsonl")).sort();
|
|
17400
17530
|
count = files.length;
|
|
17401
17531
|
newest = files[files.length - 1];
|
|
17402
17532
|
} catch {
|
|
@@ -17564,8 +17694,8 @@ init_cabane_command();
|
|
|
17564
17694
|
init_errors();
|
|
17565
17695
|
init_src();
|
|
17566
17696
|
init_transcript_writer();
|
|
17567
|
-
import { existsSync as existsSync20, readFileSync as readFileSync18, readdirSync as
|
|
17568
|
-
import { basename as basename4, isAbsolute as isAbsolute2, join as
|
|
17697
|
+
import { existsSync as existsSync20, readFileSync as readFileSync18, readdirSync as readdirSync5 } from "fs";
|
|
17698
|
+
import { basename as basename4, isAbsolute as isAbsolute2, join as join31 } from "path";
|
|
17569
17699
|
async function transcript(opts = {}) {
|
|
17570
17700
|
const dir2 = transcriptsDir();
|
|
17571
17701
|
if (opts.follow) {
|
|
@@ -17582,7 +17712,7 @@ async function transcript(opts = {}) {
|
|
|
17582
17712
|
process.stdout.write(emptyMessage(dir2));
|
|
17583
17713
|
return;
|
|
17584
17714
|
}
|
|
17585
|
-
process.stdout.write(renderFile(
|
|
17715
|
+
process.stdout.write(renderFile(join31(dir2, newest)) + "\n");
|
|
17586
17716
|
return;
|
|
17587
17717
|
}
|
|
17588
17718
|
printList(dir2);
|
|
@@ -17665,7 +17795,7 @@ function isComplete(content) {
|
|
|
17665
17795
|
async function followTranscripts(dir2) {
|
|
17666
17796
|
const follower = new TranscriptFollower({
|
|
17667
17797
|
listFiles: () => listFiles(dir2),
|
|
17668
|
-
read: (f) => readFileSync18(
|
|
17798
|
+
read: (f) => readFileSync18(join31(dir2, f), "utf8"),
|
|
17669
17799
|
write: (s) => process.stdout.write(s),
|
|
17670
17800
|
// CSI: cursor up `n` lines, then erase from cursor to end of screen.
|
|
17671
17801
|
clearLines: (n) => process.stdout.write(`\x1B[${n}A\x1B[0J`),
|
|
@@ -17691,7 +17821,7 @@ async function followTranscripts(dir2) {
|
|
|
17691
17821
|
function listTranscriptFiles(dir2) {
|
|
17692
17822
|
let entries;
|
|
17693
17823
|
try {
|
|
17694
|
-
entries =
|
|
17824
|
+
entries = readdirSync5(dir2, { withFileTypes: true });
|
|
17695
17825
|
} catch {
|
|
17696
17826
|
return [];
|
|
17697
17827
|
}
|
|
@@ -17701,7 +17831,7 @@ function listTranscriptFiles(dir2) {
|
|
|
17701
17831
|
if (!entry.isDirectory() || entry.name === "anomalies") continue;
|
|
17702
17832
|
let inner;
|
|
17703
17833
|
try {
|
|
17704
|
-
inner =
|
|
17834
|
+
inner = readdirSync5(join31(dir2, entry.name));
|
|
17705
17835
|
} catch {
|
|
17706
17836
|
continue;
|
|
17707
17837
|
}
|
|
@@ -17720,7 +17850,7 @@ function printList(dir2) {
|
|
|
17720
17850
|
|
|
17721
17851
|
`);
|
|
17722
17852
|
for (const f of files.slice(0, 20)) {
|
|
17723
|
-
const { meta, outcome } = peek(
|
|
17853
|
+
const { meta, outcome } = peek(join31(dir2, f));
|
|
17724
17854
|
const when = fmtTime(rec(meta)?.ts);
|
|
17725
17855
|
const ws = str2(rec(meta)?.workspaceSlug);
|
|
17726
17856
|
const o = rec(outcome);
|
|
@@ -17756,13 +17886,13 @@ function peek(path) {
|
|
|
17756
17886
|
function resolveTarget(dir2, target) {
|
|
17757
17887
|
if (isAbsolute2(target) || target.includes("/")) {
|
|
17758
17888
|
if (existsSync20(target)) return target;
|
|
17759
|
-
if (!isAbsolute2(target) && existsSync20(
|
|
17889
|
+
if (!isAbsolute2(target) && existsSync20(join31(dir2, target))) return join31(dir2, target);
|
|
17760
17890
|
throw new CompanionError(`no transcript at ${target}.`);
|
|
17761
17891
|
}
|
|
17762
|
-
const exact =
|
|
17892
|
+
const exact = join31(dir2, target);
|
|
17763
17893
|
if (existsSync20(exact)) return exact;
|
|
17764
17894
|
const matches = listFiles(dir2).filter((f) => f.includes(target));
|
|
17765
|
-
if (matches.length === 1) return
|
|
17895
|
+
if (matches.length === 1) return join31(dir2, matches[0]);
|
|
17766
17896
|
if (matches.length === 0) {
|
|
17767
17897
|
throw new CompanionError(
|
|
17768
17898
|
`no transcript matching "${target}" in ${dir2}. Run \`${cabane("transcript")}\` to list them.`
|