[BUG] Desktop Code tab usage chart miscounts tokens: duplicate transcript lines summed, workflow agents skipped, 7d legend shows all-time
Where: Claude desktop app on Windows 11, Code tab home screen → usage card → Models tab (All / 30d / 7d). Sessions run on the bundled Claude Code 2.1.280–2.1.281 (transcript entrypoint: claude-desktop). Max plan. Seen 2026-09-25.
Summary
The card's per-model and per-day token counts don't match what the session transcripts record. I rebuilt the card's numbers from ~/.claude/projects/**/*.jsonl. They match every model row and every daily bar only when I do two things:
- Add up the
usageon every transcript line, without de-duplicating by API response. - Skip workflow agent transcripts.
There is also a third bug: with 7d or 30d selected, the "in · out" figures stay all-time.
Bugs 1 and 2 partly cancel each other out, so they need to be fixed together. Fixing only one of them would make the numbers worse.
Possibly related: #82738, which covers the same chart showing no bar on days with real activity. If those days were mostly workflow agents, Bug 2 would explain it.
Bug 1: each API response is counted once per content block
Claude Code writes one JSONL line for each content block of an assistant response (thinking, text, tool_use). Every one of those lines repeats the response's message.id, requestId and usage. The card adds up all of the lines, so a response with thinking, text and a tool call is counted three times.
- In my data, 23,179 API responses are stored as 52,530 lines. One response has 27 lines.
- For sessions without workflows, output tokens come out about 2.7× too high.
- Example, Opus 4.7, all time: the card shows 70k in · 3.4M out. Counting each response once gives 17.3k in · 1.57M out.
Expected: count each response once. Key on message.id + requestId and keep the final usage.
Bug 2: workflow agents aren't counted
- Agents started by the Workflow tool write to
<session>/subagents/workflows/wf_<id>/agent-<id>.jsonl. - The card counts ordinary subagents in
<session>/subagents/agent-<id>.jsonl, but none of the workflow ones. - In my data, workflow agents hold 41% of all real output tokens.
Example: a 19-agent workflow ran 2026-09-24 23:27 → 00:03 CEST. It made 667 API calls: 0.62M output, 3.3M cache writes and 108.6M cache reads. The card shows none of it. Meanwhile the plan meters moved from 18% to 40% (5-hour) and from 81% to 84% (weekly).
Expected: include workflow agent transcripts by scanning subagents/ recursively.
Bug 3: with 7d or 30d selected, "in · out" stays all-time
The percentage column switches to the selected range, but the "X in · Y out" figures don't. With 7d selected, Opus 4.8 shows "3.2M in · 18.7M out · 0.1%". The token figures are all-time totals, while the 0.1% is the 7-day share.
Expected: the in/out figures follow the selected range.
Net effect
Bugs 1 and 2 partly cancel, so the total looks plausible, but the breakdown is wrong.
| | Card | Actual |
|---|---|---|
| All-time in + out (sum of rows) | 50.7M | 32.8M |
| Opus 4.8 share | 43.3% | 31.1% |
| Opus 5.5 share | 14.1% | 23.6% |
| Opus 5 share | 13.9% | 21.3% |
| 2026-09-16 (mostly workflow agents) | 1.25M | 3.26M |
| 2026-09-23 (single agent, heavy thinking) | 5.05M | 2.38M |
Daily figures use CEST dates. "Actual" counts each API response once, across all agents.
Reproduce
Run this against any ~/.claude/projects that has sessions with extended thinking and at least one Workflow run.
- The
chartcolumn matches the card's in + out per model. - The
actualcolumn counts each response once and includes workflow agents.
import json, glob, os, collections
root = os.path.expanduser("~/.claude/projects")
chart = collections.Counter() # what the chart does: every line, workflow agents skipped
real = {} # one entry per API response, all agents included
for path in glob.glob(os.path.join(root, "**", "*.jsonl"), recursive=True):
is_workflow = os.sep + "workflows" + os.sep in path
for line in open(path, encoding="utf-8", errors="replace"):
if '"usage"' not in line:
continue
try:
o = json.loads(line)
except ValueError:
continue
m = o.get("message") or {}
u = m.get("usage")
if o.get("type") != "assistant" or not u or m.get("model") in (None, "<synthetic>"):
continue
tokens = (u.get("input_tokens") or 0) + (u.get("output_tokens") or 0)
if not is_workflow:
chart[m["model"]] += tokens
key = (m.get("id"), o.get("requestId"))
if tokens >= real.get(key, ("", -1))[1]:
real[key] = (m["model"], tokens)
actual = collections.Counter()
for model, tokens in real.values():
actual[model] += tokens
for model in sorted(set(chart) | set(actual), key=lambda k: -actual[k]):
print(f"{model:28s} chart={chart[model] / 1e6:6.2f}M actual={actual[model] / 1e6:6.2f}M")
Related: not bugs, but misleading
- "in" is uncached input only. The card shows
input_tokens, which by API definition excludes cache reads and writes. - In my data, uncached input is 3.9M, against 252M of cache writes and about 6.1B of cache reads.
- So the card hides almost all input and much of what counts toward plan limits. Showing cache reads and writes separately would fix this.
- Workflow notices call a context-size figure "tokens". The
<subagent_tokens>value in the completion notice is the sum of each agent's final context size (input + cache on its last call). - The notice reported 3,773,555; my computation gives 3,773,440.
- It reads like tokens used, but that run actually used 0.62M output and about 112M tokens in total.
- Renaming the field or reporting consumed tokens would help.
- The card covers Code tab sessions only. Cowork sessions aren't included. If that's intended, a label would stop people comparing it with the plan meters.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗