Stats "Peak hour" ignores the 7d/30d/All range selector — hourCounts is a flat dateless histogram, and it goes stale
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?
On the desktop Home / "What's up next" overview, the Peak hour tile is identical for 7d, 30d, and All. The other tiles (Sessions, Messages, Total tokens, Active days) all respond correctly to the selector — only Peak hour is inert.
The cause is visible in ~/.claude/stats-cache.json. The cache stores per-day arrays for the windowed metrics:
"dailyActivity": [ { "date": "2026-06-16", "messageCount": 4666, "sessionCount": 22, ... }, ... ],
"dailyModelTokens": [ { "date": "2026-06-16", "tokensByModel": { ... } }, ... ],
…but stores hours as a flat 24-bucket histogram with no date dimension at all:
"hourCounts": { "0": 5, "1": 4, "2": 4, "3": 2, "4": 43, "5": 31, "6": 8, "7": 10,
"8": 25, "9": 13, "10": 12, "11": 16, "12": 14, "13": 14, "14": 6,
"15": 4, "16": 21, "17": 7, "18": 4, "19": 6, "20": 14, "21": 9,
"22": 18, "23": 6 }
There is no date attached to any bucket, so the range selector has nothing to filter on. Peak hour is all-time by construction, whatever window is selected.
sum(hourCounts) == 296 == totalSessions exactly, confirming one bucket per session keyed on its start hour — consistent with the root-cause snippet in #65775.
This compounds badly because the cache also goes stale. On my machine:
lastComputedDate: "2026-07-07", file mtime2026-07-08 18:13— not rewritten in ~4 weeks.- The windowed tiles nonetheless show current data (51 sessions / 7,008 messages / 13.8M tokens for 7d), so those are computed live from
~/.claude/projects/**.jsonlwhile Peak hour comes from the frozen cache. - The
hourCounts[4] = 43spike came from a scheduled job that launched headlessclaude -psessions at 04:00 daily for about two weeks in June. That job has been disabled since late June. - Retention cleanup has since removed those June transcripts. Across the 984 transcripts currently on disk, exactly 0 start at hour 4 — and 0 messages land in that hour over the last 45 days.
So the dashboard reports a peak hour that has had zero recorded activity for over a month, and the only surviving evidence for it is a cache that stopped updating. My actual peak is 5–6 PM by every measure:
| Aggregation over current on-disk data | Peak hour |
|---|---|
| Session start hour (984 transcripts, all-time) | 17:00 (234) |
| Session starts, last 7d | 18:00 |
| User messages, last 7d | 18:00 |
| Assistant API calls (dedup by message.id), last 7d | 18:00 |
| Output tokens, last 7d | 18:00 |
| Dashboard "Peak hour" (7d, 30d, and All) | 4 AM |
Hour 4 does not appear in the top 20 by any method.
What Should Happen?
Two things:
- Peak hour should respect the selected range.
hourCountsneeds a date dimension — e.g. per-day hour buckets ({ date, hourCounts[24] }), or day+hour keys — so the 7d/30d/All selector can filter it the same waydailyActivityalready allows for Sessions/Messages/Active days. Right now the tile silently reports an all-time figure under a "7d" label, which is worse than showing nothing.
- The cached histogram should be invalidated/recomputed on the same cadence as the live-computed tiles, so a long-dead usage pattern can't stay pinned indefinitely. Presenting a stale all-time aggregate alongside freshly-computed windowed tiles makes the panel internally inconsistent.
Related but distinct, both closed as stale:
- #65775 — same
hourCountsstructure, but reports start-hour vs per-message bucketing. Fixing that alone would not fix this; a per-message histogram with no date dimension is still unfilterable. - #63044 — Peak hour off by one hour (timezone bucketing). Different root cause; here the value is off by 13–14 hours and is window-independent.
Error Messages/Logs
$ python3 -c "
import json,os
d=json.load(open(os.path.expanduser('~/.claude/stats-cache.json')))
print('lastComputedDate:', d['lastComputedDate'])
print('sum(hourCounts):', sum(d['hourCounts'].values()), ' totalSessions:', d['totalSessions'])
print('peak bucket:', max(d['hourCounts'].items(), key=lambda kv: kv[1]))
"
lastComputedDate: 2026-07-07
sum(hourCounts): 296 totalSessions: 296
peak bucket: ('4', 43)
$ stat -f "%Sm %N" -t "%Y-%m-%d %H:%M:%S" ~/.claude/stats-cache.json
2026-07-08 18:13:17 /Users/<user>/.claude/stats-cache.json
Steps to Reproduce
- Use Claude Code normally for a few months.
- Produce a burst of sessions at an unusual hour — e.g. a cron/launchd job invoking
claude -pat 04:00 — for a week or two, then stop it. (Any concentrated off-hours burst will do; it just has to out-count the individual normal-usage hours.) - Wait long enough for retention cleanup to remove those transcripts from
~/.claude/projects/. - Open the desktop Home / "What's up next" overview and toggle All → 30d → 7d.
- Observe Sessions / Messages / Total tokens / Active days all change, while Peak hour stays fixed at the burst hour.
- Inspect
~/.claude/stats-cache.json→hourCountshas no date dimension, and its max bucket is the burst hour. - Aggregate
~/.claude/projects/**/*.jsonlby local hour for the last 7 days — the real peak differs, and the reported hour may have zero activity.
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.220
Platform
Claude subscription
Operating System
macOS
Terminal/Shell
Terminal.app (macOS)
Additional Information
- Desktop app:
1.24012.9 - macOS 14.6.1 (23G93)
- Local timezone: EDT (UTC-4)
Note that a plain cache rebuild is not a complete user-side workaround: deleting stats-cache.json forces recomputation from surviving transcripts only, which — because retention has already trimmed them — also discards the all-time history the cache uniquely holds (totalSessions: 296, totalMessages: 50997, firstSessionDate). #62397 (durable stats ledger) is relevant to that side of the problem.