[BUG] Desktop Code tab usage chart miscounts tokens: duplicate transcript lines summed, workflow agents skipped, 7d legend shows all-time

Status Open
Reported on v2.1.280
Maintainer reply None cached
Activity 2 comments · opened Sep 25, 2026

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:

  1. Add up the usage on every transcript line, without de-duplicating by API response.
  2. 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 chart column matches the card's in + out per model.
  • The actual column 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.

View original on GitHub ↗

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