Composer shows Cursor 'Auto' for a thread whose stored model it can't resolve; sending would switch the model#16526

openedOct 6, 2026bySsonhyrd

Version: v0.0.46-nightly.20261005.2702 (desktop app plus a desktop-managed SSH environment)

What happens

A subagent thread created with delegate_task and target { providerInstanceId: "cursor", model: "claude-opus-5-5", options: { effort: "medium" } } runs on the right model: the provider log shows agent.open and run.start with model: { id: "claude-opus-5-5", params: [{ id: "effort", value: "medium" }] }. t3_thread_configuration returns that modelSelection too.

But that thread's composer model picker shows "Auto" with the "Balance" trait: Cursor's auto-smart and its default optimize_for.

Why (reading the source at cfa4f765)

  • The composer derives its selection from the thread's modelSelection (ChatComposer.tsx:2147-2155 to composerDraftStore.ts:1248-1335). That part is fine.
  • resolveAppModelSelectionForInstance (apps/web/src/modelSelection.ts:288-326) matches the stored model against the instance's model options, after applyInstanceModelPreferences drops hidden models (:141-153). When nothing matches, only opencode and antigravity keep the unknown model (:100, :313-324). Cursor falls back to options.find(isDefault) ?? options[0], which is auto-smart. The trait picker then shows auto-smart's defaults and drops effort.
  • The server forwards the stored id unchanged (cursorSdkModel.ts:37-50), so the run and the display disagree.

We couldn't find which case triggers it here: no providerModelPreferences exist on either server, and claude-opus-5-5 is in the instance's Cursor model list from orchestrator_capabilities. It may be a client-side preference, a stale composer draft, or a list mismatch in the SSH environment.

Why it matters

Sending a message from that composer uses the displayed selection (ChatComposer.tsx:2314). Replying in the thread would silently switch it to Cursor Auto.

Expected

When the stored model can't be resolved, the picker should show the stored model as "unavailable", or keep it as for opencode and antigravity. It shouldn't display, and then send, a different model.

Note

Grok responding on behalf of Julius.

Triage

Thanks @sonhyrd for the clear report and the pointer into resolveAppModelSelectionForInstance. Reading current main, this looks like a real composer/send mismatch, not only a label glitch.

What I found

  • The stored selection is what the server runs. cursorSdkModelSelection forwards modelSelection.model and its options unchanged (apps/server/src/provider/cursorSdkModel.ts).
  • The composer resolves a different one. deriveEffectiveComposerModelState resolves the thread model through resolveAppModelSelectionForInstance, and for Cursor an unmatched slug becomes options.find(isDefault)?.slug ?? options[0]?.slug (apps/web/src/modelSelection.ts). Cursor's discovered models never set isDefault (buildCursorDiscoveredModelsFromSdk), so that is the first catalog row. Auto / Balance is that row when it is auto-smart: the trait picker reads the resolved model's descriptors, so the stored effort: medium is dropped and optimize_for shows its default. Only OpenCode and Antigravity keep an unknown slug as unavailable. That split is intentional in appendUnavailableDynamicModelSelection and covered by modelSelection.test.ts ("does not add unavailable options for other providers").
  • Sending uses the resolved selection. ChatComposer builds selectedModelSelection from it, and startThreadTurn puts that on message.dispatch. If it differs from the thread, the orchestrator emits thread.model-selection-updated and the next run uses it (Orchestrator.ts, the modelSelectionsEqual check in dispatch). A reply from that composer would switch this thread to Auto.
  • Why the catalog might not contain the stored slug. orchestrator_capabilities listing claude-opus-5-5 does not by itself prove the composer's option list contains it. That tool returns provider.models with no preference filter. The composer then hides slugs in settings.providerModelPreferences, and a per-thread composer draft (modelSelectionByProvider for that instance) replaces the thread selection even when the stored slug would resolve. That draft is on the desktop client, not in either server's settings. A client snapshot that is stale or for a different instance fails the same way and lands on the first row.
  • Related. No open duplicate of this Cursor fallback. Nearby reports (Claude permission Auto, Codex effort, hidden-default settings) are different mechanisms.

Likely fix area

apps/web/src/modelSelection.ts / composer model resolution. Some options:

  • Treat an unmatched Cursor slug as unavailable the way OpenCode and Antigravity do, instead of falling back to the first catalog row.
  • Keep the stored selection for send when the composer only failed to match it for display.

Workaround: don't send from that composer. t3_thread_send / another turn started from the stored selection keeps claude-opus-5-5.

A maintainer will decide on the fix direction.