Skip to content

fix(cmux-next): replay agent page events across bridge gaps - #24617

Merged
teamleaderleo merged 34 commits into
feat-cmux-nextfrom
perf-agent-chat-bridge-buffer
Oct 24, 2026
Merged

teamleaderleo merged 34 commits into
feat-cmux-nextfrom
perf-agent-chat-bridge-buffer

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Agent-page host events are now buffered during a WebView bridge gap and replayed in order on reconnect. Replay-state events stay ahead of gap events, events arriving during subscription handoff cannot overtake the queue, and cancellation returns an undelivered suffix when no listener remains. The bounded queue applies backpressure instead of silently evicting events, and page-reset drops are requeued.

Leo said yes.

Performance evidence

The opt-in Swift harness uses 1,024 ordered events on the page-provider bridge and asserts all 1,024 are delivered in order. The same before/after benchmark was submitted at the final head, but the fleet is currently host-lock saturated:

  • baseline parent: earlier attempts 3dab501dc0cdad7327cc73d2 and e19... failed before timing because the benchmark source lacked its CmuxNextSettings import; that harness compile defect is fixed in this branch.
  • fixed head a2176e62e5fcf06d227de90cfefd933bbe990be4: final focused/benchmark submissions were cancelled after the branch changed; the fleet is currently host-lock saturated and produced no terminal benchmark receipt.
  • No publish_main_ms or replay_window_ms value is claimed until a terminal fleet receipt exists.

Validation

  • AgentPageProviderEventsTests covers replay ordering, fan-out, cancelled-replay recovery, reset-drop ordering, backpressure, reconnect clearing, and pending replay recovery.
  • Opt-in fixed 1,024-event harness: AgentPageProviderBridgeGapBench.
  • Independent Codex review of exact head a2176e62e5fcf06d227de90cfefd933bbe990be4: pending; prior exact-source review SHIP on the unchanged implementation before the base-only rebase.
  • Exact head: a2176e62e5fcf06d227de90cfefd933bbe990be4.
  • No new timers or observers were added.
  • No capture: bridge buffering/replay behavior only; no visual surface change.

Agent session: big-red

@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

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

Review a2176e6: 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=pr24617 -f ref=perf-agent-chat-bridge-buffer -f platform=macos -f backend_mode=local

red: cmux-next checks (god files, concurrency, crash safety, l10n); no finished base run on 1e2573c to compare; do not merge until green; see lane-rules
red: cmux-next crash ratchet; NEW: green on the base 1e2573c; do not merge until green; see lane-rules

Tour of a2176e62e5 (run): no media. The tour job failed before the tour ran (build or setup); the run log has the output.

@github-actions github-actions Bot added the risk:high Risk tier (docs/ci/risk-tiers.md) label Oct 23, 2026
@blacksmith-sh

This comment has been minimized.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Bead cx-4b57c created for this PR by the cmux coordinator.

@cursor

cursor Bot commented Oct 23, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@teamleaderleo
teamleaderleo merged commit 4abd404 into feat-cmux-next Oct 24, 2026
95 of 102 checks passed
@teamleaderleo
teamleaderleo deleted the perf-agent-chat-bridge-buffer branch October 24, 2026 00:12
@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

lawrencecchen added a commit that referenced this pull request Oct 24, 2026
…hen one script

Rebased from pl-scrolling d493af1 and 87895e9 onto the tip's
router (the drop-and-replay sends of #24617), on the one send path and the
visibility seam of the previous commits.

PageRouter.deliver no longer sends an event to a page nobody can see (out
of a window, hidden, parked, the pool's parked spare, or a covered or
minimized window; automation launches keep covered windows live, as
WindowDrawPolicy does for every surface). The events wait in order, with
their drop actions, and go in one send when the page shows. Every event is
kept (streams carry deltas). Anything else that reaches the page first
releases what waits, so nothing reorders: a command and a popup event (the
person's own action) go at once after it, the reply to a page message goes
after it, a host call carries it ahead in the same send. Unsubscribe drops
that subscription's waiting events; close drops them all and runs their
drop actions after the script FIFO's, so a provider replays in send order.
A busy hidden stream goes in batches of PageRouter.heldLimit (1024: a bound
on memory and on scripts, not a measured number; such a batch is over the
pipeline's inline budget, so it renders off the main actor).

The agent pane opts out through its descriptor
(PageDescriptor.holdsEventsWhileHidden false on .agent): its frame pacer
already holds a hidden pane's frames and still sends the ones the page
waits on (a reply, prompt_accepted, a turn's end). The flag is on the
descriptor, so a pooled host rebound to another page takes that page's rule.

debug.page state reports events_held and events_waiting.

Guard: PageRouterHiddenPageTests (red in the previous commit: 100 sends
while hidden), plus the batch limit and the descriptor opt-out cases. Not
built or run here (code-only task): no compile-check, package test build
or bench has run on this commit.
lawrencecchen added a commit that referenced this pull request Oct 24, 2026
… the page after the reply; with none held the next event goes at once (red)

#24617 (4abd404) inverted the early-events check in PageRouter.subscribe: when a provider
sent its current state from inside `subscribe` (the icon picker's open session), the router
released and dropped those events; when nothing was held, it queued the next events behind a
main-actor task instead of sending them at once. Both tests fail on 71df513.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lawrencecchen added a commit that referenced this pull request Oct 24, 2026
…the page after the reply; with none held the next event goes at once

PageRouter.subscribe held a provider's events sent from inside `subscribe` and was to flush them
after the reply. #24617 (4abd404) inverted the check when it added
`eventsAfterSubscribeReply`: with events held it released and dropped them (a provider's current
state never reached the page), and with none held it scheduled the flush task, so every event
until that task ran waited behind it. The check is `early.isHolding` again; a provider whose
events follow the reply never calls `hold`, so it needs no case of its own.

Guard: PageRouterEarlyEventsTests (red at the previous commit). PageRouterHiddenPageTests, which
emits right after subscribing, failed at its fix commit for the same cause.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lawrencecchen added a commit that referenced this pull request Oct 24, 2026
…hen one script

Rebased from pl-scrolling d493af1 and 87895e9 onto the tip's
router (the drop-and-replay sends of #24617), on the one send path and the
visibility seam of the previous commits.

PageRouter.deliver no longer sends an event to a page nobody can see (out
of a window, hidden, parked, the pool's parked spare, or a covered or
minimized window; automation launches keep covered windows live, as
WindowDrawPolicy does for every surface). The events wait in order, with
their drop actions, and go in one send when the page shows. Every event is
kept (streams carry deltas). Anything else that reaches the page first
releases what waits, so nothing reorders: a command and a popup event (the
person's own action) go at once after it, the reply to a page message goes
after it, a host call carries it ahead in the same send. Unsubscribe drops
that subscription's waiting events; close drops them all and runs their
drop actions after the script FIFO's, so a provider replays in send order.
A busy hidden stream goes in batches of PageRouter.heldLimit (1024: a bound
on memory and on scripts, not a measured number; such a batch is over the
pipeline's inline budget, so it renders off the main actor).

The agent pane opts out through its descriptor
(PageDescriptor.holdsEventsWhileHidden false on .agent): its frame pacer
already holds a hidden pane's frames and still sends the ones the page
waits on (a reply, prompt_accepted, a turn's end). The flag is on the
descriptor, so a pooled host rebound to another page takes that page's rule.

debug.page state reports events_held and events_waiting.

Guard: PageRouterHiddenPageTests (red in the previous commit: 100 sends
while hidden), plus the batch limit and the descriptor opt-out cases. Not
built or run here (code-only task): no compile-check, package test build
or bench has run on this commit.
lawrencecchen added a commit that referenced this pull request Oct 24, 2026
… the page after the reply; with none held the next event goes at once (red)

#24617 (4abd404) inverted the early-events check in PageRouter.subscribe: when a provider
sent its current state from inside `subscribe` (the icon picker's open session), the router
released and dropped those events; when nothing was held, it queued the next events behind a
main-actor task instead of sending them at once. Both tests fail on 71df513.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lawrencecchen added a commit that referenced this pull request Oct 24, 2026
…the page after the reply; with none held the next event goes at once

PageRouter.subscribe held a provider's events sent from inside `subscribe` and was to flush them
after the reply. #24617 (4abd404) inverted the check when it added
`eventsAfterSubscribeReply`: with events held it released and dropped them (a provider's current
state never reached the page), and with none held it scheduled the flush task, so every event
until that task ran waited behind it. The check is `early.isHolding` again; a provider whose
events follow the reply never calls `hold`, so it needs no case of its own.

Guard: PageRouterEarlyEventsTests (red at the previous commit). PageRouterHiddenPageTests, which
emits right after subscribing, failed at its fix commit for the same cause.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lawrencecchen added a commit that referenced this pull request Oct 24, 2026
…hen one script

Rebased from pl-scrolling d493af1 and 87895e9 onto the tip's
router (the drop-and-replay sends of #24617), on the one send path and the
visibility seam of the previous commits.

PageRouter.deliver no longer sends an event to a page nobody can see (out
of a window, hidden, parked, the pool's parked spare, or a covered or
minimized window; automation launches keep covered windows live, as
WindowDrawPolicy does for every surface). The events wait in order, with
their drop actions, and go in one send when the page shows. Every event is
kept (streams carry deltas). Anything else that reaches the page first
releases what waits, so nothing reorders: a command and a popup event (the
person's own action) go at once after it, the reply to a page message goes
after it, a host call carries it ahead in the same send. Unsubscribe drops
that subscription's waiting events; close drops them all and runs their
drop actions after the script FIFO's, so a provider replays in send order.
A busy hidden stream goes in batches of PageRouter.heldLimit (1024: a bound
on memory and on scripts, not a measured number; such a batch is over the
pipeline's inline budget, so it renders off the main actor).

The agent pane opts out through its descriptor
(PageDescriptor.holdsEventsWhileHidden false on .agent): its frame pacer
already holds a hidden pane's frames and still sends the ones the page
waits on (a reply, prompt_accepted, a turn's end). The flag is on the
descriptor, so a pooled host rebound to another page takes that page's rule.

debug.page state reports events_held and events_waiting.

Guard: PageRouterHiddenPageTests (red in the previous commit: 100 sends
while hidden), plus the batch limit and the descriptor opt-out cases. Not
built or run here (code-only task): no compile-check, package test build
or bench has run on this commit.
lawrencecchen added a commit that referenced this pull request Oct 24, 2026
…hen one script

Rebased from pl-scrolling d493af1 and 87895e9 onto the tip's
router (the drop-and-replay sends of #24617), on the one send path and the
visibility seam of the previous commits.

PageRouter.deliver no longer sends an event to a page nobody can see (out
of a window, hidden, parked, the pool's parked spare, or a covered or
minimized window; automation launches keep covered windows live, as
WindowDrawPolicy does for every surface). The events wait in order, with
their drop actions, and go in one send when the page shows. Every event is
kept (streams carry deltas). Anything else that reaches the page first
releases what waits, so nothing reorders: a command and a popup event (the
person's own action) go at once after it, the reply to a page message goes
after it, a host call carries it ahead in the same send. Unsubscribe drops
that subscription's waiting events; close drops them all and runs their
drop actions after the script FIFO's, so a provider replays in send order.
A busy hidden stream goes in batches of PageRouter.heldLimit (1024: a bound
on memory and on scripts, not a measured number; such a batch is over the
pipeline's inline budget, so it renders off the main actor).

The agent pane opts out through its descriptor
(PageDescriptor.holdsEventsWhileHidden false on .agent): its frame pacer
already holds a hidden pane's frames and still sends the ones the page
waits on (a reply, prompt_accepted, a turn's end). The flag is on the
descriptor, so a pooled host rebound to another page takes that page's rule.

debug.page state reports events_held and events_waiting.

Guard: PageRouterHiddenPageTests (red in the previous commit: 100 sends
while hidden), plus the batch limit and the descriptor opt-out cases. Not
built or run here (code-only task): no compile-check, package test build
or bench has run on this commit.
lawrencecchen added a commit that referenced this pull request Oct 24, 2026
… the page after the reply; with none held the next event goes at once (red)

#24617 (4abd404) inverted the early-events check in PageRouter.subscribe: when a provider
sent its current state from inside `subscribe` (the icon picker's open session), the router
released and dropped those events; when nothing was held, it queued the next events behind a
main-actor task instead of sending them at once. Both tests fail on 71df513.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lawrencecchen added a commit that referenced this pull request Oct 24, 2026
…the page after the reply; with none held the next event goes at once

PageRouter.subscribe held a provider's events sent from inside `subscribe` and was to flush them
after the reply. #24617 (4abd404) inverted the check when it added
`eventsAfterSubscribeReply`: with events held it released and dropped them (a provider's current
state never reached the page), and with none held it scheduled the flush task, so every event
until that task ran waited behind it. The check is `early.isHolding` again; a provider whose
events follow the reply never calls `hold`, so it needs no case of its own.

Guard: PageRouterEarlyEventsTests (red at the previous commit). PageRouterHiddenPageTests, which
emits right after subscribing, failed at its fix commit for the same cause.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lawrencecchen added a commit that referenced this pull request Oct 24, 2026
…hen one script

Rebased from pl-scrolling d493af1 and 87895e9 onto the tip's
router (the drop-and-replay sends of #24617), on the one send path and the
visibility seam of the previous commits.

PageRouter.deliver no longer sends an event to a page nobody can see (out
of a window, hidden, parked, the pool's parked spare, or a covered or
minimized window; automation launches keep covered windows live, as
WindowDrawPolicy does for every surface). The events wait in order, with
their drop actions, and go in one send when the page shows. Every event is
kept (streams carry deltas). Anything else that reaches the page first
releases what waits, so nothing reorders: a command and a popup event (the
person's own action) go at once after it, the reply to a page message goes
after it, a host call carries it ahead in the same send. Unsubscribe drops
that subscription's waiting events; close drops them all and runs their
drop actions after the script FIFO's, so a provider replays in send order.
A busy hidden stream goes in batches of PageRouter.heldLimit (1024: a bound
on memory and on scripts, not a measured number; such a batch is over the
pipeline's inline budget, so it renders off the main actor).

The agent pane opts out through its descriptor
(PageDescriptor.holdsEventsWhileHidden false on .agent): its frame pacer
already holds a hidden pane's frames and still sends the ones the page
waits on (a reply, prompt_accepted, a turn's end). The flag is on the
descriptor, so a pooled host rebound to another page takes that page's rule.

debug.page state reports events_held and events_waiting.

Guard: PageRouterHiddenPageTests (red in the previous commit: 100 sends
while hidden), plus the batch limit and the descriptor opt-out cases. Not
built or run here (code-only task): no compile-check, package test build
or bench has run on this commit.
lawrencecchen added a commit that referenced this pull request Oct 24, 2026
…pt; later ones go at once

#24617 swapped the two cases of the guard after provider.subscribe: when the
provider sent its current state inside subscribe (the icon picker's open
session), the held events were released and dropped; when it sent none,
every later event waited in PageEarlyEvents for a flush task, a hop per
subscription and out of step with the reply. The guard now flushes only when
something is held and the provider does not send after the reply itself.

Found by the PL-scrolling landing guard: PageRouterHiddenPageTests (every
test that emits after the reply) went red on the rebased head.
Tests: eventsSentDuringSubscribeFollowTheReplyAndLaterOnesGoAtOnce,
eventsAfterAReplyWithNothingHeldGoAtOnce.
lawrencecchen added a commit that referenced this pull request Oct 25, 2026
Each fails on the bare tip (full run at 2ee04b0; again alone at e869761, step 280e825d1b47e01c8df4b8ff).
Stale premises (the test follows the change's intent):
- IconFamilyAttributeTests: 5e7966c made folding finished thoughts the default; the test passes that default.
- ContextMenuStandardTests: #24707 (0bf7675) put Inspect Element in the tab bar's + menu.
- AcpmuxRefusedDaemonTests: #24795 (c7daff2) rewrote the refusal copy (no socket path; Quit that app; the
  holder-less refusal reconnects on its own) and left its own test on the old copy.
- AgentPaneTranscriptStepTests, AgentPageProviderEventsTests: since #24617 (4abd404) a new subscription's
  events go out a main-actor turn later; the tests wait for them (bounded by the suite time limit).
Real breaks (the code is fixed):
- ActionSurfaceParityTests: #24707 made the sidebar section menu 13 top-level rows (the limit is 12); its Inspect
  Element goes in the Options folder.
- TabSelectionActionsTests: Save and Close Tab (#23176) had a target reason for a menu row only, so the palette's run
  on a terminal tab was enabled and then refused; the reason now covers the untargeted run too.
- SettingsSchemaExportTests: #24631 (78d7472) added four New Tab widget settings (clock, timer, warm greeting,
  quote) without strings or an agent policy: their 8 strings in all 21 locales, and the agent may set them.
- NativeTooltipGuardTests: Mac Screen (ffc998b, 0372726) and No Color (c3bd2b2) drew raw tool tips: the
  No Color swatch uses themedToolTip, the toolbar's own helper is renamed setThemedHelp (it already set
  themedToolTip), the menu bar status item is marked native.
- WorkspaceNameSourceTests: #24627 (f8c226e) counted any renamed '~' as unnamed (nameRevision > 0); only the
  automatic namer's '~' is.
- AgentPaneMediaNoAutoActionTests: #24619 (5ff4b0c) resolved every image.load through the raster-only image
  roots, so a chat's own SVG or PDF was refused; non-raster files resolve in the chat's roots again.
- BrowserProfileLinkMenuTests: e01bdd4 gave menu symbols their natural size; a profile's symbol is centered in
  the 16 pt square again, so names line up.
- PhoneProcessRunner (passed alone, failed in the full run): a captured run ended before its stderr was read; it now
  waits for the end of stderr too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lawrencecchen added a commit that referenced this pull request Oct 25, 2026
Each fails on the bare tip (full run at 2ee04b0; again alone at e869761, step 280e825d1b47e01c8df4b8ff).
Stale premises (the test follows the change's intent):
- IconFamilyAttributeTests: 5e7966c made folding finished thoughts the default; the test passes that default.
- ContextMenuStandardTests: #24707 (0bf7675) put Inspect Element in the tab bar's + menu.
- AcpmuxRefusedDaemonTests: #24795 (c7daff2) rewrote the refusal copy (no socket path; Quit that app; the
  holder-less refusal reconnects on its own) and left its own test on the old copy.
- AgentPaneTranscriptStepTests, AgentPageProviderEventsTests: since #24617 (4abd404) a new subscription's
  events go out a main-actor turn later; the tests wait for them (bounded by the suite time limit).
Real breaks (the code is fixed):
- ActionSurfaceParityTests: #24707 made the sidebar section menu 13 top-level rows (the limit is 12); its Inspect
  Element goes in the Options folder.
- TabSelectionActionsTests: Save and Close Tab (#23176) had a target reason for a menu row only, so the palette's run
  on a terminal tab was enabled and then refused; the reason now covers the untargeted run too.
- SettingsSchemaExportTests: #24631 (78d7472) added four New Tab widget settings (clock, timer, warm greeting,
  quote) without strings or an agent policy: their 8 strings in all 21 locales, and the agent may set them.
- NativeTooltipGuardTests: Mac Screen (ffc998b, 0372726) and No Color (c3bd2b2) drew raw tool tips: the
  No Color swatch uses themedToolTip, the toolbar's own helper is renamed setThemedHelp (it already set
  themedToolTip), the menu bar status item is marked native.
- WorkspaceNameSourceTests: #24627 (f8c226e) counted any renamed '~' as unnamed (nameRevision > 0); only the
  automatic namer's '~' is.
- AgentPaneMediaNoAutoActionTests: #24619 (5ff4b0c) resolved every image.load through the raster-only image
  roots, so a chat's own SVG or PDF was refused; non-raster files resolve in the chat's roots again.
- BrowserProfileLinkMenuTests: e01bdd4 gave menu symbols their natural size; a profile's symbol is centered in
  the 16 pt square again, so names line up.
- PhoneProcessRunner (passed alone, failed in the full run): a captured run ended before its stderr was read; it now
  waits for the end of stderr too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
teamleaderleo added a commit that referenced this pull request Oct 28, 2026
…ge's push (#26778)

Subscribing to the agent page host queues the replay and flushes it in a
task (#24617), so a command published right after subscribe arrives on a
later main-actor turn. eachStepSendsItsPageCommand and
stopTurnSendsTheComposerStopCommand read the events synchronously and saw
none (commands -> []). They now read the stream until the pushes arrive,
as paletteSessionInfoReachesItsOwnPageCommand does since #25209.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
cmux-bot Bot pushed a commit that referenced this pull request Oct 30, 2026
#28492) (#28496)

AgentPageProvider.subscribe sends a new subscription's events in a later
main-actor turn, so they reach the page after the subscribe reply (#24617).
routerResetDropUsesTheCapturedProviderAfterRouterCloses published right
after router.handle(sub) and read the drop closure in the same turn, before
the delivery. It has failed at AgentPageProviderEventsTests.swift:110
('dropped' nil) in every cmux-next swift test run (jobs 124152289450,
124161816855, 124162143724, 124160068616) and at feat-cmux-next df9b52b.

The test now waits for the router's delivery of that event (CountWaiter, as
resetOfMultipleSubscribersRequeuesOnePublication does) before it checks
that the drop requeues the event on the captured provider. No timing: it
waits on the delivery itself.
lawrencecchen added a commit that referenced this pull request Oct 31, 2026
…s the page (#27468)

* test(cmux-next pages): a provider's state sent inside subscribe reaches the page (red)

PageRouterEarlyEventsTests: a provider's state sent from inside subscribe
reaches the page once, after the reply that names its subscription; an
event after the reply goes to the page in its own turn. Both fail on
feat-cmux-next 356f13f: #24617 inverted the guard in
PageRouter.subscribe (fix in the next commit).

Found on perf/chrome-parity, which has the same code
(cp/fix-tip-page-script-batch-crash).

* fix(cmux-next pages): a provider's state sent inside subscribe reaches the page

PageRouter.subscribe keeps the events a provider sends while subscribe runs
(its current state) and flushes them once after the reply
(20066e1: `guard early.isHolding else { release }`). #24617
(4abd404) rewrote that guard for providers that send only after the
reply and inverted it:

- events held during subscribe were released and thrown away: the first
  state of the agent activity, icon picker and update surface pages
  (AgentActivityPageProvider, IconPickerProvider, UpdateSurfaceIsland call
  onEvent inside subscribe) never reached the page, which showed nothing
  until its next change;
- with nothing held, events after the reply were caught and sent a main
  turn late.

The guard is now `guard early.isHolding, !provider.eventsAfterSubscribeReply`.

Regression test: PageRouterEarlyEventsTests (red in the previous commit).

---------

Co-authored-by: lawrencecchen <>

This branch was previously deployed

1 inactive deployment
ci-routing-agent — a2176e62 Deployed Oct 24, 2026 by teamleaderleo via namespace autofix #2187
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant