You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I run a Claude instance and an OpenRouter instance (opencode driver) side by side, plus local Ollama. Partway through a thread I often want to hand the same conversation to a different provider — start on Claude, continue on OpenRouter, or drop to a local model for something cheap. Today a thread is pinned to whatever provider it started on:
Thread '…' is bound to driver 'claudeAgent' and cannot switch to 'opencode'.
Thread '…' cannot switch from instance '…' to '…' because their provider resume state is incompatible.
Thread '…' cannot switch models after the conversation has started. Start a new thread to use '…'.
The model picker also disables the entry outright with "Start a new chat to change models." The only workaround is a new thread, which loses the very context I wanted the second provider to have.
Why the guards exist — I don't think they're a bug
Reading apps/server/src/orchestration/Layers/ProviderCommandReactor.ts, these look deliberate. A thread's conversation lives inside the provider's own session (codex thread id, Claude session, opencode session), and continuationIdentity.continuationKey decides whether resume state can carry across. Two different drivers share no resume state, so allowing the switch naively would silently hand the new provider an empty conversation. Refusing is the safe default. I'm not proposing to just delete the checks.
Proposal — treat a cross-provider switch as a re-seat, not a resume
When the driver or continuationKey differs, stop the current provider session instead of erroring.
Start a fresh session on the requested instance.
Seed that session with the conversation the server already has: the thread transcript is persisted in the read model, so it can be replayed into the next turn as a prefix (character-capped, oldest turns dropped first).
This preserves the invariant the guards protect — no provider is ever asked to resume state it doesn't own — while letting the conversation continue.
A second bug the same mechanism fixes
Providers with no resume cursor (opencode, ollama, llamacpp) lose context without any provider switch. Their history is in-memory only, so after an app relaunch — or after the 30-minute idle session reaper — the next turn starts blank and the model answers that it has no prior context, while the full transcript sits in SQLite. Replaying persisted history whenever a session starts without a resume cursor fixes that too. Providers that resume natively (codex, claudeAgent) are untouched, because they return a cursor.
What I actually changed
Three changes, described against the source locations they correspond to:
ProviderCommandReactor.ts — ensureSessionForThread. The driver-mismatch and continuationKey-mismatch branches no longer raise. They set a crossProviderSwitch flag, stop the existing session, and fall through to the existing restart path with no resume cursor.
ProviderCommandReactor.ts — rejectStartedThreadModelChangeIfRequired. Instead of raising for providers flagged requiresNewThreadForModelChange, the result folds into the existing shouldRestartForModelChange condition, so those providers get a clean restart rather than an error. Client side, getStartedThreadModelChangeBlockReason (apps/web/src/components/ChatView.logic.ts:408) is what greys the picker out; it needs to stop returning a block reason for the same case.
History replay. A pending-replay set keyed by thread id, marked whenever a session starts and the started session has no resumeCursor while the thread has persisted messages. On the next buildSendTurnRequestForThread, the stored transcript is rendered oldest-first into a <conversation_history> prefix (~24k char budget, oldest dropped first, the in-flight user message excluded) and prepended to the outgoing turn text. The flag is consumed on use, so it applies once per fresh session.
How it was validated, and on what build
Originally built and run against 0.0.28 (build commit ecb35f758399, 2026-07-15). I have since re-applied the same changes to v0.0.34-nightly.20260817.1120 on macOS arm64 and re-tested there. The changes were applied to the packaged bundle rather than a source build, so the descriptions above are the equivalent source-level changes rather than a diff I can paste verbatim.
What I can state from the 0.0.34 run: switching a started thread between two provider instances no longer raises. I moved one live thread across instances four times (opencode -> openrouter -> opencode -> openrouter) and never hit is bound to driver, resume state is incompatible, or cannot switch models after the conversation has started. Each switch restarted the session and the turn was dispatched to the newly selected provider.
What I could not re-verify on 0.0.34: the history replay actually landing in the second provider's context. Every turn after the switch failed for reasons outside T3 Code — my OpenRouter account restricts allowed upstream providers and then ran out of credits, and the opencode instance wants a billing method. So on this build I have verified the switch path but not the recall. On 0.0.28 the recall did work (the second provider answered using earlier turns), which is what motivated the change.
For completeness: the native Claude driver was unavailable on my machine during the test (Claude Agent CLI is installed but failed to run. Timed out while running command.), so the cross-driver case specifically was exercised on 0.0.28 rather than 0.0.34. The cross-instance case on 0.0.34 takes the same code path, since two instances of one driver have different continuationKeys.
Open questions
Is a character-budgeted text replay the shape you'd want, or would you rather the transcript go through a provider-specific structured history API where one exists?
Should the re-seat be implicit on switch, or surfaced in the UI ("this will restart the session on the new provider") before it happens?
Raising this here rather than as a PR, per CONTRIBUTING — it's a design change, not a small fix. Happy to leave it as a suggestion, or to put up a scoped PR if it's a direction you'd want.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
What I hit as a user
I run a Claude instance and an OpenRouter instance (opencode driver) side by side, plus local Ollama. Partway through a thread I often want to hand the same conversation to a different provider — start on Claude, continue on OpenRouter, or drop to a local model for something cheap. Today a thread is pinned to whatever provider it started on:
Thread '…' is bound to driver 'claudeAgent' and cannot switch to 'opencode'.Thread '…' cannot switch from instance '…' to '…' because their provider resume state is incompatible.Thread '…' cannot switch models after the conversation has started. Start a new thread to use '…'.The model picker also disables the entry outright with "Start a new chat to change models." The only workaround is a new thread, which loses the very context I wanted the second provider to have.
Why the guards exist — I don't think they're a bug
Reading
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts, these look deliberate. A thread's conversation lives inside the provider's own session (codex thread id, Claude session, opencode session), andcontinuationIdentity.continuationKeydecides whether resume state can carry across. Two different drivers share no resume state, so allowing the switch naively would silently hand the new provider an empty conversation. Refusing is the safe default. I'm not proposing to just delete the checks.Proposal — treat a cross-provider switch as a re-seat, not a resume
continuationKeydiffers, stop the current provider session instead of erroring.This preserves the invariant the guards protect — no provider is ever asked to resume state it doesn't own — while letting the conversation continue.
A second bug the same mechanism fixes
Providers with no resume cursor (opencode, ollama, llamacpp) lose context without any provider switch. Their history is in-memory only, so after an app relaunch — or after the 30-minute idle session reaper — the next turn starts blank and the model answers that it has no prior context, while the full transcript sits in SQLite. Replaying persisted history whenever a session starts without a resume cursor fixes that too. Providers that resume natively (codex, claudeAgent) are untouched, because they return a cursor.
What I actually changed
Three changes, described against the source locations they correspond to:
ProviderCommandReactor.ts—ensureSessionForThread. The driver-mismatch andcontinuationKey-mismatch branches no longer raise. They set acrossProviderSwitchflag, stop the existing session, and fall through to the existing restart path with no resume cursor.ProviderCommandReactor.ts—rejectStartedThreadModelChangeIfRequired. Instead of raising for providers flaggedrequiresNewThreadForModelChange, the result folds into the existingshouldRestartForModelChangecondition, so those providers get a clean restart rather than an error. Client side,getStartedThreadModelChangeBlockReason(apps/web/src/components/ChatView.logic.ts:408) is what greys the picker out; it needs to stop returning a block reason for the same case.resumeCursorwhile the thread has persisted messages. On the nextbuildSendTurnRequestForThread, the stored transcript is rendered oldest-first into a<conversation_history>prefix (~24k char budget, oldest dropped first, the in-flight user message excluded) and prepended to the outgoing turn text. The flag is consumed on use, so it applies once per fresh session.How it was validated, and on what build
Originally built and run against 0.0.28 (build commit
ecb35f758399, 2026-07-15). I have since re-applied the same changes to v0.0.34-nightly.20260817.1120 on macOS arm64 and re-tested there. The changes were applied to the packaged bundle rather than a source build, so the descriptions above are the equivalent source-level changes rather than a diff I can paste verbatim.What I can state from the 0.0.34 run: switching a started thread between two provider instances no longer raises. I moved one live thread across instances four times (
opencode->openrouter->opencode->openrouter) and never hitis bound to driver,resume state is incompatible, orcannot switch models after the conversation has started. Each switch restarted the session and the turn was dispatched to the newly selected provider.What I could not re-verify on 0.0.34: the history replay actually landing in the second provider's context. Every turn after the switch failed for reasons outside T3 Code — my OpenRouter account restricts allowed upstream providers and then ran out of credits, and the
opencodeinstance wants a billing method. So on this build I have verified the switch path but not the recall. On 0.0.28 the recall did work (the second provider answered using earlier turns), which is what motivated the change.For completeness: the native Claude driver was unavailable on my machine during the test (
Claude Agent CLI is installed but failed to run. Timed out while running command.), so the cross-driver case specifically was exercised on 0.0.28 rather than 0.0.34. The cross-instance case on 0.0.34 takes the same code path, since two instances of one driver have differentcontinuationKeys.Open questions
Raising this here rather than as a PR, per CONTRIBUTING — it's a design change, not a small fix. Happy to leave it as a suggestion, or to put up a scoped PR if it's a direction you'd want.
All reactions