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

opened25d 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."}}

#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.

Synced from GitHub

Confirmed again on 0.1.8-nightly.20261104.4179, Linux x64 / CachyOS, desktop app with its local server. This occurrence is a cross-provider self-switch from Claude (claudeAgent, claude-opus-5-5) to Codex (codex, gpt-6.1-sol), rather than switching accounts within the same provider.

The agent called this during its active turn:

json
1{"modelSelection":{"instanceId":"codex","model":"gpt-6.1-sol"}}

The switch succeeded, but T3 terminated the current turn before it could finish its response. Correlated persisted events, effect-outbox records, and native provider logs show:

text
12026-11-06, UTC
223:24:42.310 thread.provider-switched -> codex
323:24:42.310 provider-session.detached
4 reason: "Provider or model selection changed."
523:24:42.321 t3_thread_configure returns success
623:24:42.331 outgoing Claude SDK {"type":"query.close"}
723:24:42.332 active run and attempt marked failed
8 "The provider event stream closed unexpectedly. Retry the turn;
9 if it keeps failing, check the provider and server logs."

The matching provider-session.detach outbox effect succeeded, with detail Provider or model selection changed. The command receipt is an accepted provider.switch. The last Claude rate-limit status was allowed_warning, not a blocking rate-limit result. This was intentional session teardown being presented as a transport failure.

I inspected the installed server bundle and source at release commit 9e85538535c8553901351112ab02dd1dd62db5ee, tag v0.1.8-nightly.20261104.4179:

  • ProviderSwitchService selects old live sessions for release on a create_with_handoff transition.
  • The thread mutation dispatch schedules provider-session.detach while the source turn is still active.
  • For the last attached thread of a non-shared session, ProviderSessionManager.detach calls releaseEntry with manual_shutdown.
  • releaseEntry calls failSubscribers, which constructs ProviderAdapterEventStreamError, then closes the adapter scope. This matches the observed query.close and failed turn.

For agent self-switches, saving the selection for the next turn and deferring source-session teardown until the current turn finishes would preserve the final response. A clear refusal of disruptive active-turn switches would also avoid the failure. Suppressing the error alone would leave the current turn without a terminal result.

The earlier comments describe a fix verified for same-driver switches. I have not established whether that change shipped in this nightly or whether this is a missed cross-driver path; this evidence confirms the behavior is still present in the installed build above.

No runtime patch, database write, app restart, or second live reproduction was performed. Evidence checks were read-only; no application tests were run. Workaround remains switching providers only after the current turn ends.

Investigated with Codex (gpt-6.1-sol) using the t3 triage playbook. Triage CLI and affected server both reported 0.1.8-nightly.20261104.4179.