Skip to content

fix(cli): preserve envBlock on all user messages to fix prompt cache invalidation - #7778

Closed
marius-kilocode wants to merge 3 commits into
mainfrom
goofy-paneer
Closed

fix(cli): preserve envBlock on all user messages to fix prompt cache invalidation#7778
marius-kilocode wants to merge 3 commits into
mainfrom
goofy-paneer

Conversation

@marius-kilocode

@marius-kilocode marius-kilocode commented Mar 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes Anthropic prompt cache invalidation at turn boundaries that was causing $32+ of waste per long session.

The environmentDetails block (containing timestamp, active file, open tabs) was only injected into the last user message ephemerally. When a new turn started, the previous user message lost its envBlock, changing the byte content in the middle of the conversation and breaking Anthropic's prefix cache match. Only the ~30k system prompt prefix survived as a cache hit, leaving 100k–180k of conversation history uncached at full input token price.

Root Cause

Anthropic's prompt caching uses longest prefix matching. The cache looks for the longest byte-identical prefix between the current request and a previous cached request.

When the envBlock moved from user_msg_1 to user_msg_2 at a turn boundary, the bytes at user_msg_1's position changed (it lost its <environment_details> block). Since user_msg_1 sits right after the system prompt (~30k), every byte after position ~30k shifted, and the longest matching prefix was only the system prompt.

Turn 1 (envBlock on user_1):

[sys, sys, user_1(+envBlock), asst, tool, asst, ...]

Turn 2 (envBlock moves to user_2, user_1 loses it):

[sys, sys, user_1(bare!), asst, ..., user_2(+envBlock), ...]
             ↑ bytes changed here — cache prefix breaks

Fix

Replace the single envBlock/envUser variables with a Map<string, string> that caches each user message's envBlock by message ID. On each loop step, inject the cached envBlock into every user message that has one — not just the last. This preserves the byte-identical prefix across turns.

Impact

Analyzed session ses_2d178770dffePbbmynWytbfSwt (Claude Opus 4.6 via Kilo Gateway):

Metric Before fix With fix (projected)
Total cost $36.33 ~$4–8
Cache misses at turn boundaries Every turn (~$0.80–1.29 each) Eliminated
Conversation prefix cached Only 30k (system prompt) Full conversation

Introduced by

The cache invalidation was introduced by #6225, specifically:

  • f6da5ded83refactor: move dynamic editor context from system prompt to user message — moved envBlock to the last user message only
  • 376cffa3c4fix: inject environment_details ephemerally to avoid stale accumulation across turns — made injection ephemeral so old user messages lose their envBlock when they're no longer "last"

The architectural decision to move dynamic content out of the system prompt was correct (it protected the system prompt cache), but the single-last-user-message injection strategy introduced the turn-boundary cache invalidation.

Related

Testing

  • Typecheck passes (no new errors)
  • Transform/caching tests pass (119/122, 3 pre-existing failures unrelated)

…invalidation

The environmentDetails block was only injected into the *last* user message
ephemerally. When a new user turn started, the previous user message lost its
envBlock, changing the byte content in the middle of the conversation and
breaking Anthropic's prefix cache match. Only the ~30k system prompt prefix
survived, leaving 100k-180k of conversation history uncached at full price.

Fix: cache each user message's envBlock in a Map keyed by message ID and
inject into ALL user messages that have one, so the conversation prefix stays
byte-identical across turns.

Introduced by: f6da5de (move dynamic editor context from system to user msg)
Worsened by: 376cffa (inject environment_details ephemerally)
Comment thread packages/opencode/src/session/prompt.ts Outdated
// Without this, the envBlock would only be on the *last* user message,
// causing earlier user messages to lose theirs on new turns and breaking
// the prefix match (everything after that point shifts).
const envCache = new Map<string, string>()

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.

WARNING: This cache is scoped to a single loop() invocation, so it resets on every new user turn or resume.

envCache only lives for the current assistant loop. When the next user message starts a fresh SessionPrompt.loop() call, all older user IDs are missing from the map, so only the newest lastUser gets <environment_details> injected again. That means earlier user messages still lose their env block at turn boundaries, and Anthropic's longest-prefix cache miss still happens. Persist the rendered block with the message, or recompute it from each stored editorContext, instead of keeping it in a local Map.

@kilo-code-bot

kilo-code-bot Bot commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0

Fix these issues in Kilo Cloud

Issue Details (click to expand)

WARNING

File Line Issue
packages/opencode/src/session/prompt.ts 298 cancel() clears the session entries from envCache at the end of every loop(), so older user messages still lose <environment_details> on the next turn.
Files Reviewed (1 files)
  • packages/opencode/src/session/prompt.ts - 1 issue

Reviewed by gpt-5.4-20260305 · 175,944 tokens

Remove second-precision time from the environment_details block. The model
has no use for knowing it's 13:32:40 — the date and timezone are sufficient.

Add tests to enforce no time-of-day leaks into environment_details.
…ries

The envCache Map was local to loop(), which is called fresh for each user
turn. This meant old user messages still lost their envBlocks on new turns.
Move envCache to module scope keyed by sessionID:messageID, and clean up
entries when a session is cancelled.
@marius-kilocode
marius-kilocode marked this pull request as draft March 27, 2026 16:41
@markijbema

Copy link
Copy Markdown
Contributor

Is this still recent/the case? do you still want to merge?

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.

2 participants