Plugins using noReply seem to cause model to switch to agent default
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
- Start OpenCode without explicit mode.build.model config
- Manually select Claude Haiku 4.5
- Verify Build indicator shows "claude-haiku-4-5"
- Call any skills_* tool
- Build mode indicator switches to "openai/gpt-oss-120b"
Screenshot and/or share link
Operating System
macOS 23.6.0
Terminal
iTerm2
prolly because of the skill plugin setting the model in its request or not using the same model
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.
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.
Updated title to reflect that this seems to affect all plugins using noReply, not just opencode-skills
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
Related #4439 I suppose. The same agent loop refactor caused both issues.
yeah i think ur right there is something going on there
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
can you see if this is fixed in bunx opencode-ai@dev (should be deployed 5min after this comment)
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?
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
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.
yeah, seems to fix the issue in build mode but if in plan mode it switches to build agent when using noReply
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
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!
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.
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
Just to report: I haven't had a single incident after this patch on top of #4622 PR.