Repository navigation
[Bug]: Desktop keeps the old model after the same thread changes on mobile #10684
Description
Activity
Triage
Confirmed as a real web composer-state bug. Complementary to #10202, not a duplicate of #10202, #6508, or #5421. PR #10205 is mobile-only and does not cover this direction.
What we verified
On current
main, the desktop composer prefers a persisted thread-draft model over the server thread model for both display and the next send.deriveEffectiveComposerModelStateresolves the draft map first and only falls back tothreadModelSelectionwhen that map has no entry for the selected instance:const instanceSelection = input.selectedInstanceId ? input.draft?.modelSelectionByProvider?.[input.selectedInstanceId] : undefined; const activeSelection = instanceSelection ?? legacySelection; const selectedModel = activeSelection?.model ? /* draft */ : baseModel;
modelSelectionExplicitis not an input and does not gate this precedence. Existing tests document the draft winning over a different thread model (modelSelection.test.ts, custom-instance draft vs thread selection).For a thread created on desktop, that stored selection is seeded once in
useHandleNewThread(applyStickyState/setModelSelectionwithoutexplicit: true). Promotion copies the whole draft onto the server-thread key (finalizePromotedDraftThread→removeDraftThreadReferences). After send,clearComposerContentkeepsmodelSelectionByProvider. Persistence keeps model-only server-thread drafts;stripLegacyModelSeedsFromEmptyDraftSessionsonly strips empty draft sessions, and the migration test explicitly retains a server-thread seed.thread.meta-updateddoes updatethread.modelSelectioninthreadReducer. Nothing re-seeds or clears the stored draft selection, so the composer never follows the remote change.The send path then syncs the stale composer model back.
persistThreadSettingsForNextTurncallsresolveThreadMetadataUpdateForNextTurn, which issuesthread.meta.updatewhenever the composer selection differs fromserverThread.modelSelection. That is the silent-reversal mechanism: the UI already shows A, so writing A back is invisible.The reported thread (
9954b2b6-9772-43f0-a447-94863a11b15f) is consistent with this path: mobile wrote two later models; the projection kept the last one; desktop stayed on the original seed.Related work (not duplicates)
- [Bug]: Mobile keeps an old model after the same thread changes on desktop #10202 is the mirror case (desktop changes, mobile goes stale). Same class of bug, opposite client.
- fix(mobile): release confirmed thread model overrides #10205 fixes the mobile draft/outbox store only. It does not change
apps/web. - [Bug]: New threads in the same project do not preserve the last-used model and working-mode settings #6508 is about new-thread defaults, not same-thread multi-client sync.
- Web: sticky provider options win over stale thread selection #5421 is web sticky provider options, and is explicitly scoped away from the selected model.
Web already has
modelSelectionExplicitfor new-thread reseeding. That flag is not enough here: seeded selections still win inderiveEffectiveComposerModelState, confirmed picks surviveclearComposerContent, and no handler consumesthread.meta-updated.Suggested fix
Treat a stored draft model on an existing thread as an unsent override, in the same spirit as #10205:
- After this client’s choice is confirmed by the server, drop or re-seed the thread-draft model so later compose/send follow
thread.modelSelection. - When
thread.meta-updated(orthread.turn-start-requested) changes the thread model, re-seed the stored draft unless this client made a newer explicit pick. - Keep a pick made on this client after the last confirmed send, including a same-value re-pick.
- Restore the draft model on failed send.
The existing
modelSelectionExplicitflag is a useful hook, but the fix also needs a release-after-confirm (or equivalent pick identity) plus a remote-meta update path. Do not wait on #10205.Verification
Need desktop A → mobile B → desktop on the same thread, checking both the picker and the next requested model. Also cover: unsent desktop pick C that must survive a remote B; failed send; empty vs non-empty draft; restart so a persisted seed cannot resurrect A. Provider: OpenCode. Linux desktop + mobile.
Labels
bug,accepted,via-triage(removeneeds-triage)- addedvia-triageFiled through npx t3 triageFiled through npx t3 triageacceptedfeature request acceptedfeature request acceptedbugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.
on Sep 8, 2026 - added a commit that references this issue
on Sep 8, 2026 Correction: the "responding as human here" line in my previous comment was a mistake by the triage agent — I am an agent filing on behalf of @rohitgirdhar, not the human user. Sorry for the confusion.
To keep this clean I opened a focused regression task with the unfixed scope (same-instance opencode model switch, code refs, version numbers): #15195. Please follow that one; this comment thread can be ignored/closed as duplicate.
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.