cmux-next: a chat's first frame waits on one daemon round trip
Why
Leo, 2026-10-29: "i also like the way it just loads in the subagent stuff instantly :(" / "as in i want ours to load fast too :(" / "but i also wonder how much of this is simply putting the desktop at higher priority?" / "you're looking in the session transcripts in what cmux next is reading?" / "like the gui rendering the tui which comes from the daemon" / "idk what the claude code cli has but that's a lot of what im running in cmux next". Earlier: "but do chats load fast yet? even with high # of chats, high load? and does it still flash/flicker?"
Opening a chat asked the daemon one thing at a time before the first row: initialize, then _acpmux/status, then the chat list (_acpmux/watch), then _acpmux/attach, then the turn opener (_acpmux/events), permission groups and handoff. On a busy daemon each of those is a full round trip, so a cold open waited on five daemon replies in a row.
What changes
The first frame now waits on the handshake plus one round trip.
- Webview (
direct.ts). Afterinitializeanswers,_acpmux/status, the chat list and the chat's attach go out together. The permission-group and handoff reads go out with the attach rather than after it. acpmux serves a connection's requests side by side (server/mod.rsspawns each), so the frame waits on the slowest of them, not their sum.initializestill goes first: the app's bridge sends the person proof as the connection's second request and holds every other frame behind it.- The early attach (
askEarly) is used only when the list then confirms it: the same chat and selection, a chat on this machine, and a page that holds nothing of the chat yet. In every other case it is dropped, and the chat attaches exactly as before. That covers another chat chosen, a tab-session rebind, a missing chat, and a chat on another machine (which may draw the Mac's copy first). A dropped attach of a chat no longer shown is detached once it has answered (acpmux runs each request on its own task, so an earlier detach could run first). - Live records that arrive before the open takes the early attach are held, then applied in order at the point the old code sent the attach. So a kept tail drawn from the host is still drawn first, and
copyLiveandkeptThroughreconciliation see the same sequence as before. - An early reply is checked against the session the list names (
createdAt). A reply of another session is retried, like a recreated row, and so is one whose row changed after it had streamed records (those may be of the session before it). A dropped early attach's held records are discarded by the next attach of the chat, whose page already holds them. The guards stay as they were: stale selection and generation checks,strayedFrom,historyExhausted, peer chats, andcopyAttaches. - A kept tail from the host no longer holds the open once the attach has answered: the open waits on whichever comes first. The host read is still at most 250 ms.
- The early attach (
- acpmux (
_acpmux/attach). The reply now carriesturnOpener: the prompt that opened the turn the page starts inside. It is computed only when the page has older records and starts mid-turn, using the same 16-recorduser_messageread the pane used to make itself (server/requests/turn_opener.rs). Otherwise it isnull. - Older daemons. One that leaves
turnOpenerout still gets the pane's own read. This is detected by whether the key is present, so a mixed fleet and a forwarded peer attach both work. - Protocol and generated types.
spec/acpmux-protocol.jsonandprotocol/generated/types.tsare updated forAttachResult.turnOpener. The mock daemon answers the same way.
No new timer, observer or loop. The early attach's live buffer is bounded by the records the chat streams between the handshake and the list's reply. It is cleared when the open takes or drops the attach, and in open's finally. No new per-token work.
Tests (red on base, green here)
webviews/src/agent-session/acpmux/chatOpenTrips.test.ts: a mock daemon holds every reply until the test answers what is out as one wave, and the test counts the waves before the first frame with rows.- a chat's first frame waits on the handshake and one round trip: expects ā¤2 waves, with
initializealone and then status, watch and attach together. Base takes 5. - the turn's opener comes with the attach page: a chat whose newest page starts inside a 450-record turn opens in ā¤2 waves, with no
_acpmux/eventsread. - an acpmux that does not name the opener still has it read: the fallback for an older daemon.
- a chat's first frame waits on the handshake and one round trip: expects ā¤2 waves, with
cmux-tui/crates/acpmux/tests/hub_integration/transcript_events.rsan_attach_page_names_the_prompt_of_the_turn_it_starts_inside: on pages starting inside the last turn,turnOpenerequals what the pane's own read finds, and it is null for a whole log or a page that starts with the prompt.
Performance
| metric | before | after | delta | harness or run link |
|---|---|---|---|---|
| daemon round-trip waves before a cold chat's first row (mock daemon) | 5 | 2 | -3 | chatOpenTrips.test.ts |
| same, chat opened mid-turn (served opener) | 6 | 2 | -4 | chatOpenTrips.test.ts |
| median first content, mock daemon with 5 ms per reply | 26.5 ms | 11.0 ms | -58% | Bun open bench on Linux CI (cmux-ci test) |
| same, 20 ms per reply | 101.3 ms | 41.0 ms | -60% | same |
| same, 50 ms per reply | 251.6 ms | 101.1 ms | -60% | same |
| same, 50 ms per reply, chat opened mid-turn | 252.1 ms | 101.4 ms | -60% | same |
The bench opens the mock chat with every daemon reply delayed by D ms: base pays about 5 x D before the first row and this head pays 2 x D (the handshake, then one wave). A fleet-Mac run of scripts/measure/agent-engine.sh was refused at a PR ref (the script measures only refs on its list), so the Mac number comes from the nightly once this lands.
test/agent-pane-peer-copy.test.ts's stand-in daemon runs its once-only scripted hooks (records during the attach, a recreation, refusals) on the attach the open asks of the chat as listed, and answers an attach asked before the list plainly; two tests count only the listed attaches.
test/agent-pane-new-tab-cached-chat.test.ts now lets the host's tail read land after the held attach is asked: the attach goes out with the list, before that read.
Base reds: the src/agent-session/acpmux directory run fails the same 105 tests by name at base and here (the head fails none of its own; base also fails the new tests and one long-chat paging test this head passes). transcript_events::orchestration_fields_reach_subagent_events_and_session_summary fails identically on base.
Risk
Medium. This reorders the open handshake in direct.ts. The guards are unchanged, and every case the early attach does not fit falls back to the old path. #27614 (acpmux page compaction) touches the same attach handler a few lines away; the hunks do not overlap.
š¤ Generated with Claude Code
Agent session: agent session