@cabane/companion 0.6.66 → 0.6.68

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 +21 -135
  2. package/dist/runtime.js +21 -135
  3. package/package.json +3 -3
package/dist/cli.js CHANGED
@@ -2287,7 +2287,6 @@ var ACTIVE_RUN_OUTBOX_SEQ = 0;
2287
2287
  function activeRunOutboxKey(conversationId, agentId) {
2288
2288
  return `activerun:${conversationId}:${agentId}`;
2289
2289
  }
2290
- var ACTIVE_CONVERSATION_HEADER = "x-cabane-active-conversation";
2291
2290
  var CabaneApi = class {
2292
2291
  constructor(opts) {
2293
2292
  this.opts = opts;
@@ -2425,45 +2424,6 @@ var CabaneApi = class {
2425
2424
  getConversation(workspaceId, conversationId) {
2426
2425
  return this.request("GET", `/api/workspaces/${workspaceId}/conversations/${conversationId}`);
2427
2426
  }
2428
- // CT339: spawn a sub-agent conversation — the public create-conversation
2429
- // endpoint (`POST /conversations/messages`) with an addressed agent ask,
2430
- // the machinery the `sub_agent` turn-control tool is sugar over. Two things make
2431
- // it distinct from an ordinary `request` call, so it does its own `fetch`:
2432
- // - a PER-CALL bearer — the turn's OBO token when the API minted one, else the
2433
- // companion PAT — so the spawn carries the same authority as the agent's other
2434
- // cabane calls this turn (mirrors build-options' `turnToken ?? agentPat`);
2435
- // - the `x-cabane-active-conversation` header naming the caller's turn, which
2436
- // the server verifies against the live run to resolve the caller pair for
2437
- // the explicit child's birth subscription (the same origin that stamps
2438
- // provenance).
2439
- // Single-shot (no outbox/retry): a `sub_agent` spawn is a live, user-visible
2440
- // action, and its result is returned to the agent immediately. Returns
2441
- // `{ status, body }` un-thrown so the caller maps validation errors
2442
- // to a legible tool result rather than an exception.
2443
- async createSubAgentConversation(workspaceId, activeConversationId, bearer, body) {
2444
- const res = await fetch(`${this.base}/api/workspaces/${workspaceId}/conversations/messages`, {
2445
- method: "POST",
2446
- headers: {
2447
- Authorization: `Bearer ${bearer}`,
2448
- "content-type": "application/json",
2449
- accept: "application/json",
2450
- // CT238/CT339: the per-turn active-conversation header — server-verified
2451
- // against the caller's live run before it's honored (never client-trusted).
2452
- [ACTIVE_CONVERSATION_HEADER]: activeConversationId
2453
- },
2454
- body: JSON.stringify(body)
2455
- });
2456
- const raw = await res.text();
2457
- let parsed = void 0;
2458
- if (raw.length > 0) {
2459
- try {
2460
- parsed = JSON.parse(raw);
2461
- } catch {
2462
- parsed = raw;
2463
- }
2464
- }
2465
- return { status: res.status, body: parsed };
2466
- }
2467
2427
  // SJ383: companion-only participant ops. All three authenticate with the
2468
2428
  // workspace's agent-bound PAT (passed as `token` on this client) — never
2469
2429
  // the user PAT.
@@ -3407,7 +3367,7 @@ var turnRequestSchema = z8.object({
3407
3367
  // EXTERNAL adapters (Codex / opencode) mount it by URL under the key
3408
3368
  // `cabane_companion` — using the same `bearer` (the turn token) and the same
3409
3369
  // active-conversation header they send to the `cabane` server — so their
3410
- // agents get `ask`/`wake_me`/`send`/`sub_agent`/`skip_turn`, the
3370
+ // agents get `ask`/`reply_to`/`wake_me`/`send`/`skip_turn`, the
3411
3371
  // verbs they can't get from the companion's in-process SDK server. Optional:
3412
3372
  // claude-code ignores it (it mounts the in-process instance instead), and
3413
3373
  // every existing `cabane`-block fixture keeps parsing unchanged; the
@@ -3770,7 +3730,7 @@ var CLAUDE_CODE_ADDENDUM = [
3770
3730
  "`mcp__cabane__finalize_upload`, and `mcp__cabane__mint_render_token`; the turn tools are",
3771
3731
  "`mcp__cabane_companion__ask`, `mcp__cabane_companion__reply_to`,",
3772
3732
  "`mcp__cabane_companion__wake_me`,",
3773
- "`mcp__cabane_companion__send`, `mcp__cabane_companion__sub_agent`, and",
3733
+ "`mcp__cabane_companion__send`, and",
3774
3734
  "`mcp__cabane_companion__skip_turn`. Where this prompt names a tool by its bare verb, call",
3775
3735
  "the prefixed form from your tool list. Your harness's own tools (shell, local file",
3776
3736
  "read/edit) keep their ordinary names and act on the local machine only."
@@ -3882,7 +3842,7 @@ function decideResume(stored, currentCwd) {
3882
3842
 
3883
3843
  // packages/agent-runtime/src/claude-code/options.ts
3884
3844
  var CABANE_MCP_SERVER = "cabane";
3885
- var ACTIVE_CONVERSATION_HEADER2 = "x-cabane-active-conversation";
3845
+ var ACTIVE_CONVERSATION_HEADER = "x-cabane-active-conversation";
3886
3846
  async function preToolUseHook(input) {
3887
3847
  const toolInput = "tool_input" in input && input.tool_input && typeof input.tool_input === "object" ? input.tool_input : {};
3888
3848
  const toolName = "tool_name" in input ? input.tool_name : void 0;
@@ -3919,7 +3879,7 @@ function buildClaudeCodeOptions(req, augment) {
3919
3879
  url: req.cabane.mcpUrl,
3920
3880
  headers: {
3921
3881
  Authorization: `Bearer ${req.cabane.bearer}`,
3922
- [ACTIVE_CONVERSATION_HEADER2]: req.cabane.activeConversationId
3882
+ [ACTIVE_CONVERSATION_HEADER]: req.cabane.activeConversationId
3923
3883
  },
3924
3884
  // Claude Code 2.1.x defers every MCP tool behind tool-search by default;
3925
3885
  // for cabane — the always-needed core toolset — force it into the turn-1
@@ -4741,7 +4701,7 @@ function selectAdapter(registry, runtime) {
4741
4701
  // packages/agent-runtime/src/opencode/addendum.ts
4742
4702
  var OPENCODE_ADDENDUM = [
4743
4703
  "Every tool here goes by its plain name: the workspace tool is `sdk`; the turn tools are",
4744
- "`ask`, `reply_to`, `wake_me`, `send`, `sub_agent`, `skip_turn`; the ancillary tools are",
4704
+ "`ask`, `reply_to`, `wake_me`, `send`, `skip_turn`; the ancillary tools are",
4745
4705
  "`list_workspaces`, `read_binary`, `upload`, `begin_upload`, `finalize_upload`,",
4746
4706
  "`mint_render_token`; and the host tools are plain verbs too (`bash`, `read`, `edit`). Mind",
4747
4707
  "the collision: a bare `read` or `edit` is the HOST tool, acting on the local machine \u2014 a",
@@ -5008,7 +4968,7 @@ function parseOpencodeModel(model) {
5008
4968
  // packages/agent-runtime/src/opencode/run-spec.ts
5009
4969
  var CABANE_MCP_SERVER2 = "cabane";
5010
4970
  var TURN_CONTROL_MCP_SERVER = "cabane_companion";
5011
- var ACTIVE_CONVERSATION_HEADER3 = "x-cabane-active-conversation";
4971
+ var ACTIVE_CONVERSATION_HEADER2 = "x-cabane-active-conversation";
5012
4972
  function buildRunSpec(req, resumeSessionId) {
5013
4973
  const { policy, config } = req;
5014
4974
  const directory = req.local.cwd ?? "";
@@ -5065,7 +5025,7 @@ function buildMcp(req) {
5065
5025
  url: req.cabane.mcpUrl,
5066
5026
  headers: {
5067
5027
  Authorization: `Bearer ${req.cabane.bearer}`,
5068
- [ACTIVE_CONVERSATION_HEADER3]: req.cabane.activeConversationId
5028
+ [ACTIVE_CONVERSATION_HEADER2]: req.cabane.activeConversationId
5069
5029
  },
5070
5030
  enabled: true
5071
5031
  };
@@ -5075,7 +5035,7 @@ function buildMcp(req) {
5075
5035
  url: req.cabane.turnControlUrl,
5076
5036
  headers: {
5077
5037
  Authorization: `Bearer ${req.cabane.bearer}`,
5078
- [ACTIVE_CONVERSATION_HEADER3]: req.cabane.activeConversationId
5038
+ [ACTIVE_CONVERSATION_HEADER2]: req.cabane.activeConversationId
5079
5039
  },
5080
5040
  enabled: true
5081
5041
  };
@@ -5718,7 +5678,7 @@ var CODEX_ADDENDUM = [
5718
5678
  "the deferred-tool inventory and invoke that exact qualified name; never conclude the SDK is",
5719
5679
  "absent without attempting discovery and invocation, and if a call fails, report the recorded",
5720
5680
  "error. The turn tools are `mcp__cabane_companion__ask` / `reply_to` / `wake_me` /",
5721
- "`send` / `sub_agent` / `skip_turn`, and the ancillary tools",
5681
+ "`send` / `skip_turn`, and the ancillary tools",
5722
5682
  "`mcp__cabane__list_workspaces` /",
5723
5683
  "`read_binary` / `upload` / `begin_upload` / `finalize_upload` / `mint_render_token` \u2014 all",
5724
5684
  "possibly deferred too. There is no Cabane `read`/`write`/`search` tool \u2014 those are",
@@ -6082,7 +6042,7 @@ function parseCodexModel(model) {
6082
6042
  // packages/agent-runtime/src/codex/run-spec.ts
6083
6043
  var CABANE_MCP_SERVER3 = "cabane";
6084
6044
  var TURN_CONTROL_MCP_SERVER2 = "cabane_companion";
6085
- var ACTIVE_CONVERSATION_HEADER4 = "x-cabane-active-conversation";
6045
+ var ACTIVE_CONVERSATION_HEADER3 = "x-cabane-active-conversation";
6086
6046
  var ENV_ENVELOPE_KEYS = [
6087
6047
  "CABANE_PLAYGROUND",
6088
6048
  "CABANE_PLAYGROUND_BIN",
@@ -6166,7 +6126,7 @@ function buildConfig(req, baseInstructionsFile) {
6166
6126
  url: req.cabane.mcpUrl,
6167
6127
  http_headers: {
6168
6128
  Authorization: `Bearer ${req.cabane.bearer}`,
6169
- [ACTIVE_CONVERSATION_HEADER4]: req.cabane.activeConversationId
6129
+ [ACTIVE_CONVERSATION_HEADER3]: req.cabane.activeConversationId
6170
6130
  },
6171
6131
  default_tools_approval_mode: "approve"
6172
6132
  };
@@ -6175,7 +6135,7 @@ function buildConfig(req, baseInstructionsFile) {
6175
6135
  url: req.cabane.turnControlUrl,
6176
6136
  http_headers: {
6177
6137
  Authorization: `Bearer ${req.cabane.bearer}`,
6178
- [ACTIVE_CONVERSATION_HEADER4]: req.cabane.activeConversationId
6138
+ [ACTIVE_CONVERSATION_HEADER3]: req.cabane.activeConversationId
6179
6139
  },
6180
6140
  default_tools_approval_mode: "approve"
6181
6141
  };
@@ -7229,8 +7189,6 @@ var ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${ASK_TOOL}`;
7229
7189
  var REPLY_TO_TOOL = "reply_to";
7230
7190
  var REPLY_TO_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${REPLY_TO_TOOL}`;
7231
7191
  var MAX_ASK_ITEMS = 20;
7232
- var SUB_AGENT_TOOL = "sub_agent";
7233
- var SUB_AGENT_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${SUB_AGENT_TOOL}`;
7234
7192
  var WAKE_ME_TOOL = "wake_me";
7235
7193
  var WAKE_ME_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WAKE_ME_TOOL}`;
7236
7194
  var CANCEL_WAKE_TOOL = "cancel_wake";
@@ -7269,8 +7227,8 @@ function resolveDeclaredReplyField(input) {
7269
7227
  );
7270
7228
  const askRaised = Boolean(input.askState.targetUserId);
7271
7229
  const wakeArmed = input.wakeState.afterSeconds !== null || input.wakeState.at !== null;
7272
- if (outwardSend || askRaised || wakeArmed || input.spawnedSubAgent) return {};
7273
- return { answersMessageId: input.owedReplyMessageId };
7230
+ if (outwardSend || askRaised || wakeArmed) return {};
7231
+ return { answersMessageId: input.owedReplyMessageId, answersAutoDeclared: true };
7274
7232
  }
7275
7233
  function wakeCommitField(state) {
7276
7234
  if (state.cancelled) return { wake: { cancel: true } };
@@ -7284,7 +7242,7 @@ function wakeCommitField(state) {
7284
7242
  }
7285
7243
  };
7286
7244
  }
7287
- function createTurnControlMcpServer(sendState, skipState, askState, subAgentCreate, wakeState, replyState, controlOrder) {
7245
+ function createTurnControlMcpServer(sendState, skipState, askState, wakeState, replyState, controlOrder) {
7288
7246
  return createSdkMcpServer({
7289
7247
  name: COMPANION_LOCAL_MCP_SERVER,
7290
7248
  version: "0.0.0",
@@ -7420,42 +7378,6 @@ function createTurnControlMcpServer(sendState, skipState, askState, subAgentCrea
7420
7378
  { annotations: { readOnlyHint: true, openWorldHint: false }, alwaysLoad: true }
7421
7379
  )
7422
7380
  ] : [],
7423
- ...subAgentCreate ? [
7424
- tool(
7425
- SUB_AGENT_TOOL,
7426
- "Ask into a child conversation \u2014 a bounded question or piece of work whose ANSWER returns to you automatically. This creates the child and dispatches its agent now, returning the child's `conversationId` immediately; the answer lands LATER as a message in THIS conversation, and you're woken once every child you asked this turn has answered. So DON'T wait: after asking, finish whatever else this turn can do and end your turn (never poll the child with reads in a loop \u2014 the wake is automatic and it carries the answer). Parallel fan-out = call this N times in one turn (they run concurrently; ONE wake carrying all the answers); series = one call per turn. `prompt` is the ask itself \u2014 write it as a request that expects an answer back, not a role you are assigning or a job description: self-contained (the child starts fresh, with only this prompt), and say what shape the answer should take. `agentId` (optional) asks a PEER instead of yourself \u2014 same mechanics, a different mind (use for capability/context you lack); default (self) is a fresh context window of you. `title` (optional) names the child thread (answers link it, so a legible title helps). A single child buys no wall-clock time (you idle either way) \u2014 it pays when the callee has capability/context you lack, or to isolate a big read from your own session; the real win is fan-out. Don't ask a child for a lookup you can do in-turn with your own tools. The answer returns to YOU to act on \u2014 reach for it when you're the consumer of the output, not as a way to notify a human: if a person just wants to read the result, dispatch a plain (no-return) conversation and link it instead.",
7427
- {
7428
- prompt: z13.string().min(1).max(65536).describe(
7429
- "The ask \u2014 a self-contained request that expects an answer back (the child starts fresh: only this prompt + the thread it lands in). Say what shape the answer should take."
7430
- ),
7431
- agentId: z13.string().uuid().optional().describe(
7432
- "Optional peer to run the sub-agent as (a workspace agent id, from your turn context's roster); omit to spawn yourself with a fresh context window."
7433
- ),
7434
- title: z13.string().max(200).optional().describe("Optional title for the child thread (result chips link it).")
7435
- },
7436
- async (args) => {
7437
- const result = await subAgentCreate(args);
7438
- if (!result.ok) {
7439
- return {
7440
- content: [{ type: "text", text: result.error }],
7441
- isError: true
7442
- };
7443
- }
7444
- return {
7445
- content: [
7446
- {
7447
- type: "text",
7448
- text: JSON.stringify({
7449
- conversationId: result.conversationId,
7450
- note: "Asked. Don't wait for it \u2014 finish what else this turn can do, then end your turn; the answer posts back here and you're woken once every child you asked this turn has answered."
7451
- })
7452
- }
7453
- ]
7454
- };
7455
- },
7456
- { annotations: { readOnlyHint: false, openWorldHint: true }, alwaysLoad: true }
7457
- )
7458
- ] : [],
7459
7381
  ...wakeState ? [
7460
7382
  tool(
7461
7383
  WAKE_ME_TOOL,
@@ -7995,7 +7917,10 @@ var TurnCommitter = class {
7995
7917
  }
7996
7918
  // The declared reply: explicit `reply_to` first, else the runtime's own
7997
7919
  // declaration for the turn that just answers — see
7998
- // `resolveDeclaredReplyField` for the whole rule and its reasons.
7920
+ // `resolveDeclaredReplyField` for the whole rule and its reasons. CT1224: an
7921
+ // auto-declaration is flagged as such on the wire, because the server drops it
7922
+ // when the turn also opened a conversation (a fan-out is mid-arc). An explicit
7923
+ // `reply_to` carries no flag and always stands.
7999
7924
  answersField(kind) {
8000
7925
  return resolveDeclaredReplyField({
8001
7926
  kind,
@@ -8004,8 +7929,7 @@ var TurnCommitter = class {
8004
7929
  replyState: this.deps.replyState,
8005
7930
  sendState: this.deps.sendState,
8006
7931
  askState: this.deps.askState,
8007
- wakeState: this.deps.wakeState,
8008
- spawnedSubAgent: this.deps.spawnedSubAgent()
7932
+ wakeState: this.deps.wakeState
8009
7933
  });
8010
7934
  }
8011
7935
  // Carry both halves of the addressed send. The server writes `dispatchBody`
@@ -8126,17 +8050,6 @@ function describeErrorBody(body) {
8126
8050
  if (text.length === 0) return void 0;
8127
8051
  return text.length > ERROR_BODY_LOG_CAP ? `${text.slice(0, ERROR_BODY_LOG_CAP)}\u2026` : text;
8128
8052
  }
8129
- function describeSubAgentError(status2, body) {
8130
- const code = body && typeof body === "object" && "error" in body ? String(body.error) : void 0;
8131
- switch (code) {
8132
- case "dispatch_agent_not_found":
8133
- return "sub_agent: no live agent in this workspace matches that `agentId`. Check the roster in your turn context, or omit `agentId` to spawn yourself.";
8134
- case "dispatch_ask_requires_dispatch":
8135
- return `sub_agent: the spawn was rejected (${code}). This is a turn-context problem, not something to retry blindly \u2014 report it rather than looping.`;
8136
- default:
8137
- return `sub_agent: the spawn failed (${code ?? `HTTP ${status2}`}).`;
8138
- }
8139
- }
8140
8053
  var Dispatcher = class {
8141
8054
  constructor(opts) {
8142
8055
  this.opts = opts;
@@ -8499,36 +8412,10 @@ ${reason}`,
8499
8412
  const wakeState = createWakeState();
8500
8413
  const replyState = createReplyState();
8501
8414
  const controlOrder = createTurnControlOrder();
8502
- let spawnedSubAgent = false;
8503
- const subAgentCreate = async (args) => {
8504
- const dispatchTarget = args.agentId ?? payload.agentId;
8505
- const projectId = turnContext.conversation.projectId;
8506
- const { status: status2, body } = await this.opts.api.createSubAgentConversation(
8507
- workspaceId,
8508
- payload.conversationId,
8509
- turnContext.turnToken ?? this.opts.credential,
8510
- {
8511
- ...projectId ? { projectId } : {},
8512
- ...args.title ? { title: args.title } : {},
8513
- body: args.prompt,
8514
- dispatch: dispatchTarget,
8515
- dispatchAsk: true,
8516
- parentConversationId: payload.conversationId
8517
- }
8518
- );
8519
- if (status2 >= 400) return { ok: false, error: describeSubAgentError(status2, body) };
8520
- const conversationId = body?.conversation?.id;
8521
- if (!conversationId) {
8522
- return { ok: false, error: "sub_agent: the spawn returned no conversation id." };
8523
- }
8524
- spawnedSubAgent = true;
8525
- return { ok: true, conversationId };
8526
- };
8527
8415
  const turnControlServer = createTurnControlMcpServer(
8528
8416
  sendState,
8529
8417
  skipState,
8530
8418
  askState,
8531
- subAgentCreate,
8532
8419
  wakeState,
8533
8420
  replyState,
8534
8421
  controlOrder
@@ -8664,8 +8551,7 @@ ${reason}`,
8664
8551
  replyState,
8665
8552
  // The ledger-derived owed reply, for the runtime's own declaration when
8666
8553
  // the agent doesn't call `reply_to` (resolveDeclaredReplyField).
8667
- owedReplyMessageId: turnContext.owedReplyMessageId ?? null,
8668
- spawnedSubAgent: () => spawnedSubAgent
8554
+ owedReplyMessageId: turnContext.owedReplyMessageId ?? null
8669
8555
  });
8670
8556
  const usesHttpTurnControl = turnRuntime === "codex" || turnRuntime === "opencode";
8671
8557
  let turnControlIntentFetched = false;
package/dist/runtime.js CHANGED
@@ -1707,7 +1707,6 @@ var ACTIVE_RUN_OUTBOX_SEQ = 0;
1707
1707
  function activeRunOutboxKey(conversationId, agentId) {
1708
1708
  return `activerun:${conversationId}:${agentId}`;
1709
1709
  }
1710
- var ACTIVE_CONVERSATION_HEADER = "x-cabane-active-conversation";
1711
1710
  var CabaneApi = class {
1712
1711
  constructor(opts) {
1713
1712
  this.opts = opts;
@@ -1845,45 +1844,6 @@ var CabaneApi = class {
1845
1844
  getConversation(workspaceId, conversationId) {
1846
1845
  return this.request("GET", `/api/workspaces/${workspaceId}/conversations/${conversationId}`);
1847
1846
  }
1848
- // CT339: spawn a sub-agent conversation — the public create-conversation
1849
- // endpoint (`POST /conversations/messages`) with an addressed agent ask,
1850
- // the machinery the `sub_agent` turn-control tool is sugar over. Two things make
1851
- // it distinct from an ordinary `request` call, so it does its own `fetch`:
1852
- // - a PER-CALL bearer — the turn's OBO token when the API minted one, else the
1853
- // companion PAT — so the spawn carries the same authority as the agent's other
1854
- // cabane calls this turn (mirrors build-options' `turnToken ?? agentPat`);
1855
- // - the `x-cabane-active-conversation` header naming the caller's turn, which
1856
- // the server verifies against the live run to resolve the caller pair for
1857
- // the explicit child's birth subscription (the same origin that stamps
1858
- // provenance).
1859
- // Single-shot (no outbox/retry): a `sub_agent` spawn is a live, user-visible
1860
- // action, and its result is returned to the agent immediately. Returns
1861
- // `{ status, body }` un-thrown so the caller maps validation errors
1862
- // to a legible tool result rather than an exception.
1863
- async createSubAgentConversation(workspaceId, activeConversationId, bearer, body) {
1864
- const res = await fetch(`${this.base}/api/workspaces/${workspaceId}/conversations/messages`, {
1865
- method: "POST",
1866
- headers: {
1867
- Authorization: `Bearer ${bearer}`,
1868
- "content-type": "application/json",
1869
- accept: "application/json",
1870
- // CT238/CT339: the per-turn active-conversation header — server-verified
1871
- // against the caller's live run before it's honored (never client-trusted).
1872
- [ACTIVE_CONVERSATION_HEADER]: activeConversationId
1873
- },
1874
- body: JSON.stringify(body)
1875
- });
1876
- const raw = await res.text();
1877
- let parsed = void 0;
1878
- if (raw.length > 0) {
1879
- try {
1880
- parsed = JSON.parse(raw);
1881
- } catch {
1882
- parsed = raw;
1883
- }
1884
- }
1885
- return { status: res.status, body: parsed };
1886
- }
1887
1847
  // SJ383: companion-only participant ops. All three authenticate with the
1888
1848
  // workspace's agent-bound PAT (passed as `token` on this client) — never
1889
1849
  // the user PAT.
@@ -2906,7 +2866,7 @@ var turnRequestSchema = z8.object({
2906
2866
  // EXTERNAL adapters (Codex / opencode) mount it by URL under the key
2907
2867
  // `cabane_companion` — using the same `bearer` (the turn token) and the same
2908
2868
  // active-conversation header they send to the `cabane` server — so their
2909
- // agents get `ask`/`wake_me`/`send`/`sub_agent`/`skip_turn`, the
2869
+ // agents get `ask`/`reply_to`/`wake_me`/`send`/`skip_turn`, the
2910
2870
  // verbs they can't get from the companion's in-process SDK server. Optional:
2911
2871
  // claude-code ignores it (it mounts the in-process instance instead), and
2912
2872
  // every existing `cabane`-block fixture keeps parsing unchanged; the
@@ -3269,7 +3229,7 @@ var CLAUDE_CODE_ADDENDUM = [
3269
3229
  "`mcp__cabane__finalize_upload`, and `mcp__cabane__mint_render_token`; the turn tools are",
3270
3230
  "`mcp__cabane_companion__ask`, `mcp__cabane_companion__reply_to`,",
3271
3231
  "`mcp__cabane_companion__wake_me`,",
3272
- "`mcp__cabane_companion__send`, `mcp__cabane_companion__sub_agent`, and",
3232
+ "`mcp__cabane_companion__send`, and",
3273
3233
  "`mcp__cabane_companion__skip_turn`. Where this prompt names a tool by its bare verb, call",
3274
3234
  "the prefixed form from your tool list. Your harness's own tools (shell, local file",
3275
3235
  "read/edit) keep their ordinary names and act on the local machine only."
@@ -3381,7 +3341,7 @@ function decideResume(stored, currentCwd) {
3381
3341
 
3382
3342
  // packages/agent-runtime/src/claude-code/options.ts
3383
3343
  var CABANE_MCP_SERVER = "cabane";
3384
- var ACTIVE_CONVERSATION_HEADER2 = "x-cabane-active-conversation";
3344
+ var ACTIVE_CONVERSATION_HEADER = "x-cabane-active-conversation";
3385
3345
  async function preToolUseHook(input) {
3386
3346
  const toolInput = "tool_input" in input && input.tool_input && typeof input.tool_input === "object" ? input.tool_input : {};
3387
3347
  const toolName = "tool_name" in input ? input.tool_name : void 0;
@@ -3418,7 +3378,7 @@ function buildClaudeCodeOptions(req, augment) {
3418
3378
  url: req.cabane.mcpUrl,
3419
3379
  headers: {
3420
3380
  Authorization: `Bearer ${req.cabane.bearer}`,
3421
- [ACTIVE_CONVERSATION_HEADER2]: req.cabane.activeConversationId
3381
+ [ACTIVE_CONVERSATION_HEADER]: req.cabane.activeConversationId
3422
3382
  },
3423
3383
  // Claude Code 2.1.x defers every MCP tool behind tool-search by default;
3424
3384
  // for cabane — the always-needed core toolset — force it into the turn-1
@@ -4240,7 +4200,7 @@ function selectAdapter(registry, runtime) {
4240
4200
  // packages/agent-runtime/src/opencode/addendum.ts
4241
4201
  var OPENCODE_ADDENDUM = [
4242
4202
  "Every tool here goes by its plain name: the workspace tool is `sdk`; the turn tools are",
4243
- "`ask`, `reply_to`, `wake_me`, `send`, `sub_agent`, `skip_turn`; the ancillary tools are",
4203
+ "`ask`, `reply_to`, `wake_me`, `send`, `skip_turn`; the ancillary tools are",
4244
4204
  "`list_workspaces`, `read_binary`, `upload`, `begin_upload`, `finalize_upload`,",
4245
4205
  "`mint_render_token`; and the host tools are plain verbs too (`bash`, `read`, `edit`). Mind",
4246
4206
  "the collision: a bare `read` or `edit` is the HOST tool, acting on the local machine \u2014 a",
@@ -4507,7 +4467,7 @@ function parseOpencodeModel(model) {
4507
4467
  // packages/agent-runtime/src/opencode/run-spec.ts
4508
4468
  var CABANE_MCP_SERVER2 = "cabane";
4509
4469
  var TURN_CONTROL_MCP_SERVER = "cabane_companion";
4510
- var ACTIVE_CONVERSATION_HEADER3 = "x-cabane-active-conversation";
4470
+ var ACTIVE_CONVERSATION_HEADER2 = "x-cabane-active-conversation";
4511
4471
  function buildRunSpec(req, resumeSessionId) {
4512
4472
  const { policy, config } = req;
4513
4473
  const directory = req.local.cwd ?? "";
@@ -4564,7 +4524,7 @@ function buildMcp(req) {
4564
4524
  url: req.cabane.mcpUrl,
4565
4525
  headers: {
4566
4526
  Authorization: `Bearer ${req.cabane.bearer}`,
4567
- [ACTIVE_CONVERSATION_HEADER3]: req.cabane.activeConversationId
4527
+ [ACTIVE_CONVERSATION_HEADER2]: req.cabane.activeConversationId
4568
4528
  },
4569
4529
  enabled: true
4570
4530
  };
@@ -4574,7 +4534,7 @@ function buildMcp(req) {
4574
4534
  url: req.cabane.turnControlUrl,
4575
4535
  headers: {
4576
4536
  Authorization: `Bearer ${req.cabane.bearer}`,
4577
- [ACTIVE_CONVERSATION_HEADER3]: req.cabane.activeConversationId
4537
+ [ACTIVE_CONVERSATION_HEADER2]: req.cabane.activeConversationId
4578
4538
  },
4579
4539
  enabled: true
4580
4540
  };
@@ -5217,7 +5177,7 @@ var CODEX_ADDENDUM = [
5217
5177
  "the deferred-tool inventory and invoke that exact qualified name; never conclude the SDK is",
5218
5178
  "absent without attempting discovery and invocation, and if a call fails, report the recorded",
5219
5179
  "error. The turn tools are `mcp__cabane_companion__ask` / `reply_to` / `wake_me` /",
5220
- "`send` / `sub_agent` / `skip_turn`, and the ancillary tools",
5180
+ "`send` / `skip_turn`, and the ancillary tools",
5221
5181
  "`mcp__cabane__list_workspaces` /",
5222
5182
  "`read_binary` / `upload` / `begin_upload` / `finalize_upload` / `mint_render_token` \u2014 all",
5223
5183
  "possibly deferred too. There is no Cabane `read`/`write`/`search` tool \u2014 those are",
@@ -5581,7 +5541,7 @@ function parseCodexModel(model) {
5581
5541
  // packages/agent-runtime/src/codex/run-spec.ts
5582
5542
  var CABANE_MCP_SERVER3 = "cabane";
5583
5543
  var TURN_CONTROL_MCP_SERVER2 = "cabane_companion";
5584
- var ACTIVE_CONVERSATION_HEADER4 = "x-cabane-active-conversation";
5544
+ var ACTIVE_CONVERSATION_HEADER3 = "x-cabane-active-conversation";
5585
5545
  var ENV_ENVELOPE_KEYS = [
5586
5546
  "CABANE_PLAYGROUND",
5587
5547
  "CABANE_PLAYGROUND_BIN",
@@ -5665,7 +5625,7 @@ function buildConfig(req, baseInstructionsFile) {
5665
5625
  url: req.cabane.mcpUrl,
5666
5626
  http_headers: {
5667
5627
  Authorization: `Bearer ${req.cabane.bearer}`,
5668
- [ACTIVE_CONVERSATION_HEADER4]: req.cabane.activeConversationId
5628
+ [ACTIVE_CONVERSATION_HEADER3]: req.cabane.activeConversationId
5669
5629
  },
5670
5630
  default_tools_approval_mode: "approve"
5671
5631
  };
@@ -5674,7 +5634,7 @@ function buildConfig(req, baseInstructionsFile) {
5674
5634
  url: req.cabane.turnControlUrl,
5675
5635
  http_headers: {
5676
5636
  Authorization: `Bearer ${req.cabane.bearer}`,
5677
- [ACTIVE_CONVERSATION_HEADER4]: req.cabane.activeConversationId
5637
+ [ACTIVE_CONVERSATION_HEADER3]: req.cabane.activeConversationId
5678
5638
  },
5679
5639
  default_tools_approval_mode: "approve"
5680
5640
  };
@@ -6728,8 +6688,6 @@ var ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${ASK_TOOL}`;
6728
6688
  var REPLY_TO_TOOL = "reply_to";
6729
6689
  var REPLY_TO_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${REPLY_TO_TOOL}`;
6730
6690
  var MAX_ASK_ITEMS = 20;
6731
- var SUB_AGENT_TOOL = "sub_agent";
6732
- var SUB_AGENT_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${SUB_AGENT_TOOL}`;
6733
6691
  var WAKE_ME_TOOL = "wake_me";
6734
6692
  var WAKE_ME_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WAKE_ME_TOOL}`;
6735
6693
  var CANCEL_WAKE_TOOL = "cancel_wake";
@@ -6768,8 +6726,8 @@ function resolveDeclaredReplyField(input) {
6768
6726
  );
6769
6727
  const askRaised = Boolean(input.askState.targetUserId);
6770
6728
  const wakeArmed = input.wakeState.afterSeconds !== null || input.wakeState.at !== null;
6771
- if (outwardSend || askRaised || wakeArmed || input.spawnedSubAgent) return {};
6772
- return { answersMessageId: input.owedReplyMessageId };
6729
+ if (outwardSend || askRaised || wakeArmed) return {};
6730
+ return { answersMessageId: input.owedReplyMessageId, answersAutoDeclared: true };
6773
6731
  }
6774
6732
  function wakeCommitField(state) {
6775
6733
  if (state.cancelled) return { wake: { cancel: true } };
@@ -6783,7 +6741,7 @@ function wakeCommitField(state) {
6783
6741
  }
6784
6742
  };
6785
6743
  }
6786
- function createTurnControlMcpServer(sendState, skipState, askState, subAgentCreate, wakeState, replyState, controlOrder) {
6744
+ function createTurnControlMcpServer(sendState, skipState, askState, wakeState, replyState, controlOrder) {
6787
6745
  return createSdkMcpServer({
6788
6746
  name: COMPANION_LOCAL_MCP_SERVER,
6789
6747
  version: "0.0.0",
@@ -6919,42 +6877,6 @@ function createTurnControlMcpServer(sendState, skipState, askState, subAgentCrea
6919
6877
  { annotations: { readOnlyHint: true, openWorldHint: false }, alwaysLoad: true }
6920
6878
  )
6921
6879
  ] : [],
6922
- ...subAgentCreate ? [
6923
- tool(
6924
- SUB_AGENT_TOOL,
6925
- "Ask into a child conversation \u2014 a bounded question or piece of work whose ANSWER returns to you automatically. This creates the child and dispatches its agent now, returning the child's `conversationId` immediately; the answer lands LATER as a message in THIS conversation, and you're woken once every child you asked this turn has answered. So DON'T wait: after asking, finish whatever else this turn can do and end your turn (never poll the child with reads in a loop \u2014 the wake is automatic and it carries the answer). Parallel fan-out = call this N times in one turn (they run concurrently; ONE wake carrying all the answers); series = one call per turn. `prompt` is the ask itself \u2014 write it as a request that expects an answer back, not a role you are assigning or a job description: self-contained (the child starts fresh, with only this prompt), and say what shape the answer should take. `agentId` (optional) asks a PEER instead of yourself \u2014 same mechanics, a different mind (use for capability/context you lack); default (self) is a fresh context window of you. `title` (optional) names the child thread (answers link it, so a legible title helps). A single child buys no wall-clock time (you idle either way) \u2014 it pays when the callee has capability/context you lack, or to isolate a big read from your own session; the real win is fan-out. Don't ask a child for a lookup you can do in-turn with your own tools. The answer returns to YOU to act on \u2014 reach for it when you're the consumer of the output, not as a way to notify a human: if a person just wants to read the result, dispatch a plain (no-return) conversation and link it instead.",
6926
- {
6927
- prompt: z13.string().min(1).max(65536).describe(
6928
- "The ask \u2014 a self-contained request that expects an answer back (the child starts fresh: only this prompt + the thread it lands in). Say what shape the answer should take."
6929
- ),
6930
- agentId: z13.string().uuid().optional().describe(
6931
- "Optional peer to run the sub-agent as (a workspace agent id, from your turn context's roster); omit to spawn yourself with a fresh context window."
6932
- ),
6933
- title: z13.string().max(200).optional().describe("Optional title for the child thread (result chips link it).")
6934
- },
6935
- async (args) => {
6936
- const result = await subAgentCreate(args);
6937
- if (!result.ok) {
6938
- return {
6939
- content: [{ type: "text", text: result.error }],
6940
- isError: true
6941
- };
6942
- }
6943
- return {
6944
- content: [
6945
- {
6946
- type: "text",
6947
- text: JSON.stringify({
6948
- conversationId: result.conversationId,
6949
- note: "Asked. Don't wait for it \u2014 finish what else this turn can do, then end your turn; the answer posts back here and you're woken once every child you asked this turn has answered."
6950
- })
6951
- }
6952
- ]
6953
- };
6954
- },
6955
- { annotations: { readOnlyHint: false, openWorldHint: true }, alwaysLoad: true }
6956
- )
6957
- ] : [],
6958
6880
  ...wakeState ? [
6959
6881
  tool(
6960
6882
  WAKE_ME_TOOL,
@@ -7494,7 +7416,10 @@ var TurnCommitter = class {
7494
7416
  }
7495
7417
  // The declared reply: explicit `reply_to` first, else the runtime's own
7496
7418
  // declaration for the turn that just answers — see
7497
- // `resolveDeclaredReplyField` for the whole rule and its reasons.
7419
+ // `resolveDeclaredReplyField` for the whole rule and its reasons. CT1224: an
7420
+ // auto-declaration is flagged as such on the wire, because the server drops it
7421
+ // when the turn also opened a conversation (a fan-out is mid-arc). An explicit
7422
+ // `reply_to` carries no flag and always stands.
7498
7423
  answersField(kind) {
7499
7424
  return resolveDeclaredReplyField({
7500
7425
  kind,
@@ -7503,8 +7428,7 @@ var TurnCommitter = class {
7503
7428
  replyState: this.deps.replyState,
7504
7429
  sendState: this.deps.sendState,
7505
7430
  askState: this.deps.askState,
7506
- wakeState: this.deps.wakeState,
7507
- spawnedSubAgent: this.deps.spawnedSubAgent()
7431
+ wakeState: this.deps.wakeState
7508
7432
  });
7509
7433
  }
7510
7434
  // Carry both halves of the addressed send. The server writes `dispatchBody`
@@ -7625,17 +7549,6 @@ function describeErrorBody(body) {
7625
7549
  if (text.length === 0) return void 0;
7626
7550
  return text.length > ERROR_BODY_LOG_CAP ? `${text.slice(0, ERROR_BODY_LOG_CAP)}\u2026` : text;
7627
7551
  }
7628
- function describeSubAgentError(status, body) {
7629
- const code = body && typeof body === "object" && "error" in body ? String(body.error) : void 0;
7630
- switch (code) {
7631
- case "dispatch_agent_not_found":
7632
- return "sub_agent: no live agent in this workspace matches that `agentId`. Check the roster in your turn context, or omit `agentId` to spawn yourself.";
7633
- case "dispatch_ask_requires_dispatch":
7634
- return `sub_agent: the spawn was rejected (${code}). This is a turn-context problem, not something to retry blindly \u2014 report it rather than looping.`;
7635
- default:
7636
- return `sub_agent: the spawn failed (${code ?? `HTTP ${status}`}).`;
7637
- }
7638
- }
7639
7552
  var Dispatcher = class {
7640
7553
  constructor(opts) {
7641
7554
  this.opts = opts;
@@ -7998,36 +7911,10 @@ ${reason}`,
7998
7911
  const wakeState = createWakeState();
7999
7912
  const replyState = createReplyState();
8000
7913
  const controlOrder = createTurnControlOrder();
8001
- let spawnedSubAgent = false;
8002
- const subAgentCreate = async (args) => {
8003
- const dispatchTarget = args.agentId ?? payload.agentId;
8004
- const projectId = turnContext.conversation.projectId;
8005
- const { status, body } = await this.opts.api.createSubAgentConversation(
8006
- workspaceId,
8007
- payload.conversationId,
8008
- turnContext.turnToken ?? this.opts.credential,
8009
- {
8010
- ...projectId ? { projectId } : {},
8011
- ...args.title ? { title: args.title } : {},
8012
- body: args.prompt,
8013
- dispatch: dispatchTarget,
8014
- dispatchAsk: true,
8015
- parentConversationId: payload.conversationId
8016
- }
8017
- );
8018
- if (status >= 400) return { ok: false, error: describeSubAgentError(status, body) };
8019
- const conversationId = body?.conversation?.id;
8020
- if (!conversationId) {
8021
- return { ok: false, error: "sub_agent: the spawn returned no conversation id." };
8022
- }
8023
- spawnedSubAgent = true;
8024
- return { ok: true, conversationId };
8025
- };
8026
7914
  const turnControlServer = createTurnControlMcpServer(
8027
7915
  sendState,
8028
7916
  skipState,
8029
7917
  askState,
8030
- subAgentCreate,
8031
7918
  wakeState,
8032
7919
  replyState,
8033
7920
  controlOrder
@@ -8163,8 +8050,7 @@ ${reason}`,
8163
8050
  replyState,
8164
8051
  // The ledger-derived owed reply, for the runtime's own declaration when
8165
8052
  // the agent doesn't call `reply_to` (resolveDeclaredReplyField).
8166
- owedReplyMessageId: turnContext.owedReplyMessageId ?? null,
8167
- spawnedSubAgent: () => spawnedSubAgent
8053
+ owedReplyMessageId: turnContext.owedReplyMessageId ?? null
8168
8054
  });
8169
8055
  const usesHttpTurnControl = turnRuntime === "codex" || turnRuntime === "opencode";
8170
8056
  let turnControlIntentFetched = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.66",
3
+ "version": "0.6.68",
4
4
  "type": "module",
5
5
  "description": "The Cabane Companion (headless): connect a coding 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",
@@ -28,8 +28,8 @@
28
28
  "prepublishOnly": "pnpm build"
29
29
  },
30
30
  "dependencies": {
31
- "@anthropic-ai/claude-agent-sdk": "0.3.233",
32
- "@openai/codex-sdk": "0.147.0",
31
+ "@anthropic-ai/claude-agent-sdk": "0.3.251",
32
+ "@openai/codex-sdk": "0.151.0",
33
33
  "@hono/node-server": "1.19.14",
34
34
  "@inquirer/prompts": "7.10.1",
35
35
  "commander": "12.1.0",