[BUG] Desktop: heavy-work stats worker rescans every transcript (182 days) at each launch and holds ~2.8 GB until quit; pre-v5 stats-cache.json is rejected so the full scan always runs
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet. Related but distinct: #87772 (same worker and cache, reported as heatmap data loss, not memory), #90452 / #80056 (desktop memory symptom reports without a cause; #90452's "returns immediately after a full restart" matches the launch-time scan described here), #94559 (different scale, cause unidentified), #98269 / #96940 / #85509 (stats correctness).
- [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?
Summary. With the desktop app open but idle (no session open, no Cowork, no browser pane), Activity Monitor shows a "Claude Helper" process at ~2.8 GB and the whole Claude process tree at 5.4–6.2 GB. The 2.8 GB process is the heavy-work NodeService utility. It got there within ~60 s of launch by running the Code tab start-screen usage statistics (codeStats) over every transcript in ~/.claude/projects modified in the last 182 days, and it keeps that heap for the lifetime of the app. On my machine the cause of the full scan is that the worker rejects any stats-cache.json that lacks dailyModelTokensVersion >= 5, which is every cache written by a CLI before that field existed, and the desktop never writes a cache of its own (#87772). An A/B test on the same machine confirms it: refreshing the cache from the CLI drops the next launch from "scanned 509/509 files" to "scanned 0/510 files" and the total Claude footprint from ~5.7 GB to ~1.5 GB.
Environment
- Claude Desktop 2.19675.0 (Electron 44.4.3), macOS 26.7.1, Apple Silicon, 24 GB RAM
- Bundled Claude Code 2.1.286; standalone CLI 2.1.273 (used for the workaround)
~/.claude/projects: 546.jsonlfiles, 2.09 GB in total, 4 files over 100 MB (largest ~490 MB). All are desktop-created transcripts, which are exempt fromcleanupPeriodDaysby design (desktopSessionCleanupPeriodDaysdefaults to 0), so this volume is the expected steady state for a desktop-first user.
Observed footprint while idle (from the app's own [process-memory] log lines and top; "footprint" is what Activity Monitor shows)
| Process | Footprint | Resident | Notes |
|---|---|---|---|
| Claude Helper, --utility-sub-type=node.mojom.NodeService (heavy-work) | 2.8 GB (vmmap peak 3.7 GB) | 36–45 MB | 2.4 GB of V8 pages compressed, almost nothing resident |
| Claude Helper (Renderer), main window | 1.3–1.7 GB | 200–300 MB | separate, bounded by the transcript tail-load limits |
| Claude (main) | 0.7 GB | ~200 MB | |
| Everything else (GPU, network, other workers, CLI sessions) | ~0.6–0.9 GB | | |
It reproduces on every launch. Peak heavy-work footprint per launch over three days: 3065 MB (Oct 1), 2776 MB (Oct 2), 2091 MB and 2709 MB (two launches Oct 3). Each additional scan during a session adds a little (2709 → 2786 → 2800 MB after the start screen was shown again). The worker then sits at that size indefinitely; I watched it hold 2.8 GB at 0% CPU for over two hours. macOS compresses the pages (this machine runs ~1.69:1, so roughly 1.7 GB of real RAM), and the app's own CliGovernor logged 93 memory pressure (warning) lines in the current main.log, but the governor only evicts CLI sessions, never this worker.
Root cause, from the shipped bundle (app.asar, .vite/build/heavy-work-worker/heavyWorkWorker.js; minified names will differ per build)
- Trigger and frequency. The renderer calls
getCodeStats→codeStatson theheavy-workworker. The result is memoized for only 60 s (Pe=6e4), so the scan runs at launch and again whenever the start screen is shown after a minute (I saw 4 full scans in one day: launch, two session opens, one navigation).
- Cache acceptance is strict. The worker's cache reader returns the cache only when
``js`
Array.isArray(n.dailyActivity) && (n.dailyModelTokensVersion ?? 0) >= Yi // Yi = 5
`
Otherwise it falls back to a full window:
js`
i = r?.lastComputedDate ? ra(r.lastComputedDate) // day after lastComputedDate
: Xi(new Date(Date.now() - 157248e5)); // 182 days ago
...
if (t.mtimeMs < o) return; // skip files not modified since fromDate
if (t.size > Ki) { _++; return; } // Ki = 1 GiB: only files above this are skipped
stats-cache.json
My was version: 4 from June 2026 with no dailyModelTokensVersion, so it was rejected and every launch logged scanned 509/509 transcript files since <today − 182 d>. The code quoted in #87772 (2026-08-18) accepted any cache with a dailyActivity` array, so the version gate is newer than that report. Because the desktop never writes the cache, a desktop-first user has no path to a valid cache except running the CLI.
- The worker never gives the memory back. Files are streamed line by line (8 files concurrently,
Ji=8; lines over 16 MiB skipped), so this is allocation-rate pressure rather than whole-file buffering, but the process is a long-livedutilityProcess.fork(bundle, [], { serviceName: "Claude Desktop Heavy Work", stdio: "pipe" })with no--max-old-space-size, no idle exit, and nothing allocates after the scan, so V8 never shrinks.vmmap --summaryon the worker:Physical footprint: 2.7G,Physical footprint (peak): 3.7G,Memory Tag 255 ... RESIDENT 288K ... SWAPPED 2.4G, malloc zones under 1 MB. The host already has a refork path ([heavy-work] worker exited (0); will refork on next request), so a post-task exit would be cheap.
A/B verification on the same machine, same transcripts
| | Before | After running claude → /stats once in Terminal, then relaunching the desktop app |
|---|---|---|
| stats-cache.json | version 4, no dailyModelTokensVersion, lastComputedDate 2026-06-27 | version 5, dailyModelTokensVersion 5, lastComputedDate 2026-10-03 |
| Scan log line | scanned 509/509 transcript files since 2026-04-04 | scanned 0/510 transcript files since 2026-10-04 |
| heavy-work footprint | 2709–2800 MB | not in the top-5 footprint list |
| Total Claude tree footprint (tree_footprint_sum) | 5545–5708 MB | 1384–1640 MB, stable for the following hour |
Nothing else changed: same 546 transcripts, same sessions, no files moved or deleted.
What Should Happen?
- An idle background worker should not hold gigabytes for the app's lifetime. Options, roughly in order of cost: exit the heavy-work process after
codeStatscompletes (the refork path exists), or run the scan in a short-lived child, or at least cap it with--max-old-space-sizeand trigger a GC after the task. - The desktop should persist the cache it computes (the same ask as #87772), so launches are incremental instead of a 182-day scan. The 60 s memo could also be keyed on transcript mtimes rather than wall-clock.
- An older-format cache should be upgraded or partially reused rather than discarded wholesale; right now the version gate turns every pre-v5 cache into a full scan with no indication to the user.
- Memory held by this worker should count for the app's own memory-pressure handling, or at least be visible in the UI as "computing usage stats".
Error Messages/Logs
# ~/Library/Logs/Claude/main.log — launch at 16:47:27, worker forked 16:47:35
16:47:35 [info] [heavy-work] worker forked
16:47:46 [info] [CCD] /stats (worker) scanned 509/509 transcript files since 2026-04-04
16:48:30 [info] [process-memory] trigger=interval tree_rss_sum=719MB tree_footprint_sum=5534MB ... top_fp=[electron_utility:1236:fp=2709MB:rss=45MB electron_renderer:1225:fp=1605MB:rss=192MB electron_main:955:fp=768MB:rss=147MB ...]
16:50:06 [warn] [CliGovernor] memory pressure (warning): evicted 0 of 0 idle session(s), 0 effective
16:58:54 [info] [CCD] /stats (worker) scanned 509/509 transcript files since 2026-04-04
16:59:18 [info] [process-memory] ... top_fp=[electron_utility:1236:fp=2786MB:rss=2895MB electron_renderer:1225:fp=1577MB:rss=1130MB ...]
17:07:23 [info] [process-memory] ... tree_footprint_sum=6229MB ... electron_utility:1236:fp=2800MB:rss=249MB
18:56:05 [info] [process-memory] ... tree_footprint_sum=5545MB ... electron_utility:1236:fp=2800MB:rss=37MB # still held, ~2 h idle
# after the CLI rewrote stats-cache.json and the app was relaunched
18:56:18 [info] [heavy-work] worker forked
18:56:19 [info] [CCD] /stats (worker) scanned 0/510 transcript files since 2026-10-04
18:57:21 [info] [process-memory] trigger=interval ... tree_footprint_sum=1384MB
19:01:23 [info] [process-memory] trigger=interval ... tree_footprint_sum=1577MB
# vmmap --summary <heavy-work pid>, before the fix
Physical footprint: 2.7G
Physical footprint (peak): 3.7G
Memory Tag 255 1.4T virtual 288K resident 2.4G swapped
MALLOC_SMALL 12.0M 16K 272K
Steps to Reproduce
- Use the desktop app's Code tab as your main surface for a few months so
~/.claude/projectsholds a couple of GB of transcripts (desktop transcripts are kept indefinitely by default, so this happens on its own). - Have no
~/.claude/stats-cache.json, or one written by a CLI before thedailyModelTokensVersionfield existed (check withpython3 -c 'import json;print(json.load(open("$HOME/.claude/stats-cache.json")).get("dailyModelTokensVersion"))';Nonereproduces). - Launch Claude Desktop. Do not open any session. Wait 60 s.
- In
~/Library/Logs/Claude/main.log, observe/stats (worker) scanned N/N transcript files since <today − 182 days>followed by a[process-memory]line withelectron_utility:<pid>:fp=<2–3 GB>:rss=<tens of MB>. Activity Monitor shows "Claude Helper" at that size and it never comes down. - Control: in Terminal run
claude, type/stats, Esc,/exit. Relaunch the desktop app. The scan line now readsscanned 0/N ... since <tomorrow>and the helper stays small.
Claude Model
Not sure / Multiple models
Is this a regression?
I don't know
Last Working Version
Unknown. The version gate that rejects older caches is newer than the reader quoted in #87772 (2026-08-18); the retained heap may predate it.
Claude Code Version
Claude Desktop 2.19675.0 (bundled Claude Code 2.1.286); standalone CLI 2.1.273
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Terminal.app (only for the CLI workaround; the bug is in the desktop app)
Additional Information
Workaround for affected users (no files touched by hand): run claude in Terminal, type /stats, press Esc, type /exit. The CLI rewrites ~/.claude/stats-cache.json in the current format; the desktop accepts it on the next launch and only parses transcripts modified after the cache date. The desktop never updates the cache, so repeat occasionally. Running /stats inside the Code tab does not work: the desktop handles that command itself and never writes the file. A worker that is already bloated only shrinks after quitting and relaunching the app.
This is deliberately scoped to the memory behaviour of the heavy-work worker. The correctness problems of the same code path are tracked in #87772, #98269 and #91775. Happy to provide full [process-memory] log excerpts or run a build with instrumentation.