@khalilgharbaoui/opencode-claude-code-plugin 0.34.0 → 0.34.1

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/README.md CHANGED
@@ -469,7 +469,7 @@ reaches the CLI, so there is nothing there to fall back from.
469
469
  | `mcpConfig` | string \| string[] | – | Extra `--mcp-config` paths/JSON passed alongside the bridged config. |
470
470
  | `strictMcpConfig` | boolean | `false` | Pass `--strict-mcp-config` so Claude loads **only** the configured servers and ignores `~/.claude/settings.json`. |
471
471
  | `hotReloadMcp` | boolean | `true` | With MCP bridging on, compare the merged MCP config and runtime status at the start of each turn and respawn the `claude` process when they drifted, so a server you just enabled, disabled or finished connecting becomes visible without restarting opencode or opening a new chat. It only ever acts at a safe boundary: never during `/compact`, never on the interactive transport, never while a proxied call is still in the air, a turn is still running or a plan-mode approval is outstanding, and the Claude session id is preserved for `--resume` so the conversation continues. A log line at INFO names which servers joined and left. A server that flaps buys at most one respawn per minute per conversation (`CLAUDE_CODE_MCP_HOT_RELOAD_COOLDOWN_MS`). Set `false` to keep a cached subprocess until the chat is reset. It does not reload other provider options and does not watch the contents of files named in `mcpConfig`. |
472
- | `mcpConnectWaitMs` | number | `3000` | How long the first turn of a conversation waits for MCP servers opencode reports as still connecting before planning the `claude` spawn without them. Only opencode 2 can report that state (`pending`); opencode 1's own status call blocks until every server has decided, so this budget is what makes the two majors behave alike. Set `0` to always plan with whatever the host says at that instant. A server slower than the budget is not lost either way: it is still bridged, and `hotReloadMcp` moves the conversation onto a process that has it on the next turn. |
472
+ | `mcpConnectWaitMs` | number | `3000` | How long the first turn of a conversation waits for MCP servers opencode reports as still connecting before planning the `claude` spawn without them. Only opencode 2 can report that state (`pending`); opencode 1's own status call blocks until every server has decided, so this budget is what makes the two majors behave alike. Set `0` to always plan with whatever the host says at that instant. Aborting the turn also ends the wait at once. A server slower than the budget is not lost either way: it is still bridged, and `hotReloadMcp` moves the conversation onto a process that has it on the next turn. |
473
473
  | `proxyOpencodeMcpTools` | boolean | `false` | Route opencode's MCP-backed tools through the in-process `opencode_proxy` server instead of bridging them straight into Claude's `--mcp-config`, so each call executes once, inside opencode, with its permission prompt and its tool row. **The default changed from `true` to `false` in this release, and no behaviour changed with it:** at `true` it used to route nothing at all, because discovery read opencode's tool registry, which contains built-ins and plugin-declared tools and has never contained an MCP tool. Discovery now reads the model tool set opencode passes the provider, which is where MCP tools actually are, so the option works, and turning it on is the operator's decision rather than a silent migration of traffic that the direct bridge is handling today. Two caveats before enabling it: pair it with `strictMcpConfig: true`, because a server also registered in Claude Code's own config is reached directly and bypasses the proxy entirely; and a routed call runs in opencode with the calling agent's permissions, the same trade [`proxyOpencodeTools`](#options-reference) makes. Servers whose tools are not found stay on the direct bridge, and a warning says so, so do not treat this as an exactly-once guarantee for write-capable tools. |
474
474
  | `proxyOpencodeTools` | string[] | `[]` | Forward explicitly named opencode tools through the proxy (for example, a plugin's `compress` or V2 Code Mode `execute`). V1 uses registry ids; V2 uses the current model tool snapshot, including its real JSON Schema and agent visibility, not the registry's empty schemas. A forwarded tool runs inside opencode with the calling agent's permissions. A name already held by a proxy def is dropped with a warning. The read-only preset refuses `execute`. See [Forwarding opencode's own tools](#forwarding-opencode-s-own-tools) and [V2 Code Mode](#v2-code-mode). |
475
475
  | `stripContextReminders` | boolean | `false` | Remove opencode-dcp's `<dcp-system-reminder>` blocks from message text when no `compress` tool is proxied, so an order the model cannot follow stops being re-sent with every message that carries it. Inert as soon as `compress` is reachable. See [Trimming unsatisfiable context reminders](#trimming-unsatisfiable-context-reminders). |
@@ -851,7 +851,7 @@ A proxied call ends when something happens to it, not when a clock runs out. The
851
851
  | What happens | What the plugin does |
852
852
  |---|---|
853
853
  | opencode returns the tool's result | resolves the call; the CLI gets the result and carries on |
854
- | you abort the turn (Esc / Ctrl+C) | sends the CLI an `interrupt`, which answers with its own result, and rejects every call the turn had pending, whether the abort lands before content, mid-turn, or while opencode is running the tool between two stream boundaries |
854
+ | you abort the turn (Esc / Ctrl+C) | sends the CLI an `interrupt`, which answers with its own result, and rejects every call the turn had pending, whether the abort lands before content, mid-turn, or while opencode is running the tool between two stream boundaries. A stop that lands before the turn has asked the CLI for anything is the one exception: it interrupts nothing and releases nothing, because the calls still pending there belong to the previous step and the next message orphans them as usual |
855
855
  | you send the next message in that chat | rejects every call the previous turn left pending as orphaned, so the CLI gets an error result and the new turn starts clean |
856
856
  | the `claude` process closes its output or exits, mid-turn or between turns | rejects its pending calls; a mid-turn death also ends the turn as a visible error |
857
857
  | you delete the chat in opencode, or opencode exits | kills the worker and rejects its pending calls |
@@ -1145,6 +1145,7 @@ Each chat keeps a long-lived `claude` subprocess so the model retains its native
1145
1145
  - **New chat** → fresh process under the new session key.
1146
1146
  - **Resumed chat after restart** → in-memory state is gone; a new process spawns and the conversation history is summarized and prepended.
1147
1147
  - **Abort (Esc / Ctrl+C)** → the plugin sends the Claude CLI a stream-json `interrupt` control request, so the CLI actually stops generating and running tools instead of finishing the abandoned turn on your bill. The process stays alive for the next message in that chat, and any proxied call the aborted turn left behind is released when that message arrives (see [How a proxied call ends](#how-a-proxied-call-ends)). If a turn is somehow still running when the next one starts, it is interrupted first (5 s cap). Contributed by [@broskees](https://github.com/broskees).
1148
+ - **Abort during the first moment of a turn** → a turn spends a little time preparing before it asks the CLI for anything: resolving the spawn directory, probing the `claude` version, reading opencode's MCP status and tool registry. A stop pressed in that window used to be dropped, and the turn spawned, ran and billed anyway. It now ends the turn there: nothing is spawned, nothing is written, no running process is interrupted, and the reply is simply empty.
1148
1149
  - **Idle timeout** → when `idleProcessTimeoutMs` is set, a completed headless turn arms an eviction timer (unset or `0` keeps workers until LRU eviction). Reuse cancels it, a worker found mid-turn when it fires is left alone and re-timed, and eviction preserves the session id, so the next message resumes the same conversation with `--resume`. An idle `claude --print` holds around 250 MB, which is the reason to set it if you keep many chats open.
1149
1150
  - **Cap**: 16 active processes, LRU eviction. A process that is mid-turn is never the victim: eviction takes the oldest **idle** one, and when every process is busy it evicts nothing and warns instead, so a running answer is never truncated to make room.
1150
1151
  - **Deleted chat** → deleting a session in opencode kills its `claude` workers at once and forgets their session ids and per-chat state; there is nothing left to resume. Other chats, and the shared fallback bucket used when no session id is known, are untouched.
package/dist/index.js CHANGED
@@ -3504,16 +3504,18 @@ async function getRuntimeMcpStatus(options = {}) {
3504
3504
  const current = await fetchRuntimeMcpStatus();
3505
3505
  const budgetMs = options.waitForPendingMs ?? 0;
3506
3506
  if (!current || budgetMs <= 0) return current;
3507
- return waitForPendingMcpServers(current, budgetMs);
3507
+ return waitForPendingMcpServers(current, budgetMs, options.signal);
3508
3508
  }
3509
- async function waitForPendingMcpServers(first, budgetMs) {
3509
+ async function waitForPendingMcpServers(first, budgetMs, signal) {
3510
3510
  let current = first;
3511
3511
  const waitedFor = pendingMcpServers(current);
3512
3512
  if (waitedFor.length === 0) return current;
3513
3513
  const started = Date.now();
3514
3514
  for (; ; ) {
3515
3515
  if (Date.now() - started >= budgetMs) break;
3516
+ if (signal?.aborted) break;
3516
3517
  await new Promise((resolve6) => setTimeout(resolve6, MCP_CONNECT_POLL_MS));
3518
+ if (signal?.aborted) break;
3517
3519
  const next = await fetchRuntimeMcpStatus();
3518
3520
  if (!next) break;
3519
3521
  current = next;
@@ -3524,7 +3526,8 @@ async function waitForPendingMcpServers(first, budgetMs) {
3524
3526
  waitedFor,
3525
3527
  stillPending,
3526
3528
  budgetMs,
3527
- elapsedMs: Date.now() - started
3529
+ elapsedMs: Date.now() - started,
3530
+ aborted: signal?.aborted === true
3528
3531
  });
3529
3532
  return current;
3530
3533
  }
@@ -8621,6 +8624,8 @@ function createTurnState(init) {
8621
8624
  reasoningIds: /* @__PURE__ */ new Map(),
8622
8625
  reasoningStarted: /* @__PURE__ */ new Map(),
8623
8626
  hadThinkingTextFromStream: false,
8627
+ cliAskedForWork: false,
8628
+ streamStarted: false,
8624
8629
  turnCompleted: false,
8625
8630
  controllerClosed: false,
8626
8631
  cleanedUp: false,
@@ -8670,6 +8675,59 @@ function createTurnState(init) {
8670
8675
  return state;
8671
8676
  }
8672
8677
 
8678
+ // src/turn-abort.ts
8679
+ function watchTurnAbort(signal) {
8680
+ let aborted = signal?.aborted === true;
8681
+ let listeners = [];
8682
+ let settle;
8683
+ const whenAborted = new Promise((resolve6) => {
8684
+ settle = resolve6;
8685
+ });
8686
+ const fire = () => {
8687
+ if (aborted && listeners === null) return;
8688
+ aborted = true;
8689
+ const pending = listeners ?? [];
8690
+ listeners = null;
8691
+ settle();
8692
+ for (const fn of pending) {
8693
+ try {
8694
+ fn();
8695
+ } catch (error) {
8696
+ log.warn("turn abort listener threw", {
8697
+ error: error instanceof Error ? error.message : String(error)
8698
+ });
8699
+ }
8700
+ }
8701
+ };
8702
+ const onSignalAbort = () => fire();
8703
+ if (aborted) {
8704
+ listeners = null;
8705
+ settle();
8706
+ } else {
8707
+ signal?.addEventListener("abort", onSignalAbort, { once: true });
8708
+ }
8709
+ return {
8710
+ get aborted() {
8711
+ return aborted;
8712
+ },
8713
+ get reason() {
8714
+ return signal?.reason;
8715
+ },
8716
+ whenAborted,
8717
+ onAbort(fn) {
8718
+ if (aborted) {
8719
+ fn();
8720
+ return;
8721
+ }
8722
+ listeners?.push(fn);
8723
+ },
8724
+ dispose() {
8725
+ listeners = null;
8726
+ signal?.removeEventListener("abort", onSignalAbort);
8727
+ }
8728
+ };
8729
+ }
8730
+
8673
8731
  // src/stream-parser.ts
8674
8732
  import { generateId as generateId2 } from "@ai-sdk/provider-utils";
8675
8733
 
@@ -10344,6 +10402,21 @@ var ClaudeCodeLanguageModel = class {
10344
10402
  }
10345
10403
  async doStreamForHost(options, attempt, mode = "stream") {
10346
10404
  const warnings = [];
10405
+ const turnAbort = watchTurnAbort(options.abortSignal);
10406
+ const abortedBeforeWork = (stage) => {
10407
+ log.info("abort before the turn asked claude for work; nothing spawned", {
10408
+ stage,
10409
+ reason: describeAbortReason(turnAbort.reason)
10410
+ });
10411
+ turnAbort.dispose();
10412
+ const aborted = new ReadableStream({
10413
+ start(controller) {
10414
+ controller.enqueue({ type: "stream-start", warnings });
10415
+ controller.close();
10416
+ }
10417
+ });
10418
+ return { stream: aborted, request: { body: { text: "" } } };
10419
+ };
10347
10420
  const skipPermissions = this.config.skipPermissions !== false;
10348
10421
  const scope = this.requestScope(options);
10349
10422
  const affinity = this.sessionAffinity(options);
@@ -10388,6 +10461,7 @@ var ClaudeCodeLanguageModel = class {
10388
10461
  modelId: effectiveModelId
10389
10462
  });
10390
10463
  let cliPath = failover.cliPath;
10464
+ if (turnAbort.aborted) return abortedBeforeWork("resolving the spawn cwd and account");
10391
10465
  const asideTransportRef = { cliPath, interactive: !!useInteractive };
10392
10466
  const doctor = !compactionMode && scope !== "no-tools" ? parseDoctorCommand(options.prompt) : null;
10393
10467
  if (doctor) {
@@ -10639,7 +10713,7 @@ var ClaudeCodeLanguageModel = class {
10639
10713
  const hasMatchedPendingResults = previousPendingProxyMatches.some(
10640
10714
  (m) => m.result !== null
10641
10715
  );
10642
- const [runtimeStatus, cliVersion] = await Promise.all([
10716
+ const prologueWork = Promise.all([
10643
10717
  compactionMode ? Promise.resolve(void 0) : getRuntimeMcpStatus({
10644
10718
  // Give a server opencode is still starting a bounded moment to
10645
10719
  // reach a decision before the spawn is planned without it. Only
@@ -10647,10 +10721,20 @@ var ClaudeCodeLanguageModel = class {
10647
10721
  // call exactly as before.
10648
10722
  waitForPendingMs: resolveMcpConnectWaitMs(
10649
10723
  this.config.mcpConnectWaitMs
10650
- )
10724
+ ),
10725
+ signal: options.abortSignal
10651
10726
  }),
10652
10727
  detectCliVersion(cliPath)
10653
10728
  ]);
10729
+ prologueWork.catch(() => void 0);
10730
+ const prologue = await Promise.race([
10731
+ prologueWork.then((value) => ({ value })),
10732
+ turnAbort.whenAborted.then(() => void 0)
10733
+ ]);
10734
+ if (!prologue) {
10735
+ return abortedBeforeWork("waiting for opencode's MCP status and the CLI version");
10736
+ }
10737
+ const [runtimeStatus, cliVersion] = prologue.value;
10654
10738
  const failoverAccounts = failoverCandidates(
10655
10739
  this.config.failoverAccounts,
10656
10740
  sourceAccount
@@ -10681,6 +10765,7 @@ var ClaudeCodeLanguageModel = class {
10681
10765
  opencodeAgent: this.getOpencodeAgent(options),
10682
10766
  providerOptionsKeys: options.providerOptions ? Object.keys(options.providerOptions) : []
10683
10767
  });
10768
+ if (turnAbort.aborted) return abortedBeforeWork("planning the spawn");
10684
10769
  const stream = new ReadableStream({
10685
10770
  start(controller) {
10686
10771
  if (compactionMode) {
@@ -10710,7 +10795,109 @@ var ClaudeCodeLanguageModel = class {
10710
10795
  });
10711
10796
  state.activeProcess = getActiveProcess(sk);
10712
10797
  state.proxyServer = state.activeProcess?.proxyServer ?? null;
10798
+ const discardUnattachedProxyServer = () => {
10799
+ const own = state.proxyServer;
10800
+ if (!own || own === state.activeProcess?.proxyServer) return;
10801
+ state.proxyServer = null;
10802
+ void own.close().catch((error) => {
10803
+ log.warn("failed to close the proxy server of an aborted turn", {
10804
+ sessionKey: sk,
10805
+ error: error instanceof Error ? error.message : String(error)
10806
+ });
10807
+ });
10808
+ };
10809
+ const releaseAbandonedProxyCalls = (reason) => {
10810
+ if (state.drainBuffer.length === 0 && getPendingProxyCalls(sk).length === 0) return;
10811
+ rejectAllPendingProxyCallsForSession(sk, new Error(reason));
10812
+ state.drainBuffer.length = 0;
10813
+ };
10814
+ turnAbort.onAbort(() => {
10815
+ state.autoContinueState.aborted = true;
10816
+ if (!state.cliAskedForWork) {
10817
+ if (state.controllerClosed) return;
10818
+ state.controllerClosed = true;
10819
+ log.info("abort while the turn was still being prepared; nothing was sent to claude", {
10820
+ sessionKey: sk,
10821
+ // A process here was spawned by this turn or reused from an
10822
+ // earlier one; either way nothing was asked of it.
10823
+ attachedProcess: !!state.activeProcess,
10824
+ reason: describeAbortReason(turnAbort.reason)
10825
+ });
10826
+ discardUnattachedProxyServer();
10827
+ state.cleanupTurn?.();
10828
+ if (!state.streamStarted) {
10829
+ state.streamStarted = true;
10830
+ controller.enqueue({ type: "stream-start", warnings });
10831
+ }
10832
+ try {
10833
+ controller.close();
10834
+ } catch {
10835
+ }
10836
+ return;
10837
+ }
10838
+ if (state.turnCompleted || state.controllerClosed) {
10839
+ const stoppedProcess = state.activeProcess;
10840
+ if (stoppedProcess && stoppedProcess.lineEmitter.listenerCount("line") === 0 && getPendingProxyCalls(sk).length > 0) {
10841
+ const reason = describeAbortReason(options.abortSignal?.reason);
10842
+ void settleSessionRunState(affinity).then((stopped) => {
10843
+ const stillParked = stoppedProcess.lineEmitter.listenerCount("line") === 0 && getPendingProxyCalls(sk).length > 0;
10844
+ if (stopped === "busy" || !stillParked) {
10845
+ log.debug("abort at a tool boundary while opencode is still running the turn; keeping pending calls", {
10846
+ sk,
10847
+ session: stopped,
10848
+ stillParked,
10849
+ reason
10850
+ });
10851
+ return;
10852
+ }
10853
+ log.info("abort between proxy tool boundaries; releasing pending calls", { sk, reason });
10854
+ void interruptTurn(stoppedProcess).then((idle) => {
10855
+ log.info("interrupt sent for aborted turn", { sk, idle });
10856
+ });
10857
+ releaseAbandonedProxyCalls(
10858
+ "Provider stream was aborted while opencode was running its proxy tool calls"
10859
+ );
10860
+ });
10861
+ }
10862
+ return;
10863
+ }
10864
+ if (state.activeProcess) {
10865
+ void interruptTurn(state.activeProcess).then((idle) => {
10866
+ log.info("interrupt sent for aborted turn", { sk, idle });
10867
+ });
10868
+ }
10869
+ if (!state.hasReceivedContent) {
10870
+ log.info(
10871
+ "abort signal received before content, closing stream immediately",
10872
+ { cwd }
10873
+ );
10874
+ releaseAbandonedProxyCalls(
10875
+ "Provider stream was aborted before pending proxy calls were emitted"
10876
+ );
10877
+ state.controllerClosed = true;
10878
+ state.cleanupTurn?.();
10879
+ try {
10880
+ controller.close();
10881
+ } catch {
10882
+ }
10883
+ return;
10884
+ }
10885
+ log.info(
10886
+ "abort signal received mid-turn, starting grace period",
10887
+ { cwd }
10888
+ );
10889
+ releaseAbandonedProxyCalls(
10890
+ "Provider stream was aborted while proxy tool calls were pending"
10891
+ );
10892
+ startResultFallback(state, 5e3);
10893
+ });
10894
+ const stoppedBeforeWork = () => {
10895
+ if (!state.controllerClosed || state.cliAskedForWork) return false;
10896
+ discardUnattachedProxyServer();
10897
+ return true;
10898
+ };
10713
10899
  const setup = async () => {
10900
+ if (stoppedBeforeWork()) return;
10714
10901
  if (!compactionMode && state.activeProcess && self.config.hotReloadMcp !== false && self.config.bridgeOpencodeMcp !== false) {
10715
10902
  const probe = self.effectiveMcpConfig(cwd, void 0, runtimeStatus);
10716
10903
  const previousHash = state.activeProcess.mcpHash ?? null;
@@ -10783,6 +10970,7 @@ var ClaudeCodeLanguageModel = class {
10783
10970
  enabled: self.config.bridgeOpencodeSkills === true,
10784
10971
  ...self.skillBridgeSpawn(failover)
10785
10972
  });
10973
+ if (stoppedBeforeWork()) return;
10786
10974
  const ap = spawnInteractiveProcess({
10787
10975
  cwd,
10788
10976
  cliPath,
@@ -10989,6 +11177,7 @@ var ClaudeCodeLanguageModel = class {
10989
11177
  spawnMcpHash = mcp.bridgedHash;
10990
11178
  spawnMcpServers = mcp.allEnabledServerNames;
10991
11179
  }
11180
+ if (stoppedBeforeWork()) return;
10992
11181
  if (state.activeProcess && !compactionMode) {
10993
11182
  state.proc = state.activeProcess.proc;
10994
11183
  state.lineEmitter = state.activeProcess.lineEmitter;
@@ -11019,6 +11208,8 @@ var ClaudeCodeLanguageModel = class {
11019
11208
  log.warn("previous turn did not stop in time; this turn may see stale output", { sk });
11020
11209
  }
11021
11210
  }
11211
+ if (stoppedBeforeWork()) return;
11212
+ state.streamStarted = true;
11022
11213
  controller.enqueue({ type: "stream-start", warnings });
11023
11214
  if (failoverNote) {
11024
11215
  controller.enqueue({
@@ -11377,72 +11568,8 @@ var ClaudeCodeLanguageModel = class {
11377
11568
  state.drainTimer = setTimeout(() => drainNow(state), DRAIN_QUIET_MS);
11378
11569
  });
11379
11570
  state.proc.on("error", procErrorHandler);
11380
- if (options.abortSignal) {
11381
- const releaseAbandonedProxyCalls = (reason) => {
11382
- if (state.drainBuffer.length === 0 && getPendingProxyCalls(sk).length === 0) return;
11383
- rejectAllPendingProxyCallsForSession(sk, new Error(reason));
11384
- state.drainBuffer.length = 0;
11385
- };
11386
- options.abortSignal.addEventListener("abort", () => {
11387
- state.autoContinueState.aborted = true;
11388
- if (state.turnCompleted || state.controllerClosed) {
11389
- const stoppedProcess = state.activeProcess;
11390
- if (stoppedProcess && stoppedProcess.lineEmitter.listenerCount("line") === 0 && getPendingProxyCalls(sk).length > 0) {
11391
- const reason = describeAbortReason(options.abortSignal?.reason);
11392
- void settleSessionRunState(affinity).then((stopped) => {
11393
- const stillParked = stoppedProcess.lineEmitter.listenerCount("line") === 0 && getPendingProxyCalls(sk).length > 0;
11394
- if (stopped === "busy" || !stillParked) {
11395
- log.debug("abort at a tool boundary while opencode is still running the turn; keeping pending calls", {
11396
- sk,
11397
- session: stopped,
11398
- stillParked,
11399
- reason
11400
- });
11401
- return;
11402
- }
11403
- log.info("abort between proxy tool boundaries; releasing pending calls", { sk, reason });
11404
- void interruptTurn(stoppedProcess).then((idle) => {
11405
- log.info("interrupt sent for aborted turn", { sk, idle });
11406
- });
11407
- releaseAbandonedProxyCalls(
11408
- "Provider stream was aborted while opencode was running its proxy tool calls"
11409
- );
11410
- });
11411
- }
11412
- return;
11413
- }
11414
- if (state.activeProcess) {
11415
- void interruptTurn(state.activeProcess).then((idle) => {
11416
- log.info("interrupt sent for aborted turn", { sk, idle });
11417
- });
11418
- }
11419
- if (!state.hasReceivedContent) {
11420
- log.info(
11421
- "abort signal received before content, closing stream immediately",
11422
- { cwd }
11423
- );
11424
- releaseAbandonedProxyCalls(
11425
- "Provider stream was aborted before pending proxy calls were emitted"
11426
- );
11427
- state.controllerClosed = true;
11428
- cleanupTurn();
11429
- try {
11430
- controller.close();
11431
- } catch {
11432
- }
11433
- return;
11434
- }
11435
- log.info(
11436
- "abort signal received mid-turn, starting grace period",
11437
- { cwd }
11438
- );
11439
- releaseAbandonedProxyCalls(
11440
- "Provider stream was aborted while proxy tool calls were pending"
11441
- );
11442
- startResultFallback(state, 5e3);
11443
- });
11444
- }
11445
11571
  if (hasMatchedPendingResults) {
11572
+ state.cliAskedForWork = true;
11446
11573
  for (const { call, result } of previousPendingProxyMatches) {
11447
11574
  if (result) {
11448
11575
  const channelClosed = isPendingProxyCallChannelClosed(call);
@@ -11500,6 +11627,7 @@ var ClaudeCodeLanguageModel = class {
11500
11627
  );
11501
11628
  }
11502
11629
  }
11630
+ state.cliAskedForWork = true;
11503
11631
  if (state.activeProcess) noteTurnStarted(state.activeProcess);
11504
11632
  state.proc.stdin?.write(userMsg + "\n");
11505
11633
  log.debug("sent user message", { textLength: userMsg.length });