Skip to content

regression: same-instance (opencode) model switch on mobile still leaves desktop composer stale on Orchestrator v2 #15195

Description

@rohitgirdhar

Summary

Regression / incomplete fix: on Orchestrator v2 nightly, switching model within the same provider instance on mobile does not update the desktop composer for the same thread. The desktop keeps showing the old model and can silently switch the thread back on next send. This is the intra-instance variant of #10684, which was closed COMPLETED by #2829.

Environment

  • opencode version: 2.0.22
  • OS: Linux hp-laptop 7.0.0-34-generic x86_64
  • T3 desktop: 0.0.46-nightly.20261003.2623
  • T3 mobile: 2.0.0
  • Reporter: @rohitgirdhar (filed via agent triage)

Reproduction

  1. Open the same T3 thread on phone + desktop, provider opencode.
  2. Both show model A: opencode/space-bunny-free.
  3. On phone, switch same thread to model B: opencode/muse-spark-1.3-contributor-free.
  4. Send "Testing again" from phone — server runs with model B.
  5. Desktop composer / thread header still shows model A. Persists without manual refresh.

Expected Behavior

Desktop composer follows the server thread model after another client changes it, for both cross-provider and same-instance model changes. A newer explicit pick on that client should still win.

Actual Behavior

Desktop stays on A. Same silent-reversal mechanism documented in #10684: stale composer model gets written back via thread.meta.update on next desktop send.

What was fixed vs what is not

Fixed (per #2829 body): V2 added durable thread.model-selection.set command + thread.model-selection-updated projection replacing per-client sticky state. That covers server durability and the inter-provider handoff path.

NOT fixed: web composer precedence in apps/web/src/composerDraftStore.ts (deriveEffectiveComposerModelState, current main ~L1297-1313):

const instanceSelection = input.selectedInstanceId
  ? input.draft?.modelSelectionByProvider?.[input.selectedInstanceId]
  : undefined;
const activeSelection = instanceSelection ?? legacySelection;
const selectedModel = activeSelection?.model ? /* draft */ : baseModel;

baseModel comes from threadModelSelection, but only wins when the draft map has no entry for that selectedInstanceId. space-bunny-free → muse-spark-... share selectedInstanceId = opencode, so the seeded desktop draft (seeded once via applyStickyState, survives promotion + clearComposerContent, per #10684 triage) keeps winning. thread.meta-updated updates thread.modelSelection in threadReducer but nothing re-seeds/clears the draft entry.

#2829's diff to composerDraftStore.ts is +312/-15 adding only threadContexts / stickyOptionsByModelByProvider — no change to this precedence, no thread.model-selection-updated reseed path. So same-instance switches remain stale.

Links

Request

Reopen or track as regression of #10684 scoped to same-instance model switch on web: re-seed/clear stored draft selection when thread.model-selection-updated / thread.meta-updated changes the thread model, unless this client made a newer explicit pick (per #10684 suggested fix points 1-4).

Activity

  1. juliusmarminge commented on Oct 3, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Triage

    Thanks @rohitgirdhar for the precise report. I confirmed this on current main (31a9da179e). It's the same web composer behavior as #10684 and is still present. The merged #2829 closed #10684 and #10202 because V2 now stores the thread model on the server, but it didn't change how the desktop composer chooses which model to show and send.

    What I verified

    deriveEffectiveComposerModelState still prefers the draft entry for the selected instance and only falls back to threadModelSelection when that entry is missing:

    const instanceSelection = input.selectedInstanceId
      ? input.draft?.modelSelectionByProvider?.[input.selectedInstanceId]
      : undefined;
    const activeSelection = instanceSelection ?? legacySelection;
    const selectedModel = activeSelection?.model ? /* draft */ : baseModel;

    modelSelectionExplicit isn't an input, and modelSelection.test.ts still expects a custom-instance draft model to beat a different thread selection.

    A thread created on desktop gets that draft entry from applyStickyState / setModelSelection. Promotion copies the whole draft onto the server-thread key (finalizePromotedDraftThread → removeDraftThreadReferences). clearComposerContent clears text and attachments but leaves modelSelectionByProvider and activeProvider, and persistence keeps model-only server-thread drafts. Nothing on thread.model-selection-updated or thread.provider-switched re-seeds or clears it.

    resolveComposerProviderSelection then tries draft.activeProvider before the thread's instance. OpenCode advertises supportsProviderSwitchingViaHandoff, so the composer isn't locked to the thread's instance. Your two models share the opencode instance, so the seeded draft keeps winning. A desktop that never stored a selection for the thread would follow thread.modelSelection, but the usual path (creating the thread or picking a model there) does store one.

    The next desktop send still writes the stale model back: startThreadTurn always sends modelSelection: ctxSelectedModelSelection, and if that differs from the thread, the orchestrator records a thread.model-selection-updated (or thread.provider-switched across instances). Since the picker already shows the old model, that reversal is invisible. Sidebar rows that read thread.modelSelection should update; the stale surface is the composer picker and the next send from it.

    The same precedence also affects switches across providers whenever this client still has a draft for the old instance. Same-instance switches are just the case where the draft entry can never miss.

    Related

    Likely fix area

    A maintainer will decide on the fix direction. Options in the same shape as #10205, on the web draft:

    1. After the server confirms this client's choice, drop or re-seed the thread-draft model so later sends follow thread.modelSelection.
    2. When thread.model-selection-updated or thread.provider-switched changes the thread model, re-seed unless this client made a newer explicit pick.
    3. Keep a pick made here after the last confirmed send, including a same-value re-pick, and restore it if the send fails.

    How to verify

    Desktop A → mobile B on the same OpenCode instance → the desktop picker shows B and the next desktop send requests B. Also: an unsent desktop pick C survives a remote B, a failed send keeps C, and reopening the thread doesn't bring back a persisted A.

  2. added
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Oct 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions