[Bug]: Desktop keeps the old model after the same thread changes on mobile
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 23k
- Forks
- 5.9k
- Avg merge
- 11h 14m
- Merged PRs (30d)
- 357
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
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 storedmodelSelectionByProvideroverthreadModelSelectionfor both the displayed model and the sent model. ThemodelSelectionExplicitflag does not gate this precedence.- For an existing thread the draft is seeded once at thread creation (
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. - The send path's
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
- #10202 is the mirror case (desktop changes, mobile goes stale) and is triage-confirmed. PR #10205 fixes the mobile draft store. The web direction is not covered by that fix.
- #5421 is about sticky provider options on web and is explicitly scoped away from the model.
- #6508 is about new-thread defaults, not same-thread changes from another client.
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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in apps/web/src/composerDraftStore.ts with deriveEffectiveComposerModelState, then trace applyStickyState, setModelSelection in useHandleNewThread, and thread.meta-updated handling. Read ChatView's persistThreadSettingsForNextTurn to understand the reversal path. Done means the desktop composer follows confirmed server changes, preserves a newer explicit local pick, and does not silently revert the thread on send.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100