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

3open issues
1open PR
10items grouped
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.

These 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

  • Issue#14821[Feature]: Named/resumable sessions for cron jobs
  • Issue#33167[Feature]: Cron jobs should support resuming existing sessions
  • Issue#39701Cron jobs create a new session on every run - add session_reuse option

Open PRs1

  • PR#47094feat(cron): support reusable sessions

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #52295 fix(cache): content-address prompt_cache_key so recurring cron jobs reuse the warm prefix

    merged to main

    files: agent/transports/codex.py, tests/agent/transports/test_codex_transport.py

    Show 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.00 means 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 Lab

At a glance

Subsystem
cron
Merged PRs
#52295
Closed issues
1
Attempts
4 PRs closed without merging
Fix link checked
25 Jun 2026

For your agent

The same facts, typed.