#6857

session-report: token shares misstate spend, and cache breaks don't say why they happened

Openviniciusestevamopened 8d ago

Verified on main at af59ee3. This doesn't overlap with #6786; that issue covers the 100-entry cap, subagent.calls and unchecked findings.

1. Findings are ranked by token share, but token types differ up to 100× in price

SKILL.md step 4 asks for figures "as a % of total tokens", where total is input_tokens.total + output_tokens. Every token counts the same. On Opus 5.5 a cache read is $0.20/M, a 1-hour cache write $8/M and an output token $20/M.

From 30 days of my transcripts (2,352 API calls, almost all Opus 5.5):

share of tokensshare of cost at list prices
cache reads97.7%45.6%
cache writes1.9%33.6%
output0.4%20.9%

So "% of total tokens" mostly measures cache reads. Cache writes and output are over half of the cost and 2.3% of the denominator. It can move rows too: in the same report personal-website is 4th by tokens (12.2%) and 2nd by cost (15.4%).

Claude Code also writes its own cost meter into the transcript (cost-state entries with totalCostUSD). The analyzer skips them.

2. Cache breaks show size and context, not the cause

A break is any call with more than 100k uncached input (analyze-sessions.mjs#L387-L389). The 11 breaks in my 30-day report have three different causes:

  • 6 came after the session sat idle longer than the cache TTL (78 minutes to 20 hours, 1-hour TTL).
  • 4 came less than a minute after the previous call, so something early in the prompt changed.
  • 1 was the first call of a resumed session.

The first group is fixed by compacting or starting fresh after a long break. The second is fixed by not changing MCP servers, tools, CLAUDE.md or the model mid-session. In the report they look the same, so the findings can't tell the user which one to do.

Suggested change

I implemented both on a fork: viniciusestevam/claude-plugins-official@55123ed (the plugin's three files, +237/−20).

  • analyze-sessions.mjs: estimate USD per call from a table of Claude prices, with 5-minute and 1-hour cache writes priced separately. Calls on models not in the table go into unpriced_calls and are not counted as $0. Add cost_usd to every scope, top_prompts and by_day. Read cost-state and report the meter as cost_usd.measured, next to measured_sessions_estimated (the estimate for the same sessions). Give each break a cause (idle_past_ttl, prefix_changed, compaction, session_start), gap_min, ttl and rewrite_usd, and add cache_break_causes to every scope.
  • template.html: estimated cost and the meter in the hero, an "est. cost" stat, a "% cost" column (default sort) next to "% tokens", cost on top prompts, the cause on each break.
  • SKILL.md: rank spend findings by cost, quote the meter only next to the estimate for the same sessions, and name the cause of each break.

What I checked:

  • Every existing JSON field is identical to main for --since 24h, 7d, 30d and all time. The change only adds fields.
  • A report built from main's JSON renders as before. Both versions render with no console errors.
  • On my history, the estimate for the 28 sessions that log a meter is $200.58 against Claude Code's $210.53 (95%). The meter also counts calls that never reach the transcript.
  • The break causes match an independent classification from the call timestamps and TTLs.

The price table will need updates when prices change. If that's unwanted, a smaller version is to drop the dollar figures and only weight token shares by type.

I haven't opened a PR because external PRs are closed automatically here, but the branch can be read or reused as is.

Environment

Claude Code 2.1.308, macOS, Node 26. Reproduce with node analyze-sessions.mjs --json --since 30d on any history with Opus calls: compare overall.input_tokens.cache_read with the total, and look at cache_breaks[*].ts against the timestamp of the call before each one in the same transcript.

Activity

0

No activity yet.