@cabane/companion 0.6.80 → 0.6.83
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 +233 -380
- package/dist/runtime.js +233 -380
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1735,7 +1735,7 @@ function getLogger() {
|
|
|
1735
1735
|
}
|
|
1736
1736
|
|
|
1737
1737
|
// src/runtime.ts
|
|
1738
|
-
import { randomUUID as
|
|
1738
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
1739
1739
|
|
|
1740
1740
|
// src/dashboard/server.ts
|
|
1741
1741
|
import { dirname as dirname4, join as join7 } from "path";
|
|
@@ -2279,7 +2279,7 @@ async function warnAboutHarnessReadiness(cfg, deps = {}) {
|
|
|
2279
2279
|
}
|
|
2280
2280
|
|
|
2281
2281
|
// src/supervisor.ts
|
|
2282
|
-
import { randomUUID as
|
|
2282
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
2283
2283
|
|
|
2284
2284
|
// src/api.ts
|
|
2285
2285
|
var RETRY_BACKOFF_MS = [250, 750];
|
|
@@ -2459,14 +2459,14 @@ var CabaneApi = class {
|
|
|
2459
2459
|
const q = `conversationId=${encodeURIComponent(conversationId)}&messageId=${encodeURIComponent(messageId2)}` + (turnId ? `&turnId=${encodeURIComponent(turnId)}` : "");
|
|
2460
2460
|
return this.request("GET", `/api/agent/turn-context?${q}`);
|
|
2461
2461
|
}
|
|
2462
|
-
// CT714: read a turn's recorded turn-control intent
|
|
2463
|
-
//
|
|
2464
|
-
//
|
|
2465
|
-
//
|
|
2466
|
-
//
|
|
2467
|
-
//
|
|
2468
|
-
//
|
|
2469
|
-
//
|
|
2462
|
+
// CT714: read a turn's recorded turn-control intent. An EXTERNAL adapter
|
|
2463
|
+
// (Codex / opencode) records `reply_to` / `skip_turn` into `turn_intents`
|
|
2464
|
+
// server-side (the URL MCP surface) rather than the dispatcher's in-memory
|
|
2465
|
+
// closures, so the dispatcher fetches this once at settle — by `turnId` —
|
|
2466
|
+
// and populates those closures, letting the unchanged settle path close the
|
|
2467
|
+
// turn identically to claude-code. CT1354: those two verbs are all it
|
|
2468
|
+
// carries now; the outgoing acts commit at the call (`turnAct`). Agent-PAT
|
|
2469
|
+
// authed + self-scoped (`:agentId` must match the PAT's agent).
|
|
2470
2470
|
getTurnIntent(workspaceId, conversationId, agentId, turnId) {
|
|
2471
2471
|
const q = `turnId=${encodeURIComponent(turnId)}`;
|
|
2472
2472
|
return this.request(
|
|
@@ -2474,6 +2474,27 @@ var CabaneApi = class {
|
|
|
2474
2474
|
`/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}/turn-intent?${q}`
|
|
2475
2475
|
);
|
|
2476
2476
|
}
|
|
2477
|
+
// CT1354: COMMIT ONE OUTGOING ACT of a running turn — a `send`, an `ask`, a
|
|
2478
|
+
// `wake_me`, a `cancel_wake` — the moment the tool is called. The server
|
|
2479
|
+
// runs `performTurnAct` inside the turn's write fence: the row is in the
|
|
2480
|
+
// ledger when this resolves, and a 200 `ok: false` is the server's REFUSAL
|
|
2481
|
+
// (an unknown target, a non-member, a wake outside its guardrails) for the
|
|
2482
|
+
// tool to hand back as its error. 409 means the turn already settled.
|
|
2483
|
+
//
|
|
2484
|
+
// Bounded retry, on purpose: the act is a durable write whose loss strands a
|
|
2485
|
+
// person or a peer, and `act.callId` — minted once per tool invocation by the
|
|
2486
|
+
// caller — is what makes the retry safe: the server keys on `(turnId,
|
|
2487
|
+
// callId)`, so an attempt that landed before the response was lost is
|
|
2488
|
+
// replayed as the same row, never a second message. The caller's `signal`
|
|
2489
|
+
// is the per-call deadline (the tool must answer the model, not hang it).
|
|
2490
|
+
turnAct(workspaceId, turnId, act, signal) {
|
|
2491
|
+
return this.request(
|
|
2492
|
+
"POST",
|
|
2493
|
+
`/api/workspaces/${workspaceId}/turns/${turnId}/acts`,
|
|
2494
|
+
act,
|
|
2495
|
+
signal ? { retry: true, signal } : { retry: true }
|
|
2496
|
+
);
|
|
2497
|
+
}
|
|
2477
2498
|
// CT1292: RENEW THE TURN'S LEASE — ask the server whether this turn is still
|
|
2478
2499
|
// running. The dispatcher calls this on a cadence for the life of the SDK loop,
|
|
2479
2500
|
// and out of cadence the moment a commit is refused, so a loop whose turn was
|
|
@@ -7542,10 +7563,11 @@ import { existsSync as existsSync11, readdirSync as readdirSync2, statSync } fro
|
|
|
7542
7563
|
import { join as join14 } from "path";
|
|
7543
7564
|
|
|
7544
7565
|
// src/turn-execution.ts
|
|
7545
|
-
import { createHash as createHash2, randomUUID } from "crypto";
|
|
7566
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
7546
7567
|
import { existsSync as existsSync10 } from "fs";
|
|
7547
7568
|
|
|
7548
7569
|
// src/turn-control-tools.ts
|
|
7570
|
+
import { randomUUID } from "crypto";
|
|
7549
7571
|
import { z as z13 } from "zod";
|
|
7550
7572
|
var COMPANION_LOCAL_MCP_SERVER = "cabane_companion";
|
|
7551
7573
|
var SEND_TOOL = "send";
|
|
@@ -7554,6 +7576,9 @@ var COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
|
|
|
7554
7576
|
var SKIP_TURN_TOOL = "skip_turn";
|
|
7555
7577
|
var ASK_TOOL = "ask";
|
|
7556
7578
|
var ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${ASK_TOOL}`;
|
|
7579
|
+
var WITHDRAW_ASK_TOOL = "withdraw_ask";
|
|
7580
|
+
var WITHDRAW_ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WITHDRAW_ASK_TOOL}`;
|
|
7581
|
+
var MAX_WITHDRAW_REASON = 400;
|
|
7557
7582
|
var REPLY_TO_TOOL = "reply_to";
|
|
7558
7583
|
var REPLY_TO_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${REPLY_TO_TOOL}`;
|
|
7559
7584
|
var MAX_ASK_ITEMS = 20;
|
|
@@ -7561,96 +7586,83 @@ var WAKE_ME_TOOL = "wake_me";
|
|
|
7561
7586
|
var WAKE_ME_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WAKE_ME_TOOL}`;
|
|
7562
7587
|
var CANCEL_WAKE_TOOL = "cancel_wake";
|
|
7563
7588
|
var CANCEL_WAKE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${CANCEL_WAKE_TOOL}`;
|
|
7589
|
+
var TURN_ACT_TIMEOUT_MS = 45e3;
|
|
7564
7590
|
function createReplyState() {
|
|
7565
|
-
return { answersMessageId: null
|
|
7566
|
-
}
|
|
7567
|
-
function createSendState() {
|
|
7568
|
-
return { agentId: null, message: null, order: null };
|
|
7569
|
-
}
|
|
7570
|
-
function createTurnControlOrder() {
|
|
7571
|
-
let calls = 0;
|
|
7572
|
-
return {
|
|
7573
|
-
next: () => {
|
|
7574
|
-
calls += 1;
|
|
7575
|
-
return calls;
|
|
7576
|
-
}
|
|
7577
|
-
};
|
|
7591
|
+
return { answersMessageId: null };
|
|
7578
7592
|
}
|
|
7579
7593
|
function createSkipState() {
|
|
7580
7594
|
return { skipped: false, reason: null };
|
|
7581
7595
|
}
|
|
7582
|
-
function createAskState() {
|
|
7583
|
-
return {
|
|
7584
|
-
targetUserId: null,
|
|
7585
|
-
question: null,
|
|
7586
|
-
headline: null,
|
|
7587
|
-
options: null,
|
|
7588
|
-
questions: null,
|
|
7589
|
-
order: null
|
|
7590
|
-
};
|
|
7591
|
-
}
|
|
7592
|
-
function createWakeState() {
|
|
7593
|
-
return { afterSeconds: null, at: null, note: null, cancelled: false };
|
|
7594
|
-
}
|
|
7595
7596
|
function resolveDeclaredReplyField(input) {
|
|
7596
7597
|
const explicit = input.replyState.answersMessageId;
|
|
7597
7598
|
if (explicit) return { answersMessageId: explicit };
|
|
7598
7599
|
if (input.kind !== "final") return {};
|
|
7599
7600
|
if (!input.owedReplyMessageId) return {};
|
|
7600
|
-
const outwardSend = Boolean(
|
|
7601
|
-
input.sendState.agentId && input.sendState.message && input.sendState.agentId !== input.agentId
|
|
7602
|
-
);
|
|
7603
|
-
const askRaised = Boolean(input.askState.targetUserId);
|
|
7604
|
-
const wakeArmed = input.wakeState.afterSeconds !== null || input.wakeState.at !== null;
|
|
7605
|
-
if (outwardSend || askRaised || wakeArmed) return {};
|
|
7606
7601
|
return { answersMessageId: input.owedReplyMessageId, answersAutoDeclared: true };
|
|
7607
7602
|
}
|
|
7608
|
-
function
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
|
|
7603
|
+
function toolError(text) {
|
|
7604
|
+
return { isError: true, content: [{ type: "text", text }] };
|
|
7605
|
+
}
|
|
7606
|
+
async function performAct(acts, act) {
|
|
7607
|
+
let result;
|
|
7608
|
+
try {
|
|
7609
|
+
result = await acts.api.turnAct(
|
|
7610
|
+
acts.workspaceId,
|
|
7611
|
+
acts.turnId,
|
|
7612
|
+
act,
|
|
7613
|
+
AbortSignal.timeout(TURN_ACT_TIMEOUT_MS)
|
|
7614
|
+
);
|
|
7615
|
+
} catch (err) {
|
|
7616
|
+
if (err instanceof ApiError) {
|
|
7617
|
+
if (err.status === 409) {
|
|
7618
|
+
return toolError(
|
|
7619
|
+
"No active turn: this turn has already settled, so the act was not performed."
|
|
7620
|
+
);
|
|
7621
|
+
}
|
|
7622
|
+
return toolError(`The act was refused (${err.status}): ${err.message}`);
|
|
7617
7623
|
}
|
|
7618
|
-
|
|
7624
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7625
|
+
return toolError(
|
|
7626
|
+
`Cabane could not be reached (${message}), so it is UNKNOWN whether this act was recorded \u2014 it may have committed with only the response lost. Do NOT simply repeat the call: a repeat is a second, separate act, not a retry. Carry on, and say in your reply that you could not confirm it.`
|
|
7627
|
+
);
|
|
7628
|
+
}
|
|
7629
|
+
if (!result.ok) return toolError(JSON.stringify(result));
|
|
7630
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
7619
7631
|
}
|
|
7620
|
-
function createTurnControlMcpServer(
|
|
7632
|
+
function createTurnControlMcpServer(acts, skipState, replyState) {
|
|
7633
|
+
const ACT_TOOL = { readOnlyHint: false, destructiveHint: false, openWorldHint: false };
|
|
7634
|
+
const CANCEL_TOOL = { ...ACT_TOOL, idempotentHint: true };
|
|
7635
|
+
const RECORDER_TOOL = { readOnlyHint: true, openWorldHint: false };
|
|
7621
7636
|
return createSdkMcpServer({
|
|
7622
7637
|
name: COMPANION_LOCAL_MCP_SERVER,
|
|
7623
7638
|
version: "0.0.0",
|
|
7624
7639
|
tools: [
|
|
7625
7640
|
tool(
|
|
7626
7641
|
SEND_TOOL,
|
|
7627
|
-
"Send one addressed message to another agent in THIS conversation. Pass the peer's `agentId` and the complete `message` they should act on
|
|
7642
|
+
"Send one addressed message to another agent in THIS conversation. Pass the peer's `agentId` and the complete `message` they should act on. The message is WRITTEN the moment you call this \u2014 durable, in the conversation's queue, in the order you called \u2014 but it is DELIVERED only after your own turn ends: the peer is not running yet and cannot answer you inside this turn. Each call is one message; two calls are two messages. Writing `@handle` in prose dispatches nobody. An unknown, deactivated, or self target is refused here, in the result. A handoff to a DIFFERENT conversation is `conversations.create` / `conversations.post` with their `dispatch` field instead. Never use send to hand an answer back to the requester \u2014 declare that with reply_to.",
|
|
7628
7643
|
{
|
|
7629
7644
|
agentId: z13.string().uuid().describe(
|
|
7630
7645
|
"The peer agent to address \u2014 a workspace agent id, from your turn context's roster."
|
|
7631
7646
|
),
|
|
7632
7647
|
message: z13.string().min(1).max(65536).describe("The complete new request the peer should receive and act on.")
|
|
7633
7648
|
},
|
|
7634
|
-
async (args) => {
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
},
|
|
7642
|
-
{ annotations: { readOnlyHint: true, openWorldHint: false }, alwaysLoad: true }
|
|
7649
|
+
async (args) => performAct(acts, {
|
|
7650
|
+
kind: "send",
|
|
7651
|
+
callId: randomUUID(),
|
|
7652
|
+
agentId: args.agentId,
|
|
7653
|
+
message: args.message
|
|
7654
|
+
}),
|
|
7655
|
+
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
7643
7656
|
),
|
|
7644
7657
|
...replyState ? [
|
|
7645
7658
|
tool(
|
|
7646
7659
|
REPLY_TO_TOOL,
|
|
7647
|
-
"Declare which addressed ask your final response answers. Pass the `messageId` shown as the owed reply id in your turn context, then finish your response normally. This records lineage only: it does not send another message. The server verifies that this agent owes that ask in this conversation; one reply per turn
|
|
7660
|
+
"Declare which addressed ask your final response answers. Pass the `messageId` shown as the owed reply id in your turn context, then finish your response normally. This records lineage only: it does not send another message. The server verifies that this agent owes that ask in this conversation; one reply per turn \u2014 a later call replaces the earlier declaration.",
|
|
7648
7661
|
{
|
|
7649
7662
|
messageId: z13.string().uuid().describe("The owed ask message id from this turn context.")
|
|
7650
7663
|
},
|
|
7651
7664
|
async (args) => {
|
|
7652
7665
|
replyState.answersMessageId = args.messageId;
|
|
7653
|
-
replyState.order = controlOrder?.next() ?? null;
|
|
7654
7666
|
return {
|
|
7655
7667
|
content: [
|
|
7656
7668
|
{
|
|
@@ -7660,7 +7672,7 @@ function createTurnControlMcpServer(sendState, skipState, askState, wakeState, r
|
|
|
7660
7672
|
]
|
|
7661
7673
|
};
|
|
7662
7674
|
},
|
|
7663
|
-
{ annotations:
|
|
7675
|
+
{ annotations: RECORDER_TOOL, alwaysLoad: true }
|
|
7664
7676
|
)
|
|
7665
7677
|
] : [],
|
|
7666
7678
|
...skipState ? [
|
|
@@ -7677,162 +7689,146 @@ function createTurnControlMcpServer(sendState, skipState, askState, wakeState, r
|
|
|
7677
7689
|
content: [{ type: "text", text: JSON.stringify({ skipped: true }) }]
|
|
7678
7690
|
};
|
|
7679
7691
|
},
|
|
7680
|
-
{ annotations:
|
|
7692
|
+
{ annotations: RECORDER_TOOL, alwaysLoad: true }
|
|
7681
7693
|
)
|
|
7682
7694
|
] : [],
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
|
|
7686
|
-
|
|
7687
|
-
|
|
7688
|
-
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
question:
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
z13.
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
const hasArray = args.questions !== void 0 && args.questions.length > 0;
|
|
7713
|
-
if (hasSingle && hasArray) {
|
|
7714
|
-
return {
|
|
7715
|
-
isError: true,
|
|
7716
|
-
content: [
|
|
7717
|
-
{
|
|
7718
|
-
type: "text",
|
|
7719
|
-
text: "Provide either `question` (single) or `questions` (array), not both."
|
|
7720
|
-
}
|
|
7721
|
-
]
|
|
7722
|
-
};
|
|
7723
|
-
}
|
|
7724
|
-
if (!hasSingle && !hasArray) {
|
|
7725
|
-
return {
|
|
7726
|
-
isError: true,
|
|
7727
|
-
content: [
|
|
7728
|
-
{
|
|
7729
|
-
type: "text",
|
|
7730
|
-
text: "Provide `question` (single) or `questions` (array)."
|
|
7731
|
-
}
|
|
7732
|
-
]
|
|
7733
|
-
};
|
|
7734
|
-
}
|
|
7735
|
-
askState.targetUserId = args.targetUserId;
|
|
7736
|
-
askState.order = controlOrder?.next() ?? null;
|
|
7737
|
-
if (hasArray) {
|
|
7738
|
-
askState.questions = args.questions;
|
|
7739
|
-
askState.question = null;
|
|
7740
|
-
askState.headline = null;
|
|
7741
|
-
askState.options = null;
|
|
7742
|
-
} else {
|
|
7743
|
-
askState.question = args.question;
|
|
7744
|
-
askState.headline = args.headline ?? null;
|
|
7745
|
-
askState.options = args.options ?? null;
|
|
7746
|
-
askState.questions = null;
|
|
7747
|
-
}
|
|
7748
|
-
return {
|
|
7749
|
-
content: [
|
|
7750
|
-
{ type: "text", text: JSON.stringify({ asked: args.targetUserId }) }
|
|
7751
|
-
]
|
|
7752
|
-
};
|
|
7753
|
-
},
|
|
7754
|
-
{ annotations: { readOnlyHint: true, openWorldHint: false }, alwaysLoad: true }
|
|
7755
|
-
)
|
|
7756
|
-
] : [],
|
|
7757
|
-
...wakeState ? [
|
|
7758
|
-
tool(
|
|
7759
|
-
WAKE_ME_TOOL,
|
|
7760
|
-
"Wake yourself later \u2014 end this turn now and be re-dispatched at a time you pick, with a note you write to yourself. Use it for \"wait until X\": when the thing you need hasn't happened yet (a PR isn't merged, a human hasn't answered), arm a wake, end your turn, and you're woken later to CHECK \u2014 read the workspace, and either act or re-arm. Ground the delay before you arm it. Almost every wake is short \u2014 seconds to a couple of hours \u2014 waiting on a condition you can name: a session limit resetting, a PR merging. Reach past a few hours only when (a) a human asked for that timing, or (b) the wait is pinned to a real external event you can name \u2014 a report that only runs Mondays, a known reset time. A speculative far-future check-in you invented yourself is the one thing not to arm: if no one asked and you can't name both what clears the wait and why it takes that long, don't arm it \u2014 finish now, or raise an `ask`. Pass EXACTLY ONE of `afterSeconds` (a relative delay \u2014 `300` for five minutes) or `at` (an absolute ISO-8601 timestamp WITH a zone, e.g. `2026-07-16T09:00:00-07:00` \u2014 YOU compute it from a phrase like \"tomorrow morning\"; the system never parses natural-language time). `note` is a message to your future self \u2014 it becomes the body of the wake message that re-dispatches you, so write the condition to re-check (\"check whether CT441 merged yet\"). The wake is armed when your turn SETTLES, not now, so the delay counts from the turn ending; one wake per turn (last call wins). This is the sanctioned way to schedule your own continuation \u2014 the ONLY one; never reach for a host cron/scheduler. Guardrails: at least 60s out, at most 14 days; widen the interval as a loop ages (5m \u2192 15m \u2192 1h\u2026) rather than hammering; after many consecutive re-arms with no other activity you'll be steered to raise an `ask` to the human instead. If a wake can't be armed you're re-dispatched with a note explaining why \u2014 never a silent drop.",
|
|
7761
|
-
{
|
|
7762
|
-
afterSeconds: z13.number().int().positive().optional().describe(
|
|
7763
|
-
"Relative delay in seconds from when this turn ends (e.g. 300 = five minutes). Provide EITHER this or `at`, not both. Floor 60s, horizon 14 days \u2014 enforced server-side."
|
|
7764
|
-
),
|
|
7765
|
-
at: z13.string().datetime({ offset: true }).optional().describe(
|
|
7766
|
-
"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."
|
|
7767
|
-
),
|
|
7768
|
-
note: z13.string().min(1).max(2e3).describe(
|
|
7769
|
-
'A note to your future self \u2014 becomes the body of the wake message that re-dispatches you. Write the condition to re-check ("check whether the PR merged").'
|
|
7695
|
+
tool(
|
|
7696
|
+
ASK_TOOL,
|
|
7697
|
+
`Ask a HUMAN a structured question (or a short LIST of them) you need answered to continue, then END your turn \u2014 don't wait for the reply. Use it when you genuinely can't proceed without a person's input (a decision only they can make, a missing fact). Pass \`targetUserId\` (a workspace member's user id \u2014 every person's id is on the roster in your turn context). Two forms: a SINGLE question \u2014 a \`headline\` (the actual question as one clear, capitalized sentence ending in \`?\`, "Do we go to prod?") plus a short \`question\` body for the framing the headline can't hold \u2014 OR, when a plan ends with SEVERAL bounded decisions at once, a \`questions\` array of 1\u2013${MAX_ASK_ITEMS} items, each \`{ headline, body?, options? }\`. **Prefer the list over cramming the extra decisions into prose or dropping them** \u2014 one ask carrying every question, never pick one and bury the rest. Each question keeps the same form rules: a one-sentence \`headline\`, a short \`body\` frame (NOT a report \u2014 your status, links, and detail go in your REPLY; inline markdown only, no lists or headings), and 2\u20134 \`options\` when the answer is a bounded choice \u2014 for a yes/no go-ahead always pass them, so it's one click, not a typed reply. Provide EITHER \`question\` (single) or \`questions\` (array), never both. The ask is a first-class attention item aimed at that person; your final reply carries the CONTEXT, the ask carries the QUESTION(S). It is written and delivered THE MOMENT you call this \u2014 the person sees it in their inbox while your turn is still running \u2014 so call it once you know the question; a second call is a second question, not an edit. The human sends ONE response: supplied answers are recorded, omissions become terminally Unanswered. Ask only when blocked \u2014 never ceremonially. \`replaces\` withdraws a stale ask of yours (or anyone's) in this conversation in the same write, with the reason shown to the person. Targets a human only; to hand work to another AGENT use send/dispatch instead.`,
|
|
7698
|
+
{
|
|
7699
|
+
targetUserId: z13.string().uuid().describe(
|
|
7700
|
+
"The workspace member (human) to ask \u2014 a user id, from your turn context's roster."
|
|
7701
|
+
),
|
|
7702
|
+
question: z13.string().min(1).max(400).optional().describe(
|
|
7703
|
+
"SINGLE-question form: a short body \u2014 one or two sentences of framing the headline can't hold. NOT a report (capped, inline markdown only). Provide EITHER this or `questions`, not both. Put the crisp one-sentence question in `headline`."
|
|
7704
|
+
),
|
|
7705
|
+
headline: z13.string().min(1).max(120).optional().describe(
|
|
7706
|
+
'SINGLE-question form: the question itself as ONE clear, capitalized sentence ending in `?` ("Do we go to prod?"). What the human reads first in the inbox and the chip \u2014 one scannable question, no elaboration (that goes in `question`). Strongly encouraged.'
|
|
7707
|
+
),
|
|
7708
|
+
options: z13.array(z13.string().min(1).max(200)).min(2).max(4).optional().describe("SINGLE-question form: optional 2\u20134 suggested one-click answers."),
|
|
7709
|
+
questions: z13.array(
|
|
7710
|
+
z13.object({
|
|
7711
|
+
headline: z13.string().min(1).max(120).describe(
|
|
7712
|
+
'The one-sentence question ("Do we go to prod?") \u2014 required for each item.'
|
|
7713
|
+
),
|
|
7714
|
+
body: z13.string().min(1).max(400).optional().describe("Optional short framing beneath the headline. NOT a report."),
|
|
7715
|
+
options: z13.array(z13.string().min(1).max(200)).min(2).max(4).optional().describe("Optional 2\u20134 one-click answers for this question.")
|
|
7716
|
+
})
|
|
7717
|
+
).min(1).max(MAX_ASK_ITEMS).optional().describe(
|
|
7718
|
+
`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.`
|
|
7719
|
+
),
|
|
7720
|
+
replaces: z13.object({
|
|
7721
|
+
askId: z13.string().uuid().describe("The id of the open ask to withdraw."),
|
|
7722
|
+
reason: z13.string().min(1).max(MAX_WITHDRAW_REASON).describe(
|
|
7723
|
+
"Why the old question is no longer the question \u2014 shown to the person on their Done row."
|
|
7770
7724
|
)
|
|
7771
|
-
}
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
|
|
7725
|
+
}).optional().describe(
|
|
7726
|
+
"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."
|
|
7727
|
+
)
|
|
7728
|
+
},
|
|
7729
|
+
async (args) => {
|
|
7730
|
+
const hasSingle = args.question !== void 0;
|
|
7731
|
+
const hasArray = args.questions !== void 0 && args.questions.length > 0;
|
|
7732
|
+
if (hasSingle && hasArray) {
|
|
7733
|
+
return toolError(
|
|
7734
|
+
"Provide either `question` (single) or `questions` (array), not both."
|
|
7735
|
+
);
|
|
7736
|
+
}
|
|
7737
|
+
if (!hasSingle && !hasArray) {
|
|
7738
|
+
return toolError("Provide `question` (single) or `questions` (array).");
|
|
7739
|
+
}
|
|
7740
|
+
return performAct(acts, {
|
|
7741
|
+
kind: "ask",
|
|
7742
|
+
callId: randomUUID(),
|
|
7743
|
+
// CT1350: the swap rides the ask, so the server commits both halves
|
|
7744
|
+
// in one transaction — the person never sees neither or both.
|
|
7745
|
+
...args.replaces ? { replaces: args.replaces } : {},
|
|
7746
|
+
ask: hasArray ? {
|
|
7747
|
+
targetUserId: args.targetUserId,
|
|
7748
|
+
questions: args.questions.map((q) => ({
|
|
7749
|
+
headline: q.headline,
|
|
7750
|
+
...q.body ? { body: q.body } : {},
|
|
7751
|
+
...q.options && q.options.length > 0 ? { options: q.options } : {}
|
|
7752
|
+
}))
|
|
7753
|
+
} : {
|
|
7754
|
+
targetUserId: args.targetUserId,
|
|
7755
|
+
question: args.question,
|
|
7756
|
+
...args.headline ? { headline: args.headline } : {},
|
|
7757
|
+
...args.options && args.options.length > 0 ? { options: args.options } : {}
|
|
7785
7758
|
}
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7759
|
+
});
|
|
7760
|
+
},
|
|
7761
|
+
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
7762
|
+
),
|
|
7763
|
+
tool(
|
|
7764
|
+
WAKE_ME_TOOL,
|
|
7765
|
+
"Wake yourself later \u2014 end this turn now and be re-dispatched at a time you pick, with a note you write to yourself. Use it for \"wait until X\": when the thing you need hasn't happened yet (a PR isn't merged, a human hasn't answered), arm a wake, end your turn, and you're woken later to CHECK \u2014 read the workspace, and either act or re-arm. Ground the delay before you arm it. Almost every wake is short \u2014 seconds to a couple of hours \u2014 waiting on a condition you can name: a session limit resetting, a PR merging. Reach past a few hours only when (a) a human asked for that timing, or (b) the wait is pinned to a real external event you can name \u2014 a report that only runs Mondays, a known reset time. A speculative far-future check-in you invented yourself is the one thing not to arm: if no one asked and you can't name both what clears the wait and why it takes that long, don't arm it \u2014 finish now, or raise an `ask`. Pass EXACTLY ONE of `afterSeconds` (a relative delay \u2014 `300` for five minutes) or `at` (an absolute ISO-8601 timestamp WITH a zone, e.g. `2026-07-16T09:00:00-07:00` \u2014 YOU compute it; the system never parses natural-language time). `note` is a message to your future self \u2014 it becomes the body of the wake message that re-dispatches you, so write the condition to re-check (\"check whether CT441 merged yet\"). The wake arms NOW, when you call this, and the delay counts from this call; a later `wake_me` in the same turn replaces it, and `cancel_wake` stands it down. A wake that comes due while your turn is still running waits and is delivered after your reply. This is the ONLY sanctioned way to schedule your own continuation; never reach for a host cron/scheduler. Guardrails: at least 60s out, at most 14 days \u2014 a wake that can't be armed returns an error here, in this call, so you can recompute; widen the interval as a loop ages (5m \u2192 15m \u2192 1h\u2026) rather than hammering; after many consecutive re-arms with no other activity you'll be steered to raise an `ask` to the human instead.",
|
|
7766
|
+
{
|
|
7767
|
+
afterSeconds: z13.number().int().positive().optional().describe(
|
|
7768
|
+
"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."
|
|
7769
|
+
),
|
|
7770
|
+
at: z13.string().datetime({ offset: true }).optional().describe(
|
|
7771
|
+
"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."
|
|
7772
|
+
),
|
|
7773
|
+
note: z13.string().min(1).max(2e3).describe(
|
|
7774
|
+
'A note to your future self \u2014 becomes the body of the wake message that re-dispatches you. Write the condition to re-check ("check whether the PR merged").'
|
|
7775
|
+
)
|
|
7776
|
+
},
|
|
7777
|
+
async (args) => {
|
|
7778
|
+
const hasAfter = args.afterSeconds !== void 0;
|
|
7779
|
+
const hasAt = args.at !== void 0;
|
|
7780
|
+
if (hasAfter && hasAt) {
|
|
7781
|
+
return toolError(
|
|
7782
|
+
"Provide either `afterSeconds` (relative) or `at` (absolute), not both."
|
|
7783
|
+
);
|
|
7784
|
+
}
|
|
7785
|
+
if (!hasAfter && !hasAt) {
|
|
7786
|
+
return toolError("Provide `afterSeconds` or `at`.");
|
|
7787
|
+
}
|
|
7788
|
+
return performAct(acts, {
|
|
7789
|
+
kind: "wake",
|
|
7790
|
+
callId: randomUUID(),
|
|
7791
|
+
wake: {
|
|
7792
|
+
...hasAfter ? { afterSeconds: args.afterSeconds } : {},
|
|
7793
|
+
...hasAt ? { at: args.at } : {},
|
|
7794
|
+
note: args.note
|
|
7791
7795
|
}
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
|
|
7800
|
-
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
}
|
|
7807
|
-
|
|
7808
|
-
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
|
|
7812
|
-
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
{}
|
|
7816
|
-
|
|
7817
|
-
|
|
7818
|
-
|
|
7819
|
-
|
|
7820
|
-
|
|
7821
|
-
|
|
7822
|
-
|
|
7823
|
-
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
|
|
7827
|
-
|
|
7828
|
-
})
|
|
7829
|
-
}
|
|
7830
|
-
]
|
|
7831
|
-
};
|
|
7832
|
-
},
|
|
7833
|
-
{ annotations: { readOnlyHint: true, openWorldHint: false }, alwaysLoad: true }
|
|
7834
|
-
)
|
|
7835
|
-
] : []
|
|
7796
|
+
});
|
|
7797
|
+
},
|
|
7798
|
+
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
7799
|
+
),
|
|
7800
|
+
// CT1350: the standalone withdrawal. Not annotated destructive — an ask
|
|
7801
|
+
// is a question, not data, and a harness that gated this on a live
|
|
7802
|
+
// operator would hang a turn nobody is watching (the `cancel_wake`
|
|
7803
|
+
// reasoning). Not idempotent either: a second call with the same id is
|
|
7804
|
+
// refused with `ask_not_open`, which is information, not a no-op.
|
|
7805
|
+
tool(
|
|
7806
|
+
WITHDRAW_ASK_TOOL,
|
|
7807
|
+
"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.",
|
|
7808
|
+
{
|
|
7809
|
+
askId: z13.string().uuid().describe(
|
|
7810
|
+
"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."
|
|
7811
|
+
),
|
|
7812
|
+
reason: z13.string().min(1).max(MAX_WITHDRAW_REASON).describe(
|
|
7813
|
+
'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.'
|
|
7814
|
+
)
|
|
7815
|
+
},
|
|
7816
|
+
async (args) => performAct(acts, {
|
|
7817
|
+
kind: "withdraw_ask",
|
|
7818
|
+
callId: randomUUID(),
|
|
7819
|
+
withdraw: { askId: args.askId, reason: args.reason }
|
|
7820
|
+
}),
|
|
7821
|
+
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
7822
|
+
),
|
|
7823
|
+
// CT990: the inverse. No arguments — it can only ever stand down the
|
|
7824
|
+
// caller's own wake in the conversation it's holding a turn in.
|
|
7825
|
+
tool(
|
|
7826
|
+
CANCEL_WAKE_TOOL,
|
|
7827
|
+
'Stand down your own wake \u2014 the inverse of `wake_me`. No arguments: it cancels the wake you have armed in this conversation, NOW, when you call it. Reach for it when the thing you armed a wake to check has already happened, or the work it was watching is over \u2014 an armed wake you no longer need fires into a turn with nothing to do, and a supervision loop with no off switch is one you can only end by leaving it running. Whether you have one armed is printed in your turn context ("Wake armed: \u2026"); calling this with nothing armed is a clean no-op, no error. Each call acts as it arrives: `cancel_wake` then `wake_me` leaves the NEW wake armed; `wake_me` then `cancel_wake` leaves nothing armed. It reaches only your own wake in this conversation \u2014 never a peer\'s, and never a recurring schedule (those are `schedules.*`).',
|
|
7828
|
+
{},
|
|
7829
|
+
async () => performAct(acts, { kind: "cancel_wake", callId: randomUUID() }),
|
|
7830
|
+
{ annotations: CANCEL_TOOL, alwaysLoad: true }
|
|
7831
|
+
)
|
|
7836
7832
|
]
|
|
7837
7833
|
});
|
|
7838
7834
|
}
|
|
@@ -8259,112 +8255,25 @@ var TurnCommitter = class {
|
|
|
8259
8255
|
return this.pump.finalEmitted;
|
|
8260
8256
|
}
|
|
8261
8257
|
// A closing textual reply and a wordless terminal marker carry the same
|
|
8262
|
-
//
|
|
8263
|
-
//
|
|
8264
|
-
//
|
|
8265
|
-
// a wordless turn has no answer to bind, and the server's mute-settle
|
|
8266
|
-
// notice speaks for it.
|
|
8258
|
+
// declared-reply projection. The KIND matters: the auto-declared reply binds
|
|
8259
|
+
// only a textual `final` — a wordless turn has no answer to bind, and the
|
|
8260
|
+
// server's mute-settle notice speaks for it.
|
|
8267
8261
|
turnControlFields(kind) {
|
|
8268
|
-
|
|
8269
|
-
...this.answersField(kind),
|
|
8270
|
-
...this.sendField(),
|
|
8271
|
-
...this.askField(),
|
|
8272
|
-
...this.wakeField()
|
|
8273
|
-
};
|
|
8274
|
-
return { ...fields, ...this.orderField(fields) };
|
|
8275
|
-
}
|
|
8276
|
-
// CT1281: the order the control tools were CALLED, for the addressed rows the
|
|
8277
|
-
// server writes from this one commit. It reports a position only for an intent
|
|
8278
|
-
// that actually made it into the commit — a self-send the committer stripped
|
|
8279
|
-
// contributes no row and so has no place in the line. An intent with no
|
|
8280
|
-
// recorded call (the runtime's auto-declared reply) is deliberately absent, and
|
|
8281
|
-
// the server sorts it after every recorded one — it binds the turn's closing
|
|
8282
|
-
// words.
|
|
8283
|
-
//
|
|
8284
|
-
// CT1347: the ask has a position too. Its carrier used to queue last however
|
|
8285
|
-
// early the tool fired, because a landed open ask held the conversation and an
|
|
8286
|
-
// ask ahead of its own turn's send would have stranded that send behind a
|
|
8287
|
-
// person's response time. Nothing holds now, so the ask is an addressed intent
|
|
8288
|
-
// like the other two and queues where it was called.
|
|
8289
|
-
orderField(fields) {
|
|
8290
|
-
const order = {};
|
|
8291
|
-
const replyOrder = this.deps.replyState.order;
|
|
8292
|
-
if (fields.answersMessageId !== void 0 && replyOrder !== null) order.reply = replyOrder;
|
|
8293
|
-
if (fields.dispatch !== void 0 && this.deps.sendState.order !== null) {
|
|
8294
|
-
order.send = this.deps.sendState.order;
|
|
8295
|
-
}
|
|
8296
|
-
if (fields.ask !== void 0 && this.deps.askState.order !== null) {
|
|
8297
|
-
order.ask = this.deps.askState.order;
|
|
8298
|
-
}
|
|
8299
|
-
return Object.keys(order).length > 0 ? { turnControlOrder: order } : {};
|
|
8262
|
+
return this.answersField(kind);
|
|
8300
8263
|
}
|
|
8301
8264
|
// The declared reply: explicit `reply_to` first, else the runtime's own
|
|
8302
8265
|
// declaration for the turn that just answers — see
|
|
8303
8266
|
// `resolveDeclaredReplyField` for the whole rule and its reasons. CT1224: an
|
|
8304
|
-
// auto-declaration is flagged as such on the wire, because the server drops
|
|
8305
|
-
// when the turn
|
|
8306
|
-
// `reply_to` carries no flag and always stands.
|
|
8267
|
+
// auto-declaration is flagged as such on the wire, because the server drops
|
|
8268
|
+
// it when the turn performed an outward act (CT1354: any of them, read off
|
|
8269
|
+
// the ledger). An explicit `reply_to` carries no flag and always stands.
|
|
8307
8270
|
answersField(kind) {
|
|
8308
8271
|
return resolveDeclaredReplyField({
|
|
8309
8272
|
kind,
|
|
8310
8273
|
owedReplyMessageId: this.deps.owedReplyMessageId,
|
|
8311
|
-
|
|
8312
|
-
replyState: this.deps.replyState,
|
|
8313
|
-
sendState: this.deps.sendState,
|
|
8314
|
-
askState: this.deps.askState,
|
|
8315
|
-
wakeState: this.deps.wakeState
|
|
8274
|
+
replyState: this.deps.replyState
|
|
8316
8275
|
});
|
|
8317
8276
|
}
|
|
8318
|
-
// Carry both halves of the addressed send. The server writes `dispatchBody`
|
|
8319
|
-
// on a distinct message row; the turn row itself remains unaddressed speech.
|
|
8320
|
-
sendField() {
|
|
8321
|
-
const { agentId: target, message } = this.deps.sendState;
|
|
8322
|
-
if (!target || !message || target === this.deps.agentId) return {};
|
|
8323
|
-
return { dispatch: target, dispatchBody: message };
|
|
8324
|
-
}
|
|
8325
|
-
// CT326: resolve the per-turn ask into the `ask` field for a `final` commit.
|
|
8326
|
-
// The server validates the target (must be a workspace member/owner) and
|
|
8327
|
-
// creates the `asks` row; an absent/incomplete ask attaches nothing. Kept
|
|
8328
|
-
// independent of `sendField` — a turn can send and ask independently.
|
|
8329
|
-
askField() {
|
|
8330
|
-
const { targetUserId, question, headline, options, questions } = this.deps.askState;
|
|
8331
|
-
if (!targetUserId) return {};
|
|
8332
|
-
if (questions && questions.length > 0) {
|
|
8333
|
-
return {
|
|
8334
|
-
ask: {
|
|
8335
|
-
targetUserId,
|
|
8336
|
-
questions: questions.map((q) => ({
|
|
8337
|
-
headline: q.headline,
|
|
8338
|
-
...q.body ? { body: q.body } : {},
|
|
8339
|
-
...q.options && q.options.length > 0 ? { options: q.options } : {}
|
|
8340
|
-
}))
|
|
8341
|
-
}
|
|
8342
|
-
};
|
|
8343
|
-
}
|
|
8344
|
-
if (!question) return {};
|
|
8345
|
-
return {
|
|
8346
|
-
ask: {
|
|
8347
|
-
targetUserId,
|
|
8348
|
-
question,
|
|
8349
|
-
// CT400: the optional one-sentence headline question, when supplied.
|
|
8350
|
-
...headline ? { headline } : {},
|
|
8351
|
-
...options && options.length > 0 ? { options } : {}
|
|
8352
|
-
}
|
|
8353
|
-
};
|
|
8354
|
-
}
|
|
8355
|
-
// CT442: resolve the per-turn wake into the `wake` field for a `final` commit.
|
|
8356
|
-
// The tool guarantees exactly one of `afterSeconds`/`at` is set once armed (both
|
|
8357
|
-
// null = no wake this turn → attach nothing). The server computes `fire_at` and
|
|
8358
|
-
// arms the CT441 schedule. Kept independent of send/ask — a turn could
|
|
8359
|
-
// conceivably ask AND arm a wake.
|
|
8360
|
-
//
|
|
8361
|
-
// CT990: the field now carries three states, and the third can't be spelled by
|
|
8362
|
-
// absence — attaching NOTHING means "leave the standing wake alone", which is
|
|
8363
|
-
// exactly what a stand-down must not do. So a `cancel_wake` turn attaches the
|
|
8364
|
-
// discriminated `{ cancel: true }` payload instead.
|
|
8365
|
-
wakeField() {
|
|
8366
|
-
return wakeCommitField(this.deps.wakeState);
|
|
8367
|
-
}
|
|
8368
8277
|
};
|
|
8369
8278
|
|
|
8370
8279
|
// src/turn-execution.ts
|
|
@@ -8455,7 +8364,7 @@ var TurnExecution = class {
|
|
|
8455
8364
|
agentId: payload.agentId,
|
|
8456
8365
|
messageId: payload.messageId
|
|
8457
8366
|
});
|
|
8458
|
-
this.turnId = handleOpts.turnId ??
|
|
8367
|
+
this.turnId = handleOpts.turnId ?? randomUUID2();
|
|
8459
8368
|
}
|
|
8460
8369
|
opts;
|
|
8461
8370
|
supervisor;
|
|
@@ -8475,12 +8384,8 @@ var TurnExecution = class {
|
|
|
8475
8384
|
effectiveCwd;
|
|
8476
8385
|
hookEnv;
|
|
8477
8386
|
turnEnv;
|
|
8478
|
-
sendState;
|
|
8479
8387
|
skipState;
|
|
8480
|
-
askState;
|
|
8481
|
-
wakeState;
|
|
8482
8388
|
replyState;
|
|
8483
|
-
controlOrder;
|
|
8484
8389
|
turnControlServer;
|
|
8485
8390
|
request;
|
|
8486
8391
|
adapter;
|
|
@@ -8650,7 +8555,7 @@ var TurnExecution = class {
|
|
|
8650
8555
|
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
8651
8556
|
body: `${MISSING_SECRET_PREFIX} ${list}`,
|
|
8652
8557
|
kind: "final",
|
|
8653
|
-
turnId:
|
|
8558
|
+
turnId: randomUUID2(),
|
|
8654
8559
|
// CT113: even a turn that fails before it runs answers a message.
|
|
8655
8560
|
parentMessageId: payload.messageId
|
|
8656
8561
|
});
|
|
@@ -8857,19 +8762,12 @@ ${reason}`,
|
|
|
8857
8762
|
}
|
|
8858
8763
|
buildRequest() {
|
|
8859
8764
|
const { payload, workspaceId } = this;
|
|
8860
|
-
const sendState = this.sendState = createSendState();
|
|
8861
8765
|
const skipState = this.skipState = createSkipState();
|
|
8862
|
-
const askState = this.askState = createAskState();
|
|
8863
|
-
const wakeState = this.wakeState = createWakeState();
|
|
8864
8766
|
const replyState = this.replyState = createReplyState();
|
|
8865
|
-
const controlOrder = this.controlOrder = createTurnControlOrder();
|
|
8866
8767
|
const turnControlServer = createTurnControlMcpServer(
|
|
8867
|
-
|
|
8768
|
+
{ api: this.opts.api, workspaceId, turnId: this.turnId },
|
|
8868
8769
|
skipState,
|
|
8869
|
-
|
|
8870
|
-
wakeState,
|
|
8871
|
-
replyState,
|
|
8872
|
-
controlOrder
|
|
8770
|
+
replyState
|
|
8873
8771
|
);
|
|
8874
8772
|
this.request = buildCompanionTurnRequest({
|
|
8875
8773
|
turnContext: this.turnContext,
|
|
@@ -8971,16 +8869,6 @@ ${reason}`,
|
|
|
8971
8869
|
signal: abortController.signal,
|
|
8972
8870
|
log: turnLog,
|
|
8973
8871
|
nextSeq: this.nextSeq,
|
|
8974
|
-
// The committer reads this at commit to carry the addressed send on the
|
|
8975
|
-
// terminal row; the server writes the send itself as a distinct message.
|
|
8976
|
-
sendState: this.sendState,
|
|
8977
|
-
// CT326: likewise the ask payload. CT1281: the server writes the ask as its
|
|
8978
|
-
// own addressed message to the human — which ENQUEUES like any other send —
|
|
8979
|
-
// and creates the `asks` row against that carrier, not against turn speech.
|
|
8980
|
-
askState: this.askState,
|
|
8981
|
-
// CT442: likewise the wake payload — attached to the `final` row so the
|
|
8982
|
-
// server arms the wake schedule atomically with the reply it rode on.
|
|
8983
|
-
wakeState: this.wakeState,
|
|
8984
8872
|
replyState: this.replyState,
|
|
8985
8873
|
// The ledger-derived owed reply, for the runtime's own declaration when
|
|
8986
8874
|
// the agent doesn't call `reply_to` (resolveDeclaredReplyField).
|
|
@@ -9003,41 +8891,8 @@ ${reason}`,
|
|
|
9003
8891
|
payload.agentId,
|
|
9004
8892
|
turnId
|
|
9005
8893
|
);
|
|
9006
|
-
if (intent.ask) {
|
|
9007
|
-
this.askState.targetUserId = intent.ask.targetUserId;
|
|
9008
|
-
if (intent.ask.questions && intent.ask.questions.length > 0) {
|
|
9009
|
-
this.askState.questions = intent.ask.questions;
|
|
9010
|
-
this.askState.question = null;
|
|
9011
|
-
this.askState.headline = null;
|
|
9012
|
-
this.askState.options = null;
|
|
9013
|
-
} else {
|
|
9014
|
-
this.askState.question = intent.ask.question ?? null;
|
|
9015
|
-
this.askState.headline = intent.ask.headline ?? null;
|
|
9016
|
-
this.askState.options = intent.ask.options ?? null;
|
|
9017
|
-
this.askState.questions = null;
|
|
9018
|
-
}
|
|
9019
|
-
}
|
|
9020
|
-
if (intent.wake) {
|
|
9021
|
-
if ("cancel" in intent.wake) {
|
|
9022
|
-
this.wakeState.cancelled = true;
|
|
9023
|
-
this.wakeState.afterSeconds = null;
|
|
9024
|
-
this.wakeState.at = null;
|
|
9025
|
-
this.wakeState.note = null;
|
|
9026
|
-
} else {
|
|
9027
|
-
this.wakeState.cancelled = false;
|
|
9028
|
-
this.wakeState.afterSeconds = intent.wake.afterSeconds ?? null;
|
|
9029
|
-
this.wakeState.at = intent.wake.at ?? null;
|
|
9030
|
-
this.wakeState.note = intent.wake.note;
|
|
9031
|
-
}
|
|
9032
|
-
}
|
|
9033
|
-
if (intent.sendAgentId && intent.sendBody) {
|
|
9034
|
-
this.sendState.agentId = intent.sendAgentId;
|
|
9035
|
-
this.sendState.message = intent.sendBody;
|
|
9036
|
-
this.sendState.order = intent.sendOrder ?? null;
|
|
9037
|
-
}
|
|
9038
8894
|
if (intent.answersMessageId) {
|
|
9039
8895
|
this.replyState.answersMessageId = intent.answersMessageId;
|
|
9040
|
-
this.replyState.order = intent.replyOrder ?? null;
|
|
9041
8896
|
}
|
|
9042
8897
|
if (intent.skipped) {
|
|
9043
8898
|
this.skipState.skipped = true;
|
|
@@ -9046,7 +8901,7 @@ ${reason}`,
|
|
|
9046
8901
|
} catch (err) {
|
|
9047
8902
|
turnLog.warn(
|
|
9048
8903
|
{ err: err instanceof Error ? err.message : String(err) },
|
|
9049
|
-
"dispatcher: turn-control intent fetch failed;
|
|
8904
|
+
"dispatcher: turn-control intent fetch failed; the declared reply / skip for this turn are dropped"
|
|
9050
8905
|
);
|
|
9051
8906
|
}
|
|
9052
8907
|
};
|
|
@@ -9189,15 +9044,13 @@ ${reason}`,
|
|
|
9189
9044
|
{ reason: this.skipState.reason, turnId, ok: o.okResult },
|
|
9190
9045
|
"agent skipped turn (skip_turn)"
|
|
9191
9046
|
);
|
|
9192
|
-
const { wake: skipWake } = wakeCommitField(this.wakeState);
|
|
9193
9047
|
try {
|
|
9194
9048
|
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
9195
9049
|
body: SKIPPED_MARKER_BODY,
|
|
9196
9050
|
kind: "skipped",
|
|
9197
9051
|
turnId,
|
|
9198
9052
|
seq: this.nextSeq(),
|
|
9199
|
-
parentMessageId: payload.messageId
|
|
9200
|
-
...skipWake ? { wake: skipWake } : {}
|
|
9053
|
+
parentMessageId: payload.messageId
|
|
9201
9054
|
});
|
|
9202
9055
|
} catch (err) {
|
|
9203
9056
|
turnLog.warn(
|
|
@@ -9215,9 +9068,9 @@ ${reason}`,
|
|
|
9215
9068
|
turnId,
|
|
9216
9069
|
seq: this.nextSeq(),
|
|
9217
9070
|
parentMessageId: payload.messageId,
|
|
9218
|
-
//
|
|
9219
|
-
//
|
|
9220
|
-
//
|
|
9071
|
+
// An explicit `reply_to` survives a wordless turn exactly as it
|
|
9072
|
+
// survives a textual final (the auto-declaration does not — a
|
|
9073
|
+
// marker has no answer to bind).
|
|
9221
9074
|
...committer.turnControlFields("silent")
|
|
9222
9075
|
});
|
|
9223
9076
|
o.silentMarkerEmitted = true;
|
|
@@ -10529,7 +10382,7 @@ var CompanionSupervisor = class {
|
|
|
10529
10382
|
);
|
|
10530
10383
|
}
|
|
10531
10384
|
const resumedTurnId = ev.id ? turnIdForEvent(workspaceId, ev.id) : null;
|
|
10532
|
-
const turnId = resumedTurnId ??
|
|
10385
|
+
const turnId = resumedTurnId ?? randomUUID3();
|
|
10533
10386
|
if (ev.id) {
|
|
10534
10387
|
const liveTurnIds = rememberTurnId(workspaceId, ev.id, turnId);
|
|
10535
10388
|
if (liveTurnIds > TURN_ID_OVERFLOW_WARN) {
|
|
@@ -10937,7 +10790,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
10937
10790
|
opencodeServerUrl: cfg.opencode?.serverUrl,
|
|
10938
10791
|
codex: isCodexEnabled(cfg)
|
|
10939
10792
|
});
|
|
10940
|
-
const instanceId =
|
|
10793
|
+
const instanceId = randomUUID4();
|
|
10941
10794
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10942
10795
|
const pre = readLiveRuntimeState();
|
|
10943
10796
|
if (pre && await verifyRuntime(pre) === "stale") clearRuntimeState();
|