[BUG] Transcript rows zero every usage counter but keep usage.cache_creation — cache_creation_input_tokens != sum(cache_creation)

Status Open
Reported on v2.1.237
Maintainer reply None cached
Activity 1 comment · opened Sep 19, 2026

What's wrong

Some assistant rows in session transcripts (~/.claude/projects/**/*.jsonl) have every top-level usage counter set to 0, while usage.cache_creation still holds the real write. That breaks the invariant documented on the prompt-caching page, under 1-hour cache duration: "the current cache_creation_input_tokens field equals the sum of the values in the cache_creation object."

In most cases another row of the same message (same message.id + requestId) carries the correct, consistent usage. So this looks like a row written with a zeroed usage snapshot, not an API response that disagrees with itself.

Example

Two rows of one message, CC 2.1.237, claude-opus-5, both with content: [text]. Usage fields only:

{"input_tokens": 2, "cache_creation_input_tokens": 977, "cache_read_input_tokens": 316685, "output_tokens": 5175, "cache_creation": {"ephemeral_1h_input_tokens": 977, "ephemeral_5m_input_tokens": 0}}
{"input_tokens": 0, "cache_creation_input_tokens": 0,   "cache_read_input_tokens": 0,      "output_tokens": 0,    "cache_creation": {"ephemeral_1h_input_tokens": 977, "ephemeral_5m_input_tokens": 0}}

How often (one machine, 2026-07-10 → 2026-09-19)

  • 540,177 assistant rows carrying usage.cache_creation, across 153,985 distinct messages
  • 153 messages have at least one row where cache_creation_input_tokens != sum(cache_creation.*)
  • in 136 of them, another row of the same message is consistent
  • 17 messages have no consistent row at all. For those, the transcript records a cache write only in the nested object.
  • Direction: almost always cache_creation_input_tokens = 0 with a non-zero split. The largest single row is 417,471 tokens reported as 0.
  • Seen on 20 CC versions from 2.1.205 through 2.1.275, and on claude-opus-5, claude-opus-4-8, claude-fable-5, claude-fable-5-1.
  • Stop reasons end_turn and tool_use. Content types text, thinking, tool_use. None are API-error rows.

Why it matters

Anything that accounts for cache writes from transcripts reads the top-level field and silently undercounts. That includes cost reports and cache-rebuild analysis. Readers that de-duplicate by message id and keep the first row are affected whenever the zeroed row comes first.

Expected

Every row written for a message carries the same usage, or at least one that satisfies the documented invariant.

Repro (read-only)

For each assistant row with message.usage.cache_creation, compare cache_creation_input_tokens with the sum of the cache_creation values. Then group by (message.id, requestId).

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗