cmux NEXT: the never-reload follow-ups (prewarm, lease race, stale probe, agent chats)
Leo's words: "why does cmux-next's browser keep refreshing and reloading? this happened before with austin using google maps"
Follow-up to #25791 ("the browser never reloads by itself"). A review of the merged code found paths that still reload or discard a page without being asked. This PR fixes them. The Settings label for browser.hibernation is a separate PR.
Findings and fixes
- Neighbor and hover prewarm restored discarded pages (
BrowserHibernation.warm). With hibernation unset, Hibernate Tab followed by selecting a neighbor (or hovering it for 150 ms) reloaded the page in the background.
Fix: a prewarm never restores a page the person discarded with Hibernate Tab. Withbrowser.hibernationunset, a prewarm never restores any discarded page (mayPrewarmRestore). A discarded page restores only when it is shown or woken (Wake Tab, a navigation, an agent). A prewarm restore that still happens (an automatic discard under an opted-in timeout) is logged at notice level asprewarm restore <tab> cause=neighbor|hover, and the restore log reads(prewarm neighbor)/(prewarm hover)instead ofshown or woken. - A lease taken during the snapshot await was ignored (
discardCheck).noteLeasereplans, but the replan skips a page that is being probed, so the probe went on to discard a page an agent held.
Fix:discardCheckchecksleasedafter every await, before the user short-circuit too. A leased page is never discarded, not even by Hibernate Tab, andhibernateNowrefuses a leased page up front (.agentLease). - Show-then-hide during a discard probe left the tab thawed forever. The hide's replan skipped the probing key, and the stale completion returned without replanning.
Fix: the.stalecompletion replans, so a page that is still hidden freezes again. A probe that the page's own answer kept (form input, audio) used to return without planning anything as well. It now replans its freeze only (replan(freezeOnly: true)); a full replan there would re-probe the page at once. The freeze probe had the same gap (a show and hide while it asked the page left the page awake), and its stale completion now replans too when the page is still hidden. - Agent chat pages ignored
discardsAutomatically(AgentPageHibernation.replancalledthresholddirectly). With hibernation unset, an idle hidden chat was released after 30 min (10 s under critical pressure) and rebuilt when shown. The release was logged at info level with no cause.
Fix: an agent chat is released only oncebrowser.hibernationis written. OtherwisekeepsPagereports.keepsStateand no deadline is armed. A release is logged at notice level asreleased agent page <tab> cause=timeout hidden=Ns/cause=memory-pressure=<level> hidden=Ns. A chat that shows its crash notice is never released, even with a timeout set, because a rebuild would reload it without the person pressing Reload. - The agent-page host reloaded its first three renderer exits on its own (
PageWebView.contentProcessTerminated, reached by WebKit termination and by Chromium.crashed). The crash notice showed only after those retries, and the reloads weren't logged at notice or error level. The old WebKit host (AgentPaneView.webContentProcessDidTerminate) did the same with its own counter.
Fix:PageWebView.reloadsAfterCrash, set to false by the agent pane'sattachPage. Every crash then logs at error level and shows the notice, and only its Reload button reloads. The crash also cancels the Chromium start deadline, so its WebKit fallback can't load the chat by itself. If the Chromium browser itself ends (.ended), the agent chat shows the notice and reopens in WebKit only on Reload. The old host shows the notice straight away and logs at error level. Other app pages (Settings, Passwords, ...) keep their bounded automatic reload, which is now logged at error level asreloading automatically cause=renderer-exit. The notice now says "The agent pane crashed." instead of "crashed repeatedly", in all 21 locales.
Agent chats: does a recreate or reload lose anything?
Yes. Both lose state the person can see, so they get the browser-tab rule (unset hibernation means the page is kept; a crash shows the notice with Reload).
- Release and rebuild (finding 1). The rebuilt page gets back only what
AgentPaneWakeStatecarries: composer chips, shell mode and its unrun command, and the scroll anchor. The text draft comes back from acpmux andreadPersistedDraft. Nothing captures an open popover or menu (model/effort pickers inComposerPickers.tsx, the composer context popover, the chat display menu), the caret or selection, a queued prompt pulled back for editing (onQueueCancelEdit), the Escape-restorable draft that+overwrote, or an in-progress IME composition (isComposinghandling inComposer.tsx/ComposerBox.tsx). The release also needswakeStateto answer within 3 s. - Crash reload (finding 2). A crashed renderer can't answer
pane.wakeState, so a reload also loses the chips, the shell text and the scroll position. Keystrokes acpmux hasn't received yet are lost too.
Tests
NeighborTabsWakeTests.aPageThePersonHibernatedRestoresOnlyWhenShownOrWoken(3): with hibernation unset and then opted in, a neighbor and a hovered page that the person hibernated stay hibernated. A show and a Wake Tab restore them.BrowserNeverReloadsByItselfTests.aLeaseTakenDuringTheProbeKeepsThePage(4): the lease arrives inside the snapshot capture. The page is kept (agent_lease), and Hibernate Tab is then refused.BrowserNeverReloadsByItselfTests.aPageShownAndHiddenDuringItsProbeFreezesAgain(5): the page is shown and hidden inside the snapshot capture. The probe is dropped and the page freezes again.BrowserNeverReloadsByItselfTests.aPageShownAndHiddenDuringItsFreezeProbeFreezesAgain(5, freeze probe): the same for the freeze probe.AgentPageHibernationTests.theDefaultKeepsEveryHiddenChatsPage(1, normal and critical pressure): the chat keeps its page for 120 min hidden, withkeeps_stateand no deadline.PageWebViewCrashReloadTests(2): withreloadsAfterCrashoff, two Chromium.crashedevents cause no reload and two notice reports, the start deadline is cancelled, and Reload reloads once..endedshows the notice and loads WebKit only on Reload. Other pages still reload up to the limit.AgentPageHibernationTests.aCrashedChatShowsTheNoticeAndDoesNotReload(2, old host): the notice shows on the first crash.AgentPageHibernationTests.aCrashedChatIsNeverReleased(1 and 2): with a timeout set and critical pressure, a crashed chat keeps its page and notice.
Ran on the fleet with the touched suites (HiddenPageHibernationTests, HibernationRankingTests, MemorySaverKeepRulesTests, BrowserNeverReloadsByItselfPlanTests, plus the ones above), and cmux-ci compile-check --app.
Capture
No capture: the only visible change is the agent chat's crash notice (it now appears on the first renderer exit, and its text drops "repeatedly"). Reaching it means killing the chat's web content process, and no capture scene or tour does that (cmux-ci capture --list-states has none). The other changes are background lifecycle with no UI of their own: whether a hidden page is discarded or restored, and when it freezes.
Risk
- Memory: with hibernation unset, hidden agent chats keep their web content process (about 75 MB each), the same as browser tabs since #25791. Setting
browser.hibernationbrings the release back. - An agent chat whose renderer exits now waits for Reload instead of reloading by itself.
- No new timers or observers. The stale-probe replan arms only the existing one-shot deadlines.
Agent session: agent session
š¤ Generated with Claude Code