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
One-off prompts (goal reminder, keep-alive, compaction prompt) take the cache breakpoint and break the cached prefix #62554
Some prompts are only sent with one request: the goal reminder after a compaction, the reconciled Plan mode reminder, the session.generate prompts (keep-alive, goal review, /btw), and the compaction summary prompt with its template reminder. The conversation-tail cache breakpoint lands on them, so each of those requests writes a cache entry that no later request can read.
The goal and Plan reminders are also placed where the next request has other content: before the user's prompt, or after the latest tool result. The next request diverges from the cached prefix there and writes the rest of the conversation again. The goal reminder is added until a continuation restates the objective, so with an active goal after a compaction this happens on every step.
Separately, when the history no longer fits, compaction sends it as a text transcript with the usual breakpoints, which writes the whole transcript to the cache although no other request starts with it.
Use an Anthropic model, set a goal, and compact the session so the objective is no longer in the visible history.
Send a prompt that makes several tool calls.
Each step's request ends with the reminder, and the tail breakpoint is on it. The next step has a tool call and its result there instead, so it can't read that entry and writes everything after the last entry it can still read.
Thanks for the detailed report. I reproduced the goal reminder part on dev at 21bf6b4, which is your commit and also the latest dev.
The script below builds Anthropic request bodies through the normal request path with the default cache policy. It adds the goal reminder the same way the context hook in packages/core/src/plugin/goal.ts does, across consecutive agent steps after a compaction:
Step 1:[summary, ok, REMINDER, prompt*]. The reminder sits before the prompt (* marks the tail breakpoint).
Step 2:[summary, ok, prompt, tool_use, tool_result, REMINDER*]. It no longer matches step 1 from the third message, and the tail breakpoint is on the reminder.
Step 3: shares only the first five messages with step 2, so step 2's tail cache entry is never read.
The hook's own comment says this placement is meant to keep the cached prefix warm, so this is a bug, not intended behaviour. It's a code-level reproduction, not a run against Anthropic: there were no credentials here. I also didn't separately check the Plan reminder, the session.generate prompts or the compaction transcript, but they go through the same tail-breakpoint logic.
// Reproduction for anomalyco/opencode#62554: the goal plugin's request-only reminder takes the// conversation-tail cache breakpoint, and moves between steps, so no later request reads that entry.// Copy into packages/ai/test/ of a dev checkout, then from packages/ai: bun test test/repro-62554.test.tsimport{expect,test}from"bun:test"import{Effect}from"effect"import{ai,Message,ToolCallPart,ToolResultPart}from"../src/effect.js"import{Auth}from"../src/route.js"import{compileRequest}from"../src/route/client.js"import*asAnthropicMessagesfrom"../src/protocols/anthropic-messages.js"constmodel=AnthropicMessages.route.with({endpoint: {baseURL: "https://api.anthropic.test/v1/"},auth: Auth.header("x-api-key","test")}).model({id: "claude-sonnet-4-5"})constREMINDER="<system-reminder>\nA goal is active for this session. Keep working toward it.\n...</system-reminder>"// Same placement as the `context` hook in packages/core/src/plugin/goal.tsconstwithGoalReminder=(messages: Message[])=>{constout=messages.slice()constat=out.at(-1)?.role==="user" ? out.length-1 : out.lengthout.splice(at,0,Message.user(REMINDER))returnout}constcompile=(messages: Message[])=>Effect.runPromise(compileRequest(ai.request({ model,system: "sys", messages })).pipe(Effect.map((p)=>p.bodyasany)))constsummary=(body: any)=>body.messages.map((m: any,i: number)=>{constmarked=m.content.some((c: any)=>c.cache_control)consttext=m.content.map((c: any)=>c.type+":"+JSON.stringify(c.text??c.content??c.name??"").slice(0,30)).join(",")return`${i}${m.role}${marked ? "[BREAKPOINT] " : ""}${text}`})test("goal reminder takes the tail breakpoint and the next step diverges before it",async()=>{consthistory=[Message.user("<compaction summary>"),Message.assistant("ok")]constprompt=Message.user("run the tests")constcall=(id: string)=>Message.assistant([ToolCallPart.make({ id,name: "bash",input: {cmd: "ls"}})])constresult=(id: string)=>Message.tool(ToolResultPart.make({ id,name: "bash",result: "out "+id}))conststep1=awaitcompile(withGoalReminder([...history,prompt]))conststep2=awaitcompile(withGoalReminder([...history,prompt,call("c1"),result("c1")]))conststep3=awaitcompile(withGoalReminder([...history,prompt,call("c1"),result("c1"),call("c2"),result("c2")]))for(const[n,b]of[["step1",step1],["step2",step2],["step3",step3]]asconst)console.log(n+"\n "+summary(b).join("\n "))// Where the tail breakpoint lands on each stepconsttail=(b: any)=>b.messages.findLastIndex((m: any)=>m.content.some((c: any)=>c.cache_control))constisReminder=(b: any,i: number)=>JSON.stringify(b.messages[i]).includes("A goal is active")console.log("tail breakpoint on reminder:",isReminder(step2,tail(step2)),isReminder(step3,tail(step3)))// Longest common message prefix between step N's cached prefix (up to its tail breakpoint) and step N+1conststrip=(m: any)=>JSON.stringify({ ...m,content: m.content.map(({ cache_control, ...c}: any)=>c)})constcommon=(a: any,b: any)=>{leti=0while(i<a.messages.length&&i<b.messages.length&&strip(a.messages[i])===strip(b.messages[i]))i++returni}console.log("step2 cached up to msg",tail(step2),"; step3 matches step2 only for msgs [0,",common(step2,step3),")")expect(isReminder(step2,tail(step2))).toBe(true)expect(common(step2,step3)).toBeLessThanOrEqual(tail(step2))// step2's tail entry can never be read by step3})
Activity
opencodejr commentedon Nov 7, 2026
Thanks for the detailed report. I reproduced the goal reminder part on
devat 21bf6b4, which is your commit and also the latestdev.The script below builds Anthropic request bodies through the normal request path with the default cache policy. It adds the goal reminder the same way the
contexthook inpackages/core/src/plugin/goal.tsdoes, across consecutive agent steps after a compaction:[summary, ok, REMINDER, prompt*]. The reminder sits before the prompt (*marks the tail breakpoint).[summary, ok, prompt, tool_use, tool_result, REMINDER*]. It no longer matches step 1 from the third message, and the tail breakpoint is on the reminder.The hook's own comment says this placement is meant to keep the cached prefix warm, so this is a bug, not intended behaviour. It's a code-level reproduction, not a run against Anthropic: there were no credentials here. I also didn't separately check the Plan reminder, the
session.generateprompts or the compaction transcript, but they go through the same tail-breakpoint logic.Related: #61775, #54107, #56422.
Reproduction script (repro-62554.test.ts)
Run it:
bun repro-62554.test.ts