@cabane/companion 0.6.201 → 0.6.203

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +442 -118
  2. package/dist/runtime.js +273 -30
  3. package/package.json +1 -1
package/dist/runtime.js CHANGED
@@ -1802,6 +1802,11 @@ function isTurnEndedRefusal(err) {
1802
1802
  const body = err.body;
1803
1803
  return typeof body === "object" && body !== null && body.reason === "turn_ended";
1804
1804
  }
1805
+ function turnEndedOutcome(err) {
1806
+ if (!isTurnEndedRefusal(err)) return void 0;
1807
+ const outcome = err.body.outcome;
1808
+ return typeof outcome === "string" ? outcome : void 0;
1809
+ }
1805
1810
  var ERROR_BODY_LOG_CAP = 2e3;
1806
1811
  function describeErrorBody(body) {
1807
1812
  if (body === void 0 || body === null) return void 0;
@@ -1945,7 +1950,7 @@ var CabaneApi = class {
1945
1950
  // text use (turnId, seq) within each kind for nonzero seq; tool activity
1946
1951
  // uses (turnId, toolUseId). Legacy zero/missing seq bypasses row deduplication.
1947
1952
  // Server fences and the outbox's discard/eviction bounds still apply.
1948
- async durableCommit(kind, path, body, turnId, seq, signal) {
1953
+ async durableCommit(kind, path, body, turnId, seq, signal, trailPath) {
1949
1954
  try {
1950
1955
  await this.request("POST", path, body, {
1951
1956
  retry: true,
@@ -1956,7 +1961,16 @@ var CabaneApi = class {
1956
1961
  if (!outbox) throw err;
1957
1962
  if (signal?.aborted || isAbortError(err)) throw err;
1958
1963
  if (!isRetryable(err)) throw err;
1959
- outbox.persist({ enqueuedAt: Date.now(), turnId, seq, method: "POST", path, body, kind });
1964
+ outbox.persist({
1965
+ enqueuedAt: Date.now(),
1966
+ turnId,
1967
+ seq,
1968
+ method: "POST",
1969
+ path,
1970
+ body,
1971
+ kind,
1972
+ ...trailPath ? { trailPath } : {}
1973
+ });
1960
1974
  this.opts.log?.debug(
1961
1975
  { kind, turnId, seq, err: err instanceof Error ? err.message : String(err) },
1962
1976
  "companion: commit queued to outbox after transient failure (will drain when the API returns)"
@@ -1981,6 +1995,29 @@ var CabaneApi = class {
1981
1995
  progressed = true;
1982
1996
  } catch (err) {
1983
1997
  if (err instanceof ApiError && err.status >= 400 && err.status < 500) {
1998
+ if (entry.kind === "message" && entry.trailPath && isAlreadyPublished(err)) {
1999
+ try {
2000
+ await this.request("POST", entry.trailPath, trailOf(entry.body), {
2001
+ retry: true
2002
+ });
2003
+ } catch (trailErr) {
2004
+ if (!(trailErr instanceof ApiError && trailErr.status >= 400 && trailErr.status < 500)) {
2005
+ break;
2006
+ }
2007
+ this.opts.log?.warn(
2008
+ {
2009
+ kind: entry.kind,
2010
+ turnId: entry.turnId,
2011
+ seq: entry.seq,
2012
+ ...apiErrorLogFields(trailErr)
2013
+ },
2014
+ `Cabane rejected a postscript; it could not be filed. ${mayNeedUpdate()}`
2015
+ );
2016
+ }
2017
+ outbox.remove(entry.turnId, entry.seq);
2018
+ progressed = true;
2019
+ continue;
2020
+ }
1984
2021
  if (entry.kind === "active-run" && isTurnEndedRefusal(err)) {
1985
2022
  this.opts.log?.debug(
1986
2023
  { kind: entry.kind, turnId: entry.turnId, ...apiErrorLogFields(err) },
@@ -2056,6 +2093,19 @@ var CabaneApi = class {
2056
2093
  (e) => e.turnId === turnId && e.kind === "message" && e.body?.kind !== "runtime_notice"
2057
2094
  );
2058
2095
  }
2096
+ // The `seq` of this turn's SELECTED ANSWER still waiting in the outbox — the
2097
+ // `declared` commit the server's `answerArmed` cannot see — or null when none
2098
+ // is queued. A resumed run that re-armed over it would let a newer text
2099
+ // consume the arm first; with the selection queued, the resumed run starts
2100
+ // declared, its texts are trail, and the drain delivers the selection.
2101
+ outboxDeclaredSeqForTurn(turnId) {
2102
+ const outbox = this.opts.outbox;
2103
+ if (!outbox) return null;
2104
+ const queued = outbox.list().find(
2105
+ (e) => e.turnId === turnId && e.kind === "message" && e.body?.declared === true
2106
+ );
2107
+ return queued ? queued.seq : null;
2108
+ }
2059
2109
  // ADOPT a turn context's issuance as the execution source of its work, once,
2060
2110
  // before preparation emits or queues anything, so every row can name a source
2061
2111
  // the server durably recorded and will never reap. Adoption is idempotent, so
@@ -2133,9 +2183,11 @@ var CabaneApi = class {
2133
2183
  signal: AbortSignal.timeout(this.opts.leaseCheckTimeoutMs ?? LEASE_CHECK_TIMEOUT_MS)
2134
2184
  }
2135
2185
  );
2136
- return res?.state === "running" || res?.state === "ended" ? res.state : "unknown";
2186
+ if (res?.state === "ended") return res.outcome === "stopped" ? "stopped" : "ended";
2187
+ return res?.state === "running" ? "running" : "unknown";
2137
2188
  } catch (err) {
2138
- return isTurnEndedRefusal(err) ? "ended" : "unknown";
2189
+ if (!isTurnEndedRefusal(err)) return "unknown";
2190
+ return turnEndedOutcome(err) === "stopped" ? "stopped" : "ended";
2139
2191
  }
2140
2192
  }
2141
2193
  // Flips `active_run_started_at` and optionally captures the SDK session
@@ -2227,15 +2279,36 @@ var CabaneApi = class {
2227
2279
  // seq also orders speech against the turn's tool/thinking/text trail.
2228
2280
  // Transport/5xx failures get bounded retries and optional outbox delivery;
2229
2281
  // terminal 4xx and cancellation stop delivery rather than proving no commit.
2230
- postTurnMessage(workspaceId, conversationId, body, signal) {
2231
- return this.durableCommit(
2232
- "message",
2233
- `/api/workspaces/${workspaceId}/conversations/${conversationId}/messages`,
2234
- body,
2235
- body.turnId,
2236
- body.seq ?? 0,
2237
- signal
2238
- );
2282
+ //
2283
+ // A turn's answer can be out before a later speech commit arrives — the
2284
+ // declaration published it, or another host's selection drained first — and
2285
+ // the server refuses the commit `409 already_published`. The text is a
2286
+ // postscript then: with `trail` (the committing agent, which names the text
2287
+ // route), it is filed as trail at the same seq, live here and from the outbox
2288
+ // on a drain. Without `trail` (a runtime notice) the refusal is terminal.
2289
+ async postTurnMessage(workspaceId, conversationId, body, signal, trail) {
2290
+ const trailPath = trail ? `/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${trail.agentId}/text` : void 0;
2291
+ try {
2292
+ await this.durableCommit(
2293
+ "message",
2294
+ `/api/workspaces/${workspaceId}/conversations/${conversationId}/messages`,
2295
+ body,
2296
+ body.turnId,
2297
+ body.seq ?? 0,
2298
+ signal,
2299
+ trailPath
2300
+ );
2301
+ } catch (err) {
2302
+ if (!trailPath || !isAlreadyPublished(err)) throw err;
2303
+ await this.durableCommit(
2304
+ "text",
2305
+ trailPath,
2306
+ trailOf(body),
2307
+ body.turnId,
2308
+ body.seq ?? 0,
2309
+ signal
2310
+ );
2311
+ }
2239
2312
  }
2240
2313
  // Report a tool transition (start/done/error). The server checks span
2241
2314
  // ownership, upserts by (turnId, toolUseId), and publishes agent_activity SSE.
@@ -2281,6 +2354,18 @@ var CabaneApi = class {
2281
2354
  );
2282
2355
  }
2283
2356
  };
2357
+ function isAlreadyPublished(err) {
2358
+ return err instanceof ApiError && err.status === 409 && apiErrorCode(err) === "already_published";
2359
+ }
2360
+ function trailOf(body) {
2361
+ const b = body;
2362
+ return {
2363
+ turnId: b.turnId,
2364
+ text: b.body,
2365
+ ...b.seq !== void 0 ? { seq: b.seq } : {},
2366
+ ...b.executionSourceId !== void 0 ? { executionSourceId: b.executionSourceId } : {}
2367
+ };
2368
+ }
2284
2369
  function isRetryable(err) {
2285
2370
  if (err instanceof ApiError) return err.status >= 500;
2286
2371
  if (isAbortError(err)) return false;
@@ -2771,6 +2856,8 @@ var turnOutcomes = ["success", "failure", "cancelled", "skipped"];
2771
2856
  var turnSessionModes = ["fresh", "resumed", "degraded"];
2772
2857
  var turnRuntimeResultKinds = ["success", "error", "no_terminal"];
2773
2858
  var turnFinalSources = [
2859
+ // The agent declared its answer with the `answer` turn act (CT1973).
2860
+ "declared",
2774
2861
  "runtime_text",
2775
2862
  "progress_promotion",
2776
2863
  "host_fallback",
@@ -3056,11 +3143,6 @@ function truncate(s, max) {
3056
3143
  return `${s.slice(0, max)}\u2026[+${s.length - max} chars]`;
3057
3144
  }
3058
3145
 
3059
- // packages/agent-runtime/src/claude-code/index.ts
3060
- import {
3061
- query as defaultQuery
3062
- } from "@anthropic-ai/claude-agent-sdk";
3063
-
3064
3146
  // packages/agent-runtime/src/turn-control/descriptions.ts
3065
3147
  var MAX_ASK_QUESTION_BYTES = 400;
3066
3148
  var MAX_ASK_HEADLINE_BYTES = 120;
@@ -3071,7 +3153,9 @@ var MAX_ASK_OPTIONS = 4;
3071
3153
  var MAX_WAKE_NOTE_BYTES = 2e3;
3072
3154
  var MAX_WITHDRAW_REASON_BYTES = 400;
3073
3155
  var MAX_SKIP_REASON_BYTES = 500;
3156
+ var TURN_CONTROL_MCP_SERVER = "cabane_companion";
3074
3157
  var TURN_CONTROL_TOOLS = [
3158
+ "answer",
3075
3159
  "ask",
3076
3160
  "cancel_wake",
3077
3161
  "skip_turn",
@@ -3079,6 +3163,7 @@ var TURN_CONTROL_TOOLS = [
3079
3163
  "withdraw_ask"
3080
3164
  ];
3081
3165
  var TURN_CONTROL_TOOL_DESCRIPTIONS = {
3166
+ 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.`,
3082
3167
  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.
3083
3168
  - 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.
3084
3169
  - \`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.
@@ -3093,6 +3178,7 @@ var TURN_CONTROL_TOOL_DESCRIPTIONS = {
3093
3178
  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.`
3094
3179
  };
3095
3180
  var TURN_CONTROL_FIELD_DESCRIPTIONS = {
3181
+ answer: {},
3096
3182
  ask: {
3097
3183
  targetUserId: "The person to ask: their `@handle` from your turn context's roster (preferred), or their user id.",
3098
3184
  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\`.`,
@@ -3121,8 +3207,14 @@ var TURN_CONTROL_FIELD_DESCRIPTIONS = {
3121
3207
  }
3122
3208
  };
3123
3209
 
3210
+ // packages/agent-runtime/src/claude-code/index.ts
3211
+ import {
3212
+ query as defaultQuery
3213
+ } from "@anthropic-ai/claude-agent-sdk";
3214
+
3124
3215
  // packages/agent-runtime/src/turn-control/prompt-guidance.ts
3125
3216
  var MEANINGS = {
3217
+ answer: "declare the text you write next as your answer",
3126
3218
  ask: "put a structured question to a person",
3127
3219
  cancel_wake: "cancel your own armed wake in this conversation",
3128
3220
  skip_turn: "end without posting a reply",
@@ -4520,6 +4612,15 @@ function deepestError(node) {
4520
4612
  }
4521
4613
 
4522
4614
  // packages/agent-runtime/src/opencode/loop.ts
4615
+ function opencodeMcpServerOf(tool2, servers) {
4616
+ let best = null;
4617
+ for (const server of servers ?? []) {
4618
+ if (tool2.length > server.length + 1 && tool2.startsWith(`${server}_`)) {
4619
+ if (best === null || server.length > best.length) best = server;
4620
+ }
4621
+ }
4622
+ return best;
4623
+ }
4523
4624
  async function* decodeOpencodeStream(events, ctx) {
4524
4625
  let held = null;
4525
4626
  const pending = /* @__PURE__ */ new Map();
@@ -4546,12 +4647,14 @@ async function* decodeOpencodeStream(events, ctx) {
4546
4647
  if (sealed) yield sealed;
4547
4648
  const key = part.callID ?? part.id;
4548
4649
  const input = part.state.input;
4650
+ const server = opencodeMcpServerOf(part.tool, ctx.mcpServers);
4651
+ const origin = server ? { mcpServer: server } : {};
4549
4652
  if (!startedTools.has(key)) {
4550
4653
  const name = prettyToolName(part.tool);
4551
4654
  const summary = summarizeToolInput(input);
4552
4655
  pending.set(key, { name, summary, input });
4553
4656
  startedTools.add(key);
4554
- yield { type: "tool", id: key, name, phase: "start", summary, input };
4657
+ yield { type: "tool", id: key, name, phase: "start", summary, input, ...origin };
4555
4658
  }
4556
4659
  const entry = pending.get(key);
4557
4660
  if (entry && hasToolInput(input)) {
@@ -4571,7 +4674,8 @@ async function* decodeOpencodeStream(events, ctx) {
4571
4674
  phase,
4572
4675
  summary: started?.summary ?? "",
4573
4676
  input: started?.input ?? input,
4574
- result
4677
+ result,
4678
+ ...origin
4575
4679
  };
4576
4680
  }
4577
4681
  } else {
@@ -4655,7 +4759,6 @@ function parseOpencodeModel(model) {
4655
4759
 
4656
4760
  // packages/agent-runtime/src/opencode/run-spec.ts
4657
4761
  var CABANE_MCP_SERVER2 = "cabane";
4658
- var TURN_CONTROL_MCP_SERVER = "cabane_companion";
4659
4762
  var ACTIVE_CONVERSATION_HEADER2 = "x-cabane-active-conversation";
4660
4763
  function buildRunSpec(req, resumeSessionId) {
4661
4764
  const { config } = req;
@@ -5049,7 +5152,10 @@ function createOpencodeAdapter(deps = {}) {
5049
5152
  }
5050
5153
  yield* decodeOpencodeStream(result.events, {
5051
5154
  signal,
5052
- sessionId: result.sessionId
5155
+ sessionId: result.sessionId,
5156
+ // The keys the turn's servers were installed under: what the decoder
5157
+ // splits a raw `<server>_<tool>` id on.
5158
+ mcpServers: Object.keys(spec.mcp)
5053
5159
  });
5054
5160
  }
5055
5161
  };
@@ -5185,7 +5291,15 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
5185
5291
  expected: [
5186
5292
  sessionEvent2(NEW_SESSION_ID),
5187
5293
  // start: fires off the first (empty) snapshot — empty input + summary, by design.
5188
- { type: "tool", id: "call_1", name: "cabane_list", phase: "start", summary: "", input: {} },
5294
+ {
5295
+ type: "tool",
5296
+ id: "call_1",
5297
+ name: "cabane_list",
5298
+ phase: "start",
5299
+ summary: "",
5300
+ input: {},
5301
+ mcpServer: "cabane"
5302
+ },
5189
5303
  // done: carries the full args captured from the later snapshot, and the
5190
5304
  // summary recomputed from them — NOT the first-seen `{}`.
5191
5305
  {
@@ -5195,8 +5309,60 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
5195
5309
  phase: "done",
5196
5310
  summary: "/product/tasks",
5197
5311
  input: { path: "/product/tasks" },
5198
- result: "CT301.md"
5312
+ result: "CT301.md",
5313
+ mcpServer: "cabane"
5314
+ },
5315
+ { type: "result", ok: true }
5316
+ ]
5317
+ },
5318
+ {
5319
+ // The turn-control server's `answer` (CT1973). opencode names an MCP tool `<server>_<tool>`,
5320
+ // so the installed keys split the raw id: `cabane_companion_answer` is the turn-control
5321
+ // server's, never the workspace server's `companion_answer`. The pump binds on that.
5322
+ name: "turn-control answer is read as the turn-control server\u2019s, not cabane\u2019s",
5323
+ request: makeRequest2({
5324
+ cabane: {
5325
+ mcpUrl: "https://cabane.test/api/mcp",
5326
+ turnControlUrl: "https://cabane.test/api/turn-control",
5327
+ bearer: "tok",
5328
+ activeConversationId: "conv"
5329
+ }
5330
+ }),
5331
+ nativeStream: [
5332
+ textPart("p1", "On it."),
5333
+ toolPart("t1", "cabane_companion_answer", { status: "running", input: {} }, "call_1"),
5334
+ toolPart(
5335
+ "t1",
5336
+ "cabane_companion_answer",
5337
+ { status: "completed", input: {}, output: '{"ok":true,"kind":"answer"}' },
5338
+ "call_1"
5339
+ ),
5340
+ textPart("p2", "The answer."),
5341
+ idle()
5342
+ ],
5343
+ expected: [
5344
+ sessionEvent2(NEW_SESSION_ID),
5345
+ { type: "text", body: "On it.", terminal: false },
5346
+ {
5347
+ type: "tool",
5348
+ id: "call_1",
5349
+ name: "cabane_companion_answer",
5350
+ phase: "start",
5351
+ summary: "",
5352
+ input: {},
5353
+ mcpServer: "cabane_companion"
5199
5354
  },
5355
+ {
5356
+ type: "tool",
5357
+ id: "call_1",
5358
+ name: "cabane_companion_answer",
5359
+ phase: "done",
5360
+ summary: "",
5361
+ input: {},
5362
+ result: '{"ok":true,"kind":"answer"}',
5363
+ mcpServer: "cabane_companion"
5364
+ },
5365
+ { type: "text", body: "The answer.", terminal: true },
5200
5366
  { type: "result", ok: true }
5201
5367
  ]
5202
5368
  },
@@ -5554,6 +5720,7 @@ function readToolItem(item, eventType) {
5554
5720
  name: server && server !== "cabane" ? `${server}.${tool2}` : tool2,
5555
5721
  input: item.arguments,
5556
5722
  status: toStatus(item.status),
5723
+ ...server ? { server } : {},
5557
5724
  ...item.status === "completed" ? { result } : item.status === "failed" ? { result: hasMcpReceipt(result) ? result : readMcpError(item.error) } : {}
5558
5725
  };
5559
5726
  }
@@ -5782,11 +5949,20 @@ async function* decodeCodexStream(events, ctx) {
5782
5949
  if (!tool2) continue;
5783
5950
  yield* flushInterim();
5784
5951
  const name = prettyToolName(tool2.name);
5952
+ const origin = tool2.server ? { mcpServer: tool2.server } : {};
5785
5953
  if (!startedTools.has(tool2.id)) {
5786
5954
  const summary = summarizeToolInput(tool2.input);
5787
5955
  pending.set(tool2.id, { name, summary, input: tool2.input });
5788
5956
  startedTools.add(tool2.id);
5789
- yield { type: "tool", id: tool2.id, name, phase: "start", summary, input: tool2.input };
5957
+ yield {
5958
+ type: "tool",
5959
+ id: tool2.id,
5960
+ name,
5961
+ phase: "start",
5962
+ summary,
5963
+ input: tool2.input,
5964
+ ...origin
5965
+ };
5790
5966
  }
5791
5967
  const entry = pending.get(tool2.id);
5792
5968
  if (entry && hasToolInput2(tool2.input)) {
@@ -5804,7 +5980,8 @@ async function* decodeCodexStream(events, ctx) {
5804
5980
  phase: tool2.status === "completed" ? "done" : "error",
5805
5981
  summary: started?.summary ?? "",
5806
5982
  input: started?.input ?? tool2.input,
5807
- result: tool2.result
5983
+ result: tool2.result,
5984
+ ...origin
5808
5985
  };
5809
5986
  }
5810
5987
  continue;
@@ -5895,7 +6072,6 @@ function parseCodexModel(model) {
5895
6072
 
5896
6073
  // packages/agent-runtime/src/codex/run-spec.ts
5897
6074
  var CABANE_MCP_SERVER3 = "cabane";
5898
- var TURN_CONTROL_MCP_SERVER2 = "cabane_companion";
5899
6075
  var ACTIVE_CONVERSATION_HEADER3 = "x-cabane-active-conversation";
5900
6076
  var ENV_ENVELOPE_KEYS = [
5901
6077
  "CABANE_CONVERSATION_ID",
@@ -6024,7 +6200,7 @@ function buildConfig(req) {
6024
6200
  default_tools_approval_mode: "approve"
6025
6201
  };
6026
6202
  if (req.cabane.turnControlUrl) {
6027
- mcp_servers[TURN_CONTROL_MCP_SERVER2] = {
6203
+ mcp_servers[TURN_CONTROL_MCP_SERVER] = {
6028
6204
  url: req.cabane.turnControlUrl,
6029
6205
  http_headers: {
6030
6206
  Authorization: `Bearer ${req.cabane.bearer}`,
@@ -6474,6 +6650,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
6474
6650
  type: "tool",
6475
6651
  id: "t1",
6476
6652
  name: "read",
6653
+ mcpServer: "cabane",
6477
6654
  phase: "start",
6478
6655
  summary: "sleep.md",
6479
6656
  input: { path: "sleep.md" }
@@ -6482,6 +6659,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
6482
6659
  type: "tool",
6483
6660
  id: "t1",
6484
6661
  name: "read",
6662
+ mcpServer: "cabane",
6485
6663
  phase: "done",
6486
6664
  summary: "sleep.md",
6487
6665
  input: { path: "sleep.md" },
@@ -6527,6 +6705,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
6527
6705
  type: "tool",
6528
6706
  id: "query-success",
6529
6707
  name: "query",
6708
+ mcpServer: "cabane",
6530
6709
  phase: "start",
6531
6710
  summary: "",
6532
6711
  input: { op: "files.read", args: { path: "/ABOUT.md" } }
@@ -6535,6 +6714,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
6535
6714
  type: "tool",
6536
6715
  id: "query-success",
6537
6716
  name: "query",
6717
+ mcpServer: "cabane",
6538
6718
  phase: "done",
6539
6719
  summary: "",
6540
6720
  input: { op: "files.read", args: { path: "/ABOUT.md" } },
@@ -6581,6 +6761,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
6581
6761
  type: "tool",
6582
6762
  id: "query-failed",
6583
6763
  name: "query",
6764
+ mcpServer: "cabane",
6584
6765
  phase: "start",
6585
6766
  summary: "",
6586
6767
  input: { op: "files.read", args: { path: "/ABOUT.md" } }
@@ -6589,6 +6770,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
6589
6770
  type: "tool",
6590
6771
  id: "query-failed",
6591
6772
  name: "query",
6773
+ mcpServer: "cabane",
6592
6774
  phase: "error",
6593
6775
  summary: "",
6594
6776
  input: { op: "files.read", args: { path: "/ABOUT.md" } },
@@ -6598,6 +6780,54 @@ var CODEX_CONFORMANCE_FIXTURES = [
6598
6780
  { type: "result", ok: true }
6599
6781
  ]
6600
6782
  },
6783
+ {
6784
+ // The turn-control server's `answer` (CT1973): the tool event carries the server Codex names
6785
+ // beside the tool, which is what the pump binds the declaration to. A non-cabane server keeps
6786
+ // its prefix on the card as before.
6787
+ name: "turn-control answer carries its server",
6788
+ request: makeRequest3(),
6789
+ nativeStream: [
6790
+ threadStarted(NEW_THREAD_ID),
6791
+ agentMessage("m1", "On it."),
6792
+ toolFrame("item.completed", {
6793
+ id: "answer-1",
6794
+ type: "mcp_tool_call",
6795
+ server: "cabane_companion",
6796
+ tool: "answer",
6797
+ arguments: {},
6798
+ status: "completed",
6799
+ result: { content: [{ type: "text", text: '{"ok":true,"kind":"answer"}' }] }
6800
+ }),
6801
+ agentMessage("m2", "The answer."),
6802
+ turnCompleted()
6803
+ ],
6804
+ expected: [
6805
+ startedSession(NEW_THREAD_ID),
6806
+ { type: "text", body: "On it.", terminal: false },
6807
+ {
6808
+ type: "tool",
6809
+ id: "answer-1",
6810
+ name: "cabane_companion.answer",
6811
+ mcpServer: "cabane_companion",
6812
+ phase: "start",
6813
+ summary: "",
6814
+ input: {}
6815
+ },
6816
+ {
6817
+ type: "tool",
6818
+ id: "answer-1",
6819
+ name: "cabane_companion.answer",
6820
+ mcpServer: "cabane_companion",
6821
+ phase: "done",
6822
+ summary: "",
6823
+ input: {},
6824
+ result: [{ type: "text", text: '{"ok":true,"kind":"answer"}' }]
6825
+ },
6826
+ { type: "text", body: "The answer.", terminal: true },
6827
+ closingSession(NEW_THREAD_ID),
6828
+ { type: "result", ok: true }
6829
+ ]
6830
+ },
6601
6831
  {
6602
6832
  // A transport/tool failure with no MCP result payload still uses the real adapter error message,
6603
6833
  // never `undefined`.
@@ -6623,6 +6853,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
6623
6853
  type: "tool",
6624
6854
  id: "mcp-failed",
6625
6855
  name: "other.lookup",
6856
+ mcpServer: "other",
6626
6857
  phase: "start",
6627
6858
  summary: "receipt",
6628
6859
  input: { q: "receipt" }
@@ -6631,6 +6862,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
6631
6862
  type: "tool",
6632
6863
  id: "mcp-failed",
6633
6864
  name: "other.lookup",
6865
+ mcpServer: "other",
6634
6866
  phase: "error",
6635
6867
  summary: "receipt",
6636
6868
  input: { q: "receipt" },
@@ -6667,6 +6899,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
6667
6899
  type: "tool",
6668
6900
  id: "mcp-image",
6669
6901
  name: "other.render",
6902
+ mcpServer: "other",
6670
6903
  phase: "start",
6671
6904
  summary: "",
6672
6905
  input: {}
@@ -6675,6 +6908,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
6675
6908
  type: "tool",
6676
6909
  id: "mcp-image",
6677
6910
  name: "other.render",
6911
+ mcpServer: "other",
6678
6912
  phase: "done",
6679
6913
  summary: "",
6680
6914
  input: {},
@@ -6722,11 +6956,20 @@ var CODEX_CONFORMANCE_FIXTURES = [
6722
6956
  ],
6723
6957
  expected: [
6724
6958
  startedSession(NEW_THREAD_ID),
6725
- { type: "tool", id: "t1", name: "list", phase: "start", summary: "", input: {} },
6726
6959
  {
6727
6960
  type: "tool",
6728
6961
  id: "t1",
6729
6962
  name: "list",
6963
+ mcpServer: "cabane",
6964
+ phase: "start",
6965
+ summary: "",
6966
+ input: {}
6967
+ },
6968
+ {
6969
+ type: "tool",
6970
+ id: "t1",
6971
+ name: "list",
6972
+ mcpServer: "cabane",
6730
6973
  phase: "done",
6731
6974
  summary: "/product/tasks",
6732
6975
  input: { path: "/product/tasks" },
@@ -8005,7 +8248,7 @@ import { join as join14 } from "path";
8005
8248
  // src/turn-control-tools.ts
8006
8249
  import { randomUUID as randomUUID2 } from "crypto";
8007
8250
  import { z as z16 } from "zod";
8008
- var COMPANION_LOCAL_MCP_SERVER = "cabane_companion";
8251
+ var COMPANION_LOCAL_MCP_SERVER = TURN_CONTROL_MCP_SERVER;
8009
8252
  var COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
8010
8253
  var ASK_TOOL = "ask";
8011
8254
  var ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${ASK_TOOL}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.201",
3
+ "version": "0.6.203",
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",