@cabane/companion 0.6.218 → 0.6.220

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 CHANGED
@@ -1730,7 +1730,7 @@ var init_api = __esm({
1730
1730
  `/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}/turn-intent?${q}`
1731
1731
  );
1732
1732
  }
1733
- // Commit one outgoing act (ask, withdraw_ask, wake_me, cancel_wake) at
1733
+ // Commit one outgoing act (open_exchange, withdraw_exchange, wake_me, cancel_wake) at
1734
1734
  // invocation. The server records the effect and receipt in one transaction;
1735
1735
  // an ok:false response is a recorded refusal for the tool to report.
1736
1736
  // Retries retain the caller's callId and recover the same receipt, even
@@ -2277,7 +2277,7 @@ var init_turn_request = __esm({
2277
2277
  bearer: z8.string(),
2278
2278
  activeConversationId: z8.string(),
2279
2279
  // Scoped turn-control MCP URL. Codex and opencode mount it as `cabane_companion` with the bearer
2280
- // and active-conversation header, for ask, withdraw_ask, wake_me, cancel_wake and skip_turn.
2280
+ // and active-conversation header, for the turn-control tools (`TURN_CONTROL_TOOLS`).
2281
2281
  // Claude Code mounts the companion's in-process instance instead. Optional; the companion
2282
2282
  // populates it.
2283
2283
  turnControlUrl: z8.string().optional(),
@@ -2572,13 +2572,18 @@ var init_transcript_sink = __esm({
2572
2572
  });
2573
2573
 
2574
2574
  // packages/agent-runtime/src/turn-control/descriptions.ts
2575
- 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;
2575
+ var MAX_ASK_QUESTION_BYTES, MAX_ASK_HEADLINE_BYTES, MAX_ASK_ITEMS, EXCHANGE_KINDS, APPROVAL_OPTIONS, REQUEST_OPTIONS, MAX_EXCHANGE_FILES, MAX_REQUEST_BODY_BYTES, 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;
2576
2576
  var init_descriptions = __esm({
2577
2577
  "packages/agent-runtime/src/turn-control/descriptions.ts"() {
2578
2578
  "use strict";
2579
2579
  MAX_ASK_QUESTION_BYTES = 400;
2580
2580
  MAX_ASK_HEADLINE_BYTES = 120;
2581
2581
  MAX_ASK_ITEMS = 20;
2582
+ EXCHANGE_KINDS = ["question", "approval", "request"];
2583
+ APPROVAL_OPTIONS = ["Yes", "No"];
2584
+ REQUEST_OPTIONS = ["Done", "Couldn't"];
2585
+ MAX_EXCHANGE_FILES = 5;
2586
+ MAX_REQUEST_BODY_BYTES = 2e3;
2582
2587
  MAX_ASK_OPTION_BYTES = 200;
2583
2588
  ASK_OPTION_NOT_A_STRING = "An ask option is a plain string label.";
2584
2589
  MIN_ASK_OPTIONS = 2;
@@ -2591,41 +2596,44 @@ var init_descriptions = __esm({
2591
2596
  TURN_CONTROL_MCP_SERVER = "cabane_companion";
2592
2597
  TURN_CONTROL_TOOLS = [
2593
2598
  "answer",
2594
- "ask",
2595
2599
  "cancel_wake",
2600
+ "open_exchange",
2596
2601
  "skip_turn",
2597
2602
  "wake_me",
2598
- "withdraw_ask"
2603
+ "withdraw_exchange"
2599
2604
  ];
2600
2605
  TURN_CONTROL_TOOL_DESCRIPTIONS = {
2601
2606
  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.`,
2602
- 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.
2603
- - 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.
2604
- - \`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.
2605
- - 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.
2606
- - 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.
2607
- - 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.
2608
- - 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.
2609
- - 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\`.`,
2607
+ open_exchange: `Put something in front of one person that comes back with a recorded outcome, then end your turn; don't wait for it. \`targetUserId\` is a person on your roster, by their \`@handle\`; to bring in an agent, name them with \`@handle\` in your reply.
2608
+ - \`kind\`: \`question\` for a choice among courses of action (two included) or free text; \`approval\` for a yes/no on one action you propose; \`request\` for something only the person can do, reported back.
2609
+ - A question is one item (\`headline\`, optional \`question\` body, \`options\`) or \`questions\`, 1\u2013${MAX_ASK_ITEMS} items; never both. An approval or request is \`headline\` and an optional \`question\` body only: its answers are fixed (${APPROVAL_OPTIONS.join(" / ")}; ${REQUEST_OPTIONS.join(" / ")}, output in the note or attachments). A request's body carries the steps and may hold a list and code blocks; every other body is a sentence or two of framing, inline Markdown only.
2610
+ - Decisions that arise together go in ONE call, in the order you want them answered. A second call is a separate exchange, never an edit.
2611
+ - 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. A quotation stays accurate: say who said it.
2612
+ - It must stand alone, since it can arrive first: the reply carries the findings, the exchange the question; don't repeat the reply in it.
2613
+ - Recorded when you call this; delivered when your turn ends, or after 3 minutes if it is still running. Open it as soon as you know. The outcome reaches you on a later turn.
2614
+ - The person sends one response; unanswered items end as Unanswered. A person can dismiss an exchange; you aren't told and nothing wakes you. Check its status before raising it again.
2615
+ - To replace a stale exchange (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_exchange\`.`,
2610
2616
  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.*\`).`,
2611
2617
  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.`,
2612
- 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.`,
2613
- 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
+ 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 open an exchange. 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 exchange instead.`,
2619
+ withdraw_exchange: `Withdraw an open exchange \u2014 yours or anyone's in this conversation \u2014 because it is moot, was wrong, or has been overtaken. \`reason\` is required and is shown to the person and, if it wasn't yours, to the agent who opened it. If there is a current one to put in its place, don't call this \u2014 call \`open_exchange\` 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.`
2614
2620
  };
2615
2621
  TURN_CONTROL_FIELD_DESCRIPTIONS = {
2616
2622
  answer: {},
2617
- ask: {
2618
- targetUserId: "The person to ask: their `@handle` from your turn context's roster (preferred), or their user id.",
2619
- 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\`.`,
2620
- 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.`,
2621
- 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.`,
2622
- 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.`,
2623
+ open_exchange: {
2624
+ targetUserId: "The person: their `@handle` from your turn context's roster (preferred), or their user id.",
2625
+ kind: `What the exchange is for; default \`question\`. \`question\`: the person picks among courses of action or types an answer. \`approval\`: a yes/no on one proposed action \u2014 the person answers ${APPROVAL_OPTIONS.join(" or ")}. \`request\`: something only the person can do \u2014 they report ${REQUEST_OPTIONS.join(" or ")}.`,
2626
+ question: `The body beneath the headline \u2014 for a question or approval one or two sentences of framing, NOT a report (up to ${MAX_ASK_QUESTION_BYTES} characters, inline markdown only); for a request the steps, with lists and code blocks allowed (up to ${MAX_REQUEST_BODY_BYTES} characters). With \`questions\`, leave it out.`,
2627
+ headline: `ONE clear, capitalized sentence, up to ${MAX_ASK_HEADLINE_BYTES} characters: for a question or approval, the question ending in \`?\` ("Should Ledger deploy once Mia approves?"); for a request, what to do ("Run these and send me the output"). What the person reads first in For you. Required for an approval or request.`,
2628
+ files: `1\u2013${MAX_EXCHANGE_FILES} workspace paths, drawn as file chips above the body; the person must be able to read each.`,
2629
+ options: `A question's single form only: ${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. Refused on an approval or request.`,
2630
+ questions: `A question only, MULTI form: 1\u2013${MAX_ASK_ITEMS} items at once, when a plan ends with several bounded decisions. Provide EITHER this or \`question\`/\`headline\`/\`options\`, not both.`,
2623
2631
  "questions.headline": `The one-sentence question ("Should Ledger deploy once Mia approves?") \u2014 required for each item, up to ${MAX_ASK_HEADLINE_BYTES} characters.`,
2624
2632
  "questions.body": `Optional short framing beneath the headline, up to ${MAX_ASK_QUESTION_BYTES} characters, inline markdown only. NOT a report.`,
2625
2633
  "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.`,
2626
- 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.",
2627
- "replaces.askId": "The id of the open ask to withdraw.",
2628
- "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.`
2634
+ replaces: "Withdraw a stale exchange in this conversation as part of opening this one. Both land in a single write, so the person goes from the old one to the new one and never sees neither or both.",
2635
+ "replaces.askId": "The id of the open exchange to withdraw.",
2636
+ "replaces.reason": `Why the old one no longer stands \u2014 shown to the person on their Done row. Up to ${MAX_WITHDRAW_REASON_BYTES} characters.`
2629
2637
  },
2630
2638
  cancel_wake: {},
2631
2639
  skip_turn: {
@@ -2636,9 +2644,9 @@ var init_descriptions = __esm({
2636
2644
  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.",
2637
2645
  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.`
2638
2646
  },
2639
- withdraw_ask: {
2640
- 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.",
2641
- 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.`
2647
+ withdraw_exchange: {
2648
+ askId: "The open exchange to withdraw. Its id rides the conversation's message rows (`ask: { id, status, items }`), and your turn context lists the ones open here.",
2649
+ reason: `Why it no longer stands ("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.`
2642
2650
  }
2643
2651
  };
2644
2652
  }
@@ -2820,11 +2828,11 @@ var init_prompt_guidance = __esm({
2820
2828
  init_descriptions();
2821
2829
  MEANINGS = {
2822
2830
  answer: "declare the text you write next as your answer",
2823
- ask: "put a structured question to a person",
2831
+ open_exchange: "put a question, go-ahead or request to a person",
2824
2832
  cancel_wake: "cancel your own armed wake in this conversation",
2825
2833
  skip_turn: "end without posting a reply",
2826
2834
  wake_me: "schedule your own return; when it fires, say what you came back to check",
2827
- withdraw_ask: "withdraw an open ask in this conversation with a reason"
2835
+ withdraw_exchange: "withdraw an open exchange in this conversation with a reason"
2828
2836
  };
2829
2837
  }
2830
2838
  });
@@ -6224,37 +6232,57 @@ function createTurnControlMcpServer(acts, skipState) {
6224
6232
  )
6225
6233
  ] : [],
6226
6234
  tool(
6227
- ASK_TOOL,
6228
- TURN_CONTROL_TOOL_DESCRIPTIONS.ask,
6235
+ OPEN_EXCHANGE_TOOL,
6236
+ TURN_CONTROL_TOOL_DESCRIPTIONS.open_exchange,
6229
6237
  {
6230
- targetUserId: z15.string().regex(ASK_TARGET_PATTERN, { error: ASK_TARGET_INVALID }).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
6231
- question: z15.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.question),
6232
- headline: z15.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.headline),
6233
- options: z15.array(z15.string({ error: ASK_OPTION_NOT_A_STRING }).min(1).max(MAX_ASK_OPTION_BYTES)).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.options),
6238
+ targetUserId: z15.string().regex(ASK_TARGET_PATTERN, { error: ASK_TARGET_INVALID }).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange.targetUserId),
6239
+ kind: z15.enum(EXCHANGE_KINDS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange.kind),
6240
+ // The request's cap; the server holds every other kind to its own.
6241
+ question: z15.string().min(1).max(MAX_REQUEST_BODY_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange.question),
6242
+ headline: z15.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange.headline),
6243
+ options: z15.array(z15.string({ error: ASK_OPTION_NOT_A_STRING }).min(1).max(MAX_ASK_OPTION_BYTES)).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange.options),
6234
6244
  questions: z15.array(
6235
6245
  z15.object({
6236
- headline: z15.string().min(1).max(MAX_ASK_HEADLINE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.headline"]),
6237
- body: z15.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.body"]),
6246
+ headline: z15.string().min(1).max(MAX_ASK_HEADLINE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange["questions.headline"]),
6247
+ body: z15.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange["questions.body"]),
6238
6248
  options: z15.array(
6239
6249
  z15.string({ error: ASK_OPTION_NOT_A_STRING }).min(1).max(MAX_ASK_OPTION_BYTES)
6240
- ).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options"])
6250
+ ).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange["questions.options"])
6241
6251
  })
6242
- ).min(1).max(MAX_ASK_ITEMS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.questions),
6252
+ ).min(1).max(MAX_ASK_ITEMS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange.questions),
6253
+ files: z15.array(z15.string().min(1).max(1024)).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange.files),
6243
6254
  replaces: z15.object({
6244
- askId: z15.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.askId"]),
6245
- reason: z15.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.reason"])
6246
- }).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.replaces)
6255
+ askId: z15.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange["replaces.askId"]),
6256
+ reason: z15.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange["replaces.reason"])
6257
+ }).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange.replaces)
6247
6258
  },
6248
6259
  async (args) => {
6260
+ const kind = args.kind ?? "question";
6249
6261
  const hasSingle = args.question !== void 0;
6250
6262
  const hasArray = args.questions !== void 0 && args.questions.length > 0;
6263
+ if (kind !== "question") {
6264
+ return performAct(acts, {
6265
+ kind: "ask",
6266
+ callId: randomUUID3(),
6267
+ ...args.replaces ? { replaces: args.replaces } : {},
6268
+ ask: {
6269
+ targetUserId: args.targetUserId,
6270
+ kind,
6271
+ ...args.headline ? { headline: args.headline } : {},
6272
+ ...args.question ? { question: args.question } : {},
6273
+ ...args.options ? { options: args.options } : {},
6274
+ ...args.questions ? { questions: args.questions } : {},
6275
+ ...args.files ? { files: args.files } : {}
6276
+ }
6277
+ });
6278
+ }
6251
6279
  if (hasSingle && hasArray) {
6252
6280
  return toolError(
6253
6281
  "Provide either `question` (single) or `questions` (array), not both."
6254
6282
  );
6255
6283
  }
6256
- if (!hasSingle && !hasArray) {
6257
- return toolError("Provide `question` (single) or `questions` (array).");
6284
+ if (!hasSingle && !hasArray && args.headline === void 0) {
6285
+ return toolError("Provide `headline` (single) or `questions` (array).");
6258
6286
  }
6259
6287
  return performAct(acts, {
6260
6288
  kind: "ask",
@@ -6268,12 +6296,14 @@ function createTurnControlMcpServer(acts, skipState) {
6268
6296
  headline: q.headline,
6269
6297
  ...q.body ? { body: q.body } : {},
6270
6298
  ...q.options && q.options.length > 0 ? { options: q.options } : {}
6271
- }))
6299
+ })),
6300
+ ...args.files ? { files: args.files } : {}
6272
6301
  } : {
6273
6302
  targetUserId: args.targetUserId,
6274
- question: args.question,
6303
+ ...args.question ? { question: args.question } : {},
6275
6304
  ...args.headline ? { headline: args.headline } : {},
6276
- ...args.options && args.options.length > 0 ? { options: args.options } : {}
6305
+ ...args.options && args.options.length > 0 ? { options: args.options } : {},
6306
+ ...args.files ? { files: args.files } : {}
6277
6307
  }
6278
6308
  });
6279
6309
  },
@@ -6316,11 +6346,11 @@ function createTurnControlMcpServer(acts, skipState) {
6316
6346
  // reasoning). Not idempotent either: a second call with the same id is
6317
6347
  // refused with `ask_not_open`, which is information, not a no-op.
6318
6348
  tool(
6319
- WITHDRAW_ASK_TOOL,
6320
- TURN_CONTROL_TOOL_DESCRIPTIONS.withdraw_ask,
6349
+ WITHDRAW_EXCHANGE_TOOL,
6350
+ TURN_CONTROL_TOOL_DESCRIPTIONS.withdraw_exchange,
6321
6351
  {
6322
- askId: z15.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.askId),
6323
- reason: z15.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.reason)
6352
+ askId: z15.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_exchange.askId),
6353
+ reason: z15.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_exchange.reason)
6324
6354
  },
6325
6355
  async (args) => performAct(acts, {
6326
6356
  kind: "withdraw_ask",
@@ -6348,7 +6378,7 @@ function createTurnControlMcpServer(acts, skipState) {
6348
6378
  ]
6349
6379
  });
6350
6380
  }
6351
- 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;
6381
+ var COMPANION_LOCAL_MCP_SERVER, COMPANION_LOCAL_TOOL_GLOB, SKIP_TURN_TOOL, OPEN_EXCHANGE_TOOL, OPEN_EXCHANGE_TOOL_NAME, WITHDRAW_EXCHANGE_TOOL, WITHDRAW_EXCHANGE_TOOL_NAME, ANSWER_TOOL2, WAKE_ME_TOOL, WAKE_ME_TOOL_NAME, CANCEL_WAKE_TOOL, CANCEL_WAKE_TOOL_NAME, TURN_ACT_TIMEOUT_MS;
6352
6382
  var init_turn_control_tools = __esm({
6353
6383
  "src/turn-control-tools.ts"() {
6354
6384
  "use strict";
@@ -6359,10 +6389,10 @@ var init_turn_control_tools = __esm({
6359
6389
  COMPANION_LOCAL_MCP_SERVER = TURN_CONTROL_MCP_SERVER;
6360
6390
  COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
6361
6391
  SKIP_TURN_TOOL = "skip_turn";
6362
- ASK_TOOL = "ask";
6363
- ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${ASK_TOOL}`;
6364
- WITHDRAW_ASK_TOOL = "withdraw_ask";
6365
- WITHDRAW_ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WITHDRAW_ASK_TOOL}`;
6392
+ OPEN_EXCHANGE_TOOL = "open_exchange";
6393
+ OPEN_EXCHANGE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${OPEN_EXCHANGE_TOOL}`;
6394
+ WITHDRAW_EXCHANGE_TOOL = "withdraw_exchange";
6395
+ WITHDRAW_EXCHANGE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WITHDRAW_EXCHANGE_TOOL}`;
6366
6396
  ANSWER_TOOL2 = "answer";
6367
6397
  WAKE_ME_TOOL = "wake_me";
6368
6398
  WAKE_ME_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WAKE_ME_TOOL}`;
package/dist/runtime.js CHANGED
@@ -1932,7 +1932,7 @@ var CabaneApi = class {
1932
1932
  `/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}/turn-intent?${q}`
1933
1933
  );
1934
1934
  }
1935
- // Commit one outgoing act (ask, withdraw_ask, wake_me, cancel_wake) at
1935
+ // Commit one outgoing act (open_exchange, withdraw_exchange, wake_me, cancel_wake) at
1936
1936
  // invocation. The server records the effect and receipt in one transaction;
1937
1937
  // an ok:false response is a recorded refusal for the tool to report.
1938
1938
  // Retries retain the caller's callId and recover the same receipt, even
@@ -2751,7 +2751,7 @@ var turnRequestSchema = z8.object({
2751
2751
  bearer: z8.string(),
2752
2752
  activeConversationId: z8.string(),
2753
2753
  // Scoped turn-control MCP URL. Codex and opencode mount it as `cabane_companion` with the bearer
2754
- // and active-conversation header, for ask, withdraw_ask, wake_me, cancel_wake and skip_turn.
2754
+ // and active-conversation header, for the turn-control tools (`TURN_CONTROL_TOOLS`).
2755
2755
  // Claude Code mounts the companion's in-process instance instead. Optional; the companion
2756
2756
  // populates it.
2757
2757
  turnControlUrl: z8.string().optional(),
@@ -2937,6 +2937,10 @@ function truncate(s, max) {
2937
2937
  var MAX_ASK_QUESTION_BYTES = 400;
2938
2938
  var MAX_ASK_HEADLINE_BYTES = 120;
2939
2939
  var MAX_ASK_ITEMS = 20;
2940
+ var APPROVAL_OPTIONS = ["Yes", "No"];
2941
+ var REQUEST_OPTIONS = ["Done", "Couldn't"];
2942
+ var MAX_EXCHANGE_FILES = 5;
2943
+ var MAX_REQUEST_BODY_BYTES = 2e3;
2940
2944
  var MAX_ASK_OPTION_BYTES = 200;
2941
2945
  var MIN_ASK_OPTIONS = 2;
2942
2946
  var MAX_ASK_OPTIONS = 4;
@@ -2946,41 +2950,44 @@ var MAX_SKIP_REASON_BYTES = 500;
2946
2950
  var TURN_CONTROL_MCP_SERVER = "cabane_companion";
2947
2951
  var TURN_CONTROL_TOOLS = [
2948
2952
  "answer",
2949
- "ask",
2950
2953
  "cancel_wake",
2954
+ "open_exchange",
2951
2955
  "skip_turn",
2952
2956
  "wake_me",
2953
- "withdraw_ask"
2957
+ "withdraw_exchange"
2954
2958
  ];
2955
2959
  var TURN_CONTROL_TOOL_DESCRIPTIONS = {
2956
2960
  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.`,
2957
- 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.
2958
- - 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.
2959
- - \`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.
2960
- - 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.
2961
- - 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.
2962
- - 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.
2963
- - 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.
2964
- - 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\`.`,
2961
+ open_exchange: `Put something in front of one person that comes back with a recorded outcome, then end your turn; don't wait for it. \`targetUserId\` is a person on your roster, by their \`@handle\`; to bring in an agent, name them with \`@handle\` in your reply.
2962
+ - \`kind\`: \`question\` for a choice among courses of action (two included) or free text; \`approval\` for a yes/no on one action you propose; \`request\` for something only the person can do, reported back.
2963
+ - A question is one item (\`headline\`, optional \`question\` body, \`options\`) or \`questions\`, 1\u2013${MAX_ASK_ITEMS} items; never both. An approval or request is \`headline\` and an optional \`question\` body only: its answers are fixed (${APPROVAL_OPTIONS.join(" / ")}; ${REQUEST_OPTIONS.join(" / ")}, output in the note or attachments). A request's body carries the steps and may hold a list and code blocks; every other body is a sentence or two of framing, inline Markdown only.
2964
+ - Decisions that arise together go in ONE call, in the order you want them answered. A second call is a separate exchange, never an edit.
2965
+ - 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. A quotation stays accurate: say who said it.
2966
+ - It must stand alone, since it can arrive first: the reply carries the findings, the exchange the question; don't repeat the reply in it.
2967
+ - Recorded when you call this; delivered when your turn ends, or after 3 minutes if it is still running. Open it as soon as you know. The outcome reaches you on a later turn.
2968
+ - The person sends one response; unanswered items end as Unanswered. A person can dismiss an exchange; you aren't told and nothing wakes you. Check its status before raising it again.
2969
+ - To replace a stale exchange (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_exchange\`.`,
2965
2970
  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.*\`).`,
2966
2971
  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.`,
2967
- 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.`,
2968
- 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.`
2972
+ 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 open an exchange. 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 exchange instead.`,
2973
+ withdraw_exchange: `Withdraw an open exchange \u2014 yours or anyone's in this conversation \u2014 because it is moot, was wrong, or has been overtaken. \`reason\` is required and is shown to the person and, if it wasn't yours, to the agent who opened it. If there is a current one to put in its place, don't call this \u2014 call \`open_exchange\` 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.`
2969
2974
  };
2970
2975
  var TURN_CONTROL_FIELD_DESCRIPTIONS = {
2971
2976
  answer: {},
2972
- ask: {
2973
- targetUserId: "The person to ask: their `@handle` from your turn context's roster (preferred), or their user id.",
2974
- 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\`.`,
2975
- 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.`,
2976
- 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.`,
2977
- 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.`,
2977
+ open_exchange: {
2978
+ targetUserId: "The person: their `@handle` from your turn context's roster (preferred), or their user id.",
2979
+ kind: `What the exchange is for; default \`question\`. \`question\`: the person picks among courses of action or types an answer. \`approval\`: a yes/no on one proposed action \u2014 the person answers ${APPROVAL_OPTIONS.join(" or ")}. \`request\`: something only the person can do \u2014 they report ${REQUEST_OPTIONS.join(" or ")}.`,
2980
+ question: `The body beneath the headline \u2014 for a question or approval one or two sentences of framing, NOT a report (up to ${MAX_ASK_QUESTION_BYTES} characters, inline markdown only); for a request the steps, with lists and code blocks allowed (up to ${MAX_REQUEST_BODY_BYTES} characters). With \`questions\`, leave it out.`,
2981
+ headline: `ONE clear, capitalized sentence, up to ${MAX_ASK_HEADLINE_BYTES} characters: for a question or approval, the question ending in \`?\` ("Should Ledger deploy once Mia approves?"); for a request, what to do ("Run these and send me the output"). What the person reads first in For you. Required for an approval or request.`,
2982
+ files: `1\u2013${MAX_EXCHANGE_FILES} workspace paths, drawn as file chips above the body; the person must be able to read each.`,
2983
+ options: `A question's single form only: ${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. Refused on an approval or request.`,
2984
+ questions: `A question only, MULTI form: 1\u2013${MAX_ASK_ITEMS} items at once, when a plan ends with several bounded decisions. Provide EITHER this or \`question\`/\`headline\`/\`options\`, not both.`,
2978
2985
  "questions.headline": `The one-sentence question ("Should Ledger deploy once Mia approves?") \u2014 required for each item, up to ${MAX_ASK_HEADLINE_BYTES} characters.`,
2979
2986
  "questions.body": `Optional short framing beneath the headline, up to ${MAX_ASK_QUESTION_BYTES} characters, inline markdown only. NOT a report.`,
2980
2987
  "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.`,
2981
- 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.",
2982
- "replaces.askId": "The id of the open ask to withdraw.",
2983
- "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.`
2988
+ replaces: "Withdraw a stale exchange in this conversation as part of opening this one. Both land in a single write, so the person goes from the old one to the new one and never sees neither or both.",
2989
+ "replaces.askId": "The id of the open exchange to withdraw.",
2990
+ "replaces.reason": `Why the old one no longer stands \u2014 shown to the person on their Done row. Up to ${MAX_WITHDRAW_REASON_BYTES} characters.`
2984
2991
  },
2985
2992
  cancel_wake: {},
2986
2993
  skip_turn: {
@@ -2991,9 +2998,9 @@ var TURN_CONTROL_FIELD_DESCRIPTIONS = {
2991
2998
  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.",
2992
2999
  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.`
2993
3000
  },
2994
- withdraw_ask: {
2995
- 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.",
2996
- 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.`
3001
+ withdraw_exchange: {
3002
+ askId: "The open exchange to withdraw. Its id rides the conversation's message rows (`ask: { id, status, items }`), and your turn context lists the ones open here.",
3003
+ reason: `Why it no longer stands ("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.`
2997
3004
  }
2998
3005
  };
2999
3006
 
@@ -3005,11 +3012,11 @@ import {
3005
3012
  // packages/agent-runtime/src/turn-control/prompt-guidance.ts
3006
3013
  var MEANINGS = {
3007
3014
  answer: "declare the text you write next as your answer",
3008
- ask: "put a structured question to a person",
3015
+ open_exchange: "put a question, go-ahead or request to a person",
3009
3016
  cancel_wake: "cancel your own armed wake in this conversation",
3010
3017
  skip_turn: "end without posting a reply",
3011
3018
  wake_me: "schedule your own return; when it fires, say what you came back to check",
3012
- withdraw_ask: "withdraw an open ask in this conversation with a reason"
3019
+ withdraw_exchange: "withdraw an open exchange in this conversation with a reason"
3013
3020
  };
3014
3021
  function turnControlPromptGuidance(prefix) {
3015
3022
  const controls = TURN_CONTROL_TOOLS.map((name) => `\`${prefix}${name}\` (${MEANINGS[name]})`);
@@ -6116,10 +6123,10 @@ import { randomUUID as randomUUID3 } from "crypto";
6116
6123
  import { z as z15 } from "zod";
6117
6124
  var COMPANION_LOCAL_MCP_SERVER = TURN_CONTROL_MCP_SERVER;
6118
6125
  var COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
6119
- var ASK_TOOL = "ask";
6120
- var ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${ASK_TOOL}`;
6121
- var WITHDRAW_ASK_TOOL = "withdraw_ask";
6122
- var WITHDRAW_ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WITHDRAW_ASK_TOOL}`;
6126
+ var OPEN_EXCHANGE_TOOL = "open_exchange";
6127
+ var OPEN_EXCHANGE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${OPEN_EXCHANGE_TOOL}`;
6128
+ var WITHDRAW_EXCHANGE_TOOL = "withdraw_exchange";
6129
+ var WITHDRAW_EXCHANGE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WITHDRAW_EXCHANGE_TOOL}`;
6123
6130
  var WAKE_ME_TOOL = "wake_me";
6124
6131
  var WAKE_ME_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WAKE_ME_TOOL}`;
6125
6132
  var CANCEL_WAKE_TOOL = "cancel_wake";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.218",
3
+ "version": "0.6.220",
4
4
  "type": "module",
5
5
  "description": "The Cabane Companion (headless): connect an agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.",
6
6
  "license": "UNLICENSED",