Agents Window: a model picked in the Workbench picker never reaches session.modelId
- Dominant language
- TypeScript
- Stars
- 193k
- Forks
- 42.4k
- PR merge metrics
- PR metrics pending
Description
## Summary
A model picked in the embedded Workbench chat picker never reaches `session.modelId`, so the Sessions picker and the Workbench picker can show different models for the same conversation until something re-syncs. Whichever refreshes last wins.
This is the remaining half of the divergence investigated during #331282. The destructive half — a reopened session being switched to whatever model was last used elsewhere in the profile — is fixed there. This one is not, and is cosmetic rather than corrupting: nothing incorrect is written to the backend, the two surfaces just disagree on screen.
## Evidence
From an `AI debug` window log (agent-host `copilotcli` session `2446d56e…`):
- The user picks GPT‑5.6 in the Workbench picker: `event=explicit-selection surface="workbench" model="agent-host-copilotcli:gpt-5.6-sol"`
- Two minutes later the Sessions side still reports `desiredModel="agent-host-copilotcli:claude-opus-5"` for that same `conversationKey`, while the Workbench side restores `gpt-5.6-sol`.
## Why — the loop is complete except for one link
The wiring already exists end to end. `_inputStateToDraft` (`agentHostSessionHandler.ts:5271`) **already carries the model** out to the host:
```ts
const model = this._createModelSelection(state.selectedModel?.identifier, state.modelConfiguration);
...
...(model ? { model } : {}),
```
| Direction | Path | Status |
|---|---|---|
| Sessions → Workbench | `setModel` → `_updateChatSessionState` → `inputModel.setState` | works |
| Workbench → host draft | picker → `_syncInputStateToModel` → `_installDraftSync` → `ChatState.draft.model` | works |
| host draft → `session.modelId` | `_hydrateModelFromDraft` | **one-shot** |
`_hydrateModelFromDraft` (added in #331282, `baseAgentHostSessionsProvider.ts`) reads `ChatState.draft.model` but calls `listener.clear()` as soon as `modelId` is defined — copied from `_hydrateAgentFromDraft` beside it, where one-shot is correct. Both subscribe to the same chat channel, so the value genuinely flows; it is simply no longer being observed.
Note this is **not** a `ChatInputPart` problem, which is where I first assumed it lived.
## Proposed fix
Roughly 20–30 lines in `baseAgentHostSessionsProvider.ts`:
1. Drop the one-shot `listener.clear()` so the observer keeps mirroring
2. Guard on "the draft model differs from the current `modelId`" so a Sessions write echoing back does not loop
3. Record mirrored changes as `ChatModelSource.Restored` — the draft records *what* the conversation runs on, never *who* chose it
## Worth proving, not assuming
- **Echo loop.** Sessions write → input state → draft → observer → `setChatModelId`. The guard must actually break it; worth a test rather than an assumption.
- **Provenance coarsening.** A `User` pick echoing back arrives as `Restored`. Both count as the conversation's own under the precedence rule in `chatInputModelSelectionController.ts`, so no outcome changes — only the label. Same trade already accepted for canonicalized writes in #331282.
- **Asymmetry with the agent**, which stays one-shot. Defensible (the model has a demonstrated bug, the agent does not) but should carry a comment so it reads as deliberate.
- **Cross-client.** A peer changing the draft model would begin moving `session.modelId`. Probably correct — it *is* the conversation's model — but it is a behavior change beyond the logged bug and should be a conscious decision.
## Context
- #331282 shares one model-selection policy between Workbench chat and the Agents Window, and fixes the destructive direction plus the missing model hydration.
- The invariant this would complete is documented in `src/vs/sessions/SESSIONS.md` under "Model selection".
Contributor guide
Assessment
This issue has not been assessed yet.