anthropics / anthropics/claude-code
[BUG] Desktop: stalls over 5s are discarded as "[likely sleep: duration_heuristic]" and dropped from the app's own counters — 91/1129 samples, zero sleep events in Windows' power log
- Langage dominant
- Python
- Étoiles
- 145k
- Forks
- 23.1k
- Métriques de merge des PR
- Métriques de PR en attente
Description
### Preflight Checklist
- [x] I have searched existing issues. The closest matches are related-but-distinct and are separated in **Related** below (#89679, #88072, #91633, #89772).
- [x] This is a single bug report.
- [x] I am using the latest version of Claude Code.
### What's Wrong?
**The Desktop main process discards its own longest event-loop stalls as "the machine probably slept", using a duration-only test. The machine never slept.**
Every stall over ~5 s is tagged `[likely sleep: duration_heuristic]`, and **a tagged stall is not counted at all** — it contributes zero to both the `total` and `cumulative` counters the app maintains. So the app's own stall accounting is systematically blind to exactly the stalls that hurt, including a 16,945 ms one logged 14 s before the process was killed for being hung.
**1. The threshold is duration and nothing else.**
Across all four rotated `main.log` files — **1,129 stalls, 2026-08-21 to 2026-09-05, spanning at least 6 desktop builds**:
| | value |
|---|---|
| stalls tagged `[likely sleep: duration_heuristic]` | 91 / 1,129 |
| **smallest tagged stall** | **5,022 ms** |
| **largest untagged stall** | **4,976 ms** |
| overlap | **none** (46 ms gap) |
| distinct reason strings in 1,129 samples | **1** — `duration_heuristic` |
Duration is necessary *and* sufficient. No second condition fires anywhere in the corpus. Two adjacent samples, same day, same task, 7 minutes apart:
```
2026-09-05 12:18:25 [warn] [event-loop-stall] main process blocked for 5022ms [likely sleep: duration_heuristic] (task sdk:stream-delta-flush=0ms, total 11, cumulative 18325ms, rss 477MB)
2026-09-05 12:25:17 [warn] [event-loop-stall] main process blocked for 4911ms (task sdk:stream-delta-flush=0ms, total 26, cumulative 37143ms, rss 476MB)
```
**2. The machine did not sleep — Windows' own power log says so.**
```powershell
Get-WinEvent -FilterHashtable @{LogName='System';
ProviderName='Microsoft-Windows-Kernel-Power';
StartTime=(Get-Date '2026-08-21'); Id=42,107,506,507}
# → 0 events
```
Zero sleep (42), zero resume (107), zero modern-standby enter/exit (506/507) across the entire 16-day window. **All 91 tags are false**, not just the ones I sampled directly.
**3. A tagged stall is erased from the app's own counters.** This is the part that makes it more than a cosmetic label:
```
2026-09-03 18:28:11 blocked for 905ms (task webrequest:before-send-headers=0ms, total 1, cumulative 905ms, rss 306MB)
2026-09-03 18:28:17 blocked for 5697ms [likely sleep: duration_heuristic] (task none, total 1, cumulative 905ms, rss 297MB)
2026-09-03 18:28:38 blocked for 13001ms [likely sleep: duration_heuristic] (task none, total 1, cumulative 905ms, rss 292MB)
2026-09-03 18:28:48 blocked for 927ms (task none, total 2, cumulative 1832ms, rss 262MB)
```
`total` stays at 1 and `cumulative` stays at 905 ms across an 18.7-second pair of stalls, then the next *untagged* 927 ms stall increments to `total 2, cumulative 1832ms` — exactly 905 + 927. **18.7 s of frozen UI left no trace in the app's own numbers.** Across the corpus that is 91 stalls totalling roughly 700 s silently dropped.
The same pattern is visible in **#88072's excerpt from a different user's machine** (`5516ms [likely sleep…] total 1, cumulative 720ms` — unchanged from the line before), so this is not local to me.
**4. Independent confirmation that the process was awake and working during these "sleeps".**
An external watchdog holds a handle on the process, polls `IsHungAppWindow`, and on detection writes a minidump plus per-thread CPU deltas. Each detection opens an *episode* that ends when the window answers again. For four of the long tagged stalls, **the watchdog's episode overlaps the stall window** (a stall line is written when the loop resumes, so the window is `[t − duration, t]`):
| tagged stall (window) | watchdog episode | overlap |
|---|---|---|
| 19:47:45 → 19:48:04 (18,611 ms) | 19:47:51 → 19:48:16 | detection inside the window |
| 20:01:20 → 20:01:32 (12,086 ms) | 20:01:15 → 20:01:32 | episode spans the entire window; detection begins 4.9 s **before** it |
| 14:24:54 → 14:25:11 (16,945 ms) | 14:25:00 → 14:25:27 | detection inside the window |
| 19:25:47 → 19:25:58 (10,459 ms) | 19:25:55 → 19:26:09 | detection inside the window |
*(Correction to the first version of this table, which listed all four as "detection inside the window". Row 2's detection precedes the window by 4.9 s — the window had already stopped answering before the stall the app logged. The episode still covers it.)*
A separate process was demonstrably executing, sampling and writing files inside each of those windows. A suspended machine produces none of that.
In one case the CPU sample itself also lands inside the stall: at **20:01:31**, during the 12,086 ms "sleep", the UI thread had consumed **625 ms of a 990 ms sampling window (63.1%)**. For the other three the CPU sample was taken 10–15 s after the stall ended, so I am *not* claiming those numbers describe the tagged stall itself.
### What the UI thread is doing
Nine captures had a resolvable UI-thread record. Read with WinDbg `cdb`, thread 0 = `CrBrowserMain`:
| UI CPU | top frame |
|---|---|
| 81.1 / 80.5 / 77.2 / 71.1 / 54.7 / 41.6 % (6 captures) | `win32u!NtUserMsgWaitForMultipleObjectsEx` via `user32!RealMsgWaitForMultipleObjectsEx` — message pump |
| 63.1 % | **`ntdll!NtQueryDirectoryFileEx+0x14`** — directory enumeration |
| 8.4 % | **`ntdll!NtClose+0x14` via `kernel32!GetLongPathNameW+0x1f9`** — path normalization |
| 21.9 % | `win32u!NtUserSetCursor+0x14` |
6/9 sit in the message pump; **3/9 are synchronous filesystem/GDI work on the Electron browser thread**, two of them sync FS. That is a concrete, fixable shape.
Across all 23 captures the UI-thread CPU ratio splits into 17 at 21.9–85.0% and 6 at 0.6–8.4%, with a 13-point gap and nothing inside it — so "not responding" is covering at least two mechanisms (spinning vs genuinely blocked), and a duration-only heuristic cannot distinguish them either.
Caveat: frames inside `claude.exe` resolve only as "nearest exported symbol plus offset" (no private PDB for this build), so **I am not asserting any `claude!...` function name** — only that the module is `claude.exe`. Every frame quoted above is an OS module and symbolizes correctly.
### Which tasks stall
2026-09-05, all 83 stalls up to 19:33:
| `task` tag | count |
|---|---|
| `sdk:stream-delta-flush` | 33 |
| `webrequest:headers-received` | 14 |
| `none` | 11 |
| `webrequest:before-send-headers` | 9 |
| `Resources.setFocusedCwd` | 3 |
| `pty:emit-flush` · `WindowControl.reportSidebarPlaceholder` · `LocalSessions.getGitDiffStats` | 2 each |
| `worker:git:stdio` · `worker:file-index:reply` · `RendererMemoryReporter.report` · `LocalSessions.setVisibility` · `LocalSessionPresets.load` · `DesktopInfo.getDefaultBrowserName` · `ComputerUseTcc.listInstalledApps` | 1 each |
`sdk:stream-delta-flush` — flushing streamed response deltas to the UI — is the largest single tag at **33 of the 72 stalls carrying a task label (46%)** — "tagged" here means a `task` label, not the `[likely sleep]` tag used elsewhere in this report. The tag records what was running when the loop unblocked, not necessarily the cause, but the concentration is the app's own label.
### What Should Happen?
1. **The sleep heuristic must not be duration-only, and a suspected sleep must not silently zero the counters.** The CLI already logs `cpu=` next to wall time for this same stall type (see the excerpts in #91633) — a stall where CPU ≈ wall is by definition not a suspend. Windows also exposes the ground truth directly (Kernel-Power 42/107, or a monotonic-vs-wall-clock delta). As written, the longer a stall is, the more certainly it is discarded, and the app's own telemetry cannot see its worst cases.
2. **Streaming response deltas should not be flushed on a path that can block the main process for seconds.**
3. **Directory enumeration and path normalization should not run synchronously on `CrBrowserMain`.**
### Error Messages/Logs
Six stalls ≥10 s in the most recent log file, verbatim and complete:
```
2026-09-01 19:48:04 [warn] [event-loop-stall] main process blocked for 18611ms [likely sleep: duration_heuristic] (task sdk:stream-delta-flush=0ms, total 47, cumulative 61575ms, rss 461MB)
2026-09-01 20:01:32 [warn] [event-loop-stall] main process blocked for 12086ms [likely sleep: duration_heuristic] (task LocalSessions.setVisibility=0ms, total 57, cumulative 75127ms, rss 463MB)
2026-09-02 21:56:11 [warn] [event-loop-stall] main process blocked for 10097ms [likely sleep: duration_heuristic] (task CoworkSpaces.getRemoteSessionSpaces=0ms, total 16, cumulative 17778ms, rss 442MB)
2026-09-03 18:28:38 [warn] [event-loop-stall] main process blocked for 13001ms [likely sleep: duration_heuristic] (task none, total 1, cumulative 905ms, rss 292MB)
2026-09-05 14:25:11 [warn] [event-loop-stall] main process blocked for 16945ms [likely sleep: duration_heuristic] (task webrequest:headers-received=0ms, total 34, cumulative 52872ms, rss 458MB)
2026-09-05 19:25:58 [warn] [event-loop-stall] main process blocked for 10459ms [likely sleep: duration_heuristic] (task none, total 30, cumulative 46170ms, rss 553MB)
```
The 14:25:11 line is the last stall line before the process was killed at 14:25:25.
The same pattern repeats five hours later the same day. A 7,652 ms tagged stall at 19:33:11 (`task WindowControl.reportSidebarPlaceholder=8ms`) is followed 4 s later by a second hang-kill at 19:33:15, same exit code `0xCFFFFFFF`. **Two hang-kills in one day, each preceded within seconds by a stall the app discarded as sleep.** (19:33:11 is just outside the "up to 19:33" cutoff of the task-tag table above, which is why it does not appear there.)
A second suspend-inference path exists and is worth checking alongside the first:
```
2026-09-04 22:11:34 [warn] [event-loop-stall] update relaunch (fresh marker) stands in for an unheard suspend
2026-09-05 08:56:37 [warn] [event-loop-stall] update relaunch (fresh marker) stands in for an unheard suspend
```
`git status` timing out on the main process — 12 occurrences on 2026-09-05, stack trimmed to the app's own frames:
```
[error] [GitStatusService] Failed to check git status: git status timed out { code: 'EGITTIMEDOUT',
at es (app.asar/.vite/build/index.chunk-BfvspfsG.js:1:172483)
at rs (app.asar/.vite/build/index.chunk-BfvspfsG.js:1:172781)
at Object.fetchPorcelainStatus (app.asar/.vite/build/index.chunk-BfvspfsG.js:1:173675)
at async e.fetchStatusSummary (app.asar/.vite/build/index.chunk-BAoKVfI_.js:1:11103)
at async e.isWorkingTreeDirty (app.asar/.vite/build/index.chunk-BAoKVfI_.js:1:17876)
at async Session. (node:electron/js2c/browser_init:2:118168) }
```
This runs on `browser_init`, i.e. the main process.
### Steps to Reproduce
**Findings 1 and 3 reproduce from the log alone, on any platform, with no external tooling:**
1. Use Claude Desktop until `%LOCALAPPDATA%\Claude\Logs\main.log` has accumulated `[event-loop-stall]` lines (here: ~83/day under normal use).
2. Every line over ~5 s carries `[likely sleep: duration_heuristic]`; none under ~5 s does. Sort by duration and the split has no overlap.
3. Compare the `total` and `cumulative` fields across consecutive lines: tagged lines never increment either.
4. Confirm against Windows' power log (command above) that no sleep or resume occurred.
Finding 4 needs something outside the process sampling it during the stall, because the app's own instrumentation is what is in question. I used an `IsHungAppWindow` poller taking per-thread CPU deltas and a minidump.
Conditions here, for context — I am not claiming these are required: 6–24 concurrent claude-code sessions with several actively streaming; sessions using git worktrees; a repository large enough that `GitStatusService` sometimes exceeds its own timeout.
**Concurrency does not explain severity.** Counting distinct `claude-code` CLI processes in the process tree at capture time, across ten paired samples: **23 sessions produced 0.6% UI CPU, and 24 sessions produced 73.4%** — near-identical load, opposite outcomes. The range across all ten is 6–24 and it does not order the severities.
### Claude Model
Multiple — not relevant; the stalls are in the desktop shell.
### Is this a regression?
I don't know. The 1,129-stall corpus spans at least 6 desktop builds (1.40609.0 through 1.46388.4) and the threshold behaves identically in all of them.
### Claude Code Version
CCD/harness `2.1.260`. Desktop app `1.46388.3` and `1.46388.4` — **both** produced hang-kills on 2026-09-05, the second one on the newer build. Non-MSIX install (`%LOCALAPPDATA%\AnthropicClaude`).
### Platform
Anthropic API
### Operating System
Windows 10 Home 19045, x64. 32 cores, 64 GB RAM. Repository on a local drive.
Memory was not the constraint at the 14:25 kill: `sys_free=18064MB/65417MB`, `tree_rss_sum=17728MB`, 261 child processes.
### Terminal/Shell
Desktop app, not launched from a terminal.
### Related
- **#89679** (freeze-then-vanish, zero forensic trace) — same install; I comment there. That issue is about the *kill*; this one is about the *stall*, which happens ~83 times a day and almost never kills anything. They touch at one point: the 16,945 ms tagged stall 14 s before the 14:25 kill.
- **#88072** (auto-updater blocks the main event loop) — **corroboration, not a duplicate.** Its excerpt shows untagged stalls at 4,284 / 1,672 / 720 ms and a tagged one at 5,516 ms: **the same ~5,000 ms boundary and the same frozen counters, on a different user's machine.** (Its title says Windows but its Operating System field says macOS, which makes the boundary cross-platform.) Distinct because its trigger is an update in flight; mine are steady-state, and my 2026-09-05 update exited cleanly with code 0.
- **#91633** (startup FileIndex blocks the loop ~41 s, CLI, non-git cwd, Linux) — independently reports its `[likely sleep/wake]` tags as misclassifications, and usefully shows the CLI *does* log `cpu=` for the same stall type. Distinct: startup-only, CLI, and its cwd is not a git repo.
- **#89772** (Bash tool dispatch spins ~80 s, CLI, large context) — CLI-side, one dispatch path, deterministic per Bash call. Mine is desktop-side and spread across many tags.
- **Explicitly not related:** the GPU-driver crash family reporting `exitCode 101457950` / `0xC0000xxx`. On this machine that was a separate defect, root-caused to MSIX/CodeIntegrity, at zero occurrences for 13 days since a non-MSIX reinstall — while these stalls continued throughout.
### Additional Information
One lead I can't yet support properly, offered as such. At 14:11 the order was: worktree reuse and `Copied 695 files from gitignored .claude` (14:11:43–45) → `git status timed out` (14:11:53) → a `worker:file-index:reply` stall at 14:11:57. That would fit the sync-FS stacks above. But the same window also contains `[createWorktree] FETCH_HEAD is 9932s old — fetching origin in the background` and a `full origin fetch completed in 4758ms` spanning the stall, so a network fetch is an unexcluded confounder, and I have this in one occurrence. Treat it as a hypothesis only.
Happy to share the raw stall ledger, the per-capture CPU and stack table, or the surviving minidumps. Note on those: the watchdog keeps 10 bundles, so of the nine stacks I read, **six dumps still exist** — the 09-01 20:01 bundle (the `NtQueryDirectoryFileEx` one) has already rotated out. I can preserve future ones on request.
Guide de contribution
Aucun guide de contribution indexé pour ce dépôt
Piste de recherche
Start with the Desktop main-process event-loop-stall instrumentation and the GitStatusService path shown in the logs; compare the duration_heuristic decision with the CLI's cpu= logging and the Windows power-log checks. Trace how tagged stalls update the total and cumulative fields, then examine the listed synchronous filesystem tasks and streaming flush path. Done should preserve real stalls in counters while distinguishing them from actual suspend events.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Évaluation
- Stack technique
- electron, javascript, node.js
- Domaine
- desktop, observability, performance
- Type d'issue
- Bug
- Difficulté
- 5/5
- Temps estimé
- Plus d'une semaine
- Activité
- Active
- Clarté
- Plutôt claire
- Accessibilité débutants
- 32/100