Skip to content

Model swap gets clobbered by model-less prompts (plugin reminders) #42893

Closed

Description

@Qiiks

Bug

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:

input.model ?? ag.model ?? currentModel(sessionID)

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

  1. opencode serve → create a session with an agent that has a configured model (e.g. GLM-5.2)
  2. Send a prompt with an explicit model swap (e.g. deepseek-v4-flash)
  3. Send a model-less prompt (e.g. a plugin reminder via promptAsync without model)
  4. 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.

Activity

  1. github-actions commented on Aug 16, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    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.

  2. tbontb-iaq commented on Aug 25, 2026

    @tbontb-iaq

    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

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

  3. neriousy commented on Oct 19, 2026

    @neriousy
    Member

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

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