cmux NEXT browser: a page never reloads unless the person asks
Leo's words: "why does cmux-next's browser keep refreshing and reloading? this happened before with austin using google maps"
What was reloading pages
- Hibernation discarded hidden pages, and a discarded page reloads when shown. With no
browser.hibernationin cmux.json the default wasmoderate: a hidden tab was discarded after 30 minutes, and under memory pressure after 10 seconds hidden (HibernationPlanner.plan: critical pressure takes 3 pages every 3 s once hidden 10 s; warning 1 per 15 s once hidden 60 s). Waking it recreates the engine page from its saved history, which loads the page again: script state is gone, and a page behind a login or a sudo prompt can come back at the sign-in page. A Mac deep in swap sits at warning or critical pressure for hours, so hidden tabs were being discarded seconds after a switch. The discard was logged atinfo, which the unified log does not keep, so it left no trace in the field. - A Chromium tab whose renderer ended while hidden reloaded itself when shown (
CEFTab.reloadWhenShown). A visible tab already showed the crash notice with its Reload button; a hidden one skipped it. - A second copy of the app opens Chromium tabs in WebKit, signed out. From the field log of the reported GitHub reload: a second instance of the same app started on the same session while the first was running; its Chromium could not start (
CEF start failed: CefInitialize failed, the first copy holds the profile), so every restored Chromium tab loaded its URL in WebKit, which has none of Chromium's cookies. That is how a signed-in GitHub settings page showsgithub.com/login?return_to=.... This PR only logs that path; keeping a second copy from starting is a separate change (see "Not covered").
What changes
- By default hidden pages only freeze (
browser.freezeHiddenTabs, 30 s: timers and rendering stop, memory and state stay) and are never discarded on their own, at any memory pressure.debug.surfacesreports them askeeps_state. - This changes a default. A discard happens on its own only once cmux.json sets
browser.hibernation(or the palette's Hibernate Hidden Tabs After ... / Turn Off Tab Hibernation writes it). The setting itself, its values, the palette actions and Hibernate Tab are unchanged; Hibernate Tab still discards on request. Agent chat page hibernation is unchanged (it does not reload a web page). - A Chromium tab whose renderer ended shows the crash notice when it is shown, and reloads only from its Reload button.
- A discard probe that started under an opted-in setting does not discard if the setting changed meanwhile.
- A page whose renderer ended is never discarded (a discard would drop the crash notice and its restore would load the page). A discard probe that is kept replans, so the page it thawed freezes again.
- Every non-user reload or discard is logged with its cause at a level the field log keeps:
app.hibernation:discarded <tab> cause=timeout hidden=Ns/cause=memory-pressure=critical hidden=Ns/cause=user,restore <tab>: the discarded page reloads from its saved history (...), andmemory pressure <level>(notice).cef:renderer ended reason=... shown=...; no automatic reload(error).webkit:web content process ended tab=...; no automatic reload(error).browser.engine:tab <id> loads in WebKit: Chromium unavailable (startFailed); its page reloads without Chromium's cookies(notice).
Read them with log show --last 1h --predicate 'subsystem == "com.cmuxterm.app.next" AND category IN {"app.hibernation","cef","webkit","browser.engine"}'.
Recordings (Google Maps in a Chromium tab, fleet capture mini)
Steps: a workspace with example.com and Google Maps, Maps shown and loaded; switch to the other tab, then to Home, so Maps is hidden; critical memory pressure for 20 s (debug.hibernation.pressure, as the dispatch source sends it); back to the workspace and the Maps tab. The video's last frames are the switch back.
Before (base 2dd2453c2488): the map tab comes back reloading (stop button, spinner, blank search box, the info card gone).
Capture steps-2feac4dc10 at 2dd2453c2488 (fleet build, capture mini, text checked for private data)
| Light | Dark |
|---|---|
![]() | ![]() |

Full quality:
After (this PR): the map is the same live page the moment it shows.
Capture steps-2feac4dc10 at a06c9280a427 (fleet build, capture mini, text checked for private data)
| Light | Dark |
|---|---|
![]() | ![]() |

Full quality:
A GitHub page was in the first takes too, but a signed-out repo page shows commit authors, which the capture's privacy check refuses; example.com stands in.
Risk
Memory: under pressure, hidden pages no longer go away unless the person set a timeout. Frozen renderers stay resident (macOS compresses and swaps them). Someone who wants memory back sets browser.hibernation or uses Hibernate Tab. No new timers or observers.
Tests
BrowserNeverReloadsByItselfPlanTests(planner): the default discards nothing at normal, warning or critical pressure and arms no deadline; it still freezes; a writtenbrowser.hibernation(string or minutes) still discards; only a written value opts in.BrowserNeverReloadsByItselfTests(real WebKit pages through the app's tab cache and hibernation): under critical pressure, a page hidden past the 10 s that used to discard it stays loaded, then tab and workspace switches (the same pane hiding and showing it) and a pane move (another pane showing it) keep the same page object, the sameperformance.timeOrigin, and a script variable set before the switches.- Existing planner tests that exercised the timeout now pass an explicit
BrowserHibernationSetting(mode: .moderate), the setting a person who wrote the timeout has.
Not covered
-
Settings shows
browser.hibernation's default as "After 30 Minutes" (the schema default). With no value written, hidden pages now only freeze; picking a value in Settings opts in as before. Relabeling the default is a follow-up. -
Restore logs name the wake kind (navigation, reload, back, forward) or "shown or woken"; they do not tell a neighbor prewarm from a show.
-
A second copy of the app starting on the same session (the reported GitHub reload). It is logged now; making a second launch hand off to the running copy is a separate PR.
-
The Chromium crash path has no unit test (it needs a live Chromium renderer); the change removes the one call that reloaded.
Agent session: agent session
š¤ Generated with Claude Code



