cmux-next: hidden agent chat pages hibernate
Each agent chat tab in cmux NEXT keeps its own web view, and every web view has its own WebKit web content process. That's about 75 MB per chat, held until the tab closes. With 30 workspaces and 10 chats, 14 web content processes used 976 MB, the largest share of the app's memory (#19832).
With this change, hidden chat pages hibernate the way hidden browser pages already do:
- A chat hidden past the browser hibernation threshold lets its page go. The threshold comes from
browser.hibernation(60 minutes for moderate, the default; 10 for aggressive; or a number of minutes), and memory pressure shortens it, as it does for browser pages. - The tab keeps its session, so showing it again builds a new page on the same chat. acpmux replays the transcript and the composer draft (
_acpmux/draft_set, saved 300 ms after typing; a page always stays at least 10 s after it hides). - The page's last frame stays in the pane until the new page paints, so the switch back doesn't flash.
- A page stays when:
- its agent is working or waiting on the person;
- the tab is pinned (unless the setting includes pinned tabs);
- it is a New Tab page;
- it is another machine's chat;
- a shell command ran in it, since closing the page would end the command or drop its output;
browser.hibernationisoff.
Before / after
scripts/cmux-next/bench_memory.py on one fleet mini, back to back, with a Release build of this branch at 3784a24bb5d6 (before the review fix below, which only adds releases). Eight workspaces, each opened on an agent chat whose first turn has ended, then 150 s idle; one workspace is on screen. browser.hibernation is off for "off" and 1 minute for "on". Footprint is Activity Monitor's "Memory".
| Off | On (1 min) | Change | |
|---|---|---|---|
| WebKit web content processes | 9 | 4 | โ5 |
| WebKit web content footprint | 369.5 MB | 141.7 MB | โ228 MB |
| App footprint | 119.1 MB | 139.0 MB | +20 MB |
| Total footprint, every process the app owns | 817 MB | 608 MB | โ209 MB (โ26%) |
The 4 web content processes left are the ones a fresh launch already has (app pages). The app's +20 MB is the hidden chats' last frames, held to draw under the pane while a page rebuilds.
With the default moderate setting, a chat's page goes after an hour hidden, or sooner under memory pressure.
bench_memory.py --fake-agent also changes. A prompt sent through the CLI waits in the new chat's composer for a Send, so the bench's chats never had a session and could never hibernate. Each chat is now an acpmux session on the app's acpmux whose first turn the test agent answered, opened with agent.openSessionWorkspace. The bench calls that action over the socket because the CLI reads --session as its own global option, so an action argument named session can't be passed through cmux action run. The run's cleanup now also ends the terminal hosts its daemons started.
Testing
AgentPageHibernationTestsdrivesAgentPageHibernationwith a realAgentTabStore,AgentTurnStateStoreand anAgentPaneViewin a window. It covers:- a page hidden past the threshold goes and comes back on the same session;
- a page hidden while still on screen goes once the pane hides it;
- a running turn keeps its page;
- hibernation off keeps every page;
- critical pressure releases after the short wait.
- Red on the test commit 1e9b48606b74: 4 issues. Green on 3784a24bb5d6.
- Review found that a lone hidden chat never armed its deadline. Its hide comes while the page is still in its window, and nothing replanned for it later; with several chats, each later hide replanned.
aLoneHiddenChatGoesOnItsOwnDeadlinecovers it: red on dfccad82ecdad, green on 182d35ee039d6 (fleet Swift suites: 2 issues, then pass). The busy check now reads the session the page runs, not the tab record's, which differs after a refused bind. - Merged head 2b3f780d9e96:
AgentPageHibernationTestspassed.PaneToolbarTests(overflow menu items) andKeybindingsFileRelaunchTests(Ctrl-Tab conformance) failed on files this PR does not touch.
Not kept across a rebuild:
- composer attachments (pasted images, files);
- shell-mode text that has not run;
- the transcript's scroll position.
The composer's text and the transcript come back from acpmux. With the default moderate setting this needs an hour hidden, or memory pressure.
If acpmux's turn watch drops, every chat counts as idle until it reconnects. A due page can then go during a turn; acpmux keeps the turn, and the page reloads it.
Changelog
- Changed: Agent chats hidden longer than the browser hibernation time free their memory (about 75 MB each) and reload when shown.
๐ค Generated with Claude Code