@cabane/companion 0.6.65 → 0.6.67

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 +31 -152
  2. package/dist/runtime.js +31 -152
  3. package/package.json +1 -1
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";
@@ -7254,14 +7212,7 @@ function createSkipState() {
7254
7212
  return { skipped: false, reason: null };
7255
7213
  }
7256
7214
  function createAskState() {
7257
- return {
7258
- targetUserId: null,
7259
- question: null,
7260
- headline: null,
7261
- options: null,
7262
- questions: null,
7263
- order: null
7264
- };
7215
+ return { targetUserId: null, question: null, headline: null, options: null, questions: null };
7265
7216
  }
7266
7217
  function createWakeState() {
7267
7218
  return { afterSeconds: null, at: null, note: null, cancelled: false };
@@ -7276,8 +7227,8 @@ function resolveDeclaredReplyField(input) {
7276
7227
  );
7277
7228
  const askRaised = Boolean(input.askState.targetUserId);
7278
7229
  const wakeArmed = input.wakeState.afterSeconds !== null || input.wakeState.at !== null;
7279
- if (outwardSend || askRaised || wakeArmed || input.spawnedSubAgent) return {};
7280
- return { answersMessageId: input.owedReplyMessageId };
7230
+ if (outwardSend || askRaised || wakeArmed) return {};
7231
+ return { answersMessageId: input.owedReplyMessageId, answersAutoDeclared: true };
7281
7232
  }
7282
7233
  function wakeCommitField(state) {
7283
7234
  if (state.cancelled) return { wake: { cancel: true } };
@@ -7291,7 +7242,7 @@ function wakeCommitField(state) {
7291
7242
  }
7292
7243
  };
7293
7244
  }
7294
- function createTurnControlMcpServer(sendState, skipState, askState, subAgentCreate, wakeState, replyState, controlOrder) {
7245
+ function createTurnControlMcpServer(sendState, skipState, askState, wakeState, replyState, controlOrder) {
7295
7246
  return createSdkMcpServer({
7296
7247
  name: COMPANION_LOCAL_MCP_SERVER,
7297
7248
  version: "0.0.0",
@@ -7407,7 +7358,6 @@ function createTurnControlMcpServer(sendState, skipState, askState, subAgentCrea
7407
7358
  };
7408
7359
  }
7409
7360
  askState.targetUserId = args.targetUserId;
7410
- askState.order = controlOrder?.next() ?? null;
7411
7361
  if (hasArray) {
7412
7362
  askState.questions = args.questions;
7413
7363
  askState.question = null;
@@ -7428,42 +7378,6 @@ function createTurnControlMcpServer(sendState, skipState, askState, subAgentCrea
7428
7378
  { annotations: { readOnlyHint: true, openWorldHint: false }, alwaysLoad: true }
7429
7379
  )
7430
7380
  ] : [],
7431
- ...subAgentCreate ? [
7432
- tool(
7433
- SUB_AGENT_TOOL,
7434
- "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.",
7435
- {
7436
- prompt: z13.string().min(1).max(65536).describe(
7437
- "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."
7438
- ),
7439
- agentId: z13.string().uuid().optional().describe(
7440
- "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."
7441
- ),
7442
- title: z13.string().max(200).optional().describe("Optional title for the child thread (result chips link it).")
7443
- },
7444
- async (args) => {
7445
- const result = await subAgentCreate(args);
7446
- if (!result.ok) {
7447
- return {
7448
- content: [{ type: "text", text: result.error }],
7449
- isError: true
7450
- };
7451
- }
7452
- return {
7453
- content: [
7454
- {
7455
- type: "text",
7456
- text: JSON.stringify({
7457
- conversationId: result.conversationId,
7458
- 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."
7459
- })
7460
- }
7461
- ]
7462
- };
7463
- },
7464
- { annotations: { readOnlyHint: false, openWorldHint: true }, alwaysLoad: true }
7465
- )
7466
- ] : [],
7467
7381
  ...wakeState ? [
7468
7382
  tool(
7469
7383
  WAKE_ME_TOOL,
@@ -7983,10 +7897,15 @@ var TurnCommitter = class {
7983
7897
  }
7984
7898
  // CT1281: the order the control tools were CALLED, for the addressed rows the
7985
7899
  // server writes from this one commit. It reports a position only for an intent
7986
- // that actually made it into the commit — an ask whose payload was incomplete,
7987
- // or a self-send the committer stripped, contributes no row and so has no place
7988
- // in the line. An intent with no recorded call (the runtime's auto-declared
7989
- // reply) is deliberately absent: the server sorts it last, which is what it is.
7900
+ // that actually made it into the commit — a self-send the committer stripped
7901
+ // contributes no row and so has no place in the line. An intent with no
7902
+ // recorded call (the runtime's auto-declared reply) is deliberately absent, and
7903
+ // the server sorts it after every recorded one it binds the turn's closing
7904
+ // words.
7905
+ //
7906
+ // The ask is absent by design, not by omission: its carrier queues last among
7907
+ // the turn's rows however early the tool fired, so there is no position to
7908
+ // report.
7990
7909
  orderField(fields) {
7991
7910
  const order = {};
7992
7911
  const replyOrder = this.deps.replyState.order;
@@ -7994,14 +7913,14 @@ var TurnCommitter = class {
7994
7913
  if (fields.dispatch !== void 0 && this.deps.sendState.order !== null) {
7995
7914
  order.send = this.deps.sendState.order;
7996
7915
  }
7997
- if (fields.ask !== void 0 && this.deps.askState.order !== null) {
7998
- order.ask = this.deps.askState.order;
7999
- }
8000
7916
  return Object.keys(order).length > 0 ? { turnControlOrder: order } : {};
8001
7917
  }
8002
7918
  // The declared reply: explicit `reply_to` first, else the runtime's own
8003
7919
  // declaration for the turn that just answers — see
8004
- // `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.
8005
7924
  answersField(kind) {
8006
7925
  return resolveDeclaredReplyField({
8007
7926
  kind,
@@ -8010,8 +7929,7 @@ var TurnCommitter = class {
8010
7929
  replyState: this.deps.replyState,
8011
7930
  sendState: this.deps.sendState,
8012
7931
  askState: this.deps.askState,
8013
- wakeState: this.deps.wakeState,
8014
- spawnedSubAgent: this.deps.spawnedSubAgent()
7932
+ wakeState: this.deps.wakeState
8015
7933
  });
8016
7934
  }
8017
7935
  // Carry both halves of the addressed send. The server writes `dispatchBody`
@@ -8132,17 +8050,6 @@ function describeErrorBody(body) {
8132
8050
  if (text.length === 0) return void 0;
8133
8051
  return text.length > ERROR_BODY_LOG_CAP ? `${text.slice(0, ERROR_BODY_LOG_CAP)}\u2026` : text;
8134
8052
  }
8135
- function describeSubAgentError(status2, body) {
8136
- const code = body && typeof body === "object" && "error" in body ? String(body.error) : void 0;
8137
- switch (code) {
8138
- case "dispatch_agent_not_found":
8139
- 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.";
8140
- case "dispatch_ask_requires_dispatch":
8141
- 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.`;
8142
- default:
8143
- return `sub_agent: the spawn failed (${code ?? `HTTP ${status2}`}).`;
8144
- }
8145
- }
8146
8053
  var Dispatcher = class {
8147
8054
  constructor(opts) {
8148
8055
  this.opts = opts;
@@ -8505,36 +8412,10 @@ ${reason}`,
8505
8412
  const wakeState = createWakeState();
8506
8413
  const replyState = createReplyState();
8507
8414
  const controlOrder = createTurnControlOrder();
8508
- let spawnedSubAgent = false;
8509
- const subAgentCreate = async (args) => {
8510
- const dispatchTarget = args.agentId ?? payload.agentId;
8511
- const projectId = turnContext.conversation.projectId;
8512
- const { status: status2, body } = await this.opts.api.createSubAgentConversation(
8513
- workspaceId,
8514
- payload.conversationId,
8515
- turnContext.turnToken ?? this.opts.credential,
8516
- {
8517
- ...projectId ? { projectId } : {},
8518
- ...args.title ? { title: args.title } : {},
8519
- body: args.prompt,
8520
- dispatch: dispatchTarget,
8521
- dispatchAsk: true,
8522
- parentConversationId: payload.conversationId
8523
- }
8524
- );
8525
- if (status2 >= 400) return { ok: false, error: describeSubAgentError(status2, body) };
8526
- const conversationId = body?.conversation?.id;
8527
- if (!conversationId) {
8528
- return { ok: false, error: "sub_agent: the spawn returned no conversation id." };
8529
- }
8530
- spawnedSubAgent = true;
8531
- return { ok: true, conversationId };
8532
- };
8533
8415
  const turnControlServer = createTurnControlMcpServer(
8534
8416
  sendState,
8535
8417
  skipState,
8536
8418
  askState,
8537
- subAgentCreate,
8538
8419
  wakeState,
8539
8420
  replyState,
8540
8421
  controlOrder
@@ -8670,8 +8551,7 @@ ${reason}`,
8670
8551
  replyState,
8671
8552
  // The ledger-derived owed reply, for the runtime's own declaration when
8672
8553
  // the agent doesn't call `reply_to` (resolveDeclaredReplyField).
8673
- owedReplyMessageId: turnContext.owedReplyMessageId ?? null,
8674
- spawnedSubAgent: () => spawnedSubAgent
8554
+ owedReplyMessageId: turnContext.owedReplyMessageId ?? null
8675
8555
  });
8676
8556
  const usesHttpTurnControl = turnRuntime === "codex" || turnRuntime === "opencode";
8677
8557
  let turnControlIntentFetched = false;
@@ -8687,7 +8567,6 @@ ${reason}`,
8687
8567
  );
8688
8568
  if (intent.ask) {
8689
8569
  askState.targetUserId = intent.ask.targetUserId;
8690
- askState.order = intent.askOrder ?? null;
8691
8570
  if (intent.ask.questions && intent.ask.questions.length > 0) {
8692
8571
  askState.questions = intent.ask.questions;
8693
8572
  askState.question = null;
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";
@@ -6753,14 +6711,7 @@ function createSkipState() {
6753
6711
  return { skipped: false, reason: null };
6754
6712
  }
6755
6713
  function createAskState() {
6756
- return {
6757
- targetUserId: null,
6758
- question: null,
6759
- headline: null,
6760
- options: null,
6761
- questions: null,
6762
- order: null
6763
- };
6714
+ return { targetUserId: null, question: null, headline: null, options: null, questions: null };
6764
6715
  }
6765
6716
  function createWakeState() {
6766
6717
  return { afterSeconds: null, at: null, note: null, cancelled: false };
@@ -6775,8 +6726,8 @@ function resolveDeclaredReplyField(input) {
6775
6726
  );
6776
6727
  const askRaised = Boolean(input.askState.targetUserId);
6777
6728
  const wakeArmed = input.wakeState.afterSeconds !== null || input.wakeState.at !== null;
6778
- if (outwardSend || askRaised || wakeArmed || input.spawnedSubAgent) return {};
6779
- return { answersMessageId: input.owedReplyMessageId };
6729
+ if (outwardSend || askRaised || wakeArmed) return {};
6730
+ return { answersMessageId: input.owedReplyMessageId, answersAutoDeclared: true };
6780
6731
  }
6781
6732
  function wakeCommitField(state) {
6782
6733
  if (state.cancelled) return { wake: { cancel: true } };
@@ -6790,7 +6741,7 @@ function wakeCommitField(state) {
6790
6741
  }
6791
6742
  };
6792
6743
  }
6793
- function createTurnControlMcpServer(sendState, skipState, askState, subAgentCreate, wakeState, replyState, controlOrder) {
6744
+ function createTurnControlMcpServer(sendState, skipState, askState, wakeState, replyState, controlOrder) {
6794
6745
  return createSdkMcpServer({
6795
6746
  name: COMPANION_LOCAL_MCP_SERVER,
6796
6747
  version: "0.0.0",
@@ -6906,7 +6857,6 @@ function createTurnControlMcpServer(sendState, skipState, askState, subAgentCrea
6906
6857
  };
6907
6858
  }
6908
6859
  askState.targetUserId = args.targetUserId;
6909
- askState.order = controlOrder?.next() ?? null;
6910
6860
  if (hasArray) {
6911
6861
  askState.questions = args.questions;
6912
6862
  askState.question = null;
@@ -6927,42 +6877,6 @@ function createTurnControlMcpServer(sendState, skipState, askState, subAgentCrea
6927
6877
  { annotations: { readOnlyHint: true, openWorldHint: false }, alwaysLoad: true }
6928
6878
  )
6929
6879
  ] : [],
6930
- ...subAgentCreate ? [
6931
- tool(
6932
- SUB_AGENT_TOOL,
6933
- "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.",
6934
- {
6935
- prompt: z13.string().min(1).max(65536).describe(
6936
- "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."
6937
- ),
6938
- agentId: z13.string().uuid().optional().describe(
6939
- "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."
6940
- ),
6941
- title: z13.string().max(200).optional().describe("Optional title for the child thread (result chips link it).")
6942
- },
6943
- async (args) => {
6944
- const result = await subAgentCreate(args);
6945
- if (!result.ok) {
6946
- return {
6947
- content: [{ type: "text", text: result.error }],
6948
- isError: true
6949
- };
6950
- }
6951
- return {
6952
- content: [
6953
- {
6954
- type: "text",
6955
- text: JSON.stringify({
6956
- conversationId: result.conversationId,
6957
- 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."
6958
- })
6959
- }
6960
- ]
6961
- };
6962
- },
6963
- { annotations: { readOnlyHint: false, openWorldHint: true }, alwaysLoad: true }
6964
- )
6965
- ] : [],
6966
6880
  ...wakeState ? [
6967
6881
  tool(
6968
6882
  WAKE_ME_TOOL,
@@ -7482,10 +7396,15 @@ var TurnCommitter = class {
7482
7396
  }
7483
7397
  // CT1281: the order the control tools were CALLED, for the addressed rows the
7484
7398
  // server writes from this one commit. It reports a position only for an intent
7485
- // that actually made it into the commit — an ask whose payload was incomplete,
7486
- // or a self-send the committer stripped, contributes no row and so has no place
7487
- // in the line. An intent with no recorded call (the runtime's auto-declared
7488
- // reply) is deliberately absent: the server sorts it last, which is what it is.
7399
+ // that actually made it into the commit — a self-send the committer stripped
7400
+ // contributes no row and so has no place in the line. An intent with no
7401
+ // recorded call (the runtime's auto-declared reply) is deliberately absent, and
7402
+ // the server sorts it after every recorded one it binds the turn's closing
7403
+ // words.
7404
+ //
7405
+ // The ask is absent by design, not by omission: its carrier queues last among
7406
+ // the turn's rows however early the tool fired, so there is no position to
7407
+ // report.
7489
7408
  orderField(fields) {
7490
7409
  const order = {};
7491
7410
  const replyOrder = this.deps.replyState.order;
@@ -7493,14 +7412,14 @@ var TurnCommitter = class {
7493
7412
  if (fields.dispatch !== void 0 && this.deps.sendState.order !== null) {
7494
7413
  order.send = this.deps.sendState.order;
7495
7414
  }
7496
- if (fields.ask !== void 0 && this.deps.askState.order !== null) {
7497
- order.ask = this.deps.askState.order;
7498
- }
7499
7415
  return Object.keys(order).length > 0 ? { turnControlOrder: order } : {};
7500
7416
  }
7501
7417
  // The declared reply: explicit `reply_to` first, else the runtime's own
7502
7418
  // declaration for the turn that just answers — see
7503
- // `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.
7504
7423
  answersField(kind) {
7505
7424
  return resolveDeclaredReplyField({
7506
7425
  kind,
@@ -7509,8 +7428,7 @@ var TurnCommitter = class {
7509
7428
  replyState: this.deps.replyState,
7510
7429
  sendState: this.deps.sendState,
7511
7430
  askState: this.deps.askState,
7512
- wakeState: this.deps.wakeState,
7513
- spawnedSubAgent: this.deps.spawnedSubAgent()
7431
+ wakeState: this.deps.wakeState
7514
7432
  });
7515
7433
  }
7516
7434
  // Carry both halves of the addressed send. The server writes `dispatchBody`
@@ -7631,17 +7549,6 @@ function describeErrorBody(body) {
7631
7549
  if (text.length === 0) return void 0;
7632
7550
  return text.length > ERROR_BODY_LOG_CAP ? `${text.slice(0, ERROR_BODY_LOG_CAP)}\u2026` : text;
7633
7551
  }
7634
- function describeSubAgentError(status, body) {
7635
- const code = body && typeof body === "object" && "error" in body ? String(body.error) : void 0;
7636
- switch (code) {
7637
- case "dispatch_agent_not_found":
7638
- 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.";
7639
- case "dispatch_ask_requires_dispatch":
7640
- 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.`;
7641
- default:
7642
- return `sub_agent: the spawn failed (${code ?? `HTTP ${status}`}).`;
7643
- }
7644
- }
7645
7552
  var Dispatcher = class {
7646
7553
  constructor(opts) {
7647
7554
  this.opts = opts;
@@ -8004,36 +7911,10 @@ ${reason}`,
8004
7911
  const wakeState = createWakeState();
8005
7912
  const replyState = createReplyState();
8006
7913
  const controlOrder = createTurnControlOrder();
8007
- let spawnedSubAgent = false;
8008
- const subAgentCreate = async (args) => {
8009
- const dispatchTarget = args.agentId ?? payload.agentId;
8010
- const projectId = turnContext.conversation.projectId;
8011
- const { status, body } = await this.opts.api.createSubAgentConversation(
8012
- workspaceId,
8013
- payload.conversationId,
8014
- turnContext.turnToken ?? this.opts.credential,
8015
- {
8016
- ...projectId ? { projectId } : {},
8017
- ...args.title ? { title: args.title } : {},
8018
- body: args.prompt,
8019
- dispatch: dispatchTarget,
8020
- dispatchAsk: true,
8021
- parentConversationId: payload.conversationId
8022
- }
8023
- );
8024
- if (status >= 400) return { ok: false, error: describeSubAgentError(status, body) };
8025
- const conversationId = body?.conversation?.id;
8026
- if (!conversationId) {
8027
- return { ok: false, error: "sub_agent: the spawn returned no conversation id." };
8028
- }
8029
- spawnedSubAgent = true;
8030
- return { ok: true, conversationId };
8031
- };
8032
7914
  const turnControlServer = createTurnControlMcpServer(
8033
7915
  sendState,
8034
7916
  skipState,
8035
7917
  askState,
8036
- subAgentCreate,
8037
7918
  wakeState,
8038
7919
  replyState,
8039
7920
  controlOrder
@@ -8169,8 +8050,7 @@ ${reason}`,
8169
8050
  replyState,
8170
8051
  // The ledger-derived owed reply, for the runtime's own declaration when
8171
8052
  // the agent doesn't call `reply_to` (resolveDeclaredReplyField).
8172
- owedReplyMessageId: turnContext.owedReplyMessageId ?? null,
8173
- spawnedSubAgent: () => spawnedSubAgent
8053
+ owedReplyMessageId: turnContext.owedReplyMessageId ?? null
8174
8054
  });
8175
8055
  const usesHttpTurnControl = turnRuntime === "codex" || turnRuntime === "opencode";
8176
8056
  let turnControlIntentFetched = false;
@@ -8186,7 +8066,6 @@ ${reason}`,
8186
8066
  );
8187
8067
  if (intent.ask) {
8188
8068
  askState.targetUserId = intent.ask.targetUserId;
8189
- askState.order = intent.askOrder ?? null;
8190
8069
  if (intent.ask.questions && intent.ask.questions.length > 0) {
8191
8070
  askState.questions = intent.ask.questions;
8192
8071
  askState.question = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.65",
3
+ "version": "0.6.67",
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",