@cabane/companion 0.6.202 → 0.6.203
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +429 -113
- package/dist/runtime.js +264 -28
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1395,6 +1395,18 @@ var init_boot_id = __esm({
|
|
|
1395
1395
|
function activeRunOutboxKey(conversationId, agentId) {
|
|
1396
1396
|
return `activerun:${conversationId}:${agentId}`;
|
|
1397
1397
|
}
|
|
1398
|
+
function isAlreadyPublished(err) {
|
|
1399
|
+
return err instanceof ApiError && err.status === 409 && apiErrorCode(err) === "already_published";
|
|
1400
|
+
}
|
|
1401
|
+
function trailOf(body) {
|
|
1402
|
+
const b = body;
|
|
1403
|
+
return {
|
|
1404
|
+
turnId: b.turnId,
|
|
1405
|
+
text: b.body,
|
|
1406
|
+
...b.seq !== void 0 ? { seq: b.seq } : {},
|
|
1407
|
+
...b.executionSourceId !== void 0 ? { executionSourceId: b.executionSourceId } : {}
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1398
1410
|
function isRetryable(err) {
|
|
1399
1411
|
if (err instanceof ApiError) return err.status >= 500;
|
|
1400
1412
|
if (isAbortError(err)) return false;
|
|
@@ -1522,7 +1534,7 @@ var init_api = __esm({
|
|
|
1522
1534
|
// text use (turnId, seq) within each kind for nonzero seq; tool activity
|
|
1523
1535
|
// uses (turnId, toolUseId). Legacy zero/missing seq bypasses row deduplication.
|
|
1524
1536
|
// Server fences and the outbox's discard/eviction bounds still apply.
|
|
1525
|
-
async durableCommit(kind, path, body, turnId, seq, signal) {
|
|
1537
|
+
async durableCommit(kind, path, body, turnId, seq, signal, trailPath) {
|
|
1526
1538
|
try {
|
|
1527
1539
|
await this.request("POST", path, body, {
|
|
1528
1540
|
retry: true,
|
|
@@ -1533,7 +1545,16 @@ var init_api = __esm({
|
|
|
1533
1545
|
if (!outbox) throw err;
|
|
1534
1546
|
if (signal?.aborted || isAbortError(err)) throw err;
|
|
1535
1547
|
if (!isRetryable(err)) throw err;
|
|
1536
|
-
outbox.persist({
|
|
1548
|
+
outbox.persist({
|
|
1549
|
+
enqueuedAt: Date.now(),
|
|
1550
|
+
turnId,
|
|
1551
|
+
seq,
|
|
1552
|
+
method: "POST",
|
|
1553
|
+
path,
|
|
1554
|
+
body,
|
|
1555
|
+
kind,
|
|
1556
|
+
...trailPath ? { trailPath } : {}
|
|
1557
|
+
});
|
|
1537
1558
|
this.opts.log?.debug(
|
|
1538
1559
|
{ kind, turnId, seq, err: err instanceof Error ? err.message : String(err) },
|
|
1539
1560
|
"companion: commit queued to outbox after transient failure (will drain when the API returns)"
|
|
@@ -1558,6 +1579,29 @@ var init_api = __esm({
|
|
|
1558
1579
|
progressed = true;
|
|
1559
1580
|
} catch (err) {
|
|
1560
1581
|
if (err instanceof ApiError && err.status >= 400 && err.status < 500) {
|
|
1582
|
+
if (entry.kind === "message" && entry.trailPath && isAlreadyPublished(err)) {
|
|
1583
|
+
try {
|
|
1584
|
+
await this.request("POST", entry.trailPath, trailOf(entry.body), {
|
|
1585
|
+
retry: true
|
|
1586
|
+
});
|
|
1587
|
+
} catch (trailErr) {
|
|
1588
|
+
if (!(trailErr instanceof ApiError && trailErr.status >= 400 && trailErr.status < 500)) {
|
|
1589
|
+
break;
|
|
1590
|
+
}
|
|
1591
|
+
this.opts.log?.warn(
|
|
1592
|
+
{
|
|
1593
|
+
kind: entry.kind,
|
|
1594
|
+
turnId: entry.turnId,
|
|
1595
|
+
seq: entry.seq,
|
|
1596
|
+
...apiErrorLogFields(trailErr)
|
|
1597
|
+
},
|
|
1598
|
+
`Cabane rejected a postscript; it could not be filed. ${mayNeedUpdate()}`
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
outbox.remove(entry.turnId, entry.seq);
|
|
1602
|
+
progressed = true;
|
|
1603
|
+
continue;
|
|
1604
|
+
}
|
|
1561
1605
|
if (entry.kind === "active-run" && isTurnEndedRefusal(err)) {
|
|
1562
1606
|
this.opts.log?.debug(
|
|
1563
1607
|
{ kind: entry.kind, turnId: entry.turnId, ...apiErrorLogFields(err) },
|
|
@@ -1633,6 +1677,19 @@ var init_api = __esm({
|
|
|
1633
1677
|
(e) => e.turnId === turnId && e.kind === "message" && e.body?.kind !== "runtime_notice"
|
|
1634
1678
|
);
|
|
1635
1679
|
}
|
|
1680
|
+
// The `seq` of this turn's SELECTED ANSWER still waiting in the outbox — the
|
|
1681
|
+
// `declared` commit the server's `answerArmed` cannot see — or null when none
|
|
1682
|
+
// is queued. A resumed run that re-armed over it would let a newer text
|
|
1683
|
+
// consume the arm first; with the selection queued, the resumed run starts
|
|
1684
|
+
// declared, its texts are trail, and the drain delivers the selection.
|
|
1685
|
+
outboxDeclaredSeqForTurn(turnId) {
|
|
1686
|
+
const outbox = this.opts.outbox;
|
|
1687
|
+
if (!outbox) return null;
|
|
1688
|
+
const queued = outbox.list().find(
|
|
1689
|
+
(e) => e.turnId === turnId && e.kind === "message" && e.body?.declared === true
|
|
1690
|
+
);
|
|
1691
|
+
return queued ? queued.seq : null;
|
|
1692
|
+
}
|
|
1636
1693
|
// ADOPT a turn context's issuance as the execution source of its work, once,
|
|
1637
1694
|
// before preparation emits or queues anything, so every row can name a source
|
|
1638
1695
|
// the server durably recorded and will never reap. Adoption is idempotent, so
|
|
@@ -1806,15 +1863,36 @@ var init_api = __esm({
|
|
|
1806
1863
|
// seq also orders speech against the turn's tool/thinking/text trail.
|
|
1807
1864
|
// Transport/5xx failures get bounded retries and optional outbox delivery;
|
|
1808
1865
|
// terminal 4xx and cancellation stop delivery rather than proving no commit.
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1866
|
+
//
|
|
1867
|
+
// A turn's answer can be out before a later speech commit arrives — the
|
|
1868
|
+
// declaration published it, or another host's selection drained first — and
|
|
1869
|
+
// the server refuses the commit `409 already_published`. The text is a
|
|
1870
|
+
// postscript then: with `trail` (the committing agent, which names the text
|
|
1871
|
+
// route), it is filed as trail at the same seq, live here and from the outbox
|
|
1872
|
+
// on a drain. Without `trail` (a runtime notice) the refusal is terminal.
|
|
1873
|
+
async postTurnMessage(workspaceId, conversationId, body, signal, trail) {
|
|
1874
|
+
const trailPath = trail ? `/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${trail.agentId}/text` : void 0;
|
|
1875
|
+
try {
|
|
1876
|
+
await this.durableCommit(
|
|
1877
|
+
"message",
|
|
1878
|
+
`/api/workspaces/${workspaceId}/conversations/${conversationId}/messages`,
|
|
1879
|
+
body,
|
|
1880
|
+
body.turnId,
|
|
1881
|
+
body.seq ?? 0,
|
|
1882
|
+
signal,
|
|
1883
|
+
trailPath
|
|
1884
|
+
);
|
|
1885
|
+
} catch (err) {
|
|
1886
|
+
if (!trailPath || !isAlreadyPublished(err)) throw err;
|
|
1887
|
+
await this.durableCommit(
|
|
1888
|
+
"text",
|
|
1889
|
+
trailPath,
|
|
1890
|
+
trailOf(body),
|
|
1891
|
+
body.turnId,
|
|
1892
|
+
body.seq ?? 0,
|
|
1893
|
+
signal
|
|
1894
|
+
);
|
|
1895
|
+
}
|
|
1818
1896
|
}
|
|
1819
1897
|
// Report a tool transition (start/done/error). The server checks span
|
|
1820
1898
|
// ownership, upserts by (turnId, toolUseId), and publishes agent_activity SSE.
|
|
@@ -2017,6 +2095,8 @@ var init_turn_diagnostics = __esm({
|
|
|
2017
2095
|
turnSessionModes = ["fresh", "resumed", "degraded"];
|
|
2018
2096
|
turnRuntimeResultKinds = ["success", "error", "no_terminal"];
|
|
2019
2097
|
turnFinalSources = [
|
|
2098
|
+
// The agent declared its answer with the `answer` turn act (CT1973).
|
|
2099
|
+
"declared",
|
|
2020
2100
|
"runtime_text",
|
|
2021
2101
|
"progress_promotion",
|
|
2022
2102
|
"host_fallback",
|
|
@@ -2495,39 +2575,145 @@ var init_transcript_sink = __esm({
|
|
|
2495
2575
|
}
|
|
2496
2576
|
});
|
|
2497
2577
|
|
|
2578
|
+
// packages/agent-runtime/src/turn-control/descriptions.ts
|
|
2579
|
+
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;
|
|
2580
|
+
var init_descriptions = __esm({
|
|
2581
|
+
"packages/agent-runtime/src/turn-control/descriptions.ts"() {
|
|
2582
|
+
"use strict";
|
|
2583
|
+
MAX_ASK_QUESTION_BYTES = 400;
|
|
2584
|
+
MAX_ASK_HEADLINE_BYTES = 120;
|
|
2585
|
+
MAX_ASK_ITEMS = 20;
|
|
2586
|
+
MAX_ASK_OPTION_BYTES = 200;
|
|
2587
|
+
ASK_OPTION_NOT_A_STRING = "An ask option is a plain string label.";
|
|
2588
|
+
MIN_ASK_OPTIONS = 2;
|
|
2589
|
+
MAX_ASK_OPTIONS = 4;
|
|
2590
|
+
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})$/;
|
|
2591
|
+
ASK_TARGET_INVALID = "An ask target is a user id or an @handle.";
|
|
2592
|
+
MAX_WAKE_NOTE_BYTES = 2e3;
|
|
2593
|
+
MAX_WITHDRAW_REASON_BYTES = 400;
|
|
2594
|
+
MAX_SKIP_REASON_BYTES = 500;
|
|
2595
|
+
TURN_CONTROL_MCP_SERVER = "cabane_companion";
|
|
2596
|
+
TURN_CONTROL_TOOLS = [
|
|
2597
|
+
"answer",
|
|
2598
|
+
"ask",
|
|
2599
|
+
"cancel_wake",
|
|
2600
|
+
"skip_turn",
|
|
2601
|
+
"wake_me",
|
|
2602
|
+
"withdraw_ask"
|
|
2603
|
+
];
|
|
2604
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS = {
|
|
2605
|
+
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.`,
|
|
2606
|
+
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.
|
|
2607
|
+
- 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.
|
|
2608
|
+
- \`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.
|
|
2609
|
+
- 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.
|
|
2610
|
+
- 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.
|
|
2611
|
+
- 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.
|
|
2612
|
+
- 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.
|
|
2613
|
+
- 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\`.`,
|
|
2614
|
+
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.*\`).`,
|
|
2615
|
+
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.`,
|
|
2616
|
+
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.`,
|
|
2617
|
+
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.`
|
|
2618
|
+
};
|
|
2619
|
+
TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
2620
|
+
answer: {},
|
|
2621
|
+
ask: {
|
|
2622
|
+
targetUserId: "The person to ask: their `@handle` from your turn context's roster (preferred), or their user id.",
|
|
2623
|
+
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\`.`,
|
|
2624
|
+
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.`,
|
|
2625
|
+
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.`,
|
|
2626
|
+
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.`,
|
|
2627
|
+
"questions.headline": `The one-sentence question ("Should Ledger deploy once Mia approves?") \u2014 required for each item, up to ${MAX_ASK_HEADLINE_BYTES} characters.`,
|
|
2628
|
+
"questions.body": `Optional short framing beneath the headline, up to ${MAX_ASK_QUESTION_BYTES} characters, inline markdown only. NOT a report.`,
|
|
2629
|
+
"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.`,
|
|
2630
|
+
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.",
|
|
2631
|
+
"replaces.askId": "The id of the open ask to withdraw.",
|
|
2632
|
+
"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.`
|
|
2633
|
+
},
|
|
2634
|
+
cancel_wake: {},
|
|
2635
|
+
skip_turn: {
|
|
2636
|
+
reason: `Short reason you are declining \u2014 used for telemetry/debugging, never posted. Up to ${MAX_SKIP_REASON_BYTES} characters.`
|
|
2637
|
+
},
|
|
2638
|
+
wake_me: {
|
|
2639
|
+
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.",
|
|
2640
|
+
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.",
|
|
2641
|
+
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.`
|
|
2642
|
+
},
|
|
2643
|
+
withdraw_ask: {
|
|
2644
|
+
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.",
|
|
2645
|
+
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.`
|
|
2646
|
+
}
|
|
2647
|
+
};
|
|
2648
|
+
}
|
|
2649
|
+
});
|
|
2650
|
+
|
|
2498
2651
|
// packages/agent-runtime/src/pump.ts
|
|
2499
|
-
|
|
2652
|
+
function isTurnControlAnswer(evt) {
|
|
2653
|
+
if (evt.mcpServer !== TURN_CONTROL_MCP_SERVER) return false;
|
|
2654
|
+
const name = evt.toolName;
|
|
2655
|
+
return name === ANSWER_TOOL || name === `${TURN_CONTROL_MCP_SERVER}.${ANSWER_TOOL}` || name === `${TURN_CONTROL_MCP_SERVER}_${ANSWER_TOOL}`;
|
|
2656
|
+
}
|
|
2657
|
+
var ANSWER_TOOL, THINKING_TEXT_MAX_CHARS, TurnPump;
|
|
2500
2658
|
var init_pump = __esm({
|
|
2501
2659
|
"packages/agent-runtime/src/pump.ts"() {
|
|
2502
2660
|
"use strict";
|
|
2503
2661
|
init_empty_result();
|
|
2504
2662
|
init_transcript();
|
|
2663
|
+
init_descriptions();
|
|
2664
|
+
ANSWER_TOOL = "answer";
|
|
2505
2665
|
THINKING_TEXT_MAX_CHARS = 3900;
|
|
2506
2666
|
TurnPump = class {
|
|
2507
2667
|
constructor(opts) {
|
|
2508
2668
|
this.opts = opts;
|
|
2509
2669
|
this.hasSpoken = opts.spoke ?? false;
|
|
2670
|
+
this.armed = opts.armed ?? false;
|
|
2671
|
+
if (opts.declaredSeq !== void 0) {
|
|
2672
|
+
this.hasSpoken = true;
|
|
2673
|
+
this.armed = false;
|
|
2674
|
+
this.declared = true;
|
|
2675
|
+
this.emittedFinal = true;
|
|
2676
|
+
this.finalSeq = opts.declaredSeq;
|
|
2677
|
+
this.emittedFinalSource = "declared";
|
|
2678
|
+
}
|
|
2510
2679
|
this.sink = {
|
|
2511
2680
|
onAssistantText: async (evt) => {
|
|
2512
2681
|
if (opts.signal.aborted) return;
|
|
2513
2682
|
const body = normalizeCommitText(evt.text);
|
|
2514
2683
|
if (!body) return;
|
|
2515
2684
|
const seq = opts.nextSeq();
|
|
2516
|
-
if (
|
|
2685
|
+
if (this.armed) {
|
|
2686
|
+
await opts.commit.commitMessage({ body, kind: "message", seq, declared: true });
|
|
2687
|
+
this.armed = false;
|
|
2688
|
+
this.declared = true;
|
|
2689
|
+
this.hasSpoken = true;
|
|
2690
|
+
this.emittedFinal = true;
|
|
2691
|
+
this.finalSeq = seq;
|
|
2692
|
+
this.lastTextSeq = seq;
|
|
2693
|
+
this.finalReplyBody = body;
|
|
2694
|
+
this.emittedFinalSource = "declared";
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
if (this.declared || !evt.final && this.hasSpoken) {
|
|
2517
2698
|
await opts.commit.reportText({ text: body, seq });
|
|
2699
|
+
this.lastTextSeq = seq;
|
|
2518
2700
|
return;
|
|
2519
2701
|
}
|
|
2520
2702
|
await opts.commit.commitMessage({ body, kind: "message", seq });
|
|
2521
2703
|
this.hasSpoken = true;
|
|
2704
|
+
this.lastTextSeq = seq;
|
|
2522
2705
|
if (evt.final) {
|
|
2523
2706
|
this.emittedFinal = true;
|
|
2524
|
-
this.
|
|
2707
|
+
this.finalSeq = seq;
|
|
2525
2708
|
this.finalReplyBody = body;
|
|
2526
2709
|
this.emittedFinalSource = "runtime_text";
|
|
2527
2710
|
}
|
|
2528
2711
|
},
|
|
2529
2712
|
onToolActivity: async (evt) => {
|
|
2530
2713
|
if (opts.signal.aborted) return;
|
|
2714
|
+
if (evt.phase === "done" && !this.declared && isTurnControlAnswer(evt)) {
|
|
2715
|
+
this.armed = true;
|
|
2716
|
+
}
|
|
2531
2717
|
let call = this.toolCallByProviderId.get(evt.toolUseId);
|
|
2532
2718
|
if (evt.phase === "start" || call === void 0) {
|
|
2533
2719
|
call = { persistedId: crypto.randomUUID(), seq: opts.nextSeq() };
|
|
@@ -2568,9 +2754,16 @@ var init_pump = __esm({
|
|
|
2568
2754
|
// A message has landed under this span — its first words. Every later
|
|
2569
2755
|
// non-terminal text is the trail. Seeded from the host on a resume.
|
|
2570
2756
|
hasSpoken;
|
|
2571
|
-
//
|
|
2757
|
+
// An accepted `answer` act this turn has not yet written the text for.
|
|
2758
|
+
armed;
|
|
2759
|
+
// The answer committed with `declared: true`; every text after it is trail.
|
|
2760
|
+
declared = false;
|
|
2761
|
+
// The answer landed — declared, or the adapter's terminal text — and its `seq`.
|
|
2572
2762
|
emittedFinal = false;
|
|
2573
|
-
|
|
2763
|
+
finalSeq = null;
|
|
2764
|
+
// The `seq` of the turn's last text through either door. Undeclared, this is
|
|
2765
|
+
// what the settle names: the server makes that text the body at the end.
|
|
2766
|
+
lastTextSeq = null;
|
|
2574
2767
|
// The turn's final reply text, captured for the dashboard feed / logging.
|
|
2575
2768
|
finalReplyBody = null;
|
|
2576
2769
|
emittedFinalSource = "none";
|
|
@@ -2592,14 +2785,20 @@ var init_pump = __esm({
|
|
|
2592
2785
|
get spoke() {
|
|
2593
2786
|
return this.hasSpoken;
|
|
2594
2787
|
}
|
|
2595
|
-
// Whether the turn has committed its
|
|
2788
|
+
// Whether the turn has committed its answer: declared, or the adapter's
|
|
2789
|
+
// terminal text.
|
|
2596
2790
|
get finalEmitted() {
|
|
2597
2791
|
return this.emittedFinal;
|
|
2598
2792
|
}
|
|
2599
|
-
//
|
|
2600
|
-
//
|
|
2793
|
+
// What the settle names, by `seq`: the declared answer, or — undeclared —
|
|
2794
|
+
// the turn's last text through either door, which the server makes the body
|
|
2795
|
+
// at the end. Null when the turn wrote no text at all.
|
|
2601
2796
|
get lastMessageSeq() {
|
|
2602
|
-
return this.
|
|
2797
|
+
return this.declared ? this.finalSeq : this.lastTextSeq;
|
|
2798
|
+
}
|
|
2799
|
+
// Whether an accepted `answer` act is still waiting for its text.
|
|
2800
|
+
get answerArmed() {
|
|
2801
|
+
return this.armed;
|
|
2603
2802
|
}
|
|
2604
2803
|
// The turn's final reply text, or null if none landed (failure / cancel /
|
|
2605
2804
|
// empty). Used for the dashboard feed's reply preview.
|
|
@@ -2613,75 +2812,6 @@ var init_pump = __esm({
|
|
|
2613
2812
|
}
|
|
2614
2813
|
});
|
|
2615
2814
|
|
|
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
2815
|
// packages/agent-runtime/src/turn-control/prompt-guidance.ts
|
|
2686
2816
|
function turnControlPromptGuidance(prefix) {
|
|
2687
2817
|
const controls = TURN_CONTROL_TOOLS.map((name) => `\`${prefix}${name}\` (${MEANINGS[name]})`);
|
|
@@ -2693,6 +2823,7 @@ var init_prompt_guidance = __esm({
|
|
|
2693
2823
|
"use strict";
|
|
2694
2824
|
init_descriptions();
|
|
2695
2825
|
MEANINGS = {
|
|
2826
|
+
answer: "declare the text you write next as your answer",
|
|
2696
2827
|
ask: "put a structured question to a person",
|
|
2697
2828
|
cancel_wake: "cancel your own armed wake in this conversation",
|
|
2698
2829
|
skip_turn: "end without posting a reply",
|
|
@@ -4231,6 +4362,15 @@ var init_events = __esm({
|
|
|
4231
4362
|
});
|
|
4232
4363
|
|
|
4233
4364
|
// packages/agent-runtime/src/opencode/loop.ts
|
|
4365
|
+
function opencodeMcpServerOf(tool2, servers) {
|
|
4366
|
+
let best = null;
|
|
4367
|
+
for (const server of servers ?? []) {
|
|
4368
|
+
if (tool2.length > server.length + 1 && tool2.startsWith(`${server}_`)) {
|
|
4369
|
+
if (best === null || server.length > best.length) best = server;
|
|
4370
|
+
}
|
|
4371
|
+
}
|
|
4372
|
+
return best;
|
|
4373
|
+
}
|
|
4234
4374
|
async function* decodeOpencodeStream(events, ctx2) {
|
|
4235
4375
|
let held = null;
|
|
4236
4376
|
const pending = /* @__PURE__ */ new Map();
|
|
@@ -4257,12 +4397,14 @@ async function* decodeOpencodeStream(events, ctx2) {
|
|
|
4257
4397
|
if (sealed) yield sealed;
|
|
4258
4398
|
const key = part.callID ?? part.id;
|
|
4259
4399
|
const input = part.state.input;
|
|
4400
|
+
const server = opencodeMcpServerOf(part.tool, ctx2.mcpServers);
|
|
4401
|
+
const origin = server ? { mcpServer: server } : {};
|
|
4260
4402
|
if (!startedTools.has(key)) {
|
|
4261
4403
|
const name = prettyToolName(part.tool);
|
|
4262
4404
|
const summary = summarizeToolInput(input);
|
|
4263
4405
|
pending.set(key, { name, summary, input });
|
|
4264
4406
|
startedTools.add(key);
|
|
4265
|
-
yield { type: "tool", id: key, name, phase: "start", summary, input };
|
|
4407
|
+
yield { type: "tool", id: key, name, phase: "start", summary, input, ...origin };
|
|
4266
4408
|
}
|
|
4267
4409
|
const entry = pending.get(key);
|
|
4268
4410
|
if (entry && hasToolInput(input)) {
|
|
@@ -4282,7 +4424,8 @@ async function* decodeOpencodeStream(events, ctx2) {
|
|
|
4282
4424
|
phase,
|
|
4283
4425
|
summary: started?.summary ?? "",
|
|
4284
4426
|
input: started?.input ?? input,
|
|
4285
|
-
result
|
|
4427
|
+
result,
|
|
4428
|
+
...origin
|
|
4286
4429
|
};
|
|
4287
4430
|
}
|
|
4288
4431
|
} else {
|
|
@@ -4480,15 +4623,15 @@ function buildMcp(req) {
|
|
|
4480
4623
|
function isStringRecord(v) {
|
|
4481
4624
|
return !!v && typeof v === "object" && Object.values(v).every((x) => typeof x === "string");
|
|
4482
4625
|
}
|
|
4483
|
-
var CABANE_MCP_SERVER2,
|
|
4626
|
+
var CABANE_MCP_SERVER2, ACTIVE_CONVERSATION_HEADER2;
|
|
4484
4627
|
var init_run_spec = __esm({
|
|
4485
4628
|
"packages/agent-runtime/src/opencode/run-spec.ts"() {
|
|
4486
4629
|
"use strict";
|
|
4630
|
+
init_descriptions();
|
|
4487
4631
|
init_mcp_secret_env();
|
|
4488
4632
|
init_policy2();
|
|
4489
4633
|
init_model2();
|
|
4490
4634
|
CABANE_MCP_SERVER2 = "cabane";
|
|
4491
|
-
TURN_CONTROL_MCP_SERVER = "cabane_companion";
|
|
4492
4635
|
ACTIVE_CONVERSATION_HEADER2 = "x-cabane-active-conversation";
|
|
4493
4636
|
}
|
|
4494
4637
|
});
|
|
@@ -4799,7 +4942,10 @@ function createOpencodeAdapter(deps = {}) {
|
|
|
4799
4942
|
}
|
|
4800
4943
|
yield* decodeOpencodeStream(result.events, {
|
|
4801
4944
|
signal,
|
|
4802
|
-
sessionId: result.sessionId
|
|
4945
|
+
sessionId: result.sessionId,
|
|
4946
|
+
// The keys the turn's servers were installed under: what the decoder
|
|
4947
|
+
// splits a raw `<server>_<tool>` id on.
|
|
4948
|
+
mcpServers: Object.keys(spec.mcp)
|
|
4803
4949
|
});
|
|
4804
4950
|
}
|
|
4805
4951
|
};
|
|
@@ -4961,7 +5107,15 @@ var init_conformance3 = __esm({
|
|
|
4961
5107
|
expected: [
|
|
4962
5108
|
sessionEvent2(NEW_SESSION_ID),
|
|
4963
5109
|
// start: fires off the first (empty) snapshot — empty input + summary, by design.
|
|
4964
|
-
{
|
|
5110
|
+
{
|
|
5111
|
+
type: "tool",
|
|
5112
|
+
id: "call_1",
|
|
5113
|
+
name: "cabane_list",
|
|
5114
|
+
phase: "start",
|
|
5115
|
+
summary: "",
|
|
5116
|
+
input: {},
|
|
5117
|
+
mcpServer: "cabane"
|
|
5118
|
+
},
|
|
4965
5119
|
// done: carries the full args captured from the later snapshot, and the
|
|
4966
5120
|
// summary recomputed from them — NOT the first-seen `{}`.
|
|
4967
5121
|
{
|
|
@@ -4971,11 +5125,63 @@ var init_conformance3 = __esm({
|
|
|
4971
5125
|
phase: "done",
|
|
4972
5126
|
summary: "/product/tasks",
|
|
4973
5127
|
input: { path: "/product/tasks" },
|
|
4974
|
-
result: "CT301.md"
|
|
5128
|
+
result: "CT301.md",
|
|
5129
|
+
mcpServer: "cabane"
|
|
4975
5130
|
},
|
|
4976
5131
|
{ type: "result", ok: true }
|
|
4977
5132
|
]
|
|
4978
5133
|
},
|
|
5134
|
+
{
|
|
5135
|
+
// The turn-control server's `answer` (CT1973). opencode names an MCP tool `<server>_<tool>`,
|
|
5136
|
+
// so the installed keys split the raw id: `cabane_companion_answer` is the turn-control
|
|
5137
|
+
// server's, never the workspace server's `companion_answer`. The pump binds on that.
|
|
5138
|
+
name: "turn-control answer is read as the turn-control server\u2019s, not cabane\u2019s",
|
|
5139
|
+
request: makeRequest2({
|
|
5140
|
+
cabane: {
|
|
5141
|
+
mcpUrl: "https://cabane.test/api/mcp",
|
|
5142
|
+
turnControlUrl: "https://cabane.test/api/turn-control",
|
|
5143
|
+
bearer: "tok",
|
|
5144
|
+
activeConversationId: "conv"
|
|
5145
|
+
}
|
|
5146
|
+
}),
|
|
5147
|
+
nativeStream: [
|
|
5148
|
+
textPart("p1", "On it."),
|
|
5149
|
+
toolPart("t1", "cabane_companion_answer", { status: "running", input: {} }, "call_1"),
|
|
5150
|
+
toolPart(
|
|
5151
|
+
"t1",
|
|
5152
|
+
"cabane_companion_answer",
|
|
5153
|
+
{ status: "completed", input: {}, output: '{"ok":true,"kind":"answer"}' },
|
|
5154
|
+
"call_1"
|
|
5155
|
+
),
|
|
5156
|
+
textPart("p2", "The answer."),
|
|
5157
|
+
idle()
|
|
5158
|
+
],
|
|
5159
|
+
expected: [
|
|
5160
|
+
sessionEvent2(NEW_SESSION_ID),
|
|
5161
|
+
{ type: "text", body: "On it.", terminal: false },
|
|
5162
|
+
{
|
|
5163
|
+
type: "tool",
|
|
5164
|
+
id: "call_1",
|
|
5165
|
+
name: "cabane_companion_answer",
|
|
5166
|
+
phase: "start",
|
|
5167
|
+
summary: "",
|
|
5168
|
+
input: {},
|
|
5169
|
+
mcpServer: "cabane_companion"
|
|
5170
|
+
},
|
|
5171
|
+
{
|
|
5172
|
+
type: "tool",
|
|
5173
|
+
id: "call_1",
|
|
5174
|
+
name: "cabane_companion_answer",
|
|
5175
|
+
phase: "done",
|
|
5176
|
+
summary: "",
|
|
5177
|
+
input: {},
|
|
5178
|
+
result: '{"ok":true,"kind":"answer"}',
|
|
5179
|
+
mcpServer: "cabane_companion"
|
|
5180
|
+
},
|
|
5181
|
+
{ type: "text", body: "The answer.", terminal: true },
|
|
5182
|
+
{ type: "result", ok: true }
|
|
5183
|
+
]
|
|
5184
|
+
},
|
|
4979
5185
|
{
|
|
4980
5186
|
// Highest risk: cancel mid-stream. The abort lands after the tool result; the adapter stops before
|
|
4981
5187
|
// the closing reply — no final text, no `result` event (the server writes the stopped marker).
|
|
@@ -5339,6 +5545,7 @@ function readToolItem(item, eventType) {
|
|
|
5339
5545
|
name: server && server !== "cabane" ? `${server}.${tool2}` : tool2,
|
|
5340
5546
|
input: item.arguments,
|
|
5341
5547
|
status: toStatus(item.status),
|
|
5548
|
+
...server ? { server } : {},
|
|
5342
5549
|
...item.status === "completed" ? { result } : item.status === "failed" ? { result: hasMcpReceipt(result) ? result : readMcpError(item.error) } : {}
|
|
5343
5550
|
};
|
|
5344
5551
|
}
|
|
@@ -5577,11 +5784,20 @@ async function* decodeCodexStream(events, ctx2) {
|
|
|
5577
5784
|
if (!tool2) continue;
|
|
5578
5785
|
yield* flushInterim();
|
|
5579
5786
|
const name = prettyToolName(tool2.name);
|
|
5787
|
+
const origin = tool2.server ? { mcpServer: tool2.server } : {};
|
|
5580
5788
|
if (!startedTools.has(tool2.id)) {
|
|
5581
5789
|
const summary = summarizeToolInput(tool2.input);
|
|
5582
5790
|
pending.set(tool2.id, { name, summary, input: tool2.input });
|
|
5583
5791
|
startedTools.add(tool2.id);
|
|
5584
|
-
yield {
|
|
5792
|
+
yield {
|
|
5793
|
+
type: "tool",
|
|
5794
|
+
id: tool2.id,
|
|
5795
|
+
name,
|
|
5796
|
+
phase: "start",
|
|
5797
|
+
summary,
|
|
5798
|
+
input: tool2.input,
|
|
5799
|
+
...origin
|
|
5800
|
+
};
|
|
5585
5801
|
}
|
|
5586
5802
|
const entry = pending.get(tool2.id);
|
|
5587
5803
|
if (entry && hasToolInput2(tool2.input)) {
|
|
@@ -5599,7 +5815,8 @@ async function* decodeCodexStream(events, ctx2) {
|
|
|
5599
5815
|
phase: tool2.status === "completed" ? "done" : "error",
|
|
5600
5816
|
summary: started?.summary ?? "",
|
|
5601
5817
|
input: started?.input ?? tool2.input,
|
|
5602
|
-
result: tool2.result
|
|
5818
|
+
result: tool2.result,
|
|
5819
|
+
...origin
|
|
5603
5820
|
};
|
|
5604
5821
|
}
|
|
5605
5822
|
continue;
|
|
@@ -5830,7 +6047,7 @@ function buildConfig(req) {
|
|
|
5830
6047
|
default_tools_approval_mode: "approve"
|
|
5831
6048
|
};
|
|
5832
6049
|
if (req.cabane.turnControlUrl) {
|
|
5833
|
-
mcp_servers[
|
|
6050
|
+
mcp_servers[TURN_CONTROL_MCP_SERVER] = {
|
|
5834
6051
|
url: req.cabane.turnControlUrl,
|
|
5835
6052
|
http_headers: {
|
|
5836
6053
|
Authorization: `Bearer ${req.cabane.bearer}`,
|
|
@@ -5855,16 +6072,16 @@ function buildConfig(req) {
|
|
|
5855
6072
|
function isStringRecord2(v) {
|
|
5856
6073
|
return !!v && typeof v === "object" && Object.values(v).every((x) => typeof x === "string");
|
|
5857
6074
|
}
|
|
5858
|
-
var CABANE_MCP_SERVER3,
|
|
6075
|
+
var CABANE_MCP_SERVER3, ACTIVE_CONVERSATION_HEADER3, ENV_ENVELOPE_KEYS, CORE_REPLACES, CONVERSATION_REPLACES;
|
|
5859
6076
|
var init_run_spec2 = __esm({
|
|
5860
6077
|
"packages/agent-runtime/src/codex/run-spec.ts"() {
|
|
5861
6078
|
"use strict";
|
|
6079
|
+
init_descriptions();
|
|
5862
6080
|
init_standing();
|
|
5863
6081
|
init_policy3();
|
|
5864
6082
|
init_model3();
|
|
5865
6083
|
init_session3();
|
|
5866
6084
|
CABANE_MCP_SERVER3 = "cabane";
|
|
5867
|
-
TURN_CONTROL_MCP_SERVER2 = "cabane_companion";
|
|
5868
6085
|
ACTIVE_CONVERSATION_HEADER3 = "x-cabane-active-conversation";
|
|
5869
6086
|
ENV_ENVELOPE_KEYS = [
|
|
5870
6087
|
"CABANE_CONVERSATION_ID",
|
|
@@ -6336,6 +6553,7 @@ var init_conformance4 = __esm({
|
|
|
6336
6553
|
type: "tool",
|
|
6337
6554
|
id: "t1",
|
|
6338
6555
|
name: "read",
|
|
6556
|
+
mcpServer: "cabane",
|
|
6339
6557
|
phase: "start",
|
|
6340
6558
|
summary: "sleep.md",
|
|
6341
6559
|
input: { path: "sleep.md" }
|
|
@@ -6344,6 +6562,7 @@ var init_conformance4 = __esm({
|
|
|
6344
6562
|
type: "tool",
|
|
6345
6563
|
id: "t1",
|
|
6346
6564
|
name: "read",
|
|
6565
|
+
mcpServer: "cabane",
|
|
6347
6566
|
phase: "done",
|
|
6348
6567
|
summary: "sleep.md",
|
|
6349
6568
|
input: { path: "sleep.md" },
|
|
@@ -6389,6 +6608,7 @@ var init_conformance4 = __esm({
|
|
|
6389
6608
|
type: "tool",
|
|
6390
6609
|
id: "query-success",
|
|
6391
6610
|
name: "query",
|
|
6611
|
+
mcpServer: "cabane",
|
|
6392
6612
|
phase: "start",
|
|
6393
6613
|
summary: "",
|
|
6394
6614
|
input: { op: "files.read", args: { path: "/ABOUT.md" } }
|
|
@@ -6397,6 +6617,7 @@ var init_conformance4 = __esm({
|
|
|
6397
6617
|
type: "tool",
|
|
6398
6618
|
id: "query-success",
|
|
6399
6619
|
name: "query",
|
|
6620
|
+
mcpServer: "cabane",
|
|
6400
6621
|
phase: "done",
|
|
6401
6622
|
summary: "",
|
|
6402
6623
|
input: { op: "files.read", args: { path: "/ABOUT.md" } },
|
|
@@ -6443,6 +6664,7 @@ var init_conformance4 = __esm({
|
|
|
6443
6664
|
type: "tool",
|
|
6444
6665
|
id: "query-failed",
|
|
6445
6666
|
name: "query",
|
|
6667
|
+
mcpServer: "cabane",
|
|
6446
6668
|
phase: "start",
|
|
6447
6669
|
summary: "",
|
|
6448
6670
|
input: { op: "files.read", args: { path: "/ABOUT.md" } }
|
|
@@ -6451,6 +6673,7 @@ var init_conformance4 = __esm({
|
|
|
6451
6673
|
type: "tool",
|
|
6452
6674
|
id: "query-failed",
|
|
6453
6675
|
name: "query",
|
|
6676
|
+
mcpServer: "cabane",
|
|
6454
6677
|
phase: "error",
|
|
6455
6678
|
summary: "",
|
|
6456
6679
|
input: { op: "files.read", args: { path: "/ABOUT.md" } },
|
|
@@ -6460,6 +6683,54 @@ var init_conformance4 = __esm({
|
|
|
6460
6683
|
{ type: "result", ok: true }
|
|
6461
6684
|
]
|
|
6462
6685
|
},
|
|
6686
|
+
{
|
|
6687
|
+
// The turn-control server's `answer` (CT1973): the tool event carries the server Codex names
|
|
6688
|
+
// beside the tool, which is what the pump binds the declaration to. A non-cabane server keeps
|
|
6689
|
+
// its prefix on the card as before.
|
|
6690
|
+
name: "turn-control answer carries its server",
|
|
6691
|
+
request: makeRequest3(),
|
|
6692
|
+
nativeStream: [
|
|
6693
|
+
threadStarted(NEW_THREAD_ID),
|
|
6694
|
+
agentMessage("m1", "On it."),
|
|
6695
|
+
toolFrame("item.completed", {
|
|
6696
|
+
id: "answer-1",
|
|
6697
|
+
type: "mcp_tool_call",
|
|
6698
|
+
server: "cabane_companion",
|
|
6699
|
+
tool: "answer",
|
|
6700
|
+
arguments: {},
|
|
6701
|
+
status: "completed",
|
|
6702
|
+
result: { content: [{ type: "text", text: '{"ok":true,"kind":"answer"}' }] }
|
|
6703
|
+
}),
|
|
6704
|
+
agentMessage("m2", "The answer."),
|
|
6705
|
+
turnCompleted()
|
|
6706
|
+
],
|
|
6707
|
+
expected: [
|
|
6708
|
+
startedSession(NEW_THREAD_ID),
|
|
6709
|
+
{ type: "text", body: "On it.", terminal: false },
|
|
6710
|
+
{
|
|
6711
|
+
type: "tool",
|
|
6712
|
+
id: "answer-1",
|
|
6713
|
+
name: "cabane_companion.answer",
|
|
6714
|
+
mcpServer: "cabane_companion",
|
|
6715
|
+
phase: "start",
|
|
6716
|
+
summary: "",
|
|
6717
|
+
input: {}
|
|
6718
|
+
},
|
|
6719
|
+
{
|
|
6720
|
+
type: "tool",
|
|
6721
|
+
id: "answer-1",
|
|
6722
|
+
name: "cabane_companion.answer",
|
|
6723
|
+
mcpServer: "cabane_companion",
|
|
6724
|
+
phase: "done",
|
|
6725
|
+
summary: "",
|
|
6726
|
+
input: {},
|
|
6727
|
+
result: [{ type: "text", text: '{"ok":true,"kind":"answer"}' }]
|
|
6728
|
+
},
|
|
6729
|
+
{ type: "text", body: "The answer.", terminal: true },
|
|
6730
|
+
closingSession(NEW_THREAD_ID),
|
|
6731
|
+
{ type: "result", ok: true }
|
|
6732
|
+
]
|
|
6733
|
+
},
|
|
6463
6734
|
{
|
|
6464
6735
|
// A transport/tool failure with no MCP result payload still uses the real adapter error message,
|
|
6465
6736
|
// never `undefined`.
|
|
@@ -6485,6 +6756,7 @@ var init_conformance4 = __esm({
|
|
|
6485
6756
|
type: "tool",
|
|
6486
6757
|
id: "mcp-failed",
|
|
6487
6758
|
name: "other.lookup",
|
|
6759
|
+
mcpServer: "other",
|
|
6488
6760
|
phase: "start",
|
|
6489
6761
|
summary: "receipt",
|
|
6490
6762
|
input: { q: "receipt" }
|
|
@@ -6493,6 +6765,7 @@ var init_conformance4 = __esm({
|
|
|
6493
6765
|
type: "tool",
|
|
6494
6766
|
id: "mcp-failed",
|
|
6495
6767
|
name: "other.lookup",
|
|
6768
|
+
mcpServer: "other",
|
|
6496
6769
|
phase: "error",
|
|
6497
6770
|
summary: "receipt",
|
|
6498
6771
|
input: { q: "receipt" },
|
|
@@ -6529,6 +6802,7 @@ var init_conformance4 = __esm({
|
|
|
6529
6802
|
type: "tool",
|
|
6530
6803
|
id: "mcp-image",
|
|
6531
6804
|
name: "other.render",
|
|
6805
|
+
mcpServer: "other",
|
|
6532
6806
|
phase: "start",
|
|
6533
6807
|
summary: "",
|
|
6534
6808
|
input: {}
|
|
@@ -6537,6 +6811,7 @@ var init_conformance4 = __esm({
|
|
|
6537
6811
|
type: "tool",
|
|
6538
6812
|
id: "mcp-image",
|
|
6539
6813
|
name: "other.render",
|
|
6814
|
+
mcpServer: "other",
|
|
6540
6815
|
phase: "done",
|
|
6541
6816
|
summary: "",
|
|
6542
6817
|
input: {},
|
|
@@ -6584,11 +6859,20 @@ var init_conformance4 = __esm({
|
|
|
6584
6859
|
],
|
|
6585
6860
|
expected: [
|
|
6586
6861
|
startedSession(NEW_THREAD_ID),
|
|
6587
|
-
{ type: "tool", id: "t1", name: "list", phase: "start", summary: "", input: {} },
|
|
6588
6862
|
{
|
|
6589
6863
|
type: "tool",
|
|
6590
6864
|
id: "t1",
|
|
6591
6865
|
name: "list",
|
|
6866
|
+
mcpServer: "cabane",
|
|
6867
|
+
phase: "start",
|
|
6868
|
+
summary: "",
|
|
6869
|
+
input: {}
|
|
6870
|
+
},
|
|
6871
|
+
{
|
|
6872
|
+
type: "tool",
|
|
6873
|
+
id: "t1",
|
|
6874
|
+
name: "list",
|
|
6875
|
+
mcpServer: "cabane",
|
|
6592
6876
|
phase: "done",
|
|
6593
6877
|
summary: "/product/tasks",
|
|
6594
6878
|
input: { path: "/product/tasks" },
|
|
@@ -8428,6 +8712,13 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
8428
8712
|
}),
|
|
8429
8713
|
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
8430
8714
|
),
|
|
8715
|
+
tool(
|
|
8716
|
+
ANSWER_TOOL2,
|
|
8717
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.answer,
|
|
8718
|
+
{},
|
|
8719
|
+
async () => performAct(acts, { kind: "answer", callId: randomUUID2() }),
|
|
8720
|
+
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
8721
|
+
),
|
|
8431
8722
|
// The inverse. No arguments — it can only ever stand down the
|
|
8432
8723
|
// caller's own wake in the conversation it's holding a turn in.
|
|
8433
8724
|
tool(
|
|
@@ -8440,7 +8731,7 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
8440
8731
|
]
|
|
8441
8732
|
});
|
|
8442
8733
|
}
|
|
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;
|
|
8734
|
+
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
8735
|
var init_turn_control_tools = __esm({
|
|
8445
8736
|
"src/turn-control-tools.ts"() {
|
|
8446
8737
|
"use strict";
|
|
@@ -8448,13 +8739,14 @@ var init_turn_control_tools = __esm({
|
|
|
8448
8739
|
init_descriptions();
|
|
8449
8740
|
init_errors();
|
|
8450
8741
|
init_api_error_shape();
|
|
8451
|
-
COMPANION_LOCAL_MCP_SERVER =
|
|
8742
|
+
COMPANION_LOCAL_MCP_SERVER = TURN_CONTROL_MCP_SERVER;
|
|
8452
8743
|
COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
|
|
8453
8744
|
SKIP_TURN_TOOL = "skip_turn";
|
|
8454
8745
|
ASK_TOOL = "ask";
|
|
8455
8746
|
ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${ASK_TOOL}`;
|
|
8456
8747
|
WITHDRAW_ASK_TOOL = "withdraw_ask";
|
|
8457
8748
|
WITHDRAW_ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WITHDRAW_ASK_TOOL}`;
|
|
8749
|
+
ANSWER_TOOL2 = "answer";
|
|
8458
8750
|
WAKE_ME_TOOL = "wake_me";
|
|
8459
8751
|
WAKE_ME_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WAKE_ME_TOOL}`;
|
|
8460
8752
|
CANCEL_WAKE_TOOL = "cancel_wake";
|
|
@@ -9111,7 +9403,7 @@ var init_turn_committer = __esm({
|
|
|
9111
9403
|
deps.onCommitFailed?.(err);
|
|
9112
9404
|
};
|
|
9113
9405
|
const commit = {
|
|
9114
|
-
commitMessage: async ({ body, kind, seq }) => {
|
|
9406
|
+
commitMessage: async ({ body, kind, seq, declared }) => {
|
|
9115
9407
|
await deps.api.postTurnMessage(
|
|
9116
9408
|
deps.workspaceId,
|
|
9117
9409
|
deps.conversationId,
|
|
@@ -9121,9 +9413,11 @@ var init_turn_committer = __esm({
|
|
|
9121
9413
|
turnId: deps.turnId,
|
|
9122
9414
|
seq,
|
|
9123
9415
|
parentMessageId: deps.parentMessageId,
|
|
9124
|
-
...source
|
|
9416
|
+
...source,
|
|
9417
|
+
...declared ? { declared } : {}
|
|
9125
9418
|
},
|
|
9126
|
-
deps.signal
|
|
9419
|
+
deps.signal,
|
|
9420
|
+
{ agentId: deps.agentId }
|
|
9127
9421
|
);
|
|
9128
9422
|
},
|
|
9129
9423
|
reportTool: async ({
|
|
@@ -9182,7 +9476,9 @@ var init_turn_committer = __esm({
|
|
|
9182
9476
|
commit,
|
|
9183
9477
|
signal: deps.signal,
|
|
9184
9478
|
nextSeq: deps.nextSeq,
|
|
9185
|
-
...deps.spoke !== void 0 ? { spoke: deps.spoke } : {}
|
|
9479
|
+
...deps.spoke !== void 0 ? { spoke: deps.spoke } : {},
|
|
9480
|
+
...deps.armed !== void 0 ? { armed: deps.armed } : {},
|
|
9481
|
+
...deps.declaredSeq !== void 0 ? { declaredSeq: deps.declaredSeq } : {}
|
|
9186
9482
|
});
|
|
9187
9483
|
this.emit = sinkEmitter(this.pump.sink, this.onError);
|
|
9188
9484
|
}
|
|
@@ -9204,14 +9500,15 @@ var init_turn_committer = __esm({
|
|
|
9204
9500
|
}
|
|
9205
9501
|
}
|
|
9206
9502
|
// 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
|
-
//
|
|
9503
|
+
// that spoke settles; one that never did is a pass. The held-text flush is
|
|
9504
|
+
// the adapter's (it emits the closing reply as a terminal `text` event before
|
|
9505
|
+
// `result`); a span that ended on working text names that text as its last,
|
|
9506
|
+
// and the server makes it the body at the end.
|
|
9211
9507
|
get spoke() {
|
|
9212
9508
|
return this.pump.spoke;
|
|
9213
9509
|
}
|
|
9214
|
-
//
|
|
9510
|
+
// What the settle names by `seq`: the declared answer, or the turn's last
|
|
9511
|
+
// text through either door; null when the turn wrote none.
|
|
9215
9512
|
get lastMessageSeq() {
|
|
9216
9513
|
return this.pump.lastMessageSeq;
|
|
9217
9514
|
}
|
|
@@ -9442,6 +9739,12 @@ var init_turn_execution = __esm({
|
|
|
9442
9739
|
// Whether the span this resumed run continues already has first words —
|
|
9443
9740
|
// from the server (`turnSpoke`) or from speech still queued in the outbox.
|
|
9444
9741
|
resumedSpoke = false;
|
|
9742
|
+
// Whether the resumed run starts with the `answer` act armed: the server
|
|
9743
|
+
// holds an accepted act (`answerArmed`) and this device's outbox holds no
|
|
9744
|
+
// `declared` commit for the turn — a queued selection stands, and the run
|
|
9745
|
+
// starts declared at its seq instead.
|
|
9746
|
+
resumedArmed = false;
|
|
9747
|
+
resumedDeclaredSeq;
|
|
9445
9748
|
turnContext;
|
|
9446
9749
|
resolvedMcpServers;
|
|
9447
9750
|
// The harness's secret environment for this turn. Only ever handed to the
|
|
@@ -9627,6 +9930,7 @@ var init_turn_execution = __esm({
|
|
|
9627
9930
|
const { payload, turnId, turnLog } = this;
|
|
9628
9931
|
const outboxFloor = this.resumed ? readOutboxFloor((id) => this.opts.api.outboxMaxSeqForTurn(id), turnId, turnLog) : 0;
|
|
9629
9932
|
const outboxSpoke = this.resumed && this.opts.api.outboxHasSpeechForTurn(turnId);
|
|
9933
|
+
const outboxDeclaredSeq = this.resumed ? this.opts.api.outboxDeclaredSeqForTurn(turnId) : null;
|
|
9630
9934
|
let turnContext;
|
|
9631
9935
|
try {
|
|
9632
9936
|
turnContext = await this.opts.api.getTurnContext(
|
|
@@ -9681,6 +9985,16 @@ var init_turn_execution = __esm({
|
|
|
9681
9985
|
this.seqCounter = Math.max(this.seqCounter, this.resumedFromSeq);
|
|
9682
9986
|
this.resumedSpoke = turnContext.turnSpoke === true || outboxSpoke;
|
|
9683
9987
|
if (this.resumedSpoke) turnLog.debug({ turnId }, "dispatcher: resumed span already spoke");
|
|
9988
|
+
this.resumedArmed = turnContext.answerArmed === true && outboxDeclaredSeq === null;
|
|
9989
|
+
this.resumedDeclaredSeq = outboxDeclaredSeq ?? void 0;
|
|
9990
|
+
if (this.resumedArmed)
|
|
9991
|
+
turnLog.debug({ turnId }, "dispatcher: resumed turn re-armed its answer");
|
|
9992
|
+
if (outboxDeclaredSeq !== null) {
|
|
9993
|
+
turnLog.debug(
|
|
9994
|
+
{ turnId, seq: outboxDeclaredSeq },
|
|
9995
|
+
"dispatcher: resumed turn keeps the answer queued in its outbox"
|
|
9996
|
+
);
|
|
9997
|
+
}
|
|
9684
9998
|
}
|
|
9685
9999
|
}
|
|
9686
10000
|
// Register this context's issuance as the execution source of the work about to
|
|
@@ -10188,6 +10502,8 @@ var init_turn_execution = __esm({
|
|
|
10188
10502
|
log: turnLog,
|
|
10189
10503
|
nextSeq: this.nextSeq,
|
|
10190
10504
|
spoke: this.resumedSpoke,
|
|
10505
|
+
armed: this.resumedArmed,
|
|
10506
|
+
...this.resumedDeclaredSeq !== void 0 ? { declaredSeq: this.resumedDeclaredSeq } : {},
|
|
10191
10507
|
// Admitted by now, so this is the source the model's work runs on.
|
|
10192
10508
|
...this.executionSourceId !== void 0 ? { executionSourceId: this.executionSourceId } : {},
|
|
10193
10509
|
// A commit that didn't land may mean the lease is gone; see
|