[BUG] Days permanently disappear from the desktop usage heatmap because only the CLI writes the stats cache
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?
The usage heatmap loses data for anyone who mainly uses the desktop app. Mine looked like this before I repaired it: history before 2026-06-18 intact, the last ~30 days intact, and 2026-06-19 → 2026-07-19 (~29 active days) gone — permanently, unless you happen to have file-level backups (I restored mine from Time Machine).
<img width="2000" height="1276" alt="Usage heatmap: cached history on the left, the last ~30 days on the right, and a blank stripe in between where the data was deleted" src="https://github.com/user-attachments/assets/575280a1-5e1b-4180-93c3-308c94339adc" />
The cause is an asymmetry between two mechanisms:
Only the CLI writes the stats cache.
~/.claude/stats-cache.json is the persistence layer for the heatmap. Everything up to its lastComputedDate is served from the cache; everything after is recomputed from transcripts on every view.
- CLI (
~/.local/share/claude/versions/2.1.234, minified; symbol names vary per build): the stats path detects a stale cache, computes the missing window incrementally, merges it in, and saves atomically. Running/usageadvanceslastComputedDate. This is the only writer I could find:
``jsStats cache stale (${s.lastComputedDate}), processing ${d} to ${l}
// ~/.local/share/claude/versions/2.1.234 (single binary, bundled JS inside)
T();Stats cache saved successfully (lastComputedDate: ${e.lastComputedDate})
let p = await Tio(t, {fromDate: d, toDate: l}, e, r);
if (m_h(p)) u = x4l(s, p, l), ... // merge into cache
// ...
await Al().atomicWrite(r, n, 384),
T()``
- Desktop (
/Applications/Claude.app/Contents/Resources/app.asar, extracted): the stringstats-cacheoccurs exactly once in the entire bundle, inside a read-only loader:
``jsstats-cache.json
// .vite/build/heavy-work-worker/heavyWorkWorker.js
async function Ct(e) {
let t = (0, n.join)(e, );utf-8
try {
let e = await a.promises.readFile(t, ), n = JSON.parse(e);``
return Array.isArray(n.dailyActivity) ? n : null;
} catch { return null }
}
The stats worker merges cached days with days recomputed live from ~/.claude/projects, and never persists the merged result:
``js``
// .vite/build/heavy-work-worker/heavyWorkWorker.js
async function At(e) {
let t = await Ct(e), // read cache
r = t?.lastComputedDate ? kt(t.lastComputedDate) // window start: day after lastComputedDate
: St(new Date(Date.now() - 157248e5)), // no cache: last ~182 days
o = await Tt(e), // scan projects/ transcripts
// ... merge and return; no write anywhere
How stats-cache.json behaved on my machine.
- The file was created 2026-06-20 (I'm not sure what wrote it that day — perhaps a one-off CLI run, or an update that introduced the cache). Its
lastComputedDatewas2026-06-18, and it stayed untouched through two months of heavy daily desktop use:
<img width="700" height="137" alt="Finder info for stats-cache.json: created June 20, 2026" src="https://github.com/user-attachments/assets/ea13005c-3148-462c-839e-337e560cc0d6" />
- Running
/usagein the CLI once immediately rewrote it (atomic write, so Finder shows a fresh creation date):
<img width="700" height="115" alt="Finder info for stats-cache.json: created Today, 5:29, right after one CLI /usage run" src="https://github.com/user-attachments/assets/8ada47b9-e47a-4bd4-8c65-bed2d16c32bf" />
Transcript cleanup keeps running regardless.
- Transcripts are deleted after
cleanupPeriodDays(default 30, per the settings schema). - On my machine: the oldest surviving main (top-level) transcripts under
~/.claude/projects/*are from 2026-07-20/21 — right attoday − 30— and.last-cleanupwas refreshed this morning, even though I only use the desktop app. (A few subagent transcripts from 2026-07-17 outlived their parent's deletion, so cleanup isn't perfectly uniform — but the main transcripts are what carry the usage data.)
So for a desktop-first user, every day in the window (lastComputedDate, today − cleanupPeriodDays] has its source data deleted before it is ever persisted. The blank stripe grows by one day per day, and nothing in the desktop app warns about it or can prevent it.
What Should Happen?
Usage history should persist. Days shown on the heatmap shouldn't silently disappear just because the user views their stats through the desktop app instead of the CLI. Possible directions:
- Have the desktop stats worker persist its merged result (or reuse the CLI's save path).
- Fold a transcript's days into the cache before cleanup deletes it.
- Unify day bucketing between CLI and desktop (see Additional Information).
Error Messages/Logs
None — the data loss is silent. No error or warning is shown anywhere.
Steps to Reproduce
- Use only the desktop app (never run
/usagein the CLI) on a machine where~/.claude/stats-cache.jsonexists with an oldlastComputedDate. - Observe that
lastComputedDatenever advances, no matter how heavily the app is used (mine sat at2026-06-18for two months). - Wait until some active days after
lastComputedDatebecome older thancleanupPeriodDays(default 30) — their transcripts under~/.claude/projects/get deleted. - Open the usage panel: those days are now blank, and the underlying data is gone.
Code-level verification without the 30-day wait:
strings ~/.local/share/claude/versions/<ver> | grep "Stats cache saved successfully"→ the CLI writer exists.- Extract
/Applications/Claude.app/Contents/Resources/app.asarand grep forstats-cache→ exactly one occurrence, a read-only loader (readFile+JSON.parse), and the stats worker never persists its merged result. - Run
/usagein the CLI →stats-cache.jsonis immediately rewritten andlastComputedDateadvances; nothing in the desktop app ever does this.
Claude Model
None
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.234 (Claude Code)
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Terminal.app (macOS)
Additional Information
A related inconsistency: CLI and desktop bucket days on different calendars.
- Both sides reduce each message timestamp to a plain
YYYY-MM-DDbucket key, but the CLI buckets by UTC day (toISOString().split("T")[0]) while the desktop recomputation buckets by local day (getFullYear()/getMonth()/getDate()). - The stored string keeps no trace of which convention produced it.
- For a UTC+9 user, activity between 00:00 and 09:00 local time lands on one day in cache-era cells and on the next day in live-computed cells — shifted cells, and a seam at the
lastComputedDateboundary.
There are several earlier issues about missing or inconsistent usage stats (e.g. #62397, #77956, #85509, #83405), but they describe the symptoms; as far as I can tell none of them identify this write/delete asymmetry in the code as the cause.
Desktop app: current as of 2026-08-19, macOS.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗