@cabane/companion 0.6.61 → 0.6.63

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 +162 -349
  2. package/dist/runtime.js +130 -340
  3. package/package.json +1 -1
package/dist/runtime.js CHANGED
@@ -2560,6 +2560,15 @@ var turnEventSchema = z5.discriminatedUnion("type", [
2560
2560
  effort: z5.string().optional(),
2561
2561
  thinking: z5.string().optional(),
2562
2562
  reasoningEffort: z5.string().optional()
2563
+ }).optional(),
2564
+ // CT1275: the harness-reported MCP inventory from this turn's init frame.
2565
+ // This is deliberately diagnostic-only: names, statuses and a count, never
2566
+ // server definitions, credentials or session ids. `initReceived:false`
2567
+ // distinguishes a missing init frame from a real empty inventory.
2568
+ mcpInventory: z5.object({
2569
+ initReceived: z5.boolean(),
2570
+ servers: z5.array(z5.object({ name: z5.string(), status: z5.string() })),
2571
+ toolCount: z5.number().int().nonnegative()
2563
2572
  }).optional()
2564
2573
  })
2565
2574
  ]);
@@ -2578,6 +2587,7 @@ var turnResultReasonSchema = z6.discriminatedUnion("kind", [
2578
2587
  z6.object({ kind: z6.literal("timeout_total") }),
2579
2588
  z6.object({ kind: z6.literal("cancelled") }),
2580
2589
  z6.object({ kind: z6.literal("skipped") }),
2590
+ z6.object({ kind: z6.literal("workspace_tools_missing") }),
2581
2591
  z6.object({ kind: z6.literal("runtime_error") })
2582
2592
  ]);
2583
2593
  var turnOutcomes = ["success", "failure", "cancelled", "skipped"];
@@ -2603,7 +2613,12 @@ var turnDiagnosticsSchema = z6.object({
2603
2613
  result: z6.number().int().nonnegative()
2604
2614
  }),
2605
2615
  runtimeResultKind: z6.enum(turnRuntimeResultKinds).nullable(),
2606
- finalSource: z6.enum(turnFinalSources)
2616
+ finalSource: z6.enum(turnFinalSources),
2617
+ mcpInventory: z6.object({
2618
+ initReceived: z6.boolean(),
2619
+ servers: z6.array(z6.object({ name: z6.string(), status: z6.string() })),
2620
+ toolCount: z6.number().int().nonnegative()
2621
+ }).optional()
2607
2622
  });
2608
2623
 
2609
2624
  // packages/agent-runtime/src/failure.ts
@@ -2737,6 +2752,8 @@ function normalizeTurnResultReason(reason) {
2737
2752
  return { kind: "cancelled" };
2738
2753
  case "skipped":
2739
2754
  return { kind: "skipped" };
2755
+ case "workspace_tools_missing":
2756
+ return { kind: "workspace_tools_missing" };
2740
2757
  default:
2741
2758
  return { kind: "runtime_error" };
2742
2759
  }
@@ -2841,12 +2858,7 @@ var turnRequestSchema = z8.object({
2841
2858
  // adapters' conformance fixtures, tests) keeps parsing unchanged — the companion
2842
2859
  // always populates it (`build-options.ts`), and the native adapter fails the
2843
2860
  // turn loudly when it is somehow absent rather than guessing.
2844
- workspaceId: z8.string().optional(),
2845
- // CT752: the server-resolved workspace surface this credential exposes.
2846
- // Readiness uses this explicit fact to require `sdk` for code mode and the
2847
- // granular floor for classic mode; inventory contents alone cannot infer it
2848
- // because `sdk` is intentionally also available on the classic surface.
2849
- workspaceToolSurface: z8.enum(["code", "classic"]).optional()
2861
+ workspaceId: z8.string().optional()
2850
2862
  }),
2851
2863
  // Machine-local resolution (host-filled): the checkout cwd, extra env from a
2852
2864
  // prepare hook, and the resolved user MCP servers.
@@ -3431,6 +3443,8 @@ async function* decodeSdkStream(iter, ctx) {
3431
3443
  out.push(event);
3432
3444
  };
3433
3445
  let sessionEmitted = false;
3446
+ let workspaceProven = false;
3447
+ let mcpInventory = { initReceived: false, servers: [], toolCount: 0 };
3434
3448
  let ok = false;
3435
3449
  let resultReason;
3436
3450
  let sawResult = false;
@@ -3448,6 +3462,12 @@ async function* decodeSdkStream(iter, ctx) {
3448
3462
  if (msg.type === "system" && msg.subtype === "init") {
3449
3463
  const initModel = msg.model;
3450
3464
  if (typeof initModel === "string" && initModel.length > 0) resolvedModel = initModel;
3465
+ mcpInventory = readMcpInventory(msg);
3466
+ workspaceProven = provesWorkspaceTools(msg);
3467
+ if (!workspaceProven) {
3468
+ resultReason = "workspace_tools_missing";
3469
+ break;
3470
+ }
3451
3471
  const sdkSessionId = msg.session_id;
3452
3472
  if (!sessionEmitted && sdkSessionId && sdkSessionId !== ctx.resumedSessionId) {
3453
3473
  sessionEmitted = true;
@@ -3457,6 +3477,9 @@ async function* decodeSdkStream(iter, ctx) {
3457
3477
  ...ctx.degraded ? { degraded: true } : {}
3458
3478
  };
3459
3479
  }
3480
+ } else if (!workspaceProven && (msg.type === "assistant" || msg.type === "user" || msg.type === "result")) {
3481
+ resultReason = "workspace_tools_missing";
3482
+ break;
3460
3483
  } else if (msg.type === "assistant") {
3461
3484
  const assistantErr = msg.error;
3462
3485
  if (typeof assistantErr === "string" && assistantErr.length > 0) {
@@ -3520,6 +3543,10 @@ async function* decodeSdkStream(iter, ctx) {
3520
3543
  sawResult = true;
3521
3544
  }
3522
3545
  if (ctx.signal.aborted) return;
3546
+ if (!workspaceProven) {
3547
+ ok = false;
3548
+ resultReason ??= "workspace_tools_missing";
3549
+ }
3523
3550
  await flushHeldText(buffer, emit, ok);
3524
3551
  yield* drain(out);
3525
3552
  if (!ok && !resultReason && !sawResult) resultReason = "no_result";
@@ -3528,9 +3555,25 @@ async function* decodeSdkStream(iter, ctx) {
3528
3555
  ok,
3529
3556
  ...resultReason ? { reason: resultReason } : {},
3530
3557
  ...usage ? { usage } : {},
3531
- ...resolvedModel ? { resolvedModel } : {}
3558
+ ...resolvedModel ? { resolvedModel } : {},
3559
+ mcpInventory
3560
+ };
3561
+ }
3562
+ function readMcpInventory(msg) {
3563
+ const frame = msg;
3564
+ const servers = Array.isArray(frame.mcp_servers) ? frame.mcp_servers.flatMap(
3565
+ (server) => typeof server?.name === "string" && typeof server.status === "string" ? [{ name: server.name, status: server.status }] : []
3566
+ ) : [];
3567
+ return {
3568
+ initReceived: true,
3569
+ servers,
3570
+ toolCount: Array.isArray(frame.tools) ? frame.tools.length : 0
3532
3571
  };
3533
3572
  }
3573
+ function provesWorkspaceTools(msg) {
3574
+ const tools = msg.tools;
3575
+ return Array.isArray(tools) && tools.includes("mcp__cabane__sdk");
3576
+ }
3534
3577
  function isSubscriptionWindow(value) {
3535
3578
  return value === "five_hour" || value === "seven_day" || value === "seven_day_opus" || value === "seven_day_sonnet" || value === "overage";
3536
3579
  }
@@ -3629,6 +3672,11 @@ var CABANE_POLICY = {
3629
3672
  uiPrompts: "never"
3630
3673
  };
3631
3674
  var CWD = "/env/here";
3675
+ var HEALTHY_MCP_INVENTORY = {
3676
+ initReceived: true,
3677
+ servers: [{ name: "cabane", status: "connected" }],
3678
+ toolCount: 1
3679
+ };
3632
3680
  function makeRequest(overrides = {}) {
3633
3681
  return {
3634
3682
  systemPrompt: "system",
@@ -3649,7 +3697,8 @@ var init = (sessionId, model) => ({
3649
3697
  type: "system",
3650
3698
  subtype: "init",
3651
3699
  session_id: sessionId,
3652
- mcp_servers: [],
3700
+ mcp_servers: HEALTHY_MCP_INVENTORY.servers,
3701
+ tools: ["mcp__cabane__sdk"],
3653
3702
  ...model ? { model } : {}
3654
3703
  });
3655
3704
  var assistantText = (text) => ({
@@ -3710,7 +3759,7 @@ var sessionEvent = (sdkSessionId, cwd = CWD, degraded = false) => ({
3710
3759
  state: encodeSession({ sdkSessionId, cwd }),
3711
3760
  ...degraded ? { degraded: true } : {}
3712
3761
  });
3713
- var CLAUDE_CODE_CONFORMANCE_FIXTURES = [
3762
+ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
3714
3763
  {
3715
3764
  // A plain text reply: the held text-only block flushes as the terminal final.
3716
3765
  name: "clean turn",
@@ -4090,6 +4139,12 @@ var CLAUDE_CODE_CONFORMANCE_FIXTURES = [
4090
4139
  ]
4091
4140
  }
4092
4141
  ];
4142
+ var CLAUDE_CODE_CONFORMANCE_FIXTURES = BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES.map((fixture) => ({
4143
+ ...fixture,
4144
+ expected: fixture.expected.map(
4145
+ (event) => event.type === "result" ? { ...event, mcpInventory: HEALTHY_MCP_INVENTORY } : event
4146
+ )
4147
+ }));
4093
4148
 
4094
4149
  // packages/agent-runtime/src/registry.ts
4095
4150
  function createAdapterRegistry(adapters) {
@@ -6595,7 +6650,7 @@ var ConnectorHealthStore = class {
6595
6650
 
6596
6651
  // src/dispatcher.ts
6597
6652
  import { createHash as createHash2, randomUUID } from "crypto";
6598
- import { appendFileSync as appendFileSync2, existsSync as existsSync10, mkdirSync as mkdirSync10, readdirSync as readdirSync2, statSync } from "fs";
6653
+ import { existsSync as existsSync10, readdirSync as readdirSync2, statSync } from "fs";
6599
6654
  import { join as join14 } from "path";
6600
6655
 
6601
6656
  // src/turn-control-tools.ts
@@ -6617,16 +6672,32 @@ var WAKE_ME_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WAKE_ME_TOOL}`;
6617
6672
  var CANCEL_WAKE_TOOL = "cancel_wake";
6618
6673
  var CANCEL_WAKE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${CANCEL_WAKE_TOOL}`;
6619
6674
  function createReplyState() {
6620
- return { answersMessageId: null };
6675
+ return { answersMessageId: null, order: null };
6621
6676
  }
6622
6677
  function createSendState() {
6623
- return { agentId: null, message: null };
6678
+ return { agentId: null, message: null, order: null };
6679
+ }
6680
+ function createTurnControlOrder() {
6681
+ let calls = 0;
6682
+ return {
6683
+ next: () => {
6684
+ calls += 1;
6685
+ return calls;
6686
+ }
6687
+ };
6624
6688
  }
6625
6689
  function createSkipState() {
6626
6690
  return { skipped: false, reason: null };
6627
6691
  }
6628
6692
  function createAskState() {
6629
- return { targetUserId: null, question: null, headline: null, options: null, questions: null };
6693
+ return {
6694
+ targetUserId: null,
6695
+ question: null,
6696
+ headline: null,
6697
+ options: null,
6698
+ questions: null,
6699
+ order: null
6700
+ };
6630
6701
  }
6631
6702
  function createWakeState() {
6632
6703
  return { afterSeconds: null, at: null, note: null, cancelled: false };
@@ -6656,7 +6727,7 @@ function wakeCommitField(state) {
6656
6727
  }
6657
6728
  };
6658
6729
  }
6659
- function createTurnControlMcpServer(sendState, skipState, askState, subAgentCreate, wakeState, replyState) {
6730
+ function createTurnControlMcpServer(sendState, skipState, askState, subAgentCreate, wakeState, replyState, controlOrder) {
6660
6731
  return createSdkMcpServer({
6661
6732
  name: COMPANION_LOCAL_MCP_SERVER,
6662
6733
  version: "0.0.0",
@@ -6673,6 +6744,7 @@ function createTurnControlMcpServer(sendState, skipState, askState, subAgentCrea
6673
6744
  async (args) => {
6674
6745
  sendState.agentId = args.agentId;
6675
6746
  sendState.message = args.message;
6747
+ sendState.order = controlOrder?.next() ?? null;
6676
6748
  return {
6677
6749
  content: [{ type: "text", text: JSON.stringify({ sent: args.agentId }) }]
6678
6750
  };
@@ -6688,6 +6760,7 @@ function createTurnControlMcpServer(sendState, skipState, askState, subAgentCrea
6688
6760
  },
6689
6761
  async (args) => {
6690
6762
  replyState.answersMessageId = args.messageId;
6763
+ replyState.order = controlOrder?.next() ?? null;
6691
6764
  return {
6692
6765
  content: [
6693
6766
  {
@@ -6770,6 +6843,7 @@ function createTurnControlMcpServer(sendState, skipState, askState, subAgentCrea
6770
6843
  };
6771
6844
  }
6772
6845
  askState.targetUserId = args.targetUserId;
6846
+ askState.order = controlOrder?.next() ?? null;
6773
6847
  if (hasArray) {
6774
6848
  askState.questions = args.questions;
6775
6849
  askState.question = null;
@@ -6932,7 +7006,6 @@ function buildCompanionTurnRequest(params) {
6932
7006
  bearer: params.turnToken ?? params.agentPat,
6933
7007
  activeConversationId: params.activeConversationId,
6934
7008
  workspaceId: params.workspaceId,
6935
- ...t.workspaceToolSurface ? { workspaceToolSurface: t.workspaceToolSurface } : {},
6936
7009
  // CT714: mount the turn-control surface ONLY when a real turn token backs
6937
7010
  // this turn — the surface admits `turn_token` auth exclusively, so a
6938
7011
  // PAT-fallback bearer would be rejected there. Absent it, external adapters
@@ -7334,12 +7407,31 @@ var TurnCommitter = class {
7334
7407
  // a wordless turn has no answer to bind, and the server's mute-settle
7335
7408
  // notice speaks for it.
7336
7409
  turnControlFields(kind) {
7337
- return {
7410
+ const fields = {
7338
7411
  ...this.answersField(kind),
7339
7412
  ...this.sendField(),
7340
7413
  ...this.askField(),
7341
7414
  ...this.wakeField()
7342
7415
  };
7416
+ return { ...fields, ...this.orderField(fields) };
7417
+ }
7418
+ // CT1281: the order the control tools were CALLED, for the addressed rows the
7419
+ // server writes from this one commit. It reports a position only for an intent
7420
+ // that actually made it into the commit — an ask whose payload was incomplete,
7421
+ // or a self-send the committer stripped, contributes no row and so has no place
7422
+ // in the line. An intent with no recorded call (the runtime's auto-declared
7423
+ // reply) is deliberately absent: the server sorts it last, which is what it is.
7424
+ orderField(fields) {
7425
+ const order = {};
7426
+ const replyOrder = this.deps.replyState.order;
7427
+ if (fields.answersMessageId !== void 0 && replyOrder !== null) order.reply = replyOrder;
7428
+ if (fields.dispatch !== void 0 && this.deps.sendState.order !== null) {
7429
+ order.send = this.deps.sendState.order;
7430
+ }
7431
+ if (fields.ask !== void 0 && this.deps.askState.order !== null) {
7432
+ order.ask = this.deps.askState.order;
7433
+ }
7434
+ return Object.keys(order).length > 0 ? { turnControlOrder: order } : {};
7343
7435
  }
7344
7436
  // The declared reply: explicit `reply_to` first, else the runtime's own
7345
7437
  // declaration for the turn that just answers — see
@@ -7408,173 +7500,6 @@ var TurnCommitter = class {
7408
7500
  }
7409
7501
  };
7410
7502
 
7411
- // src/workspace-readiness.ts
7412
- var CLASSIC_REQUIRED = ["read", "list", "search", "write", "edit"];
7413
- var INITIALIZE_RETRY_DELAYS_MS = [250, 750, 1500];
7414
- var INITIALIZE_ATTEMPT_TIMEOUT_MS = 4e3;
7415
- var INITIALIZE_RETRY_BUDGET_MS = 1e4;
7416
- async function proveWorkspaceTools(req, runtime, opts = {}) {
7417
- const base = {
7418
- ok: false,
7419
- proofType: "authenticated_mcp_tools_list",
7420
- runtime,
7421
- harnessFingerprint: opts.harnessFingerprint ?? runtime,
7422
- endpoint: safeEndpoint(req.cabane.mcpUrl),
7423
- initialized: false,
7424
- authenticated: false,
7425
- discoveredTools: [],
7426
- requiredTools: [],
7427
- acceptedNames: ["sdk", "mcp__cabane__sdk"],
7428
- failedCapability: null,
7429
- detail: null
7430
- };
7431
- if (!req.cabane.mcpUrl) return fail(base, "server_not_configured", "Cabane MCP URL absent");
7432
- if (!req.cabane.bearer) return fail(base, "authentication_failed", "Cabane bearer absent");
7433
- const fetchImpl = opts.fetchImpl ?? fetch;
7434
- const headers = {
7435
- authorization: `Bearer ${req.cabane.bearer}`,
7436
- accept: "application/json, text/event-stream",
7437
- "content-type": "application/json",
7438
- "x-cabane-active-conversation": req.cabane.activeConversationId
7439
- };
7440
- try {
7441
- const initialized = await initializeWithRetry(
7442
- fetchImpl,
7443
- req.cabane.mcpUrl,
7444
- headers,
7445
- {
7446
- jsonrpc: "2.0",
7447
- id: 1,
7448
- method: "initialize",
7449
- params: {
7450
- protocolVersion: "2025-03-26",
7451
- capabilities: {},
7452
- clientInfo: { name: "cabane-companion-readiness", version: "1" }
7453
- }
7454
- },
7455
- opts.retryDelaysMs ?? INITIALIZE_RETRY_DELAYS_MS
7456
- );
7457
- if (initialized.status === 401 || initialized.status === 403)
7458
- return fail(base, "authentication_failed", `initialize returned HTTP ${initialized.status}`);
7459
- if (initialized.transient)
7460
- return fail(base, "workspace_endpoint_unreachable", initialized.detail);
7461
- if (!initialized.ok) return fail(base, "initialization_failed", initialized.detail);
7462
- base.initialized = true;
7463
- base.authenticated = true;
7464
- if (initialized.sessionId) headers["mcp-session-id"] = initialized.sessionId;
7465
- const listed = await rpc(fetchImpl, req.cabane.mcpUrl, headers, {
7466
- jsonrpc: "2.0",
7467
- id: 2,
7468
- method: "tools/list",
7469
- params: {}
7470
- });
7471
- if (listed.status === 401 || listed.status === 403)
7472
- return fail(base, "authentication_failed", `tools/list returned HTTP ${listed.status}`);
7473
- if (!listed.ok) return fail(base, "tool_discovery_failed", listed.detail);
7474
- const result = asRecord3(asRecord3(listed.value)?.result);
7475
- const tools = Array.isArray(result?.tools) ? result.tools : null;
7476
- if (!tools) return fail(base, "tool_discovery_failed", "tools/list returned no tool inventory");
7477
- base.discoveredTools = tools.map(
7478
- (tool2) => tool2 && typeof tool2 === "object" && typeof tool2.name === "string" ? tool2.name : null
7479
- ).filter((name) => name !== null).sort();
7480
- if (!req.cabane.workspaceToolSurface)
7481
- return fail(base, "required_tool_missing", "resolved workspace tool surface absent");
7482
- base.requiredTools = req.cabane.workspaceToolSurface === "code" ? ["sdk"] : CLASSIC_REQUIRED;
7483
- const missing = base.requiredTools.filter((name) => !base.discoveredTools.includes(name));
7484
- if (missing.length > 0)
7485
- return fail(
7486
- base,
7487
- "required_tool_missing",
7488
- `missing initialized tools: ${missing.join(", ")}`
7489
- );
7490
- base.ok = true;
7491
- return base;
7492
- } catch (error) {
7493
- return fail(
7494
- base,
7495
- "workspace_endpoint_unreachable",
7496
- error instanceof Error ? error.message : String(error)
7497
- );
7498
- }
7499
- }
7500
- async function initializeWithRetry(fetchImpl, url, headers, body, retryDelaysMs) {
7501
- let lastFailure = null;
7502
- const deadline = Date.now() + INITIALIZE_RETRY_BUDGET_MS;
7503
- for (let attempt = 0; attempt <= retryDelaysMs.length; attempt += 1) {
7504
- try {
7505
- const remainingMs = deadline - Date.now();
7506
- if (remainingMs <= 0) break;
7507
- const result = await rpc(
7508
- fetchImpl,
7509
- url,
7510
- headers,
7511
- body,
7512
- Math.min(INITIALIZE_ATTEMPT_TIMEOUT_MS, remainingMs)
7513
- );
7514
- if (result.status === 401 || result.status === 403 || result.ok) return result;
7515
- if (result.status < 500) return result;
7516
- lastFailure = { ...result, transient: true };
7517
- } catch (error) {
7518
- lastFailure = {
7519
- ok: false,
7520
- status: 0,
7521
- sessionId: null,
7522
- value: null,
7523
- detail: error instanceof Error ? error.message : String(error),
7524
- transient: true
7525
- };
7526
- }
7527
- const retryDelayMs = retryDelaysMs[attempt];
7528
- if (retryDelayMs === void 0 || Date.now() + retryDelayMs >= deadline) break;
7529
- await delay(retryDelayMs);
7530
- }
7531
- return lastFailure;
7532
- }
7533
- function delay(ms) {
7534
- return new Promise((resolve) => setTimeout(resolve, ms));
7535
- }
7536
- function fail(proof, capability, detail) {
7537
- proof.failedCapability = capability;
7538
- proof.detail = detail.slice(0, 300);
7539
- return proof;
7540
- }
7541
- function safeEndpoint(value) {
7542
- try {
7543
- const url = new URL(value);
7544
- return `${url.origin}${url.pathname}`;
7545
- } catch {
7546
- return null;
7547
- }
7548
- }
7549
- async function rpc(fetchImpl, url, headers, body, timeoutMs) {
7550
- const response = await fetchImpl(url, {
7551
- method: "POST",
7552
- headers,
7553
- body: JSON.stringify(body),
7554
- ...timeoutMs ? { signal: AbortSignal.timeout(timeoutMs) } : {}
7555
- });
7556
- const text = await response.text();
7557
- const value = parseRpcBody(text);
7558
- return {
7559
- ok: response.ok && !!value && !value.error,
7560
- status: response.status,
7561
- sessionId: response.headers.get("mcp-session-id"),
7562
- value,
7563
- detail: typeof asRecord3(value?.error)?.message === "string" ? String(asRecord3(value?.error)?.message) : `HTTP ${response.status}`
7564
- };
7565
- }
7566
- function parseRpcBody(text) {
7567
- const trimmed = text.trim();
7568
- if (trimmed.startsWith("{")) return JSON.parse(trimmed);
7569
- for (const line of trimmed.split("\n")) {
7570
- if (line.startsWith("data:")) return JSON.parse(line.slice(5).trim());
7571
- }
7572
- return null;
7573
- }
7574
- function asRecord3(value) {
7575
- return value !== null && typeof value === "object" ? value : null;
7576
- }
7577
-
7578
7503
  // src/dispatcher.ts
7579
7504
  var PREPARING_TOOL_NAME = "preparing";
7580
7505
  var PREPARE_FAILED_PREFIX = "**Couldn't prepare your environment.** I wasn't able to provision a working directory for this conversation, so I can't run this turn. The provisioning command reported:";
@@ -7965,6 +7890,7 @@ ${reason}`,
7965
7890
  const askState = createAskState();
7966
7891
  const wakeState = createWakeState();
7967
7892
  const replyState = createReplyState();
7893
+ const controlOrder = createTurnControlOrder();
7968
7894
  let spawnedSubAgent = false;
7969
7895
  const subAgentCreate = async (args) => {
7970
7896
  const dispatchTarget = args.agentId ?? payload.agentId;
@@ -7996,7 +7922,8 @@ ${reason}`,
7996
7922
  askState,
7997
7923
  subAgentCreate,
7998
7924
  wakeState,
7999
- replyState
7925
+ replyState,
7926
+ controlOrder
8000
7927
  );
8001
7928
  const request = buildCompanionTurnRequest({
8002
7929
  turnContext,
@@ -8067,152 +7994,7 @@ ${reason}`,
8067
7994
  `runtime_unavailable:${err.runtime}`
8068
7995
  );
8069
7996
  }
8070
- let turnReceiptPath = null;
8071
7997
  const totalTimeoutMs = this.opts.totalTimeoutMs ?? DEFAULT_AGENT_TOTAL_TIMEOUT_MS;
8072
- const closeTurnReceipt = (ok, reason) => {
8073
- if (!turnReceiptPath) return;
8074
- const target = turnReceiptPath;
8075
- turnReceiptPath = null;
8076
- try {
8077
- appendFileSync2(
8078
- target,
8079
- `${JSON.stringify({
8080
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8081
- event: "settled",
8082
- turnId,
8083
- agentId: payload.agentId,
8084
- conversationId: payload.conversationId,
8085
- ok,
8086
- reason
8087
- })}
8088
- `,
8089
- { mode: 384 }
8090
- );
8091
- } catch (error) {
8092
- turnLog.warn(
8093
- { err: error instanceof Error ? error.message : String(error) },
8094
- "dispatcher: turn-settled diagnostic write failed"
8095
- );
8096
- }
8097
- };
8098
- if (prepareHook && hookEnv?.CABANE_TASK_ID) {
8099
- const checkout = checkoutState(effectiveCwd);
8100
- if (!effectiveCwd || !checkout.ok) {
8101
- const reason = `checkout_missing: ${checkout.reason}; task=${hookEnv.CABANE_TASK_ID}; recovery=re-dispatch this conversation (the prepare hook re-provisions the environment)`;
8102
- turnLog.error({ checkout: effectiveCwd ?? null, checkoutState: checkout }, reason);
8103
- try {
8104
- await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
8105
- body: `**Couldn't prepare your environment.** ${reason}`,
8106
- kind: "final",
8107
- turnId,
8108
- parentMessageId: payload.messageId
8109
- });
8110
- } catch (postErr) {
8111
- turnLog.warn(
8112
- { err: postErr instanceof Error ? postErr.message : String(postErr) },
8113
- "dispatcher: checkout-missing notice post failed"
8114
- );
8115
- }
8116
- return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
8117
- }
8118
- const receiptPath = join14(effectiveCwd, ".git", "cabane", "readiness.jsonl");
8119
- const receiptLine = (fields) => `${JSON.stringify({
8120
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8121
- taskId: hookEnv.CABANE_TASK_ID,
8122
- binding: hookEnv.CABANE_TASK_BINDING ?? null,
8123
- checkout: effectiveCwd,
8124
- // CT1022: the two fields the environment reaper reads — which turn this
8125
- // is (so its settle can be matched among interleaved agents) and how long
8126
- // it may legitimately run (so an unclosed receipt expires on this turn's
8127
- // real deadline, not the reaper's guess).
8128
- turnId,
8129
- totalTimeoutMs,
8130
- // CT1062: WHOSE turn. A task env is shared — between agents, and between
8131
- // an agent's own sequential conversations — so a reader asking "is a turn
8132
- // of THIS agent, other than mine, running here?" (the host's run-lock
8133
- // does, before it lets a second conversation into the tree) can only
8134
- // answer it if the line says who. An unattributed open line has to count
8135
- // for everyone, which refuses work that should have been admitted.
8136
- agentId: payload.agentId,
8137
- conversationId: payload.conversationId,
8138
- ...fields
8139
- })}
8140
- `;
8141
- try {
8142
- mkdirSync10(join14(effectiveCwd, ".git", "cabane"), { recursive: true });
8143
- appendFileSync2(
8144
- receiptPath,
8145
- // `starting` is the honest classification before the proof has run. The
8146
- // line the proof appends below carries the same `turnId`, so a reader
8147
- // replaying the file sees one turn, not two.
8148
- receiptLine({ classification: "starting" }),
8149
- { mode: 384 }
8150
- );
8151
- turnReceiptPath = receiptPath;
8152
- } catch (error) {
8153
- const detail = error instanceof Error ? error.message : String(error);
8154
- const reason = `turn_receipt_unwritable: ${detail}; task=${hookEnv.CABANE_TASK_ID}; checkout=${effectiveCwd}; recovery=restore write access to the checkout's .git/cabane, then re-dispatch`;
8155
- turnLog.error({ err: detail, receiptPath }, reason);
8156
- try {
8157
- await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
8158
- body: `**Couldn't prepare your environment.** ${reason}`,
8159
- kind: "final",
8160
- turnId,
8161
- parentMessageId: payload.messageId
8162
- });
8163
- } catch (postErr) {
8164
- turnLog.warn(
8165
- { err: postErr instanceof Error ? postErr.message : String(postErr) },
8166
- "dispatcher: turn-receipt failure notice post failed"
8167
- );
8168
- }
8169
- return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
8170
- }
8171
- const proof = await proveWorkspaceTools(request, adapter.name, {
8172
- ...this.opts.workspaceProofFetch ? { fetchImpl: this.opts.workspaceProofFetch } : {},
8173
- ...this.opts.workspaceProofRetryDelaysMs ? { retryDelaysMs: this.opts.workspaceProofRetryDelaysMs } : {},
8174
- harnessFingerprint: turnContext.runtime
8175
- });
8176
- turnLog[proof.ok ? "info" : "error"](
8177
- { workspaceProof: proof, checkout: effectiveCwd },
8178
- `dispatcher: workspace tool proof ${proof.ok ? "passed" : "failed"}, checkout usable`
8179
- );
8180
- try {
8181
- appendFileSync2(
8182
- receiptPath,
8183
- receiptLine({
8184
- classification: proof.ok ? "ready" : "workspace_tools_missing",
8185
- failedCapability: proof.failedCapability,
8186
- workspaceTools: proof
8187
- }),
8188
- { mode: 384 }
8189
- );
8190
- } catch (error) {
8191
- turnLog.warn(
8192
- { err: error instanceof Error ? error.message : String(error) },
8193
- "dispatcher: workspace-proof diagnostic write failed (the turn receipt is open)"
8194
- );
8195
- }
8196
- if (!proof.ok) {
8197
- const recovery = proof.failedCapability === "workspace_endpoint_unreachable" ? "the Cabane workspace endpoint was unreachable; retry this dispatch" : "restart the connector after restoring the Cabane workspace tool mount";
8198
- const reason = `workspace_tools_missing: ${proof.failedCapability}; checkout=${effectiveCwd}; runtime=${adapter.name}; recovery=${recovery}`;
8199
- closeTurnReceipt(false, reason);
8200
- try {
8201
- await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
8202
- body: `**Couldn't prepare your environment.** ${reason}`,
8203
- kind: "final",
8204
- turnId,
8205
- parentMessageId: payload.messageId
8206
- });
8207
- } catch (postErr) {
8208
- turnLog.warn(
8209
- { err: postErr instanceof Error ? postErr.message : String(postErr) },
8210
- "dispatcher: workspace-proof failure notice post failed"
8211
- );
8212
- }
8213
- return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
8214
- }
8215
- }
8216
7998
  const transcript = this.opts.transcriptDir ? new TranscriptWriter(
8217
7999
  this.opts.transcriptDir,
8218
8000
  {
@@ -8235,6 +8017,7 @@ ${reason}`,
8235
8017
  let turnUsage;
8236
8018
  let turnResolvedModel;
8237
8019
  let turnResolvedConfig;
8020
+ let turnMcpInventory;
8238
8021
  const eventCounts = {
8239
8022
  session: 0,
8240
8023
  text: 0,
@@ -8259,11 +8042,12 @@ ${reason}`,
8259
8042
  signal: abortController.signal,
8260
8043
  log: turnLog,
8261
8044
  nextSeq,
8262
- // The committer reads this at commit to attach the addressed send onto the
8263
- // turn's `final` row.
8045
+ // The committer reads this at commit to carry the addressed send on the
8046
+ // terminal row; the server writes the send itself as a distinct message.
8264
8047
  sendState,
8265
- // CT326: likewise the ask payload attached to the `final` row so the
8266
- // server creates the `asks` row atomically with the message it rides on.
8048
+ // CT326: likewise the ask payload. CT1281: the server writes the ask as its
8049
+ // own addressed message to the human which ENQUEUES like any other send
8050
+ // and creates the `asks` row against that carrier, not against turn speech.
8267
8051
  askState,
8268
8052
  // CT442: likewise the wake payload — attached to the `final` row so the
8269
8053
  // server arms the wake schedule atomically with the reply it rode on.
@@ -8288,6 +8072,7 @@ ${reason}`,
8288
8072
  );
8289
8073
  if (intent.ask) {
8290
8074
  askState.targetUserId = intent.ask.targetUserId;
8075
+ askState.order = intent.askOrder ?? null;
8291
8076
  if (intent.ask.questions && intent.ask.questions.length > 0) {
8292
8077
  askState.questions = intent.ask.questions;
8293
8078
  askState.question = null;
@@ -8316,8 +8101,12 @@ ${reason}`,
8316
8101
  if (intent.sendAgentId && intent.sendBody) {
8317
8102
  sendState.agentId = intent.sendAgentId;
8318
8103
  sendState.message = intent.sendBody;
8104
+ sendState.order = intent.sendOrder ?? null;
8105
+ }
8106
+ if (intent.answersMessageId) {
8107
+ replyState.answersMessageId = intent.answersMessageId;
8108
+ replyState.order = intent.replyOrder ?? null;
8319
8109
  }
8320
- if (intent.answersMessageId) replyState.answersMessageId = intent.answersMessageId;
8321
8110
  if (intent.skipped) {
8322
8111
  skipState.skipped = true;
8323
8112
  skipState.reason = intent.skipReason;
@@ -8400,6 +8189,7 @@ ${reason}`,
8400
8189
  turnUsage = event.usage;
8401
8190
  turnResolvedModel = event.resolvedModel;
8402
8191
  turnResolvedConfig = event.resolvedConfig;
8192
+ turnMcpInventory = event.mcpInventory;
8403
8193
  runtimeResultKind = event.ok ? "success" : event.reason === "no_terminal" ? "no_terminal" : "error";
8404
8194
  } else if (event.type === "text" && skipState.skipped) {
8405
8195
  } else {
@@ -8476,7 +8266,6 @@ ${reason}`,
8476
8266
  } finally {
8477
8267
  if (idleTimer) clearTimeout(idleTimer);
8478
8268
  clearTimeout(totalTimer);
8479
- closeTurnReceipt(okResult, resultReason ?? null);
8480
8269
  const userCancelled = abortController.signal.aborted && timeoutReason === null;
8481
8270
  if (timeoutReason !== null) {
8482
8271
  resultReason = timeoutReason;
@@ -8558,6 +8347,7 @@ ${reason}`,
8558
8347
  sessionFingerprint: fingerprintSessionState(latestSessionState),
8559
8348
  eventCounts,
8560
8349
  runtimeResultKind,
8350
+ ...turnMcpInventory ? { mcpInventory: turnMcpInventory } : {},
8561
8351
  finalSource: outcome === "skipped" || outcome === "cancelled" || silentMarkerEmitted ? "marker" : committer.finalSource
8562
8352
  };
8563
8353
  body.diagnostics = settledDiagnostics;
@@ -8707,7 +8497,7 @@ async function enumerateOpencodeModels(serverUrl, fetchImpl = fetch) {
8707
8497
  // src/outbox.ts
8708
8498
  import {
8709
8499
  existsSync as existsSync11,
8710
- mkdirSync as mkdirSync11,
8500
+ mkdirSync as mkdirSync10,
8711
8501
  readdirSync as readdirSync3,
8712
8502
  readFileSync as readFileSync8,
8713
8503
  renameSync as renameSync3,
@@ -8737,7 +8527,7 @@ var Outbox = class {
8737
8527
  // per-workspace bounds.
8738
8528
  persist(entry) {
8739
8529
  const dir2 = this.dir();
8740
- mkdirSync11(dir2, { recursive: true });
8530
+ mkdirSync10(dir2, { recursive: true });
8741
8531
  const target = this.fileFor(entry.turnId, entry.seq);
8742
8532
  const tmp = `${target}.${process.pid}.tmp`;
8743
8533
  try {
@@ -10008,14 +9798,14 @@ function handleUncaught(log, err, origin) {
10008
9798
  }
10009
9799
 
10010
9800
  // src/crash-marker.ts
10011
- import { existsSync as existsSync12, mkdirSync as mkdirSync12, readFileSync as readFileSync9, rmSync as rmSync8, writeFileSync as writeFileSync8 } from "fs";
9801
+ import { existsSync as existsSync12, mkdirSync as mkdirSync11, readFileSync as readFileSync9, rmSync as rmSync8, writeFileSync as writeFileSync8 } from "fs";
10012
9802
  import { join as join16 } from "path";
10013
9803
  function crashMarkerPath() {
10014
9804
  return join16(cabaneDir(), "last-error.json");
10015
9805
  }
10016
9806
  function recordCrash(rec) {
10017
9807
  try {
10018
- mkdirSync12(cabaneDir(), { recursive: true });
9808
+ mkdirSync11(cabaneDir(), { recursive: true });
10019
9809
  writeFileSync8(crashMarkerPath(), JSON.stringify(rec, null, 2) + "\n");
10020
9810
  } catch {
10021
9811
  }