Skip to content

cmux NEXT: opening a chat on the prewarmed page draws it first time (a hidden adopted page reports its paint) - #31619

Open
teamleaderleo wants to merge 6 commits into
feat-cmux-nextfrom
lane/warm-spare-adopt-fix-b
Open

teamleaderleo wants to merge 6 commits into
feat-cmux-nextfrom
lane/warm-spare-adopt-fix-b

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

HOLD: with adoption on, an adopted reopen is slower than a cold open, and under load adopted opens still time out (table below). Not to land until an adopted open beats the cold one idle and under load.

Leo, 2026-11-01: "i had to retry to get it to load???" and "i hate the "Starting Claude Code... cancel" thing that it starts with then the stupid ass layout shift surely we can have a better load/faster load/intro than that"

Opening an existing chat adopts the app's prewarmed agent page again (#27759, #31033), and the adopted page now shows its chat on the first try. #31486 had turned adoption off because of this bug. This turns it back on.

Cause

The parked spare page can be hidden: Chromium marks the page hidden, so document.visibilityState is hidden. A hidden document runs no animation frames.

The page told the host it drew its chat (pane.painted) one animation frame after that render. The host keeps an adopted page away and covered until that report (AgentPaneView+SessionAdopt.swift), and the cover keeps it hidden. So the report never came. Every reopen held its cover until the loading state gave up, about 15 s later, and so did some sidebar opens.

A diagnostic bench at 7d8c57e read this from the page itself in each failed open:

  • visibilityState was "hidden";
  • the last animation frame came before the report was asked for;
  • the chat itself was drawn (88 transcript rows, chatRead, showsWhatItIs).

Fix

  • paintReport.ts afterDrawnFrame. The report still waits one frame, then a task. A page that is hidden, or goes hidden while it waits, reports on a task instead. The host then shows the page, and its own leave already re-syncs Chromium's visibility (AgentPaneLoadingView.leave).
    • One visibilitychange listener, removed when the report runs (stop condition). It reads no data, and it is listed in check-switch-fetches.py.
  • The chat-open trace. An adopted page counts as drawn once its pane painted (traceSurface). The page's own first frame (page_painted) came before the trace started, back when it drew New Tab, so every adopted open read page_empty in the bench.
  • AgentPaneSpareReadiness.adoptsExistingChats is gone. That was cmux NEXT: existing chats open cold again until the adopted spare page draws its chat #31486's switch, so the spare serves existing chats again.

Takes back #31486: it switched adoption off until the adopted page drew its chat; this PR is that fix, so adoption is on again.

Changelog

Fixed: opening an existing chat in cmux NEXT draws it from the prewarmed page on the first try, without the long loading cover.

Tests

webviews/test/paint-report.test.ts:

  • a hidden page reports although it draws no frame;
  • a shown page reports after its frame;
  • a page hidden while it waits reports exactly once.

Command: cd webviews && bun test test/paint-report.test.ts.

  • Red on the first commit (the old behaviour, moved unchanged into paintReport.ts): 2 fail.
  • Green at the head: 3 pass.

test/spare-session-adopt.test.ts passes at the head.

Performance

scripts/measure/chat-open.sh (chat-open-bench.py: milliseconds from the action to the first frame of chat content, median of 5), run on the same fleet display Mac (Mac16,10) for both columns. Before is the feat-cmux-next tip this head merges (a0741bc, adoption off since #31486: cold opens); after is this head.

metric before (a0741bc, cold) after (fe66961, adopted) delta harness or run link
sidebar open, idle (median ms) 770.6 780.2 +9.6 chat-open.sh, 5 runs
sidebar open, load 2508.9 2 of 5 valid regressed same
reopen, idle 806.2 1115.3 +309 same
reopen, load 2476.5 2 of 5 valid (13.6 s, 15.0 s) regressed same
flashes, idle sidebar / reopen 0 / 0 3 / 5 worse same

At e143c04 (the paint fix only), idle sidebar was 628.6 ms and idle reopen 1092.5 ms. No idle open hangs any more: the ~15 s cover is gone. But an adopted page takes about 800 ms from adoption to its paint report, where a cold page takes about 600 ms in total, and under load adopted opens still wait out the page's 10 s hold. The likely cause is under investigation: the parked spare was seen hidden before its adoption (diagnostic run), and Chromium throttles a hidden page's tasks.

Timers and observers

One visibilitychange listener per paint report, removed when the report runs. No new timers.

Observed / Not run

  • Ran: bun run check in webviews (green), the cmux-next pre-push gate, both bun suites above, and the diagnostic bench described above.
  • Ran: the CmuxNext compile check at e143c04 (passed).
  • Pending: the before/after bench.

Risk: normal. This turns the warm-spare open path back on for every existing local chat. A spare that is not connected still opens the chat cold (servesChat).
Agent session: agent session

🤖 Generated with Claude Code

teamleaderleo and others added 2 commits November 2, 2026 01:40
…s: it waits for a frame)

The page's pane.painted report moves to paintReport.ts unchanged (one frame,
then a task). paint-report.test.ts fails on it: a hidden document runs no
animation frames, so the report never comes (HQ#5914).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-14c3a892e82255726cbb
…existing chats take the spare again

The parked spare can be hidden (Chromium marks the page hidden), and a hidden
document runs no animation frames. The page's pane.painted waited for one, and
the host keeps an adopted page away and covered until that report: reopens held
the cover for the ~15 s give-up (HQ#5914; diag bench at 7d8c57e:
visibilityState hidden, last frame before the report was asked). A hidden page
now reports on a task, at once or when it goes hidden while it waits.

The chat-open trace counts an adopted page as drawn once its pane painted: its
first frame (page_painted) came before the trace, as New Tab, so every adopted
open read page_empty.

Turns adoption back on (#31486 switched it off).

paint-report.test.ts: red on the previous commit (2 fail), green here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-14c3a892e82255726cbb
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review fe66961: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr31619 -f ref=lane/warm-spare-adopt-fix-b -f platform=macos -f backend_mode=local

Gallery before/after for b9bc608: 16 states rendered, 2 images differ, 2993 more touched states not rendered. Images are in the run's artifact, not on this public PR; the independent review of this head reads them on its own, so the PR needs no hand-made screenshots of these states.

gh run download 41480478226 -R manaflow-ai/cmux -n gallery-advisory-31619-b9bc6086f36f1c1b97e0f1f4f5ecd6ae252a6855 (then read gallery-review.json)

States
State Theme Status Diff Picked by
agent-pane.chat-width/standard Apple System Colors Light unchanged 0% own App.tsx
agent-pane.chat-width/standard Apple System Colors unchanged 0% own App.tsx
agent-pane.chat-width/wide Apple System Colors Light unchanged 0% own App.tsx
agent-pane.chat-width/wide Apple System Colors unchanged 0% own App.tsx
agent-pane.chat-width/wide-blocks Apple System Colors Light unchanged 0% own App.tsx
agent-pane.chat-width/wide-blocks Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-edge/short-edge Apple System Colors changed 0% own App.tsx
agent-pane.composer-edge/short-edge Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-edge/wide-edge Apple System Colors changed 0% own App.tsx
agent-pane.composer-edge/wide-edge Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-edge/no-edge Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-edge/no-edge Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-floats/scrolled-to-end Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-floats/scrolled-to-end Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-floats/composer-grown Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-floats/composer-grown Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-rest-gap/gap-40 Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-rest-gap/gap-40 Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-rest-gap/gap-56 Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-rest-gap/gap-56 Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-rest-gap/gap-72 Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-rest-gap/gap-72 Apple System Colors unchanged 0% own App.tsx
agent-pane.continuation-spacing/shipped Apple System Colors Light unchanged 0% own App.tsx
agent-pane.continuation-spacing/shipped Apple System Colors unchanged 0% own App.tsx
agent-pane.continuation-spacing/two-thirds Apple System Colors Light unchanged 0% own App.tsx
agent-pane.continuation-spacing/two-thirds Apple System Colors unchanged 0% own App.tsx
agent-pane.continuation-spacing/half-tucked Apple System Colors Light unchanged 0% own App.tsx
agent-pane.continuation-spacing/half-tucked Apple System Colors unchanged 0% own App.tsx
agent-pane.continuation-spacing/before Apple System Colors Light unchanged 0% own App.tsx
agent-pane.continuation-spacing/before Apple System Colors unchanged 0% own App.tsx
agent-pane.copy-controls/output Apple System Colors Light unchanged 0% own App.tsx
agent-pane.copy-controls/output Apple System Colors unchanged 0% own App.tsx

Pick other states with a Gallery: entry.id, entry.id/variant line in the PR body.

web and size checks: green

@blacksmith-sh

This comment has been minimized.

@teamleaderleo teamleaderleo left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review (claude-opus-5-5): SHIP at e143c04

Review (subagent claude-opus-5-5 medium; carried): SHIP at e143c04

The SHIP at 2d763e3 carries: the PR's own diff has the same git patch-id --verbatim (2f5384580e97) at both heads, and the merge of feat-cmux-next between them (for the Swift 6.3 crash fix #31525) resolved nothing by hand. CmuxNext compile check passed at this head.

Follow-ups: #31622.

teamleaderleo and others added 2 commits November 2, 2026 03:03
… read beside its attach (fails)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-14c3a892e82255726cbb
…ttach fails

adoptSession read the whole list beside the attach, on the same socket; a busy
acpmux paces that list for seconds, and adopted reopens drew later than a cold
page (HQ#5914 bench). The list now decides only a failed attach, and only a list
that was read says the chat is missing (#31033's review follow-up).

spare-session-adopt.test.ts: 1 fail on the previous commit, 7 pass here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-14c3a892e82255726cbb

@teamleaderleo teamleaderleo left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review (claude-opus-5-5): SHIP at fe66961

Review (subagent claude-opus-5-5 medium): SHIP at fe66961

Delta from e143c04 (SHIP above). adoptSession re-reads the list only after select() throws. Only a list that was read and lacks the chat marks it missing (stillOpen, or leftByRead when the read itself fell back from the chat). A stale fallback cannot match, because generations only increase. A user pick during the failed open is never overridden. The attach reply carries the title and summary, so an unlisted chat draws no worse, and the row fills in later through the watch and readLaterSessions. The new test is red on edebfcc and green here.

Follow-ups:

  • [follow-up] Confirm that an unlisted chat on another machine, opened by adoption while that machine is down, still keeps the Mac's copy.
  • [follow-up] Tests: an attach that fails while the list has the chat (says failed, not missing); a list read that fails after a failed attach; a pick during a failed adoption.
  • [follow-up] Skip the read when a newer pick already replaced the adoption (direct.ts:2080).

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Awareness note from an agent lane that does not own this PR; it is not a review or a request to take the PR over.


For awareness: #32448 carries this PR's commits and adds the root cause its bench pointed at. The pane's paint hold made the incoming pane transparent, which hid the held-away adopted page from Chromium (#28002's rule), so the page drew only at the hold's 500 ms limit. #32448 keeps a page laid out outside the window visible to Chromium, and it does not carry the hidden-page task report (a likely source of the flashes). Before/after chat-open bench runs are queued on the fleet.

teamleaderleo added a commit that referenced this pull request Nov 5, 2026
… drawing while it is held off screen (#32448)

* test(agent pane): a hidden adopted page still reports its paint (fails: it waits for a frame)

The page's pane.painted report moves to paintReport.ts unchanged (one frame,
then a task). paint-report.test.ts fails on it: a hidden document runs no
animation frames, so the report never comes (HQ#5914).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-14c3a892e82255726cbb

* cmux NEXT: an adopted spare page reports its paint while hidden, and existing chats take the spare again

The parked spare can be hidden (Chromium marks the page hidden), and a hidden
document runs no animation frames. The page's pane.painted waited for one, and
the host keeps an adopted page away and covered until that report: reopens held
the cover for the ~15 s give-up (HQ#5914; diag bench at 7d8c57e:
visibilityState hidden, last frame before the report was asked). A hidden page
now reports on a task, at once or when it goes hidden while it waits.

The chat-open trace counts an adopted page as drawn once its pane painted: its
first frame (page_painted) came before the trace, as New Tab, so every adopted
open read page_empty.

Turns adoption back on (#31486 switched it off).

paint-report.test.ts: red on the previous commit (2 fail), green here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-14c3a892e82255726cbb

* test(agent pane): an adopted page opens an unlisted chat with no list read beside its attach (fails)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-14c3a892e82255726cbb

* cmux NEXT: the adopted page reads the chat list again only when its attach fails

adoptSession read the whole list beside the attach, on the same socket; a busy
acpmux paces that list for seconds, and adopted reopens drew later than a cold
page (HQ#5914 bench). The list now decides only a failed attach, and only a list
that was read says the chat is missing (#31033's review follow-up).

spare-session-adopt.test.ts: 1 fail on the previous commit, 7 pass here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-14c3a892e82255726cbb

* test(agent pane): an adopted page held outside the window keeps drawing under the pane's paint hold (fails)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-23bb1f8ee2895ef522be

* cmux NEXT: an adopted page held outside the window keeps drawing under the pane's paint hold

The pane's paint hold makes its incoming view transparent, and a transparent
ancestor hides a Chromium page (#28002). An adopted New Tab spare waits
100 000 pt outside the window until it drew the chat (adoptSession), so the
hold hid a page nobody could see: Chromium throttled it, and it drew the chat
only once the hold gave up at 500 ms. A page laid out wholly outside its
window's content now stays visible to Chromium whatever its ancestors' alpha;
in place, a hold still hides it.

Drops #31619's hidden-page paint report (on a task, without a frame): with the
held page drawing, the report waits for a real frame again, and a report with
no frame drawn showed the page's previous frame when the host revealed it.

AgentPaneChromiumVisibilityTests.anAdoptedPageHeldAwayKeepsDrawingUnderAPaintHold:
red on the previous commit, green here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-23bb1f8ee2895ef522be

* test: cover selection after a failed spare adoption

Agent-Session: https://cmux.com/deeplink/agent?id=aid-81082f85640c1fa29ca9

* fix: retain a newer selection after spare adoption fails

Agent-Session: https://cmux.com/deeplink/agent?id=aid-81082f85640c1fa29ca9

* Agent pane: an adopted spare turns its cover on before its loading state begins

The chat-open bench counted a flash on 7-10 of 10 adopted opens: its trace
recorded the chat's saved frame ("image"), then the empty cover, then the
chat. No such frame was drawn. beginLoadingState binds the trace and
samples the surface at once, while the New Tab page's cover was still
hidden; adoptSession turned the cover on only after that, in the same main
turn, so the first sample read a state no frame showed. The cover now goes
on first, so the pane is in its loading state from the first sample.

Diagnosed on the fleet with a probe build that recorded every cover change
after the adoption; it found none, which left the trace's own first sample.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-758f5f82cc10b26ecf3c

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

This branch was previously deployed

1 inactive deployment
ci-routing-agent — b9bc6086 Deployed Nov 2, 2026 by teamleaderleo via wait for same-key tree publisher #101974
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant