Skip to content

preview_evaluate and preview_snapshot silently read a different browsing context when two automation hosts are registered #13051

Description

@tomhmoses

What happened

I was signed in to an internal web app by hand, in the collaborative browser preview panel, and could see three tabs showing a logged-in dashboard. The agent I asked to read a page from that app told me I was not signed in. It said so three times, each time with what looked like solid evidence: the login form's markup, and document.cookie === "". Nothing errored, nothing timed out. I only worked out it was wrong by screenshotting my own screen and showing it.

Quitting and relaunching T3 Code fixed it.

Diagnosis

Two preview automation hosts were registered in the environment at the same time. Every automation request went to the one the user was not looking at, and it answered successfully from its own browsing context.

1. Two clients were connected with preview surfaces for the whole session. ws.rpc.previewAutomation.connect opened for connection 44c44045… at 08:45:37 and for 4842efea… at 08:55:10. Both stayed open until PreviewAutomationBroker.disconnect fired for both at 10:49:07, 64 ms apart, when the app quit. Both reported ws.rpc.preview.reportStatus throughout.

2. Every automation request in the fault window went to the client that was not being focused. Between 10:38:00 and 10:49:00 (local, UTC+1): PreviewAutomationBroker.invoke × 61, ws.rpc.previewAutomation.respond × 61 — all 61 responses from 4842efea…, none from 44c44045…. In that same window 44c44045… issued ws.rpc.previewAutomation.focusHost × 11, including 20 s and 4 s before the first wrong read. 4842efea… issued 2.

3. The desktop main process never executed any of them. The pre-restart desktop trace has 5,024 spans in that window, including desktop.ipc.preview.createTab × 6, registerWebview × 18, PreviewManager.createControlSession × 6 and closeTab × 2 — and zero automationEvaluate, automationStatus, automationClick or automationSnapshot. It logged those before the fault (last at 10:23:33, PreviewManager.performAutomationEvaluate) and again immediately after the restart (10:49:47). So tab creation went through the desktop — which is why the panel visibly followed preview_open — while the reads were answered somewhere else entirely.

4. The restart collapsed it to one host and the fault went away. PreviewAutomationBroker.make at 10:49:23, then a single connection d74d3b7c… at 10:49:26 issuing both focusHost and respond. Desktop automation spans returned, preview_open returned visible: true, and reads matched the panel.

Why the wrong host wins. In apps/server/src/mcp/PreviewAutomationBroker.ts, a provider session with no live lease picks its host by:

.sort((left, right) =>
    right.supportedOperations.size - left.supportedOperations.size ||
    Number(right.focused) - Number(left.focused) ||
    right.focusOrder - left.focusOrder)[0]

and the winner is pinned in assignments, keyed by environmentId \0 providerSessionId, then deliberately never re-evaluated while the connection lives. apps/web/src/components/preview/PreviewAutomationHosts.tsx registers every host with supportedOperations: [...PREVIEW_AUTOMATION_OPERATIONS], so between two current clients the first sort key always ties and the decision falls through to focused / focusOrder — effectively registration order. Because the lease is keyed by providerSessionId, a new agent session re-runs that sort, which matches the timeline: the session that started at 10:38 was new, and reads at 10:20 and 10:23 in the same app run were correct.

Why preview_status cannot detect it. currentStatus in the same file returns available: Boolean(previewBridge?.automation) — true whenever the Electron bridge object exists, regardless of whether the responding client has that tab rendered — and when the tab is not desktop-backed it falls back to the server snapshot's navStatus for url and title rather than reading the live page. Hence available: true and a plausible URL throughout. No field in any preview_* response distinguishes this from a healthy read.

Two things that look like the cause and are not.

  • visible: false is not a signal. It was false for the correct reads at 10:20 and 10:23 in the same app run, false throughout the fault, and false on a healthy single-host session afterwards while reads were correct. Only the post-restart call returned true.
  • The preload TypeError in the evidence below fired on every page load in the fault window and looks causal. It is not specific to this fault: the same two lines appear in preview_snapshot consoleEntries in local provider logs from 2026-09-11, 09-16 and 09-17, in sessions whose reads were correct.

What I could not determine. Server traces rotate every 2–4 minutes on this machine, so the window before 10:23:50 was already gone and I cannot show which host served the correct earlier reads, nor the moment routing changed. Span attributes in both trace files are empty, so environmentId, clientId and tabId are not recoverable from them — the connection identities above are traceId/parentSpanId pairs for the WebSocket connections. I also could not identify what created the second host: the desktop trace shows no new renderer at 08:55:10, only a desktop.ipc.connectionCatalog.set moments earlier, and the on-disk catalog is encrypted.

Two minor asides, not filed separately. preview_status on a tab id that does not exist returns {"available":true,"visible":false,"tabId":null} rather than an error, which reads like a healthy answer. And tab ids in the fault window were tab_7, tab_8, tab_b, tab_c, restarting at tab_1 after the relaunch — they are not decimal and not enumerable, which cost time during triage.

Steps to reproduce

I could not reproduce this deterministically, and would rather say so than invent steps. Observed once; traces for most of the fault window are preserved and I can attach them.

Predicted from the traces and the v0.0.42 source, but not confirmed:

  1. Get two preview automation hosts registered in one environment at once (I have not identified what created the second one).
  2. Sign in to a site by hand in the preview panel you can see.
  3. Start a new agent session — a new providerSessionId — and have it call preview_evaluate or preview_snapshot against that site.

Verified negative control: with a single registered host, the same sequence against a throwaway localhost server behaves correctly. preview_navigate to a cookie-setting endpoint followed by preview_evaluate returns the signed-in document and the real document.cookie.

Version

0.0.42 (tag v0.0.42, commit 719a76c)

Environment

macOS 27.0 (26A428) darwin arm64, Electron 44.1.0, Chrome 152.0.7977.65, desktop app against the local server, provider Claude Code (claudeAgent) with Opus 5, preview device toolbar active (Responsive, 1280x800)

Evidence

# two hosts connected, both alive until the app quit  (server.trace.ndjson*)
08:45:37  ws.rpc.previewAutomation.connect     trace 44c44045 parent f7386175
08:55:10  ws.rpc.previewAutomation.connect     trace 4842efea parent 2dc7c84e
10:49:07  PreviewAutomationBroker.disconnect   4842efea
10:49:07  PreviewAutomationBroker.disconnect   44c44045

# fault window 10:38:00-10:49:00, routing counts
PreviewAutomationBroker.invoke         61
ws.rpc.previewAutomation.respond       61   ALL from 4842efea, 0 from 44c44045
ws.rpc.previewAutomation.focusHost     11   from 44c44045   (2 from 4842efea)

# same window, pre-restart desktop trace (5024 spans total)
desktop.ipc.preview.createTab           6
desktop.ipc.preview.registerWebview    18
PreviewManager.createControlSession     6
desktop.ipc.preview.automationEvaluate  0
desktop.ipc.preview.automationStatus    0
desktop.ipc.preview.automationClick     0
desktop.ipc.preview.automationSnapshot  0

# last desktop-executed automation before the fault, first after the restart
10:23:33.105  PreviewManager.performAutomationEvaluate    Success 2.0ms
10:49:47      desktop.ipc.preview.automationEvaluate      Success

# agent-visible results across the restart boundary (site redacted: private intranet)
10:38:30  preview_open      {"available":true,"visible":false,"tabId":"tab_7", ...}
10:45:43  preview_open      {"available":true,"visible":false,"tabId":"tab_b", ...}
10:47:06  preview_open      {"available":true,"visible":false,"tabId":"tab_c", ...}
10:46:27  preview_evaluate  {"cookie":"","txt":"Username Password Sign in with ...", ...}
10:47:13  preview_evaluate  tabId=tab_c -> "CLAUDE-MARKER-TAB-C"   (user saw no change on screen)
10:47:15  preview_evaluate  tabId=tab_b -> "CLAUDE-MARKER-TAB-B"   (user saw no change on screen)
--- app quit and relaunch at 10:49:07 ---
10:49:44  preview_open      {"available":true,"visible":true,"tabId":"tab_1", ...}

# on every page load in the fault window, and also in unaffected sessions since 2026-09-11
Electron sandboxed_renderer.bundle.js script failed to run
TypeError: Cannot destructure property 'preloadScripts' of 'binding.startupData' as it is null.
    at node:electron/js2c/sandbox_bundle:2:133687
    at node:electron/js2c/sandbox_bundle:2:134791
    at ___electron_webpack_init__ (node:electron/js2c/sandbox_bundle:2:134795)
    at node:electron/js2c/sandbox_bundle:2:134918

Related issues

Not a duplicate of #11167 (wrong client, but calls time out; here it answers successfully), #12146, #12898, #12273, #12319 (host eviction/disconnect, all loud), #11223, #7212, #3715 or #8332.

Fix applied or workaround

Nothing was written to the machine. Workaround is to quit and relaunch T3 Code: the Electron profile survives, so a persistent login survives, but a non-persistent session cookie does not and you will need to sign in again.

Filed by

claude (opus-5) in T3 Code, following .github/triage/PLAYBOOK.md

Activity

  1. juliusmarminge commented on Sep 22, 2026

    @juliusmarminge
    Member

    Triage

    Verdict: Confirmed on v0.0.42 (719a76ca1) and current main (aff9318bf). Keep open. Not fixed by nightlies after 0.0.42. Not a duplicate of #11167 / #12146 / #12898 / #12273 / #12319.

    When two preview automation hosts are registered for the same environment, a new provider session can be pinned to the host the user is not looking at. That host answers successfully from its own browsing context (empty cookies / login markup), while the visible desktop panel keeps creating/showing tabs. Quit + relaunch collapses to one host and the fault goes away. That matches the traces and the code.

    What the code does

    PreviewAutomationBroker.invoke pins a provider session to one host (environmentId + providerSessionId) for the life of that connection so multi-step browser flows do not jump cookie/DOM runtimes. A live lease is not re-sorted on later focusHost updates; only disconnect prunes it. Tests encode that: failover only after disconnect; a pinned session stays on the first host even if another host becomes focused.

    When there is no live lease, the picker sorts by:

    1. supportedOperations.size
    2. focused
    3. focusOrder

    PreviewAutomationHosts always advertises the full PREVIEW_AUTOMATION_OPERATIONS set, so between two current desktop clients the first key ties and the decision falls through to focus / registration order. A new agent session (new providerSessionId) re-runs that sort and can pin the wrong host for the whole turn.

    currentStatus reports available: Boolean(previewBridge?.automation) whenever the Electron bridge exists, and when the tab is not desktop-backed on that host it falls back to the server snapshot’s navStatus for URL/title. So preview_status / preview_open can look healthy (available: true, plausible URL) while evaluates read a different cookie jar. visible: false is not diagnostic; it appears on healthy single-host sessions too.

    The preload TypeError about preloadScripts is a red herring (also present in unaffected sessions), as the report already notes.

    Related, not duplicates

    Issue / PR Why it is different
    #11167 (closed) Wrong / zombie client, but calls time out; here the wrong host answers successfully
    #12146 / #12535 Host eviction / “no host available” after timeouts; #12535 recovered registration after timeout, did not change sticky wrong-host selection
    #12898, #12273, #12319 Loud timeout / eviction paths
    #11223, #7212, #3715, #8332 Other preview failures; not silent multi-host routing

    Nightlies that include #12535 (v0.0.43-nightly.20260919.* onward) still have the same sort + sticky assignment. Updating past 0.0.42 alone will not fix this.

    Workaround

    Quit and relaunch T3 Code so only one automation host remains. Electron profile/login can survive; non-persistent session cookies will not.

    Next step

    Keep open as a real silent multi-host routing bug:

    1. Prefer the focused host (or the host that owns the live desktop tab / webview) when choosing a new lease, and/or refuse to keep two full-capability hosts for one environment without a clear owner.
    2. Make preview_status / evaluate responses distinguishable when the responding host does not have the tab’s live desktop webview (do not report available: true from bridge presence alone).
    3. Optional: re-evaluate a live lease when focusHost flips, or surface clientId / host identity in preview tool results so agents can detect a mismatch.

    The report’s predicted repro (two registered hosts → sign in by hand on the visible panel → new agent session → preview_evaluate) matches the broker behavior; a deterministic dual-host fixture would unlock a regression test beside the existing pin/failover tests.

  2. added
    acceptedfeature request accepted
    bugSomething is broken or behaving incorrectly.
    on Sep 22, 2026
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

    acceptedfeature request acceptedbugSomething 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