Repository navigation
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/web
Summary
After a thread has started, the web composer can follow a model selection the user never made. New-thread drafts are seeded without the explicit flag from sticky state and from defaultModelSelection. When the draft is promoted, those seeds move with it to the server thread, and they still take priority over the thread's own modelSelection. This causes two problems:
- Now: a phantom "Context handoff pending · Claude Opus 5.5 → Claude Sonnet 5.5" appears under every turn (Phantom "Context handoff pending" when the composer's sticky selection points at a disabled provider instance #19956), because the banner reads the raw draft selection.
- Later: while the seeded provider is disabled, the send path skips it, so turns run on the thread's model. Once that provider is enabled again, it becomes the first candidate, and the next send switches the thread to it (a cross-provider handoff) even though no one picked it. fix(web,mobile): no pending handoff for a provider the next send would skip #19523's own verification table shows this: "Turn Copilot back on, picker untouched → divider returns and the picker switches".
#19523 only hides the banner while the seeded provider is unavailable. It doesn't stop the seed from overriding the thread.
Steps to reproduce
- In Settings, set the default model to an instance that is later disabled. For example,
defaultModelSelection = claudeAgent / claude-sonnet-5-5. - Disable that instance. Keep another enabled, for example a custom
cliproxyinstance using the codex driver. - Start a new thread without touching the picker. The draft is seeded with
activeProvider = claudeAgent, andmodelSelectionExplicitstays unset. The send falls back tocliproxy / claude-opus-5-5. - After the first turn, "Context handoff pending · Claude Opus 5.5 → Claude Sonnet 5.5" appears and stays.
- Re-enable
claudeAgentand send without touching the picker. The picker moves to Claude Sonnet 5.5, and the turn is handed off to a provider nobody picked.
Expected behavior
A started thread keeps the model it runs on. Only a model or trait the user picks for that thread should change it or show a pending handoff.
Actual behavior
On a started thread, a non-explicit draft seed outranks the thread's model in three places:
- the composer's candidate list (
ChatComposer.tsx~2278,composerDraft.activeProviderfirst) - the effective model and options (
deriveEffectiveComposerModelState, draft per-instance entries first) - the pending-handoff banner (
ChatView.tsx~1208 →resolvePendingHandoff)
For handoff-capable V2 threads the picker isn't locked (modelPickerLockedProvider = null), so the only thing stopping the seed today is the enabled && isAvailable filter (ChatView.logic.ts:589).
Related writer gaps in setProviderModelOptions:
- It keeps the stored per-instance model ahead of the model the composer shows. A traits or effort change can therefore make a hidden seed explicit.
- Its no-op early return ignores
modelSelectionExplicitandactiveProvider.
Impact
Major degradation or frequent failure. Every affected thread shows a misleading banner. Re-enabling a provider silently moves threads to a different provider and model.
Version or commit
main @ d0918f1. Seen on nightly 0.1.1-nightly.20261017.3238.
Environment
Windows desktop app, local server. Instances: cliproxy (codex driver) enabled; built-in claudeAgent, codex and cursor disabled.
Logs or stack traces
# settings.json
defaultModelSelection = claudeAgent / claude-sonnet-5-5 {effort: medium}
projectSettingsOverrides[<project>].defaultModelSelection = codex / gpt-6.1-sol
providerInstances: claudeAgent=disabled, codex=disabled, cliproxy=enabled
# persisted t3code:composer-drafts:v1, server-thread drafts vs thread modelSelection
25 server-thread drafts; 4 disagree with their thread and have modelSelectionExplicit unset:
thread cliproxy/claude-opus-5-5 -> draft claudeAgent/claude-sonnet-5-5 (x3, == global default incl. effort: medium)
thread cliproxy/gpt-6.1-sol -> draft codex/gpt-6.1-sol (== project default)Workaround
Pick the thread's current model once in the picker. That sets modelSelectionExplicit on the draft.
Related
#19956 (the phantom banner symptom) and #19523 (hides the banner while the target is unavailable). This report covers the selection precedence that causes both. A fix is proposed in the linked PR.
Activity
SwiggitySwerve commentedon Oct 18, 2026
Proposed fix: #21140. On a started thread, a draft selection only overrides the thread's model when
modelSelectionExplicitis set. The same rule now applies to the composer, the pending-handoff banner, new-thread carry and prompt links.setProviderModelOptionsalso keeps the model the composer shows, so a traits change can't make a hidden seed explicit again.t3dotgg commentedon Oct 21, 2026
Note
🤖 Claude Opus 5.5 responding on behalf of Theo
Thanks for the report. One part of this is fixed on main in #19523. Please test on the nightly built from main on 2026-10-21, or on the next stable release. One part is still open: a non-explicit seed can still override a started thread after its provider is enabled. If that part still matters to you, please open a new issue for that part only.