You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Model swap gets clobbered by model-less prompts (plugin reminders) #42893
When a user swaps the model in an opencode serve / mobile session (e.g. to deepseek-v4-flash), the swap is lost on the next model-less prompt — typically a plugin completion-reminder (e.g. oh-my-openagent's promptAsync without a --model flag). The session row reverts to the agent's configured model.
The TUI is unaffected because it always sends model: input.draft.model explicitly on every prompt.
Root cause
In packages/opencode/src/session/prompt.ts, createUserMessage resolves the model as:
When input.model is absent, ag.model (the agent's configured model) wins over the session's persisted model (the user's swap). Then setAgentModel overwrites the session row to the resolved model, permanently clobbering the swap.
This was introduced by #26765 ("Persist session model switches outside event flag"), which intentionally made ag.model outrank currentModel so an agent's configured model applies when no explicit model is given. The design is correct for the turn's model — but setAgentModel should not overwrite the session row when the model was resolved from the agent fallback rather than an explicit user choice.
Repro
opencode serve → create a session with an agent that has a configured model (e.g. GLM-5.2)
Send a prompt with an explicit model swap (e.g. deepseek-v4-flash)
Send a model-less prompt (e.g. a plugin reminder via promptAsync without model)
Query the session row — it reverts to the agent's model (GLM-5.2), losing the swap
Fix
Guard the setAgentModel write: skip it when the prompt carried no explicit model AND the session already has a stored model that differs from the agent's resolved model. Fresh sessions (no stored model) and explicit-model prompts still persist normally.
The model resolution precedence (input.model ?? ag.model ?? currentModel) is unchanged — model-less prompts still use the agent's model for the turn (existing design, tested by "applies agent variant only when using agent model"). Only the row persistence is guarded.
Your report is well-detailed and the opencode serve / plugin-reminder angle is a useful specific repro. It may be worth cross-referencing or consolidating with the above rather than tracking separately.
Reproduced on CLI 1.18.21 with a third-party plugin injection path, with database forensics. Adding a data point your fix write-up may want to account for: the clobber propagates through downstream model-less injections, not just the immediate turn.
Repro (CLI, plugin reminder path)
Agent pinned to volcengine-agent-plan/glm-5.3 via a plugin (oh-my-openagent's Sisyphus agent), user switched to opencode/x-preview-f-free mid-session. Injected message: opencode-pty's <pty_exited> notification (session.promptAsync with agent but no model).
User-message rows in opencode.db for that session, in order:
role model text
user opencode/x-preview-f-free (variant max) manual message (B)
user opencode/x-preview-f-free (variant max) manual message (B)
user volcengine-agent-plan/glm-5.3 <pty_exited> injection (reverted to A)
user volcengine-agent-plan/glm-5.3 [SYSTEM DIRECTIVE: ... TODO CONTINUATION] (inherited A)
user opencode/x-preview-f-free (variant max) manual "继续" (re-pinned B)
The first revert is exactly the mechanism described above (ag.model wins, setAgentModel persists it). But note rows 4 and 5: oh-my-openagent's continuation hooks resolve the model from the most recent message that carries one, so once the <pty_exited> turn stores model A, every subsequent model-less injection from that plugin also resolves to A — the clobber is self-reinforcing until the next manual message re-pins the selection. A guard on setAgentModel alone fixes the session row, but any injection that explicitly re-reads message history for its model (like this plugin does) will still inherit the wrong turn model unless the injected turn itself resolves to the session's current model.
Two thoughts on the proposed guard
Guarding persistence is necessary but arguably not sufficient. Even if the session row keeps model B, the turn itself still runs on ag.model (A) under the unchanged precedence. For programmatic wake-ups (promptAsync from plugins), answering on a model the user didn't select is still surprising — the user sees their session "answer" a <pty_exited> on a different model than every message around it. It might be worth considering precedence input.model ?? currentModel(sessionID) ?? ag.model for sessions that already have a stored model, keeping ag.model authoritative only for fresh sessions. That would also fix the downstream-inheritance problem above for free.
variant is clobbered too. In the trace above the manual selection carried variant: "max"; the injected turn's stored model dropped it (agent model has no variant). Worth including in the guard's comparison so a guard doesn't also silently strip a user's variant.
Also for cross-referencing: the plugin-side half of my repro (pty exit notification omitting model entirely) is filed at shekohex/opencode-pty#57 — the plugin can pass session.get(...).model through in promptAsync, same as the TUI does. But as this issue argues, core shouldn't rely on every injector being diligent about it.
V2 keeps model selection separate from prompt submission. A plugin reminder without a model uses the session's selected model and variant instead of replacing them with the agent default. To get v2: npm i -g @opencode/cli@latest or curl -fsSL https://opencode.ai/v2/install | bash (desktop: update the app). If you still see this on v2, please open a new issue with the details.
Activity
github-actions commentedon Aug 16, 2026
This issue might be a duplicate of existing issues. Please check:
setAgentModeloverwriting the session row when the model was resolved from agent fallback), same fix directionYour report is well-detailed and the
opencode serve/ plugin-reminder angle is a useful specific repro. It may be worth cross-referencing or consolidating with the above rather than tracking separately.tbontb-iaq commentedon Aug 25, 2026
Reproduced on CLI 1.18.21 with a third-party plugin injection path, with database forensics. Adding a data point your fix write-up may want to account for: the clobber propagates through downstream model-less injections, not just the immediate turn.
Repro (CLI, plugin reminder path)
Agent pinned to
volcengine-agent-plan/glm-5.3via a plugin (oh-my-openagent's Sisyphus agent), user switched toopencode/x-preview-f-freemid-session. Injected message:opencode-pty's<pty_exited>notification (session.promptAsyncwithagentbut nomodel).User-message rows in
opencode.dbfor that session, in order:The first revert is exactly the mechanism described above (
ag.modelwins,setAgentModelpersists it). But note rows 4 and 5: oh-my-openagent's continuation hooks resolve the model from the most recent message that carries one, so once the<pty_exited>turn stores model A, every subsequent model-less injection from that plugin also resolves to A — the clobber is self-reinforcing until the next manual message re-pins the selection. A guard onsetAgentModelalone fixes the session row, but any injection that explicitly re-reads message history for its model (like this plugin does) will still inherit the wrong turn model unless the injected turn itself resolves to the session's current model.Two thoughts on the proposed guard
ag.model(A) under the unchanged precedence. For programmatic wake-ups (promptAsyncfrom plugins), answering on a model the user didn't select is still surprising — the user sees their session "answer" a<pty_exited>on a different model than every message around it. It might be worth considering precedenceinput.model ?? currentModel(sessionID) ?? ag.modelfor sessions that already have a stored model, keepingag.modelauthoritative only for fresh sessions. That would also fix the downstream-inheritance problem above for free.variantis clobbered too. In the trace above the manual selection carriedvariant: "max"; the injected turn's stored model dropped it (agent model has no variant). Worth including in the guard's comparison so a guard doesn't also silently strip a user's variant.Also for cross-referencing: the plugin-side half of my repro (pty exit notification omitting
modelentirely) is filed at shekohex/opencode-pty#57 — the plugin can passsession.get(...).modelthrough inpromptAsync, same as the TUI does. But as this issue argues, core shouldn't rely on every injector being diligent about it.neriousy commentedon Oct 19, 2026
V2 keeps model selection separate from prompt submission. A plugin reminder without a model uses the session's selected model and variant instead of replacing them with the agent default. To get v2:
npm i -g @opencode/cli@latestorcurl -fsSL https://opencode.ai/v2/install | bash(desktop: update the app). If you still see this on v2, please open a new issue with the details.