Stats "Peak hour" ignores the 7d/30d/All range selector — hourCounts is a flat dateless histogram, and it goes stale

Status Open
Reported on v2.1.220
Maintainer reply None cached
Activity 0 comments · opened Aug 3, 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?

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 mtime 2026-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/**.jsonl while Peak hour comes from the frozen cache.
  • The hourCounts[4] = 43 spike came from a scheduled job that launched headless claude -p sessions 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:

  1. Peak hour should respect the selected range. hourCounts needs 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 way dailyActivity already 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.
  1. 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 hourCounts structure, 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

  1. Use Claude Code normally for a few months.
  2. Produce a burst of sessions at an unusual hour — e.g. a cron/launchd job invoking claude -p at 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.)
  3. Wait long enough for retention cleanup to remove those transcripts from ~/.claude/projects/.
  4. Open the desktop Home / "What's up next" overview and toggle All → 30d → 7d.
  5. Observe Sessions / Messages / Total tokens / Active days all change, while Peak hour stays fixed at the burst hour.
  6. Inspect ~/.claude/stats-cache.jsonhourCounts has no date dimension, and its max bucket is the burst hour.
  7. Aggregate ~/.claude/projects/**/*.jsonl by 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.

View original on GitHub ↗