Skip to content

[Bug]: preview_snapshot can fail or time out on loaded pages and leave tab automation unusable #3713

Closed

Description

@gregbartell

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/desktop

Steps to reproduce

  1. Start T3 Code Desktop.

  2. Open a Codex-backed thread with the T3 preview MCP tools available.

  3. Serve any local web page:

    python -m http.server 8000 --directory build/site
  4. Open the page in a fresh preview tab:

    {
      "show": true,
      "reuseExistingTab": false,
      "url": "http://localhost:8000/index.html"
    }
  5. Confirm the page is loaded with preview_status.

  6. Call preview_snapshot.

  7. Retry preview_snapshot if the first call fails.

  8. Call a simple preview_evaluate afterward:

    ({ href: location.href, ready: document.readyState })

Expected behavior

On a loaded page, preview_snapshot should return page state or a clear recoverable error.

A snapshot failure should not poison the tab. Follow-up preview_evaluate should continue to work, and preview_open(show: true) should recover the tab if the preview needs to be reattached.

Actual behavior

On a loaded page, the first snapshot returned a generic execution error:

{
  "error": {
    "_tag": "PreviewAutomationExecutionError",
    "operation": "snapshot",
    "failureCount": 1
  }
}

A retry timed out after 15 seconds:

{
  "error": {
    "_tag": "PreviewAutomationTimeoutError",
    "operation": "snapshot",
    "failureCount": 1
  }
}

A simple preview_evaluate immediately afterward also timed out:

PreviewAutomationTimeoutError: Preview automation evaluate timed out after 15000ms.

Reopening the same tab with preview_open(show: true) did not restore evaluation in this run. Creating a fresh tab did restore evaluation.

In another run, snapshot failed with an UnknownVizError in the action timeline, while evaluation stayed responsive. This suggests there may be more than one snapshot failure mode.

Impact

Major degradation or frequent failure

Version or commit

T3 Code desktop 0.0.28-1 via t3code-bin AUR package.

Environment

Arch Linux x86_64, kernel 7.0.13-arch1-1, T3 Code desktop AppImage package via t3code-bin, Codex provider.

Logs or stack traces

Representative timeout:

PreviewAutomationTimeoutError: Preview automation evaluate timed out after 15000ms.

Representative snapshot error:

{
  "error": {
    "_tag": "PreviewAutomationExecutionError",
    "operation": "snapshot",
    "failureCount": 1
  }
}

Screenshots, recordings, or supporting files

No screenshot included. The page was loaded and inspectable by status before snapshot; the useful evidence is the snapshot/evaluate error sequence.

Workaround

Use preview_evaluate and preview_status for inspection when a full snapshot is not required. If snapshot or a follow-up evaluation starts timing out, abandon that tab and open a fresh one.

Activity

  1. daemon-james commented on Aug 3, 2026

    @daemon-james

    Confirmed on T3 Code 0.0.31 on Linux with the Codex provider.

    During a dashboard inspection session:

    • preview_snapshot timed out while preview_status still reported the page as loaded.
    • Follow-up preview_evaluate and preview_navigate calls also timed out, and preview action entries remained in a running state.
    • The t3 serve process sustained roughly 100–146% CPU and about 2.1 GiB RSS during the degraded period.
    • The host itself remained 77–95% idle with no I/O wait and about 13 GiB memory available; the local API and frontend were still responsive.
    • When the automation backlog eventually cleared, t3 serve CPU fell to about 3% and preview calls recovered.

    This adds a server-side resource symptom to the existing tab-poisoning report. It suggests timed-out preview automation may leave queued or stalled work inside T3, rather than the target application being responsible. I avoided further retries once this was isolated, since repeated calls appeared to make the degraded period longer.

  2. JorgeMenaDev commented on Aug 7, 2026

    @JorgeMenaDev

    Confirming this still reproduces on 0.0.32-nightly.20260807.1022 (macOS desktop), with what looks like a deterministic trigger: the tab must be visible.

    • preview_open with {"open": false} → preview_snapshot → Preview snapshot failed. (reproduced ~10 times today across 3 models and 2 MCP clients)
    • preview_open with {"open": true} → preview_snapshot → works perfectly: PNG plus visibleText, interactiveElements, full accessibility tree, console and network entries.

    Suggestion, separate from the underlying cause: the client-facing error is opaque. previewSnapshotFailure in apps/server/src/mcp/McpHttpServer.ts logs the real errorTag server-side via Effect.logWarning, but sends only the fixed string "Preview snapshot failed.". Callers cannot distinguish "the tab isn't rendered" from a genuine capture failure, and the server logs aren't persisted to disk, so there's no way to recover the tag after the fact. Surfacing errorTag in the text (it's already in structuredContent) would have saved us a lot of guessing.

    Impact: background/headless agent threads always have visible: false, so visual verification is unavailable to automated agents. DOM inspection via preview_evaluate still works and covers most of the need.

    — Jorge Mena

  3. marijnbent commented on Aug 16, 2026

    @marijnbent

    same for me!

  4. areidyOTH commented on Aug 24, 2026

    @areidyOTH

    Confirming this failure mode on T3 Code Desktop
    0.0.34-nightly.20260824.1172 on Linux, with Codex driving a visible preview
    through the t3-code MCP server.

    This occurrence adds process-level evidence and narrows one likely expensive path.

    Reproduction

    1. Open a visible preview on a moderately content-heavy local development page.
    2. Call preview_snapshot before the page has rendered fully. It returns normally.
    3. Navigate or reload, allow the page to render, then call preview_snapshot again.

    The first snapshot completed in 80.84 ms. The second MCP call timed out after
    15.03 seconds, while the corresponding desktop operation continued for 47.83 seconds
    and was recorded as successful.

    Afterward:

    • The preview renderer remained at approximately 100% CPU for 42 minutes.
    • Peak renderer RSS was 5,535,960 kB (5.54 GB).
    • RSS repeatedly climbed and collapsed in a GC-like sawtooth.
    • The main renderer thread and six V8 foreground worker threads were consuming CPU.
    • Total process I/O over the degraded period was only approximately 1.3 MB.
    • Subsequent preview_status, preview_navigate, and
      preview_open(reuseExistingTab: false) calls timed out.
    • Killing only the affected renderer restored the preview; the desktop app itself
      survived.

    Trace evidence

    First snapshot:
       80.84 ms  PreviewManager.automationSnapshot
    
    Wedged snapshot:
    47832.38 ms  PreviewManager.automationSnapshot
    47831.18 ms    PreviewManager.captureAutomationSnapshot
    14132.04 ms      PreviewManager.sendCommand
    14132.04 ms      PreviewManager.sendCommand
     2180.70 ms      PreviewManager.sendCommand
    31366.84 ms      PreviewManager.sendCommand
    
    MCP boundary:
    15030 ms       preview_snapshot -> PreviewAutomationTimeoutError
    

    By source order, the 31.37-second command is
    Accessibility.getFullAXTree.

    At the affected tag, PreviewManager.captureAutomationSnapshot caps
    interactiveElements at 200 and visibleText at 20,000 characters, but calls:

    send("Accessibility.getFullAXTree")

    without CDP's optional depth parameter, then includes the complete result as
    accessibilityTree.

    This is a strong candidate for the pathological renderer work, but the available
    evidence does not prove it is the sole allocator. wc.capturePage() runs concurrently
    with the AX-tree request and is not separately timed in this trace, while the screenshot
    resize happens only after capture. The two 14.1-second enable calls also show that the
    renderer was already severely degraded before getFullAXTree began.

    Timeout mismatch

    The MCP broker defaults to 15 seconds. When that timeout expires it removes the pending
    request, but there is no cancellation message to the web host or desktop operation.
    The host Promise and CDP request therefore continue. A late success response is ignored
    because its pending broker entry no longer exists.

    The desktop trace consequently records the 47.8-second snapshot as successful even
    though the MCP caller was told it failed after 15 seconds.

    One correction to an earlier interpretation: Accessibility.disable is not a cleanup
    step skipped specifically because of this timeout. The snapshot path does not schedule
    it at all; the debugger/control session is intentionally persistent until a separate
    detach event.

    Suggested direction

    • Give Accessibility.getFullAXTree a conservative depth, or query selected nodes
      with getPartialAXTree; additionally bound the returned node array before crossing
      the MCP boundary.
    • Add end-to-end request cancellation so broker expiry interrupts the host/desktop
      operation and releases or resets the affected control session.
    • Isolate and time capturePage() separately.
    • Add the CDP method name to PreviewManager.sendCommand trace attributes.
    • Provide a renderer reload/replacement path that does not depend on the wedged
      renderer responding.

    Related but distinct: #7410 / #7408 bounds snapshot metadata at the MCP response
    boundary. That protects provider context but does not currently bound the desktop
    renderer’s full AX-tree query.

    Diagnosed by Codex (GPT-5) via t3 triage.

  5. ezzy1630 commented on Aug 26, 2026

    @ezzy1630

    Confirming on macOS 15 (darwin arm64 25.6.0), desktop 0.0.35-nightly.20260826.1193 with the Claude provider. This reproduces @JorgeMenaDev's open: false trigger, and the desktop traces on this machine let me add the mechanism behind the "leaves tab automation unusable" half of the report.

    The open: false trigger holds

    Across one session, every preview_open with "open": false produced a tab that could not be snapshotted; the two calls with "open": true were fine. preview_open, preview_navigate, and preview_status never failed — only the operations that need a painted frame or a ready overlay.

    The desktop-side cause is visible in the trace: capturePage rejects with Error: UnknownVizError, which is the same failure mode noted in the original report.

    PreviewOperationError: Desktop preview operation failed: automationSnapshot.capturePage
      (tab [<env>,<thread>,<session>,"tab_1"], WebContents 3)
      [cause]: Error: UnknownVizError
    

    1. The client's wait deadline equals the server's, so the useful error can never arrive

    waitForDesktopOverlay in apps/web/src/components/preview/PreviewAutomationHosts.tsx:87 is handed request.timeoutMs:

    const deadline = Date.now() + timeoutMs;
    while (Date.now() <= deadline) {
      ...
      if (state.desktopByTabId[tabId] && previewBridge) {
        const status = await previewBridge.automation.status(runtimeTabId);
        if (status.available) return;
      }
      await new Promise((resolve) => window.setTimeout(resolve, 50));
    }
    throw new PreviewAutomationOverlayTimeoutError({ ... });

    That timeoutMs is the same 15,000 ms the broker is already counting down (apps/server/src/mcp/PreviewAutomationBroker.ts:429, input.timeoutMs ?? 15_000). For a hidden tab the overlay never reports available, so the loop runs to the full deadline and throws PreviewAutomationOverlayTimeoutError at the exact moment the broker has already discarded the pending deferred.

    The specific, actionable error is therefore unreachable by construction. Callers only ever see the generic PreviewAutomationTimeoutError. A client-side deadline meaningfully shorter than the server's would surface "the preview overlay never became available" instead.

    This compounds the opacity @JorgeMenaDev already flagged at apps/server/src/mcp/McpHttpServer.ts:121, where previewSnapshotFailure logs the real errorTag and then sends only the fixed string "Preview snapshot failed.".

    2. Head-of-line blocking is why one bad snapshot poisons the tab

    withControlSession runs every action under a single per-tab permit (apps/desktop/src/preview/Manager.ts:1166, ending in control.semaphore.withPermit(...)), and nothing on the desktop side has a timeout. So a stalled call blocks every later call, which then burns its own full 15 s server budget without ever executing.

    Two consecutive evaluate calls from the desktop trace, sorted by start time:

    START          END              DUR(ms)  NAME
    05:34:46.250   05:35:26.280     40029.4  PreviewManager.withControlSession
    05:34:46.250   05:35:26.280     40029.5  desktop.ipc.preview.automationEvaluate
    05:34:46.251   05:34:46.251         0.2  PreviewManager.sendCommand
    05:34:46.251   05:35:26.279     40028.5  PreviewManager.sendCommand      <-- CDP stalled 40s
    05:35:04.986   05:35:26.281     21295.0  PreviewManager.withControlSession
    05:35:04.986   05:35:26.281     21295.1  desktop.ipc.preview.automationEvaluate
    05:35:26.280   05:35:26.280         0.2  PreviewManager.sendCommand      <-- ran only after the permit freed
    05:35:26.280   05:35:26.281         0.5  PreviewManager.sendCommand
    

    The second call spent 21,294 of its 21,295 ms waiting for the permit and under 1 ms executing. Both ended within 2 ms of each other. The server had abandoned both at their own 15 s marks.

    This is the same shape as @areidyOTH's "MCP call timed out after 15.03 s while the desktop operation continued for 47.83 s", and it explains the tab-poisoning: the caller sees N consecutive timeouts, but only the first one was actually slow.

    3. The server records these as successful spans

    Worth noting for anyone grepping traces: the failures exit Success at the span level, because the MCP layer returns an isError result rather than failing the effect.

    McpServer.@effect/mcp/2025-06-18/tools/call | preview_snapshot | 15004.0ms | Success
      event: ["preview snapshot failed",{"operation":"snapshot",
              "errorTag":"PreviewAutomationTimeoutError","failureCount":1}]
    

    Timeline from this session

    05:30:49  preview_snapshot   15004ms  TIMEOUT (desktop kept going to 33.5s, then UnknownVizError)
    05:31:05  preview_snapshot   15003ms  TIMEOUT
    05:31:22  preview_open         490ms  OK      (open:false)
    05:31:23  preview_snapshot   15003ms  TIMEOUT
    05:31:42  preview_evaluate   15002ms  TIMEOUT
    05:31:59  preview_open         473ms  OK      (open:false, reuseExistingTab:false)
    05:32:01  preview_evaluate      18ms  OK      <-- brief window right after open
    05:32:03  preview_resize     15002ms  TIMEOUT
    05:32:20  preview_snapshot      33ms  FAIL    PreviewAutomationExecutionError (UnknownVizError)
    05:32:28  preview_snapshot   15003ms  TIMEOUT
    05:32:51  preview_evaluate   15001ms  TIMEOUT
    05:33:08  preview_evaluate   15001ms  TIMEOUT
    05:33:26  preview_evaluate   15001ms  TIMEOUT
    05:33:42  preview_evaluate   15002ms  TIMEOUT
    

    For the four evaluates from 05:32:51 onward, the desktop trace shows a single automationStatus call at each timestamp and no automationEvaluate at all — consistent with waitForDesktopOverlay spinning to its deadline and the request never being dispatched.

    Still present on main

    PreviewAutomationHosts.tsx, PreviewAutomationBroker.ts, Manager.ts, and McpHttpServer.ts are byte-identical between tag v0.0.34 and main at b0a0281, so this is not fixed in any newer release.

    Environment

    • T3 Code CLI/server 0.0.34, desktop app 0.0.35-nightly.20260826.1193 (version skew was present on this machine)
    • macOS darwin arm64 25.6.0, Node v24.18.1
    • Claude provider, preview target a local dev server on localhost:3000

    Investigated by claude (opus-5) via t3 triage.

  6. grey-tsosie commented on Aug 27, 2026

    @grey-tsosie
    Contributor

    Confirming the failure mode on 0.0.33, Arch Linux x86_64, kernel
    7.1.8-arch1-3, Hyprland, t3code-bin AUR package from the AppImage, Claude
    provider via Claude Code.

    The reason I am commenting is that the visibility trigger @JorgeMenaDev
    identified is the right lead, but on this machine I cannot get a visible tab at
    all, by any documented route. If that holds for other Linux users, then
    open: true is not a usable workaround here.

    open: true never produced a visible tab

    preview_open {"url": "http://127.0.0.1:9126/", "open": true, "reuseExistingTab": true}
      -> {"available":true,"visible":false,"tabId":"tab_d", ... }
    

    Every preview_open with "open": true on this install has returned
    "visible": false, across six sessions and three projects. The page loads, the
    title is correct, loading is false, and preview_evaluate works against it.
    The tab simply never reports visible, so it is never capturable.

    show: true produced one visible tab and I could not reproduce it

    One call did return "visible": true:

    preview_open {"tabId": "tab_d", "show": true, "open": true, "reuseExistingTab": true}
      -> {"available":true,"visible":true,"tabId":"tab_f","url":"about:blank", ... }
    

    I have not managed it again. Two direct attempts afterwards both came back
    "visible": false:

    preview_open {"url": "http://127.0.0.1:9126/", "show": true, "open": true, "reuseExistingTab": false}
      -> {"available":true,"visible":false,"tabId":"tab_g", ... }
    
    preview_open {"tabId": "tab_f", "show": true, "open": true, "reuseExistingTab": true}
      -> {"available":true,"visible":false,"tabId":"tab_f", ... }
    

    The same tab that reported "visible": true reported "visible": false a few
    minutes later without being touched. So whatever sets that flag here is transient
    and is not controlled by open or show in any pattern I could find.

    A note for anyone testing this from the client side

    I tried using preview_click as a cheap proxy for "is the overlay painted",
    since click needs a painted surface in the way snapshot does. It is not usable
    for that. preview_click returns the same result on a visible tab and on a
    non-visible one, because its reply fails schema validation at the client before
    any outcome is visible:

    MCP server "t3-code" returned a malformed result that failed schema validation:
    Invalid result for tools/call: [ { "expected": "record", "code": "invalid_type",
    "path": [ "structuredContent" ], "message": "Invalid input: expected record,
    received null" } ]
    

    That is an effect 4.0.0-beta.103 defect rather than yours. It builds every
    MCP reply with typeof result.encodedResult === "object", and JavaScript reports
    "object" for null, so any tool of yours that returns void serializes to
    structuredContent: null. I filed it upstream as Effect-TS/effect#7494. I
    mention it here only so nobody else wastes time treating a click result as
    evidence of overlay state.

    Scope

    preview_resize also times out here, including on the one tab that did report
    "visible": true. That looks like a separate cause rather than another instance
    of this issue, so I have put that evidence on #3712 instead.

  7. ElliotDrel commented on Aug 30, 2026

    @ElliotDrel
    Contributor

    Reproduced on Windows 11 (build 10.0.26200.0), T3 Code Nightly 0.0.37-nightly.20260829.1223, via the mcp__t3-code__preview_* tools exposed to a Claude Code agent session (no thread-switching involved, so this isn't the #6355 trigger — the tab simply went bad on its own after repeated snapshot failures).

    UnknownVizError, reproduced with actionTimeline evidence:

    On a freshly opened tab (preview_open → preview_navigate to https://example.com), the first preview_snapshot call failed with only "Preview snapshot failed." surfaced to the caller. A second preview_snapshot call on the same tab succeeded, and its response's actionTimeline retroactively revealed what actually happened to both attempts:

    {"id":"browser-action-mtf80yum-f","action":"snapshot","status":"failed","startedAt":"2026-08-30T02:59:32.590Z","completedAt":"2026-08-30T02:59:32.599Z","error":"UnknownVizError"},
    {"id":"browser-action-mtf811l9-g","action":"snapshot","status":"failed","startedAt":"2026-08-30T02:59:36.141Z","completedAt":"2026-08-30T02:59:48.457Z","error":"UnknownVizError"},
    {"id":"browser-action-mtf81ghy-h","action":"snapshot","status":"running","startedAt":"2026-08-30T02:59:55.462Z"}

    Note the second failure took ~12.3s to resolve (not an instant rejection), and neither failure's real error tag (UnknownVizError) was visible anywhere in the tool's own error response — only in a later successful call's timeline. This matches the opacity problem described in #8257.

    Tabs going fully unusable without a thread switch:

    After several of these snapshot retries across three separate tabs (each independently opened via preview_open with reuseExistingTab: false, never revisited after a thread change — there was no thread change), preview_status on all three previously-healthy tabs simultaneously started returning:

    {"available":true,"visible":false,"tabId":null,"url":null,"title":null,"loading":false}

    i.e. available: true but every identifying field null — self-contradictory, and there was no explicit error to act on. Retrying preview_status with no tabId (agent's "current tab") returned the same null-everything shape. The only recovery was preview_open with reuseExistingTab: false to mint a brand-new tab, which worked immediately and normally.

    This suggests the "poisoned tab" failure mode in this issue and in #6355/#8257 isn't exclusive to the hidden-tab (open: false) trigger or to leaving/returning to a thread — it can also arise from ordinary snapshot retries alone, and can take out multiple independently-created tabs at once rather than just the one being retried.

  8. juliusmarminge commented on Sep 5, 2026

    @juliusmarminge
    Member

    Fixed by merged #4685 — capturePage/screenshot/snapshot attempts are bounded with retries and guest-identity checks; hung captures release control so later automation is not poisoned.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions