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
Plan mode leave reminder becomes the final user turn after a tool-driven agent switch, models refuse #57013
When the agent switches away from plan partway through a turn, so the last message is an assistant or tool message rather than a user prompt, the opencode.plancontext hook adds the "NO LONGER in Plan mode" reminder at the very end of the request:
The request then ends with a user turn that holds only a <system-reminder> and no instruction. gpt-5.3-codex replies "I'm sorry, but I cannot assist with that request." every time. The reminder is also persisted as a synthetic message, so it stays at the end on retries, and the session keeps refusing until the user types something.
This shows up with plan-approval plugins (Plannotator submit_plan): the tool result reads "Plan approved…", the plugin switches the agent to build, and the next request ends assistant(tool_call) > tool(result) > user(<leave reminder>). In my gateway logs, every refused request has this exact tail: 6 of 6 refusals on 2026-10-19.
Steps to reproduce
Start a session in the plan agent.
Have a tool call switch the session to build (for example, approve a plan in Plannotator).
Look at the next model request: its last message is a user message containing only the leave reminder. Seen with gpt-5.3-codex through an OpenAI-compatible Responses provider.
Proposed fix
If a switch reminder would be the last message and the message before it is not a user message, move the reminder to the start of the trailing assistant/tool run. The request then ends on the tool result, as a normal agent loop does, and the enter/leave order is kept. The same check also fixes reminders that were already persisted at the tail. Tests: two new cases fail before the fix and pass after it. All 15 tests in plan.test.ts pass, and tsc -b and prettier are clean. I have a patch ready and can open a PR if a maintainer is happy with this approach.
Thanks for the detailed report. I reproduced this on 2.0.32 and on the latest dev (ba06ff9e0b).
To reproduce, I started a session in Plan and switched it to build through the server API while the first model request was still running, standing in for Plannotator's submit_plan. The model then made a tool call. The next request ended like this:
... user > assistant(tool_call) > tool(result) > user("<system-reminder> You are NO LONGER in Plan mode … </system-reminder>")
It ends with a user turn that contains only the leave reminder, as you described. In this run, that message is the synthetic reminder saved by the plan plugin when the agent switches, not one added by the context hook, so a fix also has to move a reminder that is already saved at the end. Your patch says it handles that case.
I used a simulated model, so I could not confirm the gpt-5.3-codex refusal itself, only the message order that causes it. The script below reproduces the problem.
Reproduction script (drive.ts)
// Repro for #57013: after a mid-turn switch from `plan` to `build` (as a plan-approval// plugin does from inside a tool call), the next model request ends with a user// message containing only the "NO LONGER in Plan mode" reminder.import{Effect,Stream}from"effect"import{Llm,OpenCodeDriver}from"opencode-drive"constbodies: unknown[]=[]exportdefaultOpenCodeDriver.use({
...(process.env.OC_DEV ? {opencode: {dev: process.env.OC_DEV}} : {}),tui: {recording: true},project: {git: true,files: {"PLAN.md": "1. do the thing\n"}},},({ ui, llm, opencode })=>Effect.gen(function*(){yield*llm.serve((request,index)=>{bodies.push(request.body)if(index===0)returnStream.unwrap(Effect.gen(function*(){// Stand-in for Plannotator's submit_plan: switch plan -> build while the turn is running.constlist: any=yield*opencode.session.list()constitems=list.items??list.data??listconstid=items[0].idyield*opencode.session.switchAgent({sessionID: id,agent: "build"}asany)yield*Effect.sleep("500 millis")returnStream.make(Llm.toolCall({index: 0,id: "call_read",name: "read",input: {path: "PLAN.md"}}),Llm.finish("tool-calls"),)}).pipe(Effect.orDie),)returnStream.make(Llm.text("Implementing the approved plan now."))})yield*ui.press("tab",{shift: true})// switch the TUI agent to Planyield*Effect.sleep("500 millis")yield*ui.screenshot("plan-agent")yield*ui.submit("Read PLAN.md and get it approved")yield*ui.waitFor("Implementing the approved plan now.")yield*ui.screenshot("after")bodies.forEach((b: any,i)=>console.log(`REQ ${i} roles:`,b.messages.map((m: any)=>m.role+(JSON.stringify(m.content??"").includes("Plan mode") ? (JSON.stringify(m.content).includes("NO LONGER") ? "[LEAVE]" : "[ENTER]") : "")).join(" > ")))constlast: any=bodies[1]constmsgs=last.messages??last.input??last.contentsconsole.log("REQUEST #2 URL-shaped body keys:",Object.keys(last))console.log("REQUEST #2 TAIL:",JSON.stringify(msgs.slice(-3),null,2))}),)
Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.
Activity
opencode-agent commentedon Oct 19, 2026
Thanks for the detailed report. I reproduced this on 2.0.32 and on the latest
dev(ba06ff9e0b).To reproduce, I started a session in Plan and switched it to
buildthrough the server API while the first model request was still running, standing in for Plannotator'ssubmit_plan. The model then made a tool call. The next request ended like this:... user > assistant(tool_call) > tool(result) > user("<system-reminder> You are NO LONGER in Plan mode … </system-reminder>")It ends with a user turn that contains only the leave reminder, as you described. In this run, that message is the synthetic reminder saved by the plan plugin when the agent switches, not one added by the
contexthook, so a fix also has to move a reminder that is already saved at the end. Your patch says it handles that case.I used a simulated model, so I could not confirm the gpt-5.3-codex refusal itself, only the message order that causes it. The script below reproduces the problem.
Reproduction script (drive.ts)
Run it with opencode-drive:
opencode-drive run ./drive.ts. It drivesopencodefrom yourPATH; setOC_DEVto the path of an OpenCode checkout to run that from source instead.