[BUG] Usage stats count a forked session's copied history again (dedup must be across transcript files, not per file)

Status Open
Reported on v2.1.291
Maintainer reply None cached
Activity 0 comments · opened Oct 15, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Forking a session writes a new transcript that starts with a copy of the parent's history. The copied assistant rows keep the parent's message.id, requestId, timestamp and sessionId, including their usage. The usage stats scan every transcript file and count these copies again, on the parent's original dates.

This is separate from #91775. That issue is about one response being split over several rows within a file. Deduplicating by (message.id, requestId) per file would fix #91775 but not this one. The copies are in another file, so the dedup has to be across all transcripts.

Minimal repro (Desktop, Code tab). I forked one session once and compared the two transcripts:

| | responses (distinct message.id, requestId) | tokens (input + output + cache read + cache write) |
|---|---:|---:|
| parent | 506 | 209.8M |
| fork | 497 | 208.6M |
| shared by both | 479 | 205.2M |
| new in the fork | 18 | 3.3M |

All 479 shared responses have identical timestamps in both files, and the fork's copies still carry the parent's sessionId. One fork adds about 205M tokens to the stats, although only 3.3M were used after forking.

All transcripts on this machine (~/.claude/projects/**/*.jsonl, including subagents/; 607 files, 128,127 usage rows):

| counting | total tokens | vs. actual |
|---|---:|---:|
| once per row (what #91775 shows Stats does) | 48.18B | 5.53x |
| once per (message.id, requestId) per file | 21.18B | 2.43x |
| once per (message.id, requestId) across all files | 8.71B | 1.00x |

The 12.47B gap between the last two rows comes entirely from responses copied into other transcript files, in 209 files. All of the copies keep the original timestamp.

  • 6.17B are copies that still carry another session's sessionId (fork history).
  • 5.21B are whole-session copies between main transcripts.
  • 1.09B are copied subagents/ transcripts.

An independent tracker that deduplicates across files (toktrack) reports 9.17B over the days it has cached. That agrees with the across-files row, not the per-file row. Its cache covers a slightly different date range, so the figures are close but not identical.

These figures come from the transcripts, not from a side-by-side comparison with the Stats screen. The Stats screen on this machine is clearly above the deduplicated total.

What Should Happen?

Each API response should be counted once across all transcripts, keyed by (message.id, requestId), so forking or branching a session does not add the parent's history to the totals again. If #91775 is fixed by deduplicating per file, this case still remains.

Error Messages/Logs

None

Steps to Reproduce

  1. In the Desktop app (Code tab), use a session for a while, then fork it.
  2. Compare the two transcripts in ~/.claude/projects/<project>/: most assistant rows in the fork have the same message.id, requestId, timestamp and sessionId as rows in the parent.
  3. The usage stats increase by roughly the parent's whole history, on the parent's dates.

Script used for the counts (read-only):

import json, os
root = os.path.expanduser("~/.claude/projects")
per_row = per_file = across = 0
seen = set()
for dp, _, fs in os.walk(root):
    for f in fs:
        if not f.endswith(".jsonl"):
            continue
        seen_file = set()
        for line in open(os.path.join(dp, f), encoding="utf-8", errors="replace"):
            try:
                r = json.loads(line)
            except ValueError:
                continue
            m = r.get("message") or {}
            u = m.get("usage")
            if r.get("type") != "assistant" or not u or m.get("model") == "<synthetic>":
                continue
            t = sum(u.get(k) or 0 for k in ("input_tokens", "output_tokens",
                    "cache_read_input_tokens", "cache_creation_input_tokens"))
            per_row += t
            key = (m.get("id"), r.get("requestId"))
            if key not in seen_file:
                seen_file.add(key)
                per_file += t
                if key not in seen:
                    seen.add(key)
                    across += t
print(per_row, per_file, across)

Claude Model

Not sure / Multiple models

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.291 (Claude Code), Claude Desktop 2.37337.1

Platform

Anthropic API

Operating System

Windows

Terminal/Shell

Other

Additional Information

Related: #91775 (per-row counting within a file), #98668 / #98578 (Desktop total-token jumps).

View original on GitHub ↗