Skip to content

fix(cache): keep prompt_cache_key warm across compression session rotation - #86733

Merged
kshitijk4poor merged 3 commits into
NousResearch:mainfrom
kshitijk4poor:fix/79017-rotation-stable-cache-scope
Aug 15, 2026
Merged

fix(cache): keep prompt_cache_key warm across compression session rotation#86733
kshitijk4poor merged 3 commits into
NousResearch:mainfrom
kshitijk4poor:fix/79017-rotation-stable-cache-scope

Conversation

@kshitijk4poor

Copy link
Copy Markdown
Collaborator

Summary

prompt_cache_key now stays warm across context-compression session rotation: the cache scope is derived from the rotation-invariant compression-lineage ROOT instead of the physical session_id, which rotates mid-conversation in legacy compaction mode (compression.in_place: false) and went cache-cold at every rotation boundary.

Fixes #79017.

Root cause

#79161 scoped prompt_cache_key by the physical session id (correct for the cross-session bucket-sharing bug it fixed). But legacy-mode compaction mints a new physical id mid-conversation, so the scope — and therefore the key — changed at every rotation even though it's the same conversation continuing:

root-session    -> pck_b2bfe5458decf53e5752b393
rotated-session -> pck_71b8ffefb95330d6cd90f3d3   # cache cold, same conversation

Changes

  • agent/prompt_cache_scope.py (new): resolve_prompt_cache_scope(agent) — walks SessionDB.get_compression_lineage() (the fork-aware walk hardened in fix(state): stop delegate/tool children corrupting compression lineage #79193) to the rotation-invariant root. Memoized on the agent per (session_id, db-presence); _persist_disabled review forks memoize the fallback; a pre-persist miss on a persisting agent deliberately re-resolves until the row lands. Explicitly NOT get_conversation_root — that walk collapses /branch and delegate trees, which would break fix(cache): scope prompt_cache_key by session to stop cross-session bucket sharing #79161 isolation.
  • agent/transports/codex.py: build_kwargs accepts cache_scope_id, preferred over session_id for the body prompt_cache_key hash and the xAI x-grok-conv-id header. The Codex session_id header keeps the raw physical id (transcript identity, Align Codex cache affinity header #57012); x-client-request-id mirrors the final body key as before.
  • agent/transports/chat_completions.py: _add_prompt_cache_key accepts cache_scope_id with the same precedence; both build paths (profile + legacy) forward it.
  • agent/chat_completion_helpers.py: build_api_kwargs resolves the scope once (after the anthropic/bedrock early returns, which don't use prompt_cache_key) and passes it to all three OpenAI-wire branches.
  • agent/auxiliary_client.py + agent/turn_context.py: set_runtime_main carries a cache_scope field, bound once per turn; the auxiliary Responses cache-key site prefers it — compression/title/MoA aux calls stay rotation-stable too.

Scope semantics (all preserved from #79161)

Boundary Scope behavior
Compression rotation inherited (the fix)
/new fresh (new lineage)
/branch (_branched_from) isolated
Delegate subagent (_delegate_from) isolated (siblings distinct)
Tool child (source="tool") isolated
Cron fires normalized per job (_cache_scope_from_session_id unchanged)
Unrelated sessions distinct buckets

Default installs compact in place (physical id never rotates) → keys byte-identical to before; the lineage walk resolves [sid] once and memoizes.

Design constraints follow the maintainer thread on #79017: no DB walk on the per-API-call hot path (resolved once per turn/segment, memoized), scope keyed off real production markers (_delegate_from/_branched_from/source, not is_subagent), no gateway_session_key (outlives /new), main's anchored cron regex untouched, explicit prompt_cache_key overrides (top-level / request_overrides / extra_body) still win.

Validation

Check Result
New tests (tests/agent/test_prompt_cache_scope.py) 25 passed — rotation continuity, chained rotations, /new/branch/delegate/tool isolation, memo invalidation on rotation, pre-persist un-pinning, lazy-DB-attach re-resolution, persist-disabled memoization, transport wiring, header contracts, cron normalization, aux threading
Targeted suites (transports, aux client, turn_context, portal_tags, lineage guard, codex responses, compression timeout, nous wire, vision) 619 passed, 0 failed
E2E (real imports, real SessionDB, temp HERMES_HOME, full build_api_kwargs chain) root and rotated segments produce the same pck_ key; physical header preserved; 1 lineage walk across 3 API-call builds
Mutation check reverting transports/wiring to pre-fix makes exactly the 4 wiring/threading guards fail; module-only tests pass — guards bind the fix
ruff clean on all touched files

…ation

Legacy compaction mode (compression.in_place: false) rotates the physical
session_id mid-conversation. The prompt-cache scope introduced in NousResearch#79161 was
derived from that physical id, so every rotation moved the same conversation
into a fresh cache bucket - the prompt cache went cold at every rotation
boundary (NousResearch#79017).

Fix: resolve a rotation-stable logical scope - the compression-lineage ROOT
of the current session (SessionDB.get_compression_lineage, fork-aware
post-NousResearch#79193) - once per turn, memoized per transcript segment, and prefer it
over the physical session_id at every prompt_cache_key derivation site:

- agent/prompt_cache_scope.py (new): resolve_prompt_cache_scope(agent) -
  lineage-root walk with per-segment memo; falls back to the physical id
  when no DB is attached or the walk fails, degrading to pre-fix behavior.
- transports/codex.py: build_kwargs accepts cache_scope_id and prefers it
  for the body prompt_cache_key, the xAI x-grok-conv-id header, and the
  Codex x-client-request-id routing header. The Codex session_id header
  keeps the raw physical id (transcript identity, NousResearch#57012 contract).
- transports/chat_completions.py: _add_prompt_cache_key accepts
  cache_scope_id with the same precedence.
- chat_completion_helpers.py: build_api_kwargs threads the resolved scope
  into all three build_kwargs call sites (codex, profile, legacy).
- auxiliary_client.py: set_runtime_main carries cache_scope; the aux
  Responses cache-key site prefers it over the physical session_id.
- turn_context.py: resolves the scope once per turn and threads it through
  set_runtime_main (no DB walk on the per-API-call hot path).

Scope semantics preserved from NousResearch#79161: /new starts a fresh scope (new
lineage), /branch children, delegate subagents, and tool children stay
isolated (explicit-fork exclusion in get_compression_lineage), unrelated
sessions keep distinct buckets, and cron per-fire timestamps still
normalize via _cache_scope_from_session_id.

Default installs compact in place (session_id never rotates), so they hit
the memo and produce byte-identical keys to before.

Fixes NousResearch#79017
- prompt_cache_scope: memo key now includes DB presence (a lazily attached
  _session_db re-resolves instead of staying pinned to the physical id);
  _persist_disabled agents (background-review forks that never get a DB row)
  memoize the fallback instead of re-querying the lineage per API call;
  module docstring cross-references get_conversation_root and why the two
  lineage resolvers must not be deduplicated.
- chat_completion_helpers: hoist the triplicated
  _prompt_cache_scope_for_agent(agent) call to a single local above the
  OpenAI-wire dispatch (after the anthropic/bedrock early returns, which
  don't use prompt_cache_key).
- codex transport docstring: x-client-request-id mirrors the derived body
  key, not the raw scope id.
- turn_context comment: acknowledge the first-turn pre-persist fallback.
- tests: +2 (persist-disabled memoization; lazy DB attach re-resolution).
/simplify-code finding: turn_context evaluated resolve_prompt_cache_scope()
inside set_runtime_main's argument list under the umbrella try/except — a
resolution failure would silently skip the ENTIRE runtime binding
(provider/model/base_url/api_key/session_id for all aux calls that turn),
not just the cache scope.

- prompt_cache_scope: add resolve_prompt_cache_scope_safe() (never raises,
  returns None on failure/empty).
- turn_context: resolve the scope into a local via the safe variant BEFORE
  the set_runtime_main call, so a failure can only lose the scope.
- chat_completion_helpers: _prompt_cache_scope_for_agent delegates to the
  shared safe variant (guarded import retained).
- tests: +1 (hostile-property agent -> None; normal/empty passthrough).
@alt-glitch alt-glitch added type/bug Something isn't working P0 Critical — data loss, security, crash loop comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint area/sessions Session lifecycle, resume, persistence, history sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Aug 15, 2026
@kshitijk4poor
kshitijk4poor merged commit e3fab04 into NousResearch:main Aug 15, 2026
48 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/sessions Session lifecycle, resume, persistence, history comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P0 Critical — data loss, security, crash loop sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

prompt_cache_key loses continuity across context-compression session rotation (needs a logical cache-scope concept)

2 participants