[BUG] Resuming a session on a later calendar day silently invalidates the entire message-history cache (currentDate block re-synthesized on resume)

Status Fixed / completed
Maintainer reply None cached
Activity 2 comments · opened Aug 13, 2026 · closed Aug 17, 2026

Summary

When a session is resumed on a later calendar day than its last turn, the injected user-context block (# userEmail / # currentDate <system-reminder> inside messages[0]) is re-synthesized with the current date. The first user message no longer byte-matches the cached prefix, so the entire message history is re-written at cache-write rates on the first turn after resume.

For long-lived sessions this is expensive and completely silent: our production session (window ~450K, 1h TTL) resumed the next day burned 454,111 cache-creation tokens with cache_read = 0 on a single turn.

Mechanism / proof

  1. The context block is not persisted in the session transcript (~/.claude/projects/.../<sid>.jsonl contains no Today's date block; verified by grep) — it is synthesized at request-build time.
  2. Captured request bodies via a logging proxy (ANTHROPIC_BASE_URL): two resume requests of the same session, one with the system date, one with TZ shifted a day back, have messages[0] byte-identical except the date:
first divergent byte at offset 7195 of messages[0]:
  "# currentDate\nToday's date is 2026-08-13.\n" 
vs
  "# currentDate\nToday's date is 2026-08-12.\n"
(lengths equal: 7421 == 7421)
  1. Usage telemetry (clean run, sonnet, default settings):
turn2 (live, same session):   cache_read=16670  cache_creation=646     <- caching healthy
turn3 (resume, SAME date):    cache_read=17316  cache_creation=234     <- resume itself is fine
turn4 (resume, date shifted): cache_read=13813  cache_creation=3749    <- read drops to system+tools,
                                                                          whole history re-written

In configurations where the first cache breakpoint sits at the end of messages[0], cache_read drops to 0 instead (that is what we observed in production: 454K re-write, read 0).

Deterministic repro (no waiting for midnight)

TZ shift changes the CLI's local date. Self-contained script (needs claude-agent-sdk):

import anyio, json, os
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient

MODEL = "claude-sonnet-4-6"

def opts(resume=None):
    return ClaudeAgentOptions(model=MODEL, system_prompt="repro", max_turns=1,
                              allowed_tools=[], setting_sources=[],
                              resume=resume, cwd="/tmp")

async def turn(client, prompt):
    await client.query(prompt)
    sid = usage = None
    async for msg in client.receive_response():
        if type(msg).__name__ == "SystemMessage" and getattr(msg, "subtype", "") == "init":
            sid = (msg.data or {}).get("session_id")
        if type(msg).__name__ == "ResultMessage":
            sid, usage = sid or msg.session_id, msg.usage or {}
    return sid, {k: usage.get(k, 0) for k in
                 ("cache_read_input_tokens", "cache_creation_input_tokens")}

async def main():
    async with ClaudeSDKClient(options=opts()) as c:
        sid, u = await turn(c, "Say: one.");  print("turn1", u)
        _, u = await turn(c, "Say: two.");    print("turn2 (live)", u)
    async with ClaudeSDKClient(options=opts(resume=sid)) as c:
        _, u = await turn(c, "Say: three."); print("turn3 (resume same date)", u)
    os.environ["TZ"] = "Etc/GMT+12"   # local date -1 day
    import time; time.tzset()
    async with ClaudeSDKClient(options=opts(resume=sid)) as c:
        _, u = await turn(c, "Say: four."); print("turn4 (resume, date shifted)", u)

anyio.run(main)

Note: setting_sources=[] recommended for a clean measurement — with user settings enabled, lazy tool loading (#75142) adds its own invalidations on top and masks this one.

Suggested fix

Persist the synthesized context block in the transcript and replay it verbatim on resume (the stale date inside the historical first message is semantically correct — it was that date; relative-time confusion is a separate concern, cf. #86219). Alternatively, exclude volatile fields from the first-message block and deliver "today's date" in the current turn instead.

Related (distinct mechanisms, same symptom family)

  • #44045 (closed): same block family — skill_listing/user-context block position instability on resume; this report is about the date value rotating, which hits every cross-day resume deterministically.
  • #75142: tools array growth mid-session — different mechanism, frequently co-occurs (we hit both while isolating this one).
  • #84011 / #81077: hook additionalContext serialization drift (our earlier reports).

Version: current stable CLI + claude-agent-sdk (reproduced 2026-08-13).

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗