[Bug]: OpenCode + Claude Agent: adding the t3-code MCP server mid-turn aborts the tool and tells Claude the user refused#30240

opened9d agobyMmirsella

Area

Not sure (provider-opencode / apps/server)

What happened

On an OpenCode 2 thread using the claude-agent/opus provider, a tool call is aborted about 150 ms after it starts, and the model ends the turn. The case below was turn 8 of a long session. New threads hit it too.

Claude Code records the abort as a user tool rejection:

  • User rejected tool use
  • [Request interrupted by user for tool use]
  • "STOP what you are doing and wait for the user to tell you how to proceed."

The model then replies "I've stopped as you asked." The user never interrupted. T3 records the run as completed, not cancelled.

The restart is the problem. If the Code Mode catalog changes while a tool is running, the tool can be cancelled and Claude is told the user refused it. A new thread's first turn is an easy way to hit this, but any turn where the server is added or re-added can do it.

T3 registers the per-thread t3-code-<threadId> MCP server after it has already built the turn's Code Mode catalog. The catalog then changes mid-turn, and OpenCode's Claude Agent provider restarts Claude because its prompt and tool list changed. The restart makes Claude treat the running tool as cancelled, and Claude Code reports that as a human refusal. The tool had already finished in OpenCode, but the result only reached the new process in the checkpoint.

Steps to reproduce

  1. Use T3 v0.1.7-nightly.20261102.4066, OpenCode v2.2.5, and Claude Code 2.1.316.
  2. Create a new OpenCode 2 thread with claude-agent/opus (effort high) in a fresh worktree.
  3. Send a prompt whose first response calls a tool. In the observed case, that was the demo skill, then a full demo recording.
  4. In the Claude transcript (or the OpenCode session messages) for the turn, check that the catalog does not contain t3-code-*.
  5. Later in the same turn (about 18 seconds in the observed case), a "The Code Mode tool catalog has changed" update adds t3-code-<threadId>.
  6. If a tool call is in flight at that moment, the Claude transcript gets a synthetic User rejected tool use result and the model stops.

The same thing happens on an existing thread when t3-code is missing from the turn's starting catalog and prepareTurn adds it back, for example after idle eviction or a reconnect. That is the case below.

This happened on 2026-11-03 in thread 400dd89f-0b24-40d6-9412-4e628f9deaa5 ("Revamp Game End Screen Particles"), OpenCode session ses_e6dc75e36ffeVlt24I1g08FTrL, Claude session 80612108-82f2-4bdd-a5fa-8ab7d938e557. The abort was on turn 8. t3-code was missing from the catalog at the start of the turn and came back mid-turn. The mechanism is the same as the new-thread case.

Expected behavior

  • A tool catalog change caused by T3's own MCP registration should not abort a running turn.
  • If Claude must restart, the interruption should be reported as a neutral harness restart, not as a user refusal.
  • A turn should either start with the complete T3 tool catalog or wait for it before starting.

Actual behavior

  • The in-flight skill tool call is cancelled about 150 ms after the tool_use is emitted.
  • OpenCode's tool handler had already completed the call at 14:50:12.211Z, but the restart discarded the result in the old Claude process.
  • Claude Code writes a synthetic user message: "The user doesn't want to proceed with this tool use... STOP what you are doing and wait for the user to tell you how to proceed." (toolDenialKind: "user-rejected").
  • The model obeys that message and ends the turn with "I've stopped as you asked."
  • T3 records the run as completed, not cancelled, so the transcript presents the stop as a normal assistant decision.

Impact

Major degradation or frequent failure.

A turn can be lost. That includes the first turn on a new thread and later turns after the server drops out of the catalog. The user has to say continue. Since Claude is told a human refused, it may not retry the tool and may ask for permission that was never denied.

The affected OpenCode session has several turns that end with "stopped as you asked". The local OpenCode database has 19 such assistant messages and 11 "Request interrupted by user" records, including this one.

Version or commit

t3 v0.1.7-nightly.20261102.4066

Environment

  • Arch Linux, kernel 7.3.1-1-cachyos
  • OpenCode 2.2.5
  • Claude Code 2.1.316
  • Provider/model: claude-agent/opus, variant high
  • T3 nightly desktop build

Logs or stack traces

Excerpt from the Claude transcript (~/.claude/projects/.../80612108-82f2-4bdd-a5fa-8ab7d938e557.jsonl, trimmed). The full transcript is available on request. It contains local paths and unrelated history.

text
114:49:54.560 user: "les go with the second bonus badge ... do a full demo now"
2
314:49:54.874 system (Code Mode catalog):
4 browser, composio, context7-broker, opencode
5 (no t3-code-* server)
6
714:50:12.151 assistant tool_use:
8 id: toolu_014suo5dMyBF1gcWuLncLWe3
9 name: mcp__opencode__skill
10 input: {"id":"demo"}
11 diagnostics.cache_miss_reason: {"type":"tools_changed","cache_missed_input_tokens":299364}
12
1314:50:12.305 user (synthetic tool_result):
14 "The user doesn't want to proceed with this tool use. ... STOP what you are
15 doing and wait for the user to tell you how to proceed. Correction to the
16 text above: this call was stopped while running, not refused before it ran.
17 ... The instruction to stop and wait still stands."
18 is_error: true
19 toolUseResult: "User rejected tool use"
20 toolDenialKind: "user-rejected"
21 tool_use_id: toolu_014suo5dMyBF1gcWuLncLWe3
22
2314:50:12.308 user (synthetic):
24 "[Request interrupted by user for tool use]"
25
2614:50:13.054 user (checkpoint):
27 <conversation-checkpoint> ... successful skill result ...
28 <system-update>
29 The Code Mode tool catalog has changed. ...
30 - t3-code-400dd89f-0b24-40d6-9412-4e628f9deaa5 (70 tools, 1 shown)
31 </system-update>
32
3314:50:25.707 assistant:
34 "I've stopped as you asked, and nothing has changed since your last message..."

T3 run row: requested 2026-11-03T14:49:54.560Z, completed 2026-11-03T14:50:25.794Z, status completed.

Root cause

  • packages/provider-opencode/src/server/v2/adapter.ts registers the per-thread MCP server in prepareTurn, after session.create. This is the same path as #22945. prepareTurn runs on every turn and adds the server back when it was evicted or its config changed, so later turns can change the catalog too.
  • OpenCode 2 builds a session's Code Mode catalog at creation. The catalog for this turn was emitted at 14:49:54.874Z without t3-code-*. The assistant request at 14:50:12.151Z has a tools_changed cache miss, so the tool list changed since the previous request. The abort follows at 14:50:12.305Z. The new catalog, with t3-code-*, arrives in the checkpoint at 14:50:13.054Z.
  • OpenCode's Claude Agent spec says: "When the list of tools changes, the Claude process restarts, because the SDK cannot swap tools while running." The same section says the process restarts when the system prompt changes. Spec: https://raw.githubusercontent.com/anomalyco/opencode/dev/specs/v2/claude-agent-provider.md
  • The restart makes Claude treat the running tool as cancelled. Claude Code records that as user-rejected and injects the STOP and wait text, which the model treats as a real human refusal.
  • I tested the "wait for the tools to register" idea with claude-agent/haiku at low effort. I created an idle thread, waited 30 seconds, then sent the first prompt. The first API request still had no t3-code namespace in its Code Mode catalog. Waiting does not help: prepareTurn adds the server when the first prompt starts, after the catalog has already been built.

Why this is not a duplicate

I checked the open issues that mention the same symptoms. They are related, but none covers this trigger.

  • #22945 is about a new thread's first turn starting without T3 tools. The tools show up later. This report is about what happens when they show up mid-turn: the catalog update aborts a running Claude turn and invents a user refusal. The open PR #22944 would remove the new-thread trigger. It would not remove the abort or the false refusal for other mid-turn catalog changes, such as an event-stream reconnect, a slow mcp.add, idle eviction, a forked thread (#28949), or a future tool change.
  • #15736 is about the t3-code server disappearing mid-session. In this case it was missing at the start of the turn and came back mid-turn.
  • #21895, #15351, and #23061 are about steers and delegated-task notifications cancelling tools. They produce the same fake refusal, but the trigger is an injected message. This thread has no steer and no t3_thread_send call. T3's own MCP registration changes the catalog.