hermes - 💡(How to fix) Fix [Bug]: browser_cdp opens stateless CDP connections, breaking Browserless/BaaS targets between tool calls

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…

Error Message

Additional Logs / Traceback (optional)

Root Cause

A target returned by one browser_cdp call cannot be reused in a subsequent browser_cdp call, because the first WebSocket has already been closed and Browserless has destroyed the browser/session that owned that target.

Fix Action

Fix / Workaround

Current workarounds:

Code Example

`browser_cdp` opens a new CDP WebSocket connection for every tool call. This breaks when using Browserless or similar Browser-as-a-Service CDP endpoints where each new WebSocket connection creates a new browser/session.

A target returned by one `browser_cdp` call cannot be reused in a subsequent `browser_cdp` call, because the first WebSocket has already been closed and Browserless has destroyed the browser/session that owned that target.

This makes multi-step raw CDP workflows fail even when `BROWSER_CDP_URL` is valid and Browserless itself is reachable.

Environment:

- Hermes running in WSL
- CDP backend: Browserless self-hosted v2
- Browserless version observed from local docs: `2.49.0`
- CDP URL configured via `.env`:

---

Also tested / considered:

---

`/chromium?token=...` is more explicit and preferable for Browserless CDP, but it does not change the issue described here.

Browserless management/discovery endpoints work with a valid token:

---

Without token, management endpoints correctly return `401`.

So the Browserless URL/token are not the root issue. The issue is the lifecycle mismatch between stateless `browser_cdp` calls and Browserless session-per-WebSocket behavior.

---

Using `browser_cdp` against Browserless:

1. Configure Browserless as the CDP backend, for example:

---

2. Call:

---

3. Receive a target id, for example an `about:blank` target.

4. Call another CDP method using that `target_id`, for example:

---

5. The second call fails because the target id no longer exists.

---

For CDP endpoints that require persistent WebSocket sessions, `browser_cdp` should be able to reuse an existing persistent CDP connection/session when available.

Specifically, if Hermes already has a live CDP supervisor/browser session, `browser_cdp(target_id=...)` should route through that persistent supervisor connection instead of opening a new WebSocket for every call.

This would allow multi-step CDP workflows like:

---

to work against Browserless and other CDP BaaS endpoints.

---

Each `browser_cdp` call creates a new CDP WebSocket. With Browserless this creates a new browser/session each time, so target IDs from previous calls become invalid.

Observed failure:

---

Current behavior appears to be:

---

So the failure is caused by a mismatch between:

- Hermes `browser_cdp`: stateless per-call WebSocket connections
- Browserless CDP: session/browser lifetime tied to the WebSocket connection

---

no report actually

---

`
Browserless-specific attempts that did not solve it:

### `keepalive`

Old Browserless v1-style keepalive/prebooting is not a solution in Browserless v2.

Example test:


wss://...?token=***&keepalive=30000 -> HTTP 400


### `timeout`

`timeout` may be accepted, but it only controls maximum request/session duration while the WebSocket is alive. It does not keep the browser alive after the WebSocket closes.

### `Browserless.reconnect`

Tried Browserless custom CDP command:


Browserless.reconnect


Observed:


'Browserless.reconnect' wasn't found


So this is not available on the tested self-hosted instance.

### Session API `/session`

Docs mention `POST /session` for persistent sessions, but on the tested self-hosted instance Swagger exposed `/sessions`, not `/session`.

Observed:


POST /session?token=... -> 404 Not Found

`

---

The root cause appears to be in the lifecycle of `browser_cdp` connections.

`browser_cdp` currently behaves as a stateless direct CDP call mechanism: it opens a new WebSocket connection for each tool call.

That is safe with a persistent Chrome/Chromium process launched with `--remote-debugging-port`, because the browser process remains alive between WebSocket connections and target IDs remain valid.

It is not safe with Browserless/BaaS endpoints where each WebSocket connection represents a new browser/session lifecycle. In that model, closing the WebSocket destroys the session and invalidates the target IDs returned by the previous call.

Therefore, target IDs returned by one `browser_cdp` call cannot reliably be used in a later `browser_cdp` call against Browserless.

---

In `tools/browser_cdp_tool.py`, when a CDP supervisor/session is available, `browser_cdp` should use the persistent supervisor connection rather than opening a fresh WebSocket per call.

Possible behavior:

- If `target_id` or `frame_id` belongs to a known live supervisor session, route the CDP command through that connection.
- Fall back to stateless direct WebSocket only when no persistent supervisor/session exists.
- Optionally document that fully stateless `browser_cdp` is only safe with persistent browser processes such as Chrome/Chromium launched with `--remote-debugging-port`.

Current workarounds:

1. Use the higher-level persistent browser tools instead of raw `browser_cdp`, for example:

---

2. Use a persistent Chrome/Chromium instance with `--remote-debugging-port=9222`, because the browser process remains alive across separate CDP connections and target IDs survive between calls.

3. Avoid multi-step `browser_cdp` workflows against Browserless until Hermes can reuse a persistent CDP supervisor connection.
RAW_BUFFERClick to expand / collapse

Bug Description

`browser_cdp` opens a new CDP WebSocket connection for every tool call. This breaks when using Browserless or similar Browser-as-a-Service CDP endpoints where each new WebSocket connection creates a new browser/session.

A target returned by one `browser_cdp` call cannot be reused in a subsequent `browser_cdp` call, because the first WebSocket has already been closed and Browserless has destroyed the browser/session that owned that target.

This makes multi-step raw CDP workflows fail even when `BROWSER_CDP_URL` is valid and Browserless itself is reachable.

Environment:

- Hermes running in WSL
- CDP backend: Browserless self-hosted v2
- Browserless version observed from local docs: `2.49.0`
- CDP URL configured via `.env`:

```text
BROWSER_CDP_URL=wss://<browserless-host>/?token=***
```

Also tested / considered:

```text
wss://<browserless-host>/chromium?token=***
```

`/chromium?token=...` is more explicit and preferable for Browserless CDP, but it does not change the issue described here.

Browserless management/discovery endpoints work with a valid token:

```text
/json/version?token=... -> 200, Chrome/Protocol info
/json/list?token=...    -> 200, empty list when no persistent sessions are active
/sessions?token=...     -> 200, empty list
/pressure?token=...     -> 200
```

Without token, management endpoints correctly return `401`.

So the Browserless URL/token are not the root issue. The issue is the lifecycle mismatch between stateless `browser_cdp` calls and Browserless session-per-WebSocket behavior.

Steps to Reproduce

Using `browser_cdp` against Browserless:

1. Configure Browserless as the CDP backend, for example:

```text
BROWSER_CDP_URL=wss://<browserless-host>/chromium?token=***
```

2. Call:

```text
Target.getTargets
```

3. Receive a target id, for example an `about:blank` target.

4. Call another CDP method using that `target_id`, for example:

```text
Page.navigate
```

5. The second call fails because the target id no longer exists.

Expected Behavior

For CDP endpoints that require persistent WebSocket sessions, `browser_cdp` should be able to reuse an existing persistent CDP connection/session when available.

Specifically, if Hermes already has a live CDP supervisor/browser session, `browser_cdp(target_id=...)` should route through that persistent supervisor connection instead of opening a new WebSocket for every call.

This would allow multi-step CDP workflows like:

```text
Target.getTargets
Page.navigate(target_id=...)
Runtime.evaluate(target_id=...)
```

to work against Browserless and other CDP BaaS endpoints.

Actual Behavior

Each `browser_cdp` call creates a new CDP WebSocket. With Browserless this creates a new browser/session each time, so target IDs from previous calls become invalid.

Observed failure:

```text
Target.attachToTarget failed: No target with given id found
```

Current behavior appears to be:

```text
call 1:
  browser_cdp opens new WS -> Browserless creates browser/session A
  Target.getTargets returns target A
  WS closes -> Browserless closes browser/session A

call 2:
  browser_cdp opens new WS -> Browserless creates browser/session B
  Hermes tries to use target A
  target A no longer exists
```

So the failure is caused by a mismatch between:

- Hermes `browser_cdp`: stateless per-call WebSocket connections
- Browserless CDP: session/browser lifetime tied to the WebSocket connection

Affected Component

Tools (terminal, file ops, web, code execution, etc.)

Messaging Platform (if gateway-related)

N/A (CLI only)

Debug Report

no report actually

Operating System

Ubuntu 24.04.4 LTS under WS

Python Version

Python 3.12.3

Hermes Version

Hermes Agent v0.14.0 (2026.5.16)

Additional Logs / Traceback (optional)

`
Browserless-specific attempts that did not solve it:

### `keepalive`

Old Browserless v1-style keepalive/prebooting is not a solution in Browserless v2.

Example test:


wss://...?token=***&keepalive=30000 -> HTTP 400


### `timeout`

`timeout` may be accepted, but it only controls maximum request/session duration while the WebSocket is alive. It does not keep the browser alive after the WebSocket closes.

### `Browserless.reconnect`

Tried Browserless custom CDP command:


Browserless.reconnect


Observed:


'Browserless.reconnect' wasn't found


So this is not available on the tested self-hosted instance.

### Session API `/session`

Docs mention `POST /session` for persistent sessions, but on the tested self-hosted instance Swagger exposed `/sessions`, not `/session`.

Observed:


POST /session?token=... -> 404 Not Found

`

Root Cause Analysis (optional)

The root cause appears to be in the lifecycle of `browser_cdp` connections.

`browser_cdp` currently behaves as a stateless direct CDP call mechanism: it opens a new WebSocket connection for each tool call.

That is safe with a persistent Chrome/Chromium process launched with `--remote-debugging-port`, because the browser process remains alive between WebSocket connections and target IDs remain valid.

It is not safe with Browserless/BaaS endpoints where each WebSocket connection represents a new browser/session lifecycle. In that model, closing the WebSocket destroys the session and invalidates the target IDs returned by the previous call.

Therefore, target IDs returned by one `browser_cdp` call cannot reliably be used in a later `browser_cdp` call against Browserless.

Proposed Fix (optional)

In `tools/browser_cdp_tool.py`, when a CDP supervisor/session is available, `browser_cdp` should use the persistent supervisor connection rather than opening a fresh WebSocket per call.

Possible behavior:

- If `target_id` or `frame_id` belongs to a known live supervisor session, route the CDP command through that connection.
- Fall back to stateless direct WebSocket only when no persistent supervisor/session exists.
- Optionally document that fully stateless `browser_cdp` is only safe with persistent browser processes such as Chrome/Chromium launched with `--remote-debugging-port`.

Current workarounds:

1. Use the higher-level persistent browser tools instead of raw `browser_cdp`, for example:

```text
browser_navigate
browser_snapshot
browser_click
browser_console
```

2. Use a persistent Chrome/Chromium instance with `--remote-debugging-port=9222`, because the browser process remains alive across separate CDP connections and target IDs survive between calls.

3. Avoid multi-step `browser_cdp` workflows against Browserless until Hermes can reuse a persistent CDP supervisor connection.

Are you willing to submit a PR for this?

  • I'd like to fix this myself and submit a PR

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