openclaw - ✅(Solved) Fix Browser bridge: "tab not found" race in ensureTabAvailable when wsUrl lags CDP discovery [1 pull requests, 2 comments, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#63343Fetched 2026-04-09 07:55:06
View on GitHub
Comments
2
Participants
1
Timeline
6
Reactions
0
Participants
Timeline (top)
referenced ×3commented ×2cross-referenced ×1

ensureTabAvailable() in server-context.selection.ts throws BrowserTabNotFoundError ("tab not found") due to a race condition where a newly opened tab's webSocketDebuggerUrl (wsUrl) hasn't populated in CDP's /json/list response yet. This is the dominant failure mode for browser tools on localhost agents.

Error Message

2026-04-08T16:04:06.399Z [tools] browser failed: tab not found 2026-04-08T16:04:06.399Z [tools] browser failed: tab not found (×10 at startup) 2026-04-08T17:08:28.422Z [tools] browser failed: TimeoutError: locator.click: Timeout 8000ms exceeded. 2026-04-08T17:08:51.594Z [tools] browser failed: tab not found (23s after click timeout)

Root Cause

In extensions/browser/src/browser/server-context.selection.ts:36-78:

const tabs1 = await listTabs();
if (tabs1.length === 0) {
  await openTab("about:blank");  // ← Return value DISCARDED
}
const tabs = await listTabs();   // ← CDP may not reflect the tab yet
const candidates = capabilities.supportsPerTabWs
  ? tabs.filter((t) => Boolean(t.wsUrl))  // ← wsUrl may not be populated
  : tabs;
// candidates can be empty → throws BrowserTabNotFoundError

Two bugs compound:

  1. openTab() returns a valid BrowserTab with targetId, but the result is thrown away
  2. When supportsPerTabWs is true (all local-managed profiles), tabs without wsUrl are filtered out — but wsUrl from CDP /json/list can lag tab creation by hundreds of milliseconds

Fix Action

Fix / Workaround

There is also a separate (lower-priority) issue where Playwright interaction actions (click, type, hover, drag, select) do not receive the dispatcher's AbortSignal, so when a transport timeout fires, the Playwright operation continues running orphaned against the live page. Only evaluate currently propagates req.signal.

PR fix notes

PR #63355: fix(browser): resolve tab-not-found race when wsUrl lags CDP discovery

Description (problem / solution / changelog)

Problem

ensureTabAvailable() throws BrowserTabNotFoundError due to a race condition where a newly opened tab's webSocketDebuggerUrl (wsUrl) hasn't populated in CDP's /json/list response yet. This is the dominant failure mode for browser tools on localhost agents — 14 of 18 browser tool failures in a single day were caused by this.

Failure modes

  1. Cold start (10 occurrences): Chrome starts, openTab("about:blank") succeeds but its return value is discarded. The subsequent listTabs() either returns empty or returns the tab without wsUrl. For local-managed profiles (supportsPerTabWs: true), the wsUrl filter eliminates the tab → "tab not found".

  2. Post-Playwright-error (4 occurrences): After a Playwright click timeout, the next ensureTabAvailable() call finds tabs but without wsUrl populated → filtered out → "tab not found".

Root Cause

In server-context.selection.ts:40-46:

if (tabs1.length === 0) {
  await openTab("about:blank");  // ← Return value DISCARDED
}
const tabs = await listTabs();   // ← CDP may not reflect the tab yet
const candidates = capabilities.supportsPerTabWs
  ? tabs.filter((t) => Boolean(t.wsUrl))  // ← wsUrl may lag
  : tabs;

Fix (3 layers of defense)

  1. Capture openTab() return value instead of discarding it
  2. Poll for wsUrl discovery (up to 1s at 100ms intervals) when supportsPerTabWs filtering empties candidates but tabs exist
  3. Fall back to unfiltered tabs if wsUrl never appears — the persistent Playwright connection can still reach tabs by targetId
  4. Use openTab() result directly if listTabs() stays completely empty

Changes

FileChange
server-context.constants.tsAdd WSURL_DISCOVERY_WINDOW_MS = 1000
server-context.selection.tsFix ensureTabAvailable() race condition
server-context.tab-selection-state.test.ts3 new tests covering wsUrl race scenarios

Tests

All 10 tests pass (7 existing + 3 new):

  • ✅ Recovers when newly opened tab initially lacks wsUrl (polls until wsUrl appears)
  • ✅ Falls back to tab without wsUrl after polling exhausts (1s deadline)
  • ✅ Uses openTab result directly when listTabs stays completely empty

Fixes #63343

Changed files

  • extensions/browser/src/browser/server-context.constants.ts (modified, +1/-0)
  • extensions/browser/src/browser/server-context.selection.ts (modified, +43/-3)
  • extensions/browser/src/browser/server-context.tab-selection-state.test.ts (modified, +262/-0)

Code Example

const tabs1 = await listTabs();
if (tabs1.length === 0) {
  await openTab("about:blank");  // ← Return value DISCARDED
}
const tabs = await listTabs();   // ← CDP may not reflect the tab yet
const candidates = capabilities.supportsPerTabWs
  ? tabs.filter((t) => Boolean(t.wsUrl))  // ← wsUrl may not be populated
  : tabs;
// candidates can be empty → throws BrowserTabNotFoundError

---

2026-04-08T16:04:06.399Z  [tools] browser failed: tab not found
2026-04-08T16:04:06.399Z  [tools] browser failed: tab not found  (×10 at startup)
2026-04-08T17:08:28.422Z  [tools] browser failed: TimeoutError: locator.click: Timeout 8000ms exceeded.
2026-04-08T17:08:51.594Z  [tools] browser failed: tab not found  (23s after click timeout)
RAW_BUFFERClick to expand / collapse

Bug Report

Summary

ensureTabAvailable() in server-context.selection.ts throws BrowserTabNotFoundError ("tab not found") due to a race condition where a newly opened tab's webSocketDebuggerUrl (wsUrl) hasn't populated in CDP's /json/list response yet. This is the dominant failure mode for browser tools on localhost agents.

Reproduction

Happens reliably on cold start and intermittently after Playwright errors. In a single day's logs, 14 of 18 browser tool failures were "tab not found" caused by this race.

Failure pattern 1 — Cold start (10 occurrences):

  • Chrome starts at T+0
  • ensureTabAvailable() calls listTabs() → empty (CDP not ready)
  • Calls openTab("about:blank") → succeeds, returns BrowserTab with targetId
  • Return value is discarded (line 41)
  • Calls listTabs() again → may still be empty, or tab appears without wsUrl
  • For local-managed profiles (supportsPerTabWs: true), candidates filtered by wsUrl → empty
  • Throws BrowserTabNotFoundError

Failure pattern 2 — Post-Playwright-error (4 occurrences):

  • Playwright click times out (8s) on a valid tab
  • 23s later, next browser operation calls ensureTabAvailable()
  • listTabs() returns tabs but wsUrl is stale/missing after the error
  • supportsPerTabWs filter eliminates all candidates → "tab not found"

Root Cause

In extensions/browser/src/browser/server-context.selection.ts:36-78:

const tabs1 = await listTabs();
if (tabs1.length === 0) {
  await openTab("about:blank");  // ← Return value DISCARDED
}
const tabs = await listTabs();   // ← CDP may not reflect the tab yet
const candidates = capabilities.supportsPerTabWs
  ? tabs.filter((t) => Boolean(t.wsUrl))  // ← wsUrl may not be populated
  : tabs;
// candidates can be empty → throws BrowserTabNotFoundError

Two bugs compound:

  1. openTab() returns a valid BrowserTab with targetId, but the result is thrown away
  2. When supportsPerTabWs is true (all local-managed profiles), tabs without wsUrl are filtered out — but wsUrl from CDP /json/list can lag tab creation by hundreds of milliseconds

Proposed Fix

  1. Capture openTab() return value and use it as a candidate when listTabs() doesn't reflect it
  2. Add wsUrl discovery polling (up to 1s) when supportsPerTabWs filtering leaves candidates empty but tabs exist
  3. Fall back to unfiltered tabs if polling exhausts — the persistent Playwright connection can still reach tabs by targetId

Related Issue

There is also a separate (lower-priority) issue where Playwright interaction actions (click, type, hover, drag, select) do not receive the dispatcher's AbortSignal, so when a transport timeout fires, the Playwright operation continues running orphaned against the live page. Only evaluate currently propagates req.signal.

Environment

  • macOS, local-managed Chrome profile
  • cdpPort: 18800
  • Gateway running on port 18791
  • Observed in daily agent workflows with multiple sequential browser tool calls

Log Evidence

2026-04-08T16:04:06.399Z  [tools] browser failed: tab not found
2026-04-08T16:04:06.399Z  [tools] browser failed: tab not found  (×10 at startup)
2026-04-08T17:08:28.422Z  [tools] browser failed: TimeoutError: locator.click: Timeout 8000ms exceeded.
2026-04-08T17:08:51.594Z  [tools] browser failed: tab not found  (23s after click timeout)

extent analysis

TL;DR

Capture the return value of openTab() and use it as a candidate when listTabs() doesn't reflect it, and consider adding wsUrl discovery polling to handle the race condition.

Guidance

  • Capture the openTab() return value and use it as a candidate to avoid discarding the newly opened tab's information.
  • Implement wsUrl discovery polling (up to 1s) when supportsPerTabWs filtering leaves candidates empty but tabs exist, to handle the lag in wsUrl population.
  • Consider falling back to unfiltered tabs if polling exhausts, to ensure the persistent Playwright connection can still reach tabs by targetId.
  • Review the related issue regarding Playwright interaction actions not receiving the dispatcher's AbortSignal, as it may be contributing to the overall instability.

Example

const newTab = await openTab("about:blank");
const tabs = await listTabs();
const candidates = capabilities.supportsPerTabWs
  ? [...tabs.filter((t) => Boolean(t.wsUrl)), newTab]  // Include the new tab in candidates
  : [...tabs, newTab];  // Include the new tab in candidates if not filtering by wsUrl

Notes

The proposed fix assumes that capturing the openTab() return value and adding wsUrl discovery polling will be sufficient to resolve the issue. However, the related issue regarding Playwright interaction actions may need to be addressed separately to ensure overall stability.

Recommendation

Apply the proposed workaround by capturing the openTab() return value and adding wsUrl discovery polling, as it directly addresses the identified root cause and provides a clear path to resolving the issue.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING