Skip to content

preview_snapshot on a background tab never returns and blocks every later action on that tab #16567

Description

@0pilatos0

What happened

Desktop app on nightly 2735. An agent opened background tabs (preview_open, open: false) on github.com and example.com. Every preview_snapshot timed out after 15s (3 of 3, across both tabs). preview_status still answered instantly. preview_open(tabId, open: true) then hung until the turn was interrupted (618s, then 358s). t3_preview_close worked. Builds before 2735 snapshotted fine all day (482 snapshots, last at 17:16 CEST). No preview_evaluate or preview_wait_for ran before the first failure.

A controlled rerun in a fresh thread confirmed the split: the same page opened with open: true snapshots in 38-65ms, while opened with open: false it times out every time.

Diagnosis

Since #15328 the server's own browser is the preferred automation host, and with the desktop app attached it drives the tab's desktop <webview> over CDP. ServerBrowserPage.snapshot runs page.evaluate, ariaSnapshot and captureViewport in one Promise.all. Only ariaSnapshot has a timeout, and captureViewport sends a bare Page.captureScreenshot (ServerBrowserPage.ts:170, :191-200).

The snapshot runs in the tab's SessionControl queue, which has no per-action timeout (SessionControl.ts:37-44, ServerBrowser.ts:1605-1617). When the capture never settles, the broker times out at 15s and evicts the host, but the tab queue stays blocked. Every later agent action on that tab waits behind it, including open: true: its reveal runs in the same queue (ServerBrowser.ts:1524-1545) and gets the 660s budget meant for Chromium installs (PreviewAutomationBroker.ts:564-565). Status and close skip the queue (ServerBrowser.ts:1463, :1589-1595), and closing the tab is what finally releases the stuck calls.

Trigger, confirmed: the tab was opened in the background. Mechanism, not confirmed: most likely Page.captureScreenshot waits for a frame a hidden guest never presents (what #13600 describes for the desktop path). The server code assumes "a desktop-rendered tab is always on screen" (ServerBrowser.ts:1367), and the old desktop host bounded every capture (capturePage, 1s x 3 attempts, apps/desktop/src/preview/Manager.ts:130-135, :610-640). The server path has no bound.

Side issue: visible in preview_open/preview_status is tab.viewers.size > 0 (ServerBrowser.ts:563, :1117), so a desktop-rendered tab always reports visible: false, even right after open: true.

Steps to reproduce

  1. Desktop app on 0.0.46-nightly.20261006.2735, a new agent thread with preview tools.
  2. Control: preview_open({ url: "https://example.com/?test=visible", open: true, reuseExistingTab: false }), then preview_snapshot({ tabId }): works.
  3. preview_open({ url: "https://example.com/?test=background", open: false, reuseExistingTab: false })
  4. preview_snapshot({ tabId }): times out after 15000ms. Any further snapshot on that tab times out too.
  5. preview_status({ tabId }): instant, loading: false.
  6. preview_open({ tabId, open: true }): never returns (do this last, it hangs for up to 11 minutes).
  7. t3_preview_close({ tabId }): returns {}, and the queued calls get their answers at that moment.

Version

0.0.46-nightly.20261006.2735 (fd1c338)

Environment

macOS 27.0 arm64, Node 26.8.2, desktop app with its local server

Evidence

# server.trace.ndjson, original session, times CEST
20:45:41 tools/call preview_open {open:false, reuseExistingTab:false} 1041ms Success
20:45:43 PreviewAutomationBroker.awaitResponse 15002ms Failure
         PreviewAutomationTimeoutError: Preview automation snapshot timed out after 15000ms.
20:45:58 PreviewAutomationBroker.disconnect; 20:45:59 connect (server host re-registers)
20:46:02 snapshot 15001ms Failure (same error)
20:46:19 tools/call preview_open {url:example.com, open:false} 138ms Success
20:46:21 snapshot 15001ms Failure (same error)
20:46:39 tools/call preview_open {tabId:tab_2, open:true} 618222ms Interrupted
20:57:08 tools/call preview_status {tabId:tab_2} 5ms Success
20:57:14 tools/call preview_open {tabId:tab_2, open:true} 358532ms Interrupted
21:03:35 t3_preview_close tab_2, tab_1 Success; 7 PreviewAutomationBroker.respond spans in the same second
         (3 snapshots + 2 opens + 2 closes draining)

# controlled rerun, fresh thread
21:45:11 preview_open {?test=visible, open:true} 492ms Success
21:45:16 snapshot (tab_5) 65ms Success
21:45:17 snapshot (tab_5) 40ms Success
21:45:23 preview_open {?test=background, open:false} 449ms Success
21:45:28 snapshot (tab_6) 15002ms Failure, timed out after 15000ms
21:45:47 preview_status (tab_6) 4ms Success
21:47:49 snapshot (tab_6) 15002ms Failure, timed out after 15000ms
21:48:13 t3_preview_close (tab_6) Success; 3 respond spans in the same second (close + both late snapshots)

# desktop.trace.ndjson: no desktop.ipc.preview.automation* spans after the update;
# a webview registered for every server tab at the moment of its preview_open

Related issues

Introduced by #15328. #13600 (open) fixes the same capture stall for hidden or occluded windows on the desktop path only. #3713 had the same symptom on the old desktop host. #16264 and #12898 cover evaluate and wait_for, neither of which ran here.

Fix applied or workaround

None applied. Open agent tabs with open: true and snapshots work. Closing a stuck tab frees it. Nightly 20261005.2702 predates #15328 and still snapshots through the desktop host.

Filed by

Claude Code (claude-opus-5-5) via t3 triage

Activity

  1. juliusmarminge commented on Oct 6, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Triage

    Confirmed: a background desktop tab's snapshot never settles, and every later action on that tab waits behind it. The controlled split in the report matches current main (9503155da67a). Nightly 0.0.46-nightly.20261006.2735 (fd1c3386c4) is an ancestor, and these files are unchanged since that tag. Nightly 20261005.2702 does not contain #15328.

    What the code does

    On the desktop app, every tab of the local server is a <webview> the server drives over CDP (ServerBrowser.ts desktopRenders / connectDesktop). ElectronBrowserHost mounts a webview for every server tab, including reveal: false. open: false does not add a right-panel surface (ChatView.tsx, hiddenTabIds), so the guest stays inactive.

    An inactive guest is parked at -100000,-100000. On macOS it keeps visibility: visible so Electron 43 does not blank it, but it is still fully outside the window. hostedBrowserWebviewStyle.ts already says Electron does not composite that guest. Recordings avoid this by marking the surface rendering-active and placing it at 0,0 behind the app. A background agent tab does not.

    preview_snapshot always takes a full snapshot, including when includeImage is false (handlers.ts). ServerBrowserPage.snapshot runs page.evaluate, ariaSnapshot (15s timeout), and captureViewport together. captureViewport sends a bare Page.captureScreenshot with no timeout. DOM reads can finish while the screenshot waits for a frame the offscreen guest never presents, so the combined promise does not settle at 15s.

    That snapshot runs on the tab's SessionControl queue, which has no per-action timeout. The broker gives snapshot 15s, then returns PreviewAutomationTimeoutError and disconnects the host. The in-flight capture is a tryPromise on a forked handler, so the disconnect does not cancel it. The server host is preferred: true and reconnects, which matches the trace. The next call on that tab waits behind the same capture.

    preview_open with open: true reveals the tab inside that same queue, so a reveal issued after a stuck snapshot never starts. An open on the preferred host gets the 660s budget meant for a Chromium install (PreviewAutomationBroker.ts). preview_status and t3_preview_close skip the queue. close does not abort the capture; destroying the webview is what lets CDP settle, which is why the queued calls answer in the same second as the close.

    visible is tab.viewers.size > 0 in both reportLiveTabs and preview_status. Viewers are screencast subscribers only. A desktop <webview> never adds one, so a desktop-rendered tab stays visible: false after open: true.

    Likely causes (ranked)

    1. Uncapped screenshot of an offscreen desktop guest, still holding the tab queue. This is the failure the traces show: open: true snapshots in tens of milliseconds, open: false hits 15000ms every time, status stays instant, and a later reveal hangs until close. Headless server tabs do not use this webview placement.

    2. The broker timeout and the tab queue are different clocks. The 15s error is real, and so is the later work. Evicting the host does not free the tab.

    Related, not duplicates

    Next step

    Bound Page.captureScreenshot the way desktop capturePage is bounded, and make a broker timeout stop counting as the tab's current action so a later open: true can reveal the guest. A background snapshot also needs a frame from a guest that is not on screen. The offscreen placement is why recordings force the guest inside the window.

    The visible: false report is a separate bug in the same status payload. It can stay on this issue.

    Helpful follow-up

    None. The version, the open: true / open: false split, and the trace are enough.

  2. added
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Oct 6, 2026
  3. derekbking commented on Oct 8, 2026

    @derekbking

    Note

    Codex responding on behalf of Derek King.

    Reproduced with a standalone local demo: Electron 44.4.2, macOS arm64.

    • Before: DOM read completes in 0.2 ms; offscreen Page.captureScreenshot remains pending after 15 seconds, blocking the next queued action.
    • Experimental repair: temporarily render the guest behind the app, capture natively with a deadline, then restore its position. 5/5 captures in 33–42 ms, preserving draft, session and document state.

    Run npm ci, then env -u ELECTRON_RUN_AS_NODE npm start. Fresh disposable profile; local synthetic page only.

    Before / after screenshots — click to enlarge

    Before: offscreen screenshot remains pending after 15 seconds
    After: five captures succeed, with the actual returned fixture image shown

    This isolates the Electron capture mechanism with a simple Promise queue. Full T3/provider/MCP integration remains unverified. It complements #16941's timeout recovery.

  4. marius-ciocoiu commented on Oct 9, 2026

    @marius-ciocoiu

    Same on macOS 26.7.1 arm64, Nightly 0.0.46-nightly.20261008.2833, Claude Code thread. The tab reported visible: false. Times are UTC, 2026-10-09.

    • 09:01:29: preview_snapshot timed out after 15 s on a heavy React Flow canvas page.
    • Every later preview_evaluate on that tab timed out at 15 s: 9 calls over 22 minutes, from 09:01:56 to 09:22:54. preview_status still answered immediately.
    • The tab's renderer process stayed alive with the same pid and normal memory (120–260 MB in renderer-history.ndjson), so it wasn't a crash.
    • t3_preview_close at 09:23:18 released it, and a new tab worked.

    Earlier the same morning, snapshot timeouts at 08:26, 08:41 and 08:48 were each followed by timeouts on that tab's next calls.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions