Skip to content

<system-reminder> keeps moving, causing unnecessary prompt processing in llama.cpp #23595

Closed as not planned

Description

@jacekpoplawski

Description

For some reason, OpenCode keeps moving <system-reminder> around. As a result, the prompt history changes, so the cache cannot work correctly. In llama.cpp, this leads to a lot of unnecessary prompt processing time being wasted.

Would it be possible to keep <system-reminder> in the same place once it has been inserted for the first time? If something changes, you could add a new <system-reminder> instead.

details explained in the comment: ggml-org/llama.cpp#22031 (comment)

Plugins

No response

OpenCode version

1.14.19

Steps to reproduce

  1. Start in "plan" mode
  2. Switch to "build" mode to trigger <system-reminder>
  3. Tell opencode to do something complex, like use lots of tools and write lots of files
  4. Use any next prompt (like "thank you") and observe

<system-reminder> moves from the previous prompt to the next prompt and all the tokens (thousands!) between prompts must be processed again because cache is broken

Screenshot and/or share link

example:

 1184:  <|im_start|>user
 1185:  add one comment to it
 1186:  <system-reminder>
 1187:  Your operational mode has changed from plan to build.
 1188:  You are no longer in read-only mode.
 1189:  You are permitted to make file changes, run shell commands, and utilize your arsenal of tools as needed.
 1190:  </system-reminder><|im_end|>
 1191:  <|im_start|>assistant
 1192:  <think>
 1193:  The user wants me to add a comment to the `build_norm_gated` function. I should add a descriptive comment before the function.
 1194:  </think>
 1195:
 1196:  <tool_call>

then it moves after another prompt:

 1213:  <|im_start|>assistant
 1214:  Added.<|im_end|>
 1215:  <|im_start|>user
 1216:  thank you
 1217:  <system-reminder>
 1218:  Your operational mode has changed from plan to build.
 1219:  You are no longer in read-only mode.
 1220:  You are permitted to make file changes, run shell commands, and utilize your arsenal of tools as needed.
 1221:  </system-reminder><|im_end|>
 1222:  <|im_start|>assistant
 1223:  <think>

Operating System

Linux and Windows

Terminal

No response

Activity

  1. added
    coreAnything pertaining to core functionality of the application (opencode server stuff)
    perfIndicates a performance issue or need for optimization
    on Apr 20, 2026
  2. github-actions commented on Apr 20, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    If your issue is distinct (e.g. it affects all turns, not just queued messages), please clarify that in a comment so maintainers can track it separately.

  3. assigned and unassigned on Apr 26, 2026
  4. removed
    bugSomething isn't working
    perfIndicates a performance issue or need for optimization
    coreAnything pertaining to core functionality of the application (opencode server stuff)
    on May 3, 2026
  5. qiujunhan commented on Jun 14, 2026

    @qiujunhan

    #32246 When updating agents.md for the qwen3.6 27b model, it always triggers prompt processing at 100%.

  6. jacekpoplawski commented on Jun 14, 2026

    @jacekpoplawski
    Author

    #32246 When updating agents.md for the qwen3.6 27b model, it always triggers prompt processing at 100%.

    But I think this is expected, AGENTS.md is probably in the beginning

  7. VanceVagell commented on Aug 1, 2026

    @VanceVagell

    I also see this issue. I am using Deepseek v4 Flash hosted on ik_llama.cpp (a CPU-inference fork of llama.cpp) with OpenCode. I'm currently running with 300k context for this model, so what I'm about to describe has nothing to do with compaction because I'm nowhere near the limit in this project at the moment.

    Here's exactly what I am seeing right now in a current multi-hour project I'm working on in build mode:

    • At the start of EACH STEP, the context is invalidated back down to 40986 tokens (down from about ~65k tokens at this point in the run)
    • My LLM hosted on ik_llama.cpp needs to re-process those ~25k tokens of context.
    • Only then can it execute the current work, generating new tokens, running, a tool, etc.
    • Then for the next step this all repeats.

    It's making the run at least an order of magnitude slower than it would otherwise be, because of this cache invalidation.

    The fact that every step of the build agent invalidates back to the exact same point in the cache feels like a hint about what's going on here, but I don't know what it is (hopefully the creator of this issue report has nailed it).

    EDIT TO ADD: Note that I had a length "plan" mode run before this "build" mode situation I described above. I can't help but wonder if the plan mode resulted in those 40986 tokens that the build mode keeps reverting to in the cache and having to reprocess everything after that for some reason.

  8. VanceVagell commented on Aug 1, 2026

    @VanceVagell

    If helpful to anyone else, the following patch resolved this issue for me (I'm not familiar enough with this code base to feel comfortable submitting a PR for it, but this got it working for me):

    --- a/packages/opencode/src/session/reminders.ts
    +++ b/packages/opencode/src/session/reminders.ts
    @@ -24,27 +24,32 @@ export const apply = Effect.fn("SessionReminders.apply")(function* (input: {
       if (!userMessage) return input.messages
    
       if (!flags.experimentalPlanMode) {
    -    if (input.agent.name === "plan") {
    -      userMessage.parts.push({
    -        id: PartID.ascending(),
    -        messageID: userMessage.info.id,
    -        sessionID: userMessage.info.sessionID,
    -        type: "text",
    -        text: PROMPT_PLAN,
    -        synthetic: true,
    -      })
    -    }
    -    const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.agent === "plan")
    -    if (wasPlan && input.agent.name === "build") {
    -      userMessage.parts.push({
    -        id: PartID.ascending(),
    -        messageID: userMessage.info.id,
    -        sessionID: userMessage.info.sessionID,
    -        type: "text",
    -        text: BUILD_SWITCH,
    -        synthetic: true,
    -      })
    -    }
    +    const assistantMessage = input.messages.findLast((msg) => msg.info.role === "assistant")
    +
    +    // Only emit a reminder at the moment a mode change actually happens, and
    +    // persist it onto exactly the user message that crossed the boundary.
    +    // Anchoring the reminder in place (rather than re-attaching it to the
    +    // newest user message every turn) keeps the serialized history
    +    // byte-stable across turns, so the provider's prompt cache stays valid.
    +    const switchToBuild = input.agent.name === "build" && assistantMessage?.info.agent === "plan"
    +    const enterPlanMode = input.agent.name === "plan" && assistantMessage?.info.agent !== "plan"
    +    const reminder = switchToBuild ? BUILD_SWITCH : enterPlanMode ? PROMPT_PLAN : null
    +    if (!reminder) return input.messages
    +
    +    const alreadyAdded = userMessage.parts.some(
    +      (p) => p.type === "text" && p.text === reminder && p.synthetic,
    +    )
    +    if (alreadyAdded) return input.messages
    +
    +    const part = yield* sessions.updatePart({
    +      id: PartID.ascending(),
    +      messageID: userMessage.info.id,
    +      sessionID: userMessage.info.sessionID,
    +      type: "text",
    +      text: reminder,
    +      synthetic: true,
    +    })
    +    userMessage.parts.push(part)
         return input.messages
       }
    

    After this change, when switching from "plan" to "build" mode, there is one big reprocessing of the context, but after that each build turn does NOT reprocess everything in the context since the switch. I'm assuming that initial big reprocess is just how opencode works (presumably freeing up part of the context for build mode to use now that plan is done?).

    For me, this speeds up development massively since each turn doesn't have to waste so much time reprocessing context.

  9. VanceVagell commented on Aug 1, 2026

    @VanceVagell

    After testing more with the patch I previously shared, I see now that there are still other situations in opencode workflows where something happens to the context such that my model's cache has to go back and reprocess on each turn of the agent (and this compounds once it starts happening, each turn of the agent adds a bit more that needs to be reprocessed on subsequent turns).

    However, the patch above does seem to significantly improve things around the plan/build mode switch at least.

    Also noteworthy: after a compaction event, any compounded context resetting goes away and the agent is back to a good / fast state, at least for a while.

    Fully resolving this issue will result in massive overall speedups / token reductions for wasted prefill on what should be cached context.

  10. VanceVagell commented on Aug 3, 2026

    @VanceVagell

    At least for Deepseek v4 Flash (where I'm seeing this issue), this insight seems relevant: https://www.reddit.com/r/LocalLLaMA/comments/1vdbgw5/psa_for_deepseekv4flash0731_users_dont_blow_out/

    TL;DR the user found that the "system" role gets interleved in a way that invalidates cache, whereas the "latest_reminder" does not.

    "latest_reminder" is documented here: https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash/blob/main/encoding/README.md

  11. github-actions commented on Oct 3, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions