@khalilgharbaoui/opencode-claude-code-plugin 0.53.2 → 0.53.4

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.
package/dist/index.d.ts CHANGED
@@ -1364,7 +1364,7 @@ declare class ClaudeCodeLanguageModel implements LanguageModelV3 {
1364
1364
  private effectiveMcpConfig;
1365
1365
  /** Resolve ProxyToolDef[] for the configured proxyTools names. */
1366
1366
  private resolvedProxyTools;
1367
- /** Resolve ProxyToolDef[] for opencode's MCP-backed tools. */
1367
+ /** Resolve ProxyToolDef[] for opencode's MCP-backed tools, and route the rest. */
1368
1368
  private resolvedProxyMcpTools;
1369
1369
  /** One `client.tool.list()` fetch, shaped for this turn's gates. */
1370
1370
  private fetchLiveToolInfo;
package/dist/index.js CHANGED
@@ -3860,8 +3860,8 @@ function buildProxyTimeoutError(toolName, ms) {
3860
3860
  return new Error(base);
3861
3861
  }
3862
3862
  var TASK_PROXY_NOTE = "This and task_batch are the ONLY tools that dispatch opencode subagents (including user @-mentions). Claude Code's built-in TaskCreate/TaskUpdate manage a local todo list and cannot dispatch subagents. Do not search config files to verify a subagent type exists: invalid types fail fast with a clear error. Calls block until the subagent finishes. For two or more independent subagents in one response use task_batch, not several task calls: those run one after another. Task calls have no proxy deadline by default: the call waits for the subagent to finish (a positive proxyToolTimeoutMs override adds a deadline).";
3863
- var TASK_BACKGROUND_NOTE = 'Fire-and-collect: set `background: true` and the call returns at once with `<task id="ses_..." state="running">` instead of the subagent\'s answer. Keep working on something that does not overlap it, then end your turn: when the subagent finishes, opencode delivers its result to this conversation on its own as a new message. Do NOT poll, sleep or loop waiting for it. The `id` in that envelope is the task_id: pass it to task_status to read a result the notification did not deliver, or to task_cancel to stop a background subagent you no longer want.';
3864
- var TASK_BACKGROUND_NOTE_V2 = 'Fire-and-collect: set `background: true` and the call returns at once with `The subagent is working in the background (sessionID: ses_...)` instead of the subagent\'s answer. Keep working on something that does not overlap it, then end your turn: when the subagent finishes, opencode delivers its result to this conversation on its own, as a `<subagent sessionID="..." state="completed">` message. Do NOT poll, sleep or loop waiting for it. That sessionID is the task_id: pass it to task_status to read a result the notification did not deliver, or to task_cancel to stop a background subagent you no longer want.';
3863
+ var TASK_BACKGROUND_NOTE = 'Fire-and-collect: set `background: true` and the call returns at once with `<task id="ses_..." state="running">` instead of the subagent\'s answer. Keep working on something that does not overlap it, then end your turn: when the subagent finishes, opencode delivers its result to this conversation on its own as a new message. Do NOT poll, sleep or loop waiting for it. The `id` in that envelope is the task_id: pass it to task_status to read a result the notification did not deliver, or to task_cancel to stop a background subagent you no longer want. The dispatch result may end with how many background subagents are running now: that is for information, never a reason to poll or wait.';
3864
+ var TASK_BACKGROUND_NOTE_V2 = 'Fire-and-collect: set `background: true` and the call returns at once with `The subagent is working in the background (sessionID: ses_...)` instead of the subagent\'s answer. Keep working on something that does not overlap it, then end your turn: when the subagent finishes, opencode delivers its result to this conversation on its own, as a `<subagent sessionID="..." state="completed">` message. Do NOT poll, sleep or loop waiting for it. That sessionID is the task_id: pass it to task_status to read a result the notification did not deliver, or to task_cancel to stop a background subagent you no longer want. The dispatch result may end with how many background subagents are running now: that is for information, never a reason to poll or wait.';
3865
3865
  var TASK_BATCH_TOOL_NAME = "task_batch";
3866
3866
  var TASK_BATCH_PROXY_NOTE = "Use this instead of several task calls in one response: Claude Code runs MCP tool calls one at a time, so separate task calls run serially even when emitted together, while one task_batch call fans them out as parallel opencode task calls. Each task takes the same fields as the task tool. Results come back in task order, each labelled. Like task it has no proxy deadline by default (a positive proxyToolTimeoutMs override adds one).";
3867
3867
  var TASK_INPUT_REQUIRED = ["description", "prompt", "subagent_type"];
@@ -4679,7 +4679,11 @@ function resolveProxyOpencodeToolDefs(options) {
4679
4679
  return out;
4680
4680
  }
4681
4681
  function resolveMcpProxyToolDefs(options) {
4682
- const empty = { defs: [], coveredServers: /* @__PURE__ */ new Set() };
4682
+ const empty = {
4683
+ defs: [],
4684
+ coveredServers: /* @__PURE__ */ new Set(),
4685
+ matchedServers: /* @__PURE__ */ new Set()
4686
+ };
4683
4687
  const serverNames = options.serverNames ?? [];
4684
4688
  if (serverNames.length === 0) return empty;
4685
4689
  const tools = options.tools;
@@ -4688,6 +4692,7 @@ function resolveMcpProxyToolDefs(options) {
4688
4692
  const taken = options.taken ?? /* @__PURE__ */ new Set();
4689
4693
  const defs = [];
4690
4694
  const coveredServers = /* @__PURE__ */ new Set();
4695
+ const matchedServers = /* @__PURE__ */ new Set();
4691
4696
  const seen = /* @__PURE__ */ new Set();
4692
4697
  const collided = [];
4693
4698
  for (const tool of tools) {
@@ -4699,6 +4704,7 @@ function resolveMcpProxyToolDefs(options) {
4699
4704
  (server2) => name === server2 || name.startsWith(`${server2}_`)
4700
4705
  );
4701
4706
  if (!matchedServer) continue;
4707
+ matchedServers.add(matchedServer);
4702
4708
  if (seen.has(name)) continue;
4703
4709
  if (taken.has(name)) {
4704
4710
  collided.push(name);
@@ -4718,7 +4724,7 @@ function resolveMcpProxyToolDefs(options) {
4718
4724
  { collided }
4719
4725
  );
4720
4726
  }
4721
- return { defs, coveredServers };
4727
+ return { defs, coveredServers, matchedServers };
4722
4728
  }
4723
4729
  function readBody(req) {
4724
4730
  return new Promise((resolve6, reject) => {
@@ -5707,7 +5713,7 @@ async function fetchSessionRunState(sessionID, directory) {
5707
5713
  async function settleSessionRunState(sessionID, options = {}) {
5708
5714
  const pollMs = options.pollMs ?? 150;
5709
5715
  const windowMs = options.windowMs ?? 1500;
5710
- const started = Date.now();
5716
+ const started2 = Date.now();
5711
5717
  let last = "unknown";
5712
5718
  let directory = options.directory;
5713
5719
  for (; ; ) {
@@ -5715,7 +5721,7 @@ async function settleSessionRunState(sessionID, options = {}) {
5715
5721
  const state = await fetchSessionRunState(sessionID, directory ?? "");
5716
5722
  if (state === "busy") return "busy";
5717
5723
  if (state !== "unknown") last = state;
5718
- if (Date.now() - started >= windowMs) return last;
5724
+ if (Date.now() - started2 >= windowMs) return last;
5719
5725
  await new Promise((resolve6) => setTimeout(resolve6, pollMs));
5720
5726
  }
5721
5727
  }
@@ -5765,9 +5771,9 @@ async function waitForPendingMcpServers(first, budgetMs, signal) {
5765
5771
  let current = first;
5766
5772
  const waitedFor = pendingMcpServers(current);
5767
5773
  if (waitedFor.length === 0) return current;
5768
- const started = Date.now();
5774
+ const started2 = Date.now();
5769
5775
  for (; ; ) {
5770
- if (Date.now() - started >= budgetMs) break;
5776
+ if (Date.now() - started2 >= budgetMs) break;
5771
5777
  if (signal?.aborted) break;
5772
5778
  await new Promise((resolve6) => setTimeout(resolve6, MCP_CONNECT_POLL_MS));
5773
5779
  if (signal?.aborted) break;
@@ -5781,7 +5787,7 @@ async function waitForPendingMcpServers(first, budgetMs, signal) {
5781
5787
  waitedFor,
5782
5788
  stillPending,
5783
5789
  budgetMs,
5784
- elapsedMs: Date.now() - started,
5790
+ elapsedMs: Date.now() - started2,
5785
5791
  aborted: signal?.aborted === true
5786
5792
  });
5787
5793
  return current;
@@ -5909,6 +5915,123 @@ function clearBackgroundTasks(sessionKey2) {
5909
5915
  collected.delete(sessionKey2);
5910
5916
  cancelled.delete(sessionKey2);
5911
5917
  }
5918
+ var MAX_STARTED = 128;
5919
+ var started = /* @__PURE__ */ new Map();
5920
+ function backgroundTaskIdsIn(text) {
5921
+ const ids = /* @__PURE__ */ new Set();
5922
+ for (const match of text.matchAll(/<task id="([^"\s]+)" state="running">/g)) {
5923
+ ids.add(match[1]);
5924
+ }
5925
+ for (const match of text.matchAll(/in the background \(sessionID: ([^)\s]+)\)/g)) {
5926
+ ids.add(match[1]);
5927
+ }
5928
+ return [...ids];
5929
+ }
5930
+ function noteBackgroundDispatchResult(sessionKey2, result, now = Date.now()) {
5931
+ if (result.kind !== "text" || result.isError === true) return;
5932
+ for (const taskId of backgroundTaskIdsIn(result.text)) {
5933
+ started.delete(taskId);
5934
+ started.set(taskId, { sessionKey: sessionKey2, at: now });
5935
+ while (started.size > MAX_STARTED) {
5936
+ const oldest = started.keys().next();
5937
+ if (oldest.done) break;
5938
+ started.delete(oldest.value);
5939
+ }
5940
+ }
5941
+ }
5942
+ async function countRunningBackgroundTasks() {
5943
+ const entries = [...started.entries()];
5944
+ const cancelledIds = cancelledTaskIds();
5945
+ const states = await Promise.all(
5946
+ entries.map(([taskId]) => backgroundTaskState(taskId, cancelledIds))
5947
+ );
5948
+ return {
5949
+ running: states.filter((state) => state === "running").length,
5950
+ started: entries.length,
5951
+ unreadable: states.filter((state) => state === "unreadable").length
5952
+ };
5953
+ }
5954
+ function cancelledTaskIds() {
5955
+ const ids = /* @__PURE__ */ new Set();
5956
+ for (const set of cancelled.values()) for (const id of set) ids.add(id);
5957
+ return ids;
5958
+ }
5959
+ async function backgroundTaskState(taskId, cancelledIds) {
5960
+ if (cancelledIds.has(taskId)) return "stopped";
5961
+ try {
5962
+ const runState = await fetchSessionRunState(taskId);
5963
+ if (runState !== "unknown") {
5964
+ return isBackgroundTaskRunning(runState, []) ? "running" : "stopped";
5965
+ }
5966
+ const replies = await fetchSessionReplies(taskId);
5967
+ if (replies === void 0) return "unreadable";
5968
+ return isBackgroundTaskRunning(runState, replies) ? "running" : "stopped";
5969
+ } catch {
5970
+ return "unreadable";
5971
+ }
5972
+ }
5973
+ var BACKGROUND_COUNT_BUDGET_MS = 500;
5974
+ var BACKGROUND_COUNT_CONCURRENCY = 4;
5975
+ async function countOtherRunningWithin(exclude, budgetMs) {
5976
+ const taskIds = [...started.keys()].filter((taskId) => !exclude.has(taskId));
5977
+ if (taskIds.length === 0) return 0;
5978
+ const cancelledIds = cancelledTaskIds();
5979
+ let overBudget = false;
5980
+ let unreadable = false;
5981
+ let next = 0;
5982
+ let running = 0;
5983
+ const worker = async () => {
5984
+ while (!overBudget && !unreadable && next < taskIds.length) {
5985
+ const taskId = taskIds[next++];
5986
+ const state = await backgroundTaskState(taskId, cancelledIds);
5987
+ if (state === "running") running++;
5988
+ if (state === "unreadable") unreadable = true;
5989
+ }
5990
+ };
5991
+ const workers = Array.from(
5992
+ { length: Math.min(BACKGROUND_COUNT_CONCURRENCY, taskIds.length) },
5993
+ worker
5994
+ );
5995
+ let timer;
5996
+ const deadline = new Promise((resolve6) => {
5997
+ timer = setTimeout(() => resolve6("over"), budgetMs);
5998
+ timer.unref?.();
5999
+ });
6000
+ try {
6001
+ const outcome = await Promise.race([Promise.all(workers).then(() => "done"), deadline]);
6002
+ if (outcome === "over") {
6003
+ overBudget = true;
6004
+ return void 0;
6005
+ }
6006
+ return unreadable ? void 0 : running;
6007
+ } catch {
6008
+ overBudget = true;
6009
+ return void 0;
6010
+ } finally {
6011
+ clearTimeout(timer);
6012
+ }
6013
+ }
6014
+ async function withBackgroundRunningCount(result, budgetMs = BACKGROUND_COUNT_BUDGET_MS) {
6015
+ if (result.kind !== "text" || result.isError === true) return result;
6016
+ const dispatched = backgroundTaskIdsIn(result.text);
6017
+ if (dispatched.length === 0) return result;
6018
+ const others = await countOtherRunningWithin(new Set(dispatched), budgetMs);
6019
+ if (others === void 0) {
6020
+ log.info("background running count omitted: a lookup was slow or unreadable", {
6021
+ budgetMs,
6022
+ dispatched: dispatched.length
6023
+ });
6024
+ return result;
6025
+ }
6026
+ return { ...result, text: result.text + formatBackgroundRunningLine(others, dispatched.length) };
6027
+ }
6028
+ function formatBackgroundRunningLine(others, dispatched) {
6029
+ const total = others + dispatched;
6030
+ const including = dispatched === 1 ? "this one" : `the ${dispatched} just started`;
6031
+ return `
6032
+
6033
+ Background subagents running now: ${total} (including ${including}).`;
6034
+ }
5912
6035
  var lastGate;
5913
6036
  function recordBackgroundSubagentGate(gate, now = Date.now()) {
5914
6037
  lastGate = { ...gate, at: now };
@@ -7635,21 +7758,21 @@ async function sessionStatus(client, sessionID) {
7635
7758
  async function waitForSessionIdle(client, sessionID, options = {}) {
7636
7759
  const pollMs = options.pollMs ?? IDLE_POLL_MS;
7637
7760
  const timeoutMs = options.timeoutMs ?? IDLE_WAIT_MAX_MS;
7638
- const started = Date.now();
7761
+ const started2 = Date.now();
7639
7762
  for (; ; ) {
7640
7763
  if (options.stop?.()) return true;
7641
7764
  if (await sessionStatus(client, sessionID) !== "busy") return true;
7642
- if (Date.now() - started >= timeoutMs) return false;
7765
+ if (Date.now() - started2 >= timeoutMs) return false;
7643
7766
  await new Promise((resolve6) => setTimeout(resolve6, pollMs));
7644
7767
  }
7645
7768
  }
7646
7769
  async function deliverAsideInline(client, sessionID, text, options = {}) {
7647
7770
  const pollMs = options.inlinePollMs ?? INLINE_POLL_MS;
7648
7771
  const timeoutMs = options.inlineWaitMs ?? INLINE_WAIT_MAX_MS;
7649
- const started = Date.now();
7772
+ const started2 = Date.now();
7650
7773
  for (; ; ) {
7651
7774
  if (emitAsideInline(sessionID, text)) return true;
7652
- if (Date.now() - started >= timeoutMs) return false;
7775
+ if (Date.now() - started2 >= timeoutMs) return false;
7653
7776
  if (await sessionStatus(client, sessionID) !== "busy") return false;
7654
7777
  await new Promise((resolve6) => setTimeout(resolve6, pollMs));
7655
7778
  }
@@ -7658,12 +7781,12 @@ async function waitForAsideProcess(client, sessionID, options = {}) {
7658
7781
  const pollMs = options.pollMs ?? IDLE_POLL_MS;
7659
7782
  const settleMs = options.settleMs ?? BUSY_SETTLE_MS;
7660
7783
  const spawnWaitMs = options.spawnWaitMs ?? SPAWN_WAIT_MAX_MS;
7661
- const started = Date.now();
7784
+ const started2 = Date.now();
7662
7785
  for (; ; ) {
7663
7786
  const active = findActiveProcessBySessionId(sessionID);
7664
7787
  if (active) return active;
7665
7788
  const busy = await sessionStatus(client, sessionID) === "busy";
7666
- const waitedMs = Date.now() - started;
7789
+ const waitedMs = Date.now() - started2;
7667
7790
  if (!busy && waitedMs >= settleMs) {
7668
7791
  log.info("btw: no live claude process for session, leaving it to the turn", { sessionID, waitedMs });
7669
7792
  return void 0;
@@ -7678,12 +7801,12 @@ async function waitForAsideProcess(client, sessionID, options = {}) {
7678
7801
  async function settleSessionBusy(client, sessionID, active, options = {}) {
7679
7802
  const pollMs = options.pollMs ?? IDLE_POLL_MS;
7680
7803
  const settleMs = options.settleMs ?? BUSY_SETTLE_MS;
7681
- const started = Date.now();
7804
+ const started2 = Date.now();
7682
7805
  for (; ; ) {
7683
7806
  const status = await sessionStatus(client, sessionID);
7684
7807
  if (status === "busy") return true;
7685
7808
  if (status === "unknown") return isProcessBusy(active);
7686
- if (Date.now() - started >= settleMs) return false;
7809
+ if (Date.now() - started2 >= settleMs) return false;
7687
7810
  await new Promise((resolve6) => setTimeout(resolve6, pollMs));
7688
7811
  }
7689
7812
  }
@@ -7787,7 +7910,7 @@ async function handleBtwCommand(client, input, options = {}) {
7787
7910
  );
7788
7911
  }
7789
7912
  if (!busy) return;
7790
- const started = Date.now();
7913
+ const started2 = Date.now();
7791
7914
  const outcome = await Promise.race([
7792
7915
  inlineDelivered,
7793
7916
  waitForSessionIdle(client, input.sessionID, { ...options, stop: () => inlineDone }).then(
@@ -7797,14 +7920,14 @@ async function handleBtwCommand(client, input, options = {}) {
7797
7920
  if (outcome === "inline") {
7798
7921
  log.info("btw: answered inside the running turn, dropping the /btw message", {
7799
7922
  sessionID: input.sessionID,
7800
- waitedMs: Date.now() - started
7923
+ waitedMs: Date.now() - started2
7801
7924
  });
7802
7925
  throw new BtwHandledError(BTW_INLINE_HANDLED_MESSAGE);
7803
7926
  }
7804
7927
  log.info("btw: turn over, releasing the /btw message", {
7805
7928
  sessionID: input.sessionID,
7806
7929
  idle: outcome === "idle",
7807
- waitedMs: Date.now() - started
7930
+ waitedMs: Date.now() - started2
7808
7931
  });
7809
7932
  if (outcome === "timeout") {
7810
7933
  showToast(client, { title: "btw", message: BTW_TURN_TOO_LONG_MESSAGE, variant: "warning", duration: 8e3 });
@@ -7933,6 +8056,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
7933
8056
  "proxy-mcp interceptor failed",
7934
8057
  "proxy-mcp rejected a request",
7935
8058
  "proxy-mcp tool call timed out",
8059
+ "proxyOpencodeMcpTools cannot route V2 Code Mode's MCP tools, which reach the model through execute; for Code Mode, explicitly allowlist execute in proxyOpencodeTools and use bridgeOpencodeMcp: false with strictMcpConfig: true",
7936
8060
  "proxyOpencodeTools entry dropped: a proxy tool already holds that name, and it keeps it",
7937
8061
  "proxyOpencodeTools is set but opencode's tool registry did not answer; forwarding nothing this spawn",
7938
8062
  "refusing the pid-named scratch directory",
@@ -9168,6 +9292,10 @@ function formatDoctorReport(report) {
9168
9292
  }
9169
9293
  function formatBackgroundSubagents(state) {
9170
9294
  const lines = [];
9295
+ if (state.running) {
9296
+ lines.push(...formatRunningCount(state.running));
9297
+ lines.push("");
9298
+ }
9171
9299
  const gate = state.gate;
9172
9300
  if (!gate) {
9173
9301
  lines.push(
@@ -9213,6 +9341,19 @@ function formatBackgroundSubagents(state) {
9213
9341
  }
9214
9342
  return lines;
9215
9343
  }
9344
+ function formatRunningCount(count) {
9345
+ if (count.started === 0) {
9346
+ return ["running now: 0 (none started by this process)"];
9347
+ }
9348
+ const lines = [`running now: ${count.running} (of ${count.started} started by this process)`];
9349
+ if (count.unreadable > 0) {
9350
+ lines.push("");
9351
+ lines.push(
9352
+ `${count.unreadable} could not be read from opencode (deleted, or no session route on this build) and are not counted as running.`
9353
+ );
9354
+ }
9355
+ return lines;
9356
+ }
9216
9357
  function describeBackgroundGateSource(gate) {
9217
9358
  if (gate.hostApi === "v2") {
9218
9359
  return "opencode 2 offers `background` unconditionally, so the registry is not consulted";
@@ -9297,7 +9438,8 @@ async function gatherDoctorReport(options) {
9297
9438
  planUsage,
9298
9439
  backgroundSubagents: {
9299
9440
  gate: snapshotBackgroundSubagentGate(),
9300
- ledgers: snapshotBackgroundTasks()
9441
+ ledgers: snapshotBackgroundTasks(),
9442
+ running: await countRunningBackgroundTasks().catch(() => void 0)
9301
9443
  }
9302
9444
  };
9303
9445
  }
@@ -11968,12 +12110,12 @@ function spawnInteractiveProcess(opts) {
11968
12110
  }
11969
12111
  }
11970
12112
  closeProxyServer();
11971
- const started = startPromise !== null;
12113
+ const started2 = startPromise !== null;
11972
12114
  try {
11973
12115
  session.dispose();
11974
12116
  } catch {
11975
12117
  }
11976
- if (!started && proc.exitCode === null && proc.signalCode === null) {
12118
+ if (!started2 && proc.exitCode === null && proc.signalCode === null) {
11977
12119
  proc.signalCode = "SIGTERM";
11978
12120
  proc.emit("exit", null, "SIGTERM");
11979
12121
  proc.emit("close", null, "SIGTERM");
@@ -12125,6 +12267,7 @@ This opencode host runs background subagents, so \`mcp__opencode_proxy__task\` t
12125
12267
  - A background dispatch returns at once with \`<task id="ses_..." state="running">\` instead of the subagent's answer. That is success, not a truncated result.
12126
12268
  - After starting one, keep working on something that does not overlap it, then end your turn normally. When the subagent finishes, opencode delivers its result into this conversation as a new message on its own. Do not sleep, poll, or ask the operator to wait.
12127
12269
  - The \`id\` in that envelope is the task_id. \`mcp__opencode_proxy__task_status\` reads a result back if the automatic delivery did not arrive (an interrupted or errored turn); \`mcp__opencode_proxy__task_cancel\` stops a background subagent you no longer want. Neither is a progress poll.
12270
+ - A background dispatch's result may end with \`Background subagents running now: N\`. That number is for information, so you can avoid starting more than the work needs; it is never a reason to poll, sleep or wait.
12128
12271
  - Foreground is still the default and still the right choice when you need the answer before you can continue. Use background only for work that genuinely runs alongside yours.`;
12129
12272
  var BACKGROUND_SUBAGENT_HINT_V2 = `## Background subagents
12130
12273
 
@@ -12133,6 +12276,7 @@ This opencode host runs background subagents, so \`mcp__opencode_proxy__task\` t
12133
12276
  - A background dispatch returns at once with \`The subagent is working in the background (sessionID: ses_...)\` instead of the subagent's answer. That is success, not a truncated result.
12134
12277
  - After starting one, keep working on something that does not overlap it, then end your turn normally. When the subagent finishes, opencode delivers its result into this conversation on its own, as a \`<subagent sessionID="..." state="completed">\` message. Do not sleep, poll, or ask the operator to wait.
12135
12278
  - That \`sessionID\` is the task_id. \`mcp__opencode_proxy__task_status\` reads a result back if the automatic delivery did not arrive (an interrupted or errored turn); \`mcp__opencode_proxy__task_cancel\` stops a background subagent you no longer want. Neither is a progress poll.
12279
+ - A background dispatch's result may end with \`Background subagents running now: N\`. That number is for information, so you can avoid starting more than the work needs; it is never a reason to poll, sleep or wait.
12136
12280
  - Foreground is still the default and still the right choice when you need the answer before you can continue. Use background only for work that genuinely runs alongside yours.`;
12137
12281
  function backgroundSubagentHint(dialect) {
12138
12282
  return dialect === "v2" ? BACKGROUND_SUBAGENT_HINT_V2 : BACKGROUND_SUBAGENT_HINT;
@@ -12737,7 +12881,13 @@ function resolvedProxyTools(config) {
12737
12881
  }
12738
12882
  return picked.length > 0 ? picked : null;
12739
12883
  }
12740
- function resolvedProxyMcpTools(config, allEnabledServerNames, modelTools, taken) {
12884
+ function decideMcpServerRoute(input) {
12885
+ if (input.covered) return "proxy";
12886
+ if (input.inToolSet) return "bridge";
12887
+ if (input.toolSetKnown && input.status === "connected") return "withhold";
12888
+ return "bridge";
12889
+ }
12890
+ function resolvedProxyMcpTools(config, allEnabledServerNames, modelTools, taken, runtimeStatus) {
12741
12891
  if (config.proxyOpencodeMcpTools !== true) return null;
12742
12892
  if (config.bridgeOpencodeMcp === false) return null;
12743
12893
  if (allEnabledServerNames.length === 0) return null;
@@ -12746,18 +12896,47 @@ function resolvedProxyMcpTools(config, allEnabledServerNames, modelTools, taken)
12746
12896
  tools: modelTools,
12747
12897
  taken
12748
12898
  });
12749
- if (resolution.defs.length === 0) {
12899
+ const codeMode = config.hostApi === "v2" && (modelTools?.some((tool) => tool.name === "execute") ?? false);
12900
+ const toolSetKnown = (modelTools?.length ?? 0) > 0 && !codeMode;
12901
+ const withheld = [];
12902
+ const bridged = [];
12903
+ for (const server2 of allEnabledServerNames) {
12904
+ const route = decideMcpServerRoute({
12905
+ covered: resolution.coveredServers.has(server2),
12906
+ inToolSet: resolution.matchedServers.has(server2),
12907
+ status: runtimeStatus?.[server2],
12908
+ toolSetKnown
12909
+ });
12910
+ if (route === "withhold") withheld.push(server2);
12911
+ else if (route === "bridge") bridged.push(server2);
12912
+ }
12913
+ if (codeMode && resolution.defs.length === 0) {
12750
12914
  log.warn(
12751
- "proxyOpencodeMcpTools is on but no MCP tool was found in opencode's tool set; those servers stay on the direct bridge this spawn" + (config.hostApi === "v2" && modelTools?.some((tool) => tool.name === "execute") ? ". For V2 Code Mode, explicitly allowlist execute in proxyOpencodeTools and use bridgeOpencodeMcp: false with strictMcpConfig: true" : ""),
12752
- { servers: allEnabledServerNames, modelTools: modelTools?.length ?? 0 }
12915
+ "proxyOpencodeMcpTools cannot route V2 Code Mode's MCP tools, which reach the model through execute; for Code Mode, explicitly allowlist execute in proxyOpencodeTools and use bridgeOpencodeMcp: false with strictMcpConfig: true",
12916
+ { servers: allEnabledServerNames }
12753
12917
  );
12754
- return null;
12755
12918
  }
12756
- log.debug("routing opencode MCP tools through the proxy", {
12757
- servers: [...resolution.coveredServers],
12758
- tools: resolution.defs.map((def) => def.name)
12759
- });
12760
- return resolution;
12919
+ if (withheld.length > 0 || bridged.length > 0) {
12920
+ log.info("proxyOpencodeMcpTools: MCP servers outside this agent's tool set", {
12921
+ proxied: [...resolution.coveredServers],
12922
+ withheld,
12923
+ bridged,
12924
+ modelTools: modelTools?.length ?? 0,
12925
+ statusKnown: runtimeStatus !== void 0
12926
+ });
12927
+ } else {
12928
+ log.debug("routing opencode MCP tools through the proxy", {
12929
+ servers: [...resolution.coveredServers],
12930
+ tools: resolution.defs.map((def) => def.name)
12931
+ });
12932
+ }
12933
+ const exclude = /* @__PURE__ */ new Set([...resolution.coveredServers, ...withheld]);
12934
+ return {
12935
+ resolution,
12936
+ withheld,
12937
+ bridged,
12938
+ excludeServers: exclude.size > 0 ? exclude : void 0
12939
+ };
12761
12940
  }
12762
12941
  async function fetchLiveToolInfo(config, modelId) {
12763
12942
  const items = await fetchOpencodeToolList(
@@ -14684,13 +14863,14 @@ var ClaudeCodeLanguageModel = class {
14684
14863
  resolvedProxyTools() {
14685
14864
  return resolvedProxyTools(this.config);
14686
14865
  }
14687
- /** Resolve ProxyToolDef[] for opencode's MCP-backed tools. */
14688
- resolvedProxyMcpTools(allEnabledServerNames, modelTools, taken) {
14866
+ /** Resolve ProxyToolDef[] for opencode's MCP-backed tools, and route the rest. */
14867
+ resolvedProxyMcpTools(allEnabledServerNames, modelTools, taken, runtimeStatus) {
14689
14868
  return resolvedProxyMcpTools(
14690
14869
  this.config,
14691
14870
  allEnabledServerNames,
14692
14871
  modelTools,
14693
- taken
14872
+ taken,
14873
+ runtimeStatus
14694
14874
  );
14695
14875
  }
14696
14876
  /** One `client.tool.list()` fetch, shaped for this turn's gates. */
@@ -15999,13 +16179,14 @@ var ClaudeCodeLanguageModel = class {
15999
16179
  void 0,
16000
16180
  runtimeStatus
16001
16181
  );
16002
- const mcpResolution = self.resolvedProxyMcpTools(
16182
+ const mcpRouting = self.resolvedProxyMcpTools(
16003
16183
  discovery.allEnabledServerNames,
16004
16184
  options.tools,
16005
- new Set((resolvedProxy ?? []).map((def) => def.name))
16185
+ new Set((resolvedProxy ?? []).map((def) => def.name)),
16186
+ runtimeStatus
16006
16187
  );
16007
- const proxyMcpTools = mcpResolution?.defs ?? null;
16008
- const excludeServers = mcpResolution ? mcpResolution.coveredServers : void 0;
16188
+ const proxyMcpTools = mcpRouting && mcpRouting.resolution.defs.length > 0 ? mcpRouting.resolution.defs : null;
16189
+ const excludeServers = mcpRouting?.excludeServers;
16009
16190
  const taskProxyEnabled = resolvedProxy?.some((t) => t.name === "task") ?? false;
16010
16191
  const questionProxyEnabled = resolvedProxy?.some((t) => t.name === "question") ?? false;
16011
16192
  const opencodeToolsRequested = (self.config.proxyOpencodeTools?.length ?? 0) > 0;
@@ -16844,8 +17025,14 @@ var ClaudeCodeLanguageModel = class {
16844
17025
  await new Promise((resolve6) => setTimeout(resolve6, FORWARD_SETTLE_MS));
16845
17026
  if (state.controllerClosed) return;
16846
17027
  }
16847
- for (const { call, result } of previousPendingProxyMatches) {
16848
- if (result) {
17028
+ for (const { call, result: opencodeResult } of previousPendingProxyMatches) {
17029
+ if (opencodeResult) {
17030
+ let result = opencodeResult;
17031
+ if (call.toolName === "task" || call.toolName === TASK_BATCH_TOOL_NAME) {
17032
+ noteBackgroundDispatchResult(sk, result);
17033
+ result = await withBackgroundRunningCount(result);
17034
+ if (state.controllerClosed) return;
17035
+ }
16849
17036
  const channelClosed = isPendingProxyCallChannelClosed(call);
16850
17037
  log.info("resolving pending proxy call from tool result prompt", {
16851
17038
  sessionKey: sk,