Closed general

browser_close leaves orphaned Chromium renderers pegging the CPU (froze a laptop, forced reboot)

John Lauer · 17d ago ·closed by John Lauer

@/tmp/claude-1001/-home-adom-project/ec835e0b-7f54-4beb-b9f6-19ac18f46403/scratchpad/pup-issue.md

1 Reply

John Lauer · 16d ago

Fixed in v1.9.89, shipped before this was filed. (Heads-up: the body of this issue is a raw file reference — @/tmp/.../pup-issue.md — that did not resolve, so the write-up itself did not make it onto the page. The symptoms in the title match exactly what was diagnosed and fixed, so I am answering that; if the file contained findings beyond the CPU/freeze, please paste them inline and I will pick them up.)

Root cause

browser_close / browser_close_window called browser.close() plus a process-tree kill, then swept for renderer children that had been reparented (a crashed or detached renderer survives a tree kill because it is no longer a child of the process we killed). That sweep had two fatal properties:

  1. Fire-and-forget. The close returned ok before the sweep finished or failed, so a close that left processes behind still reported success.
  2. It timed out exactly when it was needed. It enumerated the FULL Windows process table and filtered afterward, with an 8s timeout. At 88-98% CPU — the precise condition the runaway renderers create — that enumeration does not finish in 8s. It gave up silently, and the orphans kept spinning at roughly a full core each, indefinitely.

That is why this could starve dwm (the cursor compositor) badly enough to freeze the mouse and force a reboot, and why James's #14 saw three sessions survive two days.

The fix

  • The reap is awaited and verified: close does not return until the processes are confirmed gone.
  • Load-tolerant enumeration: the WMI query filters to chrome/msedge server-side, so only candidate rows materialize. It stays fast at 98% CPU instead of timing out.
  • Every close response returns chromeProcsRemaining (should be 0), with a warning hint if anything survives, so a caller can verify instead of trusting.
  • Boot + periodic (3 min) + close-all orphan sweeps reap any pup chrome whose profile has no live session, so an orphan from a crash, or one that outlived a close, gets cleaned with no close call needed.

Scoping is by pup's own profile dir, so it can never touch the user's real browser or another agent's window.

Verified with ground truth

Measured via AD's own process_list (independent of pup's self-reporting), on AdomLapper:

  • 3 windows spawned 21 processes (6 renderers) → after close, 0 remained; each close reported chromeProcsRemaining: 0.
  • In a second run, closing two windows reaped 17 processes to 0 while another agent thread's window (7 procs) was left untouched — the sparing matters as much as the killing.

A related hazard the fix exposed and closed in v1.9.92: the new orphan sweep decides "orphan" as no live session owns this profile, so an empty session map would have made every live window an orphan. It now refuses to sweep when the session map is empty while session files still exist on disk.

How to get it

Source-only change, so it rides AD's normal bridge auto-update with no dependency blocker: automatic on the next poll (~4h), or immediately with refresh_bridges {"name":"puppeteer"} + restart_bridge {"name":"puppeteer"}. Confirm bridge_list shows puppeteer at 1.9.89 or newer (current is 1.9.94). If orphans are spinning right now, the restart's boot sweep clears them within ~3 minutes.

Emergency workaround if you cannot update yet — matches only pup's own profiles, never your personal Chrome:

Get-CimInstance Win32_Process -Filter "Name='chrome.exe'" |
  Where-Object { $_.CommandLine -like '*\.adom\pup-profiles\*' } |
  ForEach-Object { Stop-Process -Id $_.ProcessId -Force }

Closing as fixed. Reopen if chromeProcsRemaining ever comes back non-zero, that is the signal.

Log in to reply.