Note
Grok responding on behalf of Julius.
Triage
Thanks so much for the careful measurements, @davidvornholt, they made this easy to trace. I checked it on current main (eac52f0087) and on v0.0.45 (6c8fed35dd), and the relevant code is the same on both: preview tabs you aren't looking at stay mounted as live Chromium guests for the rest of the session.
What I found
ElectronBrowserHostsits outside the router and mounts aHostedBrowserWebviewfor every preview session in every thread, not just the thread on screen (apps/web/src/browser/ElectronBrowserHost.tsx). A thread stays in that index as long as it has sessions (syncActivePreviewThreadinapps/web/src/previewStateStore.ts).closeTabruns only when the React guest unmounts (apps/web/src/browser/desktopTabLifetime.ts), which happens only after an explicitpreview.closefrom the UI or frompreview_close/t3_preview_close. Closing the panel or switching threads doesn't close tabs.- On Linux, an inactive guest is parked at
(-100000, -100000)withvisibility: hidden(apps/web/src/browser/hostedBrowserWebviewStyle.ts).data-preview-rendering="suspended"is only a marker for automation. Nothing calls Chromium freeze or discard, and nothing destroys the guest. - The webview preferences are only
contextIsolation=false,sandbox=true,nodeIntegration=false(apps/desktop/src/preview/WebviewPreferences.ts). Electron's default background throttling only applies once Chromium considers the page backgrounded, and the guest is temporarily set tosetBackgroundThrottling(false)while recording or in picture-in-picture. - The repo never calls
navigator.getGamepads(), so the Gamepad polling wakeups very likely come from a script inside one of those guests, which fits your theory. ResourceCleanupServiceonly cleans up terminals and attachments, and thread deletion, archive, and settlement don't touch previews (apps/server/src/orchestration-v2/ThreadDeletion.ts).
How this differs from the related issues you linked:
- #14730 is the macOS case, where inactive guests stay visible and keep painting. Linux already takes the hidden path, and hiding wouldn't stop these renderers anyway.
- #14966 covers sessions left behind after a thread is deleted. Here the threads are still alive.
- #13610 is an orphaned hidden tab after a
preview_resizetimeout. These tabs were never evicted.
Likely fix area
A few options a maintainer could weigh:
- Really suspending inactive guests (panel closed, other thread, and not recording, in picture-in-picture, or held by automation). That could mean freezing them, which keeps page state, or discarding the webview and recreating it from the session URL when the thread is shown again, which frees the memory but reloads the page.
- Keeping recording, picture-in-picture, and automation on the existing
renderingActivepath. - Separately, closing previews when a thread is idle or settled, the way idle terminals already close. That would change current behavior, since tabs are kept today so they're still there when you come back.
Restarting the app or closing each thread's previews remains the workaround for now.
A maintainer will decide on the fix direction.