[Bug]: Server browser: a browser action that never finishes wedges its tab, evicts the preview host and stops other tabs' setting changes#32849
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Steps to reproduce
The trigger seen in real use was a preview_wait_for on a desktop-drawn tab whose locator check never answered. That trigger has its own fix (#32846). This report is about what any never-settling action does to the rest of the server. A deterministic repro:
- On main, make one Playwright call in
executeTabOperationhang. For example, makesetColorScheme'spage.emulateMediareturnnew Promise(() => {}). - Start a dev server and open a server-browser preview tab from an agent with
preview_open. - From the agent, call
preview_set_appearanceon that tab withtimeoutMs: 3000. - Right away, call
preview_actorpreview_resizeon the same tab. Then callpreview_statusfrom a second thread. - In the Browser panel, change another tab's colour scheme or zoom, and close another tab.
Expected behavior
- The stuck call fails at about its timeout.
- Later agent calls on that tab get a fast, clear answer that the tab is busy and say how to recover.
- Other threads and other tabs keep working. Setting changes and closes on other tabs still apply.
Actual behavior
SessionControlserialises agent actions per tab, but it checks the deadline only before an action starts. A run that never settles holds that tab's queue forever.- Every later agent call on that tab waits until the broker's response timeout (
timeoutMsplus 1 s). Each of those timeouts disconnects the shared server-browser host (#16921). For about 1 s, every thread loses preview automation and gets "No preview automation host is available". This repeats for each queued call. - The agent only sees a generic "timed out after ms" error. Nothing tells it the tab is stuck, so it keeps retrying the same tab.
- The wedge escapes the tab. The server's manager-event mirror is one sequential stream, and it awaits each tab's queued work for
resizedevents. One resize, colour scheme, zoom or hard-reload event for the stuck tab blocks the mirror for every tab. After that, closes, opens and setting changes on all tabs stop applying until the server restarts. - Related:
SessionControl.system()throws synchronously when the control is closed. Aresizedevent that arrives while an agent closes its tab throws inside the mirror and ends the mirror stream until restart.
Impact
Major degradation or frequent failure
Version or commit
main @ a1af1841ed; the code paths are unchanged on main @ 09a54f01bc. Also seen on the desktop app, Nightly 0.1.8-nightly.20261105.4205.
Environment
macOS, T3 Code desktop app (Electron 44.10.1, Playwright 1.60), server-browser tabs drawn by the desktop. The code path is the same for headless server-browser tabs and for every provider, because it is reached through MCP.
Workaround
preview_openwithreuseExistingTab: falsecontinues in a new tab.- If the agent owns the stuck tab,
t3_preview_closewith itstabIdskips the queue and closes it. - If setting changes or closes have stopped applying on other tabs, restart the server (or the desktop app).
Proposed fix (summary)
I have a fix ready and will open a PR once this is triaged:
- Answer each agent call by its deadline, but keep the queue waiting for the real run, so runs never overlap.
- While a started agent run is overdue, refuse new agent actions on that tab at once with a new
busycontrol reason. A pending dialog still takes precedence. - Do not await per-tab work in the mirror, and make
system()reject instead of throwing.
Out of scope: cancelling the stuck run; fast refusal for stuck human, system or tracked-navigation work (callers with a deadline are still answered on time); human input and viewer teardown on a stuck streamed tab; the mirror's opened branch awaiting ensureTab.
Related work
- #16921 and #17871: the broker evicts the server-browser host on a response timeout. #17871 stops that eviction. It does not free the stuck tab's queue.
- #23564: answers callers at their deadline with a timeout around the whole request handler. The run still holds the tab's queue, and the tab gets no
busyrefusal. It fits with this fix. - #30460: rewrites
mirrorManagerEventwith one event chain per tab, which also fixes the mirror part above. If it lands first, the fix here drops that part.