[QUESTION] Does stats-cache.json rebuild lose history for transcripts already cleaned up by cleanupPeriodDays or `project purge`?

Status Open
Reported on v2.1.272
Maintainer reply None cached
Activity 3 comments · opened Sep 15, 2026

Related to #59248.

Context

The docs (claude-directory#application-data) list ~/.claude/stats-cache.json (aggregated totals shown by /usage) under "Kept until you delete them" — explicitly exempt from both the cleanupPeriodDays retention sweep and claude project purge. This is good: it means the historical /usage totals are already decoupled from the raw session transcripts they were presumably built from.

What I found reading the client

Reading the strings in the installed claude CLI binary (v2.1.272), the stats aggregation code (functions building an object shaped like {lastComputedDate, dailyActivity, dailyModelTokens, totalSessions, totalMessages, ...}) has a fallback path: when the cache's version doesn't match, or lastComputedDate looks invalid, it logs "resetting aggregates for a full rescan" and appears to rebuild the aggregate by scanning ~/.claude/projects/**/*.jsonl from scratch.

The question

If that full-rescan path is ever triggered (cache corruption, a stats-cache schema version bump on upgrade, etc.), and the user has since had some transcripts removed — either by the default 30-day cleanupPeriodDays sweep, or by claude project purge — does the rebuilt cache still contain the historical totals for that now-gone period, or does that period silently disappear from /usage's lifetime totals?

The docs don't say either way, and neither project purge's dry-run/plan output nor /usage itself warns about this possibility.

If the answer is "it's lost," that's the same class of problem as #59248 (silent, unrecoverable, no warning) but specific to the aggregated stats rather than the transcripts themselves.

Ask

  1. Clarify whether the full-rescan path can actually drop already-known historical periods when their source transcripts no longer exist.
  2. If so, consider having the rescan preserve previously-cached daily/period entries instead of discarding them when the source transcript is gone — i.e. treat stats-cache.json as the source of truth for periods it already covers, and only use transcripts to fill gaps it doesn't have yet.
  3. Either way, documenting this explicitly in the "Kept until you delete them" section would help, since a user reading that section today would reasonably assume /usage totals are permanent regardless of cleanupPeriodDays or project purge.

Environment

  • Claude Code CLI 2.1.272 (also bundled in Claude Desktop)
  • macOS

View original on GitHub ↗

3 Comments

Strofai · 20 days ago

Follow-up from tracing a related desktop bug (#96940). Checked on Claude Desktop 2.9939.2 and CLI 2.1.281, macOS.

Only the CLI ever writes stats-cache.json. The desktop app's stats worker reads the cache, recomputes the days after lastComputedDate from transcripts on every view, and never persists the result (same finding as #87772). So the file is only as fresh as the last time the CLI Stats screen was opened.

The full-rebuild path is still present in 2.1.281, and a missing file takes it too. If the cache cannot be loaded (Failed to load stats cache: not found), has a version that is not migratable (returning empty cache), or has no usable date (resetting aggregates for a full rescan), the loader returns an empty cache. The next Stats run then logs Stats cache empty, processing all historical data and rebuilds from whatever .jsonl transcripts are still under ~/.claude/projects, then saves that as the new cache.

So the answer to the question above looks like yes: any day whose transcripts were already removed by cleanupPeriodDays or claude project purge would not come back after such a rebuild. One easy way to hit this is moving the file aside while troubleshooting the desktop usage widget, which is a natural thing to try for #96940, and then opening the CLI Stats screen before putting it back. Without a cache, the desktop widget itself only looks back about 182 days.

Would it be possible for the loader to keep a backup of the previous cache (or refuse to overwrite it with a smaller history) before a full rebuild?

quanxiaoxiao · 16 days ago

A concrete answer from my machine (CLI 2.1.284, macOS), at least for the per-model daily breakdown: yes, it is lost.

In my ~/.claude/stats-cache.json (version: 5, dailyModelTokensVersion: 5):

  • dailyActivity goes back to 2026-03-16 (151 days).
  • dailyModelTokens starts at 2026-08-11 (35 entries).
  • 2026-08-11 is the oldest date for which a transcript still exists on disk (transcripts before that were removed by the cleanupPeriodDays sweep).

That matches the code path that logs Rebuilding stats dailyModelTokens through <date> when dailyModelTokensVersion is older than current: it rebuilds dailyModelTokens by scanning the transcripts that exist at that moment, so every earlier day's per-model tokens disappear, while dailyActivity and the lifetime modelUsage totals are kept.

I also see a Stats cache version … not migratable … returning empty cache path, which would presumably drop everything at once; I have not hit it.

So your suggestion (2) — treat the cache as the source of truth for periods it already covers, and only use transcripts to fill gaps — would have prevented this. At minimum the "Kept until you delete them" docs should say that a schema bump can still shrink the history to the retention window.

Strofai · 13 days ago

A new, more direct way to hit this: since 2.1.284, deleting a session from the Desktop sidebar removes its transcript immediately. Because the stats cache only computes completed days, a session deleted on the same day is never counted at all. My heatmap now shows 2026-09-30 as empty after a full day of work. See #98642.