Skip to content

Plugins using noReply seem to cause model to switch to agent default  #4475

Closed

Description

@omaclaren

Description

Update: see comments. Issue appears to affect plugins using noReply in general not just opencode-skill

Version: OpenCode 1.0.78
Plugin: opencode-skills 0.1.0

Issue: When calling a skill tool (e.g., skills_test_skill), Build mode switches from the manually selected model to its default (?) (in my case openai/gpt-oss-120b).

Expected: Model selection should remain stable when plugin tools execute.

Workaround: Explicitly set mode.build.model in opencode.json

Test skill

in e.g. ~/.opencode/skills/test-skill/SKILL.md):


name: test-skill
description: A minimal test skill to debug loading issues

Test Skill

This is a simple test skill. If you can see this, the skill loaded successfully.

Just respond with "Test skill loaded!" when called.

OpenCode version

1.0.78

Steps to reproduce

  1. Start OpenCode without explicit mode.build.model config
  2. Manually select Claude Haiku 4.5
  3. Verify Build indicator shows "claude-haiku-4-5"
  4. Call any skills_* tool
  5. Build mode indicator switches to "openai/gpt-oss-120b"

Screenshot and/or share link

Image

Operating System

macOS 23.6.0

Terminal

iTerm2

Activity

  1. added
    opentuiThis relates to changes in v1.0, now that opencode uses opentui
    on Nov 19, 2025
  2. removed
    opentuiThis relates to changes in v1.0, now that opencode uses opentui
    on Nov 19, 2025
  3. rekram1-node commented on Nov 19, 2025

    @rekram1-node
    Contributor

    prolly because of the skill plugin setting the model in its request or not using the same model

  4. omaclaren commented on Nov 19, 2025

    @omaclaren
    Author

    Thanks but I don't think this is specific to this plugin (and I don't think the opencode-skills plugin sets a model in its requests).

    To verify, here is a minimal test plugin that uses the same noReply pattern as opencode-skills.

    // .opencode/plugin/test-noreply.ts
    import type { Plugin } from "@opencode-ai/plugin";
    import { tool } from "@opencode-ai/plugin";
    
    export const TestNoReplyPlugin: Plugin = async (ctx) => {
      return {
        tool: {
          test_noreply: tool({
            description: "Minimal test of noReply pattern",
            args: {},
            async execute(args, toolCtx) {
              await ctx.client.session.prompt({
                path: { id: toolCtx.sessionID },
                body: {
                  noReply: true,
                  parts: [{ type: "text", text: "Test message" }]
                }
              });
              return "Test completed";
            }
          })
        }
      };
    };

    This minimal test plugin also seems to trigger the model switch to gpt-oss-120b.

    So it is maybe OpenCode's handling of client.session.prompt({ noReply: true }) calls from plugins?

    Built-in tools (bash, read, etc.) that don't use this pattern seem to work fine.

  5. omaclaren commented on Nov 19, 2025

    @omaclaren
    Author

    I think the issue is maybe that resolveModel doesn't check the current session's active model before falling back to defaults, so noReply calls inherit the default model instead of the session's current selection.

  6. omaclaren commented on Nov 19, 2025

    @omaclaren
    Author

    Updated title to reflect that this seems to affect all plugins using noReply, not just opencode-skills

  7. changed the title [-]Calling opencode-skills tools causes Build mode to reset to default model[/-] [+]Plugins using noReply seem to cause model to switch to agent default [/+] on Nov 19, 2025
  8. omaclaren commented on Nov 19, 2025

    @omaclaren
    Author

    Another update.
    For 1.0.68 both the minimal test_noreply plugin and skills_test_skill work correctly and use the session's selected model (here Claude Haiku 4.5). Seems to be an issue introduced by the agent loop refactor and/or subsequent fixes to this, hence doesn't work for 1.0.78

  9. arsham commented on Nov 19, 2025

    @arsham

    Related #4439 I suppose. The same agent loop refactor caused both issues.

  10. rekram1-node commented on Nov 19, 2025

    @rekram1-node
    Contributor

    yeah i think ur right there is something going on there

  11. thdxr commented on Nov 19, 2025

    @thdxr
    Member

    let me summarize the issue:

    what is happening here is the loop now will use the model associated with the LAST user message

    right now you can call prompt without specifying agent or model

    and the current logic is to use the default model when that's the case

    better logic would be to use the last known model from the last user message that has one - will fix

  12. thdxr commented on Nov 19, 2025

    @thdxr
    Member

    can you see if this is fixed in bunx opencode-ai@dev (should be deployed 5min after this comment)

  13. wolfie82 commented on Nov 19, 2025

    @wolfie82
    Contributor

    The fix is working, but one thing that is interesting to point out is that the prompts sent with noReply appear to be using the build agent, instead of the agent that started the session. So the model selection problem is gone (if there isnt a default configured in opencode.json), but is it intended that the noReply pattern will always be processed by the in-built build agent?

  14. wolfie82 commented on Nov 19, 2025

    @wolfie82
    Contributor

    So I just tested this. When a prompt is sent with noReply, the agent switches to build. If the build agent decides to continue, but its now a different agent and trying to run commands it doesn't have access to. It bombs like the image below. /cc @thdxr @rekram1-node

    Image
  15. wolfie82 commented on Nov 19, 2025

    @wolfie82
    Contributor

    This bug also appears to impact compaction. Compaction switches to build agent, doesn't return to the agent that initiated. Build agent tries to run a tool it doesn't have access to and bombs.

  16. omaclaren commented on Nov 19, 2025

    @omaclaren
    Author

    yeah, seems to fix the issue in build mode but if in plan mode it switches to build agent when using noReply

  17. malhashemi commented on Dec 6, 2025

    @malhashemi
    Contributor

    Hi @omaclaren maintainer of the plugin here, the default behavior was to not provide a target agetn, as you have already discovered, now the messages are going to the build agent if non is provided. I released an update to the plugin and now it explicitly pass the message to the the agent that called the tool, it should be all fixed now at least on the plugin end. Please feel free to open an issue on the plugin repo if the issue still persists, make sure you are on the latest version

  18. omaclaren commented on Dec 7, 2025

    @omaclaren
    Author

    thanks @malhashemi -- I think this was a more general issue with opencode itself as well, but looks like there's lots of activity fixing it!

  19. arsham commented on Dec 7, 2025

    @arsham

    I've used this patch to fix the issue locally:

    diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts
    index b8b7af742..d7498098b 100644
    --- a/packages/opencode/src/session/index.ts
    +++ b/packages/opencode/src/session/index.ts
    @@ -54,6 +54,8 @@ export namespace Session {
             })
             .optional(),
           title: z.string(),
    +      currentAgent: z.string().optional(),
    +      agent: z.string().optional(),
           version: z.string(),
           time: z.object({
             created: z.number(),
    diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
    index d5010bc47..0088d9336 100644
    --- a/packages/opencode/src/session/prompt.ts
    +++ b/packages/opencode/src/session/prompt.ts
    @@ -242,7 +242,13 @@ export namespace SessionPrompt {
           })
         }
     
    -    using _ = defer(() => cancel(sessionID))
    +    using _ = defer(() => {
    +      cancel(sessionID)
    +      // Clear currentAgent when loop completes
    +      Session.update(sessionID, (draft) => {
    +        draft.currentAgent = undefined
    +      }).catch(() => {})
    +    })
     
         let step = 0
         while (true) {
    @@ -437,6 +443,13 @@ export namespace SessionPrompt {
           // normal processing
           const cfg = await Config.get()
           const agent = await Agent.get(lastUser.agent)
    +      // Track current agent in session state (only if changed)
    +      const session = await Session.get(sessionID)
    +      if (session.currentAgent !== agent.name) {
    +        await Session.update(sessionID, (draft) => {
    +          draft.currentAgent = agent.name
    +        })
    +      }
           const maxSteps = agent.maxSteps ?? Infinity
           const isLastStep = step >= maxSteps
           msgs = insertReminders({
    @@ -825,7 +838,20 @@ export namespace SessionPrompt {
       }
     
       async function createUserMessage(input: PromptInput) {
    -    const agent = await Agent.get(input.agent ?? "build")
    +    const session = await Session.get(input.sessionID)
    +
    +    // If no agent specified, infer from last assistant message's mode
    +    let agentName = input.agent
    +    if (!agentName) {
    +      const msgs = await MessageV2.filterCompacted(MessageV2.stream(input.sessionID))
    +      const lastAssistant = msgs.findLast((m) => m.info.role === "assistant") as MessageV2.Assistant | undefined
    +      // Updated fallback chain - add session.currentAgent before session.agent
    +      agentName = lastAssistant?.mode ?? session.currentAgent ?? session.agent ?? "general"
    +    }
    +
    +    const agent = (await Agent.get(agentName)) || (await Agent.get("general"))
    +    if (!agent) throw new Error(`Agent not found: ${agentName}`)
    +
         const info: MessageV2.Info = {
           id: input.messageID ?? Identifier.ascending("message"),
           role: "user",
    @@ -937,7 +963,7 @@ export namespace SessionPrompt {
                         const result = await t.execute(args, {
                           sessionID: input.sessionID,
                           abort: new AbortController().signal,
    -                      agent: input.agent!,
    +                      agent: agent.name,
                           messageID: info.id,
                           extra: { bypassCwdCheck: true, model },
                           metadata: async () => {},
    @@ -997,7 +1023,7 @@ export namespace SessionPrompt {
                       t.execute(args, {
                         sessionID: input.sessionID,
                         abort: new AbortController().signal,
    -                    agent: input.agent!,
    +                    agent: agent.name,
                         messageID: info.id,
                         extra: { bypassCwdCheck: true },
                         metadata: async () => {},

    So far it is working well, but I will test it for a couple of days and if things go smoothly I will make a PR. I appreciate it if you try this patch as well and let me know if works for you too.

  20. binajmen commented on Dec 12, 2025

    @binajmen

    god bless you @arsham, I am currently wasting premium requests because of this issue right now! I will see how i can apply the patch to opencode as it updates automatically nearly every day ;D

  21. arsham commented on Dec 12, 2025

    @arsham

    Just to report: I haven't had a single incident after this patch on top of #4622 PR.

  22. Nindaleth commented on Dec 18, 2025

    @Nindaleth
    Contributor

    @arsham Would you be willing to send your patch as an independent PR so it can be reviewed by the maintainers too?

  23. arsham commented on Dec 19, 2025

    @arsham

    @Nindaleth I have updated the PR. I think they both are related. I could create a separate PR but it requires testing to see if the patch itself is the fix.

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions