Skip to content

[Bug]: A seeded composer model overrides a started thread: phantom handoff now, unchosen provider switch once enabled #21139

Closed

Description

@SwiggitySwerve

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:

  1. 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.
  2. 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

  1. In Settings, set the default model to an instance that is later disabled. For example, defaultModelSelection = claudeAgent / claude-sonnet-5-5.
  2. Disable that instance. Keep another enabled, for example a custom cliproxy instance using the codex driver.
  3. Start a new thread without touching the picker. The draft is seeded with activeProvider = claudeAgent, and modelSelectionExplicit stays unset. The send falls back to cliproxy / claude-opus-5-5.
  4. After the first turn, "Context handoff pending · Claude Opus 5.5 → Claude Sonnet 5.5" appears and stays.
  5. Re-enable claudeAgent and 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.activeProvider first)
  • 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 modelSelectionExplicit and activeProvider.

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

  1. SwiggitySwerve commented on Oct 18, 2026

    @SwiggitySwerve
    Author

    Proposed fix: #21140. On a started thread, a draft selection only overrides the thread's model when modelSelectionExplicit is set. The same rule now applies to the composer, the pending-handoff banner, new-thread carry and prompt links. setProviderModelOptions also keeps the model the composer shows, so a traits change can't make a hidden seed explicit again.

  2. t3dotgg commented on Oct 21, 2026

    @t3dotgg
    Member

    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.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions