Repository navigation
fix(cmux-next): replay agent page events across bridge gaps - #24617
Conversation
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
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 Tour of |
This comment has been minimized.
This comment has been minimized.
|
Bead |
Bugbot needs on-demand usage enabledBugbot 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. |
|
Shipped in:
|
…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.
… 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>
…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>
…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.
… 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>
…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>
…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.
…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.
… 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>
…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>
…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.
…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.
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>
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>
…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>
#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.
…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 <>
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:
3dab501dc0cdad7327cc73d2ande19...failed before timing because the benchmark source lacked itsCmuxNextSettingsimport; that harness compile defect is fixed in this branch.a2176e62e5fcf06d227de90cfefd933bbe990be4: final focused/benchmark submissions were cancelled after the branch changed; the fleet is currently host-lock saturated and produced no terminal benchmark receipt.publish_main_msorreplay_window_msvalue is claimed until a terminal fleet receipt exists.Validation
AgentPageProviderEventsTestscovers replay ordering, fan-out, cancelled-replay recovery, reset-drop ordering, backpressure, reconnect clearing, and pending replay recovery.AgentPageProviderBridgeGapBench.a2176e62e5fcf06d227de90cfefd933bbe990be4: pending; prior exact-source review SHIP on the unchanged implementation before the base-only rebase.a2176e62e5fcf06d227de90cfefd933bbe990be4.Agent session: big-red