Skip to content

fix(core): keep one-off prompts out of the cached prefix - #63498

Open
swf-cmd wants to merge 5 commits into
anomalyco:devfrom
swf-cmd:request-local-cache
Open

swf-cmd wants to merge 5 commits into
anomalyco:devfrom
swf-cmd:request-local-cache

Conversation

@swf-cmd

@swf-cmd swf-cmd commented Nov 12, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #62554

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Some text goes out with one request only: the goal reminder after a compaction, the reconciled Plan mode reminder, the session.generate prompt with the instruction update previewed before it, and the compaction summary prompt with its template nudge. The conversation-tail cache breakpoint landed on that text, so the request wrote a cache entry that no later request could read. With an active goal after a compaction this happened on every step, so each step wrote the conversation again instead of reading it. #62554 has numbers from a 22-hour goal session on 2.2.7.

Changes:

  • Text parts get an optional transient flag for request-local text. Automatic message breakpoints now stop before the first transient part, so the tail breakpoint sits on the last content the next request repeats. That can be the leading parts of a message whose last part is a reminder. The flag is never sent to a provider, and manual cache hints are not touched.
  • The goal and Plan reminders are a transient part at the tail: appended to the last user message, or sent as their own message after a tool result or a reply. Before, they were a separate message, placed before the user's prompt when the history ended with one, so the next request also diverged from the cached prefix at that point.
  • session.generate marks its prompt and the previewed instruction update as transient. Compaction marks the summary prompt and the nudge.
  • When the history doesn't fit and compaction sends the text transcript instead, that request uses cache: "none". No other request starts with the transcript, so breakpoints there only paid for writes.

One Anthropic detail: on models that take native system updates, an update followed by user content is sent after that content, and one followed by a reply stays in place. When request-local text follows a system update, the breakpoints therefore end before the update as well, so the next request still reads the entry.

This conflicts with #61690 in markMessages (cache-policy.ts). I'll rebase whichever of the two is still open when the other lands.

How did you verify your code works?

  • New tests in cache-policy.test.ts compile two consecutive Anthropic requests of a turn that carries a reminder and check that the first request's breakpoint is on content the second one still sends, and that a system update before request-local text stays out of the cached prefix. The goal, Plan, compaction and session.generate tests check where the reminder or prompt is placed and that it is marked.
  • With dev's sources those tests fail (2 in packages/ai, 10 in packages/core). With this branch they pass.
  • On this head, which includes dev at 101776024: bun test (2104 pass, 0 fail) and bun typecheck in packages/ai; oxlint and prettier on the changed files.
  • The full packages/core suite on Windows: 6244 pass, 107 fail. The 107 are shell, ripgrep, MCP, config reload and symlink tests that don't run in my setup, and the same 107 fail with dev's versions of the changed files.
  • bun typecheck in packages/core reports errors only in perf/*.bench.ts and src/mcp/client.ts, because my install is older than dev's lockfile. None are in the files changed here.
  • A throwaway check compiled a request with a transient part on eight routes (Anthropic Messages, Bedrock Converse, OpenAI Chat and Responses, OpenAI-compatible Chat, Gemini, Mistral, Cohere): the text is sent and the flag is not.
  • Not measured against the live API. The tests check where breakpoints land in compiled requests, not billed cache reads.

Screenshots / recordings

Not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

🤖 Generated with Claude Code


Added by the opencode bot (AI-generated).

Related: #61775

Public API: adds optional transient to TextPart in @opencode/ai (backwards compatible)

swf-cmd and others added 3 commits November 7, 2026 19:04
Several prompts are only sent with one request: the goal reminder after a
compaction, the reconciled Plan mode reminder, the keep-alive, goal review
and /btw prompts, and the compaction summary prompt with its template
reminder. The conversation-tail cache breakpoint landed on them, so each
request wrote a cache entry that no later request could read.

The goal and Plan reminders also sat where the next request has other
content: before the user's prompt, or after the latest tool result. The
next request diverged from the cached prefix there and wrote the rest of
the conversation again. The goal reminder stays until a continuation
restates the objective, so this repeated on every step.

Text parts can now be marked transient. Automatic message breakpoints stop
before the first transient part, and before a system update that it
follows, since Anthropic sends such an update in another form once a reply
follows it. The reminders are appended at the tail as transient text, and
the generate and compaction prompts are transient. The text transcript
that compaction falls back to when the history no longer fits is sent
without breakpoints: no other request starts with it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
No conflicts. dev changed session/generate.ts (usage is now returned and
recorded), session/compaction.ts (model resolve hook) and
schema/messages.ts (Freeform) next to this branch's edits.

Co-Authored-By: Claude <[email protected]>
The assertion compares a whole content block, so the local body type
needs the `type` field for the file to type-check.

Co-Authored-By: Claude <[email protected]>

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see anything blocking. I ran the #62554 reproduction script on the base and on this branch. It compiles consecutive Anthropic request bodies for three steps after a compaction:

  • Base: each step's tail breakpoint covers content the next step no longer sends (in step 2 it sits on the goal reminder), so that cache entry is never read.
  • This branch: the breakpoint stays before the reminder, and the next request sends the same blocks up to it. transient doesn't appear in the body.

cache-policy.test.ts and the goal, plan, compaction and generate tests pass, and typecheck:tsc is clean in packages/ai and packages/core. This was not checked against the live API.

Public API: TextPart in @opencode/ai gets a new optional transient?: boolean. This is backwards compatible: existing callers and messages don't change, and it doesn't reach the server's OpenAPI spec.

One small trade-off is noted inline on the compaction transcript fallback.

Related: #61775

Comment thread packages/core/src/session/compaction.ts Resolved
swf-cmd and others added 2 commits November 13, 2026 21:42
One conflict, in session-compaction.test.ts: dev's anomalyco#63590 added the stored
metadata assertions next to this branch's `transient` summary prompt. Both
are kept.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
Claude-Session: https://claude.ai/code/session_01X7arpqRMkdsRWVTteqLJM9
Since anomalyco#63590, request messages carry the stored Session message's metadata
for hooks to read. The goal and Plan reminders replaced the last user
message with a new UserMessage, whose constructor drops that field, so a
context hook that ran after them no longer saw the prompt's metadata.

Spread the message instead. The request is built from it either way:
metadata is stripped and the message is made again before it is sent.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
Claude-Session: https://claude.ai/code/session_01X7arpqRMkdsRWVTteqLJM9

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commits look fine. The merge leaves this PR's diff unchanged. The reminders now copy the last user message instead of building a new UserMessage, which keeps the request metadata #63590 added. That works with how #63590 strips the metadata and rebuilds each message before sending. I checked the new head: the goal, plan, compaction, generate and session-runner tests pass in packages/core, cache-policy.test.ts passes, and typecheck:tsc is clean in packages/ai and packages/core.

Public API: the same as before. TextPart in @opencode/ai gets an optional transient?: boolean, which is backwards compatible.

I have no new concerns.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

One-off prompts (goal reminder, keep-alive, compaction prompt) take the cache breakpoint and break the cached prefix

1 participant