Skip to content

fix(session): keep the cached system prompt prefix stable across steps - #1631

Draft
RaghwendraSingh wants to merge 1 commit into
mainfrom
fix/cache-stable-system-prefix
Draft

RaghwendraSingh wants to merge 1 commit into
mainfrom
fix/cache-stable-system-prefix

Conversation

@RaghwendraSingh

@RaghwendraSingh RaghwendraSingh commented Nov 1, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #1633

Type of change

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

What does this PR do?

The system prompt is the cached prefix of every request, so any change to it between two requests of one session is a full cache miss: the gateway bills the whole prefix (system prompt plus tool schemas, about 75K tokens on a typical repo) again as cache writes at the full input rate, instead of as cache reads at 0.1×. Two parts of the prompt changed for reasons unrelated to the task.

1. The Altimate Workspace section flipped on a timer (altimate/workspace/identity.ts).

  • It rendered one sentence for a fresh link check and another for a memoised one. For an unlinked repo that was "No Altimate Workspace is linked to this project." vs "…was linked … as of the last check, up to five minutes ago…". The outcome memo is 30 s and the miss memo 5 minutes, so a long session alternated between the two.
  • Separately, when a re-check outlasted the 1.5 s deadline, lastKnown marked the previous (fresh) outcome stale. For a linked repo that removed the workspace contents and the team-memory line from the prompt for that step.

Fix: one hedged sentence per link state, true whether or not the last check is fresh ("…as of the last check. The link is re-checked every few minutes, so a change made elsewhere may not show yet."), and a slow re-check reuses the previous outcome as it was. Only the resolver marks a link stale (the server could not confirm it), and that case still withholds contents and the memory line, as before.

2. Hoisted reminders went into the cached system message (session/prompt.ts).
After a compaction, the original-task pin (and the plan-switch reminders) are hoisted out of the user turn for models that are not Anthropic-like (#887/#888). The altimate-backend/altimate-default model is registered as OpenAI-compatible, so it takes that path, and the hoisted text was appended to the system array, which is the message carrying the cache breakpoint. The first request after every compaction therefore missed on the whole prefix, not just on the replaced history.

Fix: the hoisted text goes with the trailing environment system message, after the breakpoint (llm.ts already splits that message out for providers with explicit breakpoints). It is still sent as system text, so the #887 behaviour is unchanged.

Behaviour change to review: the model no longer distinguishes "last known" from "is linked" in the identity sentence; both read as "as of the last check". The stale-and-unconfirmed case still drops contents and the memory line.

Not in this PR: freezing the whole system prompt per session (skills synced from the workspace, memory injection and team rules can still change it mid-session, though on events rather than a timer), and a per-request prefix-hash log/counter.

How did you verify your code works?

  • New test/session/system-prefix-stability.test.ts: on the gateway model, two LLM.stream requests with and without a hoisted task pin have a byte-identical first system message (cache marker included), and the pin appears only in the trailing message. A source guard keeps hoisted reminders out of the system array.

  • identity.test.ts: stale and fresh renders are byte-identical for unbound and for bound (pinned × team memory).

  • identity-section.test.ts: a verified link whose re-check outlasts the deadline renders byte-identical to the previous step (memory line kept); a memoised miss renders byte-identical to the server's own answer.

  • The new tests fail against main (checked by reverting the source files) and pass on this branch.

  • Updated the tests that asserted the old stale wording, including the v0.12.1 release-contract tests.

  • bun test (packages/opencode): 19,937 pass, 11 fail. The same tests fail on main (mcp.headers, mcp HttpApi status, --time-budget cases in json-output.test.ts), except apply_patch "Unicode punctuation", which timed out under load and passes alone. The json-output time-budget cases are timing-sensitive: "silent-turn followup" failed 2/8 runs on both main and this branch.

  • Typecheck clean; script/check-tracker-leaks.ts and analyze.ts --markers --strict pass.

  • Request-body capture of both builds against a local mock gateway (comment): on main the first system message changed twice in a 7-minute session on an unlinked project, and once after a compaction; on this branch it was byte-identical across every request in both scenarios.

Still to add: a short gateway A/B (cache_read on the request after a timer flip and after a compaction).

Cost side effects

Reduces cost; adds no model calls, queries or resources. In a headless evaluation of about 5,400 requests, the system-prompt hash changed 785 times; 315 of those were full mid-session cache misses, and prefix re-writes were about 43% of billed tokens. This PR removes the two causes identified there; the remaining misses need the per-request hash log to attribute.

Screenshots / recordings

N/A

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR
  • I checked for cost side effects (model calls, queries or cloud resources that grow with user data) and described them, or wrote "none"

🤖 Generated with Claude Code

Two pieces of text in the cached system prompt changed between requests of
the same session, so the provider prompt cache missed and the whole prefix
(system prompt plus tool schemas) was written again at the full input rate:

- The Altimate Workspace section rendered one sentence for a fresh link check
  and another for a memoised one, so it flipped on the 30 s memo timer. A
  re-check that outlasted the 1.5 s deadline also marked the previous outcome
  stale, which dropped the workspace contents and memory line. The section
  now renders one hedged sentence per link state, and a slow re-check reuses
  the previous outcome as it was. Only the resolver marks a link stale.
- Hoisted reminders (the post-compaction task pin, the plan switch) were
  appended to the system array on non-Anthropic-like models, which includes
  the Altimate gateway model. They now ride in the trailing environment
  message, after the cache breakpoint, and still reach the model as system
  text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Nov 1, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@RaghwendraSingh

Copy link
Copy Markdown
Author

Request-body capture: main vs this branch against a local mock gateway

Each run is a headless altimate-code run --model altimate-backend/altimate-default against a local OpenAI-compatible mock that records every /chat/completions body. Each run has its own temporary home with altimateUrl pointing at the mock, and outbound proxies point at a dead port, so nothing reaches the real gateway. The project is unlinked (the binding lookup gets a 404). The mock drives a fixed sequence of bash tool calls, then DONE. The hash is over the first system message, the one carrying the cache breakpoint.

scenario build agent requests distinct first-system hashes changes
9 steps, 50 s apart (crosses the 5-min miss memo) main 9 2 2 (requests 7 and 8)
this branch 9 1 0
compaction on step 2 main 5 2 1 (first request after compaction)
this branch 5 1 0

Timer, main. At t≈301 s the miss memo expires and that step's resolve asks the server itself, so it renders the fresh wording; 50 s later the step reads the memoised miss again and renders the other wording:

-No Altimate Workspace was linked to this project as of the last check, up to five minutes ago; a link made elsewhere since then would not show yet.
+No Altimate Workspace is linked to this project.

and back at request 8. That is two full-prefix cache writes every five minutes on an unlinked project. On this branch all nine requests have the same first system message.

Compaction, main. The first request after the compaction adds the hoisted Original task — authoritative over any summary… reminder to the first system message. On this branch that message is unchanged, and the reminder is in the trailing system message after the breakpoint.

On both builds the tools hash and the number of cache-marked system messages (2) are constant within each run.

@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown

Thanks for updating your PR! It now meets our contributing guidelines. 👍

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.

System prompt prefix changes mid-session and misses the prompt cache (workspace wording, hoisted reminders)

1 participant