Static system prompt: share the appended block across sessions via the prompt cache - #425
Open
pufit wants to merge 2 commits into
Open
Static system prompt: share the appended block across sessions via the prompt cache#425pufit wants to merge 2 commits into
pufit wants to merge 2 commits into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Static system prompt: share the appended block across sessions via the prompt cache
What
build_system_promptjoins SOUL/TASK/IDENTITY/USER/AGENTS/TOOLS/MEMORY.md, a# Session Contextblock, the skills list and# Recalled Memoriesinto one string that the Claude Code CLI receives as a single appended system-prompt block. That block carried the per-sessionSession IDand the per-session pre-recall list, so no two sessions ever had a byte-identical system prompt.This PR makes the appended system prompt static across sessions of the same workspace / source / tool set and delivers the per-session parts in a
<session-context>block prepended to the first user message of each native conversation.nerve/agent/prompts.py:build_system_prompt(..., static=True)no longer rendersSession IDor# Recalled Memories(the arguments stay for signature compatibility;static=Falserenders the legacy shape). Newbuild_session_preamble(session_id, source, recalled_memories)renders the per-session block andprepend_session_preamble()wraps it in<session-context>…</session-context>ahead of the user text. The static prompt states where the per-session details live and namesmcp__nerve__session_contextas the live fallback.nerve/agent/engine.py: recall + freeze logic is unchanged. The rendered preamble is parked per session at client build and popped by_run_innerinto the FIRST user message of the native conversation — the same mechanism as the trailingCurrent timereminder (not persisted to the DB message, so the UI stays clean). It is delivered when the transcript is fresh (no resume target; codex resume-miss), on forks (new id), and once for resumable sessions that pre-date this change (preamble_sentmarker in session metadata) — so persistent cron sessions and long-lived chats still see their id exactly once. A query-phase crash retry re-renders it from the base text, never stacking two blocks.agent.static_system_prompt(new, defaulttrue).falserestores the old prompt shape and skips the preamble. Documented inconfig.example.yamlanddocs/config.md;docs/memory.md,docs/architecture.mdand the README pre-recall bullet updated.Why
Measured on a production fleet: 814 cron session starts/day, ~118K-token appended block, cache-WRITTEN at every session start — about $847/day of cache writes. 87% of those session starts happen within 5 minutes of the previous one, i.e. inside the default 5-minute cache TTL, so with a byte-identical block nearly all of them become cache READS (0.1x base input instead of 1.25x for a write). Until now the API could only ever share the CLI's own ~29K preset block, because everything after it differed per session.
Mechanism
Anthropic prompt caching is exact-prefix: a cached prefix (tools → system → messages, in that order) is reused only when the new request's bytes match it exactly up to a breakpoint. The appended system prompt sits inside that prefix, so a single differing byte in it — a session id, a different recall list — invalidates the whole block for every other session. Moving the per-session bytes AFTER the system prompt (into the first user message) keeps the shared prefix identical; only the small per-session tail differs and is written once per session. The TTL policy in
nerve/agent/cache_policy.pyis untouched; the 1h TTL now also pays off across sessions, not just within one.Risks / caveats
Current dateline rolls the block once a day (unchanged; deliberate — see the comment inprompts.py).mcp__nerve__session_contextin that case. Scripts that grep the system prompt file for the id must read the transcript instead.Sourcestays in the block as before, so the cache is shared per source (web / cron / telegram …). Sources also differ in model, so this splits nothing that could have been shared.How to verify on a fleet
After two sessions of the same source have started within 5 minutes, compare the FIRST API request of the second session (
session_usage, or the Langfuse / OTEL spans):cache_creation_input_tokens≈ the full prefix (~118K appended block + ~29K CLI block) andcache_read_input_tokens≈ 29K (the CLI block only);cache_read_input_tokens≈ the full prefix andcache_creation_input_tokens≈ only the small per-session tail (session-context block + user text).Also spot-check a new session's native transcript: the first user message starts with
<session-context>carryingSession ID:, and the second turn does not repeat it.agent.static_system_prompt: falseis the one-line fallback.