Skip to content

cli: default_agent=plan appends plan-mode reminder as a synthetic turn after the first response #58287

Description

@aeomonlolo

Summary

When default_agent is set to plan and a run does not pass --agent, the plan-mode <system-reminder> is appended as a synthetic user message after the first assistant response instead of before it. That starts a second generation the user never asked for, and the model invents a task to work on.

Environment

  • opencode version: 2.1.3
  • OS: Microsoft Windows NT 10.0.26200.0 (win32 x64)
  • Terminal: Windows Terminal (WT_SESSION set), TERM=xterm-256color
  • Shell: C:\windows\system32\cmd.exe (ComSpec); agent shell tool uses PowerShell 5.1
  • Install/channel: npm global install of @opencode/cli 2.1.3 (release)
  • Active plugins: none configured; ~/.config/opencode/package.json only has an unused @opencode-ai/plugin dependency

Reproduction

  1. Set the global config (~/.config/opencode/opencode.jsonc):

    {
      "$schema": "https://opencode.ai/config.json",
      "default_agent": "plan"
    }
  2. Run a single trivial prompt without --agent:

    opencode run --model opencode/deepseek-v4.1-flash-free "Reply with exactly PROBE_C and do not use any tools."
  3. The run produces a first reply (PROBE_C), then a second assistant turn appears unprompted.

  4. Inspect the stored order:

    opencode session export <session-id>

    Message sequence:

    user       | "Reply with exactly PROBE_C and do not use any tools."
    assistant  | PROBE_C
    synthetic  | <system-reminder> You are in Plan mode. ...
    assistant  | "Understood — I'm in Plan mode. ... What would you like to plan?"
    idle       |
    

With --agent plan the same prompt yields the correct order:

agent-switched | 
synthetic      | <system-reminder> You are in Plan mode. ...
user           | "Reply with exactly PROBE_C ..."
assistant      | PROBE_C
idle           |

With --agent build there is no reminder at all:

agent-switched | 
user           | "Reply with exactly PROBE_C ..."
assistant      | PROBE_C
idle           |

Expected Behavior

The plan-mode reminder should be injected before the user prompt (as it is with --agent plan), or omitted for a single-shot run. One prompt should produce one assistant response; default_agent should behave identically to passing the same agent explicitly with --agent.

Actual Behavior

With default_agent: "plan", the reminder is delivered as a synthetic message after the first assistant turn completes, and the agent loop treats it as new input and generates again.

Database evidence (session_message, type = synthetic), from a reproduction with a local OpenAI-compatible provider:

seq 4   user       02:32:09.288  "Reply with exactly LOCAL_OK and do not use any tools."
seq 6   assistant  02:32:09.300  -> completed 02:32:37.188  "LOCAL_OK"
seq 13  synthetic  02:32:37.194  "<system-reminder> You are in Plan mode. ..."
seq 14  assistant  02:32:37.204  -> completes 02:32:59.317

The synthetic message lands 6 ms after the first response completes. The model's reasoning in the second turn assumed a task nobody sent ("The user is asking for an analysis of OpenCode's current session management") and proceeded to run tools for several minutes.

Additional Context

  • Model-independent: reproduced with opencode/deepseek-v4.1-flash-free on the OpenCode provider and with a local Qwen 3.8 model served through a custom @opencode/ai/providers/openai-compatible provider. Only the agent selection method changes the behavior.
  • Consistent: 3/3 reproductions with default_agent: plan; 0/2 with explicit --agent plan / --agent build.
  • Workarounds: set default_agent to build, or pass --agent plan explicitly on CLI runs (reminder then lands before the prompt).
  • No plugins, no extra tooling: default permissions, no MCP servers, plain global config.
  • Local paths and provider credentials removed from this report.

Activity

  1. opencode-agent commented on Oct 23, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I reproduced this on 2.1.3, and it still happens on the latest dev (53730daad5).

    I used a mock OpenAI-compatible provider that always replies PROBE_C, set "default_agent": "plan" in the global config, and ran opencode run --model mock/m "Reply with exactly PROBE_C and do not use any tools.":

    • Without --agent: the CLI printed PROBE_C twice. The model got a second request, user prompt → assistant: PROBE_C → plan-mode reminder. It ends on the reminder alone, so the model starts a turn no one asked for.
    • With --agent plan: one request, with the reminder before the prompt, and one reply.

    The script below reproduces it. As you found, passing --agent plan explicitly avoids it for now. Related: #57013, where a plan-mode reminder also ends up as the last user turn, after a mid-turn agent switch.

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Repro for #58287: default_agent=plan makes `opencode run` generate a second, unprompted turn.
    # Usage: ./repro.sh            (uses `opencode` from PATH)
    #        OC_DEV=/path/to/opencode ./repro.sh   (runs from a source checkout)
    set -u
    WORK=$(mktemp -d); PORT=${PORT:-18787}
    mkdir -p "$WORK/home/.config/opencode" "$WORK/project"
    LOG="$WORK/requests.log"
    # Mock OpenAI-compatible server: every chat completion replies "PROBE_C", and logs each request.
    cat > "$WORK/mock.ts" <<'TS'
    const log = process.env.LOG!
    let n = 0
    Bun.serve({ port: Number(process.env.PORT), async fetch(req) {
      const url = new URL(req.url)
      if (!url.pathname.endsWith("/chat/completions")) return new Response("{}", { headers: { "content-type": "application/json" } })
      const body: any = await req.json()
      n++
      const tail = body.messages.map((m: any) => {
        const c = typeof m.content === "string" ? m.content : JSON.stringify(m.content)
        return `${m.role}:${c.includes("You are in Plan mode") ? "[PLAN-REMINDER]" : c.slice(0, 60).replace(/\n/g, " ")}`
      }).filter((s: string) => !s.startsWith("system:"))
      await Bun.write(log, (await Bun.file(log).exists() ? await Bun.file(log).text() : "") + `REQUEST ${n}: ${tail.join(" | ")}\n`)
      const chunk = (d: any) => `data: ${JSON.stringify({ id: "x", object: "chat.completion.chunk", created: 0, model: "m", choices: [d] })}\n\n`
      const text = `PROBE_C`
      return new Response(chunk({ index: 0, delta: { role: "assistant", content: text } }) + chunk({ index: 0, delta: {}, finish_reason: "stop" }) + "data: [DONE]\n\n", { headers: { "content-type": "text/event-stream" } })
    } })
    TS
    LOG=$LOG PORT=$PORT bun "$WORK/mock.ts" & MOCK=$!
    sleep 1
    cat > "$WORK/home/.config/opencode/opencode.jsonc" <<JSON
    {
      "\$schema": "https://opencode.ai/config.json",
      "default_agent": "plan",
      "providers": {
        "mock": {
          "package": "@opencode/ai/providers/openai-compatible",
          "env": ["MOCK_API_KEY"],
          "settings": { "baseURL": "http://127.0.0.1:$PORT/v1" },
          "models": { "m": { "name": "Mock" } }
        }
      }
    }
    JSON
    if [ -n "${OC_DEV:-}" ]; then
      OC=(bun --conditions=browser --preload="$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js" "$OC_DEV/packages/cli/src/index.ts")
    else OC=(opencode); fi
    cd "$WORK/project" && git init -q
    # The source CLI respawns its server as plain `bun <entry>`; a bunfig keeps the Solid preload for it.
    [ -n "${OC_DEV:-}" ] && printf 'preload = ["%s"]\n' "$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js" > bunfig.toml
    export HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/home/.config" XDG_DATA_HOME="$WORK/home/.local/share" XDG_STATE_HOME="$WORK/home/.local/state" XDG_CACHE_HOME="$WORK/home/.cache" MOCK_API_KEY=x
    echo "version: $("${OC[@]}" --version)"
    for agent in "" "--agent plan"; do
      rm -f "$LOG"
      echo "=== opencode run ${agent:-(no --agent, default_agent=plan)} ==="
      timeout 90 "${OC[@]}" run $agent --model mock/m "Reply with exactly PROBE_C and do not use any tools." 2>&1 | tail -n 20
      echo "--- model requests ---"; cat "$LOG"
    done
    kill $MOCK

    Run it: bash repro.sh

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions