@cabane/companion 0.6.202 → 0.6.203

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +429 -113
  2. package/dist/runtime.js +264 -28
  3. package/package.json +1 -1
package/dist/runtime.js CHANGED
@@ -1950,7 +1950,7 @@ var CabaneApi = class {
1950
1950
  // text use (turnId, seq) within each kind for nonzero seq; tool activity
1951
1951
  // uses (turnId, toolUseId). Legacy zero/missing seq bypasses row deduplication.
1952
1952
  // Server fences and the outbox's discard/eviction bounds still apply.
1953
- async durableCommit(kind, path, body, turnId, seq, signal) {
1953
+ async durableCommit(kind, path, body, turnId, seq, signal, trailPath) {
1954
1954
  try {
1955
1955
  await this.request("POST", path, body, {
1956
1956
  retry: true,
@@ -1961,7 +1961,16 @@ var CabaneApi = class {
1961
1961
  if (!outbox) throw err;
1962
1962
  if (signal?.aborted || isAbortError(err)) throw err;
1963
1963
  if (!isRetryable(err)) throw err;
1964
- 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
+ });
1965
1974
  this.opts.log?.debug(
1966
1975
  { kind, turnId, seq, err: err instanceof Error ? err.message : String(err) },
1967
1976
  "companion: commit queued to outbox after transient failure (will drain when the API returns)"
@@ -1986,6 +1995,29 @@ var CabaneApi = class {
1986
1995
  progressed = true;
1987
1996
  } catch (err) {
1988
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
+ }
1989
2021
  if (entry.kind === "active-run" && isTurnEndedRefusal(err)) {
1990
2022
  this.opts.log?.debug(
1991
2023
  { kind: entry.kind, turnId: entry.turnId, ...apiErrorLogFields(err) },
@@ -2061,6 +2093,19 @@ var CabaneApi = class {
2061
2093
  (e) => e.turnId === turnId && e.kind === "message" && e.body?.kind !== "runtime_notice"
2062
2094
  );
2063
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
+ }
2064
2109
  // ADOPT a turn context's issuance as the execution source of its work, once,
2065
2110
  // before preparation emits or queues anything, so every row can name a source
2066
2111
  // the server durably recorded and will never reap. Adoption is idempotent, so
@@ -2234,15 +2279,36 @@ var CabaneApi = class {
2234
2279
  // seq also orders speech against the turn's tool/thinking/text trail.
2235
2280
  // Transport/5xx failures get bounded retries and optional outbox delivery;
2236
2281
  // terminal 4xx and cancellation stop delivery rather than proving no commit.
2237
- postTurnMessage(workspaceId, conversationId, body, signal) {
2238
- return this.durableCommit(
2239
- "message",
2240
- `/api/workspaces/${workspaceId}/conversations/${conversationId}/messages`,
2241
- body,
2242
- body.turnId,
2243
- body.seq ?? 0,
2244
- signal
2245
- );
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
+ }
2246
2312
  }
2247
2313
  // Report a tool transition (start/done/error). The server checks span
2248
2314
  // ownership, upserts by (turnId, toolUseId), and publishes agent_activity SSE.
@@ -2288,6 +2354,18 @@ var CabaneApi = class {
2288
2354
  );
2289
2355
  }
2290
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
+ }
2291
2369
  function isRetryable(err) {
2292
2370
  if (err instanceof ApiError) return err.status >= 500;
2293
2371
  if (isAbortError(err)) return false;
@@ -2778,6 +2856,8 @@ var turnOutcomes = ["success", "failure", "cancelled", "skipped"];
2778
2856
  var turnSessionModes = ["fresh", "resumed", "degraded"];
2779
2857
  var turnRuntimeResultKinds = ["success", "error", "no_terminal"];
2780
2858
  var turnFinalSources = [
2859
+ // The agent declared its answer with the `answer` turn act (CT1973).
2860
+ "declared",
2781
2861
  "runtime_text",
2782
2862
  "progress_promotion",
2783
2863
  "host_fallback",
@@ -3063,11 +3143,6 @@ function truncate(s, max) {
3063
3143
  return `${s.slice(0, max)}\u2026[+${s.length - max} chars]`;
3064
3144
  }
3065
3145
 
3066
- // packages/agent-runtime/src/claude-code/index.ts
3067
- import {
3068
- query as defaultQuery
3069
- } from "@anthropic-ai/claude-agent-sdk";
3070
-
3071
3146
  // packages/agent-runtime/src/turn-control/descriptions.ts
3072
3147
  var MAX_ASK_QUESTION_BYTES = 400;
3073
3148
  var MAX_ASK_HEADLINE_BYTES = 120;
@@ -3078,7 +3153,9 @@ var MAX_ASK_OPTIONS = 4;
3078
3153
  var MAX_WAKE_NOTE_BYTES = 2e3;
3079
3154
  var MAX_WITHDRAW_REASON_BYTES = 400;
3080
3155
  var MAX_SKIP_REASON_BYTES = 500;
3156
+ var TURN_CONTROL_MCP_SERVER = "cabane_companion";
3081
3157
  var TURN_CONTROL_TOOLS = [
3158
+ "answer",
3082
3159
  "ask",
3083
3160
  "cancel_wake",
3084
3161
  "skip_turn",
@@ -3086,6 +3163,7 @@ var TURN_CONTROL_TOOLS = [
3086
3163
  "withdraw_ask"
3087
3164
  ];
3088
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.`,
3089
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.
3090
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.
3091
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.
@@ -3100,6 +3178,7 @@ var TURN_CONTROL_TOOL_DESCRIPTIONS = {
3100
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.`
3101
3179
  };
3102
3180
  var TURN_CONTROL_FIELD_DESCRIPTIONS = {
3181
+ answer: {},
3103
3182
  ask: {
3104
3183
  targetUserId: "The person to ask: their `@handle` from your turn context's roster (preferred), or their user id.",
3105
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\`.`,
@@ -3128,8 +3207,14 @@ var TURN_CONTROL_FIELD_DESCRIPTIONS = {
3128
3207
  }
3129
3208
  };
3130
3209
 
3210
+ // packages/agent-runtime/src/claude-code/index.ts
3211
+ import {
3212
+ query as defaultQuery
3213
+ } from "@anthropic-ai/claude-agent-sdk";
3214
+
3131
3215
  // packages/agent-runtime/src/turn-control/prompt-guidance.ts
3132
3216
  var MEANINGS = {
3217
+ answer: "declare the text you write next as your answer",
3133
3218
  ask: "put a structured question to a person",
3134
3219
  cancel_wake: "cancel your own armed wake in this conversation",
3135
3220
  skip_turn: "end without posting a reply",
@@ -4527,6 +4612,15 @@ function deepestError(node) {
4527
4612
  }
4528
4613
 
4529
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
+ }
4530
4624
  async function* decodeOpencodeStream(events, ctx) {
4531
4625
  let held = null;
4532
4626
  const pending = /* @__PURE__ */ new Map();
@@ -4553,12 +4647,14 @@ async function* decodeOpencodeStream(events, ctx) {
4553
4647
  if (sealed) yield sealed;
4554
4648
  const key = part.callID ?? part.id;
4555
4649
  const input = part.state.input;
4650
+ const server = opencodeMcpServerOf(part.tool, ctx.mcpServers);
4651
+ const origin = server ? { mcpServer: server } : {};
4556
4652
  if (!startedTools.has(key)) {
4557
4653
  const name = prettyToolName(part.tool);
4558
4654
  const summary = summarizeToolInput(input);
4559
4655
  pending.set(key, { name, summary, input });
4560
4656
  startedTools.add(key);
4561
- yield { type: "tool", id: key, name, phase: "start", summary, input };
4657
+ yield { type: "tool", id: key, name, phase: "start", summary, input, ...origin };
4562
4658
  }
4563
4659
  const entry = pending.get(key);
4564
4660
  if (entry && hasToolInput(input)) {
@@ -4578,7 +4674,8 @@ async function* decodeOpencodeStream(events, ctx) {
4578
4674
  phase,
4579
4675
  summary: started?.summary ?? "",
4580
4676
  input: started?.input ?? input,
4581
- result
4677
+ result,
4678
+ ...origin
4582
4679
  };
4583
4680
  }
4584
4681
  } else {
@@ -4662,7 +4759,6 @@ function parseOpencodeModel(model) {
4662
4759
 
4663
4760
  // packages/agent-runtime/src/opencode/run-spec.ts
4664
4761
  var CABANE_MCP_SERVER2 = "cabane";
4665
- var TURN_CONTROL_MCP_SERVER = "cabane_companion";
4666
4762
  var ACTIVE_CONVERSATION_HEADER2 = "x-cabane-active-conversation";
4667
4763
  function buildRunSpec(req, resumeSessionId) {
4668
4764
  const { config } = req;
@@ -5056,7 +5152,10 @@ function createOpencodeAdapter(deps = {}) {
5056
5152
  }
5057
5153
  yield* decodeOpencodeStream(result.events, {
5058
5154
  signal,
5059
- 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)
5060
5159
  });
5061
5160
  }
5062
5161
  };
@@ -5192,7 +5291,15 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
5192
5291
  expected: [
5193
5292
  sessionEvent2(NEW_SESSION_ID),
5194
5293
  // start: fires off the first (empty) snapshot — empty input + summary, by design.
5195
- { 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
+ },
5196
5303
  // done: carries the full args captured from the later snapshot, and the
5197
5304
  // summary recomputed from them — NOT the first-seen `{}`.
5198
5305
  {
@@ -5202,11 +5309,63 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
5202
5309
  phase: "done",
5203
5310
  summary: "/product/tasks",
5204
5311
  input: { path: "/product/tasks" },
5205
- result: "CT301.md"
5312
+ result: "CT301.md",
5313
+ mcpServer: "cabane"
5206
5314
  },
5207
5315
  { type: "result", ok: true }
5208
5316
  ]
5209
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"
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 },
5366
+ { type: "result", ok: true }
5367
+ ]
5368
+ },
5210
5369
  {
5211
5370
  // Highest risk: cancel mid-stream. The abort lands after the tool result; the adapter stops before
5212
5371
  // the closing reply — no final text, no `result` event (the server writes the stopped marker).
@@ -5561,6 +5720,7 @@ function readToolItem(item, eventType) {
5561
5720
  name: server && server !== "cabane" ? `${server}.${tool2}` : tool2,
5562
5721
  input: item.arguments,
5563
5722
  status: toStatus(item.status),
5723
+ ...server ? { server } : {},
5564
5724
  ...item.status === "completed" ? { result } : item.status === "failed" ? { result: hasMcpReceipt(result) ? result : readMcpError(item.error) } : {}
5565
5725
  };
5566
5726
  }
@@ -5789,11 +5949,20 @@ async function* decodeCodexStream(events, ctx) {
5789
5949
  if (!tool2) continue;
5790
5950
  yield* flushInterim();
5791
5951
  const name = prettyToolName(tool2.name);
5952
+ const origin = tool2.server ? { mcpServer: tool2.server } : {};
5792
5953
  if (!startedTools.has(tool2.id)) {
5793
5954
  const summary = summarizeToolInput(tool2.input);
5794
5955
  pending.set(tool2.id, { name, summary, input: tool2.input });
5795
5956
  startedTools.add(tool2.id);
5796
- 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
+ };
5797
5966
  }
5798
5967
  const entry = pending.get(tool2.id);
5799
5968
  if (entry && hasToolInput2(tool2.input)) {
@@ -5811,7 +5980,8 @@ async function* decodeCodexStream(events, ctx) {
5811
5980
  phase: tool2.status === "completed" ? "done" : "error",
5812
5981
  summary: started?.summary ?? "",
5813
5982
  input: started?.input ?? tool2.input,
5814
- result: tool2.result
5983
+ result: tool2.result,
5984
+ ...origin
5815
5985
  };
5816
5986
  }
5817
5987
  continue;
@@ -5902,7 +6072,6 @@ function parseCodexModel(model) {
5902
6072
 
5903
6073
  // packages/agent-runtime/src/codex/run-spec.ts
5904
6074
  var CABANE_MCP_SERVER3 = "cabane";
5905
- var TURN_CONTROL_MCP_SERVER2 = "cabane_companion";
5906
6075
  var ACTIVE_CONVERSATION_HEADER3 = "x-cabane-active-conversation";
5907
6076
  var ENV_ENVELOPE_KEYS = [
5908
6077
  "CABANE_CONVERSATION_ID",
@@ -6031,7 +6200,7 @@ function buildConfig(req) {
6031
6200
  default_tools_approval_mode: "approve"
6032
6201
  };
6033
6202
  if (req.cabane.turnControlUrl) {
6034
- mcp_servers[TURN_CONTROL_MCP_SERVER2] = {
6203
+ mcp_servers[TURN_CONTROL_MCP_SERVER] = {
6035
6204
  url: req.cabane.turnControlUrl,
6036
6205
  http_headers: {
6037
6206
  Authorization: `Bearer ${req.cabane.bearer}`,
@@ -6481,6 +6650,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
6481
6650
  type: "tool",
6482
6651
  id: "t1",
6483
6652
  name: "read",
6653
+ mcpServer: "cabane",
6484
6654
  phase: "start",
6485
6655
  summary: "sleep.md",
6486
6656
  input: { path: "sleep.md" }
@@ -6489,6 +6659,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
6489
6659
  type: "tool",
6490
6660
  id: "t1",
6491
6661
  name: "read",
6662
+ mcpServer: "cabane",
6492
6663
  phase: "done",
6493
6664
  summary: "sleep.md",
6494
6665
  input: { path: "sleep.md" },
@@ -6534,6 +6705,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
6534
6705
  type: "tool",
6535
6706
  id: "query-success",
6536
6707
  name: "query",
6708
+ mcpServer: "cabane",
6537
6709
  phase: "start",
6538
6710
  summary: "",
6539
6711
  input: { op: "files.read", args: { path: "/ABOUT.md" } }
@@ -6542,6 +6714,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
6542
6714
  type: "tool",
6543
6715
  id: "query-success",
6544
6716
  name: "query",
6717
+ mcpServer: "cabane",
6545
6718
  phase: "done",
6546
6719
  summary: "",
6547
6720
  input: { op: "files.read", args: { path: "/ABOUT.md" } },
@@ -6588,6 +6761,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
6588
6761
  type: "tool",
6589
6762
  id: "query-failed",
6590
6763
  name: "query",
6764
+ mcpServer: "cabane",
6591
6765
  phase: "start",
6592
6766
  summary: "",
6593
6767
  input: { op: "files.read", args: { path: "/ABOUT.md" } }
@@ -6596,6 +6770,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
6596
6770
  type: "tool",
6597
6771
  id: "query-failed",
6598
6772
  name: "query",
6773
+ mcpServer: "cabane",
6599
6774
  phase: "error",
6600
6775
  summary: "",
6601
6776
  input: { op: "files.read", args: { path: "/ABOUT.md" } },
@@ -6605,6 +6780,54 @@ var CODEX_CONFORMANCE_FIXTURES = [
6605
6780
  { type: "result", ok: true }
6606
6781
  ]
6607
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
+ },
6608
6831
  {
6609
6832
  // A transport/tool failure with no MCP result payload still uses the real adapter error message,
6610
6833
  // never `undefined`.
@@ -6630,6 +6853,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
6630
6853
  type: "tool",
6631
6854
  id: "mcp-failed",
6632
6855
  name: "other.lookup",
6856
+ mcpServer: "other",
6633
6857
  phase: "start",
6634
6858
  summary: "receipt",
6635
6859
  input: { q: "receipt" }
@@ -6638,6 +6862,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
6638
6862
  type: "tool",
6639
6863
  id: "mcp-failed",
6640
6864
  name: "other.lookup",
6865
+ mcpServer: "other",
6641
6866
  phase: "error",
6642
6867
  summary: "receipt",
6643
6868
  input: { q: "receipt" },
@@ -6674,6 +6899,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
6674
6899
  type: "tool",
6675
6900
  id: "mcp-image",
6676
6901
  name: "other.render",
6902
+ mcpServer: "other",
6677
6903
  phase: "start",
6678
6904
  summary: "",
6679
6905
  input: {}
@@ -6682,6 +6908,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
6682
6908
  type: "tool",
6683
6909
  id: "mcp-image",
6684
6910
  name: "other.render",
6911
+ mcpServer: "other",
6685
6912
  phase: "done",
6686
6913
  summary: "",
6687
6914
  input: {},
@@ -6729,11 +6956,20 @@ var CODEX_CONFORMANCE_FIXTURES = [
6729
6956
  ],
6730
6957
  expected: [
6731
6958
  startedSession(NEW_THREAD_ID),
6732
- { type: "tool", id: "t1", name: "list", phase: "start", summary: "", input: {} },
6733
6959
  {
6734
6960
  type: "tool",
6735
6961
  id: "t1",
6736
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",
6737
6973
  phase: "done",
6738
6974
  summary: "/product/tasks",
6739
6975
  input: { path: "/product/tasks" },
@@ -8012,7 +8248,7 @@ import { join as join14 } from "path";
8012
8248
  // src/turn-control-tools.ts
8013
8249
  import { randomUUID as randomUUID2 } from "crypto";
8014
8250
  import { z as z16 } from "zod";
8015
- var COMPANION_LOCAL_MCP_SERVER = "cabane_companion";
8251
+ var COMPANION_LOCAL_MCP_SERVER = TURN_CONTROL_MCP_SERVER;
8016
8252
  var COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
8017
8253
  var ASK_TOOL = "ask";
8018
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.202",
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",