Browser/Chrome plugin setupBrowserRuntime invalidates existing tab handles on repeated setup

Open 💬 0 comments Opened Jun 9, 2026 by NAEL592

What version of the Codex App are you using?

26.602.40724

What platform is your computer?

Windows 11 x64

What issue are you seeing?

The bundled Browser/Chrome browser-use client can invalidate an already claimed tab handle when setupBrowserRuntime({ globals }) is called a second time in the same JavaScript kernel/session.

Observed behavior:

  1. Connect to the Chrome extension backend. In this case the backend is a real Brave/Chromium browser extension backend.
  2. List user tabs with browser.user.openTabs().
  3. Claim an existing user tab with browser.user.claimTab(tabInfo).
  4. Verify the returned Tab handle works with tab.title() / tab.url().
  5. Call setupBrowserRuntime({ globals }) again in the same kernel.
  6. Try to use the original Tab handle again.

Before the local patch, the old handle failed with:

native pipe is closed

In earlier runs, a similar stale-handle failure appeared as:

Tab not found ... Existing tabs: none

The real browser tab was still open and could be found again via browser.user.openTabs(), so this was not a user tab being closed. It appears to be the browser-use runtime disposing/recreating the browser connection during repeated setup, which changes the backend id and invalidates handles from the previous connection.

What steps can reproduce the bug?

Use the bundled browser client from the Codex Browser/Chrome plugin and run this sequence:

const { setupBrowserRuntime } = await import('<plugin root>/scripts/browser-client.mjs');
await setupBrowserRuntime({ globals: globalThis });

const backends1 = await agent.browsers.list();
const extension1 = backends1.find((b) => b.type === 'extension');
const browser1 = await agent.browsers.get(extension1.id);
const tabInfo = (await browser1.user.openTabs()).find((t) => t.url && !t.url.startsWith('chrome://'));
const tab = await browser1.user.claimTab(tabInfo);
await tab.title(); // works

await setupBrowserRuntime({ globals: globalThis });
await tab.title(); // fails before patch: native pipe is closed

What is the expected behavior?

Calling setupBrowserRuntime({ globals }) repeatedly in the same live JavaScript kernel should be idempotent, or at least should not dispose an existing healthy browser-use runtime and invalidate live Tab handles.

If the existing connection is healthy, setup should reuse it. If the connection is dead, setup can rebuild it.

Additional information

A local patch that fixed the repro was to add an idempotency marker and health check in setupBrowserRuntime:

  • after assigning globals.agent and globals.display, mark the runtime as initialized for the current module URL;
  • on subsequent calls, if the marker matches and agent/display exist, first run a cheap health check such as await globals.agent.browsers.list();
  • if the health check passes, return without disposing/recreating browser connections;
  • if the health check fails, clear the marker and rebuild normally.

After that local patch, the repro passed:

  • backend id remained stable across the second setup call;
  • the original claimed Tab handle still worked;
  • the claimed Brave tab stayed controllable after an additional wait;
  • cleanup with browser.tabs.finalize({ keep: [{ tab, status: "handoff" }] }) still worked.

This is especially noticeable in workflows where agents bootstrap the browser runtime more than once while continuing an unfinished real-browser task, because the page remains open but the agent loses its handle unexpectedly.

View original on GitHub ↗