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.mredwin1 commentedon Oct 26, 2026
A related case on released 0.1.2 (macOS, Retina) where
preview_recording_startdoes succeed, but recording a busy page wedges the server:preview_recording_start, then send a prompt that streams a long answer with an embedded app iframe.preview_evaluate/preview_wait_for/preview_statusall time out at 15 s.preview_recording_stopafterwards timed out at 120 s three times; the recording was never saved andpreview_snapshoton the tab hung too.event-loop-stalls.ndjsonat recording start:"utilization":1,"minorPageFaults":310094. Once, the desktop then respawned the backend and every attempt failed withServerAlreadyRunningError(the old process still held the home lock), so MCP calls got ECONNREFUSED until restart.From the bundle, the likely cause:
RECORDING_SCREENCASTis{ format: "jpeg", quality: 90, everyNthFrame: 1 }with nomaxWidth/maxHeight, so every frame is a 2560x1600 q90 JPEG shipped as base64 through the server into the encoder page; andstopRecordingholdscaptureLockwhile awaitingPromise.all(framesInFlight), so a frame backlog blocks stop and every capture-locked call. Capping screencast size, dropping frames while the encoder is busy, and bounding the in-flight wait in stop would likely fix it.