Repository navigation
[Bug]: preview_recording_start on a tab that draws no frames never returns and blocks every later action on that tab #23514
Description
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
Follow-up to #16567, covering only the part left open there ("the underlying screenshot capture remains unbounded").
- Desktop app, an agent thread with preview tools.
preview_opena page withopen: false, so the desktop tab is not drawn.preview_snapshot: fails after ~10 s (the fix(server): keep sensitive values out of settings reads, startup logs and preview snapshots #20446 bound works). The tab is still usable.preview_recording_starton the same tab: the broker gives up at 15 s and evicts the host.preview_evaluate,preview_clickon the same tab: each times out at 15 s and evicts the host again.t3_preview_closeand a new tab is the only way out.
Server trace from that sequence on current main, run with vp run dev:desktop (UTC; durations from PreviewToolkit.invoke spans):
09:20:04 open 298 ms ok
09:20:07 snapshot 10,010 ms PreviewAutomationTimeoutError
09:20:20 recordingStart 16,002 ms timed out; broker disconnect/reconnect
09:20:39 evaluate 16,003 ms timed out; disconnect/reconnect
09:20:58 click 16,003 ms timed out; disconnect/reconnect
09:21:18 close 6 ms ok
The same pattern happened on the released 0.1.1 with a tab opened with the default open: true (every tab there reported visible: false): snapshots failed at 10 s, then recordingStart, type, evaluate and click each failed at 16 s with an eviction until the tab was closed. It is intermittent there: earlier the same day, snapshots and a recording worked on a visible: false tab.
Expected behavior
A screenshot capture that cannot finish fails within a bound, the way preview_snapshot has since #20446, and the tab stays usable for the next action.
Actual behavior
captureViewport (apps/server/src/preview/ServerBrowserPage.ts) sends a bare Page.captureScreenshot with no deadline. preview_snapshot survives it only because snapshot() races the whole capture against SNAPSHOT_TIMEOUT_MS. The other callers have no such race:
startRecording(ServerBrowser.ts) awaits it for the first recording frame, inside the tab'sSessionControlqueue and its capture lock. When it never settles, the queue never advances, so every later agent action on the tab waits behind it until the tab closes.- The viewer still frame (
pushStill) awaits it inside the same capture lock, so one stuck still blocks later snapshots and recordings too.
Side note that made this harder to read: a server-side PreviewAutomationTimeoutError from the 10 s snapshot bound reaches the agent as "Preview automation snapshot timed out after 15000ms", because the broker formats it with its own timeoutMs.
Impact
Major degradation or frequent failure
Version or commit
T3 Code (Beta) 0.1.1 desktop (tag at fe27e8e, includes #20446); reproduced on main @ 5ffe045 with vp run dev:desktop.
Environment
macOS 26.6.2, desktop app with its local server, server-side preview automation driving the desktop tab over CDP.
Workaround
Close the tab and open a new one. Avoid preview_recording_start on a tab whose snapshots are failing.
Notes on a fix
#22877 makes hidden desktop tabs draw while the server captures them, which should remove the usual trigger. Separately, a PR with a bound on captureViewport follows: any capture that still can't finish fails instead of holding the tab's queue and capture lock, for all three callers.
Filed by Claude Code (claude-opus-5-5) for @DavidMendesOS.
Activity
DavidMendesOS commentedon Oct 22, 2026
Related: #23022 (filed a day earlier) reports the same underlying fault from the viewer side: an unbounded
captureViewportin the remote viewer's still frame holdstab.captureLock, so an agent snapshot waits before its own deadline starts. This issue is thepreview_recording_startcaller of the same function, which wedges the tab's agent queue instead of the capture lock.#23515 bounds
captureViewportitself, so it covers both callers. For #23022 that only partly helps: a stuck still frame now releases the lock within 5 s, but a snapshot can still wait that long before its own 10 s bound begins.