Before submitting
Area
apps/web
Steps to reproduce
- Start a new thread on desktop with model A, either picked in the composer or seeded from sticky/project defaults.
- Send a message. The thread runs on A.
- Switch the same thread's model to B from the mobile app and send a message there. The server records the change and the phone shows B.
- Return to the desktop app and open the same thread.
- The composer still shows A, even though the thread is running on B.
- Send another message from desktop.
Expected behavior
The desktop composer follows the thread's server-side model after another client changes it. A newer explicit pick made on that client should override it.
Actual behavior
The composer keeps showing A. Step 6 is worse than a stale label: the desktop send path syncs the composer's model back into the thread, so the thread's model silently reverts to A and the turn runs on A. The UI never changes (it was already showing A), so the reversal is invisible to the user.
Evidence from the affected thread
Thread 9954b2b6-9772-43f0-a447-94863a11b15f (OpenCode provider, Linux desktop + mobile). Read-only check of the local server DB, 2026-09-08 UTC:
| Time |
Event |
Model |
| 03:05:30 |
thread.created |
nvidia/moonshotai/kimi-k3 (variant max) |
| 04:29:36 |
thread.meta-updated, then turn-start-requested |
opencode/muse-spark-1.3-contributor-free (variant xhigh) |
| 04:30:10 |
thread.meta-updated, then turn-start-requested |
asu/glm-5-3-flash |
Both model changes came from the phone, 34 seconds apart. The projection's model_selection_json points at asu/glm-5-3-flash and later turns use it. The desktop composer kept showing the original model for the rest of the session.
Where the behavior lives in the code
deriveEffectiveComposerModelState (apps/web/src/composerDraftStore.ts) prefers the draft's stored modelSelectionByProvider over threadModelSelection for both the displayed model and the sent model. The modelSelectionExplicit flag does not gate this precedence.
- For an existing thread the draft is seeded once at thread creation (
applyStickyState / setModelSelection in useHandleNewThread) and the seed survives draft promotion. Nothing re-seeds or clears the stored draft selection when thread.meta-updated arrives with a different model. The thread reducer does update thread.modelSelection from that event, but the composer never consults it once the draft holds a stored selection.
- The send path's
persistThreadSettingsForNextTurn (ChatView) compares the composer model with serverThread.modelSelection and issues thread.meta.update when they differ. That is the silent-reversal mechanism.
Impact
Major degradation or frequent failure. The UI lies about which model is running, and the next send from the stale client flips the thread's model with no visible signal.
Version or commit
Current desktop and mobile releases, observed 2026-09-08. Code path verified against current main.
Environment
Linux, T3 Code desktop app and mobile app, OpenCode provider.
Related work
Suggested fix
Treat a stored draft model for an existing thread the way #10205 treats the mobile one: an override that is released once the server confirms its use. When thread.meta-updated changes the thread model, re-seed the stored draft selection unless this client made a newer explicit pick. Keep a pick made on this client after the last confirmed send, and restore it if a send fails.
Before submitting
Area
apps/web
Steps to reproduce
Expected behavior
The desktop composer follows the thread's server-side model after another client changes it. A newer explicit pick made on that client should override it.
Actual behavior
The composer keeps showing A. Step 6 is worse than a stale label: the desktop send path syncs the composer's model back into the thread, so the thread's model silently reverts to A and the turn runs on A. The UI never changes (it was already showing A), so the reversal is invisible to the user.
Evidence from the affected thread
Thread
9954b2b6-9772-43f0-a447-94863a11b15f(OpenCode provider, Linux desktop + mobile). Read-only check of the local server DB, 2026-09-08 UTC:Both model changes came from the phone, 34 seconds apart. The projection's
model_selection_jsonpoints atasu/glm-5-3-flashand later turns use it. The desktop composer kept showing the original model for the rest of the session.Where the behavior lives in the code
deriveEffectiveComposerModelState(apps/web/src/composerDraftStore.ts) prefers the draft's storedmodelSelectionByProvideroverthreadModelSelectionfor both the displayed model and the sent model. ThemodelSelectionExplicitflag does not gate this precedence.applyStickyState/setModelSelectionin useHandleNewThread) and the seed survives draft promotion. Nothing re-seeds or clears the stored draft selection whenthread.meta-updatedarrives with a different model. The thread reducer does updatethread.modelSelectionfrom that event, but the composer never consults it once the draft holds a stored selection.persistThreadSettingsForNextTurn(ChatView) compares the composer model withserverThread.modelSelectionand issuesthread.meta.updatewhen they differ. That is the silent-reversal mechanism.Impact
Major degradation or frequent failure. The UI lies about which model is running, and the next send from the stale client flips the thread's model with no visible signal.
Version or commit
Current desktop and mobile releases, observed 2026-09-08. Code path verified against current main.
Environment
Linux, T3 Code desktop app and mobile app, OpenCode provider.
Related work
Suggested fix
Treat a stored draft model for an existing thread the way #10205 treats the mobile one: an override that is released once the server confirms its use. When
thread.meta-updatedchanges the thread model, re-seed the stored draft selection unless this client made a newer explicit pick. Keep a pick made on this client after the last confirmed send, and restore it if a send fails.