Skip to content

SessionReminders: build-switch banner mislabels/repeats when a custom primary agent (e.g. read-only 'ask') is used between plan and build #38066

Description

@Duo-Huang

Summary

SessionReminders.apply's legacy (non-experimentalPlanMode) branch decides whether to inject the "operational mode has changed from plan to build" reminder using:

https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/session/reminders.ts#L37

const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.agent === "plan")
if (wasPlan && input.agent.name === "build") {
  // ...push BUILD_SWITCH synthetic reminder
}

.some() here checks "has this session ever contained a plan-agent message", not "was the immediately preceding turn plan". input.messages comes from MessageV2.filterCompactedEffect(sessionID) (see prompt.ts around the main agent loop), which is the full uncompacted session history, not a per-turn window.

Why this matters for custom agents

opencode explicitly supports custom primary agents (https://opencode.ai/docs/agents), which can be switched to via Tab like build/plan. With only the two built-in agents, "session ever had a plan message" and "previous turn was plan" are almost always equivalent in practice, so this has presumably gone unnoticed.

Once a third primary agent is in rotation — e.g. a custom read-only advisor agent (config agent: { ask: { mode: "primary", permission: { edit: "deny" } } }) — the assumption breaks:

  1. User is in plan, does some read-only exploration.
  2. User switches to a custom agent (e.g. ask) for a few turns of Q&A, unrelated to planning.
  3. User switches to build.

At step 3, wasPlan is still true (the plan message from step 1 is still in the uncompacted history), so BUILD_SWITCH fires — even though the actual previous turn's agent was ask, not plan. The model receives:

<system-reminder>
Your operational mode has changed from plan to build.
...
</system-reminder>

which is factually wrong for that turn (it changed from the custom agent to build, not from plan). Worse: because the check scans the entire uncompacted history rather than being edge-triggered, this reminder can re-fire on every subsequent build turn for the rest of the session (until compaction eventually drops the old plan-agent message out of the window), regardless of how many other agents were used in between. A reminder that's supposed to be a rare, high-signal "your mode just changed" notice instead becomes noisy and occasionally false, which risks the model learning to discount it.

Reproduction

  1. Define a custom primary agent, e.g.:
    ---
    name: ask
    mode: primary
    permission:
      edit: deny
    ---
    You are a read-only advisor.
  2. In a session: switch to plan, send one message. Switch to ask, send one or more messages. Switch to build, send a message.
  3. Inspect the synthetic parts on that last user message (or just observe the model's response) — the BUILD_SWITCH reminder appears and claims the transition was "from plan to build", despite the immediately preceding turn being ask.

Confirmed against a live 1.18.3 session with exactly this custom-agent setup (transcript inspected via opencode db / sqlite).

Suggested fix

Mirror the pattern already used a few lines below in the same file for the experimentalPlanMode branch (assistantMessage = input.messages.findLast((msg) => msg.info.role === "assistant"), then check assistantMessage?.info.agent === "plan"), which is edge-triggered and doesn't have this issue. I've opened a PR with this fix: (linked below).

Environment

  • opencode 1.18.3 (macOS, Homebrew install)
  • Reproduced conceptually against dev branch source (same logic present, reminders.ts unchanged between the two as of this writing)

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions