Switching a thread's provider instance during a running turn kills the turn with "provider event stream closed unexpectedly"#22748

opened2d agobyDDhruv Sharma

What happened

I run two accounts per harness as separate provider instances (personal claudeAgent / codex, work claude-work / a managed Codex instance). Turns randomly fail with "The provider event stream closed unexpectedly", on whichever provider happened to be running. It looked like a provider or account problem.

Diagnosis

The failures are caused by T3 itself: switching a thread's provider instance while the thread has a run in progress tears down the live session mid-turn. The turn then ends as a generic unknown failure instead of being refused, deferred, or reported clearly.

In my case an orchestrator agent was moving a set of worker threads from claude-work back to claudeAgent with t3_thread_configure (one call per thread). Several of those workers were mid-turn, and the orchestrator also switched its own thread. Every running thread it touched failed within ~150 ms.

Code path at v0.1.2-nightly.20261021.3457 (unchanged on main as of today):

  1. t3_thread_configure (apps/server/src/mcp/toolkits/thread/handlers.ts:182) dispatches provider.switch when the instance changes (modelSelectionCommandType).
  2. ProviderSwitchService (apps/server/src/orchestration-v2/ProviderSwitchService.ts:180) puts every live session from another instance in releaseProviderSessionIds for a create_with_handoff transition. It does not check whether a run is active.
  3. The decider (apps/server/src/orchestration-v2/Orchestrator.ts:3640-3712) emits provider-session.detached and queues a provider-session.detach effect with detail "Provider or model selection changed.". There is no active-run guard, unlike thread.settle, which refuses with "is still running. Stop it before settling." (Orchestrator.ts:2835-2850).
  4. ProviderSessionManager.detach closes the session (query.close goes to the Claude SDK while the model is still producing tool calls). The run's subscribers get a stream error, and the turn is recorded as class: "unknown" / "The provider event stream closed unexpectedly. Retry the turn; if it keeps failing, check the provider and server logs."

The detail string that explains the failure exists in the outbox, but the user never sees it.

Expected, any of:

  • refuse provider.switch / thread.model-selection.set on a thread with an active run (like settle), or
  • defer the switch until the current run ends, or
  • at minimum, interrupt the run cleanly and report "Provider was switched while this turn was running" rather than a generic stream failure.

Steps to reproduce

  1. Configure two provider instances of the same driver (e.g. claudeAgent and a second claudeAgent instance with another homePath).
  2. Start a long turn on a thread using instance A (e.g. ask it to run several tool calls).
  3. While it runs, switch the thread to instance B, either from another agent via the T3 MCP tool t3_thread_configure with modelSelection.instanceId = B, or by having the running agent call it on its own thread.
  4. The running turn fails immediately with "The provider event stream closed unexpectedly".

Seen 8 out of 8 times in my logs: every switch issued against a running thread failed its turn. A switch in the same batch against an idle thread (07:42:15) caused no error.

Version

0.1.2-nightly.20261021.3457 (desktop app, nightly)

Environment

macOS 26 (Darwin 25.6.0) arm64, Node 26.8.2, Claude Code 2.1.306, codex-cli 0.163.0

Evidence

shell
1# effect outbox: every "stream closed" failure has a matching detach from an MCP provider.switch
2provider-session.detach | Provider or model selection changed. | mcp:1bffb82f-… | 2026-10-21T07:40:19.508Z
3provider-session.detach | Provider or model selection changed. | mcp:f167cd1a-… | 2026-10-21T07:42:12.129Z
4provider-session.detach | Provider or model selection changed. | mcp:85ebf963-… | 2026-10-21T07:42:12.922Z
5provider-session.detach | Provider or model selection changed. | mcp:5db90f4e-… | 2026-10-21T07:42:14.059Z
6provider-session.detach | Provider or model selection changed. | mcp:3bbdd80f-… | 2026-10-21T07:42:16.336Z
7provider-session.detach | Provider or model selection changed. | mcp:378614ad-… | 2026-10-21T07:42:17.531Z
8provider-session.detach | Provider or model selection changed. | mcp:c044a900-… | 2026-10-21T07:44:04.767Z # orchestrator switching its own thread
9provider-session.detach | Provider or model selection changed. | mcp:541eab28-… | 2026-10-21T07:51:57.508Z
10
11# server trace, same instant
1207:42:12 orchestrationV2.dispatch.once {"command_type":"provider.switch","thread_id":"mcp:670e053c-…"}
1307:42:12 ProviderSessionManager.detach -> ProviderSessionManager.closeScopeWithin
14
15# provider event log for that thread: model mid-turn, then T3 closes it
16[07:42:08.391Z] incoming system/status "requesting"
17[07:42:12.144Z] outgoing {"type":"query.close"}
18
19# resulting turn item
20{"title":"…","failure":{"class":"unknown","message":"The provider event stream closed unexpectedly. Retry the turn; if it keeps failing, check the provider and server logs."}}

Related issues

#15135 is the same failure shape for thread.archive (self-archive detaches the live session without an active-run guard). This one is provider.switch / thread.model-selection.set, a different command and code branch, so a fix for archive alone won't cover it. #21369 covers the poor reporting after this error in general.

Fix applied or workaround

None applied on the machine. Workaround: only switch a thread's provider instance when it has no active run, and never let an agent switch its own thread mid-turn.

Filed by

Claude Code (claude-opus-5-5) via t3 triage

Reproduced on current main at 8acbefefc5: Linux web dev server, Claude Opus 5.5 running a foreground sleep 90, then provider.switch to a second Claude instance. The turn immediately failed with the reported unknown stream error. Runtime transcript. Existing draft fix #22754 is being rechecked under the revised triage brief.

Fix #22754 now refuses disruptive provider switches while a turn is active. Same-driver Claude runtime verification preserved the running turn; idle switches and next-turn selections remain available. On merged head 896c0e10d3, 109 focused tests, server typecheck and targeted lint passed. Both independent reviews found no blocking defects. Evidence and limits.