Cron jobs prompt cache and session reuse
NousResearch/hermes-agent · cluster 14821 · cron
Data as of
4 open items. Fix merged in #52295. Check it covers your case.
The short answer
- Known?
- Yes. 10 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #52295. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
partially resolved, deciding PR #52295. That is separate from the fix event. - Still open
- 3 issues and 1 PR.
Next step: read #52295, then check whether your report is one of the 3 still open.
Read #52295Pick up a Lab taskThese issues and pull requests address cron jobs creating a new session or cache key on every run, preventing reuse of warm prompt caches. Proposed solutions include stable or content-addressed prompt cache keys and optional resumable/named sessions for recurring jobs.
Open issues3
Open PRs1
- PR#47094feat(cron): support reusable sessions
Fixes on record
-
#52295 fix(cache): content-address prompt_cache_key so recurring cron jobs reuse the warm prefix
files:
agent/transports/codex.py,tests/agent/transports/test_codex_transport.pyShow the diff
-
agent/transports/codex.py@@ -5,12 +5,47 @@ streaming, or the _run_codex_stream() call path. """ +import hashlib +import json from typing import Any, Dict, List, Optional from agent.transports.base import ProviderTransport from agent.transports.types import NormalizedResponse, ToolCall +def _content_cache_key(instructions: str, tools: Optional[List[Dict[str, Any]]]) -> Optional[str]: + """Content-address the prompt cache key from the static request prefix. + + Returns ``pck_<sha256[:24]>`` of (instructions + sorted tool schemas), or + None when there is nothing static to key on. The cache key is a routing + hint only — never a correctness boundary — so two requests sharing a system + prompt and tool set intentionally resolve to the same warm prefix bucket. + + The fix this exists for: recurring cron jobs build session_id as + ``cron_<id>_<timestamp>``, so using session_id as the cache key made every + fire cache-cold. The static prefix (identity + tools) is identical across + fires, so hashing it gives a stable key that stays warm within the + provider's cache TTL. Sorting tools by name keeps the hash insertion-order + independent. + """ + if not instructions and not tools: + return None + tools_part = "" + if tools: + sorted_tools = sorted( + (t for t in tools if isinstance(t, dict)), + key=lambda t: str(t.get("name") or t.get("type") or ""), + ) + tools_part = json.dumps( + sorted_tools, sort_keys=True, ensure_ascii=False, separators=(",", ":") + ) + # \x00 separator so instructions ending in the tool JSON can't collide with + # a request whose instructions contain that JSON and whose tools are empty. + content = f"{instructions or ''}\x00{tools_part}" + digest = hashlib.sha256(content.encode("utf-8", errors="replace")).hexdigest()[:24] + return f"pck_{digest}" + + class ResponsesApiTransport(ProviderTransport): """Transport for api_mode='codex_responses'. @@ -71,7 +106,10 @@ def build_kwargs( params: instructions: str — system prompt (extracted from messages[0] if not given) reasoning_config: dict | None — {effort, enabled} - session_id: str | None — used for prompt_cache_key + xAI conv header + session_id: str | None — transcript/session id; drives the xAI + x-grok-conv-id header and the Codex cache-scope headers, and is + the fallback prompt_cache_key when there is no static prefix to + content-address max_tokens: int | None — max_output_tokens timeout: float | None — per-request timeout forwarded to the SDK request_overrides: dict | None — extra kwargs merged in @@ -212,10 +250,17 @@ def build_kwargs( kwargs["parallel_tool_calls"] = True session_id = params.get("session_id") + # prompt_cache_key is content-addressed from the static prefix + # (instructions + tools), NOT session_id — recurring cron jobs carry a + # per-fire timestamp in session_id (cron_<id>_<ts>) that made every run + # cache-cold. session_id is left untouched for transcript isolation and + # the cache-scope routing headers below. Falls back to session_id when + # there is no static content to hash. + cache_key = _content_cache_key(instructions, response_tools) or session_id # xAI Responses takes prompt_cache_key in extra_body (set further # down); GitHub Models opts out of cache-key routing entirely. - if not is_github_responses and not is_xai_responses and session_id: - kwargs["prompt_cache_key"] = session_id + if not is_github_responses and not is_xai_responses and cache_key: + kwargs["prompt_cache_key"] = cache_key if reasoning_enabled and is_xai_responses: from agent.model_metadata import grok_supports_reasoning_effort @@ -326,7 +371,7 @@ def build_kwargs( merged_extra_body: Dict[str, Any] = {} if isinstance(existing_extra_body, dict): merged_extra_body.update(existing_extra_body) - merged_extra_body.setdefault("prompt_cache_key", session_id) + merged_extra_body.setdefault("prompt_cache_key", cache_key) kwargs["extra_body"] = merged_extra_body return kwargs -
tests/agent/transports/test_codex_transport.py@@ -83,13 +83,34 @@ def test_reasoning_disabled(self, transport): ) assert "reasoning" not in kw or kw.get("include") == [] - def test_session_id_sets_cache_key(self, transport): + def test_cache_key_is_content_addressed_not_session_id(self, transport): + """prompt_cache_key is content-addressed from the static prefix + (instructions + tools), not the session_id. This keeps recurring cron + jobs — whose session_id carries a per-fire timestamp — on a stable warm + cache key. The key is a 'pck_' hash and must NOT equal session_id.""" messages = [{"role": "user", "content": "Hi"}] kw = transport.build_kwargs( model="gpt-5.4", messages=messages, tools=[], - session_id="test-session-123", + session_id="cron_job42_20260624_143000", ) - assert kw.get("prompt_cache_key") == "test-session-123" + pck = kw.get("prompt_cache_key", "") + assert pck.startswith("pck_") + assert pck != "cron_job42_20260624_143000" + + def test_cache_key_stable_across_session_ids(self, transport): + """Same static prefix + different session_id (e.g. two cron fires of the + same job) must yield the same prompt_cache_key — the whole point of the + fix: repeated fires reuse the warm prefix instead of going cold.""" + messages = [{"role": "user", "content": "Hi"}] + kw1 = transport.build_kwargs( + model="gpt-5.4", messages=messages, tools=[], + session_id="cron_job42_20260624_143000", + ) + kw2 = transport.build_kwargs( + model="gpt-5.4", messages=messages, tools=[], + session_id="cron_job42_20260624_143500", + ) + assert kw1["prompt_cache_key"] == kw2["prompt_cache_key"] def test_github_responses_no_cache_key(self, transport): messages = [{"role": "user", "content": "Hi"}] @@ -118,7 +139,12 @@ def test_xai_responses_sends_cache_key_via_extra_body(self, transport): is_xai_responses=True, ) assert "prompt_cache_key" not in kw - assert kw.get("extra_body", {}).get("prompt_cache_key") == "conv-xai-1" + # Body-level prompt_cache_key is content-addressed (pck_ hash), not the + # raw session_id, so recurring cron fires stay on a stable warm key. + eb_pck = kw.get("extra_body", {}).get("prompt_cache_key", "") + assert eb_pck.startswith("pck_") + assert eb_pck != "conv-xai-1" + # x-grok-conv-id stays the session/transcript id, not the cache key. assert kw.get("extra_headers", {}).get("x-grok-conv-id") == "conv-xai-1" def test_xai_responses_extra_body_preserves_caller_fields(self, transport):
-
4 PRs closed without merging
From the close comments: merged via another change 3 · duplicate 1
-
#51396 fix(cron): stable prompt_cache_key so recurring jobs reuse the warm cache · merged via another change →
#51395,#52295 -
#51580 fix(cache): content-address prompt_cache_key so cron jobs hit provider prefix cache · duplicate →
#51395,#51396,#51057 -
#51585 fix(cache): content-address prompt_cache_key so cron jobs hit provider prefix cache · merged via another change →
#52295 -
#51942 fix(cron): use stable prompt cache key for recurring jobs, closes #51395 · merged via another change →
#51395,#52295
Closed issues · 1
- Closed#51395Recurring cron jobs are prompt-cache-cold on every fire (session_id is the cache key)
How this cluster was grouped
-
Link evidence (
partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text. - Members outside that structural core may belong to a different underlying problem.
-
Pairwise link density
0.19: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Subsystem
- cron
- Merged PRs
- #52295
- Closed issues
- 1
- Attempts
- 4 PRs closed without merging
- Fix link checked
- 25 Jun 2026